Page 1 of 1

XNA 3.0 Game Tutorials - Part 4 Creating an Asteroids Clone

#1 SixOfEleven  Icon User is offline

  • using Caffeine;
  • member icon

Reputation: 942
  • View blog
  • Posts: 6,342
  • Joined: 18-October 08

Posted 02 May 2009 - 03:58 PM

Getting Started with XNA 3.0 - Creating an Asteroids Clone Part 4

You can download the complete project here: Attached File  AsteroidsClone.zip (42.38K)
Number of downloads: 1211

I've been asked that instead of posting seperate tutorials for each part to put them all into one. So this tutorial will be a little long. It will cover collisions between the ship, asteroids and bullets, adding a score, increasing asteroids each time the field is cleared, adding two states to the game and adding lives for the player.

Before you can go through this tutorial you will have to have finished the first three tutorials. You can find the tutorials at these links:

Part 1
Part 2
Part 3

So, go ahead and load up your project from Part 3.

I will start with the ship and asteroid collision. Since the asteroids and the ship are rotated it does complicate things a little. If they weren't a simple rectangular collision test would be possible by creating a rectangle around each object and see it they intersect. Instead of a rectangle, I'm going to create a circle around each object. Then using the Pythagerium therom see if they collide. Find your UpdateAsteroids method. To it, after checking to see if the asteroids are on the screen add this line:

if (CheckShipCollision(a))
    a.Kill();



You will have to add a variable to the program:

float distance;



The call to CheckShipCollision method takes the current asteroid as a parameter. Here's the method:

private bool CheckShipCollision(Sprite asteroid)
{
    Vector2 position1 = asteroid.Position;
    Vector2 position2 = ship.Position;

    float Cathetus1 = Math.Abs(position1.X - position2.X);
    float Cathetus2 = Math.Abs(position1.Y - position2.Y);

    Cathetus1 *= Cathetus1;
    Cathetus2 *= Cathetus2;

    distance = (float)Math.Sqrt(Cathetus1 + Cathetus2);

    if ((int)distance < ship.Width)
        return true;

    return false;
}



In game programming sometimes there is a little math involved. This is how this works. By finding the distance between the position of the X and Y coordinates of each object. By subtracting then taken the absolute value the result will always be posative.

Then, you square the distances because the Pythogariam theory says that A^2 = B^2 + C^2. To find the distance between them you take the square root of that value. Here I did a little magic. I compared the distance with the width of the ship. In this case if the distance is less than the width of the ship there is a collision. This isn't perfect though, the objects can miss each other by a little and sometimes the asteroid can pass partly throgh the ship. Typically you would want to do a per pixel collision test but that would greatly complicate things. If you are going to create your own game with rotated and scaled sprites you would want to look into per pixel collision tests. If there is a collision true is returned, otherwise false. For now, it there is a collision I just kill the asteroid.

So, what I will do now is check for collisions between the bullets and the asteroids. The first thing to do is makes some changes to the UpdateBullets method. There are quite a few so I will show the entire method. Once again, I will show the code and then explain. Here is the new UpdateBullets method:

private void UpdateBullets()
{
    List<Sprite> destroyed = new List<Sprite>();

    foreach (Sprite b in bullets)
    {
        b.Position += b.Velocity;
        foreach (Sprite a in asteroids)
        {
            if (a.Alive && CheckAsteroidCollision(a, b ))
            {
                a.Kill();
                destroyed.Add(a);
                b.Kill();
            }
        }
        if (b.Position.X < 0)
            b.Kill();
        else if (b.Position.Y < 0)
            b.Kill();
        else if (b.Position.X > ScreenWidth)
            b.Kill();
        else if (b.Position.Y > ScreenHeight)
            b.Kill();
    }

    for (int i = 0; i < bullets.Count; i++)
    {
        if (!bullets[i].Alive)
        {
            bullets.RemoveAt(i);
            i--;
        }
    }

    foreach (Sprite a in destroyed)
    {
        SplitAsteroid(a);
    }
}



