XNA 2D Game Tutorial (Part 8)

November 13, 2010

5 comments

Previous posts in this series:

 

What do we have so far? A player controlling a ship capable of firing missiles (with sound), flying through space with a backdrop of passing stars. Clearly, no real challenge for the player yet. It’s time to add some enemies to challenge the player. In this part, we’ll handle setting up the data, and in the next part we’ll implement the actual enemies.

Creating enemy sprites and managing them is no different in principal than dealing with the player and her missiles. It’s just a matter of creating and initializing sprites, managing their movements (if any) and drawing them. The real challenge is designing the enemies for versatility, challenge, balance and re-playability. For this demo game, I’d like to have levels the player goes through. In each level, she must destroy a certain number of enemies. Before moving to the next level, I’d like to have a “boss” alien – this is classic (although somewhat a cliché). Difficulty will increase in each level simply by force of numbers: more enemies, more frequent firing, faster speeds, etc. This is not an “intelligent” way to go, but it’s simple; and we’re taking the simple approach in this tutorial.

The concept of “intelligence” is a complex one, and we’ll not deal with “Artificial Intelligence” techniques in this tutorial. We’ll focus on XNA stuff and simply use randomness and shear numbers to give the player a sense of some kind of intelligent behavior from the enemy aliens.

The first order of business is to define various alien enemy types with properties, such as their texture, animation parameters, maximum speed, score if hit, etc. This allows us to select aliens to be used in each level, gradually using “stronger” aliens. Let’s define a simple alien type class that holds all that info:

class AlienType {

   public TimeSpan AnimationRate = TimeSpan.FromMilliseconds(500);

   public string Name;

   public int Score;

   public int Space;

   public float MaxXSpeed;

   public float MaxYSpeed;

   public Texture2D Texture;

   public int Frames = 1;

   public bool IsHorizontal = true;

   public Rectangle? FirstFrame;

}

 

This simple class uses fields only for performance reasons discussed a previous post. Some fields are technical (Texture, Space, Frames, etc.) and some functional (MaxXSpeed, MaxYSpeed). The Name field will be used as a key to access a particular alien as we’ll see shortly.

How do we get data into a collection of AlienType objects? One simple way is to create a collection and populate it in code. This works, but not the best approach. one of the important issues on creating a game is the question of balance. It’s fairly difficult to get a game to be “quite right” in terms of difficulty. You want it to be difficult, but not too difficult. if it’s easy, the player is bored. If it’s too difficult, the player might give up. And yes, it’s very tricky, as players themselves are different. And no, your perception is not sufficient, as you may not be a typical player.

What this all means, is that various game parameters will have to be tweaked quite a bit. If this is all in code, every tweak requires a recompilation, meaning time consumption, and more importantly – a developer must be at the seat. Why should this be? A game designer (in a simple case, a friend) may want to balance the game by tweaking parameters, but she may not have a Visual Studio or something. That’s why it’s better to put all possible parameters outside the code, in some data files (e.g. XML). This is the approach we’ll take.

Add a new folder to the project named Data. Right-click the new folder and select Add –> New Item. Navigate to the Visual C#->Data node and select “XML file”. Type AlienTypes in the Name box and click “Add”.

image

Right click the resulting file and select properties. Change the “Build action” to “None” and the “Copy to output directory” to “Copy if newer”. This will ensure the file is located with the project output and is always up to date.

Now let’s fill some alien types within the newly created XML file:

