书接上回,在上一篇中当说到有些交叉知识的时候,会进行简单标注,在这里围绕敌人来进行详细的解说。
首先会简单介绍一下敌人的逻辑以及行为,敌人会随机在地图上的五个出生点刷新和重生,敌人的数量开始会从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状态,形成状态的循环
target = GameManager.instance.GetPoints();
navMeshAgent.SetDestination(target);
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
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() 根据攻击动画,在合适的模型处添加碰撞体 代码逻辑:
void StartAttack()
{
weapon.StartAttack();
}
void EndAttack()
{
weapon.EndAttack();
}
在敌人脚本中声明武器 因为动画事件在ZombieController中可以监听到,需要新建敌人武器的脚本来对武器的碰撞体进行监听控制
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的被击杀效果,主要是当前界面整体调为红色,人物的主摄像机模拟出倒下的效果。
|