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

/+ Multimedia development for the D Programming Language +/