当前位置:   article > 正文

在unity中使用状态机编写敌人的AI(纯代码方式)_unity 敌人ai

unity 敌人ai

最终效果:

目录

编写思路

基本代码框架

巡逻状态

追击状态

攻击状态

受击状态


编写思路

状态机如下:

(前排提示:本文是使用纯代码方式来实现状态机,包括各种状态的设定,以及状态间的切换,其他很多教程都不是这样的形式,而是直接使用unity内置的animator controller,然后手动设定不同状态间的衔接条件,然后在代码中控制条件,通过这种方式实现状态机,例如:

 

而本文的实现方式是使用纯代码层面实现状态机。

代码思路,书写一个通用的状态接口,包括onEnter、onExit、和onUpdate三个函数。然后接下来其他状态只需要根据不同的状态去实现该接口即可。

 

书写一个有限状态机的脚本,其包含了各个状态,用字典来存储,状态机在任何一个时刻只会处于一个状态。在状态机脚本里书写各个状态通用的功能,例如各个状态的切换函数,人物的转向函数。

在每个状态脚本都有onEnter、onExit函数,我们在状态机的脚本中,执行状态切换的时候去调用当前状态的onExit函数,以及新状态的onEnter函数。

在该状态的的update中,我们持续执行对应状态的onUpdate函数。然后当某个状态达到切换条件时,就需要切换状态,切换状态的方式就是让该状态的脚本去调用有限状态机的切换函数。

以Idle状态举例,在Idle状态的update函数中,玩家会停留IdleTime的时间,时间过后将会切换到Patrol状态,此时就调用有限状态机FSM中的切换状态函数。

 基本代码框架

先书写一个状态的接口,接下来的各种状态都要实现该接口。

  1. public interface IState_
  2. {
  3. void OnEnter();
  4. void OnUpdate();
  5. void OnExit();
  6. }

在动画控制器中为敌人创建动画控制器:

由于动画控制器本身就是状态机,因此不需要给这些动画间设置衔接,用脚本控制切换即可。

(当然你可以在很多地方的教程发现他们手动设置了很多条件,然后在不同动画的切换间设定了条件,这样也是一种状态机的实现方式,就是使用unity内置的animator的那种状态切换来实现的。那样的有限状态机的实现会更为简单易上手,并且各种状态的切换也更为可视化。玩家只需要设定好不同条件的衔接切换条件即可)

而本文是使用代码层面去完整的实现一个有限状态机的方式)

设定巡逻点和追击点:

接下来书写简易的状态控制机的框架:

包含了各种状态的枚举,TransititionState即状态切换时需要执行的操作以及flipto转向功能,以及所有功能中包含的参数,如巡逻时间,追踪时间。

除此之外,该状态机作为一个总的状态机,其包含所有的状态,此处使用字典来存储。

  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using UnityEngine;
  5. public enum StateType_
  6. {
  7. Idle,Patrol,Chase,React,Attack
  8. }
  9. [Serializable]
  10. public class Parameter_
  11. {
  12. public int health;
  13. public float moveSpeed;
  14. public float chaseSpeed;
  15. public float idleTime;
  16. public Transform[] patrolPoints;
  17. public Transform[] chasePoints;
  18. }
  19. public class FSM_ : MonoBehaviour
  20. {
  21. public Animator animator;
  22. private IState_ currentState;
  23. private Dictionary<StateType_, IState_> states = new Dictionary<StateType_, IState_>();
  24. public Parameter_ parameter;
  25. void Start()
  26. {
  27. //初始往状态机里新建两个字典
  28. states.Add(StateType_.Idle, new IdleState_(this));
  29. states.Add(StateType_.Patrol, new PatrolState_(this));
  30. TransititionState(StateType_.Idle);
  31. animator = GetComponent<Animator>();
  32. }
  33. private void Update()
  34. {
  35. currentState.OnUpdate();
  36. }
  37. public void TransititionState(StateType_ type)
  38. {
  39. if (currentState != null)
  40. currentState.OnExit();
  41. currentState = states[type];
  42. currentState.OnEnter();
  43. }
  44. public void FlipTo(Transform target)
  45. {
  46. if (target != null)
  47. {
  48. if (transform.position.x > target.position.x)
  49. {
  50. transform.localScale = new Vector3(-1, 1, 1);
  51. }
  52. else if (transform.position.x < target.position.x)
  53. {
  54. transform.localScale = new Vector3(1, 1, 1);
  55. }
  56. }
  57. }
  58. }

接下来以IdleState为例,书写等候的状态函数。

