Unity ECS 实现 5 万条鱼群游:DOTS 下的 Boids 群体行为算法实战
【免费下载链接】EntityComponentSystemSamples项目地址: https://gitcode.com/GitHub_Trending/en/EntityComponentSystemSamples
Boids 是 1986 年提出的经典群体行为算法,能让大量独立个体仅凭三条简单规则就涌现出鱼群、鸟群般的自然运动。这个 Unity DOTS(Data-Oriented Technology Stack,数据导向技术栈)示例项目用它驱动了 5 万条"鱼"的实体模拟:默认场景直接实例化两群、各 25000 条鱼,核心逻辑却只有三层 Job。本文拆解它如何用 ECS(Entity-Component-System,实体组件系统)撑起这场大规模模拟。
项目速览
Boids 是 Unity Entities 生态的官方示例之一,位于EntitiesSamples/Assets/Boids/。它模拟的是一幕海底场景:鱼群自动聚合成群,追逐 2 条红色"目标鱼",同时躲避 1 条鲨鱼。技术栈是 ECS + Job System + Burst 编译器:实体只存数据,行为由系统(System)驱动,计算并行到多个线程。
四个核心文件:
- BoidSystem.cs:主模拟系统,包含三层 Job 流水线
- BoidAuthoring.cs:定义
Boid共享组件及 6 个行为参数 - BoidSchoolSpawnSystem.cs:开局从预制体批量生成鱼
- BoidSchoolAuthoring.cs:控制鱼群数量与生成半径
为什么这样设计
1. 参数用"共享组件"而不是普通组件。在BoidAuthoring.cs中,Boid实现了ISharedComponentData接口(值完全相同时被运行时归入同一批次的组件类型)。这样设计有两个收益:同参数鱼的数据在内存中紧密排布,CPU 缓存命中率高;同"变体"的鱼只与同类互相作用,主系统可以逐变体独立跑一遍模拟,批次之间没有依赖。BoidSystem.cs 里对uniqueBoidTypes的循环正是为这个服务的。
2. 空间哈希代替暴力邻居搜索。标准 Boids 要求每个个体和所有其他个体两两比较,复杂度 O(n²)。5 万条鱼意味着每帧约 25 亿次成对比较,单帧根本算不完。该项目把 3D 空间按CellRadius量化成格子,只比较同格内的邻居,把复杂度降到接近 O(n)。
3. 三层 Job 流水线代替单循环。邻居统计需要多个实体共享数据,单个并行循环里不能"边读边写"。所以流程被拆成:先把每条鱼的位置、朝向写入数组;再并行合并每个格子的统计量;最后并行计算新朝向。Job 之间的依赖用句柄显式声明,竞态条件由编译器替你检查。
核心机制是怎么跑起来的
BoidSystem每帧调度三个 Job,下面分三步走读。
第一步:空间哈希。InitialPerBoidJob遍历所有鱼,把位置和朝向写入 NativeArray(预分配好、无 GC 压力的托管外数组),同时把位置量化进哈希桶。这一步让后两个 Job 能用常数时间拿到"谁是邻居":
var hash = (int)math.hash( new int3(math.floor(localToWorld.Position * InverseBoidCellRadius))); ParallelHashMap.Add(hash, entityIndexInQuery);InverseBoidCellRadius是1.0f / CellRadius的预算倒数,避免热循环里反复除法。
第二步:格子合并。MergeCellsJob 并行遍历哈希桶,为每个格子累加成员数、朝向之和(对齐量)、位置之和(分离量),同时顺手找出该格最近的目标鱼和鲨鱼。跑完后,每条鱼需要的"邻居统计"都躺在自己的格子里:
cellCount[cellIndex] += 1; cellAlignment[cellIndex] += cellAlignment[cellIndex]; cellSeparation[cellIndex] += cellSeparation[cellIndex];第三步:三大规则与避障。SteerBoidJob读取格子统计量,计算每条鱼的新朝向。核心是 Boids 三条经典规则的加权和——对齐(跟上邻居的平均朝向)、分离(离人群中心远一点)、趋目标(红色鱼的方向):
var alignmentResult = CurrentBoidVariant.AlignmentWeight * math.normalizesafe((alignment / neighborCount) - forward); var separationResult = CurrentBoidVariant.SeparationWeight * math.normalizesafe((currentPosition * neighborCount) - separation); var targetHeading = CurrentBoidVariant.TargetWeight * math.normalizesafe(nearestTargetPosition - currentPosition);如果鲨鱼进入ObstacleAversionDistance距离内,最后一行会直接覆盖正常航向,改成逃离方向——这就是"鱼群瞬间散开"效果的来源:
var targetForward = math.select(normalHeading, avoidObstacleHeading, nearestObstacleDistanceFromRadius < 0);性能体现在哪
- 规模:默认子场景
Simulation.unity里两个鱼群实体的Count均为 25000,共 5 万条鱼实体,另有 2 个目标、1 个障碍物。 - 复杂度:空间哈希把每帧邻居搜索从约 2.5×10⁹ 次成对比较压缩为各格子内的局部累加,接近 O(n)。
- 并行度:三个 Job 全部标注
[BurstCompile],由 Burst 编译器做向量化和指令优化,Job System 再把实体切分到多个 CPU 线程;系统内dt被钳制在 0.05 秒以内,帧时间波动时模拟仍然稳定。 - 具体加速比与硬件相关,建议用 Unity Profiler 自行测量对比。
三步跑起来
- 克隆仓库:
git clone https://gitcode.com/GitHub_Trending/en/EntityComponentSystemSamples - 用 Unity Hub 打开
EntitiesSamples项目,加载Assets/Boids/Boids.unity场景 - 按 Play,5 万条鱼由
BoidSchoolSpawnSystem并行实例化,立即开始群体行为
想调整规模,改子场景里BoidSchool的Count即可,低配机器设 10000 条也足够观察效果。
参数调优速查表
参数位于Boid共享组件,默认值取自 BoidAuthoring.cs:
| 参数 | 仓库默认值 | 作用 | 调整建议 |
|---|---|---|---|
| CellRadius | 8.0 | 空间哈希格子边长,即"邻居"判定范围 | 偏小易碎群,偏大趋同成密集团 |
| AlignmentWeight | 1.0 | 对齐权重:跟随邻居平均朝向 | 调高后队列更整齐,转向更像整体 |
| SeparationWeight | 1.0 | 分离权重:保持个体间距 | 调高更松散,调低易扎堆 |
| TargetWeight | 2.0 | 追逐目标鱼的权重 | 三权重中默认最大,主导行为 |
| ObstacleAversionDistance | 30.0 | 触发躲避鲨鱼的"警戒距离" | 调高则鱼群更早散开 |
| MoveSpeed | 25.0 | 每秒移动距离(单位/秒) | 调高整体游速更快 |
生成侧参数在BoidSchool组件(BoidSchoolAuthoring.cs):Count控制条数(默认 25000),InitialRadius控制出生球半径。
还能拿它做什么
- 水下生态:最贴切的应用,鱼群、水母群、深海生物带
- 空中编队:换权重即可做 V 字编队的鸟群
- 交通仿真:分离权重当车道间距,目标当前方车流
- NPC 群体 AI:怪物群、僵尸潮的集群移动
- 疏散演练:把障碍物换成出口方向,模拟人群撤离
总结与延伸
三条核心收获:
- 数据布局决定性能:共享组件把同参数实体排成连续内存,是并行与缓存命中的前提
- 空间分区是 O(n²) 变 O(n) 的通用技巧,Boids 是最小的完整示范
- 把"读-改-写"拆成三层 Job,依赖显式化,并行才安全
可探索的扩展方向:
- 加行为状态机:按鲨鱼距离在觅食、迁徙、逃散三种模式间切换
- 引入个体差异:少量实体挂"领导者"组件,其余跟随,观察群体涌现
- 结合仓库自带的动画系统 SampledAnimationClipPlaybackSystem.cs,用速度驱动游泳动画,让高速鱼摆尾更快
【免费下载链接】EntityComponentSystemSamples项目地址: https://gitcode.com/GitHub_Trending/en/EntityComponentSystemSamples
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考