A Rudimentary 3D Game Engine, Built with C++, OpenGL and GLSL

Published September 21, 2015 by dimi309, posted by dimi309
Do you see issues with this article? Let us know.
Advertisement

The small3d game engine is no longer using a package manager, but self contained scripts that build its dependencies. More details, source code, documentation and sample games can be found here: https://www.gamedev.net/projects/515-small3d/.

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:

design.png

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:

components.png

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.

GoatBoundingBoxes.png

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.

CollisionDetection.png

Dependency Management

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.

Conclusion

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.

Changes

[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 :)

Cancel Save
0 Likes 18 Comments

Comments

ongamex92

No offence, but why does this exists under "Articles" category, how approves those things?

September 10, 2015 05:13 PM
dimi309
Uhm... That's not very nice to hear the way you put it, but I sure am open to constructive criticism :)
September 10, 2015 05:47 PM
Ravyne

Articles are a fairly broad categorization, so I don't think its misplaced. I've seen an article that was not much more than "this new graphics card is faster than last years", and voiced concern that it was a bridge too far, but this one here seems fine in my estimation, even if I agree a blog entry might be a better match in its current form.

For the OP: for constructive criticism, I would say that there's a lot more value you can tap from this experience and fit into this article (or perhaps follow-on articles, though committing to a series of any size is always more difficult). Simply sharing what you've learned through the process of creating this small engine would add value worthy of the 'Article' badge for the audience here.

For example, everyone always says that writing your own engine is hard, the smaller scale of your engine not withstanding, did you find it to be as hard as people say? What solutions/patterns did you apply to overcoming the architectural challenges inherent in something with so many desperate moving parts? What turned out to be harder (or easier) than you expected? What early design decisions made a later design decision easier or harder to implement, and why? What's unique or noteworthy in your approach, why did you choose it, did it work out as expected, and what are the pros/cons? What major refactoring did you undertake, if any, and why did it become necessary?

Stuff like that elevates articles like this from "Here's what I did, I hope you can learn from it. Here's an orientation, now go dive in." to "Here's what I did, and what I learned from the experience. Some things worked out great, others proved to be problematic. Here's where I found the speed-bumps. Check out my code for all the details."

September 10, 2015 06:25 PM
dimi309
[quote name="Ravyne" timestamp="1441909519"]Articles are a fairly broad categorization, so I don't think its misplaced. I've seen an article that was not much more than "this new graphics card is faster than last years", and voiced concern that it was a bridge too far, but this one here seems fine in my estimation, even if I agree a blog entry might be a better match in its current form. ...."[/quote] Thank you Ravyne for your comments. I do see your point. The thing though is that I had discussed a lot of these matters in a previous post about a year ago (http://goo.gl/2iwxOy) before I had decided to make an engine out of the game. So I was afraid of repeating myself but maybe you are right and I should have added more details about this stage of the project.
September 10, 2015 06:48 PM
Krohm

I think it is CourierNew is useful when referring to code instead of using <>.

I'd appreciate some elaborations on the Design & architecture section, the current observations are almost trivial. Do you use any kind of acceleration structure? Is there something you need to vent regarding collision?

It all feels like a 10'000 ft view.

September 11, 2015 06:02 AM
dimi309
[quote name="Krohm" timestamp="1441951338"]I think it is [font='courier new', courier, monospace]CourierNew[/font] is useful when referring to code instead of using <>. I'd appreciate some elaborations on the [i]Design & architecture[/i] section, the current observations are almost trivial. Do you use any kind of acceleration structure? Is there something you need to vent regarding collision? It all feels like a 10'000 ft view.[/quote] Many thanks for the feedback Krohm! I think all comments point in the same direction. I will update the article (asap) accordingly.
September 11, 2015 06:44 AM
Aardvajk
Your code would benefit greatly from some more modern use of smart pointers and container classes. For example:

  BoundingBoxes::BoundingBoxes() {
    initLogger();
    vertices = new vector<float *>();
    vertices->clear();
    facesVertexIndexes = new vector<int *>();
    facesVertexIndexes->clear();
    numBoxes = 0;
  }

  BoundingBoxes::~BoundingBoxes() {
    if (vertices != NULL)
      {
    for (int i = 0; i != vertices->size(); ++i)
      {
        delete[] vertices->at(i);
      }
    vertices->clear();
    delete vertices;
    vertices = NULL;
      }

    if (facesVertexIndexes != NULL)
      {
    for (int i = 0; i != facesVertexIndexes->size(); ++i)
      {
        delete[] facesVertexIndexes->at(i);
      }
    facesVertexIndexes->clear();
    delete facesVertexIndexes;
    facesVertexIndexes = NULL;
      }
  }
