31 Mar 2010 ruoso   » (Journeyer)

Writing Games in Perl - Part 4 - Implementing a Camera

Following posts 1, 2 and 3 on the subject of writing games in Perl, now we are going to add a camera.

Up to this point, we've been coupling the positional information in our simulated universe with the screen position. This is very easy to do, but very limiting as well, how can we represent off-screen elements that way?

Our next step is to implement a camera. The idea is quite simple, instead of asking for each model object to render itself, We're going to:

  1. Initialize a view object for each of the model objects.
  2. Detect which model objects are in the current view sight of the camera.
  3. Send to the view objects the positional information of the model objects.
  4. Ask the view objects to draw themselves.

At this point you might have noticed that I'm using the terms "model" and "view" as a direct reference to the Model-View-Controller architecture, and that's precisely my intention. The basic idea is: The model is only responsible for managing the simulation of our universe, while the view is only responsible for turning that simulation visible to the user. The Controller here is the code that implements the main loop, receiving the user events, and coordinating the FPS management.

I could list several reasons on why having the model and the view as separated objects is a good idea, but I'd like two point just two of them, since these are going to be future topics in this tutorial:

  1. You can apply "themes" to the visualization, like "hi-res" and "low-res" simply by changing the initialization of the view, adding support for zoom and rotation is also very simple.
  2. You can have the calculations on the simulation side in a different thread then the rendering of the objects, enabling our game to take advantage on multi-core systems.

Code Layout

The first thing I'm going to do is reorganize our current module layout. Up to this point our code had:

  • ball.pl
  • lib/Ball.pm
  • lib/Wall.pm
  • lib/Util.pm

But now we're going to need a different layout, here is our target organization:

ball.pl
This is still going to be our main script, but we're going to have less code in it.
lib/BouncingBall/Model/Ball.pm
Yes, I named our game BouncingBall, and the first model class is the ball itself, it will look much like the current code, but the "get_rect" and the "draw" methods won't be there.
lib/BouncingBall/Model/Wall.pm
This looks like our current Wall code, but as with the ball, "get_rect" and "draw" won't be there.
lib/BouncingBall/Controller/InGame.pm
At this point we're only going to have one controller, but the general idea is that we're going to have one controller for each of the main states of the game, like "MainMenu", "Paused", "InGame", "GameOver" etc. The InGame controller will implement the code that is currently inside the main loop of ball.pl
lib/BouncingBall/View.pm
This defines the types for the things that implement draw.
lib/BouncingBall/View/Plane.pm
This implements the background.
lib/BouncingBall/View/FilledRect.pm
Currently, our ball and our wall are just filled rectangles, so we're going to preserve that for now. This is interesting to make the view vs model point even more clear. The view doesn't need to be aware of what it is representing, as long as it knows how to do it. In our case, it doesn't need to know if it is representing a Ball or a Wall, it simply needs to know where it is and what color to paint.
lib/BouncingBall/View/MainSurface.pm
This class represents the main application surface, it is special because it needs to configure the video mode, but it is also a dependency for the FilledRect view, since it needs to blit itself somewhere.
lib/BouncingBall/View/Camera.pm
This is the view class that will implement the mapping of coordinates from the simulated universe to the MainSurface, the FilledRect also depends on this class.
lib/BouncingBall/Event/RectMoved.pm
Typed event that describes the movement of some object represented by its enclosing rect.
lib/BouncingBall/Event/Rect.pm
Object describing a simple rect (using floating-point instead of integer), to be used by RectMoved.
lib/BouncingBall/Event/MovingRectObservable.pm
Moose role that implements the logic for being a class that can be observed.
lib/BouncingBall/Event/MovingRectObserver.pm
Moose role that defines the type of the observer class.

General flow

  1. ball.pl initializes the BouncingBall module.
  2. BouncingBall initializes the MainSurface view, as this view is special and is preserved during the entire application lifetime, independent of the controller in charge right now.
  3. As we don't implement game menu or any other fancy stuff, we go directly to the game. That means we'll initialize the InGame Controller.
  4. The connection between the views and the models is defined by the controller, so it needs to initialize the models, the views and connect them together.
  5. After the initialization, we're ready for the game loop, which is, at this point, entirely handled by the InGame controller.

