Skip to content

My first attempt at writing a from-scratch OpenGL game engine in C++

Notifications You must be signed in to change notification settings

BGR360/CppGameEngine3D

Repository files navigation

VERSION 2 CURRENTLY UNDER CONSTRUCTION!! :D

CppGameEngine3D

My first attempt at writing a from-scratch OpenGL game engine in C++, humbly called the "Bengine." The Qt GUI Framework is used extensively to provide OpenGL Widgets and make up the GUI for the Bengine Editor (learn more about the Bengine Editor here). The entire engine was programmed using Test-Driven Development strategies, utilizing the Google Test Framework to swiftly implement unit tests (learn more about the Bengine Tests here).

Demo Video

A demonstration of the latest capabilities of the engine and the editor:

Engine Demo

Jump To:

  1. Overview
  2. The Core Engine
  3. Dynamic DLL
  4. OpenGL
  5. Math Library
  6. GameObject API
  7. Resource Management
  8. Rendering Engine
  9. RenderingEngine
  10. MeshRenderer
  11. Meshes
  12. Shaders and Uniform Variables
  13. Materials
  14. The Editor
  15. Qt Framework
  16. Data-Driven Design
  17. The Tests
  18. Google Test
  19. Believing in Test-Driven Development

Overview

After transitioning from a Windows to a Macbook, I aborted this C++ version of my game engine for a revamped Java version (see https://github.com/BGR360/JavaGameEngine3D). However, I then discovered that that was a silly decision and have stopped working on the Java version. I have not put any work into this engine for a while and will probably restart from scratch if I want to continue working on it.

Why do I do this?

When I tell people about this project, I often hear them say, "You know, you could always just download Unity or UDK and use that to make games... Why you gotta go and write your own?" So really, why the hell am I doing this?

I'm not making a game engine so that I can make games. Sure, once it's done, I'll be able to do that if I want, but that's not what it's about. It's about the learning experience and the sense of accomplishment that comes with being able to write something from scratch. Sure, it'll never ever be the most efficient or groundbreaking or innovative game engine, but it'll be mine. I think that's pretty damn cool.

Acknowledgements

I owe many thanks to Jamie King and Benny Bobaganoosh for their great tutorials and learning resources on C++, OpenGL, and Game Engine programming. These guys do great work and you should check them out.

Jamie King: YouTube -- Website

Bennybox: YouTube -- GitHub

The Core Engine

The Core engine functionality was written entirely by me in standard C++11.

See Also: Core Engine Source Code

Dynamic DLL

The Engine is implemented as a DLL, which at times led to some annoying problems with DLL-boundaries and memory allocation, but was ultimately meant to make the engine more robust and easy to use.

OpenGL

Access to OpenGL function calls was provided by GLEW (OpenGL Extension Wrangler). A rendering context and surface upon which to render was provided by Qt's QOpenGLWidget class. Aside from this, all rendering code in my engine is pure OpenGL function calls, no high-level garbage.

See Also: Include folder

Homemade Math Library

One of the most important things required for a 3D game engine is a solid Vector and Matrix math library so that Matrix-Vector transformations in 3D space can be made easily. All of the common Vector and Matrix operations were coded by hand by me. I'm not going to say it was fun.

See Also: Math Library Source Code

GameObject API

The overall design of the Bengine external API is the GameObject. The GameObject API that I wrote is very similar to the structure of the Unity game engine.

Composition over Inheritance

Inheritance and polymorphism are great, but they certainly have their limitations, especially in the game world. For instance, you might define a class of objects that is Movable and one that is Static, and another class of objects that is Renderable. The problem is that a GameObject (for instance, a NonPlayerCharacter) may be Movable and Renderable. This leads to a diamond-shaped inheritance tree, which is a problem.

             GameObject
             /        \
            /          \
      Renderable      Movable
            \          /
             \        /
         NonPlayerCharacter

The solution is Composition. Instead of using multiple inheritance, a GameObject is comprised of multiple GameComponents which define its behavior. This leads to a much safer sort of inheritance tree:

                GameComponent                                   GameObject
              /               \                                     |
             /                 \                       +------------|-----------+
      PhysicalComponent       RenderableComponent      |   NonPlayerCharacter   |
       /            \                                  +------------------------+
      /              \                                 | - MovableComponent     |
StaticComponent   MovableComponent                     | - RenderableComponent  |
                                                       +------------------------+

In the Bengine, every GameObject has a list of GameComponents which can be dynamically assigned. The GameComponents define the GameObject's behavior at runtime by specifying a custom update() function. For example:

// Create GameObject and add Components
GameObject go;
go.addComponent(new MeshRenderer);
go.addComponent(new PhysicsComponent);

// Get Component and modify properties
MeshRenderer* renderingComp = go.getComponent<MeshRenderer>();
renderingComp.setMesh(aMesh);
renderingComp.setMaterial(aMaterial);
Parenting GameObjects

See Also: GameObject API

Resources

For any game to function well, there needs to be a centralized way to manage the assets (or Resources) in a scene. This is done through the ResourceManager class. In hindsight, the design of ResourceManager is pretty poor and hardly object-oriented, but it worked.

The ResourceManager stores all resource-like object (Images, Textures, Materials, Meshes, Shaders) in maps so that each resource is mapped to a name (a string). To obtain a reference to a resource in-game, call one of the get() functions with the name of the resource.

// Load resources
ResourceManager res;
res.loadMesh("res/meshes/monkey.obj", "monkey");
res.loadShader("res/shaders/basic_shader.vs", "basic_shader");
res.loadTexture("res/textures/bricks.jpg", "bricks", 0);

// Create a material
res.createMaterial("bricks", res.getShader("basic_shader"), res.getTexture("bricks"));

// Create a Monkey GameObject
GameObject monkey;
monkey.addComponent(
    new MeshRenderer(
        res.getMesh("monkey"),
        res.getMaterial("bricks")
    )
);

See Also: Resources Source Code

Rendering Engine

Designing the Rendering Engine was one of the most difficult parts of the whole project. I really struggled with finding my own way to make a robust object-oriented system. What I ended up with definitely worked but was far from elegant (at least in my eyes).

RenderingEngine

The RenderingEngine class is responsible for rendering the Game's current Scene. Each Scene has its own ResourceManager and root GameObject. The RenderingEngine traverses through the GameObject tree and calls render() on any GameObjects which have RenderingComponents.

The strange thing is that RenderingEngine inherits from Qt's QOpenGLWidget. This is what throws off much of the system's object-orientedness. I had to do this because of how Qt works with OpenGL. When using Qt, any calls to OpenGL functions must occur within the QOpenGLWidget's paintGL() function.

MeshRenderer

Any GameObject which can be rendered has a RenderingComponent. Currently, the only known RenderingComponent in the Bengine is the MeshRenderer. The MeshRenderer stores a pointer to a Mesh and a Material, which is all that it needs to properly render a mesh (because a Material has a pointer to a ShaderProgram).

Meshes

A Mesh is defined by a set of vertices, normals, and UV coordinates. Meshes are also indexed to help save space. Currently, only .obj files can be loaded to create Meshes. You can view the new-and-improved obj parsing algorithm here.

Shaders and Uniform Variables

The compilation and loading of GLSL Shaders in the Bengine is done through the Shader (.h .cpp) and ShaderProgram (.h .cpp) classes. Just like in OpenGL, a ShaderProgram is comprised of multiple Shaders at different stages (the Vertex Shader, Fragment Shader, Geometry Shader, etc.).

The system I created for shader uniform variables, I think, was a good one. The whole UniformVariable class (.h .inl .cpp) was templated so that different datatypes could specify how they are bind()ed to the shader program.

One really cool thing is that Uniform Variables are automatically detected while parsing the GLSL source code. Seriously, check it out!

Materials

In the Bengine, a Material is defined by a Texture and a ShaderProgram which is used to render it. The creation of Materials is done by the ResourceManager (see here).

See Also: Rendering Source Code

The Editor

See Also: Editor Source Code

Qt Framework

See Also: Qt Home Page

Data-Driven Design

See Also: Example Game and Scene Files

The Tests

Google Test

See Also: Google Test Framework

Believing in Test-Driven Development

See Also: All of the Testing Code

About

My first attempt at writing a from-scratch OpenGL game engine in C++

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published