Unity 實(shí)體組件系統(tǒng)(ECS)——性能測(cè)試

作者:ProcessCA
Hi,大家好。
趁著Unity前幾天更新了Unity ECS,正好把之前的坑填上。

在上一片文章我們動(dòng)手體驗(yàn)了一下Unity ECS(Entities),嘗試了一些基礎(chǔ)操作。
這次我們嘗試用Job System優(yōu)化我們的ECS實(shí)現(xiàn),我們會(huì)把精力放在Job System之上,然后測(cè)試它在游戲幀數(shù)這種比較直觀的參數(shù)上能拉開傳統(tǒng)的Monobehaviour多大的差距。
如果不熟悉Unity ECS,強(qiáng)烈推薦瞄一眼上一篇文章,熟悉ECS的核心概念與基礎(chǔ)用法
Unity 實(shí)體組件系統(tǒng)(ECS)——預(yù)覽與體驗(yàn)
我們打算做一個(gè)測(cè)試小游戲——瘋狂吃豆人,游戲效果如下圖所示:

我們先用Monobehavior(后文簡(jiǎn)稱Mono)快速實(shí)現(xiàn)該游戲,然后再用Unity ECS嘗試優(yōu)化,對(duì)比兩者后再看看會(huì)發(fā)生什么。相信大家都熟悉Mono的使用,我們需要實(shí)現(xiàn)以下幾個(gè)功能:
1.玩家,敵人移動(dòng)(不使用Unity的物理系統(tǒng)而是直接修改Transform)
2.玩家碰撞檢測(cè)(可以用一個(gè)簡(jiǎn)單的碰撞算法實(shí)現(xiàn))
3.敵人隨機(jī)生成與銷毀(需要一個(gè)單獨(dú)的關(guān)卡系統(tǒng)控制敵人生成的位置與生成速度)
首先我們創(chuàng)建兩個(gè)Sphere放在場(chǎng)景中,然后創(chuàng)建兩個(gè)新的Material,修改一下Shader改為Unlit/Color實(shí)現(xiàn)球體的扁平化風(fēng)格,然后再設(shè)置一下顏色(玩家設(shè)置為橘色,敵人設(shè)置為藍(lán)色)。
為了區(qū)分玩家與敵人,我們把橘色的材質(zhì)掛載到玩家上,藍(lán)色的材質(zhì)掛載到敵人上。
由于不使用Unity物理系統(tǒng),所以我們移除掉兩個(gè)球體上的Sphere Collider。
記得設(shè)置一下他們的Tag,分別為Player與Enemy,別忘了把玩家跟敵人做成預(yù)制體。
接著修改一下相機(jī)的Position為0, 0, -10。

創(chuàng)建Canvsa并且添加上敵人數(shù)量(EnemyCountText)這個(gè)Text:

準(zhǔn)備工作完成后畫面看上去是這樣的:

程序員審美,簡(jiǎn)單顏色跟純黑背景

