In a way, this article is the continuation of the post I published about a year ago, on my little self-styled course on game development. At the time I had gotten down all the basics for rendering and animating a model of a goat I had created in Blender.
The process and the difficulty of it all
What I was doing until I reached that point, in order to motivate myself and not leave an another ambitious yet half-finished project somewhere on the web or a hard disk, was to keep up the habit of writing a series of blog posts on my personal website about progress made, once a month more or less.
That worked out pretty well. Each post helped me organise what I had learned during every iteration and have it somewhere written in my own way, so that I never forget. Publishing these progress notes also allowed me to have some feedback from time to time, as well as encouragement (I have no game developer friends so I have to rely on the kindness of strangers).
There was a lot of work to do. You see, even though had taken about two semesters of C++ programming in University a long time ago, I had never worked with it professionally. My professional life revolved around C#, Java and PowerBuilder. My relationship with math was pretty much comparable to my relationship with C++ and, finally, about 3D modelling skills, I guess they were elementary, as they still are. But hey, I know how to model a goat!
I did have the choice of selecting a couple of these areas to focus on, and find solutions to save me time from the rest, like using something like Unity or acquiring a couple of ready made 3D models. As a matter of fact, a quick scanning of game development courses which I performed on line showed that that is the way the industry is going now and you have to specialise on something. But I wanted to have a sense of every aspect of making a game, coding, writing shaders, putting together the game loop, collision detection and the like. So even though I found no evidence that this was a good idea timewise I just went for it.
I do not regret doing it. I now have this little engine put together and, indeed, I do have a sense of what it takes to make one. The problem while working like this, at least for me, is that many times you feel like you are fighting against your own brain. Just as you get comfortable modelling something and you feel like doing more of that and learning more, it is time to export your work and code a bit. Or right after you have finished this complicated model reading and rendering code which puts your goat on your scene you have to go back and model the bug chasing it. And while you are switching, you do not necessarily feel confident on what you have covered or learned already. The mind has an amazing capacity to forget what it senses it will not need in the immediate future. Ask me now about why I used a dot product somewhere and how it works exactly and I will need half an hour looking at my own code and maybe going through a few pages from one of my books before I can tell you (but I will, later on). In that respect, it is much harder to be a generalist than a specialist in my humble opinion, at least if you manage to become more than a jack of all traits.
The end product
Anyway, somehow I have completed the game, making the goat controllable via the keyboard, adding a flying bug that chases it and developing the game logic, together with sound, collision detection and a tree, to make the 3D scene a bit more interesting. So as to be able to reuse a lot of the code I have written, I have reorganised the project, converting it from a one-off game codebase to a little game engine (I have named it small3d) which comes packaged with the game as its sample use case. So we now have a full game. The engine abstracts away enough details for me to be able to play around with some effects, like rapid nightfall. Just to see if the camera is robust or if I was just lucky positioning it in the right place, I have also tried sticking it on the bug, so as to see the scene through its eyes, as it chases the goat.
I suppose it can be said that small3d is not really a game engine but a renderer packed with some sound and collision detection facilities. This is the current list of features:
- Developed in C++
- Using OpenGL (v3.3 if available, falling back to v2.1 if not)
- Using GLSL (no fixed pipeline)
- Plays sounds
- Offers bounding box collision detection
- Reads models from Wavefront files and renders them
- Provides animation out of a series of models
- Textures can be read from PNG files and mapped to the models
- Alternatively the models can be assigned a single colour
- PNG files can also be rendered as independent rectangles
- Provides text rendering
- Provides basic lighting
- Provides camera positioning
- It has been released with a permissive license (3 - clause BSD) and only libraries with the same or similar licenses are referenced
- Allows for cross-platform compilation. It has been tested on Windows 7, 8 and 10, OSX and Debian.
- It is available via a dependency manager
Design & Architecture
These are the main classes that make up the engine:
A SceneObject is any solid body that appears on the screen, be that a character (like the goat) or an inanimate object, like the tree. The SceneObject is represented visually by Models, which are loaded from WaveFront files by the WaveFrontLoader. ModelLoader is a generalisation of WaveFrontLoader, which provides the option of developing loaders for other file formats in the future, always conforming to the same interface. The SceneObject can also accept an Image to be mapped on the Model. Finally, if some boxes are created in a tool like Blender, properly positioned over a model and exported to a separate Wavefront file, the SceneObject can pick them up using the BoundingBoxes class and provide some basic collision detection.
The Renderer can render Models provided by the SceneObjects. It uses the Image class, either for holding textures to be mapped to the Models, or to be rendered as separate rectangles. These rectangles work as objects of the scene themselves and can be used for representing the ground, the sky, splash screens, etc.
The Text class can be used to load text and display it on the screen, via the Renderer.
The Sound class works as a sound library, loading sounds into SoundData objects and playing them when given the relevant instruction.
Finally, the Exception and Logger classes are used throughout the engine for reporting errors and logging, as their names imply. They can also be used by the code of each game being developed with the engine.
Even though I have avoided utilising a lot of pre-developed game facilities, some library dependencies were necessary. This is what a typical game would look like in relation to the engine and these referenced components:
There is no limitation for the game code to only go through the engine for everything it is developed to do. This allows for flexibility and, as a matter of fact, sometimes it is necessary to use some of the features from the libraries directly. For example, the engine does not provide user input facilities. The referenced SDL2 library is very good at that so it is left to the developer to use it directly.
I would not want to bore you with every little detail, but there are a couple elements that would be interesting to discuss at this point.
First of all, about my design choices, I did not base them on any literature and therein may lie the reason for any potential imperfections. I was coding each piece of functionality while learning how to do it and then, after it worked, I tried to organise the code into some classes or structures that made sense.
Initially, I was only experimenting with rendering and I can tell you that that is probably the hardest thing I have done for this project. It may be that something else like physics or AI is harder to do for a larger game. But for the purposes of this project, the first year went into rendering and animation. Once that was done, it just took me a few months to work on user input (super easy), add a splash screen (a bit less easy), develop the bug's "AI" and add collision detection.
The problem with rendering is that there are a lot of things to know about OpenGL and GLSL itself before you can even write code that actually does something. And then, once you have put together some instructions for the CPU and the GPU that are supposed to work, many things can go wrong like off-by-one errors, wrong datatypes used for pushing vertices to the GPU, wrong positioning or wrong matrices used, etc. And the only way to find out what is wrong in many cases, is to have also written code that picks up errors from the GPU, because those are not just going to get output to your screen.
I will not discuss rendering further because it will make this article a bit too long and anyway, you can figure out a lot of things by reading the literature I mention in the previous article and looking through my code.
I can mention a couple of things about collision detection and "AI" where, rather than following existing literature to the letter, I have tried to think up solutions myself, without believing of course that what I have come up with is novel in any way.
Leniently, I suppose it can be said that the bug uses some elements of AI. It does not really think. What happens is that it always detects whether or not it is moving towards the goat. The program basically calculates the dot product of the normalised horizontal component of the vector connecting the bug to the goat and the bug's direction. That is equal to the cosine of the angle between the two. If the angle is not close to zero, the bug starts turning. This way it always tries to be moving towards the goat on the horizontal plane. On the vertical one, things are much simpler. When the bug is kind of close to the goat, it takes a dive and hopes to touch it.
But how do we know when the bug has touched the goat? Well, for that I have just manually placed a couple of bounding boxes over the goat in Blender, to be used for collision detection.
An instance of the BoundingBoxes structure loads these and, when the bug is diving, it checks whether or not the two game characters are touching each other. The bug has no bounding box. It is small enough to be considered to be a point, without affecting gameplay much. A little shortcut that I have taken is that I only have the bounding boxes rotate around the Y axis, since the goat is only moving horizontally.
An interesting feature I was able to experiment with and provide for this project, is dependency management. I have discovered a service called Biicode, which allowed me to do that.
Biicode can receive projects that support CMake, with minor and (if done well) non-intrusive modifications to their CMakeFile.txt. Each project can reference other projects (library source code in effect) hosted on the service, and Biicode will analyse the dependencies and automatically download and compile them during builds. All the developer has to do is add an #include statement with the address of a desired .h file from a project hosted on the service and Biicode will do the rest. I suppose it can be said that it is an equivalent of Nuget or Maven, but for C++.
The reason I have chosen to use this service, even though it is relatively new, was speed of development. CMake is fantastic on its own as well, but setting up and linking libraries is a time-consuming procedure especially when working cross-platform or switching between debug and release builds. Since Biicode will detect the files needed from each library and download and compile them on the fly, the developer is spared the relevant intricacies of project setup.
I am not mentioning all of this to advertise the service. I find it very useful but my first commitment is to the game engine. Biicode is open source, so even if the service in its present form were to become unavailable at some point, I would either figure out how to set it up locally, go back to plain vanilla CMake (maybe with ExternalProject_Add, which would still be more limited feature-wise) or look for another dependency manager. But the way things stand right now, it is the best solution for my little project.
One difficulty I had not mentioned earlier is actually starting up OpenGL from a single codebase on various platforms. There are different libraries to link to. Moreover, on Windows and Linux it is kind of easy to check which version is available and select it. On the Mac however, you have to make an assumption about the version because there are some detection capabilities missing, at least as far as I have been able to find out. I suppose having all of these things preconfigured and offered via a library from a dependency manager is one of the awesomest things about this project. It may be silly to say that but, if you experience how nice it is to just add an #include statement pointing to some hosted rendering code and be ready to program on three operating systems without doing much else, you may see my point.
The other thing I like about the dependency manager is separation of concerns. In the same way rendering functionality can be covered and set up by one person, others can be maintaining other useful libraries. Each new project can do more things, saving time by reusing what is there and, if the project itself is a new library, adding more useful features developers can use. By keeping the libraries small and focused, a pool of ever increasing possibilities of no fuss code reuse gets created.
For example, I am planning to improve small3d but I am wondering whether or not I will add more features to it. If I want to make a platformer game, instead of adding its reusable elements to small3d itself, I can create another library called small3d_platformer. Another developer can make a small3d_shooter. This is not novel, in the sense that library reuse works that way anyway, but having it online with a dependency manager for C++ is the advantage. It makes code reuse much faster and it is also a guarantee that the various libraries will always interoperate, since a record is always kept of the relationships between specific versions. Every time someone uses one part of the "chain", it is a verification that it works, or it gets communicated that it needs to be fixed.
This article does not contain any step-by-step instructions on using the engine because, looking through the code which I have uploaded, I believe that a lot of things will be made very clear. Also, see the references below for further documentation. You will need to get started with Biicode in order for the code to compile (or convert the project to a simple CMake project, even though that will take more time).
I hope that the provided code and information will help some developers who have chosen to do things the slow way move faster in their learning than I had to. There is a lot of information available today about how to set up OpenGL, use shaders and the like. The problem is that it might be too much to absorb on one go and little technical details can take a long time to sort out.
Using my code, you can either develop your own little game quickly, help me improve this engine, or keep going on your own learning path, referring here from time to time when something you read in a book or tutorial does not work out exactly the way it is supposed to. I am using this engine to develop my own games so, whatever its disadvantages, I am putting a lot of effort into maintaining it operational at all times.
You may be wondering if I now believe that it is worth doing things the way I did or selecting a more pragmatic approach. It is really hard to say. The first thing that leaps to mind is that, just because everyone is saying that something should be done in a certain manner, it does not necessarily have to be so. Of course there is always a risk involved. You may end up stubbornly completing your self-assigned project and showing the world that you have done it your way. Or you may spend the rest of your life watching a goat walking around and wonder in old age what the big deal with it was :)
The outcome depends on many things that cannot all be known in advance. One is your background. If you are more familiar than I was with a lot of the concepts I have discussed, it will most certainly be easier for you and you will finish faster. Then there is commitment and perseverance. Just because you want to do something, it does not mean that the whole process will be fun. And finally, there is life itself. Even if you do everything right, heading towards one direction, a sort of "storm" can come and pick you up and throw you at a place where you never thought you would be.
[2015-09-10] Updated article with some corrections and more information, as requested by reader comments.
[2015-09-15] Uploaded a new version of the source code with many dynamic allocations removed and some other minor improvements.
[2015-09-22] Uploaded a new version of the source code, corresponding to the latest stable release (v1.0.2).
[2016-08-23] The biicode service has been taken offline, so a new version of the small3d source code has been attached to this article, which can be built independently. In addition to that, small3d is also available on a new package manager, called conan.io.
[2016-09-21] Removed some silly humorous elements :)