Sign In/My Account | View Cart  
advertisement


Listen Print Discuss

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.

Pages: 1, 2, 3, 4, 5

Next Pagearrow