Previous posts in this series:
In the previous post we prepared the data for the aliens and for the game levels. In this post, we’ll put that data to action.
The first thing we’ll do is create a custom Alien class, that extends the Sprite class and adds some specific alien attributes. This will make our lives a little bit easier when we managed all those aliens. Add a new class named Alien and derive it from Sprite. We’ll need a constructor that satisfies the requirements of Sprite – we’ll get all the needed information from an AlienType:
class Alien : Sprite {
public readonly bool IsBoss;
public readonly AlienType Type;
public Alien(AlienType type, bool isBoss)
: base(type.Texture, type.FirstFrame, type.Frames, type.IsHorizontal, type.Space) {
IsBoss = isBoss;
Type = type;
Origin.X = type.FirstFrame.Value.Width / 2;
AnimationInterval = type.AnimationRate;
}
}
The IsBoss field indicates whether this alien is, in fact, a boss alien. A more “object oriented” approach would be to create an AlienBoss class, derive it from Alien, thus customizing its behavior. I’ll leave that as an exercise for the reader. I’ll take the (somewhat) simpler approach here, but if I had wanted to make the boss behave very differently from that of a regular alien, I would have done that with the derived class approach.
Now we’ll create an array of Alien references in the AlienComponent class (created in the previous part), somewhat similar to the missiles array we created for the player. In this case, however, as every alien may look different, we may have to create aliens dynamically as needed. Another possible approach would be to allow the Texture of a sprite to be changed after the sprite has been initialized. Currently, the Texture of a sprite is passed via the constructor, and is read only since.
In the Update method, we’ll update all existing aliens (for animation and movement purposes), and in the Draw method we’ll draw them. Nothing has been created at this time:
public override void Update(GameTime gameTime) {
foreach(var alien in _aliens)
if(alien != null && alien.Active)
alien.Update(gameTime);
base.Update(gameTime);
}
public override void Draw(GameTime gameTime) {
_batch.Begin();
foreach(var alien in _aliens)
if(alien != null && alien.Active)
alien.Draw(gameTime, _batch);
_batch.End();
base.Draw(gameTime);
}
_aliens is the array declared in the AliensComponent class. Next, we’ll add the component to the game’s Components collection in the game’s constructor (as usual for game components):
Components.Add(new AliensComponent(this));
Running the game at this time has no special effect. Our next task would be to actually create an alien (based on some random factor and the game level). The Update method is the proper place for this:
// get current level data
LevelData data = AlienRaidGame.Current.GetCurrentLevelData();
if((_elapsedTime += gameTime.ElapsedGameTime) > data.AlienGenerationTime) {
_elapsedTime = TimeSpan.Zero;
if(CreateAlien() != null)
_currentLiveAliens++;
}
We first get the current level data by calling the game’s GetCurrentLevelData method we developed in the previous part. Note that to get to the game object itself I’ve added a static property called Current that is set in the game’s constructor. The alternative is to cast the Game property back to the actual AlienRaidGame type, which I find annoying (and like all casts – is applied at runtime, which is slower).
_elapsedTime is incremented by the elapsed time since the last frame and compared to the alien generation time obtained from the level data. This indicates when it’s time to attempt creation of a new alien. CreateAlien is a helper method that does the actual creation and setting up of the alien:
private Alien CreateAlien() {
var data = AlienRaidGame.Current.GetCurrentLevelData();
if(_currentLiveAliens > data.MaxActiveAliens) return null;
int chance = 0;
int value = _rnd.Next(100);
foreach(var sd in data.SelectionData) {
if(value < sd.Chance + chance) {
// selected
while(_aliens[_currentAlienIndex] != null && _aliens[_currentAlienIndex].Active)
_currentAlienIndex = (_currentAlienIndex + 1) % _aliens.Length;
Alien alien = new Alien(sd.Alien, false);
alien.Position = new Vector2((float)(_rnd.NextDouble() * Game.Window.ClientBounds.Width), -50);
alien.Velocity = new Vector2((float)(_rnd.NextDouble() * alien.Type.MaxXSpeed * 2 - alien.Type.MaxXSpeed),
(float)(_rnd.NextDouble() * alien.Type.MaxYSpeed + 2));
alien.Scale = 2;
_aliens[_currentAlienIndex] = alien;
return alien;
}
chance += sd.Chance;
}
return null;
}
_currentLiveAliens counts the total aliens currently active and compared to the data from the current level prevents too many aliens to be present at any one time. Then the SelectionData collection is used to select an alien type at random, based on probabilities in the current SelectionData. If selected, a free index is located and a new alien instance is placed there. The alien’s position is set randomly in the X direction and at Y=-50 (just above the visible screen, so it can make an entrance). Speed is set based on maxima read from the alien type. The alien is now active!
Running the code as is causes various aliens to move in a certain direction and disappear. After a while, aliens stop appearing. This is because we increment the current live aliens but never decrement it. There is no checking for going off the screen. Let’s take care of that.
First, if an alien moves off the screen in the X axis, I want it to simply change direction (bounce back). This we can do in the Alien.Update override (of the same Sprite method):
public override void Update(GameTime gameTime) {
if(Active) {
if(Position.X < 0 || Position.X > AlienRaidGame.Current.Window.ClientBounds.Width)
Velocity.X = -Velocity.X;
}
base.Update(gameTime);
}
Although the bounds checks are not exact, they are good enough. The aliens now bounces off the right and left edges of the game screen.
Now for the Y position: if it’s beyond the screen, then mark the alien as inactive and decrement the active alien count. It’s a bit problematic to do that in the same Update method, as we don’t have access to the parent AliensComponent, and we wouldn’t want a direct access anyway. One option is to check all that in the AliensComponent.Update method. This is somewhat awkward, but possible. Another alternative is to use an event. The alien can fire an event when it’s Y position is out of bounds. Any interested party (the AliensCompoenent object) can handle that event appropriately. For the sake of versatility, let’s use an event. First, let’s declare it in the Alien class:
public event Action<Alien> OutOfBounds;
In “classic” .NET an event delegate should be of type EventHandler or EventHandler<T> (that accept an object and a T that must derive from EventArgs). Here we’re cutting another corner, as we prefer type safety and fewer arguments.
Raising (or firing the event) should be done during Update:
if(Position.Y > AlienRaidGame.Current.Window.ClientBounds.Height + 40) {
Active = false;
if(OutOfBounds != null)
OutOfBounds(this);
}
Subscribing to the event is conveniently done in the CreateAlien helper. A simple lambda expression (or anonymous delegate if you prefer, or a regular method) can be used:
alien.OutOfBounds += a => {
_currentLiveAliens--;
};
If that’s a bit scary, we can write it like this instead:
alien.OutOfBounds += delegate {
_currentLiveAliens--;
};
If that’s still scary, then we could write that as a separate method and do the _currentLiveAliens—in there.
If the anonymous delegate or lambda expression is unfamiliar, or you chose to stay from those “complications”, I strongly suggest you read about those and get comfortable with that. They’re not going away any time soon.
Running the game now shows a bunch of aliens that keep flying, changing direction at the seams, while the player moves her ship and fires at the aliens. Didn’t hit them, did you? The upside is the aliens couldn’t hit you, either.
Why? There is no collision detection or handling of such collisions. That’s the topic of the next part.