导航服务体:NavagationService

using System;
using System.Collections;
using System.Collections.Generic;

using Pathfinding;

using UnityEngine;

public class NavigationService : FSMServiceBase
{
    public override void OnAnimationEnd(PlayerState state)
    {
        base.OnAnimationEnd(state);
    }

    public override void OnBegin(PlayerState state)
    {
        base.OnBegin(state);
    }

    public override void OnDisable(PlayerState state)
    {
        base.OnDisable(state);
    }

    public override void OnEnd(PlayerState state)
    {
        base.OnEnd(state);
    }

    public override void OnUpdate(float normalizedTime, PlayerState state)
    {
        base.OnUpdate(normalizedTime, state);
        OnMove();
    }

    List<Vector3> _path;
    public int state;//0空闲 1正在寻找路线 2返回寻路结果
    Vector3 _point;//寻路的目的地
    public int currentWaypoint;//路点索引(当前的)
    public Vector3 _pathLast;//路径的最后一个位置 
    public Action _success;//寻路成功后要触发的事件

    public void Move(Vector3 position, Action success)
    {
        if (state == 0 || (state == 1 && position != _point))
        {
            _point = position;
            this._success = success;
            state = 1;
            var p = NavHelper.Instance.GetWalkNearestPosition(position);
            player.seeker.StartPath(player._transform.position, p, OnPathComplete);
        }

    }

    private void OnPathComplete(Path path)
    {
        if (state == 1)
        {
            state = 2;

            if (path.error == false)
            {
                _path = path.vectorPath;
                if (_path.Count <= 1)
                {
                    currentWaypoint = 0;
                }
                else
                {
                    currentWaypoint = 1;
                }
                _pathLast = _path[_path.Count - 1];
                this.player._transform.LookTarget(_path[currentWaypoint]);
                this._success?.Invoke();
            }
            else
            {
                Stop();
            }
        }
    }

    private void OnMove()
    {
        if (_path == null) { return; }
        //每帧朝下一个路点进行移动
        //判断是否接近路点了,是的话 更新到下一个路点

        if (currentWaypoint >= _path.Count)
        {
            Stop();
            return;
        }
        else
        {
            var next_point = _path[currentWaypoint];
            Vector3 dir = next_point - player._transform.position;
            dir.y = 0;
            this.player.Move(dir * player.GetMoveSpeed(), false);

            if (Vector3.Distance(this.player._transform.position, next_point) <= 0.5f)
            {
                if (currentWaypoint >= _path.Count - 1)
                {
                    Stop();
                }
                else
                {
                    currentWaypoint += 1;
                    this.player._transform.LookTarget(_path[currentWaypoint]);
                }
            }

        }
    }


    public void Stop()
    {
        _path = null;
        state = 0;
    }


    public override void ReLoop(PlayerState state)
    {
        base.ReLoop(state);
    }

    public override void ReStart(PlayerState state)
    {
        base.ReStart(state);
    }

    internal bool IsEnd()
    {
        return _path == null;
    }
}

Move方法 

   List<Vector3> _path;
    public int state;//0空闲 1正在寻找路线 2返回寻路结果
    Vector3 _point;//寻路的目的地
    public int currentWaypoint;//路点索引(当前的)
    public Vector3 _pathLast;//路径的最后一个位置 
    public Action _success;//寻路成功后要触发的事件

public void Move(Vector3 position, Action success)
{
    // 条件检查:空闲状态 或 寻路中但新目标不同
    if (state == 0 || (state == 1 && position != _point)) 
    {
        _point = position;         // 保存目标位置
        _success = success;         // 保存回调函数
        state = 1;                  // 设为寻路中状态
        
        // 获取实际可行走的最近点(处理不可达位置)
        Vector3 p = NavHelper.Instance.GetWalkNearestPosition(position);
        
        // 开始寻路请求(异步)
        player.seeker.StartPath(
            player._transform.position, // 起点:角色当前位置
            p,                         // 终点:修正后的可行走点
            OnPathComplete              // 寻路完成回调(代码中未展示)
        );
    }
}

核心逻辑说明

  1. 状态控制

    • 仅在空闲(0)正在寻路但新目标不同时响应新移动请求

    • 避免重复请求相同目标位置的路径

  2. 路径修正

    • 通过NavHelper将用户输入位置转换为导航网格( NavMesh )上的可行走点

  3. 异步寻路

    • 使用第三方寻路库(如A* Pathfinding)的StartPath方法

    • 异步计算路径,完成后触发OnPathComplete回调(需在类中实现)

  4. 回调机制

    • 保存_success委托,用于路径计算成功后触发业务逻辑


当调用Move()时:

  1. 先通过GetWalkNearestPosition获取单个可行走目标点

  2. StartPath生成由多个可行走点连成的完整路线

  3. 路径生成后立即触发OnPathComplete(此时角色尚未移动)

  4. 角色开始逐个行走路径点,直到走完最后一个点后停止"

 OnPathComplete方法

    List<Vector3> _path;
    public int state;//0空闲 1正在寻找路线 2返回寻路结果
    Vector3 _point;//寻路的目的地
    public int currentWaypoint;//路点索引(当前的)
    public Vector3 _pathLast;//路径的最后一个位置 
    public Action _success;//寻路成功后要触发的事件

private void OnPathComplete(Path path)
    {
        if (state == 1)//只有当当前状态为 1(表示"路径计算中")时才处理路径结果

        {
            state = 2;//将状态标记为 2(可能表示"路径已就绪")

            if (path.error == false)//路径计算成功时处理路径点,失败时执行终止操作
            {
                _path = path.vectorPath;  // 存储路径点列表
                _pathLast = _path[_path.Count - 1];  // 记录终点坐标

                // 设置当前路点索引
                if (_path.Count <= 1)
                {
                  currentWaypoint = 0;   // 只有一个点(起点即终点)
                } 
                else 
                {
                  currentWaypoint = 1;   // 跳过起点(索引0),从第二个点开始移动
                }

                // 玩家转向目标点
                this.player._transform.LookTarget(_path[currentWaypoint]);

                // 触发成功回调
                this._success?.Invoke();
                }
                else
               {
                Stop();
               }
        }
    }

