1️⃣ 系统基类全面改为 struct
旧版写法(class 系统)
public class MoveSystem : SystemBase
{
protected override void OnUpdate()
{
Entities.ForEach((ref Translation pos, in MoveSpeed speed) =>
{
pos.Value += math.forward() * speed.Value * Time.DeltaTime;
}).ScheduleParallel();
}
}
-
系统基于 class,需要 GC Heap 分配。
-
生命周期类似 MonoBehaviour,有一定调度开销。
新版写法(struct 系统)
public partial struct MoveSystem : ISystem
{
public void OnUpdate(ref SystemState state)
{
float dt = SystemAPI.Time.DeltaTime;
foreach (var (transform, speed) in SystemAPI.Query<RefRW<LocalTransform>, RefRO<MoveSpeed>>())
{
transform.ValueRW.Position += transform.ValueRW.Forward() * speed.ValueRO.Value * dt;
}
}
}
-
系统实现
ISystem
,为 struct,无GC分配。 -
调度零开销,性能更优,完全数据驱动。
2️⃣ GameObject 转换改为 Baking Workflow
旧版使用 ConvertToEntity
或 IConvertGameObjectToEntity
,运行时转换Prefab为Entity,加载慢且调试麻烦。
新版使用 Baker,所有转换在编辑器完成:
public class MoveAuthoring : MonoBehaviour
{
public float Speed;
class Baker : Baker<MoveAuthoring>
{
public override void Bake(MoveAuthoring authoring)
{
AddComponent(new MoveSpeed { Value = authoring.Speed });
}
}
}
-
无运行时开销。
-
转换过程清晰、可调试。
-
替代旧的
ConvertToEntity
和 Conversion Workflow。
这里需要介绍一下新版本中非常重要的SubScene
SubScene是Dots在1.0版本后提供的,批量将GameObject对象转为Entity的工具,在Hierarchy右键鼠标,找到NewSubScene 新建,名字随意。
SubScene的作用是在Runtime时,将SubScene中的GameObject转成Entity。
设计SubScene的原因是当项目很大的时候,可以用不同的SubScene将不同的工作分类开,不执行的系统整个SubScene卸载掉。
在美术设计环节,比如做一个很大的场景,当场景中有数百万个对象,可以存放在SubScene中,但不载入它,这样不占用内存,只在RunTime的时候载入,这样场景设计时不用加载太多的模型
3️⃣ Entities.ForEach
改为 SystemAPI.Query
旧版:
Entities.ForEach((ref Translation pos, in MoveSpeed speed) => { ... });
新版:
foreach (var (transform, speed) in SystemAPI.Query<RefRW<LocalTransform>, RefRO<MoveSpeed>>())
{
transform.ValueRW.Position += transform.ValueRW.Forward() * speed.ValueRO.Value * dt;
}
-
查询更接近原生
foreach
,编译速度更快。 -
RefRO
/RefRW
明确标注读写权限,Burst优化更好。
4️⃣ Transform 组件合并为 LocalTransform
旧版:
public struct Translation : IComponentData { public float3 Value; }
public struct Rotation : IComponentData { public quaternion Value; }
新版:
public struct LocalTransform : IComponentData
{
public float3 Position;
public quaternion Rotation;
public float Scale;
}
-
组件数量减少。
-
数据连续存储,缓存友好。
-
代码更简洁。
5️⃣ IJobChunk 改为 IJobEntity
旧版需要手动处理Chunk:
public struct MoveJob : IJobChunk
{
public ComponentTypeHandle<Translation> translationType;
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) { ... }
}
新版可直接访问组件:
[BurstCompile]
partial struct MoveJob : IJobEntity
{
public float DeltaTime;
void Execute(ref LocalTransform transform, in MoveSpeed speed)
{
transform.Position += transform.Forward() * speed.Value * DeltaTime;
}
}
-
无需手动管理组件句柄。
-
更易读、模块化,支持 ScheduleParallel。
本来想用新版Dots做一个鱼群效果,但是发现新版本的DOTS好像并不是很完善,蒙皮动画并不兼容,还要再使用别的过渡插件,预感后面还会有很大的更新,文档也很少,只能去啃官方的文档,现在学习成本太高,感觉可以等一个稳定版本再用。
最后附上一个简单的鱼群脚本
这个是代码结构图
Component脚本
using Unity.Entities;
using Unity.Mathematics;
public struct FishData : IComponentData
{
public float3 velocity;
public float speed;
}
public struct FishTag : IComponentData{}
public struct FishSpawnerData : IComponentData
{
public Entity Prefab;
public int Count;
public float3 AreaCenter;
public float3 AreaSize;
public float SpeedMin;
public float SpeedMax;
}
这个脚本挂在一个空对象上,然后放到SubScene中 ,fishPrefab把鱼的预制件放上去
using UnityEngine;
using Unity.Entities;
public class FishSpawnerAuthoring : MonoBehaviour
{
[Header("预制体 & 数量")] public GameObject fishPrefab;
public int fishCount = 100;
public Vector3 areaCenter;
public Vector3 areaSize;
public Vector2 speedRange = new Vector2(0.1f, 0.2f);
class Baker : Baker<FishSpawnerAuthoring>
{
public override void Bake(FishSpawnerAuthoring authoring)
{
var entity = GetEntity(TransformUsageFlags.Dynamic);
AddComponent(entity, new FishSpawnerData()
{
Prefab = GetEntity(authoring.fishPrefab, TransformUsageFlags.Dynamic),
Count = authoring.fishCount,
AreaCenter = authoring.areaCenter,
AreaSize = authoring.areaSize,
SpeedMin = authoring.speedRange.x,
SpeedMax = authoring.speedRange.y
});
}
}
}
鱼类的脚本,挂在鱼对象上,然后把鱼拖成预制件
using Unity.Entities;
using UnityEngine;
public class GoldFishAuthoring : MonoBehaviour
{
public float speed;
class Baker : Baker<GoldFishAuthoring>
{
public override void Bake(GoldFishAuthoring authoring)
{
var entity = GetEntity(TransformUsageFlags.Dynamic);
AddComponent(entity, new FishData
{
speed = authoring.speed
});
}
}
}
然后是两个系统脚本
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;
public partial struct FishSpawnerSystem : ISystem
{
private bool _spawned;
public void OnCreate(ref SystemState state)
{
_spawned = false;
}
public void OnUpdate(ref SystemState state)
{
if(_spawned)return;
var ecb = new EntityCommandBuffer(Unity.Collections.Allocator.Temp);
foreach (var spawner in SystemAPI.Query<FishSpawnerData>())
{
for (int i = 0; i < spawner.Count; i++)
{
var fish = ecb.Instantiate(spawner.Prefab);
// 给每条鱼一个随机位置
float3 pos = spawner.AreaCenter + new float3(
UnityEngine.Random.Range(-spawner.AreaSize.x/2f, spawner.AreaSize.x/2f),
UnityEngine.Random.Range(-spawner.AreaSize.y/2f, spawner.AreaSize.y/2f),
UnityEngine.Random.Range(-spawner.AreaSize.z/2f, spawner.AreaSize.z/2f)
);
var dir = math.normalize(new float3(
UnityEngine.Random.Range(-1f,1f),
0,
UnityEngine.Random.Range(-1f,1f)
));
ecb.SetComponent(fish, LocalTransform.FromPositionRotationScale(pos, quaternion.LookRotationSafe(dir, math.up()), 1));
ecb.AddComponent(fish, new FishData
{
velocity = dir,
speed = UnityEngine.Random.Range(spawner.SpeedMin, spawner.SpeedMax)
});
ecb.AddComponent<FishTag>(fish);
}
}
ecb.Playback(state.EntityManager);
_spawned = true; // 防止重复生成
}
}
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
[BurstCompile]
[UpdateInGroup(typeof(SimulationSystemGroup))]
public partial struct FishMoveSystem : ISystem
{
private const float NeighborRadius = 2f;
private const float AvoidDistance = 1f;
private const float CellSize = 3f;
private const int MaxSampleCount = 8; // 每条鱼最多采样8个邻居
private NativeParallelMultiHashMap<int, int> grid;
public void OnCreate(ref SystemState state)
{
grid = new NativeParallelMultiHashMap<int, int>(1024, Allocator.Persistent);
}
public void OnDestroy(ref SystemState state)
{
if (grid.IsCreated) grid.Dispose();
}
public void OnUpdate(ref SystemState state)
{
var query = SystemAPI.QueryBuilder().WithAll<FishData, LocalTransform>().Build();
var entityArray = query.ToEntityArray(Allocator.Temp);
int count = entityArray.Length;
if (count == 0) return;
var positions = new NativeArray<float3>(count, Allocator.TempJob);
var velocities = new NativeArray<float3>(count, Allocator.TempJob);
var speeds = new NativeArray<float>(count, Allocator.TempJob);
var eManager = state.EntityManager;
for (int i = 0; i < count; i++)
{
var tr = eManager.GetComponentData<LocalTransform>(entityArray[i]);
var data = eManager.GetComponentData<FishData>(entityArray[i]);
positions[i] = tr.Position;
velocities[i] = data.velocity;
speeds[i] = data.speed;
}
// 清空并填充格子(持久化容器)
grid.Clear();
if (grid.Capacity < count)
{
grid.Capacity = count * 2;
}
for (int i = 0; i < count; i++)
{
int hash = Hash(positions[i]);
grid.Add(hash, i);
}
var newPositions = new NativeArray<float3>(count, Allocator.TempJob);
var newVelocities = new NativeArray<float3>(count, Allocator.TempJob);
var job = new FishJob
{
positions = positions,
velocities = velocities,
speeds = speeds,
newPositions = newPositions,
newVelocities = newVelocities,
grid = grid,
cellSize = CellSize,
deltaTime = SystemAPI.Time.DeltaTime
};
job.Schedule(count, 64).Complete();
// 写回数据
for (int i = 0; i < count; i++)
{
var tr = eManager.GetComponentData<LocalTransform>(entityArray[i]);
var data = eManager.GetComponentData<FishData>(entityArray[i]);
data.velocity = math.normalize(newVelocities[i]);
tr.Position = newPositions[i];
tr.Rotation = quaternion.LookRotationSafe(data.velocity, math.up());
eManager.SetComponentData(entityArray[i], data);
eManager.SetComponentData(entityArray[i], tr);
}
positions.Dispose();
velocities.Dispose();
speeds.Dispose();
newPositions.Dispose();
newVelocities.Dispose();
}
private static int Hash(float3 pos)
{
int x = (int)math.floor(pos.x / CellSize);
int z = (int)math.floor(pos.z / CellSize);
return x * 73856093 ^ z * 19349663;
}
[BurstCompile]
private struct FishJob : IJobParallelFor
{
[ReadOnly] public NativeArray<float3> positions;
[ReadOnly] public NativeArray<float3> velocities;
[ReadOnly] public NativeArray<float> speeds;
[ReadOnly] public NativeParallelMultiHashMap<int, int> grid;
[ReadOnly] public float cellSize;
[ReadOnly] public float deltaTime;
public NativeArray<float3> newPositions;
public NativeArray<float3> newVelocities;
private int Hash(float3 pos)
{
int x = (int)math.floor(pos.x / cellSize);
int z = (int)math.floor(pos.z / cellSize);
return x * 73856093 ^ z * 19349663;
}
public void Execute(int index)
{
var selfPos = positions[index];
var selfVel = velocities[index];
float speed = speeds[index];
float3 center = float3.zero;
float3 avoid = float3.zero;
float speedSum = 0f;
int count = 0;
int sampleCount = 0;
// 遍历相邻格子
for (int dx = -1; dx <= 1; dx++)
{
for (int dz = -1; dz <= 1; dz++)
{
int hash = ((int)math.floor((selfPos.x / cellSize) + dx) * 73856093) ^
((int)math.floor((selfPos.z / cellSize) + dz) * 19349663);
if (grid.TryGetFirstValue(hash, out var other, out var it))
{
do
{
if (other == index) continue;
float dist = math.distance(selfPos, positions[other]);
if (dist < NeighborRadius)
{
center += positions[other];
speedSum += speeds[other];
count++;
if (dist < AvoidDistance)
avoid += (selfPos - positions[other]);
// 限制采样数量
sampleCount++;
if (sampleCount >= MaxSampleCount)
break;
}
} while (sampleCount < MaxSampleCount && grid.TryGetNextValue(out other, ref it));
}
}
}
float3 direction = selfVel;
if (count > 0)
{
center /= count;
speed = math.clamp(speedSum / count, 0.1f, 1f);
direction = math.normalize((center + avoid) - selfPos);
}
float3 pos = selfPos + direction * speed * deltaTime;
newPositions[index] = pos;
newVelocities[index] = direction;
}
}
}
运行之后,生成了1w只在游动的鱼,跑起来有60多帧,主要是鱼群算法开销有点大。
关掉鱼群算法,停止鱼的移动后,帧率还是很高的。