Pyrge Tutorial 4: Mixins

in Programming

It’s been about a month since the last tutorial, and two versions of Pyrge have come and gone since then. Hopefully you didn’t mind the wait, and you won’t mind waiting a bit longer for a more interesting tutorial. This time, we’re just bridging the gap, so to speak.

Round and Round

When we last left our intrepid hero, he had just learned how to walk around and shoot his gun. There’s one problem with his walking about, though. What happens when he hits the side of the “world”? Well, nothing really. He just keeps on going, off into the unknown (unknowable?) void.

Now, that’s not very heroic of him, so let’s fix it. There are two ways we can go about it, though. First, we can just make the edge of the world solid and impassable. That’s easily accomplished by adding conditions to lines 30, 33, 36, and 39 of our last tutorial script. But there’s another, more interesting, option: wrapping! Take a look at “tutorial8.py”, shown here:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
from pyrge import *
 
class TutorialWorld(World):    
    def __init__(self):        
        super(TutorialWorld, self).__init__()
        sprite = TutorialImage()
        self.add(sprite)
        self.followBounds()
 
class TutorialImage(Image, mixin.Wrapper):
    def __init__(self):        
        super(TutorialImage, self).__init__(100,100)
        self.loadAnimation('animatedsprite.bmp')
        self.addAnimation('walkleft', [0,4], 8)
        self.addAnimation('walkright', [1,5], 8)
        self.addAnimation('walkup', [2,6], 8)
        self.addAnimation('walkdown', [3,7], 8)
        self.showFrame(0)
        Game.world.addHandler(Game.events.KEYDOWN, self.fire)
 
    def update(self):
        anim = None
        if Game.keys[Constants.K_LEFT]:
            anim = 'walkleft'
            self.x -= 2
        if Game.keys[Constants.K_RIGHT]:
            anim = 'walkright'
            self.x += 2
        if Game.keys[Constants.K_UP]:
            anim = 'walkup'
            self.y -= 2
        if Game.keys[Constants.K_DOWN]:
            anim = 'walkdown'
            self.y += 2
 
        if anim is not None:
            self.play(anim)
        else:
            self.stop()
 
        super(TutorialImage, self).update()
 
    def fire(self, event):
        if event.key == Constants.K_SPACE:
            Game.world.add(TutorialBullet(self))
 
class TutorialBullet(Entity, mixin.Wrapper):
    def __init__(self, parent):
        super(TutorialBullet, self).__init__(position=parent.position, size=(4,4))
        v = 180
        Game.Draw.circle(self.pixels, Game.color('yellow'), (2,2), 2)
 
        if parent.currentAnimation == 'walkleft' or parent.currentAnimation is None:
            self.velocity = Vector(-v,0)
        elif parent.currentAnimation == 'walkright':
            self.velocity = Vector(v,0)
        elif parent.currentAnimation == 'walkup':
            self.velocity = Vector(0,-v)
        elif parent.currentAnimation == 'walkdown':
            self.velocity = Vector(0,v)
 
        self.lifetime = 1200
 
    def update(self):
        super(TutorialBullet, self).update()
        self.lifetime -= Game.elapsed
        if self.lifetime < = 0:
            self.kill()
 
theGame = TutorialWorld()
theGame.loop()

Very little of the code has changed, but what a difference it makes! Now, the player character can move around without worrying about going off the screen. When he reaches the edge, he warps to the opposite side, just like in some of those old arcade games.

So, what’s changed? First, on line 8, you’ll notice that the TutorialWorld.update method has effectively been replaced by a call to followBounds(). This method comes from the World class, and it just defines the “range” of the world based on the numbers you pass to it. If you pass it nothing at all, like we did here, then it locks the game world to the size of the screen.

The player class is still the same, but with one small addition on line 10. Instead of inheriting only from Pyrge’s Image class, it also inherits from something called mixin.Wrapper. I’ll explain what Pyrge means by “mixin” later, for now just know that a class with this will wrap around when it reaches the edge of the game world.

Line 47 is where the bullet class starts, and that class is the big winner this time, in terms of new code. First, it also uses mixin.Wrapper as a base class, meaning that bullets will wrap around the screen just like the player does.

You might remember how we determined when to destroy a bullet object in the last tutorial. There, we decided that a bullet that was more than 10 pixels off the screen would be considered “dead”. But a bullet that wraps is never off the screen, so we need a new tactic. Lines 62-68 show one possibility. We give each bullet a “lifetime” property (set to 1200 milliseconds, or 1.2 seconds), and then, when the bullet updates, we’ll subtract the amount of game time from the bullet’s life. Once the lifetime hits zero, it’s time to die. Of course, you can play around with the lifetime property. Combining that with the velocity of the bullet gives you a quick and dirty way of making different kinds of weapons.

Mixin’ It Up

The key to wrapping the player and bullet sprites in the above tutorial was the Wrappable mixin. Mixins are Pyrge’s implementation of a programming concept found in some form in many languages, and known by many names: multiple inheritance, interfaces, traits, etc. But you don’t need to understand all that to use them. All you really need to know is that a Pyrge mixin is a little “bonus” class that adds one or two features to a sprite. In this case, the feature is screen-wrapping, but there are also mixins that make sprites bounce (Bouncer), fade out (Fader), and react to mouse clicks (Clickable). In each case, the mixin adds a bit of code to the sprite’s constructor or update method. It all happens behind the scenes, and the only thing you have to do is add the mixin to the list of a sprite’s base classes.

You can make your own mixins, too. They should inherit from the mixin.SpriteMixin class, as a convention. That class doesn’t do anything by itself; it’s up to you to do the work. Also as a convention, you can safely assume that your mixin will have access to any of the Image properties and methods, but you should at least document if you’re using those of Image subclasses. (The Bouncer mixin, for example, uses velocity, a property that isn’t present in Image, but is in Entity.)

Finally, mixins use Python’s multiple inheritance scheme, which means that the order of base classes can change the effects. Typically, when defining a class, it’s best to put mixins first if they add update code that will change the way a sprite is rendered. (The Wrapper that we used here only affects the sprite’s position, so it doesn’t really matter what the order is.)

Next Time

In the next tutorial, it’s time to get serious, because we’re adding an enemy. Well, at first he’s really more of a target, but, hey, we’ve got to start somewhere.