The case for Observers

A naive implementation of the connection between the models and the views would be, at each step, to fetch the relevant information from the model and set it into the view. Or possibly have the view itself fetch the data from the model. But there's one important point to consider, if the model and the view ends in a different thread, accessing each other's data would become significantly complicated.

That being said, a different mechanism for view-model integration is necessary. If we go to the way GUI toolkits work, we'll notice a pattern called "The Observer Pattern", basically, one object registers itself as "observing" the other. When that specific type of event happens, a pre-defined method is called in the observing object.

So what we're going to do is to preserve a local cache of the information that view needs from the model, use it directly and get it updated using the observer pattern. That way we have the model and the view decoupled in terms of threading.

To the code

The first thing we need to do is porting our Ball and Wall into proper model objects, at first, simply removing the "get_rect" and "draw" methods. I'm not going to put all its code again here, but renaming the modules and removing that methods is the only thing I'm doing right now. The time_lapse is also simplfied to remove the automatic bounce, we're going to put extra walls to enclose the ball.

Now we need to step-by-step get to the final code layout presented earlier, let's start by the view classes, and in that case, let's get the most important view class, the MainSurface.

package BouncingBall::View::MainSurface;
use Moose;
use SDL ':all';
use SDL::Video;# ':all';

has 'surface' => (is => 'rw');
has 'width' => (is => 'ro', default => 800);
has 'height' => (is => 'ro', default => 600);
has 'depth' => (is => 'ro', default => 32);

sub BUILD {
    my ($self) = @_;

    die 'Cannot init ' . SDL::get_error()
      if ( SDL::init( SDL_INIT_VIDEO ) == -1 );

    $self->surface
      ( SDL::Video::set_video_mode
        ( $self->width, $self->height, $self->depth,
          SDL_HWSURFACE | SDL_DOUBLEBUF | SDL_HWACCEL ))
        or die 'Error initializing video.';

    return 1;
}

Here we use Moose object initialization to initialize the SDL Video subsystem and get a new surface for the required videomode. This is a bit different then what we were doing in the original ball.pl, but not much. The most important difference is that we're now using double hardware buffering, which is more efficient then doing individual updates.

Now we need the Plane view, which implements the background.

package BouncingBall::View::Plane;
use Moose;
use SDL::Video ':all';
use SDL::Surface;

with 'BouncingBall::View';
has color     => ( is => 'rw',
                   default => 0 );
has surface   => ( is => 'rw' );
has rect_obj  => ( is => 'rw' );
has color_obj => ( is => 'rw' );
has main      => ( is => 'ro',
                   required => 1 );

sub BUILD {
    my ($self) = @_;
    $self->_init_surface;
    $self->_init_color_object;
    $self->_init_rect;
    $self->_fill_rect;
    return 1;
}

sub _init_surface {
    my ($self) = @_;
    $self->surface
      ( SDL::Surface->new
        ( SDL_SWSURFACE,
          $self->main->width,
          $self->main->height,
          $self->main->depth,
          0, 0, 0, 255 ) );
    return 1;
}

sub _init_color_object {
    my ($self) = @_;
    $self->color_obj
      ( SDL::Video::map_RGB
        ( $self->main->surface->format,
          ((0xFF0000 & $self->color)>>16),
          ((0x00FF00 & $self->color)>>8),
          0x0000FF & $self->color ));
    return 1;
}

sub _init_rect {
    my ($self) = @_;
    $self->rect_obj
      ( SDL::Rect->new
        ( 0, 0,
          $self->main->width,
          $self->main->height ) );
    return 1;
}

sub _fill_rect {
    my ($self) = @_;
    SDL::Video::fill_rect
        ( $self->surface,
          $self->rect_obj,
          $self->color_obj );
    return 1;
}

after 'color' => sub {
    my ($self, $color) = @_;
    if ($color) {
        $self->_init_color_object;
        $self->_fill_rect;
    }
    return 1;
};