关键行为说明

  • 路径点处理

    • _path 存储路径坐标序列(起点 → 途径点 → 终点)

    • currentWaypoint 指向玩家即将移动到的下一个目标点索引

    • 当路径点 > 1 时,直接跳过起点(索引0),因为角色通常已在起点位置

  • 角色转向
    LookTarget(_path[currentWaypoint]) 使玩家立即面向第一个移动目标点(通常是路径的第二个点)

  • 事件通知
    _success?.Invoke() 触发订阅该事件的回调(通知其他系统路径就绪)

OnMove方法 

private void OnMove()
{
    if (_path == null) { return; } // 1. 路径不存在时直接返回
    
    if (currentWaypoint >= _path.Count) // 2. 终点检查
    {
        Stop(); // 到达终点停止移动
        return;
    }
    else
    {
        var next_point = _path[currentWaypoint]; // 3. 获取当前目标点
        Vector3 dir = next_point - player._transform.position;
        dir.y = 0; // 4. 忽略垂直方向
        
        // 5. 执行移动(水平移动)
        this.player.Move(dir * player.GetMoveSpeed(), false);

        // 6. 距离检测与路点切换
        if (Vector3.Distance(this.player._transform.position, next_point) <= 0.5f) 
        {
            if (currentWaypoint >= _path.Count - 1) // 7. 如果是最终点
            {
                Stop(); // 到达最终点停止运动
            }
            else
            {
                currentWaypoint += 1; // 8. 切换到下一个路点(即路点索引号+1)
                this.player._transform.LookTarget(_path[currentWaypoint]); // 9. 转向新目标
            }
        }
    }
}

关键步骤说明

  1. 路径有效性检查
    if (_path == null) return;
    确保存在有效路径时才执行移动

  2. 终点判定
    currentWaypoint >= _path.Count
    当当前路点索引超过路径点总数时,说明已到达终点

  3. 移动方向计算
    dir = next_point - player.position
    计算从角色当前位置指向目标点的向量

  4. 水平移动限制
    dir.y = 0
    忽略Y轴差异,确保角色只在水平面移动(适合地面角色)

  5. 移动执行
    player.Move(dir * speed, false)
      这里的Move方法是FSM中的Move方法,这里false指的是让AI敌人基于世界坐标朝向进行移动,为什么输入距离可以看链接中——帧的无关化案例进行理解Fsm Move方法https://blog.csdn.net/2303_80204192/article/details/149228501?spm=1001.2014.3001.5501#t21

  6. 路点接近检测
    距离 ≤ 0.5f
    当角色与当前目标点距离小于0.5单位时,判定为已到达

  7. 路径终点处理
    检测是否是最后一个路点,是则停止移动

  8. 路点切换
    currentWaypoint += 1
    移动到路径中的下一个点

  9. 方向调整
    LookTarget(新路点)
    角色立即转向新的目标点

OnEnd方法 

List<Vector3> _path;
public int state;//0空闲 1正在寻找路线 2返回寻路结果
public void Stop()
    {
        _path = null;
        state = 0;
    }

        将路径存储列表设置为空, 将寻路状态设置为空闲状态。

Stop方法 

public void Stop()
    {
        _path = null;
        state = 0;
    } 

        清空路径点列表,且使导航状态state设置为空闲。

IsEnd方法 

internal bool IsEnd()
{
    return _path == null;
} 

 
  • 检查 _path 是否为 null,并返回检查结果

  • _path 值 导航状态 IsEnd() 返回值
    null 导航结束 true
    非 null 导航中 false

IntEX类

public static class IntEx
{
    public static int Range(int min, int max)
    {
        return UnityEngine.Random.Range(min, max + 1);
    }

    public static bool InRange(this int x)
    {
        if (x <= 0) return false;
        return UnityEngine.Random.Range(0, 101) <= x;
    }

 IntEx 的静态工具类,用于处理角色状态的数学逻辑

1、随机数生成

public static int Range(int min, int max)
{
    return UnityEngine.Random.Range(min, max + 1);
}
  • 生成 min 到 max(包含最大值)的随机整数

  • 修正了 Unity 原生 Random.Range 对整数的行为

方法 原生 Random.Range(1, 10) IntEx.Range(1, 10)
范围 [1, 10) → 1-9 [1, 11) → 1-10
包含 不包含最大值 包含最大值

2、InRange 扩展方法

public static bool InRange(this int x)
{
    if (x <= 0) return false;
    return UnityEngine.Random.Range(0, 101) <= x;
}

功能

  • 判断是否触发概率事件(百分比形式)

    • 看随机生成的整数 UnityEngine.Random.Range(0, 101)与导入的x大小关系

    • x大返回true,即生成暴击,反之则不会暴击。
       

有关扩展方法的知识内容:
扩展方法https://www.cnblogs.com/yuer20180726/p/10901123.html扩展方法的调用:

if (unitEntity.pacing_probability.InRange())
//直接用int类型的实例unitEntity.pacing_probability进行调用
//而不是IntEX.InRange(unitEntity.pacing_probability);

 UnitManager类

public class UnitManager
{
    static UnitManager instance = new UnitManager();
    public static UnitManager Instance => instance;

    public FSM player;
}

