Grease logo

Table Of Contents

Previous topic

Grease Tutorial Part I

Next topic

Grease Tutorial Part III

This Page

Grease Tutorial Part II

Making a Game of it

In part 1 of the tutorial the basis was laid for the Blasteroids game, but it is far from complete or even playable. By the end of this chapter, we’ll have rectified that. To start with, let’s build on the techniques we used to create the Asteroid class, and create an entity for the player’s ship.

The start should look pretty familiar, and even a bit simpler than the asteroids:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class PlayerShip(BlasteroidsEntity):
    """Thrust ship piloted by the player"""

    THRUST_ACCEL = 75
    TURN_RATE = 240
    SHAPE_VERTS = [
        (-8, -12), (-4, -10), (0, -8), (4, -10), (8, -12), # flame
        (0, 12), (-8, -12), (0, -8), (8, -12)]
    COLOR = "#7f7"
    COLLISION_RADIUS = 7.5
    COLLIDE_INTO_MASK = 0x1
    GUN_COOL_DOWN = 0.5

    def __init__(self, world, invincible=False):
        self.position.position = (0, 0)
        self.position.angle = 0
        self.movement.velocity = (0, 0)
        self.movement.rotation = 0
        self.shape.verts = self.SHAPE_VERTS
        self.shape.closed = False
        self.renderable.color = self.COLOR

First we have some class attributes that configure various aspects of the ship, including thrust acceleration, turn rate, shape (vertex points) and color. Separating these values out of the code makes them easier to tweak while testing the game, and also will allow us to refer to them from other code, which can be convenient.

It’s probably difficult to envision the shape from just the vertex coordinates, so here’s what the ship will look like rendered:

../_images/ship_shape.png

The shape has some intentionally overlapping vertices (the part labelled flame on line 7), so we can easily create a simple animated flame effect coming from the rear of the ship when the thrust is activated.

In the constructor, we setup the initial position and angle (centered and pointing up), stationary movement and rotation (lines 15-18). Next the shape is initialized, this time with the closed field set to false since we have overlapping vertices in the shape. Last, we set the color. This puts the ship entity into all of the components we need for movement and rendering.

Unlike asteroids, the player’s ship needs to be able to move dynamically in response to player inputs. Specifically, the ship needs to be able to turn (rotate) left and right, and accelerate forward in the direction it is facing to simulate thrust. Let’s start with the turn method:

    def turn(self, direction):
        self.movement.rotation = self.TURN_RATE * direction
    

This simple method lets us turn the ship left or right by supplying the proper direction value: -1 for turn left, 1 for turn right, 0 for straight ahead.

Let’s move on to the thrust method:

    def thrust_on(self):
        thrust_vec = geometry.Vec2d(0, self.THRUST_ACCEL)
        thrust_vec.rotate(self.position.angle)
        self.movement.accel = thrust_vec
        self.shape.verts[2] = (0, -16 - random.random() * 16)        

This method accelerates the ship in the direction it is facing. We start by defining an upward-facing vector with a magnitude set to the class’s THRUST_ACCEL value. This vector is then rotated in-place to face the direction of the ship using Vec2d.rotate(). The accel field of the entity’s movment component is then set to the rotated thrust vector. The EulerMovement system, already in the world takes care of calculating the ship’s velocity and position over time based on the acceleration.

The last line changes one of the shape vertices, moving it to a random position behind the ship. This will create a simple flickering flame animation that will act as an import cue to the player that the thrust is active. Notice that the vertex is simply moved to a random position vertically relative to the origin, the renderer will automatically take care of translating and rotating the vertex to the proper window coordinates according to the ship’s current position and rotation, as well as the current camera settings.

We will be wiring the thrust() method to fire every frame that the thrust key is held down. That way the acceleration vector will always be pointing in the right direction if the ship is also turning and the thrust flame will continuously flicker.

The last thing we need is a method to turn the ship’s thrust off. We’ll wire this up to fire when the thrust key is released:

    def thrust_off(self):
        self.movement.accel = (0, 0)
        self.shape.verts[2] = (0, -8)

This resets the ship’s acceleration and flame tip vertex back to their original values.

