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

Controlling the View

Now that the code can handle keypress events, it's time to control the view using the keyboard.

Instead of having the view completely recalculated every frame, I'd rather have each keypress modify the existing view state. To specify the initial state, I add another initialization routine:

sub init_view
{
    my $self = shift;

    $self->{world}{view} = {
        position    => [6, 2, 10],
        orientation => [0, 0, 1, 0],
        d_yaw       => 0,
    };
}

The new entry, d_yaw, tells update_view if there is a pending change (aka delta, hence the leading d_) in facing. The code so far can only handle yaw (left and right rotation), so that's the only delta key needed right now.

init calls this routine as usual in its new last line:

$self->init_view;

update_view applies the yaw delta to the view orientation, then zeroes out d_yaw so that it won't continue to affect the rotation in succeeding frames (without the user pressing the rotation keys again):

sub update_view
{
    my $self   = shift;

    my $view   = $self->{world}{view};

    $view->{orientation}[0] += $view->{d_yaw};
    $view->{d_yaw}           = 0;
}

A command action assigned to the yaw_left and yaw_right commands updates d_yaw:

sub init_command_actions
{
    my $self = shift;

    $self->{lookup}{command_action} = {
        quit      => \&action_quit,
        yaw_left  => \&action_move,
        yaw_right => \&action_move,
    };
}

To assign keys for these commands, I update the bind hash in init_conf:

bind   => {
    escape => 'quit',
    left   => 'yaw_left',
    right  => 'yaw_right',
}

The big change is the new command action routine action_move:

sub action_move
{
    my $self = shift;

    my $command     = shift;
    my $view        = $self->{world}{view};
    my $speed_yaw   = 10;
    my %move_update = (
        yaw_left  => [d_yaw =>  $speed_yaw],
        yaw_right => [d_yaw => -$speed_yaw],
    );
    my $update = $move_update{$command} or return;

    $view->{$update->[0]} += $update->[1];
}

action_move starts by grabbing the command parameter and current view. It then sets the basic rotation speed, measured in degrees per key press. Next, the %move_update hash defines the view update associated with each known command. If it knows the command, it retrieves the corresponding update. If not, action_move returns.

The last line interprets the update. The view key specified by the first element of the update array is incremented by the amount specified by the second element. In other words, receiving a yaw_left command causes the routine to add $speed_yaw to $view->{d_yaw}; a yaw_right command adds -$speed_yaw to $view->{d_yaw}, effectively turning the view the opposite direction.

With these changes in place, the program starts up looking directly at the scene as it appeared near the beginning of this article. Each press of the left or right arrow keys turns the view ten degrees in the appropriate direction (remember that the scene appears to turn the opposite direction around the view). Holding the keys down does nothing; only a change from unpressed to pressed does anything, and it only rotates the view one increment. This, as they say, is suboptimal.

Angular Velocity

In order to solve this, the code has to change from working purely in terms of angular position to working in terms of angular velocity. Pressing a key should start the view rotating at a constant speed, and it should stay that way until the key is released.

Velocity goes hand in hand with time. In particular, for each frame, update_view needs to know how much time has passed since the last frame to determine the change in angle matching the rotation speed. To compute this time delta, the first change is to make sure the code always has a valid world time by initializing it at program start:

sub init_time
{
    my $self             = shift;

    $self->{world}{time} = now();
}

Of course, this requires another line at the end of init:

$self->init_time;

With this in place, I can change update_time to record the time delta for each frame:

sub update_time
{
    my $self = shift;

    my $now  = now();

    $self->{world}{d_time} = $now - $self->{world}{time};
    $self->{world}{time}   = $now;
}

I've made a few changes that shouldn't affect the behavior of the program, and I'm about to make several more that definitely will change the behavior, so now is a good time for a quick sanity test. All is fine, so it's time to contemplate the design for the remaining code.

Continuing Commands

There are really two classes of keyboard commands that I want to handle:

  • Single-shots like quit, drop_object, and pull_pin
  • Continuing commands like yaw_left and scream_head_off

To differentiate them, I borrow an existing game convention and use a leading + to indicate a continuing command. This changes the bind mapping in init_conf:

bind   => {
    escape => 'quit',
    left   => '+yaw_left',
    right  => '+yaw_right',
}

and the command_action lookup:

sub init_command_actions
{
    my $self = shift;

    $self->{lookup}{command_action} = {
          quit       => \&action_quit,
        '+yaw_left'  => \&action_move,
        '+yaw_right' => \&action_move,
    };
}

To process the key release events, I need to assign an event processor for the SDL_KEYUP event. I'll reuse the existing process_key routine:

sub init_event_processing
{
    my $self = shift;

    $self->{resource}{sdl_event} = SDL::Event->new;
    $self->{lookup}{event_processor} = {
        &SDL_QUIT    => \&process_quit,
        &SDL_KEYUP   => \&process_key,
        &SDL_KEYDOWN => \&process_key,
    };
}

process_key needs some training to be able to differentiate the two types of events:

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} || '';
    my $down    = $event->type == SDL_KEYDOWN;

    if ($command =~ /^\+/) {
        return [$command, $down];
    }
    else {
        return $down ? $command : '';
    }
}

The new code (everything after the my $command line) first sets $down to true if the key is being pressed or to false if the key is being released. The remaining changes replace the old return $command line. For continuing commands (those that start with a +), there's a new class of command packet, containing both the $command and the $down boolean to indicate whether the command should begin or end. Single-shot commands (those without a leading +), send a simple command packet only for keypresses; they ignore key releases.

