Pyweek 19 / Animations
Pyweek 19 is coming up in October, running from Sunday 5th (00:00 UTC) to Sunday 12th (again, 00:00 UTC). Pyweek is a games programming contest in which you have exactly one week to develop a game in Python, on a theme that is selected by vote and announced at the moment the contest starts.
You don't need to have a great deal of games programming savvy to take part: whatever your level you'll find it fun and informative to program along with other entrants, exchanging your experiences on the challenge blog (link is to the previous challenge).
I'd encourage everyone to take part, either individually or as a team, because there's a lot you can learn, about games programming and software development generally.
In celebration of the upcoming Pyweek, here's a little primer on how to write an animation display system.
Animations
An animation is made up of a set of frames, like these from Last Train to Nowhere:
Frames could be 2D sprites or 3D meshes, but for simplicity, let's assume sprites. I always draw my sprites, but you can find sprite sheets on the web if you're less confident in your art skills.
Playing the animation in a game involves displaying one frame after another, at the right place on the screen, so that the action looks smooth.
First of all, let's look at the typical requirements for such a piece of code:
- It needs to be able to draw multiple copies of the same animation, each at different frames and positions.
- It needs to be cycle through the frames at a certain rate, which will usually be slower than the rate I'm redrawing the entire game screen.
- It needs to draw each frame the right places relative to a fixed "insertion point". That is, if the game treats the animation as being "at" a point (x, y), then the frames should be drawn at an offset (x + ox, y + oy) that will cause them to appear in the right place. The offset vector (ox, oy) may vary between frames if the sprites are different sizes.
Another feature I've found useful in the past is to be able to select from a number of different animations to "play" at the appropriate moment. So rather than needing to create a new animation object to play a different sequence of frames, I just instruct the animation to play a different named sequence:
anim.play('jump')
The chief benefit of this is that I can preconfigure what happens when animations end. Some animations will loop - such as a running animation. Other animations will segue into another animation - for example, shooting might run for a few frames and then return to standing.
The best approach for a system like this is with the flyweight pattern. Using the flyweight pattern we can split our animation system into two classes:
- The Animation class will contain all the details of an animation: the sprite sequences, per-sprites offsets, frame rates, details of what happens when each sequence finishes, and so on. It's basically a collection of data so it doesn't need many methods.
- The Animation Instance class will refer to the Animation for all of the data it holds, and include just a few instance variables, things like the current frame and coordinates of the animation on the screen. This will need a lot more code, because your game code will move it around, draw it, and tell it to play different animation sequences.
So the API for our solution might look something like this:
# A sprite and the (x, y) position at which it should be drawn, relative # to the animation instance Frame = namedtuple('Frame', 'sprite offset') # Sentinel value to indicate looping until told to stop, rather than # switching to a different animation loop = object() # A list of of frames plus the name of the next sequence to play when the # animation ends. # # Set next_sequence to ``loop`` to make the animation loop forever. Sequence = namedtuple('Sequence', 'frames next_sequence') class Animation: def __init__(self, sequences, frame_rate=DEFAULT_FRAME_RATE): self.sequences = sequences self.frame_rate = frame_rate def create_instance(self, pos=(0, 0)): return AnimationInstance(self, pos, direction) class AnimationInstance: def __init__(self, animation, pos=(0, 0)): self.animation = animation self.pos = pos self.play('default') # Start default sequence clock.register_interval(self.next_frame, self.animation.frame_rate) def play(self, sequence_name): """Start playing the given sequence at the beginning.""" self.currentframe = 0 self.sequence = self.animation.sequences[sequence_name] def next_frame(self): """Called by the clock to increment the frame number.""" self.currentframe += 1 if self.currentframe >= len(self.sequence.frames): next = self.sequence.next_sequence if next is loop: self.currentframe = 0 else: self.play(next) def draw(self): """Draw the animation at coordinates given by self.pos. The current frame will be drawn at its corresponding offset. """ frame = self.sequence.frames[self.currentframe] ox, oy = frame.offset x, y = self.pos frame.sprite.draw(pos=(x + ox, y + oy)) def destroy(self): clock.unregister_interval(self.next_frame)
This would allow you to define a fully-animated game character with a number of animation sequences and transitions:
player_character = Animation({ 'default': Sequence(..., loop), 'running': Sequence(..., loop), 'jumping': Sequence(..., 'default'), 'dying': Sequence(..., 'dead'), 'dead': Sequence(..., loop) })
You'd have to find a suitable clock object to ensure next_frame() is called at the right rate. Pyglet has just such a Clock class; Pygame doesn't, but there might be suitable classes in the Pygame Cookbook. (Take care that the next_frame() method gets unscheduled when the animation is destroyed - note the destroy() method above. You'll need to call destroy(), or use weakrefs, to avoid keeping references to dead objects and "leak" memory.)
There's potentially a lot of metadata there about the various animation sequences, offsets and so on. You might want to load it from a JSON file rather than declaring it in Python - that way opens it up to writing some quick tools to create or preview the animations.
Next, you need to create the flyweight instance and play the right animations as your character responds to game events - perhaps you'd wrap a class around it like this:
class Player: def __init__(...): self.anim = player_character.create_instance() def jump(...): self.anim.play('jumping') def update(...): self.anim.pos = self.pos def draw(...): self.anim.draw()
I've used this basic pattern in a fair number of games. There are lots of different ways to extend it. Your requirements will depend on the kind of game you're writing. Here are a couple of ideas:
- You might want the flyweight instances to be able to flip or rotate the animation. You might store a direction flag or angle variable.
- You might want to be able to register arbitrary callback functions, to be called when an animation finishes. This would allow game events to be cued from the animations, which will help to make sure everything syncs up, even as you change the animations.
- You could use vector graphics of some sort, and interpolate between keyframes rather than selecting just one frame to show. This would offer smoother animation, but you'd need good tooling to create the animations (tools like Spine).
- I've assumed some sort of global clock object. You might want to link animations to a specific clock that you control. This would allow you to pause the animations when the game is paused, by pausing the animation clock. You could go more advanced and manipulate the speed at which the clock runs to do cool time-bending effects.
Have fun coding, and I hope to see your animations in Pyweek!
Comments
Comments powered by Disqus