« Previous - Version 2/14 (diff) - Next » - Current version
coder, 24/11/2014 20:39


Diving In: The Firebirds Sample

This tutorial introduces libavg programming by means of the sample <i>Firebirds</i>. This sample is a small 2D Shooter similar to "1942: Joint Strike": The player pilots a World War II fighter, shooting at enemy fighters and dodging them while the landscape scrolls vertically below the planes. Firebirds is based on an XNA tutorial by the magazine c't and developed by Alfred Bigler. In the tutorial, we will go through the basic structure of a libavg app, including initialization, the update loop and general scene structure. We'll also cover deriving your own classes from libavg classes and introduce libavg plugins written in C++.

The complete finished firebirds code can be found in the libavg distribution under src/samples/firebirds (or online), with images, videos and sounds under media/ and the plugin source code in the plugin/ subdirectory. The libavg code for the game is under 500 lines long - half as many as the original XNA code, even though we've added code to generate shadows of the planes on-the-fly. In addition, we detect collisions between two objects on the screen in a pixel-precise way using a simple libavg plugin. The tutorial assumes that libavg is already installed on the computer.

As development environment, you can use either eclipse or the commandline and an editor of your choice.

App Structure

Like all libavg apps, Firebirds makes use of the libavg App class. This class is instantiated in the last source line of the file:

1 app.App().run(FireBirds(), app_resolution='1280x720')

This line and an empty FireBirds class are enough to open a 1280x720 window and run the libavg main loop until the Esc key is pressed. The app has several command-line parameters (try starting it with --help or --app-fullscreen=True) and debug keys ('?' shows a list).

In libavg, the main loop (for game programmers: 'Game Loop') is handled internally by the library, which calls Python functions in application code whenever certain events occur. In Firebirds, these Python functions are located in the FireBirds class:

Application flow looks like this:

  • gameloop.png ***

Initialization
--------------

onInit is called once at program start, directly after the window has been opened. In it, the scene is initialized and media such as video and sound clips as well as images are loaded. Of course, the scene can be changed and content loaded later as well.

Content in libavg is stored in a tree structure, the so-called scene graph. This tree contains all media elements (called Nodes) along with the information necessary to render them - such as position, size and opacity. Similar to html, the graph is structured using DivNodes. The scene graph can contain objects of classes defined by the App programmer. In our case, for instance, the aircraft are defined in an aircraft class, with two derived classes for the player's and enemy aircraft. Other elements in the scene graph include bullets, a scrolling background, and the components of the heads-up display such as the score, lives left and a gun heat gauge:

  • scenegraph.png ***

Here are the parts of the onInit function that create the initial scene graph:

class FireBirds(app.MainDiv):
def onInit(self):
self.__gameMusic = avg.SoundNode(href='Fire_Birds.mp3', loop=True,
volume=0.75, parent=self)
[...]
self.__shadowDiv = avg.DivNode(parent=self)
self.__gameDiv = avg.DivNode(size=self.size, parent=self)
self.__guiDiv = avg.DivNode(parent=self)
[...]
self.__player = PlayerAircraft(self.__shadowDiv, gunCtrl, parent=self.__gameDiv)
[...]
self.__gameMusic.play()

Game Flow
---------

Once init has been called, the application enters the main loop. In the main loop consists of handling events, updating the world state and redrawing the screen. Events from input devices (keyboard, mouse, touch, etc.) and timers are handled in callbacks that the application defines, such as __onKeyDown() and __onKeyUp(). In __onFrame(), the application calculates a new world state.

The actual rendering is handled internally by libavg. By default, updates in libavg are synced to screen updates. Usually, these take place 60 times per second, so there are 16.7 seconds available for one iteration of the game loop. If the loop takes longer, we miss a screen update and there is a chance the game will stutter.

