找回密码
 立即注册
查看: 597|回复: 0

Unity用DOTS实现爆炸效果

[复制链接]
发表于 2020-11-23 18:45 | 显示全部楼层 |阅读模式
想要爆炸啊。
看了KILL la KILL后突然很想做爆炸效果。
所以来unity试试。


成品是下面这种效果。
无限点击的话真特别有快感,果然爆炸就是爽


先说说DOTS

在讲制作过程前,先提一下DOTS是什么。
官网的话是这样:
借助Unity的新型高性能、多线程面向数据的技术堆栈(DOTS),您将能够充分利用多核处理器的优势。
DOTS让您能够创建更丰富的用户体验,并使用更易读和能够在其他项目中重用的C#代码进行更快的迭代。

什么叫技术栈呢?就是一系列技术,换句话说,DOTS是由 Entities、Job System、Burst等一系列代码包组成的。
这些玩意一起完成一个目标——提高游戏性能。
DOTS为什么能提高性能我们不做深究。有计算机基础的读者,看到SIMD,内存排列和并行化多少就能猜到大概。
感兴趣的话可以看看这个:
基于 Game Object Conversion 和 SubScene 的 DOTS 开发工作流


除了提高性能外,DOTS第二个目标是易用。
这才是关键啊。
易用性。
unity牛逼。


DOTS支持将unity原本的gamobject对象转成DOTS里的对象,也就是实体(Entity)。这个实体(Entity)可以看做一个只有数据的游戏对象。
很多Unity原生对象都可以自动转换。比如Rigidbody和MeshCollider。
自动转换就意味着在使用原生组件的情况下,我们可以很简单的使用DOTS,甚至不需要写代码。


前提准备

在Windows package里下载好这几个包。注意那个HybridRenderer,没有的话转换后的实体不会显示。如果你的实体没有显示,那很有可能是因为没装这个包。
在菜单把FullStackTrace打开,方便查看错误。
开始吧

1 搭建场景
搭建一个这样的简单场景
Cube的BoxCllider改成MeshCollider,并选上Convex。
不使用BoxCollider因为在使用BoxCollider时会Burst的bug,原因我暂时不知道。
2 创建实体
然后创建一个C#脚本,叫ExplodeManager。
初始化时,创建EntityManager。并在OnDestroy时,释放blobAssetStore。blobAssetStore是一个提供缓存的类,缓存能让你对象创建时更快。
  1. void Start()
  2.     {
  3.         _manager = World.DefaultGameObjectInjectionWorld.EntityManager;
  4.         _blobAssetStore = new BlobAssetStore();
  5.         _settings = GameObjectConversionSettings.FromWorld(World.DefaultGameObjectInjectionWorld,  _blobAssetStore);
  6.     }
  7.     private void OnDestroy()
  8.     {
  9.         _blobAssetStore.Dispose();
  10.     }
复制代码
第二步就是创建多个实体了。核心为两步:


    转换原本的GameObject为实体
2. 用NativeArray大量创建实体
  1. public void Explode(GameObject target){
  2.         ...省略1 初始化各种参数...
  3.         var targetECS = GameObjectConversionUtility.ConvertGameObjectHierarchy(target,_settings);
  4.         //创建数组
  5.         NativeArray<Entity> many = new NativeArray<Entity>(amount,Allocator.Temp);
  6.         _manager.Instantiate(targetECS, many);
  7.         ...省略2 给每个实体配置位置和速度...
  8.         _manager.DestroyEntity(targetECS);
  9.         //销毁数组
  10.         many.Dispose();
  11. }
复制代码
在上面代码中,省略2中,用SetComponentData方法,给实体配置位置和速度。具体的位置和速度计算可以按你自己的想法任意操作。
如果你不知道有哪些ComponentData的话,可以看EntityDebugger,或者查阅文档。
  1. _manager.SetComponentData(entity,new Translation{Value = pos});
  2. _manager.SetComponentData(entity,new PhysicsVelocity{Linear = velocity});
复制代码
第一个Tip,实体的Scale不好改,所以最好在初始化这就改变原型的Scale。
  1. public void Explode(GameObject target){
  2.         ...省略 初始化各种参数...
  3.         //实体的Scale不好改,所以最好在初始化这就改变原型的scale
  4.         var old = target.transform.localScale;
  5.         target.transform.localScale = old*cellSize;
  6.         var targetECS = GameObjectConversionUtility.ConvertGameObjectHierarchy(target,_settings);
  7.         target.transform.localScale = old;
  8.         ... 其他代码
  9. }