To handle the new class of command packets, I update do_events as well:

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) {
        my @args;
        $command          = shift @$queue;
        ($command, @args) = @$command if ref $command;

        $action = $lookup->{$command} or next;
        $self->$action($command, @args);
    }
}

The only new code is inside the loop. It starts off by assuming that the command packet is a simple one, with no arguments. If the command turns out to be a reference instead of a string, it unpacks it into a command string and some arguments. The $action lookup remains unchanged, but the last line changes slightly to add @args to the parameters of the action routine. If there are no arguments, this has no effect, so a single-shot action routine such as action_quit can remain unchanged.

View, Meet Velocity

The view needs to keep track of the current yaw velocity and the velocity delta when the user presses or releases a key; I initialize them to 0 in init_view:

sub init_view
{
    my $self = shift;

    $self->{world}{view} = {
        position    => [6, 2, 10],
        orientation => [0, 0, 1, 0],
        d_yaw       => 0,
        v_yaw       => 0,
        dv_yaw      => 0,
    };
}

update_view needs a few more lines to handle the new variables:

sub update_view
{
    my $self   = shift;

    my $view   = $self->{world}{view};
    my $d_time = $self->{world}{d_time};

    $view->{orientation}[0] += $view->{d_yaw};
    $view->{d_yaw}           = 0;

    $view->{v_yaw}          += $view->{dv_yaw};
    $view->{dv_yaw}          = 0;
    $view->{orientation}[0] += $view->{v_yaw} * $d_time;
}

After adding any velocity delta to the current yaw velocity, this method multiples the total yaw velocity by the time delta for this frame to determine the change in orientation. This is accumulated with the current orientation and any other facing change for this frame.

Finally, I update action_move to handle the new semantics:

sub action_move
{
    my $self = shift;

    my ($command, $down) = @_;
    my $sign             = $down ? 1 : -1;
    my $view             = $self->{world}{view};
    my $speed_yaw        = 36;
    my %move_update      = (
        '+yaw_left'  => [dv_yaw =>  $speed_yaw],
        '+yaw_right' => [dv_yaw => -$speed_yaw],
    );
    my $update = $move_update{$command} or return;

    $view->{$update->[0]} += $update->[1] * $sign;
}

The $sign variable converts the $down parameter from 1/0 to +1/-1. I changed the last line of the routine to multiply the delta by this sign before updating the value. Adding a negated value is the same as subtracting the original value; this means that pressing a key requires adding the update, and releasing it will subtract it back out.

Related Reading

Games, Diversions & Perl Culture
Best of the Perl Journal

To make sure the new yaw commands update velocity, I also fixed up the %move_update hash to update dv_yaw instead of d_yaw and used the + versions of the command names. Finally, to bring back the old rotation rate, I set $speed_yaw to 36 degrees per second.

This version responds the way most people expect. Holding down a key turns the proper direction until the key is released. What about when the user presses multiple keys at once? This is why I was careful always to accumulate updates and deltas by using += instead of plain old =. If the user holds both the right and left arrow keys down at the same time, the view remains motionless because they've added in equal and opposite values to dv_yaw. If the user releases just one of the keys, the view rotates in the proper direction for the key that is still held down because the opposing update has now been subtracted back out. Press the released key back down while still holding the other, and the rotation stops again as expected.

Of course, there's no requirement that the speeds for yawing left and right must be the same. In fact, for an airplane or spaceship simulation, the game engine might set these differently to represent damage to the control surfaces or maneuvering thrusters. It may even be part of the gameplay to hold both direction keys down at the same time to compensate partially for this damage, perhaps tapping one key while holding the other steady.

One thing that doesn't magically work is making sure that if several keys map to the same command, pressing them all won't make the command take effect several times over. As it stands, the user could map five keys to the same movement command and move five times as fast. You might try fixing this on your own as a quick puzzle; I'll try to address it in a later installment.

Eyes in the Back of Your Head

You might be curious why I left d_yaw hanging around, since nothing uses it now. I could use it in the above-mentioned space simulation to simulate a thruster stuck on--continuously trying to veer the ship off course. In a first-person game, it allows one of my favorite commands, +look_behind. Holding down the appropriate key rotates the view 180 degrees. Releasing the key snaps the view back forward. To implement this, I need to add another entry to the bind hash:

bind   => {
    escape => 'quit',
    left   => '+yaw_left',
    right  => '+yaw_right',
    tab    => '+look_behind',
}

Then another command_action entry:

sub init_command_actions
{
    my $self = shift;

    $self->{lookup}{command_action} = {
          quit         => \&action_quit,
        '+yaw_left'    => \&action_move,
        '+yaw_right'   => \&action_move,
        '+look_behind' => \&action_move,
    };
}

Last but not least, another entry in %move_update:

my %move_update      = (
    '+yaw_left'    => [dv_yaw =>  $speed_yaw],
    '+yaw_right'   => [dv_yaw => -$speed_yaw],
    '+look_behind' => [d_yaw  =>  180       ],
);

That's it: a whopping three lines, all of which were entries in lookup hashes.

Conclusion

That's it for this article; it's already quite long. I started where I left off in the last article. From there, I talked about translation and rotation of the view; millisecond resolution SDL time; animation from jerky beginnings to smooth movement; basic SDL event and keyboard handling; single-shot and continuing commands; and a whole lot of refactoring.

Next time, I'll talk about moving the position of the viewpoint, clean up draw_view, and spend some more time on the OpenGL side of things with the basics of lighting and materials. In the meantime, I've covered quite a lot in this article, so go forth and experiment!