Hello everyone,
I would like to share a framework i am currently developing which is dedicated for 2D game development
updated:
PreambleThe story began when i wanted to develop a game like
Secret of Grindea. great game by the way :3
As you may definitely noticed, when starting up a game for the first time, at a certain point, your code starts to become heavy in a way that, it is not flexible anymore. It works only for what you've made and any future changes, or adding new ideas requires major changes in core code.
W're not talking about adding a new enemy or changing hero base health.
This is why it is
necessary to structure your code, and create some base classes to manage your game, before even creating your first
asset.
I was inspired from several references, such as the
SFML Game Development ebook from packtpub. It was very good to start with, but does not have a flexible structuring of code.
What i mean by flexible structuring of code, is that, The game may grasp almost any kind of new concept/idea without having to make changes in core code. (or at least very few changes)
I've also tested UDK and Unity (documentation), which they helped me a LOT to learn more about organizing things together. Especially the GameObject/Component pattern.
The GameObject/Component design makes your game entities INCREDIBLY flexible.
Will be discussed laterPresentationGameDevFramwork (GDF) is an open-source C++ framework for 2D Game development extending:
- The SFML library 2.3.2
- Box2D Library 2.3.1: for physics
- Qt 5.7.0 : used for the meta-object system ( i will find a new non-qt replacement for that )
- and more ...
More .... ? Yes... The purpose of the project is to build a totally modular open-source framework, by providing a set of base classes ( kernel module ) to make it possible to accept and easily integrate newly created modules.
The Kernel moduleThe kernel is a minimum set of classes that implements the core of the framework,
Here is the class diagram of the kernel
starting by:
- GameInfo: Class that defines the application logic, how to initialize, handle events, update and draw the object.
- Scene: The scene class provides a way for creating and organizing items(GameObjects) in a tree structure.
- GameObject: GameObjects are the main items for scene's nodes, every thing on the scene is a GameObject. However, GameObjects have no concrete representation on the scene, they are nothing but containers for Components
- Component: A component is an object that defines a specific logic and is attached to certain object ( such as GameObjects ),
- HierarchicalContainer: This class provides a multi-root tree structure in order to organize Components. And any class that inherits from this class becomes a container for Components, as it is for GameObject, Scene and GameInfo
- Transform: A Transform is a component that provides a GameObject with a 2D Coordinate system (position, rotation, scale) and, organize it in a Tree-Structure ( Parent/Children)
The GameObject/Component pattern allows the creation of a meaningless GameObject. And its context is defined by the logic carried by its components. Components can be added and removed at will.
For exampleIt can be seen as, equipping your hero (GameObject) with different weapons and skills (Component).
If you give your hero a
sword, a
helmet and a
shield, then you've made a
Warrior. But if you give him a
staff, and a
spellbook , then you've chosen to be a
Sorcerer, despite it is the same GameObject.
GameObject* hero = GameObject::instantiate();
// Choosing to be a Warrior
hero->addComponent<Sword>();
hero->addComponent<Shield>();
hero->addComponent<Helmet>();
// Choosing to be a Sorcerer
hero->addComponent<Staff>();
hero->addComponent<Spellbook>();
// or ...
Spellbook* spell_book = hero->addComponent<Spellbook>();
But....What if i want my
Sorcerer to have not more than one
Staff and one
Spellbook. And cannot have a
Staff without wearing a
Cape ?
Well, thanks to the
KernelRules class that defines a set of rules ( dependency relationship between components ) in order to check the satisfiability of the add and the remove actions.
In other way
rule: X → A, B, C
A Component X cannot be added inside GameObject G if A, B, C Components are not available within G
vice-versa
A Component A cannot be removed if one or more other Components depends on it. ( herein, X )
Example-Code make_singleton("Staff");
make_singleton("Spellbook");
create_rule("Staff", "Cape");
make_singleton creates a cardinality rules, imposing the (HierarchicalContainer) or GameObject to only accept one instance of a given Component-type. Herein, "Staff", "Spellbook" are limited to one instance each.
AS for, create_rule method, it creates a dependency relationship between
Staff and
Cape, telling that,
Staff depends on/
requires Cape to be available first in order to be created..
There are pre-defined rules applied for built-in components in order to ensure the good functioning of the kernel module
By taking advantage of these methods, and of the generic implementation of the GameInfo,
any kind of component can be attached to the GameObject.
Herein above, one elementary cycle of the gameloop.
Based on
timestep update. Each elementary cycle, the GameInfo
- Handles application events
- Performs regular updates of the Scene
- Performs physics-related updates of the Scene
- Performs (late) update of the Scene
- Then proceed to rendering.
Minimal example-codeTo start in with the framework,
You must create a sub-class of
GameInfo and
Scene. ( And also override pure methods )
Note that, i am not using too much 'getters' in my code, but accessing data members directly, this is why some instruction are too long.
TestCaseGameInfo.h#ifndef TESTCASEGAMEINFO_H
#define TESTCASEGAMEINFO_H
#include "kernel/gameinfo.h"
using namespace gdf::kernel;
class TestCaseGameInfo : public gdf::kernel::GameInfo
{
// GameInfo already define a Logic for updates/drawing and does not require to override them
public:
TestCaseGameInfo(const sf::Vector2i& size, std::string title);
~TestCaseGameInfo();
protected:
void on_init() override;
void on_event(const sf::Event &event) override;
};
#endif // TESTCASEGAMEINFO_H
TestCaseGameInfo.cpp#include "testcasegameinfo.h"
#include "Assets/TestCases/testcasescene.h"
#include "resource_management/resourcemanager.h"
#include "kernel/garbagecollector.h"
#include "time_management/chrono.h"
TestCaseGameInfo::TestCaseGameInfo(const sf::Vector2i& size, std::string title)
:GameInfo(size, title)
{
}
TestCaseGameInfo::~TestCaseGameInfo(){
}
void TestCaseGameInfo::on_init(){
// Add Components to the GameInfo
// Chrono: Like Trasnform allows an Object to integrate into a 2D Coordinate system,
// Chrono Is a component that gives an Object ([i]HierarchicalContainer[/i]) time concepts, allowing it to be a part of a "TimeLine" ( [i]lifetime[/i], [i]spawn_time[/i] .... )
addComponent<Chrono>();
// A ResourceManager manages all the resource of the GameInfo
addComponent<ResourceManager>();
// A GarbageCollector is used to destroy in a safe way objects ( GameObject & Components )
addComponent<GarbageCollector>();
// Creates a scene
Scene* t1 = new TestCaseScene();
// Add t1 to the scene container
scenes_["TSpawn"] = t1;
// Set TSpawn as active scene
set_active_scene( "TSpawn" );
}
void TestCaseGameInfo::on_event(const sf::Event &event){
}
TestCaseScene.h#ifndef TESTCASESCENE_H
#define TESTCASESCENE_H
// c++-includes
// sfml-includes
// box2d-includes
// qt-includes
// user-includes
#include "kernel/scene.h"
class TestCaseScene : public gdf::kernel::Scene
{
public:
TestCaseScene();
~TestCaseScene();
public:
void on_init();
void load_resources();
void build();
void on_event(const sf::Event& event);
};
#endif // TESTCASESCENE_H
TestCaseScene.cpp#include "testcasescene.h"
#include "kernel/gameinfo.h"
#include "kernel/gameobject.h"
#include "Core/System/camera.h"
#include "Core/System/monobehavior.h"
#include "Core/System/transform.h"
#include "Core/Graphic/spriterenderer.h"
#include "resource_management/texture2d.h"
#include "time_management/chrono.h"
TestCaseScene::TestCaseScene()
{
}
TestCaseScene::~TestCaseScene(){
}
// Initializing your scene
void TestCaseScene::on_init(){
// Add time concept for the scene
addComponent<Chrono>();
}
// Loading all your resources
void TestCaseScene::load_resources(){
// Loading Resources ( Texture2D & SpriteTile )
Texture2D* a = GameInfo::game_info.get()->getComponent<ResourceManager>()->load_resource<Texture2D>(4, true, "Cockatrice");
a->init("./Cockatrice.png");
Texture2D* a2 = GameInfo::game_info.get()->getComponent<ResourceManager>()->load_resource<Texture2D>(2, true, "Cockatrice2");
a2->init("./Cockatrice.png");
SpriteTile* s = GameInfo::game_info.get()->getComponent<ResourceManager>()->load_resource<SpriteTile>(3, true, "Flash");
s->init("./Flash_2.png", 3, 5 );
}
// Building your Scene
void TestCaseScene::build(){
//Create a Camera on the root GameObject
Camera* c1 = root_->addComponent<Camera>(&cameras_);
c1->setViewportPosition( sf::Vector2f(100, 100) );
c1->setCameraLocation( sf::Vector2f(20, 10) );
//------------------------------------------------------------------------------------
// Create a GameObject: g0 @position (160, 120)
GameObject* g0 = GameObject::instantiate("g0", sf::Vector2f(160, 120) );
Chrono* ch = g0->addComponent<Chrono>();
// Add another component that makes the game object move using keyboard.
g0->addComponent<MoveObject>(); // make the object move
// Create a SpriteRendeer to display some graphics
SpriteRenderer* ssprt = g0->addComponent<SpriteRenderer>();
// Load the resource from the ResourceManager
ssprt->sprite.setTexture2D( GameInfo::game_info.get()->getComponent<ResourceManager>()->get<Texture2D>( 4 ) );
//------------------------------------------------------------------------------------
// Create a second GameObject: g1 as a child of g0
GameObject* g1 = GameObject::instantiate(g0, "g1");
// Create a SpriteRendeer and load a resource on it
SpriteRenderer* sa = g1->addComponent< SpriteRenderer >();
sa->sprite.setTexture2D( GameInfo::game_info.get()->getComponent<ResourceManager>()->get<Texture2D>(4) );
// Move the game object by (40, 20 )
g1->transform()->translate( sf::Vector2f( 40 ,20) );
// Destroy test: Destroy g1 after 2420 milliseconds ( Managed by the GarbageCollector )
GameObject::destroy(g1, 2420);
}
void TestCaseScene::on_event(const sf::Event& event){
if( event.type == sf::Event::Closed ){
GameInfo::game_info.get()->close();
}
}
Adding a new Component: the pre-declared MoveObjectg0 had a component called "MoveObject" that makes it move in the scene. i.e
- MoveObject is retrieving the Transform Component of g0 in order to apply a translate.
- MoveObject requires Transform: i.e:
create_rule("MoveObject", "Transform");
- must be defined in KernelRules
- MoveObject is systematically initialized, updated and drawn if necessary thanks to the GameInfo & Scene.
Here is the implementation of
MoveObjectMoveObject.h#ifndef MOVEOBJECT_H
#define MOVEOBJECT_H
#include <SFML/System/Vector2.hpp>
#include "Core/System/monobehavior.h"
class MoveObject : public MonoBehavior
{
Q_OBJECT
public:
MoveObject();
void init()();
void update(sf::Time dt);
};
#endif // MOVEOBJECT_H
MoveObject.cpp#include "moveobject.h"
#include "Core/System/transform.h"
MoveObject::MoveObject()
{
}
void MoveObject::init(){
}
void MoveObject::update(sf::Time dt){
// Safety test, if the Component is really attached
if( game_object() == nullptr )
return;
float speed_per_sec = 300.f;
sf::Vector2f velocity;
// Using real-time inputs
if( sf::Keyboard::isKeyPressed(sf::Keyboard::Left) ){
velocity.x = (-speed_per_sec/1000.f/1000.f) * dt.asMicroseconds();
}else if( sf::Keyboard::isKeyPressed(sf::Keyboard::Right) ){
velocity.x = (+speed_per_sec/1000/1000) * dt.asMicroseconds();
}
if( sf::Keyboard::isKeyPressed(sf::Keyboard::Up) ){
velocity.y = (-speed_per_sec/1000/1000) * dt.asMicroseconds();
}else if( sf::Keyboard::isKeyPressed(sf::Keyboard::Down) ){
velocity.y = (+speed_per_sec/1000/1000) * dt.asMicroseconds();
}
// Move the GameObject ( by moving its Transform )
game_object()->transform()->translate( sf::Vector2f( velocity.x , velocity.y ) );
}
}
A MonoBehavior is a Component with more features than a regular Component, it is the base class for user-components
Note that a GameObject cannot be overriden.
Built-in ComponentsIn addition to the kernel module, there are some built-in components used in order to create basic objects, like:
Graphics, Sounds, Animations & animator, Shaders, Physics , Colliders, Joints, Gui, Renderers, AudioListener, Cameraetc.
Modules... as addonsCreating new modules and integrating them to the framework is very simple.
What needs to be done is to
- Define your set of Component-classes
- Attaches them to the right Container
And they'll be fully functioning. ( as long as Component's methods are overridden and used )
I've made aside some external modules to implement new functionalities for the framework.
resource management moduleResource management module offers a set of classes in order to load, use and unload resource automatically avoiding any kind of memory leak.
- Resource Manager: The resource manager keeps track of all loaded resources, and manages their lifetime. i.e Allocating when it is required, and deallocating resource whenever it's not needed
- Resource is the base class for all resource.
The resource manager unloads resource in a safe manner, means that, if a resource is still being used by any object, it cannot be unloaded. This is achieved using smart pointers ( the
use_count member of
std::shared_ptr ) . See, documentation for implementation details
time management moduleThe time management module define concepts about time.
the main classes of this module are:
- Chrono: Is a component that gives an object time properties, and become part of a timeline. ( spawn_time, lifetime etc )
- TimeKeeper: Is a GameObject's component that records the timeline of a GameObject, i.e recording the past of the GameObject. This technique is Events-triggered rather than Time-Events ( Continuous recording )
- TimeWinderThe TimeWinder is the core Component of this module, it manipulates the time of any GameObject. Time freeze, time reversal, normal play with a play factor.
- Event Is a base class for time related events, used by the timekeeper to record the timeline, they define the type of the event it happened, the target object, the previous value before the action happened, and the new one
exec .... When the TimeWinder is launched ( eventually from the scene ), it retrieves all the TimeKeeper Components of all GameObjects. And plays the recorded timeline of each GameObject backwards, by generating an opposite event of the one initially recorded. Thus, simulating a backward execution.
However, any property that needs to be affected by time must have a dedicated event and must be called at the right place.
Example:- VelocityEvent: Stores velocity data ( old, new, target, timestamp )
- TranslationEvent: Stores translation data ( old, new, target, timestamp )
- ValueChangedEvent: Stores changes in value ( old, new, target, timestamp )
- ....
In the diagram above, it displays the stack's content of the timekeeper at time = ti. The stack is divided in two part:
Events from the past, and events from the future
On a normal play, the timekeeper registers time-events in the past part of the stack, as shown on
'normal play' case
When entering a time manipulation session. The timekeeper stops recording events.
-
ON BACKWARD PLAY - rewind: The timekeeper pops events one by one from the past, and pushes them onto future.
-
ON FORWARD PLAY: The timekeeper pops events one by one from the future, and pushes them onto the past.
This allows to go back and forth in time at will,
When the session ends, events from the future are discarded, and time keeper starts to record new events.
Similar mechanisms of time manipulation can be found in the game Braid, steam link:
http://store.steampowered.com/app/26800/.
And so on .... As future tasks, I am willing to add and/or integrate already built modules to the framework
- IA Algorithm: Search algorithm, Dijkstra, A* etc.
- Complex Systems algorithms: Artificial Neural Network, Celullar Automaton, Swarm intelligence etc
- Parallel programming plateform - open-mpi: Can be used in combination with IA & Complex systems.
- Interact with an Arduino Board: Makes it possible to communicate with an Arduino-board through the application,
As a sample, Making a LED blink whenever a GameObject leaves or enter the Viewport of the Camera .
simply by using the Renderer's callback functions 'onBecomeVisible' and onBecomeInvisible', to send a message through the serial port to the arduino. See, ArduinoTest in source code
ExamplesThere is not a lot of things to display right now, but i can show you a sample of the
time management module.
( Time Reversal mechanisms )c.f Attached video. The video shows
Time Reversal mechanism. The scene contains 2 (Chicken=Cockatrice) GameObjects ( c.f RPG Maker Sprite ) spawned at the same location.
One Cockatrice does not move, while the other is moving under constant velocity ( x-axis only ) for a short period of time ..
- Whenever the user presses the left mouse's button, it teleports the cockatrice to the cursor position.
- When Left Shift is button is held, time reversal starts, on release, time goes back to normal.
Scenario:- The Cockatrice starts moving positive x-axis due to pre-applied velocity. Then teleports several time to cursor position ( while it keeps moving )
- After a short period of time, the Cockatrice goes to idle state ( stops moving )
- [Time Reversal is triggered at this moment- LeftShift pressed]
- The TimeWinder is executed causing the Cockatrice to play its TimeLine backward. Until its lifetime reaches 0. Thus, its state will be the same as it was on t=0.
- When time reversal is canceled - Left Shift released
- The Cockatrice will start to move again as it is its first time
Content of the Log windowWhat is displayed at the terminal is:
- Current applied rules of the KernelRules class
-------------------------------------------
#Singletons:
ArduinoTest 1
Chrono 1
GarbageCollector 1
ResourceManager 1
TimeKeeper 1
TimeWinder 1
Transform 1
#Requirements:
ArduinoTest 'requires' Transform,
GarbageCollector 'requires' Chrono,
TimeKeeper 'requires' Chrono,
VelocityDef 'requires' Transform,
-------------------------------------------
→ means → : The forum cant draw it ?!! ( or may be it is inside 'code' block )
- Added Components for each Object
+Chrono → TestCaseGameInfo::components
+ResourceManager → TestCaseGameInfo::components
+gdf::kernel::GarbageCollector→ TestCaseGameInfo::components
+Transform → gdf::kernel::GameObject::components
+Chrono → TestCaseScene::components
+TimeWinder → TestCaseScene::components
+gdf::kernel::GarbageCollector→ TestCaseScene::components
+Camera → gdf::kernel::GameObject::components
+Transform → gdf::kernel::GameObject::components
+Chrono → gdf::kernel::GameObject::components
+TimeKeeper → gdf::kernel::GameObject::components
+ArduinoTest → gdf::kernel::GameObject::components
+MoveObject → gdf::kernel::GameObject::components
+VelocityDef → gdf::kernel::GameObject::components
+SimulatorComponent → gdf::kernel::GameObject::components
+SpriteRenderer → gdf::kernel::GameObject::components
+Transform → gdf::kernel::GameObject::components
+SpriteRenderer → gdf::kernel::GameObject::components
- Content of the Resource Manager (loaded resource)
--------------------------------------------------------------------------------
Resource manager content:
Texture2D Name: Cockatrice2 id= 2 shared = true count = 1
SpriteTile Name: Flash id= 3 shared = true count = 1
Texture2D Name: Cockatrice id= 4 shared = true count = 3
--------------------------------------------------------------------------------
- Event recording ( Normal Play )
Event: < Timestamp, Event-Type, old value, new value >
Request: GameObject::destroy(g0) @ 0 ms
1000 ms +Velocity: old( 0, 0) → new(100, 0)
1000 ms +ValueChanged: old( 0) → new( 1)
2271 ms +Translation: old(287, 120) → new(268, 273)
2559 ms +Translation: old(296, 273) → new(234, 286)
2815 ms +Translation: old(259, 286) → new(234, 286)
3055 ms +Translation: old(258, 286) → new(234, 286)
3327 ms +Translation: old(261, 286) → new(234, 286)
6000 ms +Velocity: old(100, 0) → new( 0, 0)
6000 ms +ValueChanged: old( 1) → new( 2)
- On Time reversal ( Backward Play )
Creates a snapshot event to register the last state of the GameObject
# REWINDING: Saving the current status
8475 ms +Velocity: old( 0, 0) → new( 0, 0)
Display the content of the TimeKeeper's Stack
------------------------------------------------------------
# Content of the TimeKeeper:
# Events of the Past
8475 ms +Velocity: old( 0, 0) → new( 0, 0)
6000 ms +ValueChanged: old( 1) → new( 2)
6000 ms +Velocity: old(100, 0) → new( 0, 0)
3327 ms +Translation: old(261, 286) → new(234, 286)
3055 ms +Translation: old(258, 286) → new(234, 286)
2815 ms +Translation: old(259, 286) → new(234, 286)
2559 ms +Translation: old(296, 273) → new(234, 286)
2271 ms +Translation: old(287, 120) → new(268, 273)
1000 ms +ValueChanged: old( 0) → new( 1)
1000 ms +Velocity: old( 0, 0) → new(100, 0)
# Events of the future
------------------------------------------------------------
Starts reverse execution of the Stack
------------------------------------------------------------
8475 ms +Velocity: old( 0, 0) → new( 0, 0)
6000 ms +ValueChanged: old( 1) → new( 2)
6000 ms +Velocity: old(100, 0) → new( 0, 0)
3327 ms +Translation: old(261, 286) → new(234, 286)
3055 ms +Translation: old(258, 286) → new(234, 286)
2815 ms +Translation: old(259, 286) → new(234, 286)
2559 ms +Translation: old(296, 273) → new(234, 286)
2271 ms +Translation: old(287, 120) → new(268, 273)
1000 ms +ValueChanged: old( 0) → new( 1)
1000 ms +Velocity: old( 0, 0) → new(100, 0)
- Stops the time reversal ( Left Shift released )
Clear the Stack and starts recording new events
-----------------------------------------------------------------
# GamePlay back to normal
1000 ms +Velocity: old( 0, 0) → new(100, 0)
1000 ms +ValueChanged: old( 0) → new( 1)
You shall pass !
ContributionThe framework is designed to be fully open-source and community repo,
I designed a minimal kernel to be used as a starting point for anyone who wants to develop modules from scratch.
However, built-in modules can be included at will depending on the type of the developed module.
You are welcome to:
- Suggest Code improvements ( as i'm writing very basic things right now to get a global architecture, then i'll refine every class ) [Note that kernel is refined but incomplete (still in progress) ]
- Suggest modifications about the kernel or an existing module.
- Suggest your own modules. even if there is an already existing one,
There is no better code. but one can fit better than the other, depending on the problem - Platform portability, { currently tested on Ubuntu 16.04. }
But since Qt & SFML & Box2D are multi-platform, there should not be a problem.(not tested on other platforms )
Thank you for your time,
And feel free to contact me or give your opinion, your feedback is welcome.
Cordially
Technical specification ( reminder )