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\
Assets\..\..
会向上跳两级目录:
第一级:从
Assets
到项目根目录HOT
第二级:从
HOT
到父目录unity practice
最终路径变成了
E:/unity practice/Tools\Excel\
所以改动这里的Excel表再重新配置就好了
复制项目文件会出现路径混乱
1、路径混乱的原因
绝对路径问题:
.sln
和.csproj
文件包含绝对路径引用当您复制项目到新位置时,这些路径仍然指向原始位置
用户特定设置:
.csproj.user
文件包含用户特定的绝对路径这些文件通常不被纳入版本控制,但会被复制
缓存问题:
Library
文件夹包含路径相关的缓存Unity 的
Library
文件夹在复制后可能仍指向旧路径
干净的复制方法:
如果已经复制了全部文件(包含 .sln):
删除自动生成的文件:
在新项目中删除:
所有
.sln
文件所有
.csproj
文件所有
.csproj.user
文件
Library/
文件夹(重要!)似乎我就随便在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:当前使用的技能数据)}
检查空值:是否存在
GameEvent.OnPlayerAtk?有则激发当前对象当前状态的技能数据
传递关键参数:
this
:当前对象(很可能是技能施放者或状态机)
currentState.skill
:当前状态关联的技能数据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()
时:
先通过
GetWalkNearestPosition
获取单个可行走目标点用
StartPath
生成由多个可行走点连成的完整路线(而不是走到最近点→再生成新路径"的循环)路径生成后立即触发
OnPathComplete
(此时角色尚未移动)角色开始逐个行走路径点,直到走完最后一个点后停止
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