Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / game

Learning XNA 2D Engine IceCream With 1945 Demo Project

5.00/5 (13 votes)
8 Aug 2012CPOL16 min read 69.3K   2.3K  
IceCream1945 is a demonstration of XNA and the IceCream 2D library in a 2D top-down scrolling shooter similar to 1942 for the NES.

1945_Demo_Package

Overview

XNA is a wonderful game development SDK from Microsoft. It handles so many core game-engine features and allows you as the developer to jump right into the fun stuff. But because it's open-ended to fit everyone, 2D and 3D games, it can be a bit much to work with once you've narrowed down the scope and type of game you want to make. If you're making a 2D game, you start with a very powerful one-size-fits-all library that takes a lot of refinement. It makes sense to use another layer on top of XNA to get you even closer to your game type without yet having to write any code.

IceCream is a framework written in XNA to handle 2D sprite-based games. If that's the sort of game you want to make, this article and framework are for you. If you want 3D, your time will be better spent reading something else.

Before diving into the meat of the article, I would encourage you to download the source code and sample demo application and give it a playthrough. It's very short but demonstrates many of the things this article discusses: loading scenes, sprite movement, player input (WASD), enemies, bullets and collisions, animation, a scrolling background, etc. The controls are:

  • Movement: W, A, S, and D
  • Fire bullet: Space
  • Drop bomb: Left Shift
  • Quit: Escape

Now that you've seen the engine in action, let's talk a little more about the framework.

What is IceCream and its history?

The IceCream library is the fruits of labor by Episcode (Loïc Dansart) and conkerjo. It hadn't seen a repository commit since 2009 until I found it and asked if there was an update available for XNA 4.0 compatibility. I was in luck, as Loïc had already done all the work but just hadn't committed it. A few hours later and the latest XNA 4.0 compatible code was committed and ready for action. The official code license as given by Loïc via email is to do whatever you want with it, as long as it's not for a public derivative editor/engine.

What's Special About IceCream/Why Should I Use It?

Why write yet another 2D engine for XNA when this one has tons of built-in functionality and a GUI editor? More specifically, IceCream has built-in support for spritesheets, static and animated sprites, layering, tile grids, particle effects, post-processing effects, and composite entities (think: 2D humanoid with arms and legs that are animated like a skeleton rather than a single whole sprite for each position). It even has a GUI editor for putting all those items into your levels ("scenes" as IceCream calls them).

IceCream is built on a component design model. Each SceneItem can have 0 or many Components, which are code pieces you write that do or control anything you want. The most basic type is a velocity component, which gives the sprite its movement. A velocity component might have a Vector2 to describe X and Y velocities, and every Update(), move the sprite by those amounts. But IceCream doesn't have any built-in components or make assumptions about how you want to write your game. It just enables you to attach reusable code to every scene item, via components, that is given an Update() call every loop.

There is no override-able Draw() method because IceCream handles all drawing. That's the only constraint of this engine. Since it does all the drawing, you don't have the opportunity to do your own drawing if that's your thing. But that's also the point: all the drawing code has been written for you. (The exception to this is that your main game class, that inherits from IceCream.Game, does get a Draw() override, but components do not.)

If you feel it's really lacking a drawing ability you want, however, it's open-source, and you can easily jump in and modify it to your hearts desire. After working in the codebase for the past few months, I can say that it's pretty easy to understand once you learn where things are. The drawing portion is a bit complicated because it's extremely capable, but it's not magic.

I'm going to skip over the details of the GUI because I'm assuming that you have some development experience and knowledge. Thus, the MilkShake UI should come pretty naturally in 10 to 15 minutes of self-exploration when opening the included project file (point MilkShake to IceCream1945/Game.icproj).

Component Properties

