4
\$\begingroup\$

I'm using the command pattern to enable actions in my game to be undoable. ICommand defines individual commands, ICommandHandler does the undo-redo logic and might be decorated with other handlers like logging etc.

public interface ICommand
{
    void Execute();
    void Undo();
}

public interface ICommandHandler
{
    void Execute(ICommand command);
    void Undo();
    void Redo();
}

public class CommandHandler : ICommandHandler
{
    Stack<ICommand> _undoStack = new Stack<ICommand>();
    Stack<ICommand> _redoStack = new Stack<ICommand>();

    public void Execute(ICommand command)
    {
        if (command == null)
            throw new ArgumentNullException("command");

        command.Execute();
        _undoStack.Push(command);
        _redoStack.Clear();
    }

    public void Undo()
    {
        if (_undoStack.Count > 0)
        {
            ICommand undoCommand = _undoStack.Pop();
            undoCommand.Undo();
            _redoStack.Push(undoCommand);
        }
    }

    public void Redo()
    {
        if (_redoStack.Count > 0)
        {
            ICommand redoCommand = _redoStack.Pop();
            redoCommand.Execute();
            _undoStack.Push(redoCommand);
        }
    }
}

So far I'm not having any problems. Please correct me if I'm doing anything horribly wrong, but my actual questions is: Can I safely use a struct as a concrete command object or should I use a class?

public struct MoveCameraCommand : ICommand
{
    public CameraController cameraController;
    public Vector2 moveDelta;

    public MoveCameraCommand(CameraController controller, Vector2 moveDelta)
    {
        this.cameraController = controller;
        this.moveDelta = moveDelta;
    }

    public void Execute()
    {
        cameraController.Move(moveDelta);
    }

    public void Undo()
    {
        cameraController.Move(-moveDelta);
    }
}

I've noticed that when using a regular class, I'm generating garbage each frame with my camera, because move commands are issues very often. Since the game will be running on Android tablets, I was worried and switched to the struct implementation, although I've never seen it before. Might this cause other problems? Is this bad design in general? Should I not be using the command pattern for something like a camera, that is updated each frame?

\$\endgroup\$
  • \$\begingroup\$ "move commands are issues very often" What means 'very often'? Do you use undo/redo programatically, or is it used exclusively by the user? \$\endgroup\$ – JanDotNet Jun 13 '16 at 5:50
  • \$\begingroup\$ Currently only the user issues commands, but there can be multiple ones within each frame. In the future I would like my AI to also issue commands; both with features like undo/redo or replay. Right now I'm generating 32 bytes of garbage each frame. \$\endgroup\$ – Xarbrough Jun 13 '16 at 8:36
4
\$\begingroup\$

It will not work well, I think. The only way to escape boxing on storing is to have a value type collection, so all your commands will need to be of the same type...

Also structs are very specific things, have a look below:

  1. Here about Boxing/unboxing.
  2. Size/speed issues - try to run this test.
  3. See Using Structs:

When you create a struct object using the new operator, it gets created and the appropriate constructor is called. Unlike classes, structs can be instantiated without using the new operator. In such a case, there is no constructor call, which makes the allocation more efficient. However, the fields will remain unassigned and the object cannot be used until all of the fields are initialized.

When a struct contains a reference type as a member, the default constructor of the member must be invoked explicitly, otherwise the member remains unassigned and the struct cannot be used. (This results in compiler error CS0171.)

There is no inheritance for structs as there is for classes. A struct cannot inherit from another struct or class, and it cannot be the base of a class. Structs, however, inherit from the base class Object. A struct can implement interfaces, and it does that exactly as classes do.

\$\endgroup\$
  • \$\begingroup\$ The boxing/unboxing turned out to be the issue here. A class would generate 32 bytes of garbage at construction, but my struct would also generate 32 bytes allocation when boxed/unboxed in my collections. So nothing won, probably only introducing other problems. Gonna stick with classes. \$\endgroup\$ – Xarbrough Jun 13 '16 at 11:58
0
\$\begingroup\$

Just an idea - I would probably try to consider serializing command history to the couple of memory streams... Then you can create a command struct on the stack, execute, and make it persist itself to the stream. It is important to be careful about keeping references to the outside resources active though - some kind of resource locator service might help.

Just don't allocate new byte buffer for the write/read operation, BinaryWriter/BinaryReader + MemoryStream should not do it also every time.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.