Discover Python and Patterns (22): Animations

In this post, I propose to add an explosion animation when someone destroys a unit. It can be achieved in an effective way using the Observer pattern. I also add some optimization to get a smoother experience!

This post is part of the Discover Python and Patterns series

Objective

The following video gives you an idea of the new explosions:

Note that the frame rate is much better, thanks to optimizations I present at the end of this post.

The explosions use a new tileset created by Chatbull and available here: https://opengameart.org/content/explosions-0.

Explosions layer

As for other cases, I want to handle the explosions in the most independent way. For the rendering, I create a new layer class ExplosionsLayer:

class ExplosionsLayer(Layer):
    def __init__(self,ui,imageFile):
        super().__init__(ui,imageFile)
        self.explosions = []
        self.maxFrameIndex = 27
        
    def add(self,position):
        self.explosions.append({
            'position': position,
            'frameIndex': 0
        })

    def unitDestroyed(self,unit):
        self.add(unit.position)
        
    def render(self,surface):
        for explosion in self.explosions:
            frameIndex = math.floor(explosion['frameIndex'])
            self.renderTile(surface,explosion['position'],Vector2(frameIndex,4))
            explosion['frameIndex'] += 0.5
        self.explosions = [ explosion for explosion in self.explosions if explosion['frameIndex'] < self.maxFrameIndex ]

This class has two specific attributes: the list of current explosions and the number of frames in the explosion animation.

It means that the game state does not contain the explosions. The motivation is simple: animations have no impact on the game rules; they are purely aesthetical. So, I have no reason to add this responsibility to the game state. Think about a game server with no screen: it does not need to handle the animations to run.

Add an explosion with several properties

Lines 7-11 is a method that adds a new explosion to the list. Note that I use a new Python syntax:

self.explosions.append({
    'position': position,
    'frameIndex': 0
})

Using this syntax, I add a new item to the list that contains two sub-items ‘position’ and ‘frameIndex’. It is a nice feature of the Python language that allows you to create variables of any name at runtime. The ‘position’ key contains the location of the explosion on the screen, and the ‘frameIndex’ key contains the current animation frame to display.

The curly braces {} defines a Python dictionary. Maybe it is clearer if I split the creation of this dictionary from its addition to the list:

explosion = {
    'position': position,
    'frameIndex': 0
}
self.explosions.append(explosion)

I could also split the creation and the filling of this dictionary:

explosion = {}
explosion['position'] = position
explosion['frameIndex'] =  0
self.explosions.append(explosion)

Line 1 is the creation of an empty dictionary. It as for the lists, except that we replace the brackets [] with the braces {}.

Lines 2 and 3 set values in this dictionary. We use the brackets [] as with lists (but not the braces, don’t ask me why!). The index of each value is between quotes. If you remove these quotes, the index is the value of the variable and not the name between the quote. Here is an example to better understand:

explosion = {}
index = 'position'
explosion[index] = position

The unitDestroyed() method adds a new explosion to the list using the add() method. It used in the Observer pattern; I explain that below.

Render explosions

Lines 16-21 contains the render() method:

def render(self,surface):
    for explosion in self.explosions:
        frameIndex = math.floor(explosion['frameIndex'])
        self.renderTile(surface,explosion['position'],Vector2(frameIndex,4))
        explosion['frameIndex'] += 0.5
    self.explosions = [ explosion for explosion in self.explosions if explosion['frameIndex'] < self.maxFrameIndex ]

Line 2 iterates through all explosions using a syntax we saw many times.

Line 3 casts the ‘frameIndex’ value of the explosion into an integer. We use float values to allow animations with a rate different from the one of the screen.

Line 4 renders the tile. All frames are in the fourth row of the tileset, from the beginning to the end of the animation. If you want to propose different animations, you can create a new item in the explosion dictionary (like ‘tilesRow’) and update all methods accordingly.

Line 5 updates the current frame of the animation. It depends on the current frame rate, and thus on the computer currently running the game. If it is powerful enough, it renders at 60 frames per second, and animations are always the same. If it is not the case, animations last longer. You can get a more robust solution if you introduce time computations (like elapsed time since the last frame).

Note that the animation speed does not depend on game time (a.k.a. epochs)! They live their own life, whatever happens to the game state (like a pause).

Line 6 rebuilds a new list that only contains all unfinished explosions. The ExplosionsLayer is the only one to use this list; we can do this with no risks.

Create explosions with the Observer pattern

If we add an explosion layer to the layers list in the UserInterface class, then adding an explosion starts the animation immediately. For instance, you can add the following after the creation of the layers list (the explosion layer is the fifth one):

self.layers[4].add(Vector2(0,0))

When running the game, an explosion appears at the top left corner of the screen.

A naive way to trigger these animations is then to call the add() method of the ExplosionsLayer class instance when a unit is destroyed (for example, in the MoveBulletCommand class).

By doing so, the game state would depend on the layers. Since the layers already depend on the game state, we would get a circular dependency. In software design, we don’t like circular dependencies and use them only if we have no choice.

The other issue with a game state that depends on layers is that it is hard to control the flow. If the game state changes layers parameters during its updates, then rendering should not happen, since we could have strange results (and in some cases crashes). It is also challenging to handle situations where game state and rendering update at different rates. Finally, we can also add all the motivations for the Command pattern, which also gives better control of the flow.

With the Observer pattern, we can remove the dependency between the game state and the layers. With this approach, the game state ignores the existence of the layers, and the layers updates themselves silently without disturbing the game state.

The Observer pattern

This pattern has two main actors, represented by two classes:

Observer pattern