搭建好場(chǎng)景后我們先從創(chuàng)建一個(gè)Player類開始,并掛載到玩家身上來實(shí)現(xiàn)玩家的移動(dòng)。
using UnityEngine;
public class Player : MonoBehaviour
{
??? public bool Dead;
??? private float speed;
?
??? void Start() => speed = 5;
?
??? void Update()
??? {
??????? float x = Input.GetAxisRaw("Horizontal");
??????? float y = Input.GetAxisRaw("Vertical");
??????? Vector3 vector = new Vector3(x, y, 0).normalized * speed * Time.deltaTime;
??????? transform.position += vector;
??? }
}
?
代碼是不是異常簡(jiǎn)單,估計(jì)初學(xué)三天的同學(xué)也可以輕松實(shí)現(xiàn)角色的移動(dòng),但看到后面,嘿嘿嘿嘿。
還有一個(gè)更簡(jiǎn)單的攝像機(jī)跟隨腳本:
using UnityEngine;
public class CameraFollow : MonoBehaviour
{
??? private GameObject player;
?
??? void Start() => player = GameObject.FindWithTag("Player");
?
??? void Update() => transform.position = new Vector3(player.transform.position.x,
??????? player.transform.position.y, gameObject.transform.position.z);
}
?
接著創(chuàng)建一個(gè)Enemy腳本掛載在敵人預(yù)制體上,提供一個(gè)供敵人生成器調(diào)用的接口,并且利用一個(gè)求兩個(gè)圓相交或相切的算法實(shí)現(xiàn)碰撞。
using UnityEngine;
public class Enemy : MonoBehaviour
{
??? private EnemySpawn spawn;
??? private float speed;
??? private Player player;
??? private float radius;
??? private float playerRadius;
?
??? //預(yù)留接口
??? public void Init(EnemySpawn spawn, float speed, Player player)
??? {
??????? this.spawn = spawn;
??????? this.speed = speed;
??????? this.player = player;
??? }
?
??? void Start()
??? {
??????? Renderer renderer = GetComponent<Renderer>();
??????? Renderer playerRenderer = player.GetComponent<Renderer>();
??????? radius = renderer.bounds.size.x / 2;
??????? playerRadius = playerRenderer.bounds.size.x / 2;
??? }
?
??? void Update()
??? {
??????? //敵人尋路
??????? Vector3 vector = (player.transform.position - transform.position).normalized *
??????????? Time.deltaTime * speed;
??????? transform.position += vector;
??????? //碰撞檢測(cè)
??????? float distance = (player.transform.position - transform.position).magnitude;
??????? if (distance < radius + playerRadius && !player.Dead)
??????? {
??????????? Destroy(gameObject);
??????????? spawn.EnemyCount--;
??????? }
??? }
}
?
Player跟Enemy腳本都已經(jīng)實(shí)現(xiàn),還需要一個(gè)創(chuàng)建Enemy的腳本EnemySpawn放在場(chǎng)景中當(dāng)作一個(gè)計(jì)時(shí)器,每隔一定時(shí)間就在玩家身邊創(chuàng)建一堆敵人。
using UnityEngine;
public class EnemySpawn : MonoBehaviour
{
??? [HideInInspector]
??? public int EnemyCount;
??? [SerializeField]
??? private GameObject enemyPrefab;
??? private Player player;
??? private float cooldown;
?
??? void Start() => player = GameObject.FindWithTag("Player").GetComponent<Player>();
?
??? void Update()
??? {
??????? if (player.Dead)
??????????? return;
??????? cooldown += Time.deltaTime;
??????? if (cooldown >= 0.1f)
??????? {
??????????? cooldown = 0f;
??????????? Spawn();
??????? }
??? }
?
??? void Spawn()
??? {
??????? Vector3 playerPos = player.transform.position;
??????? for (int i = 0; i < 50; i++)
??????? {
??????????? GameObject enemy = Instantiate(enemyPrefab);
??????????? EnemyCount++;
?
??????????? int angle = Random.Range(1, 360);??????? //在玩家什么角度刷出來(1-359)
??????????? float distance = Random.Range(15f, 25f); //距離玩家多遠(yuǎn)刷出來
??????????? //角度與距離確定好之后算一下Enemy的初始坐標(biāo)
??????????? float y = Mathf.Sin(angle) * distance;
??????????? float x = y / Mathf.Tan(angle);
?
??????????? enemy.transform.position = new Vector3(playerPos.x + x, playerPos.y + y, 0);
??????????? Enemy enemyScript = enemy.AddComponent<Enemy>();
??????????? enemyScript.Init(this, 2.5f, player);
??????? }
??? }
}
?
設(shè)置場(chǎng)景中的EnemySpawn:

