三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Unity DOTS Component深度解析:从IComponentData到Hybrid Component实战指南

Unity DOTS Component深度解析:从IComponentData到Hybrid Component实战指南

1. 项目概述:为什么我们需要深入理解DOTS Component?

如果你正在用Unity做项目,尤其是那种需要处理成千上万个单位、粒子或者复杂逻辑的游戏,你大概率已经听过DOTS(Data-Oriented Technology Stack)的大名。DOTS的核心是ECS(Entity Component System),它彻底颠覆了传统面向对象的GameObject/Component模式。但当你真正上手时,第一个拦路虎往往就是“Component”这个概念。在DOTS里,Component不再是挂载在GameObject上的脚本,它变成了一种纯粹的数据结构。这种转变带来的性能提升是巨大的,但理解和使用它的门槛也着实不低。今天,我们就来彻底拆解一下Unity3D DOTS中的Component,从最基础的IComponentData,到复杂的动态缓冲区,再到让人又爱又恨的Hybrid Component,我会结合我踩过的坑和实战经验,帮你理清思路,真正把DOTS Component用起来。

2. DOTS Component的核心设计哲学与类型解析

2.1 从GameObject.Component到ECS.IComponentData:思维的彻底转变

在传统Unity开发中,一个MonoBehaviour脚本挂到GameObject上,它就既是数据容器(字段),又是行为逻辑(方法)。这种“数据与行为强耦合”的模式,在对象数量少时很直观,但当你要管理十万个士兵时,问题就来了。CPU缓存命中率低,大量虚函数调用,GC(垃圾回收)压力山大。

DOTS的Component,准确说是IComponentData,其设计哲学就八个字:数据与行为分离。它只是一个struct(结构体),里面只包含数据,没有任何方法。所有的逻辑处理,都交给System(系统)来批量执行。这种设计带来了几个根本性优势:

  1. 内存布局紧凑(SoA/AoS优化):相同类型的Component数据在内存中是连续存储的(Archetype块内存管理),这极大提高了CPU缓存利用率。系统遍历处理数据时,就像在高速公路上飙车,而不是在乡间小道上绕来绕去。
  2. 支持Burst编译与Job系统:因为IComponentData是纯值类型的struct,它可以被安全地放入NativeArray,交给Burst编译器优化成接近C++性能的机器码,并利用多核并行Job系统处理。
  3. 明确的依赖与组合:实体(Entity)是什么?它就是一个ID,加上一组Component。实体的“身份”和“能力”完全由它拥有的Component组合定义。这比复杂的类继承层次要清晰和灵活得多。

注意:这里有个关键点,IComponentData必须是不可变的struct。这意味着你不能在Component内部持有托管对象(如class)的引用,因为那会破坏Burst编译和Job的安全性。如果你需要字符串、数组等复杂数据,需要使用FixedStringBlobAssetReferenceDynamicBuffer

2.2 三大核心Component类型详解与选型指南

DOTS中的Component并非只有一种,针对不同场景,Unity提供了三种主要类型,用错了地方性能会大打折扣。

1. IComponentData:主力军,用于绝大多数场景这是最常用、性能最好的Component类型。它就是一个简单的数据结构。