Controlling the Ship

None of the capabilities we’ve coded for the player’s ship mean anything unless the player can control them. Here we are going to see how easy it is to wire up our game logic to the keyboard.

To do this, we are going to create our own custom System to house our top-level game state, logic, and keyboard bindings. Because the example game is simple, we can easily fit all of these things into a single system class. In a more complex games, it may be more appropriate to separate these things into separate systems.

Remember that systems are behavioral aspects of our application, and are invoked each time step. So they are the perfect place to define and glue together the logic for the game.

class GameSystem(KeyControls):
    """Main game logic system

    This subclass KeyControls so that the controls can be bound
    directly to the game logic here
    """

    def set_world(self, world):
        KeyControls.set_world(self, world)
        self.player_ship = PlayerShip(self.world)

We start by defining our GameSystem as a subclass of KeyControls. KeyControls is a system subclass that provides a convenient mechanism for binding its methods to keyboard events.

The set_world() method is overridden to include a call to create a PlayerShip entity and store it in the system as game state. Since there is only one player ship, this is an easy way to keep track of it so that we can call it’s methods in response to particular key presses. We make the entity here in this method – instead of, say __init__() – because this method is called when the system is added to the world. Since we need a reference to the world in order to create an entity, this is the most convenient place to do so.

Next let’s add a method to turn the ship left when either the “a” or left arrow keys are pressed:

    @KeyControls.key_press(key.LEFT)
    @KeyControls.key_press(key.A)
    def start_turn_left(self):
        if self.player_ship.exists:
            self.player_ship.turn(-1)

The first thing of interest are the two decorators at the top. The KeyControls.key_press() decorator binds a method to a key press event for a specific key. As you can see from the code, we can have multiple key binding decorators for a given method to bind it to multiple keys. The decorator method takes one or two arguments. The first argument is the Pyglet key code from pyglet.window.key. The second optional argument is to specify modifier keys (shift, alt, etc). By default, no modifier keys are assumed.

The logic in this method is quite simple. First we check that the player_ship entity exists. This ensures that the entity has not been deleted from the world before we use it. Just holding a reference to an entity does not prevent it from being deleted. In this way entity references in your code are like weak references. This check will prove useful when the player ship can be destroyed later on. Next we call the ship entity’s turn_left() method we defined earlier passing it a direction of -1.

Next we add a complimentary method to stop turning left:

    @KeyControls.key_release(key.LEFT)
    @KeyControls.key_release(key.A)
    def stop_turn_left(self):
        if self.player_ship.exists and self.player_ship.movement.rotation < 0:
            self.player_ship.turn(0)

The decorators here bind this method to the key release event for the same keys. The methods check for the existence of the entity as above, but also that it is currently turning left (negative rotation). This is to properly handle simultaneous key presses, e.g., left down, right down, then left up.

The methods for handling turning right are the same as above with the direction reversed:

    @KeyControls.key_press(key.RIGHT)
    @KeyControls.key_press(key.D)
    def start_turn_right(self):
        if self.player_ship.exists:
            self.player_ship.turn(1)

    @KeyControls.key_release(key.RIGHT)
    @KeyControls.key_release(key.D)
    def stop_turn_right(self):
        if self.player_ship.exists and self.player_ship.movement.rotation > 0:
            self.player_ship.turn(0)
    
    @KeyControls.key_hold(key.UP)
    @KeyControls.key_hold(key.W)
    def thrust(self, dt):
        if self.player_ship.exists:
            self.player_ship.thrust_on()
        
    @KeyControls.key_release(key.UP)
    @KeyControls.key_release(key.W)
    def stop_thrust(self):
        if self.player_ship.exists:
            self.player_ship.thrust_off()
            

For activating thrust, we use the key_hold() decorator. This works differently than the key press and release decorators we used for turning. The press and release decorators configure a method to fire once for each specific key event. The key hold decorator configures a method to fire continuously, once per time step, as long as the specified key is held down. This is perfect for thrust, which needs to be adjusted continuously as the ship turns, and runs a continuous animation while activated.

