2D Game Engine


A custom 2D game engine made in C++ with OpenGL

This Game Engine was completely made by me, taking into account lots of the well known programming patterns and designs. A pac-man clone was made with the engine to test the engine functionality. Example game running in the engine

About the project

The game engine was written taking into account software design patterns and game programming patterns. It includes a game loop, command pattern, update method, components, items pools, an event system and some Singletons that manage resources and input. On top of these features, I decided to include a basic collision scene to the project as well. Currently, there is support for colliders(blocking) and overlappers(non-blocking) collisions. An objects can easily become a collider by adding the collision component to it. A collider can be set as static or, dynamic. Static colliders are only checked with dynamic colliders as it is assumed they will not move into each other during gameplay. Dynamic colliders will be checked with both all static and all other dynamic colliders. Ever collider component hold 2 vectors, one containing all overlapping objects and one containing all blocking objects. This way the programmer can react to overlaps and blocking collisions. The scene itself will not block any items from moving at this point, this is still the responsibility of the programmer creating the game in the engine.

For this whole project, I set up a Perfoce server to have source control. Ever since I started using it for this project, I have put all of my other projects on that server as well. This meant learning all of the administrator tools for Perforce, by now I’m fairly familiar with the whole program.

Code snippets

Core loop

The following snippet contains the core code, that runs the game loop. The game loop, along with the Time, resourcemanager and input singletons are encapsulated in this main Engine class. This way I can access some specific friend functions in these singeletons that I don’t want the game programmers to have access to.

The Initialize method sets the core settings for the engine. There is an option to lock the frame rate to a certain value, this is done by changing the msPerFrame value. However, this value is ignored when the lockFrameRate boolean is set to false. All other managers are also initialized in this method. The input manager looks for input from the keyboard or controllers and passes it to the correct object, using the command pattern. The renderManager has a pool of renderComponents, this way I cut down on memory allocations during gameplay.

The initialize method is called with a pool size, this way you can change how much space is allocated for potential renderComponents. The TimeManager keeps track of the time and is responsible for locking the framerate if the framerate is locked. It is initialized with the msPerFrame value and the lockFrameRate boolean.

If the framerate is locked the time.CalculateDeltaTime() function will make the main thread sleep for the needed time to lock the framerate to the desired value.

 1#include "MiniginPCH.h"
 2#include "Engine.h"
 3
 4#include "Renderer.h"
 5#include "../../../Managers/InputManager.h"
 6#include "../../../Managers/SceneManager.h"
 7
 8#include "Time.h"
 9#include "../../../Managers/RenderManager.h"
10
11void dae::Engine::Initialize()
12{
13	const int msPerFrame = 16;
14
15	auto& input = dae::InputManager::GetInstance();
16	input.Initialize();
17
18	auto& renderManager = RenderManager::GetInstance();
19	renderManager.Initialize(450);
20
21	bool lockFrameRate = false;
22
23	auto& time = Time::GetInstance();
24	time.SetLockFramerate(lockFrameRate, msPerFrame);
25	time.Initialize();
26}
27
28void dae::Engine::LoadGame(std::shared_ptr<Game> pGame)
29{
30	pGame->Initialize();
31	m_pGame = pGame;
32}
33
34void dae::Engine::Run()
35{
36	auto& renderer = Renderer::GetInstance();
37	auto& input = dae::InputManager::GetInstance();
38	auto& sceneManager = dae::SceneManager::GetInstance();
39	auto& time = Time::GetInstance();
40	bool doContinue = true;
41
42	while (doContinue)
43	{
44		doContinue = input.ProcessInput();
45
46		sceneManager.Update();
47		renderer.Render();
48
49		time.CalculateDeltaTime();
50	}
51}
52
53std::shared_ptr<dae::Game> dae::Engine::GetCurrentGame()
54{
55	return m_pGame;
56}

Render Manager

The next snippet shows the code for the RenderManager. This class has an object pool of RenderComponents, and can be called by a gameObject to get a new RenderComponent or to return a RenderComponent when an object gets destroyed. When the pool gets too small, it doubles the size of the pool.

This means we will have some dynamic allocations during the game, but by doubling the size we make sure it doesn’t happen too often. However, this can lead to memory waste if the pool gets doubled if only one object needed to be added. Therefore the programmer has to set an appropriate starting size of the pool, if memory is an issue.

