DOTS是Unity在17年左右提出的一个概念,其核心是ECS。
提示:以下是本篇文章正文内容,下面案例可供参考
全称:(Multi-Thread)Data-Oriented-Tech-Stack
(多线程式)数据导向型技术堆栈
Tips:
- jobsystem和ecs是两个不同的东西,但是配合起来使用会有1+1>2的效果
- burst与ecs的高度适配也使得ecs运行效率很高
除非您在寻求短期或中期的性能改进,否则很难判定是否需要过渡到DOTS或何时过渡到DOTS。
DOTS几乎可以为每个应用程序带来一定程度的性能改进。这其中包括性能、电池使用寿命、迭代及项目可扩展性。过渡到DOTS不会造成任何性能的下降,但评估过渡到DOTS所增加的费用却至关重要,尤其是对于那些仅带来较小改进的项目。
对于所有应用程序而言,DOTS适合处理大量数据,例如开放式环境或使用大量相同材料的复杂结构。通过在实例之间共享公共数据以减少内存访问,DOTS也同样适用于重复的元素。
DOTS将来会帮助您开发高质量的内容,而不使用DOTS的Unity却很难做到,这一点务必要考虑清楚。例如,当今的标准游戏和Unity项目已经取代了过去的AAA游戏。放眼未来,您需要采用DOTS来保持竞争力。
针对不同的垂直行业,DOTS可以适用于不同的解决方案:
在改善Unity项目的绩效方面,DOTS有着巨大的潜力。 但是,在使用DOTS时需要做出一些考量,它们会影响到项目的时间表、预算和开发团队。以下是一些需要与项目优先事项进行比较和对比的事项。这些事项可以归类为风险与机遇。
随着时间的推移,晶体管电路逐渐接近性能极限,在摩尔定律逐渐失效的今天,人们面临的数据也呈几何倍数暴增,我们有理由去发明并且学习使用一种效率更高,更能完全发挥硬件性能的软件编程方式,目前看来也许ECS也许能做到。
想要熟悉DOTS以及ECS框架,最好还是要上手做一个小项目,使用部分基础组件,想要熟悉以及精通还需要大量的练习以及使用,开发过程中要配合官方Entities文档使用。
Entities最新版本0.17的官方说明文档
如果是Unity2020.X及以上版本(推荐,作者使用2020.3.26f1c1):
我们准备做一个类似Pac-Man的小游戏,主要熟悉Physics包以及Entities的基本使用,所以不会开发怪物AI之类的,因为使用DOTS开发所以就叫DOTS-MAN好了。
主要功能有:玩家移动,镜头跟随,分数显示,因为如果用ECS来修改UGUI的TEXT可能比较麻烦,这里选择使用HybridECS开发,使用MonoBehaviours开发一些基础功能比如镜头跟随以及物体生产之类。
在开发过程中,因为收集物以及玩家还有地形之类的都要有碰撞,但是ECS无法使用object上面的collider之类的组件,所以就要用Entities包自带的一些脚本。
记得在挂Entities脚本之前删掉不用的Object脚本,避免混淆以及无意义的空间占用
把Object转化成Entity的脚本:
一般配合一起使用的脚本就是PhysicsShape和PhysicsBody,一个控制物理碰撞的类型,一个控制entity的物理性质(例如重力之类的),各个属性的作用都有明确说明:
添加physicsbody之后碰到List越界报错问题解决方案:
go into YOURPROJECTLibrary/PackageCache/
copy com.unity.collections@0.15.0-preview.21 into YOURPROJECT/Packages/
open com.unity.collections@0.15.0-preview.21\Unity.Collections\NativeList.cs
change line 599 from Allocator.None to Allocator.Invalid
组件只有三个,两个存储分别存储移动和旋转的速度,一个负责标记收集物(所以里面没有数据)
要记得把Serializable属性改为GenerateAuthoringComponent,这样把component挂上object之后就会把他变成entity。
创建component和system都可以直接使用右键 -> create -> ECS进行快速选择自带模板
using Unity.Entities;
[GenerateAuthoringComponent]
public struct MoveComponent : IComponentData
{
public float moveSpeed;
}
using Unity.Entities;
[GenerateAuthoringComponent]
public struct RotationComponent : IComponentData
{
public float rotateSpeed;
}
要注意在脚本中配置Collision Filter相关以及Collision Response相关,即某个entity属于哪个标签,他能与其他哪些标签的entity发生碰撞
搭建一个使用场景(renderer相关的根据自己喜好来整):
因为mono和ECS是相互穿插的,所以如果mono中有需要的system可以直接先去看看system的代码,配合官方文档理解为何这么做,这样才能把整个流程梳理清楚(至少我学习的时候是这样的)
这里需要一个全局的mono behaviour来控制游戏,例如entity与object的连接,这里我们换一种方式,把之前的玩家小球弄成prefab,然后在这个全局mono控制玩家的生成,起名就叫做GameManager吧(具体说明看注释):
using System.Collections;
using UnityEngine;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Physics;
using UnityEngine.UI;
using Unity.Transforms;
public class GameManager : MonoBehaviour
{
public static GameManager instance;
public bool insaneMode;
//在实体object世界中的prefab
public GameObject ballPrefab;
public GameObject cubePrefab;
public Text scoreText;
public int maxScore;
public int cubesPerFrame;
public float cubeSpeed = 3f;
private int curScore;
private Entity ballEntityPrefab;
private Entity cubeEntityPrefab;
private EntityManager entityManager;
private BlobAssetStore blobAssetStore;
//private bool insaneMode;
private void Awake()
{
if (instance != null && instance != this)
{
Destroy(gameObject);
return;
}
instance = this;
//初始化EntityManager
entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
blobAssetStore = new BlobAssetStore();
//从object世界获得setting
//即inspector中可以获取的prefab
GameObjectConversionSettings settings = GameObjectConversionSettings.FromWorld(World.DefaultGameObjectInjectionWorld, blobAssetStore);
//通过GameObjectConversionUtility的ConvertGameObjectHierarchy来把object变成entity
//参数(GameObject root, World dstEntityWorld)
ballEntityPrefab = GameObjectConversionUtility.ConvertGameObjectHierarchy(ballPrefab, settings);
cubeEntityPrefab = GameObjectConversionUtility.ConvertGameObjectHierarchy(cubePrefab, settings);
}
private void OnDestroy()
{
//重置BlobAssetStore中的blobasset缓存,释放清空blobAssetStore
blobAssetStore.Dispose();
}
private void Start()
{
curScore = 0;
insaneMode = false;
//显示分数,这个函数在每一帧都会调用
DisplayScore();
//创建初始球球
SpawnBall();
}
private void Update()
{
//如果符合条件就开启insanemode疯狂造方块,这里改成手动开启
//if (!insaneMode && curScore >= maxScore)
if (insaneMode)
{
//开启协程造方块
//insaneMode = true;
StartCoroutine(SpawnLotsOfCubes());
}
}
//回调,造方块
IEnumerator SpawnLotsOfCubes()
{
while (insaneMode)
{
//每一帧造cubesPerFrame量的方块
for (int i = 0; i < cubesPerFrame; i++)
{
SpawnNewCube();
}
yield return null;
}
}
void SpawnNewCube()
{
//使用entityManager造方块并且给予属性
Entity newCubeEntity = entityManager.Instantiate(cubeEntityPrefab);
Vector3 direction = Vector3.up;
Vector3 speed = direction * cubeSpeed;
PhysicsVelocity velocity = new PhysicsVelocity()
{
Linear = speed,
Angular = float3.zero
};
//最后记得往entity添加component数据
entityManager.AddComponentData(newCubeEntity, velocity);
}
public void IncreaseScore()
{
curScore++;
DisplayScore();
}
private void DisplayScore()
{
scoreText.text = "Score: " + curScore;
}
//造第一个球
void SpawnBall()
{
Entity newBallEntity = entityManager.Instantiate(ballEntityPrefab);
Translation ballTrans = new Translation
{
//初始位置
Value = new float3(0f, 0.5f, 0f)
};
//还是要记得添加component
entityManager.AddComponentData(newBallEntity, ballTrans);
//设置镜头跟随的对象
CameraFollow.instance.ballEntity = newBallEntity;
}
}
using Unity.Entities;
using Unity.Transforms;
using Unity.Mathematics;
using UnityEngine;
public class CameraFollow : MonoBehaviour
{
public static CameraFollow instance;
public Entity ballEntity;
//设置一个偏移量用来调整相机位置
public float3 offset;
private EntityManager manager;
private void Awake()
{
if (instance != null && instance != this)
{
Destroy(gameObject);
return;
}
instance = this;
manager = World.DefaultGameObjectInjectionWorld.EntityManager;
}
private void LateUpdate()
{
if (ballEntity == null) { return; }
Translation ballPos = manager.GetComponentData<Translation>(ballEntity);
transform.position = ballPos.Value + offset;
}
}
记得把相机脚本挂到main camera上!
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Physics;
using UnityEngine;
public class MoveSystem : SystemBase
{
protected override void OnUpdate()
{
float deltaTime = Time.DeltaTime;
float2 curInput = new float2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"));
Entities.ForEach((ref PhysicsVelocity vel, ref MoveComponent speedData) =>
{
float2 newVel = vel.Linear.xz;
newVel += curInput * speedData.moveSpeed * deltaTime;
vel.Linear.xz = newVel;
}).Run();
}
}
相关要点:
ForEach就是对包含参数相关Component的entity在每一帧都进行一定的操作,其中ref关键字表示对数据进行读取也可以修改,而in关键字表示对数据只读,而且in一定要全部放在ref后面。
后面的的.Run()表示在主线程中运行,如果要在子线程可以使用Schedule。
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
public class RotateSystem : SystemBase
{
protected override void OnUpdate()
{
float deltaTime = Time.DeltaTime;
Entities.ForEach((ref Rotation rotation, in RotationComponent rotationSpeed) =>
{
rotation.Value = math.mul(rotation.Value, quaternion.RotateX(math.radians(rotationSpeed.rotateSpeed * deltaTime)));
rotation.Value = math.mul(rotation.Value, quaternion.RotateY(math.radians(rotationSpeed.rotateSpeed * deltaTime)));
rotation.Value = math.mul(rotation.Value, quaternion.RotateZ(math.radians(rotationSpeed.rotateSpeed * deltaTime)));
}).Run();
}
}
记得这时候往你的object上面挂component!如果想让玩家移动就挂movecomponent,让收集物旋转就挂上rotationcomponent。可以想一想,如果你往收集物上挂了movecomponent会发生什么?为什么会这样?
这时候你的收集物应该是旋转的,玩家小球可以通过wasd或者方向键控制移动:
3. CollectSystem:
然后就是最难的碰撞收集系统了!本来在mono中两三行就可以解决的问题,现在要写几十行才能解决!但是对于后期优化以及性能上的提升,这些困难都不算什么!
相关的解释说明都在注释中了:
using Unity.Entities;
using Unity.Collections;
using Unity.Physics;
using Unity.Physics.Systems;
[UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
public class CollectSystem : SystemBase
{
//用bufferSystem来处理这些碰撞事件
private EndFixedStepSimulationEntityCommandBufferSystem bufferSystem;
//初始化entity的物理世界
private BuildPhysicsWorld buildPhysicsWorld;
private StepPhysicsWorld stepPhysicsWorld;
protected override void OnCreate()
{
bufferSystem = World.GetOrCreateSystem<EndFixedStepSimulationEntityCommandBufferSystem>();
buildPhysicsWorld = World.GetOrCreateSystem<BuildPhysicsWorld>();
stepPhysicsWorld = World.GetOrCreateSystem<StepPhysicsWorld>();
}
protected override void OnUpdate()
{
//每一帧都添加一个triggerjob来进行碰撞判断,因为需要判断的是有MoveComponent的玩家
//以及有DeleteTag的收集物,所以就要在job中进行选择
Dependency = new TriggerJob
{
speedEntities = GetComponentDataFromEntity<MoveComponent>(),
entitiesToDelete = GetComponentDataFromEntity<DeleteTag>(),
commandBuffer = bufferSystem.CreateCommandBuffer(),
}.Schedule(stepPhysicsWorld.Simulation, ref buildPhysicsWorld.PhysicsWorld, Dependency);
//把job传递到buffer中
bufferSystem.AddJobHandleForProducer(Dependency);
}
//创建一个triggerjob来进行碰撞处理
private struct TriggerJob : ITriggerEventsJob
{
//初始化处理的entity
public ComponentDataFromEntity<MoveComponent> speedEntities;
[ReadOnly] public ComponentDataFromEntity<DeleteTag> entitiesToDelete;
public EntityCommandBuffer commandBuffer;
public void Execute(TriggerEvent triggerEvent)
{
TestEntityTrigger(triggerEvent.EntityA, triggerEvent.EntityB);
TestEntityTrigger(triggerEvent.EntityB, triggerEvent.EntityA);
}
//处理碰撞,如果被碰撞的物品没有DeleteTag,就把DeleteTag挂上去,移除它的物理组件
private void TestEntityTrigger(Entity entity1, Entity entity2)
{
if (speedEntities.HasComponent(entity1))
{
if (entitiesToDelete.HasComponent(entity2)) { return; }
commandBuffer.AddComponent<DeleteTag>(entity2);
commandBuffer.RemoveComponent<PhysicsCollider>(entity2);
}
}
}
}
using Unity.Entities;
[UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
//这里添加一个属性,就是在collectionsystem发生之后再更新,因为要先碰撞之后再进行处理
[UpdateAfter(typeof(CollectSystem))]
public class DeleteSystem : SystemBase
{
private EndFixedStepSimulationEntityCommandBufferSystem _endSimulationECBSystem;
protected override void OnStartRunning()
{
_endSimulationECBSystem = World.GetOrCreateSystem<EndFixedStepSimulationEntityCommandBufferSystem>();
}
protected override void OnUpdate()
{
var ecb = _endSimulationECBSystem.CreateCommandBuffer();
Entities
.WithAll<DeleteTag>()
.WithoutBurst()
.ForEach((Entity entity) =>
{
//修改分数
GameManager.instance.IncreaseScore();
ecb.DestroyEntity(entity);
}).Run();
_endSimulationECBSystem.AddJobHandleForProducer(Dependency);
}
}
这里ForEach之前有一系列限定条件,比如.WithAll()的意思就是对带有deletetag的entity执行下面的操作,这样能更加方便的进行处理,所以大部分情况下entity都会被打一个标签来区别其他entity
太棒啦!你成功的使用了目前领先的开发模式开发了一个小游戏,虽然这个小游戏的功能在mono中实现的话可以很简单的实现,但是随着工程规模的扩大以及性能需求的提高,ECS只会愈发强大!因为目前DOTS相关教程不完善,所以如果在上述开发中碰到问题主要需要参考官方文档以及一些论坛大牛的解答,想要更深入的理解还需要更多项目的磨练。
Entities最新版本0.17的官方说明文档:
https://docs.unity3d.com/Packages/com.unity.entities@0.17/api/Unity.Entities.html
油管ECS大神Turbo的说明文档:
https://www.tmg.dev/tuts/roll-a-ball-entities-0-17/