- Introduction
- Main parts of ecs
- Other classes
- Engine integration
- Other features
- Benchmarks
- Plans
- Contributors
This is a ECS library for Delphi/FreePascal.
Supported Delphi version: I've tested it on Delphi 11.2, should work on older versions with generics too. Win32 and Win64 works, Linux should work too, Android is planned. Supported FPC version: I've tested it on FPC 3.2.2.
The library is a single file ecs.pas
, add it to your project and then do:
type
// declare components
// they are just records
TPosition = record
x, y: Integer;
constructor Create(x, y: integer); //not required, just for convenience
end;
TVelocity = record
vx, vy: Integer;
constructor Create(vx, vy: integer); //not required, just for convenience
end;
// declare systems
TUpdatePositionSystem = class(TECSSystem)
function Filter: TECSFilter; override;
procedure Process(e: TECSEntity); override;
end;
function TUpdatePositionSystem.Filter: TECSFilter;
begin
Result := world.Filter.Include<TPosition>.Include<TVelocity>;
end;
procedure TUpdatePositionSystem.Process(e: TECSEntity);
var
pos: ^TPosition;
speed: TVelocity;
begin
pos := e.GetPtr<TPosition>;
speed := e.Get<TVelocity>;
pos^.x := pos^.x+speed.x;
pos^.y := pos^.y+speed.y;
// alternatively, use e.Get<TPosition> and then e.Update<TPosition>(pos)
end;
//now main loop of ECS:
var
world: TECSWorld;
ent: TECSEntity;
systems: TECSSystems;
i: Integer;
begin
// create world
world := TECSWorld.Create;
// create entities
for i := 1 to 5 do
world.NewEntity.Add<TPosition>(TPosition.Create(10, 10));
for i := 1 to 5 do
begin
ent := world.NewEntity;
ent.Add<TPosition>(TPosition.Create(2, 2));
ent.Add<TVelocity>(TVelocity.Create(1, 1));
end
// create systems
systems = TECSSystems.Create(world);
systems.add(UpdatePositionSystem) //you can add a created system or just pass a class
// run systems
systems.Init;
for i := 1 to 5 do
systems.Execute;
systems.Teardown;
end.
Сontainer for components. Consists from UInt64 and pointer to World
:
TECSEntity = record
World: TECSWorld;
id: TEntityID;
...
// Creates new entity in world context.
// Basically just allocates a new identifier so it's fast.
Entity := World.NewEntity;
// Entity is destroyed when last component removed from it.
Entity.RemoveAll;
Container for user data without / with small logic inside. It is just records (could be any type actually, but records are most useful here) :
TComp1 = record
x : Integer;
y : Integer;
name : String;
end;
Components can be added, requested, removed:
comp1.x := 0;
comp1.y := 0;
comp1.name := 'name';
Entity := World.NewEntity;
Entity.Add<TComp1>(comp1);
// Will raise exception if component isn't present
comp1 = Entity.Get<TComp1>;
if Entity.TryGet<TComp2>(comp2) then ... //will return false if component isn't present
Entity.Remove<TComp1>;
They can be updated (changed) using several ways:
var
comp1: TComp1;
pcomp1: ^TComp1;
begin
Entity := World.NewEntity;
Entity.Add<TComp1>(TComp1.Create(0, 0, 'name'));
// Replace Comp1 with another instance of Comp1.
// Will raise exception if component isn't present
entity.Update<TComp1>(TComp1.Create(1, 1, 'name1'));
entity.AddOrUpdate<TComp1>(TComp1.Create(2, 2, 'name2')); // Adds TComp1 or replace it if already present
// returns Pointer(Comp1), so you can access it directly
pcomp1 := Entity.GetPtr<TComp1>;
pcomp1^.x := 5;
// important - after deleting, component in a pool would be reused
// so don't save a pointer if you are not sure that component won't be deleted
It is not uncommon in ECS to use a components without data, they are called "tags". As constructing of such entity can be cumbersome in pascal, PasECS provides overload of Add
and AddOrUpdate
without params:
type
TFloating = record
end;
...
ent.Add<TColor>(clBlue); // adds normal component TColor
ent.Add<TFloating>; //adds tag TFloating to entity
Сontainer for logic for processing filtered entities.
User class can override Init
, Execute
, Teardown
, Filter
and Process
(in any combination. Just skip methods you don't need).
TUserSystem = class(TECSSystem)
// property World: TECSWorld - world that system belongs
// virtual constructor if you need extra logic in constructor
// (creating fields etc).
// Called when `systems.Add(TUserSystem)` is used
constructor Create(AOwner: TECSWorld);override;
// Will be called once during `TECSSystems.Init` call
procedure Init; override;
// Called once during `TECSSystems.Init`, after `Init` call.
// If this method present, it should return a filter that will be
// applied to a world.
// Example:
// Result := World.Filter.Include<SomeComponent>.Exclude<Other>;
function Filter: TECSFilter; override;
// Will be called on each `TECSSystems.Execute` call
procedure Execute; override;
// Will be called during `TECSSystems.Execute` call, before `sys.Execute`, once for every entity that match `Filter`
procedure Process(e: TECSEntity); override;
// Will be called once during `TECSSystems.Teardown` call
procedure Teardown; override;
end;
Root level container for all entities / components, is iterated with TECSSystems:
World := TECSWorld.Create;
// you can delete all entities
World.Clear;
// you can create entity
Entity := World.NewEntity;
// you can iterate all entities in world
for Entity in World do
writeln(Entity.ToString);
// you can create filters
Filter := World.Filter.Include(comp1).Exclude(comp2).Include(comp3);
Allows to iterate over entities with specified conditions.
Created by call World.Filter
or inside a TECSSystem.Filter
.
Filters that is possible:
Include<TComp1>
: Component of typeComp1
must be present on entityExclude<TComp2>
: Entities that contain TComp2 will be excluded from filter
All of them can be called 0, 1, or many times using method chaining. Currently, limitation is that Include
must be called at least once.
You can iterate filter using usual for entity in filter do ...
Group of systems to process TECSWorld
instance:
World := TECSWorld.Create;
Systems = TECSSystems.Create(World);
Systems
.Add(MySystem1.Create(world, SomeParam))
.Add(MySystem2) { shortcut for add(MySystem2.new(systems.World)) }
.Add(MySystem3);
Systems.Init;
repeat
Systems.Execute;
until ShouldQuit;
Systems.Teardown;
You can add Systems to Systems to create hierarchy.
System can be in states:
Created
- constructor was called, butInit
wasn't. more systems could be added in this stateInitialized
-Init
was called, now no more systems can be added, but nowExecute can be called
TearedDown
-Teardown
was called, can't doExecute
anymore.
You can inherit your class from TECSSystems
to add systems automatically:
TSampleSystem = class(TECSSystems)
constructor Create(AOwner: TECSWorld); override;
end;
constructor TSampleSystem.Create(AOwner: TECSWorld);
begin
inherited;
Add(TInitPlayerSystem);
Add(TKeyReactSystem.Create(aOwner, CONFIG_PRESSED,CONFIG_DOWN));
Add(TReactPlayerSystem);
Add(MovePlayerSystem);
Add(RotatePlayerSystem);
Add(StopRotatePlayerSystem);
Add(SyncPositionWithPhysicsSystem);
Add(DrawDebugSystem);
end;
//TODO
In a folder bench
there is a tests suite and benchmark, you can see it for some examples. Proper example is planned.
In a folder vcl_example i've added simple example of adding ecs to VCL application.
You can get total number of alive entities using world.EntitiesCount
It is also possible to get statistics of how much components exists in world:
var
w: TECSWorld;
stats: TECSWorld.TStatsArray;
stat: TECSWorld.TStatsPair;
begin
...
stats := w.Stats; //alternatively, you can use `w.Stats(stats)` to avoid allocating array every time
for stat in stats do
writeln(' ', stat.Key, ': ', stat.Value);
end;
Sometimes you just need to check if some component is present in a world. No need to create a filter for it - just use
if world.Exists<SomeComponent> then ...
You can also count number of components using
world.Count<SomeComponent>
You can also iterate over single component without creating TECSFilter
using world.Query<T>
.
It returns a lightweight enumerable, that can be iterated using
for entity in world.Query<TMyComponent> do ...
Note that it returns entities, not components. To obtain actual components you can do
for entity in world.Query<TMyComponent> do
begin
comp1 := entity.Get<TMyComponent>;
...
end;
This could be useful when iterating inside System#process
:
function TFindNearestTarget.Filter(World: TECSWorld);
begin
Result := World.Include<Pos>.Include<FindTarget>;
end
procedure TFindNearestTarget.Process(Entity: TECSEntity);
var
Target, Nearest: TECSEntity;
OurPos, Range, NearestRange: Double;
begin
OurPos := entity.Get<Pos>;
Nearest := nil;
NearestRange := Inf;
// world.Filter.Include<IsATarget> will allocate a Filter
// so you should create it at constructor and store it somewhere
// so here is an easier way:
for Target in world.query<IsATarget> do
begin
Range := distance(Target.get<Pos>, pos);
if Range < NearestRange then
begin
Nearest := Target;
NearestRange := Range;
end
end;
// ...
end;
//TODO
//TODO
- runtime statistics
- automatic benchmark in TECSSystems?
-
for entity in World.Query<T>...
-
if World.Exists<T> then...
- check correctness when deleting entities during iteration
- nonoengine integration example, maybe example with VCL
- CI with FPC
- generations in EntityID
- SingleFrame components
- Singleton components
- Callbacks on adding\deleting components
- Multiple components
- Android target (
[weak]
annotations etc) - Switch to sparsesets? archetypes?
- Andrey Konovod - creator and maintainer