The first change is a list of asteroids that have been hit by a bullet. Then, inside the loop where the bullets' positions are updated, I check the bullet agains each asteroid and see if they have collided, and if the asteroid is alive. If both conditions are true, kill the asteroid, kill the bullet and add the asteroid to the list of dead asteroids. At the end of the method I go through each of the destroyed asteroids and call the split method, passing the asteroid as an arguement.

I will deal with the collision method first. It is similar to the other collision method. It is passed an asteroid and a bullet. This is the CheckAsteroidCollision method:

private bool CheckAsteroidCollision(Sprite asteroid, Sprite bullet)
{
    Vector2 position1 = asteroid.Position;
    Vector2 position2 = bullet.Position;

    float Cathetus1 = Math.Abs(position1.X - position2.X);
    float Cathetus2 = Math.Abs(position1.Y - position2.Y);

    Cathetus1 *= Cathetus1;
    Cathetus2 *= Cathetus2;

    distance = (float)Math.Sqrt(Cathetus1 + Cathetus2);

    if ((int)distance < asteroid.Width)
        return true;

    return false;
}



The math is basically the same. The only difference is that you use the bullet's position instead of the ship's position. Then where I checked the distance, I compared it to the asteroid's width. That way if the player misses just by a little it will register as a hit.

Now, if there is hit the SplitAsteroid method is called. It is passed the asteroid that had been destroyed. Before I get the the SplitAsteroid method I want to make a few changes. I want to add a variable to the program and remove one from all of the methods and create a new method. I want to add:

Random random = new Random();



At the top of the Game1 class and remove that line from all of the methods that use it. The new method will be used to get a random velocity for the asteroids. I just exctracted what I did in the CreateAsteroids method and made it a method. This is the new method. I called it RandomVelocity.

private Vector2 RandomVelocity()
{
    float xVelocity = (float)(random.NextDouble() * 2 + .5);
    float yVelocity = (float)(random.NextDouble() * 2 + .5);

    if (random.Next(2) == 1)
        xVelocity *= -1.0f;

    if (random.Next(2) == 1)
        yVelocity *= -1.0f;

    return new Vector2(xVelocity, yVelocity);
}



Don't worry about the CreateAsteroids method. It is going to have to be updated later, so I will deal with the changes to it then.

So, now I will show you the SplitAsteroid method then explain it. Here is the SplitAsteroid method:

private void SplitAsteroid(Sprite a)
{
    if (a.Index < 3)
    {
        for (int i = 0; i < 2; i++)
        {
            int index = random.Next(3, 6);
            NewAsteroid(a, index);
        }
    } 
    else if (a.Index < 6)
    {
        for (int i = 0; i < 2; i++)
        {
            int index = random.Next(6, 9);
            NewAsteroid(a, index);
        }
    }
}



This method is just an if-else bloack. If the index of the asteroid is less than three it is a large asteroid and is split into medium asteroids. Then, if the index is less than six, it is a medium asteroid and it is split into two small asteroids. To actually split the asteroid I created a new method called NewAsteroid. This method will take two parameters the asteroid and an index for the new asteroid. This is the NewAsteroid method:

private void NewAsteroid(Sprite a, int index)
{
    Sprite tempSprite = new Sprite(asteroidTextures[index]);

    tempSprite.Index = index;
    tempSprite.Position = a.Position;
    tempSprite.Velocity = RandomVelocity();

    tempSprite.Rotation = (float)random.NextDouble() *
        MathHelper.Pi * 4 - MathHelper.Pi * 2;

    tempSprite.Create();
    asteroids.Add(tempSprite);
}



This method creates a temporary sprite passing it the texture from the asteroidsTextures list using the index passed. It then sets the index, the position to the position of the asteroid, creates a random velocity and rotaion. It creates the sprite and adds it to the list of asteroids.

I extracted the RandomVelocity method form the CreateAsteroids method. It returns a Vector2. This is the code for the method:

private Vector2 RandomVelocity()
{
    float xVelocity = (float)(random.NextDouble() * 2 + .05);
    float yVelocity = (float)(random.NextDouble() * 2 + .05);

    if (random.Next(2) == 1)
        xVelocity *= -1.0f;

    if (random.Next(2) == 1)
        yVelocity *= -1.0f;

    return new Vector2(xVelocity, yVelocity);
}