<AlienTypes>

  <AlienType Name="alien1" Score="10" MaxXSpeed="3" MaxYSpeed="2"

             Texture="Images/sprites" Frames="2" Space="1"

             FirstFrame="104,84,16,13" AnimationRate="500" />

  <AlienType Name="alien2" Score="15" MaxXSpeed="4" MaxYSpeed="3"

             Texture="Images/sprites" Frames="2" Space="0"

             FirstFrame="36,82,16,15" AnimationRate="400" />

  <AlienType Name="alien3" Score="20" MaxXSpeed="5" MaxYSpeed="4"

             Texture="Images/sprites" Frames="2" Space="0"

             FirstFrame="2,81,16,15" AnimationRate="400" />

  <AlienType Name="alien4" Score="25" MaxXSpeed="5" MaxYSpeed="5"

             Texture="Images/sprites" Frames="2" Space="1"

             FirstFrame="36,123,16,14" AnimationRate="400" />

  <AlienType Name="alien5" Score="30" MaxXSpeed="4" MaxYSpeed="6"

             Texture="Images/sprites" Frames="2" Space="0"

             FirstFrame="37,143,13,9" AnimationRate="300" />

 

  <AlienType Name="boss1" Score="500" MaxXSpeed="5" MaxYSpeed="0"

             Texture="Images/sprites" Frames="2" FirstFrame="186,85,54,36" AnimationRate="1000" />

  <AlienType Name="boss2" Score="800" MaxXSpeed="6" MaxYSpeed="0"

             Texture="Images/sprites" Frames="2" FirstFrame="2,1,63,38" AnimationRate="900" />

  <AlienType Name="boss3" Score="1000" MaxXSpeed="7" MaxYSpeed="0"

             Texture="Images/sprites" Frames="2" FirstFrame="133,0,66,40" AnimationRate="800" />

  <AlienType Name="boss4" Score="1500" MaxXSpeed="8" MaxYSpeed="0"

             Texture="Images/sprites" Frames="2" FirstFrame="138,43,52,36" AnimationRate="600" />

</AlienTypes>

The “Images/sprites” asset name is a new image file that holds all the alien images (and some other things like explosion images we’ll use later).

To read all this data, we’ll create a Dictionary in the game class and read the data in the Initialize method. We’ll expose the Dictionary as property for any interested party. The reading of data is done using LINQ to XML. If you’re not familiar with LINQ in general, or LINQ to XML, you should head out to the web and study it. It’s one of those things that once you really understand – you’ll use it time and time again and never go back. If you’re feeling unsure, you can read the data using the old XML API (in System.XML – XmlDocument, XmlElement, XmlAttribute, etc. This API is way too verbose for my taste Smile):

protected override void Initialize() {

   base.Initialize();

 

   _alienTypes = (from at in XElement.Load("data/alientypes.xml").Descendants("AlienType")

                  select new AlienType {

                     Name = (string)at.Attribute("Name"),

                     Score = (int)at.Attribute("Score"),

                     MaxXSpeed = (float)at.Attribute("MaxXSpeed"),

                     MaxYSpeed = (float)at.Attribute("MaxYSpeed"),

                     Texture = Content.Load<Texture2D>((string)at.Attribute("Texture")),

                     FirstFrame = ParseRectangle((string)at.Attribute("FirstFrame")),

                     Space = (int)at.Attribute("Space"),

                     Frames = (int)at.Attribute("Frames"),

                     AnimationRate = TimeSpan.FromMilliseconds((int)at.Attribute("AnimationRate"))

                  }).ToDictionary(t => t.Name);

}

 

The ToDictionary extension method turns the created list into a dictionary, keyed by the name of the AlienType. The resulting data is exposed as a simple method from the game class:

internal AlienType GetAlienType(string name) {

   return _alienTypes[name];

}

 

The next step is to create some data that represents what’s going on in each game level. We’ll add another XML file called Levels.Xml to the Data folder. Let’s create some levels:

<Levels>

  <Level Number="1" MaxActiveAliens="7" TotalAliensToFinish="15" Boss="boss1"

         AlienGenerationTime="800" ChangeDirChance="2" FireChance="2" MaxAlienBullets="5">

    <AlienTypes>

      <AlienType Name="alien1" Chance="25" />

      <AlienType Name="alien2" Chance="20" />

      <AlienType Name="alien3" Chance="20" />

      <AlienType Name="alien4" Chance="5" />

    </AlienTypes>

  </Level>

  <Level Number="2" MaxActiveAliens="10" TotalAliensToFinish="25" Boss="boss2"

         AlienGenerationTime="600" ChangeDirChance="2" FireChance="3" MaxAlienBullets="7">

    <AlienTypes>

      <AlienType Name="alien1" Chance="20" />

      <AlienType Name="alien2" Chance="20" />

      <AlienType Name="alien3" Chance="20" />

      <AlienType Name="alien4" Chance="15" />

      <AlienType Name="alien5" Chance="10" />

    </AlienTypes>

  </Level>

