\documentclass{projdoc} \title{Software Design} \begin{document} \tablestables \newpage \section{Introduction} This document outlines the design and development process of the cr\^epe game engine, detailing the key decisions made during its creation. The primary goal of this engine is to offer a streamlined, Unity-like experience tailored for developing 2D games similar to Jetpack Joyride. The cr\^epe engine is designed to ease the transition for developers familiar with Unity, ensuring minimal friction when switching platforms. Our aim is to preserve many of Unity’s core features while introducing a lightweight and open-source alternative, licensed under the MIT License. The engine is primarily aimed at indie developers who have prior experience with Unity and are looking for a flexible, cost-effective solution with familiar workflows. \section{Overview} As described above, the cr\^epe game engine's goal is to offer a Unity-like experience tailored for developing 2D games similar to Jetpack Joyride. That is why Jetpack Joyride and Unity provided the main inputs for this game engine design. Firstly, a quick overview will be given of the Unity game engine, in particular the \gls{ecs}. Secondly, this Overview will quickly talk you through some of the most important parts of the game engine, and why these parts are needed to create the Jetpack Joyride game. \subsection{ECS} The Unity game engine is structured using the Entity Component System (\gls{ecs}) (as shown in \cref{fig:ecs-block-diagram}). The \gls{ecs} is made out of three main subsystems, namely entities, components and systems. Entities are just IDs. An entity is also called a GameObject in Unity and it is made out of one (or more) components. Components are the classes that hold the data. The components determine what kind of entity it is (e.g. an enemy, audio, and so on). Systems take care of the behavior of the entities. Systems mainly read and write the enity's components data. The \gls{ecs} clearly distinguishes the data (components) from the functionality (systems). \begin{figure} \centering \includegraphics[width=0.5\textwidth]{img/ECSBlockDiagram.png} \caption{ECS design pattern} Source: \autocite{img:ecs-block-diag} \label{fig:ecs-block-diagram} \end{figure} The \gls{ecs} will also be used at the cr\^epe game engine. Everything (from the protagonist and bullets to the walls and enemies) in the cr\^epe game engine will be a GameObject (i.e.~entity). The game programmer must program his game by creating all kind of GameObjects and placing them in one (or multiple) scenes, just like Unity. \subsection{Jetpack Joyride} Firstly, some background information about Jetpack Joyride. Jetpack Joyride is a side-scrolling endless runner action video game created by Halfbrick Studios. The protagonist is called Barry Steakfries, who the player controls as he steals a bullet-powered jet pack from a top-secret laboratory \autocite{wikipedia:jetpack-joyride}. A screenshot from the game can be seen in \cref{fig:jetpack-joyride} (pleae be aware that the goal of this project is not to create an exact replica of Jetpack Joyride, it is only used as a source of inspiration). \begin{figure} \centering \includegraphics[width=0.5\textwidth]{img/JetpackJoyride.jpg} \caption{Jetpack Joyride} Source: \autocite{img:jetpack-joyride} \label{fig:jetpack-joyride} \end{figure} The protagonist wears a jetpack with which he can float in the air. The player must avoid obstacles (such as lasers, missiles and zappers) by floating at the right height. The player can control the protagonist's jetpack, thereby also controlling the protagonist's height. The protagonist experiences gravity and other forces (like the force from his jetpack pushing him upwards). These forces should be easily programmable by the game programmer. That is why a physics system is needed in the cr\^epe game engine. Only very limited/easy physics are needed for Jetpack Joyride, that is why this is only supported by the cr\^epe game engine. The protagonist must avoid obstacles. That is why the cr\^epe game engine should also support a collision system. Again, only very limited/easy collision is needed for Jetpack Joyride, that is why only very limited/easy collision is supported by the cr\^epe game engine. The game must, of course, also be visible to and playable by the user. A rendering system will take care of rendering (displaying) the game and its GameObjects. An input system will take care of all the inputs (mouse and keyboard). Jetpack Joyride also offers audio. A system will take care of the audio in the cr\^epe game engine. Particles are very common in Jetpack Joyride, e.g. underneath the jetpack and behind the rockets. Particles will be supported by the particle system. The start of a scene is described in a scene. However, the game programmer might also want to write game logic code which is running during the game (e.g. to switch to a new scene or to perform a custom action at a collision). For these purposes, Unity uses scripts. These scripts will also be supported by the cr\^epe game engine. Finally, as an extra, replay functionality will be supported by the cr\^epe game engine. A dedicated replay system will be used to support replay. It turns out that a physics, collision, rendering, input, audio, particle, script, and replay system are needed to create the Jetpack Joyride game. These systems form the main part of the \gls{ecs}. The design of these eight systems in combination with \gls{ecs}, will be briefly discussed in the next parts of this design document. \section{Design} \subsection{Game Loop} \subsubsection{Problem statement:} A game loop is essential for maintaining a continuous flow of game actions, ensuring that updates to game logic, physics, and rendering occur in a synchronized manner. Without a game loop, the game would lack consistent timing and leading to unpredictable behavior. The game loop is mainly responsible for these 2 purposes: \begin{itemize} \item Updating all systems in the correct order. \item Making sure the gameloop timer is up to date \end{itemize} The game loop can be external where the user has the ability to update the systems themselves or an intergrated game loop which is managed by the gameloop. Both of these approaches have advantages and disadvantages when it comes to flexibility and reliability. \subsubsection{Design:} The game loop of this engine is integrated into the engine, this is done for the following reasons: \begin{itemize} \item \emph{Simplify development} The user only has to call startGame(). \item \emph{Uniform system calls} The systems are always updated in the same order limiting overwrites and undefined system behavior. \item \emph{Reliable timer update} Each cycle the game loop timer is always updated limiting timing issues. \end{itemize} As seen in figure \ref{gameloop-flow} the gameloop is divided into different steps. \begin{itemize} \item \emph{Update loop timer} The loop timer gets updated and the expected frame time is calculated. \item \emph{Check events} Queued events get dispatched and callback functions are handled acordingly. \item \emph{Process input} The input system is called and user input is processed. \item \emph{Fixed update} A fixed loop for timing sensitive systems such as physics. \item \emph{Update} A per frame update for all per frame updates. \item \emph{Render} Calling the render system to render the frame. \end{itemize} This is done as illustrated in Figure \ref{gameloop-flow}, the game loop continues to call the fixed update function as long as sufficient time is available. Delta time, calculated using the time between the start of the last frame and the current frame, is used to measure the duration of each frame. This value is converted into a time-based unit, enabling other systems or game developers to create behavior independent of frame rate. Rendering and animations are handled separately on a per-frame basis. A delay, in conjunction with the delta time calculation, is applied to maintain consistent visual behavior, even when frame rates vary. As seen in figure Figure \ref{gameloop-class} to access the deltaTime anywhere in the system a timerClass is created using a singleton desing pattern which ensures only one instance of the class is created; the gameloop updates the timing and delta time of this class to ensure it is accurate. The gameloops two main functions are the setup() and loop(). The first is called when the game starts and handles all startup procedures this function only runs once. The loop() function keeps looping as long as the game is running. The gamedeveloper start the game engine/game using the code example below: \begin{blockcode} Gameloop loop; loop.start(); \end{blockcode} This starts calls the setup() and loop() functions and starts the game loop timer; To get the current frames Delta time \codeinline{LoopTimer::getInstance().getDeltaTime()} can be used which will return the expected frame time. \begin{figure} \centering \includepumldiag{img/gameloop-flow.puml} \caption{Gameloop Flowchart Diagram} \label{gameloop-flow} \end{figure} \begin{figure} \centering \includepumldiag{img/gameloop-class.puml} \caption{Gameloop Flowchart Diagram} \label{gameloop-class} \end{figure} \subsection{Event system} \subsubsection{Problem Statement:} The game engine utilizes the Entity-Component-System (ECS) architecture, where components store data, and systems process that data to apply changes. Each system is responsible for managing a specific domain, such as physics in the physics system and rendering in the rendering system. To facilitate communication between systems without introducing direct dependencies, a method of inter-system communication is required to maintain loose coupling. Additionally, a mechanism that allows one object's trigger to manipulate adn affect multiple other objects is beneficial for game developers, providing greater flexibility in designing interactions within the game. \subsubsection{Architecture:} The sollution to connect the various systems and BehaviorScripts together without inducing high coupling is an event system that facilitates communication between systems and BehaviorScripts using various types of events. The event system includes several pre-defined events, all derived from a parent Event class, capable of handling user input and application-level events, such as window resizing. Furthermore, a specific event is designated for the collision handler within the physics system, which can be triggered when two objects collide. The event system also allows developers to create custom events, such as "onPlayerDeath," and assign callback functions that execute when the event is triggered. \begin{figure} \centering \includegraphics[width=\linewidth]{img/event-uml.drawio.png} \caption{event system class diagram} \label{fig:event-uml} \end{figure} the event system as seen in \ref{fig:event-uml} includes several parts such as: \begin{description} \item[eventManager] The manager has the functions to subscribe/trigger/queue/dispatch events. It also stores all callback functions corresponding to specific event. The manager is a singleton and can therefor only exist once so all events are stored in one place. \item[IEventWrapper] This is a EventWrapper \emph{interface} which is used to store all the different templated eventshandlers in one map in the event manager. this wrapper contains the logic to convert the parent class \emph{event} to the correct subclasses. It also contains a variable onSuccessDestroy which can be set to destroy the callback call onces completed. This can be used to make a one time only event. \item[Event] This is the parent class where all specific event classes are derived from. Each event contains a \begin{itemize} \item \emph{\codeinline{static std::uint32_t getStaticEventType()}} to set type during compiling. \item \emph{\codeinline{virtual std::uint32_t getEventType() const override }} function to manage the type conversion during runtime. \end{itemize} Other functions can be freely added when creating a custom function. When an event is triggered a specific derived class must be used to indicate which event is triggered. A reference to this event is then transfered to all callback functions subscribed. \end{description} The EventManager is a singleton so all all callbacks are stored in one place and it can be called everywhere in the system or game. The user can use the EventManager for the following functions: \begin{itemize} \item \emph{Subscribe:} This subscribes a function pointer or lambda function to a given event. The function can be subscribed either to all event triggers or a specifc ID. \item \emph{Trigger:} This triggers a given event and all callbacks correlating to this event are executed immediately. \item \emph{Queue event:} This queues an event to be executed at a fixed time during the gameloop. \item \emph{Unsubscibe:} This removes the callback function from the event and it will no longer be executed. \end{itemize} Figure \ref{fig:event-seq} shows that when a specific function is triggered or dispatched using the callback(eventHandler) is executed. \begin{figure} \centering \includepumldiag{img/event-sequence.puml} \caption{Sequence diagram for event calling} \label{fig:event-seq} \end{figure} % \subsection{Physics} \subsection{Rendering} \subsection{Scripting} The scripting interface was designed around a `target' \gls{api} (described by \cref{req:script:interface,req:script:user-class,req:script:direct-instance,req:script:direct-run}). An example of this \gls{api} is shown below:\noparbreak \begin{blockcode} class MyScript : public BehaviorScript { void update() { // update code here } // init() also exists, but is empty by default }; { // in scene initialization GameObject & obj = ...; obj.add_component(); } \end{blockcode} The above call to \codeinline{GameObject::add_component} cannot work correctly without significantly increasing the complexity of the component manager, so the following restrictions were taken into account when creating the script system architecture:\noparbreak \begin{itemize} \item The first template parameter passed to \codeinline{GameObject::add_component} \emph{must} be a base `script \emph{component}' class, so each derived user script class is instantiated in the same generic script list. \item C++ does not allow passing types (i.e.~\codeinline{MyScript} in this case) as function parameters, so a function call like \codeinline{add_component(MyScript)} cannot be realized. \end{itemize} \subsubsection{Architecture} \label{sec:scripts:architecture} The restrictions detailed at the start of this section are mitigated as follows:\noparbreak \begin{itemize} \item User scripts are split into two classes--- \begin{enumerate} \item a script \emph{interface} class (\codeinline{Script}) \item a script \emph{component} class (\codeinline{BehaviorScript}) \end{enumerate} \item \codeinline{GameObject::add_component} receives the script \emph{component} as template parameter \item \codeinline{GameObject::add_component} now always returns a reference to the component instance \item The script component class has a setter function that takes a template parameter for classes derived from the base script \emph{interface} class \end{itemize} \Cref{fig:class-scripts} shows the resulting structure as a class diagram. It contains the following classes:\noparbreak \begin{description} \item[Script] This is the script \emph{interface}, and is used by the game programmer to create derived script classes. All virtual methods in this class have an empty implementation by default, and are optionally implemented by the game programmer. This class' virtual methods are protected by default, and a friend relation to \codeinline{ScriptSystem} is used to ensure only \codeinline{ScriptSystem} is able to call these methods. Only classes derived from \codeinline{Script} can be used with \codeinline{BehaviorScript::set_script}'s template parameter \codeinline{T}. This function returns a reference to the \codeinline{BehaviorScript} instance it was called on so it can be chained after the call to \codeinline{GameObject::add_component}. \codeinline{Script} also has a reference to its parent \codeinline{BehaviorScript} instance so components can easily be retrieved using the component manager. \item[BehaviorScript] This is the script \emph{component}, and is given as the template parameter to \codeinline{GameObject::add_component}. This class also uses a friend relation to \codeinline{ScriptSystem} to restrict access to its private reference member \codeinline{script}. \item[ScriptSystem] This is the system class that runs the methods implemented in the derivative instances of \codeinline{Script}. Described further in \cref{sec:scripts:sytem}. \end{description} \begin{figure} \centering \includepumldiag{img/class-scripts.puml} \caption{User script class diagram} \label{fig:class-scripts} \end{figure} \subsubsection{System} \label{sec:scripts:sytem} Because most of the complexity in the scripting interface comes from the containers described in \cref{sec:scripts:architecture}, the script system class itself is relatively simple. The script system provides a method \codeinline{ScriptSystem::update} that calls all active script's update functions. Because of the limitation that types cannot be passed as parameters in C++, the user-defined script class (derived from \codeinline{Script}) can not directly be instantiated when adding the component to the component manager. To work around this limitation, the method \codeinline{BehaviorScript::set_script} was created. This results in the possibility that an instance of \codeinline{BehaviorScript} does not reference an instance of \codeinline{Script}. In addition to the non-active script components, the script system skips over these `invalid' instances. This is illustrated in \cref{fig:activity-scripts}. \begin{figure} \centering \includepumldiag{img/activity-scripts.puml} \caption{Script system update method} \label{fig:activity-scripts} \end{figure} A \gls{poc} for the script system is shown in \cref{poc:scripts}. \subsection{Audio} Since writing a custom real-time audio mixing engine is outside the scope of this project\mref and C++ does not provide a built-in cross-platform audio \gls{api}, the audio system inside the cr\^epe engine is implemented as a \gls{facade} around an existing audio library. \subsubsection{Libraries} \label{sec:audio:libs} This subsection compares various standalone audio libraries for suitability. After searching for libraries (search terms: `dynamic/adaptive audio', `real-time audio', `audio library', `game audio engine'), several libraries were found. These libraries were checked against the audio engine requirements \autocite{crepe:requirements} and then tested by writing the same benchmark-style \gls{poc} using the remaining qualifying libraries. These \glspl{poc} are detailed in \cref{poc:audio}. Of these libraries the following were determined to be unsuitable for use in this project:\noparbreak \begin{description} \item[FMOD \autocite{lib:fmod}] Is proprietary (violates \cref{req:lib:license}). \item[PortAudio \autocite{lib:portaudio}] Does not handle mixing. \item[miniaudio \autocite{lib:miniaudio}] Tested by implementing a \gls{poc}, but dropped due to very limited codec support (WAV, MP3 and FLAC only); Also does not have an \gls{api} reference (only programming manual). \item[YSE \autocite{lib:yse}] Attempted to write \gls{poc}, but CMake configuration in repository is broken; This project seems to have been abandoned. \end{description} The only library that remained after these tests is SoLoud \autocite{lib:soloud}. It is Zlib/LibPng licensed and provides a high-level object-oriented C++ \gls{api}. \Cref{sec:audio:architecture} describes the \gls{facade} written for this library. \subsubsection{Architecture} \label{sec:audio:architecture} \Cref{fig:class-audio-facade} shows a class diagram of the audio \gls{facade}. It contains the following classes: \begin{description} \item[SoundContext] This is a wrapper around the \codeinline{SoLoud::soloud} `engine' class, and is therefore implemented as a singleton. This ensures the audio engine is initialized before \codeinline{Sound} is able to use it. This class is friends with \codeinline{Sound}, so only \codeinline{Sound} is able to get the \codeinline{SoundContext} instance. \item[Sound] This is a wrapper around the \codeinline{SoLoud::Wav} class, and uses cr\^epe's \codeinline{Asset} class to load an audio sample instead. \end{description} \begin{figure} \centering \includepumldiag{img/facade-audio.puml} \caption{Audio \glsfmtshort{facade} class diagram} \label{fig:class-audio-facade} \end{figure} A \gls{poc} for the final Audio \gls{facade} is also showcased in \cref{poc:audio}. % \subsection{Save manager} % % The save manager \gls{api} is designed to give the game programmer an easy to use % interface for retrieving and storing game-specific data (\cref{req:savemgr}). % % Because the engine validation app only stores statistics and highscores, the save % manager is not required to support loading different save files % (\cref{req:savemgr:multi-file}), nor storing complicated data types % (\cref{req:savemgr:types-custom}). The save manager only supports storing simple % types (\cref{req:savemgr:types-scalar,req:savemgr:types-string}). % % In order to reduce complexity for the game programmer further, the following % requirements were also set:\noparbreak % % \begin{itemize} % \item Prevent data loss in the case of crashes (\cref{req:savemgr:journalling}) % \item Handle opening/closing/flushing of the underlying file automatically % (\cref{req:savemgr:file-manage}) % \item Save file variables are uniquely identified (\cref{req:savemgr:var-key}) % \end{itemize} % % \subsubsection{Architecture} % \label{sec:savemgr:architecture} % % \begin{figure} % \centering % \includepumldiag{img/class-savemgr.puml} % \caption{Save manager class diagram} % \label{fig:class-savemgr} % \end{figure} % % In order to realize \cref{req:savemgr:journalling,req:savemgr:var-key}, a third-party % key-value database library is used. \subsection{Global configuration interface} Because the game programmer only has access to interfaces within the public \gls{api} namespace (\codeinline{crepe::api}), they would not be able to configure aspects of engine-internal components. To work around this access restriction, a global interface was made that stores arbitrary data, which can be accessed both internally and via the public \gls{api}. \subsubsection{Architecture} \label{sec:config:architecture} The global configuration interface consists of a single singleton class that can be accessed globally (see \cref{fig:class-config}). This class holds several anonymous structs, which are used to organize options per system or engine component. \begin{figure} \centering \includepumldiag{img/class-config.puml} \caption{Global configuration interface class diagram} \label{fig:class-config} \end{figure} % \subsection{Input} % \subsection{Physics} \appendix \section{\Glsfmtlongpl{poc}} The full (documented) source code of these \glspl{poc} is available on GitHub \autocite{crepe:code-repo}. \subsection{Script system} \label{poc:scripts} The script system \gls{poc} \autocite[script example]{crepe:code-repo} consists of the following:\noparbreak \begin{itemize} \item A user-defined class (\codeinline{MyScript}) derived from \codeinline{Script}, which only implements the \codeinline{update()} function. \item A main function that--- \begin{itemize} \item Creates a game object with \codeinline{Transform} and \codeinline{BehaviorScript} components. \item A call to \codeinline{ScriptSystem::update}, which results in \codeinline{MyScript::update} being called. \end{itemize} \end{itemize} Running the \gls{poc} results in the output shown in \cref{fig:poc-output-scripts}, demonstrating that the system works as intended. \begin{figure} \centering \fitimg{\includegraphics[scale=0.7]{img/poc-output-scripts.png}} \caption{Script system \glsfmtshort{poc} output} \label{fig:poc-output-scripts} \end{figure} \subsection{Logging utilities} \label{poc:log} A small \gls{poc} was written to test the engine's logging functions \autocite[log example]{crepe:code-repo}. The following calls are used in this example: \begin{blockcode} dbg_trace(); // the dbg_* macros automatically show dbg_logf("test printf parameters: %d", 3); // where the message is coming from logf(LogLevel::INFO, "info message"); logf(LogLevel::WARNING, "warning"); logf(LogLevel::ERROR, "error"); \end{blockcode} The output of this test is shown in \cref{fig:poc-log}. \begin{figure} \centering \includegraphics[scale=0.7]{img/poc-log.png} \caption{Logging function outputs} \label{fig:poc-log} \end{figure} \subsection{Audio} \label{poc:audio} A test that consists of the following steps was written for each audio library mentioned in \cref{sec:audio:libs}:\noparbreak \begin{enumerate} \item Load a background track (Ogg Vorbis) \item Load three short samples (WAV) \item Start the background track \item Play each sample sequentially while pausing and resuming the background track \item Play all samples simultaniously \item Stop all audio and exit \end{enumerate} The repository \autocite{crepe:code-repo} contains two finished \glspl{poc} under the \codeinline{mwe/audio/} subdirectory for miniaudio and SoLoud. The SoLoud \gls{poc} was later converted to a full audio \gls{facade}, which is currently part of the cr\^epe engine. The \gls{poc} using the audio \gls{facade} is available from the same repository, under the \codeinline{src/example/audio_internal.cpp} file. \end{document}