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

Real-time Strategy Engine in MonoGame

4.44/5 (8 votes)
24 May 2023CPOL3 min read 16.3K  
Engine for .NET cross-platform development
This article is about game-engines and object-oriented programming.

Logo

Introduction

The code demonstrates the possibilities of MonoGame in developing real-time strategy gaming engines using object-oriented programming approach, which is based on the class hierarchy.

The article is based upon the prior results in programming with entities and objects for regular expression matching which is also available on CodeProject.

StarDust Gaming Engine

The engine is balanced by Update procedure of the class derived from the class Game as it's defined in classical engines by XNA.

In order to run this code, we need only Visual Studio 2017 and MonoGame setup package as it supports only this latest flavour of Microsoft Visual Studio.

Using the Code

The code can be re-used in overridding style by applying this methodology to the base classes like Map, Cell, Player and Unit.

The Map class is a class for the game map which is covered by 2D-cells and, thus, each cell is defined in Cell class.

The Map class is defined as follows (in this code sample, we define the random creation of game map along with Draw method for the visualization of the map on target device provided by MonoGame in XNA-like style):

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using StarDust.Cells;

namespace StarDust
{
    public class Map
    {
        public int Width { get; set; } = -1;
        public int Height { get; set; } = -1;
        public int CellSize { get; set; } = -1;
        public int CellColCount { get; set; } = -1;
        public int CellRowCount { get; set; } = -1;
        public Cell[,] Cells { get; set; } = null;
        public Dictionary<int, Player> Players { get; set; } = 
                                       new Dictionary<int, Player>();
        public int SpacePercentage { get; set; } = 20;
        public int FerrumPercentage { get; set; } = 40;
        public StarDust StarDust { get; set; } = null;
        
        public Map(StarDust StarDust, int Width, int Height, int CellSize)
        {
            this.StarDust = StarDust;

            this.Width = Width;
            this.Height = Height;
            this.CellSize = CellSize;

            this.CellColCount = this.Width / CellSize;
            this.CellRowCount = this.Height / CellSize;

            this.Cells = new Cell[this.CellRowCount, this.CellColCount];

            for (int i = 0; i < this.CellRowCount; ++i)
            {
                for (int j = 0; j < this.CellColCount; ++j)
                {
                    if (Utils.RandomNumber(100) > (100 - SpacePercentage))
                    {
                        if (Utils.RandomNumber(100) > (100 - FerrumPercentage))
                        {
                            this.Cells[i, j] = new CellFerrum(this, i, j);
                        } else
                        {
                            this.Cells[i, j] = new CellSpace(this, i, j);
                        }
                    } else
                    {
                        this.Cells[i, j] = new CellDust(this, i, j);
                    }
                }
            }

            this.Cells[0, 0] = new CellDust(this, 0, 0);
            this.Cells[0, 1] = new CellDust(this, 0, 1);
            this.Cells[1, 0] = new CellDust(this, 1, 0);
            this.Cells[1, 1] = new CellDust(this, 1, 1);

            this.Cells[this.CellRowCount - 1, this.CellColCount - 1] = 
                new CellDust(this, this.CellRowCount - 1, this.CellColCount - 1);
            this.Cells[this.CellRowCount - 1, this.CellColCount - 2] = 
                new CellDust(this, this.CellRowCount - 1, this.CellColCount - 2);
            this.Cells[this.CellRowCount - 2, this.CellColCount - 1] = 
                new CellDust(this, this.CellRowCount - 2, this.CellColCount - 1);
            this.Cells[this.CellRowCount - 2, this.CellColCount - 2] = 
                new CellDust(this, this.CellRowCount - 2, this.CellColCount - 2);
        }

        public virtual void Draw()
        {
            for (int i = 0; i < this.CellRowCount; ++i)
            {
                for (int j = 0; j < this.CellColCount; ++j)
                {
                    this.Cells[i, j].Draw();
                }
            }
        }
    }
}