sub draw {
    my ($self) = @_;
    SDL::Video::blit_surface
        ( $self->surface, $self->rect_obj,
          $self->main->surface, $self->rect_obj );
    return 1;
}

Now we need the Camera view, which is going to implement the logic on translating the coordinates from each object to the current visualization. This is a very important decoupling in our logic, because it is here that we relativize the points from the simulated universe to the screen. And this is going to be done by setting a camera position which is going to serve as pivot in the coordinate translation.

This is going to be implemented through three methods in our camera, one that converts a distance in meters to pixels, other that converts a coordinate to the screen and finally one that checks if a coordinate is visible at that moment.

package BouncingBall::View::Camera;
use Moose;

with 'BouncingBall::Event::RectMovingObserver';

has pointing_x => ( is => 'rw',
                    default => 0 );
has pointing_y => ( is => 'rw',
                    default => 0 );
has dpi        => ( is => 'rw',
                    default => 0.96 );
has pixels_w   => ( is => 'ro',
                    required => 1 );
has pixels_h   => ( is => 'ro',
                    required => 1 );

sub m2px {
    my ($self, $input) = @_;
    return int((($input) * ($self->dpi / 0.0254)) + 0.5);
}

sub px2m {
    my ($self, $input) = @_;
    return ($input) / ($self->dpi / .0254);
}

sub width {
    my ($self) = @_;
    return $self->px2m($self->pixels_w);
}

sub height {
    my ($self) = @_;
    return $self->px2m($self->pixels_h);
}

sub translate_point {
    my ($self, $x, $y) = @_;
    my $uplf_x = $self->pointing_x - ($self->width / 2);
    my $uplf_y = $self->pointing_y - ($self->height / 2);
    my $rel_x = $x - $uplf_x;
    my $rel_y = $y - $uplf_y;
    my $pix_x = $self->m2px($rel_x);
    my $pix_y = $self->m2px($rel_y);
    my $inv_y = $self->pixels_h - $pix_y;
    return ($pix_x, $inv_y);
}

sub translate_rect {
    my ($self, $x, $y, $w, $h) = @_;
    my ($pix_x, $inv_y) = $self->translate_point($x, $y);
    my $pix_h = $self->m2px($h);
    my $pix_w = $self->m2px($w);
    return ($pix_x, $inv_y - $pix_h, $pix_w, $pix_h);
}

sub is_visible {
    my ($self, $x, $y) = @_;
    my ($tx, $ty) = $self->translate($x, $y);
    if ($tx > 0 && $ty > 0 &&
        $tx pixels_w &&
        $ty pixels_h) {
        return 1;
    } else {
        return 0;
    }
}

The Camera requires the information from the MainSurface on the ammount of pixels it has to be able to do the translations.

Now we need to implement the FilledRect view class.

package BouncingBall::View::FilledRect;
use Moose;
use SDL::Video ':all';
use SDL::Surface;

with 'BouncingBall::View';
with 'BouncingBall::Event::RectMovingObserver';

has x         => ( is => 'rw',
                   default => 0 );
has y         => ( is => 'rw',
                   default => 0 );
has w         => ( is => 'rw',
                   default => 0 );
has h         => ( is => 'rw',
                   default => 0 );
has color     => ( is => 'rw',
                   default => 0 );
has rect_obj  => ( is => 'rw' );
has surface   => ( is => 'rw' );
has color_obj => ( is => 'rw' );
has camera    => ( is => 'rw',
                   required => 1 );
has main      => ( is => 'rw',
                   required => 1 );

sub BUILD {
    my ($self) = @_;
    $self->_init_surface;
    $self->_init_color_object;
    $self->_init_rect;
    $self->_fill_rect;
    return 1;
}

sub _init_surface {
    my ($self) = @_;
    $self->surface
      ( SDL::Surface->new
        ( SDL_SWSURFACE,
          $self->camera->m2px($self->w),
          $self->camera->m2px($self->h),
          $self->main->depth,
          0, 0, 0, 255 ) );
    return 1;
}

sub _init_color_object {
    my ($self) = @_;
    $self->color_obj
      ( SDL::Video::map_RGB
        ( $self->main->surface->format,
          ((0xFF0000 & $self->color)>>16),
          ((0x00FF00 & $self->color)>>8),
          0x0000FF & $self->color ));
    return 1;
}

