For the past week or two I have been working on a small library that aims to ease development of SFML games, and after finishing it I would love to get some feedback on it. I don't have access to any tutors so feedback is very valuable to me when trying to get better at coding. Thanks in advance to anyone who sets some time aside to go through my code.
Link to SFML if you're not familiar with it. It's a library for making simple 2D games (so yes my library is a library for a library).
The reason I started working on this little hobby project is because I was making a few games with SFML, and I noticed that main.cpp just got busier and busier, with more and more calls to handlers to update the logic and draw sprites to the window. I wanted to streamline the program flow so that it could be read almost like those program flow paradigms you see in game development books:
while (gameRunning)
{
handleInput();
updateLogic();
render();
}
What I came up with in the end is two abstract base classes, LogicHandler
and GraphicalHandler
, that split up all the handlers into logic and graphics. Every handler that handles game logic should be derived from LogicHandler
and every handler that handles graphics should be derived from GraphicalHandler
.
A handler in my code is an object that keeps track of a certain aspect of the game. Logic handlers keep track of game logic while graphical handlers keep track of graphical objects that can be drawn to the screen. If a handler needs updates from another handler, then it should store a pointer to that handler so it can always update itself using that pointer.
I wanted to be able to update all handlers with just a few method calls in the main game loop. To achieve this, LogicHandler
and GraphicalHandler
both utilize a technique where they store pointers to every single instance of their class, in a static vector called s_allInstances_
(s_ for static). This allows them to, in static methods, loop over every handler that is currently alive and call methods on them. LogicHandler
uses this to loop through all logic handlers and deliver events and update them. GraphicalHandler
uses this to loop through all graphical handlers and update them and draw their sprites to the screen.
This may sound confusing, so let me briefly explain how these classes use s_allInstances_
.
LogicHandler
consists of two static methods - handleEvent
, updateLogic
and two purely virtual methods - receiveEvent
and update
. Each virtual method corresponds to one of the static methods, you can probably see which belongs to which.
When LogicHandler::handleEvent
is called from the game loop when a player input is discovered, it loops through s_allInstances_
and calls receiveEvent
(and passes the event) on every single logic handler.
When LogicHandler::updateLogic
is called during every iteration of the game loop, it loops through s_allInstances_
and calls update
on every single logic handler.
This makes it so that no matter how many logic handlers exist, they can all be simultaneously updated or receive player input with just a single method call.
GraphicalHandler
has two static methods - updateGraphicalObjects
, render
and one purely virtual method - update
All graphical handlers inherit a vector of pointers to drawables (sprites, shapes etc) called graphicalObjects_
. Everything that a graphical handler wants displayed on the screen should be pushed onto graphicalObjects_
.
When GraphicalHandler::updateGraphicalObjects
is called, it loops through all graphical handlers and calls the update
method on them, which is when the handlers can update the sprites in their graphicalObjects_
vector, using pointers to logic handlers that they have stored as members.
When GraphicalHandler::render
is called, it loops through all graphical handlers and renders the sprites inside their graphicalObjects_
vectors.
Just as with the logic handlers, it doesn't matter how many graphical handlers exist, they can all be updated or rendered from with just one method call.
Combining LogicHandler
and GraphicalHandler
leaves you with a very straightforward game loop, similar to the paradigm I showed in the beginning:
while(window.isOpen())
{
sf::Event event;
while(window.pollEvent(event)
{
LogicHandler::handleEvent(event, window);
}
LogicHandler::updateLogic();
GraphicalHandler::updateGraphicalObjects();
GraphicalHandler::render();
}
With the general idea out of the way, let's get to the actual code.
Besides general review of the code, I would love to know what you think of the approach as a whole. Is this a good way of dealing with program flow, logic handling, and rendering? What are the cons of this system? Don't hold back, my feelings won't be hurt.
OBS: I also added a class and a main.cpp
at the end that use the library to make a little program where you control a square, just to show you how I intended for the code base to be used.
Finally, here's the code:
InstanceTracker.h
#pragma once
#include <vector>
template <typename T>
class InstanceTracker
{
public:
InstanceTracker() noexcept
{
s_allInstances_.push_back(static_cast<T*>(this));
}
InstanceTracker(const InstanceTracker& source) noexcept
: InstanceTracker()
{
}
InstanceTracker(const InstanceTracker&& source) noexcept
: InstanceTracker()
{
}
virtual ~InstanceTracker() noexcept
{
auto it = std::find(s_allInstances_.begin(), s_allInstances_.end(), this);
int index = it - s_allInstances_.begin();
s_allInstances_.erase(s_allInstances_.begin() + index);
}
void moveMyInstanceToLast()
{
auto it = std::find(s_allInstances_.begin(), s_allInstances_.end(), this);
std::rotate(it, it + 1, s_allInstances_.end());
}
void moveMyInstanceToFirst()
{
auto it = std::find(s_allInstances_.begin(), s_allInstances_.end(), this);
s_allInstances_.erase(it);
s_allInstances_.insert(s_allInstances_.begin(), static_cast<T*>(this));
}
protected:
static std::vector<T*> s_allInstances_;
};
template<typename T>
std::vector<T*> InstanceTracker<T>::s_allInstances_;
LogicHandler.h
#pragma once
#include "InstanceTracker.h"
#include "SFML/Graphics.hpp"
#include <vector>
class LogicHandler : public InstanceTracker<LogicHandler>
{
public:
virtual bool receiveEvent(const sf::Event& event, const sf::RenderWindow& window) = 0;
/* The reason receiveEvent() returns a boolean is to avoid unnecessary looping
* in handleEvent(). If a handler discovers an event that definitely only applies
* to that handler, then it should return true from receiveEvent().
* handleEvent() will then stop looping when it encounters it, thus avoiding a lot of looping.
* To take advantage of this, handlers should be declared in order of how common the events
* they're listening for are. E.g., a handler that listens for the arrow keys being pressed
* to move the player should be declared earlier than a handler that listens for a button being
* pressed, since the arrow keys input will be much more common.
*/
virtual void update() = 0;
static void updateLogic();
static void handleEvent(const sf::Event& event, const sf::RenderWindow& window);
};
LogicHandler.cpp
#include "LogicHandler.h"
void LogicHandler::updateLogic()
{
for (LogicHandler* handler : s_allInstances_)
{
handler->update();
}
}
void LogicHandler::handleEvent(const sf::Event& event, const sf::RenderWindow& window)
{
for (LogicHandler* handler : s_allInstances_)
{
if(handler->receiveEvent(event, window))
{
break;
}
}
}
PositionHandler.h
//Implementation in the header file for such a simple class
#pragma once
#include "LogicHandler.h"
class PositionHandler : public LogicHandler
{
public:
PositionHandler() { position_ = sf::Vector2f(0, 0); rotation_ = 0; }
sf::Vector2f getPosition() const { return position_; }
float getRotation() const { return rotation_; }
protected:
sf::Vector2f position_;
float rotation_;
};
Button.h
#pragma once
#include "LogicHandler.h"
class Button : public LogicHandler
{
public:
Button(sf::Vector2f size, sf::Vector2f position = sf::Vector2f(0.f, 0.f));
virtual bool receiveEvent(const sf::Event& event, const sf::RenderWindow& window) override;
virtual void buttonPressed() = 0;
protected:
sf::Rect<float> hitbox_;
};
Button.cpp
#include "Button.h"
Button::Button(sf::Vector2f size, sf::Vector2f position)
{
hitbox_.width = size.x;
hitbox_.height = size.y;
hitbox_.left = position.x;
hitbox_.top = position.y;
}
bool Button::receiveEvent(const sf::Event& event, const sf::RenderWindow& window)
{
if (event.type == sf::Event::MouseButtonPressed && hitbox_.contains(sf::Vector2f(sf::Mouse::getPosition(window))))
{
buttonPressed();
return true;
}
return false;
}
PauseButton.h
#pragma once
#include "Button.h"
class PauseButton : public Button
{
friend class PauseButtonIcon;
friend class GameEngine;
public:
PauseButton(sf::Vector2f size, sf::Vector2f position);
void update() override;
void buttonPressed() override;
private:
bool bPaused_;
};
PauseButton.cpp
#include "PauseButton.h"
PauseButton::PauseButton(sf::Vector2f size, sf::Vector2f position)
: Button(size, position), bPaused_(false)
{
}
void PauseButton::buttonPressed()
{
bPaused_ = bPaused_ ? false : true;
}
void PauseButton::update()
{
}
GraphicalHandler.h
#pragma once
#include "InstanceTracker.h"
#include "ClonableDrawable.h"
#include "LogicHandler.h"
#include <SFML/Graphics.hpp>
#include <vector>
#include <memory>
class GraphicalHandler : public InstanceTracker<GraphicalHandler>
{
public:
GraphicalHandler();
GraphicalHandler(const GraphicalHandler& source);
virtual void update() = 0;
static void updateGraphicals();
static void renderObjects(sf::RenderWindow& window);
protected:
std::vector<std::shared_ptr<ClonableDrawable>> graphicalObjects_;
bool bVisible_;
};
GraphicalHandler.cpp
#include "GraphicalHandler.h"
GraphicalHandler::GraphicalHandler()
: bVisible_(true)
{
}
GraphicalHandler::GraphicalHandler(const GraphicalHandler& source)
: bVisible_(source.bVisible_), InstanceTracker<GraphicalHandler>(source)
{
for (std::shared_ptr<ClonableDrawable> ptr : source.graphicalObjects_)
{
graphicalObjects_.push_back(ptr->clone());
}
}
void GraphicalHandler::updateGraphicals()
{
for (GraphicalHandler* handler : s_allInstances_)
{
handler->update();
}
}
void GraphicalHandler::renderObjects(sf::RenderWindow& window)
{
for (GraphicalHandler* handler : s_allInstances_)
{
if (handler->bVisible_)
{
for (std::shared_ptr<ClonableDrawable> object : handler->graphicalObjects_)
{
window.draw(*object);
}
}
}
}
ClonableDrawable.h
/* The reason ClonableDrawable exists is because graphical handlers cannot be copied if they can't copy
the contents of their vector of drawables. `sf::Drawable` is abstract
so to be able to copy them I added these two classes below. */
#pragma once
#include "SFML/Graphics.hpp"
#include <memory>
class ClonableDrawable
{
public:
virtual ~ClonableDrawable() = default;
virtual std::unique_ptr<ClonableDrawable> clone() const = 0;
virtual operator sf::Drawable& () = 0;
};
template <typename T>
class ClonableDraw : public T, public ClonableDrawable
{
public:
ClonableDraw() = default;
template<typename... Args>
ClonableDraw(Args&... args): T(args...) {}
template<typename... Args>
ClonableDraw(Args&&... args): T(args...) {}
std::unique_ptr<ClonableDrawable> clone() const override
{
return std::make_unique<ClonableDraw<T>>(*this);
}
operator sf::Drawable& () override { return *this; }
};
PauseButtonIcon.h
#pragma once
#include "GraphicalHandler.h"
#include "PauseButton.h"
#include "SFML/Graphics.hpp"
class PauseButtonIcon : public GraphicalHandler
{
public:
PauseButtonIcon(std::shared_ptr<PauseButton> handler, sf::Color color);
void update() override;
private:
std::shared_ptr<PauseButton> handler_;
std::shared_ptr<ClonableDraw<sf::VertexArray>> playIcon_;
std::shared_ptr<ClonableDraw<sf::RectangleShape>> pauseIcon1_;
std::shared_ptr<ClonableDraw<sf::RectangleShape>> pauseIcon2_;
};
PauseButtonIcon.cpp
#include "PauseButtonIcon.h"
PauseButtonIcon::PauseButtonIcon(std::shared_ptr<PauseButton> handler, sf::Color color)
: handler_(handler)
{
playIcon_ = std::make_shared<ClonableDraw<sf::VertexArray>>(sf::Triangles, 3);
(*playIcon_)[0].position = sf::Vector2f(handler->hitbox_.left, handler->hitbox_.top);
(*playIcon_)[1].position = sf::Vector2f(handler->hitbox_.left, handler->hitbox_.top+handler_->hitbox_.height);
(*playIcon_)[2].position = sf::Vector2f(handler->hitbox_.left+handler_->hitbox_.width, handler->hitbox_.top+handler_->hitbox_.width/2);
(*playIcon_)[0].color = color;
(*playIcon_)[1].color = color;
(*playIcon_)[2].color = color;
pauseIcon1_ = std::make_shared<ClonableDraw<sf::RectangleShape>>(sf::Vector2f(handler_->hitbox_.width / 3, handler_->hitbox_.height));
pauseIcon1_->setPosition(handler_->hitbox_.left, handler_->hitbox_.top);
pauseIcon1_->setFillColor(color);
pauseIcon2_ = std::make_shared<ClonableDraw<sf::RectangleShape>>(sf::Vector2f(handler_->hitbox_.width / 3, handler_->hitbox_.height));
pauseIcon2_->setFillColor(color);
pauseIcon2_->setPosition(handler_->hitbox_.left + 2*pauseIcon1_->getSize().x, handler_->hitbox_.top);
}
void PauseButtonIcon::update()
{
if (handler_->bPaused_ && graphicalObjects_.size() != 1)
{
graphicalObjects_.clear();
graphicalObjects_.push_back(playIcon_);
}
else if (!handler_->bPaused_ && graphicalObjects_.size() != 2)
{
graphicalObjects_.clear();
graphicalObjects_.push_back(pauseIcon1_);
graphicalObjects_.push_back(pauseIcon2_);
}
}
SimpleGraphical.h
/* SimpleGraphical can be used when you don't need a very complicated graphical handler */
#include "GraphicalHandler.h"
#include "PositionHandler.h"
template<typename T>
class SimpleGraphical : public GraphicalHandler
{
public:
template<typename... Args>
SimpleGraphical(Args... args)
: graphical_(std::make_shared<ClonableDraw<T>>(args...))
{
graphicalObjects_.push_back(graphical_);
}
void setVisible(bool bVisible) { bVisible_ = bVisible; }
void setHandler(std::shared_ptr<PositionHandler> handler) { handler_ = handler; }
std::shared_ptr<ClonableDraw<T>> get() { return graphical_; }
void update() override
{
if (handler_ != nullptr)
{
graphical_->setPosition(handler_->getPosition());
graphical_->setRotation(handler_->getRotation());
}
}
private:
std::shared_ptr<ClonableDraw<T>> graphical_;
std::shared_ptr<PositionHandler> handler_;
};
GameEngine.h
#pragma once
#include "GraphicalHandler.h"
#include "SimpleGraphical.h"
#include "PauseButton.h"
#include "PauseButtonIcon.h"
#include "SFML/Graphics.hpp"
#include <memory>
class GameEngine
{
public:
GameEngine
(
std::string&& gameTitle = "New Window",
int windowWidth = 800,
int windowHeight = 800,
sf::Color backgroundColor = sf::Color(sf::Color::White)
);
void run();
private:
bool bPaused_;
std::unique_ptr<sf::RenderWindow> window_;
std::shared_ptr<PauseButton> pauseButton_;
std::unique_ptr<PauseButtonIcon> pauseButtonIcon_;
std::unique_ptr<SimpleGraphical<sf::RectangleShape>> pauseOverlay_;
sf::Color windowBackgroundColor_;
};
GameEngine.cpp
#include "GameEngine.h"
GameEngine::GameEngine(std::string&& title, int width, int height, sf::Color backgroundColor)
: bPaused_(false), windowBackgroundColor_(backgroundColor)
{
window_ = std::make_unique<sf::RenderWindow>(sf::VideoMode(width, height), title);
pauseButton_ = std::make_shared<PauseButton>(sf::Vector2f(50.f, 50.f), sf::Vector2f(float(width - 60) , 10.f));
pauseButtonIcon_ = std::make_unique<PauseButtonIcon>(pauseButton_, sf::Color::White);
pauseOverlay_ = std::make_unique<SimpleGraphical<sf::RectangleShape>>(sf::Vector2f((float)width, (float)height));
pauseOverlay_->get()->setFillColor(sf::Color(0, 0, 0, 170));
pauseOverlay_->setVisible(false);
}
void GameEngine::run()
{
pauseButton_->moveMyInstanceToLast();
pauseOverlay_->moveMyInstanceToLast();
pauseButtonIcon_->moveMyInstanceToLast();
while (window_->isOpen())
{
sf::Event event;
while (window_->pollEvent(event))
{
if (event.type == sf::Event::Closed)
{
window_->close();
break;
}
if (!bPaused_)
{
LogicHandler::handleEvent(event, *window_);
}
else
{
pauseButton_->receiveEvent(event, *window_);
pauseButtonIcon_->update();
}
}
if (!bPaused_)
{
LogicHandler::updateLogic();
GraphicalHandler::updateGraphicals();
}
bPaused_ = pauseButton_->bPaused_;
pauseOverlay_->setVisible(bPaused_);
window_->clear(windowBackgroundColor_);
GraphicalHandler::renderObjects(*window_);
window_->display();
}
}
That's it for the actual library but as stated above here's a simple little program that uses the library to move around a square with the arrow keys or WASD. I used SimpleGraphical
for this, since the square doesn't need to do anything more complicated than just moving to a position during every iteration.
ControllablePlayer.h
#pragma once
#include "PositionHandler.h"
class ControllablePlayer : public PositionHandler
{
public:
bool receiveEvent(const sf::Event& event, const sf::RenderWindow& window) override;
void update() override;
};
ControllablePlayer.cpp
#include "ControllablePlayer.h"
bool ControllablePlayer::receiveEvent(const sf::Event& event, const sf::RenderWindow& window)
{
if (event.type == sf::Event::KeyPressed)
{
switch (event.key.code)
{
case sf::Keyboard::Left:
case sf::Keyboard::A:
position_.x -= 20.f;
return true;
case sf::Keyboard::Right:
case sf::Keyboard::D:
position_.x += 20.f;
return true;
case sf::Keyboard::Down:
case sf::Keyboard::S:
position_.y += 20.f;
return true;
case sf::Keyboard::Up:
case sf::Keyboard::W:
position_.y -= 20.f;
return true;
}
}
return false;
}
void ControllablePlayer::update()
{
}
main.cpp
#include "GameEngine.h"
#include "ControllablePlayer.h"
#include <SFML/Graphics.hpp>
#include <memory>
int main()
{
GameEngine engine{"New", 800, 800, sf::Color::Red};
auto rectHandler = std::make_shared<ControllablePlayer>();
SimpleGraphical<sf::RectangleShape> rect(sf::Vector2f(100.f, 100.f));
rect.get()->setFillColor(sf::Color::Green);
rect.setHandler(rectHandler);
engine.run();
return 0;
}