复制代码
第二个Tip,在数组前做数量校验,否则你的电脑可能会卡死。写完代码,我开心的设了100x100x100个实体,电脑就爆炸了。
  1. //简单的校验,但很关键
  2. if (amount > 5000)
  3. {
  4.     Debug.LogError("To Many Piece !!! limit is 5000");
  5.     return;
  6. }
复制代码
这样就OK啦,一个产生Entity的方法就制作出来了。


3 用Data 和 JobSystem控制实体


但现在有个问题。
这样子创建的实体,是不能用Destroy销毁的。
为此,需要写一个JobSystem,这也是DOTS另一个重要的核心了。


之前提到Entity只是数据,但对游戏来说,仅有数据是不够的。而JobSystem就是用来操作数据的。


首先我们创造数据,创建LifeTimeToDestroyData.cs
  1. public struct LifeTimeToDestroyData : IComponentData
  2. {
  3.    public float restTime;
  4. }
复制代码
然后创建LifeTimeToDestroySystem.cs文件
  1. [AlwaysSynchronizeSystem]
  2. public class LifeTimeToDestroySystem : JobComponentSystem
  3. {
  4.     protected override JobHandle OnUpdate(JobHandle inputDeps)
  5.     {
  6.         var ecb = new EntityCommandBuffer(Allocator.TempJob);
  7.         Entities.WithoutBurst().ForEach((Entity entity, ref LifeTimeToDestroyData life) =>
  8.         {
  9.             life.restTime -= Time.DeltaTime;
  10.             if (life.restTime < 0)
  11.             {
  12.                 ecb.DestroyEntity(entity);
  13.             }
  14.         }).Run();
  15.         ecb.Playback(EntityManager);
  16.         ecb.Dispose();
  17.         return default;
  18.     }
  19. }
复制代码
注意那个ForEach里面的参数。
系统会根据你的参数,自动找到符合条件的Entity去执行内部的代码。对于我们来说,就是找到包含LifeTimeToDestroyData的实体。
这里销毁实体必须使用EntityCommandBuffer,它将指令缓存并批量执行,提高了性能。


然后,在之前配置实体速度和位置的地方,加上
  1. _manager.AddComponent<LifeTimeToDestroyData>(entity);
  2. _manager.SetComponentData(entity,new LifeTimeToDestroyData {restTime = time);
复制代码
就大功告成啦。
效果

产生1000个方块实体
产生1000个实体时的CPU情况
产生10000+实体
说实话,不停点击爆炸按钮那感觉确实爽。
DOTS与面向对象

说回DOTS,在开始用时踩了一些坑。包不匹配啊,没有装Hybrid Renderer啊,各种问题。但整体来说解决的都很快,包依赖管理真是造福程序员的伟大发明。
JOBSystem那种编程方法对于程序员来说应该还是蛮有趣的。第一次看到ForEach那块的感觉就像我刚学JAVA时在Spring里看到IOC和AOP的感觉一样。
就看到那代码感觉很爽。


搜索资料时,看到有人说DOTS不够解耦,虽然提高了性能但加重了耦合性。
我觉得这种说法不太对。就像AOP虽然不是面向对象,但在某些场合上有很强的解耦能力。
相反,我觉得DOTS这套模式一定程度上是解耦的,因为它满足了最小接口信息原则。每个JobSystem只需要知道自己关心的Data就可以了。所以我才会说看到那块代码很爽,因为它最小信息,所以代码看着特别地简洁。
但另一方面,似乎没有看到封装性,好像所有的Data对每个System都是可见的。程序变得十分复杂时,可能存在调试困难。谁都不知道这个Data被哪个system改的,如果某个System没有高可用,这个Data的修改可能会带来一系列难以解决的问题。因为System对Data解耦了,但通过Data之间,System又混淆在一起了。
但在游戏开发中,这种不封装性往往又是很有必要的。因为在创造游戏机制时,往往要调用各种组件。比如设计师突然让你实现个无敌的BUFF。一堆设计模式可能不如一个全局变量来的快,虽然你可能之后要付出代价(比如设计师又让你实现个无视无敌的buff),但谁知道还会不会有之后呢。
总之刚学DOTS所知确实有限,目前看来至少上手比较简单。之后我还想继续用DOTS制作试试看,在实际中踩了坑并学到经验后,再来和各位分享啦。


参考

[官网](https://unity.com/dots)
[使用DOTS制作一款第三人称僵尸射击游戏](https://unity.cn/projects/li-yong-dotszhi-zuo-yi-kuan-di-san-ren-cheng-sang-shi-she-ji-you-xi )
[基于 Game Object Conversion 和 SubScene 的 DOTS 开发工作流](https://zhuanlan.zhihu.com/p/109943463 )
[官方的教程](https://www.bilibili.com/video/av88027106)

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-17 12:32 , Processed in 0.092862 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表