Unity简单的轻量级ECS框架 LeoECS中文文档

LeoECS是一个独立于任何游戏引擎的轻量级C#框架,专注于高性能和小内存占用。它采用结构化设计,支持组件、实体和系统,强调零/小内存分配。框架要求C#7.3及以上版本,并提供了多线程处理的实现。EcsSystems用于管理系统,支持数据注入和多系统组合。组件和实体的生命周期管理,以及EcsFilter用于处理特定组件的实体集合。该框架还支持Unity集成,并提供了用于优化性能的工具和特性。
摘要由CSDN通过智能技术生成

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实例可用作嵌套系统(支持任何类型的IEcsInitSystemIEcsRunSystem、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:

发布的游戏:

拓展Extensions

常见问题(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);
    }
}
  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

雪野Solye

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值