Gå til indholdet

Intro til MonoGame

MonoGame er et open-source framework, der giver udviklere mulighed for at opbygge cross-platform spil og applikationer. Det er baseret på Microsofts populære XNA Framework, der oprindeligt blev udviklet til at bygge spil til Xbox 360 og Windows-platformen. MonoGame bygger videre på dette fundament og udvider understøttelsen til flere platforme som Windows, macOS, Linux, iOS, Android, PlayStation 4, Xbox One og mange flere.

TReX Game

Du kan evt se et eksempel på konvertering af et “simpelt” TRex Game til MonoGame TRexGame. Der er ligeledes en (meget lang) tilhørende Youtube serie.

I øvrigt kender GitHub CoPilot og ChatGPT også til MonoGame og kan hjælpe med kodeeksempler.

Som eksempel kan du gemme sprite og tilføje den til Content.mgcb filen. Og så tilføje følgende kode som vil vise TReX sprite i et loop:

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace Project1
{
    public class Game1 : Game
    {
        private GraphicsDeviceManager _graphics;
        private SpriteBatch _spriteBatch;
        private Texture2D _spriteSheet; 
        private int _startPosition; 
        private double _timeCounter; 
        private double _timePerFrame; 
        private int _frameNumber;
        private int _frameStart;
        private int _frameEnd;
        private int _frameWidth; 
        private int _frameHeight; 

        public Game1()
        {
            _graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            IsMouseVisible = true;
        }

        protected override void Initialize()
        {
            base.Initialize();
        }

        protected override void LoadContent()
        {                        
            _spriteBatch = new SpriteBatch(GraphicsDevice);
            _spriteSheet = Content.Load<Texture2D>("TrexSpritesheet");
            _startPosition = 848;
            _frameWidth = 44;
            _frameHeight = 52;            
            _timeCounter = 0;
            _timePerFrame = 0.1; 
            _frameStart = 11;
            _frameNumber = _frameStart;
            _frameEnd = 15;
        }

        protected override void Update(GameTime gameTime)
        {
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
                Exit();

            _timeCounter += gameTime.ElapsedGameTime.TotalSeconds;

            if (_timeCounter >= _timePerFrame)
            {
                if (_frameNumber >= _frameEnd) { 
                    _frameNumber = _frameStart; 

                }else
                {
                    _frameNumber++;
                }

                _timeCounter -= _timePerFrame;
            }

            base.Update(gameTime);
        }

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.LightGray);

            _spriteBatch.Begin();
            var r = new Rectangle((_startPosition + (_frameWidth * (_frameNumber-_frameStart))) , 0, _frameWidth, _frameHeight);
            _spriteBatch.Draw(_spriteSheet, new Vector2(100, 100), r, Color.White);
            _spriteBatch.End();
            base.Draw(gameTime);
        }
    }
}

MonoGame tilbyder en række funktioner og værktøjer, der gør det muligt at udvikle grafisk intensive og interaktive applikationer. Det understøtter 2D- og 3D-grafik, lyd, inputhåndtering, fysiksimulering og meget mere. Frameworket er skrevet i C# og kan bruges med forskellige integrerede udviklingsmiljøer (IDE’er) som Visual Studio, Visual Studio Code og Xamarin Studio.

En af fordelene ved MonoGame er, at det tillader udviklere at skrive kode én gang og derefter distribuere det til flere platforme. Dette reducerer tids- og ressourcekravene ved at udvikle separate versioner af applikationen til hver platform. MonoGame abstraherer også platformspecifik funktionalitet, hvilket gør det lettere at håndtere forskelle mellem forskellige enheder og operativsystemer.

MonoGame giver udviklere mulighed for at udnytte hardwareacceleration og grafik API’er som DirectX og OpenGL. Dette betyder, at spil og applikationer kan drage fordel af den fulde ydeevne i målplatformen. MonoGame giver også mulighed for at udnytte eksisterende XNA-kode og ressourcer, hvilket gør det lettere for udviklere at migrere fra XNA til MonoGame.

MonoGame har et aktivt community og en bred vifte af dokumentation, tutorials og eksempler til rådighed. Dette gør det nemt for nye udviklere at komme i gang og lære at udvikle spil og applikationer med MonoGame.

