目录

创建主角

3:主角移动和摄像机跟随

4:实现敌人角色

5:子弹,死亡,机器人

6:粒子与音效


这次我们来使用Unity ECS系统制作一个俯视角度的射击游戏。虽然现在网上有不少ECS的资料和项目,但是制作时又和实际游戏需求有较大差距。在制作这个小游戏的过程中我遇到了很多ECS特有的问题,也给出了还可以的解决方案,相信能通过实例让大家了解到ECS的优缺点是什么。

(文章不会再解释Unity DOTS的一些基本概念,感兴趣的朋友可以查阅文档了解)。

本游戏具体玩法如下:

1:完全使用键盘控制,WASD键控制角色方向移动,j 键控制射击。(这样做主要为了简化游戏输入逻辑)

2:玩家有手枪和霰弹枪两种武器形态,按Q切换。

3:当敌人低于一定量,会在玩家一定距离周围生成敌人。敌人会朝玩家移动并射击玩家。

4:玩家和敌人都有生命值,中弹后生命减少,减为0的时候死亡

这里放下Unity和相关Package版本,以免误导后来者:

Unity 版本:Unity2020.3.3f1,Universal Render Pipeline

Hybrid Renderer: Version 0.11.0-preview.44

Unity Physics: Version 0.6.0-preview.3

Jobs:  Version 0.8.0-preview.23

Entities: 0.17.0-preview.41

创建主角

先简单搭建场景,再创建主角。

首先建一个平面,扔上贴图,再建个圆圆胖胖的主角,添加物理组件Physics Shape 和Physics Body:

Physic Shape的碰撞框同样可以在场景中进行编辑,你也可以点击Fit to Enabled Meshs来直接适配:

以及实体转换组件,Convert To Entity:

我们需要其中主角能被敌人的子弹打中并获取碰撞事件,所以点击Collision Response,选择Raise Trigger Event ( 开启触发器事件),并点击PhysicBody的Motion Type,选择Kinematic :

3:主角移动和摄像机跟随

首先为主角创建一个Component,包含初始速度:

using Unity.Entities;

[GenerateAuthoringComponent]
public struct Character :IComponentData
{
    public float speed;
}

将组件挂到主角身上,speed设为10,再单独创建一个System,控制主角移动:

using Unity.Entities;
using Unity.Jobs;
using Unity.Transforms;
using Unity.Mathematics;
using UnityEngine;

public class CharacterSystem : SystemBase
{
    protected override void OnUpdate()
    {
        float deltaTime = Time.DeltaTime;
        float3 input;
        string h = "Horizontal";
        string v = "Vertical";

Entities.
            WithoutBurst().
            WithName("Player").
            ForEach((ref Translation translation, ref Rotation rotation, in Character character) =>
            {
                input.x = Input.GetAxis(h);
                input.y = 0;
                input.z = Input.GetAxis(v);
                var dir = character.speed * deltaTime * input;
                dir.y = 0;
                //令角色前方和移动方向一致
                if (math.length(input) > 0.1f)
                {
                    //Debug.Log("Dir " + dir);
                    rotation.Value = quaternion.LookRotation(math.normalize(dir), math.up());
                }
                translation.Value += dir;
            }).Run();
    }
}

相机不支持转换为Entity,所以我们还是用老办法做一个跟随脚本,通过查找包含CharacterComponent的Entity,获取其Translation,得到主角位置,进行跟随,代码如下:

using UnityEngine;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using Unity.Collections;

public class CameraController : MonoBehaviour
{
    [SerializeField] private Vector3 offset;//相机相对于玩家的位置

private Vector3 pos;
    public float speed;

private EntityManager _manager;
    private float3 tempPos;
    public Entity targetEntity;
    void Start()
    {
        _manager = World.DefaultGameObjectInjectionWorld.EntityManager;
        //定义一个查询 :查询实体必须包含Character组件和Translation组件
        var queryDescription = new EntityQueryDesc
        {
            None = new ComponentType[] { },
            All = new ComponentType[] { ComponentType.ReadOnly<Character>(), ComponentType.ReadOnly<Translation>() }
        };

EntityQuery players = _manager.CreateEntityQuery(queryDescription);
        //场景中只有主角有Character组件,所以直接获取引用
        if (players.CalculateEntityCount() != 0)
        {
            NativeArray<Entity> temp = new NativeArray<Entity>(1, Allocator.Temp);
            temp = players.ToEntityArray(Allocator.Temp);
            targetEntity = temp[0];
            temp.Dispose();
        }
        players.Dispose();

}

void Update()
    {
        if (targetEntity != Entity.Null)
        {
            if (_manager.HasComponent<Translation>(targetEntity))
            {
                tempPos = _manager.GetComponentData<Translation>(targetEntity).Value;
            }
        }

transform.position = Vector3.Lerp(transform.position, (Vector3)tempPos + offset, speed * Time.deltaTime);//调整相机与玩家之间的距离
    }
}

