This article is about game-engines and object-oriented programming.
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):
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:
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:
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:
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:
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:
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
{
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";
}
protected override void Initialize()
{
this.IsMouseVisible = true;
base.Initialize();
}
protected override void LoadContent()
{
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);
this.Map = new Map(this, this.graphics.PreferredBackBufferWidth,
this.graphics.PreferredBackBufferHeight, 32);
this.User = new User(this.Map);
this.Computer = new Computer(this.Map);
}
protected override void UnloadContent()
{
}
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;
}
}
}
}
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
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:
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