NAME

SDLx::Controller - Handles the loops for events, movement and rendering

CATEGORY

Extension, Controller

SYNOPSIS

use SDLx::Controller;

# create our controller object
my $app = SDLx::Controller->new;

# but we usually do:
my $app = SDLx::App->new;
# because App is also a controller

# register some callbacks
$app->add_event_handler( \&on_event );
$app->add_move_handler( \&on_move );
$app->add_show_handler( \&on_show );

# run our game loop
$app->run;

DESCRIPTION

The core of an SDL application/game is the main loop, where you handle events and display your elements on the screen until something signals the end of the program. This usually goes in the form of:

while (1) {
    ...
}

The problem most developers face, besides the repetitive work, is ensuring that the screen update is independent of the frame rate. Otherwise, your game will run at different speeds on different machines, which is never a good thing (old MS-DOS games, anyone?).

One way to circumvent this is by capping the frame rate so it's the same no matter what, but this is not the right way to do it as it penalizes better hardware.

This module provides an industry-proven standard for frame independent movement. It calls the movement handlers based on time (hi-res seconds) rather than frame rate. You can add/remove these handlers and control your main loop with ease. This module also provides methods for your other timing needs, such as pausing the game.

METHODS

new

SDLx::Controller->new(
    dt             => 0.05,
    min_t          => 0,
    max_t          => 1e9999,
    delay          => 1 / 200,
    event_handlers => [ @event_callbacks ],
    move_handlers  => [ @move_callbacks ],
    show_handlers  => [ @show_callbacks ],
    stop_handler   => \&stop_handler,
    event          => $event,
    time           => 99,
);

Creates and returns a new controller object with the specified params. All params are optional and have sane defaults.

dt

The time, in seconds, of a full movement step. Defaults to 0.1. In most cases, the dt could be set to any number and the game could run almost identically (except for floating-point precision differences). This is because the dt is an enforcement of the maximum time between calling movement handlers. The actual time between calling move handlers may be much less, as the movement handlers are called at least once per frame. The specifics of this are explained in "add_move_handler".

Usually you wouldn't need this value to be lower than the time it takes an average computer to complete a cycle and render the frame. When you do need to run multiple move handlers per frame though, such as if you were checking collision between fast-moving objects, you can set the dt to some low value (less than 1/60). Otherwise, leaving it at 0.1 is fine.

Regardless of whether you need to enforce a maximum time between move handlers, this system has its benefits. Modifying the dt without touching any of the other code in your program will result in a time-scaling effect. If you lower the dt, everything will move faster than it did with it set higher, and vice-versa. This is useful to add slo-mo and fast-forward features to the game. All you would have to do is change the dt.

min_t

The minimum time, in seconds, that has to accumulate before any move or show handlers are called. Defaults to 1 / 60. A min_t of 1 / 60 ensures that the controller can update the screen at a maximum of 60 times per second. A "V-Sync" such as this is necessary to prevent video "tear", which occurs when the app is updating faster than the monitor can display. Setting it to 0, as seen above, will not delay the loop at all.

max_t

The maximum time, in seconds, that the movement handlers are allowed to handle in a single cycle of the run loop. If more time has passed since the last cycle, this time will not be handled and the game will slow down. This is to protect against the slippery slope effect that happens when lag causes more movement handlers to be called which, in turn, causes more lag. Setting max_t to the same value as dt guarantees that movement handlers will each only be called once per frame. Defaults to 0.1, meaning the game will slow down if it is running at lower than 10 FPS. Setting it to 1e9999 (or infinity), as seen above, will disable this action.

delay

The time, in seconds or milliseconds, to delay after every full "run" loop. Defaults to 0. If you specify a number greater than or equal to 1, it will be treated as milliseconds instead of seconds. Note: Picking an appropriate delay based on the needs of your game can greatly reduce CPU load.

event_handlers
move_handlers
show_handlers