最后给主角手里整把枪,OK,现在主角已经能跑了:

4:实现敌人角色

敌人造型和玩家基本一致,由于玩家需要随时找到并攻击玩家角色,所以需要在定义它的Componnet 中存一个玩家Entity的引用:

using Unity.Entities;

[GenerateAuthoringComponent]
public struct Enemy : IComponentData
{
    public float speed;
    //追踪目标
    public Entity targetEntity;
}

首先我们需要在主角身旁一定范围外生成这些这些敌人,方便起见,我们可以在场景中创建一个管理类,存一个已经转换成实体的的敌人预制体,每次生成的时候直接按照这个模版生成即可,代码如下:

using UnityEngine;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;

public class FPSGameManager : MonoBehaviour
{
    public static FPSGameManager instance;
    public GameObject enemyprefab;

private EntityManager _manager;
    //blobAssetStore是一个提供缓存的类,缓存能让你对象创建时更快。
    private BlobAssetStore _blobAssetStore;
    private GameObjectConversionSettings _settings;

public Entity enemyEntity;

void Start()
    {
        instance = this;
        _manager = World.DefaultGameObjectInjectionWorld.EntityManager;
        _blobAssetStore = new BlobAssetStore();
        _settings = GameObjectConversionSettings.FromWorld(World.DefaultGameObjectInjectionWorld, _blobAssetStore);
        enemyEntity = GameObjectConversionUtility.ConvertGameObjectHierarchy(enemyprefab, _settings);
        Translation translation = new Translation
        {
            Value = float3.zero
        };
        _manager.SetComponentData(test, translation);
    }

private void OnDestroy()
    {
        _blobAssetStore.Dispose();
    }

EnemySystem负责控制敌人追踪主角,并在敌人数量少于一定量时生成新的敌人:

using Unity.Entities;
using Unity.Jobs;
using Unity.Transforms;
using Unity.Mathematics;
using UnityEngine;

public class EnemySystem : SystemBase
{
    EndSimulationEntityCommandBufferSystem endSimulationEcbSystem;
    //保存筛选出来的敌人的对象
    private EntityQuery query;
    private uint seed = 1;

protected override void OnCreate()
    {
        base.OnCreate();
        endSimulationEcbSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    }
    protected override void OnUpdate()
    {
        Unity.Mathematics.Random random = new Unity.Mathematics.Random(seed++);

float deltaTime = Time.DeltaTime;

EntityCommandBuffer ecb = endSimulationEcbSystem.CreateCommandBuffer();
        Entity template = FPSGameManager.instance.enemyEntity;
        //对所有敌人操作
        Entities.
            WithStoreEntityQueryInField(ref query).
            ForEach((Entity entity, ref Translation translation, ref Rotation rotation, ref Enemy enemy) =>
        {
            if (HasComponent<LocalToWorld>(enemy.targetEntity))
                {
                    //追踪主角
                    LocalToWorld targetl2w = GetComponent<LocalToWorld>(enemy.targetEntity);
                    float3 targetPos = targetl2w.Position;
                    translation.Value = Vector3.MoveTowards(translation.Value, targetPos, enemy.speed * deltaTime);

var targetDir = targetPos - translation.Value;
                    quaternion temp1 = quaternion.LookRotation(targetDir, math.up());
                    rotation.Value = temp1;
                }
        }).Run();

//敌人数量少于6,在主角周围新生成6个敌人
        if (query.CalculateEntityCount() < 6)
        {
            Entity characterEntity = GetSingletonEntity<Character>();
            float3 characterPos=float3.zero;
            if (characterEntity!=Entity.Null)
            {
                if (HasComponent<Translation>(characterEntity))
                {
                    Translation translation = GetComponent<Translation>(characterEntity);
                    characterPos = translation.Value;
                }
            }

for (int i = 0; i < 6; i++)
            {
                Entity temp = ecb.Instantiate(template);

#region 随机位置生成敌人
                 //略。。。详见工程
                #endregion

Translation translation = new Translation
                {
                    Value=new float3(x,characterPos.y,z)
                };
                //这里可能有疑问为何预制体组件已经有enemy的数据了,这里为何要重新赋值?
                //这是因为场景中的主角预制体要在场景运行后才能转换为Entity,并且转换时间不确定,所以等待其生成后重新赋值
                Enemy enemy = new Enemy
                {
                    speed=5f ,
                    targetEntity=characterEntity
                };

ecb.SetComponent(temp, translation);
                ecb.SetComponent(temp, enemy);
            }
        }
    }
}

点击运行,敌人也生成出来并开始工作了:

5:子弹,死亡,机器人

接下来我们要定义武器和子弹。虽然Convert to Entity会把面板的物体的子物体也转换为Entity,并在Entity Debugger中可以看到,但目前GameObject 方便的父子关系还不能在Unity ECS中使用,所以我们需要先记录枪口的位置。

首先定义武器:

using Unity.Entities;

//手枪,霰弹枪,自动模式
public enum WeaponType
{
    gun,
    shotgun,
    gunAutoshot
}
[GenerateAuthoringComponent]
public struct  Weapon : IComponentData
{
    //枪口位置
    public Entity gunPoint;
    //武器类型
    public WeaponType weaponType;
    //是否允许切换武器
    public bool canSwitch;
    //开枪间隔
    public float firingInterval;
    //用来记录每次开枪的时间
    public float shotTime;
}

接着定义子弹组件,制作子弹预制体的流程和上文一样,这里就不赘述了:

using Unity.Entities;

[GenerateAuthoringComponent]
public struct Bullet: IComponentData
{
    public float flySpeed;
}

再定义一个作删除标签功能的组件:DeleteTag,为了尽量避免频繁的结构性变化(增删组件等),我们需要在可以被删除的物体的预制件上添加这个组件,并将其lifeTime设置为1 :

using Unity.Entities;

[GenerateAuthoringComponent]
public struct DeleteTag :IComponentData
{
    public float lifeTime;
}

这样的话,我们就可以定下规则,当物体身上DeleteTag组件的lifeTime<=0时,系统会将其删除:

using Unity.Entities;
using Unity.Jobs;

public class DeleteSystem : SystemBase
{
    EndSimulationEntityCommandBufferSystem endSimulationEcbSystem;
    protected override void OnCreate()
    {
        base.OnCreate();
        endSimulationEcbSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    }
    protected override void OnUpdate()
    {
         // 请求一个ECS并且转换成可并行的
        var ecb = endSimulationEcbSystem.CreateCommandBuffer().AsParallelWriter();
        Entities
           .ForEach((Entity entity, int entityInQueryIndex, in DeleteTag deleteTag) =>
      {
          if (deleteTag.lifeTime <=0f)
          {
              ecb.DestroyEntity(entityInQueryIndex, entity);
          }
      }).ScheduleParallel();
        // 保证ECB system依赖当前这个Job
        endSimulationEcbSystem.AddJobHandleForProducer(this.Dependency);
    }
}

子弹的生命会不断减少,所以BulletSystem中需要自行对lifeTime 做减法:

using Unity.Entities;
using Unity.Jobs;
using Unity.Transforms;
using Unity.Mathematics;

public class BulletSystem : SystemBase
{
    EndSimulationEntityCommandBufferSystem endSimulationEcbSystem;
    protected override void OnCreate()
    {
        base.OnCreate();
        endSimulationEcbSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    }
    protected override void OnUpdate()
    {
        float deltaTime = Time.DeltaTime;
        var ecb = endSimulationEcbSystem.CreateCommandBuffer();
        Entities.
        ForEach(( ref Translation translation, ref DeleteTag deleteTag, in Rotation rot, in Bullet bullet) =>
        {
            //子弹向前飞行
            translation.Value += bullet.flySpeed * deltaTime * math.forward(rot.Value);
            //生命不断减少
            deleteTag.lifeTime-= deltaTime;

}).Run();
    }
}

WeaponSystem,不同枪械的子弹生命周期也不同,手枪子弹为1s,霰弹枪0.5f:

using Unity.Entities;
using Unity.Jobs;
using Unity.Transforms;
using Unity.Mathematics;
using UnityEngine;
public class WeaponSystem : SystemBase
{
    EndSimulationEntityCommandBufferSystem endSimulationEcbSystem;
    protected override void OnCreate()
    {
        base.OnCreate();
        endSimulationEcbSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();

}
    protected override void OnUpdate()
    {
        float deltaTime = Time.DeltaTime;
        float time = UnityEngine.Time.time;
        EntityCommandBuffer ecb = endSimulationEcbSystem.CreateCommandBuffer();
        Entities.
            WithoutBurst().
            ForEach((ref Weapon weapon, in Rotation rotation) =>
        {

if (weapon.weaponType == WeaponType.gunAutoshot)
            {

if (weapon.shotTime == -1f)
                {
                    weapon.shotTime = time;
                }

//  Debug.Log("当前时间" + time);
                if (time - weapon.shotTime >= weapon.firingInterval)
                {
                    weapon.shotTime = time;
                    float3 pos = new float3();
                    LocalToWorld gunPointL2w = new LocalToWorld();

if (HasComponent<LocalToWorld>(weapon.gunPoint))
                    {
                        gunPointL2w = GetComponent<LocalToWorld>(weapon.gunPoint);

Entity tempBullet = ecb.Instantiate(FPSGameManager.instance.bulletEntity);

Translation translation = new Translation
                        {
                            Value = gunPointL2w.Position
                        };
                        Rotation rot = new Rotation
                        {
                            Value = rotation.Value
                        };
                        Bullet bullet = new Bullet
                        {
                            lifetime = 2,
                            flySpeed = 20,
                        };

ecb.SetComponent(tempBullet, translation);
                        ecb.SetComponent(tempBullet, rot);
                        ecb.SetComponent(tempBullet, bullet);
                        FPSGameManager.instance.PlayShoot();
                    }
                }
                return;
            }

if (weapon.canSwitch)
            {
             //武器切换逻辑,按Q修改武器类型,详见工程
            }

#region 开枪
            if (Input.GetKeyDown(KeyCode.J))
            {
                float3 pos = new float3();
                LocalToWorld gunPointL2w = new LocalToWorld();

if (HasComponent<LocalToWorld>(weapon.gunPoint))
                {
                    gunPointL2w = GetComponent<LocalToWorld>(weapon.gunPoint);
                    pos = gunPointL2w.Position;
                }

switch (weapon.weaponType)
                {
                    case WeaponType.gun:
                        #region 手枪
                        Entity tempBullet = ecb.Instantiate(FPSGameManager.instance.bulletEntity);

//=====初始化手枪组件并赋给Entity,和上文初始化子弹逻辑相同,略========

//播放射击音效
                        FPSGameManager.instance.PlayShoot();
                        #endregion
                        break;
                    case WeaponType.shotgun:
                        #region  霰弹枪

// ====初始化子弹的translation2和bullet组件,略======

for (int i = -5; i < 5; i++)
                        {
                            Entity tempBullet2 = ecb.Instantiate(FPSGameManager.instance.bulletEntity);

#region 传统写法
                            //Quaternion q = rotation.Value;
                            //Quaternion tempRot = Quaternion.Euler(0, q.eulerAngles.y + i * 7, 0);
                            #endregion

//使用Unity.Mathematics库写法,这里默认按照弧度旋转
                            quaternion temp = math.mul( quaternion.EulerXYZ(0, i *0.1f, 0), rotation.Value) ;
                            Rotation rotation2 = new Rotation
                            {
                                Value = temp
                            };
                            ecb.SetComponent(tempBullet2, translation2);
                            ecb.SetComponent(tempBullet2, rotation2);
                            ecb.SetComponent(tempBullet2, bullet1);
                        }

FPSGameManager.instance.PlayShoot();
                        #endregion
                        break;
                    default:
                        break;
                }

}
            #endregion

}).Run();
    }
}

在主角和敌人身上分别挂上Weapon组件,主角便可以使用两种武器了,敌人也能自动发射子弹了:

接下来就要用到ECS中新版的物理组件了,我们先在组件中设置子弹和敌人的碰撞层级,保证同类物体不会触发碰撞事件,只有子弹和敌人碰撞会触发事件:

这里搜索资料后发现比较简单的做法是去定义一个Job继承ITriggerEventsJob接口,去接收事件,但由于Job中是并行处理数据,遇到了新的问题,由于代码比较长,上部分伪代码来说明:

[BurstCompile]
    private struct TriggerJob :ITriggerEventsJob
    {
        #region 传递进来的各类group数据
        #endregion
        public void Execute(TriggerEvent triggerEvent)
        {
            //triggerEvent包含两个碰撞实体,需要我们自行判断他们属于哪个ComponentGroup
            if (EnemyGroup.HasComponent(triggerEvent.EntityA))
            {
                //敌人与主角碰撞效果
                if (!BulletGroup.HasComponent(triggerEvent.EntityB) && BeatBackGroup.HasComponent(triggerEvent.EntityB))
                {
                    #region 击退
                    #endregion
                    return;
                }
                isbehit[0] = true;

#region 删除子弹
                #endregion

#region 子弹击退敌人效果
                #endregion

#region 扣血并生成爆炸粒子实体
                #endregion
            }
            if (EnemyGroup.HasComponent(triggerEvent.EntityB)){}
        }
    }
}

