![]() |
Copperfield Engine 0.1
C++ Game Engine
|
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.
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.
When deleting entities, if the entity you are trying to delete has any children, those children will also be deleted.
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.
Once an entity is created, you can add more components to it or get a specific component to read or modify it:
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.
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:LightCmp::calculateAttenuation
will set the three values to something realistic.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.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.
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.
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.
The engine offers a simplified way of iterating through the components:
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.
The engine supports loading various assets through the ResourceManager:
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.
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:
These functions will typically be used when creating or modifying a RenderCmp
or AudioCmp
:
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
:
BasicMesh contains a triangle, a plane, a cube, a cube with inverted normals, a sphere, and a capsule.
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:
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.
The InputManager
has functions that will indicate the state of an action:
You can retrieve the mouse cursor in several ways:
InputManager::getMousePosition
will return the position in pixels relative to the top-left corner.InputManager::getMouseOffset
returns how many pixels the cursor moved since the last frame.InputManager::getMouseRotation
and InputManager::setMouseRotation
use the InputManager::getMouseOffset
to update the rotation, useful for a first-person camera. InputManager::getMouseRotation
returns the value and leaves the original vector untouched, while InputManager::setMouseRotation
modifies the original vector.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.
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).
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.
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.