Unity Dots入门
阅前须知
- 本文为个人Dots学习笔记,基于
Unity2019.4.40f1c1、Entities0.11.2-preview.1 - 本文约5w字,可以帮助你了解Dots的基本内容(对应版本),但由于dots在1.X之前API变动较多,以下内容可能会根据版本变化有所不同,详情请参考官方文档。
Entities overview | Entities | 1.0.16
一、HelloWorld
Dots和传统方式对比
为何传统方式无法创建大量物体:
- 数据冗余
例如实现一个移动功能,我们需要继承Monobehaviour,但我们只需要使用transfrom就足够,但Monobehaviour包含很多属性和方法,很多不用的东西也要被带到内存
- 单线程处理
- 编译器问题
现有编译器无法把C#编译成一个很高效的机器码
Dots
(Data Oriented Technology Stack)数据导向型技术栈
- ECS(Entity Component System):源自守望先锋,数据和行为分离,且数据精确、聚集
- JobSystem :多线程,充分发挥多核心Cpu特点
- Burst编译器:编译生成高效的代码
环境搭建(2019.4.40f1c1)
- 安装package
Entities
显示的包
HelloWorld
namespace HelloWord
{
using Unity.Entities;
using UnityEngine;
public class DebugSystem : ComponentSystem
{
protected override void OnUpdate()
{
Entities.ForEach((ref PrintComponentData printDta) =>
{
Debug.Log(printDta.printData);
});
}
}
}
namespace HelloWord
{
using Unity.Entities;
using UnityEngine;
public class PrintAuthoring : MonoBehaviour, IConvertGameObjectToEntity
{
public float printData;
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
{
dstManager.AddComponentData(entity, new PrintComponentData() { printData=this.printData});
}
}
}
namespace HelloWord
{
using Unity.Entities;
public struct PrintComponentData : IComponentData
{
public float printData;
}
}
系统屏蔽
多个系统会同时生效,如何屏蔽不需要的系统
[DisableAutoCreation]
public class TranslateSystem : ComponentSystem实例化
//获取Settings
var tempSettings = GameObjectConversionSettings.FromWorld(World.DefaultGameObjectInjectionWorld, null);
//Prefab转换Entity
Entity tempEntityPrefab = GameObjectConversionUtility.ConvertGameObjectHierarchy(cube, tempSettings);
//创建Entity
EntityManager entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
Entity tempCube = entityManager.Instantiate(tempEntityPrefab);性能对比
一万个cube 传统90fps ecs 160fps
JobSystem和Burst继续优化
public class JobComponentSystemTest : JobComponentSystem
{
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
var jobHandle = Entities.ForEach((ref RotationEulerXYZ rotationEulerXYZComponent) =>
{
rotationEulerXYZComponent.Value = new float3(0, 45, 0);
})
.WithBurst()
.Schedule(inputDeps);
return jobHandle;
}
}帧数对比 mono(90) 纯Ecs(160) Dots(170-180)
批处理优化
最后结合Gpu Instancing
- ECS
- 传统
质的飞跃
二、CURD(World、Entity、Component)
World类
管理该World下面所有的组件、实体、系统
此外World可以有多个,并且互相无法通信
World、World操作系统的CURD
void Start()
{
//创建一个World
World world = new World("TestA");
//默认的world
Debug.Log(World.DefaultGameObjectInjectionWorld.Name);
//所有的World
foreach (var item in World.All)
{
Debug.Log(item.Name);
}
//释放
world.Dispose();
}
#region System CURD
//获取所有System
foreach (var item in World.DefaultGameObjectInjectionWorld.Systems)
{
// Debug.Log(item);
}
//获取/创建System
DebugSystem debugSystem = World.DefaultGameObjectInjectionWorld.GetOrCreateSystem<DebugSystem>();
// Debug.Log(debugSystem);
//创建系统
//报错,一个世界不能有重复系统
// World.DefaultGameObjectInjectionWorld.AddSystem(debugSystem);
//不同世界是隔离的,但一个世界下某个系统只能有一个
World world = new World("TestWorld");
DebugSystem temp = world.AddSystem(debugSystem);
// 直接删除系统
// 会报错(InvalidOperationException: object is not initialized or has already been destroyed),
// 需要借助组进行
//获取组,并将系统从Group中移除,在进行删除
//由于我们的系统是从默认世界创建又移动到新的世界,所以他还是在默认世界的组中
InitializationSystemGroup group = World.DefaultGameObjectInjectionWorld.GetExistingSystem<InitializationSystemGroup>();
group.RemoveSystemFromUpdateList(temp);
world.DestroySystem(temp);
#endregionGroup
InitializationSystemGroup->SimulationSystemGroup->PresentationSystemGroup顺序执行
[UpdateInGroup(typeof(InitializationSystemGroup))]
public class DebugSystem : ComponentSystem转换Entity的方式
1.使用
可以同时添加
//绑定,执行ConvertToEntity的时候会调用这个接口,可以给实体添加一些Component,做一些操作
public class PrintAuthoring : MonoBehaviour, IConvertGameObjectToEntity
{
public float printData;
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
{
Debug.Log("PrintAuthoring");
dstManager.AddComponentData(entity, new PrintComponentData() { printData = this.printData });
}
}-
var tempSettings = GameObjectConversionSettings.FromWorld(World.DefaultGameObjectInjectionWorld, null);
//Prefab转换Entity
Entity tempEntityPrefab = GameObjectConversionUtility.ConvertGameObjectHierarchy(cube, tempSettings);
//创建Entity
EntityManager entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
Entity tempCube = entityManager.Instantiate(tempEntityPrefab);- SubScene
从0创建Entity
Chunk和ArcheType
- Chunk和Archetype
- Ecs会把有相同组件的实体放到一个chunk中:
Chunk:例如chunk1只存有PrintComponent chunk2只存储有RotationEulerXYZ组件
ArcheType:但一个chunk的空间有限,如果一个类型的chunk存不下,就会会创建多个该类型的Chunk,这多个同种类型的Chunk就被称为一个ArcheType
为什么要这么做?
为了访问实体更加快速(涉及CPU Cache的知识)
使用ArcheType和NaticeArray创建多个Entity
- NativeArray需要使用Dispose释放
//直接创建一个entity
Entity tempEntity = World.DefaultGameObjectInjectionWorld.EntityManager
.CreateEntity(typeof(PrintComponentData), typeof(RotationEulerXZY));
//利用Instantiate以一个现有实体作为模板
World.DefaultGameObjectInjectionWorld.EntityManager.Instantiate(tempEntity);
//创建一个原型
EntityArchetype tempArcheType = World.DefaultGameObjectInjectionWorld.EntityManager
.CreateArchetype(typeof(PrintComponentData), typeof(RotationEulerXZY));
//和Instantiate类似,效率更高
World.DefaultGameObjectInjectionWorld.EntityManager.CreateEntity(tempArcheType);
//循环创建多个Entity
for (int i = 0; i < 100; i++)
{
World.DefaultGameObjectInjectionWorld.EntityManager.CreateEntity(tempArcheType);
}
//使用NativeArray(JobSystem多线程不能使用List等)
//Allocator代表这些NativeArray存储的空间
NativeArray<Entity> tempNativeArray = new NativeArray<Entity>(5, Allocator.TempJob);
World.DefaultGameObjectInjectionWorld.EntityManager.CreateEntity(tempArcheType, tempNativeArray);
//Instantiate也可以
World.DefaultGameObjectInjectionWorld.EntityManager.Instantiate(tempEntity, tempNativeArray);查找Entity
- Query
//获取所有实体
NativeArray<Entity> entities = World.DefaultGameObjectInjectionWorld.EntityManager.GetAllEntities();
// foreach (var item in entities)
// {
// Debug.Log(item.Index);
// }
//筛选有特定组件的实体(类似于Entitas的Filter和Collector)
//获取一个查询
EntityQuery query = World.DefaultGameObjectInjectionWorld.EntityManager
.CreateEntityQuery(typeof(PrintComponentData), typeof(RotationEulerXZY));
// NativeArray<Entity> queryEntities = query.ToEntityArray(Allocator.TempJob);
// Debug.Log("Query Entities");
// foreach (var entity in queryEntities)
// {
// Debug.Log(entity.Index);
// }删除Entity
//单独删除
World.DefaultGameObjectInjectionWorld.EntityManager.DestroyEntity(tempEntity);
//使用NativeArray批量删除
World.DefaultGameObjectInjectionWorld.EntityManager.DestroyEntity(tempNativeArray);
//对EntityQuery查询的结果删除
World.DefaultGameObjectInjectionWorld.EntityManager.DestroyEntity(query);组件的增删改查
- 注意使用query删除的时候,要注意顺序,他应该是使用query的时候才去查询
如果顺序改变无法删除全部的两个组件
EntityQuery query = World.DefaultGameObjectInjectionWorld.EntityManager
.CreateEntityQuery(typeof(PrintComponentData), typeof(RotationEulerXZY));
//传递查询(应该是使用的时候才查询,如果和下一行切换顺序,会无法删除PrintComponentData,应该是没查到)
World.DefaultGameObjectInjectionWorld.EntityManager.RemoveComponent(query, typeof(PrintComponentData));
//批量删除(多个type只删除其中一个)
World.DefaultGameObjectInjectionWorld.EntityManager.RemoveComponent(tempNativeArray, typeof(RotationEulerXZY));- 删除不存在的组件会报错
- 注意引用类型和值类型组件,值类型需要再次设置
#region 组件的创建
//给单个实体添加组件
// World.DefaultGameObjectInjectionWorld.EntityManager.AddComponent(tempEntity, typeof(PrintComponentData));
// World.DefaultGameObjectInjectionWorld.EntityManager.AddComponent<PrintComponentData>(tempEntity);
//给多个实体添加单个组件 For循环
// World.DefaultGameObjectInjectionWorld.EntityManager.AddComponent(tempNativeArray, typeof(RotationEulerZYX));
// World.DefaultGameObjectInjectionWorld.EntityManager.AddComponent<RotationEulerZYX>(tempNativeArray);
//同样支持Query
// World.DefaultGameObjectInjectionWorld.EntityManager.AddComponent<RotationEulerZYX>(query);
//单个实体添加多个组件
// World.DefaultGameObjectInjectionWorld.EntityManager
// .AddComponents(tempEntity, new ComponentTypes(typeof(RotationEulerYZX), typeof(RotationEulerYXZ)));
#endregion
#region 组件数据的初始化
World.DefaultGameObjectInjectionWorld.EntityManager.AddComponentData(tempEntity, new RotationEulerYXZ()
{
Value = new float3(1, 1, 1)
});
//拖拽添加,组件添加[GenerateAuthoringComponent]
#endregion
#region 组件的查询和修改
//获取某个Entity上的组件
var component = World.DefaultGameObjectInjectionWorld.EntityManager.GetComponentData<RotationEulerXZY>(tempEntity);
Debug.Log(component);
//如果获取不存在的组件,会报错
//修改
//值类型需要重新设置,引用类型不需要
component.Value = new float3(-1, -1, 1);
World.DefaultGameObjectInjectionWorld.EntityManager.SetComponentData(tempEntity, component);
#endregion
#region 组件的删除
//单个实体删除组件
World.DefaultGameObjectInjectionWorld.EntityManager.RemoveComponent(tempEntity, typeof(RotationEulerXZY));
//传递查询(应该是使用的时候才查询,如果和下一行切换顺序,会无法删除PrintComponentData,应该是没查到)
World.DefaultGameObjectInjectionWorld.EntityManager.RemoveComponent(query, typeof(PrintComponentData));
//批量删除(多个type只删除其中一个)
World.DefaultGameObjectInjectionWorld.EntityManager.RemoveComponent(tempNativeArray, typeof(RotationEulerXZY));
#endregioSharedComponent
相同值的Entity共享一份数据
假如有一万个SizeComponent 其size均为1,那么此时这一万个实体共用一个Component
如果我们修改其中100个size为2,那么其实是9900个实体公用size为1的Component
而新的100个共用size为2的Componen
有点HashSet的意思
公用一个SharedComponent会被放到一个chunk中CURD
var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
//增加
// entityManager.AddSharedComponentData(entity, new SharedComponent() { data=1});
NativeArray<Entity> entities = entityManager.GetAllEntities();
foreach (var entity in entities)
{
Debug.Log(entity.Index);
}
//修改
entityManager.SetSharedComponentData(entities[0], new SharedComponent() { data = 10 });
//查找
SharedComponent sharedComponent = entityManager.GetSharedComponentData<SharedComponent>(entities[0]);
Debug.Log($"GetSharedComponent {sharedComponent.data}");
//移除
entityManager.RemoveComponent<SharedComponent>(entities[0]);SharedComponent的相等判断
public struct SharedComponent : ISharedComponentData, IEquatable<SharedComponent>
{
public int data;
//重写Equals
public bool Equals(SharedComponent other)
{
return data == other.data;
}
public override int GetHashCode()
{
int tempHash = 0;
tempHash ^= data.GetHashCode();
return tempHash;
}
}
entityManager.SetSharedComponentData(entities[0], new SharedComponent() { data = 10 });
//查找
SharedComponent sharedComponent = entityManager.GetSharedComponentData<SharedComponent>(entities[0]);
Debug.Log($"GetSharedComponent {sharedComponent.data}");
//移除
// entityManager.RemoveComponent<SharedComponent>(entities[0]);
//判断是否相等,注意当我们SetSharedComponent之后,其实这个共享组件就已经变了,他是一个新的
//所以要重写Equals
SharedComponent sharedComponent2 = entityManager.GetSharedComponentData<SharedComponent>(entities[1]);
Debug.Log(sharedComponent.Equals(sharedComponent2));
entities.Dispose();
状态组件
DOTS的ECS没有回调
使用ISystemStateComponentData
当实体被销毁时
- 该组件不会销毁,从而得到组件状态,彻底销毁需要RemoveComponet(EntityManager)
- 可以通过查询实体查找
var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
EntityQuery query = entityManager.CreateEntityQuery(typeof(StateComponent));
NativeArray<Entity> entities = query.ToEntityArray(Allocator.TempJob);
//删除实体
entityManager.DestroyEntity(query);
//仍然可以设置组件
entityManager.SetComponentData<StateComponent>(entities[0], new StateComponent() { data = 11 });
//打印数据
var component = entityManager.GetComponentData<StateComponent>(entities[0]);
Debug.Log($"after destroy: {component.data}");
// entityManager.RemoveComponent<StateComponent>(query);
entities.Dispose();解开注释,实体被销毁
entityManager.RemoveComponent<StateComponent>(query);BufferElement
动态缓冲区,可以存储多个组件(类似于List),可相同
实现
- 如果想使用多个数据需要自己写一个转换类
//使用该特性只能有一个字段
// [GenerateAuthoringComponent]
public struct BufferComponent : IBufferElementData
{
public int data;
public int data2;
}转换类
public class BufferAuthoring : MonoBehaviour, IConvertGameObjectToEntity
{
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
{
DynamicBuffer<BufferComponent> buffer = dstManager.AddBuffer<BufferComponent>(entity);
buffer.Add(new BufferComponent() { data = 1, data2 = 2 });
buffer.Add(new BufferComponent() { data = 1, data2 = 2 });
buffer.Add(new BufferComponent() { data = 1, data2 = 2 });
}
}操作,基本和List相似
public class BufferElementOperation : MonoBehaviour
{
void Start()
{
EntityManager entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
//查
EntityQuery query = entityManager.CreateEntityQuery(typeof(BufferComponent));
NativeArray<Entity> entities = query.ToEntityArray(Allocator.TempJob);
//获取buffer
DynamicBuffer<BufferComponent> buffer = entityManager.GetBuffer<BufferComponent>(entities[0]);
Debug.Log("buffer" + buffer[0].data);
//插入
buffer.Insert(3, new BufferComponent() { data = 11, data2 = 22 });
entities.Dispose();
foreach (BufferComponent item in buffer)
{
Debug.Log($"buffer item {item.data}");
}
}
}
ChunkComponent
修改时,所有拥有这个组件的实体的值都会修改
三、系统System[重要]
包含 SystemBase、ComponentSystem、JobSystem
public class TestSystem : SystemBase
{
protected override void OnUpdate()
{
Entities.ForEach((ref Translation translationComponent) =>
{
translationComponent.Value = new float3(1, 1, 1);
}).Schedule();
}
}
System的生命周期
OnCreate->OnStartRunning->OnUpdate->OnStopRunning->OnDestroy
public class TestSystem : SystemBase
{
//创建系统
protected override void OnCreate()
{
Debug.Log("OnCreate");
}
//开始运行
protected override void OnStartRunning()
{
Debug.Log("OnStartRunning");
}
//更新
protected override void OnUpdate()
{
Debug.Log("OnUpdate");
Entities.ForEach((ref Translation translationComponent) =>
{
translationComponent.Value = new float3(1, 1, 1);
}).Schedule();
}
//停止运行
protected override void OnStopRunning()
{
Debug.Log("OnStopRunning");
}
//销毁
protected override void OnDestroy()
{
Debug.Log("OnDestroy");
}
}筛选相关修饰方法基本使用
以下均可同时使用,注意同时使用不要过分复杂,类似于Entitas的Collector和Filter
WithAll<>
筛选有对应组件的实体执行,必须同时拥有
protected override void OnUpdate()
{
//ref:读写
//in:只读
//同时有ref和in前面放ref:有助于JobSystem的运行
Debug.Log("OnUpdate");
//对同时拥有Translation和PrintComponent组件的实体进行操作
Entities.ForEach((ref Translation translationComponent) =>
{
Debug.Log($"Translation:{translationComponent.Value}");
}).WithAll<PrintComponentData>().Run();
}筛选多个
.WithAll<PrintComponentData,Rotation>().Run();WithAny<>
筛选包含任意其中任意一个组件的实体执行
WithNone<>
筛选不包含其中任意一个组件的实体执行
WithChangeFilter<>
只会对 对应组件发生变化的实体执行
- 对应筛选的组件必须要在Foreach中添加
Entities.ForEach((ref Translation translation,ref PrintComponentData printComponentData) =>
{
Debug.Log($"Translation:{translation.Value}");
})
.WithChangeFilter<PrintComponentData>()
.Run();仅打印一次,因为只有第一次赋值的时候PrintComponentData 有变化
WithSharedComponentFilter
筛选具有特定共享组件的实体执行
//筛选具有data为1共享组件的Entity
.WithSharedComponentFilter(new SharedComponent(){data=1})WithStoreEntityQueryInfield
类似于EntityQuery,可以将Foreach遍历的实体通过该Query存储,进行操作
- EntityQuery系统创建时就会被赋值,而非运行时,即便交换Debug顺序,也不会报错
//系统创建时就会被赋值,而非运行时
private EntityQuery entityQuery;
protected override void OnUpdate()
{
//ref:读写
//in:只读
//同时有ref和in前面放ref:有助于JobSystem的运行
//对同时拥有Translation和PrintComponent组件的实体进行操作
Entities.ForEach((ref Translation translation) =>
{
Debug.Log($"Translation:{translation.Value}");
})
.WithStoreEntityQueryInField(ref entityQuery)
.Run();
NativeArray<Entity> entities = entityQuery.ToEntityArray(Allocator.TempJob);
foreach (var entity in entities)
{
Debug.Log(entity.Index);
}
entities.Dispose();
Debug.Log("OnUpdate");
}WIthEntitiyQueryOptions<>
筛选属于对应Group的实体
EntityQueryOptions 介绍
以下为AI生成
- Default (默认)
// 作用:默认行为
// - 不包含禁用的实体
// - 不包含预制体实体
// - 考虑组件启用状态
// - 不过滤写入组
EntityQueryOptions.Default- IncludeDisabled
// 作用:包含被禁用的实体
// 场景:需要处理所有实体,无论是否启用
// 示例:调试、编辑器工具、状态恢复
EntityQueryOptions.IncludeDisabled- IncludePrefab
// 作用:包含预制体实体
// 场景:编辑器模式、预制体处理、初始化系统
// 注意:运行时通常不需要处理预制体
EntityQueryOptions.IncludePrefab- FilterWriteGroup
筛选对应WirteGroup的组件
// 作用:过滤写入组,避免组件冲突
// 场景:多个系统可能写入同一组件时
// 用途:确保只有合适的系统处理特定实体
EntityQueryOptions.FilterWriteGroup筛选没有对应Group的组件,适用于只想让某个系统处理某些组件
// 1. 定义WriteGroup标记
[WriteGroup(typeof(LocalToWorld))] // CustomMatrix会写入LocalToWorld
public struct CustomMatrix : IComponentData
{
public float4x4 Value;
}
[WriteGroup(typeof(LocalToWorld))] // RigidBodyTransform也会写入LocalToWorld
public struct RigidBodyTransform : IComponentData
{
public float4x4 Value;
}
// 2. 使用FilterWriteGroup的查询
Entities.ForEach((ref LocalToWorld localToWorld, in Translation translation) =>
{
localToWorld.Value = float4x4.Translate(translation.Value);
})
.WithEntityQueryOptions(EntityQueryOptions.FilterWriteGroup)
.Run();
// 3. ECS自动执行的过滤逻辑:
// - 检测到你要写入 LocalToWorld 组件
// - 查找所有标记了 [WriteGroup(typeof(LocalToWorld))] 的组件
// - 发现 CustomMatrix 和 RigidBodyTransform 都标记了
// - 自动添加 .WithNone<CustomMatrix, RigidBodyTransform>()- IgnoreComponentEnabledState
// 作用:忽略 IEnableableComponent 的启用状态
// 场景:需要处理所有组件,无论组件是否启用
// 性能:避免额外的启用状态检查
EntityQueryOptions.IgnoreComponentEnabledState- 可以多个一起使用
// 单个选项
.WithEntityQueryOptions(EntityQueryOptions.IncludeDisabled)
// 多个选项组合(使用 | 操作符)
.WithEntityQueryOptions(
EntityQueryOptions.IncludeDisabled |
EntityQueryOptions.IncludePrefab
)
// 包含所有可能的实体
.WithEntityQueryOptions(
EntityQueryOptions.IncludeDisabled |
EntityQueryOptions.IncludePrefab |
EntityQueryOptions.IgnoreComponentEnabledState
)Dots多线程相关
注意Run、Schedule和ScheduleParallel不能同时使用
Run
Entities.ForEach在主线程运行
Schedule(异步单线程执行)
Entities.ForEach在其他线程(单线程执行)异步执行
Entities.ForEach((ref Translation translation) =>
{
Debug.Log($"Translation:{translation.Value}");
})
.Schedule()
;下图可以看到Debug的部分放到了Job的Work0去执行打印
可以使用WithName修改在Profiler的显示名称
.WithName("ColdPlay")
.Schedule()ScheduleParallel
异步多线程执行
WithBurst
使用Burst编译器(默认使用),如不适用需要写 .WithoutBurst()
注意!!!
在多线程中尽量不使用Unity原生的类,因为Unity的很多类都是线程不安全的
Entities.Foreach的变量修饰(多线程)
示例
int i = 3;
//对同时拥有Translation和PrintComponent组件的实体进行操作
Entities.ForEach((ref Translation translation) =>
{
i = 5;
translation.Value = new float3(2, 2, 2);
})
.WithName("ColdPlay")
.ScheduleParallel();
;
直接报错,这种写法只能支持单线程
Native容器
包含NativeArray、NativeHashMap、NativeMultiHashMap、NativeQueue
需要分配到对应NativeContent中包含Temp(适用于1帧)、JobTemp(适用于4帧内)、Persistent(长久使用),生命周期依次递增、性能依次递减
如tempJob四帧内没有Dispose就会有Warning
以NativeArray为例
NativeArray<int> tempArray = new NativeArray<int>(5, Allocator.TempJob);
Entities.ForEach((ref Translation translation) =>
{
tempArray[0] = 5;
})
//修改Profiler中显示的名字
.WithName("ColdPlay")
//变量不受线程安全的限制(慎用,可能导致线程同时读写,除非确定不会有多个线程同时读写,否则不要使用,但使用会提高一定性能)
.WithNativeDisableContainerSafetyRestriction(tempArray)
//修饰变量释放
.WithDeallocateOnJobCompletion(tempArray)
//本地容器只读
.WithReadOnly(tempArray)
.ScheduleParallel()
;
//阻塞线程释放
CompleteDependency();
tempArray.Dispose();释放
//修饰变量释放
.WithDeallocateOnJobCompletion(tempArray)
//阻塞线程释放
CompleteDependency();
tempArray.Dispose();System中获取、操作实体
错误示例
不能在多线程中修改实体
- Run()
Entities.ForEach((Entity entity, ref Translation translation) =>
{
translation.Value = new float3(2, 2, 2);
//添加组件
EntityManager.AddComponentData(entity, new PrintComponentData() { printData = 111 });
})
.ScheduleParallel()
;正确示例
方式1:EntityManager
- 需使用
.WithStructuralChanges().WithoutBurst().Run()修饰方法
Entities.ForEach((Entity entity, ref Translation translation) =>
{
translation.Value = new float3(2, 2, 2);
//添加组件
EntityManager.AddComponentData(entity, new PrintComponentData() { printData = 111 });
})
//修饰
.WithStructuralChanges()
//不适用Burst编译器,且在主线程运行
.WithoutBurst()
.Run()
;方式2:EntityComponentBuffer
第二种方式效率更高,是比较推荐的做法
EntityCommandBuffer entityCommandBuffer = new EntityCommandBuffer(Allocator.TempJob);
//对同时拥有Translation和PrintComponent组件的实体进行操作
Entities.ForEach((Entity entity, ref Translation translation) =>
{
//EntityManager添加组件
// EntityManager.AddComponentData(entity, new PrintComponentData() { printData = 111 });
//EntityCommandBuffer添加组件
entityCommandBuffer.AddComponent(entity, new PrintComponentData() { printData = 111 });
})
.Run()
;
//调用
entityCommandBuffer.Playback(EntityManager);
entityCommandBuffer.Dispose();JobWithCode
适合复杂逻辑(不针对实体),放在一个其他线程去跑,整体类似于Entities.ForEach()
NativeArray<float> temp = new NativeArray<float>(1000, Allocator.TempJob);
Job.WithCode(() =>
{
for (int i = 0; i < temp.Length; i++)
{
temp[i] = i;
}
}).Schedule();
for (int i = 0; i < temp.Length; i++)
{
Debug.Log(temp[i]);
}
CompleteDependency();
temp.Dispose();ComponentSystem 和 JobComponentSystem
ComponentSystem
//所有代码均在主线程执行
//仅支持Entities.ForEach()
//且没有类似于SystemBase的修饰方法
public class TestComponentSystem : ComponentSystem
{
protected override void OnUpdate()
{
//类似于.Run()
//推荐SystemBase.Run()可以支持Burst编译器
Entities.ForEach((Entity entity, ref Translation translation) =>
{
translation.Value = new float3(1, 1, 1);
});
}
}
JobComponentSystem
//支持多线程
public class TestJobSystem : JobComponentSystem
{
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
var tempReturn = Entities.ForEach((Entity entity, ref Translation translation) =>
{
translation.Value = new float3(1, 1, 1);
}).Schedule(inputDeps);
return tempReturn;
}
}三种系统对比 SystemBase、JobComponentSystem、ComponentSystem
注意
以下内容基于AI生成修改,推荐使用SystemBase
🧩 Unity DOTS 系统基类对比表
| 特性 | ComponentSystem | JobComponentSystem | SystemBase |
|---|---|---|---|
| 引入版本 | Unity 2018.3 (旧版ECS) | Unity 2019.1 (过渡方案) | Unity 2020.1+ (现代ECS) |
| 当前状态 | ❌ 已过时 ([Obsolete]) | ❌ 已过时 ([Obsolete]) | ✅ 官方推荐 |
| 线程模型 | 🔁 主线程运行 | ⚡ 支持 Job 多线程并行 | ⚡ 原生深度并行支持 |
| 依赖管理 | 🚫 无自动依赖管理 | ⚙️ 手动管理 JobHandle 依赖链 | 🤖 自动管理 (Dependency 属性) |
| Burst 支持 | ⚠️ 有限支持 | ⚠️ 需手动配置 | ✅ 原生深度集成 |
| 编码复杂度 | ░░░░░ 低 (简单逻辑) | ░░░░░░░░░ 高 (需理解 Job 系统) | ░░░░░░ 中 (平衡简洁与灵活) |
| 典型代码结构 | Entities.ForEach (主线程) | 需实现 IJobChunk | Entities.ForEach + ScheduleParallel |
| 未来兼容性 | ❌ 无保障 | ❌ 无保障 | ✅ 持续维护 |
⚙️ 核心差异详解
1. ComponentSystem
[Obsolete("Use SystemBase instead")]
public class LegacySystem : ComponentSystem
{
protected override void OnUpdate()
{
// 主线程运行,性能瓶颈
Entities.ForEach((ref Translation trans) => {
trans.Value.y += 0.1f;
});
}
}- 定位:初代 ECS 实现
缺点:
- 所有逻辑在主线程执行
- 无法利用多核 CPU
- 无自动依赖管理
- 适用场景:已淘汰,不推荐使用
2. JobComponentSystem
[Obsolete("Use SystemBase instead")]
public class ParallelSystem : JobComponentSystem
{
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
var job = new MoveJob { deltaTime = Time.DeltaTime };
// 手动管理依赖链
return job.Schedule(this, inputDeps);
}
[BurstCompile]
struct MoveJob : IJobForEach<Translation>
{
public float deltaTime;
public void Execute(ref Translation trans)
{
trans.Value.y += 1f * deltaTime;
}
}
}- 定位:并行计算的过渡方案
改进:
- 引入
IJobForEach/IJobChunk 支持多线程 - 可通过 Burst 编译优化
- 引入
痛点:
- 需手动管理
JobHandle 依赖链 - 代码冗余(需声明嵌套 Job 结构)
- 调试复杂
- 需手动管理
3. SystemBase (推荐方案)
public class ModernSystem : SystemBase
{
protected override void OnUpdate()
{
float dt = Time.DeltaTime;
// 一行代码实现并行逻辑 + 自动依赖管理
Entities
.ForEach((ref Translation trans) => {
trans.Value.y += 1f * dt;
})
.ScheduleParallel(); // 自动更新 Dependency
}
}核心优势:
- 依赖自动管理:通过
Dependency 属性隐式处理 - 简洁 API:链式调用
Entities.ForEach().ScheduleParallel() 深度优化:
- 自动 Burst 编译(配合
[BurstCompile]) - 支持
IJobEntity 代码生成
- 自动 Burst 编译(配合
- 混合模式:可通过
Run() 切回主线程
- 依赖自动管理:通过
IJob和IJobParallelFor
IJob
使用多线程和不使用多线程的对比:
- 使用多线程
- 不使用多线程
namespace System
{
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
public class TestIJob : MonoBehaviour
{
public bool useThread = true;
//使用Burst编译器
[BurstCompile]
//代表一个线程,去做一些操作
//注意不能用UnityEngine下的(线程不安全)
public struct Job : IJob
{
[ReadOnly]
public int i, j;
public float result;
public void Execute()
{
for (int i = 0; i < 100000; i++)
{
result = math.exp10(math.sqrt(i * j));
}
}
}
void Update()
{
float startTime = Time.realtimeSinceStartup;
if (useThread)
Thread();
else
NoThread();
float sumTime = Time.realtimeSinceStartup - startTime;
Debug.Log("执行时间" + sumTime);
}
public void NoThread()
{
for (int i = 0; i < 10; i++)
{
for (int j = 0; j < 100000; j++)
{
float result = math.exp10(math.sqrt(6 * 7));
}
}
}
public void Thread()
{
NativeList<JobHandle> jobHandles = new NativeList<JobHandle>(Allocator.TempJob);
for (int i = 0; i < 10; i++)
{
Job tempJob2 = new Job() { i = 6, j = 7 };
JobHandle tempJobHandle2 = tempJob2.Schedule();
jobHandles.Add(tempJobHandle2);
}
JobHandle.CompleteAll(jobHandles);
jobHandles.Dispose();
}
}
}
IJobParallelFor
传统执行
namespace System
{
using System.Collections.Generic;
using Unity.Mathematics;
using UnityEngine;
public class TestIJobParallelFor : MonoBehaviour
{
public GameObject cube;
private List<GameObject> goList = new List<GameObject>();
void Start()
{
Vector3 temp = Vector3.zero;
for (int i = 0; i < 100; i++)
{
for (int j = 0; j < 10; j++)
{
GameObject tempCube = Instantiate(cube);
tempCube.transform.position = (Vector3.right * i + Vector3.up * j) * 1.5f;
goList.Add(tempCube);
}
}
}
void Update()
{
//耗性能操作
foreach (var go in goList)
{
go.transform.eulerAngles += new Vector3(0, 30 * Time.deltaTime, 0);
for (int i = 0; i < 10; i++)
{
for (int j = 0; j < 100000; j++)
{
float result = math.exp10(math.sqrt(6 * 7));
}
}
}
}
}
}
IJobParallelFor执行
namespace System
{
using System.Collections.Generic;
using Unity.Collections;
using Unity.Entities.UniversalDelegates;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
public class TestIJobParallelFor : MonoBehaviour
{
public GameObject cube;
public bool useIJobParallelFor = true;
private List<GameObject> goList = new List<GameObject>();
void Start()
{
Vector3 temp = Vector3.zero;
for (int i = 0; i < 100; i++)
{
for (int j = 0; j < 10; j++)
{
GameObject tempCube = Instantiate(cube);
tempCube.transform.position = (Vector3.right * i + Vector3.up * j) * 1.5f;
goList.Add(tempCube);
}
}
}
void Update()
{
if (useIJobParallelFor)
{
//IJobParallelFor
NativeArray<float3> temp = new NativeArray<float3>(goList.Count, Allocator.TempJob);
ParallelJob parallelJob = new ParallelJob();
for (int i = 0; i < goList.Count; i++)
{
temp[i] = goList[i].transform.eulerAngles;
}
parallelJob.deltaTime = Time.deltaTime;
parallelJob.eulerAngles = temp;
JobHandle jobHandle = parallelJob.Schedule(goList.Count, 10);
jobHandle.Complete();
for (int i = 0; i < goList.Count; i++)
{
goList[i].transform.eulerAngles = temp[i];
}
temp.Dispose();
}
else
{
//传统方式
foreach (var go in goList)
{
go.transform.eulerAngles += new Vector3(0, 30 * Time.deltaTime, 0);
for (int j = 0; j < 1000; j++)
{
float result = math.exp10(math.sqrt(6 * 7));
}
}
}
}
}
public struct ParallelJob : IJobParallelFor
{
public NativeArray<float3> eulerAngles;
public float deltaTime;
public void Execute(int index)
{
eulerAngles[index] += new float3(0, 30 * deltaTime, 0);
for (int j = 0; j < 1000; j++)
{
float result = math.exp10(math.sqrt(6 * 7));
}
}
}
}
传统方式和IJobParallelFor性能对比
调度job意味着只能有一项工作在做一件事。在一个游戏中,想要在大量物体上
执行相同操作的情况非常普遍。对于这种情况,有一个单独的工作类型:IJobParallelFor。
IJobParallelFor的行为与lJob类似,但不是单次执行得到一个结果而是一次得到批量结果。
系统实际上不会为每个项目安排一个job,它会为每个CPU核心最多安排一个job,并重新分配工作
负载,这些都在系统内部处理的。
在调度ParallelForJobs时,必须指定要分割的数组的长度,
因为如果结构中有多个数组,则系统无法知道要将哪个数组用作主数据。你还需要指定批次的数
量。
批处理计数控制你将获得多少job,以及线程之间的工作重新分配的细化程度如何。拥有较
低的批处理数量(如1)会使你在线程之间进行更均匀的工作分配。但是,它会带来一些开销
因此在某些情况下,稍微增加批次数量会更好。传统
IJobParallelFor
不使用Burst
使用Burst
AI总结
🔧 IJobParallelFor 工作类型核心特性总结
设计目的
- 专为在大量物体上执行相同操作的高并发场景设计
- 适用于游戏开发中常见的批量数据处理需求(如:同时更新 10,000+ 个物体的位置)
调度机制
智能任务分割
- 不为每个项目单独创建 Job
- 根据 CPU 核心数量动态分配(每个核心最多分配一个 Job)
- 自动重新分配工作负载以平衡线程压力
批处理控制
- 需手动指定待处理数组的总长度(
arrayLength) - 需明确设置批次数量(
batchCount)
- 需手动指定待处理数组的总长度(
批处理参数优化
低批次数量(如 1)
- ✅ 优点:线程间工作分配更均匀
- ❌ 缺点:调度开销增大,可能降低整体性能
较高批次数量(如 32~64)
- ✅ 优点:减少调度开销,提升吞吐量
- ❌ 缺点:可能导致线程负载不均衡
黄金法则:
- 简单操作 → 使用较大批次(减少开销)
- 复杂操作 → 使用较小批次(提升并行度)
四、实践
一万个小球下落
物理效果
使用刚体,安装Unity Physic包
安装后记得重启
此时给cube加上Rigidbody小球可以转换为实体后正常受到重力(不安装这个包无法下落)
注意此时如果需要发生碰撞,需要两者都是Entity
产生弹性
可使用物理材质
Physic Body(Ecs的刚体)
该组件必须要配合Convet To Entity脚本使用,除此之外还有ECS的碰撞体(Physic Body可以搭配传统的Collider使用)
一万个小球自由下落
- 注意打开Burst编译
- 可以使用Gpu Instancing
代码:
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;
namespace SphereTest
{
// 该脚本用于管理小球的生成和销毁
public class SphereManager : MonoBehaviour
{
public GameObject spherePrefab;
//小球数量
public int sphereCount;
//小球生成间隔
public float interval;
BlobAssetStore blobAssetStore;
void Start()
{
//创建BlobAssetStore
blobAssetStore = new BlobAssetStore();
//获取Settings
var tempSettings = GameObjectConversionSettings.FromWorld(World.DefaultGameObjectInjectionWorld, blobAssetStore);
//Prefab转换Entity
Entity tempEntityPrefab = GameObjectConversionUtility.ConvertGameObjectHierarchy(spherePrefab, tempSettings);
Translation translation = new Translation();
EntityManager entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
for (int i = 0; i < sphereCount; i++)
{
Entity tempSphere = entityManager.Instantiate(tempEntityPrefab);
//按照一个16**16*16的多边形生成
translation.Value = new float3(i % 16 * interval * UnityEngine.Random.Range(-0.1f, 0.1f), i / 16 % 16 * interval, i / (16 * 16) * UnityEngine.Random.Range(-0.1f, 0.1f));
entityManager.SetComponentData(tempSphere, translation);
}
//创建Entity
}
void OnDestroy()
{
blobAssetStore.Dispose();
}
}
}运行
鱼群移动
- 分隔规则:尽量避免与伙伴过于拥挤
- 对准规则:尽量与临近伙伴的平均方向一致
- 内聚规则:尽量朝附近伙伴的中心移动
单线程
using System.Collections.Generic;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;
namespace FishTest
{
// 该脚本用于管理小球的生成和销毁
public class FishManager : MonoBehaviour
{
public GameObject fishPrefab;
//鱼的数量
public int fishCount;
//生成鱼的圆心
public Transform sphereCenter;
//生成球的半径
public float sphereRadius;
//移动目标
public Transform target;
//移动速度
public float moveSpeed;
//旋转速度
public float rotationSpeed;
//目标方向权重
public float targetDirWeight;
//远离方向权重
public float farAwayDirWeight;
//中心方向权重
public float centerDirWeight;
BlobAssetStore blobAssetStore;
List<Entity> tempList = new List<Entity>();
EntityManager entityManager;
void Start()
{
//创建BlobAssetStore
blobAssetStore = new BlobAssetStore();
//获取Settings
var tempSettings = GameObjectConversionSettings.FromWorld(World.DefaultGameObjectInjectionWorld, blobAssetStore);
//Prefab转换Entity
Entity tempEntityPrefab = GameObjectConversionUtility.ConvertGameObjectHierarchy(fishPrefab, tempSettings);
entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
Translation translation = new Translation();
//生成球形鱼群
for (int i = 0; i < fishCount; i++)
{
//随机方向
Vector3 dir = new Vector3
(UnityEngine.Random.Range(-1f, 1f), UnityEngine.Random.Range(-1f, 1f), UnityEngine.Random.Range(-1f, 1f)).normalized;
//鱼的位置
translation.Value = sphereCenter.position + dir * sphereRadius;
//创建Entity
Entity tempFish = entityManager.Instantiate(tempEntityPrefab);
//设置位置
entityManager.SetComponentData(tempFish, translation);
Rotation tempRotation = new Rotation();
tempRotation.Value = quaternion.LookRotationSafe(dir, math.up());
//设置旋转
entityManager.SetComponentData(tempFish, tempRotation);
tempList.Add(tempFish);
}
}
void Update()
{
//获取鱼群的原理方向
float3 tempSumPos = float3.zero;
//鱼群中心点
float3 tempCenterPos = float3.zero;
foreach (var fish in tempList)
{
tempSumPos += entityManager.GetComponentData<LocalToWorld>(fish).Position;
}
//计算中心位置
tempCenterPos = tempSumPos / tempList.Count;
foreach (var fish in tempList)
{
LocalToWorld localToWorld = entityManager.GetComponentData<LocalToWorld>(fish);
//和目标点方向(对准规则)
float3 tempTargetDir = math.normalize((float3)target.position - localToWorld.Position);
//计算远离方向(分隔原则)
float3 tempFarAwayDir = math.normalize(tempList.Count * localToWorld.Position - tempSumPos);
//中心方向(内聚原则)
float3 tempCenterDir = math.normalize(tempCenterPos - localToWorld.Position);
//计算最终方向
float3 finalDir = math.normalize(tempTargetDir * targetDirWeight + tempFarAwayDir * farAwayDirWeight + tempCenterDir * centerDirWeight);
//前方与最终方向的差值
float3 tempOffsetRotation = math.normalize(finalDir - localToWorld.Forward);
localToWorld.Value = float4x4.TRS
//默认朝着自己前方移动
(localToWorld.Position + localToWorld.Forward * moveSpeed * Time.deltaTime,
//同时朝着目标方向旋转
quaternion.LookRotationSafe(localToWorld.Forward + tempOffsetRotation * rotationSpeed * Time.deltaTime, math.up()),
new float3(1, 1, 1)
);
entityManager.SetComponentData(fish, localToWorld);
}
}
void OnDestroy()
{
blobAssetStore.Dispose();
}
}
}使用系统
创建TargetComponent和FishComponent
using Unity.Entities;
[GenerateAuthoringComponent]
public struct FishComponent : IComponentData
{
public float moveSpeed;
//旋转速度
public float rotationSpeed;
//目标方向权重
public float targetDirWeight;
//远离方向权重
public float farAwayDirWeight;
//中心方向权重
public float centerDirWeight;
}using Unity.Entities;
[GenerateAuthoringComponent]
public struct TargetComponent : IComponentData
{
}修改FishManager只负责创建鱼
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;
namespace FishTest
{
// 该脚本用于管理小球的生成和销毁
public class FishManager : MonoBehaviour
{
public GameObject fishPrefab;
//鱼的数量
public int fishCount;
//生成鱼的圆心
public Transform sphereCenter;
//生成球的半径
public float sphereRadius;
BlobAssetStore blobAssetStore;
EntityManager entityManager;
void Start()
{
//创建BlobAssetStore
blobAssetStore = new BlobAssetStore();
//获取Settings
var tempSettings = GameObjectConversionSettings.FromWorld(World.DefaultGameObjectInjectionWorld, blobAssetStore);
//Prefab转换Entity
Entity tempEntityPrefab = GameObjectConversionUtility.ConvertGameObjectHierarchy(fishPrefab, tempSettings);
entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
Translation translation = new Translation();
//生成球形鱼群
for (int i = 0; i < fishCount; i++)
{
//随机方向
Vector3 dir = new Vector3
(UnityEngine.Random.Range(-1f, 1f), UnityEngine.Random.Range(-1f, 1f), UnityEngine.Random.Range(-1f, 1f)).normalized;
//鱼的位置
translation.Value = sphereCenter.position + dir * sphereRadius;
//创建Entity
Entity tempFish = entityManager.Instantiate(tempEntityPrefab);
//设置位置
entityManager.SetComponentData(tempFish, translation);
Rotation tempRotation = new Rotation();
tempRotation.Value = quaternion.LookRotationSafe(dir, math.up());
//设置旋转
entityManager.SetComponentData(tempFish, tempRotation);
}
}
void OnDestroy()
{
blobAssetStore.Dispose();
}
}
}创建对应系统
尚未使用Burst编译器和多线程,单线程改为系统
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
public class BoldMoveSystem : SystemBase
{
protected override void OnUpdate()
{
//获取鱼群的平均方向
float3 tempSumPos = float3.zero;
//鱼群中心点
float3 tempCenterPos = float3.zero;
//目标点
float3 targetPos = float3.zero;
int fishCount = 0;
//获取整体位置和鱼的数量
Entities.ForEach((Entity fish, ref LocalToWorld localToWorld, ref FishComponent fishComponent) =>
{
tempSumPos += localToWorld.Position;
fishCount++;
}).Run();
//获取中心方向
tempCenterPos = tempSumPos / fishCount;
//获取Target
Entities.ForEach((Entity fish, ref LocalToWorld localToWorld, ref TargetComponent target) =>
{
targetPos = localToWorld.Position;
}).Run();
Entities.ForEach((Entity fish, ref LocalToWorld localToWorld, ref FishComponent fishComponent) =>
{
//和目标点方向(对准规则)
float3 tempTargetDir = math.normalize(targetPos - localToWorld.Position);
//计算远离方向(分隔原则)
float3 tempFarAwayDir = math.normalize(fishCount * localToWorld.Position - tempSumPos);
//中心方向(内聚原则)
float3 tempCenterDir = math.normalize(tempCenterPos - localToWorld.Position);
//计算最终方向
float3 finalDir = math.normalize
(tempTargetDir * fishComponent.targetDirWeight +
tempFarAwayDir * fishComponent.farAwayDirWeight +
tempCenterDir * fishComponent.centerDirWeight);
//前方与最终方向的差值
float3 tempOffsetRotation = math.normalize(finalDir - localToWorld.Forward);
localToWorld.Value = float4x4.TRS
//默认朝着自己前方移动
(localToWorld.Position + localToWorld.Forward * fishComponent.moveSpeed * Time.DeltaTime,
//同时朝着目标方向旋转
quaternion.LookRotationSafe(localToWorld.Forward + tempOffsetRotation * fishComponent.rotationSpeed * Time.DeltaTime, math.up()),
new float3(1, 1, 1)
);
}).WithoutBurst().Run();
}
}使用Burst编译器,并使用多线程
//提前存储deltaTime
float deltaTime = Time.DeltaTime;修改赋值
localToWorld.Value = float4x4.TRS
//默认朝着自己前方移动
(localToWorld.Position + localToWorld.Forward * fishComponent.moveSpeed * deltaTime,
//同时朝着目标方向旋转
quaternion.LookRotationSafe(localToWorld.Forward + tempOffsetRotation * fishComponent.rotationSpeed * deltaTime, math.up()),
new float3(1, 1, 1)
);将计算鱼群位置的部分修改为ScheduleParallel()
性能提升及其显著!!!
添加鼠标点击事件
需要使用ECS的射线检测
void Update()
{
// 检测鼠标左键是否被按下
if (Input.GetMouseButton(0))
{
// 从摄像机屏幕坐标创建射线(从摄像机位置指向鼠标点击位置)
UnityEngine.Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
// 执行ECS物理射线检测,射线长度设为100000单位
Entity hitEntity = Raycast(ray.origin, ray.direction * 100000);
// 输出被击中的实体信息到控制台
Debug.Log(hitEntity);
}
}
/// <summary>
/// 在ECS物理世界中执行射线检测
/// </summary>
/// <param name="originalPos">射线起始位置</param>
/// <param name="targetPos">射线结束位置</param>
/// <returns>被击中的实体,如果没有击中则返回Entity.Null</returns>
private Entity Raycast(float3 originalPos, float3 targetPos)
{
// 获取ECS物理世界构建系统
BuildPhysicsWorld buildPhysicsWorld = World.DefaultGameObjectInjectionWorld.GetExistingSystem<BuildPhysicsWorld>();
// 从物理世界获取碰撞检测世界
CollisionWorld collisionWorld = buildPhysicsWorld.PhysicsWorld.CollisionWorld;
// 创建射线输入参数
RaycastInput raycastInput = new RaycastInput
{
Start = originalPos, // 射线起点
End = targetPos, // 射线终点
Filter = CollisionFilter.Default // 使用默认碰撞过滤器(检测所有可碰撞物体)
};
// 创建射线击中结果的容器
Unity.Physics.RaycastHit raycastHit = new Unity.Physics.RaycastHit();
// 执行射线检测
if (collisionWorld.CastRay(raycastInput, out raycastHit))
{
// 射线击中了物体
// 通过刚体索引从物理世界中获取对应的实体
Entity hitEntity = buildPhysicsWorld.PhysicsWorld.Bodies[raycastHit.RigidBodyIndex].Entity;
return hitEntity;
}
else
{
// 射线没有击中任何物体
return Entity.Null;
}
}本文是Unity Dots的学习笔记,基于Unity 2019.4.40f1c1和Entities 0.11.2版本。文章首先对比了传统游戏开发方式与Dots(数据导向技术栈)的差异,指出传统方式在数据冗余、单线程处理和编译器效率上的局限性,而Dots通过ECS(实体组件系统)、JobSystem多线程和Burst编译器优化性能。接着详细介绍了环境搭建步骤,包括安装Entities包。通过一个简单的HelloWorld示例展示了Dots的基本用法,涉及DebugSystem、PrintAuthoring和PrintComponentData的实现。文章还介绍了系统屏蔽、实例化方法以及性能对比,显示ECS在大量物体处理上的优势。最后提到通过JobSystem和Burst编译器进一步优化性能的代码示例。













































1 条评论
果博东方客服开户联系方式【182-8836-2750—】?薇- cxs20250806】
果博东方公司客服电话联系方式【182-8836-2750—】?薇- cxs20250806】
果博东方开户流程【182-8836-2750—】?薇- cxs20250806】
果博东方客服怎么联系【182-8836-2750—】?薇- cxs20250806】