sub _init_rect {
    my ($self) = @_;
    $self->rect_obj
      ( SDL::Rect->new
        ( 0, 0,
          $self->camera->m2px($self->w),
          $self->camera->m2px($self->h) ) );
    return 1;
}

sub _fill_rect {
    my ($self) = @_;
    SDL::Video::fill_rect
        ( $self->surface(),
          $self->rect_obj(),
          $self->color_obj() );
    return 1;
}

after 'color' => sub {
    my ($self, $color) = @_;
    if ($color) {
        $self->_init_color_object;
        $self->_fill_rect;
    }
    return 1;
};

after qw(w h) => sub {
    my ($self, $newval) = @_;
    if ($newval) {
        $self->_init_surface;
        $self->_init_rect;
        $self->_fill_rect;
    }
    return 1;
};

sub draw {
    my ($self) = @_;
    my $rect = SDL::Rect->new
      ( $self->camera->translate_rect( $self->x, $self->y,
                                       $self->w, $self->h ) );

    SDL::Video::blit_surface
        ( $self->surface, $self->rect_obj,
          $self->main->surface, $rect );

    return 1;
}

Ok, now that we have the Model and the View classes, we can implement the observer pattern, so the view can be updated as the model changes.

One important aspect on how the MVC model works is that the controller should have just a limited control on the interaction between the model and the view, otherwise you'll get a very complicated code in the controller. Ideally you should have the same level of abstraction in the model as you have in the view, so you have componentization of your application.

That being said, we need to plan the communication pattern between the view and the model. It is important that they should be mostly unaware of each other, in the sense that the view doesn't need to know that it's a ball being modelled, but just that it has a point describing its position and a rect describing its measures - We can even keep only the rect for our current purposes.

That is our RectMoved event class and the Rect class which is used by it:

package BouncingBall::Event::RectMoved;
use Moose;

has old_rect => ( is => 'ro',
                  isa => 'BouncingBall::Event::Rect',
                  required => 0 );
has new_rect => ( is => 'ro',
                  isa => 'BouncingBall::Event::Rect',
                  required => 1 );

The rect class is implemented here because SDL::Rect expects integers as its members, and we don't want to loose the precision.

package BouncingBall::Event::Rect;
use Moose;

has x => ( is => 'ro',
           isa => 'Num',
           required => 1 );
has y => ( is => 'ro',
           isa => 'Num',
           required => 1 );
has w => ( is => 'ro',
           isa => 'Num',
           required => 1 );
has h => ( is => 'ro',
           isa => 'Num',
           required => 1 );

Now we need the role that implements the observable part, meaning that any class that wants to fire events for moving rects just need to compose that role and call the fire_rect_moved method.

package BouncingBall::Event::RectMovingObservable;
use Moose::Role;
use MooseX::Types::Moose qw(ArrayRef);

use aliased 'BouncingBall::Event::RectMovingObserver';
use aliased 'BouncingBall::Event::RectMoved';

has 'rect_moving_listeners' => ( traits => ['Array'],
                                 is => 'ro',
                                 isa => ArrayRef[RectMovingObserver],
                                 default => sub { [] },
                                 handles => { add_rect_moving_listener => 'push' } );

sub remove_rect_moving_listener {
    my ($self, $object) = @_;
    my $count = $self->rect_moving_listeners->count;
    my $found;
    for my $i (0..($count-1)) {
        if ($self->rect_moving_listeners->[$i] == $object) {
            $found = $i;
            last;
        }
    }
    if ($found) {
        splice @{$self->rect_moving_listeners}, $found, 1, ();
    }
}

sub fire_rect_moved {
    my ($self, $old_rect, $new_rect) = @_;
    my %args = ( new_rect => $new_rect );
    $args{old_rect} = $old_rect if $old_rect;
    my $ev = RectMoved->new(\%args);
    $_->rect_moved($ev) for @{$self->rect_moving_listeners};
}

And the RectMovingObserver role:

package BouncingBall::Event::RectMovingObserver;
use Moose::Role;