图中代码的意思大概是这样:当接收到世界中发生的碰撞事件后,首先Job会判断碰撞物属于哪个ComponentGroup,如果Enemy,扣一滴血;包含Bullet,则直接销毁子弹实体,但实际上写完运行确遇到了这样的问题:

删除子弹实体的操作并非立即执行,同时删除子弹实体的操作和TriggerJob也是并行的(不在同一线程,两者先后顺序不确定),所以可能会出现图中的状况(箭头长度代表时间长度):

为了解决这个问题,我首先的思路是为子弹增加一个bool值记录它的状态,如果接触到敌人,再次触发碰撞事件时会直接返回,代码如下:

if (EnemyGroup.HasComponent(triggerEvent.EntityA))
   {
           //A是敌人,自然EntityB是子弹
           if (BulletGroup[triggerEvent.EntityB].isDestory)
           {
                Debug.Log("子弹已被删除");
                return;
           }

Bullet a = BulletGroup[triggerEvent.EntityB];
           a.isDestory = true;
           BulletGroup[triggerEvent.EntityB] = a;
   }

结果连续触发碰撞事件时,直接报错The entity does not exist,bullet Group 中并不包含这个引发碰撞的子弹:

造成这个的原因也比较好猜,当我们执行删除子弹实体的代码时,子弹实体并不会立即删除,而是要等到EntityCommandBufferSystem回放命令时统一调度,所以已经子弹可能已经被系统标记为空,自然不在BulletGroup中了,自然也找不到该实体。