        这段代码实现了一个单例模式(Singleton)的 UnitManager 类,其核心目的是创建一个全局可访问的游戏单位管理器,特别是为了存储和管理玩家角色的引用

public FSM player;
  • 设计意图:集中管理玩家对象,避免频繁使用 FindObjectOfType 等低效查找 

UTransform类 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public static class UTransform
{
    public static void LookTarget(this Transform t, Transform target)
    {
        if (target == null)
        {
            return;
        }
        Vector3 targetPosition = new Vector3(target.position.x, t.position.y, target.position.z);
        t.LookAt(targetPosition);
    }

    public static void LookTarget(this Transform t, Vector3 target)
    {
        Vector3 targetPosition = new Vector3(target.x, t.position.y, target.z);
        t.LookAt(targetPosition);
    }

    public static Vector3 GetOffsetPoint(this Transform t, float radius, float angle)
    {
        if (radius == 0 && angle == 0)
        {
            return t.transform.position;
        }
        float x = Mathf.Sin(angle * Mathf.PI / 180) * radius;
        float z = Mathf.Cos(angle * Mathf.PI / 180) * radius;
        Vector3 end = t.position + t.rotation * new Vector3(x, 0, z);
        return end;
    }


}

LookTarget方法(两种) 

public static void LookTarget(this Transform t, Transform target)//传入当前位置和目标位置
    {
        if (target == null)
        {
            return;
        }
        Vector3 targetPosition = new Vector3(target.position.x, t.position.y, target.position.z);
//新建目标变量使其x轴,z轴不变,y轴与目标位置一致,即保证看向目标动作是在一个水平面,而不会因为y轴使看向动作让模型发生Y轴偏移
        t.LookAt(targetPosition);
    }
  • 参数类型:接收一个 Transform 类型的目标对象,当目标是一个游戏对象时使用

public static void LookTarget(this Transform t, Vector3 target)
    {
        Vector3 targetPosition = new Vector3(target.x, t.position.y, target.z);
        t.LookAt(targetPosition);
    }
  • 参数类型:接收一个 Vector3 类型的目标坐标,当目标是一个坐标点时使用

 GetOffsetPoint方法

        定义了一个名为 GetOffsetPoint 的扩展方法,用于计算以物体为中心、基于给定半径和角度的偏移位置点,(即坐标系中)

public static Vector3 GetOffsetPoint(this Transform t, float radius, float angle)
    {
        if (radius == 0 && angle == 0)//如果半径和角度数值为0,则返回
        {
            return t.transform.position;
        }
        float x = Mathf.Sin(angle * Mathf.PI / 180) * radius;
        float z = Mathf.Cos(angle * Mathf.PI / 180) * radius;
        Vector3 end = t.position + t.rotation * new Vector3(x, 0, z);
        return end;
    }

 1、角度转换成弧度

float x = Mathf.Sin(angle * Mathf.PI / 180) * radius;
float z = Mathf.Cos(angle * Mathf.PI / 180) * radius;

角度转换部分:

csharp

angle * Mathf.PI / 180
  • angle:输入的角度值(单位:度)

  • Mathf.PI:Unity的数学常量,表示π(约3.14159)

  • 转换原理:将角度转换为弧度(三角函数需要弧度制)

    • 弧度 = 角度 × π / 180

三角函数计算:

float x = Mathf.Sin(angle * Mathf.PI / 180) //括弧里面是弧度
float z = Mathf.Cos(angle * Mathf.PI / 180)
这些三角函数得出的结果——半径为1的单位圆的弧点

float x = Mathf.Sin(angle * Mathf.PI / 180) * radius;
将三角函数的结果乘以半径值,将单位圆上的点(半径为1)缩放到实际需要的尺寸


计算过程示例:

角度转弧度:30 × π/180 ≈ 0.5236弧度

计算X坐标:sin(0.5236) ≈ 0.5 × 5 = 2.5

计算Z坐标:cos(0.5236) ≈ 0.866 × 5 ≈ 4.33

结果坐标:(2.5, 0, 4.33

2、对整体对象进行旋转

Vector3 end = t.position + t.rotation * new Vector3(x, 0, z);

new Vector3(x, 0, z);
这是一个在物体局部坐标系中的向量,基于之前的三角函数计算:
1.x = sin(角度) * 半径 → 右侧偏移
2.z = cos(角度) * 半径 → 前方偏移

t.rotation *  new Vector3(x, 0, z);
关键操作:将局部偏移向量从物体坐标系转换到世界坐标系(跟四元数的旋转机制有关,这里不深究)

Vector3 end = t.position + t.rotation * new Vector3(x, 0, z);
将旋转后的偏移量加到物体的世界坐标上,得到相对于物体位置和朝向的最终世界坐标

        这里大概就是方法,根据当前物体作为原点,获取目标点坐标的图示原理

NavHelper类:导航助手

using Pathfinding;
using UnityEngine;
public class NavHelper
{
    //通过静态实例确保全局只有一个NavHelper对象,通过Instance属性提供全局访问入口。
    static NavHelper instance = new NavHelper();
    public static NavHelper Instance => instance;

    //NNConstraint是A Pathing插件中用于定义寻路搜索条件的类
    NNConstraint nNConstraint;
    public Vector3 GetWalkNearestPosition(Vector3 pos)
    {
        if (nNConstraint == null)
        {
            nNConstraint = new NNConstraint();
            nNConstraint.constrainWalkability = true; // 启用可行走性约束
            nNConstraint.walkable = true;             // 只搜索可行走的点
            nNConstraint.constrainDistance = true;    // 启用距离约束
        }
        return AstarPath.active.GetNearest(pos, nNConstraint).position;

    }
}

if (nNConstraint == null)
        {
            nNConstraint = new NNConstraint();
            nNConstraint.constrainWalkability = true; // 启用可行走性约束
            nNConstraint.walkable = true;             // 只搜索可行走的点
            nNConstraint.constrainDistance = true;    // 启用距离约束
        }
        //使用A*插件的GetNearest方法,根据约束条件查找距离pos最近的可行走点坐标。
        return AstarPath.active.GetNearest(pos, nNConstraint).position;
  1. 初始化约束(首次调用时):

    • 创建一个只搜索可行走区域的过滤器(walkable=true)。

  2. 查询最近点

    • 通过AstarPath.active.GetNearest()方法,结合约束条件搜索导航网格。

    • 返回最近可行走点的坐标

  3. 计算最近可行走点的特性:
    • 同步执行

      csharp
      
      AstarPath.active.GetNearest(pos, nNConstraint).position;

      这是单次即时计算,调用后立即返回结果(同一帧内完成)

    • 非渐进式

      • 不涉及分帧处理

      • 不会逐步生成多个点

      • 每次调用只返回一个最近的有效点

    • 底层机制

      • 基于预先生成的导航网格(Grid/Point Graph)

      • 使用四叉树/KD树空间分区加速查询

FSM新加入的AI相关类

AI监听机制 

       if (AI == false)//禁用AI,即当前FSM持有者是玩家的时候
        {
           ……
        }
        else(AI)启用AI,即当前FSM持有者是AI的时候
        {
            foreach (var item in stateData)
            {
                //进入状态超过多长时间
                if (item.Value.excel_config.active_attack > 0)
                {
                    AddListener(item.Key, StateEventType.update, AutoTriggerAtk_AI);
                }

                if (item.Value.excel_config.trigger_pacing > 0)
                {
                    AddListener(item.Key, StateEventType.onAnmEnd, TriggerPacing);
                }

                if (item.Value.excel_config.tag == 4)
                {
                    AddListener(item.Key, StateEventType.update, OnPacingUpdate);
                }

                if (item.Value.excel_config.trigger_patrol > 0)
                {
                    AddListener(item.Key, StateEventType.update, TriggerPatrol);
                }
            }
            //AI要注册的事件
            GameEvent.OnPlayerAtk += OnPlayerAtk;
            AddListener(1014, StateEventType.onAnmEnd, OnDashEnd);
            AddListener(1042, StateEventType.update, OnMoveToPoint);
            AddListener(1042, StateEventType.end, NavStop);
            AddListener(1013, StateEventType.update, AI_Defending);
            AddListener(10131, StateEventType.update, AI_Defending);
            AddListener(1043, StateEventType.update, OnPatrolUpdate);
            AddListener(1043, StateEventType.begin, ChangeMoveSpeed);
            AddListener(1043, StateEventType.end, OnPatrolEnd);

        }
        //AI与玩家都要用到的监听器
        AddListener(1017, StateEventType.update, OnBashUpdate);
        AddListener(1017, StateEventType.end, OnBashEnd);
        AddListener(1018, StateEventType.update, OnBashUpdate);
        AddListener(1018, StateEventType.end, OnBashEnd);

    }

