前几天DOTS1.0的预览版已经发布,我第一时间就进行了学习,Dots1.0的更新内容我认为非常震撼,有必要写一篇文章详细说明一下。
Dots1.0更新的包如下
com.unity.logging (异步日志方案,比Debug.log同步日志方案速度更快)
开始之前需要升级一下Unity的版本,需要Unity 2022.2.0b8或者以上版本,除了com.unity.logging以外其他包都是Dots核心的package不再赘述。

1.调试部分

Dots从2018年问世到现在已经4年时间了,阻碍Dots发展的大家普遍的共识是:“开发方式对机器友好,对人不友好”。传统GameObject的开发流程中是对人友好的,但对机器不友好。Dots是则对机器友好,对人不友好!这样的开发模式人就不容易理解和看懂了。显然需要在人和机器中找一个平衡点,Dots1.0这次是带着诚意来的。

调试面板

    
    
Windows -> Entities -> Hierarchy (Dots层次面板视图) -> Components(组件视图,展示所有Dots组件信息) -> Systems(系统视图,展示所有系统) -> Archetypes(展示当前Archetype的分部) -> Journaling(可录制一段时间记录Entities的变化过程)

Entities Hierarchy(实体层次面板视图)

在实体层次面板视图中可看到最终运行时实体的排布信息。
如下图所示,在实体检测面板中可看到所有的实体组件信息,还可以单独展开查看,具体的组件中的数值。
在Relationships中还可以看到当前实体被那些系统所关注,展开后还可以看到具体Query的组件,以及读写状态。
以上创作流程已经和传统的GameObject方式类似了。

创作模式与运行模式

新版Dots提出了创作模式与运行模式的概念,之前的方式在关闭运行游戏以后所有的改动就会自动还原。当处于创作模式下,无论游戏运行中都是可以修改并且保存的。这就能解决有些编辑工作需要依赖游戏运行后,以前由于引擎限制了无法解决此类问题,所有修改都必须在非运行模式下。
创作模式(点击橘黄色的小圆圈切换)
运行模式
混合模式(表示可同时创作模式和运行模式)
创作模式中在运行游戏状态中也可以直接修改具体数值,然后点击保存即可。
组件中属性前面如果有橘黄色竖杠 “|”表示属于运行模式数据,关闭游戏数据会还原。没有则表示它是创作模式下的数据,运行时不会进行修改它。

组件

这里包含了所有组件,可以搜索反向找到它与那些实体之间的有关联

系统

这里列出了当前所有的系统,可以随时的关闭与启动某个系统,方便快速定位问题。

Archetypes

具体每个Archetypes所使用的组件分部以及精准的内存占用
通过这一组调试面板可以帮助开发者快速定位问题。

2.代码部分

这次代码部分的更新在使用侧这边并不是特别大,API的设计也围绕着使用者更加方便而展开,下面让我们来盘一盘有哪些改变。

Baker 全新烘焙管道

Conversion的替代者,用于将创作端的GameObject对象转换成实体对象。主要原因是Dots1.0支持了创作模式与运行模式,随时都可以对数据进行修改。看文档的意思以前都是运行时进行GameObject到Entity的转换,这导致编辑模式下需要额外维护创作的数据。通过新的Baker API可以得到更快的速度以及更稳健的性能。
从使用的角度来说就是换个API名字,主要还是把Monobehaviour上的数据转成实体组件。
    
    
public class SpawnerAuthoring : MonoBehaviour { public GameObject Prefab ; class Baker : Baker < SpawnerAuthoring > { public override void Bake ( SpawnerAuthoring authoring ) { AddComponent ( new Spawner { Prefab = GetEntity ( authoring . Prefab ) } ) ; } } } struct Spawner : IComponentData { public Entity Prefab ; }

Aspect 包装器