最后把控制的UI腳本加上掛載到場(chǎng)景中就大功告成了。
using UnityEngine;
using UnityEngine.UI;
public class UI : MonoBehaviour
{
??? private Text enemyCountText;
??? private EnemySpawn enemySpawn;
?
??? void Start()
??? {
??????? enemyCountText = GameObject.Find("EnemyCountText").GetComponent<Text>();
??????? enemySpawn = GameObject.Find("EnemySpawn").GetComponent<EnemySpawn>();
??? }
?
??? void Update() => enemyCountText.text = "敵人數(shù)量:" + enemySpawn.EnemyCount;
}
?
我們利用Monobehavior輕車熟路地實(shí)現(xiàn)了我們的游戲。
運(yùn)行游戲:

以為這就結(jié)束了嗎?
下面的才是重點(diǎn)加難點(diǎn)。
我們使用Entities實(shí)現(xiàn)同樣的功能,最后進(jìn)行性能上的比較。
在開始前確保安裝上了Entities,在菜單欄Window->Package Manager->All可以找到,如果網(wǎng)絡(luò)出現(xiàn)問題可以反復(fù)嘗試幾次。
首先我們需要想清楚Component與System的關(guān)系再開始編寫代碼。
我們的游戲有三個(gè)關(guān)鍵的實(shí)體:玩家,敵人,攝像機(jī)
這里畫一張圖方便大家理解,從上往下依次是:實(shí)體,系統(tǒng),組件,系統(tǒng)。他們的關(guān)系通過連線一目了然。

提高性能的關(guān)鍵在于腳本的并行,我們看看那些系統(tǒng)應(yīng)該實(shí)現(xiàn)并行:EnemyCollisionSystem,EnemyMoveSystem,這兩個(gè)系統(tǒng)因?yàn)槭顷P(guān)鍵系統(tǒng)并且不存在邏輯與引用的依賴所以可以實(shí)現(xiàn)并行。
首先我們創(chuàng)建一個(gè)新的場(chǎng)景,Camera的設(shè)置需要從第一個(gè)場(chǎng)景中Copy過來使用。
然后我們?cè)賵?chǎng)景中創(chuàng)建一個(gè)空物體代表Player(Tag選擇Player),并且掛上一個(gè)組件:

我們?cè)贛esh一欄中選擇Sphere圓球,然后選擇之前創(chuàng)建的對(duì)應(yīng)的材質(zhì)。
Enemy也是一樣,只需要在Material一欄中選擇不同的材質(zhì)就好了。
UI也可以從之前的場(chǎng)景中復(fù)制過來:

