Excel表格的再次配置

        新加入一些元素给配置表需要对Excel进行更新 

1、在Editor文件夹中的ExcelTools:excelInput交代了所配置的Excel文件的位置

public class ExcelTools 
{
    public static string excelInput;
    public static string outputCS;
    public static string outJson;
    public static void Init() {
        //添加2个路径
        excelInput = Application.dataPath + @"\..\..\Tools\Excel\"; 
        outputCS = Application.dataPath + @"\Script\Hotfix\ExcelConfig\"; 
        outJson= Application.dataPath + @"\Resources\Res\UnitConfig\"; 
    }

2、调用Excel2CS.Start方法,传入了excelInput此路径中的文件作为输入文件,传入了outputCS将配置表输出到这个路径文件夹

var r= Excel2CS.Start(excelInput, outputCS,false);
        if (r)
        {
            CompilationPipeline.RequestScriptCompilation();
            CompilationPipeline.compilationFinished += (obj) => {
                UnityEngine.Debug.Log("批处理执行完毕!");
                UnityEngine.Debug.Log($"Excel输入路径: {excelInput}");
                UnityEngine.Debug.Log($"CS输出路径: {outputCS}");
                AssetDatabase.Refresh();
            };
        }



注意:

日志显示:Excel输入路径: E:/unity practice/HOT/Assets\..\..\Tools\Excel\

  1. Assets\..\.. 会向上跳两级目录:

    • 第一级:从 Assets 到项目根目录 HOT

    • 第二级:从 HOT 到父目录 unity practice

  2. 最终路径变成了 E:/unity practice/Tools\Excel\

    所以改动这里的Excel表再重新配置就好了

复制项目文件会出现路径混乱 

1、路径混乱的原因
 

绝对路径问题

  • .sln 和 .csproj 文件包含绝对路径引用

  • 当您复制项目到新位置时,这些路径仍然指向原始位置

  • 用户特定设置

    • .csproj.user 文件包含用户特定的绝对路径

    • 这些文件通常不被纳入版本控制,但会被复制

  • 缓存问题

    • Library 文件夹包含路径相关的缓存

    • Unity 的 Library 文件夹在复制后可能仍指向旧路径


干净的复制方法:
 

如果已经复制了全部文件(包含 .sln):

  1. 删除自动生成的文件

    • 在新项目中删除:

      • 所有 .sln 文件

      • 所有 .csproj 文件

      • 所有 .csproj.user 文件

      • Library/ 文件夹(重要!)

