Unity 之 Pure版Entity Component Sy
以往没有了解过Unity ECS的小伙伴建议先看看我写过的两篇ECS文章
最近两月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;
//获取星球上对应的 GameObjectEntity
var planetEntity = go.GetComponent<GameObjectEntity>().Entity;
//获取渲染星球的对应的子物体
var meshGo = go.GetComponentsInChildren<Transform>().First(c => c.gameObject != go).gameObject;
//获取碰撞体
var collider = go.GetComponent<SphereCollider>();
//获取渲染星球的对应的子物体的 GameObjectEntity
var 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);
}
//设置字典对应的GameObject
entities[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)
{
//指定时间运行一次 Execute
var 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()
{
//获取场景中所有的Planet
planets = 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 unload
enabled = 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)
{
//获取发射星球的GameObjectEntity
var entity = fromPlanet.GetComponent<GameObjectEntity>().Entity;
//获取渲染星球对应的GameObjectEntity
var meshComponent = fromPlanet.GetComponentsInChildren<GameObjectEntity>().First(c => c.gameObject != fromPlanet.gameObject);
//获取 PlanetData
var occupantData = entityManager.GetComponentData<PlanetData>(entity);
//获取需要攻击星球的GameObjectEntity
var 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
};
//发射完飞船数量剩余0
occupantData.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数据
解析五
遍历生产清单开始实例化
解析六
设定每个飞船的位置