The Cell class represents the cell on the grid of the map and is defined as follows - we define methods and attributes of the cell by overriding this class which could be defined as an interface, however, the prototyping for enumerations gives us the ability to avoid multiple class hierarchy and operate on upper level at the same time:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace StarDust
{
    public class Cell
    {
        public int Row { get; set; } = -1;
        public int Col { get; set; } = -1;
        public int Width { get; set; } = -1;
        public int Height { get; set; } = -1;
        public Map Map { get; set; } = null;
        public Texture2D View { get; set; } = null;
        public Dictionary<int, Unit> Units { get; set; } = new Dictionary<int, Unit>();
        public static int CellSelectionSize = 3;

        public virtual CellType Type()
        {
            return CellType.EMPTY;
        }

        public Cell(Map Map, int Row, int Col)
        {
            this.Map = Map;
            this.Height = this.Map.CellSize;
            this.Width = this.Height;

            this.Row = Row;
            this.Col = Col;
        }

        public int PositionX()
        {
            return this.Width * this.Col;
        }

        public int PositionY()
        {
            return this.Height * this.Row;
        }

        public virtual void Draw()
        {
            if (this.View != null)
            {
                this.Map.StarDust.spriteBatch.Draw
                         (this.View, new Rectangle(this.PositionX(), 
                this.PositionY(), this.Width, this.Height), Color.White);
            }

            foreach (Unit Unit in this.Units.Values)
            {
                if (Unit.IsSelected() && Unit.SelectedView != null)
                {
                    this.Map.StarDust.spriteBatch.Draw(Unit.SelectedView, 
                    new Rectangle(this.PositionX(), this.PositionY(), 
                                  this.Width, this.Height), Color.White);
                }

                if (Unit.View != null)
                {
                    this.Map.StarDust.spriteBatch.Draw
                         (Unit.View, new Rectangle(this.PositionX() + 
                    CellSelectionSize, this.PositionY() + CellSelectionSize, 
                    this.Width - 2 * CellSelectionSize, 
                    this.Height - 2 * CellSelectionSize), Color.White);
                }
            }
        }

        public virtual bool MoveableTo()
        {
            return false;
        }

        public bool IsEmpty()
        {
            return this.Units.Count == 0;
        }
    }
}

The Player class represents the player which can be user or computer and is defined as follows - the player derives the necessary combination of methods and attributes in order to create the whole gaming process:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using StarDust.Units;

namespace StarDust
{
    public class Player
    {
        public Map Map { get; set; } = null;
        public Dictionary<int, Unit> Units { get; set; } = new Dictionary<int, Unit>();
        public Dictionary<int, Unit> SelectedUnits { get; set; } = 
                                     new Dictionary<int, Unit>();
        public static int CurrentPlayerId = 0;
        public int PlayerId { get; set; } = -1;
        public Color Color { get; set; } = 
        new Color(Utils.RandomNumber(256), 
                  Utils.RandomNumber(256), Utils.RandomNumber(256));
        public Unit Artifact { get; set; } = null;
        public int Deposited { get; set; } = 0;

        public Player(Map Map)
        {
            this.Map = Map;
            this.PlayerId = CurrentPlayerId++;

            this.Map.Players.Add(this.PlayerId, this);
        }

        public virtual PlayerType Type()
        {
            return PlayerType.EMPTY;
        }

        public virtual void Step()
        {

        }

        public bool Lost()
        {
            if (this.Artifact.HealthPoints <= 0)
            {
                return true;
            }
            
            int ferrumCount = this.Deposited;
            int workerCount = 0;
            int armyCount = 0;

            for (int i = 0; i < this.Map.CellRowCount; ++i)
            {
                for (int j = 0; j < this.Map.CellColCount; ++j)
                {
                    foreach (Unit unit in this.Map.Cells[i, j].Units.Values)
                    {
                        if (unit.Type() == UnitType.WORKER && unit.Player == this)
                        {
                            ++workerCount;
                        }

                        if ((unit.Type() == UnitType.AUTOGUN || 
                        unit.Type() == UnitType.SOLDIER) && unit.Player == this)
                        {
                            ++armyCount;
                        }
                    }
                }
            }

            if (ferrumCount < Worker.Cost && workerCount == 0 && armyCount == 0)
            {
                return true;
            }

            return false;
        }
    }
}

