C++ Game Engine
Video⌗
View on GitHub!What is it?⌗
- Engine: My own, based on SFML!
- Language: C++
- Goal: To learn how game engines are made at a low level and use advanced C++ features.
This project is my first attempt at making a game engine. It uses SFML as a base library to add support for windows, images, sound, and basic input. The API is similar to Unity, with GameObjects and Components, but you cannot nest GameObjects to create more complex entities yet.
This engine was originally made to create a clone of Sinistar, by Williams Electronics. However, just in case, I’ve (hopefully) removed all traces of Sinistar from the repository before uploading it!
Features⌗
- Supports Windows and Linux compilation thanks to Premake
- GameObjects with Component-based architecture
- Texture memory management
- Sprite rendering
- Circle collision
- Basic Rigidbody physics
- Movable camera
- Scene management
- Frame delta time and fixed timestep
- Basic test scene:
- Player ship
- Smooth following camera
- Basic collision test
How does resource management work?⌗
The resource management in the engine is done via smart pointers, which is a C++ 20 feature that allows the program to track how many times a pointer is used and automatically delete the data when there are no references left. This saves having to make a reference-counted pointer class ourselves, much like how Godot’s source code does it. Perhaps there are benefits to this approach that I don’t yet understand, though - compatibility with older C++ comes to mind.
When a script needs to get a reference to a texture, it does so through the ResourceManager
class. This class provides a static method that returns a shared pointer to the texture, called GetTexture()
.
This is how we would get a texture using the ResourceManager
in practice:
placeholderShip->GetComponent<SpriteRenderer>()->SetTexture(ResourceManager::GetTexture("assets/textures/ship.png"));
The ResourceManager
class holds a map of weak pointers referenced by their file path, which is passed in by GetTexture()
. In the future this should be done using an ID or a hash of the file, but this will do for now.
ResourceManager.h
:,
namespace LLGP
{
class ResourceManager
{
public:
static std::shared_ptr<sf::Texture> GetTexture(std::string path);
private:
static std::map<std::string, std::weak_ptr<sf::Texture>> _textureMap;
};
}
(note: the “LLGP” namespace is left over from what the university module was called!)
The reason I am using weak pointers instead of shared pointers here is so the resource manager can hold a non-counted reference. If it held a shared pointer, then anything that the game would no longer be using would stay in memory and slowly leak.
The function first checks if there is an existing pointer in that location in the map and if that pointer has any data. If both are true, then it will make a new shared pointer to that location and pass it on.
However, if there is a pointer with no data or no pointer at all, then it will load a new texture and pass on the resulting shared pointer to it.
ResourceManager.cpp
:
namespace LLGP
{
std::map<std::string, std::weak_ptr<sf::Texture>> ResourceManager::_textureMap;
std::shared_ptr<sf::Texture> ResourceManager::GetTexture(std::string path)
{
// If weak pointer exists in map and isn't expired, lock it and return a new shared pointer to it.
if (_textureMap.contains(path) and not _textureMap[path].expired())
{
return _textureMap[path].lock();
}
// Else, load a new texture and make a shared pointer to it. Then store a weak pointer to the shared pointer.
else
{
const std::shared_ptr<sf::Texture> tex = std::make_shared<sf::Texture>();
_textureMap[path] = tex;
if (!tex->loadFromFile(path))
{
std::cout << "FILE FAILED TO LOAD SOMEHOW" << std::endl;
}
return tex;
}
}
}
And that’s it! This is all the code needed to create a reference-counted resource manager that automatically frees memory that isn’t being used. Just for textures, though.
How are sprites rendered?⌗
Sprites in this little engine are rendered using a component called a SpriteRenderer
, which you may have spotted when looking back at the ResourceManager! When this component is added to a GameObject, it will show a texture that follows the GameObject’s position, scale, and rotation.
However, to understand how it is rendered on the screen, we must first take a look at the backbone of the system: the Renderer
class.
Renderer.h
:
namespace LLGP
{
class Renderer
{
public:
static void Init();
static void RegisterDrawable(sf::Drawable* drawable);
static void DeregisterDrawable(sf::Drawable* drawable);
static void Draw();
static void SetView(sf::View* newView) { m_currentView = newView; }
static sf::RenderWindow* GetRenderWindow() { return m_renderWindow; }
private:
static std::vector<sf::Drawable*> m_drawables;
static sf::RenderWindow* m_renderWindow;
static sf::View* m_currentView;
};
}
This static class is what renders images onto the screen. It holds a vector of sf::Drawable
pointers, which is SFML’s parent class for anything that can be drawn onto the screen. The RegisterDrawable()
and DeregisterDrawable()
functions allow you to add and remove objects from the renderer respectively. Draw()
goes though each Drawable and renders it to the screen with SFML.
Renderer.cpp
:
void Renderer::Draw()
{
m_renderWindow->clear();
m_renderWindow->setView(*m_currentView);
for (const sf::Drawable* drawable : m_drawables)
{
if (drawable == nullptr) continue;
m_renderWindow->draw(*drawable);
}
m_renderWindow->display();
}
So how are Drawables handled? Well, the idea of this engine was to wrap SFML into a structure that is classed as (or at least resembles) a game engine, so I looked at how SFML divides children of the sf::Drawable
class:
Seen above, anything that can be drawn on screen inherits the Drawable class in order to be drawn on the screen. So to that end, I created a DrawableComponent
to act as the base class for anything drawable! Any further classes would inherit this and simply add extra options to wrap their respective sf::Drawables
into components.
Drawable.h
:
class DrawableComponent : public Component
{
protected:
sf::RenderStates _renderStates;
public:
DrawableComponent(GameObject *owner) : Component(owner) {}
~DrawableComponent();
protected:
void SetDrawable(sf::Drawable* drawablePointer);
sf::Drawable* GetDrawable() const { return _drawable; }
private:
sf::Drawable* _drawable;
};
Inside the SetDrawable()
function, a call is made to the Renderer
to add that Drawable to the list:
void DrawableComponent::SetDrawable(sf::Drawable *drawablePointer)
{
_drawable = drawablePointer;
Renderer::RegisterDrawable(_drawable);
}
So, to recap thus far:
- The
DrawableComponent
acts as a base class that wrapssf::Drawable
’s into a component. - The
Renderer
gets given thesf::Drawable
by theDrawableComponent
when it is set. - These then get drawn by the
Rendeer
whenDraw()
is called.
This finally brings us to the SpriteComponent
class! This component wraps the sf::Sprite
class into a component structure, storing a reference to it in both Sprite and Drawable form:
SpriteRenderer.h
:
class SpriteRenderer : public DrawableComponent
{
public:
SpriteRenderer(GameObject* owner);
bool flipX;
bool flipY;
void SetTextureRect(const sf::IntRect rect) { m_sprite->setTextureRect(rect); }
const sf::IntRect GetTextureRect() { return m_sprite->getTextureRect(); }
void SetTexture(std::shared_ptr<sf::Texture> newTexture);
inline std::shared_ptr<sf::Texture> GetTexture() { return m_texturePointer; }
void Update(float deltaTime) override;
private:
sf::Sprite* m_sprite;
std::shared_ptr<sf::Texture> m_texturePointer;
};
Inside of the SpriteComponent
’s constructor, a new sf::Sprite
is created which gets set as the Drawable, which is when the pointer is passed to the Renderer
:
SpriteRenderer::SpriteRenderer(GameObject *owner) : DrawableComponent(owner)
{
SetDrawable(new sf::Sprite());
m_sprite = dynamic_cast<sf::Sprite*>(GetDrawable());
flipX = false;
flipY = false;
}
This abstraction means that when the class is created, its Drawable does not need to be manually registered to the Renderer
. The beauty of this approach is that, in theory, any sf::Drawable
derivative can have its own component-based wrapper that produces results on-screen just as fast as any other Drawable. I considered making a TextComponent, but I did not have time before the submission.
What could be improved?⌗
Currently, the resource manager can only handle textures - not sound. However, this can very easily be extended to do so using SFML’s sound classes. Perhaps more data could be handled in future, such as meshes.
The renderer does not currently know the concept of Z indexes, which are basically how layered rendering works. Instead, it just uses the order in which the Drawables are registered, which is not ideal. A vector of vectors could be used here, but a more robust system would be preferred.
The engine also does not have a way to save scenes as a markup-based file format, like Unity and Godot do. Instead, scenes are created directly as classes and their constructors are used to create the objects necessary, which is time-consuming and difficult for end-users. Because of how limiting this is for most people, creating a scene format that is read by the program would be a massive improvement.
GameplayScene.cpp
:
void GameplayScene::Init()
{
GameObject* background = CreateGameObject();
background->AddComponent<SpriteRenderer>();
background->GetComponent<SpriteRenderer>()->SetTexture(ResourceManager::GetTexture("assets/textures/background.png"));
background->GetComponent<SpriteRenderer>()->GetTexture()->setRepeated(true);
Vector2u size = Vector2u(background->GetComponent<SpriteRenderer>()->GetTexture()->getSize());
background->GetComponent<SpriteRenderer>()->SetTextureRect(sf::IntRect(sf::Vector2i(0,0), size * 1000000));
background->transform.SetPosition(Vector2f(-500, -500));
GameObject* player = CreateGameObject();
player->transform.SetPosition(Vector2f(20, 0));
RigidBody* rb = player->AddComponent<RigidBody>();
rb->damp = 2;
player->AddComponent<Player>();
player->AddComponent<CircleCollider>();
[...]
I will probably return to this project someday as having my own engine is a very exciting opportunity to create something unique. I’m just not sure what that is yet! Perhaps a visual scene editor or an entirely new scripting language? Maybe even something no-one has thought of!
Or just a Sinistar clone.