Aug. 18, 2008

GameJam0808 was awesome fun, which is really what games are all about. In the end I got a crapload of code written, much of which I will be able to re-use again in the future, just as I used quite a bit of old code for my engine. I refined a lot of my existing code, made it more modular, and more re-useable than it was before. I now have a load of generic game code that has been separated from the underlying engine I use, PyGame. It should be easier to switch engines to Pyglet, or something else if I want to in the future.

Huge thanks go to Simon Wittber who organised GameJam, and wrote the entire GameJam site in time for the competition. He also did the awesome procedural audio ambience for my game, and on top of that, he did the most incredibly annoying bit of making a game for me: the deployment.

Contact Agent zipfile

Contact Agent screenshot

I didn't finish the game, so it's just one level, and all you can do is run and jump and explore. I can't even really call it an art game because I feel like that would be selling other, really awesome art games short. The vector graphics engine I wrote is massively slow because I didn't have time to optimise anything, which had the side effect of making you fall through floors and stuff on any computer that isn't massively fast. I shouldn't have multiplied all vectors by frame-elapsed time since that number gets really large on those slower computers. Oh well. Good lessons for next time!

Aug. 11, 2008

Update: the article below is for an Inkscape extension I built a while back for doing animation. For a newer and much better solution for doing frame-by-frame animation in both Inkscape and Adobe Illustrator check out SVG Flipbook. Enjoy!

Inkscape is an awesome, free and open source vector graphics program. It's the free world's version of Adobe's Illustrator program. I can't comment on whether it's better or worse since I've never used Illustrator, but there are many things to love about Inkscape and it's very capable when it comes to vector graphics.

One thing that is hard to come by is a good, free and open source vector animation program. The Inkscape website says that they are looking at introducing animation into the program some time in the future, but who knows how long that will take. I needed to make animated vector graphics for my GameJam 0808 entry, which is a vector graphics platformer, so I knocked together a quick script to do layer based animation in Inkscape. Luckily Inkscape's extensions system is quite extensible, so I was able to write a small Python script that renders each layer separately using Inkscape itself, and then uses Imagemagick's "convert" program to mash the layers together into an animated GIF. Finally, the extension automatically launches the default browser to display the result. This currently only works under GNU/Linux systems with Imagemagick installed, but I'm sure it wouldn't be too big a stretch to get it working on other operating systems.

Here is the Inkscape extension XML file. Place this in your .inkscape/extensions/ directory in a file called gifanimate.inx:

<inkscape-extension>
  <_name>GIF Animate</_name>
  <id>org.ekips.filter.GIFAnimate</id>
  <dependency type="executable" location="extensions">gifanimate.py</dependency>
  <dependency type="executable" location="extensions">inkex.py</dependency>
  <!-- <param name="what" type="string" _gui-text="What would you like to greet?">World</param> -->
  <effect>
    <object-type>all</object-type>
    <effects-menu>
       <submenu _name="Animate"/>
    </effects-menu>
  </effect>
  <script>
    <command reldir="extensions" interpreter="python">gifanimate.py</command>
  </script>
</inkscape-extension>

Here is the Python script that does all the work. Put this in the same place, and name it 'gifanimate.py'.

#!/usr/bin/env python
import sys, os
sys.path.append('/usr/share/inkscape/extensions')

import inkex
from simplestyle import *

class GIFAnimate(inkex.Effect):
    def __init__(self):
        inkex.Effect.__init__(self)

    def effect(self):
        infile = sys.argv[-1]

        # Get access to main SVG document element and get its dimensions.
        from xml.dom.minidom import parse, parseString
        dom = parse(infile)
        svg = dom.getElementsByTagName('svg')[0]

        layers = svg.getElementsByTagName('g')
        pngcommand = "inkscape %s -C -i '%s' -j -e /tmp/%s.png 2>&1 > /dev/null"

        layerids = []
        delays = []
        for l in layers:
            bits = l.getAttribute('inkscape:label').split("#")
            if not "ignore" in bits:
                layerids.append(l.getAttribute('id'))
                delays.append(bits[0])

        [os.system(pngcommand % (infile, id, id)) for id in layerids]
        bits = " ".join(["-delay %d /tmp/%s.png" % (int(delays[x]), layerids[x]) for x in range(len(layerids))])
        gifcmd = "convert -loop 0 " + bits + " /tmp/inkscape-anim.gif"
        os.system(gifcmd)
        import webbrowser
        webbrowser.open("file:///tmp/inkscape-anim.gif")

