Crystal

Crystal Game Engine

Werkplan

Omschrijving

Tot nu toe is “Crystal” een simpele 2D game engine geschreven in C++. Graphics worden gedaan door mijn eigen abstractielaag rondom OpenGL. De engine is opgesplits in een “Core” met alle Renderer2D/Vertexbuffer/Vertexarray functionaliteit. En een “Physics” gedeelte met een start van een Entity Component System om componenten van “Entity’s” goed in memory op te slaan zodat je niet memory hoeft te kopieeren als je een entity aan een functie doorgeeft bijvoorbeeld. En een “Editor” project waarin de Engine word gebruikt en dingen laat zien aan het scherm bijvoorbeeld een 2D textured quad.

Plan

Verslag

Crystal Game Engine

Door: Jorben van der Wal
Klas: H4F
Datum: 06-01-24

Inhoud

Inleiding

Ik heb gekozen om een game engine te ontwerpen en te bouwen. Ik heb hiervoor gekozen om meerdere redenen. De eerste reden is dat ik al games aan het maken was in C++ zonder game engine. Het probleem hiermee is alleen dat je als je een game maakt steeds dezelfde stappen opnieuw moet maken/coderen. Een game engine doet dat voor je en geeft je ‘tools’ om het creëren van een game makkelijker te maken. Ik heb het maken van een game engine op dat moment toch uitgesteld, want ik had verwacht dat dit een veelste groot en ingewikkeld project zou worden. Later zag ik een man op YouTube die ook zijn eigen game engine maakte. Dit gaf mij genoeg inspiratie om dit zelf ook te doen. Vandaar nu dit project.

Materiaal/methode

Ik heb alles gecodeerd op een Windows machine in C++. Ik heb dit gedaan omdat ik al C++ kende en ik weet dat C++ de snelste object georiënteerde taal is. Ik heb gebruikt gemaakt Microsoft Visual Studio vanwege de goeie support van C++. Om grafisch iets te laten zien om een scherm heb ik gebruik gemaakt van de rendering API: “OpenGL”. Ik heb deze keuze gemaakt omdat de YouTube vanwaar ik inspiratie kreeg dit ook gebruikte en omdat hij hier een tutorial serie van 30+ afleveringen over heeft gemaakt. Bij de start van dit project wist ik nog helemaal niks over OpenGL. Ik ging OpenGL leren door middel van deze tutorial serie en door learnopengl. Om meer te leren over hoe een game engine in elkaar zit qua architectuur heb ik nog diverse tutorials over game engine architectuur gekeken. Om het scripten van een game engine op te zetten heb ik veel C# gebruikt. Lua heb ik ook gebruikt om de Microsoft Visual Studio project bestanden met elkaar te linken.

Logboek

In hoofdlijnen heb ik mijn engine zo gebouwd:

Om dieper in alle stappen te gaan kunt u mijn github commits bekijken:

Resultaten

Het begon allemaal met een simpele driehoek:

Maar inmiddels ziet het er zo uit:

Het resultaat is dus nu een goedwerkende game engine waarmee je vooral 2D games kunt maken. De engine zorgt ervoor dat je alleen nog maar entities hoeft aan te maken en zelf een script te schrijven in C# en voila je hebt een game die je kan shippen via de Crystal Runtime. De engine bestaat uit meerdere versies de editor en de runtime. Deze 2 samen vormen 9000+ regels aan code in zowel C++ als C#.

Discussie

Ik heb tijdens dit project veel geleerd. Ik snap C++ veel beter dan toen ik begon met dit project. Toen ik starte codeerde ik op een python manier in C++, dit is niet optimaal. Na veel te coderen in C++ snap ik meer hoe de compiler naar de code kijkt en hoe ik de code op laag niveau kan optimaliseren. Voor dit project had ik geen enkele kennis van C# en Lua. Maar om dit project te bouwen heb ik beide tot een bepaald niveau moeten leren. Ook heb ik geleerd hoe de graphische pijplijn werkt en hoe de videokaart kijkt naar data. Dit allemaal met OpenGL. Alleen niet alles ging natuurlijk soepel. Ik heb veel memory leaks gehad die ik heb moeten fixen, beide op de CPU en GPU. Ik heb Stackoverflow en GPT-4 gebruikt om deze problemen op te lossen. Ik heb ook naar oplossing gezocht door iets aan te passen en te proberen en aan te passen en weer te proberen tot dat het werkt. Ik heb geleerd dat je moet oppassen met heap memory en je zo veel mogelijk memory op de stack wilt hebben (zie Probleem 2).

Grootste lessons learned

Voorbeelden

Probleem 1:

Als ik binnen in mijn editor tussen mijn scene’s binnen in mijn game switch zie ik dat de memory wel omhoog gaat maar niet meer naar beneden. Oftewel een memory leak. Maar waar? Om dit probleem op te lossen ben ik op elk punt van mijn code gaan kijken hoe hoog de memory usage was en dan te kijken wanneer gaat het omhoog. Leuk en aardig, maar eigenlijk wil ik weten waarom het niet weer naar beneden gaat. Wat ik ook heb geprobeerd is een deleaker software te gebruiken. Het probleem was dat ik hier niks van snapte. Ik heb over dit probleem lang na gedacht en ik dacht misschien kan de Visual Studio Performance profiler mij helpen. Ik heb 2 snapshots gemaakt van de memory, 1 voor dat het omhoog gaat en 1 daarna. Daarna heb ik ze vergeleken en zag ik waar de memory vandaan kwam en niet werd opgeruimd. Gefixt.

OpenGLTexture2D::~OpenGLTexture2D()
{
	if (m_Data) stbi_image_free((void*)m_Data); // Deze line was ik vergeten.
	glDeleteTextures(1, &m_RendererID);
}

Probleem 2:

Ik wilde graag dat als ik mijn runde dat hij na dat ik de game stopte weer terug ging naar de originele staat. Simpel genoeg dacht ik. Ik dacht ik kopieer gewoon mijn eigen Storage class waar alle informatie over alles in de scene in staat en sla die kopie gewoon op. Zo simpel was het echter niet. In mijn storage class gebruikte ik het ‘new’ keyword wat memory op de heap aanmaakt. Dit betekent dat mijn Storage de memory eigenlijk niet ‘ownt’. Hier kwam het probleem vandaan. Ik probeerde memory te kopieren dat niet van ‘mij’ was. Dit kan niet en zorgde ervoor dat mijn engine crashte.
Om dit op te lossen heb ik het ‘new’ keyword weg gehaald en alle memory op de stack aan te maken direct in mijn Storage class. Dit betekent dat ik alles kan met alle memory wat ik maar wil. Ik wist dat ik dit zo moest oplossen omdat ik zelf al verstand heb van de heap en de stack en dus door te kijken naar hoe de memory in mijn class werkte dacht ik meteen het probleem te zien. En mijn vermoedens waren correct.

// Notitie: Dit is niet code die word gebruikt in mijn engine, maar om te laten zien wat ik bedoel.
Foo::Foo()
{
    m_Component = new Component(NULL);
    delete m_Component; // Deze word gecalled in de destructor
    // verandert naar
    m_Component = Component(NULL);
}

Dingen die ik anders zou doen

Bekijk mijn project op github