DOTS介绍+Unity DOTS-MAN小游戏项目实战
Posted 楠箨
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了DOTS介绍+Unity DOTS-MAN小游戏项目实战相关的知识,希望对你有一定的参考价值。
文章目录
前言
DOTS是Unity在17年左右提出的一个概念,其核心是ECS。
提示:以下是本篇文章正文内容,下面案例可供参考
一、1. What is DOTS and why we use it?
全称:(Multi-Thread)Data-Oriented-Tech-Stack
(多线程式)数据导向型技术堆栈
1.DOTS包含的主要元素(三件套)
- 实体组件系统(ECS) - 提供使用面向数据的方法进行编码的框架。在Unity中它通过Entities软件包进行分发,您可以通过Package Manager来添加编辑器。
- C#作业系统 (JobSystem)- 提供一种生成多线程代码的简单方法。它通过Jobs软件包进行分发。
- Burst编译器 - 可生成快速、优化的本机代码。它通过Burst软件包进行分发,可通过Package Manager在编辑器中使用。
- 本机容器 - 属于ECS数据结构,可提供对内存的控制,值得注意的是Unity专门对内存管理进行了一部分优化以降低MissCache。
2.Why we use it?
- 许多并行编程范式,尤其是SIMD(单指令多数据)型范式,更倾向于使用SoA(结构体数组)。在CUDA C编程中也普遍倾向于SoA,一维数据元素是为全局内存的有效合并访问而预先准备好的,而相同内存操作引用的同字段元素在存储时时彼此相邻的,使用SoA能够显著减少MissCache。
- 实体组件系统(ECS)提供了一种面向数据的编码设计方法。利用面向数据的方法,可以对数据结构加以组织,以免出现高速缓存未命中的情况,从而令随后的数据访问更加高效、快捷。由于面向对象的设计并不专注于数据的组织,因此高速缓存未命中的情况很常见,这样就减慢了CPU访问数据的速度,因为它必须频繁地返回访问主内存中的数据。
- C#作业系统可以轻松地用C#编写快速、并行化的代码,以充分利用当今的多核处理器。
- Burst编译器会生成高度优化的代码,而这些代码可以利用您要编译的平台硬件。
Tips:
- jobsystem和ecs是两个不同的东西,但是配合起来使用会有1+1>2的效果
- burst与ecs的高度适配也使得ecs运行效率很高
3.Where we use it? (摘自Unity官方)
除非您在寻求短期或中期的性能改进,否则很难判定是否需要过渡到DOTS或何时过渡到DOTS。
DOTS几乎可以为每个应用程序带来一定程度的性能改进。这其中包括性能、电池使用寿命、迭代及项目可扩展性。过渡到DOTS不会造成任何性能的下降,但评估过渡到DOTS所增加的费用却至关重要,尤其是对于那些仅带来较小改进的项目。
对于所有应用程序而言,DOTS适合处理大量数据,例如开放式环境或使用大量相同材料的复杂结构。通过在实例之间共享公共数据以减少内存访问,DOTS也同样适用于重复的元素。
DOTS将来会帮助您开发高质量的内容,而不使用DOTS的Unity却很难做到,这一点务必要考虑清楚。例如,当今的标准游戏和Unity项目已经取代了过去的AAA游戏。放眼未来,您需要采用DOTS来保持竞争力。
针对不同的垂直行业,DOTS可以适用于不同的解决方案:
(1)对于AEC(工程建设)应用
- DOTS适合处理大型数据集并确保内容的可扩展性。
- DOTS非常适合进行大型交互式地图和具有大量模型和重复内容(例如建筑物和道路)的环境设计。
- DOTS适用于复杂的工程可视化,可大规模地模拟现实环境。例如,DOTS非常适合进行粒度级工厂和基础架构设计。
(2) 对于汽车应用
- 自动驾驶的仿真和可视化
- DOTS非常适合进行大型交通和行人模拟,这需要成千上万的志愿Agent以逼真的方式移动和交互。
(3) 对于游戏独立开发者和自由职业者
- DOTS可以帮助您减轻游戏中一些高成本操作的负担,并有助于提高性能,尤其是对于一些重复性进程。
- 许多轻量级游戏(例如用于移动设备的游戏)并不能最大限度地提高硬件性能。即使有些游戏能够做到这一点,但这可能并不是它的主要关注点。不过,随着游戏的不断发展和硬件需求的持续增加,明智的做法是为将来使用DOTS做好准备。同样,Project Tiny也提供了使用DOTS开发较小应用程序和游戏的解决方案。
- 如果您没有使用DOTS的迫切需求,那么最好先未雨绸缪,提高自己的DOTS技能,以便在DOTS成为Unity开发的标准方法时能够整装待发。
(4)对于游戏工作室
- 当前格式的DOTS可以帮助您逐步达到Unity或其他方式所无法达到的规模和性能。具体而言,更长的电池使用寿命、温度控制以及DOTS所提供的代码可重用性是其主要优势所在。这些方面的性能改进还使您可以开发更多的低端设备,尤其是在西方市场以外的地区,这些设备会受到一定的硬件限制。
- 通过让研发团队以DOTS开展工作,可以帮助您逐步了解所能采取的最佳方法,以及哪些最新的功能和领域最具性能优势和发展影响力。
- DOTS并非要取代引擎团队的作用,而是让工程师腾出更多精力在自己的专业领域(例如阴影或着色器)进行创新。
4.DOTS的优劣(机遇以及风险)
在改善Unity项目的绩效方面,DOTS有着巨大的潜力。 但是,在使用DOTS时需要做出一些考量,它们会影响到项目的时间表、预算和开发团队。以下是一些需要与项目优先事项进行比较和对比的事项。这些事项可以归类为风险与机遇。
机遇
- 改进性能。默认情况下,我们经常使用“性能”一词来描述DOTS。这是什么意思呢?借助面向数据的设计和多线程,DOTS可以显著提升内存、运行时间和电池性能。随着游戏中显示的项目数量不断增加,提高性能的潜力也随之上升。相反,对于项目较少的游戏,您会发现游戏性能的改善程度却不太明显。
- 代码控制。随着项目规模的不断增大,DOTS可以更好地控制代码的复杂性。为DOTS编写的代码通常可以更好地分离关注点。因此,使用DOTS工作时,代码重构、编写单元测试以及在开发人员之间分配工作就变得更加容易。
风险
- 学习成本。如果您不熟悉DoD,那么面对DOTS时就会有一个学习曲线。尽管DoD在计算机科学领域有着良好的根基并已存在数年,而且DoD方法与OOP方法也有很大的不同,但DoD本质上并不比OOP复杂。ECS是一种不同于当前Unity MonoBehaviour方法的代码体系架构,因此学习需要一定的时间。目前,我们认为一名普通的Unity专业开发人员平均需要1个月才能熟练使用DOTS。这一准备时间可以被使用DOTS时的代码质量和性能改进所抵消。当然,具体要取决于项目。
- 有限支持。DOTS当前仅与Unity中一组有限的功能兼容。 最终,DOTS将与Unity的所有功能完全兼容,但我们目前尚无实现完全兼容的时间表。不过,DOTS允许在单个项目中同时使用游戏对象和DOTS,因此您可以将DOTS用于最频繁的处理任务,而将非DOTS Unity用于其余任务。
- 过渡。如果之前的项目是基于Mono开发,那么跟ECS之间的转换可能比较简单,使用Unity自带的一些Hybrid工具就可以较为简单的做到,但是想要把ECS转化为目前常用的Mono的话,我们认为可以做到,但是十分困难,而且也不建议这么做(为什么要尝试把高效率转为低效率呢)。目前比较推荐的是HybridECS开发,ECS与Mono混合在一起,ECS再配合Jobsystem处理最需要多线程的那一部分。
随着时间的推移,晶体管电路逐渐接近性能极限,在摩尔定律逐渐失效的今天,人们面临的数据也呈几何倍数暴增,我们有理由去发明并且学习使用一种效率更高,更能完全发挥硬件性能的软件编程方式,目前看来也许ECS也许能做到。
二、DOTS-Man小游戏项目实战
想要熟悉DOTS以及ECS框架,最好还是要上手做一个小项目,使用部分基础组件,想要熟悉以及精通还需要大量的练习以及使用,开发过程中要配合官方Entities文档使用。
Entities最新版本0.17的官方说明文档
1.环境配置
- 如果是Unity2020.X以下版本:
- windows -> package manager
- advanced -> show preview package
- install三件套 (Entities,Jobs,Burst)
- install其他组件(Hybrid Renderer,Mathematics)
-
如果是Unity2020.X及以上版本(推荐,作者使用2020.3.26f1c1):
- 进入package manager
- 点击 + 号点击add package from gir url手动添加三件套以及其他组件
- com.unity.dots.editor
- com.unity.physics
- com.unity.entities
- com.unity.rendering.hybrid
2.游戏设计
我们准备做一个类似Pac-Man的小游戏,主要熟悉Physics包以及Entities的基本使用,所以不会开发怪物AI之类的,因为使用DOTS开发所以就叫DOTS-MAN好了。
需求分析
主要功能有:玩家移动,镜头跟随,分数显示,因为如果用ECS来修改UGUI的TEXT可能比较麻烦,这里选择使用HybridECS开发,使用MonoBehaviours开发一些基础功能比如镜头跟随以及物体生产之类。
3.正式开发
一些自带脚本
在开发过程中,因为收集物以及玩家还有地形之类的都要有碰撞,但是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
Component
组件只有三个,两个存储分别存储移动和旋转的速度,一个负责标记收集物(所以里面没有数据)
要记得把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;
Component配置:
玩家:
墙体和收集物:
要注意在脚本中配置Collision Filter相关以及Collision Response相关,即某个entity属于哪个标签,他能与其他哪些标签的entity发生碰撞
搭建一个使用场景(renderer相关的根据自己喜好来整):
因为mono和ECS是相互穿插的,所以如果mono中有需要的system可以直接先去看看system的代码,配合官方文档理解为何这么做,这样才能把整个流程梳理清楚(至少我学习的时候是这样的)
Mono Behaviour
这里需要一个全局的mono behaviour来控制游戏,例如entity与object的连接,这里我们换一种方式,把之前的玩家小球弄成prefab,然后在这个全局mono控制玩家的生成,起名就叫做GameManager吧(具体说明看注释):
- 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;
- CameraFollow:
相机跟随的mono脚本:
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上!
System
- MoveSystem:
控制玩家移动,获取玩家输入放入一个float2中,具体的Mathematics相关class可以看官方文档,这是一个用起来比vector要快的东西(因为ECS是数据驱动,不用特别关注object):
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。
- RotateSystem:
控制收集物旋转的system,具体的quaternion用法可以参考官方文档:
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);
- DeleteSystem:
控制删除有deletetag的entity的system:
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>()
.WithoutBurstUnity开源项目精选Entitas:Unity DOTS的先行者
洪流学堂,让你快人几步。你好,我是你的技术探路者郑洪智,你可以叫我大智。
Entitas
Entitas是Unity官方推出DOTS之前的一个开源ECS框架。不过自从Unity官方启动DOTS以来,Entitas就不再更新了,版本停留在了2019年2月20日发布的1.13.0。
虽然Entitas不再更新了,但是在旧版本的Unity(2019之前)和纯C#环境中还是可以使用的。
源码链接:
https://github.com/sschmid/Entitas-CSharp
ECS是什么?
ECS即实体(Entity),组件(Component),系统(System),其中Entity,Component都是纯数据的类,System包含逻辑负责操控他们,这种模式会一定程度上优化我们的代码速度。
- Entities 实体:游戏中的事物。
- Components 组件:与Entity相关的数据,但是应该由数据本身而不是实体来组织。(这种组织上的差异正是面向对象和面向数据的设计之间的关键差异之一)
- Systems 系统:Systems是把Components的数据从当前状态转换为下一个状态的逻辑,例如,一个system可能会通过他们的速度乘以从前一帧到这一帧的时间间隔来更新所有的移动中的entities的位置。
这么说好像还是很抽象,那咱们先来说说为什么ECS会很快吧。
为什么ECS性能好?
首先需要补充一些计算机组成原理前置知识。
- CPU处理数据的速度非常快,往往会出现CPU处理完数据在那干等着的情况,所以需要设计能跟上CPU的高速缓存区来尽量保证CPU有事干,同时也提高了数据访问效率。
- CPU自身有三级缓存,第一级最快,容量最小,第三级最慢,容量最大。
- 我们常说的内存是指CPU拿取数据的起源点,CPU访问内存的速率远小于三级缓存速率。
- CPU操作数据会先从一,二,三级缓存中取得数据,速度非常快,尤其在一级缓存处速率基本可以满足CPU的需求(即不让CPU歇着),但是有些情况下我们请求的数据不在这三级缓存中(缺页中断),就需要寻址到内存中的数据(包含这个数据的一整块数据都将被存入缓存),并且把目标数据放到三级缓存中,提高下一次的访问速度(因为这一次调用的数据块可能在不久的将来还会用到)。
ECS的数据组织与使用形式
ECS架构在执行逻辑时,只会操作需要操作的数据,而E和C这两者的配合把相关数据紧密的排列在一起,并且通过Fliter组件过滤掉不需要的数据,这样就减少了缺页中断次数,整体上提高了程序效率。
此外现代CPU中的使用数据对齐的先进技术(SIMD,Single Instruction Multiple Data,一条CPU指令多个数据)与这种数据密集的架构相性极好,可以进一步提高性能,但是需要有一定的SIMD编程经验(虽然C#编译器内部本来就有做这种SIMD优化)来处理具体的业务逻辑。
ECS有什么优势
对比传统的面向对象编程,ECS模式无疑更加适合现代CPU架构,因为它可以做到高效的处理数据而不用把多余的数据字段存入宝贵的缓存从而导致多次缺页中断。
举个例子就是传统模式下我们操作Unity对象的Position属性,他会把GameObject所有相关数据都加入缓存,浪费了宝贵的缓存空间。
而如果在ECS模式下,将只会把Position属性集放入内存,节省了缓存空间,也一定程度上减少了缺页频率,即常说的提高缓存命中率。
ECS真有那么神吗
很遗憾,答案是否定的,ECS在正常情景应用下性能和传统模式不分伯仲,有时甚至因为我们额外的管理分类操作而导致反而不如传统模式性能好的情况出现,它只适用于超多对象的统一管理与操作情形,这也是我们经常看到的ECS的Demo很震撼的原因之一了。
并且真正启用ECS还是有一定技术门槛的,需要有一定的SIMD编程经验,才能把良好数据管理的优势进一步的发挥出来。
如果以上链接无法下载或者下载太慢,可以在洪流学堂公众号回复Entitas
获取网盘链接。
扩展阅读
- 【Unity开源项目精选】AirSim
- 【Unity开源项目精选】ML-Agents:给你的游戏加入AI
- 【Unity开源项目精选】AssetStudio:提取Unity游戏的资源
- 【Unity开源项目精选】xLua:Unity热更新首选
- 【Unity开源项目精选】Unity引擎源码的C#部分
我是大智(vx:zhz11235),你的技术探路者,下次见!
别走!点赞,收藏哦!
好,你可以走了。
以上是关于DOTS介绍+Unity DOTS-MAN小游戏项目实战的主要内容,如果未能解决你的问题,请参考以下文章