Sprite (OpenGL i .NET Framework)

Jag har dragit nytta av ett gammalt C#-repository som wrappar OpenGL för .NET Framework för att bygga ett minimalistiskt spelramverk. Följande kod visar stjärnor som flyger över skärmen, från höger till vänster. Projektet är .NET Framework 4.8 som jag valde för att det finns inbyggt Windows 10/11.

Exemplet visar initiering av spelmotorn, att skapa sprites med GDI+, användning av en scen, tangentbordsavläsning, en sprite batch (som erbjuder fire-and-forget sprites).

Projekttypen är en console application, och referenserna är Sprite och System.Drawing. Repositoryt finns här och detta är den kompletta koden:

using System;
using System.Drawing;
using Sprite;

namespace Starfield
{
    public class Program
    {
        public static SpriteBitmap Star;
        public static GameEngine GameEngine { get; private set; }

        public static void Main()
        {
            // Create and initialize the game engine.
            GameEngine = new GameEngine("Starfield", OnLoadResources);
            GameEngine.LoadResources();

            // Run the game.
            GameEngine.Run(new StarfieldScene());
        }

        private static void OnLoadResources(object sender, EventArgs e)
        {
            // Draw a "star".
            var star = new Bitmap(2, 2);
            star.SetPixel(0, 1, Color.White);
            star.SetPixel(0, 0, Color.White);
            star.SetPixel(1, 1, Color.White);
            star.SetPixel(1, 0, Color.White);

            // Create a sprite bitmap from the star.
            Star = SpriteBitmap.FromImage(star, 1, 1);

            // Tell engine that loading is completed.
            GameEngine.SetLoadResourcesComplete();
        }
    }

    // Define the star sprite that will fly from right to left.
    public class StarSprite : BatchGameSprite
    {
        private int SpeedX { get; }

        public StarSprite(SpriteBitmap spriteBitmap, int y, int speedX) : base(spriteBitmap, 320, y, 1)
        {
            SpeedX = speedX;
        }

        public override void ApplyLogic() =>
            X += SpeedX;

        public override bool Alive() =>
            X > -5;
    }

    public class StarfieldScene : IScene
    {
        private readonly Random _rnd = new Random();

        // Create a sprite batch (used for fire-and-forget sprites) to hold the stars.
        private readonly SpriteBatch _spriteBatch = new SpriteBatch();

        public void Render(GameEngine gameEngine)
        {
            // Check if the user wants to exit.
            if (gameEngine.SpriteWindow.IsKeyDown(VirtualKeys.Escape))
                gameEngine.SpriteWindow.Running = false;

            // Add a new star each frame.
            _spriteBatch.Add(new StarSprite(Program.Star, _rnd.Next(199), -(_rnd.Next(5) + 1)));

            // Make the stars act.
            _spriteBatch.ApplyLogic();

            // Draw the frame.
            _spriteBatch.Draw(gameEngine.SpriteWindow);
            gameEngine.SpriteWindow.Swap();
        }
    }
}

En gameloop med input i GLBasic

Att komma igång och skapa indiegames för t.ex. Windows är inte speciellt svårt. Har man minimal programmeringskunskap kan man komma igång, och det du gjorde på din gamla Commodore 64 räknas som meriterande programmeringserfarenhet i detta fall.

GLBasic är en kommersiell produkt, men det finns en gratisversion som tillåter utveckling av 2D-spel för bl.a. Windows. Styrkan med GLBasic är att kompilatorn kompilerar till flera plattformar, däribland GPX2 Wiz som är min favoritenhet för casual gaming. Den stora svagheten delar GLBasic med andra enklare utvecklingsmiljöer för spel: Språket är vansinnigt begränsat, så ett komplicerat spel innebär en komplicerad kod.

Tänk på att ett GLBasic-projekt består av flera filer, och att du därför bör skapa en mapp åt ditt projekt. Projektet utgörs av en GBAP-fil – det är denna som du öppnar för att återkomma till ditt projekt. Exe-filen som produceras hamnar i en undermapp till den mapp som GBAP-filen ligger i.

Skärmupplösningen sätts med kommandot SETSCREEN. Förutom bredd och höjd ska 1 anges för att indikera att spelet ska köras i fullskärmsläge. Därför anger jag 0 medan jag testar min kod, eftersom 0 anger att spelet körs i ett fönster.

SETSCREEN 640, 480, 0

En egenskap som underlättar utvecklandet av en gameloop är automatisk synkronisering. Istället för att behöva klocka dina beräkningar och pausa, kan du helt enkelt be GLBasic om ett önskat antal uppdateringar per sekund (FPS) så sköts detta genom magi. Jag upplever att 30 räcker, men 50 ger aningen mjukare rörelser. I fullscreen-spel bör du välja 60.

