10万蜜蜂同屏作战 UnityECS蜜蜂大战项目实践(附实现前后对比)

news2024/11/30 4:56:40

本文禁止转载

本项目是Unity官方推荐的ECS入门训练中的蜜蜂大战项目

知乎文章同步链接

浅谈ECS工作栈

提到ECS就不得不提JobSystem和Burst编译器,三者共同组成了Unity面向数据的DOTS(Data-Oriented Technology Stack)框架。

ECS(Entity Component System)是Unity引擎中的一种编程模型,旨在提高性能和可扩展性。ECS通过将数据分离,采用实体(Entity)、组件(Component)和系统(System)的结构,以更好地利用现代处理器的多线程能力。

Job System是Unity中一个用于并行执行代码的系统,Job System通过将任务分解成小的作业(Jobs),以结构的形式封装了几类代码执行模式(串行/并行/分批并行),并封装了线程同步和数据依赖等操作,降低了多线程代码开发的难度。

Burst是Unity的一个编译器后端,它可以与Job System一起使用,以进一步优化代码的执行速度。Burst通过生成SIMD(Single Instruction, Multiple Data)指令,使得数据处理效率成倍增长。

简而言之,ECS提供了一种新的编程范式,通过数据驱动和并行执行来提高性能。Job System使得编写并行代码更为容易,而Burst则进一步优化了这些代码,提高了执行速度。在Unity中,这御三家的使用都是独立的,可以分开单独使用某个组件,但如果一起用就可以更充分的提高性能,尤其是在需要处理大量数据的情况下。

为什么要用ECS

性能优势: ECS 提供了在某些场景下更好的性能,例如在处理大规模数据时。它能够充分发挥多核处理器的优势,以实现更高的并行性,提高游戏的运行效率。
同时ECS的按类别组织数据的模式也有利于利用处理器的Cache和SIMD技术,提升数据处理效率。

可扩展性: ECS 天生就支持代码模块化,使得项目更易于扩展和维护。在ECS下,每个业务逻辑被处理为一个System,这种模块化的结构使开发人员更容易添加、删除或替换业务逻辑,而不会影响其他部分的代码。

适应性: ECS 是 Unity 引擎中的一部分,已经被广泛用于一些复杂的游戏和模拟项目。市面上已有大型游戏项目通过采用 ECS 提高了其渲染和物理模拟的性能,如重返帝国等。

实战项目

蜜蜂大战项目介绍

丛林中有两队蜜蜂,两队蜜蜂为了生存,要和对方队伍争抢资源和掐架。此次项目要实现的具体效果描述如下:

  1. 资源生成在中间。两队蜜蜂分别出生在两边的基地中。
  2. 在相同位置生成的资源将堆叠在一起。
  3. 蜜蜂捡起资源并将其放在它们的基地。
  4. 当资源击中基地的地面时,它会爆炸,生成几只该基地颜色的蜜蜂。
  5. 没有携带资源的蜜蜂可能会攻击并摧毁敌方蜜蜂。
  6. 被摧毁的蜜蜂会散发蜜蜂碎片和血点,而最终携带的资源会掉落到地面上。
  7. 表面上的血点会随时间逐渐减小至消失。
  8. 每只蜜蜂的显示比例在所有三个轴上振荡。
    场景整体效果大概类似这样

在这里插入图片描述

我们最终要完成以上8点需求,达成和sample一致的效果,但不同的是,sample中采用的是Unity传统的MonoBehavior脚本加逻辑的实现方式,执行在单线程上,运行效率可谓非常之低。而我们采用ECS工作栈来实现,利用这面向数据三把斧的利器加持,最终要实现10万只蜜蜂也能流畅运行的目标。

使用ECS重新实现前后对比

先放一个对比视频,对比下项目在原版实现和ECS实现两种实现方式下的性能差异。测试机器配置是:CPUi5-13600KF,GPU 4070Ti, 内存64GB。视频前30s是原版实现下3万蜜蜂+1千资源,后30s是10万蜜蜂+1万资源,ECS实现。搭场景的时候偷懒了,在蜜蜂和资源以及特效的外表上,没有完全复刻原版,但是模拟的那8点逻辑是都实现了的,没有偷工减料。