The Subject class is the observed one, and holds a list of observers. Anyone can add or remove an observer in this list using the registerObserver() and unregisterObserver() method. When something happens, one calls the notifyObservers() method that notifies all observers on the list. Note that the Subject class, as well as anyone outside this class, can trigger the notification.

The instances of the Observer class receive the notifications. For instance, if something happens to the subject, then the somethingHappens() method is called. In this example, there is a single notification, but you can create as many cases as you want, with or without arguments, like somethingCreated(index: int) or mouveMoved(info: MouseInfo). For each case, you need a corresponding notification method in the Subject class, like notifySomethingCreated(index: int) or notifyMouseMoved(info: MouseInfo).

The most exciting feature of this pattern is that the subject knows nothing about its observers. If something happens, one sends the notifications, and the subject life goes on with no changes. There are no dependencies between the subject and the observer classes.

A typical use of this pattern is in Graphic User Interface libraries, where controls (like push buttons) notifies any observer that something happens (like the button was pushed). The vocabulary can be different; for instance, some libraries talk about listeners instead of observers, and events replace notifications. The implementation can also be more advanced, but the principle remains the same.

Observe the game state

We can use the Observer pattern to triggers the explosions (note: I only show the appropriate methods of GameState):

Game state observer pattern

The implementation of the methods of GameState is easy; we add items to a list in addObserver() and iterate through all items in this list in notifyUnitDestroyed():

class GameState:
    ...
    def addObserver(self,observer):
        self.observers.append(observer)
        
    def notifyUnitDestroyed(self,unit):
        for observer in self.observers:
            observer.unitDestroyed(unit)

The GameStateObserver class contains a single method that can be implemented by its children:

class GameStateObserver():
    def unitDestroyed(self,unit):
        pass

Note that the default behavior of the unitDestroyed() method is to do nothing. It is not mandatory to react to all notifications!

The Layer class inherits this class, and now any child class can react when someone destroys a unit. The ExplosionsLayer class is the only interested one, so it is the only one to implement to the unitDestroyed() method:

Layers observe game state

Like we saw before, it adds a new explosion at the location of the destroyed unit (in the arguments):

def unitDestroyed(self,unit):
    self.add(unit.position)

Finally, something needs to tell that a unit is destroyed, and the best one for that is the one who destroys it: the MoveBulletCommand class! More specifically, we add a single line self.state.notifyUnitDestroyed(unit) in the run() method when we destroy the unit:

class MoveBulletCommand(Command):
    ...
    def run(self):
        ...
        # If the bullet hits a unit, destroy the bullet and the unit 
        unit = self.state.findLiveUnit(newCenterPos)
        if not unit is None and unit != self.bullet.unit:
            self.bullet.status = "destroyed"
            unit.status = "destroyed"
            self.state.notifyUnitDestroyed(unit)
            return
        ...

And that’s all! Thanks to the call to notifyUnitDestroyed(), the explosion layers get notified, and we create a new explosion animation. Since the explosions layer is working independently, there is no more to do.

Optimizations

The current frame rate of the game may be slow on most computers because Pygame is not well optimized. It is an excellent library for beginners, but not the best one for optimal user experience. We could hide this dependency behind a facade (using a Facade pattern), and then quickly switch to a better (but more complicated) one later. If you are curious about this approach, you can have a look at the AWT GUI Facade series. It is in Java language, but the principle remains the same.

Any call to a Pygame function is usually costly, so if we find a way to reduce the number of these calls, then we speed up the rendering. Furthermore, the blitting of surfaces with an alpha channel is much slower: we can try to avoid it as much as possible.

We can reduce the number of Pygame function calls in layers through the rendering of background layers on a static surface. Backgrounds never change; we don’t need to redraw them every frame. Furthermore, the first background doesn’t need an alpha channel; we can create a static surface with no transparency for this case. I updated the ArrayLayer class with these optimizations:

class ArrayLayer(Layer):
    def __init__(self,ui,imageFile,gameState,array,surfaceFlags=pygame.SRCALPHA):
        super().__init__(ui,imageFile)
        self.gameState = gameState
        self.array = array
        self.surface = None
        self.surfaceFlags = surfaceFlags
        
    def render(self,surface):
        if self.surface is None:
            self.surface = pygame.Surface(surface.get_size(),flags=self.surfaceFlags)
            for y in range(self.gameState.worldHeight):
                for x in range(self.gameState.worldWidth):
                    tile = self.array[y][x]
                    if not tile is None:
                        self.renderTile(self.surface,Vector2(x,y),tile)
        surface.blit(self.surface,(0,0)) 

There are two new attributes:

  • surface: a Pygame surface that contains the rendered layer. If it is None, it means we did not render it yet, or we should refresh the rendering.
  • surfaceFlags: flags for the creation of a Pygame surface. We are interested in two cases: 0=no alpha channel, and pygame.SRCALPHA=with alpha channel.

In the render() method, if the surface attribute is None (line 10), we create this surface (line 11) and render the layer into it (lines 12-16). Note that self.surface is the class attribute, and surface is the method argument! The blitting at line 16 is as before, except that we replace surface by self.surface. Finally, we blit the surface attribute into the surface argument: this is a single Pygame call, faster than hundreds of them.

Finally, when we create the layers in the constructor of the UserInterface class, the first layer has a surfaceFlags=0 value to create a surface with no alpha channel:

self.layers = [
    ArrayLayer(self.cellSize,"ground.png",self.gameState,self.gameState.ground,0),
    ...
]

Thanks to these optimizations, you should get 60 frames per second.

Final program

In the next post, I’ll show how to create a level for our game using Tiled Map Editor.

This entry was posted in Tutorial and tagged , , . Bookmark the permalink.

Leave a Reply