本文主要是介绍Unity 之 Pure版Entity Component System【ECS】 案例【GalacticConquest】解析【上】,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
以往没有了解过Unity ECS的小伙伴建议先看看我写过的两篇ECS文章
- Unity之浅析 Entity Component System (ECS)
- Unity 之 Pure版Entity Component System (ECS) 官方Rotation示例解析
最近两月Unity官方一直在更新ECS的版本,有一些原来的工程在新的版本中是无法运行的,所以今天再写一篇示例解析,虽然ECS目前是测试版本,可能还会有很多的改变,正式版本上线的日期也没有明确说明,但还是希望能帮助喜欢新技术的小伙伴,互相帮助,互相学习~
有说的不准确和不正确的地方欢迎留言指正
大家的帮助是我写下去最有效的动力
点击下载工程
示例效果展示如下

这个示例的规则是这样的,启动时随机生成大小位置不同的球体,然后从球体周围发射小飞船去攻击其他的星球,飞船分为红绿两队,占领后星球变成指定队伍的颜色
此次使用的Unity版本为 2018.2.9f1 Entities版本为0.0.12-preview.15。而且在启动Unity加载Entities的时候保持网路畅通,因为有朋友反映在内网无法使用Entities的情况。【最好能科学上网】


下面还是按照老规矩,分布逐渐创建工程
我们先创建一个PlanetSpawner脚本(产卵器)并添加到空物体Spawners上

作用如下:
- 创建指定的数量的星球
- 使星球位置随机分布
- 向这些星球上动态添加数据
- 星球旋转的数据
- 星球旋所在的队伍、飞船数量、位置、半径数据(用于后期生产飞船使用)
- 更改对应星球队伍的颜色
using System.Collections.Generic;
using System.Linq;
using Data;
using Unity.Entities;
using Unity.Mathematics;
using UnityEngine;public class PlanetSpawner : MonoBehaviour
{/// <summary>/// 星球预制体/// </summary>[SerializeField]GameObject _planetPrefab;/// <summary>/// 初始化星球的个数/// </summary>[SerializeField]int _initialCount = 20;/// <summary>/// 产生星球的随机半径/// </summary>[SerializeField] readonly float radius = 100.00f;/// <summary>/// 场景中所有的Entity的控制者、容器/// </summary>EntityManager _entityManager;/// <summary>/// 灰色 红色 绿色对应材质数组/// </summary>[SerializeField]public Material[] _teamMaterials;/// <summary>/// 飞船Entity对应的GameObject 的字典/// </summary>static Dictionary<Entity, GameObject> entities = new Dictionary<Entity, GameObject>();public static Material[] TeamMaterials;void Awake(){TeamMaterials = _teamMaterials;}void OnEnable(){_entityManager = World.Active.GetOrCreateManager<EntityManager>();//初始化Instantiate(_initialCount);}/// <summary>/// 初始化/// </summary>/// <param name="count">产生星球的数量</param>void Instantiate(int count){//产生飞船队伍列表 1绿色 2 红色var planetOwnership = new List<int>{1, 1,2, 2};for (var i = 0; i < count; ++i){//获取星球对应的半径var sphereRadius = UnityEngine.Random.Range(5.0f, 20.0f);var safe = false;float3 pos;int attempts = 0;do{if (++attempts >= 500){Debug.Log("新创建的行星找不到合适的位置");return;}//在半径为1的范围内返回一个随机点(只读)var randomValue = (Vector3)UnityEngine.Random.insideUnitSphere;randomValue.y = 0;//星球的实际位置pos = (randomValue * radius) + new Vector3(transform.position.x, transform.position.z);//检测星球是否有重合的物体var collisions = Physics.OverlapSphere(pos, sphereRadius);//如果没有重合的地方就是安全地if (!collisions.Any())safe = true;} while (!safe);//在半径为1的范围内返回一个随机点(只读)var randomRotation = UnityEngine.Random.insideUnitSphere;//实例化星球var go = GameObject.Instantiate(_planetPrefab, pos, quaternion.identity);go.name = "Sphere_" + i;//获取星球上对应的 GameObjectEntityvar planetEntity = go.GetComponent<GameObjectEntity>().Entity;//获取渲染星球的对应的子物体var meshGo = go.GetComponentsInChildren<Transform>().First(c => c.gameObject != go).gameObject;//获取碰撞体var collider = go.GetComponent<SphereCollider>();//获取渲染星球的对应的子物体的 GameObjectEntityvar meshEntity = meshGo.GetComponent<GameObjectEntity>().Entity;//把碰撞体的半径设置和圆球一直collider.radius = sphereRadius;//半径*2等于实际扩大的倍数meshGo.transform.localScale = new Vector3(sphereRadius * 2.0f, sphereRadius * 2.0f, sphereRadius * 2.0f);var planetData = new PlanetData{//星球所在的队伍TeamOwnership = 0,//星球的半径Radius = sphereRadius,//星球的位置Position = pos};var rotationData = new RotationData{RotationSpeed = randomRotation};//队伍列表是否有任何元素 没有元素的划分为灰色星球if (planetOwnership.Any()){//给星球分队 【红队或者绿色队伍】planetData.TeamOwnership = planetOwnership.First();//移除对应的队伍planetOwnership.Remove(planetData.TeamOwnership);}else{//设定飞船数量planetData.Occupants = UnityEngine.Random.Range(1, 100);}//设置字典对应的GameObjectentities[planetEntity] = go;//设置对于队伍的颜色 1绿色 2红色SetColor(planetEntity, planetData.TeamOwnership);//动态添加对应的数据 减少了拖拖拽拽_entityManager.AddComponentData(planetEntity, planetData);_entityManager.AddComponentData(meshEntity, rotationData);}}/// <summary>/// 设置对应星球的颜色/// </summary>/// <param name="entity">对应字典</param>/// <param name="team"></param>public static void SetColor(Entity entity, int team){var go = entities[entity];go.GetComponentsInChildren<MeshRenderer>().First(c => c.gameObject != go).material = TeamMaterials[team];}
}
解读一
初始化获取场景中所有的Entity的控制者、容器

