IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 游戏开发 -> Unity3D | FPS游戏_敌人相关 -> 正文阅读

[游戏开发]Unity3D | FPS游戏_敌人相关

书接上回,在上一篇中当说到有些交叉知识的时候,会进行简单标注,在这里围绕敌人来进行详细的解说。


首先会简单介绍一下敌人的逻辑以及行为,敌人会随机在地图上的五个出生点刷新和重生,敌人的数量开始会从0增加到10,并且在击杀之后持续重生保持数量。敌人会在各个出生点之间巡逻,当敌人与人物的距离到达一定限制或者人物在敌人一定范围内射击(可以理解为开枪吸引敌人),敌人会锁定人物,切换为追击状态。

思路

有限状态机

敌人的状态机和人物的类似,尤其特殊需要注意的一点是,敌人有一个终点状态,同时使用对象池管理
①在进入Dead状态之后,不会再进行任何状态转换
:同时关闭所有协程(在本例中每次敌人受伤,会开启一个击退协程)
②在每次从对象池中取出,要对敌人的状态初始化:简单距离描述一个敌人的完整过程:实例化敌人 -> HP<=0 -> SetActive(false),进入对象池,此时的敌人状态是Dead -> 需要一个新的对象,同时池中存在对象 -> SetActive(true),同时状态需要切换为Idle
在初始状态机开始有一个关键性判断,实现上面的两点的注意

if (zombieState == ZombieState.Dead
                && value != ZombieState.Idle)
            {
                return;
            }

这段判断表示的意思:如果当前的状态是Dead,并且希望去往的状态不是Idle,则结束函数。这样一来既满足了Dead是终点状态的定义,也可以满足初始化时可以从Dead切换到Idle
初始化代码:

public void Init()
    {
        Debug.Log("初始化");
        animator.SetTrigger("init");
        capsuleCollider.enabled = true;
        hp = 100;
        ZombieState = ZombieState.Idle;
    }

动画开关init控制AnyState到Idle

对象池

这里首先演示使用对象池之后敌人的游戏项目管理
在这里插入图片描述1.为什么使用对象池
这里个人理解会和两点有关:
①Destroy(gameObject);
根据老师的指导,个人增加了一些对Destory的理解,当代码中执行Destroy(gameObject);时候,虽然在Hierarchy中已经看不到该游戏项目,但是在Unity的底层中,并没有删除该项目,而是类似于给他打上一个删除的标签,之后游戏中的项目愈来愈多,到达内存和资源的占用越来越多都内存满,然后会检视有哪些是带有删除标签的,再将其删除。
综合上述的原理,如果是每击杀一个敌人之后,删除该游戏项目,再实例化一个新的敌人,内存占用会越来越大。
②需要重复使用对象
使用对象池,每次击杀的敌人会将其游戏项目SetActive(false);然后将其添加到对象池(队列Queue)下面作为子项目。这样之后每次需要一个新的对象时,会先去查看对象池是否为空,若不为空,则SetActive(true)使用;若为空,则实例化新的对象。
以下为代码管理敌人生成的协程,以及消灭之后进入对象池的函数

IEnumerator CheckZombie()
   {
       while(true)
       {
           yield return new WaitForSeconds(1f);
           if(zombies.Count<10)//场景现存僵尸数量不够
           {
               if(zombiesPool.Count>0)//首先检查对象池中是否有
               {
                   ZombieController zb = zombiesPool.Dequeue();//取出队列
                   zb.transform.SetParent(transform);//从对象池中取出
                   zb.transform.position=GameManager.instance.GetPoints();//随机出生点
                   zb.gameObject.SetActive(true);
                   zb.Init();//初始化僵尸状态
                   zombies.Add(zb);//场景中存在的僵尸列表
                   yield return new WaitForSeconds(3f);
               }
               else
               {//直接新建实例化
                   GameObject zb=Instantiate(pre_Zombie,GameManager.instance.GetPoints(),Quaternion.identity,transform);
                   zombies.Add(zb.GetComponent<ZombieController>());
               }
           }
       }
   }
   //死亡状态调用函数
    public void ZombieDead(ZombieController zombie)
    {
        zombies.Remove(zombie);
        zombiesPool.Enqueue(zombie);
        zombie.gameObject.SetActive(false);
        zombie.transform.SetParent(Pool);
    }

联想:子弹是否可以使用对象池?
如果是在完整的大型多人FPS射击游戏,追求模拟真实的枪战效果,采用子弹对象,一定要使用对象池。
但是,因为游戏场景中的物体每一帧都在渲染,如果是子弹对象,模拟真实子弹射出的速度,有可能在这一帧,子弹在物体的正前方,但是下一帧会刷新到物体的后面。

导航组件

AI -> Navigation -> Bake ,会自动烘焙出地图上可以行走的区域
在这里插入图片描述通过navMeshAgent.isStopped = true;来开关敌人的导航功能(初次接触,只是简单了解这个组件可以规划出世界中两个位置之间的路径,具体的有关属性还未使用,以后会深入了解)

实现

AI

在这里插入图片描述主要表现为不同状态之间的转换,对应状态执行对应的行为函数,涉及AI的主要状态有:Idle(待机)、Walk(巡逻)、Run(追击)、Attack(攻击),因为状态和动画是同步的,讲解可以参考上图
待机 -> 巡逻
在敌人实例化或者从池子取出生成,会默认进入待机状态,在Idle状态中演示执行巡逻函数

