最终效果:
目录
编写思路
基本代码框架
巡逻状态
追击状态
攻击状态
受击状态
状态机如下:
(前排提示:本文是使用纯代码方式来实现状态机,包括各种状态的设定,以及状态间的切换,其他很多教程都不是这样的形式,而是直接使用unity内置的animator controller,然后手动设定不同状态间的衔接条件,然后在代码中控制条件,通过这种方式实现状态机,例如:
而本文的实现方式是使用纯代码层面实现状态机。
代码思路,书写一个通用的状态接口,包括onEnter、onExit、和onUpdate三个函数。然后接下来其他状态只需要根据不同的状态去实现该接口即可。
书写一个有限状态机的脚本,其包含了各个状态,用字典来存储,状态机在任何一个时刻只会处于一个状态。在状态机脚本里书写各个状态通用的功能,例如各个状态的切换函数,人物的转向函数。
在每个状态脚本都有onEnter、onExit函数,我们在状态机的脚本中,执行状态切换的时候去调用当前状态的onExit函数,以及新状态的onEnter函数。
在该状态的的update中,我们持续执行对应状态的onUpdate函数。然后当某个状态达到切换条件时,就需要切换状态,切换状态的方式就是让该状态的脚本去调用有限状态机的切换函数。
以Idle状态举例,在Idle状态的update函数中,玩家会停留IdleTime的时间,时间过后将会切换到Patrol状态,此时就调用有限状态机FSM中的切换状态函数。
先书写一个状态的接口,接下来的各种状态都要实现该接口。
public interface IState_
{
void OnEnter();
void OnUpdate();
void OnExit();
}
在动画控制器中为敌人创建动画控制器:
由于动画控制器本身就是状态机,因此不需要给这些动画间设置衔接,用脚本控制切换即可。
(当然你可以在很多地方的教程发现他们手动设置了很多条件,然后在不同动画的切换间设定了条件,这样也是一种状态机的实现方式,就是使用unity内置的animator的那种状态切换来实现的。那样的有限状态机的实现会更为简单易上手,并且各种状态的切换也更为可视化。玩家只需要设定好不同条件的衔接切换条件即可)
而本文是使用代码层面去完整的实现一个有限状态机的方式)
设定巡逻点和追击点:
接下来书写简易的状态控制机的框架:
包含了各种状态的枚举,TransititionState即状态切换时需要执行的操作以及flipto转向功能,以及所有功能中包含的参数,如巡逻时间,追踪时间。
除此之外,该状态机作为一个总的状态机,其包含所有的状态,此处使用字典来存储。
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum StateType_
{
Idle,Patrol,Chase,React,Attack
}
[Serializable]
public class Parameter_
{
public int health;
public float moveSpeed;
public float chaseSpeed;
public float idleTime;
public Transform[] patrolPoints;
public Transform[] chasePoints;
}
public class FSM_ : MonoBehaviour
{
public Animator animator;
private IState_ currentState;
private Dictionary states = new Dictionary();
public Parameter_ parameter;
void Start()
{
//初始往状态机里新建两个字典
states.Add(StateType_.Idle, new IdleState_(this));
states.Add(StateType_.Patrol, new PatrolState_(this));
TransititionState(StateType_.Idle);
animator = GetComponent();
}
private void Update()
{
currentState.OnUpdate();
}
public void TransititionState(StateType_ type)
{
if (currentState != null)
currentState.OnExit();
currentState = states[type];
currentState.OnEnter();
}
public void FlipTo(Transform target)
{
if (target != null)
{
if (transform.position.x > target.position.x)
{
transform.localScale = new Vector3(-1, 1, 1);
}
else if (transform.position.x < target.position.x)
{
transform.localScale = new Vector3(1, 1, 1);
}
}
}
}
接下来以IdleState为例,书写等候的状态函数。
IdleState需要一些信息,比如等待时长,目标点,动画器,这些参数应该放在另外一个脚本中,然后挂接到enemy身上。为了方便,这里把那些参数放在了FSM这个脚本中,于是,使用IdleState这些状态的时候需要用到这些参数,此处就通过将FSM传给这些状态用来实例化。(而且此处的状态切换的函数也是在FSM脚本中实现的,为了方便的使用该函数,
代码如下:
IdleState实现了当等顿时间到了的时候就会自动切换到另外一个状态。
public class IdleState_ : IState_
{
private FSM_ manager;
private Parameter_ parameter;
private float timer;
public IdleState_(FSM_ manager)
{
this.manager = manager;
this.parameter = manager.parameter;
}
public void OnEnter()
{
parameter.animator.Play("Idle");
}
public void OnUpdate()
{
timer += Time.deltaTime;
if (timer >= parameter.idleTime)
{
manager.TransititionState(StateType_.Patrol);
}
}
public void OnExit()
{
timer = 0;
}
}
此处实现了一个敌人的状态机,如果想要将该状态机的代码给其他敌人使用,可以将其通用的部分保留下来,然后用其他角色的状态机去继承这个状态机即可。
接下来书写巡逻状态:
整体与上面类似,
public class PatrolState_ : IState_
{
private FSM_ manager;
private Parameter_ parameter;
private int patrolPosition;//巡逻到第几个点了
public PatrolState_(FSM_ manager)
{
this.manager = manager;
this.parameter = manager.parameter;
}
public void OnEnter()
{
parameter.animator.Play("Walk");
}
public void OnUpdate()
{
manager.FlipTo(parameter.patrolPoints[patrolPosition]);//让敌人始终朝向巡逻点的方向
manager.transform.position = Vector2.MoveTowards(manager.transform.position, parameter.patrolPoints[patrolPosition].position, parameter.moveSpeed * Time.deltaTime);
if (Vector2.Distance(manager.transform.position, parameter.patrolPoints[patrolPosition].position)<0.1f) {
manager.TransititionState(StateType_.Idle);
}
}
public void OnExit()
{
patrolPosition++;
if (patrolPosition >= parameter.patrolPoints.Length)
{
patrolPosition = 0;
}
}
}
实现后效果如下:
为敌人添加一个子物体,用于充当敌人的眼睛,当进入敌人的视野范围内,将会切换到追击状态:
private void OnTriggerEnter2D(Collider2D collision)
{
parameter.target = collision.transform;
}
接下来书写追击状态的函数:
public class ChaseState_ : IState_
{
private FSM_ manager;
private Parameter_ parameter;
private float timer;
public ChaseState_(FSM_ manager)
{
this.manager = manager;
this.parameter = manager.parameter;
}
public void OnEnter()
{
parameter.animator.Play("Walk");
}
public void OnUpdate()
{
manager.FlipTo(parameter.target);
if (parameter.target != null)
{
manager.transform.position = Vector2.MoveTowards(manager.transform.position, parameter.target.position, parameter.chaseSpeed * Time.deltaTime);
}
if(parameter.target==null ||
manager.transform.position.x < parameter.chasePoints[0].position.x ||
manager.transform.position.x > parameter.chasePoints[1].position.x)
{
manager.TransititionState(StateType_.Patrol);
}
if (true)//进入攻击范围,执行攻击)
{
}
}
public void OnExit()
{
timer = 0;
}
}
当玩家进入敌人的攻击范围时,敌人将切换到攻击状态。
进入到攻击范围的状态虽然可以简单的通过与玩家的距离来实现,但是那样需要手动测量较为麻烦,此处使用范围检测的函数实现,需要设定敌人的攻击点,攻击范围半径,在parameter里添加这两个参数。
private void OnTriggerEnter2D(Collider2D collision)
{
parameter.target = collision.transform;
}
private void OnTriggerExit(Collider other)
{
if (other.CompareTag("Player"))
{
parameter.target = null;
}
}
为了直观的看清楚圆的范围,可以使用画图将其画出来:
private void OnDrawGizmos()
{
Gizmos.DrawWireSphere(parameter.attackPoint.position, parameter.attackAreaRadius);
}
然后在追击的update中添加转换到攻击的条件及切换攻击状态:
if (Physics2D.OverlapCircle(parameter.attackPoint.position,parameter.attackAreaRadius,parameter.targetLayer))//进入攻击范围,执行攻击)
//注意,此处需要添加层级,以防止敌人检测到其他的物体则也进入攻击
{
manager.TransititionState(StateType_.Attack);
}
添加反应状态
为了使敌人在巡逻时也能进入攻击玩家的状态,在idleState和patrolState中添加代码:
如果存在目标,并且目标没有超出追击范围以外的话则追击,如果即使看到了目标,超出了追击范围,则也将不再追击。
对于反应状态其实比较简单,只需要播放动画,当动画播放完成时切换回追击状态即可:
攻击状态与此类似
public class AttackState_ : IState_
{
private FSM_ manager;
private Parameter_ parameter;
private AnimatorStateInfo animStateInfo;//动画状态信息
public AttackState_(FSM_ manager)
{
this.manager = manager;
this.parameter = manager.parameter;
}
public void OnEnter()
{
parameter.animator.Play("Attack");
}
public void OnUpdate()
{
animStateInfo = parameter.animator.GetCurrentAnimatorStateInfo(0);
if (animStateInfo.normalizedTime >= .95f)//当动画进度接近1的时候,认为动画接近完成了
{
manager.TransititionState(StateType_.Chase);
}
}
public void OnExit()
{
}
}
这样就实现了站岗、巡逻、反应、追击、攻击的状态切换。
总结一下就是其实逻辑很简单,先实现简单的站岗巡逻逻辑,在站岗巡逻相互切换的逻辑之前添加一个更优先的判定条件,如果站岗或巡逻中发现目标(通过触发器实现)则切换到反应状态,反应状态就只是简单的播放反应动画,播放完后玩家立马进入追击动画,追击动画足够近时(通过球形判定)则进入攻击状态即可。每攻击完一次,自动进入追击状态,如果还处于攻击范围则会立马再次进入攻击状态,如果超出攻击范围则进行追击,超出追击范围则回去巡逻。
这样的状态机,如果出现了新的状态只需要注册新的状态,并书写切换条件即可
受击状态比较特别,受击状态的判定在上述的各种状态中优先级更高,因此在上述所有状态的onupdate函数中都应该嵌入若受击则切换到受击状态。
添加一个伤害判定:
在所有状态那里添加:
if (parameter.getHit)
{
manager.TransititionState(StateType_.Hit);
}
受击状态和死亡状态:
public class HitState_ : IState_
{
private FSM_ manager;
private Parameter_ parameter;
private AnimatorStateInfo animStateInfo;//动画状态信息
public HitState_(FSM_ manager)
{
this.manager = manager;
this.parameter = manager.parameter;
}
public void OnEnter()
{
parameter.animator.Play("Hit");
parameter.health--;
}
public void OnUpdate()
{
animStateInfo = parameter.animator.GetCurrentAnimatorStateInfo(0);
if (parameter.health <= 0)
{
manager.TransititionState(StateType_.Death);
}
if (animStateInfo.normalizedTime >= .95f)
{
parameter.target = GameObject.FindWithTag("Player").transform;
manager.TransititionState(StateType_.Chase);
}
}
public void OnExit()
{
parameter.getHit = false;
}
}
public class DeathState_ : IState
{
private FSM manager;
private Parameter parameter;
public DeathState_(FSM manager)
{
this.manager = manager;
this.parameter = manager.parameter;
}
public void OnEnter()
{
parameter.animator.Play("Dead");
}
public void OnUpdate()
{
}
public void OnExit()
{
}
}