The Draw method will call the draw function on every active RenderComponent. The order in which these components are drawn is very important as we want some object to be displayed above others, this is handled by layers, where higher level layers are drawn on top of lower level layers. When the layer of one of the RenderComponents changes, it has to flag this to the Rendermanager so that all components can be sorted, so that the correct components will be drawn at the correct time. This is done by the SetNeedsSorting() function and is checked every frame before drawing.

 1#include "MiniginPCH.h"
 2#include "RenderManager.h"
 3#include "../../../Components/RenderComponent.h"
 4#include <algorithm>
 5#include "../../../Engine/Texture2D.h"
 6
 7dae::RenderManager::~RenderManager()
 8{
 9	std::cout << "Destroying render manager";
10}
11
12void dae::RenderManager::Initialize(int poolSize)
13{
14	//Create a pool of inactive renderComponents
15	if(!m_IsInitialized)
16		CreateNewRenderComponents(poolSize);
17	m_IsInitialized = true;
18}
19
20//Render all the assigned render components
21void dae::RenderManager::Render()
22{
23	if(m_NeedsSorting)
24	{
25		std::sort(m_pActiveRenderComponents.begin(), m_pActiveRenderComponents.end(),
26		[](const std::shared_ptr<RenderComponent>& obj1, const std::shared_ptr<RenderComponent>& obj2)
27		{
28			return obj1->GetDepth() < obj2->GetDepth();
29		});
30	}
31
32	for (unsigned int i = 0; i < m_pActiveRenderComponents.size(); ++i)
33	{
34		m_pActiveRenderComponents.at(i)->Render();
35	}
36}
37
38std::shared_ptr<dae::RenderComponent> dae::RenderManager::GetRenderComponent()
39{
40	//if there are no new rendercomponents left, create a couple more
41	if (m_pInactiveRenderComponents.size() == 0)
42		CreateNewRenderComponents(int(m_pActiveRenderComponents.size()));
43
44	//get the last element in the vector
45	auto toReturn = m_pInactiveRenderComponents.at(m_pInactiveRenderComponents.size() - 1);
46	//delete the component from the vector
47	m_pInactiveRenderComponents.pop_back();
48	//push the component onto the active vector
49	m_pActiveRenderComponents.push_back(toReturn);
50	//return the component to the gameObject;
51	return toReturn;
52}
53
54void dae::RenderManager::ReturnRenderComponent(std::shared_ptr<RenderComponent> pRenderComponent)
55{
56	//add the component to the inactive component list
57	m_pInactiveRenderComponents.push_back(pRenderComponent);
58
59	//find the component in the active list and remove it from the active list
60	auto toRemove = std::find(m_pActiveRenderComponents.begin(), m_pActiveRenderComponents.end(), pRenderComponent);
61
62	//remove the component from the active list
63	if (toRemove != m_pActiveRenderComponents.end())
64		m_pActiveRenderComponents.erase(std::remove(m_pActiveRenderComponents.begin(), m_pActiveRenderComponents.end(), pRenderComponent), m_pActiveRenderComponents.end());
65	else
66		std::cout << "WARNING: tried to return a render component that was not created by the rendermanager!!!\n";
67}
68
69void dae::RenderManager::SetNeedSorting()
70{
71	m_NeedsSorting = true;
72}
73
74void dae::RenderManager::CreateNewRenderComponents(int amount)
75{
76	for (int i = 0; i < amount; ++i)
77	{
78		std::shared_ptr<RenderComponent> temp(new RenderComponent());
79		m_pInactiveRenderComponents.push_back(temp);
80	}
81}

Simple multithreading

The next snippet shows the code that makes the ghosts in the PacMan game run on different threads. First we have the header file, some of the functions are defined in line therefore I added the whole header to the code snippet.

The three functions implemented here are the triggers for both the update and the lateUpdated methods and the exit method. These should be pretty straightforward. The trigger functions tell the thread they need to update. When the exit function gets called, the thread will detach and destroy itself when it’s done.

The Run method will keep checking if the owned ghost has to be updated. This is only the case when the thread is flagged for an update (or lateUpdate) AND the atomic int GhostThreadUpdate (or GhostThreadLateUpdate) value is greater than 0. This ensures the ghosts only get updated once per frame, and is enforced by the update method of the PacManScene. If the thread’s ghost pointer becomes invalid, the thread will break execution and return. After which it gets destroyed.

 1#pragma once
 2#include <thread>
 3#include <mutex>
 4#include <atomic>
 5
 6namespace dae
 7{
 8	class Ghost;
 9	class GhostThread
10	{
11	public:
12		GhostThread(std::weak_ptr<Ghost> phost);
13		~GhostThread();
14
15		void Run();
16
17		void TriggerUpdate() { m_ShouldUpdate = true; }
18		void TriggerLateUpdate() { m_ShouldLateUpdate = true; }
19
20		void Exit()
21		{
22			if (m_Thread.joinable())
23				m_Thread.detach();
24		}
25		static std::atomic<int> GhostThreadUpdate;
26		static std::atomic<int> GhostThreadLateUpdate;
27		static std::atomic<bool> GhostThreadRun;
28		static std::mutex Mutex;
29	private:
30		std::shared_ptr<Ghost> m_pGhost;
31		std::thread m_Thread;
32
33		bool m_ShouldUpdate = false;
34		bool m_ShouldLateUpdate = false;
35		bool m_ShouldExit = false;
36	};
37}
 1#include "MiniginPCH.h"
 2#include "GhostThread.h"
 3#include "Characters/Ghost.h"
 4#include <mutex>
 5
 6std::atomic<int> dae::GhostThread::GhostThreadUpdate = 0;
 7std::atomic<int> dae::GhostThread::GhostThreadLateUpdate = 0;
 8std::atomic<bool> dae::GhostThread::GhostThreadRun = true;
 9std::mutex dae::GhostThread::Mutex;
10
11dae::GhostThread::GhostThread(std::weak_ptr<Ghost> ghost)
12	:m_pGhost(ghost)
13{
14	m_Thread = std::thread(&GhostThread::Run, this);
15}
16
17dae::GhostThread::~GhostThread()
18{
19}
20
21void dae::GhostThread::Run()
22{
23	while(GhostThreadRun)
24	{
25		if (!m_pGhost)
26			break;
27
28		if(m_ShouldUpdate &&GhostThreadUpdate > 0)
29		{
30			m_pGhost->Update();
31			m_ShouldUpdate = false;
32			--GhostThreadUpdate;
33		}
34		else if(m_ShouldLateUpdate &&GhostThreadLateUpdate > 0)
35		{
36			m_pGhost->LateUpdate();
37			m_ShouldLateUpdate = false;
38			--GhostThreadLateUpdate;
39		}
40	}
41}