Discover Python and Patterns (20): Better commands

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

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.

Command pattern: commands hierarchy

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: 

The 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.

The 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.

The TargetCommand is also a child class of Command and handles the orientation of a weapon towards a unit.

Changes to the Unit class hierarchy

In the previous posts, we created a Unit class hierarchy (the Tank and 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!

Considering our 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)   

Implementation of Command

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()

Implementation of MoveCommand

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.

The 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 self instance.

Implementation of TargetCommand

This class is easy to implement; we only update the weaponTarget of the unit attribute:

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 Tank and 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.

Store the commands

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 UserInterface class:

class UserInterface():
    def __init__(self):
        ...
        
        # Controls
        self.commands = []
        self.playerUnit = self.gameState.units[0]

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.

Create the commands

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[0] / self.cellWidth - 0.5
    targetCell.y = mousePos[1] / 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 playerUnit attribute.

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

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!

Final program

In the next post, I’ll use this new implementation of the Command pattern to handle weapon bullets!

This entry was posted in Tutorial and tagged , , . Bookmark the permalink.

Leave a Reply