The Unit class represents the unit on the map for which the player has a control. It's defined as follows - this is the most important class as it defines the entry point for a unit on the map which is operated by user, which, in turn, could be either player or computer:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace StarDust
{
    public class Unit
    {
        public static Texture2D SelectedView = null;

        public Texture2D View { get; set; } = null;
        public int Row { get; set; } = -1;
        public int Col { get; set; } = -1;
        public int HealthPoints { get; set; } = 100;
        public float Speed { get; set; } = 1.0f;
        public static int UnitsCurrentId = 0;
        public int UnitId { get; set; } = -1;
        public Player Player { get; set; } = null;
        public Cell Cell { get; set; } = null;
        public int TargetCellRow { get; set; } = -1;
        public int TargetCellCol { get; set; } = -1;
        public int LastTime { get; set; } = -1;
        public double Radius { get; set; } = -1.0f;
        public int Damage { get; set; } = -1;

        public virtual UnitPositionType Position()
        {
            return UnitPositionType.GROUND;
        }

        public virtual UnitType Type()
        {
            return UnitType.EMPTY;
        }

        public virtual bool Moveable()
        {
            return true;
        }

        public Unit(Player Player, int Row, int Col)
        {
            this.Player = Player;
            this.UnitId = UnitsCurrentId++;
            this.Row = Row;
            this.Col = Col;

            this.Player.Units.Add(this.UnitId, this);

            this.Player.Map.Cells[Row, Col].Units.Add(this.UnitId, this);
        }

        public virtual bool IsSelected()
        {
            return this.Player.SelectedUnits.ContainsKey(this.UnitId);
        }

        public void Select()
        {
            if (this.IsSelected())
            {
                this.Player.SelectedUnits.Remove(this.UnitId);
            } else
            {
                this.Player.SelectedUnits.Add(this.UnitId, this);
            }
        }

        public virtual void Move(int NewTime)
        {
            int CurrentTime = this.LastTime;

            this.LastTime = NewTime;

            if (!this.Moveable())
            {
                return;
            }

            double iter = 0.0f;
            
            if (this.TargetCellCol == -1 || this.TargetCellRow == -1) return;

            if (CurrentTime != -1)
            {
                iter = Math.Round((((float)(NewTime - CurrentTime)) * this.Speed) 
                       / 1000000.0f);
            }

            double distance = 0.0f;

            do
            {
                if (this.Row == this.TargetCellRow && this.Col == this.TargetCellCol)
                {
                    this.TargetCellCol = this.TargetCellRow = -1;
                    break;
                }

                int drow = Utils.Sign(this.TargetCellRow - this.Row);
                int dcol = Utils.Sign(this.TargetCellCol - this.Col);

                int nrow = this.Row + drow;
                int ncol = this.Col + dcol;

                if (nrow < 0 || nrow >= this.Player.Map.CellRowCount) break;
                if (ncol < 0 || ncol >= this.Player.Map.CellColCount) break;

                if (!this.Player.Map.Cells[nrow, ncol].MoveableTo()) break;

                if (!this.Player.Map.Cells[nrow, ncol].IsEmpty()) break;

                this.Player.Map.Cells[this.Row, this.Col].Units.Remove(this.UnitId);

                this.Row = nrow;
                this.Col = ncol;

                this.Player.Map.Cells[this.Row, this.Col].Units.Add(this.UnitId, this);

                distance += Math.Sqrt(drow * drow + dcol * dcol);
            } while (distance <= iter);
        }

        public static void LoadContent(StarDust StarDust)
        {
            SelectedView = StarDust.Content.Load<Texture2D>("Unit\\Selected");
        }
    }
}

As for any other entity described above, the type for a class can be obtained from the pre-defined enumeration as the solution without using the interfaces:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace StarDust
{
    public enum UnitType
    {
        EMPTY,
        ARTIFACT,
        AUTOGUN,
        WORKER,
        SOLDIER,
        GATE
    }
}

Basic Principles

The main principle is the usage of the primitives provided with the MonoGame. They are identical to XNA framework. Thus the game code for MonoGame is as follows with respect to the XNA style and composition of the application as whole:

C#
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using StarDust.Cells;
using StarDust.Units;
using StarDust.Players;

namespace StarDust
{
    /// <summary>
    /// This is the main type for your game.
    /// </summary>
    public class StarDust : Game
    {
        public GraphicsDeviceManager graphics;
        public SpriteBatch spriteBatch;
        public Map Map { get; set; } = null;
        public Player User { get; set; } = null;
        public Player Computer { get; set; } = null;