10万蜜蜂同屏作战 UnityECS蜜蜂大战项目实践对比视频

蜜蜂和资源数量相差如此悬殊的情况下,前者的帧率一度来到10-20fps,而ECS实现全程帧率不低于60,性能提升之显著无需多言了(ECS,牛!)。再来看下Profiler,每个线程都基本拉满了,JobSytem恐怖如斯。

在这里插入图片描述

方案设计

我们来逐个分析业务的需求,列出对应的System实现相应的业务逻辑。

System设计
  • 对于资源和蜜蜂出生而言,都是开始时刻一批产生大量实例,并随着系统的运行会持续产生,我们需要两个System分别用来创建蜜蜂和资源的对象实例(ResourceBirthSystem、BeeBirthSystem)

  • 蜜蜂分成两种,一种是专门抢资源带回家生小蜜蜂的Carrier,一种专门战斗的Attacker,由于Carrier的目标可能被提前抢走,Attacker的目标可能被其他蜜蜂干掉,因此两种蜜蜂都需要每帧重新生成目标,如果目标状态不变,就继续维持当前逻辑否则就要寻找新的目标了。这里需要一个为蜜蜂更新目标的逻辑BeeTargetUpdatingSystem。

  • 蜜蜂确定好目标后,实现蜜蜂的移动需要对速度以及位置进行更新,为了尽量保持每个system处理数据的独立性,减少耦合,这里我们将速度和位置更新的逻辑拆成两个system,先更新速度,再更新位置。因此这里需要两个System:Bee(VelocityUpdating/Moving)System

  • 资源实现堆叠是一个相对复杂的逻辑,处理这个问题的第一直觉是,将地面分成不同的2D网格,每个格子我们称为一个tile,每个tile上记录一个高度,表示已堆叠的资源到达的最高位置,在ResourceMovingSystem之后,每个资源根据当前所在的tile,检查tile的高度和自身位置,如果可以停住则去更新tile的高度,更新自身状态。但这样实际上是有问题的,如果从资源角度出发去更新tile高度,串行执行太慢,并行更新tile高度又会data racing,你可能会想问,为什么不用原子操作?由于原子操作只支持有限的数据类型如int、float等,因此即使原子修改了tile高度,也无法原子记录tile的堆顶resource。最后我采用的解决办法是:tile记录自己区域上空的所有资源,由tile执行这些resource的位置更新,以及tile高度的更新。在tile的视角下并行,虽然在一定程度上减少了并行度,但是避免了data racing带来的程序执行错误的问题,牺牲了一点性能换来正确性。由tile主导的resource移动和状态更新的逻辑放在ResourceMovingSystem。

  • 网格在更新资源位置时,需要知道有哪些资源的位置该被更新,或者说,哪些resource处于tile的管辖范围。这里可以为每个tile添加一个DynamicBufferComponent,这也是Entities下的一个特殊的Component类型,类似于动态数组的概念,长度可变。每个resource在出生后,根据自己的位置和地板上tile的位置,判断自己应该被加入到哪个tile的队列中,你可能想问了:更新dynamic buffer的时候难道不会出现data racing问题了吗?ECB很贴心的为我们的DynamicBuffer准备了单线程修改的逻辑,只需要为每个resource记录下“向这个tile的dynamic buffer中添加此resource的entity”这个Command,ECB在下一帧的playback中会采用单线程执行这些CommandBuffer,就不会有线程安全的问题了。这个逻辑我们在ResourceTileUpdatingSystem中实现。

其他的一些System逻辑就相对简单,就不一一列举了。下面这张图列出了所有的system以及它们的执行顺序,从名字上应该也能看出这些system是干嘛的。

在这里插入图片描述

Component设计