解决问题思路还有很多,我们当然可以在代码中修改Collision Filter,或是关闭子弹的碰撞事件来达成效果。。但实际上这两种操作都非常麻烦,目前Dots还没有这么的自由。

在尝试过上述做法后,我所想到的一个简单的思路:在发生碰撞时,将子弹挪到一个看不见位置去,这样就不会造成多次触发碰撞事件;

同时每个子弹都有自己的生命周期,所以也可能发生子弹生命到了,被标记删除,但又刚好触发碰撞的情况。为了避免这样的冲突,我们需要在每个Group中都对子弹进行HasComponent判定,子弹删除代码如下:

if (EnemyGroup.HasComponent(triggerEvent.EntityA))
   {
            //A是敌人,自然EntityB是子弹
            if (TranslationGroup.HasComponent(triggerEvent.EntityB))
               {
                   Translation temp = TranslationGroup[triggerEvent.EntityB];
                   //将子弹移到天上去
                   temp.Value = new float3(0, 100, 0);
                   TranslationGroup[triggerEvent.EntityB] = temp;

if (DeleteGroup.HasComponent(triggerEvent.EntityB))
                   {
                      DeleteTag temp1 = DeleteGroup[triggerEvent.EntityB];
                      temp1.lifeTime = 0f;
                      DeleteGroup[triggerEvent.EntityB] = temp1;
                   }
               }
   }

