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
2D arrays
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
Add a background
Using a 2D array, I wish to add a background to our game:

I also created a tileset using the sprites found here: https://zintoki.itch.io/ground-shaker, created by zintoki.

Create the 2D array
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.
Render the tiles in the 2D array
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])```
Python properties
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)
Final program
In the next post, I’ll show how to create layers in a more efficient way.
Hi! I am following your tutorial and I have a question concerning @property. Why can’t we just create a new variable worldWidth and assign to it int(self.worldSize.x). Like:
self.worldWidth = int(self.worldSize.x)
I am new to Pyhon and would appreciate an explanation.
And thank you so much for sharing your experience via this tutorial. It is really helpful. 🙂
You can create a new attribute worldWidth and assign any value inside or outside the class.
However, you can’t control who is updating it. For instance, a user (e.g. someone outside the class) could change it:
class GameState:
def __init__(self):
self.worldSize = Vector2(1,2)
self.worldWidth = int(self.worldSize.x)
self.worldHeight = int(self.worldSize.y)
state = GameState()
state.worldWidth = 3
# => state.worldSize.x is 1 and state.worldWidth is 3, which is inconsistent
With a property, the user can’t change the value of worldWidth, so state.worldWidth always equals int(self.worldSize.x).
If you want to only change the width of the world, you can use a setter:
class GameState:
def __init__(self):
self.worldSize = Vector2(1,2)
@property
def worldWidth(self):
return int(self.worldSize.x)
@worldWidth.setter
def worldWidth(self, width):
self.worldSize.x = width
state = GameState
state.worldWidth = 3
# => state.worldSize.x is 3, state.worldWidth returns 3
properties and setters can “fake” an attribute and let you full control of what happens.
I hope this answer helps 🙂