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

书接上回:Unity 之 Pure版Entity Component System 案例【GalacticConquest】解析【上】

点击下载工程

我们已经完成飞船的实例化,下面就是让飞船动起来~~~

创建脚本ShipMovementSystem飞船的运动系统,作用:

  • 让飞船朝着目标移动
  • 达到目标是添加标签
using Data;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Transforms;
using UnityEngine;
using Unity.Mathematics;

namespace Systems
{
    [UpdateAfter(typeof(ShipArrivalSystem))]
    public class ShipMovementSystem : JobComponentSystem
    {
        //所有飞船
        struct Ships
        {
            public readonly int Length;
            public ComponentDataArray<Position> Positions;
            public ComponentDataArray<Rotation> Rotations;
            public ComponentDataArray<ShipData> Data;
            public EntityArray Entities;
        }
        //所有星球
        struct Planets
        {
            public readonly int Length;
            public ComponentDataArray<PlanetData> Data;
        }
        /// <summary>
        /// 计算飞船位移 多核心运算
        /// </summary>
        [BurstCompile]
        struct CalculatePositionsJob : IJobParallelFor
        {
            public float DeltaTime;
            [ReadOnly]
            public ComponentDataArray<ShipData> Ships;
            [ReadOnly] public EntityArray Entities;
            public ComponentDataArray<Position> Positions;
            public ComponentDataArray<Rotation> Rotations;

            [ReadOnly] public ComponentDataArray<PlanetData> Planets;
            [ReadOnly] public ComponentDataFromEntity<PlanetData> TargetPlanet;

            //并发版本 因为使用的是IJobParallelFor
            public NativeQueue<Entity>.Concurrent ShipArrivedQueue;

            public void Execute(int index)
            {
                //飞船的数据
                var shipData = Ships[index];
                //对应需要攻击星球的位置
                var targetPosition = TargetPlanet[shipData.TargetEntity].Position;
                //飞船的位置
                var position = Positions[index];
                //飞船的角度
                var rotation = Rotations[index];

                //逐渐靠近需要攻击的星球
                var newPos = Vector3.MoveTowards(position.Value, targetPosition, DeltaTime * 4.0f);
                //逐一遍历所有找到的星球
                for (var planetIndex = 0; planetIndex < Planets.Length; planetIndex++)
                {
                    var planet = Planets[planetIndex];
                    //如果与星球的距离小于半径
                    if (Vector3.Distance(newPos, planet.Position) < planet.Radius)
                    {
                        //判断这个是不是需要攻击的星球
                        if (planet.Position == targetPosition)
                        {
                            //添加到飞船队列里面
                            ShipArrivedQueue.Enqueue(Entities[index]);
                        }
                        //防止飞船进入到星球内部,重新计算。很精确啊~
                        var direction = (newPos - planet.Position).normalized;
                        newPos = planet.Position + (direction * planet.Radius);
                        break;
                    }
                }
                //计算飞船朝向
                var shipCurrentDirection = math.normalize((float3)newPos - position.Value);
                rotation.Value = quaternion.LookRotation(shipCurrentDirection, math.up());
                //将计算完的结果赋值
                position.Value = newPos;
                Positions[index] = position;
                Rotations[index] = rotation;
            }
        }
        //单核心运算 IJob允许您安排与其他作业和主线程并行运行的单个作业
        struct ShipArrivedTagJob : IJob
        {
            public EntityCommandBuffer EntityCommandBuffer;
            public NativeQueue<Entity> ShipArrivedQueue;

            public void Execute()
            {
                Entity entity;
                while (ShipArrivedQueue.TryDequeue(out entity))
                {
                    //添加已经到达指定星球的标记
                    EntityCommandBuffer.AddComponent(entity, new ShipArrivedTag());
                }
            }
        }

        [Inject]
        EndFrameBarrier m_EndFrameBarrier;

        [Inject]
        Ships m_Ships;    //所有飞船
        [Inject]
        Planets m_Planets;//所有星球

        NativeQueue<Entity> m_ShipArrivedQueue;

        protected override void OnCreateManager()
        {
            m_ShipArrivedQueue = new NativeQueue<Entity>(Allocator.Persistent);
        }

        protected override void OnDestroyManager()
        {
            m_ShipArrivedQueue.Dispose();
            base.OnDestroyManager();
        }