It is time to rewrite the CreateAsteroids method. It isn't perfect but it does start the asteroids at the edges of the screen with random velocities. I will show the code and then explain it. This is the method:

private void CreateAsteroids()
{
    int value;

    for (int i = 0; i < 4; i++)
    {
        int index = random.Next(0, 3);

        Sprite tempSprite = new Sprite(asteroidTextures[index]);
        asteroids.Add(tempSprite);
        asteroids[i].Index = index;

        double xPos = 0;
        double yPos = 0;

        value = random.Next(0, 4);

        switch (value)
        {
            case 0:
            case 1:
                xPos = asteroids[i].Width + random.NextDouble() * 40;
                yPos = random.NextDouble() * ScreenHeight;
                break;
            case 2:
            case 3:
                xPos = ScreenWidth - random.NextDouble() * 40;
                yPos = random.NextDouble() * ScreenHeight;
                break;
            case 4:
            case 5:
                xPos = random.NextDouble() * ScreenWidth;
                yPos = asteroids[i].Height + random.NextDouble() * 40;
                break;
            default:
                xPos = random.NextDouble() * ScreenWidth;
                yPos = ScreenHeight - random.NextDouble() * 40;
                break;
        }

        asteroids[i].Position = new Vector2((float)xPos, (float)yPos);

        asteroids[i].Velocity = RandomVelocity();

        asteroids[i].Rotation = (float)random.NextDouble() *
                MathHelper.Pi * 4 - MathHelper.Pi * 2;

        asteroids[i].Create();
    }
}



The first change to the method is that I added an integer to variable to the method. I did this because I'm going to get a random number between 0 and 7 to determine where on the screen the asteroid will appear. Up until it I find the position everything is the same. Finding the position is the biggest change. I created two doubles to hold the X position and Y position of the asteroid. Then I got a number between 0 and 7. I used a switch statement to find the location of the asteroid on the screen. If the value is between 0 and 1, the asteroid starts on the left side of the screen. If it is between 2 and 3 it starts on the right side of the screen. Between 4 and 5 it starts at the top. Otherwise it starts at the bottom. Then I set the position of the asteroid, set it's velocity, rotation and create it.

I also changed the FireBullet method a little. The bullets act better now. This is the new FireBullets method:

private void FireBullet()
{
    Sprite newBullet = new Sprite(bullet.Texture);

    Vector2 velocity = new Vector2(
        (float)Math.Cos(ship.Rotation - (float)MathHelper.PiOver2),
        (float)Math.Sin(ship.Rotation - (float)MathHelper.PiOver2));
    
    velocity.Normalize();
    velocity *= 6.0f;

    newBullet.Velocity = velocity;

    newBullet.Position = ship.Position + newBullet.Velocity;
    newBullet.Create();

    bullets.Add(newBullet);            
}



The code is basically the same. I created a new bullet. Created a vector to hold the velocity of the the bullet. Then I normalized the vector. What does that mean? By normalizing the vector it has a length of 1 and has a uniform direction. Then I multiplied the velocity by 6. Gave the velocity of the bullet that value. Gave it position, created it and added it to the list of bullets.

I will now add the ability to clear the field of asteroids and go to the next level with more asteroids on the field. To start with you will want to add a variable to your code to hold the current level. It is just an integer as follows:

int level = 0;



Now you just have to modify the CreateAsteroids method a little. Find the for loop at the start of the method and repalce the 4 with 4 + level. Now you just need to add a method call after the call to UpdateBullets to a method that you will write now. It is called AllDead. As usual I will give the code and explain it. This is the method:

private void AllDead()
{
    bool allDead = true;

    foreach (Sprite s in asteroids)
    {
        if (s.Alive)
            allDead = false;
    }

    if (allDead)
    {
        SetupShip();
        level++;
        asteroids.Clear();
        CreateAsteroids();
    }

}



I don't think the method is very complicated. It creates variable to determine if all the asteroids are dead. It then loops through all of the asteroids. If it finds one that is alive it sets the variable to false. If all the asteroids are dead it rests the ship, increments the level by one, clears the asteroid list and creates more asteroids.

