Copperfield Engine 0.1
C++ Game Engine
Loading...
Searching...
No Matches
Documentation

This section serves as your comprehensive guide to understanding every aspect of the engine, covering graphics rendering and advanced systems like the Entity Component System (ECS). Whether you're an experienced developer fine-tuning your craft or a newcomer eager to unravel the engine's inner workings, this is the technical roadmap.



Creating Entities


In our Entity-Component System (ECS), game objects (Entities) are represented as a collection of data (components). In our engine, everything related to entities and components must go through the ComponentManager, which can always be retrieved from the Engine.

When creating an entity, you'll need to supply a parent entity. The location, rotation, and scale of the new object will always be relative to the parent. This means that if you move the parent entity, the new entity will move with it. You can also specify that your entity doesn't have any parents.

It also requires a string, which is the name that will appear in the editor. It doesn't need to be unique, and you don't need to reference it anywhere else.

Another requirement to create an entity is its components. You can pass as many components as you want to ComponentManager::addEntity , but remember that an entity cannot have two components of the same type. If you need that, you can create another child entity with the desired component.

ComponentManager& component_manager = engine.getComponentManager();
Entity SceneRoot = component_manager.addEntity(
nullptr,
"SceneRoot"
TransformCmp({5.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}, {1.0f, 1.0f, 1.0f})
);
Entity FirstChild = component_manager.addEntity(
&SceneRoot,
"FirstChild",
TransformCmp({5.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}, {1.0f, 1.0f, 1.0f})
);
component_manager.addEntity(
&SceneRoot,
"SecondChild",
TransformCmp({-5.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}, {1.0f, 1.0f, 1.0f})
);
Handles anything related to entities and components in the engine.
Definition: component_manager.hpp:29
Entity addEntity(Entity *parent, const std::string &editor_name, const T &... args)
Creates an entity and assigns the specified components to it.
Definition: component_manager.hpp:58
Identifies an entity. This class can only be modified by the engine.
Definition: component_manager.hpp:16
Component used to give a position, rotation, and scale to an entity.
Definition: default_components.hpp:284


Deleting entities precautions

When deleting entities, if the entity you are trying to delete has any children, those children will also be deleted.

component_manager.deleteEntity(SceneRoot);
void deleteEntity(Entity &entity)
Deletes an entity from the system and all its children.

This operation will also delete FirstChild and SecondChild from the scene.

Note that we have saved the FirstChild entity. When the parent is deleted, using that entity will cause undefined behavior.

Most times, you won't need to store the value returned from addEntity, as most entities should be read and modified inside the systems.

It's not possible to modify the parents and children of an already created entity. You will need to create a new one and all its children, and then delete the old one.

Deleting and creating entities can be somewhat expensive; you should try to avoid them during runtime if possible.


Components

Retrieve and modify components

Once an entity is created, you can add more components to it or get a specific component to read or modify it:

// Create an entity with a transform component
Entity Camera = component_manager.addEntity(
nullptr,
"Camera"
TransformCmp({5.0f, 0.0f, 0.0f}, {0.0f, 180.0f, 0.0f}, {1.0f, 1.0f, 1.0f})
);
// Add a camera component to the previous entity
component_manager.setComponent(Camera, CameraCmp(CameraProjection::kOrtho));
// Retrieve a reference to the created camera component
CameraCmp* camera_component = component_manager.getComponent<CameraCmp>(Camera);
void setComponent(const Entity &entity, const T &component)
Adds a component to the specified entity.
Definition: component_manager.hpp:97
T * getComponent(Entity entity) const
Retrieves a component for a specific entity.
Definition: component_manager.hpp:117
@ kOrtho
Renders a 3D scene as 2D without depth.
Component used to render the scene. There must be one and only one of this component on the scene.
Definition: default_components.hpp:161

These functions should be rarely used but may be needed for specific entities.

ComponentManager::getComponent will return null if the entity doesn't have a component of the specified type.