requires 'rect_moved';

Now we need to make our Ball model class fire that event whenever its position or size attributes are changed. So we're goint to add the following modifiers to the attributes. At first we're not going to support the old_rect attribute of the event, so we're just sending the new_rect.

with 'BouncingBall::Event::RectMovingObservable';

after qw(cen_v cen_h) => sub {
    my ($self, $newval) = @_;
    if ($newval) {
        $self->fire_rect_moved( undef,
                                Rect->new({ x => $self->pos_h,
                                            y => $self->pos_v,
                                            w => $self->width,
                                            h => $self->height }) );
    }
}

And finally adding the observer code in the view class.

with 'BouncingBall::Event::RectMovingObserver';

sub rect_moved {
    my ($self, $ev) = @_;
    $self->$_($ev->$_()) for grep { $self->$_() != $ev->$_() } qw(x y w h);
}

Everything's set! To the controller!

Now we finally can have the controller code written. It should:

  1. Initialize the models
  2. Initialize the views
  3. Connect them together
  4. Orchestrate the time_lapse and the general rendering

So, in the end our controller looks like the following:

package BouncingBall::Controller::InGame;
use Moose;
use MooseX::Types::Moose qw(ArrayRef);

use SDL::Events ':all';
use aliased 'BouncingBall::Model::Ball';
use aliased 'BouncingBall::Model::Wall';
use aliased 'BouncingBall::View';
use aliased 'BouncingBall::View::Plane';
use aliased 'BouncingBall::View::FilledRect';
use aliased 'BouncingBall::View::Camera';
use aliased 'BouncingBall::View::MainSurface';
use aliased 'BouncingBall::Event::Rect';

has 'ball' => ( is => 'rw',
                isa => Ball,
                default => sub { Ball->new() } );

has 'main_surface' => ( is => 'ro',
                        isa => MainSurface,
                        required => 1 );

has 'camera' => ( is => 'ro',
                  isa => Camera );

has 'walls' => ( traits => ['Array'],
                 is => 'rw',
                 isa => ArrayRef[Wall] );
has 'views' => ( traits => ['Array'],
                 is => 'rw',
                 isa => ArrayRef[View] );


sub BUILD {
    my $self = shift;

    my $background = Plane->new({ main => $self->main_surface,
                                  color => 0xFFFFFF });

    my $camera = Camera->new({ pixels_w => $self->main_surface->width,
                               pixels_h => $self->main_surface->height,
                               pointing_x => $self->ball->cen_h,
                               pointing_y => $self->ball->cen_v });

    my $ball_view = FilledRect->new({ color => 0x0000FF,
                                      camera => $camera,
                                      main => $self->main_surface,
                                      x => $self->ball->pos_h,
                                      y => $self->ball->pos_v,
                                      w => $self->ball->width,
                                      h => $self->ball->height });
    $self->ball->add_rect_moving_listener($ball_view);

    $self->views([]);
    push @{$self->views}, $background, $ball_view;

    $self->walls([]);

    # now we need to build four walls, to enclose our ball.
    foreach my $rect ( Rect->new({ x => 0,
                                   y => 0,
                                   w => 20,
                                   h => 1 }),
                       Rect->new({ x => 0,
                                   y => 0,
                                   h => 20,
                                   w => 1 }),
                       Rect->new({ x => 20,
                                   y => 0,
                                   h => 20,
                                   w => 1 }),
                       Rect->new({ x => 0,
                                   y => 20,
                                   w => 21,
                                   h => 1 }),
                       Rect->new({ x => 10,
                                   y => 0,
                                   h => 10,
                                   w => 1 })) {

        my $wall_model = Wall->new({ pos_v => $rect->y,
                                     pos_h => $rect->x,
                                     width => $rect->w,
                                     height => $rect->h });

        push @{$self->walls}, $wall_model;

        my $wall_view = FilledRect->new({ color => 0xFF0000,
                                          camera => $camera,
                                          main => $self->main_surface,
                                          x => $rect->x,
                                          y => $rect->y,
                                          w => $rect->w,
                                          h => $rect->h });

        push @{$self->views}, $wall_view;

    }

}