 遍历表格的含有判断条件的监听器如下:

        创建监听表看看哪些当前状态能触发所设置好的事件。

这里AI注册事件是两种监听机制: 


1、全局游戏事件监听

GameEvent.OnPlayerAtk += OnPlayerAtk;

GameEvent新加入方法如下
public class GameEvent
{
    public static Action<FSM, SkillEntity> OnPlayerAtk;
}
  • 监听全局的玩家攻击事件

    • 当玩家发起攻击时,触发 OnPlayerAtk 方法

  • 使用 C# 原生的 事件委托机制 (event/delegate

    • += 操作符:将右侧方法添加到事件的调用列表

    • 效果:当GameEvent.OnPlayerAtk触发时,当前类的OnPlayerAtk方法会被调用


2、状态机事件监听

AddListener(1014, StateEventType.onAnmEnd, OnDashEnd);
AddListener(1042, StateEventType.update, OnMoveToPoint);
AddListener(1042, StateEventType.end, NavStop);
  • 监听特定状态机的事件

  • 特性 GameEvent 事件系统 AddListener 状态机事件
    作用范围 全局事件(跨系统通信) 局部事件(特定状态机内部)
    事件类型 游戏逻辑事件(如攻击/死亡等) 状态机生命周期事件
    解耦程度 完全解耦(发布者不知道订阅者存在) 需要知道具体状态机实例
    典型使用场景 系统间通信(如UI响应游戏事件) 状态机内部行为控制
    执行效率 反射/委托调用(稍慢) 直接调用(更快)
    内存管理 需手动注销(否则内存泄漏) 通常随状态机自动销毁

OnPacingUpdate方法 

private void OnPacingUpdate()
    {
        // 状态超时检查:如果当前状态持续时间≥5秒
        if (GameTime.time - currentState.begin >= 5)
        {
        // 切换到状态ID 1001
        ToNext(1001);
        }

       // 检查是否存在攻击目标
        if (atk_target != null)
        {
          // 使AI面向攻击目标
          LookAtkTarget();

        // 检查与目标的距离是否≤3个单位
          if (GetDst() <= 3)
        {
            // 执行攻击行为
            AIAtk();
            // 立即结束本次状态更新
            return;
        }
}
    }

这是一个AI行为的状态更新方法,主要处理两个逻辑:

  1. 状态超时切换:如果当前状态持续超过5秒,则切换到新状态

  2. 目标攻击处理:如果有攻击目标,处理面向目标和攻击逻辑

OnMoveToPoint方法 

float _OnMoveToPoint_CheckTime;
private void OnMoveToPoint()
    {
        //每0.1秒执行一次核心逻辑
        if (GameTime.time - _OnMoveToPoint_CheckTime > 0.1)
        {
            _OnMoveToPoint_CheckTime = GameTime.time;
            //预定要执行的攻击技能ID和攻击目标都存在时
            if (next_atk != 0 && atk_target != null)
            {
                var dst = GetDst();//获取目标与自身的距离
                if (dst <= stateData[next_atk].skill.atk_distance)//判断是否在施法范围内
                {
                    navigationService.Stop();//停止导航移动
                    this._transform.LookTarget(atk_target._transform);//看向目标
                    ToNext(next_atk);//切换到攻击状态
                    next_atk = 0;//清空预定攻击
                }
                else
                {
                    //寻路到终点了  或者移动超时5f
                    if (navigationService.IsEnd() || GameTime.time - currentState.begin >= 5f)
                    {
                        navigationService.Stop();//停止移动
                        /*ToNext(1001);*/
                        AIAtk();//进行攻击
                        next_atk = 0;
                    }
                }
            }
        }
    }

1、时间差定时器 

 if (GameTime.time - _OnMoveToPoint_CheckTime > 0.1)
        {
            _OnMoveToPoint_CheckTime = GameTime.time;
        }
  • GameTime.time:当前游戏运行的总时间(秒)

  • _OnMoveToPoint_CheckTime:上次执行逻辑时记录的时间戳

  • 两者相减得到从上一次执行到现在经过的时间