最后再做个敌人被击退的效果,给敌人添加BeatBack组件,每次被子弹击中时,敌人都会获得一个持续衰减的速度,被连续击中时,获得的加速度也会逐渐衰减:

using Unity.Entities;
using Unity.Transforms;

[GenerateAuthoringComponent]
public struct BeatBack : IComponentData
{
    public float velocity;
    public float curVelocity;
    public Rotation rotation;
    public float timer;
}

BeatBackSystem :

using Unity.Entities;
using Unity.Jobs;
using Unity.Transforms;
using Unity.Mathematics;

public class BeatBackSystem : SystemBase
{
    protected override void OnUpdate()
    {
        float deltaTime = Time.DeltaTime;
        Entities.
            ForEach((ref BeatBack beatBack,ref Translation translation ) =>
        {
            if (beatBack.velocity <0.1f)
            {
                beatBack.velocity = 0;
                beatBack.timer = 0;
                beatBack.curVelocity = 0;
                return;
            }
            float temp = beatBack.velocity;
            beatBack.timer += 2*deltaTime;

temp = math.lerp(beatBack.velocity, 0,beatBack.timer);
            if (temp < 0.1f)
            {
                beatBack.velocity = 0;
            }
            beatBack.curVelocity = temp;
            translation.Value += beatBack.velocity * deltaTime * math.forward(beatBack.rotation.Value);
        }).Run();
    }
}