</Levels>

These are just 2 levels, but we can add as many as we want. The downloadable source has 4 levels defined.

A level has some attributes, such as the maximum number of active aliens, the number of aliens needed to advanced to the next level, the name of the boss alien, etc. The most interesting part is the AlienTypes element that lists the chance of each alien type to be generated during game play.

Similarly to the AlienType class, we’ll create some data structures that can hold the information for a particular level:

class AlienSelectionData {

   public AlienType Alien;

   public int Chance;

}

 

class LevelData {

   public int Number;

   public TimeSpan AlienGenerationTime;

   public int MaxActiveAliens;

   public int TotalAliensToFinish;

   public AlienType Boss;

   public List<AlienSelectionData> SelectionData;

   public int ChangeDirChance;

   public int FireChance;

   public int MaxAlienBullets;

}

 

Although we can read all levels data in one swoop – this is unnecessary. We just need to read a single level, corresponding to the current level.

First we’ll add a Level field to the game and create a helper method named InitLevel that reads the current level data – in preparation for a new level. That data is exposed through a method, similarly to what we did with alien types:

private void InitLevel(int levelNum) {

   Level = levelNum;

   _levelData = (from level in XElement.Load("Data/Levels.xml").Descendants("Level")

                 where (int)level.Attribute("Number") == levelNum

                 select new LevelData {

                    Number = levelNum,

                    ChangeDirChance = (int)level.Attribute("ChangeDirChance"),

                    MaxActiveAliens = (int)level.Attribute("MaxActiveAliens"),

                    TotalAliensToFinish = (int)level.Attribute("TotalAliensToFinish"),

                    Boss = _alienTypes[(string)level.Attribute("Boss")],

                    FireChance = (int)level.Attribute("FireChance"),

                    MaxAlienBullets = (int)level.Attribute("MaxAlienBullets"),

                    AlienGenerationTime = TimeSpan.FromMilliseconds((int)level.Attribute("AlienGenerationTime")),

                    SelectionData = (from sel in level.Descendants("AlienType")

                                     select new AlienSelectionData {

                                        Chance = (int)sel.Attribute("Chance"),

                                        Alien = _alienTypes[(string)sel.Attribute("Name")]

                                     }).ToList()

                 }).SingleOrDefault();

   Debug.Assert(_levelData != null);

}

 

internal LevelData GetCurrentLevelData() {

   return _levelData;

}

 

The technique used here is the same used for the alien types data. It’s a bit more complex, as we need to read a collection within the level data, indicating the chance a certain alien type should be created.

The InitLevel method should be called when the game starts, and when advancing to a new level. We’ll add a call in the AlienRaidGame.Initialize method, setting the level to 1:

InitLevel(1);

 

Now that we have all the right data set up, when can move on to actually creating and managing the aliens. We’ll do that in a separate component, for the same reasons we created separate components for the stars and the player. Add a new DrawableGameComponent named AliensComponent by right-clicking the project, selecting Add->New Item, navigating to XNA/Game Component, setting the name to AliensComponent and clicking Add. Then change the base class from GameComponent to DrawableGameComponent.

In the next part, we’ll fill the newly created component with some live aliens!

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

5 comments

  1. דניNovember 14, 2010 ב 12:06

    בקבצי ה XML חסרים ערכים לשדה ה Space
    בעבור הבוסים שבמשחק

    Reply
  2. pavelyNovember 14, 2010 ב 12:40

    צודק, חשבתי שתיקנתי את זה… כנראה שצריך לעדכן את ה-ZIP.

    Reply
  3. pavelyNovember 14, 2010 ב 12:42

    דני,
    זה מופיע ב-ZIP, אבל לא בפוסט עצמו…

    Reply
  4. דניNovember 14, 2010 ב 14:11

    תודה, אני מחכה בקוצר רוח לפוסט הבא

    Reply
  5. MorrowOctober 4, 2012 ב 06:09

    How do you stop yourself from losing interest from video gaming?

    Serious thought.

    Reply