public struct Health : IComponentData { public float Value; // 只包含数据字段 } public struct MovementSpeed : IComponentData { public float MetersPerSecond; }

在System中,你可以通过EntityManagerComponentSystemBase的API来查询和修改它们。它的数据直接存储在Entity所在的Archetype Chunk中,访问速度最快。

2. ISharedComponentData:用于数据分组与筛选ISharedComponentData的特殊之处在于,拥有相同ISharedComponentData值的实体会被分组到同一个Chunk中。这常用于渲染分组(如共享同一材质球RenderMesh)、逻辑分组等。

public struct TeamAffiliation : ISharedComponentData, IEquatable<TeamAffiliation> { public int TeamId; public bool Equals(TeamAffiliation other) => TeamId == other.TeamId; public override int GetHashCode() => TeamId.GetHashCode(); }

实操心得ISharedComponentData虽然方便分组,但修改它的代价很高。因为修改一个实体的SharedComponent值,会导致该实体被移动到另一个匹配该值的Chunk中,这可能引发内存重排。所以,适合那些在实体生命周期内极少改变的数据,比如渲染网格、阵营。切勿用它来存储每帧都在变化的数据。

3. DynamicBufferComponentData:处理可变长度数组数据当你需要为实体关联一个列表数据时,比如一个单位的库存物品列表、路径点序列,就需要用到DynamicBuffer<T>

public struct PathBuffer : IBufferElementData { public float3 Position; } // 使用时,通过 EntityManager.AddBuffer<PathBuffer>(entity) 添加

它在内存中并不直接存储在实体Chunk内,而是有一个单独的缓冲区,但通过Entity可以高效访问。它解决了IComponentData无法动态扩容的问题。

选型速查表:

组件类型数据特点性能影响典型应用场景
IComponentData固定大小,值类型,数据独立最优,内存连续,缓存友好生命值、速度、位置(Translation)、状态标识
ISharedComponentData固定大小,值类型,数据共享修改成本高,查询效率高渲染网格、材质、团队、关卡分区
IBufferElementData可变长度数组访问比IComponentData稍慢,但灵活技能队列、路径点、库存物品ID列表

2.3 Hybrid Component:连接旧世界与新世界的桥梁

这是资料中重点提及,也是实践中最容易让人困惑的部分。正如官方文档所说,很多Unity现有的功能(如渲染器、灯光、音频源)还没有原生的DOTS版本。为了让DOTS项目能利用这些现有资源,Hybrid Component登场了。

它是什么?本质上,Hybrid Component是一种特殊的机制,允许你在ECS代码中“访问”传统的UnityEngine.Component。但它不是为了性能,而是为了兼容和过渡。

核心工作原理与“伴侣GameObject”:

  1. 创建时机:Hybrid Component只能在转换系统(GameObjectConversionSystem)中,通过AddHybridComponent方法声明。你不能在运行时动态添加。
  2. 伴侣GameObject:系统会为每个拥有Hybrid Component的实体,在背后创建一个隐藏的(HideFlags)GameObject,称为“Companion GameObject”。这个GameObject上挂载着你声明的那些UnityEngine组件。
  3. 单向链接:实体通过一个名为CompanionLink的托管组件来持有对这个伴侣GameObject的引用。关键点来了:这个链接是单向的(Entity -> GameObject)。你不能从伴侣GameObject反向找到实体。
  4. 数据同步:通常,ECS侧的数据(如实体的LocalToWorld矩阵)会单向同步到伴侣GameObject的Transform上,以驱动渲染位置。反之则不行,直接修改伴侣GameObject的Transform是错误操作,会被ECS系统覆盖。
// 转换系统示例 public class MyRendererConversionSystem : GameObjectConversionSystem { protected override void OnUpdate() { Entities.ForEach((MeshRenderer meshRenderer, MeshFilter meshFilter) => { var entity = GetPrimaryEntity(meshRenderer); // 将传统的MeshRenderer和MeshFilter声明为Hybrid Component AddHybridComponent(meshRenderer); AddHybridComponent(meshFilter); // 同时,我们可能还需要添加ECS原生的渲染组件(如果可用) // DstEntityManager.AddComponentData(entity, new RenderMesh {...}); }); } }

主要限制与使用警告:

