Simple Tower

From TRCCompSci - AQA Computer Science
Revision as of 16:54, 5 January 2019 by Admin (talk | contribs) (Collision Detection)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

This tutorial was originally created to show the barebones of a tower defence game, however I have added more at the end of this tutorial to turn it into more of a game.

Creating and load the map

Create a tiled map or create a background image

You will firstly need to define the size of your game, so add the following dimensions into the Game1 constructor:

            graphics.PreferredBackBufferHeight = 600;
            graphics.PreferredBackBufferWidth = 1024;
            graphics.ApplyChanges();

Now you have a size to work with. If your tileset tiles are 32 pixels by 32 pixels your map will be 18 tiles high by 32 tiles wide. Remember a tower defence game will normally not have a scrolling map, so we need to draw the whole map onto a single screen.

Set the enemy waypoints to match the path of your map / image

I have created the map below, This has two tile layers. One for the background and one for the path. I have also created an Objects layer which contains all of the objects below:

Tower map.gif

I have also created a custom property on the Objects layer called Points, i have set this to the number of points on the map:

Tower custom property.gif

Load the map

Setup Square.Tiled

If you have a project ready, create a new class in your project. Click project and new class and call it Tiled.cs, then copy the code from this document over the code in your new class: Square.Tiled Class

Or get it from GitHub: GitHub TRCCompSci tiled-xna

Remember to set the name space to Squared.Tiled.

You will need to add references in the using section for the following:

using System.IO;
using Squared.Tiled;

Map Variables

At the top of your Game1 class add these additional variables:

Map map;
Vector2 viewportPosition;

LoadContent for map

In the LoadContent method add the following lines to load the map:

map = Map.Load(Path.Combine(Content.RootDirectory, "SimpleTower.tmx"), Content);

The Draw Method

Add the following to the draw method to draw the map to the screen.

If you already have spriteBatch.Begin() or spriteBatch.End() then just place the middle line inbetween your lines.

spriteBatch.Begin();
map.Draw(spriteBatch, new Rectangle(0, 0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height), Vector2.Zero);
spriteBatch.End();

Make sure your map is the first thing drawn within your spriteBatch. At this point your project should run an display your map.

Reading points from map

Add the following code into the LoadContent method. This firstly reads the number of points from the property in the map. It then creates a Queue of Vectors, and the for loop will read each of the point values from the map. Finally it add the End point to the end of the queue to make the enemy go to the end:

int points = Convert.ToInt32(map.ObjectGroups["Objects"].Properties["Points"]);
Queue<Vector2> path = new Queue<Vector2>();
for (int i = 1; i <= points; i++)
{
    path.Enqueue(new Vector2(map.ObjectGroups["Objects"].Objects["Point" + i].X, map.ObjectGroups["Objects"].Objects["Point" 
         + i].Y));
}
path.Enqueue(new Vector2(map.ObjectGroups["Objects"].Objects["End"].X, map.ObjectGroups["Objects"].Objects["End"].Y));

The Enemy

You will need to create a new class, so click on project & select new class. Obviously give the class the name enemy. You will need to add the MonoGame references at the top of the code (in the using section).

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

No to create a moving enemy you will need to declare some variables within the class. The enemy is going to need a texture to display, but also a path to follow. The best way to represent a path would be to have a Queue of vectors, and each vector would be a waypoint for the enemy:

public Texture2D texture;
Queue<Vector2> path = new Queue<Vector2>();

In order to move the the enemy we will also need to know the current position, the destination, and how to move between the two. We also need a way to start the enemy, so declare a bool called active and set it to false:

Vector2 position;
Vector2 movement;
Vector2 destination;

bool active = false;

When you create an enemy we can write an Initialise method to setup the enemy. This will allow us to set the Texture and Position of the Enemy. This method can also be used to set the path of the enemy. The path p can be passed from Game1:

        public void Initialize(Texture2D text, Vector2 pos, Queue<Vector2> p)
        {
            texture = text;
            position = pos;
            movement = new Vector2(0, 0);
            path = p;
        }


Now we need a way of starting the enemy, the easiest way is to create a property to get & set the Active boolean. This will also allow us to run additional code when active is set. So add the property below:

        public bool Active {
            get{return active;}
            set {
                active = value;
            }
        }

So if the value of active is set, we want to see if we have a vector in the path queue. If we do have a vector we want to set that as our destination. Now we have a destination we can work out the movement required to get there:

        public bool Active {
            get{return active;}
            set {
                active = value;
                if (path.Count() > 0)
                    destination = path.FirstOrDefault<Vector2>();
                Vector2 difference = (destination - position);
                movement = difference / Vector2.Distance(destination, position);
            }
        }