As I mentioned, IceCream is component based, and the primary location for your code is within these components. Each component can override the following methods:

  • CopyValuesTo(object target) - Called regularly by the IceCream engine to perform deep-copies. Anytime you add properties to your component, you'll need to add them to the copy operation that happens in this method. It's up to you to decide what parts of the object's state are relevant to a deep copy, and which should be skipped.
  • OnRegister() - Called whenever the parent SceneItem is registered with the scene, which happens when the scene is loaded into memory, or the next Update() after a SceneItem is new-ed and added to the scene (footnote: it's also possible to tell a scene to register an item immediately rather than next Update()).
  • OnUnRegister() - Called whenever a SceneItem is marked for deletion and removed from the scene.
  • Update(float elapsedTime) - Called every loop for your component to advance state, whatever that might mean. This is where the real meat happens. A component to check for player input would do the input checking in this method. Likewise, our aforementioned VelocityComponent example would use this method to modify the X and Y position of it's parent SceneItem.

Sometimes it makes sense for these properties to display in the MilkShake GUI settings area when building the scene. To do this, we decorate those properties with [IceComponentProperty("text")]. This attribute is used to tell the MilkShake UI that this property should be editable in the property list UI, and what text description to use. Properties without this attribute are not exposed in MilkShake. The easy way to think of this is, if it has an IceComponentProperty decorator, it's a configuration value in the editor. If not, it's probably an internally managed state property.

Example of IceComponentProperty
C#
[IceComponentProperty("Velocity Vector")]
public Vector2 Velocity { get; set; }
Various Examples of IceCream Component Properties in the UI

327977/Property_Samples.jpg

Example: Full VelocityComponent
C#
namespace IceCream1945.Components
{
    [IceComponentAttribute("VelocityComponent")]
    public class VelocityComponent : IceComponent
    {
        [IceComponentProperty("Velocity Vector")]
        public Vector2 Velocity { get; set; }

        public VelocityComponent() {
            Enabled = false;
            //we manually Enable the component in other locations
            //of code. By default, all components are enabled.
        }
        public override void OnRegister() { }

        public override void CopyValuesTo(object target) {
            base.CopyValuesTo(target);
            if (target is VelocityComponent) {
                VelocityComponent targetCom = target as VelocityComponent;
                targetCom.Velocity = this.Velocity;
            }
        }

        public override void Update(float elapsedTime) {
            if (Enabled) {
                this.Owner.PositionX += Velocity.X * elapsedTime;
                this.Owner.PositionY += Velocity.Y * elapsedTime;
            }
        }

    }
}

Getting Into the Code

Moving past components and getting into initial game startup for IceCream1945 (not part of the core IceCream library), we have our MainGame class, which inherits from IceCream.Game. We can optionally override the common XNA methods like Update() and Draw(). I use this class for basic initialization, holding the current scene, and navigating among scenes (e.g., title screen to level 1, level 1 to game over, etc). In my sample, I wanted this class to be very lightweight and relatively dumb.

Again, this class is custom code, and not part of the standard IceCream codebase.

C#
public class MainGame : IceCream.Game
{
    public static GameScene CurrentScene;

    public static readonly string SplashIntroSceneName = "Splash";
    public static readonly string Level1SceneName = "Level1";
    public static readonly string EndingSceneName = "Ending";

    public MainGame() {
        GlobalGameData.ContentDirectoryName = ContentDirectoryName = "IceCream1945Content";
        GlobalGameData.ContentManager = Content;
    }

    protected override void LoadContent() {
        base.LoadContent();
        this.IsFixedTimeStep = false;

        CurrentScene = new MenuScene(SplashIntroSceneName);
        CurrentScene.LoadContent();
    }

    protected override void Update(GameTime gameTime) {
        if (GlobalGameData.ShouldQuit) {
            if (CurrentScene != null)
                CurrentScene.UnloadContent();
            this.Exit();
            return;
        }

        if (CurrentScene.MoveToNextScene) {
            if (CurrentScene.SceneName == SplashIntroSceneName || 
                          CurrentScene.SceneName == EndingSceneName) {
                CurrentScene.UnloadContent();
                CurrentScene = new PlayScene(Level1SceneName);
                CurrentScene.LoadContent();
            }
            else if (CurrentScene.SceneName == Level1SceneName) {
                CurrentScene.UnloadContent();
                CurrentScene = new MenuScene(EndingSceneName);
                CurrentScene.LoadContent();
            }
        }
        base.Update(gameTime);

        CurrentScene.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime) {
        base.Draw(gameTime);

        CurrentScene.Draw(gameTime);
    }

    protected override void UnloadContent() {
        base.UnloadContent();
    }
}

The scene that I'm referring to here is different than the IceCream.Scene. I've created an abstract GameScene class that is the base class of PlayScene and MenuScene. This allows me to reuse common methods in GameScene but have specialized circumstance handling for a "menu"-like scene vs a "gameplay" scene (and potentially others). In a GameScene, I'm expecting for the player to be actively controlling a game object, for the AI to be thinking, and for the camera to be moving. But a MenuScene is all about presenting information and waiting for user input. Hence, I felt it would be more clear to split the two cases into separate objects rather than litter a single object with conditionals.

MenuScene Class Update() method
C#
public override void Update(GameTime gameTime) {
    base.Update(gameTime);
    float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;

    IceCream.Debug.OnScreenStats.AddStat(string.Format("FPS: {0}", 
                   DrawCount / gameTime.TotalGameTime.TotalSeconds));

    if (ReadInput || WaitBeforeInput.Stopwatch(100)) {
        ReadInput = true;

        if (InputCore.IsAnyKeyDown()) {
            MoveToNextScene = true;
        }
    }
}

Preloading, Caching, and GlobalGameData

GlobalGameData Class and Startup Caching
C#
public static class GlobalGameData
{
    public static ContentManager ContentManager = null;

    public static bool ShouldQuit = false;
    public static string ContentDirectoryName = string.Empty;
    public static int ResolutionHeight = 720, ResolutionWidth = 1280;
    public static int PlayerHealth;
    public static int MaxPlayerHealth = 18;
    public static bool SoundOn = true;
    public static bool MusicOn = true;
    public static float SoundEffectVolume = 0.3f;
    public static float MusicVolume = 0.3f;

    public static List<sceneitem> InactiveSceneItems = new List<SceneItem>();
    public static List<sceneitem> ActiveSceneItems = new List<SceneItem>();
    public static SceneItem PlayerAnimatedSprite = null;
    public static PostProcessAnimation ScreenDamageEffect = null;
    public static PointTracking PlayerOnePointTracking = new PointTracking();
}

C#
GlobalGameData.PlayerAnimatedSprite = 
  scene.GetSceneItem<AnimatedSprite>("PlayerPlane_1");
HealthBarItem = scene.GetSceneItem<Sprite>("HealthBar");
GlobalGameData.PlayerHealth = GlobalGameData.MaxPlayerHealth;
//[...]
GlobalGameData.ScreenDamageEffect = 
  scene.CreateCopy<PostProcessAnimation>("PlayerDamageScreenEffect");
GlobalGameData.ScreenDamageEffect.Stop();
scene.RegisterSceneItem(GlobalGameData.ScreenDamageEffect);
//[...]
foreach (SceneItem si in scene.SceneItems)
    GlobalGameData.InactiveSceneItems.Add(si);

//sort the inactive list so we only have to look at the very
// first one to know if there is anything to activate.
GlobalGameData.InactiveSceneItems.Sort(delegate(SceneItem a, SceneItem b)
   { return b.PositionY.CompareTo(a.PositionY); });

GlobalGameData is a singleton class (my custom code, not part of base IceCream) that holds references to game settings and commonly used objects. During things like collision detection, it's necessary to run through all scene items for detection. Something like that is extremely slow and can catch up to you after your game is past a simple proof of concept. So I've created Active and InactiveSceneItems lists for just this purpose. When running through items looking for collisions, I only look through what's active, which I consider to be sprites that are shown on screen or have moved beyond it (though those should be eliminated automatically by a bounds detection component). This way I'm not checking scene items at the end of the level when the player just starts, and I can control the enabling of scene items as the player moves throughout the level rather than having all sprites immediately start traversing the level.

There are faster methods, such as dividing the screen into quadrants or other sectioning, but for now, an active list of only on-screen SceneItems is more than fast enough.

Additionally, things like the health box and score are moved every frame to be at the top of the screen while the camera moves "upward". This requires the code to always touch the PositionY property, and it's very wasteful to find these objects every frame. So we find them once and keep a reference at our fingertips.

Essentially, GlobalGameData is where "global" variables are held. In a more robust game, these references would probably be split into more concise objects, such as a static Settings object and a SceneItemCache object, or corresponding management-type objects. But in all cases, we only need and want one copy of this data, and we want it accessible from nearly everywhere in our game. We don't want to have to pass around some sort of context object to all our methods when a globally-accessible context object will work just the same (because we'll never need to have two context's co-exist in a single running instance). There is a fine line between "global variables are bad" and bloating all your method signatures to pass a reference to an object throughout your code.

Game Loop: Scene Movement and Background Scrolling

The scene is moved along with actual movement of the camera. Some scrollers work by moving the scene into the view of a static camera or other indirect ways, but I decided it was most efficient and mentally straight-forward to move just the camera and have it scan across the level. Some of the reasons include:

  1. If I had to move the scene into the view of the camera, I would need to modify the position of every single scene item every single frame to move them into the view of the camera. By moving the camera, only the scene items actually moving via AI have to move.
  2. Keeping the camera static but spawning objects just outside the view of the camera via a script adds significant code and GUI complexity.
  3. The GUI editor is already setup for camera movement over a laid-out scene. The point of using IceCream is to leverage the tools someone else has written.
Code of Camera Movement
C#
float ScrollPerUnit = 60.0f;
[...].float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;
float distance = elapsed * ScrollPerUnit;

scene.ActiveCameras[0].PositionY -= distance;
BoundsComponent.ResetScreenBounds();

The background movement is only partially real. It's just a single sprite the width of the screen and 32 pixels tall, duplicated to the height of the screen plus two. Each time the bottom strip disappears from the view of the camera, it is shuffled to the top. So the background images are perpetually being moved bottom to top as the camera moves along the scene, and we don't have to waste memory copying the strip to the entire vertical length of the scene. We only need enough memory to cover the users view.

Code of Construction of Background Sprites
C#
/* Load Background Water sprites */
Sprite water = scene.CreateCopy<Sprite>("Water1280");
WaterHeight = water.BoundingRectSize.Y;
double totalWaters = Math.Ceiling(GlobalGameData.ResolutionHeight / WaterHeight) + 1;
for (int i = -1; i < totalWaters; ++i) {
    Sprite w = scene.CreateCopy<Sprite>("Water1280");
    w.Layer = 10;
    w.PositionX = 0;
    w.PositionY = i * WaterHeight;
    scene.RegisterSceneItem(w);
    BackgroundWaters.Add(w);
}
Code of Background Sprite Movement and Swapping
C#
/* Background Water Movement (totally fake) 
 * Since our camera is moving up the Y axis, all we have to do is shift the background upwards
 * everytime the camera moves the equivalent of a tile height. */
BackgroundOffset += distance;
if (BackgroundOffset > WaterHeight) {
    foreach (Sprite water in BackgroundWaters) {
        water.PositionY -= WaterHeight;
    }
    BackgroundOffset -= WaterHeight;
}

For sprite and camera movement, I decided to move things in the game based upon the time that has passed between update calls. XNA likes to run at 60 fps regardless of whether you enable IsFixedTimeStep, and in a low complexity game as it is right now it doesn't make much difference. But if our framerate ever starts to drop due to scene complexity, this design decision will keep the game more playable and consistent.

Example of Time-Based Sprite Movement
C#
public override void Update(float elapsedTime) {
    if (Enabled) {
        this.Owner.PositionX += Velocity.X * elapsedTime;
        this.Owner.PositionY += Velocity.Y * elapsedTime;
    }
}

The other way to write this method is to move each sprite a flat X number of pixels each call to update, regardless of how much time has passed. Very old games written in the 80's and early 90's that used this method are out of control on today's PCs. Back then, developers didn't think their games would still be played today, so they were written to go all-out with no limiter of any kind in place. I think it's important to realize your code has much more life in it than you think.

To prevent that scenario from happening in the inevitable future, the XNA framework is supposed to be intelligent enough to cap the framerate at 60 frames per second even if the hardware can play your game much faster.

Player Movement and Firing: PlayerControllableComponent

The PlayerControllableComponent is applied to the sprite that the player controls. It watches for input via IceInput and moves the sprite accordingly. It's setup for multiple players, but I have not implemented it. The rest of the code could easily be modified for co-op, but I decided it was out of scope for my first exercise.

PlayerControllableComponent Class Update()
C#
public override void Update(float elapsedTime) {
    // when the owner uses PlayerIndex.One
    if (Playerindex == PlayerIndex.One) {
        // if W button is pressed
        if (InputCore.IsKeyDown(Keys.W)) {
            // we go upwards
            Owner.PositionY -= Velocity.Y;
        }
        // if S key is pressed
        if (InputCore.IsKeyDown(Keys.S)) {
            // we go downwards
            Owner.PositionY += Velocity.Y;
        }
        // if A button is pressed
        if (InputCore.IsKeyDown(Keys.A)) {
            // we go to the left
            Owner.PositionX -= Velocity.X;
        }
        // if D button is pressed
        if (InputCore.IsKeyDown(Keys.D)) {
            // we go to the right
            Owner.PositionX += Velocity.X;
        }

        if (BulletTimer.Stopwatch(100) && InputCore.IsKeyDown(Keys.Space)) {
            //fire projectile
            AnimatedSprite newBullet = Owner.SceneParent.CreateCopy<AnimatedSprite>("FlamingBullet");
            newBullet.Visible = true;
            newBullet.Position = Owner.Position;
            newBullet.PositionX += Owner.BoundingRectSize.X / 2;
            VelocityComponent velocityCom = newBullet.GetComponent<VelocityComponent>();
            velocityCom.Enabled = true;
            Owner.SceneParent.RegisterSceneItem(newBullet);
            Sound.Play(Sound.Laser_Shoot_Player);
        }

        if (BombTimer.Stopwatch(100) && InputCore.IsKeyDown(Keys.LeftShift)) {
            //drop bomb
            Sprite newBullet = Owner.SceneParent.CreateCopy<Sprite>("Bomb");
            newBullet.Visible = true;
            newBullet.Position = Owner.Position;
            newBullet.PositionX += Owner.BoundingRectSize.X / 2;
            Owner.SceneParent.RegisterSceneItem(newBullet);
        }
    }
    //[...] Unused code relating to PlayerTwo

    if (InputCore.IsKeyDown(Keys.Escape)) {
        GlobalGameData.ShouldQuit = true;
    }
}

This component watches for all player input, including the fire and drop-bomb buttons, so it also handles spawning those scene items when necessary. IceCream allows us to handle this very easily. With MilkShake, I created a FlamingBullet animated sprite with the following components already attached and configured:

  • VelocityComponent: Give this bullet X and Y velocity.
  • LifeComponent: If this bullet lives for longer than two seconds, destroy it. This prevents bullets from slipping through other checks and living forever. This is more useful for testing by throwing an exception if 2 seconds is ever reached. Currently, no player bullets should live that long, so anything that does is an error. It's possible and even prudent to write some components as small tests to ensure other components are doing their job.
  • BoundsComponent: If this bullet moves completely out of view of the camera, destroy it. We don't want the player destroying objects all the way down the level.
  • BulletComponent: This component handles checking for collisions and dealing damage.

When the player fires a flaming bullet, I ask IceCream to make a copy of this template, tweak the speed on the VelocityComponent, and register the scene item with IceCream. The IceCream engine and my components then take over the rest, and the PlayerControllableComponent can completely forget about it.

Collision Detection

Collision detection in this demo application is only relevant for "bullets", aka projectiles. The player fires bullets, and enemies fire bullets. Each bullet is an animated sprite with a BulletComponent, which has two flags for whether it can damage the player and whether it can damage enemies (it could be both).

When this component executes Update(), it looks through all scene items in the ActiveSceneItems list and compares its bounding rectangle to the bullets. If they intersect, it's a collision. A better way of it doing it that I didn't implement here is, after it's determined the rectangles cross, drop into a more granular test of either pixel-by-pixel, or polygon detection. However, I again decided this was out of scope for this initial exercise. IceCream has some initial support for polygon collision detection, but it's not yet fully implemented.

If there is a collision, an explosion animation is spawned and damage is applied appropriately. For enemies, right now they simply die. The player, however, has a life bar that decreases per hit. Additionally, if the player is hit, a post-processing effect is briefly played that causes the screen to blur and flash very briefly. This gives the player visual feedback that they were struck that is far more noticeable than looking to see if bullets hit or life decreased. It catches their attention regardless of what part of the screen they're looking at.

C#
public override void Update(float elapsedTime) {
    //We get all scene items ( not recommended in bigger games, at least not every frame)
    for (int i=0; i < GlobalGameData.ActiveSceneItems.Count; ++i) {
        SceneItem si = GlobalGameData.ActiveSceneItems[i];

        if (si != Owner && (si.GetType() == typeof(Sprite) || 
                 si.GetType() == typeof(AnimatedSprite)) && !Owner.MarkForDelete) {
            if (CanDamageEnemy) {
                if (si.CheckType(2) && Owner.BoundingRect.Intersects(si.BoundingRect)) {
                    si.MarkForDelete = true;
                    Owner.MarkForDelete = true;
                    GlobalGameData.ActiveSceneItems.Remove(si);
                    GlobalGameData.ActiveSceneItems.Remove(Owner);
                    --i;

                    AnimatedSprite explosion = 
                      Owner.SceneParent.CreateCopy<AnimatedSprite>("BigExplosion");
                    explosion.Visible = true;
                    explosion.Position = si.Position;
                    explosion.PositionX += si.BoundingRectSize.X / 2;
                    explosion.PositionY += si.BoundingRectSize.Y / 2;
                    explosion.CurrentAnimation.LoopMax = 1;
                    explosion.CurrentAnimation.HideWhenStopped = true;
                    Owner.SceneParent.RegisterSceneItem(explosion);

                    GlobalGameData.PlayerOnePointTracking.AddScore(50);

                    Sound.Play(Sound.ExplosionHit);
                }
            }
            if (CanDamagePlayer) {
                if (Owner.BoundingRect.Intersects(GlobalGameData.PlayerAnimatedSprite.BoundingRect)) {
                    --GlobalGameData.PlayerHealth;
                    Owner.MarkForDelete = true;

                    //if the player is getting bombarded, we want many flashes
                    GlobalGameData.ScreenDamageEffect.Reset();
                    GlobalGameData.ScreenDamageEffect.Play();

                    Sound.Play(Sound.Quick_Hit);
                }
            }
        }
    }
}

Post Processing Effect on Player-Bullet Collision

As I mentioned earlier, IceCream supports various post-processing effects out of the box. Many classic scrolling shooters have some sort of screen-flashing effect when the player is hit and loses a health point. A post-processing effect is the natural way to pull off this trick. I load and cache the effect when the scene is first loaded, and just instruct IceCream to play it a single time whenever a collision occurs.

I set up the effect through this UI in Milkshake:

327977/PostProcessorUI.jpg

And then call it in the BulletComponent. I call Reset() before I call play, so if the player is hit repeatedly the effect will start over instantly rather than only finishing playing once. I think this sort of immediate feedback to the player is key to a game that feels "tight".

C#
if (CanDamagePlayer) {
    if (Owner.BoundingRect.Intersects(GlobalGameData.PlayerAnimatedSprite.BoundingRect)) {
        --GlobalGameData.PlayerHealth;
        Owner.MarkForDelete = true;

        GlobalGameData.ScreenDamageEffect.Reset();//if the player is getting bombarded, we want many flashes
        GlobalGameData.ScreenDamageEffect.Play();

        Sound.Play(Sound.Quick_Hit);
    }
}

Enemy Movement and AI, and Tags Feature

One of the wonderful things about IceCream being Open-Source is that you're free to modify it if you don't think it serves all your needs. One of the neat changes I made was to add a "tags" property, which is a simple list of strings that I use for grouping and other attributes. Because it would be slow to parse this tag data during game-play, I parse and cache in one pass when the scene is loaded.

Group (Tag) Caching
C#
protected Dictionary<string, List<SceneItem>> Cache_TagItems;

public GameScene(string sceneName) {
    [...]
    Cache_TagItems = new Dictionary<string, List<SceneItem>>();
}

public virtual void LoadContent() {
    [...]
    CacheTagItems();
}
[...].public string GetGroupTagFor(SceneItem si) {
    foreach (string tag in si.Tags) {
        if (tag.StartsWith("group"))
            return tag;
    }
    return string.Empty;
}

protected void CacheTagItems() {
    if (scene == null)
        throw new Exception("Can't cache tags before loading a scene (scene == null)");

    foreach (SceneItem si in scene.SceneItems) {
        foreach (string tag in si.Tags) {
            if (!string.IsNullOrEmpty(tag)) {
                if (!Cache_TagItems.ContainsKey(tag)) {
                    Cache_TagItems[tag] = new List<SceneItem>();
                }
                Cache_TagItems[tag].Add(si);
            }
        }
    }
}

public List<sceneitem> GetItemsWithTag(string tag) {
    if (Cache_TagItems.ContainsKey(tag))
        return Cache_TagItems[tag];
    else
        return new List<sceneitem>();
}

In IceCream1945, the primary use of tags is for grouping enemies. As the level is scrolled, each Update() call checks to see if a sprite has crossed a threshold near the camera's view and should come to life in the scene. Because this is a top-down shooter, sprites tend to come to life in waves. That is, four or more sprites will move across the scene as one. To facilitate this, each sprite is assigned a group through the tag mechanism. When one sprite is activated, the group tag is read, and all other scene items with a matching group are activated. This activates the entire "wave" and keeps them moving as one without extra code or level design complexity.

SceneItem Activation and Group Activation Snippet
C#
int activationBufferUnits = 20;
for (int i=0; i < GlobalGameData.InactiveSceneItems.Count; ++i) {
    SceneItem si = GlobalGameData.InactiveSceneItems[i];

    if (si.BoundingRect.Bottom + activationBufferUnits > scene.ActiveCameras[0].BoundingRect.Top) {
        //grab all other scene items in the same group and turn them on as well
        EnableSceneItem(si);
        GlobalGameData.InactiveSceneItems.RemoveAt(i);
        --i;

        string groupTag = GetGroupTagFor(si);
        if (groupTag == string.Empty)
            continue;
        List<sceneitem> groupItems = GetItemsWithGroupTag(groupTag);
        foreach (SceneItem gsi in groupItems) {
            EnableSceneItem(gsi);
            GlobalGameData.InactiveSceneItems.Remove(gsi);
        }
    }
    else //if we didn't find anything to activate, we can just abort the loop. Our inactive list is sorted
        break; //so that the "earliest" items are first. Thus, continuing the loop is pointless.
}

Scoring and Achievements

Finally, for scoring and achievement tracking, I have a PointTracking singleton that can be created per-player (or configured for multi-player storage) if you wanted to add multi-player support. Right now it's only used for scoring, but it's intended to be in scope for the entire application for other things like number of shots fired, number of kills, etc that can be fun stats to view at the end of a level, or even track for the life of a player and grant achievements.

PointTracking code snippet
C#
/// <summary>
/// General class for tracking score, number of kills, shots fired,
/// and other metrics for achivements and similar
/// </summary>
public class PointTracking
{
    public int PlayerScore { get; private set; }
    public void AddScore(int additionalPoints) {
        PlayerScore += additionalPoints;
    }
}

That concludes this intro to the 2D XNA IceCream library through the sample game IceCream1945. I hope you enjoyed the article and will download the sample application and source code. IceCream is a phenomenal library that deserves more attention and contributions.

Enjoy! Tyler Forsythe, http://www.tylerforsythe.com/.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)