    • 检查时间差是否超过0.1秒(100毫秒),如果条件成立,说明距离上次执行已超过0.1秒

    • 将当前时间赋值给_OnMoveToPoint_CheckTime,即重置记录的时间,为下一次检查做准备

时间差计时器的使用案例:

t=0.000s(游戏开始):
GameTime.time = 0.0
_OnMoveToPoint_CheckTime = 0.0
差值 = 0.0 < 0.1 → 不执行

t=0.100s:
GameTime.time ≈ 0.100
差值 = 0.100 - 0.000 = 0.100 > 0.1 → 执行逻辑
更新 _OnMoveToPoint_CheckTime = 0.100

t=0.116s:
差值 = 0.116 - 0.100 = 0.016 < 0.1 → 不执行

t=0.200s:
差值 = 0.200 - 0.100 = 0.100 > 0.1 → 再次执行
更新 _OnMoveToPoint_CheckTime = 0.200

if (navigationService.IsEnd() || GameTime.time - currentState.begin >= 5f)
  1. currentState.begin:

    • 表示当前状态开始执行的时间点

    • 通过 SetBeginTime() 方法设置:

      public bool ToNext(int Next)
          {
              if (stateData.ContainsKey(Next))//先检查stateData是否含有此状态
              {         
                  currentState = stateData[Next];
                  currentState.SetBeginTime();//currentState.begin:进入新状态的开始计时的计时器
              }
              return false;
          }​
  2. GameTime.time:

    • 表示当前游戏运行的总时间(秒)

    • 在 GameTime.Update() 中每帧更新

  3. GameTime.time - currentState.begin:

    • 计算从状态开始到现在经过的时间

    • 相当于:当前时间 - 状态开始时间 = 状态持续时间

