Since we saw the class inheritance, I can show you how to get a better implementation of the Command pattern. It eases a lot the management of commands and introduces exciting features.
This post is part of the Discover Python and Patterns series
The Command pattern proposes to store the information required for updating data and execute these updates at a later time, eventually in a different order.
In the previous posts, we simply implement this recipe, using a variable to store our command. For instance, the
moveTankCommand attribute of the
UserInterfaceclass memorizes the direction in which the tank should move. Then, during the game update, this attribute is used to move the tank.
Storing in a single attribute is limited, and we can do much better if we store the commands data in a class instance. Furthermore, we can also embed the update process in this class, rather than putting it somewhere in the main game state class.
Using these ideas, we can implement the Command pattern for our two current commands (move the tank and target a unit) using the following class hierarchy:
Command is the base class. It only contains a
run() method that executes the tasks described by the command instance. Considering methods, we don't need more. Considering attributes, we could add data used by all child classes like a reference to the game state, but I don't know yet if all child classes will require it.
MoveCommand is a child class of
Command and stores all the information we need to move a unit. Note that I extended the command to control any unit (tanks or towers), which leads to funny gameplay for free, like controlling several tanks or towers.
TargetCommand is also a child class of
Command and handles the orientation of a weapon towards a unit.
In the previous posts, we created a
Unit class hierarchy (the
Tower classes) to represent all units and handles the update of their data.
It is a simple approach I selected to ease the understanding. Many software architectures use this recipe because it is easy to use and understand. However, it has several flaws and fewer features than the one I propose to use in this post. The main issue is that all updates in centered around a single object. For instance, we implemented the move of the tank in the
Tank class. It is okay because the changes only concern the tank.
As the game grows in complexity, this case is less likely to happen. When we have to implement the update of several game items resulting from one command, we must choose one of the classes involved. For instance, when one unit destroy one another, which class should implement this? The destroying or the destroyed class? It is a simple case; some commands can update the data of many items or parts of the world.
With the approach I propose to follow, there are no such flaws. The game state classes store data on one side, and the command classes update data on the other side. It follows the most important rule of software design: divide problems into sub-problems!
Unit class hierarchy, we no more need it. A single
Unit class can store the data of any unit:
class Unit(): def __init__(self,state,position,tile): self.state = state self.position = position self.tile = tile self.orientation = 0 self.weaponTarget = Vector2(0,0)
Note that you can still use a class hierarchy for the beauty of the design. For instance, you can add the two following classes:
class Tank(Unit): def __init__(self,state,position,tile): super().__init(state,position,tile) class Tower(Unit): def __init__(self,state,position,tile): super().__init(state,position,tile)
The implementation of the base class is straightforward. The
run() method only raises an exception, in case we forgot to implement this method in a child class:
class Command(): def run(self): raise NotImplementedError()
The implementation of the
MoveCommand class is a refactoring of what we did previously:
class MoveCommand(Command): def __init__(self,state,unit,moveVector): self.state = state self.unit = unit self.moveVector = moveVector def run(self): # Update unit orientation if self.moveVector.x < 0: self.unit.orientation = 90 elif self.moveVector.x > 0: self.unit.orientation = -90 if self.moveVector.y < 0: self.unit.orientation = 0 elif self.moveVector.y > 0: self.unit.orientation = 180 # Compute new tank position newPos = self.unit.position + self.moveVector # Don't allow positions outside the world if newPos.x < 0 or newPos.x >= self.state.worldWidth \ or newPos.y < 0 or newPos.y >= self.state.worldHeight: return # Don't allow wall positions if not self.state.walls[int(newPos.y)][int(newPos.x)] is None: return # Don't allow other unit positions for otherUnit in self.state.units: if newPos == otherUnit.position: return self.unit.position = newPos
The constructor copies its arguments to the attributes.
run() method is very similar to what we did in the
move() method of the previous
Tank class. Everything is the same except that we update a
self.unit unit instead of the
This class is easy to implement; we only update the
weaponTarget of the
class TargetCommand(Command): def __init__(self,state,unit,target): self.state = state self.unit = unit self.target = target def run(self): self.unit.weaponTarget = self.target
It is similar to what we did in the
orientWeapon() methods of the previous
Unit classes. Note that this time, there is no more binding to the first unit of the unit lists. It is then more straightforward and safer to set what the player can control.
We need a place to store all the commands we create. We could create an attribute for each case, but I propose a more interesting approach. I propose to stores all the commands in a list. That way, we can have any number of commands. I create this list as a new
commands attribute of the
class UserInterface(): def __init__(self): ... # Controls self.commands =  self.playerUnit = self.gameState.units
Note that I also created a new
playerUnit attribute. It references the unit controlled by the player. You can try to change its value and control any other unit with no more changes.
The creation of the commands is still in the
processInput() method of the
UserInterface class. This time, we create child classes of
Command rather than setting a single attribute:
def processInput(self): # Pygame events (close & keyboard) moveVector = Vector2() for event in pygame.event.get(): if event.type == pygame.QUIT: self.running = False break elif event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: self.running = False break elif event.key == pygame.K_RIGHT: moveVector.x = 1 elif event.key == pygame.K_LEFT: moveVector.x = -1 elif event.key == pygame.K_DOWN: moveVector.y = 1 elif event.key == pygame.K_UP: moveVector.y = -1 # Keyboard controls the moves of the player's unit if moveVector.x != 0 or moveVector.y != 0: command = MoveCommand(self.gameState,self.playerUnit,moveVector) self.commands.append(command) # Mouse controls the target of the player's unit mousePos = pygame.mouse.get_pos() targetCell = Vector2() targetCell.x = mousePos / self.cellWidth - 0.5 targetCell.y = mousePos / self.cellHeight - 0.5 command = TargetCommand(self.gameState,self.playerUnit,targetCell) self.commands.append(command) # Other units always target the player's unit for unit in self.gameState.units: if unit != self.playerUnit: command = TargetCommand(self.gameState,unit,self.playerUnit.position) self.commands.append(command)
Lines 4-20 computes the move vector using the Pygame keyboard events, and store it in the
moveVector variable. Lines 23-25 create an instance of the
MoveCommand class and add it to the list of commands.
Lines 28-33 computes the cell targeted by the mouse cursor as before. Then, it creates an instance of the
TargetCommand class and adds it to the list of commands. The controlled unit is also the one referenced by the
Lines 36-39 are certainly the most strange. They create a target command for all units except the one controlled by the player. It is to get the same behavior as in the previous post, where all towers target the tank.
A legitimate question is the following one: why update the non-playing game items with commands? It looks much more natural to update the towers in their respective classes directly. A quick answer is: what if I want to control another unit? With the proposed scheme, I only need to change the
playerUnitattribute, and the player can control a tower, and all other units, including the tank, will point this tower.
Using commands to control any update of the game state is even more powerful. Following this approach, we have full control of the game updates. Thanks to the commands list, we know the order of command execution. We can also change this order afterward. For instance, it can be interesting always to move the tank firstly, even if it not controlled by the player. We can print these commands and better understand why we get one behavior instead of another. Admittedly, for a small game like the one we are creating, it is not mandatory. But think about a more complex game, like a MMORPG where 10,000 of players send dozen of commands each second. If there is a problem, it is infinitely easier to find it when the execution flow is under full control.
If the motivation behind the use of this pattern is not clear, don't worry. Please trust the experimented developers as I did many years ago, and it will save you valuable time!
Update the game state is easy: we only have to run all the commands. This task is in the
update() method of the
UserInterface class, and replace the call to the previous
update() method of the
GameState class (which is no more required):
class UserInterface(): ... def update(self): for command in self.commands: command.run() self.commands.clear()
Once we have executed these commands (lines 4-5), we must clear the commands list (line 6). Otherwise, they will repeat endlessly!
In the next post, I'll use this new implementation of the Command pattern to handle weapon bullets!