  2. 似乎我就随便在Asset里面任意改动了文件夹位置就断除了元文件联系

玩家释放操作  AI对应操作

if (AI == false)
        {
            ……

            }
        }
        else
        {
            foreach (var item in stateData)
            {
                ……
            }
            //AI要注册的事件(OnPlayerAtk是当玩家发动攻击时,AI需要执行的动作)
            GameEvent.OnPlayerAtk += OnPlayerAtk;

1、全局事件监听:

GameEvent.OnPlayerAtk += OnPlayerAtk;

GameEvent新加入方法如下
public class GameEvent
{
    public static Action<FSM, SkillEntity> OnPlayerAtk;
}

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


2、触发GameEvent.OnPlayerAtk

        这里需要额外讲述下为什么玩家角色释放普通攻击和技能时,可以在OnSkillBegin()方法中触发事件:

if (item.Value.skill != null)
 {
   AddListener(item.Key, StateEventType.begin, OnSkillBegin);
}
以普通攻击为例子,根据配置表设置监听表如下:
1005,begin,onSkillBegin

意味着切换状态后id=1005,且在ToNext方法进入begin状态,由DOStateEvent方法就能触发OnSkillBegin方法

  当玩家角色释放普通攻击和技能时,在OnSkillBegin()方法中触发事件:

private void OnSkillBegin()
    {
        GameEvent.OnPlayerAtk?.Invoke(this, currentState.skill);
        //传递参数(发起攻击的玩家FSM实例,currentState.skill:当前使用的技能数据)
    }
  1. 检查空值:是否存在GameEvent.OnPlayerAtk?有则激发当前对象当前状态的技能数据

  2. 传递关键参数

    • this:当前对象(很可能是技能施放者或状态机)

    • currentState.skill:当前状态关联的技能数据

  3. SkillEntity的属性如下,且skill

    public SkillEntity(int id,int tag,float cd,int hit_max,float phy_damage,float magic_damage,float[] add_fly,int ignor_collision,float atk_distance){
               
               this.id = id;
               this.tag = tag;
               this.cd = cd;
               this.hit_max = hit_max;
               this.phy_damage = phy_damage;
               this.magic_damage = magic_damage;
               this.add_fly = add_fly;
               this.ignor_collision = ignor_collision;
               this.atk_distance = atk_distance;
    
            }

3、OnPlayerAtk 为AI角色提供以下核心功能

        1. 实时响应玩家攻击

        2. 概率化防御决策:       

private void OnPlayerAtk(FSM atk, SkillEntity arg2)
    {
        Debug.Log("OnPlayerAtk");
        if (att_crn.hp <= 0)
        {
            return;
        }
        //5米距离内,玩家位于AI前方(非背后攻击)
        if (GetDst(atk._transform) <= 5
            && _transform.ForwardOrBack(atk._transform.position) > 0)
        {   // 格挡概率判定
            if (unitEntity.block_probability.InRange())
            {
                if (CheckConfig(currentState.excel_config.on_defense))
                {
                    // 转向玩家并切换到格挡状态
                    _transform.LookTarget(atk._transform);
                    var result = ToNext((int)currentState.excel_config.on_defense[2]);
                    if (result)
                    {
                        return;
                    }
                }
            }
            // 闪避概率判定
            else if (unitEntity.dodge_probability.InRange())
            {
                // 执行闪避动作
                if (currentState.excel_config.trigger_dodge > 0)
                {
                    _transform.LookTarget(atk._transform);
                    var next = IntEx.Range(1032, 1035);
                    var result = ToNext(next);
                    if (result)
                    {
                        return;
                    }
                }
            }
            // 反击概率判定
            else if (unitEntity.atk_probability.InRange())
            { //抢手攻击的检测
                if (currentState.excel_config.first_strike > 0)
                {
                    TriggerAtk_AI();
                }
            }
        }

AI格挡操作 

1、进入格挡状态并激活对应状态的服务体(如受击服务体和动画服务体)


格挡概率:



进行格挡状态的数组:


2、如果格挡时收到攻击,则调用HitService服务体以及FSM中OnBlock方法

角色格挡机制文章浏览阅读944次,点赞22次,收藏21次。需要在这里注意一下:动画播放结束后进入下一阶段的状态切换是在 AnimationOnPlayEnd()通过这种设计,状态机实现了输入响应、状态切换、动画控制、物理移动的解耦,符合有限状态机的核心原则:​。状态1001注册了OnMove到它的update事件。状态1002注册了OnMove到它的update事件。,发生在游戏初始化阶段(Awake中)。状态切换发生在当配置的。其他有on_move配置的状态同理。(在stateData字典中存在))在每帧更新时检测动画进度。:重启当前状态(不切换) https://blog.csdn.net/2303_80204192/article/details/149327602?spm=1001.2014.3001.5502#t39
3、如果一直没有收到攻击

AddListener(1013, StateEventType.update, AI_Defending);

private void AI_Defending()
{
    LookAtkTarget(); // 持续面向玩家
    
    // 超时检测(2.5秒)
    if (GameTime.time - currentState.begin >= 2.5f)
    {
        ToNext(10132); // 切换到格挡收回状态
    }
}

4、格挡相关注意事项

平时隐藏使用激活的原因:       

  • 避免误触发伤害

    • 非攻击时段(如待机/移动)碰撞体激活会导致空气挥刀也能伤敌

  • 性能优化

    • 持续激活的碰撞体会参与每帧物理计算(即使未使用)



需要注意的是格挡的射线检测:

武器Tag进行比对:

public bool Linecast(Vector3 begin, Vector3 end, HitConfig config, PlayerState state)
    {
        var result = Physics.Linecast(begin, end, out var hitInfo, player.GetEnemyLayerMask(), QueryTriggerInteraction.Collide);
        if (result)
        {
            //处于格挡状态
            if (hitInfo.transform.CompareTag(GameDefine.WeaponTag))
            {
                OnBlock(hitInfo);
 
            }
    }

AI闪避操作 

1、进入闪避状态并激活对应状态的服务体(如受击服务体和动画服务体)


躲闪概率:

 

躲闪状态的数据如下:


躲闪状态物理配置如下:


2、随机做出闪避动作,并进入该状态执行当前状态服务体

else if (unitEntity.dodge_probability.InRange())
            {
                if (currentState.excel_config.trigger_dodge > 0)
                {
                    _transform.LookTarget(atk._transform);//看向攻击方
                    var next = IntEx.Range(1032, 1035);//随机做出一个闪避动作
                    var result = ToNext(next);
                    if (result)
                    {
                        return;
                    }
                }
            }

AI抢手攻击 

强攻状态的数据如下:


抢攻概率如下:


2、进入抢攻状态

private void AutoTriggerAtk_AI()
    {
    //进入状态时间超过预制表中的设置的攻击状态时间
        if (GameTime.time - currentState.begin >= currentState.excel_config.active_attack)
        {
            AIAtk();
        }
    }

3、进入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);
            }
        }
    }

 寻路系统设置