I modsætning til nogle kommercielle spilmotorer eller frameworks tilbyder MonoGame mere fleksibilitet og kontrol for udviklerne. Dette gør det ideelt til både indie-udviklere og større teams, der ønsker at tilpasse deres spiloplevelse til deres specifikke behov.

Alt i alt er MonoGame et kraftfuldt værktøj til udvikling af cross-platform spil og applikationer. Dets brede platformunderstøttelse, ydeevne og fleksibilitet gør det til et populært valg for udviklere i spilindustrien. Uanset om du er ny til spiludvikling eller en erfaren udvikler, der ønsker at udforske nye muligheder, er MonoGame værd at overveje.

Her er en ældre video omkring installation (bemærk - et par år gammel og derfor ikke helt opdateret):

Installation

Tip

Du kan finde en masse grafik til spil hos OpenGameArt - se eksempelvis Simplified Platformer Pack

Simpel brug af Game-klassen

Et MonoGame spil er bygget op omkring klassen Game som blandt andet består af de virtuelle metoder

  • Initialize
    • Initialiseringskode
  • LoadContent
    • oprettelse af sprites, lyde mv
  • Update
    • Kaldes løbende af MonoGame og bruges blandt til at håndtere input fra tastatur
  • Draw
    • Kaldes løbende af MonoGame og bruges til at tegne sprites

Her er et eksempel med tegning af en sprite og brug af pile tasterne. Eksemplet forudsætter af platformChar_walk2.png er kompileret via MCBT værktøjet jf. ovenfor. Forudsætter også at projektet er oprettet som Game1 (ellers ret program.cs)

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace Game1
{
    public class Game1 : Game
    {
        // felt til opbevaring af oplysning om grafik, skærm mv
        private GraphicsDeviceManager graphics;

        // felt til opbevaring af tegning af sprites mv
        private SpriteBatch spriteBatch;

        // felt til opbevaring af en sprite
        Texture2D character;

        // felt opbevaring af til position af sprite (x,y)
        Vector2 position;

        // konstant til opbevaring af den hastighed sprite skal bevæge sig
        const float speed = 3;

        public Game1()
        {
            // opret objekt til info om grafik
            graphics = new GraphicsDeviceManager(this);
            // sæt mappe til sprites, lyde mv
            Content.RootDirectory = "Content";
            // vis mus
            IsMouseVisible = false;
        }

        protected override void Initialize()
        {
            base.Initialize();
        }

        // Bruges til at oprette sprites mv
        protected override void LoadContent()
        {
            // Opret SpriteBatch til tegning mv
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // Hent sprite fra Content (platformChar_walk2.png)
            character = Content.Load<Texture2D>("platformChar_walk2");
            // Sæt position (f = float)
            position = new Vector2(100f, 100f);
        }

        // Kaldes af MonoGame mange gange i sekundet
        protected override void Update(GameTime gameTime)
        {

            // Luk program hvis...
            if (Keyboard.GetState().IsKeyDown(Keys.Escape))
                Exit();

            // Find højde og bredde på vindue
            int width = graphics.PreferredBackBufferWidth;
            int height = graphics.PreferredBackBufferHeight;

            // Beregn en delta værdi for at tage hensyn til at
            // spil kan afvikles på maskiner med forskellige CPU kraft
            // gametime leveres af Update-metoden
            var delta = 0.15f * (float)gameTime.ElapsedGameTime.TotalMilliseconds;

            // Hvis højrepil og position ikke er uden for vindue
            if (Keyboard.GetState().IsKeyDown(Keys.Right) && position.X + character.Width < width)
                position.X += speed * delta;

            // Hvis venstrepil og position ikke er uden for vindue 
            if (Keyboard.GetState().IsKeyDown(Keys.Left) && position.X > 0)
                position.X -= speed * delta;

            base.Update(gameTime);
        }

        // Kaldes af MonoGame mange gange i sekundet
        protected override void Draw(GameTime gameTime)
        {
            // Baggrund
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // Start tegning
            spriteBatch.Begin();

            // Tegn sprite (color = white = ingen farve)
            Color color = Color.White;
            spriteBatch.Draw(character, position, color);

            // Afslut tegning
            spriteBatch.End();            

            base.Draw(gameTime);
        }
    }
}

