3 May 2010 ruoso   » (Journeyer)

Writing Games in Perl - Part 7 - Game Map

Following posts 1, 2, 3, 4, 5 and 6 on the subject of writing games in Perl, now we are going to add support for maps.

At this moment, the initial ball position, as well as the walls are being defined in Perl code, during the controller initialization. What we are going to do now is creating a serialization format that describes our simulated universe, then have a set of maps in a directory navigating through them as the goals in each map are achieved.

The Map Format

There are several options to serialize and deserialize data, some are easier to use, others provide more introspection and others are better performant. I've read once a good advice in game development, which is: keep your map format accessible to art people.

A lot of people hate XML, I'm not of that club, I do like XML a lot, specially because it allows introspection and validation via XML Schema. And after the advent of XML::Compile::Schema, it's very simple to handle XML in Perl. Basically, once you have a XML Schema, you can think just in Perl data structures that will be serialized/deserialized from/to XML with associated validation.

That being said, let's proceed to our map format, which is going to be expressed as a XML Schema Definition:

<?xml version="1.0" encoding="UTF-8"?>
  <xs:element name="map">
        <xs:element name="ball">
            <xs:attribute name="radius" type="xs:float" />
            <xs:attribute name="x" type="xs:float" />
            <xs:attribute name="y" type="xs:float" />
        <xs:element name="goal">
            <xs:attribute name="x" type="xs:float" />
            <xs:attribute name="y" type="xs:float" />
        <xs:element name="wall" maxOccurs="unbounded">
            <xs:attribute name="x" type="xs:float" />
            <xs:attribute name="y" type="xs:float" />
            <xs:attribute name="w" type="xs:float" />
            <xs:attribute name="h" type="xs:float" />

What the above means is:

  • The root element is "map", it is composed as a sequence.
  • The first element in the "map" sequence is "ball", it should appear once and only once, it has "radius", "x" and "y" as attributes.
  • The second element is "goal" it also should appear only once and has "x" and "y" as attributes.
  • Finally, the third element is "wall" which can happen more than once and has "x", "y", "w" and "h" as attributes.

In Perl data structures that will mean the following:

  • The main "map" structure is a hash, with "ball", "goal" and "wall" as keys.
  • The value for "ball" will be another hash, with "radius", "x" and "y" as keys and the floats as values
  • The value for "goal" will be another hash, with "x" and "y" as keys and the floats as values
  • The value for "wall" is going to be an arrayref containing one hash for each wall define, where those will have "x", "y", "w" and "h" as keys with the floats as values

The map currently implemented in Perl code would be the following perl structure:

{ ball => { radius  => 0.5,
            x       => 4,
            y       => 10 },
  goal => { x       => 10,
            y       => 12.5 },
  wall => [{ x => 0, y => 0, w => 20, h => 1 },
           { x => 0, y => 0, h => 20, w => 1 },
           { x => 20, y => 0, h => 20, w => 1 },
           { x => 0, y => 20, w => 21, h => 1 },
           { x => 7, y => 0, h => 9, w => 1 },
           { x => 7, y => 11, h => 9, w => 1 },
           { x => 12, y => 0, h => 9, w => 1 },
           { x => 12, y => 11, h => 9, w => 1 },
           { x => 9.2, y => 11, h => 1, 1.6 } ] }

That same structure as XML looks like:

<?xml version="1.0"?>
<map xmlns="http://daniel.ruoso.com/categoria/perl/games-perl-7">
 <ball radius="0.5" x="4" y="10"/>
 <goal x="10" y="12.5"/>
 <wall x="0" y="0" w="20" h="1"/>
 <wall x="0" y="0" w="1" h="20"/>
 <wall x="20" y="0" w="1" h="20"/>
 <wall x="0" y="20" w="21" h="1"/>
 <wall x="7" y="0" w="1" h="9"/>
 <wall x="7" y="11" w="1" h="9"/>
 <wall x="12" y="0" w="1" h="9"/>
 <wall x="12" y="11" w="1" h="9"/>
 <wall x="9.2" y="11" w="1.6" h="1"/>

With the advantage that non-Perl-Programmers can edit this map in a very confortable way. They can even validate the XML outside our game by using the XML Schema.

We're going to use a "maps" directory where we're going to load the maps in alphabetical order, so I'm going to save it as "zz_original_map.xml".

Loading the map

As the various objects were being created in the InGame controller initialization, we're simply going to replace the hard-coded initialization for the map-based loading.

The first step, which might happen at compile time, is building the XML::Compile::Schema closure that will parse the map.

use XML::Compile::Schema;
use XML::Compile::Util qw(pack_type);
use constant MAP_NS => 'http://daniel.ruoso.com/categoria/perl/games-perl-7';
my $s = XML::Compile::Schema->new('schema/map.xsd');
my $r = $s->compile('READER', pack_type(MAP_NS, 'map'),
                    sloppy_floats => 1);