# Create effect instance and apply it.
effect = GIFAnimate()
effect.affect()

To use it go to the 'effects' menu and choose 'Animate > GIF Animate'. Specify frame lengths (in milliseconds) by renaming the layer to the length of time that you'd like each frame to last. Inkscape will add a hash followed by a number for each duplicate entry, but my script will ignore that.

Main character walkcycle

Have fun!

Aug. 5, 2008

A few weeks back my friend Simon came up with the great idea of doing a game jam here in Perth. A game jam is where a bunch of people make teams and try to develop full games in a time limited situation. Similar competitions are Ludum Dare, and Pyweek. They are a lot of fun, and you tend to get lots of code and experience out of it.

This is the tenative title, and main character, from my entry into the competition:

Contact Agent - my game

Anyway, Simon built this cool community site in a matter of days using Pylons, which you can see at http://gamejam.org/ and then we put out the word on the internets, and now we are in the midst of it, with eleven days left to go. The turnout has been pretty superb and I am looking forward to seeing everyone's finished projects. The competition is very loose; you can use any engine you want and pretty much do any kind of game you want, as long as you use the 'significant asset' in your game. Also, it isn't really limited to Perth only and it only just started not long ago, so if you have a hankering to develop a game idea then this is the forum for you; go ahead and join the fray!

My own game is a vector graphics platformer. I think I'll probably get all the code done in time with a basic playable game, but I'm not so sure about getting it deployed onto all platforms. We'll see, I guess!

We'll almost certainly do more game jams in future months. Can't wait.

Aug. 4, 2008

Recursive Dimensional Clustering is a fast algorithm for finding collisions and clusters in sets of data. It scales well from one dimension upwards, making it appropriate for high dimensional scientific applications as well as the obvious application in 2d and 3d games. The algorithm first appeared in the book Game Programming Gems 2 in an entry by Steve Rabin. There is a lot more info and an interactive Flash implementation on this page.

I took the code from Polygonal Laboratory and ported it to Javascript, and then later I took that implementation and ported it to Python. I like to think that my implementation is quite clean, and scalable up to many dimensions with very little effort. It's also fast, as you will see from the demo, detecting about 26 collisions amongst 100 objects per frame at 90 frames per second on my sub-2Ghz dual core machine.

To use it, the programmer needs to inherit the main RDCEntity class and supply 'axis function' methods which return the min and max bounds of the entity in each dimension or axis. The resulting class will have the 'Collide' method called whenever it collides with another entity, and the 'SetGroup' method called when it is detected to be part of a group; see the Polygonal Labs post for more info about what these terms relate to. Note that this does simple axis-aligned bounding-box collisions. If you want more complicated collisions you could use RDC to narrow down which polys are potentially colliding, and then use a poly-poly collision function to see if they're actually colliding. Another approach is to use RDC with multiple bounding boxes per entity. Put the three Python classes below into a file called 'RDC.py' to use them.

The first class represents a 'Boundary' which is what the RDC algorithm uses to determine if things are overlapping in a particular dimension or not. Boundaries are either 'open' (leading edge) or 'closed' (trailing edge).

class Boundary:
    def __init__(self, type, position, obj):
        self.type = type
        self.position = position
        self.obj = obj

The next class is the RDCEntity class which the programmer needs to inherit from in order to run the RDC algorithm on their own game entities.

class RDCEntity:
    """
    Base class you should override to make things which are RDC collideable.

    axisFns is a list of dimension bounding axis conditions.
    These must be methods which return the values of each bound on a particular axis for this entity.
    """
    axisFns = [["GetLeft", "GetRight"], ["GetTop", "GetBottom"]]

    def Collide(self, who):
        """
        'who' is the other entity who we collided with.
        """

    def SetGroup(self, group, friends):
        """
        Set the group identifier number that we are part of.
        group is the integer ID.
        friends is a list of friends in my collision group.
        """

    ##################
    # Axis functions #
    ##################

    def GetLeft(self):
        """
        Example axis function which returns the left bound on the x axis.
        """

The next class makes up the bulk of the actual algorithm. It finds collisions and groups amongst entities. Call it with the DoRDC() method.