The vector called difference can be calculated by subtracting the current position from the destination vector. If we used this for movement we would instantly jump to that position, so instead we can divide the difference vector by the distance. This will give us a movement vector which we can keep applying to make the enemy move.

We can now add the Active = true line to the Initialize method. This uses the property we created above:

        public void Initialize(Texture2D text, Vector2 pos, Queue<Vector2> p)
        {
            texture = text;
            position = pos;
            movement = new Vector2(0, 0);
            path = p;
            Active = true;
        }

We will also need an method to get the current position of the enemy. Create the following property:

        public Vector2 Position {
            get{return position;}
        }

Now for the update method for your enemy, create the following method:

        public void Update(GameTime gameTime)
        {
            if (Active)
            {
                position += movement;
            }
        }

This will allow your enemy to move to the first destination, however we will need to check when it arrives at its destination. So add the following:

        public void Update(GameTime gameTime)
        {
            if (Active)
            {
                Vector2 difference = (destination - position);
                if (difference.X > -1 && difference.X < 1 && difference.Y > -1 && difference.Y < 1)
                {
                    Console.WriteLine(position + " " + destination);
                    path.Dequeue();
                }

                position += movement;
            }
        }

We can check we have arrived by check the difference between the position and the destination. If the X & Y are between 1 & -1 we should be at the destination. At the moment the destination is removed from the path, but we also need to get a new destination so we can use the Active property to get a new destination:

        public void Update(GameTime gameTime)
        {
            if (Active)
            {
                Vector2 difference = (destination - position);
                if (difference.X > -1 && difference.X < 1 && difference.Y > -1 && difference.Y < 1)
                {
                    Console.WriteLine(position + " " + destination);
                    path.Dequeue();
                    if (path.Count == 0)
                        Active = false;
                    else
                        Active = true;
                }

                position += movement;
            }
        }

Finally we need to create the Draw method:

        public void Draw(SpriteBatch spriteBatch)
        {
            if (active)
                spriteBatch.Draw(texture, position);
        }

You should now have an enemy which will move from waypoint to waypoint.

Creating Enemy in Game1

In the Game1.cs, add the following in the declaration section (look for SpriteBatch spriteBatch;):

        Texture2D enemyTexture;
        Enemy enemy;

In LoadContent, we need to add the following to create the enemy and set its texture & starting postion:

            enemy = new Enemy();
            enemyTexture = Content.Load<Texture2D>("enemy");
            enemy.Initialize(enemyTexture, new Vector2(map.ObjectGroups["Objects"].Objects["Start"].X, Map.ObjectGroups["Objects"].Objects["Start"].Y),path);

In Update, add the following to update the enemy and also to set active on a key press:

            if (Keyboard.GetState().IsKeyDown(Keys.H))
            {
                enemy.Active = true;
            }

            enemy.Update(gameTime);

Finally we need to add the enemy to the draw method:

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

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

            base.Draw(gameTime);
        }

If you test it and assuming you set each coordinate to a different vector, your enemy should move from waypoint to waypoint. It will disappear when it reaches its final destination.

List of Enemies

To allow multiple enemies create a list of enemies, similar to the list of bullets in the enemy class.

Declarations

In your Game1.cs we want to change this into a list so make sure the using section has:

using System.Collections.Generic;

In the declarations towards the top of your code you should see:

Enemy enemy;

We want to change this into a list so make change it to:

List<Enemy> enemies = new List<Enemy>();

This creates a list of Enemy called enemies.

LoadContent

Now you have changed the declaration, you will have several errors in the LoadContent section. What we want to do is to create a new enemy in this section, and then add it to the list of enemies. Your current code should be:

enemy = new Enemy();
enemyTexture = Content.Load<Texture2D>("enemy");
enemy.Initialize(enemyTexture, new Vector2(300,300),path);

Change this by adding the declaration bit onto the first line, and set the vector to the start position from the map:

Enemy enemy = new Enemy();
enemyTexture = Content.Load<Texture2D>("enemy");
enemy.Initialize(enemyTexture, new Vector2(map.ObjectGroups["Objects"].Objects["Start"].X,  
     map.ObjectGroups["Objects"].Objects["Start"].Y),path);

Now we need to add this new enemy into our list of enemies:

enemies.Add(enemy);

Update Method

Your update method currently has this code to start the single enemy. We need to change this:

            if (Keyboard.GetState().IsKeyDown(Keys.H))
            {
                enemy.Active = true;
            }

In the if statement for the H key we can copy the code from the LoadContent method to create a new enemy:

