LeoECS - 简单的轻量级 C# 实体-组件-系统框架
性能,零/小 内存 分配/占用空间,这个项目的主要目标——不依赖于任何游戏引擎。
**重要!**它是“基于结构”的版本,如果你搜索“基于类”的版本-检查基于类的分支!
本框架要求C#7.3或以上。
**重要!**不要忘记在生产环境中使用调试版本进行开发和发布版本:所有内部错误检查/异常抛出只在调试版本中起作用,并在发布环境中出于性能原因而被删除。
**重要!**Ecs核心API不安全,永远不会安全!如果您需要多线程处理-您应该在您的ecs系统中实现它。
下载
作为Unity模块
此存储库可以直接从git url安装为unity模块。这样,新行应该添加到“Packages/manifest.json”中:
"com.leopotam.ecs": "https://github.com/Leopotam/ecs.git",
默认情况下,将使用最新发布的版本。如果您需要中继/开发版本,则应在哈希之后添加分支的“开发”名称:
"com.leopotam.ecs": "https://github.com/Leopotam/ecs.git#develop",
作为来源
如果您不能/不想使用unity模块,可以从Releases page下载代码作为所需版本的存档源。
ECS主要部件
Component(组件)
用户数据容器,内部没有/有小逻辑:
struct WeaponComponent {
public int Ammo;
public string GunName;
}
**重要!**别忘了手动初始化每个新组件的所有字段-它们将在回收到池时重置为默认值。
Entity(实体)
组件容器。实现为“EcsEntity”,用于包装内部标识符:
//在世界上下文中创建新实体
EcsEntity entity = _world.NewEntity ();
//Get()返回实体上的组件。如果组件不存在-将添加该组件。
ref Component1 c1 = ref entity.Get<Component1> ();
ref Component2 c2 = ref entity.Get<Component2> ();
//Del()从实体中删除组件。
entity.Del<Component2> ();
//可以用组件的新实例替换组件。如果组件不存在-它将被添加。
var weapon = new WeaponComponent () { Ammo = 10, GunName = "Handgun" };
entity.Replace (weapon);
//使用Replace()可以链接组件的创建:
var entity2 = world.NewEntity ();
entity2.Replace (new Component1 { Id = 10 }).Replace (new Component2 { Name = "Username" });
//任何实体都可以与所有组件一起复制:
var entity2Copy = entity2.Copy ();
//任何实体都可以合并/“移动”到另一个实体(源将被销毁):
var newEntity = world.NewEntity ();
entity2Copy.MoveTo (newEntity);
//entity2Copy中的所有组件已移动到newEntity,entity2Copy已销毁。
//任何实体都可以被摧毁。
entity.Destroy ();
**重要!**没有组件的实体将在最后一次
EcsEntity.Del()
调用时自动删除。
System(系统)
用于处理过滤实体的逻辑容器。用户类应实现 “IECsInitSystem”、 “IEcsDestroySystem”、 “IEcsRunSystem”(或其他支持的)接口:
class WeaponSystem : IEcsPreInitSystem, IEcsInitSystem, IEcsDestroySystem, IEcsPostDestroySystem {
public void PreInit () {
//将在EcsSystems.Init()调用期间和iecsintsystem.Init之前调用一次。
}
public void Init () {
//将在EcsSystems.Init()调用期间调用一次。
}
public void Destroy () {
//将在EcsSystems.Destroy()调用期间调用一次。
}
public void PostDestroy () {
//将在EcsSystems.Destroy()调用期间和IEcsDestroySystem.Destroy之后调用一次。
}
}
class HealthSystem : IEcsRunSystem {
public void Run () {
//将在每个EcsSystems.Run()调用上调用。
}
}
数据注入
ECS系统的所有兼容“EcsWorld”和“EcsFilter”字段将自动初始化(自动注入):
class HealthSystem : IEcsSystem {
//自动注入字段。
EcsWorld _world = null;
EcsFilter<WeaponComponent> _weaponFilter = null;
}
任何自定义类型的实例都可以通过EcsSystems.Inject()
方法注入到所有系统:
var systems = new EcsSystems (world)
.Add (new TestSystem1 ())
.Add (new TestSystem2 ())
.Add (new TestSystem3 ())
.Inject (a)
.Inject (b)
.Inject (c)
.Inject (d);
systems.Init ();
每个系统都将被扫描到具有适当初始化的兼容字段(可以包含所有字段或没有一个字段)。
**重要!**任何用户类型的数据注入都可以用于在系统之间共享外部数据。
多ECS系统的数据注入
如果您想使用多个“EcsSystems”,您可以找到DI的奇怪行为
struct Component1 { }
class System1 : IEcsInitSystem {
EcsWorld _world = null;
public void Init () {
_world.NewEntity ().Get<Component1> ();
}
}
class System2 : IEcsInitSystem {
EcsFilter<Component1> _filter = null;
public void Init () {
Debug.Log (_filter.GetEntitiesCount ());
}
}
var systems1 = new EcsSystems (world);
var systems2 = new EcsSystems (world);
systems1.Add (new System1 ());
systems2.Add (new System2 ());
systems1.Init ();
systems2.Init ();
您将在控制台获得“0”。问题是DI从每个“EcsSystems”中的“ Init()”方法开始。这意味着任何新的“EcsFilter”实例(具有延迟初始化)将只正确地注入到当前的“EcsSystems”中。
要修复此行为,应按以下方式修改启动代码:
var systems1 = new EcsSystems (world);
var systems2 = new EcsSystems (world);
systems1.Add (new System1 ());
systems2.Add (new System2 ());
systems1.ProcessInjects ();
systems2.ProcessInjects ();
systems1.Init ();
systems2.Init ();
修复后你应该在控制台得到“1”。
指定类
EcsFilter
用于保存具有指定组件列表的筛选实体的容器:
class WeaponSystem : IEcsInitSystem, IEcsRunSystem {
//自动注入字段:EcsWorld实例和EcsFilter。
EcsWorld _world=null;
//我们想要得到有“WeaponComponent”而没有“HealthComponent”的实体。
EcsFilter<WeaponComponent>.Exclude<HealthComponent> _filter = null;
public void Init () {
_world.NewEntity ().Get<WeaponComponent> ();
}
public void Run () {
foreach (var i in _filter) {
//包含WeaponComponent的实体。
ref var entity = ref _filter.GetEntity (i);
//Get1将返回链接到附加的“WeaponComponent”。
ref var weapon = ref _filter.Get1 (i);
weapon.Ammo = System.Math.Max (0, weapon.Ammo - 1);
}
}
}
**重要!**如果要销毁此数据的一部分(实体或组件),则不应对此筛选器上foreach循环之外的任何筛选器数据使用“ref”修饰符—这将破坏内存完整性。
filterInclude
约束中的所有组件都可以通过EcsFilter.Get1()
、EcsFilter.Get2()
等进行快速访问—顺序与在筛选器类型声明中使用的顺序相同。
如果不需要快速访问(例如,对于没有数据的基于标志的组件),组件可以实现“IEcsIgnoreInFilter”接口,以减少内存使用并提高性能:
struct Component1 { }
struct Component2 : IEcsIgnoreInFilter { }
class TestSystem : IEcsRunSystem {
EcsFilter<Component1, Component2> _filter = null;
public void Run () {
foreach (var i in _filter) {
//它的有效代码。
ref var component1 = ref _filter.Get1 (i);
//由于内存/性能原因,_filter.Get2()的缓存导致其无效代码为空。
ref var component2 = ref _filter.Get2 (i);
}
}
}
重要信息:任何过滤器都支持最多6种组件类型,如“include”约束,最多支持2个组件类型作为“排除”约束。约束更短—性能更好。
重要提示:如果您尝试使用两个具有相同组件但顺序不同的筛选器,则会出现异常,其中包含有关冲突类型的详细信息,但仅限于“DEBUG”模式。在“RELEASE”模式下,将跳过所有检查。
EcsWorld
所有实体/组件的根级容器,工作方式与隔离环境类似。
重要提示:当实例不再使用时,不要忘记调用
EcsWorld.Destroy()
方法。
EcsSystems
要处理“EcsWorld”实例的系统组:
class Startup : MonoBehaviour {
EcsWorld _world;
EcsSystems _systems;
void Start () {
//创建ecs环境。
_world = new EcsWorld ();
_systems = new EcsSystems (_world)
.Add (new WeaponSystem ());
_systems.Init ();
}
void Update () {
//处理所有相关系统。
_systems.Run ();
}
void OnDestroy () {
//销毁系统逻辑组。
_systems.Destroy ();
//毁灭世界。
_world.Destroy ();
}
}
EcsSystems
实例可用作嵌套系统(支持任何类型的IEcsInitSystem
、IEcsRunSystem
、ECS行为):
//initialization初始化。
var nestedSystems = new EcsSystems (_world).Add (new NestedSystem ());
//不要在这里调用nestedSystems.Init(),rootSystems会自动执行。
var rootSystems = new EcsSystems (_world).Add (nestedSystems);
rootSystems.Init ();
//update loop 更新循环。
//不要在这里调用nestedSystems.Run(),rootSystems将自动执行它。
rootSystems.Run ();
// destroying 销毁
//不要在这里调用nestedSystems.Destroy(),rootSystems会自动执行。
rootSystems.Destroy ();
在运行时可以进行处理启用或禁用任何“IEcsRunSystem”或“EcsSystems”实例:
class TestSystem : IEcsRunSystem {
public void Run () { }
}
var systems = new EcsSystems (_world);
systems.Add (new TestSystem (), "my special system");
systems.Init ();
var idx = systems.GetNamedRunSystem ("my special system");
//这里的状态为真,默认情况下所有系统都处于活动状态。
var state = systems.GetRunSystemState (idx);
//禁止系统执行。
systems.SetRunSystemState (idx, false);
引擎集成
Unity
在unity 2019.1上测试(不依赖于它),并包含用于编译到单独的程序集文件的程序集定义(出于性能原因)。
Unity编辑器集成包含代码模板和world debug viewer。
自定义引擎Custom engine
代码示例-每个部分应集成在引擎执行流的适当位置
using Leopotam.Ecs;
class EcsStartup {
EcsWorld _world;
EcsSystems _systems;
//ecs世界和系统初始化。
void Init () {
_world = new EcsWorld ();
_systems = new EcsSystems (_world);
_systems
// 在此处注册系统,例如:
// .Add (new TestSystem1 ())
// .Add (new TestSystem2 ())
// 注册一帧组件(顺序很重要),例如:
// .OneFrame<TestComponent1> ()
// .OneFrame<TestComponent2> ()
// 在此处插入服务实例(顺序并不重要),例如
// .Inject (new CameraService ())
// .Inject (new NavMeshSupport ())
.Init ();
}
//引擎更新循环。
void UpdateLoop () {
_systems?.Run ();
}
//清理。
void Destroy () {
if (_systems != null) {
_systems.Destroy ();
_systems = null;
_world.Destroy ();
_world = null;
}
}
}
LeoECS支持的项目
With sources:
- 太空入侵者(枪弹变异)游戏
- 跑步者游戏
- 吃豆人游戏
- TicTacToe game (obsoleted api). “Making of” video (in Russian)
- GTA5自定义模式(由基于类的版本提供支持))
发布的游戏:
拓展Extensions
- Unity editor integration
- Unity uGui events support
- Multi-threading support
- Service locator
- Engine independent types
常见问题(FAQ)
基于结构,基于类的版本?哪个更好?为什么?
基于类的版本是稳定的,但在活跃的开发环境下不会再稳定了——除了错误修复(可以在“基于类”的分支中找到)。
结构只基于一个正在进行开发的版本。它应该比基于类的版本更快,组件清理更简单,并且您可以稍后更轻松地切换到“unity ecs”(如果您愿意)。即使在“unity ecs”发布之后,这个框架仍将处于开发阶段。
我想知道——组件是否已经添加到实体中,并获得它/添加新组件,否则,我如何做到?
如果您不关心组件是否已添加,并且您只想确保实体包含该组件-只需调用EcsEntity.Get<T>
-它将返回已存在的组件,如果不存在,则添加全新的组件。
如果您想知道该组件是否存在(稍后在自定义逻辑中使用它),请使用EcsEntity.Has<T>
方法,该方法将返回该组件之前添加的事实。
我想在MonoBehaviour.Update() 处理一个系统,在MonoBehaviour.FixedUpdate()处理另一个系统。我该怎么做?
For splitting systems by MonoBehaviour
-method multiple EcsSystems
logical groups should be used:
应使用多个“EcsSystems”逻辑组的MonoBehaviour
方法来拆分系统:
EcsSystems _update;
EcsSystems _fixedUpdate;
void Start () {
var world = new EcsWorld ();
_update = new EcsSystems (world).Add (new UpdateSystem ());
_update.Init ();
_fixedUpdate = new EcsSystems (world).Add (new FixedUpdateSystem ());
_fixedUpdate.Init ();
}
void Update () {
_update.Run ();
}
void FixedUpdate () {
_fixedUpdate.Run ();
}
我喜欢依赖注入的工作方式,但是我想跳过初始化中的一些字段。我该怎么做?
您可以在系统的任何字段上使用[EcsIgnoreInject]
属性:
...//将被注入。EcsFilter<C1> _filter1 = null;//将跳过。[EcsIgnoreInject]EcsFilter<C2> _filter2 = null;
我不喜欢foreach循环,我知道for循环更快。我怎么用?
当前foreach循环的实现速度足够快(自定义枚举器,无内存分配),在10k项和更多项上可以发现较小的性能差异。当前版本不再支持for循环迭代。
我一次又一次地复制和粘贴我的重置组件代码。我怎么能用其他方式做呢?
如果要简化代码并将reset/init代码保留在一个位置,可以设置自定义处理程序来处理组件的清理/初始化:
struct MyComponent : IEcsAutoReset<MyComponent>
{
public int Id;
public object LinkToAnotherComponent;
public void AutoReset(ref MyComponent c)
{
c.Id = 2; c.LinkToAnotherComponent = null;
}
}
对于全新的组件实例,在从实体中移除组件之后,在回收到组件池之前,将自动调用此方法。
重要提示:对于自定义的“AutoReset”行为,引用类型字段没有任何附加检查,您应该提供正确的cleanup/init行为,而不会出现内存泄漏。
我将组件用作只工作一个帧的事件,然后在执行序列的最后一个系统中删除它。太无聊了,我怎么能把它自动化呢?
如果要删除单帧组件而不附加自定义代码,可以在“EcsSystems”中注册它们:
struct MyOneFrameComponent
{
}
EcsSystems _update;
void Start()
{
var world = new EcsWorld(); _update = new EcsSystems(world);
_update
.Add(new CalculateSystem())
.Add(new UpdateSystem()).OneFrame<MyOneFrameComponent>().Init();
}
void Update() { _update.Run(); }
重要提示:具有指定类型的所有单帧组件都将在执行流中的 调用OneFrame()注册该组件的位置处 删除。
我需要对内部结构的默认缓存大小进行更多的控制,我该怎么做?
可以使用EcsWorldConfig
实例设置自定义缓存大小:
var config = new EcsWorldConfig()
{
// World.Entities default cache size.
WorldEntitiesCacheSize = 1024,
// World.Filters default cache size.
WorldFiltersCacheSize = 128,
// World.ComponentPools default cache size.
WorldComponentPoolsCacheSize = 512,
// Entity.Components default cache size (not doubled).
EntityComponentsCacheSize = 8,
// Filter.Entities default cache size.
FilterEntitiesCacheSize = 256,
};
var world = new EcsWorld(config); ...
我需要超过6个“包括”或超过2个“排除”在过滤器组件,我怎么做呢?
You can use EcsFilter autogen-tool and replace EcsFilter.cs
file with brand new generated content.
我想添加一些反应行为对过滤器项目的变化,我怎么做呢?
可以使用 LEOECS_FILTER_EVENTS
定义来启用自定义事件侦听器对筛选器的支持:
class CustomListener : IEcsFilterListener
{
public void OnEntityAdded(in EcsEntity entity)
{
// reaction on compatible entity was added to filter. 对兼容实体的反应已添加到筛选器。
}
public void OnEntityRemoved(in EcsEntity entity)
{
// reaction on noncompatible entity was removed from filter.
//对不相容实体的反应已从筛选器中移除。
}
}
class MySystem : IEcsInitSystem, IEcsDestroySystem
{
readonly EcsFilter<Component1> _filter = null;
readonly CustomListener _listener = new CustomListener(); public void Init()
{
// subscribe listener to filter events.
//订阅侦听器以筛选事件。
_filter.AddListener(_listener);
}
public void Destroy()
{
// unsubscribe listener to filter events.
//取消订阅侦听器以筛选事件。
_filter.RemoveListener(_listener);
}
}