第一步,我們照著圖來編寫好我們的組件,首先創(chuàng)建一個(gè)名為Bootstrap的腳本,為了方便起見我們就把所有的類都放在一個(gè)文件中進(jìn)行管理:
namespace MultiThread
{
??? using UnityEngine;
??? using UnityEngine.UI;
??? using Unity.Entities;
??? using Unity.Jobs;
??? using Unity.Burst;
??? using Unity.Rendering;
??? using Unity.Transforms;
??? using Unity.Mathematics;
??? using Unity.Collections;
??? using Random = UnityEngine.Random;
?
??? public struct PlayerInput : IComponentData
??? {
??????? public float3 Vector;
??? }
?
??? public struct EnemyComponent : IComponentData
??? {
??? }
?
??? public struct CameraComponent : IComponentData
??? {
??? }
?
??? public struct Health : IComponentData
??? {
??????? public int Value;
??? }
?
??? public struct Velocity : IComponentData
??? {
??????? public float Value;
??? }
}
?
值得注意的是Unity ECS里面有一個(gè)bug導(dǎo)致不能在結(jié)構(gòu)中聲明bool類型。
以上的組件屬于自定義組件,除此之外還有三個(gè)Unity提供的組件:
Position,MeshInstanceRenderer,Transform
然后我們創(chuàng)建一個(gè)Bootstrap類,在其中創(chuàng)建一個(gè)能用被Unity自動(dòng)調(diào)用的方法:
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
public static void Start()
{
}
?
再Start方法中創(chuàng)建EntityManager開始,并進(jìn)行初始化操作。
EntityManager manager = World.Active.GetOrCreateManager<EntityManager>();
?
GameObject player = GameObject.FindWithTag("Player");
GameObject enemy = GameObject.FindWithTag("Enemy");
GameObject camera = GameObject.FindWithTag("MainCamera");
Text enemyCount = GameObject.Find("EnemyCountText").GetComponent<Text>();
?
//獲取Player MeshInstanceRenderer
MeshInstanceRenderer playerRenderer = player.GetComponent<MeshInstanceRendererComponent>().Value;
Object.Destroy(player);
//獲取Enemy MeshInstanceRenderer
MeshInstanceRenderer enemyRenderer = enemy.GetComponent<MeshInstanceRendererComponent>().Value;
Object.Destroy(enemy);
//初始化玩家實(shí)體
Entity entity = manager.CreateEntity();
manager.AddComponentData(entity, new PlayerInput { });
manager.AddComponentData(entity, new Position { Value = new float3(0, 0, 0) });
manager.AddComponentData(entity, new Velocity { Value = 7 });
manager.AddSharedComponentData(entity, playerRenderer);
//初始化攝像機(jī)實(shí)體
GameObjectEntity gameObjectEntity = camera.AddComponent<GameObjectEntity>();
manager.AddComponentData(gameObjectEntity.Entity, new CameraComponent());
?
上面代碼比較簡(jiǎn)單不做過多講解。
創(chuàng)建第一個(gè)系統(tǒng)PlayerInputSystem,照著上面的設(shè)計(jì)圖紙,關(guān)注相應(yīng)的組件并進(jìn)行操作就好了。
public class PlayerInputSystem : ComponentSystem
{
??? struct Player
??? {
??????? public readonly int Length;
??????? public ComponentDataArray<PlayerInput> playerInput;
??? }
?
??? [Inject] Player player; //加上這個(gè)標(biāo)簽,Unity會(huì)自動(dòng)注入我們聲明的結(jié)構(gòu)中的屬性
?
??? protected override void OnUpdate()
??? {
??????? for (int i = 0; i < player.Length; i++)
??????? {
??????????? float3 normalized = new float3();
??????????? float x = Input.GetAxisRaw("Horizontal");
??????????? float y = Input.GetAxisRaw("Vertical");
??????????? if (x != 0 || y != 0) //注意:直接歸一化0向量會(huì)導(dǎo)致bug
??????????????? normalized = math.normalize(new float3(x, y, 0));
??????????? player.playerInput[i] = new PlayerInput { Vector = normalized };
??????? }
??? }
}
?
以上的操作在上一篇ECS文章中有提及。
PlayerMoveSystem應(yīng)該關(guān)注相應(yīng)的組件并且進(jìn)行相應(yīng)的操作:
public class PlayerMoveSystem : ComponentSystem
{
??? struct Player
??? {
??????? public readonly int Length;
??????? public ComponentDataArray<Position> positions;
??????? public ComponentDataArray<PlayerInput> playerInput;
??????? public ComponentDataArray<Velocity> velocities;
??? }
?
??? [Inject] Player player;
?
??? protected override void OnUpdate()
??? {
??????? float deltaTime = Time.deltaTime;
??????? for (int i = 0; i < player.Length; i++)
??????? {
??????????? //Read
??????????? Position position = player.positions[i];
??????????? PlayerInput input = player.playerInput[i];
??????????? Velocity velocity = player.velocities[i];
?
??????????? position.Value += new float3(input.Vector * velocity.Value * deltaTime);
??????????? //Write
??????????? player.positions[i] = position;
??????? }
??? }
}
?
現(xiàn)在我們就已經(jīng)可以控制我們的玩家小球移動(dòng)了,趁熱打鐵繼續(xù)深入。
CameraMoveSystem與上面的系統(tǒng)在實(shí)現(xiàn)上不會(huì)有太大差別。
[UpdateAfter(typeof(PlayerMoveSystem))] //存在依賴關(guān)系, 我們控制該系統(tǒng)的更新在PlayerMoveSystem之后
public class CameraMoveSystem : ComponentSystem
{
??? struct Player
??? {
??????? public readonly int Length;
??????? public ComponentDataArray<PlayerInput> playerInputs;
??????? public ComponentDataArray<Position> positions;
??? }
??? struct Cam
??? {
??????? public ComponentDataArray<CameraComponent> cameras;
??????? public ComponentArray<Transform> transforms;
??? }
??? [Inject] Player player;
??? [Inject] Cam cam;
?
??? protected override void OnUpdate()
??? {
??????? if (player.Length == 0) //玩家死亡
??????????? return;
??????? float3 pos = player.positions[0].Value;
??????? //相機(jī)跟隨
??????? cam.transforms[0].position = new Vector3(pos.x, pos.y, cam.transforms[0].position.z);
??? }
}
?
UI系統(tǒng)還算比較好理解的,不做闡述細(xì)節(jié)了:
[AlwaysUpdateSystem] //持續(xù)更新系統(tǒng)
public class UISystem : ComponentSystem
{
??? Text enemyCount;
?
??? public void Init(Text enemyCount) => this.enemyCount = enemyCount;
?
??? struct Player
??? {
??????? public readonly int Length;
??????? public ComponentDataArray<PlayerInput> playerInputs;
??? }
??? struct Enemy
??? {
??????? public readonly int Length;
??????? public ComponentDataArray<EnemyComponent> enemies;
??? }
??? [Inject] Player player;
??? [Inject] Enemy enemy;
?
??? protected override void OnUpdate()
??? {
??????? if (player.Length == 0) //玩家死亡
??????????? return;
?
??????? enemyCount.text = "敵人數(shù)量:" + enemy.Length;
??? }
}
?
敵人生成系統(tǒng)中使用了一個(gè)生成的小算法,以玩家為原點(diǎn)在圓球的周長(zhǎng)上隨機(jī)一個(gè)點(diǎn)生成敵人

