This is a submission for the Capstone project in the Udacity C++ Nanodegree Program. The code for this repo, and the base Snake game implementation, was inspired by this excellent StackOverflow post and set of responses.
The completed submission looks like the following.
- cmake >= 3.7
- All OSes: click here for installation instructions
- make >= 4.1 (Linux, Mac), 3.81 (Windows)
- Linux: make is installed by default on most Linux distros
- Mac: install Xcode command line tools to get make
- Windows: Click here for installation instructions
- SDL2 >= 2.0
- All installation instructions can be found here
Note that for Linux, an
apt
orapt-get
installation is preferred to building from source. - gcc/g++ >= 5.4
- Linux: gcc / g++ is installed by default on most Linux distros
- Mac: same deal as make - install Xcode command line tools
- Windows: recommend using MinGW
- Clone this repo.
- Make a build directory in the top level directory:
mkdir build && cd build
- Compile:
cmake .. && make
- Run it:
./SnakeGame
.
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
A new poisoned food type was added to the game. This introduces a new loss condition - when the snake ingests too much poison. Eating the poisoned food also decreases the player's score by 1. However, it does not reduce the length of the snake or decrease the speed of the snake - this means that more skill is required to reach the highest score. The snake changes color each time that it eats one of the poisoned food sources, giving visual feedback to the player.
The background has been changed to a checkered pattern instead of the a plain black background. I made this change to make it easier for the player to determine when the snake is in the same row or column as the food. I found the early part of the original game frustrating, it was difficult to line up the snake with the food from a long distance when the food was not close to the edges. Adding this geometric pattern makes it easier to line up the snake. The coloured backround also makes the game more visually appealing.
The implementation of the checkered pattern demonstrates the understanding of c++ functions and control structures in a few different ways.
- An understanding of functions is demonstrated in the introduction of the two new functions
InitGrid
andRenderGrid
. These functions can be found on lines 44 and 106 of renderer.cpp. TheInitGrid
function was created to perform the, one-time-only, work of caching the co-ordinates of the rectangles that are rendered to create the checkered pattern background. TheRenderGrid
function encapsulates the logic of looping over the rectangles' co-ordinates, alternating the colors and rendering the rectangles on the background, each loop of the game engine. - An understanding of c++'s control structures is demonstrated in the two new functions
InitGrid
andRenderGrid
. TheInitGrid
function uses a nested for loop to enumerate the co-ordinates of the rectangles that will be rendered to the screen, and store them in an array. This code can be found on lines 49 - 56 of renderer.cpp. TheRenderGrid
function uses both a for loop and an if-else statement to implement the logic required to render the correctly colored rectangle to the screen. This code can be found on lines 110 - 119 of renderer.cpp.
The implementation of the checkered pattern demonstrates the use of arrays and constant values.
- The
grid_rectangles
andtotal_num_rect_in_grid
variables are constants defined in renderer.h. Thetotal_num_rect_in_grid
variable holds the calculated number of rectangles needed to form the checkered pattern of the background; which is used to initialize thegrid_rectangles
array. Thetotal_num_rect_in_grid
variable is set as part of theRenderer
constructor's member initialization list in renderer.cpp, on line 10. Thegrid_rectangles
array is populated in theRenderer
constructor when theInitGrid
function is called on line 35 in renderer.cpp. - The newly introduced classes
Food
(declared in food.h),Poison
(declared in poison.h), andLocation
(declared in location.h) are all immutable.
- Whilst the game is running, the escape key can be pressed to quit the game. The user input handler (
Controller
defined in controller.h) has been refactored. As part of the refactoring, an enumUserInput
was introduced to define the possible input options - including a newquit
type. The implementation of theHandleInput
function (found in the controller.cpp file) was modified to detect when the escape key has been pressed by the player.
- The abstract class
Interactable
has been added (found in interactable.h). Two implementations of theInteractable
interface have been added -Food
(declared in food.h) andPoison
(declared in poison.h). TheLocation
class has also been added to store the co-ordinates of derrived,Interactable
classes.
- The abstract class
Interactable
provides two constructors that utilize member initialization lists. TheFood
andPoison
classes also define, two convinience constructors that utilize member initialization lists. - The
Location
class also utilizes member initialization lists.
All of the classes in the project have been designed this way, the most interesting examples are in the more complex classes.
- The
Renderer
class (defined in render.h and implemented in render.cpp) has been updated to include two new functionsInitGrid
andRenderGrid
. The definition of the functions' interfaces does not how what data type is used to store the data or how the logic is implemented. The names of the functions are pretty self explanatory. - The
Snake
class has been updated to include two new functions -IsOpositeDirection
andHandleInput
(defined in snake.h and implemented in snake.cpp from lines 96 - 144). The logic was refactored out of theController
class and into theSnake
class in order to decouple the two classes.
- The
Interactable
abstract class inFood
andPoison
derrived classes demonstrate the behavior of following an appropriate inheritance hierarchy with virtual and override functions. TheGetType
function is a virtual function of theInteractable
abstract class and overridden in the derrived classes.
- A template
PlaceInteractable
has been introduced to generalize the logic for placing an interactable item (e.g. food or poison). The implementation of this template can be found in game.h lines 43 - 56. - The
Location
class has a template to provide an implementation of a hashing function for the type - based on the logic in the std::hash documentation. The implementation of this template can be found in location.h lines 17 - 25.
- The
Snake
classes updatedUpdate
function uses pass-by-reference for thestd::mutex
andstd::conditional_variable
function arguments. Thestd::mutex
can not be copied, and it wouldn't make sense to create a copy of thestd::conditional_variable
argument since the thread is waiting on notify to be called (to inform the snake that a new frame is being created). The updated code can be found on line 6 of snake.cpp. - The
Game
classes newly introducedIsLocationOccupied
function uses pass-by-reference to avoid copying theLocation
function argument. This addition can be found on line 114 of game.cpp.
- The
Renderer
class' destructor has been updated to delete the dynamically, heap allocated arraygrid_rectangles
. This addition can be found on line 41 of renderer.cpp.
Variables have been scoped as locally as possible througout the project and RAII-compliant standard classes have been leveraged.
- For example, the
latest_input
variable of typestd::shared_ptr
is scoped to within theGame
'sRun
function (found on line 25 of game.cpp). This makes sure that the user's input can be communicated between classes through a pointer, and the destructor is called when theRun
function terminates. - Another example, is the
lastTickLock
variable of typestd::unique_lock
. This is also scoped to within theGame
'sRun
function (found on line 29 of game.cpp). This makes sure that the lock associated with the class' mutex is unlocked when theRun
function terminates.
The project makes use of std::shared_ptr
s to share data between classes and threads.
- The data associated with the
Snake
instance is required both in theGame
andRenderer
classes. Astd::shared_ptr
is used pass a pointer to theSnake
instance to theRenderer
class. ThisSnake
's instance is created and the ownership passed to the Smart Pointer in theGame
's constructor (found on line 12 of game.cpp). - The
Snake
class needs to check what the most recent user input was each frame, and to also check when the last frame occurred to guard against spurious wakeups. This is accomplished usingstd::shared_ptr
s to theGame
-ownedlast_tick
andlatest_input
variables. The code for theSnake
's input function can be found on lines 6 - 35 of snake.cpp. - The
Game
's map of object locations (objects
- see line 24 of game.cpp) is needed in both theGame
andRenderer
classes. Astd::shared_ptr
is used pass a pointer to the instancestd::unorded_map
to theRenderer
class. This make sure that the game's view of the world state is being displayed correctly without copying the map or data structures. The design also allows for there to be more objects in the game world in a memory efficient way.
The Snake
class has been updated to run in its own thread. The Snake
's Update
function has been modified to run in an infinite loop, until one of the loss conditions is reached.
The Snake
's instance is created and the ownership passed to the Smart Pointer in the Game
's constructor (found on line 12 of game.cpp). A thread is then created using the std::thread
template in the Game
's Run
function (defined on line 26 of game.cpp).
A std::mutex
and a std::unique_lock
are used in the project (alongside of a std::condition_variable
) to communicate from the Game
's , main, thread to the Snake
's thread that a new frame is being created.
The std::mutex
is defined in game.h on line 26. The std::unique_lock
on the std::mutex
is created in game.cpp on line 29.
A std::condition_variable
is used to communicate from the Game
's , main, thread to the Snake
's thread that a new frame is being created.
The std::condition_variable
is defined in game.h on line 27. The std::condition_variable
's wait
function is called in the snake.cpp class on line 9 - as part of the Snake
class' Update
function. The std::condition_variable
's notify_all
function is called in game.cpp on line 45 as part of the Game
's Run
function. The notify_all
function was chosen so that the design could accomodate multiple threads waiting on a tick event e.g. if another snake was introduced to the game or new enemy types were added.