65.9K
CodeProject 正在变化。 阅读更多。
Home

自动化滑冰者 MonoGame 解决方案

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2023 年 6 月 27 日

CPOL

3分钟阅读

viewsIcon

2645

downloadIcon

46

开始使用 MonoGame 并使用滑冰者 C# 项目

背景

有一些关于其他 MonoGame 项目的 CodeProject 文章,但没有关于冰上滑冰者的。所以我开始创建这篇文章/技巧。

第一个程序版本在 PathFinding 方面存在一些问题,但现在看起来没问题了。

Using the Code

这是一个快速概览

主窗口概念和代码

Program.cs 初始化并运行游戏

static void Main()
{
using var game = new IceSkater.GameControl();
game.Run();
}

GameControl.cs 是游戏的核心,有两个方法 - UpdateDraw - 它们在一个循环中每秒被调用 60 次 => 循环间隔为 16.7 毫秒。

GameControl 包含这些重要方法(以及更多)

  • Initialize()
  • LoadContent():此方法加载项目的 content。
  • Update(GameTime gameTime):此方法包含游戏逻辑,例如更新游戏对象的位置。
  • Draw(GameTime gameTime) 这是渲染当前游戏场景的方法。

GameControl 还包括对 Pathfinding 的调用以及一个名为 NewEndPos() 的方法。

令我惊讶的是,Pathfinding 并不是主要问题,而挑战在于一次又一次地为首席滑冰者设置一个新的 EndPos

使用 AStar2 进行 Pathfinding (取自 [3])

"A 2-dimensional implementation of AStar<T> that uses for positions, and the manhattan distance as its heuristic.“

整合 - 概念和代码

GUI

游戏可以使用 ENTER 键启动。

补间动画可以开启或关闭。

为了访问复选框事件,需要在加载控件时使用一个变量

cBox1 = box.AddChild(new MLEM.Ui.Elements.Checkbox
        (Anchor.AutoLeft, new Vector2(25, 35), " Tweening")

时间以秒为单位,显示的 滑冰者数量是硬编码的。

寻路和补间动画

玩家通过从一个 EndPos 跳到下一个 EndPos 来跟随路径。

可以通过使用 补间动画 功能模拟动画来改进此行为。

取自 [6]:“Inbetweening,或简称补间动画,允许您在中间帧中生成位置、大小、颜色、不透明度等的值,从而产生动画的错觉。”

使用 NewEndPos(),我们寻找一个离当前位置不太远的空闲区域。

ChiefSkater 在这里和那里移动,并试图避免与其他 Skaters 发生碰撞

这是一个简化方法 - 仅检查了 ChiefSkater 与其他游戏对象之间的距离!

好吧 - 游戏的 code 相当简单 - 尽情享受吧

//
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;
        }
    }
}
//

致谢/参考

历史

  • 2023 年 6 月 26 日 - 版本 1.0
  • 2023 年 6 月 28 日 - 修复了一些拼写错误,更新了 MainWindow 概念和代码中的内容,并为 MGCB 编辑器和图像添加了致谢
© . All rights reserved.