if (Keyboard.GetState().IsKeyDown(Keys.H))
{
     int points = Convert.ToInt32(map.ObjectGroups["Objects"].Properties["Points"]);
     Queue<Vector2> path = new Queue<Vector2>();
     for (int i = 1; i <= points; i++)
     {
         path.Enqueue(new Vector2(map.ObjectGroups["Objects"].Objects["Point" + i].X, map.ObjectGroups["Objects"].Objects["Point" 
             +i].Y));
     }
     path.Enqueue(new Vector2(map.ObjectGroups["Objects"].Objects["End"].X, map.ObjectGroups["Objects"].Objects["End"].Y));
     enemy.Initialize(enemyTexture, new Vector2(map.ObjectGroups["Objects"].Objects["Start"].X, 
          map.ObjectGroups["Objects"].Objects["Start"].Y),path);
     enemies.Add(enemy);
}

Now for calling the update for each enemy, this involves moving most of the update code into a foreach loop:

 foreach(Enemy enemy in enemies)
            {
                if (enemy.Active)
                {
                    enemy.Update(gameTime);
                    Rectangle enemyRec = new Rectangle((int)enemy.Position.X, (int)enemy.Position.Y, enemy.texture.Width, 
                         enemy.texture.Height);
                }
            }

I have also added enemy.Active = false to kill the enemy when it collides with a bullet. You should be left with:

            tower.Update(gameTime);
The entire update method should currently be:

        protected override void Update(GameTime gameTime)
        {
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
                Exit();

            if (Keyboard.GetState().IsKeyDown(Keys.H))
            {
                int points = Convert.ToInt32(map.ObjectGroups["Objects"].Properties["Points"]);
                Queue<Vector2> path = new Queue<Vector2>();
                for (int i = 1; i <= points; i++)
                {
                    path.Enqueue(new Vector2(map.ObjectGroups["Objects"].Objects["Point" + i].X, 
                         map.ObjectGroups["Objects"].Objects["Point" + i].Y));
                }
                path.Enqueue(new Vector2(map.ObjectGroups["Objects"].Objects["End"].X, map.ObjectGroups["Objects"].Objects["End"].Y));

                Enemy enemy = new Enemy();
                enemyTexture = Content.Load<Texture2D>("turret");
                enemy.Initialize(enemyTexture, new Vector2(map.ObjectGroups["Objects"].Objects["Start"].X, 
                     map.ObjectGroups["Objects"].Objects["Start"].Y), path);
                enemies.Add(enemy);
            }
            
            foreach(Enemy enemy in enemies)
            {
                if (enemy.Active)
                {

                    enemy.Update(gameTime);
                    Rectangle enemyRec = new Rectangle((int)enemy.Position.X, (int)enemy.Position.Y, enemy.texture.Width, enemy.texture.Height);

                 }
            }

            // TODO: Add your update logic here
            base.Update(gameTime);
        }

Draw Method

The draw method is relatively simple, we need to change this line:

enemy.Draw(spriteBatch);

to use a foreach loop:

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

Timers

We have added the create new enemy to the key press of H, you could use a boolean and a timer (similar to firing rate of the tower) to create a new enemy every n seconds.

Enemy Count

You will also need to count the number of enemies you are creating, that way you could control how many enemies appear in a swam.

The Bullet

Create a new class called Bullet, remember click project and select new class.

You will need to add the MonoGame references to the using section of the new class.

A bullet will require the following variables to be declared. The texture is the sprite for the bullet, the position is its current position, the movement will be how it gets to the target. The boolean will be used to kill the bullet on collision (or when it leaves the screen):

        public Texture2D texture;
        public Vector2 position;
        public Vector2 movement;
        public bool active = false;
        public bool dead = false;

Now when a button is created we will need to initialise it to provide the bullet with its position, movement and texture. Create the Init method below:

public void Init(Texture2D text, Vector2 pos, Vector2 move)
        {
            texture = text;
            position = pos;
            movement = move;
        }

If the bullet is still alive we need to update its position using the movement vector, so create the update method below:

        public void Update(GameTime gameTime)
        {
            if (!dead)
            {
                position += movement;
            }
        }

We will also need to get the position of the bullet, so add the following property:

        public Vector2 Position {
            get{return position;}
        }

Finally if the bullet is still alive we need to draw it on the screen, so create the draw method below:

        public void Draw(SpriteBatch spriteBatch)
        {
            if (!dead)
                spriteBatch.Draw(texture, position);
        }

The Tower

You will need to create a new class for the tower (click project and select new class). You will also need to add the MonoGame references into the using section of your new class.