AstarPath插件

 下面这篇文章主要看如何使用插件的内容【Unity3D插件】A*Pathfinding插件分享《A*寻路插件》_a* pathfinding-CSDN博客文章浏览阅读1.5w次,点赞12次,收藏75次。Unity3d 寻路插件A*Pathfinding学习与研究(一)参考资料1.A* Pathfinding Project 2.从A*寻路项目开始 3.[Unity3D插件系列]-A* Pathfinding Project 学习(一) 下载链接https://arongranberg.com/astar/download https://pan.baidu.com/s..._a* pathfinding https://blog.csdn.net/q764424567/article/details/80528457?locationNum=9&fps=1


下面这段代码主要第二部分内容

UnityAI角色寻路——A星寻路 - 代码先锋网UnityAI角色寻路——A星寻路,代码先锋网,一个为软件开发程序员提供代码片段和技术文章聚合的网站。https://codeleading.com/article/7295911553/

该插件的当前使用案例 

1、新建组件

 

2、设置层级和寻路网格

        这个层级通过下面这个按钮进行编辑



3、给敌人NPC添加Seeker组件

寻路机制的运行 

1、在FSM中,进入AIAtk方法会进入寻路状态

private void AIAtk()
    {      
        next_atk = IntEx.Range(1005, 1012);
        if (stateData[next_atk].skill.atk_distance > 0)
        {
            if (atk_target == null)
            {
                atk_target = UnitManager.Instance.player;

            }

            var dst =Vector3.Distance(_transform.position,atk_target._transform.position);
            
            if (dst >= stateData[next_atk].skill.atk_distance)//AI当前攻击状态的攻击范围与大于与玩家的距离
            {
                this._transform.LookTarget(atk_target._transform);//这个AI看向玩家(场上可能有多个ai,this代表了当前FSM的持有者)
                if (IntEx.Range(0, 100) <= 50)//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);
            }
        }
    }

 2、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              // 寻路完成回调(代码中未展示)
        );
    }

}

//Action success的事件如下:
private void MoveToPoint()
    {
        //寻路的状态
        ToNext(1042);
    }