In addition, Firebirds uses two functions, _start() and __stop(), that it calls when the game starts (_start()) and when the player has no more lives left (__stop()). Subscribing to the ON_FRAME event and deregistering is also done in these functions, while subscribing to keyboard events happens in onInit(). The subscribe() calls shown below cause libavg to call specific functions whenever an event occurs:

class FireBirds(app.MainDiv):
def onInit(self):
[... Initialize scene graph ...]
self.__start()
player.subscribe(player.KEY_DOWN, self.__onKeyDown)
player.subscribe(player.KEY_UP, self.__onKeyUp)
[...]
def _start(self):
[...]
self.
_frameHandlerId = player.subscribe(player.ON_FRAME, self.__onFrame)
[...]
def _stop(self):
player.unsubscribe(player.ON_FRAME, self.
_frameHandlerId)
self.__frameHandlerId = None
[...]

Firebirds uses the following callbacks:

class FireBirds(app.MainDiv):
[...]
def __onKeyDown(self, event):
[...]
def __onKeyUp(self, event):
[...]
def __onFrame(self):
[...]

In our application, __onKeyDown() and __onKeyUp() functions only keep track of the current key states. Actions resulting from key presses are handled during __onFrame, in PlayerAircraft.update(). __onFrame() calculates a new world state: It moves some Nodes and creates and deletes others. All game logic is here, collisions are detected, points are counted, etc.. The time delta between one frame and the next is available using player.getFrameDuration() and used to calculate movement in the game world. All game elements are moved in accordance with the time elapsed. The alternative - moving them a fixed distance per frame - should be avoided, since that would cause the gameplay to become faster or slower depending on the capabilities of the computer. Worse, if syncing to screen updates is off, the game might update so quickly that the player has no chance to react quickly enough.

The PlayerAircraft Class
------------------------

Elements in a libavg scene graph are derived from the Node class. libavg provides a number of derived Node classes for images, videos, etc., and the application can derive it's own classes as well.

As an example, we'll go through the PlayerAircraft class. It's derived from _Aircraft, which is in turn derived from DivNode, so it can be inserted into the scene graph and will be rendered each frame along with the rest of the scene. DivNodes are intrinsically invisible, and since an invisible aircraft makes for bad gameplay, the _Aircraft class creates several nodes for the visuals and adds them as child nodes into the tree. The most important one is VideoNode that shows the actual airplane in motion. Since libavg is able to start and stop videos as well as loop them instantaneously, this is the easiest way to create moving images of game elements. The videos have an alpha (transparency) channel so they blend seamlessly into the background. If the plane explodes, a second video containing the explosion is played. Shadows are handled here as well, but placed in a separate layer in the scene graph so they're behind all aircraft. Shadows are ImageNodes with an added blur effect.

Collision detection
-------------------

Detecting collisions between aircraft in a pixel-precise way would be too slow if it were done in Python. For this reason, we implemented a small libavg plugin - a C++ library that extends libavg. The collisiondetector plugin defines a new class that is exposed to python, the CollisionDetector. It is initialized with two bitmaps, and when detect() is called, it compares the bitmaps pixel-by pixel to see if any non-transparent pixels are at the same position. If so, we have a collision.

Conclusion
----------

This tutorial has covered many of the things necessary to build general libavg apps: The lifecycle, main loop and scene graph are the same whether you're building a scrolling game or a serious application. The same goes for deriving Node classes, reacting to input events and handling application state changes. Of course, we could only introduce a few of the libavg features. We've missed a lot of interesting things, such as gesture handling (and touch input in general) and offscreen rendering as well as masking and other effects.

Still, there should be nothing preventing you from writing your own libavg apps at this point.

scenegraph.png (34.3 kB) coder, 24/11/2014 20:37

screenshot.png (142.5 kB) coder, 24/11/2014 20:42

Scenegraph.graffle (22.9 kB) coder, 29/11/2014 10:27

gameloop.graffle (10.8 kB) coder, 29/11/2014 10:28

gameloop.png (36.4 kB) coder, 29/11/2014 10:30