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和传统方式对比

为何传统方式无法创建大量物体:

  1. 数据冗余

例如实现一个移动功能,我们需要继承Monobehaviour,但我们只需要使用transfrom就足够,但Monobehaviour包含很多属性和方法,很多不用的东西也要被带到内存

  1. 单线程处理
  2. 编译器问题

现有编译器无法把C#编译成一个很高效的机器码

Dots

(Data Oriented Technology Stack)数据导向型技术栈

  1. ECS(Entity Component System):源自守望先锋,数据和行为分离,且数据精确、聚集
  2. JobSystem :多线程,充分发挥多核心Cpu特点
  3. Burst编译器:编译生成高效的代码

环境搭建(2019.4.40f1c1)

  1. 安装package

Entities

image

显示的包

image

HelloWorld

image

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

  1. ECS

image

  1. 传统

image

质的飞跃

二、CURD(World、Entity、Component)

SikiDots入门脑图

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);
#endregion

Group

InitializationSystemGroup->SimulationSystemGroup->PresentationSystemGroup顺序执行


 [UpdateInGroup(typeof(InitializationSystemGroup))]
 public class DebugSystem : ComponentSystem

转换Entity的方式

1.使用

image

可以同时添加

image

//绑定,执行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);
  1. SubScene

image

image

image

image

从0创建Entity

Chunk和ArcheType

  1. 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

  1. 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));
#endregio

SharedComponent

image

相同值的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的相等判断

image

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

当实体被销毁时

  1. 该组件不会销毁,从而得到组件状态,彻底销毁需要RemoveComponet(EntityManager)
  2. 可以通过查询实体查找
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();

image

解开注释,实体被销毁

entityManager.RemoveComponent<StateComponent>(query);

image

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}");
        }
    }
}

image

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");
    }
}

image

筛选相关修饰方法基本使用

以下均可同时使用,注意同时使用不要过分复杂,类似于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();

image

image

WithAny<>

筛选包含任意其中任意一个组件的实体执行

WithNone<>

筛选不包含其中任意一个组件的实体执行

WithChangeFilter<>

只会对 对应组件发生变化的实体执行

  • 对应筛选的组件必须要在Foreach中添加
 Entities.ForEach((ref Translation translation,ref PrintComponentData printComponentData) =>
            {
                Debug.Log($"Translation:{translation.Value}");
            })
            .WithChangeFilter<PrintComponentData>()
            .Run();

image

仅打印一次,因为只有第一次赋值的时候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");

}

image

WIthEntitiyQueryOptions<>

筛选属于对应Group的实体

EntityQueryOptions 介绍

以下为AI生成

  1. Default (默认)
// 作用:默认行为
// - 不包含禁用的实体
// - 不包含预制体实体
// - 考虑组件启用状态
// - 不过滤写入组
EntityQueryOptions.Default
  1. IncludeDisabled
// 作用:包含被禁用的实体
// 场景:需要处理所有实体,无论是否启用
// 示例:调试、编辑器工具、状态恢复
EntityQueryOptions.IncludeDisabled
  1. IncludePrefab
// 作用:包含预制体实体
// 场景:编辑器模式、预制体处理、初始化系统
// 注意:运行时通常不需要处理预制体
EntityQueryOptions.IncludePrefab
  1. 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>()
  1. IgnoreComponentEnabledState
// 作用:忽略 IEnableableComponent 的启用状态
// 场景:需要处理所有组件,无论组件是否启用
// 性能:避免额外的启用状态检查
EntityQueryOptions.IgnoreComponentEnabledState
  1. 可以多个一起使用
// 单个选项
.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去执行打印

image

可以使用WithName修改在Profiler的显示名称

.WithName("ColdPlay")
.Schedule()

image

ScheduleParallel

异步多线程执行

image

image

WithBurst

使用Burst编译器(默认使用),如不适用需要写.WithoutBurst()

image

注意!!!

在多线程中尽量不使用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();
;

直接报错,这种写法只能支持单线程

image

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()
            ;

image

正确示例

方式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()
            ;

image

方式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

image

🧩 Unity DOTS 系统基类对比表