完整TriggerEventSystem代码如下:

using Unity.Entities;
using Unity.Jobs;
using Unity.Transforms;
using Unity.Mathematics;
using Unity.Collections;
using Unity.Physics.Systems;
using Unity.Physics;
using Unity.Burst;

[UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]

public class TriggerEventSystem : SystemBase
{
    private BuildPhysicsWorld buildPhysicsWorld;
    private StepPhysicsWorld stepPhysicsWorld;
    EndSimulationEntityCommandBufferSystem endSimulationEcbSystem;

protected override void OnCreate()
    {
        buildPhysicsWorld = World.GetOrCreateSystem<BuildPhysicsWorld>();
        stepPhysicsWorld = World.GetOrCreateSystem<StepPhysicsWorld>();
        endSimulationEcbSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    }

protected override void OnUpdate()
    {
        var ecb = endSimulationEcbSystem.CreateCommandBuffer();
        //传入两个bool值,用来判断是否播放被击中或者被击杀的音效
        NativeArray<bool> isbehit = new NativeArray<bool>(2, Allocator.TempJob);

TriggerJob triggerJob = new TriggerJob
        {
            PhysicVelocityGroup = GetComponentDataFromEntity<PhysicsVelocity>(),
            EnemyGroup = GetComponentDataFromEntity<Enemy>(),
            BeatBackGroup = GetComponentDataFromEntity<BeatBack>(),
            RotationGroup = GetComponentDataFromEntity<Rotation>(),
            HpGroup = GetComponentDataFromEntity<Hp>(),
            BulletGroup = GetComponentDataFromEntity<Bullet>(),
            DeleteGroup = GetComponentDataFromEntity<DeleteTag>(),
            TranslationGroup = GetComponentDataFromEntity<Translation>(),
            ecb = ecb,
            PhysicsColliderGroup = GetComponentDataFromEntity<PhysicsCollider>(),
            CharacterGroup = GetComponentDataFromEntity<Character>(),
            boom = FPSGameManager.instance.boomEntity,
            isbehit = isbehit,

};
        Dependency = triggerJob.Schedule(stepPhysicsWorld.Simulation, ref buildPhysicsWorld.PhysicsWorld,this.Dependency );
        Dependency.Complete();

if (isbehit[0])
        {
            isbehit[0] = false;
            FPSGameManager.instance.PlayBehit();
        }

if (isbehit[1])
        {
            isbehit[1] = false;
            FPSGameManager.instance.PlayBoom();
        }
        isbehit.Dispose();
    }

[BurstCompile]
    private struct TriggerJob :ITriggerEventsJob
    {
        public ComponentDataFromEntity<PhysicsVelocity> PhysicVelocityGroup;
        //初始化数据略

public void Execute(TriggerEvent triggerEvent)
        {

if (EnemyGroup.HasComponent(triggerEvent.EntityA))
            {
                //敌人与主角碰撞效果
                if (!BulletGroup.HasComponent(triggerEvent.EntityB) && BeatBackGroup.HasComponent(triggerEvent.EntityB))
                {
                    #region 击退
                    BeatBack beatBack1 = BeatBackGroup[triggerEvent.EntityB];
                    if (beatBack1.curVelocity > 0.1f)
                    {
                        beatBack1.velocity += (5f - beatBack1.curVelocity) * 0.1f;
                    }
                    else
                    {
                        beatBack1.velocity = 5f;
                    }
                    if (RotationGroup.HasComponent(triggerEvent.EntityB))
                    {
                        Rotation rotation = RotationGroup[triggerEvent.EntityB];
                        beatBack1.rotation = rotation;
                    }
                    BeatBackGroup[triggerEvent.EntityB] = beatBack1;
                    #endregion
                    return;
                }
                isbehit[0] = true;

#region 删除子弹

float3 boomPos = float3.zero;
                if (TranslationGroup.HasComponent(triggerEvent.EntityB))
                {
                    Translation temp = TranslationGroup[triggerEvent.EntityB];
                    boomPos = temp.Value;
                    temp.Value = new float3(0, 100, 0);
                    TranslationGroup[triggerEvent.EntityB] = temp;
                    if (DeleteGroup.HasComponent(triggerEvent.EntityB))
                    {
                       DeleteTag temp1 = DeleteGroup[triggerEvent.EntityB];
                       temp1.lifeTime = 0f;
                        DeleteGroup[triggerEvent.EntityB] = temp1;
                    }
                }
                #endregion

#region 子弹击退敌人效果
                //略
                #endregion

#region 扣血并生成爆炸粒子实体
                if (HpGroup.HasComponent(triggerEvent.EntityA))
                {
                    Hp hp = HpGroup[triggerEvent.EntityA];
                    hp.HpValue--;
                    HpGroup[triggerEvent.EntityA] = hp;
                    if (hp.HpValue == 0)
                    {
                        //播放死亡音效
                        isbehit[1] = true;
                        Entity boomEntity = ecb.Instantiate(boom);
                        Translation translation = new Translation
                        {
                            Value = boomPos
                        };
                        ecb.SetComponent(boomEntity, translation);
                    }
                }
                #endregion
            }

if (EnemyGroup.HasComponent(triggerEvent.EntityB))
           {
               //与A逻辑相同,略
            }

}
    }

}