IdleState需要一些信息,比如等待时长,目标点,动画器,这些参数应该放在另外一个脚本中,然后挂接到enemy身上。为了方便,这里把那些参数放在了FSM这个脚本中,于是,使用IdleState这些状态的时候需要用到这些参数,此处就通过将FSM传给这些状态用来实例化。(而且此处的状态切换的函数也是在FSM脚本中实现的,为了方便的使用该函数,

代码如下:

IdleState实现了当等顿时间到了的时候就会自动切换到另外一个状态。

  1. public class IdleState_ : IState_
  2. {
  3. private FSM_ manager;
  4. private Parameter_ parameter;
  5. private float timer;
  6. public IdleState_(FSM_ manager)
  7. {
  8. this.manager = manager;
  9. this.parameter = manager.parameter;
  10. }
  11. public void OnEnter()
  12. {
  13. parameter.animator.Play("Idle");
  14. }
  15. public void OnUpdate()
  16. {
  17. timer += Time.deltaTime;
  18. if (timer >= parameter.idleTime)
  19. {
  20. manager.TransititionState(StateType_.Patrol);
  21. }
  22. }
  23. public void OnExit()
  24. {
  25. timer = 0;
  26. }
  27. }

此处实现了一个敌人的状态机,如果想要将该状态机的代码给其他敌人使用,可以将其通用的部分保留下来,然后用其他角色的状态机去继承这个状态机即可。

巡逻状态

接下来书写巡逻状态:

整体与上面类似,

  1. public class PatrolState_ : IState_
  2. {
  3. private FSM_ manager;
  4. private Parameter_ parameter;
  5. private int patrolPosition;//巡逻到第几个点了
  6. public PatrolState_(FSM_ manager)
  7. {
  8. this.manager = manager;
  9. this.parameter = manager.parameter;
  10. }
  11. public void OnEnter()
  12. {
  13. parameter.animator.Play("Walk");
  14. }
  15. public void OnUpdate()
  16. {
  17. manager.FlipTo(parameter.patrolPoints[patrolPosition]);//让敌人始终朝向巡逻点的方向
  18. manager.transform.position = Vector2.MoveTowards(manager.transform.position, parameter.patrolPoints[patrolPosition].position, parameter.moveSpeed * Time.deltaTime);
  19. if (Vector2.Distance(manager.transform.position, parameter.patrolPoints[patrolPosition].position)<0.1f) {
  20. manager.TransititionState(StateType_.Idle);
  21. }
  22. }
  23. public void OnExit()
  24. {
  25. patrolPosition++;
  26. if (patrolPosition >= parameter.patrolPoints.Length)
  27. {
  28. patrolPosition = 0;
  29. }
  30. }
  31. }

实现后效果如下:

追击状态

为敌人添加一个子物体,用于充当敌人的眼睛,当进入敌人的视野范围内,将会切换到追击状态:

  1. private void OnTriggerEnter2D(Collider2D collision)
  2. {
  3. parameter.target = collision.transform;
  4. }

接下来书写追击状态的函数:

  1. public class ChaseState_ : IState_
  2. {
  3. private FSM_ manager;
  4. private Parameter_ parameter;
  5. private float timer;
  6. public ChaseState_(FSM_ manager)
  7. {
  8. this.manager = manager;
  9. this.parameter = manager.parameter;
  10. }
  11. public void OnEnter()
  12. {
  13. parameter.animator.Play("Walk");
  14. }
  15. public void OnUpdate()
  16. {
  17. manager.FlipTo(parameter.target);
  18. if (parameter.target != null)
  19. {
  20. manager.transform.position = Vector2.MoveTowards(manager.transform.position, parameter.target.position, parameter.chaseSpeed * Time.deltaTime);
  21. }
  22. if(parameter.target==null ||
  23. manager.transform.position.x < parameter.chasePoints[0].position.x ||
  24. manager.transform.position.x > parameter.chasePoints[1].position.x)
  25. {
  26. manager.TransititionState(StateType_.Patrol);
  27. }
  28. if (true)//进入攻击范围,执行攻击)
  29. {
  30. }
  31. }
  32. public void OnExit()
  33. {
  34. timer = 0;
  35. }
  36. }

当玩家进入敌人的攻击范围时,敌人将切换到攻击状态。

攻击状态

进入到攻击范围的状态虽然可以简单的通过与玩家的距离来实现,但是那样需要手动测量较为麻烦,此处使用范围检测的函数实现,需要设定敌人的攻击点,攻击范围半径,在parameter里添加这两个参数。

  1. private void OnTriggerEnter2D(Collider2D collision)
  2. {
  3. parameter.target = collision.transform;
  4. }
  5. private void OnTriggerExit(Collider other)
  6. {
  7. if (other.CompareTag("Player"))
  8. {
  9. parameter.target = null;
  10. }
  11. }

为了直观的看清楚圆的范围,可以使用画图将其画出来:

  1. private void OnDrawGizmos()
  2. {
  3. Gizmos.DrawWireSphere(parameter.attackPoint.position, parameter.attackAreaRadius);
  4. }

然后在追击的update中添加转换到攻击的条件及切换攻击状态:

  1. if (Physics2D.OverlapCircle(parameter.attackPoint.position,parameter.attackAreaRadius,parameter.targetLayer))//进入攻击范围,执行攻击)
  2. //注意,此处需要添加层级,以防止敌人检测到其他的物体则也进入攻击
  3. {
  4. manager.TransititionState(StateType_.Attack);
  5. }

添加反应状态

为了使敌人在巡逻时也能进入攻击玩家的状态,在idleState和patrolState中添加代码:

如果存在目标,并且目标没有超出追击范围以外的话则追击,如果即使看到了目标,超出了追击范围,则也将不再追击。

对于反应状态其实比较简单,只需要播放动画,当动画播放完成时切换回追击状态即可:

攻击状态与此类似

  1. public class AttackState_ : IState_
  2. {
  3. private FSM_ manager;
  4. private Parameter_ parameter;
  5. private AnimatorStateInfo animStateInfo;//动画状态信息
  6. public AttackState_(FSM_ manager)
  7. {
  8. this.manager = manager;
  9. this.parameter = manager.parameter;
  10. }
  11. public void OnEnter()
  12. {
  13. parameter.animator.Play("Attack");
  14. }
  15. public void OnUpdate()
  16. {
  17. animStateInfo = parameter.animator.GetCurrentAnimatorStateInfo(0);
  18. if (animStateInfo.normalizedTime >= .95f)//当动画进度接近1的时候,认为动画接近完成了
  19. {
  20. manager.TransititionState(StateType_.Chase);
  21. }
  22. }
  23. public void OnExit()
  24. {
  25. }
  26. }

这样就实现了站岗、巡逻、反应、追击、攻击的状态切换。

总结一下就是其实逻辑很简单,先实现简单的站岗巡逻逻辑,在站岗巡逻相互切换的逻辑之前添加一个更优先的判定条件,如果站岗或巡逻中发现目标(通过触发器实现)则切换到反应状态,反应状态就只是简单的播放反应动画,播放完后玩家立马进入追击动画,追击动画足够近时(通过球形判定)则进入攻击状态即可。每攻击完一次,自动进入追击状态,如果还处于攻击范围则会立马再次进入攻击状态,如果超出攻击范围则进行追击,超出追击范围则回去巡逻。

这样的状态机,如果出现了新的状态只需要注册新的状态,并书写切换条件即可

受击状态

 受击状态比较特别,受击状态的判定在上述的各种状态中优先级更高,因此在上述所有状态的onupdate函数中都应该嵌入若受击则切换到受击状态。

添加一个伤害判定:

在所有状态那里添加:

  1. if (parameter.getHit)
  2. {
  3. manager.TransititionState(StateType_.Hit);
  4. }

受击状态和死亡状态:

  1. public class HitState_ : IState_
  2. {
  3. private FSM_ manager;
  4. private Parameter_ parameter;
  5. private AnimatorStateInfo animStateInfo;//动画状态信息
  6. public HitState_(FSM_ manager)
  7. {
  8. this.manager = manager;
  9. this.parameter = manager.parameter;
  10. }
  11. public void OnEnter()
  12. {
  13. parameter.animator.Play("Hit");
  14. parameter.health--;
  15. }
  16. public void OnUpdate()
  17. {
  18. animStateInfo = parameter.animator.GetCurrentAnimatorStateInfo(0);
  19. if (parameter.health <= 0)
  20. {
  21. manager.TransititionState(StateType_.Death);
  22. }
  23. if (animStateInfo.normalizedTime >= .95f)
  24. {
  25. parameter.target = GameObject.FindWithTag("Player").transform;
  26. manager.TransititionState(StateType_.Chase);
  27. }
  28. }
  29. public void OnExit()
  30. {
  31. parameter.getHit = false;
  32. }
  33. }
  34. public class DeathState_ : IState
  35. {
  36. private FSM manager;
  37. private Parameter parameter;
  38. public DeathState_(FSM manager)
  39. {
  40. this.manager = manager;
  41. this.parameter = manager.parameter;
  42. }
  43. public void OnEnter()
  44. {
  45. parameter.animator.Play("Dead");
  46. }
  47. public void OnUpdate()
  48. {
  49. }
  50. public void OnExit()
  51. {
  52. }
  53. }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/blog/article/detail/90713
推荐阅读
相关标签
  

闽ICP备14008679号