Writing Games in Perl - Part 7 - Game Map

| No Comments | No TrackBacks

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:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://daniel.ruoso.com/categoria/perl/games-perl-7"
elementFormDefault="qualified">
<xs:element name="map">
<xs:complexType>
<xs:sequence>
<xs:element name="ball">
<xs:complexType>
<xs:attribute name="radius" type="xs:float" />
<xs:attribute name="x" type="xs:float" />
<xs:attribute name="y" type="xs:float" />
</xs:complexType>
</xs:element>
<xs:element name="goal">
<xs:complexType>
<xs:attribute name="x" type="xs:float" />
<xs:attribute name="y" type="xs:float" />
</xs:complexType>
</xs:element>
<xs:element name="wall" maxOccurs="unbounded">
<xs:complexType>
<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" />
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>

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"/>
</map>

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.
$self->ball->cen_h($map->{ball}{x});
$self->ball->cen_v($map->{ball}{y});
$self->ball->radius($map->{ball}{radius});

# attach the ball to the camera.
$self->ball->add_rect_moving_listener($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 });
$self->ball->add_rect_moving_listener($ball_view);

# now create the goal
$self->goal(Point->new($map->{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 });

$self->views([]);
push @{$self->views}, $background, $ball_view, $goal_view;
$self->walls([]);

# 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 );
SDL::Events::push_event($event);
}

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) {
exit;
my $nextmap = shift @maps;
if ($nextmap) {
$controller = InGame->new({ main_surface => $surf,
mapname => $nextmap });
} else {
print 'Finished course in '.(($now - $first_time)/1000)."\n";
exit;
}
} 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.

No TrackBacks

TrackBack URL: http://daniel.ruoso.com/cgi-bin/mt/mt-tb.cgi/156

Leave a comment