Friday, 8 April 2011

Tutorial 15 : Adding Health Bars – Extended

Up until now, we use the enemies colour to represent his health, and while this works quite nicely for our little black blob, it doesn’t work so well with more complex sprites.
In this tutorial we are going to add little health bars that will float above our enemies heads. A lot of the code in this tutorial has been inspired from George Clingerman’s amazing tutorial on health bars. So if you are struggling with this tutorial, I would recommend reading his first as he explains it in more depth than I will.
The first thing that we’re going to do is stop the enemies colour from changing when the enemy loses health. If you still want this to happen just skip over this next bit.
To do that we need to go to Enemy.cs and find the Draw method. Then you need to replace these lines :
float healthPercentage = (float)currentHealth / (float)startHealth;

Color color = new Color(new Vector3(1 - healthPercentage,
   1 - healthPercentage, 1 - healthPercentage));

base.Draw(spriteBatch, color);

With this :

base.Draw(spriteBatch, Color.Black);

Now instead of tinting our sprite a different colour based on it’s health percentage, we just draw it black.

While we are still in Enemy.cs we are going to add a new property to keep track of how much health an enemy has left :

/// <summary>
/// The % of health that an enemy has left
/// </summary>
public float HealthPercentage
{
get { return currentHealth / startHealth; }
}

With that done, we can move onto drawing the health bars! The first thing we need to do is add a new texture to the content project :
health bar

Note : This texture should be called "health bar.png" however it's name get messed up when I uploaded it!

This is the texture we are going to use for the health bar. The reason that it is just white at the moment is so that we can easily tint it different colours when we draw it.

We are going to draw our health bars in the Wave class, so go to Wave.cs and at the top of the class, add the following field :

private Texture2D healthTexture; // A texture for the health bar.

We will use this field to store the texture we just added.

Next we are going to change a couple of constructors so we can pass our texture to this class. Seeing as we are already looking at the wave class, we will start by changing the Wave constructor. Find the Wave constructor and change it so it looks like this :

public Wave(int waveNumber, int numOfEnemies, Player player,
   Level level, Texture2D enemyTexture, Texture2D healthTexture)
{
   this.waveNumber = waveNumber;
   this.numOfEnemies = numOfEnemies;

   this.player = player;
   this.level = level;

   this.enemyTexture = enemyTexture;
   // Initialze our new texture field
   this.healthTexture = healthTexture;
}

We have added in an extra parameter so that we can pass in a texture to use for the health bars. Next go to WaveManager.cs and find the WaveManager constructor and change it to look like this :

public WaveManager(Player player, Level level, int numberOfWaves,
    Texture2D enemyTexture, Texture2D healthTexture)
{
    this.numberOfWaves = numberOfWaves;
    this.enemyTexture = enemyTexture;

    this.level = level;

    for (int i = 0; i < numberOfWaves; i++)
    {
        int initialNumerOfEnemies = 6;
        int numberModifier = (i / 6) + 1;

        // Pass the wave class our health texture.
        Wave wave = new Wave(i, initialNumerOfEnemies * numberModifier,
            player, level, enemyTexture, healthTexture);

        waves.Enqueue(wave);
    }

    StartNextWave();
}

Again we have added a new parameter to the constructor, and then passed this parameter straight to the Wave class.

The last thing we need to do is go to Game1.cs and load our texture and pass it to our WaveManager. So in Game1.cs find the LoadContent method and more specifically this line :

waveManager = new WaveManager(player, level, 24, enemyTexture);

And replace it with this :

Texture2D healthTexture = Content.Load<Texture2D>("health bar");

waveManager = new WaveManager(player, level, 24, enemyTexture,
                                                healthTexture);

The only difference is that we load in our healthbar texture and pass it to the WaveManager. Now that we have finished the tedious process of changing constructors and passing textures, we can move onto actually drawing our health bars!

The health bars will be drawn as two layers, we will first draw a gray ‘base’ layer that will indicate how much health the enemy started with. On top of that we will draw a gold layer that will indicate how much health the enemy has left.

So go to Wave.cs and find the Draw method, it should look something like this at the moment :

foreach (Enemy enemy in enemies)
{
   enemy.Draw(spriteBatch);
}

Add the following code just underneath where we draw the enemy, but before the last curly bracket :

