Minimal game engine written in C++ using OpenGL

In this post I would like to showcase a game engine based on the OpenGL API, developed in C++. It is still in an early stage and only covers the bare minimum that is necessary to qualify as a game engine. Nontheless, it provides some basic classes, which hide a lot of OpenGL calls and provide methods to create simple games quickly. In the following, I will demonstrate how to set up a “Hello World” game and touch on some of the engine’s classes and their purpose.

#include "Game.h"
#include "Camera.h"
#include "Cube.h"

class HelloWorld : public Game
{
public:
	~HelloWorld();

private:
	void init();
	void update();
	void draw();

	Shader* shader;
	Cube* cube;
	Camera* camera;
};

First of all, we create a new class for our game and let it derive from Game. The Game class provides virtual initupdate and draw methods, which will later be implemented by the HelloWorld class and build a skeleton for our game logic. Additionally, Game initializes GLFW, a third party library for window and context creation as well as input handling.

void HelloWorld::init()
{
	shader = new Shader("vertex.glsl", "fragment.glsl");
	glUseProgram(shader->id);

	cube = new Cube("texture.png");

	camera = new Camera(screenWidth, screenHeight);
	camera->position = glm::vec3(0, 2.5f, 2.5f);
	camera->direction = glm::vec3(0, -1, -1);
	camera->updateProjectionView(shader);
}

Next up is implementing the init method, which will be called only once at the start of our game and is meant for all kinds of initialization that is needed before rendering the first frame. Here we assign a new Shader instance, providing vertex and fragment shader files, which we will implement in the next step. The Shader class takes care of compiling and linking the shaders and has public methods to expose values of different types to the shader program.

Cube is one of the available primitves, besides Plane and Triangle. All primitives inherit from the WorldObject class, which holds information about position, rotation and scale of an object, but also generates and populates buffers for vertices and texture coordinates. When calling the Cube with a texture, an instance of the Texture class loads the texture data from disk and transfers it to GPU memory.

The Camera object holds important camera parameters, like field of view and clipping distances. It also has update logic for a first person flying camera. updateProjectionView needs to be called at least once and also everytime camera parameters change, because it sets up view and projection matrices and makes them available to the shader.

#version 330 core

layout (location = 0) in vec3 position;
layout (location = 1) in vec2 texCoordIn;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

out vec2 texCoord;

void main()
{
	texCoord = texCoordIn;
	gl_Position = projection * view * model * vec4(position, 1);
}

In this basic vertex shader, we calculate the fragment position by multiplying each vertex position with model, view and projection matrices and also hand over the texture coordinate of the vertex to the fragment shader.

#version 330 core

uniform sampler2D tex;

in vec2 texCoord;
out vec4 FragColor;

void main()
{
	FragColor = texture(tex, texCoord);
}

Because we earlier provided a texture for the cube, in the fragment shader we use the texture function to color each fragment accordingly.

void HelloWorld::update()
{
	cube->rotation += glm::vec3(0, 0.2f, 0);
}

void HelloWorld::draw()
{
	cube->draw(shader);
}

HelloWorld::~HelloWorld()
{
	delete shader;
	delete cube;
	delete camera;
}

The update method is called once for every rendered frame and should contain the main game logic, like receiving player input and transforming world objects. To make something happen on the screen, we rotate our cube around the y-axis. The draw method is called once per frame as well and is meant to separate drawing from game logic. By calling the cube’s draw method we create its model matrix and instruct OpenGL to draw it into the frame buffer using our shader.

int main()
{
	HelloWorld* helloWorld = new HelloWorld();
	helloWorld->run();

	delete helloWorld;

	return 0;
}

All that is left to do is to create an instance of the HelloWorld class in the main function and run it. This is the result:

Find the engine’s source code and samples on Github, or read more about the Portals and TerrainGeneration samples.

Latest Posts

Categories

Archives

WordPress Cookie Plugin by Real Cookie Banner