【前言】 本篇来说一下关于简单有限状态机的算法实现,当然我们的几个状态会比较简单,而且本身我也是处于入门状态,所以当成一个简单的知识积累。有限状态机,顾名思义,状态是有限个的,而且状态之间是关联的,本篇写的状态机,其实是类似写游戏里面的AI机器人,就是那些游戏里面的怪啊,npc啊,本篇也是针对几个行为或者是状态,进行设计,编写相关脚本。
1.新建·一个项目,再资源商店搜索Zombie,导入敌人的模型和动画,搜索Yurowm导入玩家的模型和动画,至于场景,搜索_Barking_Dog,导入场景
2.新建之后,先把这个放一边,首先我们来分析怪物状态以及过渡条件
如下图:
3.根据以上简单的集中关系组合,我们来进行设计。首先新建一个文件夹,取名Animators,创建一个动画控制器,把Zombie文件夹下面的动画idle,attack,walk拉进动画控制器,设置以下关系以及新建参数。至于动画之间的过渡,idle过渡到walk和attack,就设置相应的为true就可以了,反之回到idle设置为false,而walk到attack或者是attack到walk,则设置目标为true,自身为false。这样,简单的控制器就完成了。
4.分析敌人获取主角的算法,这里我采用的是获取角度获取视野的方式,当然这个不限于这种简单的方式,我这里就简单用一下这种算法。获取主角的原理如下,很简单:
判断角度a即可,然后判断距离,获取是否在攻击范围之内。
5。还有一个,在移动的方式上,我选择了unity自带的AI寻路移动,所以不可避免地要在Zombie的模型上添加一个Nav Mesh Agent组件,到时候直接调用组件移动即可。把_Barking_Dog文件下的Test_Map拉进场景,然后把_Level和摄像机拉进我们自己的Scene,当然原来的摄像机和灯光要删除。检查一下_Level下面的子物体Floor_01是否都设置为静态,如果是就可以开始烘焙寻路了。
6.为Zombie添加Animator组件,把控制器赋上,然后在资源库里面找到Yurowm的模型,拉进场景里面,取名Player,设置标签为Player。
7.接下来上脚本,新建一个脚本为FsmDemo,赋给敌人,解释在脚本里面:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public enum EnemyState//状态枚举
{
Patrol,//巡逻
Attack,//攻击
Rest,//休息
Chase//追击
}
public class FsmDemo : MonoBehaviour {
public EnemyState enemyState;
private Transform Player;//主角
public float EnemyRestTime = 3;//控制休息时间
private float RestTime;//休息时间
private Animator EnemyAnimator;//动画控制器
public float MaxSightDistance = 10;//最大的视野范围
public float CanAttackDistance = 1;//攻击范围
public Transform MoveTarget;//最终要移动到的目标
public Transform point;//路点父物体
public List Points = new List();//路点的链表数组
private NavMeshAgent m_navmeshment;//寻路组件
private int index = 0;//路点索引
public bool canmove = true;//是否可以移动
void Start() {
m_navmeshment = GetComponent();//获取寻路组件
RestTime = EnemyRestTime;//初始化休息时间
EnemyAnimator = GetComponent();//获取动画控制器
Player = GameObject.FindWithTag("Player").transform;//获取主角
enemyState = EnemyState.Rest;//初始化状态
for (int i = 0; i < point.childCount; i++)//获取父物体下面的路点
{
Points.Add(point.GetChild(i));
}
MoveTarget = Points[0];//初始化移动目标
if (m_navmeshment == null || Player == null || EnemyAnimator == null)//以防出错
{
Debug.LogError("未添加完整组件或者是标签");
enabled = false;
return;
}
}
// Update is called once per frame
void Update () {
DealWithEnemyState();
//Debug.DrawLine(transform.position, transform.forward, Color.red);
//Debug.DrawLine(transform.position, Player.position - transform.position, Color.red);
}
private void DealWithEnemyState()//处理状态的方法
{
switch (enemyState)
{
case EnemyState.Patrol://巡逻
OnPatrolState();
ChangePatrolToOtherState();
break;
case EnemyState.Attack://攻击
OnAttackState();
ChangeAttackToOtherState();
break;
case EnemyState.Rest://休息
OnRestState();
ChangeRestToOtherState();
break;
case EnemyState.Chase://追击
OnChaseState();
ChangeChaseToOtherState();
break;
default:
break;
}
}
private void OnRestState()//处理休息
{
RestTime -= Time.deltaTime;//休息时间倒计时
EnemyAnimator.SetBool("walk", false);
EnemyAnimator.SetBool("attack", false);//播放动画
m_navmeshment.speed = 0;//移动速度设置为0
}
private void OnAttackState()//处理攻击
{
m_navmeshment.speed = 0;//速度设置为0
EnemyAnimator.SetBool("attack", true);
EnemyAnimator.SetBool("walk",false);//播放动画
}
private void OnChaseState()//处理追击
{
EnemyAnimator.SetBool("walk", true);
EnemyAnimator.SetBool("attack", false);//播放动画
MoveTarget = Player;//设置目标位Player
m_navmeshment.speed = 0.1f;//设置移动速度
MoveToTarget(MoveTarget);//开始移动追击
}
private void OnPatrolState()//处理巡逻
{
//移动到目标点,进行位移
m_navmeshment.speed = 0.1f;
if (canmove)
{
MoveToTarget(MoveTarget);
}
EnemyAnimator.SetBool("walk", true);
EnemyAnimator.SetBool("attack", false);
//播放移动动画
}
private void ChangeRestToOtherState()//休息状态到别的状态
{
if (IsOnSightOfEnemy())//在视野范围内
{
MoveTarget = Player; //设置目标MoveToTarget
print("在视野内");
if (IsOnAttackDistance())//在攻击范围内
{
print("在攻击范围内");
enemyState = EnemyState.Attack;//改变状态
}
else
{
print("不在攻击范围内");
enemyState = EnemyState.Chase;
}
}
else//不在视野范围内
//判断倒计时,且目标点还是巡逻点
{
//设置目标巡逻点
if (RestTime <= 0)//倒计时结束
{
RestTime = EnemyRestTime;//重置倒计时时间
canmove = true;//可以移动
if (index == Points.Count - 1)//循环路点
{
index = 0;
}
else
{
index++;
}
MoveTarget = Points[index];
enemyState = EnemyState.Patrol;//改变状态
}
else//倒计时未结束,不能移动
{
canmove = false;
}
}
}
private void ChangeAttackToOtherState()
{
//时刻监视状态
if (IsOnSightOfEnemy())
{
MoveTarget = Player; //设置目标MoveToTarget
print("在视野内");
if (IsOnAttackDistance())
{
print("在攻击范围内");
enemyState = EnemyState.Attack;
}
else
{
print("不在攻击范围内");
enemyState = EnemyState.Chase;
}
}
else
{
enemyState = EnemyState.Patrol;//不在视野内,也就是说主角逃走了,设置为巡逻
}
//判断血量
//判断目标
}//攻击到别的状态
private void ChangeChaseToOtherState()
{
if (IsOnSightOfEnemy())//在视野内
{
MoveTarget = Player; //设置目标MoveToTarget
print("在视野内");
if (IsOnAttackDistance())
{
print("在攻击范围内");
enemyState = EnemyState.Attack;
}
else
{
print("不在攻击范围内");
enemyState = EnemyState.Chase;
}
}
else
{
MoveTarget = Points[index];
enemyState = EnemyState.Patrol;
}
}//追击到别的状态
private void ChangePatrolToOtherState()
{
//监测状态
if (IsOnSightOfEnemy())
{
MoveTarget = Player; //设置目标MoveToTarget
print("在视野内");
if (IsOnAttackDistance())
{
print("在攻击范围内");
enemyState = EnemyState.Attack;
}
else
{
print("不在攻击范围内");
enemyState = EnemyState.Chase;
}
}
else
{
float dis = Vector3.Distance(transform.position, MoveTarget.position);
if (dis < 0.5f)
{
enemyState = EnemyState.Rest;
}
}
//如果不在视野内,目标还是巡逻点
}//巡逻到别的状态
private bool IsOnSightOfEnemy()//是否在可视范围内
{
bool onsight=false;
float dis = Vector3.Distance(Player.position, transform.position);
float angle = Vector3.Angle(transform.forward, Player.position - transform.position);
if (dis < MaxSightDistance)
{
if (angle < 70)
{
onsight = true;
}
}
return onsight;
}
private bool IsOnAttackDistance()//判断是否在攻击范围内的方法
{
if (IsOnSightOfEnemy())
{
float dis = Vector3.Distance(Player.position, transform.position);
if (dis < CanAttackDistance)
{
return true;
}
}
return false;
}
private void MoveToTarget(Transform target)//移动的方法
{
m_navmeshment.SetDestination(target.transform.position);
}
}
8.回到Unity中,设置巡逻路点,我这里设置3个点,其实设置多少个都可以,算法里会进行循环执行,如下图
9.把父路点赋给FsmDemo脚本里面的point,调整Zombie的位置,运行。主角在这里没有进行脚本控制,所以你可以在Scene面板中移动主角进行测试。
10.总结:其实总结下来,这种简单的有限状态机实现起来很简单,但是遇到复杂一点的状态机,就要考虑优化的问题了。所以说,学习的时候要有很强的逻辑性,也要研究一下自己的实现方式,是用什么模式实现,毕竟如果真是做项目,需要团队合作,这一点还是要考虑的。