ScapeGameEngine¶
Hello! This is the ScapeGameEngine project documentation. ScapeGameEngine is a small OpenGL game engine written in C++, and supporting Windows. It is currently developed just by me, Forwardspace.
If you are somehow still interested, check out the Quick Start Guide to the left - a couple of lines of code later and you’re golden (barring the not-so-fast installation procedure)!
Features¶
Currently, some of its features include:
- mesh rendering
- shader support
- input support
- texturing
- mesh loading
- Windows! A lot of windows!
I’m currently working on the following:
- GUI system (about half-way done to the minimal working prototype)
- an editor
- instanced rendering
Table Of Contents¶
Usage¶
Installation guide¶
Although SGE (ScapeGameEngine) hasn’t been optimised for installation on every, or indeed any, device, it is possible.
Prerequisites¶
- Visual Studio 2019 (any edition)
- Windows, obviously
- An internet connection
- That’s theoretically it
Installation Procedure, seriously, why are you trying to install this??¶
- Begin by cloning the SGE repository from GitHub (https://www.github.com/Forwardspace/SGE)
- Go to https://glad.dav1d.de/ (GLAD homepage) and set the gl API version to the latest value
- Check “Generate a loader”, “Generate” and dowload glad.zip, extract it in SGE/packages/
- (important) Drag the glad.c file to the “Source files” folder in Visual Studio to add it to the project
- Configure Bullet Physics, build LinearMath, BulletCollision, BulletDynamics and BulletSoftBody
- Copy the built libraries in a new folder called “lib” under “bullet3”.
- Copy the “src” directory from the bullet source to the previously created “bullet3”
- Open the .sln file in Visual Studio
- Click Build > Build all and wait
You’ve now got an up-to-date version of SGE. Why are you torturing yourself??? You can edit the source code or run the program using the button “Local Windows Debugger”. When you’re ready, continue to the Quick Start Guide.
Quick Start Guide¶
If you’re here, that means you’ve beaten the remarkable odds of not actually being interested in this project. Congratulations. Now, let’s get started.
First, install SGE using the intructions outlined in the Installation Guide. Now you’ve got a functional and up-to-date version of SGE.
Example Code¶
If you open the file “test.cpp”, you’ll see the main()
function.
The code here sets up SGE by calling sge::Renderer::init(...)
and creating a simple scene using physics and instancing.
It also enables the FPS camera to allow the player to move.
This demonstrates a very simple SGE program. If you want more details, carry on reading.
Details¶
You may be wondering how to create your own games/programs.
First off, include the SGE.h
header.
Everything SGE provides is placed in the aptly named sge
namespace (except GUI, which is in the sgeui
namespace).
Before using anything from those namespaces, call
sge::Renderer::init(int w, int h, std::string title, bool fullscreen);
function, which will initialize SGE and open a window.
When creating 3D objects, (currently) you there are static and instanced objects: doc:StaticObject <../classes/objects/staticobject>, doc:StaticInstancedObject <../classes/objects/staticinstancedobject>
To create a StaticObject from a file, call
sge::StaticObject::StaticObject(fs::path filename);
//eg.
auto object = sge::StaticObject("path/to/file.obj");
This will load the model from filename
and render it every frame.
Nearly all model formats are supported (through Assimp).
To create an instanced object (used when you have to render multiple identical objects), check out its documentation.
If you don’t want an object to render, set the renderObject flag to false.
someObject.renderObject = false;
Set it back to true if you want to render it again.
A Camera has to be created to describe the positon and rotation of the viewer:
sge::Camera::Camera();
//eg.
auto camera = sge::Camera();
The Camera then has to be set as the current one in Renderer:
sge::Renderer::setCurrentCamera(camera);
All done! Now, in a loop, do:
sge::Renderer::renderFrame();
Note
If you want a simple FPS camera, you can enable it by calling:
sge::FPSCamera::enable();
Putting it all together, it could look something like:
...
sge::Renderer::init(1024, 768, "Exempli gratia", false);
auto camera = sge::Camera();
sge::Renderer::setCurrentCamera(camera);
auto myObject = sge::StaticObject("path/to/file.obj");
while (true) {
sge::Renderer::renderFrame();
}
...
See also
Style Guide¶
We have STANDARDS here! To better understand existing code and contribute by adding new code, please follow the simple style guide below.
Brackets/Parentheses¶
The opening bracket is placed on the same line as the expression requiring it.
Example:
if (foo == bar) {
...
}
class FooBar {
...
};
Variable naming¶
Generally, variables are named using camelCase. Private variables have an underscore after their name, especially if they have externally available getters/setters.
Example:
int fooBar = 3;
class ... {
int fooBar_ = 3;
...
};
Classes¶
Classes use PascalCase.
The private:
and public:
labels in classes are inline with the class
declaration.
It is also worth mentioning that each important class should have its own .cpp and .h files.
Example:
class FooBarManager {
public:
...
private:
...
};
Classes¶
Renderer: .h, .cpp¶
Usage¶
Renderer is a singleton class that manages the (as the name implies) rendering of frames to the screen. As such, it is also the main class that initializes and terminates other SGE modules.
Note
Before any SGE modules, classes, functions or systems can be used, initialize SGE by calling:
static void Renderer::init(w, h, std::string name, bool fullscreen);
This opens a window of w by h pixels with the title name. If fullscreen is true, the window will be fullscreen. It then initializes all other SGE systems.
SGE uses GLFW as the window and input library of choice and GLAD as the OpenGL extension loader. DevIL is used for loading images and Assimp for loading models. Those libraries are also initialized here.
Before exiting, call:
static void Renderer::terminate(bool exit = true);
Which will exit the program unless exit is false.
Rendering¶
When all the setup is done for any one frame, the function
static void Renderer::renderFrame();
will take over and render that frame, as well as update the GUI and all other systems
The pair
static void Renderer::updateProjectionMatrix(float FoV, float NCP, float FCP);
static glm::mat4x4 sge::Renderer::projectionMatrix();
(where NCP = near clipping plane and FCP = far clipping plane) will update and return the projection matrix, respectfully.
The pair
static void Renderer::setCurrentCamera(Camera* cam);
static Camera* sge::Renderer::currentCamera();
set and get the Camera used for rendering frames.
Callbacks¶
Renderer also deals with callbacks; they are called after each frame. These functions allow you to add and remove callbacks, respectively
static void Renderer::registerWindowCallback(std::function<void()>);
static void Renderer::removeWindowCallback(std::function<void()>);
They are used mainly to handle window events.
Internal¶
The function
static GLFWwindow* sge::Renderer::wind();
Returns the GLFW window and is generally only used internally. The same is true of
static void sge::Renderer::registerObject();
static void sge::Renderer::removeObject();
which register and remove the object from being drawn automatically every frame.
Objects¶
Camera: .h, .cpp¶
The Camera class represents the viewer in 3D space, and serves as a storage for the 4 by 4 view matrix used when rendering. As such, it only has functions modifying that matrix.
How to use it?¶
Construct the Camera and hold the reference to it as long as you want to use it:
Camera::Camera();
//eg.
auto myCamera = sge::Camera();
Details¶
Like the Object class, it stores a 4 by 4 matrix and recalculates it only when the position, location or scale of the camera has been modified.
To modify its position, location and scale, use:
void setTransform(glm::vec3 pos, glm::vec3 rot, glm::vec3 scale);
//or
void setPos(float posX, float posY, float posZ);
void setRot(float rotX, float rotY, float rotZ);
To get its position and rotation, use:
glm::vec3 pos();
glm::vec3 rot();
Mesh: .h, .cpp¶
struct Vertex3D¶
Stores one 3D coordinate. Has functions to convert from other representations of coordinates to this one.
It is equivalent to 3 side-by-side floats (12 bytes).
struct Vertex2D¶
Stores one 2D coordinate. Also has functions to convert from other representations of coordinates to this one.
It is equivalent to 2 side-by-side floats (8 bytes).
struct MeshInVBOs¶
This class does not store any vertex data on its own. Instead, it stores the position and size of the vertex attributes that have been moved over to OpenGL buffers. This way saves a significant amount of memory and increases performance.
class Mesh¶
The Mesh class stores raw vertex data and is used in StaticObject. Currently, Meshes store 3D vertex coordinates, 2D texture UV coordinates and 1D vertex coordinate indices.
You generally don’t need to deal with meshes directly; instead let the StaticObject class handle it. Nevertheless, check out the .h file to see the various functions dealing with modifying various Mesh data - they’re pretty self-explanatory.
A notable function is the
MeshInVBOs moveToVBOs(BufferTargetDescriptor target);
function, which moves the mesh data to VBOs marked with target
and returns a MeshInVBOs object so the caller can use the data.
Unless you want to have easy access to the data, use this function to improve performance
Note
The moveToVBos
function does not invalidate the data in the Mesh, and thus can be called multiple times, producing copies
of the data stored separately in VBOs.
Object: .h, .cpp¶
class Object¶
The Object class is a base class for the StaticObject class, and contains all of the information needed to describe an object in 3D space, minus the actual vertex data. The actual data is stored differently based on the object’s type.
The class stores a 4x4 model matrix, which is recalculated when any of the Object’s attributes are changed. The matrix is not available externally. Any class deriving from Object must have a
void render();
function, which renders the object (currently) directly onto the screen. It is virtual in Object.
Note
Do not use the Object class directly; instead derive from it or store pointers to Objects.
namespace ObjectType¶
Describes the type of an object. The Object class has the ObjectType of “GENERIC”
StaticObject: .h, .cpp¶
StaticObject is a class derived from Object. It represents a mesh in 3D space and it is used for non-animated and static meshes (not that there’s much choice currently). It does not store its vertex data, instead storing the data’s positions in OpenGL buffers in a MeshInVBOs. This is done to reduce the overhead of uploading the data to OpenGL.
How to use it?¶
When you create a StaticObject and until you stop keeping a refrence to it, it will automatically be drawn to the screen each frame (unless the renderObject flag is false). Therefore, all that is needed is to construct the StaticObject using
StaticObject::StaticObject(fs::path path);
//eg.
auto myObject = sge::StaticObject("path/to/file");
Details¶
When the function
void render();
is called, StaticObject calculates all relevant data and calls the glDrawElementsBaseVertex function, drawing itself to the screen.
Note
You generally do not need to call this function directly; it is instead called when Renderer::renderFrame()
gets called
Besides the MeshInVBOs, StaticObject stores a Texture object and the corresponding useDefaultTexure flag (set true to, obviously, use the default checkerboard texture). Both the MeshInVBOs and Texture within a StaticObject can be get and set using:
void setMesh(MeshInVBOs& mesh);
Mesh mesh();
void setTexture(Texture& tex);
Texture texture();
Note
Currently, just one Texture can be used. Further down the line, the Materal class should be implemented, which will allow the user to use more than one Texture per StaticObject.
The StaticObject class has the ObjectType of “STATIC”
See also
using namespace std;
- class Cup {
- int fillCup(int i) {
contents = i;
return i;
}
int contents;
};
Textures¶
PackedTexture: .h, .cpp¶
namespace PackedTextureType¶
Defines the type of the texture inside a PackedTexture. Feel free to extend this if you’d like more types.
class PackedTexture¶
The PackedTexture class inherits from Texture and (along with the OpenGL handle stored by Texture) stores a vector describing the layout of images stored one after the other in the texture. It is mostly used by the SGE GUI system to store images for 3 different button states in one texture.
Along with the texture file, you need to have an XML file in which you need to specify in what order are the images in the texture.
The file has to have a root element named textureTypes
with a children element for each texture (named type
).
For example, if you have a file called myTexture.png in which the textures are (in order) normal, hover and click textures, create the file myTexture.xml, inside of which is:
<textureTypes>
<type>NORMAL</type>
<type>CLICK</type>
<type>HOVER</type>
</textureTypes>
Then, simply supply the filename of the texture file to the contructor:
PackedTexture::PackedTexture(fs::path filename);
//eg.
auto myTexture = sge::PackedTexture("path/to/file");
To access the individual texture coordinates, use:
std::array<glm::vec2, 2> unpackTexture(PackedTextureType::Enum type);
//eg.
auto texCoords = myTexture.unpackTexture(PackedTextureType::HOVER);
which will return the texture coordinates (bottom left and upper right) of that specific texture.
Note
All of the images inside one texture file need to be of the same size and should preferably be power-of-two.
The function unpackTexture gets the index of the specific texture starting from 0 and the total number of textures and then calculates the bounds as follows:
bottomLeftBound = { index / totalNumberOfTextures, 0 }
upperRightBound = { (index + 1) / totalNumberOfTextures, 0 }
Note
If no matching texture was found, it will return the coordinates of first texture.
SkyboxTexture: .h, .cpp¶
SkyboxTexture holds 8 individual OpenGL textures that can be rendered to a cube to give the illusion of a sky or background.
How to use it?¶
Construct the SkyboxTexture by supplying it with a path:
SkyboxTexture::SkyboxTexture(fs::path);
//eg.
auto mySkybox = sge::SkyboxTexture("path/to/file");
Then, set the skybox to render every frame by calling (e.g.):
sge::Renderer::setCurrentSkyboxTex(&mySkybox);
Details¶
A skybox is composed of 6 image files with the same base and file type, but ending with
_bk, _ft, _lf, _rt, _up, _dn
(back, front, left, right, up and down textures, respectively).
The path supplied to SkyboxTexture constructor does not include any of those extensions.
For example, if you have files named .\\miramar_bk.png, .\\miramar_up.png, ...
, supply the path ".\\miramar.png"
to the constructor
Internally, the 6 textures are loaded similarily to the normal Texture class.
Skyboxes generate a new VAO with the ID of 1001 and configure one attrib. array for storing the vertices of a cube. They also compile a custom shader.
Texture: .h, .cpp¶
The Texture class represents an image as used by OpenGL. It sends image data (either from a file loaded by DevIL or supplied by the user) to OpenGL and stores the OpenGL handle of the texture.
How to use it?¶
Create it either by using the contructor with a path:
Texture::Texture(fs::path);
//eg.
auto myTex = sge::Texture("path/to/file");
(which will load the texture from the supplied file) or the contructor with the raw pixel data:
Texture(GLubyte* data, int w, int h, GLenum format);
//eg.
auto myTex = sge::Texture(myDataPtr, 512, 512, GL_RGB);
Which will load w * h * 3
bytes from myDataPtr and send it to OpenGL with the format of format
(for example GL_RGB
or GL_BGR
).
Details¶
Refer to the .cpp file (more specifically, void loadFromFile()
and void makeTexture()
) to see how the textures are loaded from files or raw data.
Managers¶
BufferManager: .h, .cpp¶
The BufferManager class provides an abstraction for access to OpenGL buffers. It is used by objects to store vertex data inside of buffers (mostly VBOs).
It provides several functions, however none of which are useful if you’re not creating new renderable objects.
struct …¶
These are various structs describing the target buffers to modify and storing OpenGL buffer metadata.
class BufferManager¶
How to use it?¶
Bind a specific VAO with a custom id
using:
void bindVAO(unsigned int id);
to store the OpenGL state for later.
To add data to the buffer, use:
BufferAppendResult appendToBuffer(BufferTargetDescriptor target, BufferDataDescriptor data);
This will return the position of the data in target
, which you can use later when rendering.
Finally, access a buffer using:
GLuint getBuffer(BufferID bd);
Which will return the raw OpenGL handle of that buffer or create it if it doesn’t exist.
All of these functions will create buffer objects if they don’t yet exist, so you don’t have to worry about instantiating them
Additionally, appendToBuffer
will not only create a new buffer, but configure it as well, as long as the BufferSubtype
is either VERTEX
or
TEXTURE
.
Miscellaneous¶
EventHandling: .h¶
Q: What is this ugly C-like sorcery?
A: This is a header-only implementation of an event library. These macros and classes ease the use of event handling; no need to keep track of handler names and registration; just use these macros! (and don’t look at what’s under the hood, please)
How to use it?¶
Well, if you don’t have to, this is made just for GUI purposes. Else:
Events are structs inheriting from Event
.
To define a new one, simply inherit from it, add the required data and call the REGISTER_EVENT(x)
like so:
struct MouseDownEvent : public Event {
MouseDownEvent(int x, int y) : mX(x), mY(y) { REGISTER_EVENT(MouseDownEvent) }
int mX, mY;
};
(note the placement of the REGISTER_EVENT(x)
macro (it’s in the constructor))
Note
Event names have to end with Event
; this is a technical limitation of the library.
Every class that recieves events is a listener. To set up one, use the LISTENER
and LISTENER_INIT(x)
macros:
class MyClass {
LISTENER;
...
MyClass() : ..., LISTENER_INIT(forwardEventToChilldren), ... {}
}
where forwardEventToChildren
is a function with the signature of:
bool forwardEventToChildren(Event* e);
which raises the event to all or some of the children of your listener class.
Note
Pass nullptr
if you are a bad parent
and don’t want to pass events to children.
Simply use the EVENT_HANDLER(x, y)
macros in your class’ body:
...
EVENT_HANDLER(MouseDown, {
event->something = Something();
if (doSomethingWith(event)) {
return true;
}
return false
});
You can see that the name of the handler consists of the name of the event it handles without the Event
part at the end.
For example, a handler that handles SomethingHappensEvent
will pass the name SomethingHappens
to EVENT_HANDLER(x, y)
.
The handler itself returns a bool. Return true if you were able to handle the event (or if you ignored it). The return value will
be forwarded back up as the return value of the RAISE_EVENT
macro.
Inside the macro’s block, you now have access to a pointer pre-casted to the required derived Event class named event
;
no need to dynamic_cast
anything!
Note
To enable event handling for any specific event, in your constructor do:
...
HANDLES_EVENT(MouseDown);
...
//To stop (temporarily or permanently):
DOES_NOT_HANDLE_EVENT(MouseDown);
Use the RAISE_EVENT(x, y)
macro like so:
RAISE_EVENT(someObjectInstancePointer, new MouseDown(132, 168));
The first argument is the object instance you wish to send the event to, and the second is the event itself.
Notes¶
There is also a MASTER_* set of macros that allow whole parent systems to operate as listeners outside of any class. Its usage is similar to the normal set of macros, so check the macros for details if you want to know more.
FPSCamera: .h, .cpp¶
class FPSCameraController¶
This is a singleton class that, when activated, takes control of the current camera and allows the player to control a first-person character with WASD, Shift, Ctrl and mouse. It assumes a flat world with downward-facing gravity.
Note
For this reason, you will most likely need to write a new controller or modify this one for your games. Feel free to use the source code of this one.
It also inits and uses a dynamic rigid body to interact with the physics world.
The most important functions are
void enable(); //Call to enable the controller
void disable(); //Call to temporarily or permanently disable the controller
There are also some publicly configurable variables such as gravity or speed (check FPSCamera.h for info)
ParsedXML¶
This class is a wrapper around the rapidxml::xmldocument<>
class.
As such, it provides a way to parse and, more importantly, access an XML document.
How to use it?¶
Construct the object:
ParsedXML::ParsedXML(fs::path sourceFile);
//or
ParsedXML::ParsedXML(std::string s);
//e.g.
auto myXMLDoc = ParsedXML(fs::path("path/to/file.xml"));
Then, find children of the root node using
auto myNode = myXMLDoc["Settings"]
Find nodes anywhere from root down:
rapidxml::xml_node<>* findNode(std::string s);
auto myNode = myXMLDoc.findNode("Brightness");
Note
For information on accessing and manipulating nodes, see http://rapidxml.sourceforge.net/manual.html
Physics¶
PhysicsObject: .h¶
class PhysicsObject¶
An abstract class all other physics object inherit from (only RigidPhysicsObject for now).