 TriggerPacing方法

private void TriggerPacing()
    {
        if (IsDead())//如果自己已经死亡,直接退出,什么都不做
        {
            return;
        }

        if (unitEntity.pacing_probability > 0 )//当单位配置里的pacing_probability大于0
        {

            if (unitEntity.pacing_probability.InRange())//判断是否踱步(使用InRange随机生成数看是否触发当前概率)
            {
                if (atk_target == null)//如果当前没有攻击目标(atk_target == null),就测量一下自己跟玩家的距离;
                {
                    var dst = Vector3.Distance(_transform.position, UnitManager.Instance.player._transform.position);
                    if (dst < 10)//距离小于 10 就把玩家设成目标,否则直接退出。
                    {
                        atk_target = UnitManager.Instance.player;
                    }
                    else
                    {
                        return;
                    }
                }

                if (currentState.excel_config.tag == 4)
                {
            //如果正处于待机状态,必须等 3 到 6 秒的随机时间后才能继续,防止动作切换太频繁。
                    if (GameTime.time - currentState.begin >= IntEx.Range(3, 6))
                    {
                        var next = IntEx.Range(1036, 1041);//随机选一个攻击动作:从 1036 到 1041 之间随机挑一个动作 ID
                        _transform.LookTarget(atk_target._transform);
                        ToNext(next);
                    }
                }
                else//如果不处于待机状态,则直接发动攻击
                {
                    var next = IntEx.Range(1036, 1041);
                    _transform.LookTarget(atk_target._transform);
                    ToNext(next);
                }

            }
        }

 OnPlayerAtk方法

private void OnPlayerAtk(FSM atk, SkillEntity arg2)
    {
        if (att_crn.hp <= 0)
        {
            return;
        }
        //5米距离内且玩家在AI面前
        if (GetDst(atk._transform) <= 5
            && _transform.ForwardOrBack(atk._transform.position) > 0)//这里是AI为当前位置,目标位置是玩家(即攻击方)
        {
            if (unitEntity.block_probability.InRange())//随机生成数判定是否成功格挡
            {
        //检查当前动画进度是否处于配置允许触发防御动作的时间区间内,确保防御动作只能在角色动画合适的阶段被激活。
                if (CheckConfig(currentState.excel_config.on_defense))//
                {
                    _transform.LookTarget(atk._transform);AI朝向攻击方
                    var result = ToNext((int)currentState.excel_config.on_defense[2]);AI进入格挡状态
                    if (result)
                    {
                        return;//进入了格挡状态后,直接退出OnPlayerAtk方法
                    }
                }
            }

            else if (unitEntity.dodge_probability.InRange())//随机生成数判定是否成功闪避
            {
            //trigger_dodge在表中只有1和0,为1时当前状态可以躲避
                if (currentState.excel_config.trigger_dodge > 0)
                {
                    _transform.LookTarget(atk._transform);
                    var next = IntEx.Range(1032, 1035);//随机从1032-1035生成一个状态id
                    var result = ToNext(next);
                    if (result)
                    {
                        return;
                    }
                }
            }

            else if (unitEntity.atk_probability.InRange())//随机生成数判定是否触发攻击
            { //抢手攻击的检测
                if (currentState.excel_config.first_strike > 0)first_strike
                {
                    TriggerAtk_AI();
                }
            }
        }
    }

        触发躲闪,触发抢攻,即调度攻击决策的表格如下:

TriggerAtk_AI方法

private void TriggerAtk_AI()
    {
    //当前动画进度大于表格中攻击参数触发点,调用AIAtk();
        if (animationService.normalizedTime >= currentState.excel_config.trigger_atk)
        {
            AIAtk();
        }
    }

AIAtk方法 

    int next_atk;
    private void AIAtk()
    {
        if (IsDead())
        {
            return;
        }
        next_atk = IntEx.Range(1005, 1012);//随机从1005~1012中挑一个出来

        //挑选攻击的决策
        //根据对方的技能ID 自己做出对应的技能ID

        if (stateData[next_atk].skill.atk_distance > 0)//当前状态的施法距离是否大于0
        {
            if (atk_target == null)//若攻击目标为空,则添加UnitManager.Instance中的player实例
            {
                atk_target = UnitManager.Instance.player;

            }

            var dst =Vector3.Distance(_transform.position,atk_target._transform.position);//取AI与目标的距离
            
            if (dst >= stateData[next_atk].skill.atk_distance)//若二者距离大于等于施法距离
            {
                this._transform.LookTarget(atk_target._transform);//看向玩家
                if (IntEx.Range(0, 100) <= 50)//取一半概率,若此时寻路状态空闲,则寻路至玩家位置
                {
                    
                    if (navigationService.state == 0)
                    {
                        navigationService.Move(atk_target._transform.position, MoveToPoint);
                    }
                }
                else
                {
                    ToNext(1014); //突进冲刺 
                }
            }
            else//二者距离小于施法距离,随机进入一种攻击状态
            {
                this._transform.LookTarget(atk_target._transform);
                ToNext(next_atk);
            }
        }
    }

OnBash方法 

//fb: 整数参数,可能是击退类型或强度标识
//atk: FSM类型对象(有限状态机),表示攻击者
//add_fly: 浮点数组,包含击退附加力的XYZ分量
//point: Vector3类型,表示击中的位置坐标

public void OnBash(int fb, FSM atk, float[] add_fly, Vector3 point)
    {
        atk_target = atk;
        bash_add_fly = new Vector3(add_fly[0], add_fly[1], add_fly[2]);//将技能表中的add_fly数据作为坐标点
        bash_fly_dir = (this._transform.position - atk.transform.position).normalized;//计算从攻击者位置指向当前对象位置的向量,归一化(normalized)后得到单位方向向量



        if (currentState.excel_config.on_bash != null)//切换到击飞状态
        {
            ToNext(currentState.excel_config.on_bash[fb]);
        }
    }

附加击飞效果的数据表如下:

OnBashUpdate

//这俩变量是全局变量
bash_add_fly = new Vector3(add_fly[0], add_fly[1], add_fly[2]);//将技能表中的add_fly数据作为坐标点(即每个轴上的期望的位移距离)
bash_fly_dir = (this._transform.position - atk.transform.position).normalized;//计算从攻击者位置指向当前对象位置的向量,归一化(normalized)后得到单位方向向量


public void OnBashUpdate()
    {
        Debug.Log("OnBash");
        //时长 0.2f (0.1)上升  (0.1)下降的过程
        var f = GameTime.time - currentState.begin;//状态持续时间
        if (f <= 0.1f)//在状态持续前0.1秒前
        {
            var d = bash_fly_dir * (bash_add_fly.z / 0.2f);// // 水平方向:按击退方向移动,除以0.2f即是在该Z轴的速度
            d.y = bash_add_fly.y / 0.2f;// 垂直方向:固定上升速度
            Move(d, false, _add_gravity: false, _do_ground_check: false);// 应用移动(禁用重力和地面检测)
        }
        else if (f <= 0.2f)
        {
            var d = bash_fly_dir * (bash_add_fly.z / 0.2f);//保持与上升阶段相同的水平速度
            d.y = -(bash_add_fly.y / 0.2f * 2);//以两倍速度下降(实现快速坠落效果)
            Move(d, false, _add_gravity: false, _do_ground_check: false);//_add_gravity:false禁用重力,完全由代码控制运动
        }
    }

         y轴为0时只是简单的击退击倒动作,y轴为正值则为击飞动作

1、向量的帧位移计算

// 结果向量d的组成:
d.x = bash_fly_dir.x * (bash_add_fly.z / 0.2f)  
d.y = bash_add_fly.y / 0.2f                     
d.z = bash_fly_dir.z * (bash_add_fly.z / 0.2f)  

计算过程如下:

bash_add_fly = new Vector3(0, 5, 10); // X未使用,Y=5(上升高度),Z=10(水平距离)
bash_fly_dir = (0.707, 0, 0.707); // 45度方向(归一化)

假设:
水平速度:10 / 0.2 = 50 单位/秒
垂直速度:5 / 0.2 = 25 单位/秒

帧位移(假设60FPS,deltaTime=0.0167秒):

X位移:0.707 * 50 * 0.0167 ≈ 0.59
Y位移:25 * 0.0167 ≈ 0.42
Z位移:0.707 * 50 * 0.0167 ≈ 0.59

OnBashEnd方法 

public void OnBashEnd()
    {
        ground_check = true;
    }

TriggerPatrol方法 

private void TriggerPatrol()
    {
        if (atk_target == null || GetDst() > 10)
        {
            if (GameTime.time - currentState.begin >= currentState.excel_config.trigger_patrol)//若是状态时间超过巡逻触发事件
            {
                //进入巡逻
                //ToNext(1043);

                //自己周边3-6米的位置 
                var r = IntEx.Range(3, 6);
                //从0-360度旋转
                var a = IntEx.Range(0, 359);
                //用于计算以物体为中心、基于给定半径和角度的偏移位置点(以Vector类型)
                var target = _transform.GetOffsetPoint(r, a);
                //导航到目标点位置,触发巡逻事件
                navigationService.Move(target, ToPatrol);
            }
        }
    }

血条更新方法:UpdateHP_OnHit 

    internal void UpdateHP_OnHit(int damage)
    {
        this.att_crn.hp -= damage;
        if (this.att_crn.hp < 0)
        {
            this.att_crn.hp = 0;
        }

        if (AI)
        {
            //更新敌人血条 
            if (unitEntity.type == 3)
            {
                //更新Boss的血条

            }
            else 
            {
                //更新小兵的血条
                UpdateEnemyHUD();
            }
        }
        else
        {
            //更新主角的血条
        }
    }

 UpdateEnemyHUD方法

   private void UpdateEnemyHUD()
    {
        if (AI)
        {
            if (unitEntity.type == 1 || unitEntity.type == 2 || unitEntity.type == 0)
            {
                if (enemyHUD == null)
                {
                    enemyHUD = ResourcesManager.Instance.CreateEnemyHUD();//使血条实例赋值
                }
                //血条更新
                enemyHUD.UpdateHP(att_crn.hp / att_base.hp, this._transform, unitEntity.info);
            }

        }
    }

Resource新加入的方法

   Stack<EnemyHUD> hud = new Stack<EnemyHUD>(50);
    internal EnemyHUD CreateEnemyHUD()
    {
        if (hud.Count > 0)//若hud存储栈中有数据存在则取用
        {
            return hud.Pop();
        }
        else
        {
            //在Resource文件夹中遵循该路径找到此物体并获取该组件
            var go = Instantiate<GameObject>("UI/HUD/Enemy_HUD");
            return go.GetComponent<EnemyHUD>();
        }
    }

    public void DestroyEnemyHUD(EnemyHUD enemyHUD)
    {
        enemyHUD.gameObject.SetActive(false);
        hud.Push(enemyHUD);
    }
}

