Game engine made from scratch in c++ with OpenGL to support the deployment of the game OverCyde.
First you have to clone the reposiory with submodules (i suggest forking the repo to simplify your work):
git clone --recurse-submodules https://github.com/bigmat18/OverCyde.git
The repository has the following dependecies:
- GLFW: contains .dll, .dylib and .h files
- glew: contains .dll, .dylib and .h files
- glm: contains .hpp files (sub-repo)
- spdlog: contains .cpp and .hpp files (sub.repo)
- stb_image: contains the stb_image.h file
Build all the project (engine and game):
make buid_all
NOTE: Use this when you work with the engine and game at the same time.
Build only the engine (this ceate .dylib or .dll):
make buid_engine
NOTE: Use this when you work with the only engine.
Build only the game:
make buid_game
NOTE: Use this when you work with the only the game.
Build all the project (engine and game):
python ./build.py engine && python ./build.py game
NOTE: Use this when you work with the engine and game at the same time.
Build only the engine (this ceate .dylib or .dll):
python ./build.py engine
NOTE: Use this when you work with the only engine.
Build only the game:
python ./build.py game
NOTE: Use this when you work with the only the game.
In src/Game/Game.cpp there is the base file of the game. First, import the main libraries:
#define ENGINE_PLATFORM
#include <Engine.h>
#include <EntryPoint.h>
#include <Engine.h>
import all the Engine modules.#include <EntryPoint.h>
in this file there's the procedure to start application.
WARN:
#define ENGINE_PLATFORM
is necessary to start the Engine without issues.
After that you can create your Application class in the following way:
class Game : public Engine::Application {
friend class Engine::Application;
};
In the class you have to declare Engine::Application as a friend class to access its protected methods.
Then, in your class you have to create the constructor to execute operations like adding layers when the Game instance is created.
// ...
private:
Game() : Engine::Application() {}
// ...
NOTE: You must set the constructor as private because the Application class (and sub-classes) are Singleton Classes.
Now, for the most important part to start your application, you have define the Application::Create() function outside your class in the following way:
Engine::Application* Engine::Application::Create() {
/* Execute code...*/
return Engine::Application::SetInstance(new Game());
}
In this function you can execute the pre-creation instance scripts and than call the Engine::Application::SetInstance()
to setup the Application, and pass your Application class' instance.
You can now build the code and execute it with
make run
or
./bin/main.exe
and should see this black window:
The Background color, title, height and width can be changed using the ApplicationProps struct.
Example:
class Game : public Engine::Application {
friend class Engine::Application;
private:
Game(const Engine::ApplicationProps& props) : Engine::Application(props) {
this->PushLayer(new GameLayer());
}
};
Engine::Application* Engine::Application::Create() {
Engine::ApplicationProps props = Engine::ApplicationProps();
props.WProps.Title = "Game";
props.WProps.Width = 720;
props.WProps.Height = 720;
props.BGColor = Vec4f(HEX_COLOR(0xBF00FFFF));
return Engine::Application::SetInstance(new Game(props));
}
NOTE: the
props.BGColor
is a Vec4f(r, g, b, a).HEX_COLOR
converts a 32bit integer to (r, g, b, a) sequence.
Layers in a game engine are similar to layers in Photoshop, for example. The layer stack will determine the order in which things are drawn on the screen. Layers in a game engine are also applicable to events and update logic. For example:
- Game layer (Root)
- Debug layer
- UI layer
- Menu layer
- ecc.
To create your own layer you can create a different file, for example MyLayer.h (for .h/.cpp patterns see this). In this file include the base Engine dependency and declare your layer that inherits the base Layer class
class YourLayer : public Engine::Layer {
public:
YourLayer() : Layer("Name of your layer") {}
}
In this example we have created a custom constructor that call the Layer constructor, takes a parameter that indicates the layer's name (it's only used in debug).
From Engine::Layer
you can override four methods:
// public
void OnAttach() override;
This method is called when a Layer is added to the LayerStack.
For example when base instaces of other classes are created.
// public
void OnDetach() override;
This method is called when a Layer is removed from the LayerStack.
For example when you deallocate all the memory allocated.
// public
void OnUpdate(float deltaTime) override;
This method is called in every iteration of the engine loop, in this method you should write the logic of the layer.
For example, in a game where the main character is continuously running, inside this method you can calculte his coordinates every frame.
// public
void OnEvent(Event& event) override;
This method is called when the events stack is processed: this is the first operation in the game loop. Every layer is called in order and the Application sends events to the OnEvent method. In this method you can write the event callback using the event dispatcer (see the Events section).
For example, in a game where the main character can jump when pressing the space key, inside the OnEvent method you can check if the key is pressed and execute a callable function.
To add your own layer to Application you can call one of this two methods in the constructor of your custom application class:
PushLayer(new YourLayer());
Adds your layer at the bottom of the layer stack.
PushOverlay(new YourLayer());
Adds your layer at the top of the layer stack
The Application needs to receive events to dispatch them to layers. The Window class can receive these events and communicate with the Application class. Set up a call back function to pass events data to the Application.
The event system is already implemented for application, mouse, key, and window events
To process events you must use the EventDispatcher class. Create an instance, for example, in the OnEvent method inside a layer
EventDispatcher dispatcher = EventDispatcher(YourEvent);
Where YourEvent is the event to dispatch.
To choose which function processes each event call the Dispatch
method and use BIND_FUN
to execute the binding.
dispatcher.Dispatch<YourEventType>(BIND_FUN(YourFunction));
In this example if the event is of type WindowCloseEvent the dispatch sends it to the OnWindowClose method. this function has this signature:
bool YourFunction(YourEventType& YourEventName)
NOTE: When the Dispatcher dispatches events to functions it sets them as Handled, so that no other Dispatcher can handle it.
Application events:
- WindowCloseEvent: When the window close button is pressed.
- WindowResizeEvent: When the window is resized.
- WindowFocusEvent: When there is a window focus.
- WindowLostFocusEvent: When a window loses its focus.
- WindowMovedEvent: When the window is moved.
Key events:
- KeyEvent: Generic key event.
- KeyPressedEvent: When a key is pressed.
- KeyReleasedEvent: When a key is released.
- KeyTypedEvent: When a key is typed.
Mouse events:
- MouseMovedEvent: When the mouse is moved.
- MouseScrolledEvent: When the mouse wheel is scrolled.
- MouseButtonPressedEvent: When a mouse button is pressed.
- MouseButtonReleasedEvent: When a mouse button is released.
Key code (list in src/Engine/Events/KeyCode.h file):
Engine::Key::TheKeyName
Mouse code (list in src/Engine/Events/MouseCode.h file):
Engine::Mouse::TheKeyName
Renders the 2D graphics on the screen. To initialize the Renderer2D you have to add the following line when initializing the props in the Create function:
props.RType = Renderer::RendererType::Renderer2D;
This is the first step when building the rendering system. Bacause this project aims to have an optmized and fast library, glm will be used instead of creating our own math library from scratch.
NOTE: the Renderer space is normalized (image below):
NOTE: Every system of coordinates is managed with Engine type: Vec2f, Vec3f, Vec4f.
Engine::Vec2f(0.25f, 0.0f),
Engine::Vec3f(0.125f, 0.125f),
Engine::Vec4f(0.0f, 0.0f, 1.0f, 1.0f),
Example:
Engine::Renderer::Draw2DTriangle(Engine::Vec2f(0.25f, 0.0f),
Engine::Vec2f(0.125f, 0.125f),
Engine::Vec4f(0.0f, 0.0f, 1.0f, 1.0f),
45.0f);
This example creates a triangle in (x,y) = (0.25f, 0.0f), scales it for (x,y) = (0.125f, 0.125f), the color is (r, g, b, a) = (0.0f, 0.0f, 1.0f, 1.0f) and rotates it of 45.0f.
WARN: Every rotation in the renderer are expressed in degrees.
Draws a triangle in a 2D space. You can change the position, size, color and rotate the shape.
void Engine::Renderer::Draw2DTriangle(Vec2f position = Vec2f(0.0f, 0.0f),
Vec2f size = Vec2f(1.0f, 1.0f),
Vec4f color = Vec4f(1.0f, 1.0f, 1.0f, 1.0f),
float degree = 0.0f);
Draw a square in a 2D space. You can change the position, size, color and rotate the shape.
void Engine::Renderer::Draw2DSquare(Vec2f position = Vec2f(0.0f, 0.0f),
Vec2f size = Vec2f(1.0f, 1.0f),
Vec4f color = Vec4f(1.0f, 1.0f, 1.0f, 1.0f),
float degree = 0.0f);
Draw a circle in a 2D space. You can change the position, radius, color and rotate the shape.
void Engine::Renderer::Draw2DCircle(float radius = 1.0f,
Vec2f position = Vec2f(0.0f, 0.0f),
Vec4f color = Vec4f(1.0f, 1.0f, 1.0f, 1.0f),
float degree = 0.0f);
WARN: the radius must be positive.
Draw a polyhedron in a 2D space.
void Engine::Renderer::Draw2DPolyhedron(ui32 sides,
Vec2f position = Vec2f(0.0f, 0.0f),
Vec2f size = Vec2f(1.0f, 1.0f),
Vec4f color = Vec4f(1.0f, 1.0f, 1.0f, 1.0f),
float degree = 0.0f);
WARN: the sides param must be greater than or equal to 5.
You can use the functions shown below to log events so the engine can communicate with the user. The goal for the application is to be the most client-facing possible. Because of that, it's nice to use a color code to differenciate the severity of the messages. It's also recommended to let know where the log is coming from. Because of the extension of this work, an external library is used for printing messages.
LOG_CLIENT_TRACE("{0} {1}", var0, var1); // Grey color
LOG_CLIENT_INFO("{0} {1}", var0, var1); // Grean color
LOG_CLIENT_WARN("{0} {1}", var0, var1); // Yellow color
LOG_CLIENT_ERROR("{0} {1}", var0, var1); // Red color
LOG_CLIENT_CRITICAL("{0} {1}", var0, var1); // Dark red color