来实现敌人能够发现Player,然后可以进入到追击或者说冲锋的状态
首先来学习如何发现Player,在这里面我们会调用一个物理检测的方法想要实现的功能就是在我们野猪面朝的方向,不断的发射一个检测范围,如果在这个范围当中碰到了我们的Player的话,发现Player,进入冲锋状态
可以发射一条射线出去,或者发射出一个形状的区域判断来检测(之前我们已经使用过这些小白圆点的位置,使用的是physics2D overlap Circle,我们要学习physic2D Box cast)
先来查看代码手册
从概念上说,Box Cast就像朝特定方向拖动一个盒体穿过场景一样。在该过程中,可以检测并报告与盒体接触的任何对象
Origin起点,size大小,angle角度,direction方向矢量,distance最大投射距离,layerMask过滤器,用于仅在特定层上检测碰撞体;minDepth,仅包括z坐标(深度)大于或等于该值的对象;maxDepth,仅包括z坐标(深度)小于或等于该值的对象;
人物的检测放到Enemy基类当中来进行,打开Enemy代码,创建函数方法
把这段代码放在TimeCounter的下边(因为下半部分的内容都是我们的event事件
创建发现敌人的函数FoundPlayer,我希望他能够返回一个bool值是否发现Player;有返回值的函数,最后需要return什么东西,我们把直接得到的值返回回去就可以了,返回Physics2D.BoxCast
public bool FoundPlayer(){return Physics2D.BoxCast();}
有一个圆点,野猪的中心点,由于我们设置野猪的锚点为脚底,所以我们不能直接使用transform.position;尺寸,角度,方向,距离,LayerMask这些条件都需要我们创建变量,方便我们来使用
[Header("检测")]
public Vector2 centerOffset;//中心点偏移量
public Vector2 checkSize;//盒子尺寸
public float checkDistance;//检测距离
public LayerMask attackLayer;//检测图层
transform.position要加上我们的偏移量centerOffset(因为两个数据类型不同,一个是Vector3,一个是Vector2)可以强制转换一下,方盒尺寸,角度0,方向(faceDir面朝方向),检测距离,检测图层
这bool直接返回的是true或false
public bool FoundPlayer()
{return Physics2D.BoxCast(transform .position+(Vector3)centerOffset,checkSize,0,faceDir,checkDistance,attackLayer);
}
想要知道函数其实返回的是什么,通用var来定义一个变量temp=
鼠标移动上去,可以看到它检测返回的值是一个RaycastHit2D类型的返回值
public bool FoundPlayer()
{var temp=Physics2D.BoxCast(transform .position+(Vector3)centerOffset,checkSize,0,faceDir,checkDistance,attackLayer);return true;}
在代码手册当中也有详细的说明
会返回一个RaycaseHit2D的值,
RaycaseHit2D这个值中包含了transform,rigidbody等等
任何类型最终都会返回是和否,有和无,所以我们直接将他返回成bool值就可以了
public bool FoundPlayer()
{return Physics2D.BoxCast(transform .position+(Vector3)centerOffset,checkSize,0,faceDir,checkDistance,attackLayer);}
这样我们就实现了FoundPlayer这个函数了
接下来,我们要写我们的状态
之前我们在BoarPatrolState当中留下了追击状态
如果发现Player就切换为追击状态,如果currentEnemy.FoundPlayer为true,我们就切换状态
public override void LogicUpdate(){//发动layer切换到chaseif (currentEnemy.FoundPlayer()){}if (!currentEnemy.physicsCheck.isGround ||( currentEnemy.physicsCheck.touchLeftWall && currentEnemy.faceDir.x < 0 || currentEnemy.physicsCheck.touchRightWall && currentEnemy.faceDir.x > 0)){currentEnemy.wait = true;currentEnemy.anim.SetBool("walk", false);}else{currentEnemy.anim.SetBool("walk", true);}}
我们要切换状态的时候,要告诉当前的Enemy转换状态,同时我们要知道对应进入的是哪一个状态的变量,把它给到我们的currentState
在Enemy代码当中来写一下切换方法,学习switch的方法,我们会学习一个新的语法糖的方法
首先创建一个函数SwitchState切换状态,标记这个状态,告诉函数接下来要到哪个状态,把它赋值给currentStste当中
public void SwitchState(){}
接下来我们创建一个枚举变量,来表明我们当前的状态
保存代码,返回unity,
我们在Script文件夹中单独来创建一个文件夹utilities(工具类型),接下来在这个文件夹中创建一个c#脚本Enums(注意尽量不要和系统文件夹有冲突)
我们用这个代码来涵盖我们所有的enum(枚举类型)
打开代码,先把里面的内容删掉,里面原有的代码我们是完全不需要的
给我们的enum起名为NPCState
我们有几种状态,首先就是我们的巡逻追踪或者我们的Skill;用逗号隔开
public enum NPCState
{Patrol,Chase,Skill
}
保存代码,返回到Enemy的类型当中,在这SwitchStste里面我们就可以调用一下枚举变量
public void SwitchState(NPCState state)//状态切换{}
这个枚举变量如何使用。我们可以在这个函数SwitchStste里面进行枚举的切换判断,来判断一下当前传进来的状态是什么
我们定义一个新的State,等于,依照于我们当前Stste进行的Switch切换(这是一个新的语法糖的写法)如果只是进行简单的变量切换,就可以用这样的方法来写
我们可以判断有几种状态
首先就是NPCState.Patrol,"=>"可以进行切换,如果判断是patrolState,我们就把这个变量切换过去;如果是Chase,我们就把当前的chaseState变量传过去。
最后如果没有了,我们要有一个最基本的(类似于通常的switch里面的default)
默认的返回值,返回一个null
大括号结尾打上分号,这样我们的switch就写好了
public void SwitchState(NPCState state)//状态切换{var newState = state switch{NPCState.Patrol => patrolState,NPCState.Chase => chaseState,_ => null}; }
在BoarPatrolStste代码中
如果我们发现了Player,我们就切换一下状态,变为追击;我们可以直接把这个枚举的变量值给进去
public override void LogicUpdate(){//发动layer切换到chaseif (currentEnemy.FoundPlayer()){currentEnemy.SwitchState(NPCState.Chase);}if (!currentEnemy.physicsCheck.isGround ||( currentEnemy.physicsCheck.touchLeftWall && currentEnemy.faceDir.x < 0 || currentEnemy.physicsCheck.touchRightWall && currentEnemy.faceDir.x > 0)){currentEnemy.wait = true;currentEnemy.anim.SetBool("walk", false);}else{currentEnemy.anim.SetBool("walk", true);}}
在Enemy代码当中
我们当前切换到追击,那我们就把当前新的这个状态定义为chaseState
然后currentState,先执行一下退出,因为我从上一个状态退出出来了;然后currentState=new state;接下来我们要添加一个进入的状态,传入一个this
public void SwitchState(NPCState state)//状态切换{var newState = state switch{NPCState.Patrol => patrolState,NPCState.Chase => chaseState,_ => null};currentState.OnExit();currentState = newState;currentState.OnEnter(this);}
这样看起来,逻辑就非常清楚了。每次我们转换状态的时候,只需要调用Enemy基类当中的切换状态,然后告诉他我们要切换为什么,他就会把对应状态的文件送进去,让他来执行;(他会执行上一个状态的退出,切换状态,执行新状态的进入)
这样我们就做好了转换,不过现在我们还没有写这个chaseStste,
保存所有代码,返回unity,
我们创建一个新的状态,在Enemy文件夹下的新脚本BoarChaseStste
打开代码
同样要继承我们的BaseState
我们让编辑器帮助我们快速生成这些函数方法
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class BoarChaseState : BaseState
{public override void LogicUpdate(){throw new System.NotImplementedException();}public override void OnEnter(Enemy enemy){throw new System.NotImplementedException();}public override void OnExit(){throw new System.NotImplementedException();}public override void PhysicsUpdate(){throw new System.NotImplementedException();}
}
我们先看一下野猪最基础的Boar代码,我们把这个变量先给他赋一个值
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Boar : Enemy
{protected override void Awake(){base.Awake();patrolState = new BoarPatrolState();chaseState = new BoarChaseState();}
}
将我们BoarChaseStste中的内容清理一下,调整一下顺序
public class BoarChaseState : BaseState
{public override void OnEnter(Enemy enemy){}public override void LogicUpdate(){}public override void PhysicsUpdate(){}public override void OnExit(){}
}
一开始同样我们要调用一下currentEnemy=enemy
在这里面我们用Debug.Log输出一段话,来帮助我们判断能不能成功切换状态
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class BoarChaseState : BaseState
{public override void OnEnter(Enemy enemy){currentEnemy = enemy;Debug.Log("Chase");}public override void LogicUpdate(){}public override void PhysicsUpdate(){}public override void OnExit(){}
}
我们在上个执行退出的状态也输出一个,方便观察
保存代码,返回unity
看一下这些状态的切换
选中野猪,更改变量AttackLayer为Player(确认一下Player也选中了这个Player图层),检测距离4.5.
centerOffset做任何修改其实当前在Scene中看不到,要像我们之前检测地面和墙壁的时候一样,在我们的场景当中绘制一个Gizmo,方便我们来查看。我们打开Enemy代码绘制一下,同样在最下面来绘制一下
中心点位置transform.position+centerOffset,范围0.2f;(centerOffset要强制转换一下)
private void OnDrawGizmosSelected()
{Gizmos.DrawWireSphere(transform.position + (Vector3)centerOffset,0.2f);//绘制一个圆形
}
保存代码,返回unity
当前页面就有了白圈,可以通过centerOffset改变位置
从这个中心位置,向我们的面朝方向发射一个盒子,这个盒子的大小,我们可以留意一下Capsule Collider2D,宽度为1.8,高度为1.2
差不多我们用1的高度,1*1的大小来进行判断
checkSize1*1,不断向这个位置发射1*1的方格子来检测我们的Player
点击运行,测试一下
在Console窗口会显示我们执行了状态的转换,野猪靠近Player,先退出了以前的状态,然后进入了追击状态
我们做好了这些之后,我们可以看到Boar的中心点,我们希望能知道我们检测的这个盒子发射了多远,希望可以在Scene中绘制出来,然而这是中心点这个位置其实并不是非常重要,那我们可以再次加上Vector3的值,把它的x值赋过去,给到我们的checkDistance,让这个坐标移动到检测范围的那个地方
打开Enemy代码,一起来写一下
在Gizmo当中的x值,我们再加上一个new Vector3,x值是我们的checkDistance*-tranform.localScale(当前的面朝方向)(这样无论翻转到左还是右,我们都能看得到);y值为0.
private void OnDrawGizmosSelected(){Gizmos.DrawWireSphere(transform.position + (Vector3)centerOffset+new Vector3(checkDistance*-transform.localScale.x,0),0.2f);//绘制一个圆形}
保存代码,返回unity,来看一下
现在这个位置就过来了,位置会受到我的CheckDistance的影响,这样我们就可以看到一个实际的检测范围的距离了
这样我们就成功实现了检测我们的Player,然后进入到追击的状态
进入到追击的状态,野猪的速度要改变,而且要播放对应的动画;并且在追击状态,野猪撞到墙,不等待,直接转身冲撞
一起来写一下代码,按照刚才的逻辑,进入状态更改速度,播放动画
在BoarChaseState代码中
public override void OnEnter(Enemy enemy){currentEnemy = enemy;//Debug.Log("Chase");currentEnemy.currentSpeed = currentEnemy.chaseSpeed;//速度变为追击速度currentEnemy.anim.SetBool("run", true);//进入追击动画}
接下来就是逻辑的判断了,在这里边我们来复制一下我们的巡逻的逻辑,(巡逻的逻辑判断我们撞墙了,我们要进入等待),在这里面我们直接转身currentEnemy.faceDir.x 实现转身(faceDir.x本身就等于我们的localScale,y轴为1,z轴为1
public override void LogicUpdate(){if (!currentEnemy.physicsCheck.isGround || (currentEnemy.physicsCheck.touchLeftWall && currentEnemy.faceDir.x < 0 || currentEnemy.physicsCheck.touchRightWall && currentEnemy.faceDir.x > 0)){currentEnemy.transform.localScale = new Vector3(currentEnemy.faceDir.x, 1,1);}
}
保存代码,返回unity,测试一下
现在我们的野猪就进入了疯狂冲锋的状态,撞墙后实现翻转。
Player跳到平台上,野猪要停止追击,回到最开始的巡逻状态,所以我们要用一个新的计时器来判断一下,我们当前是否已经丢失了我的攻击目标
我们又创建一个变量Attacker,也就是我们的Player;如果我们丢失了这个目标之后,我们就要让他进入一个倒计时,我们也要场景一个新的计时器,之前我们有一个等待实际,我们也要有一个丢失目标的等待时间,然后我们计时器过了一段时间之后,就把状态转换回去
打开Enemy代码来写一下
先来创建计时器的一些变量,等候时间,计数器
[Header("计时器")]//撞墙的等候时间public float waitTime;public float waitTimeCounter;public bool wait;//丢失玩家的等候时间public float lostTime;public float lostTimeCounter;
超出等候时间后,我们就有追击状态回到普通巡逻状态,所以我们先来写这个计时器;我们统一写到TimeCounter这个函数当中;
条件如果丢失了玩家,(我们可以直接调用我们写好的FoundPlayer的这个函数方法)我们就用lostTimeCounter减去我们的时间修正,慢慢的减去,直到lostTimeCounter变为0或者小于0
如果发现了Player,那我们就重置一下时间
public void TimeCounter(){if(wait){waitTimeCounter -= Time.deltaTime;//减去时间的修正if (waitTimeCounter<=0){wait = false;waitTimeCounter = waitTime;transform.localScale = new Vector3(faceDir.x, 1, 1);}}if (!FoundPlayer()){lostTimeCounter -= Time.deltaTime;//减去时间的修正}else{lostTimeCounter = lostTime;}}
我们什么时候转换这个条件呢;我们在BoarChaseState的LogicUpdate当中去检测
如果当前的敌人的lostTimecounter<=0,就切换回巡逻状态,
退出时将run=false,(因为PatrolState进入的时候,我们会将它改为true,在Update当中持续不断地进入
public class BoarChaseState : BaseState
{public override void OnEnter(Enemy enemy){currentEnemy = enemy;//Debug.Log("Chase");currentEnemy.currentSpeed = currentEnemy.chaseSpeed;//速度变为追击速度currentEnemy.anim.SetBool("run", true);//进入追击动画}public override void LogicUpdate(){if (currentEnemy.lostTimeCounter <= 0)currentEnemy.SwitchState(NPCState.Patrol);if (!currentEnemy.physicsCheck.isGround || (currentEnemy.physicsCheck.touchLeftWall && currentEnemy.faceDir.x < 0 || currentEnemy.physicsCheck.touchRightWall && currentEnemy.faceDir.x > 0)){currentEnemy.transform.localScale = new Vector3(currentEnemy.faceDir.x, 1,1);}}public override void PhysicsUpdate(){}public override void OnExit(){currentEnemy.anim.SetBool("run", false);}
}
还有一点,我们刚才修改了速度,那么再回到PatrolState的过程当中,进来的时候,我们再把这个速度修改一下当前速度等于普通速度
这样我们就切换了动画,也改变了速度
public override void OnEnter(Enemy enemy){currentEnemy = enemy;currentEnemy.currentSpeed = currentEnemy.normalSpeed;}public override void LogicUpdate(){//发动layer切换到chaseif (currentEnemy.FoundPlayer()){currentEnemy.SwitchState(NPCState.Chase);}if (!currentEnemy.physicsCheck.isGround ||( currentEnemy.physicsCheck.touchLeftWall && currentEnemy.faceDir.x < 0 || currentEnemy.physicsCheck.touchRightWall && currentEnemy.faceDir.x > 0)){currentEnemy.wait = true;currentEnemy.anim.SetBool("walk", false);}else{currentEnemy.anim.SetBool("walk", true);}}
保存代码返回unity,
我们设置一个丢失时间,3秒
测试一下
现在就成功的实现了野猪回到之前的状态,丢失一段时间后,回到之前的追踪状态
我们可以注意到LastTimeCounter一直在减,显得数字非常的大,如果想要避免这些内容,我们可以在代码当中添加一些约束
在TimeCounter的第二个条件!FoundPlayer当中再加上我们的lostTimeCounter>0,那我们就持续不断的减少
public void TimeCounter(){if(wait){waitTimeCounter -= Time.deltaTime;//减去时间的修正if (waitTimeCounter<=0){wait = false;waitTimeCounter = waitTime;transform.localScale = new Vector3(faceDir.x, 1, 1);}}if (!FoundPlayer()&&lostTimeCounter>0){lostTimeCounter -= Time.deltaTime;//减去时间的修正}else{lostTimeCounter = lostTime;}}
保存代码,返回unity,再来运行测试一下
我们注意到这个Lost Time Counter虽然不会到0以下了,但是它还是持续不断地在变化;原因是我们过程这个代码修改的时候,我们还要注意下面有一个else的情况,所以一旦LostTimeCounter>0时,它就不会执行上面if的内容,直接执行下面else的内容。
其实我们上什么时候才要重置这个时间呢,我们通过脱离追击冲锋的这种状态的时候,我们可以把它重置。
打开ChaseState代码,找到退出的时候,将计数器重置,把刚才写的else内容注释掉
public class BoarChaseState : BaseState
{public override void OnEnter(Enemy enemy){currentEnemy = enemy;//Debug.Log("Chase");currentEnemy.currentSpeed = currentEnemy.chaseSpeed;//速度变为追击速度currentEnemy.anim.SetBool("run", true);//进入追击动画}public override void LogicUpdate(){if (currentEnemy.lostTimeCounter <= 0)currentEnemy.SwitchState(NPCState.Patrol);if (!currentEnemy.physicsCheck.isGround || (currentEnemy.physicsCheck.touchLeftWall && currentEnemy.faceDir.x < 0 || currentEnemy.physicsCheck.touchRightWall && currentEnemy.faceDir.x > 0)){currentEnemy.transform.localScale = new Vector3(currentEnemy.faceDir.x, 1,1);}}public override void PhysicsUpdate(){}public override void OnExit(){currentEnemy. lostTimeCounter =currentEnemy. lostTime;currentEnemy.anim.SetBool("run", false);}
}
保存代码,返回unity,测试一下
现在野猪未发现Player时,不会计数;发现Player之后,LastTimeCounter开始计时;我们攻击一下,野猪开始追击Player
我们会在测试中发现,如果这个野猪持续的朝玩家这个方向来冲的时候,这个时候我对他产生攻击,他并没有一个击退的效果,而是原地定一下;而在背后攻击野猪的时候,他就会被击退,原因是野猪面朝我们的时候受伤,他仍然保持着一个向前冲的力,这个力使它抵消了这个击退的力。所以我们可以做的方法就是在受伤的时候,先把野猪x轴的方向上的力停下来
打开Enemy代码,找到事件执行方法OnTakeDamage,我们获得攻击的方向之后,修改一下他的velocity,把他x轴改为0,y轴保持不变
public void OnTakaeDamage(Transform attackTrans)//接收方向,传递进来的攻击方向
{attacker = attackTrans;//翻转if (attackTrans.position.x - transform.position.x > 0)transform.localScale = new Vector3(-1, 1, 1);if (attackTrans.position.x - transform.position.x < 0)transform.localScale = new Vector3(1, 1, 1);//受伤被击退isHurt = true;anim.SetTrigger("hurt");Vector2 dir = new Vector2(transform.position.x - attackTrans.position.x, 0).normalized;rb.velocity = new Vector2(0, rb.velocity.y);//野猪受到攻击,x轴的力变为0StartCoroutine(OnHurt(dir));//启动携程迭代器
}
保持代码,返回unity,测试一下
这样正面和背面,都可以实现攻击,将野猪击退
这样我们就成功实现了,所有的和野猪有关的内容