Engine Created Components

  • TransformCmp: Contains Position, Rotation, and Scale. Probably, all your entities should have a Transform since you will likely want a position on all your entities.
  • CameraCmp: There must be one and only one Camera component in the scene. The direction in which the camera is looking can be set to use the TransformCmp rotation or to look at a specific point in the scene. With this component, you can also set an orthographic or perspective view and define the field of view and minimum and maximum render distances.
  • RenderCmp: Used for any object you want to display in the scene. These must contain a mesh and a texture. If you don't want to use a texture and prefer a simple color, you can pass it a completely white texture with ResourceManager::getEmptyTexture. Additionally, this component is affected by reflections and can cast shadows.
  • LightCmp: Used for anything you want to emit light. You can set its color intensity and type. Depending on the type, it can receive more parameters:
    • Point: Emits light in every direction and has attenuation over distance. Setting the distance it should reach can be tricky, but a simplified version is available where you just need the maximum distance, and LightCmp::calculateAttenuation will set the three values to something realistic.
    • Spot: Emits light in a cone. You'll need to specify its direction, radius, and how hard or smooth the edge of the light should be.
    • Directional: The whole scene will receive light from the specified direction. You'll need to specify its direction.
  • AudioCmp: A component needed in entities you want to emit sound.
  • BoxCmp: Defines a cube in the scene and is used to define bounding boxes for collisions.

Creating Custom Components

Components are nothing more than a struct or class. Ideally, you'll want them to be just data, but the engine will work the same if you add functions to this class. To use these structs as engine components, you just need to register them with the ComponentManager.

struct MovementCmp {
coma::Vec2 dir = coma::Vec2(0.0f, 0.0f);
float speed = 2.0f;
};
component_manager.addComponentClass<MovementCmp>();
void addComponentClass(ComponentListType t=ComponentListType::kCompact)
Defines a list for a custom component and makes it ready to use with the engine.
Definition: component_manager.hpp:82
represents mathematical vector with 2 components
Definition: vector_2.h:7

ComponentManager::addComponentClass accepts a ComponentListType. This is because components can be stored in two different ways internally. Most of the time, you'll want to use the default (compact), which is more efficient for components that won't be used by most entities in the scene. Sparse lists will be more efficient when the component is present in most entities of the scene. For example, TransformCmp and RenderCmp are stored in Sparse Lists. This will only significantly affect performance if you have thousands of entities.



Updating Components (Systems)


In ECS, we usually use systems to update the data of the components and, therefore, the entities. A system will loop through all the components of the needed types and perform operations on them. The engine contains an AudioSystem, which updates the entities with TransformCmp and AudioCmp, and a MoveColliderSystem, which updates all entities containing TransformCmp and BoxCmp. Internally, we have a render system, which updates the rest of the default components.

Creating Custom Systems

The engine offers a simplified way of iterating through the components:

void MoveSystem(ComponentManager& cmp_manager) {
// Retrieve component lists
auto& transform_list = cmp_manager.getSparseList<TransformCmp>();
auto& movement_list = cmp_manager.getComponentList<MovementCmp>();
// Iterate through the components
ComponentIterator it(movement_list, transform_list);
while (it.next()) {
auto [movement_cmp, position_cmp] = it.get();
// Operating with the components
coma::Vec3 dir = coma::Vec3(movement_cmp.dir.x, movement_cmp.dir.y, 0.0f);
dir *= (movement_cmp.speed * Time::DeltaTime());
position_cmp.position_ += dir;
}
}
Helper to iterate several component lists simultaneously.
Definition: lists_iterator.hpp:9
ComponentListCompact< T > & getComponentList() const
Retrieves the list of all the components of the same type.
Definition: component_manager.hpp:161
ComponentListSparse< T > & getSparseList() const
Retrieves the list of all the components of the same type.
Definition: component_manager.hpp:178
static float DeltaTime()
Retrieves the duration of the last frame.
represents mathematical vector with 3 components
Definition: vector_3.h:12