Aspect可以将多个实体组件包装在一个结构体中。如原本在遍历组件的时候需要在OnUpdate中写一些获取组件以及对应执行的方法,如果有多个系统或者代码公用就可以写在包装器中,代码写起来会更加方便。
    
    
readonly partial struct VerticalMovementAspect : IAspect { readonly RefRW < LocalToWorldTransform > m_Transform ; readonly RefRO < RotationSpeed > m_Speed ; public void Move ( double elapsedTime ) { m_Transform . ValueRW . Value . Position . y = ( float ) math . sin ( elapsedTime * m_Speed . ValueRO . RadiansPerSecond ) ; } }
然后在OnUpdate或者JobUpdate中可以直接去Query这个Aspect,并且执行对应的方法。
    
    
public void OnUpdate ( ref SystemState state ) { foreach ( var movement in SystemAPI . Query < VerticalMovementAspect > ( ) ) { movement . Move ( elapsedTime ) ; } }
上面的例子看似比较简单大家可以看看这两个内置的TransformAspect.cs和RigidBodyAspect.cs就明白为什么引擎会提供Aspect了。
在调试面板中也可以看到Aspects的数据结构

可启动/关闭组件

一个小小的启动或者关闭组件在DOTS中可以说是很复杂的操作,因为它会改变Archetypes的内存结构,还会影响Query非常影响性能。新版Dots引用了IEnableableComponent接口,配合SetComponentEnabled方法动态开关组件,这是一个底层的操作,号称比普通的删除添加组件要快9倍左右。
    
    
public struct TargetEnableable : IComponentData , IEnableableComponent { public float3 target ; } // ... var archetype = m_Manager . CreateArchetype ( typeof ( EcsTestData ) , typeof ( TargetEnableable ) ) ; var entities = m_Manager . CreateEntity ( archetype , 1024 , World . UpdateAllocator . ToAllocator ) ; EntityQuery query = GetEntityQuery ( typeof ( TargetEnableable ) ) ; //关闭第数组中下标4的实体组件TargetEnableable m_Manager . SetComponentEnabled < TargetEnableable > ( entities [ 4 ] , false ) ; int fooCount = query . CalculateEntityCount ( ) ; // 关闭了一个所以这里返回 1023

ISystem

进一步优化性能Dots提供了ISystem接口,让System系统中的代码也具备Burst编译的优化可能,使用HCP而且不需要垃圾回收,性能直接起飞。
    
    
[ BurstCompile ] public partial struct RotationSpeedSystem : ISystem { [ BurstCompile ] public void OnCreate ( ref SystemState state ) { } [ BurstCompile ] public void OnDestroy ( ref SystemState state ) { } [ BurstCompile ] public void OnUpdate ( ref SystemState state ) { var job = new RotationSpeedJob { deltaTime = SystemAPI . Time . DeltaTime } ; job . ScheduleParallel ( ) ; } }

IJobEntity

原本的Entities.ForEach已经不推荐使用,它虽然简单方便但在一些复杂的环境下不好用,比如无法在ForEach中实现嵌套,无法在多个System中共同操作,取而代之的则是IJobEntity。用法非常简单在Execute中传入需要的实体组件即可。
    
    
partial struct RotationSpeedJob : IJobEntity { public float deltaTime ; void Execute ( ref LocalToWorldTransform transform , in RotationSpeed speed ) { transform . Value = transform . Value . RotateY ( speed . RadiansPerSecond * deltaTime ) ; } }
在任意位置也可以对当实体组件进行循环遍历。
    
    
foreach ( var ( rotateAspect , speedModifierRef ) in SystemAPI . Query < RotateAspect , RefRO < SpeedModifier > > ( ) ) rotateAspect . Rotate ( time , speedModifierRef . ValueRO . Value ) ;

3.性能调试部分

在Profiler中提供了Entity性能调试的窗口,如下图中可看到
CreateEntity AddComponent Set ShareCommponent RemoveComponet Destory Entity所有的耗时
在Entity Memory中也可以看到某帧分配的Archetype的内存分部情况,快速定位内存瓶颈。
启动日志收集以后可以看到每一步对实体组件操作的行为,这个功能在调试的时候有很大作用。比如一个数值不知道为什么变成0.5,可以通过搜索功能定位出修改到0.5的这个日志看看它的行为即可快速定位问题。

4.渲染部分

渲染部分的更新也是我认为非常震撼的部分,因为之前Dots之所以使用不起来主要就是渲染部分无法得到很好的支持,它对HDRP和URP都做了很好的支持,以下我会以URP的视角来进行说明。