6:粒子与音效

目前Particle System 也能正常的转换为Entity ,但和physic shape等组件一样,它们还并没有那么方便使用,所以这里采用了和子弹组件一样的策略,写了一个粒子生命周期的组件,在单独的系统去处理,也不过多赘述了。

至于声音,没必要转换为实体,正常使用就好了~

工程地址:

https://github.com/ydwj/Unity-ECS-FpsGame

ps:工程里面下的商店的免费素材有点大~

Unity ECS实例:制作俯视角射击游戏!相关推荐

  1. 【Unity实战】制作类元气骑士、挺进地牢——俯视角射击游戏多种射击效果(二)(附源码)

    文章目录 前言 一.火箭筒 1. 编写火箭筒脚本 2. 创建火箭弹和新爆炸特效的预制体 3. 编写火箭弹脚本 4. 设置好火箭弹和火箭筒的脚本和参数 5. 运行效果 二.激光枪 1. 编写激光枪脚本 ...

  2. 【unity实战】制作类元气骑士、挺进地牢——俯视角射击游戏多种射击效果(一)(附源码)

    文章目录 本期目标 前言 欣赏 开始 1. 角色移动和场景搭建 2. 绑定枪械 2.1 首先将各种枪械的素材添加给人物作为子物体 2.2 给枪械也分别添加两个子物体用作标记枪口和弹仓位置 3. 枪械动 ...

  3. 关于hot miami的沙盒生存俯视角射击游戏

    一些关卡设计元素 撞门杀 扇形视角 转载于:https://www.cnblogs.com/dandansang/p/6667046.html

  4. unity判断鼠标移动方向_【反向元气骑士】用unity实现俯视角射击是一种怎样的体验...

    哈喽大家好我是yumir. 最近沉迷元气骑士,自从官方把各种"bug"修复之后,这个游戏对我这种手残玩家来说实在是太难了.既然打不过,那我就自己写一个,不就是俯视角射击吗,分分钟给 ...

  5. unity fixedupdate_3D俯视角射击——用Unity还原东方弹幕(上)

    前言 之前我们的专栏中介绍过2D的俯视角射击,这次就来试试在3D场景下的实现.移动射击的实现方法差不多,所以本次的主要目标是在3D场景下还原东方的符卡(弹幕)效果. 我们先来看看最终结果: 可配置的弹 ...

  6. dotween unity 延时_3D俯视角射击——用Unity还原东方弹幕(上)

    作者:QXYO 前言 之前我们的专栏中介绍过2D的俯视角射击,这次就来试试在3D场景下的实现.移动射击的实现方法差不多,所以本次的主要目标是在3D场景下还原东方的符卡(弹幕)效果. 我们先来看看最终结 ...

  7. 【Unity俯视角射击】我们来做一个《元气骑士》的完整Demo1

    ---------------------------相关功能实现-------------------------------- 1.人物移动 人物的移动依旧是利用在Update中获取键盘输入的&q ...

  8. Unity制作第三人称射击游戏-成为创新游戏开发者学习教程

    用Unity游戏引擎学习和制作第三人称射击游戏-成为创新游戏开发者- TPS战地克隆 你会学到什么 Unity 3D游戏开发 如何打造TPP第三人称射击游戏 第三人控制者 第三人称动画 第三人运动控制 ...

  9. Unity VR学习:第一人称射击游戏(1)

    Unity VR学习:第一人称射击游戏(1) 1.封装标签和场景淡入淡出效果实现 (1) 封装标签 1.标签有Player,Enemy,GameController,Fader(画布),MainCam ...