        protected override JobHandle OnUpdate(JobHandle inputDeps)
        {
            if (m_Ships.Length == 0)
                return inputDeps;

            //在 IJobParallelFor 中的逻辑
            var handle = new CalculatePositionsJob
            {
                Ships = m_Ships.Data,
                Planets = m_Planets.Data,
                TargetPlanet = GetComponentDataFromEntity<PlanetData>(),
                DeltaTime = Time.deltaTime,
                Entities = m_Ships.Entities,
                Positions = m_Ships.Positions,
                Rotations = m_Ships.Rotations,
                ShipArrivedQueue = m_ShipArrivedQueue.ToConcurrent()//并发
            }.Schedule(m_Ships.Length, 32, inputDeps);

            //在 IJob 中执行的逻辑
            handle = new ShipArrivedTagJob
            {
                //自动执行的队列
                EntityCommandBuffer = m_EndFrameBarrier.CreateCommandBuffer(),
                ShipArrivedQueue = m_ShipArrivedQueue
            }.Schedule(handle);

            return handle;
        }
    }
}

解析一

筛选指定的飞船和星球实体

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

解析二

因为是IJobParalleFor,所以队列用的事并行版本

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

解析三

通过多核并发计算出飞船的位移,然后对接触到目标的飞船添加到已经到达的队列中

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

解析四

单核心处理需要添加到达标记的队列

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

解析五

需要注意的事声明时不是NativeQueue<Entity>.Concurrent这种带有【Concurrent】类型需要多核心并行执行时需要在赋值时用【.ToConcurrent()】转换下。

EntityCommandBuffer我理解的是这种数据结构,适合并发且安全的不断改变数据长度的缓冲。且可有效较少因为不断的改变造成GC。

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

创建脚本 ShipMovementSystem ,作用:

  • 处理被添加ShipArrivedTag的飞船
  • 处理星球被攻击到达一定程度时的转换颜色逻辑
using Data;
using Unity.Collections;
using Unity.Entities;
using Unity.Rendering;

namespace Systems
{
    public class ShipArrivalSystem : ComponentSystem
    {
        EntityManager _entityManager;

        //初始化
        public ShipArrivalSystem()
        {
            _entityManager = World.Active.GetOrCreateManager<EntityManager>();
        }

        //到达星球的飞船
        struct Ships
        {
            public readonly int Length;
            public ComponentDataArray<ShipData> Data;
            public EntityArray Entities;
            public ComponentDataArray<ShipArrivedTag> Tag;
        }
        [Inject]
        Ships _ships;

        protected override void OnUpdate()
        {
            if (_ships.Length == 0)
                return;

            var arrivingShipTransforms = new NativeList<Entity>(Allocator.Temp);
            var arrivingShipData = new NativeList<ShipData>(Allocator.Temp);

            for (var shipIndex = 0; shipIndex < _ships.Length; shipIndex++)
            {
                var shipData = _ships.Data[shipIndex];
                var shipEntity = _ships.Entities[shipIndex];
                arrivingShipData.Add(shipData);
                arrivingShipTransforms.Add(shipEntity);
            }

            HandleArrivedShips(arrivingShipData, arrivingShipTransforms);

            arrivingShipTransforms.Dispose();
            arrivingShipData.Dispose();
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="arrivingShipData">到达飞船的数据集合</param>
        /// <param name="arrivingShipEntities">到达飞船的实体集合</param>
        void HandleArrivedShips(NativeList<ShipData> arrivingShipData, NativeList<Entity> arrivingShipEntities)
        {
            //逐一遍历所有飞船数据
            for (var shipIndex = 0; shipIndex < arrivingShipData.Length; shipIndex++)
            {

                var shipData = arrivingShipData[shipIndex];
                //获取对应飞船需要攻击的星球
                var planetData = _entityManager.GetComponentData<PlanetData>(shipData.TargetEntity);

                //不同队伍减少
                if (shipData.TeamOwnership != planetData.TeamOwnership)
                {
                    planetData.Occupants = planetData.Occupants - 1;
                    if (planetData.Occupants <= 0)
                    {
                        //本地飞船没有时转换队伍
                        planetData.TeamOwnership = shipData.TeamOwnership;
                        PlanetSpawner.SetColor(shipData.TargetEntity, planetData.TeamOwnership);
                    }
                }
                else//相同队伍相加
                {
                    planetData.Occupants = planetData.Occupants + 1;
                }
                //星球重新赋值
                _entityManager.SetComponentData(shipData.TargetEntity, planetData);
            }
            //删除这些已经到达的飞船
            _entityManager.DestroyEntity(arrivingShipEntities);
        }
    }
}

解析一

获取所有到达飞船的实体集合 与对应的数据集合

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

解析二

根据得到的数据集合判断到达的星球是否为同一队伍,相同增加飞船,不同减少飞船,星球的飞船小于0变换队伍和颜色

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

为了增加互动性,添加脚本UserInputSystem