// Draw the health bar normally.
Rectangle healthRectangle = new Rectangle((int)enemy.Position.X,
                                         (int)enemy.Position.Y,
                                         healthTexture.Width,
                                         healthTexture.Height);

spriteBatch.Draw(healthTexture, healthRectangle, Color.Gray);

The first thing this code does is initialize a Rectangle that has the same dimensions as our health bar texture, and is located directly above our enemies head. Then we actually draw our health bar texture in this rectangle and tint it gray – this code basically draws our ‘base’ layer.

Underneath the last bit of code but before the curly bracket, add the following :

float healthPercentage = enemy.HealthPercentage;
float visibleWidth = (float)healthTexture.Width * healthPercentage;

healthRectangle = new Rectangle((int)enemy.Position.X,
                               (int)enemy.Position.Y,
                               (int)(visibleWidth),
                               healthTexture.Height);

spriteBatch.Draw(healthTexture, healthRectangle, Color.Gold);

The first two lines determine how wide our gold layer should be based off of how much health the enemy has left. We then modify our health rectangle to use this new width and draw our gold texture in the new rectangle!

And that’s all there is to it! If you hit F5 and run you game now you should see something like this :

image

I know that the health bars don’t look very pretty at the moment, but it’s your job to make the game look nice, I'm just here to help with the nuts and bolts of it Winking smile

I hope that this is useful to some of you!

Extra Reading :

One of the commenters on this tutorial was kind enough to provide some code to make the healthbars look a bit more pretty.

The code tints the colour of the health bar based on how much health is left, so when the enemy has full health his health bar will be tinted green and when the enemy has very little health the healthbar will be tinted red.

To do this find this line in Wave.cs in the Draw() method :

spriteBatch.Draw(healthTexture, healthRectangle, Color.Gold);
And replace it with this :

float red = (healthPercentage < 0.5 ? 1 : 1 - (2 * healthPercentage - 1));
float green = (healthPercentage > 0.5 ? 1 : (2 * healthPercentage));

Color healthColor = new Color(red, green, 0);

spriteBatch.Draw(healthTexture, healthRectangle, healthColor);

For more information see the below comment by BlueRaja
Here is the source code for this tutorial.