最新文章

  1. Django博客系统注册(图形验证码接口设计和定义)
  2. R语言数据结构之数组
  3. Git Merge VS Rebase
  4. python中pygame模块的Linux下安装过程
  5. [POJ3249]Test for Job [拓扑排序+DAG上的最长路径]
  6. NOIP2016 愤怒的小鸟
  7. linux 文件重命名或文件移动
  8. Memory Management Concepts overview(内存管理基本概念)
  9. CSS3属性box-shadow详解[盒子阴影]
  10. Atitit..jdk java 各版本新特性 1.0 1.1 1.2 1.3 1.4 1.5(5.0) 1.6(6.0) 7.0 8.0 9.0 attilax 大总结
  11. html打印word文档,HTML文件到WORD文档双面打印三步曲
  12. 中国分布式光纤传感(DFOS)行业竞争现状与未来趋势预测报告2022-2027
  13. 3D游戏开发套件指南(入门篇,转自Unity官方平台公众号)
  14. 克鲁斯卡尔算法、并查集
  15. C# OpenCv 证件照换底色
  16. 华为路由器配置网络地址转换NAT/静态NAT/动态NAT/Easy-ip/NAPT代理上网
  17. Python初学者必须吃透的69个内置函数
  18. Ubuntu设置自动登录
  19. Android 9.0 蓝牙电话BluetoothHeadsetClient
  20. Redis集群搭建(转自一菲聪天的“Windows下搭建Redis集群”)

热门文章

  1. 【8.9】重拾编程之心 Java基础
  2. H5移动端适配 rem 和 vw 的区别
  3. 一个成功人士应该具备的能力
  4. CentOS 7安装OpenMPI
  5. 2023年有哪些蓝牙耳机推荐?好用不贵的蓝牙耳机推荐
  6. web前端开发课程安排,webpack版本管理
  7. 【艾米莉娅】matrix:valid parentheses括号匹配代码分享(非堆栈版)
  8. 【分布式篇】什么是分布式ID?分布式ID常见解决方案有哪些?
  9. 设计模式之观察者模式(1)
  10. Gouraud Shading(高洛德着色/高氏着色)