Unity 之 Pure版Entity Component System【ECS】 案例【GalacticConquest】解析【上】

本文涉及的产品
云服务器 ECS,每月免费额度280元 3个月
云服务器ECS,u1 2核4GB 1个月
简介: 以往没有了解过Unity ECS的小伙伴建议先看看我写过的两篇ECS文章Unity之浅析 Entity Component System (ECS)Unity 之 Pure版Entity Component System (ECS) 官方R...

以往没有了解过Unity ECS的小伙伴建议先看看我写过的两篇ECS文章

最近两月Unity官方一直在更新ECS的版本,有一些原来的工程在新的版本中是无法运行的,所以今天再写一篇示例解析,虽然ECS目前是测试版本,可能还会有很多的改变,正式版本上线的日期也没有明确说明,但还是希望能帮助喜欢新技术的小伙伴,互相帮助,互相学习~


有说的不准确和不正确的地方欢迎留言指正

大家的帮助是我写下去最有效的动力


点击下载工程

示例效果展示如下

img_3aa48488468484032738dbed0ed63956.gif

这个示例的规则是这样的,启动时随机生成大小位置不同的球体,然后从球体周围发射小飞船去攻击其他的星球,飞船分为红绿两队,占领后星球变成指定队伍的颜色


此次使用的Unity版本为 2018.2.9f1 Entities版本为0.0.12-preview.15。而且在启动Unity加载Entities的时候保持网路畅通,因为有朋友反映在内网无法使用Entities的情况。【最好能科学上网】

img_0dee20f4b38a5d561e5a75583f7cb9c1.png
img_5f8fc9e53cdc722d896baa2fbe8dd7ce.png

下面还是按照老规矩,分布逐渐创建工程

我们先创建一个PlanetSpawner脚本(产卵器)并添加到空物体Spawners上

img_ad7f58ee98d8917290a2100c5ed9e59b.png

作用如下:

  • 创建指定的数量的星球
  • 使星球位置随机分布
  • 向这些星球上动态添加数据
    • 星球旋转的数据
    • 星球旋所在的队伍、飞船数量、位置、半径数据(用于后期生产飞船使用)
  • 更改对应星球队伍的颜色
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的控制者、容器

img_2172c35828aae0f2e6249b1f9a5c4683.png

解读二

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

img_c63e2e3dda2484372cd98c8dc56432ec.png

解读三

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

img_1e9858f9853632ee439fe64b986b3785.png

解读四

常规实例化,不在熬述


img_dd4b620bb88a6cd6ccad7ca762f8c8ab.png

解读五

img_4d78a041f72ec5c08a65f43014b11420.png

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

效果如下

img_afb85d56c1ab07004129a7b43e8e6d9c.gif

创建OccupantsTextUpdater脚本并添加到对应的Text上,他的作用是

  • 更新所在星球上含有的飞船数量并显示
img_154f7aaf06c92c86f7f6168b7b0a3cf7.png
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();
        }
    }
}
img_1880df086105dc450e8a77a0cde58ed3.png

创建一个旋转系统,作用

  • 让每一个星球转动起来
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);
        }
    }
}

解析一

设置步接参数和筛选的实体

img_8588eaa8cbf739ddb54c088c089287ad.png

解析二

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

img_a8e38a04ae1e5d4da88924fba0b37394.png

解析三

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

img_6b306c2b95eb2462cf08e0c53d761f7c.png

效果如下

img_4fa6cc6b9ecdc47c3721faea827d8c57.gif

下面就开始写飞船有关的业务逻辑了。在写之前我先说下示例的设计思想,星球本地的飞船数量数据在不断增加,然后通过脚本,每一帧把红色或者绿色的飞船数量数据提取出来,然后放在等待实例化的数据中,再为每个飞船添加出生点、目标等信息。

创建脚本 AutoPlay添加到任意物体上 作用

  • 红色或者绿色星球寻找除自己以外的攻击目标
img_7dbf228666fdd4dba5f3a0030f74e5fa.png
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);
        }

    }
}

解析一

这部分逻辑主要有两部分循环遍历,第一部分是随机找到一个红色或者绿色要攻击别的人的星球
然后找到需要攻击的星球, 第二部遍历的主要作用是防止攻击的目标是自己

img_118cf4bca89d63dbbeccbddbe01c63e1.png

然后创建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和他们身上含有的纯数据

img_e9214f11f6b77fd8a67b57e833b776e3.png

解析二

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

img_53ba582d04253bc9f1dc3e0785a0d567.png

效果如下

img_e1a02f2a89f434553b9f51cb50883c32.gif

在这里要特别说名一下,如果一个系统脚本中的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();
        }
    }
}
img_53e822cbd857b65a6ea9c99b994258a2.png

现在我们已经明确知道了出发地点和攻击目标,接下来我们就开始实例化飞船,创建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

img_f9864f9f309fa2635bd0ae37d6637d59.png

解析二

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

img_5d22b284f0700cd94f801aff8f26c387.png

解析三

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

img_c8c1da51f64c10be1f4e22e6bb41f183.png

解析四

更新使用过的PlanetShipLaunchData数据

img_cc393fcaff80dc03ffdb4cb5c7697910.png