The tower will obviously be stationary, so we will need to know its position, but also to fire bullets from the center of the tower we will also need to calculate the center vector. The tower will need to create bullets so we also need a texture for the bullet as well as the tower itself. The list of bullets will be used to update and draw each bullet fired from the tower. The radius will be used to see if an enemy is in range, and the active bool will be used to start the tower.

So add the following variables into your tower class:

        public Texture2D texture;
        public Texture2D bulletTexture;
        public Vector2 position;
        public Vector2 center;

        public List<Bullet> bullets = new List<Bullet>();

        private int radius;

        private bool active;

Next we need to create an Init method for the tower. This will need to accept the tower texture, the bullet texture and the tower position. We will also need to calculate the center of the tower and also set the radius:

        public void Init(Texture2D text, Texture2D bullet, Vector2 pos)
        {
            texture = text;
            bulletTexture = bullet;
            position = pos;                
            center = new Vector2((pos.X + texture.Width/2),(pos.Y + texture.Height/2));
            radius = 250;
            active = true;
        }

The tower will need a way of checking what is in range, so create the method below. We know the current position of the tower so we only need to accept the position we are checking. We calculate the distance between the tower and the pos vector. If this is within our radius the tower will run the fire method. Create the following method:

        public void InRange(Vector2 pos)
        {
            float distance = Vector2.Distance(pos, position);
            if (distance <= radius)
            {
                FireOnTarget(pos, distance);
            }
        }

Now a target is aquired, we can calculate the movement required to fire a bullet from our center to the target vector. We can then create a new bullet and run the Init method, passing the texture to use, its starting position and its movement vector. We then add the new bullet to our list of bullets. Create the following method:

        public void FireOnTarget(Vector2 pos, float distance)
        {
            if (active)
            {
                Vector2 movement = ((pos - center) / distance); ;
                Bullet newBullet = new Bullet();
                newBullet.Init(bulletTexture, position, movement);
                bullets.Add(newBullet);
            }
        }

Create an update method for the tower class. The tower will need to update each bullet within the list of bullets, bullets which have died are ignored:

        public void Update(GameTime gameTime)
        {
            foreach (Bullet b in bullets)
            {
                if (!b.dead)
                    b.Update(gameTime);
            }
        }

Finally we need to create a draw method, we need to draw the tower itself and then any bullet it has fired:

        public void Draw(SpriteBatch spriteBatch)
        {
            spriteBatch.Draw(texture, position);
            foreach (Bullet b in bullets)
                if (!b.dead)
                    b.Draw(spriteBatch);
        }

Adding Tower to Game1

We will need to add the following variables into Game1.cs. This will declare the instance of the tower and also the variables to store the texture of the tower & bullet:

        Texture2D towerTexture;
        Texture2D bullet;
        Tower tower;

Now in the LoadContent method add the following to load in the tower & bullet textures. The code also create the tower, which is then initialized:

            towerTexture = Content.Load<Texture2D>("turret");
            bullet = Content.Load<Texture2D>("bullet");
            tower = new Tower();
            tower.Init(towerTexture, bullet, new Vector2(300,300));

Now in the update method, we need to check if the enemy is in range of the tower. Remember if the enemy is in range, the tower will fire on its position. We then need to update the tower:

            tower.InRange(enemy.Position);
            tower.Update(gameTime);

finally in the Draw method, we need to add the code to draw the tower:

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

List of Towers

To allow multiple towers create a list of towers, similar to the list of Enemy above.

Declaration

Change the line:

 Tower tower;

to the code below to create a list of towers:

List<Tower> towers = new List<Tower>();

LoadContent

Changing the line above will mean that several errors are now in the LoadContent method. We need to changed these lines:

            tower = new Tower();
            tower.Init(towerTexture, bullet, new Vector2(300,300));

We need to declare tower fully, and then after running the Init method we need to add it to the tower list:

            Tower tower = new Tower();
            tower.Init(towerTexture, bullet, new Vector2(300,300));
            towers.Add(tower);

We can also create a second tower:

            Tower tower1 = new Tower();
            tower1.Init(towerTexture, bullet, new Vector2(600,300));
            towers.Add(tower1);

Update Method Changes

My update method currently has a few references to tower, these are all underlined as errors.

We need to add another foreach loop after the foreach (Enemy enemy in enemies). We need to create a foreach loop to cycle through each tower, and then within this we will need to cycle through each enemy:

foreach(Enemy enemy in enemies)
{
    enemy.Update(gameTime);
}