public class EnemySpawnSystem : ComponentSystem
{
??? EntityManager manager;
??? MeshInstanceRenderer enemyLook;
??? float timer;
?
??? public void Init(EntityManager manager, MeshInstanceRenderer enemyLook)
??? {
??????? this.manager = manager;
??????? this.enemyLook = enemyLook;
??????? timer = 0;
??? }
?
? ??struct Player
??? {
??????? public readonly int Length;
??????? public ComponentDataArray<PlayerInput> playerInputs;
??????? public ComponentDataArray<Position> positions;
??? }
?
??? [Inject] Player player;
?
??? protected override void OnUpdate()
??? {
??????? timer += Time.deltaTime;
??????? if (timer >= 0.1f)
??????? {
??????????? timer = 0;
??????????? CreatEnemy();
??????? }
??? }
?
??? void CreatEnemy()
??? {
??????? if (player.Length == 0) //玩家死亡
??????????? return;
??????? float3 playerPos = player.positions[0].Value;
?
??????? for (int i = 0; i < 50; i++)
??????? {
??????????? Entity entity = manager.CreateEntity();
?
??????????? int angle = Random.Range(1, 360);??????? //在玩家什么角度刷出來
??????????? float distance = Random.Range(15f, 25f); //距離玩家多遠(yuǎn)刷出來
??????????? //計(jì)算該點(diǎn)的x, y分量
??????????? float y = Mathf.Sin(angle) * distance;
??????????? float x = y / Mathf.Tan(angle);
??????????? float3 positon = new float3(playerPos.x + x, playerPos.y + y, 0);
??????????? //初始化敵人及屬性
??????????? manager.AddComponentData(entity, new EnemyComponent { });
??????????? manager.AddComponentData(entity, new Health { Value = 1 });
??????????? manager.AddComponentData(entity, new Position { Value = positon });
??????????? manager.AddComponentData(entity, new Velocity { Value = 1 });
??????????? manager.AddSharedComponentData(entity, enemyLook);
??????? }
??? }
}
?
到這里我們已經(jīng)實(shí)現(xiàn)了玩家的輸入,移動(dòng),攝像機(jī)跟隨,UI,與敵人生成。還沒完,最關(guān)鍵的并行系統(tǒng):EnemyMove跟EnemyCollision還沒有實(shí)現(xiàn)。
難點(diǎn)中的難點(diǎn)來了,EnemyMoveSystem需要繼承JobComponent系統(tǒng)來實(shí)現(xiàn)并行。
public class EnemyMoveSystem : JobComponentSystem
{
??? ComponentGroup enemyGroup;?? //由一系列組件組成
??? ComponentGroup playerGroup;
?
??? protected override void OnCreateManager() //系統(tǒng)創(chuàng)建時(shí)調(diào)用
??? {
??????? //聲明該組所需的組件,包括讀寫依賴
??????? enemyGroup = GetComponentGroup
??????? (
??????????? ComponentType.ReadOnly(typeof(Velocity)),
??????????? ComponentType.ReadOnly(typeof(EnemyComponent)),
??????????? typeof(Position)
??????? );
??????? playerGroup = GetComponentGroup
??????? (
??????????? ComponentType.ReadOnly(typeof(PlayerInput)),
??????????? ComponentType.ReadOnly(typeof(Position))
??????? );
??? }
?
??? [BurstCompile] //使用Burst編譯
??? struct EnemyMoveJob : IJobParallelFor //繼承該接口實(shí)現(xiàn)并行
??? {
??????? public float deltaTime;
??????? public float3 playerPos;
??????? //記得聲明讀寫關(guān)系
??????? public ComponentDataArray<Position> positions;
??????? [ReadOnly] public ComponentDataArray<Velocity> velocities;
?
??????? public void Execute(int i) //會(huì)被不同的線程調(diào)用,所以方法中不能存在引用類型。
??????? {
??????????? //Read
??????????? float3 position = positions[i].Value;
??????????? float speed = velocities[i].Value;
??????????? //算出朝向玩家的向量
??????????? float3 vector = playerPos - position;
??????????? vector = math.normalize(vector);
?
??????????? float3 newPos = position + vector * speed * deltaTime;
??????????? //Wirte
??????????? positions[i] = new Position { Value = newPos };
??????? }
??? }
?
??? protected override JobHandle OnUpdate(JobHandle inputDeps) //每幀調(diào)用
??? {
??????? if (playerGroup.CalculateLength() == 0) //玩家死亡
??????????? return base.OnUpdate(inputDeps);
?
??????? float3 playerPos = playerGroup.GetComponentDataArray<Position>()[0].Value;
?
??????? EnemyMoveJob job = new EnemyMoveJob
??????? {
??????????? deltaTime = Time.deltaTime,
??????????? playerPos = playerPos,
??????????? positions = enemyGroup.GetComponentDataArray<Position>(), //聲明了組件后,Get時(shí)會(huì)進(jìn)行組件的獲取
??????????? velocities = enemyGroup.GetComponentDataArray<Velocity>()
??????? };
??????? return job.Schedule(enemyGroup.CalculateLength(), 64, inputDeps); //第一個(gè)參數(shù)意味著每個(gè)job.Execute的執(zhí)行次數(shù)
??? }
}
?
上面這個(gè)系統(tǒng)比較復(fù)雜但卻是Unity ECS的核心,特別是OnUpdate中進(jìn)行的操作,返回的JobHandle會(huì)被不同線程執(zhí)行,理解這一點(diǎn)是關(guān)鍵。
EnemyCollisionSystem在實(shí)現(xiàn)上幾乎與上述系統(tǒng)一致:
[UpdateAfter(typeof(PlayerMoveSystem))] //邏輯上依賴于玩家移動(dòng)系統(tǒng),所以聲明更新時(shí)序
public class EnemyCollisionSystem : JobComponentSystem
{
??? float playerRadius;
??? float enemyRadius;
??? public void Init(float playerRadius, float enemyRadius)
??? {
??????? this.playerRadius = playerRadius;
??????? this.enemyRadius = enemyRadius;
??? }
?
??? ComponentGroup enemyGroup;
??? ComponentGroup playerGroup;
?
??? protected override void OnCreateManager()
??? {
??????? enemyGroup = GetComponentGroup
??????? (
??????????? ComponentType.ReadOnly(typeof(EnemyComponent)),
??????????? typeof(Health),
??????????? ComponentType.ReadOnly(typeof(Position))
??????? );
??????? playerGroup = GetComponentGroup
??????? (
??????????? ComponentType.ReadOnly(typeof(PlayerInput)),
??????? ????ComponentType.ReadOnly(typeof(Position))
??????? );
??? }
?
??? [BurstCompile]
??? struct EnemyCollisionJob : IJobParallelFor
??? {
??????? public int collisionDamage; //碰撞對(duì)雙方造成的傷害
??????? public float playerRadius;
??????? public float enemyRadius;
??????? public float3 playerPos;
??????? [ReadOnly] public ComponentDataArray<Position> positions;
??????? public ComponentDataArray<Health> enemies;
?
??????? public void Execute(int i)
??????? {
??????????? float3 position = positions[i].Value;
??????????? float x = math.abs(position.x - playerPos.x);
??????????? float y = math.abs(position.y - playerPos.y);
??????????? //距離
??????????? float magnitude = math.sqrt(x * x + y * y);
?
??????????? //圓形碰撞檢測(cè)
??????????? if (magnitude < playerRadius + enemyRadius)
??????????? {
??????????????? //Read
??????????????? int health = enemies[i].Value;
??????????????? //Write
??????????????? enemies[i] = new Health { Value = health - collisionDamage };
??????????? }
??????? }
??? }
?
??? protected override JobHandle OnUpdate(JobHandle inputDeps)
??? {
??????? if (playerGroup.CalculateLength() == 0) //玩家死亡
??????????? return base.OnUpdate(inputDeps);
?
??????? float3 playerPos = playerGroup.GetComponentDataArray<Position>()[0].Value;
?
??????? EnemyCollisionJob job = new EnemyCollisionJob
??????? {
??????????? collisionDamage = 1,
??????????? playerRadius = this.playerRadius,
??????????? enemyRadius = this.enemyRadius,
??????????? playerPos = playerPos,
??????????? positions = enemyGroup.GetComponentDataArray<Position>(),
??????????? enemies = enemyGroup.GetComponentDataArray<Health>()
??????? };
??????? return job.Schedule(enemyGroup.CalculateLength(), 64, inputDeps);
??? }
}
?
最后別忘了加上移除死亡的敵人的系統(tǒng),按照Unity官方的說法我們需要使用如下的格式進(jìn)行實(shí)體的移除,要注意的是IJobProcessComponentData接口,繼承這個(gè)接口可以獲得所有的帶有指定組件的實(shí)體。
public class RemoveDeadBarrier : BarrierSystem
{
}
public class RemoveDeadSystem : JobComponentSystem
{
??? struct Player
??? {
??????? public readonly int Length;
??????? [ReadOnly] public ComponentDataArray<PlayerInput> PlayerInputs;
??? }
??? [Inject] Player player;
??? [Inject] RemoveDeadBarrier barrier;
?
??? [BurstCompile]
??? struct RemoveDeadJob : IJobProcessComponentDataWithEntity<Health>
??? {
??????? public bool PlayerDead;
??????? public EntityCommandBuffer Command;
?
??????? //該方法會(huì)獲取所有帶有Health組件的實(shí)體。
??????? public void Execute(Entity entity, int index, [ReadOnly] ref Health health)
??????? {
??????????? if (health.Value <= 0 || PlayerDead)
??????????????? Command.DestroyEntity(entity);
??????? }
??? }
?
??? protected override JobHandle OnUpdate(JobHandle inputDeps)
??? {
??????? bool playerDead = player.Length == 0;
?
??????? RemoveDeadJob job = new RemoveDeadJob
??????? {
??????????? PlayerDead = playerDead,
??????????? Command = barrier.CreateCommandBuffer(),
??????? };
??????? return job.ScheduleSingle(this, inputDeps); //這里使用ScheduleSingle可以不需要指定Execute的指定順序。
??? }
}
?
最后,當(dāng)然別忘了在Bootstrap.Start中初始化這三個(gè)系統(tǒng):
//初始化UI系統(tǒng)
UISystem uISystem = World.Active.GetOrCreateManager<UISystem>();
uISystem.Init(enemyCount);
//初始化敵人生成系統(tǒng)
EnemySpawnSystem enemySpawnSystem = World.Active.GetOrCreateManager<EnemySpawnSystem>();
enemySpawnSystem.Init(manager, enemyRenderer);
//初始化敵人碰撞系統(tǒng)
EnemyCollisionSystem collisionSystem = World.Active.GetOrCreateManager<EnemyCollisionSystem>();
collisionSystem.Init(playerRenderer.mesh.bounds.size.x / 2, enemyRenderer.mesh.bounds.size.x / 2);
?
當(dāng)這些系統(tǒng)都完成之后我們運(yùn)行游戲看一下效果:

