目录
介绍
该代码演示了MonoGame使用基于类层次结构的面向对象编程方法开发实时战略游戏引擎的可能性。
本文基于使用实体和对象进行正则表达式匹配编程的先前结果。
星尘游戏引擎
引擎通过从类Game派生的类的更新过程来平衡,因为它在经典引擎中由XNA定义。
为了运行此代码,我们只需要Visual Studio 2017和MonoGame安装包,因为它仅支持Microsoft Visual Studio的最新版本。
使用代码
通过将此方法应用于基类(如 Map、Cell、Player和Unit)。
该Map类是游戏地图的一个类,由2D单元覆盖,因此,每个单元都在Cell类中定义。
该Map类定义如下(在此代码示例中,我们定义了随机创建游戏地图以及Draw方法,用于在目标设备上以类似XNA的样式可视化地图):
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();
}
}
}
}
}
该Cell类表示映射网格上的单元格,定义如下——我们通过覆盖该类来定义单元格的方法和属性,该类可以定义为接口,但是,枚举的原型设计使我们能够避免多个类层次结构并同时在上层操作:
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;
}
}
}
该Player类表示可以是用户或计算机的玩家,定义如下——玩家派生方法和属性的必要组合以创建整个游戏过程:
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;
}
}
}
该Unit类表示玩家可以控制的地图上的单位。它的定义如下——这是最重要的类,因为它定义了地图上由用户操作的单位的入口点,而用户又可以是玩家或计算机:
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");
}
}
}
对于上述任何其他实体,可以从预定义的枚举中获取类的类型作为解决方案,而无需使用接口:
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
}
}
基本原则
主要原则是使用MonoGame提供的原语。它们与XNA框架相同。因此,MonoGame的游戏代码对于整个应用程序的XNA风格和组成如下:
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);
}
}
}
分步演练
为了运行代码,读者需要使用Visual Studio 2017并安装MonoGame安装包。游戏画面如下:
结论和兴趣点
现在我们已经熟悉了如何使用其免费版本(如MonoGame)开发基于XNA的游戏。本文介绍了实时策略,同时也可以进行3D编程。
因此,XNA由于其像MonoGame一样的免费和开源实现而拥有了新的呼吸和未来。
https://www.codeproject.com/Tips/5338838/Real-time-Strategy-Engine-in-MonoGame