Unity的ECS的缺点是什么?
最近自研游戏引擎,也参考了一些经典游戏引擎的架构,主要有idtech3,UE4,Unity;发现在gameplay层貌似Unity的架构最领先,主要源于ECS

ECS的好处网上介绍很多了,对其整体结构介绍的也不少;先说几个优点:

1,组合>继承,避免了oop范式带来的复杂度问题

2,内存布局提高了缓存命中率

3,便于多线程处理;

当我想实现ECS架构时发现有以下几个问题,第一

1,虽然ecs提高了缓存命中率,但是大前提是 会对多个同原型的 entity的某几个组件进行操作;这种一般发生在大规模集群运动的场景;

对于一般场景,反而是多个不同类型的entity的独立作业,这种似乎ecs作用不大,反而降低了缓存命中

2,关于gameobject的树状结构如何更新的问题一直困惑着我,也就是untiy的gameobject下的那个transform的数据存储 是否不归于 ECS管理?

3,ecs用的组合模式,如何处理 指针 ,引用这类数据结构?可以归为2

 

Unity 的 ECS(Entity Component System) 是一种高效的数据驱动架构,在性能优化方面确实非常强大。然而,它并不是万能的,也存在一些局限性和缺点,尤其在实际开发以及某些特定场景下。这些缺点正好对应你的问题,以下是针对它的深入分析和代码示例。


1. ECS 对缓存命中率提升的限制性

问题描述

ECS 提高缓存命中率的前提是对大量同类型的 Entity 的某些组件进行批量操作(如大规模 AI、粒子、物理系统等)。但在实际项目中,通常会有多种类型的实体和多样化的组件组合,而这些实体是独立且多样化作业的。此时,ECS 的线性内存布局优势无法完全发挥,甚至会降低缓存命中率。

示例代码

适合 ECS 的场景:大规模同类实体操作

假设你需要批量更新一群敌人的位置和速度(典型的 ECS 优化场景):

public struct Position : IComponentData
{
    public float3 Value;
}

public struct Velocity : IComponentData
{
    public float3 Value;
}

public partial struct EnemyMovementSystem : ISystem
{
    public void OnUpdate(ref SystemState state)
    {
        foreach (var (position, velocity) in SystemAPI.Query<RefRW<Position>, RefRO<Velocity>>())
        {
            // 批量处理:更新敌人的位置
            position.ValueRW.Value += velocity.ValueRO.Value * SystemAPI.Time.DeltaTime;
        }
    }
}

优势

  • PositionVelocity 组件存储在连续的内存中,可以高效地批量读取和更新,极大提高了缓存命中率。
问题场景:多样化实体操作

在实际游戏中,场景往往包含多种类型的实体(如玩家、敌人、道具等)。如果需要对每种实体执行不同的逻辑操作,ECS 的组件分离存储可能会导致性能下降:

public struct PlayerData : IComponentData
{
    public int HP;
}

public struct EnemyData : IComponentData
{
    public int Damage;
}

public struct ItemData : IComponentData
{
    public int Value;
}

public partial struct MixedEntitySystem : ISystem
{
    public void OnUpdate(ref SystemState state)
    {
        // 处理玩家数据
        foreach (var player in SystemAPI.Query<RefRW<PlayerData>>())
        {
            player.ValueRW.HP -= 1; // 玩家生命值减少
        }
        
        // 处理敌人数据
        foreach (var enemy in SystemAPI.Query<RefRW<EnemyData>>())
        {
            enemy.ValueRW.Damage += 1; // 敌人伤害增加
        }
        
        // 处理道具数据
        foreach (var item in SystemAPI.Query<RefRW<ItemData>>())
        {
            item.ValueRW.Value += 10; // 道具价值增加
        }
    }
}

劣势

  • 每种组件的数据存储在不同的内存块中,导致 CPU 缓存命中率降低。
  • 无法批量处理多个组件的操作,需要多次遍历,增加了性能开销。

总结

  • ECS 优势场景:同类实体的大规模批量处理(如粒子系统、AI 群体行为)。
  • ECS 劣势场景:多样化实体的独立操作场景。此时,传统的 OOP 或其他混合架构(如 GameObject)可能更高效。

2. GameObject 的树状结构与 Transform 的管理问题

问题描述

Unity 的 GameObject 使用树状结构(父子关系)来管理层级和 Transform 数据。然而,ECS 是完全去中心化的架构,实体之间不存在天然的层级关系。要在 ECS 中实现类似的父子关系,必须通过额外的组件(如 ParentChild)以及系统来手动管理,这会导致复杂性增加。

示例代码

传统 GameObject 的 Transform 管理

在传统的 GameObject 系统中,父子关系和变换更新是自动完成的:

void Update()
{
    transform.position += Vector3.forward * Time.deltaTime; // 父物体移动
    Debug.Log(transform.GetChild(0).position); // 子物体的位置自动更新
}
ECS 中的 Transform 管理

