赞
踩
前面做过有限状态机制作一个敌人AI:【unity实战】在Unity中使用有限状态机制作一个敌人AI
之前做的是2d平台的,但是俯视角怎么做呢?俯视角可能要复杂一些,要考虑4个方向和躲避障碍物,这里我就用A寻路插件来实现,关于A寻路插件,之前我也简单用过一次,感兴趣可以去看看:【推荐100个unity插件之1】2d使用A*Pathfinding插件实现敌人AI,并自动躲避障碍物
A*官网下载地址:https://arongranberg.com/astar/download
我们下载免费版即可
新建空物体,添加PathFinder组件,用在地图导航
点击生成寻路网格
我们可以修改节点的类型,我们选择四个方向就好了,这样算法更高效也能提升游戏性能
同时我们把障碍物全部剔别除出去,这边有指定要剔除的图层,找到我们的围墙和障碍物,比如Wall
效果
添加测试敌人,添加对应寻路组件
然后选择2D游戏常用的Y轴方向,并取消重力改为None,目标设置为玩家
运行效果,敌人跟随玩家的时候,有一条绿线,那就是自动局路的路线
我们可以给敌人的速度设置快一点
不过实际使用我们只要它的寻路功能来追击玩家,并到达攻击范围后停下来攻击玩家,离开追击范围便放弃追击,这就需要我们通过代码来实现,特定的需求。
我们也不需要在敌人身上挂那么多脚本,我们只需要保留Seeker组件
提供的寻路算法
其实官方文档有个简单的寻路demo
供我们参考:
https://arongranberg.com/astar/documentation/4_2_17_c030646a/astaraics.html
注意,调用生成路径的函数是一个相对较耗时的操作,如果每帧都立刻生成路径可能会对性能造成负担,所以这里我们就用到计时器了,每0.5秒调用一次路径生成函数
定义状态类型枚举
namespace Enemy
{
// 定义状态类型枚举
public enum StateType
{
Idle, //待机
Patrol, //巡逻
Chase, //追击
React, //反应
Attack, //攻击
Hit, //受击
Death //死亡
}
}
可序列化的参数类,存储了角色的各种状态参数和配置
namespace Enemy { [Serializable] public class Parameter { [Header("属性")] public int health; // TODO:测试 健康值 [HideInInspector] public Animator animator; // 角色动画控制器 [HideInInspector] public AnimatorStateInfo animatorStateInfo; // 动画状态信息 [HideInInspector] public Rigidbody2D rb; [Header("移动")] public float moveSpeed; // 移动速度 public float chaseSpeed; // 追击速度 [HideInInspector] public float currentSpeed; // 当前速度 [Header("巡逻")] public float idleTime; // 空闲时间 public Transform[] patrolPoints; // 巡逻点数组 [Header("追逐")] public LayerMask targetLayer; // 目标层 public int chaseDistance;//追逐的距离 [HideInInspector] public Transform target; // 目标对象 [Header("A*寻路")] [HideInInspector] public Seeker seeker;// 用于处理路径计算的 Seeker 组件。 [HideInInspector] public Path path; // Seeker 计算出的路径 [HideInInspector] public int currentWaypoint = 0; // 当前路径点的索引。 [HideInInspector] public bool reachedEndOfPath; // 标志位,指示 AI 是否到达路径的末尾。 [HideInInspector] public float nextWaypointDistance = 3f; // 到达路径点之前的距离。减速距离 [HideInInspector] public float repathRate = 0.5f; // 重新计算路径的频率(秒)。 [HideInInspector] public float lastRepath = float.NegativeInfinity; // 上次计算路径的时间。 [HideInInspector] public bool isPathRefresh;//是否刷新 [Header("攻击")] public Transform attackPoint; // 攻击点的位置 public float attackArea; // 攻击范围 [Header("受击")] [HideInInspector] public bool isHurt; // 是否被击中、 [HideInInspector] public bool isDead; // 是否死亡 } }
//抽象基类,定义了所有状态类的基本结构
namespace Enemy
{
//抽象基类,定义了所有状态类的基本结构
public abstract class IState
{
protected FSM manager;// 当前状态机
protected Parameter parameter;// 参数
public abstract void OnEnter();// 进入状态时的方法
public abstract void OnUpdate();// 更新方法
public abstract void OnFixedUpdate();// 固定更新方法
public abstract void OnExit();// 退出状态时的方法
}
}
有限状态机类
using System.Collections; using System.Collections.Generic; using Pathfinding; using UnityEngine; namespace Enemy { // 有限状态机类 public class FSM : MonoBehaviour { private IState currentState; // 当前状态接口 protected Dictionary<StateType, IState> states = new Dictionary<StateType, IState>(); // 状态字典,存储各种状态 public Parameter parameter; // 状态机参数 protected virtual void Awake() { } protected virtual void OnEnable() { parameter.animator = transform.GetComponent<Animator>(); // 获取角色上的动画控制器组件 parameter.seeker = GetComponent<Seeker>();// parameter.rb = GetComponent<Rigidbody2D>(); TransitionState(StateType.Idle); // 初始状态为Idle currentState.OnEnter(); } void Update() { parameter.animatorStateInfo = parameter.animator.GetCurrentAnimatorStateInfo(0);// 获取当前动画状态信息 GetPlayerTransform(); currentState.OnUpdate(); // 每帧更新当前状态 //TODO:用于测试 如果按下回车键,设置被击中状态为true if (Input.GetKeyDown(KeyCode.Return)) { parameter.isHurt = true; } } void FixedUpdate() { currentState.OnFixedUpdate(); } // 状态转换方法 public void TransitionState(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(-Mathf.Abs(transform.localScale.x), transform.localScale.y, transform.localScale.z); } else if (transform.position.x < target.position.x) { transform.localScale = new Vector3(Mathf.Abs(transform.localScale.x), transform.localScale.y, transform.localScale.z); } } } // 查找玩家的方法 public void GetPlayerTransform() { // 使用Physics2D.OverlapCircleAll获取位于指定距离内的所有Collider2D数组 Collider2D[] chaseColliders = Physics2D.OverlapCircleAll(transform.position, parameter.chaseDistance, parameter.targetLayer); // 如果找到了玩家 if (chaseColliders.Length > 0) { // 将第一个找到的玩家设为追踪目标 parameter.target = chaseColliders[0].transform; // 计算与目标的距离 // distance = Vector2.Distance(parameter.target.position, transform.position); } else { // 如果没有找到玩家,则目标置空 parameter.target = null; } } #region A*寻路方法 //每隔一段时间重新计算路径路径 public void StartPath(Transform target) { if (Time.time > parameter.lastRepath + parameter.repathRate && parameter.seeker.IsDone()) { parameter.lastRepath = Time.time; parameter.seeker.StartPath(transform.position, target.position, OnPathComplete); } } //寻路移动 public void Move() { // 还没有路径可以跟随,所以不执行任何操作 if (parameter.path == null) return; // 循环检查是否已经接近当前路径点,可以切换到下一个点 // 使用循环是因为许多路径点可能非常接近,可能在同一帧内到达多个路径点 parameter.reachedEndOfPath = false; // 当前路径点到代理的距离 float distanceToWaypoint; while (true) { // 如果希望最大化性能,可以检查平方距离而不是实际距离,避免使用平方根计算,但这超出了本教程的范围 distanceToWaypoint = Vector3.Distance(transform.position, parameter.path.vectorPath[parameter.currentWaypoint]); if (distanceToWaypoint < parameter.nextWaypointDistance) { // 检查是否还有下一个路径点,或者是否已经到达路径的末尾 if (parameter.currentWaypoint + 1 < parameter.path.vectorPath.Count) { parameter.currentWaypoint++; } else { // 设置一个状态变量,表示代理已经到达路径的末尾 // 如果你的游戏需要,可以使用这个变量来触发一些特殊代码 parameter.reachedEndOfPath = true; break; } } else { break; } } // 在接近路径末尾时平滑减速 // 这个值会在代理接近路径的最后一个路径点时,从 1 平滑过渡到 0 var speedFactor = parameter.reachedEndOfPath ? Mathf.Sqrt(distanceToWaypoint / parameter.nextWaypointDistance) : 1f; // 到下一个路径点的方向 // 归一化,使其长度为1个世界单位 Vector3 dir = (parameter.path.vectorPath[parameter.currentWaypoint] - transform.position).normalized; // 将方向乘以我们期望的速度,得到速度向量 Vector3 velocity = dir * parameter.currentSpeed * speedFactor; // 移动到目标点 // transform.position += velocity * Time.deltaTime; parameter.rb.velocity = velocity; } //路径计算完成时回调方法 public void OnPathComplete(Path p) { Debug.Log("计算出一条路径。是否出现错误?" + p.error); // 路径池。为了避免不必要的内存分配,路径使用引用计数。 // 调用 Claim 方法将引用计数加一,调用 Release 方法将其减一, // 当引用计数为零时,路径将被放入池中,其他脚本可以重用该路径。 // ABPath.Construct 和 Seeker.StartPath 方法会尽可能从池中获取路径。详见路径池文档页面。 p.Claim(this); if (!p.error) { if (parameter.path != null) parameter.path.Release(this); parameter.path = p; // 重置路径点计数器,以便开始移动到路径的第一个点 parameter.currentWaypoint = 0; } else { p.Release(this); } } #endregion // 触发器进入事件,检测到玩家时设置目标为玩家 private void OnTriggerEnter2D(Collider2D other) { if (other.CompareTag("Player")) { parameter.target = other.transform; } } // 触发器离开事件,玩家离开时清空目标 private void OnTriggerExit2D(Collider2D other) { if (other.CompareTag("Player")) { parameter.target = null; } } // 开始路径刷新计时 public void RefreshTiming() { StartCoroutine(nameof(DodgeOnCooldownCoroutine)); } public IEnumerator DodgeOnCooldownCoroutine() { parameter.isPathRefresh = false; yield return new WaitForSeconds(0.5f); parameter.isPathRefresh = true; } // 在Scene视图中绘制攻击范围的辅助图形 private void OnDrawGizmos() { //攻击范围 Gizmos.DrawWireSphere(parameter.attackPoint.position, parameter.attackArea); //追击范围 Gizmos.color = Color.yellow; Gizmos.DrawWireSphere(transform.position, parameter.chaseDistance); } } }
待机状态
using UnityEngine; namespace Enemy { public class IdleState : IState { private float timer; // 计时器 public IdleState(FSM manager) { this.manager = manager; this.parameter = manager.parameter; } public override void OnEnter() { parameter.animator.Play("Idle"); } public override void OnUpdate() { timer += Time.deltaTime; // 计时器累加 // 如果被击中了,转换到受击状态 if (parameter.isHurt) { manager.TransitionState(StateType.Hit); } // 如果有目标且目标在追逐范围内,则转换到反应状态 if (parameter.target) { manager.TransitionState(StateType.React); } // 如果达到空闲时间上限,则转换到巡逻状态 if (timer >= parameter.idleTime) { manager.TransitionState(StateType.Patrol); } } public override void OnFixedUpdate() { } public override void OnExit() { timer = 0; // 重置计时器 } } }
巡逻状态
using UnityEngine; //巡逻状态 namespace Enemy { public class PatrolState : IState { private int patrolPosition; // 当前巡逻点索引 public PatrolState(FSM manager) { this.manager = manager; this.parameter = manager.parameter; } public override void OnEnter() { parameter.animator.Play("Walk"); parameter.currentSpeed = parameter.moveSpeed; GeneratePatrolPoint(); manager.RefreshTiming(); } public override void OnUpdate() { // 如果被击中了,转换到受击状态 if (parameter.isHurt) { manager.TransitionState(StateType.Hit); } // 如果有目标且目标在追逐范围内,则转换到反应状态 if (parameter.target) { manager.TransitionState(StateType.React); } //如果已经接近当前巡逻点,则转换到空闲状态 if (Vector2.Distance(manager.transform.position, parameter.patrolPoints[patrolPosition].position) < .1f) { manager.TransitionState(StateType.Idle); } manager.StartPath(parameter.patrolPoints[patrolPosition]); // 如果速度接近静止(每一段时间检测,防止敌人互相卡住) if (parameter.rb.velocity.magnitude < 0.1f && parameter.isPathRefresh) { manager.RefreshTiming(); GeneratePatrolPoint(); } } public override void OnFixedUpdate() { // 朝向当前巡逻点 manager.FlipTo(parameter.patrolPoints[patrolPosition]); //移动到当前巡逻点 manager.Move(); } public override void OnExit() { GeneratePatrolPoint(); } //随机选择下一个巡逻点 public void GeneratePatrolPoint() { while (true) { // 选择一个随机的巡逻点索引 int i = Random.Range(0, parameter.patrolPoints.Length); // 确保新选择的巡逻点与当前不同 if (patrolPosition != i) // if (parameter.targetPointIndex != i) { // parameter.targetPointIndex = i; patrolPosition = i; break; // 退出循环 } } } } }
反应状态
//反应状态 namespace Enemy { public class ReactState : IState { public ReactState(FSM manager) { this.manager = manager; this.parameter = manager.parameter; } public override void OnEnter() { parameter.animator.Play("React"); } public override void OnUpdate() { // 如果被击中标志为true,转换到受击状态 if (parameter.isHurt) { manager.TransitionState(StateType.Hit); } // 如果动画播放进度超过95%,转换到追逐状态 if (parameter.animatorStateInfo.normalizedTime >= 0.95f) { manager.TransitionState(StateType.Chase); } } public override void OnFixedUpdate() { } public override void OnExit() { } } }
追击状态
using UnityEngine; //追击状态 namespace Enemy { public class ChaseState : IState { // 构造函数 public ChaseState(FSM manager) { this.manager = manager; this.parameter = manager.parameter; } public override void OnEnter() { parameter.animator.Play("Run"); parameter.currentSpeed = parameter.chaseSpeed; } public override void OnUpdate() { // 如果被击中了,转换到受击状态 if (parameter.isHurt) { manager.TransitionState(StateType.Hit); } // 如果目标不存在或者超出追逐范围,则转换到空闲状态 if (parameter.target == null) { manager.TransitionState(StateType.Idle); } // 如果检测到攻击范围内有目标,则转换到攻击状态 if (Physics2D.OverlapCircle(parameter.attackPoint.position, parameter.attackArea, parameter.targetLayer)) { manager.TransitionState(StateType.Attack); } if(parameter.target != null) manager.StartPath(parameter.target); } public override void OnFixedUpdate() { manager.FlipTo(parameter.target); // 面向目标 // 向目标位置移动 if (parameter.target != null) { //移动到当前目标点 manager.Move(); } } public override void OnExit() { } } }
攻击状态
namespace Enemy { public class AttackState : IState { public AttackState(FSM manager) { this.manager = manager; this.parameter = manager.parameter; } public override void OnEnter() { parameter.animator.Play("Attack"); parameter.rb.velocity = Vector2.zero; } public override void OnUpdate() { if (parameter.isHurt) { manager.TransitionState(StateType.Hit); } if (parameter.animatorStateInfo.normalizedTime >= .95f) { manager.TransitionState(StateType.Chase); } } public override void OnFixedUpdate() { } public override void OnExit() { } } }
受击状态
using UnityEngine; namespace Enemy { public class HitState : IState { public HitState(FSM manager) { this.manager = manager; this.parameter = manager.parameter; } public override void OnEnter() { parameter.animator.Play("Hit"); } public override void OnUpdate() { // 转换到死亡状态 if (parameter.isDead) { manager.TransitionState(StateType.Death); } // 如果动画播放进度超过95%,重新寻找玩家目标并转换到追逐状态 if (parameter.animatorStateInfo.normalizedTime >= 0.95f) { manager.TransitionState(StateType.Chase); // 转换到追逐状态 } } public override void OnFixedUpdate() { } public override void OnExit() { parameter.isHurt = false; // 离开状态时重置受击标志 } } }
死亡状态
namespace Enemy { public class DeathState : IState { public DeathState(FSM manager) { this.manager = manager; this.parameter = manager.parameter; } public override void OnEnter() { parameter.animator.Play("Dead"); parameter.rb.velocity = Vector2.zero; } public override void OnUpdate() { } public override void OnFixedUpdate() { } public override void OnExit() { } } }
配置
可以给敌人刚体加一个2d物理材质,去除摩檫力,防止敌人和碰撞体粘在一起
默认巡逻,发现敌人发起追击,靠近时发起攻击
玩家跑出追击范围,回到巡逻状态
赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注
,以便我第一时间收到反馈,你的每一次支持
都是我不断创作的最大动力。当然如果你发现了文章中存在错误
或者有更好的解决方法
,也欢迎评论私信告诉我哦!
好了,我是向宇
,https://xiangyu.blog.csdn.net
一位在小公司默默奋斗的开发者,出于兴趣爱好,最近开始自学unity,闲暇之余,边学习边记录分享,站在巨人的肩膀上,通过学习前辈们的经验总是会给我很多帮助和启发!php是工作,unity是生活!如果你遇到任何问题,也欢迎你评论私信找我, 虽然有些问题我也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。