This post will continue the topic we started in the previous article regarding Design Patterns https://dev.to/almirs/design-patterns-4bbf . This time the subject is Command Pattern. Just as all other patterns, it is a design technique that provides a solution to a specific situation. Like for other patterns, it is crucial to recognise where to apply it to get the most of its efficiency and power. So let’s start with the basics.
In its essence, the Command pattern represents a method call wrapped in an object, or some sort of data that can be related to a variable, passed to a function etc.
By definition in Design Patterns: Elements of Reusable Object-Oriented Software book:
“Commands are an object-oriented replacement for callbacks.”
So why and when would we use it? This whole article tries to address that question, let’s just mention some of the pattern advantages here, and try to prove them in the following sections. In the first place, it is due to reducing coupling between “command sender” and “ command receiver”. Queuing commands, scheduling their executions is a way easier to implement by following this pattern. Other than that, implementing reversible operation is most suitable to do with the Command pattern.
Let’s introduce some examples. Suppose we need to configure the input for some game engine, map user’s actions(mouse click, keyboard event) to action in the game. A primitive, simple implementation would be something like:
void InputHandler::handleInput()
{
if (isPressed(BUTTON_X)) jump();
else if (isPressed(BUTTON_Y)) fire();
else if (isPressed(BUTTON_A)) run();
}
Obviously, it is not difficult to understand this code snippet and what it is supposed to do. In its basics it works, but with a few things to consider. First, are we fine with connection input to game action directly? We potentially need to configure inputs per different preferences. Actually, we will need some sort of variable or object to be able to swap out buttons functionality, which leads us slowly but certainly to the Command pattern.
Let’s define a basic class that represents command:
class Command {
public:
virtual ~Command() {}
virtual void execute(GameActor&) = 0;
};
GameActor here represents the Game object, it is passed so that derived command can invoke methods on the game actor of choice.
Obviously, we are going to need implementations for specific commands:
class JumpCommand : public Command {
public:
virtual void execute(GameActor& actor) { actor.jump(); }
};
class FireCommand : public Command {
public:
virtual void execute(GameActor& actor) { actor.fire(); }
};
class RunCommand : public Command {
public:
virtual void execute(GameActor& actor) { actor.run(); }
};
Input handler should keep pointers for commands for each button:
class InputHandler {
public:
~InputHandler() {}
std::unique_ptr<Command> handleInput(int input) {
if (input == BUTTON_X)
return std::move(buttonX_);
else if (input == BUTTON_Y)
return std::move(buttonY_);
else if (input == BUTTON_Z)
return std::move(buttonZ_);
return nullptr;
}
private:
int BUTTON_X = 0;
int BUTTON_Y = 1;
int BUTTON_Z = 2;
std::unique_ptr<Command> buttonX_ = std::make_unique<JumpCommand>();
std::unique_ptr<Command> buttonY_ = std::make_unique<FireCommand>();
std::unique_ptr<Command> buttonZ_ = std::make_unique<RunCommand>();
};
handleInput() method delegates commands, so now there is a layer between sender and receiver.
Than we need some code that takes command and run it on the specific game actor:
GameActor gameActor;
InputHandler inputHandler;
std::unique_ptr<Command> command = inputHandler.handleInput(1);
if (command) command->execute(gameActor);
The important thing to notice here is that we can delay command execution which by itself is one of the pattern advantages.
Now we can spot on that we are on the same point as from the beginning, but with an additional layer between the command and actor that performs it, which gives us the ability to run command on any actor in the game. We removed the tight coupling of a direct method call, it is now possible to generate commands and put them in some kind of queue or a stream, where on one side is the producer, on the other is the consumer.
Additionally, if we take those commands and make them serializable, they can be transferred over the network or replayed on another machine which brings another level of flexibility especially in terms of networked games.
We have just demonstrated the basic pattern idea, we decoupled classes that invoke operations from classes that perform them. We also can introduce new commands, without breaking existing code, which is a way to support Open/Closed principle or we can combine more simple commands into more complex ones.
Undo and Redo
With all being mentioned above, it should be easy to implement a basic version of undo/redo feature in our game, but we will go through implementation details just to wrap up the whole picture.
Since this is a widely used feature in games due to many rollbacks in moves that are wrong or simply not desired, and we are already using objects to do things, it should be easy to undo them.
Let’s start with MoveUnitCommand, which instance represents a specific concrete move in the game’s sequence of turns
class MoveUnitCommand : public Command {
public:
MoveUnitCommand(const Unit& unit, int x, int y)
: unit_(unit), x_(x), y_(y)) {}
virtual void execute() {
unit_.moveTo(x_, y_);
}
private:
Unit unit_;
int x_, y_;
}
Input handler will generate this type of command every time the move button is pressed:
class InputHandler {
public:
~InputHandler() {}
std::unique_ptr<Command> handleInput(int input) {
Unit unit;
if (input == BUTTON_UP)
return std::make_unique<MoveUnitCommand>(unit, unit.x(), unit.y() - 1);
else if (input == BUTTON_DOWN)
return std::make_unique<MoveUnitCommand>(unit, unit.x(), unit.y() + 1);
return nullptr;
}
private:
int BUTTON_UP = 0;
int BUTTON_DOWN = 1;
};
To make “Undo” operation possible we are going to need to remember the current game state, so our MoveUnitCommand with “Undo” support would be like:
class MoveUnitCommand : public Command {
public:
MoveUnitCommand(const Unit& unit, int x, int y)
: unit_(unit), x_(x), y_(y), xBefore_(0), yBefore_(0) {}
virtual void execute() {
xBefore_ = unit_.x();
yBefore_ = unit_.y();
unit_.moveTo(x_, y_);
}
virtual void undo() { unit_.moveTo(xBefore_, yBefore_); }
private:
Unit unit_;
int x_, y_, xBefore_, yBefore_;
};
Basically, we introduced new variables for saving the current position, which is obviously needed in order to roll back the previous move. To enable undo ability we keep the last command executed and we are there.
Note that, the two “undo” in this context means “redo” and the command is executed again.
Supporting multiple levels of “undo” should not be hard as well, we would need to remember the last n commands instead of just the previous one.
For more details and more examples on this pattern, it is strongly recommended to go through the book (https://gameprogrammingpatterns.com/command.html), the web version is free, and it goes deep into the subject so you can expertise the matter and find more examples and applications suitable to the topic.
Here is Github repository with examples provided in the article
https://github.com/almir-s/design_patterns/tree/main/command
Top comments (0)