ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

EntityComponentSystemSamples 之 Tornado 示例深度解析:用 ECS、Burst 与数组实现 50Hz 的杆件撕裂模拟

EntityComponentSystemSamples 之 Tornado 示例深度解析:用 ECS、Burst 与数组实现 50Hz 的杆件撕裂模拟 EntityComponentSystemSamples 之 Tornado 示例深度解析用 ECS、Burst 与数组实现 50Hz 的杆件撕裂模拟【免费下载链接】EntityComponentSystemSamples项目地址: https://gitcode.com/GitHub_Trending/en/EntityComponentSystemSamplesTornado 示例位于 Dots101/Entities101/Assets/Tornado 目录是 Unity ECSEntities 1.x入门套件中一个极具教学价值的软体模拟一座由端点points与杆件bars拼成的建筑群被一个按8字轨迹移动、由上千个旋转方块构成的龙卷风逐根拆解。本文以该目录下的 README.md 为核心骨架结合仓库内全部源码逐层剖析其系统划分、数据结构、Burst 约束求解、渲染管线以及实体 vs 数组这一核心设计权衡读完你将掌握如何用 ECS 编排一次大规模的、自包含的物理模拟并理解 DOTS 数据导向设计Data-Oriented Design在实际样例中的取舍。模拟概述一辆只有杆件端点的风暴破坏机README 用三句话概括了整个示例的运行效果与玩法边界龙卷风沿figure-88 字形轨迹移动并撕裂由端点处相互连接的杆件构成的建筑龙卷风本身表现为一群旋转翻滚的立方体粒子杆件与立方体只与地面碰撞彼此之间不碰撞不使用物理引擎见下文。从源码看这一效果由七个 ECS 系统 一个配置 Authoring 组件协作完成。场景最初只有地面、一盏灯、相机和一个配置 GameObject见 BuildingSpawnSystem.cs 与 TornadoSpawnSystem.cs所有杆件与立方体都在运行时由 Spawn 系统实例化——这正是运行时内容生成runtime instantiation的典型演示。系统架构七个系统各司其职README 的 Code 一节列出了全部系统。下表把职责、更新时机与对应源码一一对应起来系统职责更新所在组源码文件BuildingSpawnSystem生成构成建筑的点与杆件初始化PointArraysInitializationSystemGroup在SceneSystemGroup之后BuildingSpawnSystem.csBuildingSystem模拟点与杆件端点积分 距离约束 断裂FixedStepSimulationSystemGroupBuildingSystem.csBuildingRenderSystem根据两个端点更新杆件的渲染变换矩阵默认组BuildingRenderSystem.csCameraSystem移动相机跟随龙卷风默认组CameraSystem.csConfigAuthoring承载全部模拟参数 杆件/粒子预制体引用烘焙BakerConfigAuthoring.csTornadoSpawnSystem生成构成龙卷风的立方体粒子InitializationSystemGroupTornadoSpawnSystem.csTornadoSystem模拟立方体粒子的螺旋运动FixedStepSimulationSystemGroupTornadoSystem.cs需要注意两个 Spawn 系统都在InitializationSystemGroup且标注了[UpdateAfter(typeof(SceneSystemGroup))]并且在生成完成后立刻state.Enabled false自我禁用如 TornadoSpawnSystem.cs保证只运行一帧。关键数据结构从 Authoring 组件到纯数据组件Config一切参数的入口ConfigAuthoring.cs 是典型的 Authoring → Baker → ECS 组件链路。它既是挂在场景 GameObject 上的 MonoBehaviour又在内部定义了Config : IComponentDatapublic struct Config : IComponentData { public Entity BarPrefab; public float BarDamping; // [0,1] 杆件阻尼 public float BarFriction; // [0,1] 地面摩擦 public float BarBreakResistance;// 断裂阈值 public float TornadoForce; // 龙卷风切向力 public float TornadoMaxForceDist;// 力场最大作用半径 public float TornadoHeight; // 力场高度 public float TornadoUpForce; // 上升力 public float TornadoInwardForce;// 向心力 public Entity ParticlePrefab; public float ParticleSpinRate; // 粒子旋转速率 public float ParticleUpwardSpeed;// 粒子上升速度 }其中BarDamping、BarFriction、TornadoForce在 Inspector 中带[Range(0,1)]约束。Baker 通过GetEntity(TransformUsageFlags.Dynamic)把两个预制体引用转成Entity引用其余 float 参数原样拷贝——这是 Entities 1.x 中最标准的配置烘焙写法。杆件与粒子最小化 ECS 组件BarAuthoring.csBar组件只有int pointA; int pointB; float length;两端点数组下标 原长另有一个BarThickness组件存杆件粗细。ParticleAuthoring.csParticle组件只有一个float radiusMult粒子半径倍率。注意杆件数据里只有两个 int指向数组下标而不是 16 字节的实体引用——这正是 README Entities vs arrays 一节的论证结果详见后文。点数据NativeContainer 大数组模拟的核心点数据全部放在一个PointArrays : IComponentData单例组件中见 BuildingSpawnSystem.cspublic struct PointArrays : IComponentData { public NativeArrayfloat3 current; // 当前位置 public NativeArrayfloat3 previous; // 上一帧位置用于 Verlet 积分 public NativeArraybyte connectivity; // 每个点连接的杆数byte.MaxValue 表示锚点 public NativeReferenceint count; // 当前有效点数随断裂增长 }其中connectivity是 README 特别强调的优化点一个点的连接杆数量与是否锚定被合并进一个 byte——锚点直接取byte.MaxValue255普通点存实际连接数。这样判断某点是否还连着其他杆只需一次字节比较代价极低而BarUpdateJob里也正是用connectivity[i] byte.MaxValue来判断锚点见 BuildingSystem.cs。核心物理剖析Verlet 积分 距离约束 断裂BuildingSystem是整份代码的精华README 的 Notes 逐条对应源码中的两个 Burst Job端点并行积分PointUpdateJobIJobParallelForREADME 指出每个点独立更新因此可以放进一个IJobParallelFor并行执行。源码验证了这一点PointUpdateJob 以pointData.count.Value为长度、batch 64 调度。它做的三件事分别是锚点短路if (connectivity[i] byte.MaxValue) return;——锚点地基不被更新龙卷风力计算点到龙卷风轴的平面距离在TornadoMaxForceDist内按1 - dist / TornadoMaxForceDist衰减施力力的大小还乘上tornadoFader math.saturate(time / 10)前 10 秒从 0 渐变到满力避免开局瞬间把建筑掀飞以及一个带随机扰动的系数Random.CreateFromIndex(randomSeed ^ (uint)i).NextFloat(-.3f, 1.3f)让撕裂效果更自然。力的方向 切向-tdz/tdx 向心分量TornadoInwardForce * yFader 上升力TornadoUpForce其中yFader saturate(1 - y / TornadoHeight)让力随高度衰减Verlet 积分 地面碰撞point (point - previous) * (1 - BarDamping)是标准的 Verlet 更新BarDamping直接以(1 - damping)缩放速度当y 0时把点压回地面并按BarFriction对 x/z 分量施加地面摩擦。距离约束与断点分裂BarUpdateJobIJobChunkBuildingSystem的OnUpdate先调度点积分 Job随后通过GetAllUniqueSharedComponentsBarCluster枚举建筑簇共享组件过滤对每个簇单独调度一个 BarUpdateJob最后JobHandle.CombineDependencies合并依赖。BarCluster : ISharedComponentData存的是该簇的并查集Union-Find根下标——这样每栋建筑被隔离成一个独立的约束求解域。BarUpdateJob 的核心逻辑与 README 的 Notes 一一对应距离约束计算extraDist dist - bar.length若超长/压缩则把修正量push按 1/2 分摊到两端锚点端则全量修正另一端见 BuildingSystem.cs断裂判定if (math.abs(extraDist) config.BarBreakResistance)即拉伸或压缩超过阈值就断杆共享点分裂断裂时若端点还被多根杆共享connectivity[i] 1且非锚点调用DuplicatePointcounter.AtomicAdd(1)原子申请一个新下标把原点的 current/previous 拷贝过去并将两端connectivity分别减一/置一见 BuildingSystem.cs。这正是 README 所说连接断裂时共享点被拆成两个独立点的代码级实现也是杆数不变、端点数增加的原因。龙卷风粒子TornadoSystemTornadoSystem.cs 用IJobEntity遍历所有Particle实体每个粒子被拉向龙卷风轴心带BuildingSystem.TornadoSway的波浪偏移叠加切向旋转ParticleSpinRate与上升ParticleUpwardSpeed高度超过 50 就回落到 0形成持续翻滚的旋风柱。粒子位置、颜色、缩放由TornadoSpawnSystem用固定种子Random.CreateFromIndex(1234)随机初始化TornadoSpawnSystem.cs一次实例化 1000 个预制体。龙卷风轨迹与相机龙卷风中心位置由BuildingSystem.Position决定BuildingSystem.csreturn new float2(math.cos(time / 6f), math.sin(time / 6f * 1.618f)) * 30f;即 x 方向按 1/6 频率、z 方向按 1.618黄金比例倍频的利萨茹曲线合成为 figure-8 轨迹。CameraSystem每帧用同一函数求当前位置把相机放在其后方 60 米、高 10 米处跟随CameraSystem.cs。注意 Position 由绝对时间驱动而非累计状态因此相机、力场、粒子三者天然同步。渲染从两个端点直接算变换矩阵README 说每根杆的渲染变换矩阵由它的两个端点计算。这在 BuildingRenderSystem.cs 的PointRenderJobIJobEntity中实现取CurrentPoints[bar.pointA]与CurrentPoints[bar.pointB]算出杆的中心点、朝向quaternion.LookRotationSafe与长度缩放x/z 用BarThicknessy 用两点距离直接写回LocalToWorldvar t (a b) / 2; var r quaternion.LookRotationSafe(norm, norm.yzx); var s new float3(new float2(thickness.Value), d); ltw.Value float4x4.TRS(t, r, s);因为CurrentPoints是只读的NativeArrayfloat3这个 Job 可完全并行且渲染与模拟解耦模拟改点数组渲染只做点 → 矩阵的映射。设计权衡实体还是数组Entities vs arraysREADME 用独立小节论证了本示例的核心设计决策这也是 Entities 系列教材中反复出现的经典论题实体的优点可独立创建/销毁、组件可增删、实体引用EntityReference在其生命周期内始终有效共享组件子集的实体可被一次查询统一处理代价一个实体引用是两个 intindex version按引用查找比直接跟指针更贵数组的优点按下标 O(1) 直查引用只需一个 int占用与访问都更省代价数组元素无法在不移动其他元素的情况下独立增删增删会改变下标。本示例的取舍是渲染用实体点数据用数组。理由有三点的结构从不变更、从不重排、从不删除——只会在断杆时新增DuplicatePoint只在数组尾部追加count只增不减实体的灵活性在此没有用武之地模拟每帧要对点做海量查找每个杆查 2 次、每个点积分 1 次、渲染每杆再查 2 次数组下标比实体引用便宜得多数据体积若把点存成实体每根杆要存两个实体引用 2 × 2 int 16 字节存成数组则只需 2 个 int 8 字节。README 明确说更小的数据总是好事因为它意味着更少的内存访问——这正是 DOTS 缓存局部性cache locality思想的直白体现。性能笔记固定步长、单字节连通性与理论规模极限固定 50Hz 步长BuildingSystem与TornadoSystem都挂在FixedStepSimulationSystemGroup下README 与 BuildingSystem.cs、TornadoSystem.cs 一致。固定步长的默认值是 50 次/秒这解释了为何在高帧率下 Profiler 中会看到周期性的帧时间振荡——每 20ms 一次的固定步进在 60/120Hz 的渲染帧上会形成锯齿。这是固定步长模拟的固有现象不是性能问题。单字节连通性的依据README 说明实验表明建筑生成代码从不产生超过约 50 根杆的建筑因此单字节最大 255存连接数是安全的。源码中connectivity用byte且锚点占用byte.MaxValue普通点的连接数从 0 递增理论上限被锁死在 255——这是一个用实验数据指导数据结构设计的绝佳案例。理论绝对规模上限README 给出了一个可复算的推算断裂后一根独立的杆 200 字节杆本身 2 × 25 字节两个点250 字节。假设现代游戏 PC 的峰值内存带宽为 60 GB/s60Hz 下理论上限约为40 亿根杆60 GB/s ÷ 60 Hz ÷ 250 B ≈ 4 × 10⁹。README 也坦承实际能跑到这个量级的十分之一就算走运——理论极限用来设定预期而不是作为性能承诺。如何运行与观察用 Unity Hub 打开 Entities101 工程Packages/manifest.json已声明com.unity.entities、com.unity.burst等依赖在 Project 窗口打开 Assets/Tornado/Tornado.unity 场景点击 Play。可观察的现象开局 10 秒内龙卷风力从 0 渐强tornadoFader建筑被逐根拆下来打开 Profiler 可看到固定步进导致的帧时间振荡暂停后可在 Hierarchy 中查看PointArrays单例上的count随断杆不断增长想调整破坏烈度时选中场景中的 Config GameObject在 Inspector 中修改 ConfigAuthoring.cs 暴露的TornadoForce、BarBreakResistance等参数后重进 Play 即可。小结从 Tornado 学到的三条经验系统编排范式Spawn一次性InitializationSystemGroup→ 模拟固定步长FixedStepSimulationSystemGroup→ 渲染/相机默认组的分层是 ECS 示例中的黄金模板数据导向选型凡结构固定、高频访问、按索引引用的数据用 NativeArray 优于实体凡需要独立增删/查组件的数据才值得用实体——16 字节 vs 8 字节的杆引用对比就是最直观的论据约束求解的工程取舍README 明确承认约束求解的稳定性高度依赖求解顺序完全并行的保序方案或许可行但为简单起见放弃了全并行——它用每杆独立解距离约束 分簇BarCluster 共享组件隔离的方式在正确性与并行度之间取了折中。这正是真实项目里先跑通、再优化的务实态度也是本示例最值得反复咀嚼的地方。想继续深入推荐继续阅读同仓库的 Dots101/Entities101/Assets/Tornado/BuildingSystem.cs约束求解核心、BuildingSpawnSystem.cs并查集建簇与初始化以及 TornadoSystem.cs粒子模拟。【免费下载链接】EntityComponentSystemSamples项目地址: https://gitcode.com/GitHub_Trending/en/EntityComponentSystemSamples创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表