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