赞
踩
最终效果:
目录
状态机如下:
(前排提示:本文是使用纯代码方式来实现状态机,包括各种状态的设定,以及状态间的切换,其他很多教程都不是这样的形式,而是直接使用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<StateType_, IState_> states = new Dictionary<StateType_, IState_>();
-
- public Parameter_ parameter;
- void Start()
- {
- //初始往状态机里新建两个字典
- states.Add(StateType_.Idle, new IdleState_(this));
- states.Add(StateType_.Patrol, new PatrolState_(this));
- TransititionState(StateType_.Idle);
-
- animator = GetComponent<Animator>();
- }
- 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()
- {
-
- }
- }
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。