17.10. tkneko—Animating the Neko on a CanvasA more interesting task is emulating Masayuki Koba's xneko game, where a neko chases after the cursor, running up, down, left, right, and in circles, stopping only when the cursor stops. If the cursor stays motionless long enough, the neko falls into a deep sleep. When the cursor moves again, the neko awakens and resumes chasing the cursor. The neko is confined to the MainWindow, but if the cursor leads him to a window edge, he scratches to get free and eventually either falls asleep or resumes running. To simulate motion, we display images of the neko at various positions on the Canvas at tenth of a second intervals (anything slower presents unacceptable flicker). The images we have to work with are shown in Figure 17-14. To make the neko run left, we repeatedly display the left1.ppm and left2.ppm images. (If the images display at the same Canvas coordinate, the neko runs in place. We might as well have used Tk::Animation if we wanted that effect.) The neko's actions are state driven.[44]
There are in fact five distinct states: the neko is either waking, moving, stopping, scratching, or sleeping. The tkneko states are encoded in a Perl hash with compiled (for efficiency) regular expressions as keys and code references (the state processors) as values: %states = ( qr/AWAKE/ => \&do_awake, qr/UP|UPRIGHT|RIGHT|DWRIGHT|DOWN|DWLEFT|LEFT|UPLEFT/ => \&do_move, qr/STOP/ => \&do_stop, qr/UTOGI|RTOGI|DTOGI|LTOGI/ => \&do_togi, qr/SLEEP/ => \&do_sleep, ); # neko state table The states that are dependent on the neko's direction (but otherwise equivalent) are further divided into substates, described by a regular expression with alternatives. go_neko, the animation main loop, is activated by a repeating 100 millisecond timer event. The subroutine's job is simply to call a subroutine based on the animation's current state, $state. The subroutine in turn selects an appropriate PPM image and displays it on the canvas. As long as the neko stays in a constant state, running left for example, the variable $state_count keeps incrementing, and the state processing subroutine do_move can use this to alternately select the left1.ppm or left2.ppm image. The debug -textvariable $where shows this state information as well as the neko's current Canvas coordinates, $nx and $ny. Figure 17-17 shows the neko in its sleep state. $mw->repeat(100 => \&go_neko); sub go_neko { $state_count++; # current state's cycle count $where = sprintf("state=%-7s state_count=%05d, nx=%04d, ny=%04d", $state, $state_count, $nx, $ny); STATES: foreach my $regex (keys %states) { next STATES unless $state =~ /^($regex)$/; &{$states{$regex}}($1); return; } } # end go_neko We create all the PPM images during initialization, make Canvas image items of them, and store the item IDs in the %pixmaps hash, indexed by filename. But we don't want all these individual animation frames visible unless they're needed, so we position them off-Canvas at the invisible coordinates (-1000, -1000). foreach my $pfn ( <$image_base/*.ppm> ) { my $bpfn = basename $pfn; $pixmaps{$bpfn} = $canvas->createImage(-1000, -1000, -image => $canvas->Photo(-file => $pfn)); } Figure 17-17. The neko has spent 79 cycles in the SLEEP state
When a state processing subroutine selects an image (animation frame) for display, it calls the frame subroutine with the new pixmap name. After hiding the old image, frame moves the new image to the neko's current Canvas position.[45]
sub frame { $canvas->coords($pixmaps{$pix}, -1000, -1000); $pix = "$_[0].ppm"; $canvas->coords($pixmaps{$pix}, $nx, $ny); } So do_move might make a call such as this to make the neko run left: frame 'left' . (($state_count % 2) + 1); Of course, in the actual program, the direction isn't a hardcoded string but the back-reference $1 from the state table's regular expression match. To make the neko follow the cursor, we use Tk's pointerxy command to get the cursor's coordinates, compute a heading from the neko to the cursor, and then map that value to a new state. $r2d is the radian-to-degree conversion factor, and $h is the new heading, in degrees. ($x, $y) = $canvas->pointerxy; my $h = int( $r2d * atan2( ($y - $ny), ($x - $nx) ) ) % 360; my($degrees, $dir); foreach ( [[ 22.5, 67.5], 'DWRIGHT'], [[ 67.5, 112.5], 'DOWN'], [[112.5, 157.5], 'DWLEFT'], [[157.5, 202.5], 'LEFT'], [[202.5, 247.5], 'UPLEFT'], [[247.5, 292.5], 'UP'], [[292.5, 337.5], 'UPRIGHT'], [[337.5, 22.5], 'RIGHT'], ) { ($degrees, $dir) = ($_->[0], $_->[1]); last if $h >= $degrees->[0] and $h < $degrees->[1]; } # forend set_state $dir; And that's really all there is to it. As you'd expect, there are many tiny details we've ignored, so the entire program is available at the O'Reilly web site. Copyright © 2002 O'Reilly & Associates. All rights reserved. |
|