特性ComponentSystemJobComponentSystemSystemBase
引入版本Unity 2018.3 (旧版ECS)Unity 2019.1 (过渡方案)Unity 2020.1+ (现代ECS)
当前状态❌ 已过时 ([Obsolete]​)❌ 已过时 ([Obsolete]​)官方推荐
线程模型🔁 主线程运行⚡ 支持 Job 多线程并行⚡ 原生深度并行支持
依赖管理🚫 无自动依赖管理⚙️ 手动管理 JobHandle​ 依赖链🤖 自动管理 (Dependency​ 属性)
Burst 支持⚠️ 有限支持⚠️ 需手动配置✅ 原生深度集成
编码复杂度░░░░░ 低 (简单逻辑)░░░░░░░░░ 高 (需理解 Job 系统)░░░░░░ 中 (平衡简洁与灵活)
典型代码结构Entities.ForEach​ (主线程)需实现 IJobChunkEntities.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​ 代码生成
    • 混合模式:可通过 Run()​ 切回主线程

IJob和IJobParallelFor

IJob

使用多线程和不使用多线程的对比:

  • 使用多线程

image

  • 不使用多线程

image

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)会使你在线程之间进行更均匀的工作分配。但是,它会带来一些开销
因此在某些情况下,稍微增加批次数量会更好。
  • 传统

image

  • IJobParallelFor

    1. 不使用Burst

      image​​​

    2. 使用Burst

      image

​​

AI总结

🔧 IJobParallelFor​ 工作类型核心特性总结

  • 设计目的

    • 专为在大量物体上执行相同操作的高并发场景设计
    • 适用于游戏开发中常见的批量数据处理需求(如:同时更新 10,000+ 个物体的位置)
  • 调度机制

    • 智能任务分割

      • 不为每个项目单独创建 Job
      • 根据 CPU 核心数量动态分配(每个核心最多分配一个 Job)
      • 自动重新分配工作负载以平衡线程压力
    • 批处理控制

      • 需手动指定待处理数组的总长度(arrayLength​)
      • 需明确设置批次数量(batchCount​)
  • 批处理参数优化

    • 低批次数量(如 1)

      • ✅ 优点:线程间工作分配更均匀
      • ❌ 缺点:调度开销增大,可能降低整体性能
    • 较高批次数量(如 32~64)

      • ✅ 优点:减少调度开销,提升吞吐量
      • ❌ 缺点:可能导致线程负载不均衡
    • 黄金法则

      • 简单操作 → 使用较大批次(减少开销)
      • 复杂操作 → 使用较小批次(提升并行度)

四、实践

一万个小球下落

物理效果

使用刚体,安装Unity Physic包

image

安装后记得重启

此时给cube加上Rigidbody小球可以转换为实体后正常受到重力(不安装这个包无法下落)

image

注意此时如果需要发生碰撞,需要两者都是Entity

产生弹性

可使用物理材质

Physic Body(Ecs的刚体)

该组件必须要配合Convet To Entity脚本使用,除此之外还有ECS的碰撞体(Physic Body可以搭配传统的Collider使用)

image

一万个小球自由下落

  • 注意打开Burst编译
  • 可以使用Gpu Instancing
  • image

代码:

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();
        }
    }
}

运行

image

image

鱼群移动

  1. 分隔规则:尽量避免与伙伴过于拥挤
  2. 对准规则:尽量与临近伙伴的平均方向一致
  3. 内聚规则:尽量朝附近伙伴的中心移动

单线程

image

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();

    }
}

image

使用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()

image

性能提升及其显著!!!

添加鼠标点击事件

需要使用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;
            }
        }

image

本文是Unity Dots的学习笔记,基于Unity 2019.4.40f1c1和Entities 0.11.2版本。文章首先对比了传统游戏开发方式与Dots(数据导向技术栈)的差异,指出传统方式在数据冗余、单线程处理和编译器效率上的局限性,而Dots通过ECS(实体组件系统)、JobSystem多线程和Burst编译器优化性能。接着详细介绍了环境搭建步骤,包括安装Entities包。通过一个简单的HelloWorld示例展示了Dots的基本用法,涉及DebugSystem、PrintAuthoring和PrintComponentData的实现。文章还介绍了系统屏蔽、实例化方法以及性能对比,显示ECS在大量物体处理上的优势。最后提到通过JobSystem和Burst编译器进一步优化性能的代码示例。

最后修改:2025 年 07 月 18 日
如果觉得我的文章对你有用,请随意赞赏