Objektorienteret tilgang

Man kan vælge at opdele de enkelte elementer i spillet i klasser og interface. Her er en keyboard manager-klasse, en sprite-klasse og et interface:

class KeyboardManager
{

    public event Action<GameTime> EscapedPressed;
    public event Action<GameTime> LeftPressed;
    public event Action<GameTime> RightPressed;

    public void Check(KeyboardState keyboardState, GameTime gameTime) {

        if (keyboardState.IsKeyDown(Keys.Escape)) {
            EscapedPressed?.Invoke(gameTime);
        }

        if (keyboardState.IsKeyDown(Keys.Left))
        {
            LeftPressed?.Invoke(gameTime);
        }

        if (keyboardState.IsKeyDown(Keys.Right))
        {
            RightPressed?.Invoke(gameTime);
        }
    }
}

interface ISprite
{
    void Draw(GameTime gameTime, SpriteBatch spriteBatch);
    void Update(GameTime gameTime);
}

class Character : ISprite
{

    const float speed = 3;
    Color drawColor = Color.White;
    const string contentName = "platformChar_walk2";

    Texture2D sprite;
    Vector2 position;
    private ContentManager content;
    private SpriteBatch spriteBatch;
    private GraphicsDeviceManager graphics;
    private KeyboardManager keyboardManager;


    public Character(ContentManager content, SpriteBatch spriteBatch, GraphicsDeviceManager graphics, KeyboardManager keyboardManager)
    {
        position = new Vector2 { X = 200, Y = 200 };
        this.content = content;
        this.spriteBatch = spriteBatch;
        sprite = content.Load<Texture2D>(contentName);
        this.graphics = graphics;
        this.keyboardManager = keyboardManager;

        keyboardManager.LeftPressed += gameTime =>
        {
            var delta = 0.15f * (float)gameTime.ElapsedGameTime.TotalMilliseconds;
            if (position.X > 0)
                position.X -= speed * delta;
        };
        keyboardManager.RightPressed += gameTime =>
        {
            var delta = 0.15f * (float)gameTime.ElapsedGameTime.TotalMilliseconds;
            if (position.X + sprite.Width < graphics.PreferredBackBufferWidth)
                position.X += speed * delta;
        };

    }

    public void Update(GameTime gameTime)
    {
    }

    public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
    {
        spriteBatch.Draw(sprite, position, drawColor);
    }

}

Nu kan Game-klasse begrænses til mere generiske elemeenter og al logik flyttes til specifikke klasser:

public class Game1 : Game
{
    private GraphicsDeviceManager graphics;
    private SpriteBatch spriteBatch;
    private Character character;
    private KeyboardManager keyboardManager;

    public Game1()
    {
        graphics = new GraphicsDeviceManager(this);
        Content.RootDirectory = "Content";
        IsMouseVisible = true;
        keyboardManager = new KeyboardManager();
        keyboardManager.EscapedPressed += (_) => { Exit(); };

    }

    protected override void Initialize()
    {
        base.Initialize();
    }

    protected override void LoadContent()
    {
        spriteBatch = new SpriteBatch(GraphicsDevice);
        character = new Character(Content, spriteBatch, graphics, keyboardManager);            
    }

    protected override void Update(GameTime gameTime)
    {
        keyboardManager.Check(Keyboard.GetState(), gameTime);
        character.Update(gameTime);
        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.CornflowerBlue);
        spriteBatch.Begin();
        character.Draw(gameTime, spriteBatch);
        spriteBatch.End();
        base.Draw(gameTime);
    }
}

Simpel animation

Animation kan foretages ved at opdele sprites i frames og vise disse med et givet interval. Med udgangspunkt i platformer pack er her et simpelt eksempel (inspireret af MonoGame TRexGame):

public class Sprite
{

    public Texture2D Texture { get; set; }

    public Color TintColor { get; set; } = Color.White;

    public Sprite(Texture2D texture)
    {
        Texture = texture;
    }

    public void Draw(SpriteBatch spriteBatch, Vector2 position)
    {

        spriteBatch.Draw(Texture, position, TintColor);

    }

}

public class SpriteAnimationFrame
{
    private Sprite _sprite;

