NAME
SDL::App::FPS - a framework for event-driven SDL games/applications
SYNOPSIS
Subclass SDL::App::FPS and override some methods:
package SDL::App::MyFPS;
use strict;
use SDL::App::FPS;
use SDL;
use base qw/SDL::App::FPS/;
# override the method draw_frame with something to draw
sub draw_frame
{
my ($self,$current_time,$lastframe_time,$current_fps) = @_;
...
}
# override post_init_handler and add some event handlers
sub post_init_handler
{
my ($self} = shift;
my $self->add_event_handler(SDL_KEYDOWN, SDLK_q, sub
{
my $self = shift; $self->quit();
} );
# or easier for often-used events (note quoted 'SDLK_f'!)
$self->watch_event( fullscreen => 'SDLK_f', pause => 'p',
quit => 'SDLK_q',
);
# You can also specify the key/mousebutton bindings for these events
# in the config file like "bind_event_fullscreen = f"
}
Then write a small script using SDL::App::MyFPS like this:
#!/usr/bin/perl -w
use strict;
use SDL::App::MyFPS;
# fill in here default options if you like
my $options = { };
# create a new application including window
# automatically uses a config file or the command line:
my $app = SDL::App::MyFPS->new( $options );
# run the application, will exit when done
$app->main_loop();
That's all!
EXPORTS
Some symbols on request, namely:
BUTTON_MOUSE_LEFT
BUTTON_MOUSE_RIGHT
BUTTON_MOUSE_MIDDLE
BUTTON_MOUSE_WHEEL_DOWN
BUTTON_MOUSE_WHEEL_UP
DESCRIPTION
This package provides you with a base class to write your own SDL Perl applications.
The Why
When building a game or screensaver displaying some continously running animation, a couple of basics need to be done to get a smooth animation and to care of copying with varying speeds of the system. Ideally, the animation displayed should be always the same, no matter how fast the system is.
This not only includes different systems (a PS/2 for instance would be slower than a 3 Ghz PC system), but also changes in the speed of the system over time, for instance when a background process uses some CPU time or the complexity of the scene changes.
In many old (especial DOS) games, like the famous Wing Commander series, the animation would be drawn simple as fast as the system could, meaning that if you would try to play such a game on a modern machine it we end before you had the chance to click a button, simple because it wizzed a couple 10,000 frames per second past your screen.
While it is quite simple to restrict the maximum framerate possible, care must be taken to not just "burn" surplus CPU cycles. Instead the application should free the CPU whenever possible and give other applications/thread a chance to run. This is especially important for low-priority applications like screensavers.
SDL::App::FPS
makes this possible for you without you needing to worry about how this is done. It will restrict the frame rate to a possible maximum and tries to achive the average framerate as close as possible to this maximum.
SDL::App::FPS
also monitors the average framerate and gives you access to this value, so that you can, for instance, adjust the scene complexity based on the current framerate. You can access the current framerate, averaged over the last second (1000 ms) by calling current_fps.
Frame-rate Independend Clock
Now that our application is drawing frames (via the method draw_frame, which you should override in a subclass), we need a method to decouple the animation speed from the framerate.
If we would simple put put an animation step every frame, we would get some sort of Death of the Fast Machine" effect ala Wing Commander. E.g. if the system manages only 10 FPS, the animation would be slower than when we do 60 FPS.
To achive this, SDL::App::FPS
features a clock, which runs independed of the current frame rate (and actually, independend of the system's clock, but more on this in the next section).
You can access it via a call to current_time, and it will return the ticks e.g. the number of milliseconds elapsed since the start of the application.
To effectively decouple animation speed from FPS, get at each frame the current time, then move all objects (or animation sequences) according to their speed and display them at the location that matches the time at the start of the frame. See the examples/
directory for examples on how to do this.
Note that it is better to draw all objects according to the time at the start of the frame, and not according to the time when you draw a particular object. Or in other words, treat the time like it is standing still when drawing a complete frame. Thus each frame becomes a snapshot in time, and you don't get nasty sideeffects like one object beeing always "behind" the others just because it get's drawn earlier.
Time Warp
Now that we have a constant animation speed independend from framerate or system speed, let's have some fun.
Since all our animation steps are coupled to the current time, we can play tricks with the current time.
The function time_warp let's you access a time warp factor. The default is 1.0, but you can set it to any value you like. If you set it, for instance to 0.5, the time will pass only half as fast as it used to be. This means instant slow motion! And when you really based all your animation on the current time, as you should, then it will really slow down your entire application to a crawl.
Likewise a time warp of 2 let's the time pass twice as fast. There are virtually no restrictions to the time warp.
For instance, a time warp greater than one lets the player pass boring moments in a game, for instance when you need to wait for certain events in a strategy game, like your next building beeing completed.
Try to press the left (fast forward), right (slow motion) and middle (normal) mousebuttons in the example application and watch the effect.
If you are very bored, press the 'b' key and see that even negative time warps are possible...
Ramping Time Warp
Now, setting the time war to factor of N is nice, but sometimes you want to make dramatic effects, like slowly freezing the time into ultra slow motion or speeding it up again.
For this, ramp_time_warp can be used. You give it a time warp factor you want to reach, and a time (based on real time, not the warped, but you can of course change this). Over the course of the time you specified, the time warp factor will be adapted until it reaches the new value. This means it is possible to slowly speeding up or down.
You can also check whether the time warp is constant of currently ramping by using time_is_ramping. When a ramp is in effect, call ramp_time_warp without arguments to get the current parameters. See below for details.
The example application uses the ramping effect instead instant time warp.
Event handlers
This section describes events as external events that typically happen due to user intervention.
Such events are keypresses, mouse movement, mouse button presses, or just the flipping of the power switch. Of course the last event cannot be handled in a sane way by our framework :)
All the events are checked and handled by SDL::App::FPS automatically. The event SDL_QUIT (which denotes that the application should shut down) is also carried out automatically. If you want to do some tidying up when this happens, override the method quit_handler.
The event checking and handling is done at the start of each frame. This means no event will happen while you draw the current frame. Well, it will happen, but the action caused by that event will delayed until the next frame starts. This simplifies the frame drawing routine tremendously, since you know that your world will be static until the next frame.
To associate an event with an action, you use the add_event_handler method. This method get's an event kind (like SDL_KEYDOWN or MOUSEBUTTONDOWN) and an event type (like SDLL_SPACE). When this specific event is encountered, the also given callback routine is called. In the simplest form, this would be an anonymous subroutine. Here is an example:
my $handler = $app->add_event_handler ( SDL_KEYDOWN, SDLK_SPACE, sub {
my ($self) = shift;
$self->pause(SDL_KEYDOWN);
} );
This would pause the game until any key is pressed.
You can easily reconfigure the event to trigger for a different key like this:
$handler->rebind( SDL_KEYDOWN, SDLK_p );
If you want the same event to be triggered by different external events, then simple add another event:
my $handler2 = $app->add_event_handler ( SDL_KEYDOWN, SDLK_P, sub {
my ($self) = shift;
$self->pause();
} );
This would also allow the user to pause with 'P'.
Event bindings can also be removed with del_event_handler(), if so desired.
See add_event_handler() for more details.
Timers
Of course not always should all things happen instantly. Sometimes you need to delay some events or have them happening at regular or irregular intervalls again.
For these cases, SDL::App::FPS
features timers. These timers are different from the normal SDL::Timers in that they run in the application clock space, e.g. the time warp effects them. So if your application is in slow motion, the events triggers by the timers will still happen at the correct time.
Asyncronous Timers
There are still some things that need a (near) real-time response and can not wait for the next frame drawn. For instance when one music piece ends and the next needs to be faded in, it would be unfortunate to wait until the next frame start, which might come a bit late.
In these cases you can still use the normal SDL timers, of course.
SUBCLASSING
SDL::App::FPS encapsulates any of it's private data under the key _app
. So you can use any hash key other than _app
to store you data, no need to encapsulate it further unless you plan on making your class subclassable, too.
When adding subroutines to your subclass, prefix them with something unique, like __
or _myapp_
so that they do not interfere with changes in this base class.
Do not access the data in the baseclass directly, always use the accessor methods!
METHODS
The following methods should be overridden to make a usefull application:
- draw_frame()
-
Responsible for drawing the current frame. Its first two parameters are the time (in ticks) at the start of the current frame, and the time at the start of the last frame. These times are warped according to
time_warp()
, see there for an explanation on how this works.The third parameter is the current framerate, averaged. You can use this to reduce dynamically the complexity of the scene to achieve a faster FPS if it falls below a certain threshold.
The following methods can be overriden if so desired:
- pre_init_handler()
-
Called by new() just before the creating the SDL application and window.
- post_init_handler()
-
Called by new() just after the creating the SDL application and window.
- quit_handler()
-
Called by main_loop() just before the application is exiting.
- resize_handler()
-
Called automatically whenever the application window size changed.
The following methods can be used, but need not be overriden except in very special cases:
- new()
-
$app = SDL::App::FSL->new($options);
Create a new application, init the SDL subsystem, create a window, starts the frame rate monitoring and the application time-warped clock.
new() gets a hash ref with options, the following options are supported:
width the width of the application window in pixel height the width of the application window in pixel depth the depth of the screen (colorspace) in bits max_fps maximum number of FPS to do (save CPU cycles) resizeable when true, the application window will be resizeable You should install an event handler to watch for events of the type SDL_VIDEORESIZE. useopengl set to 1 to enable OpenGL support config Path and name of the config file, defaults to 'config/client.cfg'. time_warp Defauls to 1.0 - initial time warp value. fullscreen 0 = windowed, 1 - fullscreen title Name of the app, will be the window title useconsole enable a console (which can be shown/hidden) show_fps print fps (0 - disable, 1 upper-left, 2 lower-left, 3 lower-right, 4 upper-right corner) font_fps name of the .fnt file containing the config for the font for the FPS font_console name of the .fnt file containing the config for the font for the Console debug 0: disable, 1 (or higher for more): print debug info
useconsole
andshow_fps
currently only work in conjunction withuseopengl
.new() also parses the command line options via Getopt::long, meaning that
./app.pl --fullscreen --width=800 --noresizeable
will work as intended. If you want to prevent command line parsing, simple clear
@ARGV = ()
before calling new().Please note that, due to the resolution of the timer, the maximum achivable FPS with capping is about 200-300 FPS even with an empty draw routine. Of course, my machine could do about 50000 FPS; but then it hogs 100% of the CPU. Thus the framerate capping might not be accurate and cap the rate at a much lower rate than you want. However, only
max_fps
> 100 is affected, anything below 100 works usually as intended.Set
max_fps
to 0 to disable the frame-rate cap. This means the app will burn all the CPU time and try to achive as much fps as possible. This is not recommended except for benchmarking!new()
calls pre_init_handler() before creating the SDL application, and post_init_handler() afterwards. So you can override thess two for your own needs. - save
-
$app->save($additional_data);
Saves the application state to a file. $addtional data can contain a reference to additional data that will also be saved. See also load().
- load
-
($data,$error) = $app->load();
Loads the application state from a file. If additional data was passed to save(), then $data will contain a references to this data afterwards.
$error
will contain any error that might occur, or undef. - screenshot
-
$app->screenshot($path,$filename);
Save a screenshot in BMP format of the current surface to a file.
$path
and$filename
are optional, default is the current directory and filenames named like 'screenshot_0000.bmp'. The first non-existing filename will be used if$filename
is undef, otherwise the caller is responsible for finding a free filename. - main_loop()
-
$app->main_loop();
The main loop of the application, only returns when an SDL_QUIT event occured, or $self->quit() was called.
- watch_event
-
$app->watch_event ( fullscreen => 'SDLK_f', pause => 'p', freeze => 'SDL_SPACE', );
watch_event
is a convenience method, that let's you add often-used event handlers to some default events. The following are supported:fullscreen the given key switches the app between fullscreen and windowed mode quit this ends the application pause pause the application until the same key is pressed again. No frames will be drawn in that time and any other event will be ignored freeze Freeze the time. Frames will still be drawn and events handled, althouhg no timer will expire (since the time does not "flow"). The same key again lets the time flow again. screenshot Take a screenshot of the current fram and store it as BMP
Instead of
SDLK_foo
, you can also pass for key one of the strings 'LMB', 'RMB' or 'MMB' meaning the left, right and middle mouse button. Also possible are the strings 'MWD' and 'MWU', meaning mouse wheel down and up, respectively.Furthermore possible are strings like 'ENTER', which will be translated to SDLK_ENTER.
- quit()
-
Set a flag to quit the application at the end of the current frame. Can be called in draw_frame(), for instance.
- pause()
-
$app->pause(); $app->pause(SDL_KEYDOWN); $app->pause(SDL_KEYDOWN,SDL_MOUSEBUTTONDWN,SDL_MOUSEMOVED);
Pauses the application until the next event occurs. Given an optional list of event types (like SDL_KEYDOWN), it will wait until one event of the given type happens. All other events will be ignored, with the exception of SDL_QUIT, which will end the pause and terminate the application.
- fullscreen()
-
$app->fullscreen(); # toggle $app->fullscreen(1); # fullscreen $app->fullscreen(0); # windowed
When called without arguments, toggles the application's fullscreen status. When given an argument that is true, set's fullscreen mode, otherwise sets windowed mode. Returns true when fullscreenmode was activated, otherwise false. See is_fullscreen().
- is_fullscreen()
-
if ($app->is_fullscreen()) { }
Retursn true if the application is currently in fullscreen mode.
- width()
-
my $w = $self->width();
Return the current width of the application's surface.
- height()
-
my $w = $self->height();
Return the current height of the application's surface.
- depth()
-
my $w = $self->depth();
Return the current bits per pixel of the application's surface in bits, e.g. 8, 16, 24 or 32.
- update()
-
$self->update($rect);
Call the SDL::App's update method.
- add_timer()
-
$app->add_timer($time,$count,$delay,$callback, @args ]);
Adds a timer to the list of timers. When time is 0, the timer fires immidiately (calls $callback). When the count was 1, and time 0, then the timer will not be added to the list (it already expired) and undef will be returned. Otherwise the unique timer id will be returned.
@args
can be empty, otherwise the contents of these will be passed to the callback function as additional parameters.The timer will fire for the first time at
$time
ms after the time it was added, and then wait$delay
ms between each shot. if$count
is positive, it gives the number of shots the timer fires, if it is negative, the timer will fire endlessly until it is removed.The timers added via add_timer() are coupled to the warped clock.
- get_timer()
-
$timer = $self->get_timer($timer_id);
Given a timer id, returns the timer object or undef.
- del_timer()
-
$app->del_timer($timer); $app->del_timer($timerid);
Delete the given timer (or the one by the given id).
- timers()
-
Return count of active timers.
-
$app->add_button ($x,$y,$w,$h, $type, $shape, $button, $callback);
Add a SDL::App::FPS::Button to the application.
- add_group
-
$group = $app->add_group();
Convienence method to create a new SDL::App::FPS::Group and bind it to this application.
- add_event_handler
-
my $handler = SDL::App::FPS::EventHandler->new( $type, $kind, $callback );
Creates a new event handler to watch out for $type events (SDL_KEYDOWN, SDL_MOUSEMOVED, SDL_MOUSEBUTTONDOWN etc) and then for $kind kind of it, like SDLK_SPACE. Mouse movement events ignore the $kind parameter.
The created handler is added to the application.
See SDL::App::FPS::EventHandler::new() for details.
One clever and usefull thing is to define the key bindings in the config file under the section
[input]
like this:bind_event_some_name = f bind_event_some_other = SPACE bind_event_more = RMB
And then do this:
my $handler = SDL::App::FPS::EventHandler->new( FPS_EVENT, 'some_name', $callback );
This means rebinding the event to a different key needs no change in your code.
- del_event_handler
-
Delete an event handler from the application.
- event_bound_to
-
$name = $app->event_bound_to('some_event');
Rreturn the name of the key/button the event handler is bound to, usefull for handlers of type
FPS_EVENT
. See add_event_handler(). - app()
-
my $sdl_app = $self->app();
Return a pointer to the SDL application object. Usefull for calling it's methods.
- option()
-
print $app->option('max_fps'),"\n"; # get $app->option('max_fps',40); # set to 40
Get/sets an option defined by the key (name) and an optional value.
- freeze_time_warp_ramp()
-
$app->freeze_time_warp_ramp();
Disables any ramping of the time warp that might be in effect.
- freeze_time()
-
$app->freeze_time();
Sets the time warp factor to 0, effectively stopping the warped clock. Note that the real clock still ticks and frames are still drawn, so you can overlay some menu/animation over a static (froozen in time) background. Of course it might be more efficient to save the current drawn frame as image and stop the drawing if the not-changing background altogether.
- thaw_time()
-
$app->thaw_time();
Sets the time warp factor back to what it was before freeze_time() was called. Does nothing when the clock is not frozen.
- ramp_time_warp
-
$app->ramp_time_warp($target_factor,$time_to_ramp);
Set a tagret time warp factor and a time it will take to get to this factor. The time warp (see time_warp()) will then be gradually adjusted to the target factor.
$time_to_ramp
is in ms (aka 1000 == one second).It is sometimes a good idea to read out the current time warp and ramp it to a specific factor, like so:
$time_warp = $app->time_warp(); $app->ramp_time_warp($time_warp * 2, 1000);
But you need to restrict this somehow, otherwise the clock might be speed up or slowed down to insanely high or low speeds. So sometimes it is just better to do this:
sub enable_slow_motion { # no matter how fast clock now is, slow it down to a fixed value $app->ramp_time_warp(0.5, 1000); }
When ramp_time_warp() is called without arguments, and ramping is in effect, it returns a list consisting of:
target factor # to where we ramp time to ramp # how long it takes (ticks) current time warp # where are currently start time warp # from where we ramp (factor) start time warp time # from where we ramp (real time ticks)
When no ramping is in effect, it returns an empty list or undef.
You can disable/stop the time warping by setting a new time warp factor directly like so:
my $t = $app->time_warp(); $app->time_warp($t);
Or easier:
$app->freeze_time_warp();
- time_warp
-
$app->time_warp(2); # fast forward
Get or set the current time warp, e.g. the factor how fast the time passes. The new time warp will be effective from the next frame onwards.
Please note that setting a time warp factor will disable time warp ramping.
- time_is_ramping
-
if ($app->time_is_ramping()) { }
Returns true if the time warp factor is currently beeing ramped, e.g. chaning.
- time_is_frozen
-
if ($app->time_is_frozen()) { }
Return true if the time is currently frozen, e.g. the clock is standing still.
- frames()
-
Return number of frames drawn since start of app.
- start_time()
-
Return the time when the application started in ticks.
- current_fps()
-
Return current number of frames per second, averaged over the last 1000ms.
- max_fps()
-
Return maximum number of frames per second we ever achieved.
- min_fps()
-
Return minimum number of frames per second we ever achieved.
- now()
-
Return current time at the start of the frame in ticks, unwarped. See current_time for a warped version. This is usefull for tracking the real time clock as opposed to the warped application clock.
- current_time()
-
Return current time at the start of this frame (the same as it is passed to draw_frame(). This time will be warped by time_warp, e.g a time_warp of 2 makes it go twice as fast as GetTicks(). Note that the returned value will only change at the start of each frame.
- lastframe_time()
-
Return time at the start of the last frame. See current_time(). The same value is passed to draw_frame().
- get_clock
-
($day,$hour,$minute,$second,$ms) = $app->get_clock();
Returns the current time (see current_time()) in a day, hour, minute, second and millisecond format. See set_clock() on how to make the current time a certain date and time.
- set_clock
-
$app->set_clock($day,$hour,$minute,$second,$ms); $app->set_clock(1,12,30); # set current time to day 1, 12:30
Sets the current time to a specific date and time so that get_clock() returns the proper format.
- clock_to_ticks
-
$app->clock_to_ticks(0,12,30); # 12 hours, 30 minutes $app->clock_to_ticks(10,5,0,12); # 10 days, 5 hours, 12 seconds $app->clock_to_ticks(0,0,0,123); # 123 seconds
Return time given as days, hours, minutes, seconds and ms (undef counts as 0). This is handy for setting timers than expire in a couple of hours, instead of just a few milli seconds.
INTERNAL METHODS
The following routines are used internally and automatically, so you need not to call them.
- _create_window
-
Initialized the SDL (and OpenGL) subsysten and creates the window.
- _next_frame
-
Updates the FPS monitoring process, the frame counter, the average frame rate, and then calls draw_frame().
- _handle_events()
-
Checks for events and hands all of them to event_handler for user handling. The only event it handles directly is SDL_QUIT. Returns 0 for keeping the application running, and > 0 for quit.
- _expire_timers()
-
Check all the timers for whether they are due ot not and let them fire. Removes unnecc. timers from the list.
- _rebound_event_handler
- _ramp_time_warp()
- _resized()
-
Automatically called whenever our window got resized.
- _activated_thing
-
$app->_activated_thing($thing);
When a thing (timer, event handler, button etc) is (re)activated, it notifies the app by calling this routine with itself as argument. Done automatically by the thing itself.
- _deactivated_thing
-
$app->_deactivated_thing($thing);
When a thing (timer, event handler, button etc) is deactivated, it notifies the app by calling this routine with itself as argument. Done automatically by the thing itself.
BUGS
useconsole
andshow_fps
currently work only in conjunction withuseopengl
.
AUTHORS
(c) 2002, 2003, 2006, Tels <http://bloodgate.com/>