        public StarDust()
        {
            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()
        {
            // TODO: Add your initialization logic here

            this.IsMouseVisible = true;

            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()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            CellDust.LoadContent(this);

            CellSpace.LoadContent(this);

            CellFerrum.LoadContent(this);

            CellPlate.LoadContent(this);

            Artifact.LoadContent(this);

            Autogun.LoadContent(this);

            Gate.LoadContent(this);

            Soldier.LoadContent(this);

            Worker.LoadContent(this);

            Unit.LoadContent(this);

            // TODO: use this.Content to load your game content here
            this.Map = new Map(this, this.graphics.PreferredBackBufferWidth,
                this.graphics.PreferredBackBufferHeight, 32);

            this.User = new User(this.Map);

            this.Computer = new Computer(this.Map);
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// game-specific 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)
        {
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == 
                ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
                Exit();

            if (this.User.Lost() || this.Computer.Lost())
            {
                return;
            }

            MouseState mouseState = Mouse.GetState(this.Window);
            KeyboardState keyboardState = Keyboard.GetState();

            int mouseRow = mouseState.Y / this.Map.CellSize;
            int mouseCol = mouseState.X / this.Map.CellSize;

            if (mouseState.LeftButton == ButtonState.Pressed)
            {                
                foreach (Unit unit in this.Map.Cells[mouseRow, mouseCol].Units.Values)
                {
                    if (unit.Player == this.User)
                    {
                        unit.Select();
                    }
                }

                base.Update(gameTime);

                return;
            }

            if (mouseState.RightButton == ButtonState.Pressed)
            {
                foreach (Unit unit in this.User.SelectedUnits.Values)
                {
                    unit.TargetCellCol = mouseCol;
                    unit.TargetCellRow = mouseRow;
                }
            }

            foreach (Unit unit in this.User.Units.Values)
            {
                unit.Move(gameTime.ElapsedGameTime.Milliseconds);
            }

            foreach (Unit unit in this.Computer.Units.Values)
            {
                unit.Move(gameTime.ElapsedGameTime.Milliseconds);
            }

            foreach (Unit unit in this.User.SelectedUnits.Values)
            {
                if (unit.Type() == UnitType.GATE)
                {
                    if (keyboardState.IsKeyDown(Keys.W) && 
                            unit.Col + 1 < this.Map.CellColCount)
                    {
                        if (this.Map.Cells[unit.Row, unit.Col + 1].IsEmpty() && 
                            this.User.Deposited >= Worker.Cost)
                        {
                            Worker worker = 
                               new Worker(this.User, unit.Row, unit.Col + 1);

                            this.User.Deposited -= Worker.Cost;

                            break;
                        }
                    }

                    if (keyboardState.IsKeyDown(Keys.A) && 
                                unit.Col + 1 < this.Map.CellColCount)
                    {
                        if (this.Map.Cells[unit.Row, unit.Col + 1].IsEmpty() && 
                            this.User.Deposited >= Autogun.Cost)
                        {
                            Autogun autogun = new Autogun
                                  (this.User, unit.Row, unit.Col + 1);

                            this.User.Deposited -= Autogun.Cost;

                            break;
                        }
                    }

                    if (keyboardState.IsKeyDown(Keys.S) && 
                              unit.Col + 1 < this.Map.CellColCount)
                    {
                        if (this.Map.Cells[unit.Row, unit.Col + 1].IsEmpty() && 
                            this.User.Deposited >= Soldier.Cost)
                        {
                            Soldier soldier = new Soldier(this.User, 
                                              unit.Row, unit.Col + 1);

                            this.User.Deposited -= Soldier.Cost;

                            break;
                        }
                    }
                }
            }

            // TODO: Add your update logic here

            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);

            // TODO: Add your drawing code here
            spriteBatch.Begin();

            SpriteFont spriteFont = Content.Load<SpriteFont>("Arial");

            if (this.User.Lost())
            {
                spriteBatch.DrawString(spriteFont, "YOU LOST", 
                                       new Vector2(100, 100), Color.White);
            } else if(this.Computer.Lost())
            {
                spriteBatch.DrawString(spriteFont, "YOU WON", 
                                       new Vector2(100, 100), Color.Red);
            }
            else
            {
                this.Map.Draw();
            }

            spriteBatch.End();

            base.Draw(gameTime);
        }
    }
}

Step by Step Walk-throughs

In order to run the code, the reader needs to use Visual Studio 2017 and install MonoGame setup package. The screen of the game is as follows:

Image 2

Conclusion and Points of Interest

Now we're familiar with how to develop XNA-based games using its free version like MonoGame. This article describes the real-time strategy while 3D-programming is also possible.

The XNA, thus, has a new breath and future due to its free and open-source realization like MonoGame.

History

  • 2nd August, 2022: Initial post
  • 3rd August, 2022: Extended abstracts
  • 2nd November, 2022: Removed links
  • 3rd November, 2022: Links deleted
  • 4th November, 2022: License changed

License

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