ECS之Component组件

洪流学堂,让你快人几步。你好,我是跟着大智学Unity的萌新,我叫小新,最近在跟着大智学习DOTS。
Entity之后,咱们来到了Component组件。Component是ECS架构的三个基本元素之一。Component组件中包含了游戏或者应用的数据。Entity是组件集合的索引,System包含具体的逻辑行为。

ECS之Component组件

ECS中的组件是一个结构体,需要实现下列之一的接口:
  • IComponentData :最基本的,也是使用最多的接口,没有特殊需求的组件都可以实现这个接口。用于通用的目的和chunk components。
  • IBufferElementData :给entity关联dynamic(动态) buffer
  • ISharedComponentData :可以通过数据来对entity的原型进行分组
  • ISystemStateComponentData :给entity关联一个系统的状态,可以检测entity何时被创建或者销毁
  • ISharedSystemStateComponentData :同时包含 ISharedComponentData 和 ISystemStateComponentData 中的功能。
  • Blob assets :技术上来说不是一个component,你可以使用blob assets存储数据。Blob asset可以被一个或者多个component通过 BlobAssetReference 只读引用。你可以使用blob asset共享数据,也可以在C# job中访问。
之前在ECS核心概念那一节说过,EntityManager会使用 原型(archetype) 来组织不同的component组合。相同原型的entity在物理内存上都在一起,叫做一个 内存块 。同一个内存块中都是相同的组件原型。
上面这个图说明了ECS是如何根据原型来存储数据的。
下面说说 例外情况 :
  • Shared components和chunk components是例外情况,ECS不在chunk中存储他们。
  • 你可以在chunk之外存储dynamic buffer
虽然ECS不在内存块中存储上面类型的组件,但是进行实体查询的时候,你还是可以使用同样的方式对待他们。

通用目的的组件

Unity的ECS中的 ComponentData 是一个结构体,仅仅包含entity的实例数据。 ComponentData 不能包含方法,程序所有的逻辑需要写在System中。 ComponentData 有点类似于面向对象的Unity中,一个Component仅仅包含成员变量,但是没有成员方法、属性等等。
ECS的API提供了一个叫 IComponentData 的接口,你可以使用结构体实现这个接口来声明一个通用目的的组件类型。

IComponentData

传统的Unity组件是面向对象的MonoBehaviour类,同时会包含数据和方法。 IComponentData 是一个纯ECS风格的组件,只有数据,没有方法。你应该使用 结构体 来实现 IComponentData 接口,结构体在传递的时候会使用值传递,需要注意值传递在修改数据的时候有些不同,需要按照类似下面的代码对数据进行修改:
    
var transform = group . transform [ index ] ; // 读取 transform . heading = playerInput . move ; // 修改 transform . position += deltaTime * playerInput . move * settings . playerMoveSpeed ; group . transform [ index ] = transform ; // 写入
IComponentData 结构体中不能包含引用到托管对象的引用类型。因为 ComponentData 存在没有GC的内存块中,对性能有很大提升。

【选学】托管IComponentData

使用托管 IComponentData (即, IComponentData 使用 class 而不是 struct 进行声明)有助于将现有代码零散地移植到ECS,与托管数据进行交互或者为数据布局进行原型设计, ISharedComponentData 不能和托管数据交互。
这些组件的使用方式与值类型 IComponentData 相同。但是,ECS在内部以完全不同(而且比较慢)的方式来处理它们。如果不需要托管组件支持,可以在Player Settings里面(Edit > Project Settings > Player > Scripting Define Symbols)设置 UNITY_DISABLE_MANAGED_COMPONENTS 宏来禁用它。
由于托管 IComponentData 是托管类型,因此与值类型 IComponentData 相比,它具有以下性能缺点:
  • 不能与Burst编译器一起使用
  • 不能在Job结构体中使用
  • 它不能使用块内存
  • 需要垃圾回收
你应该尽可能不用托管组件,并尽可能使用blittable类型。
托管 IComponentData 必须实现 IEquatable<T> 接口并覆盖 Object.GetHashCode() 方法。此外,出于序列化的目的,托管组件需要有默认无参构造方法。
你必须在主线程上设置组件的值,可以使用 EntityManager 或 EntityCommandBuffer 。由于组件是引用类型,因此与ISharedComponentData不同,你可以更改组件的值而无需在块之间移动实体。(之前咱们知道修改ISharedComponentData的值会导致entity所在的内存块发生变化)
但是,尽管在逻辑上将托管组件与值类型的组件分开存储,但它们都会有 EntityArchetype 原型定义。这样,向实体添加新的托管组件仍然会导致ECS创建新的原型(如果尚不存在匹配的原型),并将实体移至新的Chunk。