終于,我們用兩種方式都已經(jīng)實(shí)現(xiàn)了該游戲,用Unity Profiler簡(jiǎn)單測(cè)試一下這兩種方式的性能:
測(cè)試機(jī)器的CPU(四核)與內(nèi)存(8GB):

測(cè)試環(huán)境:關(guān)閉了絕大部分進(jìn)程,CPU空閑的情況下使用Unity2019.1 Editor運(yùn)行游戲。
測(cè)試方法:使用玩家小球朝著一個(gè)特定的方向移動(dòng)。
首先來看一下基于Monobehavior的實(shí)現(xiàn):

由于沒有使用Unity的物理系統(tǒng),所以基本上是腳本跟渲染兩大塊占用CPU的性能。

在主線程中一幀的時(shí)間已經(jīng)超過30毫秒了,其中腳本執(zhí)行就占用了幾乎27毫秒。

我們可以看到Job System上的線程也幫我們分擔(dān)了不少渲染上的負(fù)擔(dān):

值得一提的是在unity2017之后加入了Job System,所以Unity的渲染也會(huì)被分配到不同的線程中去執(zhí)行,在一定程度上提高了整體運(yùn)行效率。
但在該游戲最吃性能的還是腳本,而我們希望在Job Sytem的不同工作線程中也能分擔(dān)主線程中的腳本運(yùn)行。
所以我們測(cè)試一下加上了Job Sytem的Unity ECS實(shí)現(xiàn):

Job System的工作線程的確為主線程分擔(dān)了相當(dāng)一部分負(fù)擔(dān),并行化的腳本分配到了不同的工作線程上,利用了多核的性能。

我們測(cè)試一下ECS的極限,看看實(shí)例化多少個(gè)單位會(huì)下降到30幀:

差不多是驚人的65000個(gè),幾乎是Mono的4倍(因?yàn)槌浞掷昧怂膫€(gè)處理器核心)。
從中我們可以看到,如果使用Unity開發(fā)某個(gè)擁有非常多相似的單位或是模型的游戲的時(shí)候使用Unity ECS會(huì)是不二之選。
摩爾定律就快失效的今天,不考慮用新的數(shù)據(jù)組織方式跟多線程模型來優(yōu)化你的代碼嗎騷年?
附上項(xiàng)目下載地址:https://github.com/ProcessCA/UnityECSTest
最后想系統(tǒng)學(xué)習(xí)游戲開發(fā)的童鞋,歡迎訪問?http://levelpp.com/? ??? ??
游戲開發(fā)攪基QQ群:869551769? ? ??
微信公眾號(hào):皮皮關(guān)