Unity 基于navMesh的怪物追踪惯性系统
今天做项目适合 策划想要实现一个在现有的怪物追踪系统上实现怪物拥有惯性功能
以下是解决方案分享:
怪物基类代码:
using UnityEngine;
using UnityEngine.AI;
[RequireComponent(typeof(NavMeshAgent))]
[RequireComponent(typeof(AudioSource))]
public abstract class MonsterBase : MonoBehaviour
{
public enum MonsterState { Idle, Patrol, Chase }//1.待机,2.巡逻,3.追击
[Header("巡逻时的速度")]
public float patrolSpeed = 2f;
[Header("追击玩家时的速度")]
public float chaseSpeed = 5f;
[Header("怪物的加速度")]
public float acceleration = 50f; // 大加速度
[Header("怪物的旋转速度")]
public float angularSpeed = 120f;
[Header("怪物与目标的停止距离")]
public float stoppingDistance = 0.1f;
[Header("怪屋听觉能检测到玩家的范围")]
public float detectionRange = 10f;
[Space(30)]
[Header("视觉检测设置")]
[Tooltip("扇形检测半径")]
public float visionRange = 5f;
[Tooltip("扇形检测角度(度)")]
[Range(0, 360)]
public float visionAngle = 90f;
[Tooltip("检测层级(应包含玩家和障碍物)")]
public LayerMask visionMask;
[Header("声音设置")]
public AudioClip howlSound;
[Header("最大音量")]
public float maxHowlVolume = 1f;
[Header("开始嚎叫距离阈值")]
public float howlDistance = 5f;
protected NavMeshAgent agent;
protected AudioSource audioSource;//怪物声音
protected Transform player;
protected MonsterState currentState = MonsterState.Patrol;
protected Animator animator;
protected SpriteRenderer spriteRenderer;//精灵组件
private Vector2 lastPosition;//上一帧位置
private Direction currentDirection = Direction.Down;//当前移动方向
protected enum Direction { Up, Down, Left, Right }
void Awake()
{
agent = GetComponent<NavMeshAgent>();
audioSource = GetComponent<AudioSource>();
animator = GetComponent<Animator>();
spriteRenderer = GetComponentInChildren<SpriteRenderer>();
player = GameObject.FindGameObjectWithTag("Player").transform;
ConfigureAgent();
}
void ConfigureAgent()
{
agent.acceleration = acceleration;
agent.angularSpeed = angularSpeed;
agent.stoppingDistance = stoppingDistance;
agent.updateRotation = false; // 禁用自动旋转,我们自己控制
agent.updateUpAxis = false; // 保持2D平面
}
void Update()
{
UpdateState();
UpdateAnimation();
UpdateHowlSound();
}
/// <summary>
/// 抽象状态机
/// </summary>
protected abstract void UpdateState();
public virtual void UpdateAnimation()
{
// 计算移动方向
Vector2 movement = (Vector2)transform.position - lastPosition;
lastPosition = transform.position;
if (movement.magnitude > 0.01f) // 正在移动
{
if (Mathf.Abs(movement.x) > Mathf.Abs(movement.y))
{
currentDirection = movement.x > 0 ? Direction.Right : Direction.Left;
}
else
{
currentDirection = movement.y > 0 ? Direction.Up : Direction.Down;
}
//TODO:考虑设计CONST表
// animator.SetBool("IsMoving", true);
// animator.SetInteger("Direction", (int)currentDirection);
}
else // 待机
{
// animator.SetBool("IsMoving", false);
}
// 更新Sprite朝向
if (currentDirection == Direction.Left)
{
spriteRenderer.flipX = true;
}
else if (currentDirection == Direction.Right)
{
spriteRenderer.flipX = false;
}
}
/// <summary>
/// 获取当前移动方向(归一化向量)
/// </summary>
protected Vector3 GetMovementDirection()
{
if (agent != null && agent.velocity.magnitude > 0.1f)
{
return agent.velocity.normalized;
}
return transform.forward; // 默认使用面朝方向
}
protected bool CanSeePlayer()
{
if (player == null) return false;
Vector3 directionToPlayer = player.position - transform.position;
float distanceToPlayer = directionToPlayer.magnitude;
// 距离检查
if (distanceToPlayer > visionRange)
return false;
// 获取当前移动方向
Vector3 moveDirection = GetMovementDirection();
// 角度检查(基于移动方向)
float angleToPlayer = Vector3.Angle(moveDirection, directionToPlayer);
if (angleToPlayer > visionAngle / 2f)
return false;
// 视线遮挡检查
RaycastHit hit;
if (Physics.Raycast(transform.position, directionToPlayer.normalized, out hit, visionRange, visionMask))
{
return hit.collider.CompareTag("Player");
}
return false;
}
/// <summary>
/// 在Scene视图中绘制视觉范围(调试用)
/// </summary>
/// <summary>
/// 绘制基于移动方向的扇形检测范围
/// </summary>
private void OnDrawGizmos()
{
// 获取当前移动方向(编辑器中使用面朝方向,运行时用实际移动方向)
Vector3 moveDir = Application.isPlaying ? GetMovementDirection() : transform.forward;
// 设置扇形颜色
Gizmos.color = new Color(1, 0.5f, 0, 0.3f); // 半透明橙色
// 计算扇形边缘
Vector3 center = transform.position;
Vector3 forward = moveDir * visionRange;
Vector3 left = Quaternion.Euler(0, -visionAngle / 2, 0) * forward;
Vector3 right = Quaternion.Euler(0, visionAngle / 2, 0) * forward;
// 绘制扇形边缘线
Gizmos.DrawLine(center, center + left);
Gizmos.DrawLine(center, center + right);
// 绘制扇形弧线
Vector3 lastPoint = center + left;
int segments = 20;
for (int i = 1; i <= segments; i++)
{
float t = (float)i / segments;
float angle = Mathf.Lerp(-visionAngle / 2, visionAngle / 2, t);
Vector3 nextPoint = center + Quaternion.Euler(0, angle, 0) * forward;
Gizmos.DrawLine(lastPoint, nextPoint);
lastPoint = nextPoint;
}
// 绘制移动方向指示器
Gizmos.color = Color.red;
Gizmos.DrawLine(center, center + moveDir * 1.5f);
}
void UpdateHowlSound()
{
if (howlSound == null) return;
float distanceToPlayer = Vector3.Distance(transform.position, player.position);
if (distanceToPlayer <= howlDistance)
{
if (!audioSource.isPlaying)
{
audioSource.clip = howlSound;
audioSource.Play();
}
// 距离越近声音越大
float volume = Mathf.Lerp(0.1f, maxHowlVolume, 1 - (distanceToPlayer / howlDistance));
audioSource.volume = volume;
}
else if (audioSource.isPlaying)
{
audioSource.Stop();
}
}
protected void ChangeState(MonsterState newState)
{
if (currentState == newState) return;
currentState = newState;
switch (currentState)
{
case MonsterState.Idle:
agent.isStopped = true;
break;
case MonsterState.Patrol:
agent.speed = patrolSpeed;
agent.isStopped = false;
break;
case MonsterState.Chase:
agent.speed = chaseSpeed;
agent.isStopped = false;
break;
}
}
void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.CompareTag("Player"))
{
Debug.Log("触碰到玩家");
//TODO:玩家死亡
//collision.gameObject.GetComponent<PlayerController>().Die();
}
}
带有惯性的怪物实现:
using UnityEngine;
using UnityEngine.AI;
[RequireComponent(typeof(NavMeshAgent))]
[RequireComponent(typeof(Rigidbody))]
public class Angry : MonsterBase
{
[Header("数据配置表")]
public MonsterConfig config;
[Header("巡逻点数组")]
public Transform[] patrolPoints;
[Header("每个巡逻点的停留时间")]
public float waitTimeAtPoint = 2f;
[Header("探测范围(圆)")]
public float AngrydetectionRange = 2f;
[Header("移动惯性超出系数")]
public float overshootFactor = 1.8f;
[Header("急撒系数")]
public float brakeForce = 4f;
[Header("最小启动惯性移动阈值速度")]
public float overshootMinSpeed = 0.5f;
[Header("触发惯性移动距离倍速")]
public float overshootTriggerDistance = 1.5f;
// 私有变量
private int currentPatrolIndex = 0;
private float waitTimer = 0f;
private bool isWaiting = false;
private bool isPlayerMove = false;
// 惯性相关
private Vector3 lastVelocity;
private bool isOvershooting = false;
private Rigidbody rb;
private Vector3 overshootStartPosition;
public void Start()
{
// 获取组件
rb = GetComponent<Rigidbody>();
agent = GetComponent<NavMeshAgent>();
player = GameObject.FindGameObjectWithTag("Player").transform;
// 初始化配置
detectionRange = config.detectionRange;
stoppingDistance = config.stoppingDistance;
acceleration = config.acceleration;
angularSpeed = config.angularSpeed;
howlDistance = config.howlDistance;
chaseSpeed = config.chaseSpeed;
patrolSpeed = config.patrolSpeed;
maxHowlVolume = config.maxHowlVolume;
howlSound = config.howlSound;
// 代理设置
agent.autoBraking = false;
agent.updatePosition = false;
agent.updateRotation = false;
// 刚体设置
rb.isKinematic = false;
rb.interpolation = RigidbodyInterpolation.Interpolate;
rb.collisionDetectionMode = CollisionDetectionMode.Continuous;
rb.constraints = RigidbodyConstraints.FreezeRotation | RigidbodyConstraints.FreezePositionY;
// 初始巡逻目标
if (patrolPoints.Length > 0)
{
SetPatrolDestination();
}
isPlayerMove = player.GetComponent<Movement>().IsPlayerMoving();
}
private void Update()
{
UpdateState();
// 记录速度用于惯性(仅当代理活跃时)
if (agent.velocity.magnitude > 0.1f && !isOvershooting && agent.isActiveAndEnabled)
{
lastVelocity = agent.velocity;
}
}
private void FixedUpdate()
{
if (isOvershooting)
{
HandleOvershoot();
}
else if (agent.isOnNavMesh)
{
// 同步NavMeshAgent和Rigidbody位置
agent.nextPosition = rb.position;
rb.velocity = agent.velocity;
}
}
protected override void UpdateState()
{
float distanceToPlayer = Vector3.Distance(transform.position, player.position);
bool canSeePlayer = CanSeePlayer();
// 只在非惯性状态下处理状态转换
if (!isOvershooting)
{
if (canSeePlayer)
{
ChangeState(MonsterState.Chase);
ChasePlayer();
}
else if (distanceToPlayer <= AngrydetectionRange && isPlayerMove)
{
ChangeState(MonsterState.Chase);
ChasePlayer();
}
else if (currentState == MonsterState.Chase)
{
ChangeState(MonsterState.Patrol);
// 返回巡逻时立即设置目标
if (patrolPoints.Length > 0) SetPatrolDestination();
}
if (currentState == MonsterState.Patrol)
{
Patrol();
}
}
}
void ChasePlayer()
{
if (isOvershooting) return;
agent.speed = chaseSpeed;
agent.SetDestination(player.position);
// 接近玩家时触发惯性
float distanceToPlayer = Vector3.Distance(transform.position, player.position);
if (distanceToPlayer < agent.stoppingDistance * overshootTriggerDistance)
{
StartOvershoot();
}
}
void Patrol()
{
if (patrolPoints.Length == 0)
{
ChangeState(MonsterState.Idle);
return;
}
if (isWaiting)
{
waitTimer += Time.deltaTime;
if (waitTimer >= waitTimeAtPoint)
{
isWaiting = false;
waitTimer = 0f;
currentPatrolIndex = (currentPatrolIndex + 1) % patrolPoints.Length;
SetPatrolDestination();
}
return;
}
// 使用路径状态和距离判断
if (!agent.pathPending && agent.remainingDistance <= agent.stoppingDistance + 0.2f)
{
isWaiting = true;
}
// 添加目标丢失的重新设置逻辑
else if (!agent.hasPath || agent.pathStatus != NavMeshPathStatus.PathComplete)
{
SetPatrolDestination();
}
}
void SetPatrolDestination()
{
if (isOvershooting || patrolPoints.Length == 0 || currentPatrolIndex >= patrolPoints.Length)
return;
agent.speed = patrolSpeed;
NavMeshHit hit;
if (NavMesh.SamplePosition(patrolPoints[currentPatrolIndex].position, out hit, 2.0f, NavMesh.AllAreas))
{
agent.SetDestination(hit.position);
agent.isStopped = false;
}
else
{
Debug.LogWarning($"无法导航到巡逻点: {patrolPoints[currentPatrolIndex].name}");
// 自动跳到下一个点
currentPatrolIndex = (currentPatrolIndex + 1) % patrolPoints.Length;
SetPatrolDestination();
}
}
void StartOvershoot()
{
isOvershooting = true;
overshootStartPosition = transform.position;
agent.isStopped = true;
// 计算惯性速度(保留原有方向但增加速度)
lastVelocity = lastVelocity.normalized * agent.speed * overshootFactor;
rb.velocity = lastVelocity;
}
void HandleOvershoot()
{
// 应用减速
lastVelocity = Vector3.Lerp(lastVelocity, Vector3.zero, brakeForce * Time.fixedDeltaTime);
rb.velocity = lastVelocity;
// 检查是否该停止惯性
if (lastVelocity.magnitude < overshootMinSpeed ||
Vector3.Distance(overshootStartPosition, transform.position) > chaseSpeed * 3f)
{
EndOvershoot();
}
}
void EndOvershoot()
{
isOvershooting = false;
rb.velocity = Vector3.zero;
if (agent.isOnNavMesh)
{
agent.Warp(transform.position);
agent.isStopped = false;
// 根据当前状态恢复行为
if (currentState == MonsterState.Patrol)
{
SetPatrolDestination();
}
else if (currentState == MonsterState.Chase &&
Vector3.Distance(transform.position, player.position) <= AngrydetectionRange)
{
ChasePlayer();
}
}
}
void OnCollisionEnter(Collision collision)
{
if (isOvershooting)
{
// 撞墙反弹效果
if (collision.gameObject.CompareTag("Wall"))
{
lastVelocity = Vector3.Reflect(lastVelocity, collision.contacts[0].normal) * 0.7f;
rb.velocity = lastVelocity;
}
// 撞到玩家
if (collision.gameObject.CompareTag("Player"))
{
//TODO:
//collision.gameObject.GetComponent<PlayerHealth>().TakeDamage(1);
EndOvershoot();
}
}
}
public override void UpdateAnimation()
{
Vector3 moveDirection = isOvershooting ? rb.velocity : agent.velocity;
//TODO:添加Animation后更新启用
// if (moveDirection.magnitude > 0.1f)
// {
// animator.SetBool("IsMoving", true);
//
// // 确定方向(2.5D游戏通常只需要4方向)
// if (Mathf.Abs(moveDirection.x) > Mathf.Abs(moveDirection.z))
// {
// animator.SetInteger("Direction", moveDirection.x > 0 ? 2 : 3); // Right/Left
// }
// else
// {
// animator.SetInteger("Direction", moveDirection.z > 0 ? 0 : 1); // Up/Down
// }
//
// // 设置动画速度(惯性时更快)
// animator.SetFloat("MoveSpeed", moveDirection.magnitude / (isOvershooting ? chaseSpeed * 1.5f : chaseSpeed));
// }
// else
// {
// animator.SetBool("IsMoving", false);
// }
//
// // 更新惯性状态
// animator.SetBool("IsOvershooting", isOvershooting);
}
void OnDrawGizmosSelected()
{
// 绘制巡逻路径
if (patrolPoints != null && patrolPoints.Length > 0)
{
Gizmos.color = Color.yellow;
for (int i = 0; i < patrolPoints.Length; i++)
{
if (patrolPoints[i] != null)
{
Gizmos.DrawSphere(patrolPoints[i].position, 0.3f);
if (i < patrolPoints.Length - 1 && patrolPoints[i+1] != null)
{
Gizmos.DrawLine(patrolPoints[i].position, patrolPoints[i+1].position);
}
}
}
}
// 绘制当前目标
if (currentState == MonsterState.Patrol && patrolPoints != null && currentPatrolIndex < patrolPoints.Length)
{
Gizmos.color = Color.red;
Gizmos.DrawLine(transform.position, patrolPoints[currentPatrolIndex].position);
}
}
ok分享结束!