当前位置: 首页 > news >正文

Arpg第二章——流程逻辑

 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()方法中触发事件:

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 自己做出对应的技能IDif (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{//寻路到终点了  或者移动超时5fif (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/0if (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

 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

http://www.lryc.cn/news/608279.html

相关文章:

  • 自动驾驶中的传感器技术15——Camera(6)
  • 数字化转型驱动中小制造企业的质量管理升级
  • TFS-2022《A Novel Data-Driven Approach to Autonomous Fuzzy Clustering》
  • 【深度学习②】| DNN篇
  • 编译器与解释器:核心原理与工程实践
  • 基于Postman进行http的请求和响应
  • 操作系统:远程过程调用( Remote Procedure Call,RPC)
  • Jupyter notebook如何显示行号?
  • SQL Server从入门到项目实践(超值版)读书笔记 22
  • Spring事务失效场景
  • kotlin小记(1)
  • 集合框架(重点)
  • linux ext4缩容home,扩容根目录
  • 网络安全基础知识【6】
  • Ext系列文件系统
  • 【软考中级网络工程师】知识点之级联
  • 错误处理_IncompatibleKeys
  • 企业资产|企业资产管理系统|基于springboot企业资产管理系统设计与实现(源码+数据库+文档)
  • 【学习笔记】MySQL技术内幕InnoDB存储引擎——第6章 锁
  • 在linux(ubuntu)服务器上安装NTQQ并使用
  • junit中@InjectMocks作用详解
  • Redisson高并发实战:Netty IO线程免遭阻塞的守护指南
  • 零基础 “入坑” Java--- 十六、字符串String 异常
  • wxPython 实践(六)对话框
  • Java 大视界 -- Java 大数据在智能安防视频监控系统中的视频摘要生成与智能检索优化进阶(377)
  • ARMv8/v9架构FAR_EL3寄存器介绍
  • 图漾AGV行业常用相机使用文档
  • UE5 Insight ProfileCPU
  • MySQL 中 count(*)、count(1) 和 count(字段名) 有什么区别?
  • 【高等数学】第七章 微分方程——第七节 常系数齐次线性微分方程