解读二
创建并初始化一个队伍列表,数组就是中的数字就是【_teamMaterials】中的索引,更改颜色会用到

解读三
尝试为一个星球找到一个不与其他星球重叠的位置,最多尝试500,还找不到的化就会出现Log信息,其中一个与以往不同,也是ECS中大量使用的float3而不是原来常用的Vector3,这是因为float3更小巧,没有多余的信息数据占用内存。

解读四
常规实例化,不在熬述
![]()
解读五

这不部分就是ECS这种套路对数据相关的操作,事先准备好初始化的数据,然后用_entityManager对相应的实体添加纯数据,有点原来AddComponent的意思,也避免了拖拖拽拽。这里的数据分别为【PlanetData】【RotationData】
效果如下

创建OccupantsTextUpdater脚本并添加到对应的Text上,他的作用是
- 更新所在星球上含有的飞船数量并显示

using Data;
using Unity.Entities;
using UnityEngine;namespace Other
{/// <summary>/// Just updates the text on the planets to represent the occupant count from the attached PlanetData/// 更新含有飞船的数量/// </summary>public class OccupantsTextUpdater : MonoBehaviour{Entity _planetEntity;TextMesh _text;int LastOccupantCount = -1;[SerializeField]EntityManager _entityManager;void Start(){_entityManager = World.Active.GetOrCreateManager<EntityManager>();_planetEntity = transform.parent.GetComponent<GameObjectEntity>().Entity;_text = GetComponent<TextMesh>();}void Update(){if(!_entityManager.Exists(_planetEntity))return;//获取所在星球上的PlanetData数据var data = _entityManager.GetComponentData<PlanetData>(_planetEntity);if (data.Occupants == LastOccupantCount)return;LastOccupantCount = data.Occupants;_text.text = LastOccupantCount.ToString();}}
}

