Building a 3D Engine in Perl, Part 2
by Geoff Broadwell
|
Pages: 1, 2, 3, 4, 5
The Big Event
Keyboard handling is a special case of SDL event handling, and not an
entirely trivial case at that. I'll start with the basic structure for
processing SDL events and handle a much simpler event first. To access SDL
events, I need to load the SDL::Event module:
use SDL::Event;
Like SDL::App, the code needs to keep track of an
SDL::Event resource object to access the event queue. In addition,
I need to keep track of which routine I'll use to process each event type. This
is a new kind of data, so I add a new branch to the engine object for various
lookup tables. To set up both of these, I add a new initialization function:
sub init_event_processing
{
my $self = shift;
$self->{resource}{sdl_event} = SDL::Event->new;
$self->{lookup}{event_processor} = {
&SDL_QUIT => \&process_quit,
};
}
SDL event types are constants in the general SDL constant convention
(UPPERCASE with a leading SDL_ marker). The event type for quit
events is SDL_QUIT, which I associate with the
process_quit routine using a subroutine reference.
A new line at the end of init calls the initialization
routine:
$self->init_event_processing;
Every time through, the main loop should process events before updating the
view (after I add keyboard control, the view should update using the latest
user input). The contents of the loop in main_loop are now as
follows:
$self->{state}{frame}++;
$self->update_time;
$self->do_events;
$self->update_view;
$self->do_frame;
do_events is very simple at this stage, just calling
process_events to, er, process pending SDL events:
sub do_events
{
my $self = shift;
my $queue = $self->process_events;
}
The Event Processing Loop
process_events is where all the magic happens:
sub process_events
{
my $self = shift;
my $event = $self->{resource}{sdl_event};
my $lookup = $self->{lookup}{event_processor};
my ($process, $command, @queue);
$event->pump;
while (not $self->{state}{done} and $event->poll) {
$process = $lookup->{$event->type} or next;
$command = $self->$process($event);
push @queue, $command if $command;
}
return \@queue;
}
The first couple of lines provide shorter names for the previously stored
SDL::Event object and event processor lookup table. The rest of
the variables respectively store:
- A reference to the processing routine for the current event
- The internal command to convert the event into
- The queue of commands collected from incoming events
The core of the code starts by telling the SDL::Event object to
gather any pending operating system events in preparation for the processing
loop, using the pump method. The processing loop checks to make
sure that a previous event has not flagged the done state, which
helps to improve responsiveness to quit events. Assuming that this has not
happened, the loop requests the next SDL event using
SDL::Event::poll. poll returns a false value when
there are no events ready for pickup, thereby exiting the loop.
The first line inside the loop uses the event type to look up the proper
event processing routine. If there is none, I use next to loop
again and check the next event. Otherwise, the next line calls the processing
routine as a dynamically chosen method to handle the event. If the processing
routine determines that the event requires additional work, it should return a
command packet to be queued. If the event should be ignored, the processor
should simply return a false value.
The last line within the loop adds the command packet (if any) to the queue
awaiting further processing. Once the loop processes all available SDL events,
process_events returns the queue so that do_events
can perform the next stage of processing.
It may seem confusing that each time through the loop the code reuses the
same $event. You might expect SDL::Event::poll to
return the next waiting event (and perhaps undef when none
remain). Instead, the SDL API specifies that poll copies the data
from the next entry in the event queue into the event object, returning a true
or false status indicating whether this operation succeeded. As with some of
the OpenGL quirks, SDL_Perl copies this odd interface directly, easing the
transition for programmers used to the C API.
A consequence of this interface decision is that the event processing
routine must make a copy of any data from the SDL event object needed for
later. The call to SDL::Event::poll in the next iteration of the
processing loop will overwrite any data left in the SDL event object, so simply
storing the object reference won't work.
The process_quit routine doesn't need to save any data; it only
matters that an SDL_QUIT event occurred:
sub process_quit
{
my $self = shift;
$self->{state}{done} = 1;
return 'quit';
}
process_quit first sets the done state flag, which
causes the loop in process_events to exit early and, more
importantly, exits main_loop. It returns the simplest type of
command packet, a string indicating the quit command. At this
point, there's no code to process this command further, but this keeps things
parallel with the keyboard version I'll show next.
What does all this buy us? For starters, we can now (finally) quit the program using the window manager before the animation runs its course. On my system, that means clicking the 'X' on the window's title bar. Still, that's not the same as having a quit key (which I find much more convenient).
Key Binding
To add a quit key, I first need to decide which key should quit the
program. I'd choose the Escape key because that makes mnemonic sense to me, but
everyone has their favorite, so I'll allow that to be a configuration setting.
To do this, I extend the configuration hash with a new bind
section:
sub init_conf
{
my $self = shift;
$self->{conf} = {
title => 'Camel 3D',
width => 400,
height => 400,
fovy => 90,
bind => {
escape => 'quit',
}
};
}
Now anyone who wants to choose a different quit key can simply change the keyboard bindings hash. In fact, several keys could be associated with the same command, so that either the Escape key or 'q' would exit the program. The hash value corresponding to each specified key is the command packet issued when the user presses that key. This one matches the command packet I'd chosen for the window manager quit message earlier.
Next, I need to process keypress events, which have the event type
SDL_KEYDOWN. I add another entry to the
event_processor hash:
sub init_event_processing
{
my $self = shift;
$self->{resource}{sdl_event} = SDL::Event->new;
$self->{lookup}{event_processor} = {
&SDL_QUIT => \&process_quit,
&SDL_KEYDOWN => \&process_key,
};
}
and define the key processor as follows:
sub process_key
{
my $self = shift;
my $event = shift;
my $symbol = $event->key_sym;
my $name = SDL::GetKeyName($symbol);
my $command = $self->{conf}{bind}{$name} || '';
return $command;
}
process_key starts by extracting the key symbol from the SDL
event. Key symbols are rather opaque for our purposes, so I request the key
name matching the extracted key symbol using SDL::GetKeyName. This
produces a friendly key name that I look up in the key bindings hash to find
the appropriate command packet. If there is none, no matter; that key isn't
bound yet so it yields an empty command packet. process_key then
returns the command packet to add to the queue for further processing.
Handling Command Packets
At this point, the code converts a press of the Escape key into a quit
command packet, but do_events ignores that packet because it does
not process the command queue it receives from process_events. To
make something happen, I first need to associate each known command with an
action routine. I create a new lookup hash for this association, initialized in
init_command_actions:
sub init_command_actions
{
my $self = shift;
$self->{lookup}{command_action} = {
quit => \&action_quit,
};
}
As usual, I call this at the end of init:
$self->init_command_actions;
It's now time to fill out do_events:
sub do_events
{
my $self = shift;
my $queue = $self->process_events;
my $lookup = $self->{lookup}{command_action};
my ($command, $action);
while (not $self->{state}{done} and @$queue) {
$command = shift @$queue;
$action = $lookup->{$command} or next;
$self->$action($command);
}
}
This is similar in form to process_events. Instead of
processing events from SDL's internal queue to create a queue of command
packets, it processes queued command packets into actions to perform. The loop
starts as usual by checking that the done is not true and that
there are still commands pending in the queue.
Within the loop, it shifts the next command off the front of the queue. The
next line determines the action routine associated with the command. If it
cannot find one, it uses next to skip to the next command.
Otherwise, it calls the action routine as a dynamically chosen method with the
command packet as a parameter. This allows a single action routine to process
several similar commands while still being able to tell the difference between
them. I'll need this later for processing movement keys.
For all of that, action_quit is very simple; it just flags
done:
sub action_quit
{
my $self = shift;
$self->{state}{done} = 1;
}
At this point, the Escape key really will quit the program early, and the window manager quit still works as well.
Now that the user can quit whenever desired, I can finally remove the
incongruous end of draw_frame. It's no longer necessary to force
the program to end after five seconds, and the dots printed each frame have
outlived their usefulness. The routine now looks like this:
sub draw_frame
{
my $self = shift;
$self->set_projection_3d;
$self->set_view_3d;
$self->draw_view;
}
Now, if you wait long enough after the objects disappear on the right, the view rotates all the way around, and the scene appears again on the left. This version of the routine is much cleaner and incidently closes the next open refactoring issue (changing engine state within a drawing routine) for free.