case ZombieState.Idle:
                    animator.SetBool("walk", false);
                    animator.SetBool("run", false);
                    //关闭导航组件
                    navMeshAgent.isStopped = true;
                    Invoke("GoWalk", Random.Range(1, 3));//每次巡逻待机的时长
                    break;
void GoWalk()
    {//僵尸的去巡逻函数
        ZombieState = ZombieState.Walk;
    }

如果完成从一个出生点到另一个出生点的巡逻,并且中途没有发现人物,会在原地切换为Idle状态,形成状态的循环

//Walk状态初始化
//随机选择一个巡逻点
target = GameManager.instance.GetPoints();
navMeshAgent.SetDestination(target);

//Walk状态Update
if (Vector3.Distance(transform.position, target) <= 1)
{
	//到达目标点,切换状态,改为待机
	ZombieState = ZombieState.Idle;
}

巡逻 -> 追击
出现这个状态的转换有两种情况:
①人物和敌人之间的距离到达一定限制

 if (Vector3.Distance(transform.position, PlayerController.instance.transform.position) < dis)
                {
                    //追击玩家
                    ZombieState = ZombieState.Run;
                    return;
                }

②人物在敌人周围射击:
在Update中每帧计算人物和敌人之间的距离时,检测人物是否处于射击状态,若为是,则判定追击距离为30;若为否,则判定追击距离为10

 //如果当前玩家的状态是Shoot,僵尸追击的范围会变大,变相实现听枪声追击
        float dis = PlayerController.instance.PlayerState == PlayerState.Shoot ? 30f : 10f;

Hurt -> 追击
当人物射击击中敌人时,在击退协程中,会首先进入Hurt状态,播放受伤动画,之后主动状态切换到追击,人物之后再次射击造成伤害,重复循环

追击 -> 攻击
当敌人到达人物一定范围内,会切换到攻击状态,执行一次完整攻击动画(中途即使受伤,不会播放受伤动画,除非HP<=0,则立即播放死亡动画,进入死亡状态),之后进入追击状态,再进行距离判断,形成循环

case ZombieState.Attack:
                //参考换弹动画回归条件
                if (animator.GetCurrentAnimatorClipInfo(0)[0].clip.name == "Attack"
                && animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= 1)
                {
                    ZombieState = ZombieState.Run;
                }
                break;

随机出生点

类似于之前“是男人就下一百层”,随机生成平台的思路,记录一个点,每次新平台的生成,在这个点的基础上,横坐标随机在一定范围内随机波动。同理,在场景世界中随机新建多个3D项目(例Cube),将组件取消勾选,通过GameManager中新建Transform的数组,来记录管理这些坐标,每次敌人的生成,会随机选择一个点的坐标

public Vector3 GetPoints()
    {
        return Points[Random.Range(0,Points.Length)].position;
    }

攻击判定

在攻击动画的播放过程中,添加Event,StartAttack()和EndAttack()
在这里插入图片描述根据攻击动画,在合适的模型处添加碰撞体
在这里插入图片描述代码逻辑:

//ZombieController
void StartAttack()
    {
        weapon.StartAttack();
    }
void EndAttack()
    {
        weapon.EndAttack();
    }

在敌人脚本中声明武器
因为动画事件在ZombieController中可以监听到,需要新建敌人武器的脚本来对武器的碰撞体进行监听控制

//Zombie_Weapon
public void StartAttack()
    {
        isAttack=false;
        boxCollider.enabled=true;
    }
public void EndAttack()
    {
        boxCollider.enabled=false;
    }

因为碰撞体判断是一段持续时间,有可能会出现一次攻击,造成多次伤害,为此的解决方法是:在Zombie_Weapon脚本中添加一个bool值,表示正在攻击,在每次开始攻击的时候为false,进入开关函数,添加判断条件(判判断碰撞的碰撞体的物体的tag是否为player,从而决定是否调用PlayerController中的受伤函数),修改为true;

private bool isAttack=false;//保证一次攻击,进行一次伤害判定
private void OnTriggerEnter(Collider other) 
    {
        if(!isAttack &&other.gameObject.tag=="Player")
        {
            isAttack=true;
            PlayerController.instance.Hurt(10);
        }    
    }

尚未解决的问题/拓展

问题1.敌人死亡动画异常
经过Debug证实,在快速射击的情况下,因为击退协程中存在yield return 语句,当HP已经满足死亡条件时,之前延迟的协程还在等待执行。暂时的想法是,在进入Dead状态的第一步,关闭所有的协程,解决未果。
拓展1.人物死亡
人物在HP<=0的时候,目前没有完善人物的Dead状态,暂时的想法时,做出类似CS的被击杀效果,主要是当前界面整体调为红色,人物的主摄像机模拟出倒下的效果。

  游戏开发 最新文章
6、英飞凌-AURIX-TC3XX: PWM实验之使用 GT
泛型自动装箱
CubeMax添加Rtthread操作系统 组件STM32F10
python多线程编程:如何优雅地关闭线程
数据类型隐式转换导致的阻塞
WebAPi实现多文件上传,并附带参数
from origin ‘null‘ has been blocked by
UE4 蓝图调用C++函数(附带项目工程)
Unity学习笔记(一)结构体的简单理解与应用
【Memory As a Programming Concept in C a
上一篇文章      下一篇文章      查看所有文章
加:2021-11-10 12:43:37  更:2021-11-10 12:45:27 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/28 0:43:47-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码