粒子特效

正常的使用粒子特效,最终放到SubScene中就可以直接支持,不需要使用特殊材质。
光源、反射探头、Sprite精灵、文本、后处理也都支持了。

烘焙贴图

正常烘焙场景,使用SubScene就可以直接支持场景烘焙贴图。

蒙皮动画

蒙皮动画我觉得暂时属于半支持状态,Animation动画文件的解析需要自己处理,关键帧控制的节点变换信息需要自己传入。它的原理是这样的,首先会把骨骼节点转成Entity对象,对应SkinMeshRender有一个实体组件,接着就是GPU蒙皮动画了,它需要把骨骼变换后的位置传入GPU中播放蒙皮动画。
    
    
internal class AnimateRotationAuthoring : MonoBehaviour { [ Tooltip ( "Local start rotation in degrees" ) ] public Vector3 FromRotation = new Vector3 ( 0f , 0f , - 10f ) ; [ Tooltip ( "Local target rotation in degrees" ) ] public Vector3 ToRotation = new Vector3 ( 0f , 0f , 10f ) ; [ Tooltip ( "Time in seconds to complete a single loop of the animation" ) ] public float Phase = 5f ; [ Range ( 0f , 1f ) ] [ Tooltip ( "Phase shift as a percentage (0 to 1.0)" ) ] public float Offset ; }
然后在System中修改实体组件的变换,对应的还有旋转、缩放、平移。
    
    
partial class AnimateRotationSystem : SystemBase { const float k_Two_Pi = 2f * math . PI ; protected override void OnUpdate ( ) { var t = ( float ) SystemAPI . Time . ElapsedTime ; Entities . ForEach ( ( ref Rotation rotation , in AnimateRotation data ) => { var normalizedTime = 0.5f * ( math . sin ( k_Two_Pi * data . Frequency * ( t + data . PhaseShift ) ) + 1f ) ; rotation . Value = math . slerp ( data . From , data . To , normalizedTime ) ; } ) . ScheduleParallel ( ) ; } }
注意此时只是修改了实体组件的变化,真正影响到蒙皮动画则是下面这几个系统,与兴趣的朋友可以跟一下代码其实传统的GPU蒙皮的那一套。

BatchRendererGroup

其实早在2019年我就研究过这个API,当时它就是一个高级的GPU Instancing。BatchRendererGroup现在终于支持SrpBatcher了,在这之前Srp Batcher无法通过代码来绘制必须是GameObject的方式,所以它是跨时代的API。
    
    
m_BatchRendererGroup = new BatchRendererGroup ( this . OnPerformCulling , IntPtr . Zero ) ; //... // Batch metadata buffer int objectToWorldID = Shader . PropertyToID ( "unity_ObjectToWorld" ) ; int worldToObjectID = Shader . PropertyToID ( "unity_WorldToObject" ) ; int colorID = Shader . PropertyToID ( "_BaseColor" ) ; var batchMetadata = new NativeArray < MetadataValue > ( 2 , Allocator . Temp , NativeArrayOptions . UninitializedMemory ) ; batchMetadata [ 0 ] = CreateMetadataValue ( objectToWorldID , 0 , true ) ; batchMetadata [ 1 ] = CreateMetadataValue ( colorID , itemCount * UnsafeUtility . SizeOf < Vector4 > ( ) * 3 , true ) ; //... m_batchID = m_BatchRendererGroup . AddBatch ( batchMetadata , m_GPUPersistanceBufferHandle ) ;
DOTS1.0还支持了物理以及NetCode等新功能,在我心里我认为最重要的还是对渲染支持的这一块。

参考

官方说明文档:[https://forum.unity.com/threads/experimental-entities-1-0-is-available.1341065/ ](https://forum.unity.com/threads/experimental-entities-1-0-is-available.1341065/ )
切到1.0分支,每个都可以单独打开,例子非常全面(我暂时使用版本是unity2022.2.0b9)
尤其是渲染这块的支持
Logo

分享前沿Unity技术干货和开发经验,精彩的Unity活动和社区相关信息

更多推荐