I see absolutely no need for the containers themselves to be dynamically allocated, and you are introducing nightmare scenarios for trying to ensure exception safety when using these old-fashioned approaches to dynamic allocation and release.

Indeed a simple std::vector<float> could remove all need for any manual allocation and deletion here, vastly simplifying your code and making it behave far better in an exception-throwing environment, not to mention the cache coherency which is potentially terrible by allocating each float individually like this.

I don't mean to be over critical, but a quick review of your source finds it very messy and full of potential issues that can easily be avoided.

That said, congratulations on getting this finished and to this point.
September 11, 2015 10:37 AM
LatroA

Your code would benefit greatly from some more modern use of smart pointers and container classes. For example:


<...snip...>
Indeed a simple std::vector<float> could remove all need for any manual allocation and deletion here, vastly simplifying your code and making it behave far better in an exception-throwing environment, not to mention the cache coherency which is potentially terrible by allocating each float individually like this.

I don't mean to be over critical, but a quick review of your source finds it very messy and full of potential issues that can easily be avoided.

That said, congratulations on getting this finished and to this point.

I read a comment like this and I realize my own coding is 20 years out of date (which, coincidentally, is the last time I wrote code professionally.)

Any idea where to start in order to "catch up" ?

September 11, 2015 11:42 AM
dimi309
[quote name="Aardvajk" timestamp="1441967870"]Your code would benefit greatly from some more modern use of smart pointers and container classes. For example: BoundingBoxes::BoundingBoxes() { initLogger(); vertices = new vector(); vertices->clear(); facesVertexIndexes = new vector(); facesVertexIndexes->clear(); numBoxes = 0; } BoundingBoxes::~BoundingBoxes() { if (vertices != NULL) { for (int i = 0; i != vertices->size(); ++i) { delete[] vertices->at(i); } vertices->clear(); delete vertices; vertices = NULL; } if (facesVertexIndexes != NULL) { for (int i = 0; i != facesVertexIndexes->size(); ++i) { delete[] facesVertexIndexes->at(i); } facesVertexIndexes->clear(); delete facesVertexIndexes; facesVertexIndexes = NULL; } } I see absolutely no need for the containers themselves to be dynamically allocated, and you are introducing nightmare scenarios for trying to ensure exception safety when using these old-fashioned approaches to dynamic allocation and release.Indeed a simple std::vector could remove all need for any manual allocation and deletion here, vastly simplifying your code and making it behave far better in an exception-throwing environment, not to mention the cache coherency which is potentially terrible by allocating each float individually like this.I don't mean to be over critical, but a quick review of your source finds it very messy and full of potential issues that can easily be avoided.That said, congratulations on getting this finished and to this point.[/quote] Thanks, also for the tips! I'll improve the code according to those and to everything else I manage to educate myself with :)
September 11, 2015 01:16 PM
dimi309

Articles are a fairly broad categorization, so I don't think its misplaced. I've seen an article that was not much more than "this new graphics card is faster than last years", and voiced concern that it was a bridge too far, but this one here seems fine in my estimation, even if I agree a blog entry might be a better match in its current form.

.....

I think it is CourierNew is useful when referring to code instead of using <>.

I'd appreciate some elaborations on the Design & architecture section, the current observations are almost trivial. Do you use any kind of acceleration structure? Is there something you need to vent regarding collision?

It all feels like a 10'000 ft view.

Your code would benefit greatly from some more modern use of smart pointers and container classes. For example:
.....