Your custom system should look similar to the previous code. The first thing you'll need is to retrieve the lists of the needed components. In this case, notice that we use two different functions for retrieving the lists. This is because TransformCmp is stored in a SparseList rather than a CompactList (Detailed Explanation). The engine will crash if you use the wrong function. The only components stored in sparse lists are TransformCmp and RenderCmp.

Once you have the lists, you can create a ComponentIterator. This class will iterate only the entities that contain all the component types of the lists passed to it. To maximize performance, you should pass the shortest list first. 'next' will set the internal state so that when you call 'get', it can return a tuple containing the components of the entity.

With that, you can implement your custom code for the system, having a set of components on each entity.



Loading Resources


The engine supports loading various assets through the ResourceManager:

ResourceManager& resource_manager = engine.getResourceManager();
// Load mesh
resource_manager.loadResource(
"../assets/obj/pollo.obj",
"Chicken"
);
// Load texture
resource_manager.loadResource(
"../assets/textures/box.png",
"Box"
);
// Load sound
resource_manager.loadResource(
"../assets/Invaders-CivilWar/other.wav",
"CivilWar"
);
// Halts the program, waiting for all assets to be loaded
resource_manager.WaitResourceLoad();
Stores and offers access to all meshes, textures, and sounds of the engine.
Definition: resource_manager.hpp:27
void loadResource(const ResourceType &resource_type, const std::string &file_path, const std::string &identifier)
Load an external asset from a file using multithreading.
bool WaitResourceLoad()
Stops the program, waiting for resource loading to complete.
@ kSound
Sound accepts .wav.
@ kTexture
Texture accepts .png and .jpg.
@ kMesh
Mesh accepts .obj.

When loading an asset, you need to specify its type, the file path, and a unique string identifier for retrieving it. This identifier must be unique, as using the same string twice will erase the data of the previous asset with the same name.

Assets are loaded using multithreading, and before trying to use any of them, there must be a call to ResourceManager::WaitResourceLoad. This will pause the program until all the assets are loaded and ready to use. It's recommended to load all the resources of your game at the same time and call ResourceManager::WaitResourceLoad only once to minimize loading times.


Retrieving Assets

To use the already loaded assets (after having called ResourceManager::WaitResourceLoad), you can call the specific function for the type of asset you need to get:

ResourceManager& resource_manager = engine.getResourceManager();
const Mesh& mesh = resource_manager.getMesh("Chicken");
const Texture& texture = resource_manager.getTexture("Box");
const SoundBuffer& sound = resource_manager.getSound("CivilWar");
const SoundBuffer & getSound(const std::string &id) const
Retrieve a SoundBuffer.
const Texture & getTexture(const std::string &id) const
Retrieve a texture.
const Mesh & getMesh(const std::string &id) const
Retrieve a mesh.

These functions will typically be used when creating or modifying a RenderCmp or AudioCmp:

ResourceManager& resource_manager = engine.getResourceManager();
&resource_manager.getMesh("Chicken"),
&resource_manager.getTexture("Box")
);
AudioCmp audio("Other", resource_manager.getSound("CivilWar"), a, b, 1.0f);
Component used for audio reproduction.
Definition: default_components.hpp:40
Component used to render an object.
Definition: default_components.hpp:237

Aside from the assets you loaded, the engine already provides some meshes that can be used by passing a BasicMesh enumeration to get the mesh instead of a string. If you want the a RenderCmp to have no texture and use a plain color, you'll need to use a white texture, which is facilitated with ResourceManager::getEmptyTexture:

ResourceManager& resource_manager = engine.getResourceManager();
&resource_manager.getMesh(BasicMesh::kCube),
&resource_manager.getEmptyTexture(),
coma::Vec3(1.0f, 0.0f, 0.0f)
);
const Texture & getEmptyTexture() const
Retrieve a white texture.

