Making an Asteroids game with Arc
by Clay Smith (clayasaurus)
1. Introduction
This tutorial will follow through the creation of
an entire Asteroids game using the Arc library. By now I'm assuming you
already know how to set up Arc, and if not, check out the
previous tutorial which will give you a good introduction. I am also assuming you know how to program with D, if not then check out
Digital Mars D website and
dsource's tutorial's.If you have any questions about this tutorial, please ask in the
forums.
Let me warn you, while
Arc will handle a lot of low level details such as windowing, input,
sounds and animations, games will still take a significant amount of
work on your part. The advantage is that instead of learning how to
program SDL and OpenGL, you will be learning how to program your game. So, enough talk, let's get on with the show!
2. Game Outline
Asteroids is a game where you fly a ship around and
try to destroy an onslaught of asteroids with a laser weapon. Since
asteroids are rocks, you will need to shot them into smaller and
smaller pieces until they dissolve and become harmless. Sound simple
enough? We'll want to keep track of the users score as well and draw
some stars in the background to make it look like space. Our work will
be split into four major sections, the score board, the star field, the
ship, and the asteroid field. We'll start with the easy scoreboard,
then move onto the star field, then the ship + laser, and then the
asteroid field. First I will tell you the design and requirements of
the code for each part, then I will show the full source code, and then
I will explain all sections of that source code.
3. Scoreboard
A) Design and Requirements
The scoreboard design is really simple, you simply
need to display the score in the top left hand corner of the screen and
it has to be visible to the player. You also need the ability to add
more to the players score. Since Arc uses TrueType fonts, the only media requirement is a TrueType
font file to load. We also will want the ability to reset the score
when the game is reset. Given the design, let's take a look at the
source I came up with.
B) Full Source Code
//1. Name of module
module scoreboard;
//2. Import our TrueType font
import arc.gfx.font;
//3. Scoreboard class will hold our scoreboard
class Scoreboard
{
public:
//4. This function will load our font and initialize our score
this()
{
font = new Font("astbin/font.ttf", 20);
score = 0;
}
//5. draw the current score in the top left corner of the screen
void draw()
{
font.draw(0,0, 0,255,0,200, "Score: ", score);
}
//6. add something to our score
void addScore(float argAdd)
{
score += argAdd;
}
//7. reset the score to zero
void reset()
{
score = 0;
}
private:
//8. our font and score
Font font;
int score;
}
C) Breakdown of the Source Code
1.
Here we just declare the name our module which is
pretty much the name of this file. Since we call our file scoreboard.d,
then the name of the module is scoreboard.
2.
This is the command to give our scoreboard access to the font drawing class.
3.
We will use a class to hold our scoreboard in so we can hide details no one needs to know about.
4.
This function loads the font and initializes the
score. The score is just an integer that we add to. The font is
initialized and the 2 parameters involved in the font initialization
are the name of our
TrueType font file and the height/size we want our font to be.
Look here to learn more about the font class.
5.
This function will draw our font to the screen. The
parameters for the font drawing function are font.draw(x location,y
location,red amount,blue amount,green amount,alpha amount,...). The
'...' means that for this you can put in any number of parameters just
like writef() and it will parse it and display it as text. In our case,
we use ""Score: ", score:".
6.
This simple function will add the amount to the score specified by the user of this class.
7.
This will reset the score back to zero.
8.
Our Font class variable to draw fonts with, and our integer to keep track of the score with.
4. Star field
A) Design and Requirements
The requirements for the star field are small. What
we want is a field of a couple hundred small glowing stars of different
colors covering the screen. Since it would be a pain to expressively
assign colors and locations to 300 stars or so, we will use a random
function that will allow us to place these stars in random locations
with random colors at random amount. To get the stars to glow, we can
simply play with their alpha values.
B) Full Source Code
//1. Name of our file
module starfield;
// 2. We'll need some physics/math utilities and
// graphical primitives to draw a pixel
import arc.phy.phyutil,
arc.gfx.prim;
//3. Our starfield class will abstract complexity for us
class Starfield
{
public:
//4. Initialize our stars with random colors and locations
this()
{
// pick a random amount of stars
stars.length = random_range(150,200);
// and initialize them with random colors and locations
for(int i = 0; i < stars.length; i++)
stars[i].init();
}
//5. Draw all the stars in the starfield
void draw()
{
for (int i = 0; i < stars.length; i++)
stars[i].draw();
}
private:
//6. Our array of stars
Star[] stars;
}
//7. Definition of a single star
struct Star
{
//8. Variables required of our stars
int x, y;
ushort r, g, b, a;
bool up = true;
//9. Initialize random colors and positions
void init()
{
x = random_range(0, arc.io.window.getWidth);
y = random_range(0, arc.io.window.getHeight);
r = random_range(0,255);
g = random_range(0,255);
b = random_range(0, 255);
a = random_range(0,255);
}
//10. Draw the stars at given position and color
void draw()
{
if (up)
a++;
else
a--;
if (a == 255 || a == 0)
up = !up;
drawPixel(x, y, r, g, b, a);
drawPixel(x+1, y, r, g, b, a);
drawPixel(x+1, y+1, r, g, b, a);
drawPixel(x, y+1, r, g, b, a);
}
}
C) Breakdown of the Source Code
1.
Simply name the module after the filename.
2.
From phyutil, which are Arc's math and physics
utilities, we will use the random_range function which will return a
random value within a specific range. From arc.gfx.prim, we will use
the draw pixel function to draw the stars with.
3.
We'll hold all our data in our star field class.
4.
What this code is doing is picking a random amount
of stars to display on the screen between 150 and 250, and then it is
adding them to my array of stars and initializing their color and
position.
5.
Simply draw every single star that we have.
6.
This is a dynamic array of stars. A dynamic array
is an array that can change length at runtime, and these are supported
within the D language. We know these arrays are dynamic because no
length is specified.
7.
Our definition of a star as a structure.
8.
Here are variables for our color and x and y position, as well as if the star alpha value is currently increasing or decreasing.
9.
Here we pick random x and y values that are on the
screen, and pick random color values with the range of the entire color
spectrum.
10.
To make our stars glow, we'll mess with their alpha
values. We want the alpha values to increase then decrease then
increase then decrease. We will increase them until they reach their
max value, then we will decrease them until they reach their minimum
value. In order to draw the star, we use 4 drawpixel functions to make
the stars a little bit bigger than 1 pixel, and the 4 drawpixel
functions draws the star as a block of 4 pixels.
5. Ship
A) Design and Requirements
Our ship will need to be able to be controlled by
people, and in order to accommodate the needs of people, there will be
easy style controls and more challenging controls. When caps lock is
on, the easy style controls will be on. In addition, our ship will need
to be able to fire laser beams at the asteroids to break them up, and
our ship will need to be able to explode when an asteroids hit it, and
we need sound effects for our laser beam and explosion sound. When our
ship accelerates, we will display a movement animation, and when it is
still, we will display a static frame. When it explodes, it will play
an explosion animation with some sound effects.
B) Full Source Code
//1. Declare module name
module ship;
//2. The ship will need to make use of all these imports
import arc.gfx.sprite,
arc.gfx.texture,
arc.snd.soundfx,
arc.io.input,
arc.io.window,
arc.phy.point,
arc.tl.dlinkedlist;
import derelict.sdl.sdl;
//3. The max speed the ship can accelerate up to
const int MAX_ACCEL = 2;
//4. Our ship class
class Ship
{
public:
//5. Initialize our ship
this()
{
// new sprite
s = new Sprite(COLLISION_TYPE.RADIUS);
// static frame
s.addFrame("astbin/ship/still.png", "static", -1, null);
// move animation
s.addFrame("astbin/ship/move1.png", "move", 75, null);
s.addFrame("astbin/ship/move2.png", "move", 75, null);
// ship explosion animation
s.addFrame("astbin/ship/exp1.png", "explode", 100, new SoundFX("astbin/explosion.wav"));
s.addFrame("astbin/ship/exp2.png", "explode", 100, null);
s.addFrame("astbin/ship/exp3.png", "explode", 100, null);
// start out with the static animation
s.setAnim("static");
// put at center of screen
s.setPosition(arc.io.window.getWidth/2,arc.io.window.getHeight/2);
// set rotate speed and amount it rotates by
s.setRotateBy(4);
s.setRotateSpeed(.0008);
// place weapon spawn point on top of ship
s.addRotationPoint(s.getWidth-10, 0);
// our laser sound effect
laserSnd = new SoundFX("astbin/ship/laser.wav");
// load our laser texture
laserTex = arc.gfx.texture.texture("astbin/ship/laser.png", 1.0);
// load up our linked list of lasers
lasers = new dlinkedlist!(LaserBeam);
}
//6. Reset the ship, this is usually done after the ship has been killed
void reset()
{
// start out with the static animation
s.setAnim("static");
// put at center of screen
s.setPosition(arc.io.window.getWidth/2,arc.io.window.getHeight/2);
// clear list of lasers
lasers.clear();
// ship will be still when we reset it
speed = 0;
// ship is alive again
alive = true;
}
//7. Accelerate
void speedUp()
{
if (speed < MAX_ACCEL)
speed = speed*1.02+.02;
}
//8. Decelerate
void slowDown()
{
if (speed > 0)
speed = speed/1.01;
}
//9. Process the ships controls
void processControls()
{
// use newbie controls when caps is down
if (arc.io.input.modDown(CAPS))
{
if (arc.io.input.keyDown(SDLK_LEFT) &&
arc.io.input.keyDown(SDLK_UP))
{
s.setAngle(315);
s.setAnim("move");
}
else if (arc.io.input.keyDown(SDLK_RIGHT) &&
arc.io.input.keyDown(SDLK_UP))
{
s.setAngle(45);
s.setAnim("move");
}
else if (arc.io.input.keyDown(SDLK_RIGHT) &&
arc.io.input.keyDown(SDLK_DOWN))
{
s.setAngle(135);
s.setAnim("move");
}
else if (arc.io.input.keyDown(SDLK_LEFT) &&
arc.io.input.keyDown(SDLK_DOWN))
{
s.setAngle(225);
s.setAnim("move");
}
else if (arc.io.input.keyDown(SDLK_UP))
{
s.setAngle(0);
s.setAnim("move");
}
else if (arc.io.input.keyDown(SDLK_DOWN))
{
s.setAngle(180);
s.setAnim("move");
}
else if (arc.io.input.keyDown(SDLK_RIGHT))
{
s.setAngle(90);
s.setAnim("move");
}
else if (arc.io.input.keyDown(SDLK_LEFT))
{
s.setAngle(270);
s.setAnim("move");
}
else
s.setAnim("static");
} // capslock is on
// capslock is off, use expert controls
else
{
if (arc.io.input.keyDown(SDLK_LEFT))
s.rotateLeft();
if (arc.io.input.keyDown(SDLK_RIGHT))
s.rotateRight();
if (arc.io.input.keyDown(SDLK_UP))
s.setAnim("move");
else
s.setAnim("static");
}
// speedup when moving, slow down when not
if (s.getAnim == "move")
speedUp();
else if (s.getAnim == "static")
slowDown();
// fire laser when control modifier is hit
if (arc.io.input.modHit(LCTRL))
{
// play laser sound effect
laserSnd.play();
// add laser to our laser list
Point p = s.getRotationPoint(0);
lasers.add(new LaserBeam(laserTex, s.getAngle, p.getX, p.getY, speed));
}
}
//10. Process the laser beams
void processLasers()
{
while (!lasers.last)
{
lasers.data.process();
lasers.data.draw();
if (!lasers.data.onScreen())
lasers.remove;
}
}
//11. Process everything
void process()
{
processLasers();
if (s.getAnim == "explode" && s.getAnimNum != 2)
{
s.process();
s.draw();
}
else if (s.getAnim == "explode" && s.getAnimNum == 2)
{
}
else
{
processControls();
s.process();
s.draw();
s.moveDir(speed);
}
}
//12. Give everyone access to our sprite
Sprite getS() { return s; }
//13. Kill the ship and only kill it once
void kill()
{
if (alive == true)
{
s.resetAnim("explode");
s.setAnim("explode");
alive = false;
}
}
//14. Linked list of laser beams
dlinkedlist!(LaserBeam) lasers;
private:
//15. Our ships variables
Sprite s;
float speed = 0;
SoundFX laserSnd;
SoundFX explodeSnd;
Texture laserTex;
bool alive=true;
}
//16. Our laser beam
class LaserBeam
{
public:
//17. Initialize the laser beam
this(Texture tex, float argAngle, float argX, float argY, float argSpeed)
{
s = new Sprite(COLLISION_TYPE.BOX);
s.addFrame(tex, DEFAULT_ANIM, DEFAULT_TIME, null);
s.setAngle(argAngle);
s.setPosition(argX, argY);
speed += argSpeed;
}
//18. Remove allcated resources when destroyed
~this()
{
delete s;
}
//19. Move laser based on the angle it's facing
void process()
{
s.process();
s.moveDir(speed);
}
//20. Draw the laser
void draw()
{
s.draw();
}
//21. Returns true when laser is on the screen
bool onScreen()
{
return s.onScreen();
}
//22. Give others access to lasers sprite
Sprite getS() { return s; }
private:
//23. Laser variables
Sprite s;
float speed = 3;
}
C) Breakdown of the Source Code
1.
Our ships module name.
2.
Our ship will need to make use of sprites,
textures, sound effects, input, window, points, and a linked list. In
addition, we import SDL to get the SDL key codes.
3.
Our ship will accelerate until it reaches this speed value.
4.
Our ship class holds all ship's variables and laser list.
5.
Here's all we need to do in order to initialize our
ship's graphics, animations, and sound. First of all we initialize our
sprite with radius collision detection, and then we add a static frame
to it. The arguments in 'addFrame' have to follow as, location of the
frame image or Texture class, the name of the animation, the time the
animation will be displayed for, and the sound effect that will play
when the frame is displayed. In this case, we are making a single frame
animation named 'static' that will always be displayed and never change
(why the -1 is given), and has no sound effects.
Now we create a 2
frame animation that will display the thrusters when we move our ship.
They won't play any sounds, but they will only be displayed for 75
milliseconds before changing frames.
The last animation we add is a three frame ship explosion that will play an explosion sound on its first frame.
In order for our ship
to not be exploding right when we start the game, we set the animation
to our static animation. By default, the animation will be set to the
last animation you add in the sprite, unless otherwise specified.
Now we set the Ship's
position to the center of the screen, sets its rotate by amount (number
of degrees to rotate per rotation) and rotate speed amount (how long to
wait between each rotation). We also place a laser weapon spawn point
at the nose of the ship, and this point will be rotated as the ship
rotates, so it looks like the laser is always coming out of the ships
nose.
Lastly, we load up
our laser sound effects and texture and we initialize our linked list
of lasers. The 1.0 in the laser texture initialization means to zoom
the sprite at 1.0 times, therefore it will keep its same size.
6.
The reset function will bring the ship back from
the dead. It will reposition it, set its animation back to the static
image, set its speed back to zero so it won't move, clear our laser
list, and declare that the ship is 'alive.'
7.
This code will speed up the ship at a faster and faster rate until it reaches its maximum acceleration speed.
8.
This code will lower the ships speed at a slower and slower rate until it approaches zero.
9.
The process controls function will use newbie
controls with caps lock off. This means to point the ship in exactly
the direction that the arrows will point. That means there is only 8
possible directions the ship can go, so I have a big else-if structure
that will handle each of those 8 possibilities, and set animation to
moving and direction as appropriate as a go. When caps lock is off, I
will use more 'advanced' controls, with left and right keys meaning
rotate left and right, and the up key meaning move forward.
In order to move the
ship appropriately, I simply test whether the ship moving animation is
playing and call the speedup function, otherwise the ship will be
slowing down.
The last piece of our
controls will be to handle the ship firing the laser. Here I test if
the control key is being hit, and if it is, firing a laser as
appropriate by playing the laser fire sound and adding a new laser to
our laser list at the same angle the ship is facing and starting at our
Ship's nose, and we pass it our ships current speed because the lasers
speed will be the ships current speed + the lasers speed.
10.
This is the code to process our laser beams. We
simply iterate through the entire list of lasers and process and draw
them, and then remove them if they are outside of the screen.
11.
This processing calls all our other processing
functions. First, we will process our laser beams no matter what. If
our ship is exploding and it hasn't reached the last explosion frame,
we process its explosion animation. If it has exploded, we don't draw
anything. If the ship is alive, then we process our controls and draw
and move our ship at the speed that's been calculated.
12.
This function will simply give everyone access to our ship's sprite data.
13.
This function will be called when our ship hits an
asteroid, it will start the sequence of events required for the ship to
be dead, which is an explosion. While the ship is exploding, more
asteroids will probably hit it, so in order to make our ship explode
only once, we only explode our ship if it is alive, then we quickly set
the alive variable to false.
14.
This is our doubly linked list of lasers.
15.
Our ship will need to have a sprite to represent
it, a current speed, a laser and explosion sound, a laser texture (so
we don't have to load it every time a laser is created), and a variable
to declare whether the ship is alive or dead.
16.
A single class for our laser.
17.
Here's our laser initialization function, it will
create a laser with box collision detection, and the laser texture to
its frame with the default animation and default time values and no
sound effects, set the angle and position to the angle and position the
ship gives it when its created, and add the ships speed to its own so
lasers do not clump up when the ship is moving, and it also adds an
incentive for the player to move around more.
18.
When we destroy our laser, which is whenever the laser leaves the screen, we will remove its sprite data.
19.
This will process the laser sprite and move it in the direction its angle is facing.
20.
Simply draw the laser.
21.
Return true if the laser is on the screen.
22.
Gives access to the lasers sprite data.
23.
Our laser is only comprised of its sprite, and its speed.
6. Asteroids
A) Design and Requirements
The final piece of the puzzle, the asteroid field.
The requirements for our asteroids is that not every asteroid looks the
same, the asteroids will get bigger and bigger as the game progresses,
and the bigger asteroids move faster and rotate slower. In addition,
the asteroid will never die unless killed by the player. If the
asteroid moves off of the screen, it will wait until it is a certain
distance away from the center of the screen and then it will pick a new
random direction that guarantees its path will cross on the screen
again. When the asteroids are shot then the asteroids will split in two
with their health being halved. For this, our required media will be
three different asteroid textures.
B) Full Source Code
//1. Module name
module asteroidfield;
//2. Imports
import derelict.sdl.sdl;
import arc.tl.dlinkedlist,
arc.phy.phyutil,
arc.phy.point,
arc.gfx.texture,
arc.gfx.sprite;
import ship;
import scoreboard;
//3. Max amount asteroid will deviate from screen when created
const float DEVIATION = 200;
///4. A whole field of asteroids
class AsteroidField
{
public:
//5. Initialize asteroid field
this()
{
tex1 = arc.gfx.texture.texture("astbin/asteroid1.png", 1.0);
tex2 = arc.gfx.texture.texture("astbin/asteroid2.png", 1.0);
tex3 = arc.gfx.texture.texture("astbin/asteroid3.png", 1.0);
asteroids = new dlinkedlist!(Asteroid);
startTime = SDL_GetTicks();
}
//6. Reset asteroid field
void reset()
{
asteroids.clear();
startTime = SDL_GetTicks();
begin = false;
}
//7. Pick a random texture to give to the asteroid
void pickTex(inout Asteroid ast)
{
int rand = random_range(1,3);
if (rand == 1)
{
ast = new Asteroid(tex1, timePassed);
}
else if (rand == 2)
{
ast = new Asteroid(tex2, timePassed);
}
else if (rand == 3)
{
ast = new Asteroid(tex3, timePassed);
}
}
//8. Process the asteroids
void process(inout Ship ship, inout Scoreboard score)
{
// get time passed since the creation of the asteroid field
timePassed = SDL_GetTicks()-startTime;
if (!begin)
{
timePassed+=25000;
for (int i = 0; i < random_range(5,8); i++)
{
Asteroid ast;
pickTex(ast);
asteroids.add(ast);
}
timePassed-=25000;
begin = true;
}
// create a new asteroid every 5 seconds
if (timePassed/1000 % 5 == 0)
{
if (curr != timePassed/1000)
{
Asteroid ast;
pickTex(ast);
asteroids.add(ast);
}
curr = timePassed/1000;
}
// process and draw all of our asteroids
while (!asteroids.last)
{
asteroids.data.process();
asteroids.data.draw();
// if our ship sprite collides with the asteroid sprite
// then we kill the ship
if (ship.getS.collide(asteroids.data.getS))
ship.kill();
// test if any our our ships lasers collide with
// the asteroids
while (!ship.lasers.last)
{
// if lasers collide with asteroid
if (ship.lasers.data.getS.collide(asteroids.data.getS))
{
// remove the laser beam
ship.lasers.remove;
// add up to our score
score.addScore(asteroids.data.getHealth/2);
// divide health by two
asteroids.data.lowerHealth();
if (asteroids.data.getHealth < 10)
{
asteroids.remove;
return;
}
else
{
// add another asteroid of the same health
int rand = random_range(1,3);
Asteroid ast;
pickTex(ast);
ast.setHealth(asteroids.data.getHealth);
ast.setPosition(asteroids.data.getX, asteroids.data.getY);
asteroids.add(ast);
}
}
}
}
}
private:
//9. Asteroid field variables
dlinkedlist!(Asteroid) asteroids;
Texture tex1, tex2, tex3;
uint startTime;
uint timePassed;
int curr = 0;
bool begin = false;
}
///10. A single asteroid
class Asteroid
{
public:
//11. will create asteroid with given texture and will create size
// based on the amount of time that has passed
this(Texture asteroidTex, uint timePassed)
{
// load up the asteroid frame
s = new Sprite(COLLISION_TYPE.RADIUS);
s.addFrame(asteroidTex, DEFAULT_ANIM, DEFAULT_TIME, null);
// determine health based on time passed
health = timePassed/1000;
// init values based on health
init();
// initialize position of the asteroid to a random
// point off the screen
initPosition();
// pick a random point on the screen to go towards
pickPoint();
comeBackDist = random_range(1000,2000);
}
//12. Initialze asteroid base on the health
void init()
{
// determine size based on health, more health = bigger
s.setSize(health,health);
// determine rotation speed based on health, more health = slower
s.setRotateSpeed(health/1000);
// rotate by 1 degree every time we rotate
s.setRotateBy(1);
// determine speed based on health, more health = faster
speed = health/50;
}
//13. Set and get health
void setHealth(float argH) { health = argH; init(); }
float getHealth() { return health; }
//14. Half half and reinitialize the asteroid
void lowerHealth()
{
health /= 2;
init();
}
//15. Set position of asteroid
void setPosition(float argX, float argY)
{
s.setPosition(argX, argY);
}
//16. Get X and Y values of asteroid
float getX() { return s.getX; }
float getY() { return s.getY; }
//17. determine a random point on the outside of the screen
void initPosition()
{
// first determine whether we come from the right or left of screen
int left = random_range(0,1);
// then from the top or the bottom of the screen
int top = random_range(0,1);
int x, y;
// pick a random off screen coordinate
if (left == 1)
{
x = random_range(-DEVIATION,0);
}
else
{
x = random_range(arc.io.window.getWidth, arc.io.window.getWidth+DEVIATION);
}
if (top == 1)
{
y = random_range(-DEVIATION, 0);
}
else
{
y = random_range(arc.io.window.getHeight, arc.io.window.getHeight+DEVIATION);
}
s.setPosition(x,y);
}
//18. Pick a random point on the screen and get the velocity
// required to point to it
void pickPoint()
{
int x = random_range(200,arc.io.window.getWidth-200);
int y = random_range(200,arc.io.window.getHeight-200);
// pick a random point on the screen to head towards
s.pointTo(x,y);
vel = s.getVelocityDir(speed);
}
//19. Draw asteroid
void draw()
{
s.draw();
}
//20. Process the asteroid
void process()
{
s.process();
s.rotateLeft();
if (s.onScreen())
{
onScreen = true;
}
else if (onScreen == true)
{
float dist = arc.phy.phyutil.distance(s.getX, s.getY,
arc.io.window.getWidth/2,
arc.io.window.getHeight/2);
if (dist > comeBackDist)
{
pickPoint();
onScreen = false;
}
}
s.moveVel(vel);
}
//21. Give access to asteroids sprite
Sprite getS() { return s; }
private:
//22. Asteroid variables
Sprite s;
float health;
float speed;
bool onScreen = false;
Point vel;
float comeBackDist;
}
C) Breakdown of the Source Code
1.
Module name as usual.
2.
We'll import SDL for the SDL key codes, our linked
list to hold our asteroids, physics utilities for the distance
function, texture to hold our texture and sprite to display the
asteroids with.
3.
This variable will determine how far from or close
to the edge of the screen the asteroid will spawn. The higher the
number, the farther away the asteroid will spawn from the screen.
4.
Our asteroid field will hold our list of asteroids.
5.
We initialize the variables in our asteroid field.
We load the 3 different types of asteroid textures with 1.0 zoom,
initialize our asteroid list, and set the start time as appropriate.
The start time will be used to determine how much time has passed since
the asteroid field was created so we can make bigger and bigger
asteroids as more time passes.
6.
This will reset our asteroid field by clearing out
all the asteroids, resetting the start time and resetting our begin
variable we use to determine if the asteroid field was just started or
not.
7.
This will initialize an asteroid and pick a random texture out of the 3 different asteroid textures that we are using.
8.
This is the meat of our asteroid field. The first
thing we do is update how much time has passed since the start. The
next thing we do is check to see if our game has just started, and if
it has then we create a couple of asteroids to start the action so the
player doesn't have to wait 30 seconds to see asteroids appear on the
screen. Here is the kicker, since our asteroid size is based on the
amount of time passed, and in the beginning the amount of time passed
just about zero, we 'hack' our timepassed variable to be bigger, add
our asteroids, and then set our timepassed variable back to normal.
This piece of code will be called every time you start or reset the
game.
Here is code that will create a new asteroid every 5 seconds.
Now we iterate through
our entire list of asteroids processing them and drawing them. We test
if they collide with the ship and if they do, we start the ship kill
sequence.
Next we have to test
if our lasers collide with the asteroids, so we have to iterate through
all of our lasers and test them each against the asteroid. If the
lasers do collide, then we remove the lasers from the list, add to the
players score half of that asteroids health, lower the hit asteroids
health by two and spawn another asteroid of the same health right on
top of it, remove the asteroid if its health is less than 10 and return
so our game won't try to access invalid memory.
9.
Our asteroid field needs to hold our list of
asteroids, the 3 possible textures the asteroids can be, the starting
time and the amount of time passed, a curr variable so we only create 1
asteroid every 5 seconds instead of 100 asteroids every 5th second, and
a variable to tell whether or not our asteroid field has just been
created.
10.
Here is the class that contains our single asteroid.
11.
The constructor will create asteroid with given texture and size based on the amount
of time that has passed. It will choose a random off screen
position and pick a point on the screen to cross the path of. It will
also determine a random distance that when it reaches that far away
from the center it will pick another random come on the screen to come
back to.
12.
This function will set size, rotation, and speed amounts based on the amount of health given to the asteroid.
13.
Simple set/get health functions.
14.
This function will divide health by half and recalculate its speed and size and rotation amounts.
15.
Will set the asteroids position.
16.
Get X and Y values of the asteroid.
17.
Initializes the asteroids position to a random
off-screen location deviating by the amount specified in the deviation
constant. What it first does is randomly pick top and left values to
determine whether the asteroid will come from the top, bottom, right,
or left. After this is done, we pick a random range from the corner of
the screen to the deviation amount. Then we set the position of the
asteroid to the one determined.
18.
This function tells the asteroid to pick a random
point on the screen and to set its path so that it will cross this
point. To get the path, we point the sprite towards the point with the
point to function which will determine the sprite's angle required to
face this point, then we use one of sprite's functions to determine the
velocity required to cross the path of the point at the given speed,
the speed will simply act as a multiplier.
19.
Draw the asteroid.
20.
To process the asteroid, we will process its sprite
and rotate it left. Next we will set the onscreen variable to true, but
if the sprite is not on the screen now but it was last frame, then we
will have to calculate the distance the asteroid is from the center of
the screen, if the distance exceeds are amount given then we tell the
asteroid to pick a new point on the screen so it's path will eventually
cross the screen again.
21.
Give others access to the asteroids sprite data.
22.
The asteroid will need to store its sprite (image)
data, its health and speed, whether or not it is currently on the
screen or not, its velocity and the distance it will come back to the
screen at.
7. Putting it all together
A)
Now we have most of the source done, we simply put
it all together and loop it in a big while loop. We will also give the
user the ability to take a screenshot and reset the game if they wish,
and we will force the game to wait a certain amount of time if the
player’s computer is too fast, and also so this simple 2D game doesn't
eat all the players CPU.
B) Full Source Code
//1. Module Name
module asteroids;
//2. Required imports
import arc.io.input,
arc.io.window,
arc.phy.time;
import ship;
import asteroidfield;
import starfield;
import scoreboard;
//3. Main
int main()
{
//4. Open window with title, screen width, height, and bits per pixel
arc.io.window.open("Asteroids", 800,600,0);
//5. open audio
arc.snd.soundfx.open(22050, AUDIO_S16, 2, 4096 );
//6. Open input handling
arc.io.input.open();
//7. Create our ship, asteroids, stars, and score
Ship ship = new Ship;
AsteroidField asteroids = new AsteroidField;
Starfield stars = new Starfield;
Scoreboard score = new Scoreboard;
//8. Keep track of time to delay game if needed
Time t = new Time;
//9. While the user doesn't want to quit
while (!(arc.io.input.keyDown(SDL_QUIT)||arc.io.input.keyDown(SDLK_ESCAPE)))
{
//10. process input
arc.io.input.process();
//11. process and delay time if needed
t.process();
t.delay(40);
//12. Clear the current screen
arc.io.window.clear();
//13. Draw and process stars, ship, asteroids, and score
stars.draw();
ship.process();
asteroids.process(ship, score);
score.draw();
//14. Reset the game if return is hit
if (arc.io.input.keyHit(SDLK_RETURN))
{
ship.reset();
asteroids.reset();
score.reset();
}
//15. Take a screenshot if the s key is hit
if (arc.io.input.keyHit('s'))
arc.io.window.screenshot("screen");
//16. swap window buffers
arc.io.window.swap();
}
//17. Close audio
arc.snd.soundfx.close();
//18. Close window
arc.io.window.close();
//19. Make main happy
return 0;
}
B) Breakdown of the Source Code
1.
Simple module name, you should know the purpose of this by now.
2.
We will need to import our input, window, and time
as well as all our previously constructed code such as our ships,
stars, asteroid field and scoreboard.
3.
The main loop, where our program starts from.
4.
Open the window with the title of Asteroids at
800x600, 0 means it is not full screen, 1 would mean that it is full
screen, or you can use true/false values.
5.
Open the audio with the rate, type, channels and chunk size.
6.
Initialize our input and get it ready to go.
7.
Here we initialize all our code that we worked on before, our ship, stars, asteroid field and scoreboard.
8.
Keep track of time so we can delay our game as necessary so fast computers don't eat it up.
9.
The game will quit when the user closes the window or hits escape, otherwise it will loop forever.
10.
Process input, figure out which keys are up or down.
11.
Process time and delay the time if the time between
frames was less than 40 milliseconds, this will force somewhere around
60 frames per second.
12.
Clear the window of all drawings.
13.
Draw and process our stars, ship, score, and asteroid field.
14.
Reset our game when return is hit.
15.
Take a screenshot when the user hits the 's' key.
16.
Swap window buffers because our window is double
buffered, which means that the next frame will be drawn upon while the
current one is displayed.
17.
We're out of our loop, close the sound effects.
18.
Close down our window.
19.
Make main happy by returning an integer.
8. Summary
Hope you enjoyed the tutorial and are now confident
to build a full fledged game using the 2D Arcade Game Library. The full
Windows binary + source package to the Asteroids game can be downloaded
here, and make sure to extract this and the
previous arc package into the same folder if you want to compile it.
Please visit me in my
forums for comments and suggestions.
this tutorial is licensed under the Creative Commons license