The stop_thrust() method is simply bound to key release, to ensure the thrust is deactivated at the proper time.

With the key control logic code in place, the next step is to add the GameSystem to our GameWorld‘s systems (Line 16 below):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class GameWorld(grease.World):

    def configure(self):
        """Configure the game world's components, systems and renderers"""
        self.components.position = component.Position()
        self.components.movement = component.Movement()
        self.components.shape = component.Shape()
        self.components.renderable = component.Renderable()
        self.components.collision = component.Collision()        
        self.components.gun = component.Component(
            firing=bool, 
            last_fire_time=float, 
            cool_down=float)

        self.systems.movement = controller.EulerMovement()
        self.systems.game = GameSystem()
        self.systems.collision = collision.Circular(
            handlers=[collision.dispatch_events])
        self.systems.sweeper = Sweeper()
        self.systems.gun = Gun()
        self.systems.wrapper = PositionWrapper()

        self.renderers.camera = renderer.Camera(
            position=(window.width / 2, window.height / 2))
        self.renderers.vector = renderer.Vector(line_width=1.5)

We also modify the main() function to push the system’s event handler onto the game window so that it receives the key events (Line #7 below):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def main():
    """Initialize and run the game"""
    global window
    window = pyglet.window.Window()
    world = GameWorld()
    pyglet.clock.schedule(world.tick)
    window.push_handlers(world)
    window.push_handlers(world.systems.game)
    for i in range(8):
        Asteroid(world)
    pyglet.app.run()

Now we can control the ship and fly it around the screen.

../_images/flying_around.png

Running Into Stuff

Flying around is way too safe at the moment, since you can’t actually run into anything! Let’s see what we can do about that. Implementing collision requires that we add a component and a system to the GameWorld:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class GameWorld(grease.World):

    def configure(self):
        """Configure the game world's components, systems and renderers"""
        self.components.position = component.Position()
        self.components.movement = component.Movement()
        self.components.shape = component.Shape()
        self.components.renderable = component.Renderable()
        self.components.collision = component.Collision()        
        self.components.gun = component.Component(
            firing=bool, 
            last_fire_time=float, 
            cool_down=float)

        self.systems.movement = controller.EulerMovement()
        self.systems.game = GameSystem()
        self.systems.collision = collision.Circular(
            handlers=[collision.dispatch_events])
        self.systems.sweeper = Sweeper()
        self.systems.gun = Gun()
        self.systems.wrapper = PositionWrapper()

        self.renderers.camera = renderer.Camera(
            position=(window.width / 2, window.height / 2))
        self.renderers.vector = renderer.Vector(line_width=1.5)

The Collision component (line 9 above) has the fields we need to make the collision system (line 13-14 above) work. The fields in this component are:

aabb
This is the axis-aligned bounding box that contains the entity. This box is used in the collision detection system to quickly reduce the number of collision checks that need to be performed. We can also use it for our own purposes when we need to find the top, left, bottom or right edges of entities.
radius
The meaning of this field is up to the specific collision system used. For Circular systems, entities are approximated as circles for the purposes of collision detection. The radius value is simply the radius of the collision circle for an entity.
from_mask and into_mask
Not all entities in the collision component need to be able to collide with each other. These two mask fields let you specify which entities can collide. Both mask fields are 32 bit integer bitmasks. When two entities are compared for collision, the from_mask value from each entity is bit-anded with the into_mask of the other. If this bit-and operation returns a non-zero result, then a collision is possible, if the result is zero, the entities cannot collide. Note that this check happens in both directions, so a collision can occur between entity A and B if A.collision.from_mask & B.collision.into_mask != 0 or B.collision.from_mask & A.collision.into_mask != 0.

Let’s take a closer look at how the collision system is configured above:

        self.systems.collision = collision.Circular(
            handlers=[collision.dispatch_events])

There are two major steps to collision handling in Grease: collision detection and collision response. The detection step happens within the collision system. A set of pairs of the currently colliding entities can be found in the collision_pairs attribute of the collision system. Applications are free to use collision_pairs directly, but they can also register one or more handlers for more automated collision response. Collision handlers are simply functions that accept the collision system they are configured for as an argument. The handler functions are called each time step to deal with collision response.

Above we have configured dispatch_events() as the collision handler. This function calls on_collide() on all entities that are colliding. The entities’ on_collide() handler methods can contain whatever logic desired to handle the collision. This method accepts three arguments: other_entity, collision_point, and collision_normal. These arguments are the other entity collided with, the point where the collision occurred and the normal vector at the point of collision respectively. It is up to the handler method to decide how these values are used. Note that when two entities collide, both of their on_collide() handler methods will be called, if defined.

In our game we will leverage the collision masks to make it so that the player’s ship collides with asteroids, but the asteroids do not collide with each other. To do that we need to modify the Asteroid and PlayerShip constructors to set the collision component fields.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class PlayerShip(BlasteroidsEntity):
    """Thrust ship piloted by the player"""

    THRUST_ACCEL = 75
    TURN_RATE = 240
    SHAPE_VERTS = [
        (-8, -12), (-4, -10), (0, -8), (4, -10), (8, -12), # flame
        (0, 12), (-8, -12), (0, -8), (8, -12)]
    COLOR = "#7f7"
    COLLISION_RADIUS = 7.5
    COLLIDE_INTO_MASK = 0x1
    GUN_COOL_DOWN = 0.5

    def __init__(self, world, invincible=False):
        self.position.position = (0, 0)
        self.position.angle = 0
        self.movement.velocity = (0, 0)
        self.movement.rotation = 0
        self.shape.verts = self.SHAPE_VERTS
        self.shape.closed = False
        self.renderable.color = self.COLOR
        self.collision.into_mask = self.COLLIDE_INTO_MASK
        self.collision.radius = self.COLLISION_RADIUS
        self.gun.cool_down = self.GUN_COOL_DOWN

Lines 21-22 above initialize the collision settings for the player ship. Note that the radius is set slightly smaller than the farthest vertex in the shape (by 0.5 units). This is common when using circular collision shapes, it prevents the appearance of false positive collisions that can make the game feel unfair.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Asteroid(BlasteroidsEntity):
    """Big floating space rock"""

    COLLIDE_INTO_MASK = 0x2
    UNIT_CIRCLE = [(math.sin(math.radians(a)), math.cos(math.radians(a))) 
        for a in range(0, 360, 18)]

    def __init__(self, world, radius=45):
        self.position.position = (
            random.choice([-1, 1]) * random.randint(50, window.width / 2), 
            random.choice([-1, 1]) * random.randint(50, window.height / 2))
        self.movement.velocity = (random.gauss(0, 700 / radius), random.gauss(0, 700 / radius))
        self.movement.rotation = random.gauss(0, 15)
        verts = [(random.gauss(x * radius, radius / 7), random.gauss(y * radius, radius / 7))
            for x, y in self.UNIT_CIRCLE]
        self.shape.verts = verts
        self.renderable.color = "#aaa"
        self.collision.radius = radius
        self.collision.from_mask = PlayerShip.COLLIDE_INTO_MASK
        self.collision.into_mask = self.COLLIDE_INTO_MASK

Lines 18-20 above setup collision for the asteroids. The radius is simply set to the asteroid’s radius. The collision masks are configured so that the asteroids will collide with the player’s ship, but not with each other.

To start with, will add a simple on_collide() method to both the Asteroid and PlayerShip classes that simply delete the entities when they collide:

def on_collide(self, other, point, normal):
    """Collision response handler"""
    self.delete()

Blowing Stuff Up

When you run the game now, you’ll notice that asteroids pass right through each other, but if you hit one with the ship, both the ship and asteroid disappear. This proves that the collision is working as expected, but it’s not very interesting yet. What would really spice things up are some simple explosion effects when entities are destroyed.

Here’s what we need to implement to make stuff explode:

  • A method to “explode” an entity into a bunch of debris fragments.
  • A system that manages the debris, fading it out and cleaning it up over time.

Let’s start with the explode() method. Since asteroids and the player ship are basically the same except for their shape, they can share the method code. The most straightforward way to do this, is to create a common base class for both entities:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class BlasteroidsEntity(grease.Entity):
    """Entity base class"""

    def explode(self):
        """Segment the entity shape into itty bits"""
        shape = self.shape.verts.transform(angle=self.position.angle)
        for segment in shape.segments():
            debris = Debris(self.world)
            debris.shape.verts = segment
            debris.position.position = self.position.position
            debris.movement.velocity = self.movement.velocity
            debris.movement.velocity += segment[0].normalized() * random.gauss(50, 20)
            debris.movement.rotation = random.gauss(0, 45)
            debris.renderable.color = self.renderable.color

We also define an entity class for the debris. This class defines no behavior of its own, it just serves to tag the debris entities so that we can easily manage them in the system we will be creating later:

class Debris(grease.Entity):
    """Floating space junk"""

Let’s take apart the BlasteroidsEntity class and see how it works. In line 6 above, we take the base shape of the entity and transform it into a new shape, rotating it to the current angle of the entity that is exploding:

    def explode(self):
        """Segment the entity shape into itty bits"""
        shape = self.shape.verts.transform(angle=self.position.angle)

Next we create the debris. This is aided by the shape.segments() method (line 7). This method returns an iterator of all of the individual line segments of the original shape as separate shapes. This effectively fragments our original entity shape.

We loop over these fragment segments creating debris entities for each. The shape of each debris fragment is a single segment of the original entity’s shape. We also set the initial position and velocity of the debris entity to that of the exploding entity (line 10-11). Next we add some random velocity outward from the exploding entity’s position to make it “explode” (line 12). Because the base shapes are centered around the origin, we can just use one of the vertex positions to determine the approximate outward direction from the center. Normalizing the first vertex vector – giving it a length of 1 – and multiplying it by a random value gives us the desired outward push. A bit of random rotation adds a little spice to the effect (line 13). Last, we set the color of the debris to the same as the original entity so they appear to be pieces of the original.

To trigger the explosions, we simply need to change the base class of our PlayerShip and Asteroid classes to BlasteroidsEntity and add a call to explode() in their on_collide() methods:

def on_collide(self, other, point, normal):
    """Collision response handler"""
    self.explode()
    self.delete()

Cleaning Up the Mess

Blowing stuff up is great fun, of course, but at some point we need to clean up the debris. We’ll accomplish this by adding a custom Sweeper system:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Sweeper(grease.System):
    """Clears out space debris"""

    SWEEP_TIME = 2.0

    def step(self, dt):
        fade = dt / self.SWEEP_TIME
        for entity in tuple(self.world[Debris].entities):
            color = entity.renderable.color
            if color.a > 0.2:
                color.a = max(color.a - fade, 0)
            else:
                entity.delete()

This is first system we’ve created from scratch, so let’s look a little deeper at what a system actually is. You’ll notice we are subclassing System an abstract base class defined by the framework. Subclassing System is optional, though it helps make it clear what type of part the class defines.

The only method that a system must implement is step(). The step() method is called by the world every time step, passing in the time delta since the last time step as a float. This is where the system implements its business logic.

Optionally a system can implement a set_world() method. If defined, this method is called when the system is added to a world, passing the World instance as its argument. This can be a good place to do system initialization where you need a world object, such as we did with the GameWorld system implementation earlier. The System base class defines a simple implementation of set_world() that stores a reference to the system’s world for convenient access to its entities, components or even other systems.

The SWEEP_TIME value defined on our class specifies the time debris will live before it is “swept up” by the system and deleted. As this time elapses, the alpha value of each debris entity’s color is slowly reduced, fading the debris away.

We determine the amount to fade the debris for each time step by dividing the time delta dt by the SWEEP_TIME (line 7 above). This works because the color component values are floating point numbers between 0 and 1.0:

        fade = dt / self.SWEEP_TIME

Next we loop iterating through the Debris entities. We do this by accessing the entity extent for Debris. This extent is accessed by using the Debris class as a key to the world:

self.world[Debris].entities

The entity extent object returned by self.world[Debris] has an attribute entities which is the set of all entities for that extent in the world. This conveniently gives us all of the debris entities in existence. You’ll notice that in the for loop on line 7, we turn this set of entities into a tuple:

        for entity in tuple(self.world[Debris].entities):

This makes a copy of the set to iterate over, so we can safely remove debris entities from the world inside the loop. Without making a copy, we might actually change the members of the entities while iterating it, which at best would cause an exception, and at worst would result in some entities being skipped over. Any time you iterate over a mutable sequence, such as a list, set or dict, you need to be careful about changes to that sequence while iterating it. This tends to be a common gotcha in Python game development.

Inside the loop things are pretty straightforward. For each debris entity, we get the color from the renderable component (line 9). Color objects have the attributes r, g, b, and a for their red, green, blue, and alpha values. We only care about the alpha in this system. For alpha values greater than 0.2, we fade it a bit to a minimum value of zero. If the alpha value is not greater than 0.2, we delete the debris entity entirely (lines 10-13).

Now that the Sweeper class is implemented, the last thing we need to do is add it to our game world. We just need to add this line to the configure() method of the GameoWorld class:

self.systems.sweeper = Sweeper()

Now the debris fragments fade away and disappear a short time after the entity explodes.

Note

What’s powerful about systems is their ability to define behavioral aspects of the world. With this simple system, we now control the behavior and lifespan of all debris, regardless of where, why or how they are created. If we add new sources of debris later, this system will automatically handle them without additional effort.

Shoot Me Now

Alright, so now we have collisions and explosions working, what more could we want in a game? Well, there’s a big problem: The only way to blow things up is to use the ship as a battering ram. We need a way to destroy things without also committing suicide. If only there was a game mechanic we could use....hmmm.

Ok, enough fooling around, we need to be able to shoot stuff! To do that we need some sort of gun. For our purposes, a gun is a device that can shoot out Shot entities. Since such a “device” might be useful for other entities besides the PlayerShip, it would be useful to implement as a behavioral aspect of the game.

The best way to implement such aspects, as we’ve seen, is to use a system. It might not seem obvious when a feature should be implemented using a system, versus just a method on the entity class, like explode() above. Of course, these things are not cut and dried and there is no right or wrong way, but systems offer some advantages in certain situations:

  1. The behavior is tied to specific components.
  2. The behavior is continuous or recurs over time.
  3. The behavior is not specific to a particular entity.

If any one of the above is true, you should consider implementing the behavior as a system. In our case, we will be defining a custom gun component to store some state for guns, and the behavior recurs periodically over time (If you hold down fire). Right now #3 above is not true, but if we were to implement alien ships that could shoot at the player later, it would be. All in all I think that makes a compelling case for using a system here.

The first thing we need is a component to store some gun state. This will be used to determine when the gun can shoot. Three pieces of information are needed for this: a flag to determine if the gun should fire, the last time the gun was shot, and the minimum “cool down” time between shots. We can add a custom component to the GameWorld to store this information in three fields:

self.components.gun = component.Component(
    firing=bool,
    last_fire_time=float,
    cool_down=float)

The Component class lets us create custom components with user-defined fields. To construct a custom component, we specify the fields as keyword arguments. The names of the arguments specify the names of the fields. The value of each argument specifies the data type for each field. Since the data type is fixed, this makes component fields more rigid than conventional Python attributes, but it provides some important benefits:

  1. Component values can be stored in compact data structures using native data types where possible (int, float, Vec2d, etc).
  2. Component data can be stored in contiguous data blocks for much faster batch operations.
  3. Fields have sensible default values.
  4. Input values can be automatically cast to the proper field type (e.g., 2-tuples to Vec2d, hex strings to RGBA)
  5. Systems and other users of component data know exactly what type of data to expect for each field.

An important drawback to this arrangement is that fields must always have a value of the proper type. So it is not possible to assign a value of None to a float field, for instance.

Note

The only difference between custom and built-in components is that the fields for the former are already specified for convenience. Using custom components has no drawbacks other than the additional configuration required.

With the gun component in place, lets modify the PlayerShip class to initialize the gun data:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class PlayerShip(BlasteroidsEntity):
    """Thrust ship piloted by the player"""

    THRUST_ACCEL = 75
    TURN_RATE = 240
    SHAPE_VERTS = [
        (-8, -12), (-4, -10), (0, -8), (4, -10), (8, -12), # flame
        (0, 12), (-8, -12), (0, -8), (8, -12)]
    COLOR = "#7f7"
    COLLISION_RADIUS = 7.5
    COLLIDE_INTO_MASK = 0x1
    GUN_COOL_DOWN = 0.5

    def __init__(self, world, invincible=False):
        self.position.position = (0, 0)
        self.position.angle = 0
        self.movement.velocity = (0, 0)
        self.movement.rotation = 0
        self.shape.verts = self.SHAPE_VERTS
        self.shape.closed = False
        self.renderable.color = self.COLOR
        self.collision.into_mask = self.COLLIDE_INTO_MASK
        self.collision.radius = self.COLLISION_RADIUS
        self.gun.cool_down = self.GUN_COOL_DOWN

We add a class attribute on line 12 to specify a one half second cooldown for the gun. On line 24 we set this value in the gun component. We do not have to explicitly set the firing and last_fire_time fields. They will automatically default to False and 0 respectively.

Shot Class

Now let’s implement the Shot entity class for our bullets:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Shot(grease.Entity):
    """Pew Pew!"""

    SPEED = 300
    TIME_TO_LIVE = 0.75 # seconds
    
    def __init__(self, world, shooter, angle):
        offset = geometry.Vec2d(0, shooter.collision.radius)
        offset.rotate(angle)
        self.position.position = shooter.position.position + offset
        self.movement.velocity = (
            offset.normalized() * self.SPEED + shooter.movement.velocity)
        self.shape.verts = [(0, 1.5), (1.5, -1.5), (-1.5, -1.5)]
        self.collision.radius = 2.0
        self.collision.from_mask = ~shooter.collision.into_mask
        self.renderable.color = "#ffc"
        world.clock.schedule_once(self.expire, self.TIME_TO_LIVE)

We start with some class attributes (line 4-5) to specify the shot’s speed and time-to-live. The latter is an indirect way to specify the range for the shot.

Next we define the constructor, which in addition to the required world argument, takes a shooter entity and angle value. The shooter is the entity that the shot is being fired from. The angle determines the direction of the shot.

Inside the constructor, we first determine the initial position of the shot (lines 8-10). This is done by computing an offset based on the shooter’s collision radius and the input angle. This way the shot appears to come from the surface of the shooter, rather than the center. The velocity is calculated by multiplying the angle’s unit vector by the shot’s speed, plus the shooter’s velocity. Without including the shooter’s velocity, the shooter could actually outrun his own shots, and strafing would feel very unnatural. Would-be skeptics out there should try it with and without adding the shooter’s velocity to see what I mean.

The shape of the shot is set to a small triangle (line 13). This is small enough so that it will appear to be a small dot when rendered. Collision is setup with a small radius and a mask specifically designed to collide with everything except for the shooter (~ is Python’s bit-invert operator). Setting the color ensures the shot is rendered.

On line 17 we schedule the shot to expire at the proper time. The world’s clock will call the expire method (defined below) when the time-to-live elapses, deleting the entity automatically. This means that we do not need to keep track of when each shot will expire ourselves, which is convenient.

We have two more methods to implement for Shot: an on_collide() handler for collision and an expire() method for handling the shot expiration. Both will simply delete the entity:

    def on_collide(self, other, point, normal):
        self.delete()
    
    def expire(self, dt):
        self.delete()

Gun System

With the gun component and Shot class implemented, we can finally finish things off by implementing the Gun system:

1
2
3
4
5
6
7
8
class Gun(grease.System):
    """Fires Shot entities"""

    def step(self, dt):
        for entity in self.world[...].gun.firing == True:
            if self.world.time >= entity.gun.last_fire_time + entity.gun.cool_down:
                Shot(self.world, entity, entity.position.angle)
                entity.gun.last_fire_time = self.world.time

This is even shorter than the Sweeper system that we implemented previously, but there are some new things here that bear explanation, in particular on line 5. In the Sweeper implementation, we discussed how to use entity classes as keys on the world to retrieve entity extents. In many cases though, you want to get the extent containing all entities instead of just one particular type. To do this you use the special Python ellipsis symbol (...) as a key:

world[...]

This means: “give me the extent of all entities in the world.”

In Sweeper, we used the extent just to get a set of entities of a particular type. But extents can do far more than that, they can also be used to create query expressions.

You can think of extents as a batch of entities, and like individual entities you can access components as attributes of extents. Only instead of accessing a single component record for an entity, an extent accessor selects them for the entire extent. For instance:

world[...].gun

This returns a special set of all entities in the gun component. It’s special because we can use it to query fields in the component. For example:

world[...].gun.firing == True

That looks like an ordinary boolean expression, but it is actually much more than that. This expression returns a set of all entities where the component field gun.firing matches the value True. So this expression returns the set of all entities currently firing their gun. We can iterate this set, as we do in line 5 above, to perform the necessary logic in our system.

On line 6 we check if the entity’s gun is ready to fire. self.world.time is the local timestamp of the world object, updated every time step. If the gun is ready to fire, we create a Shot entity and update the last_fire_time so the gun can begin its cool down cycle again.

As you may have guessed, now that we have our Gun system class implemented, we need to add an instance of it to the GameWorld:

self.systems.gun = Gun()

In addition, we need to add some methods to the GameSystem class to fire the player ship’s gun when the space bar is pressed:

            
    @KeyControls.key_press(key.SPACE)
    def start_firing(self):
        if self.player_ship.exists:
            self.player_ship.gun.firing = True

    @KeyControls.key_release(key.SPACE)
    def stop_firing(self):
        if self.player_ship.exists:
            self.player_ship.gun.firing = False

These methods simply set the gun.ship flag when space is pressed, and reset it when space is released.

With all of this in place we can finally blast those asteroids to smithereens!

../_images/blast_em.png

Wrapping Things Up

We’ve now implemented all of the major game mechanics, save one. You’ll notice if you fly around for a few seconds, all of the asteroids fly off the screen and disappear. And so will the player’s ship if you accelerate it in one direction. We forgot to implement the all-important toroidal spatial topology! That’s fancy-talk for making objects wrap around when they fly off the edge of the screen.

So, how do you suppose we’re gonna fix this? Surprise! Another system! This is a textbook example of a behavioral aspect of the application:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class PositionWrapper(grease.System):
    """Wrap positions around when they go off the edge of the window"""

    def __init__(self):
        self.half_width = window.width / 2
        self.half_height = window.height / 2

    def step(self, dt):
        for entity in self.world[...].collision.aabb.right < -self.half_width:
            entity.position.position.x += window.width + entity.collision.aabb.width
        for entity in self.world[...].collision.aabb.left > self.half_width:
            entity.position.position.x -= window.width + entity.collision.aabb.width
        for entity in self.world[...].collision.aabb.top < -self.half_height:
            entity.position.position.y += window.height + entity.collision.aabb.height 
        for entity in self.world[...].collision.aabb.bottom > self.half_height:
            entity.position.position.y -= window.height + entity.collision.aabb.height

The constructor (lines 4-6) pre-calculates the half width and height of the window for convenience later. Remember that the origin is in centered in the window, so these values are handy for finding the edges.

The step() method performs four entity extent queries, in the same spirit as in the Gun system. Here we are querying the edges of the entities using their collision bounding boxes, comparing them to the window edges. In the first loop (line 9-10) we iterate over all entities whose right edge has moved left beyond the left edge of the window. This extent query does the job:

self.world[...].collision.aabb.right < -self.half_width

For all of the entities matched by this expression, we move them to the right the full width of the window plus the width of the entity which we can also conveniently get using the bounding box.

The remaining 3 loops are essentially the same except each handles a different window edge.

Naturally once we have this class implemented, you guessed it, we need to add it to the GameWorld:

self.systems.wrapper = PositionWrapper()

With this now in place, all of the entities in the collision component (asteroids, the player ship, shots) will automatically wrap around when they fly offscreen.

blasteroids2.py contains the full source of the second revision of the game.

Next: Grease Tutorial Part III: Spit and Polish