This article/tip and the demo are about getting started with MonoGame and using my IceSkater C# project. The automated ChiefSkater moves here and there and tries to avoid collisions with the other Skaters.
Background
There are some CodeProject articles about other MonoGame projects, but nothing with IceSkater. So I started to create this article/tip.
The first program versions had some issues with PathFinding
but now it looks OK.
Using the Code
Here is a Quick Overview
MainWindow Concept and Code
Program.cs initializes and runs the game:
static void Main()
{
using var game = new IceSkater.GameControl();
game.Run();
}
GameControl.cs is the core of the game and has two methods - Update
and Draw
- which are called in a loop 60 times per second =>The loop interval is 16.7 milliseconds.
GameControl
includes these important methods (and more):
GameControl
includes also a call for Pathfinding
and a method called NewEndPos()
.
To my big surprise, Pathfinding
wasn’t the main issue but the challenge is to set a new EndPos
for the Chief Skater again and again.
Pathfinding
with AStar2 (taken from [3]):
"A 2-dimensional implementation of AStar<T> that uses for positions, and the manhattan distance as its heuristic.“
Putting Things Together - Concept and Code
GUI
The game can be started with ENTER key.
Tweening can be switched on or off.
To get access to the checkbox events, a variable is needed when the control is loaded:
cBox1 = box.AddChild(new MLEM.Ui.Elements.Checkbox
(Anchor.AutoLeft, new Vector2(25, 35), " Tweening")
Time is in seconds and the displayed number of skaters is hard coded.
Pathfinding and Tweening
The player follows the path by jumping from one EndPos
to the next EndPos
.
This behaviour can be improved by simulating an animation with the Tweening
feature.
Taken from [6]: „Inbetweening, or just tweening for short, allows you to generate values for position, size, color, opacity, etc in intermediate frames giving the illusion of animation“.
With NewEndPos()
, we look for a free area not too far away from current position.
The ChiefSkater
moves here and there and tries to avoid collisions with the other Skaters
:
It’s a simplified method - only the distance between the ChiefSkater
and others game objects is checked!
Well - the code for the game is rather simple – enjoy it:
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using IceSkater.GameObjects;
using System;
using System.Diagnostics;
using IceSkater.GameMngr;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Linq;
using IceSkater.Interfaces;
using MonoGame.Extended.Collisions;
using MonoGame.Extended.Tweening;
using MLEM.Ui;
using MLEM.Ui.Style;
using MLEM.Extensions;
using MLEM.Pathfinding;
using MLEM.Ui.Elements;
using MLEM.Textures;
using MLEM.Font;
using MonoGame.Extended;
namespace IceSkater
{
public class GameControl : Game
{
Texture2D _texture;
Texture2D _skaterSprite;
Texture2D _chiefSkaterSprite;
SpriteFont gameFont;
Vector2 _position;
Vector2 endPosAI;
Vector2 oldChiefPos;
ChiefSkater mySkater = new ChiefSkater();
Mngr gameMngr = new Mngr();
public UiSystem UiSystem;
public Panel panel;
public Panel box;
public Panel boxBottom;
private Paragraph txtTime;
private Paragraph txtObst;
private Checkbox cBox1;
public bool gamePause = false;
private GraphicsDeviceManager _graphics;
public SpriteBatch _spriteBatch;
private Color _backgroundColour = Color.Snow;
private List<Component> _gameComponents;
private List<Skater> waste= new List<Skater>();
private bool[,] world;
private bool crash;
private bool init;
private bool processing = true;
private AStar2 pathfinder;
private List<Point> path;
private int scale = 38;
private int interval = 0;
private int i;
private int iColX =19;
private int veloAngle;
bool findPath;
bool moveRev;
public Vector2 Linear;
private readonly Tweener _tweener = new Tweener();
public Vector2 Size = new Vector2(50, 50);
public readonly Random Random = new Random(Guid.NewGuid().GetHashCode());
public readonly CollisionComponent _collisionComponent;
public const int MapWidth = 880;
public const int MapHeight = 800;
public GameControl()
{
_graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
IsMouseVisible = true;
_collisionComponent = new CollisionComponent
(new MonoGame.Extended.RectangleF(0, 0, MapWidth, MapHeight));
Content.RootDirectory = "Content";
IsMouseVisible = true;
}
protected override void Initialize()
{
_graphics.PreferredBackBufferWidth = 900;
_graphics.PreferredBackBufferHeight = 800;
_graphics.ApplyChanges();
endPosAI.X = 2;
endPosAI.Y = 2;
mySkater.position.X = 40;
mySkater.position.Y = 30;
processing = true;
base.Initialize();
_graphics.PreferredBackBufferHeight = MapHeight;
_graphics.PreferredBackBufferWidth = MapWidth;
_graphics.ApplyChanges();
}
protected override void LoadContent()
{
_spriteBatch = new SpriteBatch(GraphicsDevice);
_skaterSprite = Content.Load<Texture2D>("Skaters/ice-skater-clipart-md");
_chiefSkaterSprite = Content.Load<Texture2D>("Skaters/skating-clipart-md");
_texture = Content.Load<Texture2D>("Textures/Test");
_position = new Vector2(0, 0);
gameFont = Content.Load<SpriteFont>("Fonts/spaceFont");
this.world = new bool[20, 20];
_spriteBatch.Begin();
NewEndPos();
_spriteBatch.End();
this.InitPathFinding();
var style = new UntexturedStyle(this._spriteBatch)
{
PanelTexture = null,
Font = new GenericSpriteFont(this.Content.Load<SpriteFont>
("Fonts/spaceFont")),
ButtonTexture = new NinePatch(new TextureRegion
(this._texture, 24, 8, 16, 16), 4),
CheckboxTexture = new NinePatch(new TextureRegion
(this._texture, 24, 8, 16, 16), 4),
CheckboxCheckmark = new TextureRegion(this._texture, 24, 0, 8, 8),
};
this.UiSystem = new UiSystem(this, style);
panel = new Panel(Anchor.AutoLeft, size: new Vector2(250, 660),
positionOffset: Vector2.Zero);
this.UiSystem.Add("ExampleUi", panel);
box = new Panel(Anchor.AutoLeft, new Vector2(250, 1), Vector2.Zero,
setHeightBasedOnChildren: true);
txtTime = box.AddChild(new Paragraph(Anchor.TopCenter, 1, "Time: " +
Math.Floor(gameMngr.totalTime).ToString()));
txtObst = box.AddChild(new Paragraph(Anchor.AutoCenter, 1, "Obstacles: "));
cBox1 = box.AddChild(new MLEM.Ui.Elements.Checkbox
(Anchor.AutoLeft, new Vector2(25, 35), " Tweening")
{
PositionOffset = new Vector2(0, 2)
});
boxBottom = new Panel(Anchor.BottomLeft, new Vector2(150, 1), Vector2.Zero,
setHeightBasedOnChildren: true);
boxBottom.AddChild(new MLEM.Ui.Elements.Button(Anchor.AutoLeft,
new Vector2(125, 35), "Pause")
{
OnPressed = element => this.Pause(),
PositionOffset = new Vector2(0, 2)
});
boxBottom.AddChild(new MLEM.Ui.Elements.Button(Anchor.AutoLeft,
new Vector2(125, 35), "Go on")
{
OnPressed = element => this.Go(),
PositionOffset = new Vector2(0, 2)
});
boxBottom.AddChild(new MLEM.Ui.Elements.Button(Anchor.AutoLeft,
new Vector2(125, 35), "Stop")
{
OnPressed = element => this.Stop(),
PositionOffset = new Vector2(0, 2)
});
boxBottom.AddChild(new MLEM.Ui.Elements.Button(Anchor.AutoLeft,
new Vector2(125, 35), "Exit")
{
OnPressed = element => this.Exit(),
PositionOffset = new Vector2(0, 2)
});
this.UiSystem.Add("InfoBox", box);
this.UiSystem.Add("BotttomBox", boxBottom);
processing = true;
cBox1.Checked = true;
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed
|| Keyboard.GetState().IsKeyDown(Keys.Escape))
Exit();
_tweener.TweenTo(this, a => a.Linear, new Vector2(mySkater.position.X,
mySkater.position.Y), duration: 1, delay: 0)
.RepeatForever(repeatDelay: 0.0f)
.AutoReverse()
.Easing(EasingFunctions.BackOut);
var elapsedSeconds = gameTime.GetElapsedSeconds();
_tweener.Update(elapsedSeconds);
base.Update(gameTime);
interval += 1;
this.UiSystem.Update(gameTime);
if (gamePause == false)
{
if (gameMngr.inGame)
{
crash = false;
mySkater.Update(gameTime);
foreach (var item in waste)
{
gameMngr.skaters.Remove(item);
}
}
gameMngr.conUpdate(gameTime, this, MapWidth, MapHeight);
foreach (IEntity entity in gameMngr._entities)
{
entity.Update(gameTime);
}
foreach (IEntity entity in gameMngr._entities)
{
int sum2 = 30 + mySkater.radius;
if (Vector2.Distance
(entity.Bounds.Position, mySkater.position) < sum2)
{
gameMngr.inGame = false;
mySkater.position = ChiefSkater.defaultPosition;
crash = true;
}
}
_collisionComponent.Update(gameTime);
base.Update(gameTime);
}
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
this._spriteBatch.Begin(SpriteSortMode.Deferred,
null, SamplerState.PointClamp,
null, null, null, Matrix.CreateScale(scale));
if (processing)
{
NewEndPos();
}
if (findPath == true && processing || Math.Floor(gameMngr.totalTime) < 2)
{
this.InitPathFinding();
}
_spriteBatch.End();
_spriteBatch.Begin();
foreach (IEntity entity in gameMngr._entities)
{
entity.Draw(_spriteBatch);
_spriteBatch.Draw(_skaterSprite,
new Vector2(entity.Bounds.Position.X - 20,
entity.Bounds.Position.Y - 60), Color.White);
}
if (crash == true && Math.Floor(gameMngr.totalTime) > 1)
_spriteBatch.DrawString(gameFont, "Obstacles: " + "COLLISION !!",
new Vector2(3, 172), Color.White);
if (cBox1.Checked)
{
_spriteBatch.Draw(_chiefSkaterSprite, new Vector2(Linear.X - 40,
Linear.Y - 40), Color.White);
}
else
{
_spriteBatch.Draw(_chiefSkaterSprite,
new Vector2(mySkater.position.X - 40,
mySkater.position.Y - 40), Color.White);
}
oldChiefPos.X = mySkater.position.X;
oldChiefPos.Y = mySkater.position.Y;
if (gameMngr.inGame == false)
{
string mnuMessage = "Press Enter to Start the Game!";
Vector2 sizeOfText = gameFont.MeasureString(mnuMessage);
int halfWidth = _graphics.PreferredBackBufferWidth / 2;
_spriteBatch.DrawString(gameFont, mnuMessage,
new Vector2(halfWidth - sizeOfText.X / 2, 200), Color.White);
}
_spriteBatch.End();
txtTime.Text = "Time: " + Math.Floor(gameMngr.totalTime).ToString();
txtObst.Text = "Skaters: " + gameMngr._entities.Count.ToString().ToString();
this.UiSystem.Draw(gameTime, this._spriteBatch);
base.Draw(gameTime);
}
private async void InitPathFinding()
{
this.path = null;
var random = new Random();
for (var x = 0; x < 20; x++)
{
for (var y = 0; y < 20; y++)
{
if (this.world[x, y] != false)
this.world[x, y] = true;
}
}
float Cost(Point pos, Point nextPos)
{
if (nextPos.X < 0 || nextPos.Y < 0 || nextPos.X >= 20 || nextPos.Y >= 20)
return float.PositiveInfinity;
return this.world[nextPos.X, nextPos.Y] ? 1 : float.PositiveInfinity;
}
this.pathfinder = new AStar2(Cost, false);
var foundPath = await Task.Run(()
=> this.pathfinder.FindPath(new Point((int)(mySkater.position.X / scale),
(int)(mySkater.position.Y / scale)),
new Point((int)endPosAI.X, (int)endPosAI.Y)));
this.path = foundPath != null ? foundPath.ToList() : null;
if (this.path == null && gameMngr.inGame == true)
{
if (iColX > 4 && moveRev == false) { iColX -= 1; }
processing = true;
_spriteBatch.Begin();
NewEndPos();
_spriteBatch.End();
mySkater.position.X = endPosAI.X * scale;
mySkater.position.Y = endPosAI.Y * scale;
this.InitPathFinding();
if (iColX == 4)
{
moveRev = true;
}
if (iColX == 19) { moveRev = false; }
if (moveRev && interval % 20 == 9) { iColX += 1; }
}
else
{
{
if (this.path != null && this.path.Count > 1 && gamePause == false)
{
float oldEndPosAI = endPosAI.Y;
for (i = 1; i < this.path.Count; i++)
{
var first = this.path[i - 1];
var second = this.path[i];
if (i < this.path.Count)
{
processing = false;
endPosAI.X = second.X;
endPosAI.Y = second.Y;
mySkater.position.X = endPosAI.X * scale;
mySkater.position.Y = endPosAI.Y * scale;
}
}
processing = true;
if (this.path.Count == 1)
{
var first = this.path[0];
processing = false;
endPosAI.X = first.X;
endPosAI.Y = first.Y;
mySkater.position.X = endPosAI.X * scale;
mySkater.position.Y = endPosAI.Y * scale;
processing = true;
}
}
}
}
}
public void NewEndPos()
{
findPath = false;
if (iColX > 4 && moveRev == false && interval % 20 == 9) { iColX -= 1; }
if (iColX == 4)
{
moveRev = true;
}
if (iColX == 19) { moveRev = false; }
if (moveRev && interval % 20 == 9) { iColX += 1; }
var tex = this._spriteBatch.GetBlankTexture();
for (var z = 5; z > 1; z--)
{
for (var x = 0; x < 20; x++)
{
for (var y = 19; y > -1; y--)
{
this.world[x, y] = true;
if (this.world[x, y])
{
var random = new Random();
foreach (var item in gameMngr._entities)
{
Vector2 texPosition = new Vector2(x, y);
int sum = 30 + tex.Width * scale / 2;
if (Vector2.Distance(item.Bounds.Position,
scale * texPosition) < sum)
{
this.world[x, y] = false;
}
}
if (this.world[x, y] == false)
this._spriteBatch.Draw(tex, new Rectangle(x, y, 1, 1),
Color.Transparent);
}
if (gameMngr.inGame && x > 2 && x < 19 && y > 2 && y < 19 &&
this.world[x, y] && this.world[x - 1, y] &&
this.world[x + 1, y] &&
this.world[x, y - 1] && this.world[x, y + 1])
{
if ((int)(mySkater.position.X / scale) - x > - z
&& x - (int)(mySkater.position.X / scale) > - z
&& (int)(mySkater.position.Y / scale) - y > - z
&& y - (int)(mySkater.position.Y / scale) > - z)
{
if (x < iColX + z)
{
endPosAI.X = x;
endPosAI.Y = y;
findPath = true;
}
}
}
}
}
}
}
void Pause()
{ gamePause = true; }
void Go()
{ gamePause = false; }
void Stop()
{
gameMngr.inGame = false;
gameMngr._entities.Clear();
init = true;
endPosAI.X = 2;
endPosAI.Y = 2;
mySkater.position.X = 40;
mySkater.position.Y = 30;
}
}
}
Credits / Reference
History
- 26th June, 2023 - Version 1.0
- 28th June, 2023 - Fixed some typos, updated content in MainWindow Concept and Code and added Credits for MGCB Editor and for Images