sub handle_sdl_event {
    my ($self, $sevent) = @_;

    my $ball = $self->ball;
    my $type = $sevent->type;

    if ($type == SDL_KEYDOWN &&
        $sevent->key_sym() == SDLK_LEFT) {
        $ball->acc_h(-5);

    } elsif ($type == SDL_KEYUP &&
             $sevent->key_sym() == SDLK_LEFT) {
        $ball->acc_h(0);

    } elsif ($type == SDL_KEYDOWN &&
             $sevent->key_sym() == SDLK_RIGHT) {
        $ball->acc_h(5);

    } elsif ($type == SDL_KEYUP &&
             $sevent->key_sym() == SDLK_RIGHT) {
        $ball->acc_h(0);

    } elsif ($type == SDL_KEYDOWN &&
             $sevent->key_sym() == SDLK_UP) {
        $ball->acc_v(5);

    } elsif ($type == SDL_KEYUP &&
             $sevent->key_sym() == SDLK_UP) {
        $ball->acc_v(0);

    } elsif ($type == SDL_KEYDOWN &&
             $sevent->key_sym() == SDLK_DOWN) {
        $ball->acc_v(-5);

    } elsif ($type == SDL_KEYUP &&
             $sevent->key_sym() == SDLK_DOWN) {
        $ball->acc_v(0);

    } else {
        return 0;
    }
    return 1;
}

sub handle_frame {
    my ($self, $oldtime, $now) = @_;

    my $frame_elapsed_time = ($now - $oldtime)/1000;

    my $ball = $self->ball;
    my $collided = 0;
    foreach my $wall (@{$self->walls}) {
        if (my $coll = collide($ball, $wall, $frame_elapsed_time)) {
            # need to place the ball in the result after the bounce given
            # the time elapsed after the collision.
            my $collision_remaining_time = $frame_elapsed_time - $coll->time;
            my $movement_before_collision_h = $ball->vel_h * $coll->time;
            my $movement_before_collision_v = $ball->vel_v * $coll->time;
            my $movement_after_collision_h = $ball->vel_h * $collision_remaining_time;
            my $movement_after_collision_v = $ball->vel_v * $collision_remaining_time;
            if ($coll->axis eq 'x') {
                $ball->cen_h(($ball->cen_h + $movement_before_collision_h) +
                             ($movement_after_collision_h * -1));
                $ball->cen_v($ball->cen_v +
                             $movement_before_collision_v +
                             $movement_after_collision_v);
                $ball->vel_h($ball->vel_h * -1);
            } elsif ($coll->axis eq 'y') {
                $ball->cen_v(($ball->cen_v + $movement_before_collision_v) +
                             ($movement_after_collision_v * -1));
                $ball->cen_h($ball->cen_h +
                             $movement_before_collision_h +
                             $movement_after_collision_h);
                $ball->vel_v($ball->vel_v * -1);
            } elsif (ref $coll->axis eq 'ARRAY') {
                my ($xv, $yv) = @{$coll->bounce_vector};
                $ball->cen_h(($ball->cen_h + $movement_before_collision_h) +
                             ($xv * $collision_remaining_time));
                $ball->vel_h($xv);
                $ball->cen_v(($ball->cen_v + $movement_before_collision_v) +
                             ($yv * $collision_remaining_time));
                $ball->vel_v($yv);
            } else {
                warn 'BAD BALL!';
                $ball->cen_h(($ball->cen_h + $movement_before_collision_h) +
                             ($movement_after_collision_h * -1));
                $ball->cen_v(($ball->cen_v + $movement_before_collision_v) +
                             ($movement_after_collision_v * -1));
                $ball->vel_h($ball->vel_h * -1);
                $ball->vel_v($ball->vel_v * -1);
            }
            $collided = 1;
        }
    }

    if (!$collided) {
        $ball->time_lapse($oldtime, $now);
    }

    foreach my $view (@{$self->views}) {
        my $ret = $view->draw();
    }

    SDL::Video::flip($self->main_surface->surface);

}

