Crush! Breakdown Part 1 - Scene setup
After spending any amount of time on a project I like to look back over what I did and break it down in a kind of postmortem. I find some retrospect helps me to consider what it is I've done wrong so that I can learn from the experience before moving on. The starting point for this particular project was creating a scene and getting it rendering in a modular and extensible way. I'm actually pretty pleased with what I did, although I do wish I had considered networking support early on in design, as it really needs to be baked in from the start (and hence will probably never be added to Crush!).
I started with the concept of a scene graph - a series of parented nodes which allow draw calls and transformations to be passed across siblings and down through children. Scene graphs are well documented and there are resources all over the internet for learning about them, so I'll not go too far into detail here. There is even an example in the SFML book, which I used as a starting point as I was using SFML as the main library. The scene graph in Crush! varies from the book's implementation, however, in that instead of relying on inheritance for distinguishing node types it tries to take a component based approach. The scene graph exists to maintain the transformations of each scene node, and allows rendering of the scene in as compact way as possible. The actual drawing of node representations and the behaviour of nodes (including how they are transformed in the scene) is left to those components, so each node can behave independently, depending on the collection of components which are attached to it. The entire node graph is kept inside a class which represents a scene. This class is essentially the root node of the graph, with a few key differences to implement ownership semantics. As well as the graph the Scene class owns any lights which may exist, as well as cameras used for rendering the scene. This means the Scene class can properly set up any views and perform lighting calculations each frame, before moving down the graph and rendering any drawables attached to the nodes. Internally the Scene's draw function looks a bit like this (in pseudo code):
Scene::draw(RenderTarget rt, States states)
{
states.shader = m_lightingShader;
for(const auto& l : m_lights)
{
m_lightingShader.setParameter(lightParam, l.property);
}
rt.setView(m_activeCamera.getView());
m_sceneGraph.draw(rt, states);
}
I'll cover the actual lighting and camera set up in another post. For now it's enough to know that the scene is responsible for the lights and cameras which have been created (and attached to nodes as part of the component strategy) which it then uses to update the shader system each frame, before drawing the scene graph.This also nicely encapsulates much of the rendering, so that externally the entire scene can be drawn at once. The overall structure of the game uses a state stack, and the current scene is a member of 'GameState' (other states being PauseState, MenuState and so on), so when the GameState is created a new scene is built from information loaded from a map file, and drawn with
m_scene.draw(m_renderWindow, states);
With little else to do to the scene the code in GameState can be kept relatively clean and easy to read, with all the implementation details tucked away inside the Scene class.
Using a component based approach with the scene nodes also helps with this, as each component can belong to a parent class where its details are relevant, without cluttering up the Node class itself. I already stated that cameras and lights are components, the Camera class being not much more than a wrapper around sf::View, and Light a small struct which contains colour and falloff values. These can be attached to nodes so that they take on any transformations as the node moves around the scene, without having to keep their own transform specific data. Lights and cameras both only exist within the scene so it makes sense that the Scene class should own all the instances, and pass out references to them should they need to be modified. For example:
auto& light = m_scene.createLight(colour, falloff);
Node::Ptr node;
node.attachLight(light);
//do other stuff to node
m_scene.addNode(node);
The same goes for cameras. Internally the Scene class creates the light (or camera), providing that certain criteria are met, and returns a reference to it so that it can be modified if necessary, and then attached to a node. Node::Ptr is a typedef for std::unique_ptr<Node> as nodes will usually need to be dynamically added and removed from the scene as game play progresses. Adding the node to the scene allows the Scene class (and, internally, the scene graph) to take ownership of the node, which is why I use a unique_ptr as opposed to a shared_ptr.
The other main components used in the game are drawables - classes which inherit sf::Drawable, and collision bodies. I used sf::Drawable as the class type for rendering nodes rather than sf::Sprite, as most of the drawables I used were custom classes, such as the water effect or animated sprites. Collision bodies represent a very basic physics engine which only supports rectangular collision detection with no rotation. For this game it was enough, and using a full blown system such as Box2D seemed overkill. It also meant that I didn't have to worry about unit conversion and could keep all values within a single domain. Collision bodies all belong to a 'CollisionWorld' class which takes care of all the physics simulation, collision detection and collision resolution. Bodies can be requested from the CollisionWorld in a similar way to how lights/cameras are requested from the Scene class. The returned bodies remain owned by the collision world, and references are attached to the nodes as needed, so that the nodes can be transformed and updated as bodies move around the world and interact with each other. I'll go into full detail of the CollisionWorld class in another post.
This was enough to create a renderable scene, and make the scene nodes interactive whilst remaining reasonably decoupled from the other classes. It sat in my mind as having a set of playing pieces laid out on a game board, ready for the player to command. To be able to manipulate these playing pieces without directly hooking up too much code, I turned to the observer pattern, as described in Game Programming Patterns by Robert Nystrom (although he's far from the first to write about it of course). Here is, perhaps, where I would make a slight change in hindsight. While the pattern worked very well it did become a little spaghetti-like in places, and it isn't always obvious which classes are observing which - in future I would perhaps replace this pattern with a message bus. In this instance, though, I followed through with the idea of the scene nodes being 'observable', watched by a set of 'controller' classes with the ability to manipulate the playing pieces, each responsible for their own part of the game. Controller classes include the player, the scoreboard, the audio system, the physics world and a controller responsible for map data loaded from an external file. This also provided handy encapsulation for drawable objects, such as the player controller looking after an animated sprite and making sure the correct animations are played, or the map controller being responsible for creating the world geometry which makes up the scenery. Each of these controllers can then provide a reference to drawable items which are attached to the corresponding nodes in the scene graph. The scene graph needs to know nothing of the internal implementation of these drawables, only how to draw them. The controllers need to know little if nothing of each other either, they just watch the scene and wait for events to be raised via the observer pattern. If an event is pertinent to a particular controller, then that controller will act on it. For example if a PlayerDied event is raised then the audio controller will play a specific sound, the scoreboard controller will reduce the number of lives and points, and the player controller will reset the player's position.
Finally, to enable the controller classes to manipulate the scene, I used a command queue which is more or less identical to that found in the SFML Game Development book. Each controller class keeps a reference to the command stack, so that when it needs to update the scene it can create a command targeted at a specific node or set of nodes, and place it on the stack. At the beginning of each frame the entire command stack is executed so that the scene is updated. After which the collision / physics world is updated and collisions resolved, the controllers respond to any events raised by the updated state, before the entire scene is then drawn. The flow within the GameState class then looks like this:
while(!m_commandStack.empty())
m_scene.doCommand(m_commandStack.pop());
m_collisionWorld.update(dt);
m_player.update(dt);
m_scoreboard.update(dt);
m_audio.update(dt);
m_scene.draw(m_renderWindow, states);
Crush! is open source, so if you want to take a look at the final implementation, or just have a play you can get it from the Github page.
Part 2: Collision
I started with the concept of a scene graph - a series of parented nodes which allow draw calls and transformations to be passed across siblings and down through children. Scene graphs are well documented and there are resources all over the internet for learning about them, so I'll not go too far into detail here. There is even an example in the SFML book, which I used as a starting point as I was using SFML as the main library. The scene graph in Crush! varies from the book's implementation, however, in that instead of relying on inheritance for distinguishing node types it tries to take a component based approach. The scene graph exists to maintain the transformations of each scene node, and allows rendering of the scene in as compact way as possible. The actual drawing of node representations and the behaviour of nodes (including how they are transformed in the scene) is left to those components, so each node can behave independently, depending on the collection of components which are attached to it. The entire node graph is kept inside a class which represents a scene. This class is essentially the root node of the graph, with a few key differences to implement ownership semantics. As well as the graph the Scene class owns any lights which may exist, as well as cameras used for rendering the scene. This means the Scene class can properly set up any views and perform lighting calculations each frame, before moving down the graph and rendering any drawables attached to the nodes. Internally the Scene's draw function looks a bit like this (in pseudo code):
Scene::draw(RenderTarget rt, States states)
{
states.shader = m_lightingShader;
for(const auto& l : m_lights)
{
m_lightingShader.setParameter(lightParam, l.property);
}
rt.setView(m_activeCamera.getView());
m_sceneGraph.draw(rt, states);
}
I'll cover the actual lighting and camera set up in another post. For now it's enough to know that the scene is responsible for the lights and cameras which have been created (and attached to nodes as part of the component strategy) which it then uses to update the shader system each frame, before drawing the scene graph.This also nicely encapsulates much of the rendering, so that externally the entire scene can be drawn at once. The overall structure of the game uses a state stack, and the current scene is a member of 'GameState' (other states being PauseState, MenuState and so on), so when the GameState is created a new scene is built from information loaded from a map file, and drawn with
m_scene.draw(m_renderWindow, states);
With little else to do to the scene the code in GameState can be kept relatively clean and easy to read, with all the implementation details tucked away inside the Scene class.
Using a component based approach with the scene nodes also helps with this, as each component can belong to a parent class where its details are relevant, without cluttering up the Node class itself. I already stated that cameras and lights are components, the Camera class being not much more than a wrapper around sf::View, and Light a small struct which contains colour and falloff values. These can be attached to nodes so that they take on any transformations as the node moves around the scene, without having to keep their own transform specific data. Lights and cameras both only exist within the scene so it makes sense that the Scene class should own all the instances, and pass out references to them should they need to be modified. For example:
auto& light = m_scene.createLight(colour, falloff);
Node::Ptr node;
node.attachLight(light);
//do other stuff to node
m_scene.addNode(node);
The same goes for cameras. Internally the Scene class creates the light (or camera), providing that certain criteria are met, and returns a reference to it so that it can be modified if necessary, and then attached to a node. Node::Ptr is a typedef for std::unique_ptr<Node> as nodes will usually need to be dynamically added and removed from the scene as game play progresses. Adding the node to the scene allows the Scene class (and, internally, the scene graph) to take ownership of the node, which is why I use a unique_ptr as opposed to a shared_ptr.
The other main components used in the game are drawables - classes which inherit sf::Drawable, and collision bodies. I used sf::Drawable as the class type for rendering nodes rather than sf::Sprite, as most of the drawables I used were custom classes, such as the water effect or animated sprites. Collision bodies represent a very basic physics engine which only supports rectangular collision detection with no rotation. For this game it was enough, and using a full blown system such as Box2D seemed overkill. It also meant that I didn't have to worry about unit conversion and could keep all values within a single domain. Collision bodies all belong to a 'CollisionWorld' class which takes care of all the physics simulation, collision detection and collision resolution. Bodies can be requested from the CollisionWorld in a similar way to how lights/cameras are requested from the Scene class. The returned bodies remain owned by the collision world, and references are attached to the nodes as needed, so that the nodes can be transformed and updated as bodies move around the world and interact with each other. I'll go into full detail of the CollisionWorld class in another post.
This was enough to create a renderable scene, and make the scene nodes interactive whilst remaining reasonably decoupled from the other classes. It sat in my mind as having a set of playing pieces laid out on a game board, ready for the player to command. To be able to manipulate these playing pieces without directly hooking up too much code, I turned to the observer pattern, as described in Game Programming Patterns by Robert Nystrom (although he's far from the first to write about it of course). Here is, perhaps, where I would make a slight change in hindsight. While the pattern worked very well it did become a little spaghetti-like in places, and it isn't always obvious which classes are observing which - in future I would perhaps replace this pattern with a message bus. In this instance, though, I followed through with the idea of the scene nodes being 'observable', watched by a set of 'controller' classes with the ability to manipulate the playing pieces, each responsible for their own part of the game. Controller classes include the player, the scoreboard, the audio system, the physics world and a controller responsible for map data loaded from an external file. This also provided handy encapsulation for drawable objects, such as the player controller looking after an animated sprite and making sure the correct animations are played, or the map controller being responsible for creating the world geometry which makes up the scenery. Each of these controllers can then provide a reference to drawable items which are attached to the corresponding nodes in the scene graph. The scene graph needs to know nothing of the internal implementation of these drawables, only how to draw them. The controllers need to know little if nothing of each other either, they just watch the scene and wait for events to be raised via the observer pattern. If an event is pertinent to a particular controller, then that controller will act on it. For example if a PlayerDied event is raised then the audio controller will play a specific sound, the scoreboard controller will reduce the number of lives and points, and the player controller will reset the player's position.
Finally, to enable the controller classes to manipulate the scene, I used a command queue which is more or less identical to that found in the SFML Game Development book. Each controller class keeps a reference to the command stack, so that when it needs to update the scene it can create a command targeted at a specific node or set of nodes, and place it on the stack. At the beginning of each frame the entire command stack is executed so that the scene is updated. After which the collision / physics world is updated and collisions resolved, the controllers respond to any events raised by the updated state, before the entire scene is then drawn. The flow within the GameState class then looks like this:
while(!m_commandStack.empty())
m_scene.doCommand(m_commandStack.pop());
m_collisionWorld.update(dt);
m_player.update(dt);
m_scoreboard.update(dt);
m_audio.update(dt);
m_scene.draw(m_renderWindow, states);
Crush! is open source, so if you want to take a look at the final implementation, or just have a play you can get it from the Github page.
Part 2: Collision
Comments
Post a Comment