|
このページは大阪弁化フィルタによって翻訳生成されたんですわ。 |
Simple DirectMedia Layer (a.k.a libsdl) is a cross-platform C library that provides access to several input and output devices. Most popularly it is used for its access to the 2D video framebuffer and inputs for games.
In addition to the core library there are several other libraries that provide useful features such as Text, Mixers, Images and GFX.
SDL Perl binds several of these libraries together int the
SDL::* namespace. Moreover, SDL Perl provides several high
level libraries in the SDLx::* namespace that encapsulate
valuable game writing abstractions.
SDLx:: layerThe main purpose of the SDLx::* layer is to smooth out the
drudgery of using the SDL::* layer directly. For example
drawing a rectangle involves the following work.
NOTE:
Don't worry about understanding the code at this moment. Just compare the two code listings below.
use SDL;
use SDL::Video;
use SDL::Surface;
use SDL::Rect;
# the size of the window box or the screen resolution if fullscreen
my $screen_width = 800;
my $screen_height = 600;
SDL::init(SDL_INIT_VIDEO);
# setting video mode
my $screen_surface = SDL::Video::set_video_mode($screen_width,
$screen_height,
32,
SDL_ANYFORMAT);
# drawing a rectangle with the blue color
my $mapped_color = SDL::Video::map_RGB($screen_surface->format(), 0, 0, 255);
SDL::Video::fill_rect($screen_surface,
SDL::Rect->new($screen_width / 4, $screen_height / 4,
$screen_width / 2, $screen_height / 2),
$mapped_color);
# update an area on the screen so its visible
SDL::Video::update_rect($screen_surface, 0, 0, $screen_width, $screen_height);
sleep(5); # just to have time to see it
While drawing a blue rectangle in the SDLx::* layer is as
simple as:
use strict;
use warnings;
use SDL;
use SDLx::App;
my $app = SDLx::App->new( width=> 800, height => 600 );
$app->draw_rect([ $app->width/4, $app->height / 4, $app->width /2, $app->height / 2 ], [0,0,255,255] );
$app->update();
sleep(5);
A secondary purpose of the SDLx::* modules are to manage
additional features for users, such as Layers, Game Loop handling and
more.
This book is written for new users of SDL Perl who have some experience with Perl, but not much experience with SDL. It is not necessary for the audience to be aware of SDL internals, as this book covers most areas as it goes.
This book will be formated into chapters that progressively increase in complexity. However each chapter can be treated as a separate tutorial to jump to and learn.
Each chapter will have a specific goal (e.g. Making Pong), which we will work towards. The source code for each chapter will be broken up and explained in some detail. Sources and data files are all provided on http://sdl.perl.org.
Finally the chapters will end with an exercise the reader can try out.
This book is intended to introduce game development to Perl programmers and at the same time introduce Modern Perl concepts through game development. The book provides a progression of simple to intermediate examples and provide suggestions for more advanced endeavors.
We assume that a recent perl language and supporting packages have been installed on your system. Depending on your platform you may need some dependencies. Then we can do a final CPAN install.
Alien::SDL will install binaries for 32bit and 64bit so
there is no need to compile anything.
Fink has packages for SDL Perl available. However Pango is not currently supported.
Alien::SDL will compile SDL dependencies from scratch with
no problems as long some prerequisites are installed.
libfreetype6, libX11, libvorbis,
libogg and libpng headers will suffice for most
examples in this book.
Most current linux distributions include all the parts needed for this tutorial in the default install and in their package management system. It is also always possible to install on linux using the available open source code from their repositories. The Alien::SDL perl module automates much of downloading, compiling and installing the needed libraries.
You can probably use your distribution's packages. On Ubuntu and Debian try:
sudo apt-get install libsdl-net1.2-dev libsdl-mixer1.2-dev \
libsdl1.2-dev libsdl-image1.2-dev libsdl-ttf2.0-dev \
libsdl-gfx1.2-dev libsdl-pango-dev
To compile from scratch a compiler, system header packages and some libraries are required.
sudo apt-get install build-essential xorg-dev libx11-dev libxv-dev \
libpango1.0-dev libfreetype6-dev libvorbis-dev libpng12-dev \
libogg-dev
sudo cpan SDL
For most platforms a CPAN install will suffice. Supported and tested platforms are listed at http://pass.cpantesters.org/distro/S/SDL.html.
Hopefully this book answers most of your questions. If you find you need assistance please contact us in one of the following methods:
SDL Perl's homepage is at http://sdl.perl.org/.
The channel #sdl on irc.perl.org is very
active and a great resource for help and getting involved.
If you need help with SDL Perl, send an to
sdl-devel@perl.org.
The code examples in this books are provided on http://github.com/PerlGameDev/SDL_Manual/tree/master/code_listings/ .
Perl community on #sdl and #perl.
SDL manages a single screen which is attached to the video device. An
SDL application may contain one or more Surfaces of different kinds. But
we'll leave that issue till later. The screen is typically created using
the SDLx::App class.
use strict;
use warnings;
use SDL;
use SDLx::App;
my $app = SDLx::App->new();
sleep( 2 );
The above code causes a window to appear on the desktop with nothing in
it. Most current systems will fill it with a default black screen as shown.
For some systems, however, a transparent window will might be shown. It is
a good idea to ensure what we intend to display is shown. So we update the
$app to ensure that.
$app->update();
SDLx::App OptionsSDLx::App also allows you to specify several options for
your application.
First are the physical dimensions of the screen itself. Lets make the screen a square size of 400×400. Change the initialization line to:
my $app = SDLx::App->new( width => 400, height => 400 );
You will notice that the window's title is either blank or on some window managers it displays the path to the script file, depending on your operating system. Suppose we want a title for a new Pong clone game:
my $app = SDLx::App->new( width => 400,
height => 400,
title => 'Ping - A clone' );
At this point your screen will be:
\includegraphics[width=0.5\textwidth]{../src/images/first.png} \caption{Your first SDL screen!} \label{fig:first_screen}
There are short-hand versions of the parameter names used in the call to
new(). The parameters width, height,
and title may be abbreviated as w, h
and t respectively. So, the previous example could be written
like this:
my $app = SDLx::App->new( w => 400,
h => 400,
t => 'Ping - A clone' );
SDL provides several ways to draw graphical elements on the screen;
these methods can be broken down into three general categories: Primitives,
Images and Text. Methods in each of the three categories draw a single
object on the Surface. The Surface is represented by
SDLx::Surface. Even our SDLx::App is a
SDLx::Surface. This means that we can draw directly on the
SDLx::App, however there are several advantages to drawing on
multiple surfaces. In this chapter we will explore these methods of
drawing, and make a pretty picture.
SDL surfaces coordinate system has x=0, y=0 in the upper left corner and work downward and to the right. The API always lists coordinates in x,y order. More discussion of these details can be found in the SDL library documentation: http://www.sdltutorials.com/sdl-coordinates-and-blitting/
Using SDL we will try to construct the following image.
\includegraphics[width=0.5\textwidth]{../src/images/flower.png} \caption{A field of flowers} \label{fig:flowers}
Below is the program that generates the above image.
use SDL;
use SDLx::App;
use SDLx::Sprite;
my $app = SDLx::App->new(
w => 500,
h => 500,
d => 32,
title => 'Pretty Flowers'
);
# Draw Code Starts here
my $flower = SDLx::Sprite->new( width => 50, height => 100 );
$flower->surface->draw_rect( [ 0, 0, 50, 100 ], [ 0, 0, 0, 0 ] );
$flower->surface->draw_rect( [ 23, 30, 4, 100 ], [ 0, 255, 0, 255 ] );
$flower->surface->draw_circle_filled( [ 25, 25 ], 10, [ 150, 0, 0, 255 ] );
$flower->surface->draw_circle( [ 25, 25 ], 10, [ 255, 0, 0, 255 ] );
$flower->alpha_key(0);
$app->draw_rect( [ 0, 0, 500, 500 ], [ 20, 50, 170, 255 ] );
$app->draw_rect( [ 0, 400, 500, 100 ], [ 50, 170, 20, 100 ] );
foreach ( 0 .. 500 ) {
my $y = 425 - rand(50);
$flower->draw_xy( $app, rand(500) - 20, $y );
}
#Draw Code Ends Here
$app->update();
sleep(2);
To begin actually drawing the flower, we need to cover some theory.
Drawing in SDL are done on Surfaces. The SDLx::Surface
object provides access to methods in the form of:
$surface->draw_{something}( .... );
Parameters are usually provided as array references, to define areas and colors.
Some parameters are just a quick definition of the positions and
dimensions. For a rectangle that will be placed at (20, 20)
pixel units on the screen, and a dimension of 40x40 pixel
units the following would suffice.
my $rect = [20, 20, 40, 40];
in SDL is described by 4 numbers. The first three numbers define the Red, Blue, Green intensity of the color. The final number defines the transparency of the Color.
my $color = [255, 255, 255, 255];
Color can also be defined as hexadecimal values:
my $color = 0xFFFFFFFF;
The values of the numbers range from 0-255 for 32 bit depth in RGBA format. Alternately color can be described as a 4 byte hexadecimal value, each two digit byte encoding the same RGBA vaues as above:
my $goldenrod = 0xDAA520FF;
NOTE: Depth of Surface
The bits of the surface are set when the
SDLx::SurfaceorSDLx::Appis made.my $app = SDLx::App->new( depth => 32 );Other options are 24,16 and 8. 32 is the default bit depth.
All SDLx::Surfaces are made of pixels that can be read and
written to via a tied array interface.
$app->[$x][$y] = $color;
The $color is defined as an unsigned integer value which is
construct in the following format, 0xRRBBGGAA. This is a
hexadecimal number. Here are some examples:
$white = 0xFFFFFFFF;
$black = 0x000000FF;
$red = 0xFF0000FF;
$blue = 0x00FF00FF;
$green = 0x0000FFFF;
Pixels can also be defined as anonymous arrays as before [$red,
$blue, $green, $alpha].
Drawing are usually simples shapes that can be used for creating graphics dynamically.
\includegraphics[width=0.5\textwidth]{../src/images/draw-1.png} \caption{Drawing a line} \label{fig:draw_line}
$app->draw_line( [200,20], [20,200], [255, 255, 0, 255] );
This will draw a yellow line from positions (200,20) to
(20,200) .
\includegraphics[width=0.5\textwidth]{../src/images/draw-2.png} \caption{Drawing a Rectangle} \label{fig:draw_rect}
Rectangles are a common building blocks for games. In SDL, rectangles are the most cost effective of the primitives to draw.
$app->draw_rect( [10,20, 40, 40 ], [255, 255, 255,255] );
The above will add a white square of size 40x40 onto the
screen at the position (10,20).
\includegraphics[width=0.5\textwidth]{../src/images/draw-3.png} \caption{Drawing a Circle} \label{fig:draw_circle}
\includegraphics[width=0.5\textwidth]{../src/images/draw-4.png} \caption{Drawing a filled Circle} \label{fig:draw_filled_circle}
Circles are drawn similarly either filled or unfilled.
$app->draw_circle( [100,100], 20, [255,0,0,255] );
$app->draw_circle_filled( [100,100], 19, [0,0,255,255] );
Now we will have a filled circle, colored blue and unfilled circle, colored as red.
For more complex drawing functions have a look at
SDL::GFX::Primitives.
Using our knowledge of Primitives in SDL, lets draw our field, sky and a simple flower.
use strict;
use warnings;
use SDL;
use SDLx::App;
my $app = SDLx::App->new(
w => 500,
h => 500,
d => 32,
title => 'Pretty Flowers'
);
#Adding the blue skies
$app->draw_rect( [ 0, 0, 500, 500 ], [ 20, 50, 170, 255 ] );
#Draw our green field
$app->draw_rect( [ 0, 400, 500, 100 ], [ 50, 170, 20, 100 ] );
# Make a surface 50x100 pixels
my $flower = SDLx::Surface->new( width => 50, height => 100 );
# Lets make the background black
$flower->draw_rect( [ 0, 0, 50, 100 ], [ 0, 0, 0, 0 ] );
# Now for a pretty green stem
$flower->draw_rect( [ 23, 30, 4, 100 ], [ 0, 255, 0, 255 ] );
# And the simple flower bud
$flower->draw_circle_filled( [ 25, 25 ], 10, [ 150, 0, 0, 255 ] );
$flower->draw_circle( [ 25, 25 ], 10, [ 255, 0, 0, 255 ] );
$flower->blit( $app, [ 0, 0, 50, 100 ] );
$app->update();
sleep(1);
\includegraphics[width=0.5\textwidth]{../src/images/flower-1.png} \caption{Looks so lonely there all alone} \label{fig:draw_flower_lone}
So far we have been drawing only on one surface, the display. In SDL it
is possible to write on several surfaces that are in memory. These surfaces
can later on be added on to the display to show them. The Surface is
defined as a SDLx::Surface type in SDL Perl.
There are several ways to create SDLx::Surface for use.
For the purposes of preparing surfaces using only draw functions ( as
mentioned above ) we can create a surface using the
SDLx::Surface's constructor.
$surface = SDLx::Surface->new( width => $width, height => $height );
Using SDL::Image and SDL::Video we can load
images as surfaces too. SDL::Image provides support for all
types of images, however it requires SDL_image library support
to be compiled with the right library.
$surface = SDL::Image::load( 'picture.png' );
In case the SDL_Image library is unavailable we can use the
build in support for the .bmp format.
$surface = SDL::Video::load_BMP( 'picture.bmp' );
Generally however the SDLx::Sprite module is used.
You might have noticed that putting another SDLx::Surface
on the $app requires the usage of a blit( )
function, which may not clarify as to what is going on there. Fortunately a
SDLx::Sprite can be used to make our flower. Besides making
drawing simpler, SDLx::Sprite adds several other features that
we need for game images that move a lot. For now lets use
SDLx::Sprite for our flowers.
use strict;
use warnings;
use SDL;
use SDLx::App;
use SDLx::Sprite;
my $app = SDLx::App->new(
w => 500,
h => 500,
d => 32,
title => 'Pretty Flowers'
);
#Adding the blue skies
$app->draw_rect( [ 0, 0, 500, 500 ], [ 20, 50, 170, 255 ] );
#Draw our green field
$app->draw_rect( [ 0, 400, 500, 100 ], [ 50, 170, 20, 100 ] );
my $flower = SDLx::Sprite->new( width => 50, height => 100 );
# To access the SDLx::Surface to write to we use the ->surface() method
# Lets make the background black
$flower->surface->draw_rect( [ 0, 0, 50, 100 ], [ 0, 0, 0, 0 ] );
# Now for a pretty green stem
$flower->surface->draw_rect( [ 23, 30, 4, 100 ], [ 0, 255, 0, 255 ] );
# And the simple flower bud
$flower->surface->draw_circle_filled( [ 25, 25 ], 10, [ 150, 0, 0, 255 ] );
$flower->surface->draw_circle( [ 25, 25 ], 10, [ 255, 0, 0, 255 ] );
$flower->draw_xy( $app, 0, 0 );
$app->update();
sleep(1);
Obviously at this point we don't want our single flower floating in the sky, so we will draw several of them on the ground. Delete everything including and after
$flower->draw_xy($app, 0,0)
and insert the below code to get a field of flowers.
foreach( 0..500 )
{
my $y = 425 - rand( 50 );
$flower->draw_xy( $app, rand(500)-20, $y );
}
$app->update();
sleep(1);
SDL process events using a queue. The event queue holds all events that
occur until they are removed. Events are any inputs such as: key presses,
mouse movements and clicks, window focuses, and joystick presses. Every
time the window sees one of these events, it puts it on the event queue
once. The queue holds SDL events, which can be read via an
SDL::Event object. We can process the Event Queue manually by
pumping and polling the queue, constantly.
use strict;
use warnings;
use SDL;
use SDL::Event;
use SDL::Events;
use SDLx::App;
my $app = SDLx::App->new( w => 200, h => 200 );
my $event = SDL::Event->new();
my $quit = 0;
while (!$quit) {
SDL::Events::pump_events(); #Updates the queue to recent events
#Process all events that are available
while ( SDL::Events::poll_event($event) ) {
#Check by Event type
do_key() if $event->type == SDL_KEYDOWN;
}
}
sub do_key { $quit = 1 }
SDLx::Controller via the SDLx::App handles
this loop by accepting Event Callbacks. Every application loop, each event
callback is called repetitively with each event in the queue. This chapter
will go through some examples of how to process various events for common
usage.
So far we have not been exiting an SDLx::App in a graceful
manner. Using the built in SDLx::Controller in the
$app we can handle events using callbacks.
use strict;
use warnings;
use SDL;
use SDL::Event;
use SDLx::App;
my $app = SDLx::App->new( w => 200, h => 200, d => 32, title => "Quit Events" );
#We can add an event handler
$app->add_event_handler( \&quit_event );
#Then we will run the app
#which will start a loop for keeping the app alive
$app->run();
sub quit_event
{
#The callback is provided a SDL::Event to use
my $event = shift;
#Each event handler also returns you back the Controller call it
my $controller = shift;
#Stopping the controller for us will exit $app->run() for us
$controller->stop if $event->type == SDL_QUIT;
}
SDLx::App calls the event_handlers, from an internal
SDLx::Controller, until a
SDLx::Controller::stop() is called. SDLx::App
will exit gracefully once it is stopped.
In the above sample SDL_QUIT was used to define the type of
event we have. SDL uses a lot of integers to define different types of
objects and states. Fortunately these integers are wrapped in constant
functions like SDL_QUIT. More defines are explained in the
SDL::Events documentation. Have a look at the perldoc for
SDL::Events.
perldoc SDL::Events
Events can also be processed without using callbacks from
SDLx::App. Chapter 5 goes more in detail for this topic. The perldoc forSDL::Eventswill also show how do the processing.
Exiting when the SDL_QUIT event is call is a common
callback so SDLx::App provides it for you, as a constructor
option.
use strict;
use warnings;
use SDL;
use SDLx::App;
my $app = SDLx::App->new( w => 200, h => 200, d => 32,
title => "Quit Events",
exit_on_quit => 1);
#exit_on_quit option exits when SDL_QUIT is processed
#Then we will run the app
#which will start a loop for keeping the app alive
$app->run();
SDL events also allow us to handle input from various devices. To demonstrate two of the common devices, lets make a simple paint program. It will provide a small black window where you can draw with the mouse. Moreover when you press the number keys 0-10 it will pick different colors. By pressing 'q' or 'Q' we will exit. Similarity pressing 'c' or 'C' will clear the screen. Pressing 'ctrl-s' will save our image to the file 'painted.bmp'.
\includegraphics[width=0.5\textwidth]{../src/images/painted.png} \caption{Simple Paint: Smile} \label{fig:Smile
To handle the keyboard specifications we will create another event callback.
use strict;
use warnings;
use SDL;
use Cwd;
use SDL::Event;
use SDLx::App;
my $app = SDLx::App->new( w => 200, h => 200, d => 32, title => "Simple Paint");
sub quit_event {
my $event = shift;
my $controller = shift;
$controller->stop() if $event->type == SDL_QUIT;
}
my @colors = ( 0xFF0000FF, 0x00FF00FF,
0x0000FFFF, 0xFFFF00FF,
0xFF00FFFF, 0x00FFFFFF,
0xCCFFCCFF, 0xFFCC33FF,
0x000000FF, 0xFFFFFFFF );
my $brush_color = 0;
sub save_image {
if( SDL::Video::save_BMP( $app, 'painted.bmp' ) == 0 && -e 'painted.bmp')
{
warn 'Saved painted.bmp to '.cwd();
}
else
{
warn 'Could not save painted.bmp: '.SDL::get_errors();
}
}
sub keyboard_event
{
my $event = shift;
#Check that our type of event press is a SDL_KEYDOWN
if( $event->type == SDL_KEYDOWN )
{
#Convert the key_symbol (integer) to a keyname
my $key_name = SDL::Events::get_key_name( $event->key_sym );
#if our $key_name is a digit use it as a color
my $brush_color = $key_name if $key_name =~ /^\d$/;
#Get the keyboard modifier perldoc SDL::Events
#We are using any CTRL so KMOD_CTRL is fine
my $mod_state = SDL::Events::get_mod_state();
#Save the image.
save_image if $key_name =~ /^s$/ && ($mod_state & KMOD_CTRL);
#Clear the screen if we pressed C or c
$app->draw_rect( [0,0,$app->w, $app->h], 0 ) if $key_name =~ /^c$/;
#Exit if we press a Q or q
$app->stop() if $key_name =~ /^q$/
}
$app->update();
}
$app->add_event_handler(\&quit_event);
$app->add_event_handler(\&keyboard_event);
$app->run()
NOTE: Globals and Callbacks
When adding a callback to
SDLx::Appwhich uses globals ($brush_colorand@colorsin this case ), be sure to define them before declaring the subroutine. Also add it to theSDLx::Appafter the subroutine is defined. The reason for this is so thatSDLx::Appis aware of the globals before it calls the callback internally.
Now we will go about capturing our Mouse events, by inserting the
following code after the keyboard_event subroutine.
#Keep track if we are drawing
my $drawing = 0;
sub mouse_event {
my $event = shift;
#We will detect Mouse Button events
#and check if we already started drawing
if($event->type == SDL_MOUSEBUTTONDOWN || $drawing)
{
# set drawing to 1
$drawing = 1;
# get the X and Y values of the mouse
my $x = $event->button_x;
my $y = $event->button_y;
# Draw a rectangle at the specified position
$app->draw_rect( [$x,$y, 2, 2], $colors[$brush_color]);
# Update the application
$app->update();
}
# Turn drawing off if we lift the mouse button
$drawing = 0 if($event->type == SDL_MOUSEBUTTONUP );
}
$app->add_event_handler( \&mouse_event );
Currently we don't make a distinction between what mouse click is done.
This can be accomplished by taking a look at the
button_button() method in SDL::Event. At this
point we have a simple paint application done.
Another point to note is that each event_handler is called in the order that it was attached.
The simplest game loop can be boiled down to the following.
while(!$quit)
{
get_events();
calculate_next_positions();
render();
}
In get_events() we get events from what input devices that
we need. It is important to process events first to prevent lag. In
calculate_next_positions we update the game state according to
animations and the events captured. In render() we will update
the screen and show the game to the player.
A practical example of this is a moving laser bolt.
use strict;
use warnings;
use SDL;
use SDL::Event;
use SDL::Events;
use SDLx::App;
my $app = SDLx::App->new(
width=> 200, height => 200,
title=> 'Pew Pew'
);
#Don't need to quit yet
my $quit = 0;
#Start laser on the left
my $laser = 0;
sub get_events{
my $event = SDL::Event->new();
#Pump the event queue
SDL::Events::pump_events;
while( SDL::Events::poll_event($event) )
{
$quit = 1 if $event->type == SDL_QUIT
}
}
sub calculate_next_positions{
# Move the laser over
$laser++;
# If the laser goes off the screen bring it back
$laser = 0 if $laser > $app->w();
}
sub render {
#Draw the background first
$app->draw_rect( [0,0,$app->w, $app->h], 0 );
#Draw the laser, in the middle height of the screen
$app->draw_rect( [$laser, $app->h/2, 10, 2], [255,0,0,255]);
$app->update();
}
# Until we quit stay looping
while(!$quit)
{
get_events();
calculate_next_positions();
render();
}
This game loop works well for consoles and devices where the share of CPU clock speed is always known. The game users will be using the same processor characteristics to run this code. This means that each animation and calculation will happen at the exact same time in each machine. Unfortunately this is typical not typical true for modern operating systems and hardware. For faster CPUs and systems with varying loads we need to regulate updates so game play will be consistent in most cases.
One way to solve this problem is to regulate the "Frames Per Second" for your games updates. A "frame" is defined as a complete redraw of the screen representing the updated game state. We can keep track of the number of frames we are delivering each second and control it using the technique illustrated below.
First run the below script with no fps fixing:
perl game_fixed.pl
You will see that the FPS is erratic, and the laser seems to speed up and slow down randomly.
Next fix the upper bounds of the FPS
perl game_fixed.pl 1
This will prevent the laser from going too fast, in this case faster then 60 frames per second.
Finally fix the lower bounds of the FPS
perl game_fixed.pl 1 1
At this point the FPS should be at a steady 60 frames per second. However if this is not the case read on to the problems below.
use strict;
use warnings;
use SDL;
use SDL::Event;
use SDL::Events;
use SDLx::App;
my $app = SDLx::App->new(
width => 200,
height => 200,
title => 'Pew Pew'
);
# Variables
# to save our start/end and delta times for each frame
# to save our frames and FPS
my ( $start, $end, $delta_time, $FPS, $frames ) = ( 0, 0, 0, 0, 0 );
# We will aim for a rate of 60 frames per second
my $fixed_rate = 60;
# Our times are in micro second, so we will compensate for it
my $fps_check = (1000/ $fixed_rate );
#Don't need to quit yet
my $quit = 0;
#Start laser on the left
my $laser = 0;
sub get_events {
my $event = SDL::Event->new();
#Pump the event queue
SDL::Events::pump_events;
while ( SDL::Events::poll_event($event) ) {
$quit = 1 if $event->type == SDL_QUIT;
}
}
sub calculate_next_positions {
$laser++;
$laser = 0 if $laser > $app->w;
}
sub render {
#Draw the background first
$app->draw_rect( [ 0, 0, $app->w, $app->h ], 0 );
#Draw the laser
$app->draw_rect( [ $laser, $app->h / 2, 10, 2 ], [ 255, 0, 0, 255 ] );
#Draw our FPS on the screen so we can see
$app->draw_gfx_text( [ 10, 10 ], [ 255, 0, 255, 255 ], "FPS: $FPS" );
$app->update();
}
# Called at the end of each frame, whether we draw or not
sub calculate_fps_at_frame_end
{
# Ticks are microseconds since load time
$end = SDL::get_ticks();
# We will average our frame rate over 10 frames, to give less erratic rates
if ( $frames < 10 ) {
#Count a frame
$frames++;
#Calculate how long it took from the start
$delta_time += $end - $start;
}
else {
# Our frame rate is our Frames * 100 / Time Elapsed in us
$FPS = int( ( $frames * 100 ) / $delta_time );
# Reset our metrics
$frames = 0;
$delta_time = 0;
}
}
while ( !$quit ) {
# Get the time for the starting of the frame
$start = SDL::get_ticks();
get_events();
# If we are fixing the lower bounds of the frame rate
if( $ARGV[1] )
{
# And our delta time is going too slow for frame check
if ( $delta_time > $fps_check ) {
# Calculate our FPS from this
calculate_fps_at_frame_end();
# Skip rendering and collision detections
# The heavy functions in the game loop
next;
}
}
calculate_next_positions();
render();
# A normal frame with rendering actually performed
calculate_fps_at_frame_end();
# if we are fixing the upper bounds of the frame rate
if ( $ARGV[0] ) {
# and our delta time is going too fast compared to the frame check
if ( $delta_time < $fps_check ) {
# delay for the difference
SDL::delay( $fps_check - $delta_time );
}
}
}
Generally this method is sufficient for most computers out there. The animations will be smooth enough that we see the same game play on differing hardware. However there are some serious problems with this method. First if a computer is too slow for 60 frames for second it will skip a lot of rendering, and the animation will look sparse and jittery. Maybe it would be better for 30 fps or lower for that machine, which is hard for the developer to predict. Secondly if a CPU is fast, a lot of CPU cycles are wasted in the delay.
Finally this method does not fix the fundamental problem that the rendering is fixed to CPU clock speed.
One way to fix the problem of a computer being consistently faster or slower for the default Frame per Second set, is to change the FPS accordingly. So far a slow CPU it will jump down to 30 FPS and so on. In our opinion, although a consistent FPS can be achieved this way, it still presents the problem of differing animation speeds for different CPUs and systems. There are better solutions available.
The problem caused by coupling rendering to the CPU speed has a convenient solution. We can derive our rendering from a physical model based on the passage of time. Objects moving according to real world time will have consistent behavior at all CPU speeds, and smooth interpolation between frames. SDLx::App provides just such features for our convenience through movement handlers and 'show' handlers.
A simple physics model for our laser has a consistent horizontal velocity in pixels per time step at the window's mid-point:
X = Velocity * time step,
Y = 100
Assuming a velocity of say 10, we will get points like:
0,100
10,100
20,100
30,100
...
200,100
Note that it no longer matters at what speed this equation is processed, instead the values are coupled to the passage of real time.
The biggest problem with this sort of solution the book keeping required
for many objects and callbacks. The implementation of such complex models
is non trivial, and will not be explored in this book. The topic is
discussed at length in the SDLx::Controller module.
This version of the laser example demonstrates the use of movement, and 'show' handlers and the simple physics model described above. This example is also much simpler since SDLx::App is doing more of the book work for us. It even implements the whole game loop for us.
use strict;
use warnings;
use SDL;
use SDL::Event;
use SDLx::App;
my $app = SDLx::App->new(
width => 200,
height => 200,
title => 'Pew Pew'
);
my $laser = 0;
my $velocity = 10;
#We can add an event handler
$app->add_event_handler( \&quit_event );
#We tell app to handle the appropriate times to
#call both rendering and physics calculation
$app->add_move_handler( \&calculate_laser );
$app->add_show_handler( \&render_laser );
$app->run();
sub quit_event {
#The callback is provided a SDL::Event to use
my $event = shift;
#Each event handler also returns you back the Controller call it
my $controller = shift;
#Stopping the controller for us will exit $app->run() for us
$controller->stop if $event->type == SDL_QUIT;
}
sub calculate_laser {
# The step is the difference in Time calculated for the
# next jump
my ( $step, $app, $t ) = @_;
$laser += $velocity * $step;
$laser = 0 if $laser > $app->w;
}
sub render_laser {
my ( $delta, $app ) = @_;
# The delta can be used to render blurred frames
#Draw the background first
$app->draw_rect( [ 0, 0, $app->w, $app->h ], 0 );
#Draw the laser
$app->draw_rect( [ $laser, $app->h / 2, 10, 2 ], [ 255, 0, 0, 255 ] );
$app->update();
}
To learn more about this topic please, see an excellent blog post by GafferOnGames.com: http://gafferongames.com/game-physics/fix-your-timestep/.
Pong is one of the first popular video games in the world. It was created by Allan Alcorn for Atari Inc. and released in 1972, being Atari's first game ever, and sparkling the beginning of the video game industry.
Pong simulates a table tennis match ("ping pong"), where you try to defeat your opponent by earning a higher score. Each player controls a paddle moving it vertically on the screen, and use it to hit a bouncing ball back and forth. You earn a point if your opponent is unable to return the ball to your side of the screen.
And now we're gonna learn how to create one ourselves in Perl and SDL.
Let's start by making a simple screen for our Pong clone. Open a file in your favourite text editor and type:
+ #!/usr/bin/perl
+ use strict;
+ use warnings;
+
+ use SDL;
+ use SDLx::App;
+
+ # create our main screen
+ my $app = SDLx::App->new(
+ width => 500,
+ height => 500,
+ title => 'My Pong Clone!',
+ dt => 0.02,
+ exit_on_quit => 1,
+ );
+
+ # let's roll!
+ $app->run;
Save this file as "pong.pl" and run it by typing on the
command line:
perl pong.pl
You should see a 500x500 black window entitled "My Pong Clone!". In our SDLx::App construction we also set a time interval (dt) of 0.02 for the game loop, and let it handle SDL_QUIT events for us. If any of the arguments above came as a surprise to you, please refer to previous chapters for an in-depth explanation.
There are three main game objects in Pong: the player's paddle, the enemy's paddle, and a bouncing ball.
Paddles are rectangles moving vertically on the screen, and can be
easily represented with SDLx::Rect objects. First, put
SDLx::Rect in your module's declarations:
use SDL;
use SDLx::App;
+ use SDLx::Rect;
Now let's add a simple hash reference in our code to store our player's
paddle, between the call to SDLx::App->new() and
$app->run.
We'll use a hash reference instead of just assigning a
SDLx::Rect to a variable because it will allow us to store
more information later on. If you were building a more complex game, you
should consider using actual objects. For now, a simple hash reference will
suffice:
+ my $player1 = {
+ paddle => SDLx::Rect->new( 10, $app->h / 2, 10, 40),
+ };
As we know, SDLx::Rect objects receive four arguments: x,
y, width and height, in this order. So in the code above we're creating a
10x40 paddle rect for player 1, on the left side of the screen (x =
10) and somewhat in the center (y = $app->h / 2).
Let's do the same for player 2, adding the following code right after the one above:
+ my $player2 = {
+ paddle => SDLx::Rect->new( $app->w - 20, $app->h / 2, 10, 40),
+ };
Player 2's paddle, also 10x40, needs to go to the right end of the
screen. So we make its x position as our screen's width minus
20. Since the paddle has a width of 10 itself and the x
position refers to the rect's top-left corner, it will leave a space of 10
pixels between its rightmost side and the end of the screen, just like we
did for player 1.
Finally, the bouncing ball, a 10x10 rect in the middle of the screen:
+ my $ball = {
+ rect => SDLx::Rect->new( $app->w / 2, $app->h / 2, 10, 10 ),
+ };
Yes, it's a "square ball", just like the original :)
Now that we created our game objects, let's add a 'show' handler to render them on the screen:
+ $app->add_show_handler(
+ sub {
+ # first, we clear the screen
+ $app->draw_rect( [0, 0, $app->w, $app->h], 0x000000FF );
+
+ # then we render the ball
+ $app->draw_rect( $ball->{rect}, 0xFF0000FF );
+
+ # ... and each paddle
+ $app->draw_rect( $player1->{paddle}, 0xFF0000FF );
+ $app->draw_rect( $player2->{paddle}, 0xFF0000FF );
+
+ # finally, we update the screen
+ $app->update;
+ }
+ );
Our approach is rather simple here, "clearing" the screen by painting a
black rectangle the size of the screen, then using draw_rect()
calls to paint opaque red (0xFF0000FF) rectangles in each
object's position.
The result can be seen on the screenshot below:
\includegraphics[width=0.5\textwidth]{../src/images/pong1.png} \caption{First view of our Pong clone} \label{fig:pong1}
It's time to let the player move the left paddle! Take a few moments to
recap what motion is all about: changing your object's position with
respect to time. If it's some sort of magical teleportation repositioning,
just change the (x,y) coordinates and be gone with it. If however, we're
talking about real motion, we need to move at a certain speed. Our paddle
will have constant speed, so we don't need to worry about acceleration.
Also, since it will only move vertically, we just need to add the vertical
(y) velocity. Let's call it v_y and add it to our paddle
structure:
my $player1 = {
paddle => SDLx::Rect->new( 10, $app->h / 2, 10, 40),
+ v_y => 0,
};
Ok, now we have an attribute for vertical velocity (v_y) in
our paddle, so what? How will this update the y position of
the paddle? Well, velocity is how much displacement happens in a unit of
time, like 20 km/h or 4 m/s. In our case, the unit of time is the app's
dt, so all we have to do is move the paddle v_y
pixels per dt. Here is where the motion handlers come in
handy:
+ # handles the player's paddle movement
+ $app->add_move_handler( sub {
+ my ( $step, $app ) = @_;
+ my $paddle = $player1->{paddle};
+ my $v_y = $player1->{v_y};
+
+ $paddle->y( $paddle->y + ( $v_y * $step ) );
+ });
If you recall previous chapters, the code above should be pretty
straightforward. When v_y is 0 at any given run cycle, the
paddle won't change its y position. If, however, there is a
vertical velocity, we update the y position based on how much
of the expected cycle time (our app's "dt") has passed. A value of 1 in
$step indicates a full cycle went through, and makes
$v_y * $step the same as $v_y * 1, thus, plain
$v_y - which is the desired speed for our cycle. Should the
handler be called in a shorter cycle, we'll move only the relative factor
of that.
We're not going to worry at this point about moving your nemesis' paddle, but since it uses the same motion mechanics of our player's, it won't hurt to prepare it:
my $player2 = {
paddle => SDLx::Rect->new( $app->w - 20, $app->h / 2, 10, 40),
+ v_y => 0,
};
And add a simple motion handler, just like our player's:
+ # handles AI's paddle movement
+ $app->add_move_handler( sub {
+ my ( $step, $app ) = @_;
+ my $paddle = $player2->{paddle};
+ my $v_y = $player2->{v_y};
+
+ $paddle->y( $paddle->y + ( $v_y * $step ) );
+ });
We have preset v_y to zero as the paddle's initial
velocity, so our player's paddle won't go haywire when the game starts. But
we still need to know when the user wants to move it up or down the screen.
In order to do that, we can bind the up and down arrow keys of the keyboard
to positive and negative velocities for our paddle, through an event hook.
Since we're going to use some event constants like SDLK_DOWN,
we need to load the SDL::Events module:
use SDL;
+ use SDL::Events;
use SDLx::App;
use SDLx::Rect;
Then we can proceed to create our event hook:
+ # handles keyboard events
+ $app->add_event_handler(
+ sub {
+ my ( $event, $app ) = @_;
+
+ # user pressing a key
+ if ( $event->type == SDL_KEYDOWN ) {
+
+ # up arrow key means going up (negative vel)
+ if ( $event->key_sym == SDLK_UP ) {
+ $player1->{v_y} = -2;
+ }
+ # down arrow key means going down (positive vel)
+ elsif ( $event->key_sym == SDLK_DOWN ) {
+ $player1->{v_y} = 2;
+ }
+ }
+ # user releasing a key
+ elsif ( $event->type == SDL_KEYUP ) {
+
+ # up or down arrow keys released, stop the paddle
+ if (
+ $event->key_sym == SDLK_UP
+ or $event->key_sym == SDLK_DOWN
+ ) {
+ $player1->{v_y} = 0;
+ }
+ }
+ }
+ );
Again, nothing new here. Whenever the user presses the up arrow key, we
want the paddle to go up. Keep in mind our origin point (0,0) in SDL is the
top-left corner, so a negative v_y will decrease the paddle's
y and send us up the screen. Alternatively, we add a
positive value to v_y whenever the user presses the down arrow
key, so the paddle will move down, away from the top of the screen.
When the user releases either the up or down arrow keys, we stop the paddle
by setting v_y to 0.
How about we animate the game ball? The movement itself is pretty
similar to our paddle's, except the ball will also have a horizontal
velocity ("v_x") component, letting it move all over the
screen.
First, we add the velocity components to our ball structure:
my $ball = {
rect => SDLx::Rect->new( $app->w / 2, $app->h / 2, 10, 10 ),
+ v_x => -2.7,
+ v_y => 1.8,
};
The ball will have an initial velocity of -2.7 horizontally (just as a
negative vertical velocity moves the object up, a negative horizontal
velocity will move it towards the left side of the screen), and 1.8
vertically. Next, we create a motion handler for the ball, updating the
ball's x and y position according to its
speed:
+ # handles the ball movement
+ $app->add_move_handler( sub {
+ my ( $step, $app ) = @_;
+ my $ball_rect = $ball->{rect};
+
+ $ball_rect->x( $ball_rect->x + ($ball->{v_x} * $step) );
+ $ball_rect->y( $ball_rect->y + ($ball->{v_y} * $step) );
+ });
This is just like our paddle's motion handler: we update the ball's
x and y positioning on the screen according to
the current velocity. If you are paying attention, however, you probably
realized the code above is missing a very important piece of logic. Need a
clue? Try running the game as it is. You'll see the ball going, going,
and... gone!
We need to make sure the ball is bound to the screen. That is, it needs to collide and bounce back whenever it reaches the top and bottom edges of the screen. So let's change our ball's motion handler a bit, adding this functionality:
# handles the ball movement
$app->add_move_handler( sub {
my ( $step, $app ) = @_;
my $ball_rect = $ball->{rect};
$ball_rect->x( $ball_rect->x + ($ball->{v_x} * $step) );
$ball_rect->y( $ball_rect->y + ($ball->{v_y} * $step) );
+ # collision to the bottom of the screen
+ if ( $ball_rect->bottom >= $app->h ) {
+ $ball_rect->bottom( $app->h );
+ $ball->{v_y} *= -1;
+ }
+
+ # collision to the top of the screen
+ elsif ( $ball_rect->top <= 0 ) {
+ $ball_rect->top( 0 );
+ $ball->{v_y} *= -1;
+ }
});
If the new x ("left") and y ("top") values
would take the ball totally or partially off the screen, we replace it with
the farthest position possible (making it "touch" that edge of the screen)
and reverse v_y, so it will go the opposite way on the next
cycle, bouncing back into the screen.
So far, so good. But what should happen when the ball hits the left or right edges of the screen? Well, according to the rules of Pong, this means the player on the opposite side scored a point, and the ball should go back to the center of the screen. Let's begin by adding a 'score' attribute for each player:
my $player1 = {
paddle => SDLx::Rect->new( 10, $app->h / 2, 10, 40),
v_y => 0,
+ score => 0,
};
my $player2 = {
paddle => SDLx::Rect->new( $app->w - 20, $app->h / 2, 10, 40),
v_y => 0,
+ score => 0,
};
Now we should teach the ball's motion handler what to do when it reaches the left and right corners:
# handles the ball movement
$app->add_move_handler( sub {
my ( $step, $app ) = @_;
my $ball_rect = $ball->{rect};
$ball_rect->x( $ball_rect->x + ($ball->{v_x} * $step) );
$ball_rect->y( $ball_rect->y + ($ball->{v_y} * $step) );
# collision to the bottom of the screen
if ( $ball_rect->bottom >= $app->h ) {
$ball_rect->bottom( $app->h );
$ball->{v_y} *= -1;
}
# collision to the top of the screen
elsif ( $ball_rect->top <= 0 ) {
$ball_rect->top( 0 );
$ball->{v_y} *= -1;
}
+ # collision to the right: player 1 score!
+ elsif ( $ball_rect->right >= $app->w ) {
+ $player1->{score}++;
+ reset_game();
+ return;
+ }
+
+ # collision to the left: player 2 score!
+ elsif ( $ball_rect->left <= 0 ) {
+ $player2->{score}++;
+ reset_game();
+ return;
+ }
});
If the ball's right hits the right end of the screen (the app's width),
we increase player 1's score, call reset_game() and return
without updating the ball's position. If the ball's left hits the left end
of the screen, we do the same for player 2.
We want the reset_game() function called above to set the
ball back on the center of the screen, so let's make it happen:
+ sub reset_game {
+ $ball->{rect}->x( $app->w / 2 );
+ $ball->{rect}->y( $app->h / 2 );
+ }
We already learned how to do some simple collision detection, namely between the ball and the edges of the screen. Now it's time to take it one step further and figure out how to check whether the ball and the paddles are overlapping one another (colliding, or rather, intersecting). This is done via the Separating Axis Theorem, which roughly states that two convex shapes in a 2D plane are not intersecting if and only if we can place a line separating them. Since our rect objects (the ball and paddles) are both axis-aligned, we can simply pick one, and there will be only 4 possible lines to test: its left, right, top and bottom. If the other object is completely to the other side of any of those lines, then there is no collision. But if all four conditions are false, they are intersecting.
To put it in more general terms, if we have 2 rects, A and B, we can establish the following conditions, illustrated by the figure below:
\includegraphics[width=0.9\textwidth]{../src/images/collision.png} \caption{if B is completely to the left, right, top or bottom of A, they do NOT intersect} \label{fig:pong1}
Keeping in mind that our origin point (0,0) in SDL is the top-left
corner, we can translate the rules above to the following generic
check_collision() function, receiving two rect objects and
returning true if they collide:
+ sub check_collision {
+ my ($A, $B) = @_;
+
+ return if $A->bottom < $B->top;
+ return if $A->top > $B->bottom;
+ return if $A->right < $B->left;
+ return if $A->left > $B->right;
+
+ # if we got here, we have a collision!
+ return 1;
+ }
We can now use it in the ball's motion handler to see if it hits any of the paddles:
# handles the ball movement
$app->add_move_handler( sub {
my ( $step, $app ) = @_;
my $ball_rect = $ball->{rect};
$ball_rect->x( $ball_rect->x + ($ball->{v_x} * $step) );
$ball_rect->y( $ball_rect->y + ($ball->{v_y} * $step) );
# collision to the bottom of the screen
if ( $ball_rect->bottom >= $app->h ) {
$ball_rect->bottom( $app->h );
$ball->{v_y} *= -1;
}
# collision to the top of the screen
elsif ( $ball_rect->top <= 0 ) {
$ball_rect->top( 0 );
$ball->{v_y} *= -1;
}
# collision to the right: player 1 score!
elsif ( $ball_rect->right >= $app->w ) {
$player1->{score}++;
reset_game();
return;
}
# collision to the left: player 2 score!
elsif ( $ball_rect->left <= 0 ) {
$player2->{score}++;
reset_game();
return;
}
+ # collision with player1's paddle
+ elsif ( check_collision( $ball_rect, $player1->{paddle} )) {
+ $ball_rect->left( $player1->{paddle}->right );
+ $ball->{v_x} *= -1;
+ }
+
+ # collision with player2's paddle
+ elsif ( check_collision( $ball_rect, $player2->{paddle} )) {
+ $ball->{v_x} *= -1;
+ $ball_rect->right( $player2->{paddle}->left );
+ }
});
That's it! If the ball hits player1's paddle, we reverse its horizontal
velocity (v_x) to make it bounce back, and set its left edge
to the paddle's right so they don't overlap. Then we do the exact same
thing for the other player's paddle, except this time we set the ball's
right to the paddle's left - since the ball is coming from the other
side.
Our Pong game is almost done now. We record the score, the ball bounces around, we keep track of each player's score, and we can move the left paddle with the up and down arrow keys. But this will be a very dull game unless our nemesis moves too!
There are several complex algorithms to model artificial intelligence, but we don't have to go that far for a simple game like this. What we're going to do is make player2's paddle follow the ball wherever it goes, by adding the following to its motion handler:
# handles AI's paddle movement
$app->add_move_handler( sub {
my ( $step, $app ) = @_;
my $paddle = $player2->{paddle};
my $v_y = $player2->{v_y};
+ if ( $ball->{rect}->y > $paddle->y ) {
+ $player2->{v_y} = 1.5;
+ }
+ elsif ( $ball->{rect}->y < $paddle->y ) {
+ $player2->{v_y} = -1.5;
+ }
+ else {
+ $player2->{v_y} = 0;
+ }
$paddle->y( $paddle->y + ( $v_y * $step ) );
});
If the ball's "y" value (its top) is greater than the
nemesis' paddle, it means the ball is below it, so we give the paddle a
positive velocity, making it go downwards. On the other hand, if the ball
has a lower "y" value, we set the nemesis' v_y to
a negative value, making it go up. Finally, if the ball is somewhere in
between those two values, we keep the paddle still.
How about we display the score so the player can see who's winning? To render a text string in SDL, we're going to use the SDLx::Text module, so let's add it to the beginning of our code:
use SDL;
use SDL::Events;
use SDLx::App;
use SDLx::Rect;
+ use SDLx::Text;
Now we need to create the score object:
+ my $score = SDLx::Text->new( font => 'font.ttf', h_align => 'center' );
The font parameter specifies the path to a TrueType Font.
Here we are loading the 'font.ttf' file, so feel free to change this
to whatever font you have in your system. The h_align
parameter lets us choose a horizontal alignment for the text we put in the
object. It defaults to 'left', so we make it 'center'
instead.
All that's left is using this object to write the score on the screen, so we update our 'show' handler:
$app->add_show_handler(
sub {
# first, we clear the screen
$app->draw_rect( [0, 0, $app->w, $app->h], 0x000000FF );
# then we render the ball
$app->draw_rect( $ball->{rect}, 0xFF0000FF );
# ... and each paddle
$app->draw_rect( $player1->{paddle}, 0xFF0000FF );
$app->draw_rect( $player2->{paddle}, 0xFF0000FF );
+ # ... and each player's score!
+ $score->write_to(
+ $app,
+ $player1->{score} . ' x ' . $player2->{score}
+ );
# finally, we update the screen
$app->update;
}
);
The write_to() call will write to any surface passed as the
first argument - in our case, the app itself. The second argument, as you
probably figured, is the string to be rendered. Note that the string's
position is relative to the surface it writes to, and defaults to (0,0).
Since we told it to center horizontally, it will write our text to the
top/center, instead of top/left.
The result, and our finished game, can be seen on the figure below:
\includegraphics[width=0.5\textwidth]{../src/images/pong2.png} \caption{our finished Pong clone, in all its glory} \label{fig:pong2}
See if you can solve the exercises above by yourself, to make sure you understand what is what and how to do things in SDL Perl. Once you're done, check out the answers below. Of course, there's always more than one way to do things, so the ones below are not the only possible answers.
1. To make the ball restart at a random direction, we can improve our
reset_game() function to set the ball's v_x and
v_y to a random value between, say, 1.5 and 2.5, or -1.5 and
-2.5:
sub reset_game {
$ball->{rect}->x( $app->w / 2 );
$ball->{rect}->y( $app->h / 2 );
+ $ball->{v_x} = (1.5 + int rand 1) * (rand 2 > 1 ? 1 : -1);
+ $ball->{v_y} = (1.5 + int rand 1) * (rand 2 > 1 ? 1 : -1);
}
2. We can either choose one colour set for both paddles or one for each. Let's go with just one set, as an array of hex values representing our colours. We'll also hold the index for the current colour for each player:
+ my @colours = qw( 0xFF0000FF 0x00FF00FF 0x0000FFFF 0xFFFF00FF );
my $player1 = {
paddle => SDLx::Rect->new( 10, $app->h / 2, 10, 40),
v_y => 0,
score => 0,
+ colour => 0,
};
my $player2 = {
paddle => SDLx::Rect->new( $app->w - 20, $app->h / 2, 10, 40),
v_y => 0,
score => 0,
+ colour => 0,
};
Next we make it update the colour every time the ball hits
the paddle:
# handles the ball movement
$app->add_move_handler( sub {
my ( $step, $app ) = @_;
my $ball_rect = $ball->{rect};
$ball_rect->x( $ball_rect->x + ($ball->{v_x} * $step) );
$ball_rect->y( $ball_rect->y + ($ball->{v_y} * $step) );
# collision to the bottom of the screen
if ( $ball_rect->bottom >= $app->h ) {
$ball_rect->bottom( $app->h );
$ball->{v_y} *= -1;
}
# collision to the top of the screen
elsif ( $ball_rect->top <= 0 ) {
$ball_rect->top( 0 );
$ball->{v_y} *= -1;
}
# collision to the right: player 1 score!
elsif ( $ball_rect->right >= $app->w ) {
$player1->{score}++;
reset_game();
return;
}
# collision to the left: player 2 score!
elsif ( $ball_rect->left <= 0 ) {
$player2->{score}++;
reset_game();
return;
}
# collision with player1's paddle
elsif ( check_collision( $ball_rect, $player1->{paddle} )) {
$ball_rect->left( $player1->{paddle}->right );
$ball->{v_x} *= -1;
+ $player1->{colour} = ($player1->{colour} + 1) % @colours;
}
# collision with player2's paddle
elsif ( check_collision( $ball_rect, $player2->{paddle} )) {
$ball->{v_x} *= -1;
$ball_rect->right( $player2->{paddle}->left );
+ $player2->{colour} = ($player2->{colour} + 1) % @colours;
}
});
Finally, we change our 'show' handler to use the current colour
referenced by colour, instead of the previously hardcoded red
(0xFF0000FF):
$app->add_show_handler(
sub {
# first, we clear the screen
$app->draw_rect( [0, 0, $app->w, $app->h], 0x000000FF );
# then we render the ball
$app->draw_rect( $ball->{rect}, 0xFF0000FF );
# ... and each paddle
- $app->draw_rect( $player1->{paddle}, 0xFF0000FF );
+ $app->draw_rect( $player1->{paddle}, $colours[ $player1->{colour} ] );
- $app->draw_rect( $player2->{paddle}, 0xFF0000FF );
+ $app->draw_rect( $player2->{paddle}, $colours[ $player2->{colour} ] );
# ... and each player's score!
$score->write_to(
$app,
$player1->{score} . ' x ' . $player2->{score}
);
# finally, we update the screen
$app->update;
}
);
This chapter's content graciously provided by Breno G. de Oliveira
(garu).
First we will make our window with a fixed size so we can place our art work in a fixed format.
use strict;
use warnings;
use SDL;
use SDL::Event;
use SDL::Events;
use SDLx::App;
# create our main screen
my $app = SDLx::App->new(
w => 400,
h => 512,
exit_on_quit => 1,
dt => 0.2,
title => 'SDLx Tetris'
);
We can load our artwork simply by storing an array of
SDLx::Surfaces.
use SDL;
+use SDLx::Surface;
...
+my $back = SDLx::Surface->load( 'data/tetris_back.png' );
+my @piece = (undef);
+push(@piece, SDLx::Surface->load( "data/tetris_$_.png" )) for(1..7);
The background is held in the $back surface, and the pieces
are held in the @piece array. Later on we will blit these onto
our main screen as we need.
In Tetris the blocks are critical pieces of data that must be represented in code such that it is easy to access, and quick to perform calculations on. A hash will allow us to quickly access our pieces, based on their keys.
my %pieces = (
I => [0,5,0,0,
0,5,0,0,
0,5,0,0,
0,5,0,0],
J => [0,0,0,0,
0,0,6,0,
0,0,6,0,
0,6,6,0],
L => [0,0,0,0,
0,2,0,0,
0,2,0,0,
0,2,2,0],
O => [0,0,0,0,
0,3,3,0,
0,3,3,0,
0,0,0,0],
S => [0,0,0,0,
0,4,4,0,
4,4,0,0,
0,0,0,0],
T => [0,0,0,0,
0,7,0,0,
7,7,7,0,
0,0,0,0],
Z => [0,0,0,0,
1,1,0,0,
0,1,1,0,
0,0,0,0],
);
Further more we have a 1-dimensional array for each piece that represents a grid of the piece.
The grid of each piece is filled with empty spaces and a number from 1 to 7. When this grid is imposed on the game grid, we can use the non zero number to draw the write piece block on to it.
What is colliding to what?
Are both colliers going to collide?
Multiple directions of collisions?
Should they trigger a game event? React?
We are now ready to write another complete game. Instead of listing the code and then explaining it, I will go through the process of how I might write it.
Puzz is a simple rearrangment puzzle. A random image from the folder Puzz is in is chosen and broken into a 4x4 grid. The top left corner piece is then taken away, and every other piece is then moved to a random position, scrambling the image up. The goal is then to move pieces which are in the 4 squares adjacent to the empty square on to the empty square, and eventually restore the image.
\includegraphics[width=0.5\textwidth]{../src/images/puzz1.png} \caption{Credits to Sebastian Riedel (kraih.com) for the Perl6 logo used with permission in the application.} \label{fig:puzz}
So, first thing we do is create the window. I've decided I want each piece to be 100x100, so the window needs to be 400x400.
use strict;
use warnings;
use SDL;
use SDLx::App;
my $App = SDLx::App->new(w => 400, h => 400, t => 'Puzz');
Next thing we usually do is figure out what global vars we will be needing. As with $App, I like to name my globals with title case, so they are easily distinguishable from lexical vars. The globals we need are the grid (the positions of the pieces), the images we have to use, the current image, and a construct that will give us piece movement, along with an animation.
my @Grid;
my @Img;
my $CurrentImg;
my %Move;
For now, lets fill in @Grid with what it's going to look like:
@Grid = (
[0, 1, 2, 3],
[4, 5, 6, 7],
[8, 9, 10, 11],
[12, 13, 14, 15],
);
0 will be our blank piece, but we could have chosen it to
be any other number. When the grid looks like this, it's solved, so
eventually we will need a way to scramble it. It's good enough for now,
though.
To load the images, we would normally use SDLx::Surface,
but we're going to do it the libsdl way with SDL::Image
because we need to do our own error handling.
use SDL::Image;
use SDL::GFX::Rotozoom 'SMOOTHING_ON';
while(<./*>) {
if(-f and my $i = SDL::Image::load($_)) {
$i = SDL::GFX::Rotozoom::surface_xy($i, 0, 400 / $i->w, 400 / $i->h, SMOOTHING_ON);
push @Img, $i;
}
else
{
warn "Cannot Load $_: " . SDL::get_error() if $_ =~ /jpg|png|bmp/;
}
}
$CurrentImg = $Img[rand @Img];
die "Please place images in the Current Folder" if $#Img < 0;
We just go through every file in the current directory, and try to load
it as an image. SDL::Image::load will return false if there
was an error, so we want to discard it when that happens. If we used
SDLx::Surface to load the images, we would get a warning every
time a file fails to load as an image, which we don't want. The my $i
= SDL::Image::load($_) is just an idiom for setting a var and
checking it for truth at the same time.
We want the image to be 400x400, and SDL::GFX::Rotozoom
makes this possible. The two Rotozoom functions that are the most useful
are surface and surface_xy. They work like
this:
$zoomed_src = SDL::GFX::Rotozoom::surface($src, $angle, $zoom, $smoothing)
$zoomed_src = SDL::GFX::Rotozoom::surface_xy($src, $angle, $x_zoom, $y_zoom, $smoothing)
The zoom values are the multiplier for that component, or for both
components at once as with $zoom. $angle is an
angle of rotation in degrees. $smoothing should be
SMOOTHING_ON or SMOOTHING_OFF (which can be
exported by SDL::GFX::Rotozoom) or just 1 or 0.
Once the image is zoomed, it is added to the image array. The current image is then set to a random value of the array.
The next part I like to write is the events. We're going to make Escape
quit, and left click will move the pieces around. We use
SDL::Events for the constants.
use SDL::Events;
sub on_event {
my ($e) = @_;
if($e->type == SDL_QUIT or $e->type == SDL_KEYDOWN and $e->key_sym == SDLK_ESCAPE) {
$App->stop;
}
elsif($e->type == SDL_MOUSEBUTTONDOWN and $e->button_button == SDL_BUTTON_LEFT) {
...
}
}
$App->add_event_handler(\&on_event);
# $App->add_move_handler(\&on_move);
# $App->add_show_handler(\&on_show);
$App->run;
Once we have something like this, it's a good time to put some
warn messages in to make sure the inputs are working
correctly. Once they are, it's time to fill it in.
my $x = int($e->button_x / 100);
my $y = int($e->button_y / 100);
if(!%Move and $Grid[$y][$x]) {`
...
}
From the pixel coordinates of the click (0 to 399), we want to find out
the grid coordinates (0 to 3), so we divide both components by 100 and
round them down. Then, we only want to continue on to see if that piece can
move if no other piece is moving (%Move is false), and the
piece clicked isn't the blank piece (0).
for([-1, 0], [0, -1], [1, 0], [0, 1]) {
my $nx = $x + $_->[0];
my $ny = $y + $_->[1];
if($nx >= 0 and $nx < 4 and $ny >= 0 and $ny < 4 and !$Grid[$ny][$nx]) {
...
}
}
We check that the blank piece is in the 4 surrounding places by
constructing 4 vectors. These will take us to those squares. The
x component is first and the second is y. We
iterate through them, setting $nx and $ny to the
new position. Then if both $nx and $ny are within
the grid (0 to 3), and that position in the grid is 0, we can move the
piece to the blank square.
%Move = (
x => $x,
y => $y,
x_dir => $_->[0],
y_dir => $_->[1],
offset => 0,
);
To make a piece move, we construct the move hash with all the
information it needs to move the piece. The x and
y positions of the piece, the x and
y directions it will be moving (the vector), and it's current
pixel offset from it's position (for the moving animation), which starts at
0.
Next we will write the move handler. All it needs to do is move any moving piece along by updating the offset, and click it in to where it's being moved to when it has moved the whole way (offset is 100 or more).
sub on_move {
if(%Move) {
$Move{offset} += 30 * $_[0];
if($Move{offset} >= 100) {
$Grid[$Move{y} + $Move{y_dir}][$Move{x} + $Move{x_dir}] = $Grid[$Move{y}][$Move{x}];
$Grid[$Move{y}][$Move{x}] = 0;
undef %Move;
}
}
}
30 has been arbitrarily chosen as the speed of the move, as it felt the
best after a little playing and tweaking. Always remember to multiply
things like this by the step value in $_[0] so that the
animation moves in correct time with the updating.
Once the offset is 100 or more, the grid place that the piece is moving
to is set to the value of the piece, and the piece is set to the blank
value. The move is then finished, so %Move is deleted.
Now that we have all the functionality we need it's finally time to see the game.
sub on_show {
$App->draw_rect( [0,0,$App->w,$App->h], 0 );
for my $y (0..3) {
for my $x (0..3) {
...
}
}
$App->flip;
}
We start the show handler by drawing a black rect over the entire app.
Entire surface and black are the defaults of draw_rect, so
letting it use the defaults is good. Next we iterate through a
y and x of 0 to 3 so that we can go through each
piece of the grid. At the end of the handler we update the app with a call
to flip.
next unless my $val = $Grid[$y][$x];
my $xval = $val % 4;
my $yval = int($val / 4);
my $move = %Move && $Move{x} == $x && $Move{y} == $y;
...
Inside the two loops we put this. First we set $val to the
grid value at the current position, and we skip to the next piece if it's
the blank piece. We have the x and y coordinates
of where that piece is on the board, but we need to figure out where it is
on the image. If you refer back to the initialisation of the grid, the two
operations to find the values should make sense. $move is set
with a bool of whether it is this piece that is moving, if there is a piece
moving at all.
$App->blit_by(
$CurrentImg,
[$xval * 100, $yval * 100, 100, 100],
[$x * 100 + ($move ? $Move{offset} * $Move{x_dir} : 0),
$y * 100 + ($move ? $Move{offset} * $Move{y_dir} : 0)]
);
Now that we have all of this, we can blit the portion of the current
image we need to the app. We use blit_by because the image
we're blitting isn't an SDLx::Surface (because we didn't load it as one),
but the app is. Here's how blit_by works as opposed to
blit:
$src->blit($dest, $src_rect, $dest_rect)
$dest->blit_by($src, $src_rect, $dest_rect)
The portion we need is from the $xval and
$yval, and where it needs to go to is from $x and
$y. All are multiplied by 100 because we're dealing with 0 to
300, not 0 to 3. If the piece is moving, the offset multiplied by the
diretion is added to the position.
When the code is run with all 3 handlers, we have a fully working game. The pieces move around nicely when clicked. The only things it still needs are a shuffled grid and a way to check if the player has won. To imlement these two things, we will make two more functions.
use List::Util 'shuffle';
sub new_grid {
my @new = shuffle(0..15);
@Grid = map { [@new[ $_*4..$_*4+3 ]] } 0..3;
$CurrentImg = $Img[rand @Img];
}
We will replace the grid initialising we did with this sub. First it
shffles the numbers 0 through 15 with List::Util::shuffle.
This array is then arranged into a 2D grid with a map and put
in to @Grid. Setting the current image is also put into this sub.
sub won {
my $correct = 0;
for(@Grid) {
for(@$_) {
return 0 if $correct != $_;
$correct++;
}
}
return 1;
}
This sub returns whether the grid is in the winning configuration, that is, all piece values are in order from 0 to 15.
Now we put a call to new_grid to replace the grid
initialisation we had before. We put won into the event
handler to make click call new_grid if you have won. Finally,
won is put into the show handler to show the blank piece if
you have won.
Here is the finished code:
use strict;
use warnings;
use SDL;
use SDLx::App;
use SDL::Events;
use SDL::Image;
use SDL::GFX::Rotozoom 'SMOOTHING_ON';
use List::Util 'shuffle';
my $App = SDLx::App->new(w => 400, h => 400, t => 'Puzz');
my @Grid;
my @Img;
my $CurrentImg;
my %Move;
while(<./*>) {
if(-f and my $i = SDL::Image::load($_)) {
$i = SDL::GFX::Rotozoom::surface_xy($i, 0, 400 / $i->w, 400 / $i->h, SMOOTHING_ON);
push @Img, $i;
}
else
{
warn "Cannot Load $_: " . SDL::get_error() if $_ =~ /jpg|png|bmp/;
}
}
die "Please place images in the Current Folder" if $#Img < 0;
new_grid();
sub on_event {
my ($e) = @_;
if($e->type == SDL_QUIT or $e->type == SDL_KEYDOWN and $e->key_sym == SDLK_ESCAPE) {
$App->stop;
}
elsif($e->type == SDL_MOUSEBUTTONDOWN and $e->button_button == SDL_BUTTON_LEFT) {
my($x, $y) = map { int($_ / 100) } $e->button_x, $e->button_y;
if(won()) {
new_grid();
}
elsif(!%Move and $Grid[$y][$x]) {
for([-1, 0], [0, -1], [1, 0], [0, 1]) {
my($nx, $ny) = ($x + $_->[0], $y + $_->[1]);
if($nx >= 0 and $nx < 4 and $ny >= 0 and $ny < 4 and !$Grid[$ny][$nx]) {
%Move = (
x => $x,
y => $y,
x_dir => $_->[0],
y_dir => $_->[1],
offset => 0,
);
}
}
}
}
}
sub on_move {
if(%Move) {
$Move{offset} += 30 * $_[0];
if($Move{offset} >= 100) {
$Grid[$Move{y} + $Move{y_dir}][$Move{x} + $Move{x_dir}] = $Grid[$Move{y}][$Move{x}];
$Grid[$Move{y}][$Move{x}] = 0;
undef %Move;
}
}
}
sub on_show {
$App->draw_rect( [0,0,$App->w,$App->h], 0 );
for my $y (0..3) {
for my $x (0..3) {
next if not my $val = $Grid[$y][$x] and !won();
my $xval = $val % 4;
my $yval = int($val / 4);
my $move = %Move && $Move{x} == $x && $Move{y} == $y;
$App->blit_by(
$CurrentImg,
[$xval * 100, $yval * 100, 100, 100],
[$x * 100 + ($move ? $Move{offset} * $Move{x_dir} : 0),
$y * 100 + ($move ? $Move{offset} * $Move{y_dir} : 0)]
);
}
}
$App->flip;
}
sub new_grid {
my @new = shuffle(0..15);
@Grid = map { [@new[ $_*4..$_*4+3 ]] } 0..3;
$CurrentImg = $Img[rand @Img];
}
sub won {
my $correct = 0;
for(@Grid) {
for(@$_) {
return 0 if $correct != $_;
$correct++;
}
}
return 1;
}
$App->add_event_handler(\&on_event);
$App->add_move_handler(\&on_move);
$App->add_show_handler(\&on_show);
$App->run;
You now hopefully know more of the process that goes in to creating a
simple game. The process of creating a complex game is similar, it just
requires more careful planning. You should have also picked up a few other
tricks, like with SDL::GFX::Rotozoom,
SDL::Image::load and blit_by.
$ARGV[0]. The grid will then be 5x5 if $ARGV[0]
is 5 and so on.This chapter's content graciously provided by Blaizer.
Sound and Music in SDL are handled by the Audio and
SDL_Mixer components. Enabling Audio devices is
provided with the Core SDL Library and only supports wav files.
SDL_Mixer supports more audio file formats and has additional
features that we need for sound in Game Development.
Similarly to video in SDL, there are several way for perl developers to
access the Sound components of SDL. For the plain Audio
component the SDL::Audio and related modules are available.
SDL_Mixer is supported with th SDL::Mixer module.
There is currently a SDLx::Sound module in the work, but not
completed at the time of writing this manual. For that reason this chapter
will use SDL::Audio and SDL::Mixer.
To begin using sound we must enable and open an audiospec:
use strict;
use warnings;
use SDL;
use Carp;
use SDL::Audio;
use SDL::Mixer;
SDL::init(SDL_INIT_AUDIO);
unless( SDL::Mixer::open_audio( 44100, AUDIO_S16SYS, 2, 4096 ) == 0 )
{
Carp::croak "Cannot open audio: ".SDL::get_error();
}
open_audio will open an audio device with frequency at
44100 Mhz, audio format AUDIO_S16SYS (Note: This is currently the most
portable format, however there are others), 2 channels and a chunk size of
4096. Fiddle with these values if you are comfortable with sound
terminology and techniques.
Next we will load sound samples that generally used for sound effects
and the like. Currently SDL_Mixer reserves samples for
.WAV, .AIFF, .RIFF
.OGG, and .VOC formats.
Samples run on one of the 2 channels that we opened up, while the other channel will be reserved for multiple plays of the sample. To load samples we will be doing the following:
+use SDL::Mixer::Samples;
+#Brillant Lazer Sound from http://www.freesound.org/samplesViewSingle.php?id=30935
+my $sample = SDL::Mixer::Samples::load_WAV('data/sample.wav');
+unless($sample)
+{
+ Carp::croak "Cannot load file data/sample.wav: ".SDL::get_error();
+}
Now we can play that sample on any open channel looping forever:
use SDL::Mixer::Samples;
+use SDL::Mixer::Channels;
my $sample = SDL::Mixer::Samples::load_WAV('data/sample.wav');
unless( $sample)
{
Carp::croak "Cannot load file data/sample.wav: ".SDL::get_error();
}
+my $playing_channel = SDL::Mixer::Channels::play_channel( -1, $sample, 0 );
play_channel allows us to assign a sample to the channel
-1 which indicates any open channel. 0 indicates
we want to play the sample only once.
Note that since the sound will be playing in an external process we will
need to keep the perl script running. In a game this is no problem but for
a single script like this we can just use a simple sleep
function. Once we are done we can go ahead and close the audio device.
+sleep(1);
+SDL::Mixer::close_audio();
Next we will use SDL::Mixer::Music to add a background
music to our script here.
use SDL::Mixer::Channels;
+use SDL::Mixer::Music;
+#Load our awesome music from http://8bitcollective.com
+my $background_music =
+ SDL::Mixer::Music::load_MUS('data/music/01-PC-Speaker-Sorrow.ogg');
+unless( $background_music )
+{
+ Carp::croak "Cannot load music file data/music/01-PC-Speaker-Sorrow.ogg: ".SDL::get_error() ;
+}
Music types in SDL::Mixer run in a seperate channel from
our samples which allows us to have sound effects (like jump, or lasers
etc) to play at the same time.
+SDL::Mixer::Music::play_music($background_music,0);
play_music also takes a parameter for how many loops you
would like to play the song for, where 0 is 1.
To stop the music we can call halt_music.
sleep(2);
+SDL::Mixer::Music::halt_music();
SDL::Mixer::close_audio();
Controlling Volume can be as simple as:
#All channels indicated by the -1 SDL::Mixer::Channels::volume(-1,10); #Specifically for the Music SDL::Mixer::Music::volume_music( 10 );Volumes can be set at anytime and range from
1-100.
use strict;
use warnings;
use SDL;
use Carp;
use SDL::Audio;
use SDL::Mixer;
use SDL::Mixer::Samples;
use SDL::Mixer::Channels;
use SDL::Mixer::Music;
SDL::init(SDL_INIT_AUDIO);
unless( SDL::Mixer::open_audio( 44100, AUDIO_S16SYS, 2, 4096 ) == 0 )
{
Carp::croak "Cannot open audio: ".SDL::get_error();
}
my $sample = SDL::Mixer::Samples::load_WAV('data/sample.wav');
unless( $sample)
{
Carp::croak "Cannot load file data/sample.wav: ".SDL::get_error();
}
my $playing_channel = SDL::Mixer::Channels::play_channel( -1, $sample, 0 );
#Load our awesome music from http://8bitcollective.com
my $background_music = SDL::Mixer::Music::load_MUS('data/music/01-PC-Speaker-Sorrow.ogg');
unless( $background_music )
{
Carp::croak "Cannot load music file data/music/01-PC-Speaker-Sorrow.ogg:".SDL::get_error();
}
SDL::Mixer::Music::play_music( $background_music,0 );
sleep(2);
SDL::Mixer::Music::halt_music();
SDL::Mixer::close_audio;
Now that we know how to prepare and play simple sounds we will apply it
to an SDLx::App.
SDLx::App will initialize everything normally for us.
However for a stream line application it is recommend to initialize only
the things we need. In this case that is SDL_INIT_VIDEO and
SDL_INIT_AUDIO.
use strict;
use warnings;
use SDL;
use Carp;
use SDLx::App;
use SDL::Audio;
use SDL::Mixer;
use SDL::Event;
use SDL::Events;
use SDL::Mixer::Music;
use SDL::Mixer::Samples;
use SDL::Mixer::Channels;
my $app = SDLx::App->new(
init => SDL_INIT_AUDIO | SDL_INIT_VIDEO,
width => 250,
height => 75,
title => "Sound Event Demo",
eoq => 1
);
It is highly recommended to perform all resource allocations before a
SDLx::App::run() method is called.
# Initialize the Audio
unless ( SDL::Mixer::open_audio( 44100, AUDIO_S16SYS, 2, 4096 ) == 0 ) {
Carp::croak "Cannot open audio: " . SDL::get_error();
}
#Something to show while we play music and sounds
my $channel_volume = 100;
my $music_volume = 100;
my $laser_status = 'none';
my $music_status = 'not playing';
# Load our sound resources
my $laser = SDL::Mixer::Samples::load_WAV('data/sample.wav');
unless ($laser) {
Carp::croak "Cannot load sound: " . SDL::get_error();
}
my $background_music =
SDL::Mixer::Music::load_MUS('data/music/01-PC-Speaker-Sorrow.ogg');
unless ($background_music) {
Carp::croak "Cannot load music: " . SDL::get_error();
}
For the purposes of describing the current state of the music lets draw
text to the screen in a show_handler.
$app->add_show_handler(
sub {
$app->draw_rect([0,0,$app->w,$app->h], 0 );
$app->draw_gfx_text( [10,10], [255,0,0,255], "Channel Volume : $channel_volume" );
$app->draw_gfx_text( [10,25], [255,0,0,255], "Music Volume : $music_volume" );
$app->draw_gfx_text( [10,40], [255,0,0,255], "Laser Status : $laser_status" );
$app->draw_gfx_text( [10,55], [255,0,0,255], "Music Status : $music_status" );
$app->update();
}
);
This will draw the channel volume of our samples, and the volume of the music. It will also print the status of our two sounds in the application.
Finally our event handler will do the actual leg work and trigger the music and sound as we need it.
$app->add_event_handler(
sub {
my $event = shift;
if ( $event->type == SDL_KEYDOWN ) {
my $keysym = $event->key_sym;
my $keyname = SDL::Events::get_key_name($keysym);
if ( $keyname eq 'space' ) {
$laser_status = 'PEW!';
#fire lasers!
SDL::Mixer::Channels::play_channel( -1, $laser, 0 );
}
elsif ( $keyname eq 'up' ) {
$channel_volume += 5 unless $channel_volume == 100;
}
elsif ( $keyname eq 'down' ) {
$channel_volume -= 5 unless $channel_volume == 0;
}
elsif ( $keyname eq 'right' ) {
$music_volume += 5 unless $music_volume == 100;
}
elsif ( $keyname eq 'left' ) {
$music_volume -= 5 unless $music_volume == 0;
}
elsif ( $keyname eq 'return' ) {
my $playing = SDL::Mixer::Music::playing_music();
my $paused = SDL::Mixer::Music::paused_music();
if ( $playing == 0 && $paused == 0 ) {
SDL::Mixer::Music::play_music( $background_music, 1 );
$music_status = 'playing';
}
elsif ( $playing && !$paused ) {
SDL::Mixer::Music::pause_music();
$music_status = 'paused'
}
elsif ( $playing && $paused ) {
SDL::Mixer::Music::resume_music();
$music_status = 'resumed playing';
}
}
SDL::Mixer::Channels::volume( -1, $channel_volume );
SDL::Mixer::Music::volume_music($music_volume);
}
}
);
The above event handler fires the laser on pressing the 'Space' key. Go
ahead and press it multiple times as if you are firing a gun in a game! You
will notice that depending on how fast you fire the laser the application
will still manage to overlap the sounds as needed. The sample overlapping
is accomplished by requiring multiple channels in the
open_audio call. If your game has lots of samples that may
play at the same time you may need more channels allocated. Additionally
you can see that the volume control is easily managed both on the channels
and the music with just incrementing or decrementing a value and calling
the appropriate function.
Finally it is worth noticing the various state the background music can be in.
Lets run this application and the make sure to clean up the audio on the way out. $app->run(); SDL::Mixer::Music::halt_music(); SDL::Mixer::close_audio;
my $app = SDLx::App->new(
init => SDL_INIT_AUDIO | SDL_INIT_VIDEO,
width => 250,
height => 75,
title => "Sound Event Demo",
eoq => 1
);
# Initialize the Audio
unless ( SDL::Mixer::open_audio( 44100, AUDIO_S16SYS, 2, 4096 ) == 0 ) {
Carp::croak "Cannot open audio: " . SDL::get_error();
}
my $channel_volume = 100;
my $music_volume = 100;
my $laser_status = 'none';
my $music_status = 'not playing';
# Load our sound resources
my $laser = SDL::Mixer::Samples::load_WAV('data/sample.wav');
unless ($laser) {
Carp::croak "Cannot load sound: " . SDL::get_error();
}
my $background_music =
SDL::Mixer::Music::load_MUS('data/music/01-PC-Speaker-Sorrow.ogg');
unless ($background_music) {
Carp::croak "Cannot load music: " . SDL::get_error();
}
$app->add_show_handler(
sub {
$app->draw_rect([0,0,$app->w,$app->h], 0 );
$app->draw_gfx_text( [10,10], [255,0,0,255], "Channel Volume : $channel_volume" );
$app->draw_gfx_text( [10,25], [255,0,0,255], "Music Volume : $music_volume" );
$app->draw_gfx_text( [10,40], [255,0,0,255], "Laser Status : $laser_status" );
$app->draw_gfx_text( [10,55], [255,0,0,255], "Music Status : $music_status" );
$app->update();
}
);
$app->add_event_handler(
sub {
my $event = shift;
if ( $event->type == SDL_KEYDOWN ) {
my $keysym = $event->key_sym;
my $keyname = SDL::Events::get_key_name($keysym);
if ( $keyname eq 'space' ) {
$laser_status = 'PEW!';
#fire lasers!
SDL::Mixer::Channels::play_channel( -1, $laser, 0 );
}
elsif ( $keyname eq 'up' ) {
$channel_volume += 5 unless $channel_volume == 100;
}
elsif ( $keyname eq 'down' ) {
$channel_volume -= 5 unless $channel_volume == 0;
}
elsif ( $keyname eq 'right' ) {
$music_volume += 5 unless $music_volume == 100;
}
elsif ( $keyname eq 'left' ) {
$music_volume -= 5 unless $music_volume == 0;
}
elsif ( $keyname eq 'return' ) {
my $playing = SDL::Mixer::Music::playing_music();
my $paused = SDL::Mixer::Music::paused_music();
if ( $playing == 0 && $paused == 0 ) {
SDL::Mixer::Music::play_music( $background_music, 1 );
$music_status = 'playing';
}
elsif ( $playing && !$paused ) {
SDL::Mixer::Music::pause_music();
$music_status = 'paused'
}
elsif ( $playing && $paused ) {
SDL::Mixer::Music::resume_music();
$music_status = 'resumed playing';
}
}
SDL::Mixer::Channels::volume( -1, $channel_volume );
SDL::Mixer::Music::volume_music($music_volume);
}
}
);
$app->run();
SDL::Mixer::Music::halt_music();
SDL::Mixer::close_audio;
Making a dynamic spectrograph.
\includegraphics[width=0.5\textwidth]{../src/images/spectro-1.png} \caption{A Spectrograph of an awesome song} \label{fig:spectro}
The songs provided with this chapters code have been used by the talent authors on http://8bitcollective.com. To see the individual credits of each song have a look at
data/music/Songs. This is truly a great site for game songs, but please give all artists their due credits.
SDLx::SoundSimple SDL Mixer initialization.
SDL::AudioCreating music on the fly.
SDL::MixerFull fledge music support.
The Comprehensive Perl Archive Network (CPAN) is the other part of the Perl language. By now most Perl developers should be aware of how to search and get modules from CPAN. This chapter will focus on why to use CPAN for games. Next we will take a look in what domain (Model, View or Controller) does a module solve a problem for. Moreover we would want to look at what is criteria to pick one module from another, using the many tools provided by CPAN.
It is good to reuse code.
See where the module fits, Model, View or Controller
SDL will do most but helper module (Clipboard) are cool to have.
The SDLx::Widget bundle comes separately, but is meant to provide you with several common game elements such as menu, dialog boxes and buttons, all seamlessly integrated with SDL.
The logic and modelling behind most popular games is already on CPAN, so you can easily plug them in to create a new game of Chess, Checkers, Go, Life, Minesweeping, Cards, etc. There are even classes for platform games (like Games::Nintendo::Mario), creating and solving mazes, generating random dungeon maps, you name it.
If your game needs to store data, like objects and status for saved games or checkpoints, you can use Storable or any of the many data serializers available.
In fact, speaking of data structures, it is common to keep game data in standard formats such as JSON, YAML or XML, to make you able to import/export them directly from third-party tools like visual map makers or 3D modeling software. Perl provides very nice modules to handle the most popular formats - and some pretty unusual ones. Parsers vary in speed, size and thoroughness, so make sure to check the possible candidates and use the one that fits your needs for speed, size and accuracy.
If you need to roll a dice, you can use Games::Dice, that even lets you receive an array of rolled dice, and use RPG-like syntax (e.g. "2d6+1" for 2 rolls of a 6-side die, adding 1 to the result).
You can also use Sub::Frequency if you need to do something or trigger a particular action or event only sometimes, or at a given probability.
Your game may need you to mix words, find substrings or manipulate word permutations in any way (like when playing scrabble), in which case you might find the Games::Word module useful.
So, you thought of a nice game, identified your needs, typed some keywords in http://seach.cpan.org, and got tons of results. What now? How to avoid vaporware and find the perfect solution for your needs?
Once you find a potential module for your application, make sure you will know how to use it. Take a look at the SYNOPSIS section of the module, it should contain some code snippets showing you how to use the module's main features. Are you comfortable with the usage syntax? Does it seem to do what you expect it to? Will it fit nicely to whatever it is you're coding?
Next, skim through the rest of the documentation. Is it solid enough for you? Does it look complete enough for your needs, or is it easily extendable?
It's useless to find a module you can't legally use. Most (if not all) modules in CPAN are free and open source software, but even so each needs a license telling developers what they can and cannot do with it. A lot of CPAN modules are released "under the same terms as Perl itself", and this means you can pick between the Artistic License or the GPL (version 1).
Below is a short and incomplete list of some popular license choices by CPAN developers:
See http://www.opensource.org/licenses/alphabetical for a comprehensive list with each license's full documentation.
You should be able to find the module's license by going to a "LICENSE AND COPYRIGHT" section, usually available at the bottom of the documentation, or by looking for a license file inside that distribution.
Note: Some modules might even be released into CPAN as public domain, meaning they are not covered by intellectual property rights at all, and you are free to use them as you see fit. Even so, it's usually considered polite to mention authors as a courtesy, you know, giving credit where credit is due.
The CPAN Ratings is a service where developers rate modules they used for their own projects, and is a great way to have some actual feedback on how it was to use the code on a real application. The ratings are compiled into a 1 to 5 grade, and displayed below the module name on CPAN. You can click on the "Reviews" link right next to the rating stars to see any additional comments by the reviewers, praising, criticizing or giving some additional comments or the distribution and/or its competition.
Modules exist so you don't have to reinvent the wheel, and for that same reason each usually depends on one or more modules itself. Don't worry if a module depends on several others - code reusability is a good thing.
You may, however, be interested in which modules it depends on, or, more practically, in the likelihood of a clean installation by your users. For that, you can browse to http://deps.cpantesters.org and input the module's name on the search box.
The CPAN Testers is a collaborative matrix designed to help developers test their modules in several different platforms, with over a hundred testers each month making more than 3 million reports of CPAN modules. This particular CPAN Testers service will show you a list of dependencies and test results for each of them, calculating the average chance of all tests passing (for any platform).
While seeing all the dependencies and test results of a couple of modules that do the same thing might help you make your pick, it's important to realize that the "chance of all tests passing" information at the bottom of the results means very little. This is because test failures can rarely be considered independent events, and are usually tied to not running on a specific type of operating system, to the perl version, or even due to the tester running out of memory for reasons that may not even concern the module being evaluated. If you don't care about your application running on AIX or on perl 5.6.0, why would you dismiss a module that only fails on those conditions?
So, how do you know the actual test results for a module on the CPAN? How can you tell if that module will run in your target machine according to architecture, operating system and perl version?
The CPAN Testers website at http://www.cpantesters.org offers a direct search for distributions by name or author. To see the results for the SDL module, for instance, you can go to http://www.cpantesters.org/distro/S/SDL.html. You can also find a test report summary directly on CPAN, by selecting the distribution and looking at the "CPAN Testers" line. If you click on the "View Reports" link, you'll be redirected to the proper CPAN Testers page, like the one shown above.
The first chart is a PASS summary, containing information about the most recent version of that module with at least one PASS report submitted, separated by platform and perl version.
Second is a list of selected reports, detailing all the submitted test results for the latest version of the given module. If you see a FAIL or UNKNOWN result that might concern you - usually at a platform you expect your application to run - you can click on it to see a verbose output of all the tests, to see why it failed.
Another interesting information displayed is the report summary on the left sidebar, showing a small colored graph of PASS-UNKNOWN-FAIL results for the latest versions of the chosen module. If you see a released version with lots of FAIL results, it might be interesting to dig deeper or simply require a greater version of that module in your application.
When picking a module to use, it is very important to check out its bug reports. You can do that by either clicking on the "View/Report Bugs" link on the module's page on CPAN, or on the "CPAN RT" (for Request Tracker) box on the right side of the documentation page.
Look for open bugs and their description - i.e. if it's a bug or a whislist - and see if it concerns your planned usage for that module. Some bug reports are simple notices about a typo on the documentation or a very specific issue, so make sure you look around the ticket description to see if it's something that blocks your usage, or if you can live with it, at least until the author delivers an update.
It may also interest you to see how long the open bugs have been there. Distributions with bugs dating for more than two years might indicate that the author abandoned the module to pursue other projects, so you'll likely be on your own if you find any bumps. Of course, being free software, that doesn't mean you can't fix things yourself, and maybe even ask the author for maintainance privileges so you can update your fixes for other people to use.
A old distribution might mean a solid and stable distribution, but it can also mean that the author doesn't care much about it anymore. If you find a module whose latest version is over 5 years old, make sure to double check test results and bug reports, as explained above.
CPAN is an amazing repository filled with nice modules ready for you to use in your games. More than often you'll find that 90% of your application is already done on CPAN, and all you have to do to get that awesome idea implemented is glue them together, worrying only about your application's own logic instead of boring sidework. This means faster development, and more fun!
Make a Perl draft first, go make it now. Why are you still here, go make it! Don't optimize anything, nothing at all.
Run NYTProf.
What is called often? Load time? Run time? In the game loop?
Are you doing something inefficent? Is there a well know algorithm to do this?
Any design patterns that can do what you need? Maybe use modules that do this for you.
Nothing else left? Move it to C.
In this chapter we will look at how to use pixel effects in Perl. Pixel
effects are operations that are done directly on the bacnk of a
SDL_Surface's pixel. These effects are used to do visual
effects in games and applications, most notably by Frozen
Bubble.
\includegraphics[h!][width=0.5\textwidth]{../src/images/effects.png} \caption{Snow Effect covering Frozen Bubble's Logo } \label{fig:frozen_bubble}
These effects can be done in purely in Perl, for 1 passes and non real time applications. Effects that need to be done real time will have to be done in C via XS. This chapter will show two methods of doing this.
For our first pixel effect we will be doing is a ripple effect from a
well known SDL resource, http://sol.gfxile.net/gp/ch02.html. This effects
uses SDL::get_ticks to animate a ripple effect across the
surface as seen at \fig{fig:ripple}.
\includegraphics[width=0.5\textwidth]{../src/images/xs_effects.png} \caption{Sol's Chapter 01 Ripple Effect} \label{fig:ripple}
First lets make the effect in pure perl. To do any operations with a
SDL::Surface we must do
SDL::Video::lock_surface() call as seen below. Locking the
surface prevents other process in SDL from accessing the surface. The
surface pixels can be accessed several ways from Perl. Here we are using
the SDL::Surface::set_pixels which takes an offset for the
SDL_Surface pixels array, and sets a value there for us. The
actual pixel effect is just a time dependent (using
SDL::get_ticks for time) render of a function. See
http://sol.gfxile.net/gp/ch02.html for a deeper explanation.
use strict;
use warnings;
use SDL;
use SDLx::App;
# Render callback that we use to fiddle the colors on the surface
sub render {
my $screen = shift;
if ( SDL::Video::MUSTLOCK($screen) ) {
return if ( SDL::Video::lock_surface($screen) < 0 );
}
my $ticks = SDL::get_ticks();
my ( $i, $y, $yofs, $ofs ) = ( 0, 0, 0, 0 );
for ( $i = 0; $i < 480; $i++ ) {
for ( my $j = 0, $ofs = $yofs; $j < 640; $j++, $ofs++ ) {
$screen->set_pixels( $ofs, ( $i * $i + $j * $j + $ticks ) );
}
$yofs += $screen->pitch / 4;
}
SDL::Video::unlock_surface($screen) if ( SDL::Video::MUSTLOCK($screen) );
SDL::Video::update_rect( $screen, 0, 0, 640, 480 );
return 0;
}
my $app = SDLx::App->new( width => 640,
height => 480,
eoq => 1,
title => "Grovvy XS Effects" );
$app->add_show_handler( sub{ render( $app ) } );
$app->run();
One you run this program you will find it pretty much maxing out the CPU
and not running very smoothly. At this point running a loop through the
entire pixel bank of a 640x480 sized screen is too much for
Perl. We will need to move the intensive calculations to
C.
In the below example we use Inline to write inline
C code to handle the pixel effect for us. SDL now
provides support to work with Inline. The render
callback is now moved to C code, using Inline C.
When the program first runs it will compile the code and link it in for
us.
use strict;
use warnings;
use Inline with => 'SDL';
use SDL;
use SDLx::App;
my $app = SDLx::App->new( width => 640,
height => 480,
eoq => 1,
title => "Grovvy XS Effects" );
$app->add_show_handler( sub{ render( $app ) } );
$app->run();
use Inline C => <<'END';
void render( SDL_Surface *screen )
{
// Lock surface if needed
if (SDL_MUSTLOCK(screen))
if (SDL_LockSurface(screen) < 0)
return;
// Ask SDL for the time in milliseconds
int tick = SDL_GetTicks();
// Declare a couple of variables
int i, j, yofs, ofs;
// Draw to screen
yofs = 0;
for (i = 0; i < 480; i++)
{
for (j = 0, ofs = yofs; j < 640; j++, ofs++)
{
((unsigned int*)screen->pixels)[ofs] = i * i + j * j + tick;
}
yofs += screen->pitch / 4;
}
// Unlock if needed
if (SDL_MUSTLOCK(screen))
SDL_UnlockSurface(screen);
// Tell SDL to update the whole screen
SDL_UpdateRect(screen, 0, 0, 640, 480);
}
END
Making it usable at least.
The Perl Data Language (PDL) is a tool aimed at a more scientific crowd. Accuracy is paramount and speed is the name of the game. PDL brings to Perl fast matrix and numerical calculations. For games in most cases a accuracy is not critical, but speed and efficiency is a great concern. For this reason we will briefly explore how to share SDL texture data between PDL and OpenGL.
This example will do the following:
\includegraphics[width=0.5\textwidth]{../src/images/pdl.png} \caption{Not terribly interesting, but the speed is phenomenal} \label{fig:pdl}
Let's start an application to use with PDL. Make sure you do use
PDL.
+ use strict;
+ use warnings;
+ use SDL;
+ use SDL::Video;
+ use SDLx::App;
+
+ use PDL;
+
+ my $app = SDLx::App->new(
+ title => 'PDL and SDL application',
+ width => 640, height => 480, depth => 32,
+ eoq => 1);
PDL core object is something called a piddle. To be able to perform PDL
calculations and show them on SDL surfaces, we need to share the memory
between them. SDL Surface memory is stored in a void * block
called pixels. void * memory has the property
that allows Surfaces to have varying depth, and pixel formats. This also
means that we can have PDL's memory as our pixels for our
surface.
+ sub make_surface_piddle {
+ my ( $bytes_per_pixel, $width, $height) = @_;
+ my $piddle = zeros( byte, $bytes_per_pixel, $width, $height );
+ my $pointer = $piddle->get_dataref();
At this point we have a pointer to the $piddle's memory
with the given specifications. Next we have our surface use that
memory.
+ my $s = SDL::Surface->new_form(
+ $pointer, $width, $height, 32,
+ $width * $bytes_per_pixel
+ );
+
+ #Wrap it into a SDLx::Surface for ease of use
+ my $surface = SDLx::Surface->new( surface => $s );
+
+ return ( $piddle, $surface );
+ }
Lets make some global variables to hold our $piddle and
$surface.
+ my ( $piddle, $surface ) = make_surface_piddle( 4, 400, 200 );
make_surface_piddle() will return to use an anonymous array
with a $piddle and $surface which we can use with
PDL and SDL. PDL will be used to operate on the $piddle. SDL
will be used to update the $surface and render it to the
SDLx::App.
+ $app->add_move_handler( sub {
+
+ SDL::Video::lock_surface($surface);
+
+ $piddle->mslice( 'X',
+ [ rand(400), rand(400), 1 ],
+ [ rand(200), rand(200), 1 ]
+ ) .= pdl( rand(225), rand(225), rand(225), 255 );
+
+ SDL::Video::unlock_surface($surface);
+ } );
SDL::Video::lock_surface prevents SDL from doing any
operations on the $surface until
SDL::Video::unlock_surface is called. Next we will blit this
surface onto the $app.
In this case we use PDL to draw random rectangles of random color.
Finally we blit the $surface and update the
$app.
+ $app->add_show_handler( sub {
+
+ $surface->blit( $app, [0,0,$surface->w,$surface->h], [10,10,0,0] );
+ $app->update();
+
+ });
+ $app->run();
use strict;
use warnings;
use SDLx::App;
use PDL;
my $app = SDLx::App->new(
title => "PDL and SDL aplication",
width => 640, height => 480, eoq => 1 );
sub make_surface_piddle {
my ( $bytes_per_pixel, $width, $height) = @_;
my $piddle = zeros( byte, $bytes_per_pixel, $width, $height );
my $pointer = $piddle->get_dataref();
my $s = SDL::Surface->new_from(
$pointer, $width, $height, 32,
$width * $bytes_per_pixel
);
my $surface = SDLx::Surface->new( surface => $s );
return ( $piddle, $surface );
}
my ( $piddle, $surface ) = make_surface_piddle( 4, 400, 200 );
$app->add_move_handler( sub {
SDL::Video::lock_surface($surface);
$piddle->mslice( 'X',
[ rand(400), rand(400), 1 ],
[ rand(200), rand(200), 1 ]
) .= pdl( rand(225), rand(225), rand(225), 255 );
SDL::Video::unlock_surface($surface);
} );
$app->add_show_handler( sub {
$surface->blit( $app, [0,0,$surface->w,$surface->h], [10,10,0,0] );
$app->update();
});
$app->run();
OpenGL is a cross platform library for interactive 2D and 3D graphics
applications. However OpenGL specifies only the graphics pipeline and
doesn't handle inputs and events. SDL can hand over the graphics component
of an application over to OpenGL and take control over the event handling,
sound, and textures. In the first example we will see how to set up Perl's
OpenGL module with SDLx::App.
\includegraphics[width=0.5\textwidth]{../src/images/opengl-1.png} \caption{The lovely blue teapot} \label{fig:opengl-1}
use strict;
use warnings;
use SDL;
use SDLx::App;
use OpenGL qw/:all/;
my $app = SDLx::App->new(
title => "OpenGL App",
width => 600,
height => 600,
gl => 1,
eoq => 1
);
$app->run();
Enabling OpenGL mode is as simple as adding the
gl flag to the SDLx::App constructor.
Next we will make a OpenGL perspective with the
$app's dimensions:
glEnable(GL_DEPTH_TEST);
glMatrixMode(GL_PROJECTION);
glLoadIdentity;
gluPerspective(60, $app->w/$app->h, 1, 1000 );
glTranslatef( 0,0,-20);
Additionally we will be initializing glut, but just to draw
something quick.
#Using glut to draw something interesting really quick
glutInit();
Now we are prepared to put something on the screen.
$app->add_show_handler(
sub{
my $dt = shift;
#clear the screen
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glColor3d(0,1,1);
glutSolidTeapot(2);
#sync the SDL application with the OpenGL buffer data
$app->sync;
}
);
At this point there should be a light blue teapot on the screen. The
only special thing to notice here is that we need to call the
sync() method on $app. This will flush the
buffers and update the SDL application for us.
Event handling is the same as any other SDLx::App. We will
use the mouse motion changes to rotate the teapot.
First add a global variable to hold your rotate values. And then use those values to rotate our teapot.
glutInit();
+ my $rotate = [0,0];
$app->add_show_handler(
sub{
my $dt = shift;
#clear the screen
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glColor3d(0,1,1);
+ glPushMatrix();
+ glRotatef($rotate->[0], 1,0,0);
+ glRotatef($rotate->[1], 0,1,0);
glutSolidTeapot(2);
#sync the SDL application with the OpenGL buffer data
$app->sync;
+ glPopMatrix();
}
);
Next we will add an event handler to the app to update the rotate values for us.
$app->add_event_handler(
sub {
my ($e ) = shift;
if( $e->type == SDL_MOUSEMOTION )
{
$rotate = [$e->motion_x, $e->motion_y];
}
}
);
Finally we run the application.
$app->run();
use strict;
use warnings;
use SDL;
use SDLx::App;
use SDL::Event;
use OpenGL qw/:all/;
my $app = SDLx::App->new(
title => "OpenGL App",
width => 600,
height => 600,
gl => 1,
eoq => 1
);
glEnable(GL_DEPTH_TEST);
glMatrixMode(GL_PROJECTION);
glLoadIdentity;
gluPerspective(60, $app->w/$app->h, 1, 1000 );
glTranslatef( 0,0,-20);
glutInit();
my $rotate = [0,0];
$app->add_show_handler(
sub{
my $dt = shift;
#clear the screen
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glColor3d(0,1,1);
glPushMatrix();
glRotatef($rotate->[0], 1,0,0);
glRotatef($rotate->[1], 0,1,0);
glutSolidTeapot(2);
#sync the SDL application with the OpenGL buffer data
$app->sync;
glPopMatrix();
}
);
$app->add_event_handler(
sub {
my ($e ) = shift;
if( $e->type == SDL_MOUSEMOTION )
{
$rotate = [$e->motion_x, $e->motion_y];
}
}
);
$app->run();