1

Hi I'm new to xna and I'm trying to make a simple game where your a little ship that moves around and avoids asteroids that fall down from the top to the bottom. I've got the ship moving and one asteroid falling but I don't know how to get lots of asteroids falling from the same texture and how to get them falling at intervals. This is my asteroids class so far:

namespace Asteroids
{
class Asteroids
{
    Texture2D AsteroidTexture;
    Vector2 Position;
    Random random = new Random();
    float AsteroidSpeed = 5;

    public void Initialize()
    {
        Position.Y = 0;
        Position.X = random.Next(0, 1000);
    }

    public void Update()
    {
        Position.Y += AsteroidSpeed;
        if (Position.Y > 600)
        {
            Position.Y = 0;
            Position.X = random.Next(0, 1000);
        }
    }

    public void Load_Content(ContentManager Content)
    {
        AsteroidTexture = Content.Load<Texture2D>("asteroid");
    }

    public void Draw(SpriteBatch SpriteBatch)
    {
        SpriteBatch.Draw(AsteroidTexture, Position, Color.White);
    }
}
}

And this is my Game1 class:

namespace Asteroids
{
public class Game1 : Microsoft.Xna.Framework.Game
{
    GraphicsDeviceManager graphics;
    SpriteBatch spriteBatch;
    KeyboardState keyboardState;

    Ship ship;
    Asteroids asteroids;

    public Game1()
    {
        graphics = new GraphicsDeviceManager(this);
        Content.RootDirectory = "Content";
    }

    /// <summary>
    /// Allows the game to perform any initialization it needs to before starting to run.
    /// This is where it can query for any required services and load any non-graphic
    /// related content.  Calling base.Initialize will enumerate through any components
    /// and initialize them as well.
    /// </summary>
    protected override void Initialize()
    {
        ship = new Ship();
        asteroids = new Asteroids();

        asteroids.Initialize();

        this.graphics.PreferredBackBufferWidth = 1000;
        this.graphics.PreferredBackBufferHeight = 600;
        //this.graphics.IsFullScreen = true;
        this.graphics.ApplyChanges();

        base.Initialize();
    }

    /// <summary>
    /// LoadContent will be called once per game and is the place to load
    /// all of your content.
    /// </summary>
    protected override void LoadContent()
    {
        // Create a new SpriteBatch, which can be used to draw textures.
        spriteBatch = new SpriteBatch(GraphicsDevice);

        ship.Load_Content(this.Content);
        asteroids.Load_Content(this.Content);
    }

    /// <summary>
    /// UnloadContent will be called once per game and is the place to unload
    /// all content.
    /// </summary>
    protected override void UnloadContent()
    {
        // TODO: Unload any non ContentManager content here
    }

    /// <summary>
    /// Allows the game to run logic such as updating the world,
    /// checking for collisions, gathering input, and playing audio.
    /// </summary>
    /// <param name="gameTime">Provides a snapshot of timing values.</param>
    protected override void Update(GameTime gameTime)
    {
        // Allows the game to exit
        if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
            this.Exit();
        keyboardState = Keyboard.GetState();
        if (keyboardState.IsKeyDown(Keys.Escape))
            this.Exit();

        ship.Update();
        asteroids.Update();

        base.Update(gameTime);
    }

    /// <summary>
    /// This is called when the game should draw itself.
    /// </summary>
    /// <param name="gameTime">Provides a snapshot of timing values.</param>
    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.Black);

        spriteBatch.Begin();
        asteroids.Draw(this.spriteBatch);
        ship.Draw(this.spriteBatch);
        spriteBatch.End();

        base.Draw(gameTime);
    }
}
}

Thanks in advance!

Andrew Russell
  • 26,924
  • 7
  • 58
  • 104
Kris123
  • 25
  • 1
  • 5

2 Answers2

6

Important: pay close attention to the capitalisation and pluralisation in my code. It is important.


