We still have no background in our game: I add one in this post using 2D arrays.
This post is part of the Discover Python and Patterns series
We have already seen lists in Python, for instance, the creation of a list of three numbers:
list = [ 1,2,3 ]
If you remember well, an item of a list can be anything, including a list:
array2d = [ [1,2,3], [4,5,6] ]
Using this syntax, I created a 2D array with two rows of three items:
1 | 2 | 3 |
4 | 5 | 6 |
You can access each row of the array with one pair of brackets:
print(array2d[0]) # [1,2,3]
print(array2d[1]) # [4,5,6]
You can use a second pair of brackets to access of the numbers of the 2D array:
print(array2d[0][1]) # 2
print(array2d[1][1]) # 5
More generally, you can access (or change) any item with the syntax array2d[y][x]
, where x
and y
are the coordinates in the array.
Resizing a 2D array is not a simple task; most of the time, we don't need to change the size of 2D arrays. For dynamic 2D arrays, we need to create or use a dedicated Python library, but this is another story.
You can iterate through 2D arrays using a double for
statement:
for y in range(2):
for x in range(3):
print(array2d[y][x]," ",end='')
print()
The end=''
argument in the print()
call means that we don't want the printing of a line return. It leads to the following result:
1 2 3
4 5 6
Using a 2D array, I wish to add a background to our game:
I also created a tileset using the sprites found here: zintoki.itch.io/ground-shaker, created by zintoki.
In the constructor of the GameState
class, I create a new attribute ground
with the coordinates of tiles in the tileset. For instance, Vector2(5,1)
are the coordinates of the green grass tile:
self.ground = [
[ Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(6,2), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1)],
[ Vector2(5,1), Vector2(5,1), Vector2(7,1), Vector2(5,1), Vector2(5,1), Vector2(6,2), Vector2(7,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(6,1), Vector2(5,1), Vector2(5,1), Vector2(6,4), Vector2(7,2), Vector2(7,2)],
[ Vector2(5,1), Vector2(6,1), Vector2(5,1), Vector2(5,1), Vector2(6,1), Vector2(6,2), Vector2(5,1), Vector2(6,1), Vector2(6,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(6,2), Vector2(6,1), Vector2(5,1)],
[ Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(6,1), Vector2(6,2), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(6,2), Vector2(5,1), Vector2(7,1)],
[ Vector2(5,1), Vector2(7,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(6,5), Vector2(7,2), Vector2(7,2), Vector2(7,2), Vector2(7,2), Vector2(7,2), Vector2(7,2), Vector2(7,2), Vector2(8,5), Vector2(5,1), Vector2(5,1)],
[ Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(6,1), Vector2(6,2), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(6,2), Vector2(5,1), Vector2(7,1)],
[ Vector2(6,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(6,2), Vector2(5,1), Vector2(5,1), Vector2(7,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(6,2), Vector2(7,1), Vector2(5,1)],
[ Vector2(5,1), Vector2(5,1), Vector2(6,4), Vector2(7,2), Vector2(7,2), Vector2(8,4), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(6,2), Vector2(5,1), Vector2(5,1)],
[ Vector2(5,1), Vector2(5,1), Vector2(6,2), Vector2(5,1), Vector2(5,1), Vector2(7,1), Vector2(5,1), Vector2(5,1), Vector2(6,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(7,4), Vector2(7,2), Vector2(7,2)],
[ Vector2(5,1), Vector2(5,1), Vector2(6,2), Vector2(6,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1), Vector2(5,1)]
]
Creating such a 2D array directly with Python code is not handy; I'll show later how to create them with software like Tiled.
I first create a new method renderGround()
in the UserInterface
class dedicated to the rendering of a single tile. It works as before (like in the renderUnit()
method):
def renderGround(self,position,tile):
# Location on screen
spritePoint = position.elementwise()*self.cellSize
# Texture
texturePoint = tile.elementwise()*self.cellSize
textureRect = Rect(int(texturePoint.x), int(texturePoint.y), int(self.cellSize.x),int(self.cellSize.y))
self.window.blit(self.groundTexture,spritePoint,textureRect)
In the render()
method of the UserInterface
class, I add the rendering of all tiles of the 2D array using a double for
statement (before the rendering of the unit!):
for y in range(int(self.gameState.worldSize.y)):
for x in range(int(self.gameState.worldSize.x)):
self.renderGround(Vector2(x,y),self.gameState.ground[y][x])```
In several locations in the program, there are expressions a little long. I want to show you here a feature of the Python language to reduce them a bit, and thus get a program easier to read.
For example, the expression to get the integer value of the width of the world is the following one:
int(self.gameState.worldSize.x)
We can shorter it in the following way:
self.gameState.worldWidth
A first solution to get this result is to create a new attribute worldWidth
in the GameState
class. We can initialize it with the int value of the x
member of the worldSize
attribute. If the world's width never changes, everything is fine.
However, this first solution is dangerous. After several months or years of improvements in our game, we could forget this trick, and forget to update worldWidth
when worldSize
changes (and vice-versa). It will lead to unexpected behavior and a difficult bug to solve.
The Python language offers a second solution: the properties. In a class, you can create a method that works as an attribute:
class GameState():
...
@property
def worldWidth(self):
return int(self.worldSize.x)
...
The worldWidth()
method returns the value of the x
member of the worldSize
attribute converted to an int
.
Pay attention to the line above: there is a @property
. This kind of line is called an annotation. It tells that the following method is an "accessor" or a "getter". It should have no arguments except self
, and must return a value. Then, we can use it as if it was an attribute (to get the value, to change it, it is another syntax).
The advantage of this solution compared to the first one is that, if we update the worldSize
attribute, then the worldWidth
property (e.g., like a "fake attribute") is also updated.
Using this syntax, I add two new properties worldWidth
and worldHeight
in the GameState
class:
class GameState():
...
@property
def worldWidth(self):
return int(self.worldSize.x)
@property
def worldHeight(self):
return int(self.worldSize.y)
...
Similarly, I add two properties in the UserInterface
class for the cell width and height:
class UserInterface ():
...
@property
def cellWidth(self):
return int(self.cellSize.x)
@property
def cellHeight(self):
return int(self.cellSize.y)
...
These properties increase the readability in several locations in the program; for instance, the computation of the tile coordinates in the texture goes from:
textureRect = Rect(int(texturePoint.x), int(texturePoint.y), int(self.cellSize.x),int(self.cellSize.y))
to:
textureRect = Rect(int(texturePoint.x), int(texturePoint.y), self.cellWidth, self.cellHeight)
In the next post, I'll show how to create layers in a more efficient way.