Arpg第二章——流程逻辑
效果:当触发时,当前类的方法会被调用2、触发当玩家角色释放普通攻击和技能时,在:是否存在this:当前对象(很可能是技能施放者或状态机):当前状态关联的技能数据3、为AI角色提供以下核心功能1.2.
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()
方法中触发事件: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:当前使用的技能数据)
}
-
检查空值:是否存在
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方法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插件
下面这段代码主要第二部分内容
该插件的当前使用案例
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
{
//寻路到终点了 或者移动超时5f
if (navigationService.IsEnd() || GameTime.time - currentState.begin >= 5f)
{
navigationService.Stop();//停止移动
/*ToNext(1001);*/
AIAtk();//进行攻击
next_atk = 0;
}
}
}
}
}
寻路到终点的条件判断
navigationService.IsEnd()
主动发起攻击
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的巡逻状态如下:
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
该方法最终效果:
红色血条(hp_top)立即显示当前血量(0.5)
含透明度血条(hp_middle)缓慢跟进,形成"伤害残留"效果
该方法解耦部分:通过数据比对,判断具体需求——是增加血条还是减少血条
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
更多推荐
所有评论(0)