创建一个旋转系统,作用
- 让每一个星球转动起来
using Data;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using UnityEngine;
using UnityEngine.Jobs;namespace Systems
{/// <summary>/// 星球自转系统/// </summary>public class RotationSystem : JobComponentSystem{//筛选实体(符合星球规则的)struct Planets{public readonly int Length;public ComponentDataArray<RotationData> Data;public TransformAccessArray Transforms;}//继承为 IJobParallelForTransform 而非 IJobParallelFor 或 IJob 这是一个专门为transform操作的接口struct RotationJob : IJobParallelForTransform{public ComponentDataArray<RotationData> Rotations;public void Execute(int index, TransformAccess transform){//设定旋转transform.rotation = transform.rotation * Quaternion.Euler( Rotations[index].RotationSpeed);}}[Inject]Planets _planets;protected override JobHandle OnUpdate(JobHandle inputDeps){var job = new RotationJob{Rotations = _planets.Data};return job.Schedule(_planets.Transforms, inputDeps);}}
}
解析一
套路还是和以往IJobParallelFor类似,但是此次需要注意的是里面的继承是【IJobParallelForTransform】,一个专门实现transform并行执行的接口
创建一个增加飞船系统,作用
- 接下来我们需要往红色和绿色的星球上添加飞船,每0.1s增加一次【可指定】
using Data;
using Unity.Collections;
using UnityEngine;
using Unity.Entities;
using Unity.Jobs;namespace Systems
{/// <summary>/// 增加可以从行星上发送的船只的数量/// </summary>// [UpdateAfter(typeof(ShipSpawnSystem))]public class OccupantIncreaseSystem : JobComponentSystem{float spawnCounter = 0.0f;float spawnInterval = 0.1f;//每次增加100个飞船int occupantsToSpawn = 100;//筛选含有PlanetData数据的实体struct Planets{public readonly int Length;public ComponentDataArray<PlanetData> Data;}struct PlanetsOccupantsJob : IJobParallelFor{public ComponentDataArray<PlanetData> Data;[ReadOnly]public int OccupantsToSpawn;public void Execute(int index){//向除了中立星球意外的星球添加飞船 每次OccupantsToSpawn个var data = Data[index];if (data.TeamOwnership == 0)return;data.Occupants += OccupantsToSpawn;Data[index] = data;}}[Inject]Planets planets;protected override JobHandle OnUpdate(JobHandle inputDeps){//指定时间运行一次 Executevar deltaTime = Time.deltaTime;spawnCounter += deltaTime;if (spawnCounter < spawnInterval)return inputDeps;spawnCounter = 0.0f;var job = new PlanetsOccupantsJob{Data = planets.Data,OccupantsToSpawn = occupantsToSpawn};return job.Schedule(planets.Length, 32, inputDeps);}}
}
解析一
设置步接参数和筛选的实体

解析二
每次更改数据相关业务逻辑,根据获取的PlanetData每次增加OccupantsToSpawn数量的飞船

解析三
赋值Data并按照规定时间执行Execute内先关的逻辑

效果如下

下面就开始写飞船有关的业务逻辑了。在写之前我先说下示例的设计思想,星球本地的飞船数量数据在不断增加,然后通过脚本,每一帧把红色或者绿色的飞船数量数据提取出来,然后放在等待实例化的数据中,再为每个飞船添加出生点、目标等信息。
创建脚本 AutoPlay添加到任意物体上 作用
- 红色或者绿色星球寻找除自己以外的攻击目标