class RDC:
    def __init__(self, entityClass):
        # Array of functions that contain the sort and position functions for each axis
        self.axisFns = entityClass.axisFns
        # which axis we are currently checking on in this pass
        self.idx = 0
        # test if there has been a division or not
        self.divided = True

    def DoSort(self, clusters, axis):
        # we're going to replace all clusters with new found ones
        newclusters = []
        # assume that we won't divide any further
        self.divided = False

        # for every sub cluster in our group of clusters
        for c in clusters:
            boundaries = []
            count = 0
            group = []

            # store the intervals for a given axis in a list data structure
            for obj in c:
                boundaries.append(Boundary('o', getattr(obj, axis[0])(), obj))
                boundaries.append(Boundary('c', getattr(obj, axis[1])(), obj))

            # sort our list of all boundaries on position
            boundaries.sort(lambda a, b: cmp(a.position, b.position))

            # finally, make new chunks out of our existing collection
            for i in xrange(len(boundaries)):
                b = boundaries[i]
                # if we find a leading edge, increment our count
                # and push it onto our stack of the current group
                if b.type == "o":
                    count += 1
                    group.append(b.obj)
                elif b.type == "c":
                    count -= 1
                    # if we have finished finding a group
                    # push this group onto the stack on new clusters
                    # empty out our group
                    if count == 0:
                        newclusters.append(group)
                        group = []
                        # if we're not at the very end of our array then we've just made a new subdivision
                        if i != len(boundaries) - 1:
                            self.divided = True

        return newclusters

    def FindGroups(self, group):
        clusters = [group]
        while (self.divided):
            # find the new clusters
            clusters = self.DoSort(clusters, self.axisFns[self.idx])
            # select the next axis for subdivision
            self.idx = (self.idx + 1) % len(self.axisFns)
        return clusters

    def DoRDC(self, entities):
        groups = self.FindGroups(entities)
        for g in xrange(len(groups)):
            # Do collision callbacks - test each entity against others in their group
            self.BruteForceRectangles(groups[g])
            # set which group each entity belongs to
            # pass in group friends
            [d.SetGroup(g, groups[g]) for d in groups[g]]

    def BruteForceRectangles(self, group):
        """
        Bruteforce collision detection.
        Pass in an array of objects, and an array that looks like this:
        [[leading1, trailing1], [leading2, trailing2], [leading3, trailing3]...]

        leading1 is the function that returns the leading edge of an axis bounding box.
        trailing1 is the function that returns the trailing edge of an axis bounding box.
        """
        for i in xrange(len(group)):
            for j in xrange(i + 1, len(group)):
                collided = True
                for a in self.axisFns:
                    if (getattr(group[i], a[0])() > getattr(group[j], a[1])()):
                        collided = False
                    if (getattr(group[j], a[0])() > getattr(group[i], a[1])()):
                        collided = False

                if collided:
                    group[i].Collide(group[j])
                    group[j].Collide(group[i])

I tried running the same code without the 'getattr' function, which gets called many times each frame, but I didn't notice any particular speedup so I think it's fine to leave that in there as it makes the code cleaner and more flexible.

Finally here is some test code that uses pygame to showcase the RDC code above. You can save it in a file called testRDC.py for example, and then run it. You will see it processing collisions between 100 random squares as fast as possible. You can click on the window to pause the simulation and examine an example frame; squares of the same colour are in a group/cluster together, whilst squares with a red border are actually colliding.

from random import random, randint
from time import time
from sys import argv, exit
from os import environ
import pygame

from RDC import *

SCREENSIZE = (640, 480)

collisions = 0

class Square(RDCEntity):
    def __init__(self):
        self.color = [100, 100, 100]
        self.border = [0, 0, 0]
        self.width = 20
        self.height = 20
        self.left = random() * (SCREENSIZE[0] - self.width)
        self.top = random() * (SCREENSIZE[1] - self.height)

    # if we collide with someone
    def Collide(self, who):
        self.border = [255, 0, 0]
        global collisions
        collisions += 1

    def SetGroup(self, g, friends):
        self.color = [(g * 10) % 255, ((g - 50) * 35) % 255, (g * -15) % 255]

    # functions for returning bounding box sides
    def GetLeft(self):
        return self.left

    def GetRight(self):
        return self.left + self.width

    def GetTop(self):
        return self.top

    def GetBottom(self):
        return self.top + self.height

environ['SDL_VIDEO_CENTERED'] = '1'
pygame.init()
screen = pygame.display.set_mode(SCREENSIZE)