解析五

遍历生产清单开始实例化

img_33f9c64b929bc0a3cd84a15deffcea32.png

解析六

设定每个飞船的位置

img_1b84f87c3bf19d86c53d5bbe21e1a781.png

效果如下

img_5b2ff598be072a590d6f96aa443bec20.gif

Unity 之 Pure版Entity Component System 案例【GalacticConquest】解析【下】

相关实践学习
一小时快速掌握 SQL 语法
本实验带您学习SQL的基础语法,快速入门SQL。
7天玩转云服务器
云服务器ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,可降低 IT 成本,提升运维效率。本课程手把手带你了解ECS、掌握基本操作、动手实操快照管理、镜像管理等。了解产品详情:&nbsp;https://www.aliyun.com/product/ecs
相关文章
|
2月前
|
弹性计算 Ubuntu Windows
2024年部署幻兽帕鲁/Palworld服务器多少钱?阿里云帕鲁主机优惠价格解析
对于热爱《幻兽帕鲁》的玩家们来说,一个稳定、高效的游戏服务器是畅享游戏乐趣的关键。那么,搭建一个这样的服务器需要多少钱呢?别担心,阿里云已经为大家准备了超值的幻兽帕鲁Palworld游戏服务器!
|
2月前
|
弹性计算 固态存储 Linux
阿里云上Palworld/幻兽帕鲁服务器搭建全解析:超详细步骤,轻松掌握
想要在阿里云上轻松开服玩《幻兽帕鲁》吗?跟着我们的步骤来,简单几步就能搞定!
|
2月前
|
弹性计算 Ubuntu Linux
新手也能玩转幻兽帕鲁联机服务器:Palworld/幻兽帕鲁搭建攻略全解析
随着《幻兽帕鲁》的持续火爆,越来越多的玩家希望与好友在这款游戏中共同冒险。为了实现这一愿望,搭建一个属于自己的《幻兽帕鲁》服务器成为不少玩家的首选。今天,就为大家带来一篇关于如何轻松搭建《幻兽帕鲁》服务器的完整攻略,即使你是新手小白,也能轻松上手!
21 0
|
2月前
|
弹性计算 Ubuntu Linux
2024年Palworld/幻兽帕鲁服务器自建手册:详细步骤解析与设置指南
爆款游戏《幻兽帕鲁》是很多玩家在与好友开黑时的首选,因为《幻兽帕鲁》有着十分丰富的游戏内容,玩家在联机游玩《幻兽帕鲁》时能够获得非常多的快乐。 但在《幻兽帕鲁》进行联机时,是需要自行搭建服务器的,下面就带来,最新《幻兽帕鲁》服务器设置全步骤大全,方便玩家更好的进行联机游玩。 以下就是幻兽帕鲁服务器自建:幻兽帕鲁服务器设置全步骤大全的相关内容。
49 3
|
2月前
|
存储 弹性计算 安全
2024阿里云服务器ECS全方位解析_云主机详解
2024阿里云服务器ECS全方位解析_云主机详解,阿里云服务器是什么?云服务器ECS是一种安全可靠、弹性可伸缩的云计算服务,云服务器可以降低IT成本提升运维效率,免去企业或个人前期采购IT硬件的成本,阿里云服务器让用户像使用水、电、天然气等公共资源一样便捷、高效地使用服务器
|
2月前
|
弹性计算 大数据 测试技术
阿里云服务器服务费怎么算的?详细解析
2024年阿里云服务器租用价格表更新,云服务器ECS经济型e实例2核2G、3M固定带宽99元一年、ECS u1实例2核4G、5M固定带宽、80G ESSD Entry盘优惠价格199元一年,轻量应用服务器2核2G3M带宽轻量服务器一年61元、2核4G4M带宽轻量服务器一年165元12个月、2核4G服务器30元3个月,幻兽帕鲁4核16G和8核32G服务器配置,云服务器ECS可以选择经济型e实例、通用算力u1实例、ECS计算型c7、通用型g7、c8i、g8i等企业级实例规格
|
2月前
|
存储 弹性计算 固态存储
阿里云服务器租用费用1t空间多少钱?全面解析
阿里云服务器租用费用1t空间多少钱?1T空间如果是系统盘SSD云盘价格是3686元一年、ESSD云盘1t空间是5222元一年,ESSD Entry云盘1024G存储空间价格是2580元一年。阿里云百科整理几款不同的云盘1t空间价格
|
2月前
|
弹性计算 大数据 测试技术
阿里云服务器服务费怎么计算?详细解析2024新版
阿里云服务器服务费怎么计算?详细解析2024新版,云服务器ECS经济型e实例2核2G、3M固定带宽99元一年、ECS u1实例2核4G、5M固定带宽、80G ESSD Entry盘优惠价格199元一年,轻量应用服务器2核2G3M带宽轻量服务器一年61元、2核4G4M带宽轻量服务器一年165元12个月、2核4G服务器30元3个月
47 1
|
2月前
|
前端开发 网络协议 JavaScript
|
2月前
|
域名解析 缓存 网络协议
【域名解析】如何将域名指向对应服务器IP
【域名解析】如何将域名指向对应服务器IP
342 1

推荐镜像

更多