在 ECS 中,Transform 数据被拆分为独立的组件(如 LocalTransformWorldTransform),父子关系需要通过 ParentChild 组件显式管理。例如:

public struct LocalTransform : IComponentData
{
    public float3 Position;
    public quaternion Rotation;
}

public struct Parent : IComponentData
{
    public Entity Value; // 父实体
}

public struct Child : IBufferElementData
{
    public Entity Value; // 子实体
}

public partial struct TransformSystem : ISystem
{
    public void OnUpdate(ref SystemState state)
    {
        // 遍历所有父实体及其子实体
        foreach (var (parentTransform, childBuffer) in
                 SystemAPI.Query<RefRO<LocalTransform>, DynamicBuffer<Child>>())
        {
            foreach (var child in childBuffer)
            {
                if (SystemAPI.HasComponent<LocalTransform>(child.Value))
                {
                    var childTransform = SystemAPI.GetComponent<LocalTransform>(child.Value);
                    // 手动更新子物体的世界变换
                    childTransform.Position += parentTransform.ValueRO.Position;
                    SystemAPI.SetComponent(child.Value, childTransform);
                }
            }
        }
    }
}

劣势

  1. 手动管理父子关系:开发者需要显式管理 ParentChild 组件,增加了实现复杂性。
  2. 性能问题:多层级嵌套的场景中,更新所有子物体的变换会带来较高的性能开销。

总结

  • 传统 GameObject 系统:父子关系和变换更新由 Unity 自动处理,简单直观。
  • ECS 系统:需要显式管理父子关系,代码复杂度增加,性能优化需要额外工作。

3. ECS 对指针和引用的处理问题

问题描述

ECS 的组件是完全解耦的,不允许直接使用指针或引用来表示组件之间的关系。这种设计虽然简化了依赖管理,但也带来了以下问题:

  1. 动态关联的实现:如何在 ECS 中实现实体之间的动态引用?
  2. 复杂关系的表示:如何高效管理复杂的实体关系(如角色与装备、敌人与目标)?

示例代码

传统 OOP 模式中的引用

在传统 OOP 模式中,可以直接使用引用或指针表示实体之间的关系:

public class Weapon
{
    public string Name;
}

public class Character
{
    public Weapon EquippedWeapon;
}

void Example()
{
    var sword = new Weapon { Name = "Sword" };
    var character = new Character { EquippedWeapon = sword };
    Debug.Log(character.EquippedWeapon.Name); // 输出 "Sword"
}
ECS 中的动态引用

在 ECS 中,引用需要通过 Entity 类型或 ID 来间接实现。例如:

public struct Weapon : IComponentData
{
    public int Damage;
}

public struct EquippedWeapon : IComponentData
{
    public Entity WeaponEntity; // 引用武器的实体
}

public partial struct EquipWeaponSystem : ISystem
{
    public void OnUpdate(ref SystemState state)
    {
        foreach (var (equipped, entity) in SystemAPI.Query<RefRO<EquippedWeapon>>().WithEntityAccess())
        {
            if (SystemAPI.HasComponent<Weapon>(equipped.ValueRO.WeaponEntity))
            {
                var weapon = SystemAPI.GetComponent<Weapon>(equipped.ValueRO.WeaponEntity);
                Debug.Log($"Entity {entity.Index} has weapon with damage {weapon.Damage}");
            }
        }
    }
}

劣势

  1. 实现复杂:需要通过实体 ID 或 Entity 类型间接访问其他实体的数据。
  2. 效率问题:每次访问关联实体的数据时,可能会导致性能开销。

总结

  • 传统 OOP 系统:通过指针和引用直接访问其他对象,简单直观。
  • ECS 系统:需要额外的组件和查询逻辑来管理实体之间的动态关联,复杂性增加,性能可能下降。

总结:Unity ECS 的缺点

1. 缓存命中率的提升有限

  • 优势:适合同类实体的大规模批量操作。
  • 缺点:多样化实体的独立操作可能导致缓存失效,降低性能。

2. 树状结构管理复杂

  • 优势:ECS 中的组件解耦提供了灵活性。
  • 缺点:父子关系和层级更新需要手动管理,增加开发复杂性和性能开销。

3. 动态引用的实现困难

  • 优势:避免了直接依赖和状态共享,结构更清晰。
  • 缺点:需要通过实体 ID 或 Entity 类型间接引用,增加了复杂性和运行时查询成本。

建议

  1. 混合架构:在需要层级关系或动态引用的部分(如 UI、场景管理)继续使用传统 GameObject,在性能敏感的部分(如 AI、物理模拟)使用 ECS。
  2. 按需使用 ECS:ECS 是工具而非万能方案。对于多样化的独立实体任务,传统 OOP 或其他架构可能更高效。
Logo

分享前沿Unity技术干货和开发经验,精彩的Unity活动和社区相关信息

更多推荐