Plants vs Zombies Load Bar

Plants vs Zombies Load Bar

I’ve written and published three tutorials of the PlanetX series in a row. I am planning on posting the final tutorial of the series soon, but I’ve decided to mix things up a little for now. This tutorial will show the creation of the ‘loading bar’ of Plants vs Zombies. This game is very addictive and replayable. The loading bar really caught my attention. You can see the original below (I couldn’t find a video of it in action).

And here is the one we are going to develop:

My other inspiration for this post was the series “Game Features Replicator” by Catalin Zima.

In this tutorial, I will explain only the core working of the Load Bar, not every line of code. Feel free to download the entire source code here. Please bear in mind that the code is fairly clean, but far from perfect. If you want to make the load bar better, tweaking the constants will get you the results you want.

First, we create a new XNA Windows Game Project. Name it appropriately. I’ve called mine “PvZLoadBar”.

Then, we create a new class called LoadBar. The class, as a whole, looks like this:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
 
namespace PvZLoadBar
{
 class LoadBar
 {
 #region Member Variables
 
 private Vector2 LoadBarPosition; //The position vector of the load bar as a whole (left, top)
 private Texture2D Ground;
 private Texture2D Grass;
 private Texture2D Roll;
 
 private float m_Progress;
 private float m_RollRotation;
 private Vector2 m_RollPosition;
 private float m_RollScale;
 
 #endregion
 
 #region Properties
 
 public float Progress
 {
 get
 {
 return m_Progress;
 }
 set
 {
 if (value < 1)
 m_Progress = value;
 //Else do stuff
 }
 }
 
 public float RollRotation
 {
 get
 {
 return m_RollRotation;
 }
 set
 {
 m_RollRotation = value;
 }
 }
 
 public Vector2 RollPosition
 {
 get
 {
 return m_RollPosition;
 }
 set
 {
 m_RollPosition = value;
 }
 }
 
 #endregion
 
 #region Methods
 
 //Constructor
 public LoadBar(Vector2 paramLoadBarPosition)
 {
 LoadBarPosition = paramLoadBarPosition;
 m_Progress = 0;
 m_RollRotation = 0;
 m_RollPosition = new Vector2(40 + LoadBarPosition.X, 50 + LoadBarPosition.Y);
 }
 
 public void LoadContent(ContentManager Content)
 {
 //Load Textures
 Ground = Content.Load<Texture2D>("Ground");
 Grass = Content.Load<Texture2D>("Grass");
 Roll = Content.Load<Texture2D>("Roll");
 }
 
 public void Update(GameTime gameTime)
 {
 
 m_RollPosition = new Vector2(40 + LoadBarPosition.X + m_Progress * 360, 50 + LoadBarPosition.Y + Roll.Height * m_Progress * 0.5f);
 m_RollRotation = m_Progress*10;
 m_RollScale = 1 - m_Progress;
 }
 
 public void Draw(SpriteBatch spriteBatch)
 {
 //DrawGround
 spriteBatch.Draw(Ground, new Vector2(LoadBarPosition.X, LoadBarPosition.Y+80), Color.White);
 spriteBatch.Draw(Grass, new Vector2(LoadBarPosition.X, LoadBarPosition.Y + 80), new Rectangle(0, 0, (int)(m_RollPosition.X-LoadBarPosition.X), (int)(Grass.Height)), Color.White, 0, new Vector2(0, 0), new Vector2(1, 0.25f), SpriteEffects.None, 0);
 
 spriteBatch.Draw(Roll, m_RollPosition, null, Color.White, m_RollRotation, new Vector2(Roll.Width / 2, Roll.Height / 2), m_RollScale, SpriteEffects.None, 0);
 
 }
 
 #endregion
 }
}

The variables are as follows:

#region Member Variables
 
 private Vector2 LoadBarPosition; //The position vector of the load bar as a whole (left, top)
 private Texture2D Ground;
 private Texture2D Grass;
 private Texture2D Roll;
 
 private float m_Progress;
 private float m_RollRotation;
 private Vector2 m_RollPosition;
 private float m_RollScale;
 
 #endregion

LoadBarPosition: Indicates the position of the load bar as a whole.

The variables with the “m_” prefix are member variables. This means that they are exposed as properties.

m_Progress: Indicates the progress of the load bar. 0 means empty, 1 means full.

m_RollRotation: It’s the rotation of the roll of grass.

m_RollPosition: Stores the position of the roll of grass.

m_RollScale: Stores the scale of the roll. Initially, it’s 1. It goes on decreasing to 0.

Let’s take a quick look at the properties section.

#region Properties
 
 public float Progress
 {
 get
 {
 return m_Progress;
 }
 set
 {
 if (value < 1)
 m_Progress = value;
 //Else do stuff
 }
 }
 
 public float RollRotation
 {
 get
 {
 return m_RollRotation;
 }
 set
 {
 m_RollRotation = value;
 }
 }
 
 public Vector2 RollPosition
 {
 get
 {
 return m_RollPosition;
 }
 set
 {
 m_RollPosition = value;
 }
 }
 
 #endregion

