Special Effects

In this chapter, you learn to create customized special effects that use direct pixel manipulation. To accomodate different effects, you will use subclasses and methods.

1. Create an effects module

Place the effects in a new file effects.py. Create a new class Effect there. This class is a superclass, a generic structure that does not represent any effect in particular.

Note that it contains a method, a function attached specifically to that class. You can recognize methods by the special parameter self. It contains only the command pass that does nothing.

import numpy as np
from pydantic import BaseModel

TILE_SIZE = 64

class Effect(BaseModel):

    x: int
    y: int
    countdown: int

    def draw(self, frame):
        pass

2. Add a subclass

Now let’s create an actual effect: random blur. Create a subclass. It has the same data structure as Effect, but replaces the draw() method:

class RandomBlur(Effect):

    def draw(self, frame):
        random_tile = np.random.randint(0, 255, size=(TILE_SIZE, TILE_SIZE, 3), dtype=np.uint8)
        frame[self.y * TILE_SIZE: self.y * TILE_SIZE + TILE_SIZE,
            self.x * TILE_SIZE: self.x * TILE_SIZE + TILE_SIZE] = random_tile

3. Add effects to the game

Add an effects attribute to the DungeonGame class:

effects: list[Effect] = []

But to add an actual effect, you would use the subclass. Add the effect either in start_game or whenever something interesting happens, e.g. telportation.

game.effects.append(RandomBlur(x=8, y=1, countdown=500))

For both, add the according imports to game.py.

4. Draw the effect

To see anything, you need to call the draw method to main.draw():

# draw special effects
for e in game.effects:
    e.draw(frame)

Now you should already see the effect, but it does not stop.

Note

You do not need to supply a value for the self parameter. For self, Python uses the intstance (in this case e) automatically.

If you find that a bit confusing, I’m with you.

5. Countdown and cleanup

For the effect to expire, you need to decrease the countdown of the effect. Then clean up the expired ones. Add a function anywhere in game.py or main.py:

def update_effects(game):
    new_effects = []

    # add a loop that decreases the countdown for all effects
    # and only collect those where it is greater than zero

    game.effects = new_effects

Call the function in the while loop in main.py or the update() function in game.py:

Hint

If you are in for a challenge, write the function directly into the DungeonGame class as a method. For clarity, rename game to self in the method. Call it with:

game.update_effects()

6. More effects

Here is a simple fading effect:

class FadeIn(Effect):

    def draw(self, frame):
        tile = frame[self.y * TILE_SIZE: self.y * TILE_SIZE + TILE_SIZE,
            self.x * TILE_SIZE: self.x * TILE_SIZE + TILE_SIZE]
        tile[tile > (255 - self.countdown)] = 0
        frame[self.y * TILE_SIZE: self.y * TILE_SIZE + TILE_SIZE,
            self.x * TILE_SIZE: self.x * TILE_SIZE + TILE_SIZE] = tile

Here is some blinking text with an extra attribute:

class ColorText(Effect):

    text: str

    def draw(self, frame):
        if self.countdown % 2 == 0:
            color = (255, 0, 255)
        else:
            color = (0, 255, 255)
        cv2.putText(frame,
            self.text,
            org=(self.y * TILE_SIZE, self.x * TILE_SIZE),
            fontFace=cv2.FONT_HERSHEY_SIMPLEX,
            fontScale=1.0,
            color=color,
            thickness=2,
        )

See also