Design Patterns and Video Games

Discover Python and Patterns (19): Mouse

In this post, I want to use the mouse to orient the unit weapons.

This post is part of the Discover Python and Patterns series

Orient the weapons

The expected result is the following. The tank weapon targets the mouse position, and the tower weapons target the tank:

Before considering the mouse handling, we need to be able to rotate the unit weapon correctly.

Render a rotated tile

The renderTile() method

In the Layer class, there is the renderTile() method that renders a tile from a tileset. I propose to add a new angle argument to this method. If this argument is not empty (not None), then we rotate the tile:

def renderTile(self,surface,position,tile,angle=None):
    # Location on screen
    spritePoint = position.elementwise()*self.ui.cellSize

    # Texture
    texturePoint = tile.elementwise()*self.ui.cellSize
    textureRect = Rect(int(texturePoint.x), int(texturePoint.y), self.ui.cellWidth, self.ui.cellHeight)

    # Draw
    if angle is None:
        surface.blit(self.texture,spritePoint,textureRect)
    else:
        # Extract the tile in a surface
        textureTile = pygame.Surface((self.ui.cellWidth,self.ui.cellHeight),pygame.SRCALPHA)
        textureTile.blit(self.texture,(0,0),textureRect)
        # Rotate the surface with the tile
        rotatedTile = pygame.transform.rotate(textureTile,angle)
        # Compute the new coordinate on the screen, knowing that we rotate around the center of the tile
        spritePoint.x -= (rotatedTile.get_width() - textureTile.get_width()) // 2
        spritePoint.y -= (rotatedTile.get_height() - textureTile.get_height()) // 2
        # Render the rotatedTile
        surface.blit(rotatedTile,spritePoint)

The first lines (2-7) are as before: we compute the location on the surface (or screen) and the rectangle of the tile in the texture tileset.

If the angle is None (line 10), then we render as before (line 11).

Note that the angle argument in the method declaration (line 1) has a default value set as None. If we call the method without this last argument, then angle gets this default value.

Tile rotation around its center

Lines 13-22 rotate and render the tile.

Lines 14-15 extract the tile from the texture tileset in textureTile. It is a Pygame surface with the size of a cell, e.g. 64 per 64 pixels in our current implementation.

Line 17 rotates the tile and store the result in rotatedTile. This new surface can be larger than the tile. The following figure can help you better understand why (I added a black border to see the edges of the tile):

Pygame rotate

As a result, if we want to draw the rotated tile centered on the base tile, we need to update spritePoint coordinates: it is what lines 19-20 do.

Since we draw from the top left corner of the tile, we have to draw the sprite with a small shift. The following figure describes the computation on the x-axis:

Pygame rotate and center

The value of the x-shift is half the width of the rotated sprite minus the half the width of the sprite:

 rotatedTile.get_width()//2 - textureTile.get_width()//2

Note the operator '//': it is the integer division. Otherwise, if you use the '/' operation, a float division is computed, which is not relevant in our case, since we need pixel coordinates.

Weapon target

In the Unit class, I propose to add a new attribute weaponTarget. This attribute contains the coordinates of the cell targeted by the unit weapon.

The render() method of the UnitsLayer class uses this attribute. It is as before, except that we compute the angle between the current unit position and the targeted cell (lines 4-5):

def render(self,surface):
    for unit in self.units:
        self.renderTile(surface,unit.position,unit.tile)
        size = unit.weaponTarget - unit.position
        angle = math.atan2(-size.x,-size.y) * 180 / math.pi
        self.renderTile(surface,unit.position,Vector2(0,6),angle)

Target selection

We are now able to render a unit with a weapon oriented towards to any cell. We need to choose which one is targeted by each unit.

I first create a new orientWeapon() method in the Unit class hierarchy:

The principle is as for the move() method: the implementation in the base class raises a NotImplementedError exception, and the implementation in the child classes is specific.

For the tank, we copy the target method argument to the weaponTarget attribute. Later, we use the mouse coordinates to define target, and the tank weapon always targets the mouse cursor:

class Tank(Unit):
    ...
    def orientWeapon(self,target):
        self.weaponTarget = target

For the towers, we ignore the method argument, and copy the tank position (the first unit in the list of units) to the weaponTarget attribute. This way, all towers weapon always target the tank:

class Tower(Unit):
    ...
    def orientWeapon(self,target):
        self.weaponTarget = self.state.units[0].position

The mouse

The most challenging part is now behind us! All that remains is the use of the mouse coordinates to orient the tank weapon.

As before, we use a basic implementation of the Command pattern. So we need:

Following this recipe, we create a new targetCommand attribute in the UserInterface class:

class UserInterface():
    def __init__(self):
        ...
        self.targetCommand = Vector2(0,0)
    ...

The processInput() method of the UserInterface class computes the target cell coordinates, and store them in the targetCommand attribute:

class UserInterface():
    ...
    def processInput(self):
        ...
        mousePos = pygame.mouse.get_pos()                    
        self.targetCommand.x = mousePos[0] / self.cellWidth - 0.5
        self.targetCommand.y = mousePos[1] / self.cellHeight - 0.5 
    ...

The pygame.mouse.get_pos() function returns the mouse pixel coordinates in the window. We divide these coordinates by the size of a cell to get cell coordinates. Then, we substract 0.5 to target the center of the cell.

The update() method of the UserInterface class transmits all commands (move and target) to the game state:

class UserInterface():
    ...
    def update(self):
        self.gameState.update(self.moveTankCommand,self.targetCommand) 
    ...

Finally, the update() method of the GameState class gives the moveTankCommand to the move() method of all units, and gives the targetCommand to the orientWeapon() method of all units. Then, each unit can use (or don't use) these commands.

class GameState():
    ...
    def update(self,moveTankCommand,targetCommand):
        for unit in self.units:
            unit.move(moveTankCommand)
        for unit in self.units:
            unit.orientWeapon(targetCommand) 
    ...

In our current implementation, only the tank uses the commands, and the towers ignore them. Try to change it, for instance, let all towers target the mouse cursor or one of the towers. To get such results, you only need to change the orientWeapon() method of the Tower class.

Final program

In the final program, I also added an orientation attribute to units, so the base of each unit can be oriented. I use this attribute to orient the tank base towards its current move.

Download program and assets

The current implementation of the Command pattern has many flaws. Now we saw class inheritance, I'll be able to show you a better implementation in the next post.