entities中常用的Component类型有四种:ComponentData、EnableableComponent、TagComponent、SharedComponent,我在下图中简单总结了四者的区别,首先添加或删除Component相当于更改了Archetype,肯定会导致StructralChange没有问题,但更新值不会,除了SharedComponent,SharedComponent值会影响entity存放在哪个chunk下,更新SharedComponent值也会导致StructralChange。EnableableComponent在chunk上采用bitmap的方式存储,当不包含任何数据时,和TagComponent一样,完全不占用chunk内的空间,且更新值不会发生StructralChange,更新成本低,缺点是Enable与否不会影响放在哪个chunk,因此同个chunk下可能同时包括Enabled和非Enabled两种类型的数据,对Cache和SIMD不太友好。

在这里插入图片描述

我个人理解中,四者的定位区分还是比较明显的,ComponentData是最常用的,Entity一定会有的Component,TagComponent适用于那些改变不频繁的、起标记作用的Component、EnableableComponent适合那些需要起标记作用,但状态切换又很频繁的Component、SharedComponent适合用来实现能区分出三类或三类以上的标签,但是每个entity又不会频繁改变这一标签的Component。几种Component各自的适用场景不同,具体该用哪个,业务中还是要具体情况具体分析。

在这里插入图片描述

资源的状态比较多,资源在开始时会下落,直到摔在地上,资源在被蜜蜂捡走后会随着蜜蜂移动,携带资源的蜜蜂死亡后会资源又会开始下坠。resource总计会有四个状态,其中Docking(停靠)、Falling(下落)、Transporting(运输)
这三个状态间切换的可能很频繁,我们使用EnableableComponent来实现这三个状态。

在这里插入图片描述

由于死亡和生还的状态是单向的,一旦切换就不可逆,因此这种状态的切换不会频繁,我们对资源和蜜蜂身上的Alive这个Component实现成ComponentData或者TagComponentData。该Component一旦移除就会发生StructralChange。
表示蜜蜂队伍的实现同理,这里用Team1和Team2两个Tag。表示蜜蜂的角色我们使用Carrier和Attacker两个Tag。

一些实现细节

  1. 蜜蜂攻击的顺序问题。在sample的实现逻辑中,会出现这样一种结果不确定的逻辑:由于每个Attacker都是随机从对方阵营中选择攻击目标,因此有可能选择的目标恰好也是个Attacker且也选择了自己,由于所有的蜜蜂的攻击范围相同,因此,因此两只蜜蜂究竟谁会死亡的结果是随机的,取决于攻击的逻辑先执行到哪只蜜蜂。在ECS的实现中,由于我们对蜜蜂的生还状态采用ComponentData实现,添加或删除该状态就是StructralChange,因此在Job中就只能使用ECB来更新。在BeeAttackingSystem中,蜜蜂攻击的逻辑是并行执行的,状态的更改又是通过ECB来延迟执行,因此在这种情况下的两只蜜蜂都会将对方的状态改为死亡,这样就出现了和原本的逻辑相差较大的结果。为了解决这个不符合原版逻辑的问题,只出现一只蜜蜂死亡的结果,稍微加了个Trick,通过在创建蜜蜂的时候在每个蜜蜂原本的攻击距离基础上增加较小的随机偏移,使得每只蜜蜂的攻击范围都不相同,这样就可以确保在两只Attacker攻击对方时,不会有两只蜜蜂同时死亡的结果出现了。

  2. 随机目标选择问题。在蜜蜂更新目标的逻辑上,需要随机选择目标资源或蜜蜂,不管是Carrier还是Attacker都需要获得一个随机数。由于该更新目标的Job是并行执行,不能共享Random的状态,因此每个Job需要创建一个Random并给定不同的随机种子来产生不同的随机数,这里我们使用EntityIndexInQuery作为种子,确保每个蜜蜂的种子都不同,同时再加上framecnt,确保同一个蜜蜂两次更新目标时获得的随机种子也不同。

  3. 蜜蜂二次实例化的问题。资源在基地坠落会产生新的小蜜蜂,这要求我们在游戏中间还可以产生新的不同类型蜜蜂的Entity。我们可以每帧创建原始蜜蜂Entity,并逐个添加Tag:Attacker/Carrier和Team1/Team2,再为每个类型蜜蜂的实例化设置不同的个数。但在创建Entity,添加Component的过程中也是会发生StructralChange的,虽然数量不多,但是坏在每帧都有。因此我们也可以预先创建四个类型的蜜蜂Entity,每帧直接根据需求的个数对应调用实例化即可。但是这样产生了另外的问题,我们预先创建的蜜蜂Entity也会被各种Query查询到,从而进入到游戏逻辑中,然后被敌对的Attacker杀死。我们的确可以加个特殊的Tag来标识这些蜜蜂不参战,但这样我们每个Query都要进行相应的改动,非常不优雅。这里我们可以使用Prefab这个Entities内置的Component类型,程序运行中EntityQuery会自动过滤掉这个Component,这些特殊的Entity就可以避免被搅入游戏逻辑中。同时Instantiate方法实例化出的Entity会自动去掉这一Component,因此PrefabComponent并不会干扰正常的业务逻辑,可以放心大胆使用。