BasicMesh contains a triangle, a plane, a cube, a cube with inverted normals, a sphere, and a capsule.



Input


Button Input

We use an action-based input system, meaning you will not be checking for specific buttons; instead, buttons will be assigned to an action, and you'll check if that action was performed. This makes it easy to reassign controls.

To use it, the first thing is to create the map:

InputButtonMap input_map{
{"Move", {InputButton::W, InputButton::UP}},
{"Jump", {InputButton::Space}},
{"OpenMenu", {InputButton::Enter}}
};
std::unordered_map< std::string, std::vector< InputButton > > InputButtonMap
Structure that binds user-created actions and physical input.
Definition: input.hpp:175

An InputButtonMap is a map that lets you bind as many keys as you want to a specific string or action.

Having this, we can create an InputManager which will have the required functions to detect the input. You can have several InputButtonMap and InputManager, which can be useful for multiplayer input.

All the InputManager must be declared after the Renderer input wont work otherwise.

InputManager input(window, input_map);
InputButtonMap input_map_player2{
{"Move", {InputButton::I}},
{"Jump", {InputButton::O}},
{"OpenMenu", {InputButton::P}}
};
InputManager input_player_2(window, input_map_player2);
Interface to retrieve keyboard and mouse input.
Definition: input.hpp:180

The InputManager has functions that will indicate the state of an action:

InputManager input = InputManager(window, input_map);
InputManager input_player_2 = InputManager(window, input_map_player2);
if (input.buttonPressed("Move")) {
// Move up
}
if (input.buttonDown("Jump")) {
// Jump
}
if (input.buttonUp("OpenMenu") ||
input_player_2.buttonUp("OpenMenu")) {
// Open menu
}
bool buttonUp(const std::string &action) const
Checks if any of the buttons assigned to an action have been released during the frame.
bool buttonPressed(const std::string &action) const
Checks if any of the buttons assigned to an action is currently held down
bool buttonDown(const std::string &action) const
Checks if any of the buttons assigned to an action have been pressed during the frame.

Mouse Movement and Cursor

You can retrieve the mouse cursor in several ways:

The InputManager also has InputManager::toggleCursor to hide and show the cursor.

If you have multiple InputManager, you can call the cursor function from any of them, and they will work as if only one InputManager existed.



Collisions


In the engine, you can check if two BoxCmp are overlapping by using coma::cols::collidesWith. Additionally, you can use coma::cols::detectFaceCollision to detect where the other box is located (up, right, down, or left).

if (coma::cols::collidesWith(player_collision, enemy_collision)) {
if(coma::cols::detectFaceCollision(*player_collision, *enemy_collision) == BOTTOM) {
// Collides, and the enemy is below the player
}
}
Face detectFaceCollision(const BoxCmp &me, const BoxCmp &other)
If two objects are colliding return the face of the box with which it collides.
bool collidesWith(const BoxCmp &me, const BoxCmp &other)
Detect if the BoxCmp are colliding.



Sound


To use sound, you'll need an entity with a sound component. In the main loop, you'll use the SoundSystem, and just by using play and stop on the component, the sound will play.

// Create the entity
component_manager.addEntity(
nullptr,
AudioCmp("Other", resource_manager.getSound("CivilWar"))
);
// Set the audio to play
auto* audio_cmp = component_manager.getComponent<AudioCmp>(audioPlayer);
audio_cmp->src.Play();
while (!window.isDone()) {
// System reproduces the audio
SoundSystem(component_manager);
render.render();
}
void SoundSystem(ComponentManager &cmp_manager)
System used for sound reproduction without it the sound cannot be played.
SoundSource src
Object used to reproduce the audio.
Definition: default_components.hpp:52



Editor


With the program running, you can hit F1 to hide or show the editor. With the editor, you can modify the component values of each entity on the scene and instantly see the results.