The next thing that I'm going to add to the project is score. The way I'm going to do score is if the player destroys a large asteroid they will get 25 points, if they destroy a medium they will get 50 points and if they destroy a small asteroid they will get 100 points. To start you will have to create a variable to hold the score, just like you did for the level, like this:

int score = 0;



Now you just have to add a couple lines to the UpdateBullets method inside this if statement:

if (a.Alive && CheckAsteroidCollision(a, b ))


This is the code you need to add:

if (a.Index < 3)
    score += 25;
else if (a.Index < 6)
    score += 50;
else
    score += 100;



The if statement first checks to see if the asteroid that was hit was a large asteroid. If it wasn't a large asteroid is checks to see if it was a medium asteroid. Otherwise it was a small asteroid and the appropriate score is added to the score.

Before you can display the score you need to add a sprite font to the project. Fortunately this is not very hard. All you have to do is right click the Content folder in the solution explorer and select new item the chose sprite font. In the Name text box type: myFont.spritefont. Just like other content you need to load it in the LoadContent method. You will first have to create a sprite font variable:

SpriteFont myFont;



Goto the LoadContent method and add the code to load in the font:

myFont = Content.Load<SpriteFont>("myFont");



That just leaves displaying the score which you will do in the draw method. This is just like drawing sprites but instead of using the Draw method of the sprite batch you use the DrawString method. You have to place between the Begin and End calls.

The overload that I will be usin has four parameters. The font to draw with, the string to draw, the position to draw the string and the color to draw the string.

Just add these two lines afer your Begin call:

Vector2 position = new Vector2(10, 10);

spriteBatch.DrawString(myFont, 
    "Score = " + score.ToString(), 
    position, 
    Color.White);



Now I'm going to add two states to the game. One where the game is over and one where the game is in play. To do this you need to add a new variable to the game:

bool GameOver = false;



Now you are going to change the Update method a little. As always, I will show you the code and explain it. Here's the new Update method:

protected override void Update(GameTime gameTime)
{
    KeyboardState newState = Keyboard.GetState();

    if (newState.IsKeyDown(Keys.Escape))
        this.Exit();

    // Allows the game to exit
    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == 
        ButtonState.Pressed)
        this.Exit();

    if (GameOver)
    {
        if (newState.IsKeyDown(Keys.Enter))
        {
            level = 0;
            score = 0;
            lives = 3;
            SetupGame();
            CreateAsteroids();
            GameOver = false;
        }
        else
        {
            asteroids.Clear();
            ship.Kill();
            return;
        }
    }

    if (newState.IsKeyDown(Keys.Left))
    {
        ship.Rotation -= 0.05f;
    }

    if (newState.IsKeyDown(Keys.Right))
    {
        ship.Rotation += 0.05f;
    }

    if (newState.IsKeyUp(Keys.Space) && 
        oldState.IsKeyDown(Keys.Space))
    {
        FireBullet();
    }

    if (newState.IsKeyDown(Keys.Up))
    {
        AccelerateShip();
    }
    else if (newState.IsKeyUp(Keys.Up))
    {
        DecelerateShip();
    }

    if (newState.IsKeyUp(Keys.LeftControl) && 
        oldState.IsKeyDown(Keys.LeftControl))
    {
        HyperSpace();
    }

    if (newState.IsKeyUp(Keys.RightControl) && 
        oldState.IsKeyDown(Keys.RightControl))
    {
        HyperSpace();
    }

    oldState = newState;

    UpdateShip();
    UpdateAsteroids();
    UpdateBullets();
    AllDead();

    base.Update(gameTime);
}



Mostly I just rearranged the method. I got rid of the one if statement that will create asteroids if there are none and added a new if statement that checks to see if the game is over. If the game is over the asteroids are cleared and the ship is killed. Then, it checks to see if the enter key has been pressed. If it has sets the level and score to 0, sets up the game, creates some asteroids and sets the game in play otherwise it returns bypassing the rest of the Update method.

That leaves displaying a message that the game is over and to press enter to start. If you look at the sprites I made you will see I made a banner with the title. You will load this in the LoadConetnt method and draw it in the Draw method along with a message to press enter to start. So, add a variable to hold the banner:

Texture2D banner;


Load it in the LoadContent method:

banner = Content.Load<Texture2D>("Sprites/BANNER");


