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):
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);
}
}