A simple Entity Component System, adapted from Austin Morlan's ECS and powered by MonoGame.
Support up to 32768 (2^15) entities in Release mode and 16384 (2^14) in Debug mode. Tested on an RTX 3070Ti + AMD Ryzen 5 2600X Six-Core Processor @ 3.60 GHz.
The ecs-core branch includes a basic ECS adapted from Austin Morlan's. The main branch includes additional code in which components are wrapped in objects (implementing IComponentWrapper
). These objects store their owning entity's ID so that their properties can be modified and so that these modifications will be reflected in the components stored in the ECS.
The advantage of these OOP wrappers is that it provides users a way to directly modify ECS components without worrying about handling struct references. The disadvantage of them is that they require redundant code and boilerplate. They would likely be used when defining custom entity logic (i.e. if you have a Player
class that extends Entity
with an Update(float deltaTime)
method that modifies the transform under some condition).
To add a new component type using the ecs-core branch:
- Define a new struct as follows:
public struct COMPONENT_NAME : IComponent
. - Register the component with the coordinator in
Game1.cs
(I recommend under theInitialize()
method). - Provide a component mask when registering a component, which needs to be added to the
ComponentMask
enum (adhering to the rules of the FlagsAttribute).
To add a new component type using the main (OOP) branch:
- Define a new struct as follows:
public struct COMPONENT_NAME : IComponentData
(I recommend appending the word 'Data' to the struct name, such as TransformData, to distinguish it from the component wrapper). - Register the component with the coordinator in
Game1.cs
(I recommend under theInitialize()
method). - Provide a component mask when registering a component, which needs to be added to the
ComponentMask
enum (adhering to the rules of the FlagsAttribute). - Define a new class inheriting
Component<T>
where T implementsIComponentData
(I recommend naming this the component name, such as Transform, since the purpose of the wrapper is to be user-friendly). - Wrap each member of the
IComponentData
with a getter/setter as follows (replacingT
with the actual data type):
public T FieldName
{
get => ComponentReference.FieldName;
set => ComponentReference.FieldName = value;
}
To add a new system type:
- Define a class as follows:
public class SYSTEM_NAME : ECSystem, SYSTEM_TYPE
(replacingSYSTEM_TYPE
withIUpdateSystem
if your system will be updated using deltaTime orIRenderSystem
if your system will be rendered usingSpriteBatch.Draw(...)
). - !!! Your system will not do anything if you don't define it as an
IUpdateSystem
orIRenderSystem
, but feel free to add another system type if necessary. - Define a component mask for the system, meaning any entity with all components specified by the mask will be affected by the system. The following example defines a component mask for a system that will affect entities with a sprite and a transform.
public override ComponentMask ComponentMask => ComponentMask.Transform | ComponentMask.Sprite;
- Register the system with the coordinator in
Game1.cs
(I recommend under theInitialize()
method).
If you're using the main (OOP) branch, you should still be referencing components directly (instead of using the object wrappers) inside of systems because the object allocations can get expensive. Example:
Good:
public void Update(float deltaTime)
{
foreach (Entity entity in Entities)
{
ref RigidBodyData rigidBody = ref entity.GetComponentReference<RigidBodyData>();
ref TransformData transform = ref entity.GetComponentReference<TransformData>();
rigidBody.Acceleration += rigidBody.Gravity * deltaTime;
rigidBody.Velocity += rigidBody.Acceleration * deltaTime;
rigidBody.Velocity = Vector2.Min(rigidBody.Velocity, TerminalVelocity);
transform.Position += rigidBody.Velocity * deltaTime;
transform.Rotation += rigidBody.AngularVelocity * deltaTime;
}
}
Bad:
public void Update(float deltaTime)
{
foreach (Entity entity in Entities)
{
RigidBody rigidBody = entity.GetComponent<RigidBody>();
Transform transform = entity.GetComponent<Transform>();
rigidBody.Acceleration += rigidBody.Gravity * deltaTime;
rigidBody.Velocity += rigidBody.Acceleration * deltaTime;
rigidBody.Velocity = Vector2.Min(rigidBody.Velocity, TerminalVelocity);
transform.Position += rigidBody.Velocity * deltaTime;
transform.Rotation += rigidBody.AngularVelocity * deltaTime;
}
}