use Collision::2D ':all';
sub collide {
    my ($ball, $wall, $time) = @_;
    my $rect = hash2rect({ x => $wall->pos_h, y => $wall->pos_v,
                           h => $wall->height, w => $wall->width });
    my $circ = hash2circle({ x => $ball->cen_h, y => $ball->cen_v,
                             radius => $ball->radius,
                             xv => $ball->vel_h,
                             yv => $ball->vel_v });
    return dynamic_collision($circ, $rect, interval => $time);
}

And with all the reorganization, the main ball.pl code is now pretty simple:

#!/usr/bin/perl

use 5.10.0;
use strict;
use warnings;

use SDL;
use SDL::Event;
use SDL::Events;

use lib 'lib';

use aliased 'BouncingBall::Controller::InGame';
use aliased 'BouncingBall::View::MainSurface';

SDL::init( SDL_INIT_EVERYTHING );

my $fps = 60;

my $surf = MainSurface->new();

my $sevent = SDL::Event->new();
my $time = SDL::get_ticks;

my $controller = InGame->new({ main_surface => $surf });

while (1) {
    my $oldtime = $time;
    my $now = SDL::get_ticks;

    while (SDL::Events::poll_event($sevent)) {
        my $type = $sevent->type;
        if ($type == SDL_QUIT) {
            exit;
        } elsif ($controller->handle_sdl_event($sevent)) {
            # handled.
        } else {
            # unknown event.
        }
    }

    $controller->handle_frame($time, $now);

    $time = SDL::get_ticks;
    SDL::delay(1000/$fps);
}

The case for the Camera!

We started this post around the idea of making a camera, but we haven't done anything really interesting with it. So now I'm going to implement the really usefull part of all this thing we've done.

Currently the camera is being set as looking at the point the ball starts, but soon enough, the ball is going to get too close to the bottom border of the screen.

The idea is pretty simple, try to keep the ball inside a threshold margin by moving the camera when it gets too close of the border.

What makes this really simple is the fact that we just need to do two things:

  1. Add the camera as an observer of the ball.
  2. Implement the chasing logic in the rect_moved method

And that's all. Really. So here are all the code we need to make the camera chase the ball:

# in the controller, we just need to add one line, after the camera
# initialization.
$self->ball->add_rect_moving_listener($camera);

And then we need to implement rect_moved on the camera:

sub rect_moved {
    my ($self, $ev) = @_;
    # implement a loose following of the ball.  if the ball gets near
    # the border of the screen, we follow it so it stays inside the
    # desired area.

    my $lf_x = $self->pointing_x - ($self->width / 2);
    my $br_lf_x = $lf_x + $self->width * 0.2;

    my $rt_x = $self->pointing_x + ($self->width / 2);
    my $br_rt_x = $rt_x - $self->width * 0.2;

    my $up_y = $self->pointing_y + ($self->height / 2);
    my $br_up_y = $up_y - $self->height * 0.2;

    my $dw_y = $self->pointing_y - ($self->height / 2);
    my $br_dw_y = $dw_y + $self->height * 0.2;

    if ($ev->new_rect->x pointing_x( $self->pointing_x - ($br_lf_x - $ev->new_rect->x))
    } elsif ($ev->new_rect->x > $br_rt_x) {
        $self->pointing_x( $self->pointing_x + ($ev->new_rect->x - $br_rt_x));
    }

    if ($ev->new_rect->y pointing_y( $self->pointing_y - ($br_dw_y - $ev->new_rect->y))
    } elsif ($ev->new_rect->y > $br_up_y) {
        $self->pointing_y( $self->pointing_y + ($ev->new_rect->y - $br_up_y));
    }

    return 1;
}

And that's all. That's the joy of a properly designed MVC architecture.

As usual, follows a small video of the game.

Syndicated 2010-03-30 23:19:05 from Daniel Ruoso

Latest blog entries     Older blog entries

New Advogato Features

New HTML Parser: The long-awaited libxml2 based HTML parser code is live. It needs further work but already handles most markup better than the original parser.

Keep up with the latest Advogato features by reading the Advogato status blog.

If you're a C programmer with some spare time, take a look at the mod_virgule project page and help us with one of the tasks on the ToDo list!