本文禁止转载

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1348332.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux-------rm命令超详解(狠狠爱住)

目录 rm 命令用于在Linux系统中删除指定的文件或目录 基本语法: 常用选项: 示例用法: 放在文末的话: 补充: rm 命令用于在Linux系统中删除指定的文件或目录 基本语法: rm [选项] 文件名/目录名 常用…

Eclipse汉化

目录 一、首先电脑已经下载好Eclipse 二、打开Eclipse Babel 三、打开Eclipse 1、工具栏——>Help——> Install New Software 2、 点击Add 3、添加复制的链接,点击Add 4、等待加载 5、勾选Chinese(Simpliied),而后Next&…

超市订单管理系统

比较简单的超市订单管理系统

在Google Colab中调用Gemini的API实现智能问答

一、引言 Google终于放出大招,在2023年12月6日正式推出规模最大、功能最强大的人工智能模型Gemini,对标ChatGPT,甚至有要赶超ChatGPT-4.0的节奏。 相比之前的Bard,Gemini的文本理解能力、图片识别能力和语义抽取能力大大增强&am…

HTML进阶

列表、表格、表单 文章目录 列表、表格、表单01-列表无序列表有序列表定义列表 02-表格表格结构标签-了解合并单元格 03-表单input 标签input 标签占位文本单选框上传文件多选框下拉菜单文本域label 标签按钮 04-语义化无语义的布局标签有语义的布局标签 05-字符实体 01-列表 …

SpringBoot之入门使用

系列文章目录 提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加 SpringBoot之入门使用 提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 系列文章目录前言一、Spring缺点分析…

Springboot整合Elasticsearch 7.X 复杂查询

这里使用Springboot 2.7.12版本&#xff0c;Elasticsearch为7.15.0。 导入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-elasticsearch</artifactId></dependency> yaml文件配置…

【AIGC-图片生成视频系列-4】DreamTuner:单张图像足以进行主题驱动生成

目录 一. 项目概述 问题&#xff1a; 解决&#xff1a; 二. 方法详解 a) 整体结构 b) 自主题注意力 三. 文本控制的动漫角色驱动图像生成的结果 四. 文本控制的自然图像驱动图像生成的结果 五. 姿势控制角色驱动图像生成的结果 2023年的最后一天&#xff0c;发个文记录…