27 comments:

  1. Great tutorials! Thanks!

    However, I have a little problem. I decided to make my own level editor, but I'm not that experienced at programming. Just like we place towers, I want to change a number from the level array to 1, for example. Here's the code I used to do this:

    -----------------
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using Microsoft.Xna.Framework;
    using Microsoft.Xna.Framework.Graphics;
    using Microsoft.Xna.Framework.Input;

    namespace Level_Editor
    {
    ///
    /// This class is the base class where
    /// features like Loading and Saving are prepared
    /// (Load.cs and Save.cs are child classes of LevelManagement.cs)
    /// It also handles editing
    ///
    class LevelManagement
    {
    #region properties
    protected int width
    {
    get { return map.GetLength(1); }
    }
    protected int height
    {
    get { return map.GetLength(0); }
    }
    public int Width
    {
    get { return width; }
    }
    public int Height
    {
    get { return height; }
    }
    #endregion

    #region array
    int[,] map = new int[,]
    {
    {0,0,0,0,0,0,0,0,},
    {0,0,0,0,0,0,0,0,},
    {0,0,0,0,0,0,0,0,},
    {0,0,0,0,0,0,0,0,},
    {0,0,0,0,0,0,0,0,},
    {0,0,0,0,0,0,0,0,},
    {0,0,0,0,0,0,0,0,},
    {0,0,0,0,0,0,0,0,},
    };
    #endregion

    public List tileTextures = new List();

    #region methods
    public void AddTexture(Texture2D texture)
    {
    tileTextures.Add(texture);
    }

    public void Draw(SpriteBatch batch)
    {
    for (int x = 0; x < Width; x++)
    {
    for (int y = 0; y < Height; y++)
    {
    int textureIndex = map[y, x];
    if (textureIndex == -1)
    continue;

    Texture2D texture = tileTextures[textureIndex];
    batch.Draw(texture, new Rectangle(x * 32, y * 32, 32, 32), Color.White);
    }
    }
    }

    public int GetIndex(int cellX, int cellY)
    {
    if (cellX < 0 || cellX > Width || cellY < 0 || cellY > Height)
    return 0;

    return map[cellY, cellX];
    }
    #endregion

    #region editing
    private MouseState mouseState;
    private MouseState oldState;

    private int cellX;
    private int cellY;

    private int tileX;
    private int tileY;

    public void Update(GameTime gametime)
    {
    mouseState = Mouse.GetState();

    cellX = (int)(mouseState.X / 32);
    cellY = (int)(mouseState.Y / 32);

    tileX = cellX * 32;
    tileY = cellY * 32;

    if (mouseState.LeftButton == ButtonState.Released
    && oldState.LeftButton == ButtonState.Pressed)
    {
    ChangeTile(tileX, tileY);
    }

    oldState = mouseState;
    }

    private void ChangeTile(int x, int y)
    {
    map[x, y] = 1;
    }
    #endregion
    }
    }
    --------------------------
    When I click, nothing changes. Is this because I should use an xml file, for example, saving to that .xml file and then reading it to draw?

    Please help me ;P. Thanks!

    ReplyDelete
  2. Sorry about your comment not appearing, for some reason it was marked as spam!

    I think the problem is in the ChangeTile method, it should be :

    map[y, x] = 1;

    Let me know if that helps or if you need any more advice!

    ReplyDelete
  3. That didn't help :(. I don't want to take your time with this question, but is it maybe possible that the array can't be changed like this? Or maybe I should give this class it's own Draw method?

    ReplyDelete
  4. I think I see your problem now, you need to pass cellX and cellY to ChangeTile instead if tileX and tileY :

    int cellX = (int)(mouseState.X / 32);
    int cellY = (int)(mouseState.Y / 32);

    if (mouseState.LeftButton == ButtonState.Released &&
    oldState.LeftButton == ButtonState.Pressed)
    {
    ChangeTile(cellX, cellY);
    }

    If you change this as well as make the change mentioned above it should work perfectly!

    Let me know if this helps!

    ReplyDelete
  5. public void Update(GameTime gametime)
    {
    mouseState = Mouse.GetState();

    int cellX = (int)(mouseState.X / 32);
    int cellY = (int)(mouseState.Y / 32);

    int tileX = cellX * 32;
    int tileY = cellY * 32;

    if (mouseState.LeftButton == ButtonState.Released
    && oldState.LeftButton == ButtonState.Pressed)
    {
    ChangeTile(cellX, cellY);
    }

    oldState = mouseState;
    }

    private void ChangeTile(int x, int y)
    {
    map[y,x] = 1;

    }

    this is what I have now, it still doesn't work. I'm currently looking on google for more information on arrays, though.

    ReplyDelete
  6. The code you just posted works perfectly for me, I know this sounds like a stupid question, but are you sure you have this line :

    level.Update(gameTime);

    In your Game1.cs update method?

    ReplyDelete
  7. It's always such a stupid thing I forget... Thanks a lot lol!

    ReplyDelete
  8. I have one question regarding the display of xna games. Your level fits the games window so you can draw everything, but what if levels were bigger and you had to scroll to move around it. Do you have to somehow limit Draw methods to only draw objects that are within your sight and window boundaries ? Or does xna take care of that and ignores it ?

    ReplyDelete
  9. Hey man, nice site.

    Here's a cool tip: I like to make my health-bars fade from green to red as the enemy loses health. We could do this by linearly decreasing the green component from 255 to 0 and linearly increasing the red component from 0 to 255. However, this leads to an ugly golden color in the middle.

    Instead, we'll linearly interpolate the hue of the color's HSV value, while keeping the saturation and lightness constant (see http://stackoverflow.com/questions/359612/how-to-change-rgb-color-to-hsv/1626175#1626175). For the simple case of green-to-red, we can do this by interpolating from 0x00FF00 to 0xFFFF00 to 0xFF0000:

    //Assumes 0.0f is 0 health, and 1.0 is full health:
    public Color GetHealthBarColor(float health)
    {
    int red = (health < 0.5 ? 255 : 255 - (int)((2*health-1)*255));
    int green = (health > 0.5 ? 255 : (int)((2*health)*255));
    return new Color(red, green, 0);
    }

    ReplyDelete
  10. Hey, thanks for positing this! I think it would be a really nice addition to the game! I think I will add a bit to the end of the post about this!

    ReplyDelete
  11. Thanks for the tutorial series, it was very well-done. An A-Star (or similar) path-finding tutorial would be incredible if you aren't finished with this yet, since there doesn't seem to be much regarding A* C# sample code.

    ReplyDelete
  12. can I use these codes in making a tower defense in win7 mobile?

    ReplyDelete
  13. Hey,

    Anonymous(1), I am actually just in the process of writing a tutorial on path finding which I will hopefully have posted by the end of the week!

    Anonymous(2), This code should work without any problems on windows phone!

    ReplyDelete
  14. Thanks for the tutorials. I hope there are more to come.

    ReplyDelete
  15. Would love to see your a* tutorial. I tried to understand the tutorials, but it is a pretty hard part of Xna. I hope you can explain it. Thanks for the tutorials they are great :-)!

    ReplyDelete
  16. Loved your tutorials on this mate. They where awesome ^^ Though why havent you posted your healthbar tutorial on the gamedev.com forum? The rest of them are there after all :) Would probs give you more trafic since im sure alot of people would love to make some health bars for their games^^

    Also, could you perchance do a tutorial with 3D?

    ReplyDelete
  17. Just wanted to say many thx for an awesome tutorial. By far better written and explained than most books. Simple and right to the point. Everything worked 100% for me. You should consider writing a programming XNA Games from scratch book / hard copy and atleast get some payback from all your hard work.
    Many thx.
    /Mike

    ReplyDelete
  18. I can't seem to run the coding in visual studio 2010, help!

    ReplyDelete
  19. How can I transfer these codes to the windows 7 emulator?

    ReplyDelete
  20. How do you add new enemy types?

    ReplyDelete
  21. I know this may seen a stupid question, but as i am a beginner, i would like to know how would you make to simulate an explosion from a bombing tower?
    Really nice tutorial!!!!! thanks a lot!!!!

    ReplyDelete
  22. I would recommend reading his first as he explains it in more depth than I will.

    ReplyDelete
  23. Nice Tutorial, but in my game the gray and the yellow bar somehow flickers and overlaps,

    ReplyDelete
  24. the png is not the

    ReplyDelete
  25. thanks for your tutorial, it's very helpfull. One more thing I wanna know, how can I play the sound when the tower shoots?

    ReplyDelete
  26. I see it has been awhile since anyone posted here but in-case anyone is looking for this info, as I was, I have some additional suggestions.

    First, let me say that this tutorial got me well on the way to adding health bars that follow both my player and enemies in a 2D top down game. However, I wanted the health bar to be vertical and to be able to rotate, scale, and draw them at a particular offset form the character/enemy sprite.

    The solution assumes you have your character or enemy sprite rotation, position, scale, origin determined before you get to the draw functions. This routine is in my Entity draw function and I have a Boolean in my Entity class to determine whether health and fuel bars should be shown.

    I am using the spritebatch that allows Texture2d, Vector2 position, source rectangle, color, rotation, origin, scale, sprite effects, and float layer depth. Source rectangle draws the portion of the Texture2D you pass in to sprite batch based on the paramaters you give the rectangle.

    The solution I used is as follows:

    First make your health bar texture oriented vertically instead of horizontal. My health bar texture is identified as Game1.statusbar in the below code.

    if (showBars)
    {
    healthBar_Offset = new Vector2(25, 0); // Offset from this entity to draw bars at

    Vector2 health_bar_pos =
    Vector2.Transform(healthBar_Offset,Matrix.CreateRotationZ(rotation)) + position;

    Rectangle healthRectangle = new Rectangle(0, 0,
    Game1.statusBar.Width,Game1.statusBar.Height);

    spritebatch.Draw(Game1.statusBar, health_bar_pos, healthRectangle, Color.Gray, rotation,
    center, scale, SpriteEffects.None, 0);

    int visableHeight = (int)(Game1.statusBar.Height * ((double)currenthitpoints /
    startingHitpoints));

    healthRectangle = new Rectangle(0, 0, Game1.statusBar.Width, visableHeight);
    spritebatch.Draw(Game1.statusBar, health_bar_pos, healthRectangle,
    Color.LightGreen, rotation, center, scale, SpriteEffects.None, 0);
    }

    ReplyDelete