总结

既然用了ECS,那就从一开始忘掉MonoBehaviour的组件的写法,拥抱ECS吧!

扩展阅读

【扩展学习】 在 洪流学堂 公众号回复 DOTS 可以阅读本系列所有文章,更有视频教程等着你!
呼~ 今天小新絮絮叨叨的真是够够的了。没讲清楚的地方欢迎评论,咱们一起探索。
我是大智(vx:zhz11235),你的技术探路者,下次见!
别走! 点赞 , 收藏 哦!
好,你可以走了。
<think>我们正在设计ECS架构中的移动组件。根据ECS架构原则,我们需要考虑三个核心部分:实体(Entity)、组件Component)和系统(System)。移动组件Component)用于存储与移动相关的数据,而移动系统(System)则负责处理移动逻辑。 设计步骤: 1. **组件设计**:移动组件应该包含哪些数据? - 通常,移动组件需要包含位置、速度、加速度、方向等数据。 - 例如,我们可以设计一个`TransformComponent`(包含位置、旋转、缩放)和一个`MovementComponent`(包含速度、加速度、最大速度等)。 2. **系统设计**:移动系统如何工作? - 移动系统会遍历所有拥有移动组件和变换组件的实体,根据速度和时间更新位置。 3. **考虑性能**:根据引用[3],ECS架构中,将同类型组件放在同一个数组中,可以更好地利用CPU缓存。因此,我们应该将移动组件的数据连续存储。 4. **与其他组件的交互**:例如,物理组件、动画组件等。移动系统可能需要在更新位置后触发其他系统的更新。 具体设计: **组件定义**: - `TransformComponent`:存储实体的位置、旋转和缩放。这是一个基础组件,很多实体都会拥有。 - 位置(position):$ (x, y, z) $ 或 $ (x, y) $(2D) - 旋转(rotation):用四元数、欧拉角或角度表示 - 缩放(scale):$ (s_x, s_y, s_z) $ 或 $ (s_x, s_y) $ - `MovementComponent`:存储移动相关的动态数据。 - 速度(velocity):$ (v_x, v_y, v_z) $ 或 $ (v_x, v_y) $ - 加速度(acceleration):$ (a_x, a_y, a_z) $ 或 $ (a_x, a_y) $ - 最大速度(max_speed):标量值 - 是否在地面(is_grounded):布尔值(可选,用于跳跃或重力) **系统定义**: - `MovementSystem`:在每一帧更新中,遍历所有同时拥有`TransformComponent`和`MovementComponent`的实体。 1. 根据加速度更新速度(考虑时间步长$ \Delta t $): $$ \vec{v}_{new} = \vec{v}_{old} + \vec{a} \cdot \Delta t $$ 然后,限制速度不超过最大速度:$ |\vec{v}_{new}| \leq \text{max\_speed} $ 2. 根据速度更新位置: $$ \vec{p}_{new} = \vec{p}_{old} + \vec{v}_{new} \cdot \Delta t $$ 3. 将新的位置设置到`TransformComponent`中。 注意:如果游戏有物理,我们可能还需要一个独立的物理系统来处理碰撞等,移动系统可能只负责处理输入驱动的移动,而物理系统负责重力和碰撞响应。在这种情况下,移动系统可能会在物理系统之前运行。 **优化**: - 连续存储:确保`MovementComponent`和`TransformComponent`在内存中连续存储(使用结构数组),以提高缓存命中率[^3]。 - 避免在系统内部进行复杂的分支:尽量使逻辑线性。 **与其他系统的交互**: - 输入系统:将玩家的输入转换为移动组件的加速度或速度。 - 物理系统:处理碰撞,可能会修改速度或位置。 - 动画系统:根据速度播放移动动画。 示例伪代码(C++风格,但使用中文注释): ```cpp // 定义组件 struct TransformComponent { Vec3 position; Quaternion rotation; Vec3 scale; }; struct MovementComponent { Vec3 velocity; Vec3 acceleration; float max_speed; bool is_grounded; }; // 移动系统 class MovementSystem : public System { public: void Update(float delta_time) override { // 遍历所有拥有TransformComponent和MovementComponent的实体 for (auto entity : GetEntities<TransformComponent, MovementComponent>()) { auto [transform, movement] = entity.GetComponents<TransformComponent, MovementComponent>(); // 更新速度 movement.velocity += movement.acceleration * delta_time; // 限制速度 if (movement.velocity.Length() > movement.max_speed) { movement.velocity = movement.velocity.Normalized() * movement.max_speed; } // 更新位置 transform.position += movement.velocity * delta_time; } } }; ``` **注意事项**: - 根据引用[4],ECS架构在处理单个实体需要连续处理业务时可能不如OOP方便,但通过合理设计系统,我们可以将连续操作分解到不同的系统中,按顺序执行。 - 移动组件应该只包含数据,不包含任何逻辑。逻辑在系统中。 **扩展**: - 如果需要网络同步,可以设计一个网络组件,移动系统在更新位置后,将位置信息标记为需要同步,由网络系统发送。 这样设计,移动组件和系统就符合ECS架构的数据驱动原则,并且能够高效运行。 §§相关问题§§ 1. ECS架构中如何处理实体间的依赖关系,例如两个实体的距离计算? 2. 在移动系统中如何整合物理碰撞检测? 3. ECS架构中如何实现动画系统与移动系统的协同工作? 4. 如何设计一个网络同步的移动系统?</think>### ECS架构中移动组件设计方法 在ECS架构中设计移动组件需遵循**数据驱动**原则,将移动相关的数据与逻辑分离。以下是具体设计方法: #### 1. **组件设计(数据层)** 移动组件(`MovementComponent`)**仅存储数据**,不包含任何逻辑: ```cpp struct MovementComponent { float velocity_x; // X轴速度 $v_x$ float velocity_y; // Y轴速度 $v_y$ float acceleration; // 加速度 $a$ float max_speed; // 最大速度 $v_{\text{max}}$ bool is_grounded; // 地面检测标志 }; ``` - **设计要点**: - 组件是纯数据结构,无方法 - 同类型组件连续存储(提升CPU缓存命中率[^3]) - 通过修改组件数据动态调整行为(如调整`max_speed`改变移动速度) #### 2. **系统设计(逻辑层)** 移动系统(`MovementSystem`)处理所有移动逻辑: ```cpp class MovementSystem : public System { public: void Update(float delta_time) { // 遍历所有拥有TransformComponent和MovementComponent的实体 for (auto& entity : GetEntities<TransformComponent, MovementComponent>()) { auto& transform = entity.GetComponent<TransformComponent>(); auto& movement = entity.GetComponent<MovementComponent>(); // 物理计算:$v = v_0 + a \cdot t$ movement.velocity_x += movement.acceleration * delta_time; // 速度限制:$|v| \leq v_{\text{max}}$ if (std::abs(movement.velocity_x) > movement.max_speed) { movement.velocity_x = std::copysign(movement.max_speed, movement.velocity_x); } // 更新位置:$x = x_0 + v \cdot t$ transform.position_x += movement.velocity_x * delta_time; } } }; ``` - **设计要点**: - 系统按固定频率执行(如每帧) - 仅处理拥有相关组件的实体 - 通过delta_time实现帧率无关的移动[^2] #### 3. **关键设计策略** 1. **组件组合**: - 移动组件需与`TransformComponent`(存储位置数据)组合使用 - 例:物理系统可通过添加`PhysicsComponent`实现碰撞检测 2. **数据流向优化**: ```mermaid graph LR 输入系统-->|写入|MovementComponent MovementSystem-->|读取/写入|MovementComponent MovementSystem-->|写入|TransformComponent 渲染系统-->|读取|TransformComponent ``` 3. **跨系统交互**: - 动画系统:根据`velocity_x`值切换行走/奔跑动画 - 网络同步:标记`MovementComponent`的脏数据标志位 #### 4. **性能优化** - **内存布局**:使用SoA(Structure of Arrays)存储组件 ```cpp // 优化后内存结构(缓存友好) struct MovementComponents { std::vector<float> velocity_x; std::vector<float> velocity_y; std::vector<float> max_speed; }; ``` - **并行处理**:对组件数组分块并行计算[^1] #### 5. **设计陷阱规避** - **避免组件耦合**:不在`MovementComponent`中存储位置数据 - **逻辑拆分**: - 移动系统仅处理基础运动 - 复杂行为(如跳跃)通过`JumpSystem`+`JumpComponent`实现 - **单例组件**:使用`GravityComponent`(全局重力参数)[^4] > **典型应用流程**: > 玩家输入 → 输入系统修改`MovementComponent.velocity_x` → 移动系统更新位置 → 物理系统修正碰撞位置 → 渲染系统绘制 此设计实现了: 1. **动态调整**:通过修改组件数据实时改变移动特性 2. **高效执行**:连续内存访问+并行处理优化 3. **解耦扩展**:新增移动类型(如飞行)只需添加新组件+系统
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值