An array ref of the corresponding handler callbacks. All default to []. This is basically a shortcut way of adding handlers. They would otherwise be added with their corresponding add_*_handler method. See below for a full explanation of the "run" loop and handlers.

stop_handler

An extra, but separate, event callback to handle all stopping of the app. It is the same in almost every aspect to an event handler (see "add_event_handler"): same received arguments, called in the same place.

One difference is that it is called in "pause" so that the app can be stopped while paused. Another difference is that it should always apply to the app; while you add, remove and clear handlers, it won't be touched. This is good, because you'd (probably) always want your app to able to be stopped. Because of this, it's a good idea to use the stop handler regardless of whether you will be using "pause".

Defaults to \&SDLx::Controller::default_stop_handler: a callback that stops the event loop on an SDL_QUIT event. Specify a code ref to use a different callback to handle stopping, or a false value to not use a stop handler. If you want to provide your own stop handler, you should give it the code of the default stop handler:

my ($event, $self) = @_;
$self->stop() if $event->type == SDL_QUIT;

followed by any other code to handle events also triggering the app to stop, such as the user pressing Esc.

event

The SDL::Event object that events going to the event callbacks are polled in to. Defaults to SDL::Event->new().

time

The time, in seconds, that you want the "run" loop to say it has been going for. This has no effect on the run loop. All it will do is alter what "time" returns. See "time". Defaults to 0. You'll seldom have to set this param.

run

$app->run;

After creating and setting up your handlers (see below), call this method to enter the main loop. This loop will run until "stop" is called.

All added handlers will be called during the run loop, in this order:

1. Events
2. Movements
3. Displaying

Please refer to each handler below for full information on what they do. Note that the second argument every callback receives is the app object.

add_event_handler

my $index = $app->add_event_handler(
    sub {
        my ($event, $app) = @_;
        # handle event ...
    }
);

Adds a callback to the end of the event handler list. You can add as many subs as you need. For each SDL::Event from the user, all registered callbacks will be called in order and supplied with it. Returns the index of the added callback.

More specifically: events from the user will, one by one, be polled into the app's "event" object. This event will then be passed to all of the registered callbacks as the first argument. The second argument is the app.

Below is an example of an event handler that sets a variable to true when the left mouse button is pressed, and back to false when it is lifted.

our $click = 0;

sub on_click {
    my ($event, $app) = @_;
    my $state =
        $event->type == SDL_MOUSEBUTTONDOWN ? 1 :
        $event->type == SDL_MOUSEBUTTONUP   ? 0 : undef
    ;
    return unless defined $state; # not a mouse click

    if($event->button_button == SDL_BUTTON_LEFT) {
        $click = $state;
    }
}
$app->add_event_handler(\&on_click);

For full details on the event object passed to the event handlers, see SDL::Event. For other event related functions and a full list of the event constants, see SDL::Events.

add_move_handler

my $index = $app->add_move_handler(
    sub {
        my ($step, $app, $time) = @_;
        # handle moving ...
    }
);

Adds a callback to the end of the movement handler list. You can add as many subs as you need. All registered callbacks will be triggered in order for as many dt as have happened between calls, and once more for the remaining time less than dt. A reasonable dt for a game will usually be a number greater than the time you would ever expect to have passed between frames. This means that your movement handlers will ordinarily only be called once per frame. See the discussion of dt in "new". Returns the index of the added callback.

The first argument passed to the callbacks is the fraction of dt time that the move callback should handle. This will be 1 for a full step and less than 1 for a partial step. Inversely, the time that each move callback should handle is equal to the step argument multiplied by the dt. All movement values should be multiplied by the step value.

It is possible for the argument to be 0 if no time has passed since the last cycle. It's best to protect against this by supplying the app a small "delay" value.

The second argument passed to the callbacks is the app object.

The third is the value returned by "time". See "time".

You should use these handlers to update your in-game objects, check collisions, etc. Below is an example of how you might move an object. Note that the movement value, a velocity in this case, is multiplied by the step argument.