[C#]使用ONNXRuntime部署一种用于边缘检测的轻量级密集卷积神经网络LDC

源码地址&#xff1a; github.com/xavysp/LDC LDC: Lightweight Dense CNN for Edge Detection算法介绍&#xff1a; 由于深度学习方法的快速发展&#xff0c;近年来&#xff0c;用于执行图像边缘检测的卷积神经网络&#xff08;CNN&#xff09;模型爆炸性地传播。但边缘检测…

【C++】手撕 Vector类

目录 1&#xff0c;vector类框架 2&#xff0c;vector () 3&#xff0c;pinrt() 4&#xff0c;vector(int n, const T& value T()) 5&#xff0c;vector(const vector& v) 6&#xff0c;vector(InputIterator first, InputIterator last) 7&#xff0c;~vector…

Adobe ColdFusion 文件读取漏洞(CVE-2010-2861)

漏洞原理 Adobe ColdFusion是美国Adobe公司的一款动态Web服务器产品&#xff0c;其运行的CFML&#xff08;ColdFusion Markup Language&#xff09;是针对Web应用的一种程序设计语言。由于AJP协议设计存在缺陷导致内部相关的属性可控&#xff0c;攻击者可以构造属性值&#xff…

C++17中的内联变量

在C11中&#xff1a; (1).声明为constexpr的函数隐式地是内联函数; (2).deleted函数隐式地是一个内联函数。 在内联函数中&#xff1a; 1.所有函数定义中的函数局部静态对象(function-local static object)在所有翻译单元之间共享(它们都引用一个翻译单…

实时交通标志检测和分类(代码)

交通标志检测和分类技术是一种基于计算机视觉和深度学习的先进技术&#xff0c;能够识别道路上的各种交通标志&#xff0c;并对其进行分类和识别。这项技术在智能交通系统、自动驾驶汽车和交通安全管理领域具有重要的应用前景。下面我将结合实时交通标志检测和分类的重要性、技…

【STM32】SPI通信

1 SPI通信 SPI&#xff08;Serial Peripheral Interface&#xff0c;串行外设接口&#xff09;是由Motorola公司开发的一种通用数据总线 四根通信线&#xff1a;SCK&#xff08;Serial Clock&#xff0c;串行时钟&#xff09;、MOSI&#xff08;Master Output Slave Input&am…

【MyBatis】操作数据库——入门

文章目录 为什么要学习MyBatis什么是MyBatisMyBatis 入门创建带有MyBatis框架的SpringBoot项目数据准备在配置文件中配置数据库相关信息实现持久层代码单元测试 为什么要学习MyBatis 前面我们肯定多多少少学过 sql 语言&#xff0c;sql 语言是一种操作数据库的一类语言&#x…

BLE Mesh蓝牙组网技术详细解析之Lower Transport Layer下传输层(四)

目录 一、什么是BLE Mesh Lower Transport Layer下传输层&#xff1f; 二、未分段消息 2.1 未分段接入层消息 2.2 未分段控制层消息 三、分段消息 3.1 超过多少个字节需要分段&#xff1f; 3.2 分段接入层消息 3.3 分段控制层消息 3.4 分段确认消息 3.5 分段和重组流程…

按行依次处理数据的文件操作(C语言版)

按行依次处理数据的文件操作(C语言版) 这段代码的目的是处理多个文件&#xff0c;为每个文件创建一个新文件&#xff0c;将以 ‘r’ 开头的行添加 “./” 前缀&#xff0c;并将修改后的内容写入新文件。在main函数中&#xff0c;通过调用process函数&#xff0c;逐个处理了一系…

牛客网SQL训练5—SQL大厂面试真题

文章目录 一、某音短视频1.各个视频的平均完播率2.平均播放进度大于60%的视频类别3.每类视频近一个月的转发量/率4.每个创作者每月的涨粉率及截止当前的总粉丝量5.国庆期间每类视频点赞量和转发量6.近一个月发布的视频中热度最高的top3视频 二、用户增长场景&#xff08;某度信…

[react]脚手架create-react-app/vite与reac项目

[react]脚手架create-react-app/vite与reac项目 环境问题描述create-react-app 脚手架根据脚手架修改项目结构安装脚手架注入配置文件-config文件夹package.json文件变更删除 serviceWorker.js新增reportWebVitals.js文件更新index.js文件 脚手架creat-react-app 缺点 vite 脚手…

数据结构期末复习(2)链表

链表 链表&#xff08;Linked List&#xff09;是一种常见的数据结构&#xff0c;用于存储一系列具有相同类型的元素。链表由节点&#xff08;Node&#xff09;组成&#xff0c;每个节点包含两部分&#xff1a;数据域&#xff08;存储元素值&#xff09;和指针域&#xff08;指…