  • 无性能收益:使用Hybrid Component不会获得Jobs、Burst或内存优化上的好处。它背后的GameObject和传统GameObject开销一样。
  • 仅数据访问:伴侣GameObject上的组件,大部分事件函数(如UpdateStart)不会被调用。它主要作为一个数据容器被ECS读取。
  • 明确使用(Opt-in):你需要显式声明哪些组件是Hybrid的,它不是自动的。
  • 生命周期绑定:伴侣GameObject的生命周期完全由所属实体管理。实体销毁,它也随之销毁。

踩坑实录:我曾经试图在运行时通过GetComponent<MeshRenderer>()从实体获取Hybrid Component,结果总是null。后来才明白,必须通过EntityManager.GetComponentObject<T>(entity)这个特定的API来获取。这提醒我们,Hybrid Component的访问路径和传统方式、ECS原生方式都不同,需要特别注意。

3. Component的实战:定义、查询与高效操作

3.1 定义最佳实践与内存布局考量

定义一个好的Component是高效使用DOTS的基础。除了选择正确的类型,还有几个细节决定成败。

1. 结构体大小与内存对齐Burst编译器与CPU对数据对齐很敏感。尽量让IComponentData的大小是2的幂次方字节(如16, 32, 64字节),并注意字段顺序,将相同类型的字段放在一起,可以减少内存填充(Padding),提高内存带宽利用率。

// 不佳示例:存在内存空洞 public struct BadLayout : IComponentData { public byte Flag; // 1字节 // 此处编译器可能会插入7字节填充以满足8字节对齐 public float Value; // 4字节 public int Id; // 4字节 } // 总大小可能为16字节 // 更佳示例:紧凑布局 public struct GoodLayout : IComponentData { public float Value; // 4字节 public int Id; // 4字节 public byte Flag; // 1字节 // 总大小可能为12字节,且更紧凑 }

你可以使用Unity.Collections.LowLevel.Unsafe.UnsafeUtility.SizeOf<T>()来检查你的结构体实际大小。

2. 使用标记组件(Tag Component)标记组件是一种不包含任何数据的IComponentData,仅用于在查询中标识实体。

public struct EnemyTag : IComponentData { } public struct ProjectileTag : IComponentData { }

这比用一个bool字段在另一个Component中更高效,因为Archetype查询可以直接通过组件存在性来筛选,无需比较数据。

3. 启用与禁用组件DOTS提供了Enableable Component的概念(通过[EnableableComponent]特性标记)。你可以在运行时动态启用或禁用某个组件,而无需从实体上真正添加或移除它,从而避免改变实体的Archetype(这是一个昂贵的操作)。

[GenerateAuthoringComponent] // 可选,用于生成GameObject挂载脚本 public struct Health : IComponentData, IEnableableComponent { public float Value; } // 在System中禁用/启用 EntityManager.SetComponentEnabled<Health>(entity, false);

3.2 系统(System)中的组件查询模式

System的核心工作就是找到拥有特定组件组合的实体,然后处理它们。EntityQuery是完成这项工作的工具。

基础查询:

public class MovementSystem : SystemBase { protected override void OnUpdate() { // 查询所有拥有Translation, Rotation, MovementSpeed组件的实体 Entities .WithAll<Translation, Rotation, MovementSpeed>() .WithNone<FrozenTag>() // 排除拥有FrozenTag的实体 .ForEach((ref Translation pos, ref Rotation rot, in MovementSpeed speed) => { // 处理逻辑... }).ScheduleParallel(); // 并行调度 } }
  • WithAll<T>: 实体必须拥有所有这些组件。
  • WithAny<T>: 实体必须拥有其中至少一个组件(使用需谨慎,可能影响性能)。
  • WithNone<T>: 实体不能拥有这些组件。
  • WithChangeFilter<T>: 仅处理自上次更新后,T组件数据发生变化的实体。这对于优化性能非常有用,避免处理静止不动的实体。
  • WithEntityQueryOptions(EntityQueryOptions.FilterWriteGroup): 使用Write Group进行更精细的组件过滤。

使用EntityQuery对象进行复杂查询:对于更复杂的查询逻辑,或者需要在多个地方复用查询,可以显式创建EntityQuery

private EntityQuery _movingEnemiesQuery; protected override void OnCreate() { // 在OnCreate中创建查询,避免每帧分配 _movingEnemiesQuery = GetEntityQuery( new EntityQueryDesc { All = new ComponentType[] { typeof(Translation), typeof(EnemyTag), typeof(MovementSpeed) }, None = new ComponentType[] { typeof(FrozenTag) } } ); } protected override void OnUpdate() { var translations = _movingEnemiesQuery.ToComponentDataArray<Translation>(Allocator.TempJob); // ... 使用translations translations.Dispose(); // 切记手动释放临时分配的内存 }

3.3 高效操作组件数据的模式与陷阱

在System中操作组件数据,尤其是使用Job时,必须遵循DOTS的规则。

1. 读写权限与in/ref关键字Entities.ForEach的Lambda参数中:

  • ref ComponentType comp: 表示你需要读写这个组件。系统会为此组件添加写依赖。
  • in ComponentType comp: 表示你只需要读取这个组件。这允许更大的并行性,多个只读该组件的Job可以同时运行。
  • 如果组件没有出现在参数中,但在查询中,默认是只读的。

错误地使用ref会导致不必要的Job依赖,限制并行度。原则:能in就绝不ref

2. 通过EntityManager进行结构性更改添加、移除组件,或销毁实体,被称为“结构性更改”(Structural Change)。这些操作会改变Archetype,绝对不能在并行Job内部或Entities.ForEach的Lambda中直接执行。

// 错误!在Job中执行结构性更改 Entities.ForEach((Entity entity, in Health health) => { if (health.Value <= 0) { EntityManager.DestroyEntity(entity); // 运行时错误! } }).Run(); // 正确做法:使用命令缓冲区(EntityCommandBuffer) private EndSimulationEntityCommandBufferSystem _ecbSystem; protected override void OnUpdate() { var ecb = _ecbSystem.CreateCommandBuffer().AsParallelWriter(); Entities.ForEach((Entity entity, int entityInQueryIndex, in Health health) => { if (health.Value <= 0) { ecb.DestroyEntity(entityInQueryIndex, entity); // 将命令记录到缓冲区 } }).ScheduleParallel(); // 依赖关系会自动处理,命令将在主线程安全执行 _ecbSystem.AddJobHandleForProducer(this.Dependency); }

EntityCommandBuffer是处理结构性更改的标准模式,它将命令记录起来,在Job完成后,在主线程一次性执行。

3. 访问其他实体的组件有时你需要在一个实体的处理逻辑中,读取或修改另一个实体的组件。这需要通过ComponentDataFromEntity<T>来实现。

protected override void OnUpdate() { // 获取一个允许从Entity索引访问Health组件的“字典” var healthFromEntity = GetComponentDataFromEntity<Health>(true); // true表示只读 var healthFromEntityWritable = GetComponentDataFromEntity<Health>(false); // false表示可写 Entities.ForEach((Entity entity, in Damage damage, in Target target) => { if (healthFromEntity.HasComponent(target.Entity)) { var targetHealth = healthFromEntityWritable[target.Entity]; targetHealth.Value -= damage.Amount; healthFromEntityWritable[target.Entity] = targetHealth; } }).ScheduleParallel(); }

注意事项ComponentDataFromEntity在Job中使用时,其读写状态(isReadOnly)必须明确指定,并且要纳入Job的依赖管理。对可写版本的并发访问需要小心竞争条件,通常需要配合NativeHashMap或使用[NativeDisableParallelForRestriction]特性,并手动管理依赖。

4. 性能调优、调试与常见问题排查

4.1 性能瓶颈分析与优化策略

使用DOTS是为了性能,但用不好反而会引入新的瓶颈。以下是几个关键的性能检查点:

1. Archetype碎片化这是最常见的性能杀手。每次为实体添加或移除一个组件,它都可能需要移动到另一个Archetype的Chunk中。频繁的操作会导致内存碎片化和大量的数据移动。

  • 优化策略
    • 使用Enableable组件:代替频繁的添加/移除操作。
    • 批量操作:使用EntityManagerAddComponentRemoveComponent等批量方法,或者通过EntityCommandBuffer在帧末统一处理。
    • 设计稳定的组件组合:在实体创建时就确定好其核心组件集,避免运行时频繁改变“身份”。

2. 低效的EntityQuery

  • 避免WithAnyWithAny查询会导致更复杂的查询计划和可能更低的性能,尽量用WithAllWithNone组合替代。
  • 善用WithChangeFilter:对于非每帧都需要处理的逻辑(如AI决策、状态检测),使用变化过滤器可以大幅减少处理实体数量。
  • 缓存查询结果:如果一组实体列表在多帧内变化不大,可以考虑将EntityQuery的结果(ToEntityArray)缓存起来,并增量更新,而不是每帧重新查询。

3. Job依赖与竞争不合理的Job依赖会阻止并行执行。

  • 检查读写声明:确保Lambda参数中只对真正需要写的组件使用ref
  • 使用ScheduleParallel而非Run:除非Job非常轻量级,或者有严格的顺序要求,否则优先使用ScheduleParallel
  • 使用Dependency属性:正确管理SystemBaseDependency句柄,系统会自动处理Job之间的依赖关系。但如果你手动创建了Job,需要显式管理其依赖。

4. 托管对象与GC压力即使在DOTS中,如果不当使用托管对象(如new数组、字符串操作),仍会触发GC。

  • 使用NativeCollection:在Job和System中使用NativeArrayNativeListNativeHashMap等,它们分配在非托管堆,不受GC管理。
  • 谨慎使用IComponentData中的class:如前所述,这通常是不允许的。对于复杂数据,考虑BlobAsset(不可变数据资产)或DynamicBuffer
  • 及时释放:所有NativeCollection都必须显式调用Dispose()释放,或者使用Allocator.TempJob并在Job完成后释放。

4.2 调试工具与技巧

DOTS的调试比传统模式更复杂,因为数据是分散的。掌握工具至关重要。

1. Entity Debugger (Window > Analysis > Entity Debugger)这是最重要的工具。它可以让你:

  • 查看所有World和System。
  • 查看每个Entity Query匹配的实体列表。
  • 查看Archetype和Chunk:这是核心。你可以看到每个Archetype由哪些组件构成,有多少Chunk,每个Chunk的使用率如何。低使用率的Chunk意味着内存浪费。
  • 实时查看和修改实体的组件数据。

2. Unity Profiler 与 Deep Profiling使用Profiler的Deep Profiling模式,可以深入到每个System和Job的内部,查看耗时。

  • 关注Entities.ForEach和Job的调度开销。
  • 查看主线程等待Job完成的时间(同步点)。
  • 使用“Burst”和“Jobs”分析器类别,查看Burst编译情况和Job执行情况。

3. 自定义调试可视化在开发阶段,为关键组件添加调试绘制功能非常有用。

// 在System中,使用UnityEngine.Debug API(必须在主线程) Entities.WithoutBurst().WithAll<Translation, EnemyTag>().ForEach((in Translation translation) => { Debug.DrawRay(translation.Value, new float3(0, 2, 0), Color.red); }).Run(); // 注意这里用.Run()在主线程执行

注意,Debug.DrawRay等是托管代码,不能在Burst编译的Job中使用,所以需要.WithoutBurst().Run()

4.3 常见问题排查速查表

问题现象可能原因排查步骤与解决方案
运行时报错:InvalidOperationException在Job中访问了托管对象或执行了不安全操作。1. 检查Job中是否使用了refin之外的参数类型(如EntityManager)。
2. 检查是否在Job中直接进行了结构性更改。
3. 确保所有NativeCollection在Job声明时已正确传递依赖。
性能不升反降Archetype碎片化严重;Job依赖过重;查询效率低。1. 用Entity Debugger查看Archetype数量和各Chunk使用率。
2. 在Profiler中查看Job调度和执行时间线,检查是否有长时间的主线程等待。
3. 审查EntityQuery,避免WithAny,尝试添加WithChangeFilter
Hybrid Component不显示/位置不对Companion GameObject创建失败或数据同步问题。1. 确认转换系统正确调用了AddHybridComponent
2. 检查实体是否有LocalToWorldTranslationRotation组件来驱动位置。
3. 使用EntityManager.GetComponentObject<Transform>(entity)获取伴侣Transform,手动检查其位置。
Burst编译警告或错误代码中存在Burst不支持的C#特性。1. 查看Console中的Burst警告信息。
2. 常见问题:使用了try-catchstring格式化、虚函数调用、委托(非函数指针)等。
3. 将不支持的逻辑移到[BurstCompile]方法之外,或用[BurstDiscard]标记。
内存泄漏(Memory Leak)NativeCollection未正确释放。1. 确保每个Allocator.PersistentAllocator.TempJob的分配都有对应的Dispose()
2. 对于Allocator.Temp,确保在方法返回前释放。
3. 使用Unity的Memory Profiler工具追踪非托管内存分配。
实体查询不到组件未正确添加;查询条件有误;实体处于禁用状态。1. 在Entity Debugger中直接搜索该Entity ID,查看其拥有的组件列表。
2. 检查查询的WithAll/WithAny/WithNone条件是否与实体组件匹配。
3. 检查相关组件是否被SetComponentEnabled禁用了。

5. 进阶模式与架构思考

5.1 组件设计模式:超越基础数据存储

当项目规模变大,良好的组件设计模式能保持代码清晰。

1. 状态机与组件不要用MonoBehaviour里那种Updateswitch-case的状态机。在ECS中,用不同的组件组合来表示状态。

// 状态:巡逻 public struct PatrolState : IComponentData { public float3 PatrolCenter; public float PatrolRadius; } // 状态:追击 public struct ChaseState : IComponentData { public Entity TargetEntity; } // 状态:攻击 public struct AttackState : IComponentData { public float AttackCooldown; }

一个敌人实体在同一时间只会拥有PatrolStateChaseStateAttackState中的一个。切换状态时,就是移除旧状态组件,添加新状态组件。然后由不同的System(PatrolSystemChaseSystemAttackSystem)分别处理对应状态的实体。

2. 事件组件(One-frame Components)用于在系统间传递事件消息。添加后,在下一帧由负责处理的系统消费并移除。

public struct DamageEvent : IComponentData { public Entity Target; public Entity Instigator; public float Amount; } // 攻击系统产生事件 public class AttackSystem : SystemBase { protected override void OnUpdate() { var ecb = ...; Entities.ForEach((Entity attacker, in AttackCommand cmd) => { ecb.AddComponent(attacker, new DamageEvent { Target = cmd.Target, Amount = 10 }); }).ScheduleParallel(); } } // 伤害处理系统消费并移除事件 public class DamageSystem : SystemBase { protected override void OnUpdate() { Entities.ForEach((Entity entity, ref Health health, in DamageEvent dmgEvent) => { health.Value -= dmgEvent.Amount; }).ScheduleParallel(); // 本系统执行完后,需要移除所有DamageEvent组件 EntityManager.RemoveComponent<DamageEvent>(GetEntityQuery(typeof(DamageEvent))); } }

3. 单例组件(Singleton Component)用于存储全局游戏状态,如游戏时间、分数、玩家实体引用等。通常通过一个特殊的单例实体来持有。

public struct GameTime : IComponentData { public float ElapsedTime; public float DeltaTime; } // 在Bootstrap系统中创建单例实体 var singletonEntity = EntityManager.CreateEntity(); EntityManager.AddComponent<GameTime>(singletonEntity); // 在其他系统中通过GetSingleton获取 var gameTime = GetSingleton<GameTime>();

5.2 与Unity引擎其他模块的协作

DOTS不是孤岛,最终还是要和渲染、物理、UI等交互。

1. 与渲染管线(URP/HDRP)协作对于自定义渲染,ECS提供了EntitiesGraphics包。你需要定义MaterialOverride等组件。对于Hybrid渲染,确保转换系统正确添加了RenderMesh(Hybrid)或MeshInstanceRenderer等组件,并且实体的LocalToWorld矩阵数据正确。

2. 与物理(Unity Physics)协作使用Unity.Physics包。物理实体同样由Component定义,如PhysicsVelocityPhysicsMassPhysicsCollider。物理模拟在一个独立的PhysicsWorld中运行,你需要通过PhysicsStep组件来配置,并通过PhysicsWorldSingleton来访问物理查询结果。

3. 与UI(UI Toolkit/UGUI)交互这是目前DOTS的薄弱环节。通常的做法是:

  • 在ECS中维护UI需要的数据(如玩家血量、敌人数量)。
  • 创建一个传统的MonoBehaviour系统,每帧从ECS单例组件中读取这些数据。
  • MonoBehaviour系统负责调用UI API(如Document.rootVisualElement.Q<Label>("health").text)来更新界面。
  • 反之,UI事件(如按钮点击)也通过这个MonoBehaviour系统接收,然后转换成ECS事件组件(如ButtonClickEvent)添加到世界中。

5.3 面向未来的组件设计考量

DOTS仍在快速发展中。在设计组件时,考虑以下趋势:

  • Netcode for Entities:如果你计划做多人游戏,需要考虑网络同步。为组件添加[GenerateAuthoringComponent]特性可以方便生成GameObject界面,但网络同步通常需要定义ICommandDataIRpcData。在设计数据结构和状态时,提前思考哪些数据需要同步、如何压缩(如使用Quantized)。
  • Burst-Compatible Mathematics:坚持使用Unity.Mathematics中的float3,quaternion,bool4等类型,它们是为SIMD和Burst优化而生的。
  • Code Generation:考虑使用Source Generators或自定义工具来生成重复的组件和系统代码,减少样板代码,例如自动为每个组件生成对应的“添加”、“移除”命令缓冲区扩展方法。

我个人在大型项目中的体会是,DOTS Component的成功应用,始于对“数据驱动”思维的真正接纳。它强迫你从“这个对象要做什么”转向“描述这个世界有哪些数据,以及这些数据如何被变换”。初期会感到束缚,但当你习惯了这种思维,并看到成千上万的实体流畅运行时的性能表现,你会觉得这一切都是值得的。最后一个小技巧:在项目早期,就建立严格的组件命名和分类规范,比如所有标签组件都以Tag结尾,所有事件组件都以Event结尾,所有状态组件都以State结尾,这能在项目复杂度提升时,极大减轻心智负担。

← 返回列表