First you need to determine what data should exist per-asteroid instance, and what data should be shared between all asteroids. And what data should be shared for your entire program.

  • The asteroid position should exist for every single asteroid
  • The asteroid texture can be shared between every instance of Asteroid
  • An instance of the Random class should exist once per program (technically once per thread - but let's not worry about that for now)

So here is what that looks like in code. (Please note that I'm spreading the contents of classes out throughout my answer for sake of readability - you will have to merge each section's code yourself.)

class Asteroid
{
    // Static variables are shared between all instances of a class
    static Texture2D asteroidTexture;

    // Non-static variables exist once for each instance of the class
    Vector2 position;

    // Constants are fixed at compile time and cannot be modified
    const float asteroidSpeed = 50; // units per second
}

// A static class can only contain static variables (and constants)
// (You can't create an instance of it, so you can't have variables.)
static class Shared
{
    // "readonly" prevents anyone from writing to a field after it is initialised
    public static readonly Random Random = new Random();
}

Next you need to decide how that data will be initialised and modified:

Notice (above) how we have already initialised Shared.Random to a new instance of the Random class. (The actual initialisation will be done by the runtime automatically before it is first used.)

First let's look at loading the texture:

class Asteroid
{
    // Static methods can only act on static data
    public static void LoadContent(ContentManager content)
    {
        asteroidTexture = content.Load<Texture2D>("asteroid");
    }
}

public class Game1 : Microsoft.Xna.Framework.Game
{
    protected override void LoadContent()
    {
        Asteroid.LoadContent(Content);
    }
}

Because we know that Game1.LoadContent is called once at the start of our program, it is an appropriate place to call Asteroid.LoadContent.


Now let's look at the per-instance data for each asteroid. Each asteroid needs to have its position set when it is first created. We do this by giving the Asteroid class a constructor, which we then call whenever we wish to create an asteroid.

class Asteroid
{
    public Asteroid(Vector2 position)
    {
        this.position = position;
    }
}

Now we want to create and store multiple instances of our asteroid class:

We use a loop to create multiple instances - each with a random X position inside the width of the screen. And we use a list to store them as they are created:

public class Game1 : Microsoft.Xna.Framework.Game
{
    List<Asteroid> asteroids = new List<Asteroid>();

    protected override void Initialize()
    {
        int screenWidth = GraphicsDevice.Viewport.Width;

        // Create 15 asteroids:
        for(int i = 0; i < 15; i++)
        {
            float xPosition = Shared.Random.Next(0, screenWidth);
            asteroids.Add(new Asteroid(new Vector2(xPosition, 0)));
        }
    }
}

Finally, we want to update and draw every asteroid in the list that we created earlier. This is a simple matter of looping through the list of asteroids, calling the Draw or Update method on each instance.

class Asteroid
{
    public void Update(float elapsedSeconds)
    {
        position.Y += asteroidSpeed * elapsedSeconds;
    }

    public void Draw(SpriteBatch spriteBatch)
    {
        spriteBatch.Draw(asteroidTexture, position, Color.White);
    }
}

public class Game1 : Microsoft.Xna.Framework.Game
{
    protected override void Update(GameTime gameTime)
    {
        foreach(Asteroid asteroid in asteroids)
        {
            asteroid.Update((float)gameTime.ElapsedGameTime.TotalSeconds);
        }
    }

    protected override void Draw(GameTime gameTime)
    {
        foreach(Asteroid asteroid in asteroids)
        {
            asteroid.Draw(spriteBatch);
        }
    }
}

Note the way that I take into consideration the elapsed time in the Update method. This is why I put the comment "units per second" on the asteroid speed earlier.

This is good practice because it makes your game logic independent of the frame rate that your game runs at. (XNA, by default, runs at a fixed frame rate - but it's good practice to write frame-rate independent code anyway.)


By this point you should have complete code for your Asteroid class and for using it. Let's make some additions:

You wanted to know how to get asteroids to fall at intervals. To do this you need to accumulate time as it elapses, and whenever it reaches some threshold, create a new asteroid and reset the timer.

public class Game1 : Microsoft.Xna.Framework.Game
{
    float asteroidSpawnTimer;
    const float asteroidSpawnDelay = 5; // seconds

    void CreateAsteroid()
    {
        // This is the same code as I used in Initialize().
        // Duplicate code is extremely bad practice. So you should now modify 
        // Initialize() so that it calls this method instead.

        int screenWidth = GraphicsDevice.Viewport.Width;
        float xPosition = Shared.Random.Next(0, screenWidth);
        asteroids.Add(new Asteroid(new Vector2(xPosition, 0)));
    }

    protected override void Update(GameTime gameTime)
    {
        // ... other stuff ...

        asteroidSpawnTimer += (float)gameTime.ElapsedGameTime.TotalSeconds;
        if(asteroidSpawnTimer >= asteroidSpawnDelay)
        {
            asteroidSpawnTimer -= asteroidSpawnDelay; // subtract "used" time
            CreateAsteroid();
        }
    }
}

While you are adding asteroids - it would be a good idea to remove the old ones you no longer need - such as when they reach the bottom of the screen.

First of all, you need some way to access the asteroid's position externally. Because we didn't specify an access modifier, our position field defaults to private and can't be accessed outside of the Asteroid class. But we can create a public property that we can access from outside, that provides the position:

class Asteroid
{
    public Vector2 Position { get { return position; } }
}

(You might want to look into getting rid of the position field entirely and using an auto-implemented property with a private setter.)

You will want to use this same method for accessing the properties of your ship object when you want it to interact with asteroids. For a simple game it is ok to do do this kind of inter-object logic in Game1.Update (here is an in-depth discussion).

Anyway, now we have a way to access Asteroid.Position, we can remove the asteroids that fall off the screen.

public class Game1 : Microsoft.Xna.Framework.Game
{
    protected override void Update(GameTime gameTime)
    {
        // ... other stuff ...

        int screenHeight = GraphicsDevice.Viewport.Height;

        // Loop backwards through all asteroids.
        //
        // Note that you must iterate backwards when removing items from a list 
        // by index, as removing an item will change the indices of all items in
        // the list following (in a forward order) the item that you remove.
        for(int i = asteroids.Count - 1; i >= 0; i--)
        {
            if(asteroids[i].Position.Y > screenHeight)
                asteroids.RemoveAt(i);
        }
    }
}

Now, this was an amazingly long answer - I've covered a lot of topics (at the beginner level). So there is plenty of detail that I've not gone into. So if you're confused about something - look for the key words that I've used to describe that thing and go searching. Microsoft's documentation on MSDN is a particularly good place to look (here are the XNA docs).

Community
  • 1
  • 1
Andrew Russell
  • 26,924
  • 7
  • 58
  • 104
  • the line: float xPosition = Shared.Random.Next(0, screenWidth); comes up with a red line under it saying it doesn't exist in the current context so i put 'Asteroids.shared' then it said it's 'inaccessible due to it's protection level' – Kris123 Jul 08 '12 at 17:57
  • 1
    @Kris123 You are quite right - that was a bug in my code. I have now fixed it - so check the `Shared` class again. The basic pattern for declaring a variable with initialisation is: `[modifiers] [type name] [variable name] = [expression];`. Because I wanted a variable called "Random" with the type "Random" - you could say that I either left out the variable name, or that I left out the type name. – Andrew Russell Jul 09 '12 at 09:02
  • 1
    I found a couple of tiny errors in my code - check my latest edit. Sorry about that, hopefully they won't have tripped you up too badly. My bad for not running it through a compiler to check it originally. – Andrew Russell Jul 09 '12 at 10:44
  • Thank you for all the help now I can start on the collisions which might be tricky – Kris123 Jul 09 '12 at 19:20
  • But doesn't each instance still load the same texture here? And then store it in the same static variable? How can we make sure a texture only gets loaded once? – Kokodoko Jun 07 '15 at 11:20
  • @Kokodoko `Game1.LoadContent` gets called once at startup (pedantic: it can get called again in rare device-lost situations). So the call to `Asteroid.LoadContent` also only happens once. But, even if it were called multiple times, XNA's `ContentManager.Load` keeps a lookup of all loaded content (until you call `Unload`) - so even if you wrote code that called `Load` repeatedly, you'd keep getting the same object back. – Andrew Russell Jun 08 '15 at 11:13
0

You may define them in an array and on fixed interval from Update method you can add a new asteroid based on the gameTime. Hope this was useful

Manar Husrieh
  • 491
  • 1
  • 5
  • 16