万泰娱乐资讯

用Unity DOTS制作4万飞剑的太极剑阵!

  【USparkle专栏】如果你深怀绝技,爱“搞点研究”,乐于分享也博采众长,我们期待你的加入,让智慧的火花碰撞交织,让知识的传递生生不息!

  这是侑虎科技第1437篇文章,感谢作者炎拳供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。()

  由于DOTS相关Package不同版本变动很大,许多老的教程也已经过时,给想要探索的小伙伴制造了不少麻烦。所以我便尝试用DOTS制作了这样一个由42804把飞剑组成的炫酷剑阵,每次点击地板,都会有10000把飞剑飞出大阵攻击目标点后返回。算是致敬了古龙小说中的“剑气纵横三万里 ,一剑光寒十九洲”的光景。本文将制作过程和学习经历分享出来,希望能给同样探索DOTS的小伙伴一些参考。

  DOTS的相关Package并未发布,也无法在Package Manager中搜索到,我们需要手动去下载这几个包。

  打开上方菜单栏,点击在左上角+号图标选择Add package from git URL,依次输入:

  由于我们将要生成大量相同材质的飞剑,所以将它们合批处理降低DrawCall是有必要的。

  值得一提的是,本工程中的飞剑已经被我用Blender手动将顶点数降低到105个了,因为我最后大致要生成4万把飞剑,原本的飞剑模型有上千个顶点,庞大的定点数会导致我的场景近乎卡死,最后测试我的电脑能顶住的最大顶点数大概是10M左右。

  我希望每把飞剑都有一个发出淡淡光晕的效果,在2020.3.3版本的URP工程中,Unity已将Post Processing Package默认置入,所以我们无需像老版本一样去自己下载Post-Processing package包。直接使用就可以了。

  首先新建一个材质球,勾选了Emission后扔给飞剑,并在场景中右键新建一个Global Volume:

  最后 我希望后处理效果只对指定层级的物体起作用,这里需要Global Volume和物体设置为相同层级,并且在主相机中指定该层级:

  最后 新建一个配置文件,点击Add Override,并添加Bloom效果即可。

  准备工作做完了,终于要进入实践环节了,本文就不赘述DOTS相关的概念了,网上关于DOTS介绍的文章很多,官方文档对于DOTS阐述也算是详尽,这里我放个链接,对DOTS不了解的小伙伴可以先理解了这些概念后再来实践:

  接下来 我们需要获取图片中每个像素点的位置,然后等比转换到Unity空间中去,让飞剑去填充这些位置。 所以图片也不能太大,这里我们导入的时候在Unity中设置就好了:

  位置拿到了,接下来需要根据位置生成大量飞剑,并将飞剑转换为Entity,我们可以选择给飞剑的预制体添加Convert To Entity脚本的方法进行转换:

  但点 开C onvert To Entity查看,发现它也是继承了MonoBehaviour,很明显它不会自己在编辑器中转换好,所以我们需要在运行的过程中将飞剑的预制体转换为Entity,代码如下:

  然后运行场景按W,在默认Burst Compliler和Job Threads开启的情况下,可以看到飞剑迅速生成到了场景中央,并且没有一点卡顿:

  图中我 们可以看到,列表长度为42802,但Entity的数量生成了2倍多,这是因为飞剑的预制体中包含一个子物体,转换过程中EntityManager会将物体的子物体一同转换为Entity:

  你也 可以点击F ilter,通过筛选Component的方式来寻找想要查看的Entity,左边的系统列表显示了项目中的系统以及系统运行每个框架所花费的时间。你可以使用为每个系统提供的复选框从列表中打开和关闭系统,来进行调试。

  飞剑已经生成了,接下来就需要创建一个System来更新这些Entity,让它们旋转起来。

  首先定义一个Component添加到预制体上,这个Component的作用是一个标签:

  Unity ECS会自动在系统中发现继承了SystemBase的类,点击运行场景,飞剑此时已经可以沿中心点旋转了:

  2. 创建System2 ,在System2中遍历所有带Tag2的飞剑,令其出阵飞向目标点。

  3. 在System2中遍历所有带Tag2的飞剑,当System2中的飞剑非常接近目标点,则移除Tag2,添加Tag3。

  所有继承SystemBase,实现的OnUpdate()方法的System都是在主线程上跑的,因此我们可以很方便地规划System1,2,3的执行顺序,代码如下:

  在ECS中,所有的Entity都是按块(Chunk)存储的,一个块里的所有实体必定拥有相同数量和类型的组件,一旦某个实体的组件数量或类型改变了,它就不属于当前的块,它会被移到其它块里,这个操作在主线程中运行并没有什么问题。

  但我希望能发挥CPU全部的性能来运行这个剑阵,所以需要去分配一些任务到子线程中执行,JobSystem帮我们解决了这个问题,它屏蔽了直接对线程的操作,而是把异步逻辑封装成一个个“Job”,由引擎来调度和分配给合适的线程去执行,官方实例代码在这:

  这时候,假设Job1中删除了某个实体的组件,该实体被移到其它Chunk,而它并行的Job2也在对这个实体进行操作,就会产生冲突(操作不存在的组件或者操作了错误块里的实体),所以Job2必须等待Job1读写数据完毕,这就是硬性同步点(Sync points)。

  每个同步点都会造成停顿,等待当前World中所有先前安排的作业完成。过多的同步点会让效率大大降低,但以上的操作还是 无法避免的。

  为了解决这种问题,Unity提供了EntityCommandBuffer(实体命令缓冲区,简称ECB)来解决这个问题。

  ECB可以将导致结构性更改的命令排入队列,存储在ECB中的命令可以在一帧中晚些时候回放执行。当回放ECB时,这会将跨帧分布的多个同步点减少到单个同步点。下面这个案例是使用系统自带的ECB,这样可以最大程度的减少同步点:

  这样,飞剑添加Component,创建临时的Entity的操作好像都没啥问题了,但实际运行起来时会发现飞剑并没有飞回原定的位置,明显比原先剑阵慢了一帧:

  前面 也说到了,EntityCommandBuffer中的命令不会立即执行,而是会在下一帧被En tityCommandBufferSystem使用,有一帧延迟。 我们生成替代出阵飞剑的Entity,在下一帧才会真正的生成并跟随剑阵旋转,所以飞剑最后和Entity同步的位置也自然是慢了一帧。 目前的解决办法是多计算一帧的距离,但这样做既麻烦又有误差,所以我们换一种简单的思路。

  对与EntityCommandBuffer时序问题有疑惑的小伙伴可以看这篇文章:

  生成TempEntity和飞剑在八卦阵中,为飞剑添加Tag1组件,记录相同位置的TempEntity。

  2. 创建System2,遍历所有无Tag2组件的飞剑,并且每帧和对应TempEntity位置同步。

  3. (System2中执行)若发生点击事件,则抽取10000把飞剑添加Tag2,Tag2记录目标点位置。

  然后在主线程中生成飞剑和TempEntity就可以了,需要注意的是,场景中的地面也需要转换为Entity,并且添加上Physic相关组件。其他的代码就不放了,感兴趣的小伙伴可以下载工程试一下,我放在文章末尾了。最后我们来看下俯视角效果:

  我的 电脑是i5-7500四核+GTX1050,场景中有12万8千个实体,正常运行能跑85fps,一万把飞剑出阵也能保持在60左右:

  文章到这就结束了,回顾下来,踩的坑非常多,希望这篇文章能给小伙伴提供帮助,也期待大佬指正!

  对于GameObject转换为Entity流程感兴趣的同学,可以移步这篇文章:

  文末,再次感谢 炎拳 的分享,作者主页:,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。()