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
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):
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:
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.
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.
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)
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.position
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:
- To create a variable that stores the cell targeted by the mouse;
- To compute the target cell coordinates;
- To transmit this value to all units.
Following this recipe, we create a new
targetCommand attribute in the
class UserInterface(): def __init__(self): ... self.targetCommand = Vector2(0,0) ...
processInput() method of the
UserInterface class computes the target cell coordinates, and store them in the
class UserInterface(): ... def processInput(self): ... mousePos = pygame.mouse.get_pos() self.targetCommand.x = mousePos / self.cellWidth - 0.5 self.targetCommand.y = mousePos / self.cellHeight - 0.5 ...
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.
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) ...
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
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.
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.