当调用Move()时:

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

  2. StartPath生成由多个可行走点连成的完整路线(而不是走到最近点→再生成新路径"的循环)

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

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



OnPathComplete方法

关于该方法的传参:

        这个 Path path 参数是由 A Pathfinding Project 框架自动创建并传递的,当路径计算完成后,A* 系统会自动实例化一个 Path 对象,并将这个对象传递给回调函数

        Path` 对象(特别是其 `vectorPath` 属性)记录的是一系列可行走点(walkable points)组成的路径。这些点都是位于导航网格上的可行走位置。

    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]);

                // 触发成功回调(这里就是MovetoPoint行为事件)
                this._success?.Invoke();
                }
                else
               {
                Stop();
               }
        }
    }
if (state == 1)
  • 目的:确保只有"正在寻路"状态处理结果

  • 防止:过期的路径计算结果干扰当前状态


设计判断起点即终点
       
这里为什么要添加这么个判断条件呢?因为生成路径的情况可能不一样。
// 设置当前路点索引
if (_path.Count <= 1)
  {
    currentWaypoint = 0;   // 只有一个点(起点即终点)
  } 
    else 
  {
    currentWaypoint = 1;   // 跳过起点(索引0),从第二个点开始移动
  }


复杂路径场景:

复杂路径(路径点>2)
text
角色位置: A(0,0,0)
目标位置: D(5,0,5)
生成路径: [A, B(2,0,2), C(4,0,4), D]


处理流程:
currentWaypoint = 1(点B)
角色转向B点
移动到B点后,currentWaypoint++(变为2,点C)
转向C点并移动
到达C点后,currentWaypoint++(变为3,点D)
转向D点并移动
到达D点后停止


原地“移动”:

角色位置: A(0,0,0)
目标位置: A(0,0,0) // 相同位置
生成路径: [A]


 

设计原因

原因 说明 无此处理的后果
效率优化 跳过当前位置点避免冗余计算 每帧计算到起点的距离(总是≈0)
行为合理 角色已在起点,不需要"移动"到自身位置 角色可能短暂"抖动"或旋转异常
逻辑简化 直接面向第一个真实移动目标 需要额外处理起点特殊情况
性能提升 减少一次距离检测 额外无意义的计算开销

路径点之间的移动

        逐帧调用Navigation的OnUpdate方法,调用OnMove方法

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

        if (currentWaypoint >= _path.Count)
        {
            Stop();
            return;
        }
        else
        {
            //由于path和currentWaypoint都是全局变量
            var next_point = _path[currentWaypoint];

            //// 计算移动方向(核心步骤)
            Vector3 dir = next_point - player._transform.position;
            dir.y = 0;

            //使该角色实例产生位移
            this.player.Move(dir * player.GetMoveSpeed(), false);
            //如果当前位置和下个点距离小于等于0.5            
            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]);
                }
            }

        }
    }

当寻路接触到玩家

AddListener(1042, StateEventType.update, 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;
                    }
                }
            }
        }
    }

OnMoveToPoint方法文章浏览阅读474次,点赞7次,收藏8次。这里的Move方法是FSM中的Move方法,这里false指的是让AI敌人基于世界坐标朝向进行移动,为什么输入距离可以看链接中——帧的无关化案例进行理解。类,其核心目的是创建一个全局可访问的游戏单位管理器,特别是为了存储和管理玩家角色的引用。检查时间差是否超过0.1秒(100毫秒),如果条件成立,说明距离上次执行已超过0.1秒。的扩展方法,用于计算以物体为中心、基于给定半径和角度的偏移位置点,(即坐标系中)当路径点 > 1 时,直接跳过起点(索引0),因为角色通常已在起点位置。 https://blog.csdn.net/2303_80204192/article/details/149715559?sharetype=blogdetail&sharerId=149715559&sharerefer=PC&sharesource=2303_80204192&spm=1011.2480.3001.8118#t16


寻路到终点的条件判断

navigationService.IsEnd()

下面两个方法用于控制导航结束Stop方法文章浏览阅读476次,点赞7次,收藏8次。这里的Move方法是FSM中的Move方法,这里false指的是让AI敌人基于世界坐标朝向进行移动,为什么输入距离可以看链接中——帧的无关化案例进行理解。类,其核心目的是创建一个全局可访问的游戏单位管理器,特别是为了存储和管理玩家角色的引用。检查时间差是否超过0.1秒(100毫秒),如果条件成立,说明距离上次执行已超过0.1秒。的扩展方法,用于计算以物体为中心、基于给定半径和角度的偏移位置点,(即坐标系中)当路径点 > 1 时,直接跳过起点(索引0),因为角色通常已在起点位置。 https://blog.csdn.net/2303_80204192/article/details/149715559?sharetype=blogdetail&sharerId=149715559&sharerefer=PC&sharesource=2303_80204192&spm=1011.2480.3001.8118#t5

IsEnd方法文章浏览阅读477次,点赞7次,收藏8次。这里的Move方法是FSM中的Move方法,这里false指的是让AI敌人基于世界坐标朝向进行移动,为什么输入距离可以看链接中——帧的无关化案例进行理解。类,其核心目的是创建一个全局可访问的游戏单位管理器,特别是为了存储和管理玩家角色的引用。检查时间差是否超过0.1秒(100毫秒),如果条件成立,说明距离上次执行已超过0.1秒。的扩展方法,用于计算以物体为中心、基于给定半径和角度的偏移位置点,(即坐标系中)当路径点 > 1 时,直接跳过起点(索引0),因为角色通常已在起点位置。 https://blog.csdn.net/2303_80204192/article/details/149715559?sharetype=blogdetail&sharerId=149715559&sharerefer=PC&sharesource=2303_80204192&spm=1011.2480.3001.8118#t6

主动发起攻击

 1、注册AI发起攻击事件

 if (AI == false)
        {
           …………
        }
        else
        {
            //AI要注册的事件
            foreach (var item in stateData)
            {
                //进入状态超过多长时间
                if (item.Value.excel_config.active_attack > 0)
                {
                    AddListener(item.Key, StateEventType.update, AutoTriggerAtk_AI);
                }
            }
        }

        遍历状态配置表,查阅哪些状态能进入随机发起攻击的情况:


2、进入自动攻击方法(表格中active_attack既作为查空,又做为持续时间的判断)

private void AutoTriggerAtk_AI()
    {
        if (GameTime.time - currentState.begin >= currentState.excel_config.active_attack)
        {
            AIAtk();
        }
    }

AI的踱步设置 

if (AI == false)
        {
            …………
        }
        else
        {
            //AI要注册的事件
            foreach (var item in stateData)
            {
                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);
                }

            }

 标签和踱步状态如下:


2、TriggerPacing

 private void TriggerPacing()
    {
        if (IsDead())
        {
            return;
        }

        if (unitEntity.pacing_probability > 0 )//该状态能否进入踱步状态
        {

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

                if (currentState.excel_config.tag == 4)//如果当前状态标签为4(此状态为过渡进入踱步的状态)
                {
                    if (GameTime.time - currentState.begin >= IntEx.Range(3, 6))//当前状态持续3——6秒后进入随机的踱步状态
                    {
                        var next = IntEx.Range(1036, 1041);
                        _transform.LookTarget(atk_target._transform);
                        ToNext(next);
                    }
                }
                else//当前状态不含标签4,则此状态为直接进入随机踱步状态
                {
                    var next = IntEx.Range(1036, 1041);
                    _transform.LookTarget(atk_target._transform);
                    ToNext(next);
                }

            }
        }
    }

OnPacingUpdate方法(持续检查漫步状态)

       
查看状态持续时间且到达距离条件进行自动攻击
private void OnPacingUpdate()
    {
        if (GameTime.time - currentState.begin >= 5)//如果当前状态持续时间超过5秒
        {
            ToNext(1001);
        }

        if (atk_target != null)//攻击目标为空,朝向目标,距离小于3时进入攻击状态
        {
            LookAtkTarget();

            if (GetDst() <= 3)
            {
                AIAtk();
                return;
            }
        }
    }

物理状态配置

击飞状态 

当敌人/玩家被攻击时,进入HitService判定:

 private void OnHit(Vector3 begin, HitConfig config, PlayerState state, RaycastHit hitInfo)
    {
        //表示击中单位
        var fsm = hitInfo.transform.GetComponent<FSM>();
        if (fsm != null)
        {
            if (hit_target.Contains(fsm.instance_id) == false)
            {
                hit_target.Add(fsm.instance_id);
                ………………
                //4.通知对方进入受击 死亡的动作
                var fb = fsm._transform.ForwardOrBack(begin) > 0 ? 0 : 1;

                if (fsm.att_crn.hp > 0)
                {
                    if (state.skill.add_fly != null)
                    {
                        //击飞的流程
                        fsm.OnBash(fb, this.player, state.skill.add_fly, hitInfo.point);
                    }
                    else
                    {
                        fsm.OnHit(fb, this.player);
                    }
                }
                …………
        }
    }


FSM中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]);
        }
    }

OnBash方法文章浏览阅读477次,点赞7次,收藏8次。这里的Move方法是FSM中的Move方法,这里false指的是让AI敌人基于世界坐标朝向进行移动,为什么输入距离可以看链接中——帧的无关化案例进行理解。类,其核心目的是创建一个全局可访问的游戏单位管理器,特别是为了存储和管理玩家角色的引用。检查时间差是否超过0.1秒(100毫秒),如果条件成立,说明距离上次执行已超过0.1秒。的扩展方法,用于计算以物体为中心、基于给定半径和角度的偏移位置点,(即坐标系中)当路径点 > 1 时,直接跳过起点(索引0),因为角色通常已在起点位置。 https://blog.csdn.net/2303_80204192/article/details/149715559?sharetype=blogdetail&sharerId=149715559&sharerefer=PC&sharesource=2303_80204192&spm=1011.2480.3001.8118#t21OnBashUpdate文章浏览阅读480次,点赞7次,收藏8次。这里的Move方法是FSM中的Move方法,这里false指的是让AI敌人基于世界坐标朝向进行移动,为什么输入距离可以看链接中——帧的无关化案例进行理解。类,其核心目的是创建一个全局可访问的游戏单位管理器,特别是为了存储和管理玩家角色的引用。检查时间差是否超过0.1秒(100毫秒),如果条件成立,说明距离上次执行已超过0.1秒。的扩展方法,用于计算以物体为中心、基于给定半径和角度的偏移位置点,(即坐标系中)当路径点 > 1 时,直接跳过起点(索引0),因为角色通常已在起点位置。 https://blog.csdn.net/2303_80204192/article/details/149715559?spm=1001.2014.3001.5502#t22        OnBash方法添加击飞相关数据且切换击飞状态,OnBashUpdate用于控制击飞状态的位移,OnBashEnd方法是检测添加接地动作。

public void OnBashEnd()
    {
        ground_check = true;
    }

每帧执行接地检测:

void Update()
    {
        if (currentState != null)
        {          
            if (ServiceOnUpdate() == true)
            {
                DOStateEvent(currentState.id, StateEventType.update);//状态每帧执行的事件
            }

            ToGround();
        }
        
    }


public void ToGround()
    {
        if (ground_check)
        {
            //射线投射,如果射线投射的结果为Ture,则说明处于接地状态,返回false 
            if (Physics.Linecast(_transform.position, _transform.position + GameDefine._Ground_Dst, GameDefine.Ground_LayerMask))
            {
                ground_check = false;
            }
            else
            {
                Move(_transform.up * -9.81f, false, false, false, false);
            }
        }
    }

巡逻机制 

 1、注册监听器

if (AI == false)
        {
           …………
        }
        else
        {
            //AI要注册的事件
            foreach (var item in stateData)
            {                
                if (item.Value.excel_config.trigger_patrol > 0)
                {
                    AddListener(item.Key, StateEventType.update, TriggerPatrol);
                }
            }
            GameEvent.OnPlayerAtk += OnPlayerAtk;
            AddListener(1043, StateEventType.update, OnPatrolUpdate);
            AddListener(1043, StateEventType.begin, ChangeMoveSpeed);
            AddListener(1043, StateEventType.end, OnPatrolEnd);


2、触发巡逻方法
        trigger_patrol的数据信息如下



3、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);
            }
        }
    }

public void ToPatrol()
    {
        ToNext(1043);
    }

id=1043的巡逻状态如下:


GetOffsetPoint文章浏览阅读483次,点赞7次,收藏8次。这里的Move方法是FSM中的Move方法,这里false指的是让AI敌人基于世界坐标朝向进行移动,为什么输入距离可以看链接中——帧的无关化案例进行理解。类,其核心目的是创建一个全局可访问的游戏单位管理器,特别是为了存储和管理玩家角色的引用。检查时间差是否超过0.1秒(100毫秒),如果条件成立,说明距离上次执行已超过0.1秒。的扩展方法,用于计算以物体为中心、基于给定半径和角度的偏移位置点,(即坐标系中)当路径点 > 1 时,直接跳过起点(索引0),因为角色通常已在起点位置。 https://blog.csdn.net/2303_80204192/article/details/149715559?sharetype=blogdetail&sharerId=149715559&sharerefer=PC&sharesource=2303_80204192&spm=1011.2480.3001.8118#t11


OnPatrolUpdate方法

        用于每帧判断是否切换到待机状态。

public void OnPatrolUpdate()//巡逻状态超过5秒或者导航结束
    {
        if (GameTime.time - currentState.begin >= 5f || navigationService.IsEnd())
        {
            ToNext(1001);
        }
    }

巡逻状态速度的改变
        AI刚进入巡逻状态时,触发ChangeMoveSpeed,使其巡逻速度=2.5;巡逻结束时速度恢复到5,并且结束巡逻状态的导航

AddListener(1043, StateEventType.begin, ChangeMoveSpeed);
AddListener(1043, StateEventType.end, OnPatrolEnd);

 private void ChangeMoveSpeed()
    {
        _speed = 2.5f;
    }

    private void OnPatrolEnd()
    {
        _speed = 5f;
        navigationService.Stop();
    }


4、如何从巡逻状态到攻击状态

        当玩家进入敌人范围时,敌人会优先进入踱步状态,从踱步状态进入自动攻击状态

血条的更新与计算

受击更新血量 

 1、OnHit方法

private void OnHit(Vector3 begin, HitConfig config, PlayerState state, RaycastHit hitInfo)
    {
        //表示击中单位
        var fsm = hitInfo.transform.GetComponent<FSM>();
        if (fsm != null)
        {
            …………
                //3.计算 扣掉血量
                var damage = AttHelper.Instance.Damage(this.player, state, fsm);
                fsm.UpdateHP_OnHit(damage);
            …………

            }
        }
    }


2、UpdateHP_OnHit方法

       根据角色type类型,观察是主角还是敌方小兵还是地方BOSS

        这里需要注意下,att_crn.hp用来存储角色生命值。

internal void UpdateHP_OnHit(int damage)
    {
        this.att_crn.hp -= damage;//计算伤害以便展示血条
        if (this.att_crn.hp < 0)
        {
            this.att_crn.hp = 0;
        }

        if (AI)如果是AI角色
        {
            //更新敌人血条 
            if (unitEntity.type == 3)
            {
                //更新Boss的血条

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


3、UpdateEnemyHUD()      

EnemyHUD enemyHUD;
public UnitAttEntity att_base;//总属性
public UnitAttEntity att_crn;//当前属性==>生命值


private void UpdateEnemyHUD()
    {
        if (AI)
        {    //敌人类型是1/2/0
            if (unitEntity.type == 1 || unitEntity.type == 2 || unitEntity.type == 0)
            {
                if (enemyHUD == null)//判断实例enemyHUD是否为空
                {
                    enemyHUD = ResourcesManager.Instance.CreateEnemyHUD();//创建血条实例
                }
                enemyHUD.UpdateHP(att_crn.hp / att_base.hp, this._transform, unitEntity.info);//当前生命值/总生命值=百分比生命值att_crn.hp / att_base.hp
            }

        }
    }


4、 enemyHUD中的UpdateHP方法

    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);

    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); }//激活当前血条样板物体
    }

EnemyHUD类 血量更新及UI显示

 1、EnemyHUD类的Update方法

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)//检查生命值是否≤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;
        }
    }
   

         每帧更新DOUpdateHP(),用于显示更新血条;且逐帧检查是否阵亡,若阵亡则将血条UI隐藏并摧毁。且把血条固定在目标位置

2、DOUpdateHP()方法

public float middle_speed = 1;//插值速度
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;
            }
        }
    }

        hp_top.fillAmount是血条的长度值,以1为最值,这里就是用此值与传入来的生命百分值进行比较,而hp_middle.fillAmount是延迟血条长度值,只是颜色与top有所不同,可能是透明度的问题。
 DOUpdateHP方法文章浏览阅读656次,点赞9次,收藏10次。这里的Move方法是FSM中的Move方法,这里false指的是让AI敌人基于世界坐标朝向进行移动,为什么输入距离可以看链接中——帧的无关化案例进行理解。类,其核心目的是创建一个全局可访问的游戏单位管理器,特别是为了存储和管理玩家角色的引用。检查时间差是否超过0.1秒(100毫秒),如果条件成立,说明距离上次执行已超过0.1秒。的扩展方法,用于计算以物体为中心、基于给定半径和角度的偏移位置点,(即坐标系中)当路径点 > 1 时,直接跳过起点(索引0),因为角色通常已在起点位置。 https://blog.csdn.net/2303_80204192/article/details/149715559?spm=1001.2014.3001.5501#t33

  1. 该方法最终效果

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

    • 含透明度血条(hp_middle)缓慢跟进,形成"伤害残留"效果

  2. 该方法解耦部分:通过数据比对,判断具体需求——是增加血条还是减少血条

 3、SetFillAmount

        这个方法是具体操作血条变化的底层逻辑 

 public bool SetFillAmount(Image image, float v, float speed)
    {
        if (image.fillAmount > v)// 情况1:当前填充值 > 目标值(需要减少)
        {
            // 计算理论过渡值:当前值 - 帧变化量
            var temp = image.fillAmount - GameTime.deltaTime * speed;
            // 防止过度减少(低于目标值)
            if (temp < v)
            {
                temp = v;
            }
            // ⚠️ 错误点:直接设置为目标值,忽略过渡计算
            image.fillAmount = v; // 应该用 temp 而不是 v
 
        }
        else if (image.fillAmount < v)// 情况2:当前填充值 < 目标值(需要增加)
        {
            // 计算理论过渡值:当前值 + 帧变化量
            var temp = image.fillAmount + GameTime.deltaTime * speed;
            // 防止过度增加(超过目标值)
            if (temp > v)
            {
                temp = v;
            }
            // ⚠️ 错误点:同上,直接设置为目标值
            image.fillAmount = v;
        }
        // 返回是否已达目标值
        return image.fillAmount == v;
 
    }

         这个方法目的是设置一个Image组件的fillAmount属性,使其逐渐变化到目标值v,变化速度由speed控制。
SetFillAmount方法文章浏览阅读659次,点赞9次,收藏10次。这里的Move方法是FSM中的Move方法,这里false指的是让AI敌人基于世界坐标朝向进行移动,为什么输入距离可以看链接中——帧的无关化案例进行理解。类,其核心目的是创建一个全局可访问的游戏单位管理器,特别是为了存储和管理玩家角色的引用。检查时间差是否超过0.1秒(100毫秒),如果条件成立,说明距离上次执行已超过0.1秒。的扩展方法,用于计算以物体为中心、基于给定半径和角度的偏移位置点,(即坐标系中)当路径点 > 1 时,直接跳过起点(索引0),因为角色通常已在起点位置。 https://blog.csdn.net/2303_80204192/article/details/149715559?sharetype=blogdetail&sharerId=149715559&sharerefer=PC&sharesource=2303_80204192&spm=1011.2480.3001.8118#t34

Logo

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

更多推荐