 EnemyHUD类:血条

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class EnemyHUD : MonoBehaviour
{
    public Image hp_top;//最顶层
    public Image hp_middle;//中间部分的血条
    public float middle_speed = 1;//插值速度
    public float hp = -1;//血量
    public Transform target;
    public Text name_text;//昵称
    public Vector3 offset = new Vector3(0, 1.8f, 0);

    void Awake()
    {
        hp_top = transform.Find("HealthBar/HP_Top").GetComponent<Image>();
        hp_middle = transform.Find("HealthBar/HP_Middle").GetComponent<Image>();
        name_text = transform.Find("Name").GetComponent<Text>();
    }


    void Update()
    {
        DOUpdateHP();
        if (hp <= 0)
        {
            if (hp_top.fillAmount == 0 && hp_middle.fillAmount == 0)
            {
                this.gameObject.SetActive(false);
                ResourcesManager.Instance.DestroyEnemyHUD(this);
            }
        }

        if (target != null)
        {
            this.transform.position = target.position + offset;
            this.transform.rotation = GameDefine._Camera.transform.rotation;
        }
    }
    bool _do_update;
    public void UpdateHP(float v, Transform target, string name)
    {
        _do_update = true;
        hp = v;
        this.target = target;
        name_text.text = name;
        if (v > 0) { this.gameObject.SetActive(true); }
    }

    private void DOUpdateHP()
    {
        if (hp == -1 || _do_update == false)
        {
            return;
        }

        if (hp_top.fillAmount > hp)
        {
            //减少
            SetFillAmount(hp_top, hp, middle_speed * 5);
        }
        else if (hp_top.fillAmount < hp)
        {
            //增加它
            SetFillAmount(hp_top, hp, middle_speed);
        }

        if (hp_middle.fillAmount > hp)
        {
            SetFillAmount(hp_middle, hp, middle_speed);
        }
        else if (hp_middle.fillAmount < hp)
        {
            SetFillAmount(hp_middle, hp, middle_speed * 5);
        }

        if (_do_update)
        {
            if (hp_top.fillAmount == hp && hp_middle.fillAmount == hp)
            {
                _do_update = false;
            }
        }
    }

    public bool SetFillAmount(Image image, float v, float speed)
    {
        if (image.fillAmount > v)
        {
            var temp = image.fillAmount - GameTime.deltaTime * speed;
            if (temp < v)
            {
                temp = v;
            }
            image.fillAmount = v;

        }
        else if (image.fillAmount < v)
        {
            var temp = image.fillAmount + GameTime.deltaTime * speed;
            if (temp > v)
            {
                temp = v;
            }
            image.fillAmount = v;
        }
        return image.fillAmount == v;

    }
}

        这个方法一般挂载在血条物体上如下:

Awake方法 

    void Awake()
    {
        hp_top = transform.Find("HealthBar/HP_Top").GetComponent<Image>();
        hp_middle = transform.Find("HealthBar/HP_Middle").GetComponent<Image>();
        name_text = transform.Find("Name").GetComponent<Text>();
    }

 获取该物体中的子物体上的组件

Update方法 

    public Image hp_top;//最顶层
    public Image hp_middle;//中间部分的血条
    public float middle_speed = 1;//插值速度
    public float hp = -1;//血量
    public Transform target;
    public Text name_text;//昵称
    public Vector3 offset = new Vector3(0, 1.8f, 0);

void Update()
    {
        DOUpdateHP();//更新血条显示(如填充值变化)
        if (hp <= 0)//检查生命值是否≤0
        {
            // 当顶部血条和中部血条均已归零(动画播完)
            if (hp_top.fillAmount == 0 && hp_middle.fillAmount == 0)
            {
                this.gameObject.SetActive(false);// 隐藏对象
                ResourcesManager.Instance.DestroyEnemyHUD(this);// 通知资源管理器销毁HUD
            }
        }

        if (target != null)// 确保目标存在
        {
            // 位置:固定在目标对象的指定偏移位置
            this.transform.position = target.position + offset;
            // 旋转:与主摄像机保持相同朝向(实现Billboard效果)
            this.transform.rotation = GameDefine._Camera.transform.rotation;
        }
    }

UpdateHP方法

public void UpdateHP(float v, Transform target, string name)
{
    // 1. 设置更新标志
    _do_update = true; // 触发血条视觉更新(可能在DOUpdateHP()中使用)
    
    // 2. 更新核心数据
    hp = v;               // 存储当前生命值(用于Update()中的死亡判断)
    this.target = target; // 更新血条跟随的目标对象
    name_text.text = name;// 更新名称显示(如显示敌人名称)
    
    // 3. 控制血条显示
    if (v > 0) { 
        this.gameObject.SetActive(true); // 生命值>0时激活血条
    }
}
  1. 血条激活条件

    • 仅当v > 0(生命值未耗尽)时激活血条对象

    • 若敌人复活(生命值从0恢复),血条会重新显示

    • 不处理死亡隐藏:死亡时的隐藏逻辑在Update()中处理(需等待血条动画归零)

  2. 数据同步机制