pygame.font.init()
fnt = pygame.font.SysFont("Arial", 12)
many = len(argv) > 1 and int(argv[1]) or 100
pause = False
frames = 0
total = 0

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT or event.type == pygame.KEYDOWN:
            exit()

        if event.type == pygame.MOUSEBUTTONDOWN:
            pause = not pause

    if not pause:
        squares = [Square() for s in range(many)]
        start = time()
        rdc = RDC(RDCEntity).DoRDC(squares)
        elapse = int(1 / (time() - start))
        total += elapse
        frames += 1

    txt = fnt.render(str("%d objects collided an average of %d times at %d fps" % (many, collisions / frames / 2, total / frames)), 1, (0, 0, 0))

    screen.fill([255, 255, 255])
    [pygame.draw.rect(screen, s.color, (s.left, s.top, s.width, s.height), 0) for s in squares]
    [pygame.draw.rect(screen, s.border, (s.left, s.top, s.width, s.height), 1) for s in squares]
    screen.blit(txt, (10, 10))
    pygame.display.flip()

For interest's sake, below is my earlier Javascript implementation of the same code. Email me for a test HTML file that uses this code:

/* 
    Recursive dimensional clustering

    Pass in an array that looks like this:
    [[leading1, trailing1], [leading2, trailing2], [leading3, trailing3]...]

    leading1 is the function that returns the leading edge of an axis bounding box.
    trailing1 is the function that returns the trailing edge of an axis bounding box.
*/
PodSix.RDC = function(axisFunctions)
{
    // Array of functions that contain the sort and position functions for each axis
    this.axisFns = axisFunctions;
    // which axis we are currently checking on in this pass
    this.idx = 0;
    // test if there has been a division or not
    this.divided = true;

    var Boundary = function(type, position, obj)
    {
        this.type = type;
        this.position = position;
        this.obj = obj;
    }

    this.DoSort = function(clusters, axis)
    {
        // we're going to replace all clusters with new found ones
        var newclusters = [];
        // assume that we won't divide any further
        this.divided = false;

        // for every sub cluster in our group of clusters
        for (c in clusters)
        {
            var boundaries = [];
            var count = 0;
            var group = [];

            // store the intervals for a given axis in a list data structure
            for (i in clusters[c])
            {
                obj = clusters[c][i];
                boundaries.push(new Boundary('o', obj[axis[0]](), obj));
                boundaries.push(new Boundary('c', obj[axis[1]](), obj));
                debug.innerHTML += obj[axis[0]]() +"," + obj[axis[1]]() + "</br>";
            }

            // sort our list of all boundaries on position
            boundaries.sort(function(a, b) { return a.position - b.position });

            // finally, make new chunks out of our existing collection
            for (i = 0; i < boundaries.length; i++)
            {
                b = boundaries[i];
                // if we find a leading edge, increment our count
                // and push it onto our stack of the current group
                if (b.type == "o")
                {
                    count++;
                    group.push(b.obj);
                }
                else if (b.type == "c")
                {
                    count--;
                    // if we have finished finding a group
                    // push this group onto the stack on new clusters
                    // empty out our group
                    if (count == 0)
                    {
                        newclusters.push(group);
                        group = [];
                        // if we're not at the very end of our array then we've just made a new subdivision
                        if (i != boundaries.length - 1)
                        {
                            this.divided = true;
                        }
                    }
                }
            }
        }

        return newclusters;
    }

    this.FindGroups = function(group)
    {
        var clusters = [group];
        while (this.divided)
        {
            // find the new clusters
            clusters = this.DoSort(clusters, this.axisFns[this.idx]);
            // select the next axis for subdivision
            this.idx = (this.idx + 1) % this.axisFns.length;
        }
        return clusters;
    }
}

/*
    Bruteforce collision detection.
    Pass in an array of objects, and an array that looks like this:
    [[leading1, trailing1], [leading2, trailing2], [leading3, trailing3]...]

    leading1 is the function that returns the leading edge of an axis bounding box.
    trailing1 is the function that returns the trailing edge of an axis bounding box.
*/
PodSix.BruteForceRectangles = function(group, axisFns)
{
    for (var i = 0; i < group.length; i++)
    {
        for (var j = i + 1; j < group.length; j++)
        {
            collided = true;
            for (a in axisFns)
            {
                if (group[i][axisFns[a][0]]() > group[j][axisFns[a][1]]()) collided = false;
                if (group[j][axisFns[a][0]]() > group[i][axisFns[a][1]]()) collided = false;
            }

            if (collided)
            {
                group[i].Collide(group[j]);
                group[j].Collide(group[i]);
            }
        }
    }
}

Aug. 1, 2008

This new tune is part of my set tonight:

Chris McCormick - Many Faces.mp3