LIMITFPS 50

För att figurer som är irreguljärt formade använder GLBasic en mask. Denna genereras automatiskt utifrån en färg i bilden som används. Kommandot SETTRANSPARENCY anger önskad färg och färger anges med funktionen RGB. Nedanstående väljer lila färg som mask.

SETTRANSPARENCY RGB(255, 0, 255)

Innan vi läser någon media, kan det vara bra att peka ut var programmet kan leta efter filerna. Jag tänkte lägga lite grafik i en mapp som heter “Media” som ligger under exe-filens mapp. Därför anger jag den relativa sökvägen till Media.

SETCURRENTDIR("Media")

Nu behöver jag ha en bild som ska agera spelets karaktär. Jag skapar en BMP-fil på 48×48 punkter med Microsoft Paint. Det är viktigt att bilden ligger på lila botten, eftersom vi angav lila som maskfärg. Bilden sparar jag som “player.bmp” i mappen Media.

sprite
Efter denna fenomenala insats för den grafiska konsten, är det dags att ladda in bilden som en sprite. Förutom filnamn tillhandahåller jag även ett index för spriten. Det är ganska smart att i förväg deklarera vilka index man tänker använda sig av, så att man slipper hålla reda på en massa siffror, men för denna gång anger jag 0 som index.

LOADSPRITE "player.bmp", 0

När spelaren är laddad, vill jag deklarera att spelaren har en position. Procenttecknet (%) indikerar att variablerna x och y är 32-bitars heltal.

TYPE Player
  x%
  y%
ENDTYPE

Därefter kan jag skapa en variabel av typen Player och initiera den. Jag vill placera spelaren långt ner på skärmen, i mitten. Den horisontella mittpunkten är lika med halva skärmens bredd minus halva spelarens bredd, alltså 320 – 24 = 296. Den tvåhundranittiosjätte pixelns x-koordinat är 295.

GLOBAL plr AS Player
plr.x% = 295
plr.y% = 440

Själva iterationen (loopen) ska käras till dess att användaren trycker på Escape. Escape har kod 1 och funktionen KEY testar om en tangent är nertryckt, alltså ser loopen ut så här:

WHILE NOT KEY(1)
   //Resten av koden skrivs här!
WEND

Resten av koden skrivs mellan WHILE och WEND. Det som återstår är att skriva kod som lyssnar på användarens tangentbordstryckningar, kod som renderar spelplanen och en anrop till GLBasic som talar om att vi är färdiga med en frame, så att GLBasic kan visa och synkronisera. Låt oss börja med koden som flyttar spelaren efter användarens tangentbordstryckningar.

//Låt användaren styra höger och vänster.
IF KEY(205) THEN plr.x% = plr.x% + 2
IF KEY(203) THEN plr.x% = plr.x% - 2

Just GLBasic har ett verktyg i Tools-menyn som låter dig ta reda på vilken kod som är kopplad till vilken tangent. 205 betyder höger och 203 betyder vänster. Vi vill även förhindra att spelaren simmar ur bild.

//Man får inte simma ur bild.
IF plr.x% < 0 THEN plr.x% = 0
IF plr.x% > 591 THEN plr.x% = 591

Sen renderar vi spelplanen. I vårt fall handlar det om ett enda anrop på DRAWSPRITE (som tar index, x-position och y-position), men ett riktigt spel kommer att ha många anrop på DRAWSPRITE här.

//Rendera spelplanen.
DRAWSPRITE 0, plr.x%, plr.y%

Och sist ber vi GLBasic sköta om presentationen av det ritade, samt synkroniseringen.

//Lämna över till GLBasic att sköta resten.
SHOWSCREEN

Och där har vi en färdig gameloop! Detta är den färdiga koden:

SETSCREEN 640, 480, 0
LIMITFPS 50
SETTRANSPARENCY RGB(255, 0, 255)
SETCURRENTDIR("Media")
LOADSPRITE "player.bmp", 0

TYPE Player
	x%
	y%
ENDTYPE

GLOBAL plr AS Player
plr.x% = 295
plr.y% = 420

WHILE NOT KEY(1)

	//Låt användaren styra höger och vänster.
	IF KEY(205) THEN plr.x% = plr.x% + 2
	IF KEY(203) THEN plr.x% = plr.x% - 2

	//Man får inte simma ur bild.
	IF plr.x% < 0 THEN plr.x% = 0
 	IF plr.x% > 591 THEN plr.x% = 591

	//Rendera spelplanen.
	DRAWSPRITE 0, plr.x%, plr.y%

	//Lämna över till GLBasic att sköta resten.
	SHOWSCREEN

WEND