Now go to the Draw method. I will do something similar to what I did in the Update method. Just after the screen is cleared add this code:

if (GameOver)
{
    spriteBatch.Begin(SpriteBlendMode.AlphaBlend);

    Vector2 position2 = new Vector2(0.0f, 20.0f);
    spriteBatch.Draw(banner, position2, Color.White);

    string text = "GAME OVER";

    Vector2 size = myFont.MeasureString(text);

    position2 = new Vector2((ScreenWidth / 2) - (size.X / 2),
        ScreenHeight / 2 - (size.Y * 2));

    spriteBatch.DrawString(myFont, text, position2, Color.White);

    text = "PRESS <ENTER> TO START";
    size = myFont.MeasureString(text);

    position2 = new Vector2((ScreenWidth / 2) - (size.X / 2),
        ScreenHeight / 2 + (size.Y * 2));

    spriteBatch.DrawString(myFont, text, position2, Color.White);

    spriteBatch.End();

    return;
}



It probably looks a little more complicated than it is. If the game is in the game over state it makes a call to Begin of the sprite batch with alpha blend, creates a vector to hold the position where things will be drawn. It then draws the banner. I created a variable to hold the string to be drawn and a variable for the size of the string. I got the size of the string using the MeasureString method of the sprite font. Then I calculated where to position the string. (ScreenWidth / 2) - (size.X / 2) centers the string horizontally. (ScreenHeight / 2) - (size.Y * 2) places the words just above the center of the screen. I draw the string like I drew the score. The only difference with the other string is I added the height times 2 instead of subtracting to put it below the center of the screen. Then I exit the method because there is no need to draw anything else.

That just leaves adding lives to the game. You will need to add a variable to hold the number of lives the player has:

int lives;


Then set it in the Update method where you find if the enter key has been pressed:

lives = 3;


That was the easy part. Now you need to go to UpdateAsteroids method and find the lines:

if (CheckShipCollision(a))
    a.Kill();



You will want to do this instead:

if (a.Alive && CheckShipCollision(a))
{
    a.Kill();
    lives--;
    SetupShip();
    if (lives < 1)
        GameOver = true;
}



What this will do is check to see if the asteroid is alive and it collides with the ship. It will then kill the asteroid, take a life away, sets up the ship and if there are no lives left it ends the game.

I changes the SetupShip method. I changed it to put the ship in the center of the screen, set the rotation back to 0, set the velocity back to the zero vector, clear the bullet list and make the ship visible. This is the modified method:

private void SetupShip()
{
    ship.Rotation = 0;
    ship.Velocity = Vector2.Zero;
    bullets.Clear();
    ship.Position = new Vector2(ScreenWidth / 2, ScreenHeight / 2);
    ship.Create();
}



There is one thing left to do. You need to display how many lives the player has. I will do this by drawing a ship under the score for each life left. You guessed it you will do this in the Draw method so go to the Draw method and find where you drew the score then add this code to draw the ships:

Rectangle shipRect;

for (int i = 0; i < lives; i++)
{
    shipRect = new Rectangle(i * ship.Width + 10,
           40,
           ship.Width,
           ship.Height);

    spriteBatch.Draw(ship.Texture, shipRect, Color.White);
}



All this code does is create a rectangle that will determine where the ship will be drawn on the screen and then draw it inside a loop. I spaced them out a little.

Well, now you have a fairly functional game. There are a few improvements that could be made. You could perform per pixel collision checks. You could add sounds to the game. You could add the UFO to the game. You could split the asteroid when it collides with the ship. You could also perform a little animation when the ship collides with the asteroid. I will leave these up to you to do.

Come back to Dream In Code and look for more game tutorials. I'm planning on doing a Tetris clone next, some type of platform game and maybe another shooter.

Is This A Good Question/Topic? 1
  • +

Replies To: XNA 3.0 Game Tutorials - Part 4

#2 chuckjessup  Icon User is offline

  • D.I.C Regular

Reputation: 33
  • View blog
  • Posts: 380
  • Joined: 26-October 09

Posted 07 May 2011 - 07:37 AM

ACK! it wont load in VS C# 2010!!!
Was This Post Helpful? 0
  • +
  • -

Page 1 of 1