our $ball = MyBall->new;

sub move_ball {
    my ($step, $app, $t) = @_;
    $ball->move_x( $ball->x_vel * $step );
    $ball->move_y( $ball->y_vel * $step );
}
$app->add_move_handler(\&move_ball);

add_show_handler

my $index = $app->add_show_handler(
    sub {
        my ($delta, $app) = @_;
        # handle showing ...
    }
);

Adds a callback to the end of the rendering handler list. You can add as many subs as you need. All registered callbacks will be triggered in order, once per cycle of the "run" loop. Returns the index of the added callback.

The first argument passed is the time, in seconds, since the previous show. This can be used to display a rough FPS value by dividing 1 by it.

The second argument is the app object.

our $ball = MyBall->new;

sub show_ball {
    my ($delta, $app) = @_;

    # the drawing below works if the app is an SDLx::App
    # and not just a controller
    $app->draw_circle_filled(
        [ $ball->x, $ball->y ], $ball->radius,
        $ball->color
    );
}
$app->add_show_handler(\&show_ball);

event_handlers

move_handlers

show_handlers

my $event_handlers = $app->event_handlers;
my $move_handlers = $app->move_handlers;
my $show_handlers = $app->show_handlers;

Returns the corresponding array ref so that you can directly modify the handler list.

remove_event_handler

remove_move_handler

remove_show_handler

my $removed_handler = $app->remove_event_handler( $index );
my $removed_handler = $app->remove_event_handler( $callback );
my $removed_handler = $app->remove_move_handler( $index );
my $removed_handler = $app->remove_move_handler( $callback );
my $removed_handler = $app->remove_show_handler( $index );
my $removed_handler = $app->remove_show_handler( $callback );

Removes the handler with the given index from the respective handler list.

You can also pass a coderef. The first coderef in the handler list that matches this will be removed.

Returns the removed handler.

remove_all_event_handlers

remove_all_move_handlers

remove_all_show_handlers

$app->remove_all_event_handlers();
$app->remove_all_move_handlers();
$app->remove_all_show_handlers();

Removes all handlers from the respective handler list. None of these will remove the app's "stop_handler".

remove_all_handlers

$app->remove_all_handlers();

Shortcut to remove all handlers at once. This will not remove the app's "stop_handler".

stop

$app->stop;

Tells the controller to end the run loop. This only has meaning when called from within a handler of the run loop. The "run" loop will complete the current cycle (handling events, moves and shows) and then return. This graceful way of ending the game loop is preferred and it is the way the default "stop_handler" does it.

Once the "run" loop has been stopped, it can be started again without problems. This technique should be used to do operations that take a long time outside of the timing of the app.

$app->run;

do_something_that_takes_a_long_time();

$app->run;

This code snippet could be used to play the first part of a game, then load the next part and resume playing. When the first run loop is stopped, the expensive operation will be executed. Once that has completed, the second run loop will resume the game, ignoring the time that passed outside the run loop. If the expensive operation was performed from within the run loop, upon completing the operation the move handlers would take into account all the time passed.

stopped

my $stopped = $app->stopped;

Returns true if the run loop is stopped (before and after being in the run loop). Also returns true when the run loop is about to stop. That is, true when the app will complete the current run cycle before stopping.

pause

$app->pause(
    sub {
        my ($event, $app) = @_;
        # handle event ...

        return 1 if ... ; # unpause
        return; # stay paused
    }
);

Pauses the application with a call to SDL::Events::wait_event|SDL::Events/wait_event. This only has meaning when called from within a handler of the run loop. Events can then be used to unpause the app. This is done outside the timing of the app with the same technique as explained in "stop".

Takes one argument, which is a callback. The application completes the current run loop, then starts waiting for the next event with wait_event. This means that pause can be called by any kind of handler in the run loop. If "stop" is called during the same run cycle, before or after calling pause, the app will just stop instead of pausing.

