A two-player game for fans of Pong and the Utah teapot!
Click on the image above to see a two minute demo of Teapong on YouTube!
This project was an attempt to write a simple game from scratch as cleanly as possible. Special focus was placed on the following goals:
- Use modern C++ and modern OpenGL.
- Keep the code cross-platform so that the game runs on macOS and Windows.
- Find a flexible way to manage resources (e.g. textures, models, shaders, etc.).
- Find an organized way to represent game states (e.g. menu, play, pause, etc.) and make them fully independent and encapsulated.
The first three goals were achieved successfully, but the last one was not, which led to code that does not scale well in the game state layer. This failure is explained in the "Game state management" subsection below.
The other subsections provide information on the libraries used by this project and the sources of the game assets, as well as some details on resource management, shading, collision detection and how to make a teapot explode.
For additional information on this project's code, see this presentation.
The libraries used by this project and their purposes are the following:
- GLFW is used to interact with the windowing system and to receive inputs.
- GLAD is used to load pointers to OpenGL functions.
- GLM is used to perform 3D math.
- Assimp is used to load 3D models.
- stb_image is used to load textures.
- irrKlang is used to play sounds.
This website explains how to use each of the libraries listed above with excellent clarity.
The sources of the assets used by this project are the following:
- The 3D models and textures were created using 3ds Max.
- The sound effects can be found here.
- The background music is Filaments by Podington Bear, and it can be found here.
The resouce manager used by this project was inspired by this code from the EnTT library. It follows these principles:
- The resource management code (resource_manager.h) is separated from the resource loading code (texture_loader.h, model_loader.h and shader_loader.h).
- A resource manager instance can only manage one type of resource (e.g. texture.h, model.h or shader.h).
- Resources are not deleted automatically if they are not being used. The user must make a request for them to be deleted.
The implementation of the resource manager may seem a bit complex because it makes use of variadic templates and perfect forwarding, but it is thanks to those C++ features that it is super flexible and easy to use:
The design pattern used by this project for game state management is a modified version of the State design pattern, which is explained brilliantly in this article.
The fundamental ideas behind my implementation of that pattern are the following:
- Each state is represented by a class (e.g. menu_state.h, play_state.h, pause_state.h and win_state.h).
- Each state is required (by inheriting from state.h) to implement the three functions that are always called in the game loop:
processInput
,update
andrender
. - Each state only has access to the resources that it needs, and it can share its resources with other states to facilitate communication between states.
- Each state is responsible for checking the conditions that could lead to a state change, and it must notify the finite state machine that contains all the states (finite_state_machine.h) to make a transition occur.
The state diagram below illustrates the states that make up this project, and the events that cause the transitions between them to occur:
So how did this design pattern lead to code that does not scale well? The root of the problem is the way I made states communicate with each other: by sharing resources.
To illustrate why this was a bad decision, consider the following situation:
- Let's say that state A has the teapot at position X.
- Now let's say that we need to transition to state B, where the teapot will be placed at position Y.
- If the teapot needs to go back to position X if we ever switch back to state A in the future, then state A needs to remember position X.
- But what if both states share the same teapot? As soon as the transition occurs, state B will change the position of the teapot to Y, and X will be lost.
- This means that the position of the teapot behaves as a global variable, so to avoid losing X, state A needs to maintain a variable external to the teapot where it stores that value.
Now imagine the same situation, but with dozens of states and shared resources. The code quickly becomes tangled and difficult to maintain.
To prevent this mess we must only share resources that do not require any external variables to be maintained, and we must use a different system to allow states to communicate with each other.
"What does that other system look like?" you might ask. That is a question that I am still trying to answer. If you have any suggestions, please let me know!
Shading is done using the Phong reflection model.
By pretending that the paddles are two-dimensional rectangles and that the teapot is a two-dimensional circle, collisions are detected using simple equations for Axis-Aligned Bounding Boxes (AABBs) and circles.
A simple geometry shader is used to make the teapot explode by pushing each polygon along its normal.
The rules are simple: the first player to score three points wins!
The controls are as follows:
- Press Esc to close the game.
- Press F to toggle between the full screen and the windowed modes. When in the windowed mode, you can manually resize the window using the mouse.
- Press 1, 2, 4 or 8 to set the number of samples used for anti-aliasing. By default, the game starts with 1 sample. The higher the number of samples, the better the game looks.
- When in the menu, press Space to start a game.
- When ready to play, press Space to launch a teapot.
- The left paddle is controled with G and B, while the right paddle is controlled with Up and Down.
- Press P to pause the game.
- Press C to toggle between the fixed and free camera modes. When the camera is free, you can position it using W, A, S, D and the mouse. You can also zoom in and out using the scroll wheel.
- Press R to reset the camera to its original position.
To run Teapong on macOS, you must follow these steps:
- Download or clone this repository and open a Terminal window in its root.
- Execute the following command to install GLFW, GLM and Assimp:
$ brew install cmake assimp glm glfw
- Execute the following commands to install GLAD:
$ cp dependencies/mac/inc/glad/glad.h /usr/local/include/
$ cp dependencies/mac/inc/KHR/khrplatform.h /usr/local/include/
- Execute the following commands to install irrKlang:
$ cp -r dependencies/mac/inc/irrklang /usr/local/include/
$ cp -R dependencies/mac/lib/ /usr/local/lib/
- Download Teapong_macOS.zip from release 1.0.0, open a Terminal in its root directory and execute the following command to launch the game:
$ ./Teapong
To run Teapong on Windows, simply download Teapong_Windows.zip from release 1.0.0 and double click Teapong.exe.
To build Teapong on macOS, you must follow the same steps listed in the "How to run Teapong" section, except for the last one, which you must replace with the following:
- Execute the following command to build the game:
$ make Teapong
Thanks to Daniel Macario for writing the Makefile!
To build Teapong on Windows, simply download or clone this repository and use the Visual Studio 2019 solution file that is stored in the VS2019_solution directory.
I hope that this project helps anyone who decides to embark on a similar adventure.
All my life I have wanted to build a game, and I always knew that when I finally did it, I would do it from scratch, just to understand every single little detail about it. It is not easy or practical to build a game this way, but I think it is the most insightful and rewarding way.
How I felt when I started working on this project.