Natura: guardian of the forest - 3rd year university group project
project timescale: october 2014 - april 2015
This is the game I developed with a team of students for my third year professional project. My team's brief was given to us by a local company called Space Budgie. We were tasked with creating a game tutorial where the player wasn't told directly what to do. Instead we had to guide the player using the environment alone. We got the freedom to choose what the game itself was so we decided on a 2D, top down, exploration game where the player controlled a mother nature figure. The aim of the game was to collect orbs in order to transform a dead world into an alive one, using tutorial techniques to guide the player to these orbs.
I was an engine programmer for this project so I focused mostly on constructing the base framework and implementing graphical tools such as lighting and post processing effects. I also acted as a gameplay programmer and implemented mechanics such as pushing objects and firing projectiles. I took on the role of lead programmer at the start of pre-production. As lead programmer, I organised meetings with the code team, maintained communication with the producer, assigned tasks to the other programmers, set up our online repository, taught myself how to use Git for source control and passed the knowledge on to the rest of the programmers. I suggested the technology we ultimately used to make the game; SFML, a C++ based graphics framework that I had previous experience using.
Below is gameplay video of the game. Underneath it is a breakdown of the code I contributed to the project and a zipped folder containing the code files described.
I was an engine programmer for this project so I focused mostly on constructing the base framework and implementing graphical tools such as lighting and post processing effects. I also acted as a gameplay programmer and implemented mechanics such as pushing objects and firing projectiles. I took on the role of lead programmer at the start of pre-production. As lead programmer, I organised meetings with the code team, maintained communication with the producer, assigned tasks to the other programmers, set up our online repository, taught myself how to use Git for source control and passed the knowledge on to the rest of the programmers. I suggested the technology we ultimately used to make the game; SFML, a C++ based graphics framework that I had previous experience using.
Below is gameplay video of the game. Underneath it is a breakdown of the code I contributed to the project and a zipped folder containing the code files described.
natura_code.zip | |
File Size: | 36 kb |
File Type: | zip |
Code Breakdown
Application
This class contains the main loop of the program. Here I created the window used to render the game, which closes when the user presses the escape key. As long as the window is open, Application will update and render the current State of the program, using the StateManager. The loop will also update the InputManager and store its previous state.
StateManager
In this class I created a std::vector of States and used four functions to manipulate this vector. The Clear function can be used to get rid of all the states in the vector when the program ends or restarts. The ChangeState function removes the state at the top of the vector and adds a new one to the vector. This is used when a new state is required and the current state is no longer needed. E.g. going from a start screen to a menu. The PushState function pauses the state at the stop of the vector and adds a new one to the vector. Needed when the game is required to keep a state stored whilst using a different state. E.g. going from in-game to a pause screen. The PopState function removes the state at the top of the vector and resumes the next state stored below it in the vector. This is useful for removing a state that is no longer needed and using the previous state. E.g. unpausing the game.
State
This class acts as a base entity for the different states of the program. It contains virtual functions for initialising, cleaning up, pausing, resuming, updating and rendering. These functions are overloaded by the different states with their own required functionality. The State class also stores a pointer to the render window so that each state can render their own graphics.
StartState
This is class represents the first state of the program. It displays the game title and logo. When the user press the enter key or start button, the game starts.
GameState
In this class's Init function, it creates and initialises several important items used in the program. These include, the TextureManager, the AnimatedPropManager, the MovingPropManager, the RenderTexture, the ltbl::LightSystem, the MapLoader and the Player. The class contains a std::map of Sections. This is used to store the different areas of the game world and switch between them. The Update function uses the std::map to update the Section currently being used and initialise a new Section when a switch occurs. The ViewUpdate function makes the view follow the player without going out of bounds of the Map, the player is currently in.
Section
This class represents an area of the game world. It stores all the GameObjects associated with it in a std::vector and in its Update function it checks if each GameObject is active or not. If a GameObject is active, it updates that object but if the GameObject isn’t active, it deletes the object and removes it from the vector. In the Render function, it checks if each GameObject is active and drawable. If a GameObject is both, then it renders the object.
Each Section stores two Maps; a dead version of the Section and an alive one. A Section starts off in its dead state and renders the dead Map. When the Player collides with an Orb, the Section changes to its alive state and renders the alive Map. A number of things happen during this transition. Control of the Player is briefly taken away from the user so that they don’t move during the change. A shader is used to pixelate the screen. It gradually makes the screen increasingly pixelated and when the screen is fully pixelated, the dead Map stops rendering and the alive Map renders instead. The screen then gradually un-pixelates until it returns to normal. Whilst the change is happening, the controller vibrates.
In this class's Init function, it creates and initialises several important items used in the program. These include, the TextureManager, the AnimatedPropManager, the MovingPropManager, the RenderTexture, the ltbl::LightSystem, the MapLoader and the Player. The class contains a std::map of Sections. This is used to store the different areas of the game world and switch between them. The Update function uses the std::map to update the Section currently being used and initialise a new Section when a switch occurs. The ViewUpdate function makes the view follow the player without going out of bounds of the Map, the player is currently in.
Section
This class represents an area of the game world. It stores all the GameObjects associated with it in a std::vector and in its Update function it checks if each GameObject is active or not. If a GameObject is active, it updates that object but if the GameObject isn’t active, it deletes the object and removes it from the vector. In the Render function, it checks if each GameObject is active and drawable. If a GameObject is both, then it renders the object.
Each Section stores two Maps; a dead version of the Section and an alive one. A Section starts off in its dead state and renders the dead Map. When the Player collides with an Orb, the Section changes to its alive state and renders the alive Map. A number of things happen during this transition. Control of the Player is briefly taken away from the user so that they don’t move during the change. A shader is used to pixelate the screen. It gradually makes the screen increasingly pixelated and when the screen is fully pixelated, the dead Map stops rendering and the alive Map renders instead. The screen then gradually un-pixelates until it returns to normal. Whilst the change is happening, the controller vibrates.
Each Map contains doors placed by the designer. These doors are invisible objects found at the edges of Sections that take the Player to other Sections. When the Player collides with a door, the GameState starts updating and rendering the Section that door leads to. The Player is also moved to the position of the corresponding door in the new Section plus an offset that is determined by the direction the door is facing. The offset is used so that the Player doesn’t immediately collide with the door and move back to the previous Section.
Originally, the switch between Sections happened instantly but this was very jarring to the user. A few things were done to make the switch smoother. Control of the Player is briefly taken away and the Player moves slowly out of the current Section and into the new one. A shader is used to vignette the screen. This is done the same way as the pixelation during the change from dead to alive. The screen gradually fades to black and when it’s completely faded out, the Section switches to the new one. Then the screen fades back in.
Originally, the switch between Sections happened instantly but this was very jarring to the user. A few things were done to make the switch smoother. Control of the Player is briefly taken away and the Player moves slowly out of the current Section and into the new one. A shader is used to vignette the screen. This is done the same way as the pixelation during the change from dead to alive. The screen gradually fades to black and when it’s completely faded out, the Section switches to the new one. Then the screen fades back in.
When the Sections switch or the Maps change, all the lights associated with that Section or Map need to be turned off. Since lights are attached to GameObjects, every GameObject is checked to see if it has a light and if one does, the light is turned off.
Map
This class loads in a map created in Tiled and creates all the GameObjects associated with it. It does this by checking the name of each layer in the map. If a layer name corresponds to a GameObject, it will check for objects on that layer. For every object found, it will create a GameObject of that type and place it at the position of that object. It will also get any properties the GameObject needs from that object. E.g. Webs and boulders are scaled to match the size of the object. The class also check each GameObject to see if it has a light. If a GameObject does have a light then it turns the light on.
Services
Used to give access to important items that are needed multiple times throughout the code. These items are as follows; the InputManager, the MusicPlayer, the SoundPlayer, the TextureManager, the AnimatedPropManager, the MovingPropManager, the LightSystem and a std::vector of GameObjects that are currently in use. These are accessed through a function called Instance, which returns a static pointer to the Services.
InputManager
I wrote this class to encapsulate SFML’s keyboard and add more functionality to it. It also adds Xbox 360 controller support. Uses an enumerator to easily get button inputs and uses XINPUT_STATE to store the state of the controller. The class has functions to find out if a key or button was pressed, held down or released during that frame of the program. It does this by comparing the current states of the keyboard and controller with the previous states. Two XINPUT_STATEs are used for the storing the current and previous state of the controller, whilst two arrays of booleans are used for the keyboard. The Update function sets the current state of the inputs every time it’s called in the main loop. It also sets up the controller sticks and triggers for use. Other functions are used to get the positions of the sticks, get the positions of the triggers, check if a controller is connected and set the controller vibration.
GameObject
This class represents a physical object in the game that can be updated and rendered. It inherits from sf::RectangleShape. A SFML class consisting of a quad that can be moved, rendered and textured. There is a Render function that the GameObjects use to render themselves and a virtual Update function that can be over loaded with logic required by classes that inherit from GameObject. This allows the program to group GameObjects together to update and render them at once, using a data structure such as a std::vector. There are several functions that can be used to move the GameObjects in 2D space in different ways. An enumerator is used to identify the different types of GameObject. There are two collision detection functions; one for checking for collisions with another GameObject and one for checking for collisions with the map. Each GameObject has a light that can be turned on and off. They are turned on by adding them to the LightSystem and turned off by removing them from the LightSystem. Various flags are used to determine if a GameObject is active, if it can collide with other objects, if it needs to be drawn and if its light is on.
Player
This class inherits from GameObject and represents the user controlled character. The Player can move in eight directions; north, south, east, west, north west, north east, south west and south east. It does this by using the InputManager to check the arrow keys, the WASD keys and the left analogue stick of the controller. If one or two of those keys are held down or if the stick has been moved, the Player moves accordingly.
Over the course of the game, the Player unlocks three powers that the user can use. It unlocks these powers by collecting specific Orbs. The class checks for collisions between the Player and Boulders. If the Player collides with a Boulder and they don’t have the Boulder pushing power, they will be moved back to the edge of the Boulder so that they can’t move passed it. If they do have the power, the Boulder will be moved in the direction the player is moving.
The class checks for collisions between the Player and river banks and pits. If they don’t have the floating power, they will moved back so they can’t move across the river or pit. If they do have the power, they will be able to move across the river or pit.
When the Player gets the fire power, they can throw FireBalls. This is done by using the InputManager to check if the spacebar or A button has been pressed. If either has been pressed, a FireBall will be created on top of the player. A limiter is used to prevent too many FireBalls being on screen at once.
Boulder
This class inherits from GameObject and represents an object that blocks the player until it is pushed out of the way. They are added to Maps and scaled in Tiled. In its Update function it checks if it is colliding with the Player. If it is then it slows the Player down and moves in the same direction the Player is moving in. If it collides with another Boulder or an object on the map, it moves back to the edge of the Boulder or object so that it doesn’t move through them.
Map
This class loads in a map created in Tiled and creates all the GameObjects associated with it. It does this by checking the name of each layer in the map. If a layer name corresponds to a GameObject, it will check for objects on that layer. For every object found, it will create a GameObject of that type and place it at the position of that object. It will also get any properties the GameObject needs from that object. E.g. Webs and boulders are scaled to match the size of the object. The class also check each GameObject to see if it has a light. If a GameObject does have a light then it turns the light on.
Services
Used to give access to important items that are needed multiple times throughout the code. These items are as follows; the InputManager, the MusicPlayer, the SoundPlayer, the TextureManager, the AnimatedPropManager, the MovingPropManager, the LightSystem and a std::vector of GameObjects that are currently in use. These are accessed through a function called Instance, which returns a static pointer to the Services.
InputManager
I wrote this class to encapsulate SFML’s keyboard and add more functionality to it. It also adds Xbox 360 controller support. Uses an enumerator to easily get button inputs and uses XINPUT_STATE to store the state of the controller. The class has functions to find out if a key or button was pressed, held down or released during that frame of the program. It does this by comparing the current states of the keyboard and controller with the previous states. Two XINPUT_STATEs are used for the storing the current and previous state of the controller, whilst two arrays of booleans are used for the keyboard. The Update function sets the current state of the inputs every time it’s called in the main loop. It also sets up the controller sticks and triggers for use. Other functions are used to get the positions of the sticks, get the positions of the triggers, check if a controller is connected and set the controller vibration.
GameObject
This class represents a physical object in the game that can be updated and rendered. It inherits from sf::RectangleShape. A SFML class consisting of a quad that can be moved, rendered and textured. There is a Render function that the GameObjects use to render themselves and a virtual Update function that can be over loaded with logic required by classes that inherit from GameObject. This allows the program to group GameObjects together to update and render them at once, using a data structure such as a std::vector. There are several functions that can be used to move the GameObjects in 2D space in different ways. An enumerator is used to identify the different types of GameObject. There are two collision detection functions; one for checking for collisions with another GameObject and one for checking for collisions with the map. Each GameObject has a light that can be turned on and off. They are turned on by adding them to the LightSystem and turned off by removing them from the LightSystem. Various flags are used to determine if a GameObject is active, if it can collide with other objects, if it needs to be drawn and if its light is on.
Player
This class inherits from GameObject and represents the user controlled character. The Player can move in eight directions; north, south, east, west, north west, north east, south west and south east. It does this by using the InputManager to check the arrow keys, the WASD keys and the left analogue stick of the controller. If one or two of those keys are held down or if the stick has been moved, the Player moves accordingly.
Over the course of the game, the Player unlocks three powers that the user can use. It unlocks these powers by collecting specific Orbs. The class checks for collisions between the Player and Boulders. If the Player collides with a Boulder and they don’t have the Boulder pushing power, they will be moved back to the edge of the Boulder so that they can’t move passed it. If they do have the power, the Boulder will be moved in the direction the player is moving.
The class checks for collisions between the Player and river banks and pits. If they don’t have the floating power, they will moved back so they can’t move across the river or pit. If they do have the power, they will be able to move across the river or pit.
When the Player gets the fire power, they can throw FireBalls. This is done by using the InputManager to check if the spacebar or A button has been pressed. If either has been pressed, a FireBall will be created on top of the player. A limiter is used to prevent too many FireBalls being on screen at once.
Boulder
This class inherits from GameObject and represents an object that blocks the player until it is pushed out of the way. They are added to Maps and scaled in Tiled. In its Update function it checks if it is colliding with the Player. If it is then it slows the Player down and moves in the same direction the Player is moving in. If it collides with another Boulder or an object on the map, it moves back to the edge of the Boulder or object so that it doesn’t move through them.
FireBall
Class that inherits from GameObject and represents a FireBall that moves across the screen, animates and has a light. It moves in the direction the Player was facing when they threw it and once the animation ends it is deleted. When it collides with a Web, it is deleted and the Web is set on fire.
Class that inherits from GameObject and represents a FireBall that moves across the screen, animates and has a light. It moves in the direction the Player was facing when they threw it and once the animation ends it is deleted. When it collides with a Web, it is deleted and the Web is set on fire.
Web
This class inherits from GameObject and represents an object that blocks the Player until it is removed. When a FireBall collides with it, it starts playing an animation where fire spreads across it. It also gains a light that increases and decreases in size with the fire animation. When the animation ends, it is deleted and the Player can move passed where it was.
This class inherits from GameObject and represents an object that blocks the Player until it is removed. When a FireBall collides with it, it starts playing an animation where fire spreads across it. It also gains a light that increases and decreases in size with the fire animation. When the animation ends, it is deleted and the Player can move passed where it was.
Prototypes
For our first client presentation we created a simple prototype that showcased two tutorial techniques we planned on implementing. These techniques were changing the appearance of the player as they approached objects of interest and placing 3D sounds in the environment that increased in volume as the player got closer to them. The prototype consisted of two coloured squares in space. The first of these squares was moved by the user and changed colour as it got closer to the second square. The second square had a 3D sound attached to it that would change in volume based on how close the player was to it. Screenshots of this prototype can be seen below.
For our first client presentation we created a simple prototype that showcased two tutorial techniques we planned on implementing. These techniques were changing the appearance of the player as they approached objects of interest and placing 3D sounds in the environment that increased in volume as the player got closer to them. The prototype consisted of two coloured squares in space. The first of these squares was moved by the user and changed colour as it got closer to the second square. The second square had a 3D sound attached to it that would change in volume based on how close the player was to it. Screenshots of this prototype can be seen below.
Early in development, we decided to add lighting to the game as a tutorial technique. The idea being that users would be drawn towards glowing objects in the environment. I looked into multiple ways of implementing lighting in 2D games. I originally considered using visibility and shadow effects as described by ncase (no date). This is done by casting rays out from a point in multiple directions and finding the points where they intersect. The points are then connected to create a visibility polygon and shadow polygons are created outside this polygon. I decided that this sort of lighting would not suit our game since, rather than draw the user’s attention to a specific object, it would just make some of the environment visible and hide the rest.
The next thing I considered was using SFML shaders, similar to how we added effects to screen transitions. I found an article by theDaftDev (2014) that described how to do this. Using a fragment shader, a RenderTexture and sf::RenderStates, the program could render the game with lights. The shader would take in a position, colour and radius as parameters and use this information to know where and how to brighten up the Map.
Below you can see this lighting in action. A light is positioned over the player with different colours and attenuations.
The next thing I considered was using SFML shaders, similar to how we added effects to screen transitions. I found an article by theDaftDev (2014) that described how to do this. Using a fragment shader, a RenderTexture and sf::RenderStates, the program could render the game with lights. The shader would take in a position, colour and radius as parameters and use this information to know where and how to brighten up the Map.
Below you can see this lighting in action. A light is positioned over the player with different colours and attenuations.
We ended up having to abandon this lighting due to performance issues. Since every light required the map to be rendered again and the maps were quite large in size, it was impractical to continue using this method. We then started using an open source library called Let There Be Light, which gave us greater functionality and didn’t cause performance issues.
REFERENCES
ncase. [no date]. Sight & Light. [online] Available from: http://ncase.me/sight-and-light/ [Accessed 12 January 2015].
theDaftDev. [2014]. SFML and shaders. [online] Available from: http://thedaftdev.com/entry-13-SFML-and-shaders.-.html [Accessed 16 January 2015].
ncase. [no date]. Sight & Light. [online] Available from: http://ncase.me/sight-and-light/ [Accessed 12 January 2015].
theDaftDev. [2014]. SFML and shaders. [online] Available from: http://thedaftdev.com/entry-13-SFML-and-shaders.-.html [Accessed 16 January 2015].