Design Patterns and Video Games

Discover Python and Patterns (17): 2D arrays

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 forstatement:

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:

Tank game with a background

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

Ground tileset

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 forstatement (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 worldSizeattribute. 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

Download code and assets

In the next post, I'll show how to create layers in a more efficient way.