All the properties are exposed as usual. The set{} block of the Progress property has a check to prevent the value from exceeding 1. Also, an arrangement can be made here to allow the execution of a certain block of code, once the loading has been done (i.e. Progress=1), e.g. displaying clickable text, progressing to the next screen, etc.

Now, let’s look at the methods, one at a time:

//Constructor
 public LoadBar(Vector2 paramLoadBarPosition)
 {
 LoadBarPosition = paramLoadBarPosition;
 m_Progress = 0;
 m_RollRotation = 0;
 m_RollPosition = new Vector2(40 + LoadBarPosition.X, 50 + LoadBarPosition.Y);
 }

This is a constructor, a method which will run automatically when you initialize the class LoadBar with the ‘new’ keyword. Here, we accept the position of the load bar and initialize the member variables.

public void LoadContent(ContentManager Content)
 {
 //Load Textures
 Ground = Content.Load<Texture2D>("Ground");
 Grass = Content.Load<Texture2D>("Grass");
 Roll = Content.Load<Texture2D>("Roll");
 }

Here, we load the texture.

public void Update(GameTime gameTime)
 {
 
 m_RollPosition = new Vector2(40 + LoadBarPosition.X + m_Progress * 360, 50 + LoadBarPosition.Y + Roll.Height * m_Progress * 0.5f);
 m_RollRotation = m_Progress*10;
 m_RollScale = 1 - m_Progress;
 }

Here, we update the position of the rolling grass. As the grass rolls, it should progress in the horizontal direction with a constant velocity.

The X coordinate of the origin (center) of the roll is:

Roll.X = 40 + LoadBarPosition.X + (Progress * 360)

40 is the initial position, because the roll width is 80 pixels. We add LoadBarPosition.X to ensure proper placement of the roll. To this, we add (Progress * Suitable Constant). As per my calculations,

Suitable Constant = Total width of Load Bar – (Width of Roll / 2)

= 400 – (80 / 2)

= 360

Now, the Y coordinate of the origin of the roll is:

Roll.Y = 50 + LoadBarPosition.Y + {Progress * (Roll.Height / 2)}

I derived this value experimentally. In hindsight, though, it seems quite simple. I have added 50 instead of 40 to make the roll overlap a little with the ground. This makes it more realistic.

Next, the rotation of the roll is simply multiplied by a suitably enlarged value of progress.

The scale is computed using the formula:

Scale = 1 – Progress

This makes sense, as the scale should be maximum when progress is minimum and vice versa.

public void Draw(SpriteBatch spriteBatch)
 {
 //DrawGround
 spriteBatch.Draw(Ground, new Vector2(LoadBarPosition.X, LoadBarPosition.Y+80), Color.White);
 spriteBatch.Draw(Grass, new Vector2(LoadBarPosition.X, LoadBarPosition.Y + 80), new Rectangle(0, 0, (int)(m_RollPosition.X-LoadBarPosition.X), (int)(Grass.Height)), Color.White, 0, new Vector2(0, 0), new Vector2(1, 0.25f), SpriteEffects.None, 0);
 
 spriteBatch.Draw(Roll, m_RollPosition, null, Color.White, m_RollRotation, new Vector2(Roll.Width / 2, Roll.Height / 2), m_RollScale, SpriteEffects.None, 0);
 
 }

We draw the ground by leaving a margin of 80 pixels on top, for the roll.

Then we draw the grass. While drawing the grass, we specify a source rectangle as shown in the figure below. This gives us the effect that the grass is being rolled on to the ground. Also, I’ve resized the grass to make it a little flat. Tweak the scale to your liking.

Then we draw the roll using the values computed earlier.

Now, let’s take a look at the main Game1 class.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
 
namespace PvZLoadBar
{
 /// <summary>
 /// This is the main type for your game
 /// </summary>
 public class Game1 : Microsoft.Xna.Framework.Game
 {
 GraphicsDeviceManager graphics;
 SpriteBatch spriteBatch;
 LoadBar myLoadBar;
 
 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()
 {
 myLoadBar = new LoadBar(new Vector2(200, 300));
 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()
 {
 spriteBatch = new SpriteBatch(GraphicsDevice);
 myLoadBar.LoadContent(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)
 {
 myLoadBar.Progress+=0.005f;
 myLoadBar.Update(gameTime);
 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();
 
 myLoadBar.Draw(spriteBatch);
 
 spriteBatch.End();
 
 base.Draw(gameTime);
 }
 }
}

Let’s have a look at the code we need to add in this class.

LoadBar myLoadBar;

We declare a new LoadBar called myLoadBar.

myLoadBar = new LoadBar(new Vector2(200, 300));

In the Initialize() method, we initialize the LoadBar.

myLoadBar.LoadContent(Content);

In this method, we call the LoadContent() method of the LoadBar.

myLoadBar.Progress+=0.005f;
 myLoadBar.Update(gameTime);

In the Update() method, we increment the progress of the LoadBar. Then we call its Update() method.

spriteBatch.Begin();
 
 myLoadBar.Draw(spriteBatch);
 
 spriteBatch.End();

In the Draw() method, we call the spriteBatch.Begin() and spriteBatch.End() methods, and call the LoadBar.Draw() method in between.

That’s it! Now, run the code and you should see the load bar in action!!

Source Code and Content