  • 左键点击需要发射的星球
  • 右键点击需要攻击的星球
  • 点击空白处取消
using System.Collections.Generic;
using System.Linq;
using Data;
using Other;
using Unity.Entities;
using UnityEngine;

namespace Systems
{

    public class UserInputSystem : ComponentSystem
    {
        struct Planets
        {
            public readonly int Length;
            public ComponentDataArray<PlanetData> Data;
        }
        [Inject] Planets planets;

        //添加?是表示可空类型,可自行Google
        Dictionary<GameObject, PlanetData?> FromTargets = new Dictionary<GameObject, PlanetData?>();
        GameObject ToTarget = null;

        EntityManager _entityManager;

        /// <summary>
        /// 构造函数中初始化 EntityManager
        /// </summary>
        public UserInputSystem()
        {
            _entityManager = World.Active.GetOrCreateManager<EntityManager>();
        }

        protected override void OnUpdate()
        {
            if (Input.GetMouseButtonDown(0))
            {
                var planet = GetPlanetUnderMouse();
                if (planet == null)
                {
                    FromTargets.Clear();
                    Debug.Log("点击外部,我们清除了选择");
                }
                else
                {
                    if (FromTargets.ContainsKey(planet))
                    {
                        Debug.Log("取消选择的目标Deselecting from target " + planet.name);
                        FromTargets.Remove(planet);
                    }
                    else
                    {
                        var data = PlanetUtility.GetPlanetData(planet, _entityManager);
                        //原来的目标
                        var previousTarget = FromTargets.Values.FirstOrDefault();
                        if ((previousTarget == null || previousTarget.Value.TeamOwnership == data.TeamOwnership) && data.TeamOwnership != 0)
                        {
                            Debug.Log("选择的目标 " + planet.name);
                            FromTargets[planet] = data;
                            Debug.Log("数量:" + FromTargets.Count);
                        }
                        else
                        {
                            if (data.TeamOwnership == 0)
                            {
                                Debug.LogWarning("不能设置中性行星");
                            }
                            else
                            {
                                Debug.Log("从目标中添加行星,但是清除之前的列表,因为它是一个不同的团队");
                                FromTargets.Clear();
                                FromTargets[planet] = data;
                            }
                        }

                    }
                }

            }
            if (Input.GetMouseButtonDown(1))
            {
                var planet = GetPlanetUnderMouse();
                if (planet == null)
                {
                    Debug.Log("取消选中目标 ");
                    ToTarget = null;
                }
                else
                {
                    if (!FromTargets.Any())
                    {
                        Debug.Log("没有任何选中的发射星球");
                        return;
                    }
                    Debug.Log($"需要攻击的星球名称为{planet.name}" );
                    ToTarget = planet;
                    foreach (var p in FromTargets.Keys)
                    {
                        if (p == ToTarget)
                            continue;
                        PlanetUtility.AttackPlanet(p, ToTarget, _entityManager);

                    }
                }
            }
        }

        GameObject GetPlanetUnderMouse()
        {
            RaycastHit hit;
            var ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            if (Physics.Raycast(ray, out hit, Mathf.Infinity, 1 << LayerMask.NameToLayer("Planet")))
            {
                return hit.collider.transform.gameObject;
            }
            return null;
        }
    }
}

为了保证逻辑能够按照指定顺序执行:添加顺序特性UpdateAfter
UserInputSystem--->ShipSpawnSystem
ShipArrivalSystem--->ShipMovementSystem

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

最终效果

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

打完收工~~~

Unity 之 Pure版Entity Component System【ECS】 案例【GalacticConquest】解析【下】
上一篇:SQL Server 性能优化之——重复索引


下一篇:ecshop 自定义 价格区间搜索