introduction
Command mode is a common mode in game development. If you only look at GoF's definition of this pattern, you will feel obscure and confused. My simple understanding of the command pattern is an object-oriented method call. We encapsulate a request method and save the required state of the request method when it is called in the corresponding class. Then we can instantiate the encapsulated request at any time according to our own needs. More simply, you can understand it as an object-oriented form of callback. The core of command mode is two points: 1. Request encapsulation 2. Requested operation
definition
Encapsulating the "request" as an object allows you to parameterize different requests from the client, and perform the requested operations with queue, record, recovery and other methods.
Class diagram
explain
Command (command interface)
An interface is declared for all commands. By calling the Execute() method of the command object, the receiver can perform relevant actions. This interface also has an Undo() method to support revocable operations
Concretecommand (command implementation)
It implements the encapsulation of commands, including the parameters of each command and the receiver (function executor). The caller only needs to call Execute() to make the request, and then the ConcreteCommand calls one or more actions of the receiver.
Receiver (function executor)
The class object that is encapsulated in the concretecommand (command implementation) class and really performs the function. Know how to do the necessary work to implement this request. Any class can act as a receiver.
Client (client / command initiator)
It is responsible for creating a ConcreteCommand, and can set the command to the receiver (function executor) as appropriate.
Invoker (command manager)
The management container or management class of the command object and is responsible for requiring each command to perform its functions. The function of executing commands at a required point in time according to the situation.
Example explanation
We used to play the alloy warhead game. Players control the game characters to make various operations through buttons. such as
1. Press the A key to jump and avoid obstacles and enemies 2. Press the S key to fire bullets at the enemy 3. Press the D key to switch weapons
For these three operations, we can abstract three specific command implementations:
// Command interface export interface Command { // The actor is the Receiver, which is set by the Client. The settings can be saved in the Command class when the Command is created // It can also be passed in when the command is called according to the situation Execute(actor: GameActor); Undo(); } // Implement jump command export class JumpCommand implements Command { Execute(actor: GameActor) { actor.Jump(); } Undo(){ } } // Achieve fire command export class FireGunCommand implements Command { Execute(actor: GameActor) { actor.FireGun(); } Undo(){ } } // Switch weapon command export class SwapWeaponCommand implements Command { Execute(actor: GameActor) { actor.SwapWeapon(); } Undo(){ } }
The player's actions include jumping, firing and switching weapons. The player here is the receiver (function executor), which is responsible for the specific behavior to be displayed when the command is called. We have implemented a gameactor (player character class):
// Player character class, equivalent to receiver (function executor) export class GameActor { Jump() { console.log("Player beat"); } FireGun() { console.log("Player fire"); } SwapWeapon() { console.log("Switch weapons"); } }
Finally, we implement the classes that generate and execute commands (Client, Invoker)
// Equivalent to client (command generator) and invoker (command manager) @ccclass('InputHandler') export class InputHandler extends Component { private jumpCommand: Command | null = null; private fireGunCommand: Command | null = null; private swapWeaponCommand: Command | null = null; private curCommand: Command | null = null; private actor: GameActor | null = null; onLoad () { systemEvent.on(SystemEvent.EventType.KEY_DOWN, this.handleInput, this); // Instantiate and save existing commands this.jumpCommand = new JumpCommand(); this.fireGunCommand = new FireGunCommand(); this.swapWeaponCommand = new SwapWeaponCommand(); // Player character class, equivalent to receiver (function executor). It can be passed in when the function is executed or set when the command is created this.actor = new GameActor(); } onDestroy () { systemEvent.off(SystemEvent.EventType.KEY_DOWN, this.handleInput, this); } // Set the corresponding command instance through the player's key input handleInput (event: EventKeyboard) { if (event.keyCode == KeyCode.KEY_A) { this.curCommand = this.jumpCommand; } else if (event.keyCode == KeyCode.KEY_S) { this.curCommand = this.fireGunCommand; } else if (event.keyCode == KeyCode.KEY_D) { this.curCommand = this.swapWeaponCommand; } if (this.curCommand != null && this.actor != null) { this.curCommand.Execute(this.actor); } } }
Above, we use command mode to solve the problem of deep coupling between player operation and behavior. Does this implementation seem to have clearer code logic and easier to expand and maintain. The advantage of this is more than that. If we need to add AI in the game, we can encapsulate the commands that AI can execute, and then the AI code only needs to be responsible for generating commands and executing them at the right time. If AI and real players can implement exactly the same commands, they can reuse the same set of command classes.
The decoupling between the AI of selecting commands and the code of representing players provides us with great flexibility. We can use different AI modules for different characters. Or we can mix and match AI for different kinds of behavior. You want a more aggressive enemy? Just insert a more aggressive AI code to generate commands for it. In fact, we can even use AI on players' characters, which is very useful for game demonstration.
By encapsulating the commands that control the role as objects, we can eliminate the tight coupling of direct function calls. We can implement a queue, put the generated commands into the queue, and then take out the commands in the queue to execute in the frame loop. As shown in the figure below
We can also parameterize the player's operation commands and send them to another client through the network, so as to realize the synchronization and playback of player behavior. As shown in the figure below
At this point, I don't think the command mode is very useful. However, it's not over yet. In the above class diagram definition, we also have an undo operation, which also says that the command mode can support undo operations. If a command object can execute some behaviors, it should be easy to undo them. Undo and Redo are well-known applications of command mode. You can often see undo in some strategy games to ensure that players can roll back some dissatisfied steps. Some game level editors also need command mode to help us undo commands.
With command mode, it's a piece of cake for us to implement Undo. Suppose we make a single turn game, we want players to Undo some actions so that they can focus more on strategy rather than speculation. We can implement a move command class and save the last location state in the command class, so that we can restore to the last state when we execute Undo.
export class MoveCommand implements Command { private _x: number; private _y: number; private _beforeX: number; private _beforeY: number; private _actor: GameActor; // Set the Receiver and required state when instantiating constructor(actor: GameActor, x: number, y: number) { this._actor = actor; this._x = x; this._y = y; this._beforeX = 0; this._beforeY = 0; } // Save the current state before executing the command behavior Execute() { this._beforeX = this._actor.posX(); this._beforeY = this._actor.posY(); this._actor.moveTo(this._x, this._y); } // Undo using the last state saved by the command Undo(){ this._actor.moveTo(this._beforeX, this._beforeY); } }
We can implement the method of generating commands in this way
handleInput (event: EventKeyboard): Command { if (event.keyCode == KeyCode.KEY_A) { let destX = this.actor.posX() - 10; return new MoveCommand(this.actor, destX, this.actor.posY()); } else if (event.keyCode == KeyCode.KEY_D) { let destX = this.actor.posX() + 10; return new MoveCommand(this.actor, destX, this.actor.posY()); } }
We can put the generated command into a stack. When we hit Control-Z, we can pop up the command at the top of the stack, and then execute Undo of the current command at the top of the stack.
If we want to satisfy not only the Undo function but also the redo function, we can implement a Redo() method for the command. Put the command in a queue and maintain a reference to the current command. If you cancel, point the application of the current command to the previous command. If you want to redo, point the current command to the next application. If you cancel and select a new command, all commands behind the current command in the list will be discarded. As shown in the figure:
reference resources: Command · Design Patterns Revisited · Game Programming Patterns