using System.Collections.Generic;
using Data;
using Unity.Entities;
using UnityEngine;namespace Other
{public class AutoPlay : MonoBehaviour{/// <summary>/// 攻击间隔/// </summary>[SerializeField]float attackInterval = 0.1f;/// <summary>/// 攻击计时器/// </summary>[SerializeField]float attackCountdown = 0.1f;/// <summary>/// 所有星球物体/// </summary>GameObject[] planets;EntityManager entityManager { get; set; }public void Start(){//获取场景中所有的Planetplanets = GameObject.FindGameObjectsWithTag("Planet");entityManager = World.Active.GetOrCreateManager<EntityManager>();}public void Update(){//飞船攻击倒计时attackCountdown -= Time.deltaTime;if (attackCountdown > 0.0f)return;attackCountdown = attackInterval;if(planets.Length <= 1)Debug.LogError("没有发现任何星球!!!");//随机获取飞船索引var sourcePlanetIndex = Random.Range(0, planets.Length);var sourcePlanetEntity = planets[sourcePlanetIndex].GetComponent<GameObjectEntity>().Entity;if(!entityManager.Exists(sourcePlanetEntity)){//可以在场景卸载过程中发生enabled = false;return;}//获取对应星球的 PlanetData 数据var planetData = PlanetUtility.GetPlanetData(sourcePlanetEntity, entityManager);//防止找到的是【0队伍】的星球while (planetData.TeamOwnership == 0){//随机获取星球列表的索引sourcePlanetIndex = Random.Range(0, planets.Length);sourcePlanetEntity = planets[sourcePlanetIndex].GetComponent<GameObjectEntity>().Entity;if(!entityManager.Exists(sourcePlanetEntity)){// Can happen during scene unloadenabled = false;return;}planetData = PlanetUtility.GetPlanetData(sourcePlanetEntity, entityManager);}var targetPlanetIndex = Random.Range(0, planets.Length);//防止找到的攻击目标星球是自己while (targetPlanetIndex == sourcePlanetIndex){targetPlanetIndex = Random.Range(0, planets.Length);}PlanetUtility.AttackPlanet(planets[sourcePlanetIndex], planets[targetPlanetIndex], entityManager);}}
}
解析一
这部分逻辑主要有两部分循环遍历,第一部分是随机找到一个红色或者绿色要攻击别的人的星球
然后找到需要攻击的星球, 第二部遍历的主要作用是防止攻击的目标是自己

然后创建PlanetUtility脚本 作用
- 对准备发动攻击的飞船添加相关数据
- ** 获取星球对应的PlanetData数据**
using System.Linq;
using Data;
using Unity.Entities;
using UnityEngine;namespace Other
{/// <summary>/// Some shared functionality between AutoPlay and UserInputSystem/// 自动布局和用户输入系统之间的一些共享功能/// </summary>public static class PlanetUtility{/// <summary>/// 攻击设定/// </summary>/// <param name="fromPlanet">发射飞船的星球</param>/// <param name="toPlanet">需要攻击的星球</param>/// <param name="entityManager"></param>public static void AttackPlanet(GameObject fromPlanet, GameObject toPlanet, EntityManager entityManager){//获取发射星球的GameObjectEntityvar entity = fromPlanet.GetComponent<GameObjectEntity>().Entity;//获取渲染星球对应的GameObjectEntityvar meshComponent = fromPlanet.GetComponentsInChildren<GameObjectEntity>().First(c => c.gameObject != fromPlanet.gameObject);//获取 PlanetDatavar occupantData = entityManager.GetComponentData<PlanetData>(entity);//获取需要攻击星球的GameObjectEntityvar targetEntity = toPlanet.GetComponent<GameObjectEntity>().Entity;//飞船发射数据var launchData = new PlanetShipLaunchData{//目标星球TargetEntity = targetEntity,//设定队伍TeamOwnership = occupantData.TeamOwnership,//设定飞船数量NumberToSpawn = occupantData.Occupants,//设定产卵的位置SpawnLocation = fromPlanet.transform.position,//产卵半径(直径*0.5f)SpawnRadius = meshComponent.transform.lossyScale.x * 0.5f};//发射完飞船数量剩余0occupantData.Occupants = 0;//重新赋值entityManager.SetComponentData(entity, occupantData);//向发射星球的entity上设定 飞船发射数据 有就更改 没有则添加if (entityManager.HasComponent<PlanetShipLaunchData>(entity)){entityManager.SetComponentData(entity, launchData);return;}entityManager.AddComponentData(entity, launchData);}/// <summary>/// 获取星球对应的PlanetData数据/// </summary>/// <param name="planet">对应星球</param>/// <param name="entityManager"></param>/// <returns></returns>public static PlanetData GetPlanetData (GameObject planet, EntityManager entityManager){var entity = planet.GetComponent<GameObjectEntity>().Entity;var data = GetPlanetData(entity, entityManager);return data;}/// <summary>/// 获取星球对应的PlanetData数据/// </summary>/// <param name="entity">星球对应的 entity</param>/// <param name="entityManager"></param>/// <returns></returns>public static PlanetData GetPlanetData(Entity entity, EntityManager entityManager){return entityManager.GetComponentData<PlanetData>(entity);}}
}
解析 一
根据fromPlanet与toPlanet两个GameObject获取对应的Entity和他们身上含有的纯数据

解析二
根据提取出来的数据为新建纯数据 PlanetShipLaunchData 赋值,他里面含有飞船从创建到攻击用到的一切数据,然后对初始化完的PlanetShipLaunchData数据添加到对应的星球(entity)上

效果如下

在这里要特别说名一下,如果一个系统脚本中的Inject注入属性没有完成,是不会执行OnStartRunning函数的执行顺序如下
using System;
using Data;
using Other;
using Unity.Collections;
using Unity.Entities;
using Unity.Transforms;
using Unity.Mathematics;
using Unity.Rendering;
using UnityEngine;
using Random = UnityEngine.Random;namespace Systems
{public class OneSystem : ComponentSystem{public OneSystem(){Debug.Log("OneSystem");}protected override void OnCreateManager(){Debug.Log("OnCreateManager");base.OnCreateManager();}protected override void OnStartRunning(){Debug.Log("OnStartRunning");base.OnStartRunning();}protected override void OnUpdate(){Debug.Log("OnUpdate");}protected override void OnStopRunning(){Debug.Log("OnStopRunning");base.OnStopRunning();}protected override void OnDestroyManager(){Debug.Log("OnDestroyManager");base.OnDestroyManager();}}
}

现在我们已经明确知道了出发地点和攻击目标,接下来我们就开始实例化飞船,创建ShipSpawnSystem脚本
- 作用实例化飞船
using System;
using Data;
using Other;
using Unity.Collections;
using Unity.Entities;
using Unity.Transforms;
using Unity.Mathematics;
using Unity.Rendering;
using UnityEngine;
using Random = UnityEngine.Random;namespace Systems
{// [UpdateAfter(typeof(UserInputSystem))]public class ShipSpawnSystem : ComponentSystem{public ShipSpawnSystem(){_entityManager = World.Active.GetOrCreateManager<EntityManager>();}//产卵的星球struct SpawningPlanets{public readonly int Length;public ComponentDataArray<PlanetShipLaunchData> Data;}/// <summary>/// 飞船产卵需要的数据 所在的星球 和这个星球上飞船的信息/// </summary>struct ShipSpawnData{public PlanetShipLaunchData PlanetShipLaunchData;public PlanetData TargetPlanetData;public int ShipCount;}protected override void OnCreateManager(){_shipsToSpawn = new NativeList<ShipSpawnData>(Allocator.Persistent);//产生持续的数据}protected override void OnDestroyManager(){_shipsToSpawn.Dispose();}protected override void OnStartRunning(){_prefabManager = GameObject.FindObjectOfType<PrefabManager>();if (_shipRenderer != null)return;//找到飞船的渲染var prefabRenderer = _prefabManager.ShipPrefab.GetComponent<MeshInstanceRendererComponent>().Value;//找到飞船的产卵器var planetSpawner = GameObject.FindObjectOfType<PlanetSpawner>();//2种颜色的飞船_shipRenderer = new MeshInstanceRenderer[planetSpawner._teamMaterials.Length];//填充渲染具体数据 网格 自发光颜色 for (var i = 0; i < _shipRenderer.Length; ++i){_shipRenderer[i] = prefabRenderer;_shipRenderer[i].material = new Material(prefabRenderer.material){color = planetSpawner._teamMaterials[i].color};_shipRenderer[i].material.SetColor("_EmissionColor", planetSpawner._teamMaterials[i].color);}base.OnStartRunning();}/// <summary>/// 产卵星集合 注入没有完成对应的OnStartRunning不会触发/// </summary>[Inject] SpawningPlanets _planets;/// <summary>/// 飞船预制体/// </summary>PrefabManager _prefabManager;EntityManager _entityManager;//待生产飞船的队列NativeList<ShipSpawnData> _shipsToSpawn;MeshInstanceRenderer[] _shipRenderer;protected override void OnUpdate(){//遍历所有产卵星球for (var planetIndex = 0; planetIndex < _planets.Length; planetIndex++){//获取需要产卵需要的数据var planetLaunchData = _planets.Data[planetIndex];if (planetLaunchData.NumberToSpawn == 0){continue;}//获取飞船数量var shipsToSpawn = planetLaunchData.NumberToSpawn;//为了实例化飞船时出现卡顿,设定一个阈值var dt = Time.deltaTime;var deltaSpawn = Math.Max(1, Convert.ToInt32(1000.0f * dt));//设定每次释放的飞船数量的最大限量if (deltaSpawn < shipsToSpawn)shipsToSpawn = deltaSpawn;//获取需要攻击的目标星球信息var targetPlanet = _entityManager.GetComponentData<PlanetData>(planetLaunchData.TargetEntity);//添加到生产的列表_shipsToSpawn.Add(new ShipSpawnData{ShipCount = shipsToSpawn,PlanetShipLaunchData = planetLaunchData,TargetPlanetData = targetPlanet});//更新使用过的PlanetShipLaunchData数据var launchData = new PlanetShipLaunchData{TargetEntity = planetLaunchData.TargetEntity,//剩余的飞船数量NumberToSpawn = planetLaunchData.NumberToSpawn - shipsToSpawn,TeamOwnership = planetLaunchData.TeamOwnership,SpawnLocation = planetLaunchData.SpawnLocation,SpawnRadius = planetLaunchData.SpawnRadius};_planets.Data[planetIndex] = launchData;}//遍历需要生产的飞船的列表for (int spawnIndex = 0; spawnIndex < _shipsToSpawn.Length; ++spawnIndex){//生产飞船的数量var spawnCount = _shipsToSpawn[spawnIndex].ShipCount;//生成飞船位置等信息var planet = _shipsToSpawn[spawnIndex].PlanetShipLaunchData;//这批飞船的目标var targetPlanet = _shipsToSpawn[spawnIndex].TargetPlanetData;//生产飞船所在星球的位置var planetPos = planet.SpawnLocation;//与目标星球的距离var planetDistance = Vector3.Distance(planetPos, targetPlanet.Position);//生产的半径var planetRadius = planet.SpawnRadius;//实例化飞船var prefabShipEntity = _entityManager.Instantiate(_prefabManager.ShipPrefab);//添加渲染数据(哪个队伍,颜色不同)_entityManager.SetSharedComponentData(prefabShipEntity, _shipRenderer[planet.TeamOwnership]);var entities = new NativeArray<Entity>(spawnCount, Allocator.Temp);//实例化这批需要生产的飞船(一次性生产指定个数)_entityManager.Instantiate(prefabShipEntity, entities);//删掉原型_entityManager.DestroyEntity(prefabShipEntity);//对这个批次所有产生的飞船出生位置操作for (int i = 0; i < spawnCount; i++){//飞船出生点float3 shipPos;do{var insideCircle = Random.insideUnitCircle.normalized;//转换float3 因为数据更小var onSphere = new float3(insideCircle.x, 0, insideCircle.y);shipPos = planetPos + (onSphere * (planetRadius + _prefabManager.ShipPrefab.transform.localScale.x));} while (math.lengthsq(shipPos - planetPos) > planetDistance * planetDistance);var data = new ShipData{TargetEntity = planet.TargetEntity,TeamOwnership = planet.TeamOwnership};_entityManager.AddComponentData(entities[i], data);var spawnPosition = new Position{Value = shipPos};var spawnScale = new Scale{Value = new float3(1.0f, 1.0f, 1.0f)// Value = new float3(0.02f, 0.02f, 0.02f)};_entityManager.SetComponentData(entities[i], spawnScale);_entityManager.SetComponentData(entities[i], spawnPosition);}entities.Dispose();}_shipsToSpawn.Clear();}}}
解析一
准备相应的数据模板并初始化,其中NativeList是unity自定义的泛型List,且显示泛型为Struct

解析二
初始化相关属性参数,为后面的实例化做准别

解析三
限制每次实例化数量并把需要实例化的数据添加到生产列表

解析四
更新使用过的PlanetShipLaunchData数据

解析五
遍历生产清单开始实例化

解析六
设定每个飞船的位置

效果如下

Unity 之 Pure版Entity Component System 案例【GalacticConquest】解析【下】
这篇关于Unity 之 Pure版Entity Component System【ECS】 案例【GalacticConquest】解析【上】的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!