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, andpull_pin - Continuing commands like
yaw_leftandscream_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 |
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!
You must be logged in to the O'Reilly Network to post a talkback.
Showing messages 1 through 6 of 6.
- Screenshots?
2005-01-28 08:46:18 bcarroll [Reply]
Do you plan to include any screenshots?
It would be nice to see what is expected to displayed so I know what to look forward to...
- microsoft support
2005-01-12 13:58:52 ckujawski [Reply]
Will I be able to build the 3d engine in microsoft windows xp? In your next article I hope to see it supported.
- Problem with events (SDL_QUIT...) [2 - Solution ?]
2005-01-02 03:43:27 ironfede [Reply]
After some experiment, I've tried to add:
use SDL::Constants
and it seems to work.
Federico
- Problem with events (SDL_QUIT...) [2 - Solution ?]
2005-03-18 15:20:45 webfiend [Reply]
Thanks for hunting that down. I was just trying to figure out the issue on my own machine!
- Problem with events (SDL_QUIT...) [2 - Solution ?]
- Problem with events (SDL_QUIT...)
2005-01-02 03:18:48 ironfede [Reply]
I have found this article very interesting but code examples after 'step032', where events are introduced, don't work on my Slackware (sdl 1.2.7). The following error is returned:
Undefined subroutine &main::SDL_QUIT called at ./step033 line 74, <DATA> line 266.
All libraries seem to be correctly installed.
Thank you for help.
Federico
- Problem with events (SDL_QUIT...)
2007-12-28 10:35:48 w33dsc00l [Reply]
Yes, Im getting these messages, too.
You have to [code]use SDL;[/code] in the program, so that these subroutines are known.
regards
- Problem with events (SDL_QUIT...)