$r is a code-reference that you call sending the xml document.

We also want to add a new attribute to the controller which will provide the map name:

has 'mapname' => ( is => 'ro',
                   isa => 'Str',
                   required => 1 );

For simplification sake, we're going to just send the name of the first map in the controller ->new call:

my $controller = InGame->new({ main_surface => $surf,
                               mapname => 'maps/zz_original_map.xml' });

And the InGame initialization code now looks like:

sub BUILD {
    my $self = shift;

    my $background = Plane->new({ main => $self->main_surface,
                                  color => 0xFFFFFF });

    my $camera = Camera->new({ pixels_w => $self->main_surface->width,
                               pixels_h => $self->main_surface->height,
                               pointing_x => $self->ball->cen_h,
                               pointing_y => $self->ball->cen_v });

    my $map = $r->($self->mapname);

    # first, let's set the ball position and radius.

    # attach the ball to the camera.

    # create the ball view
    my $ball_view = FilledRect->new({ color => 0x0000FF,
                                      camera => $camera,
                                      main => $self->main_surface,
                                      x => $self->ball->pos_h,
                                      y => $self->ball->pos_v,
                                      w => $self->ball->width,
                                      h => $self->ball->height });

    # now create the goal
    my $goal_view = FilledRect->new({ color => 0xFFFF00,
                                      camera => $camera,
                                      main => $self->main_surface,
                                      x => $self->goal->x - 0.1,
                                      y => $self->goal->y - 0.1,
                                      w => 0.2,
                                      h => 0.2 });

    push @{$self->views}, $background, $ball_view, $goal_view;

    # now we need to build four walls, to enclose our ball.
    foreach my $rect (map { Rect->new($_) } @{$map->{wall}}) {

        my $wall_model = Wall->new({ pos_v => $rect->y,
                                     pos_h => $rect->x,
                                     width => $rect->w,
                                     height => $rect->h });

        push @{$self->walls}, $wall_model;

        my $wall_view = FilledRect->new({ color => 0xFF0000,
                                          camera => $camera,
                                          main => $self->main_surface,
                                          x => $rect->x,
                                          y => $rect->y,
                                          w => $rect->w,
                                          h => $rect->h });

        push @{$self->views}, $wall_view;



At this point, the game is fully functional with the original map, now we can proceed to the next point.

Map cycling

We already have a goal in each map, so we need to react when the goal is reached so the next map is loaded. As you might have noticed, the InGame controller is completely tied to each map, so what we need to do is replace the controller instance by one with the new map.

There's one important point in the way our ball.pl script handles the main loop, it is not fully delegated to the controller, but it tries to handle the global events before it sends it to the controller.

What this means is that we can use an User SDL event to signal the main application that the goal for this controller instance was already achieved and that it should initialize the next controller.

So, first we're going to fire the event in the InGame controller as soon as the ball reaches the goal:

    if (collide_goal($ball, $self->goal, $frame_elapsed_time)) {
        my $event = SDL::Event->new();
        $event->type( SDL_USEREVENT );

We're not doing putting any additional data in the event because this is the only user event we have in the game, we could use the event_code and the two pointers for data in the SDL::Event if we wanted to have a better qualification of the event.

Now we just need to handle that event. First, we're going to get the list of available maps in the beggining of ball.pl:

my @maps = sort <maps/*.xml>;

Then we're going to replace the hard-coded map selection with the first map in that array.

my $controller = InGame->new({ main_surface => $surf,
                               mapname => shift @maps });

And, finally, handle the SDL_USEREVENT replacing the controller with a new instance while there are still maps in @maps.

    while (SDL::Events::poll_event($sevent)) {
        my $type = $sevent->type;
        if ($type == SDL_QUIT) {
            my $nextmap = shift @maps;
            if ($nextmap) {
                $controller = InGame->new({ main_surface => $surf,
                                            mapname => $nextmap });
            } else {
                print 'Finished course in '.(($now - $first_time)/1000)."\n";
        } elsif ($controller->handle_sdl_event($sevent)) {
            # handled.
        } else {
            # unknown event.

As usual, follows a small video of the game, where it starts in one map and when the goal is achieved, the second map is loaded.

Syndicated 2010-05-02 20:29:32 from Daniel Ruoso

Latest blog entries     Older blog entries

New Advogato Features

New HTML Parser: The long-awaited libxml2 based HTML parser code is live. It needs further work but already handles most markup better than the original parser.

Keep up with the latest Advogato features by reading the Advogato status blog.

If you're a C programmer with some spare time, take a look at the mod_virgule project page and help us with one of the tasks on the ToDo list!