foreach(Tower tower in towers)
{

    foreach(Enemy enemy in enimies)
    {
         if (enemy.Active)
         {
             tower.InRange((enemy.Position));
             Rectangle enemyRec = new Rectangle((int)enemy.Position.X, (int)enemy.Position.Y, enemy.texture.Width, enemy.texture.Height);
          }
     }
     tower.Update(gameTime);
}

Draw Method

Within the Draw method, find the line below:

tower.Draw(spriteBatch);

We need to add a foreach loop here to cycle through each tower:

foreach(Tower tower in towers)
     tower.Draw(spriteBatch);

Test The Game

When you test your game the enemy will move and the tower will fire when in range. However it will fire so many bullets that they will appear almost as a solid line. the way to solve this is to add a timer to the tower so that it can only fire a bullet every second or half a second. To user a timer you need to add the following to the using section of Game1.cs:

using System.Timers;

So in the tower class you need to define a timer & a Boolean for firing, this should be with the other variable declarations:

Timer aTimer = new System.Timers.Timer();
bool firing = false;

Now we need to change the FireOnTarget method for the tower, you should currently have:

        public void FireOnTarget(Vector2 pos, float distance)
        {
            if (active)
            {
                Vector2 movement = ((pos - center) / distance); ;
                Bullet newBullet = new Bullet();
                newBullet.Init(bulletTexture, position, movement);
                bullets.Add(newBullet);
            }
        }

We need to change it to implement the timer:

        public void FireOnTarget(Vector2 pos, float distance)
        {
            if (active && !firing)
            {
                firing = true;
                Vector2 movement = ((pos - center) / distance); ;
                Bullet newBullet = new Bullet();
                newBullet.Init(bulletTexture, position, movement);
                bullets.Add(newBullet);
                aTimer.Elapsed+=new ElapsedEventHandler(OnTimedEvent);
                aTimer.Interval=500;
                aTimer.Enabled=true;
            }
        }

Now we only fire if active and firing is currently false. When we fire we set firing to true and start the timer. The timer is set for 500 milliseconds, and when the timer elapses it will run a timed event called OnTimedEvent.

Finally we need to create the method below for OnTimedEvent:

 private static void OnTimedEvent(object source, ElapsedEventArgs e)
 {
     firing = false;
 }

this will set firing back to false after the timer as elapsed.

Collision Detection

We should now have a tower, an enemy which the tower fires at when in range. The final thing to code is the collision between the enemy and the bullet.

We need to create a rectangle for the enemy, We can then use the intersect method of a rectangle. A rectangle has an X, a Y, a width, and a height. Add the code below into the update method of Game1.cs:

Rectangle enemyRec = new Rectangle((int)enemy.Position.X, (int)enemy.Position.Y, enemy.texture.Width, enemy.texture.Height);

Now we need to create a loop which will cycle through each bullet, so add the following below the enemy rectangle:

            foreach (var bull in tower.bullets)
            {
            }

Now within the foreach loop we need to test if the current bullet is dead, add the following:

            foreach (var bull in tower.bullets)
            {
                if (!bull.dead)
                {

                }
            }

Now we can create a rectangle for the current bullet, add this code:

            foreach (var bull in tower.bullets)
            {
                if (!bull.dead)
                {
                    Rectangle bullRec = new Rectangle((int)bull.Position.X, (int)bull.Position.Y, bull.texture.Width, 
                         bull.texture.Height);
                }
            }

Finally add the code to check if the bullet rectangle intersect the enemy rectangle, if it does you should set the bullet to be dead:

            foreach (var bull in tower.bullets)
            {
                if (!bull.dead)
                {
                    Rectangle bullRec = new Rectangle((int)bull.Position.X, (int)bull.Position.Y, bull.texture.Width, 
                         bull.texture.Height);
                    if (bullRec.Intersects(enemyRec))
                        bull.dead = true;
                }
            }

The complete logic should look something like this:

foreach(Enemy enemy in enemies)
{
    enemy.Update(gameTime);
}

foreach(Tower tower in towers)
{

    foreach(Enemy enemy in enimies)
    {
         if (enemy.Active)
         {
             tower.InRange((enemy.Position));
             Rectangle enemyRec = new Rectangle((int)enemy.Position.X, (int)enemy.Position.Y, enemy.texture.Width, enemy.texture.Height);

             foreach (var bull in tower.bullets)
             {
                 if (!bull.dead)
                 {
                     Rectangle bullRec = new Rectangle((int)bull.Position.X, (int)bull.Position.Y, bull.texture.Width, bull.texture.Height);

                     if (bullRec.Intersects(enemyRec))
                     {
                         enemy.Active = false;
                         bull.dead = true;
                     }
                 }
              }
          }
     }
     tower.Update(gameTime);
}