    public Sprite Sprite {
        get
        {
            return _sprite;
        }
        set
        {
            if (value == null)
                throw new ArgumentNullException("value", "The sprite cannot be null.");

            _sprite = value;

        }
    }

    public float TimeStamp { get; }

    public SpriteAnimationFrame(Sprite sprite, float timeStamp)
    {
        Sprite = sprite;
        TimeStamp = timeStamp;
    }

}

public class SpriteAnimation
{

    private List<SpriteAnimationFrame> _frames = new List<SpriteAnimationFrame>();

    public SpriteAnimationFrame this[int index]
    {
        get
        {
            return GetFrame(index);

        }

    }

    public int FrameCount => _frames.Count;

    public SpriteAnimationFrame CurrentFrame
    {

        get
        {
            return _frames
                .Where(f => f.TimeStamp <= PlaybackProgress + 0.0005f)
                .OrderBy(f => f.TimeStamp)
                .LastOrDefault();

        }

    }

    public float Duration
    {

        get
        {

            if (!_frames.Any())
                return 0;

            return _frames.Max(f => f.TimeStamp);

        }

    }

    public bool IsPlaying { get; private set; }

    public float PlaybackProgress { get; private set; }

    public bool ShouldLoop { get; set; } = true;

    public void AddFrame(Sprite sprite, float timeStamp)
    {

        SpriteAnimationFrame frame = new SpriteAnimationFrame(sprite, timeStamp);

        _frames.Add(frame);

    }

    public void Update(GameTime gameTime)
    {
        if(IsPlaying)
        {
            PlaybackProgress += (float)gameTime.ElapsedGameTime.TotalSeconds;                
            if (PlaybackProgress > Duration)
            {
                if (ShouldLoop)
                    PlaybackProgress -= Duration;
                else
                    Stop();
            }
        }
    }

    public void Draw(SpriteBatch spriteBatch, Vector2 position)
    {

        SpriteAnimationFrame frame = CurrentFrame;

        if (frame != null)
            frame.Sprite.Draw(spriteBatch, position);

    }

    public void Play()
    {

        IsPlaying = true;

    }

    public void Stop()
    {

        IsPlaying = false;
        PlaybackProgress = 0;

    }

    public SpriteAnimationFrame GetFrame(int index)
    {
        if (index < 0 || index >= _frames.Count)
            throw new ArgumentOutOfRangeException(nameof(index), "A frame with index " + index + " does not exist in this animation.");

        return _frames[index];

    }

    public void Clear()
    {

        Stop();
        _frames.Clear();

    }

    public static SpriteAnimation CreateAnimation(float frameLength, params Texture2D[] texture)
    {

        SpriteAnimation anim = new SpriteAnimation();

        for (int i = 0; i < texture.Length; i++)
        {
            Sprite s = new Sprite(texture[i]);
            anim.AddFrame(s, frameLength * i);
        }
        Sprite sprite = new Sprite(texture[0]);
        anim.AddFrame(sprite, frameLength * texture.Length);

        return anim;
    }

}

og her brugen af klasserne (tilret evt Game1):

public class Game1 : Game
{
    private GraphicsDeviceManager graphics;
    private SpriteBatch spriteBatch;
    SpriteAnimation sa;

    public Game1()
    {
        graphics = new GraphicsDeviceManager(this);
        Content.RootDirectory = "Content";
        IsMouseVisible = true;
    }

    protected override void Initialize()
    {
        // TODO: Add your initialization logic here

        base.Initialize();
    }

    protected override void LoadContent()
    {
        spriteBatch = new SpriteBatch(GraphicsDevice);
        var pos1 = Content.Load<Texture2D>("platformChar_walk1");
        var pos2 = Content.Load<Texture2D>("platformChar_walk2");
        sa = SpriteAnimation.CreateAnimation(0.5f, pos1, pos2);
    }

    protected override void Update(GameTime gameTime)
    {
        if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
            Exit();

        sa.Play();
        sa.Update(gameTime);
        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.CornflowerBlue);

        spriteBatch.Begin();
        sa.Draw(spriteBatch, new Vector2 { X = 200, Y = 200 });
        spriteBatch.End();

        base.Draw(gameTime);
    }
}