When wait_event receives an event, it is passed to the callback as the first argument. Just like an event handler, the second argument passed is the app. If the callback then returns a true value, pause will return. If the callback returns a false value, the app will stay paused and the process will be repeated.

If a "stop_handler" is defined, then each event will also be passed to that. This will allow the app to be stopped while being paused. If the stop handler calls "stop", then the app will unpause and then stop. If your app doesn't have a stop handler, then you'll have to handle stopping yourself in the pause callback. The app will also unpause if the callback calls "stop", regardless of whether the callback then returns true or false.

Below is an example of pause used to implement a pause and unpause when the app loses and gains focus. As a neat shortcut, the callback is recursively defined and used as both an event handler and the pause callback.

sub window {
    my ($event, $app) = @_;
    if($event->type == SDL_ACTIVEEVENT) {
        if($event->active_state & SDL_APPINPUTFOCUS) {
            if($event->active_gain) { # gained focus
                return 1; # unpause
            }
            else {
                $app->pause(\&window);
                # recursive, but only once since the window
                # can't lose focus again without gaining it first
            }
        }
    }
    return;
}
$app->add_event_handler(\&window);

paused

my $paused = $app->paused;

Returns true if the run loop is currently paused. Also returns true when the run loop is about to pause. That is, true when the app will complete the current run cycle before pausing.

Both of these cases can be useful for handling pausing. Being able to tell if the app is currently paused is useful from within the "pause" callback:

sub toggle_pause { # press P to toggle pause
    my ($event, $app) = @_;
    if($event->type == SDL_KEYDOWN) {
        if($event->key_sym == SDLK_p) {
            # We're paused, so end pause
            return 1 if $app->paused;

            # We're not paused, so pause
            $app->pause(\&toggle_pause);
        }
    }
    return;
}
$app->add_event_handler(\&toggle_pause);

Knowing if the app is about to pause is useful from within the app's handlers. If "pause" is called from within an event handler, then the move and show handlers can check and respond to "paused" in the remainder of the run cycle. In this time, both paused and "stopped" will return true. When the app is actually paused, only paused will return true. This true value is actually the callback you passed with "pause", and can be checked to tell between different pauses. For example, a show handler could make the screen say PAUSED when "paused" is true. This is preferred to having the event handler display this, because "stop" could be called in the meantime.

dt

min_t

delay

stop_handler

event

my $dt           = $app->dt;
my $min_t        = $app->min_t;
my $max_t        = $app->max_t;
my $delay        = $app->delay;
my $stop_handler = $app->stop_handler;
my $event        = $app->event;
$app->dt          ($dt);
$app->min_t       ($min_t);
$app->max_t       ($max_t);
$app->delay       ($delay);
$app->stop_handler($stop_handler);
$app->event       ($event);

If an argument is passed, modifies the corresponding parameter to the argument. For each of these parameters the "run" loop will give the handlers its old value until the start of the next cycle. The "pause" callback, however, will always receive the new value. See "new" for details on what these params do.

Returns the corresponding value.

time

my $time = $app->time;
$app->time($time);

Returns the sum of all the dts that have been handled by all move handlers. In other words, the total amount of time that has passed in the run loop. When the run loop is stopped and resumed, this value is not reset. This should be a useful value to have, but isn't a replacement for SDL::get_ticks. Use Time::HiRes::time instead of get_ticks.

Specify a value to count from that time. This will have no effect on the run loop itself, but may be useful for the code in your handlers.

sleep

$app->sleep($time);

Causes the app to sleep for the specified time, in hi-res seconds, or forever if no argument is specified. Use this instead of SDL::delay.

AUTHORS

See "AUTHORS" in SDL.

ACKNOWLEDGEMENTS

The idea and base for the "run" loop comes from Lazy Foo's Frame Independent Movement tutorial, and Glenn Fiedler's Fix Your Timestep article on timing.

SEE ALSO

SDLx::App, SDL::Event, SDLx::Surface