Hi everyone, thanks again for taking the time to give me your feedback. I have updated the article. I hope you like it better now but, if not, feel free to say so. Negative feedback is much more useful than compliments!

About the code, I will improve it as was suggested. I was planning to work on it further, anyway. But I will not make another release before I am sure that everything is stable after the changes. If you are interested in the progress, make sure to have a look at the GitHub repository from time to time.

Finally, Khrom, unfortunately I could not change the style of the classes to CourierNew. I have tried adding formatting elements but it does not seem to work. The <> symbols appear if I surround the classes by the "datatype" bbcode from the editor. I have not found something more appropriate, even though I agree that with your suggestion it would look better. If anyone can propose something with regard to this, please let me know.

September 11, 2015 08:04 PM
Dave Hunt

Finally, Khrom, unfortunately I could not change the style of the classes to CourierNew. I have tried adding formatting elements but it does not seem to work. The <> symbols appear if I surround the classes by the "datatype" bbcode from the editor. I have not found something more appropriate, even though I agree that with your suggestion it would look better. If anyone can propose something with regard to this, please let me know.

I would just use italics.

September 12, 2015 12:40 AM
dimi309

I would just use italics.

Good idea. Done! Thanks :)

September 12, 2015 12:51 AM
Aardvajk

Your code would benefit greatly from some more modern use of smart pointers and container classes. For example:


<...snip...>
Indeed a simple std::vector<float> could remove all need for any manual allocation and deletion here, vastly simplifying your code and making it behave far better in an exception-throwing environment, not to mention the cache coherency which is potentially terrible by allocating each float individually like this.

I don't mean to be over critical, but a quick review of your source finds it very messy and full of potential issues that can easily be avoided.

That said, congratulations on getting this finished and to this point.


I read a comment like this and I realize my own coding is 20 years out of date (which, coincidentally, is the last time I wrote code professionally.)

Any idea where to start in order to "catch up" ?


My rules of thumb (not hard rules)

- Only use dynamic allocation when you need to
- Where you must allocate dynamically, avoid manually calling delete by using container classes or smart pointers where possible
- Where off the shelf containers/smart pointers aren't sufficient, consider expressing your specialist logic in your own classes that take advantage of RAII/automatic destructors/class invariants
- Try to model the conceptual lifetime of data with scope lifetime in the program

Not meant as great commandments, just the approach I have found useful over the years to automate things for myself.
September 12, 2015 07:48 AM
dimi309

My rules of thumb (not hard rules)


- Only use dynamic allocation when you need to
- Where you must allocate dynamically, avoid manually calling delete by using container classes or smart pointers where possible
- Where off the shelf containers/smart pointers aren't sufficient, consider expressing your specialist logic in your own classes that take advantage of RAII/automatic destructors/class invariants
- Try to model the conceptual lifetime of data with scope lifetime in the program

Not meant as great commandments, just the approach I have found useful over the years to automate things for myself.

Thank you for sharing! I've just made the first small correction to BoundingBoxes...

September 12, 2015 01:59 PM
Geometrian

Whatever happened to "Write Games not Engines"?

September 17, 2015 03:19 AM
dimi309

Whatever happened to "Write Games not Engines"?

It's sound advice. I set out to just make a game initially, but since I was coding almost everything by hand I ended up making an engine too, so that I can "write games not engines" next time :)

September 17, 2015 05:07 AM
dimi309

Your code would benefit greatly from some more modern use of smart pointers and container classes.

I have uploaded a new version of the source code with a lot of dynamic allocations removed and some other modifications. There will be more improvements over time, since I will keep working on it, also adding features to the engine as I develop a new game. I hope it is better now. I am also checking it with a static analysis and code quality tool, doing what I can to correct it as much as possible :) As I've already mentioned, the latest version is always available on GitHub.

September 19, 2015 07:33 AM
emilyjane

Thanks for the interesting post!

March 15, 2016 06:01 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!

Engine to be used as an aid in learning how to develop 3D games that compile on Windows, OSX and Linux, with C++, OpenGL and GLSL. It works with a dependency manager.

Advertisement
Advertisement