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

Automated Ice Skater MonoGame Solution

0.00/5 (No votes)
27 Jun 2023CPOL3 min read 2.4K   19  
Get started MonoGame and use IceSkater C# project
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

Image 1

MainWindow Concept and Code

Program.cs initializes and runs the game:

C#
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):

  • Initialize()
  • LoadContent(): This method loads the content of the project.
  • Update(GameTime gameTime): This method contains game logic, like updating the positions of the game objects.
  • Draw(GameTime gameTime): This is the method for rendering the current game scene.

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:

C#
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:

C#
//
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
    {
        // The texture is what we show or draw on the screen
        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;
            // Collision
            _collisionComponent = new CollisionComponent
            (new MonoGame.Extended.RectangleF(0, 0, MapWidth, MapHeight));

            Content.RootDirectory = "Content";
            IsMouseVisible = true;
        }

        protected override void Initialize()
        {
            // initialization logic
            _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()
        {
            // TODO: use this.Content to load your game content here
            // Create a new SpriteBatch, which can be used to draw textures.
            _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");

            // (0, 0) is the top-left corner
            _position = new Vector2(0, 0);
            gameFont = Content.Load<SpriteFont>("Fonts/spaceFont");

            this.world = new bool[20, 20];
            _spriteBatch.Begin();
            NewEndPos();
            _spriteBatch.End();
            this.InitPathFinding();

            //Initialize the Ui system
            var style = new UntexturedStyle(this._spriteBatch)
            {
                PanelTexture = null,
                //TextScale = 0.75F,
                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;

            // Update the Ui system
            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);
                }
                // Collision
                foreach (IEntity entity in gameMngr._entities)
                {
                    // simple collision detection
                    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();

            // Collision
            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();

            // Call Draw at the end to draw the Ui on top of your game
            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);
        }

        // SOURCE: https://github.com/Ellpeck/MLEM/blob/main/Demos/PathfindingDemo.cs
        private async void InitPathFinding()
        {
            this.path = null;

            // generate a simple world for testing, where true is walkable area, 
            // and false is a wall
            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;
                }
            }

            // Create a cost function, which determines how expensive (or difficult) it 
            // should be to move from a given position
            // to the next, adjacent position. In our case, the only restriction should 
            // be walls and out-of-bounds positions, which
            // both have a cost of AStar2.InfiniteCost, meaning they are completely 
            // unwalkable.
            // If your game contains harder-to-move-on areas like, say, a muddy pit, 
            // you can return a higher cost value for those
            // locations. If you want to scale your cost function differently, 
            // you can specify a different default cost in your
            // pathfinder's constructor
            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;
            }

            // Actually initialize the pathfinder with the cost function, as well as 
            // specify if moving diagonally between tiles should be
            // allowed or not (in this case it's not)
            this.pathfinder = new AStar2(Cost, false);

            // Now find a path from the top left to the bottom right corner and store 
            // it in a variable
            // If no path can be found after the maximum amount of tries 
            // (10000 by default),
            // the pathfinder will abort and return no path (null)
            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
            {
                {
                    // draw the path
                    // in a real game, you obviously make your characters walk along 
                    // the path instead of drawing it
                    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();
            // draw the world with simple shapes

            // 2nd version with less delta x / y
            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

License

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