Getting started with xygine - Part 1
Introduction
xygine is a small game framework based around SFML that I have developed over the past few years to create 2D games (examples of which can be found here and here), which runs on Windows, linux and macOS. It has become mature enough now, I think, that it’s time to attempt to document the process of creating a small breakout style game, in order to (hopefully) better explain xygine’s features and the functionality it provides. If you’re reading this I’m going to assume some knowledge on your part of C++ as well as potentially SFML, and that you are here because you want a shortcut to skip writing much of the boilerplate code required for creating a game in favour of diving right in. This is not a beginner's tutorial in gamedev, rather an exploration of what xygine has to offer. Let’s begin!Getting started
xygine is a fully open-source project, licensed under the zlib license. The best way to get started is to clone the repository available on github, and build it yourself. The repository can be found at https://github.com/fallahn/xygine and the build instructions are on the wiki: https://github.com/fallahn/xygine/wiki/BuildingOnce you have configured, built and installed xygine for your platform of choice, it’s time to create your first project! The repository contains a folder named
cmake_template
. This folder has all the files necessary to start a new project,
including a default CMake file. Create a copy of this directory and rename it to
something else such as ‘breakout’. If you want to use the included CMake file then open
that in your text editor of choice and edit the PROJECT_NAME variable to something more
relevant too. If, like me, you prefer using an IDE you can open the CMake file directly
if your IDE has support (such as Visual Studio 2017) or create a new project, adding
the source and header files included in the template folder, and linking to the SFML
and xygine libraries as appropriate (eg the correct debug/release versions). If you are
taking the CMake route then the src and include subdirectories both contain a
CMakeLists.txt file, which will need to be updated when you add any new source files to
your project. Once you have the project configured it should be possible to build and
run it, which will open a console window (on Windows) and a blank game window titled
“xyginext game”. Pressing F1 will open a console window which displays any messages
printed with the xy::Logger
class, and provides basic video and audio options. More
information on the console can be found in the documentation:
https://github.com/fallahn/xygine/wiki A closer look at the source files
The template project contains three sources files.Main.cpp contains the entry point into the program, which instantiates a
Game
object
and runs it. There’s not much else done here and the file will probably not be brought
up again for the rest of the tutorial.Game.cpp is where the magic begins to happen. The
Game
class inherits the xy::App
class which is the root of any xygine game or application. The App
class contains the
render window instance, as well as manages events and system messages, making sure that
they are dispatched correctly to the rest of the game. Documentation on App
specific
features (such as the message bus) can be found on the xygine wiki
https://github.com/fallahn/xygine/wiki . Because the Game
class inherits App
it has
access to these features through the App
interface. The Game
class is home to the
StateStack
- which will be covered later - and makes sure that events, messages and
updates are correctly forwarded, as well as ensuring the active states are rendered.
Most important to note about the Game
class, however, are the initialise()
and
finalise()
functions, which are overrides of the App
class functions. These are used
to correctly create, configure and tidy up any extra game-wide resources which you may
use throughout the lifetime of the application. They are called automatically on startup
and shutdown and should be used if a game uses any third party libraries such as a
physics system which requires specific initialisation and shutdown. The initialise
function returns a bool - this should return false if any initialisation errors occur,
particularly if initialising a third party library, as this will cause xygine to quit
safely, rather than try to continue.MyFirstState.cpp implements a simple
State
. States are used with xygine’s StateStack
class, which is optional but recommended. The StateStack
is a concept which allows
several states to be active at any one time, and controls how events and updates are
passed down through the active states. A State
object encapsulates a particular state
of the game, such as the main menu, a gameplay state, or a pause menu. The advantage of
a StateStack
is the ability to keep existing states alive, such as a game state, while
placing a another state on top. States are rendered from the bottom of the stack
upwards, and updated from the top down. This means that a game state at the bottom will
be drawn first, with a menu (for example) in a pause state drawn on top. Conversely the
pause state will receive updates and events such as keyboard and mouse first, and can
‘consume’ these events by returning false from the corresponding state functions:
handleEvent()
and update()
. This means that the menu remains responsive and active,
while preventing the lower game state from being updated, effectively pausing it.
Popping the the pause state from the top of the stack then allows the game state below
to continue to execute as it starts to receive events and updates again. When used
correctly this can be a very powerful and useful concept. For a more detailed look at
the implementation of a StateStack
I suggest reading ‘SFML Game Development’ by
Jan Haller, Henrik Vogelius Hansson and Artur Moreira - a book which was very
influential when I started creating xygine.
MyFirstState
itself implements the interface of xy::State
.
The functions are similar to that of the Game
class - handleEvent()
which passes
SFML events such as keyboard and mouse input down to any game classes which need them,
handleMessage()
used to forward system messages to interested parties, update()
which processes delta-time critical logic functions, and draw()
, used to render the
state to the screen. The state also returns an identifier via stateID()
which
is used when registering the state with the StateStack
in the Game::initialise()
function. The specifics of this are covered later. Usually when creating a new project
it is preferable to rename the MyFirstState
class to something more coherent such as
MenuState
- but for this particular case I will leave it as it is.Setting the Scene
MyFirstState
contains an instance of a xy::Scene
object. Scenes are used to
encapsulate the Entity Component System or ECS of xygine, and multiple scenes can exist
within a single state - for example one might be used to create the game area, and
another to display the user interface. Entities are effectively no more than an integer
ID, used to index arrays of components which are attached to them. As such they are very
lightweight and easy to copy around, while still providing access to their attached
components. Components are nothing more than data containers, usually structs, although
some provide a selection of functions to create a cleaner interface, such as
xy::Sprite
. The key point is, however, that there is no logic held within a component,
this is all performed by a series of Systems. A system class will look at all the active
entities in a Scene and process each one based on the data held within its components.
xygine already has several component and system classes for often used tasks, but the
ECS really shines when, later on, you create your own custom systems and their
corresponding components. To start with we’ll use some of the existing components and
systems to create a basic menu in MyFirstState
. At the top of the cpp file includexyginext/ecs/components/Transform.hpp
xyginext/ecs/components/Text.hpp
xyginext/ecs/components/Drawable.hpp
xyginext/ecs/systems/TextSystem.hpp
xyginext/ecs/systems/RenderSystem.hpp
A brief explanation: The transform component is used by almost all entities to correctly place them in the
Scene
. There are very few cases where an entity will not need a transform - text and
sprites cannot be drawn without one, buttons and UI components will not work, and audio
emitters cannot be correctly placed. Transforms can also be parented to one another to
form a ‘scene graph’. More information can be found about this in the wiki
https://github.com/fallahn/xygine/wiki Text components are very similar to the
Text
class in SFML, although slightly modified
to better fit the ECS. The interface is nearly the same, including functions such as
setString()
, setFillColour()
etc, and require an instance of sf::Font
to render. For
now a font can be added as a member to MyFirstScene
as m_font
for example - later this
will be replaced with a resource manager.Drawable components are required for any entities which are rendered to the screen. They contain an
sf::VertexArray
which, in this case, will be updated by the TextSystem
with data held in the Text
component. Sprites and the SpriteSystem
work in a similar way.
Later, when creating custom systems, the Drawable
component can be used to draw
arbitrary shapes, having their vertex data updated via the custom system. Drawable
components also have texture and shader properties, which allow SFML resources
sf::Texture
and sf::Shader
to be applied to them. This is not covered here, but
more information can be found in the documentation. Drawables have another
important feature: the depth property. The setDepth()
and getDepth()
accessors can
be used to define the ‘z depth’ of a drawable, providing runtime sorting of visible
drawables. Setting a depth with a higher value ensures that the drawable is drawn in
front of those with a lower value. It’s worth noting that there is no guarantee of order
with drawables that have the same value as another - this might cause some visible
flickering so in these cases a specific value should be set. The default value is 0.
Finally the RenderSystem
is used by the Scene
to sort and cull any visible entities
which have a Drawable
component, and render them to the screen.Setting up the ECS
With the correct files included, theScene
can now be initialised. For convenience
create a private member function in MyFirstState
called createScene()
, and call it
from the MyFirstState
constructor. Then define createScene()
void MyFirstState::createScene()
{
//add the systems
auto& messageBus = getContext().appInstance.getMessageBus();
m_scene.addSystem<xy::TextSystem>(messageBus);
m_scene.addSystem<xy::RenderSystem>(messageBus);
//load resources
m_font.loadFromFile(“assets/fonts/my_lovely_font.ttf”);
//create an entity
auto entity = m_scene.createEntity();
entity.addComponent<xy::Transform>().setPosition(200.f, 200.f);
entity.addComponent<xy::Text>(m_font).setString(“Hello world!”);
entity.addComponent<xy::Drawable>();
}
So what have we just done? Firstly we’ve added the necessary systems to the scene. All
systems require a reference to the xygine message bus so that they may subscribe to it.
This is retrieved through the getContext()
function, which is useful for accessing
game/application properties such as the render window or message bus.
xy::Scene::addSystem()
is a templated function which takes the system type as a
template parameter. The function parameters are all forwarded to the constructor of the
system itself - multiple parameters can be passed, which is useful when creating custom
systems. The default xygine systems, however, take only a reference to the message bus.
Systems are updated and drawn in the order in which they are added to the scene, which
is sometimes important. In this case we want the Text
components to be updated before
the RenderSystem
draws them.Next the font is loaded, which was added as a member to
MyFirstState
as m_font
. This
will eventually be replaced with a resource manager. Note that when loading the font
there are no defaults supplied with xygine - you need to provide a path to your own. If
the text is not visible then this is probably the first thing to check.Finally a new entity is created. Entities are created through a
Scene
’s factory function
createEntity()
. This is important because it ensures that the entity is correctly
registered to its parent scene. The entity class is merely a handle, however, and instances
can happily be overwritten when creating new entities. For example:auto entity = m_scene.createEntity();
//add components to entity
entity = m_scene.createEntity();
//add other components to new entity
All entities will continue to live within a scene until explicitly destroyed, which will
mark them for garbage collection:m_scene.destroyEntity(entity);
This generally means that entities will exist until the end of the current frame.NOTE: xygine has a hard limit of 1024 active entities at a time. While this can be changed by modifying the source code it is usually enough as long as expired entities are explicitly destroyed. There is very little overhead in removing and creating entities at runtime because xygine automatically pools and reallocates the memory required.
Components are added to an entity with
addComponent<T>()
. Similarly to the Scene
’s
addSystem()
function, the entity’s addComponent()
function is templated, taking the
component type as a type parameter. The function also forwards any parameters to the
component constructor, such as in this example where a reference to the font is
forwarded to the constructor of the Text
component. Entity::addComponent()
will return a reference to the
newly created component allowing properties to be immediately set, for example the
string used in the Text
component. A reference to a component can also be retrieved
with entity.getComponent<T>()
, allowing other component properties to be updated, or
components to be copied elsewhere. Note that not all components are copyable, for
example the Transform
component is only moveable.Building and running the project should now display the window as before, only with the words “Hello World!” in the top corner.
From here I encourage you to experiment with the entities and components - update the
createScene()
function to create 3 entities, each with a different text strings:
A title, one which says ‘Play’ and another which says ‘Quit’. Try updating different
component properties such as the scale, position or rotation of the Transform
component, or character size or fill colour of the Text
component. When you have a
basic menu laid out, move on to the next section.Adding interactivity
To make the menu usable ideally the entities rendering the text will need to act as buttons when clicked on. xygine includes aUIHitBox
component and a UISystem
for
this purpose. At the top of the MyFirstState.cpp includexyginext/ecs/components/UIHitBox.hpp
xyginext/ecs/systems/UISystem.hpp
Now in createScene()
add a UISystem
to the scene, right before adding the
RenderSystem
m_scene.addSystem<xy::UISystem>(messageBus);
UISystems are slightly different from other systems - they require event input from the
state, so that they know when to react to keyboard or mouse input. In
MyFirstState::handleEvent()
add the linem_scene.getSystem<xy::UISystem>().handleEvent(evt);
With this set up it’s time to make the quit button do something. Hopefully in
createScene()
you should now have something likeentity = m_scene.createEntity();
entity.addComponent<xy::Transform>();
entity.addComponent<xy::Text>(m_font).setString(“Quit”);
entity.addComponent<xy::Drawable>();
To add interactivity to this entity add a UIHitBox
component. This is used to define
an area in the scene in which mouse events, such as button presses, perform a callback.
The size of the text can be retrieved with the static function
xy::Text::getLocalBounds()
.entity.addComponent<xy::UIHitBox>().area = xy::Text::getLocalBounds(entity);
Now that the entity has an area defined for it, it needs to know what to do when it
receives an event within the given area. These are done by callbacks registered with the
UISystem
. Callbacks are registered with a lambda expression or functor passed to
UISystem::addMouseButtonCallback()
which returns an integer ID. The ID is useful
because it means a single callback function can be assigned via its ID to multiple
components, should those components wish to perform the same thing.auto callbackID =
m_scene.getSystem<xy::UISystem>().addMouseButtonCallback(
[&](xy::Entity e, sf::Uint64 flags)
{
if(flags & xy::UISystem::LeftMouse)
{
getContext().appInstance.quit();
}
});
This looks a little wordy, so let’s break it down. A reference to the UISystem
can be
retrieved from the scene with getSystem<T>()
. Then a lambda expression is passed to
addMouseButtonCallback()
. The lambda signature for a mouse button callback passes in a
copy of the entity which is executing the callback - that is, the entity which has the
UIHitBox
component attached to it, and a copy of the event flags. Event flags can be
used to check which mouse button was pressed. In this case we’re looking for the left
mouse button, and if it is true the game is quit. (xy::App::quit()
is the preferred
method, as it makes sure everything is finalised properly before closing the game).
Other available callbacks, their signatures and their flags are detailed in the
documentation. https://github.com/fallahn/xygine/wikiThe result of registering the callback is returned as
callbackID
. To associate this
with our hitbox (and potentially any others) it needs to be added to the hitbox callback
array.entity.getComponent<xy::UIHitBox>().callbacks[xy::UIHitBox::MouseUp] = callbackID;
Now whenever the hitbox detects a mouse button up event the callback with callbackID is performed. In this case the button is checked to see if it is the left button, and if it is the game is quit.
Compile and run the project. Clicking on the text which says ‘Quit’ should now close the window.
From here, using the documentation as reference, experiment with the
UISystem
and its
callbacks. As well as mouse events try handling keyboard events. Mouse move events can
be used to trigger callbacks which highlight the selected text for example.The source for this tutorial can be found here.
Comments
Post a Comment