    • hp变量在Update()中用于死亡检测(if (hp <= 0)

    • target更新确保血条位置正确跟随新目标

    • name_text实时更新敌人名称(如精英怪特殊标识)

  3. 更新触发标志

    • _do_update = true 可能用于:

      • 控制DOUpdateHP()中的血条动画(如缓动填充效果)

      • 避免无变化时的冗余计算(性能优化)

DOUpdateHP方法 

    private void DOUpdateHP()
    {
        if (hp == -1 || _do_update == false)//hp == -1未初始化状态||_do_update:更新标志(由UpdateHP()方法设置)

        {
            return;//确保只在需要更新时执行(性能优化)
        }

        if (hp_top.fillAmount > hp)//血量减少(受伤)
        {
            //受伤特效:血条快速下降(5倍速度),增强打击感
            SetFillAmount(hp_top, hp, middle_speed * 5);
        }
        else if (hp_top.fillAmount < hp)//血量增加(治疗)
        {
            //治疗特效:血条匀速上升,表现平缓恢复
            SetFillAmount(hp_top, hp, middle_speed);
        }

        if (hp_middle.fillAmount > hp)// 延迟血条高于实际血量(需下降)
        {
            SetFillAmount(hp_middle, hp, middle_speed);// 慢速下降
        }
        else if (hp_middle.fillAmount < hp)// 延迟血条低于实际血量(需上升)
        {
            SetFillAmount(hp_middle, hp, middle_speed * 5); // 快速追平
        }

        if (_do_update)
        {
            if (hp_top.fillAmount == hp && hp_middle.fillAmount == hp)当两个血条都达到目标值hp
            {
                _do_update = false;// 停止更新
            }
        }
    }

1. 更新条件检查

csharp

if (hp == -1 || _do_update == false) return;
  • 作用:确保只在需要更新时执行(性能优化)


2. 实时血条(hp_top)控制

csharp

// 血量减少(受伤)
if (hp_top.fillAmount > hp) {
    SetFillAmount(hp_top, hp, middle_speed * 5); // 快速下降
} 
// 血量增加(治疗)
else if (hp_top.fillAmount < hp) {
    SetFillAmount(hp_top, hp, middle_speed); // 正常速度上升
}
  • 受伤特效:血条快速下降(5倍速度),增强打击感

  • 治疗特效:血条匀速上升,表现平缓恢复

  • 设计意图:突出"受伤"的视觉反馈强于"治疗"


3. 延迟血条(hp_middle)控制

csharp

// 延迟血条高于实际血量(需下降)
if (hp_middle.fillAmount > hp) {
    SetFillAmount(hp_middle, hp, middle_speed); // 慢速下降
} 
// 延迟血条低于实际血量(需上升)
else if (hp_middle.fillAmount < hp) {
    SetFillAmount(hp_middle, hp, middle_speed * 5); // 快速追平
}
  • 经典延迟效果:受伤后白色血条缓慢追平(middle_speed

  • 快速复位:治疗时立即追上实时血条(5倍速度)

  • 视觉意义:延迟血条代表"潜在伤害",增强战斗紧张感


4. 更新状态检测

csharp

if (_do_update) {
    if (hp_top.fillAmount == hp && hp_middle.fillAmount == hp) {
        _do_update = false; // 停止更新
    }
}
  • 完成条件:当两个血条都达到目标值hp

  • 自动停止:重置_do_update避免持续计算

  • 性能优化:确保动画完成后不再消耗资源

 使用场景:玩家从满血(1.0)受到伤害到半血(0.5)

  1. 初始化状态

    hp_top.fillAmount = 1.0
    hp_middle.fillAmount = 1.0
    hp = 0.5(新血量)
    _do_update = true(由UpdateHP()设置)

  2. 第一帧处理

    csharp
    
    // hp_top(1.0) > hp(0.5) → 需要减少
    SetFillAmount(hp_top, 0.5, middle_speed * 5); // 5倍速下降
    
    // hp_middle(1.0) > hp(0.5) → 需要减少
    SetFillAmount(hp_middle, 0.5, middle_speed); // 正常速度下降

    视觉效果

    • 红色血条(hp_top)快速下降到0.5(middle_speed * 5)

    • 白色血条(hp_middle)开始缓慢下降(middle_speed)


3、后续帧处理

假设middle_speed = 1,每帧下降0.1
第2帧:hp_top=0.5,hp_middle=0.9
第3帧:hp_top=0.5,hp_middle=0.8
...
第6帧:hp_middle=0.5
  1. 完成检测

    csharp
    
    if (hp_top.fillAmount == hp && hp_middle.fillAmount == hp) {
        _do_update = false; // 第6帧满足条件,停止更新
    }

    最终效果

    • 红色血条立即显示当前血量(0.5)

    • 白色血条缓慢跟进,形成"伤害残留"效果

 SetFillAmount方法

public bool SetFillAmount(Image image, float v, float speed)
{
    if (image.fillAmount > v)
    {
        // 当前值 > 目标值 → 递减
        float temp = image.fillAmount - GameTime.deltaTime * speed;
        if (temp < v)
        {
            temp = v;
        }
        image.fillAmount = temp; // ✅ 修正:使用 temp
        return false; // 还没到目标值
    }
    else if (image.fillAmount < v)
    {
        // 当前值 < 目标值 → 递增
        float temp = image.fillAmount + GameTime.deltaTime * speed;
        if (temp > v)
        {
            temp = v;
        }
        image.fillAmount = temp; // ✅ 修正:使用 temp
        return false;
    }

    // 已经等于目标值
    return true;
}

这段代码 SetFillAmount() 是一个血条填充动画的实现方法:        

设计意图(理想功能)

  1. 平滑过渡:使血条图像(Image)从当前填充值逐渐变化到目标值(v

  2. 速度控制:通过speed参数控制变化速度

  3. 状态返回:返回布尔值表示是否已完成动画
     

修正后效果举例(1.0→0.5,速度=2,帧时间=0.1s):

        如果游戏运行在60帧每秒(FPS)的情况下,每帧的时间大约是0.0167秒(1/60≈0.0167)。在此使用0.1秒是为了计算方便

帧数 计算值 实际显示 完成状态
1 1.0 - 0.2 = 0.8 0.8 false
2 0.8 - 0.2 = 0.6 0.6 false
3 0.6 - 0.2 = 0.4 < 0.5 → 0.5 0.5 true

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