Arpg第二章——方法合集
这里的Move方法是FSM中的Move方法,这里false指的是让AI敌人基于世界坐标朝向进行移动,为什么输入距离可以看链接中——帧的无关化案例进行理解。类,其核心目的是创建一个全局可访问的游戏单位管理器,特别是为了存储和管理玩家角色的引用。检查时间差是否超过0.1秒(100毫秒),如果条件成立,说明距离上次执行已超过0.1秒。的扩展方法,用于计算以物体为中心、基于给定半径和角度的偏移位置点,(即
导航服务体:NavagationService
using System;
using System.Collections;
using System.Collections.Generic;
using Pathfinding;
using UnityEngine;
public class NavigationService : FSMServiceBase
{
public override void OnAnimationEnd(PlayerState state)
{
base.OnAnimationEnd(state);
}
public override void OnBegin(PlayerState state)
{
base.OnBegin(state);
}
public override void OnDisable(PlayerState state)
{
base.OnDisable(state);
}
public override void OnEnd(PlayerState state)
{
base.OnEnd(state);
}
public override void OnUpdate(float normalizedTime, PlayerState state)
{
base.OnUpdate(normalizedTime, state);
OnMove();
}
List<Vector3> _path;
public int state;//0空闲 1正在寻找路线 2返回寻路结果
Vector3 _point;//寻路的目的地
public int currentWaypoint;//路点索引(当前的)
public Vector3 _pathLast;//路径的最后一个位置
public Action _success;//寻路成功后要触发的事件
public void Move(Vector3 position, Action success)
{
if (state == 0 || (state == 1 && position != _point))
{
_point = position;
this._success = success;
state = 1;
var p = NavHelper.Instance.GetWalkNearestPosition(position);
player.seeker.StartPath(player._transform.position, p, OnPathComplete);
}
}
private void OnPathComplete(Path path)
{
if (state == 1)
{
state = 2;
if (path.error == false)
{
_path = path.vectorPath;
if (_path.Count <= 1)
{
currentWaypoint = 0;
}
else
{
currentWaypoint = 1;
}
_pathLast = _path[_path.Count - 1];
this.player._transform.LookTarget(_path[currentWaypoint]);
this._success?.Invoke();
}
else
{
Stop();
}
}
}
private void OnMove()
{
if (_path == null) { return; }
//每帧朝下一个路点进行移动
//判断是否接近路点了,是的话 更新到下一个路点
if (currentWaypoint >= _path.Count)
{
Stop();
return;
}
else
{
var next_point = _path[currentWaypoint];
Vector3 dir = next_point - player._transform.position;
dir.y = 0;
this.player.Move(dir * player.GetMoveSpeed(), false);
if (Vector3.Distance(this.player._transform.position, next_point) <= 0.5f)
{
if (currentWaypoint >= _path.Count - 1)
{
Stop();
}
else
{
currentWaypoint += 1;
this.player._transform.LookTarget(_path[currentWaypoint]);
}
}
}
}
public void Stop()
{
_path = null;
state = 0;
}
public override void ReLoop(PlayerState state)
{
base.ReLoop(state);
}
public override void ReStart(PlayerState state)
{
base.ReStart(state);
}
internal bool IsEnd()
{
return _path == null;
}
}
Move方法
List<Vector3> _path;
public int state;//0空闲 1正在寻找路线 2返回寻路结果
Vector3 _point;//寻路的目的地
public int currentWaypoint;//路点索引(当前的)
public Vector3 _pathLast;//路径的最后一个位置
public Action _success;//寻路成功后要触发的事件
public void Move(Vector3 position, Action success)
{
// 条件检查:空闲状态 或 寻路中但新目标不同
if (state == 0 || (state == 1 && position != _point))
{
_point = position; // 保存目标位置
_success = success; // 保存回调函数
state = 1; // 设为寻路中状态
// 获取实际可行走的最近点(处理不可达位置)
Vector3 p = NavHelper.Instance.GetWalkNearestPosition(position);
// 开始寻路请求(异步)
player.seeker.StartPath(
player._transform.position, // 起点:角色当前位置
p, // 终点:修正后的可行走点
OnPathComplete // 寻路完成回调(代码中未展示)
);
}
}
核心逻辑说明
状态控制:
仅在
空闲(0)
或正在寻路但新目标不同
时响应新移动请求避免重复请求相同目标位置的路径
路径修正:
通过
NavHelper
将用户输入位置转换为导航网格( NavMesh )上的可行走点异步寻路:
使用第三方寻路库(如A* Pathfinding)的
StartPath
方法异步计算路径,完成后触发
OnPathComplete
回调(需在类中实现)回调机制:
保存
_success
委托,用于路径计算成功后触发业务逻辑
当调用
Move()
时:
先通过
GetWalkNearestPosition
获取单个可行走目标点用
StartPath
生成由多个可行走点连成的完整路线路径生成后立即触发
OnPathComplete
(此时角色尚未移动)角色开始逐个行走路径点,直到走完最后一个点后停止"
OnPathComplete方法
List<Vector3> _path;
public int state;//0空闲 1正在寻找路线 2返回寻路结果
Vector3 _point;//寻路的目的地
public int currentWaypoint;//路点索引(当前的)
public Vector3 _pathLast;//路径的最后一个位置
public Action _success;//寻路成功后要触发的事件
private void OnPathComplete(Path path)
{
if (state == 1)//只有当当前状态为 1(表示"路径计算中")时才处理路径结果
{
state = 2;//将状态标记为 2(可能表示"路径已就绪")
if (path.error == false)//路径计算成功时处理路径点,失败时执行终止操作
{
_path = path.vectorPath; // 存储路径点列表
_pathLast = _path[_path.Count - 1]; // 记录终点坐标
// 设置当前路点索引
if (_path.Count <= 1)
{
currentWaypoint = 0; // 只有一个点(起点即终点)
}
else
{
currentWaypoint = 1; // 跳过起点(索引0),从第二个点开始移动
}
// 玩家转向目标点
this.player._transform.LookTarget(_path[currentWaypoint]);
// 触发成功回调
this._success?.Invoke();
}
else
{
Stop();
}
}
}
关键行为说明:
路径点处理
_path
存储路径坐标序列(起点 → 途径点 → 终点)
currentWaypoint
指向玩家即将移动到的下一个目标点索引当路径点 > 1 时,直接跳过起点(索引0),因为角色通常已在起点位置
角色转向
LookTarget(_path[currentWaypoint])
使玩家立即面向第一个移动目标点(通常是路径的第二个点)事件通知
_success?.Invoke()
触发订阅该事件的回调(通知其他系统路径就绪)
OnMove方法
private void OnMove()
{
if (_path == null) { return; } // 1. 路径不存在时直接返回
if (currentWaypoint >= _path.Count) // 2. 终点检查
{
Stop(); // 到达终点停止移动
return;
}
else
{
var next_point = _path[currentWaypoint]; // 3. 获取当前目标点
Vector3 dir = next_point - player._transform.position;
dir.y = 0; // 4. 忽略垂直方向
// 5. 执行移动(水平移动)
this.player.Move(dir * player.GetMoveSpeed(), false);
// 6. 距离检测与路点切换
if (Vector3.Distance(this.player._transform.position, next_point) <= 0.5f)
{
if (currentWaypoint >= _path.Count - 1) // 7. 如果是最终点
{
Stop(); // 到达最终点停止运动
}
else
{
currentWaypoint += 1; // 8. 切换到下一个路点(即路点索引号+1)
this.player._transform.LookTarget(_path[currentWaypoint]); // 9. 转向新目标
}
}
}
}
关键步骤说明
路径有效性检查
if (_path == null) return;
确保存在有效路径时才执行移动终点判定
currentWaypoint >= _path.Count
当当前路点索引超过路径点总数时,说明已到达终点移动方向计算
dir = next_point - player.position
计算从角色当前位置指向目标点的向量水平移动限制
dir.y = 0
忽略Y轴差异,确保角色只在水平面移动(适合地面角色)移动执行
player.Move(dir * speed, false)
这里的Move方法是FSM中的Move方法,这里false指的是让AI敌人基于世界坐标朝向进行移动,为什么输入距离可以看链接中——帧的无关化案例进行理解Fsm Move方法
https://blog.csdn.net/2303_80204192/article/details/149228501?spm=1001.2014.3001.5501#t21
路点接近检测
距离 ≤ 0.5f
当角色与当前目标点距离小于0.5单位时,判定为已到达路径终点处理
检测是否是最后一个路点,是则停止移动路点切换
currentWaypoint += 1
移动到路径中的下一个点方向调整
LookTarget(新路点)
角色立即转向新的目标点
OnEnd方法
List<Vector3> _path;
public int state;//0空闲 1正在寻找路线 2返回寻路结果
public void Stop()
{
_path = null;
state = 0;
}
将路径存储列表设置为空, 将寻路状态设置为空闲状态。
Stop方法
public void Stop()
{
_path = null;
state = 0;
}
清空路径点列表,且使导航状态state设置为空闲。
IsEnd方法
internal bool IsEnd()
{
return _path == null;
}
检查
_path
是否为null
,并返回检查结果
_path
值导航状态 IsEnd()
返回值null
导航结束 true
非 null
导航中 false
IntEX类
public static class IntEx
{
public static int Range(int min, int max)
{
return UnityEngine.Random.Range(min, max + 1);
}
public static bool InRange(this int x)
{
if (x <= 0) return false;
return UnityEngine.Random.Range(0, 101) <= x;
}
IntEx
的静态工具类,用于处理角色状态的数学逻辑
1、随机数生成
public static int Range(int min, int max) { return UnityEngine.Random.Range(min, max + 1); }
生成
min
到max
(包含最大值)的随机整数修正了 Unity 原生
Random.Range
对整数的行为
方法 原生 Random.Range(1, 10)
IntEx.Range(1, 10)
范围 [1, 10) → 1-9 [1, 11) → 1-10 包含 不包含最大值 包含最大值
2、
InRange
扩展方法public static bool InRange(this int x) { if (x <= 0) return false; return UnityEngine.Random.Range(0, 101) <= x; }
功能:
判断是否触发概率事件(百分比形式)
看随机生成的整数
UnityEngine.Random.Range(0, 101)与导入的x大小关系
x大返回true,即生成暴击
,反之则不会暴击。
有关扩展方法的知识内容:
扩展方法https://www.cnblogs.com/yuer20180726/p/10901123.html扩展方法的调用:
if (unitEntity.pacing_probability.InRange()) //直接用int类型的实例unitEntity.pacing_probability进行调用 //而不是IntEX.InRange(unitEntity.pacing_probability);
UnitManager类
public class UnitManager
{
static UnitManager instance = new UnitManager();
public static UnitManager Instance => instance;
public FSM player;
}
这段代码实现了一个单例模式(Singleton)的
UnitManager
类,其核心目的是创建一个全局可访问的游戏单位管理器,特别是为了存储和管理玩家角色的引用public FSM player;
设计意图:集中管理玩家对象,避免频繁使用
FindObjectOfType
等低效查找
UTransform类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public static class UTransform
{
public static void LookTarget(this Transform t, Transform target)
{
if (target == null)
{
return;
}
Vector3 targetPosition = new Vector3(target.position.x, t.position.y, target.position.z);
t.LookAt(targetPosition);
}
public static void LookTarget(this Transform t, Vector3 target)
{
Vector3 targetPosition = new Vector3(target.x, t.position.y, target.z);
t.LookAt(targetPosition);
}
public static Vector3 GetOffsetPoint(this Transform t, float radius, float angle)
{
if (radius == 0 && angle == 0)
{
return t.transform.position;
}
float x = Mathf.Sin(angle * Mathf.PI / 180) * radius;
float z = Mathf.Cos(angle * Mathf.PI / 180) * radius;
Vector3 end = t.position + t.rotation * new Vector3(x, 0, z);
return end;
}
}
LookTarget方法(两种)
public static void LookTarget(this Transform t, Transform target)//传入当前位置和目标位置
{
if (target == null)
{
return;
}
Vector3 targetPosition = new Vector3(target.position.x, t.position.y, target.position.z);
//新建目标变量使其x轴,z轴不变,y轴与目标位置一致,即保证看向目标动作是在一个水平面,而不会因为y轴使看向动作让模型发生Y轴偏移
t.LookAt(targetPosition);
}
参数类型:接收一个
Transform
类型的目标对象,当目标是一个游戏对象时使用
public static void LookTarget(this Transform t, Vector3 target)
{
Vector3 targetPosition = new Vector3(target.x, t.position.y, target.z);
t.LookAt(targetPosition);
}
参数类型:接收一个
Vector3
类型的目标坐标,当目标是一个坐标点时使用
GetOffsetPoint方法
定义了一个名为
GetOffsetPoint
的扩展方法,用于计算以物体为中心、基于给定半径和角度的偏移位置点,(即坐标系中)
public static Vector3 GetOffsetPoint(this Transform t, float radius, float angle)
{
if (radius == 0 && angle == 0)//如果半径和角度数值为0,则返回
{
return t.transform.position;
}
float x = Mathf.Sin(angle * Mathf.PI / 180) * radius;
float z = Mathf.Cos(angle * Mathf.PI / 180) * radius;
Vector3 end = t.position + t.rotation * new Vector3(x, 0, z);
return end;
}
1、角度转换成弧度
float x = Mathf.Sin(angle * Mathf.PI / 180) * radius; float z = Mathf.Cos(angle * Mathf.PI / 180) * radius;
角度转换部分:
csharp angle * Mathf.PI / 180
angle
:输入的角度值(单位:度)
Mathf.PI
:Unity的数学常量,表示π(约3.14159)转换原理:将角度转换为弧度(三角函数需要弧度制)
弧度 = 角度 × π / 180
三角函数计算:
float x = Mathf.Sin(angle * Mathf.PI / 180) //括弧里面是弧度 float z = Mathf.Cos(angle * Mathf.PI / 180) 这些三角函数得出的结果——半径为1的单位圆的弧点 float x = Mathf.Sin(angle * Mathf.PI / 180) * radius; 将三角函数的结果乘以半径值,将单位圆上的点(半径为1)缩放到实际需要的尺寸
计算过程示例:角度转弧度:30 × π/180 ≈ 0.5236弧度 计算X坐标:sin(0.5236) ≈ 0.5 × 5 = 2.5 计算Z坐标:cos(0.5236) ≈ 0.866 × 5 ≈ 4.33 结果坐标:(2.5, 0, 4.33
2、对整体对象进行旋转
Vector3 end = t.position + t.rotation * new Vector3(x, 0, z); new Vector3(x, 0, z); 这是一个在物体局部坐标系中的向量,基于之前的三角函数计算: 1.x = sin(角度) * 半径 → 右侧偏移 2.z = cos(角度) * 半径 → 前方偏移 t.rotation * new Vector3(x, 0, z); 关键操作:将局部偏移向量从物体坐标系转换到世界坐标系(跟四元数的旋转机制有关,这里不深究) Vector3 end = t.position + t.rotation * new Vector3(x, 0, z); 将旋转后的偏移量加到物体的世界坐标上,得到相对于物体位置和朝向的最终世界坐标
这里大概就是方法,根据当前物体作为原点,获取目标点坐标的图示原理
NavHelper类:导航助手
using Pathfinding;
using UnityEngine;
public class NavHelper
{
//通过静态实例确保全局只有一个NavHelper对象,通过Instance属性提供全局访问入口。
static NavHelper instance = new NavHelper();
public static NavHelper Instance => instance;
//NNConstraint是A Pathing插件中用于定义寻路搜索条件的类
NNConstraint nNConstraint;
public Vector3 GetWalkNearestPosition(Vector3 pos)
{
if (nNConstraint == null)
{
nNConstraint = new NNConstraint();
nNConstraint.constrainWalkability = true; // 启用可行走性约束
nNConstraint.walkable = true; // 只搜索可行走的点
nNConstraint.constrainDistance = true; // 启用距离约束
}
return AstarPath.active.GetNearest(pos, nNConstraint).position;
}
}
if (nNConstraint == null) { nNConstraint = new NNConstraint(); nNConstraint.constrainWalkability = true; // 启用可行走性约束 nNConstraint.walkable = true; // 只搜索可行走的点 nNConstraint.constrainDistance = true; // 启用距离约束 } //使用A*插件的GetNearest方法,根据约束条件查找距离pos最近的可行走点坐标。 return AstarPath.active.GetNearest(pos, nNConstraint).position;
初始化约束(首次调用时):
创建一个只搜索可行走区域的过滤器(
walkable=true
)。查询最近点:
通过
AstarPath.active.GetNearest()
方法,结合约束条件搜索导航网格。返回最近可行走点的坐标
- 计算最近可行走点的特性:
同步执行:
csharp AstarPath.active.GetNearest(pos, nNConstraint).position;
这是单次即时计算,调用后立即返回结果(同一帧内完成)
非渐进式:
不涉及分帧处理
不会逐步生成多个点
每次调用只返回一个最近的有效点
底层机制:
基于预先生成的导航网格(Grid/Point Graph)
使用四叉树/KD树空间分区加速查询
FSM新加入的AI相关类
AI监听机制
if (AI == false)//禁用AI,即当前FSM持有者是玩家的时候 { …… } else(AI)启用AI,即当前FSM持有者是AI的时候 { foreach (var item in stateData) { //进入状态超过多长时间 if (item.Value.excel_config.active_attack > 0) { AddListener(item.Key, StateEventType.update, AutoTriggerAtk_AI); } if (item.Value.excel_config.trigger_pacing > 0) { AddListener(item.Key, StateEventType.onAnmEnd, TriggerPacing); } if (item.Value.excel_config.tag == 4) { AddListener(item.Key, StateEventType.update, OnPacingUpdate); } if (item.Value.excel_config.trigger_patrol > 0) { AddListener(item.Key, StateEventType.update, TriggerPatrol); } } //AI要注册的事件 GameEvent.OnPlayerAtk += OnPlayerAtk; AddListener(1014, StateEventType.onAnmEnd, OnDashEnd); AddListener(1042, StateEventType.update, OnMoveToPoint); AddListener(1042, StateEventType.end, NavStop); AddListener(1013, StateEventType.update, AI_Defending); AddListener(10131, StateEventType.update, AI_Defending); AddListener(1043, StateEventType.update, OnPatrolUpdate); AddListener(1043, StateEventType.begin, ChangeMoveSpeed); AddListener(1043, StateEventType.end, OnPatrolEnd); } //AI与玩家都要用到的监听器 AddListener(1017, StateEventType.update, OnBashUpdate); AddListener(1017, StateEventType.end, OnBashEnd); AddListener(1018, StateEventType.update, OnBashUpdate); AddListener(1018, StateEventType.end, OnBashEnd); }
遍历表格的含有判断条件的监听器如下:
创建监听表看看哪些当前状态能触发所设置好的事件。
这里AI注册事件是两种监听机制:
1、全局游戏事件监听GameEvent.OnPlayerAtk += OnPlayerAtk; GameEvent新加入方法如下 public class GameEvent { public static Action<FSM, SkillEntity> OnPlayerAtk; }
监听全局的玩家攻击事件
当玩家发起攻击时,触发
OnPlayerAtk
方法使用 C# 原生的 事件委托机制 (event/delegate
+=
操作符:将右侧方法添加到事件的调用列表效果:当
GameEvent.OnPlayerAtk
触发时,当前类的OnPlayerAtk
方法会被调用
2、状态机事件监听
AddListener(1014, StateEventType.onAnmEnd, OnDashEnd); AddListener(1042, StateEventType.update, OnMoveToPoint); AddListener(1042, StateEventType.end, NavStop);
监听特定状态机的事件
特性 GameEvent
事件系统AddListener
状态机事件作用范围 全局事件(跨系统通信) 局部事件(特定状态机内部) 事件类型 游戏逻辑事件(如攻击/死亡等) 状态机生命周期事件 解耦程度 完全解耦(发布者不知道订阅者存在) 需要知道具体状态机实例 典型使用场景 系统间通信(如UI响应游戏事件) 状态机内部行为控制 执行效率 反射/委托调用(稍慢) 直接调用(更快) 内存管理 需手动注销(否则内存泄漏) 通常随状态机自动销毁
OnPacingUpdate方法
private void OnPacingUpdate()
{
// 状态超时检查:如果当前状态持续时间≥5秒
if (GameTime.time - currentState.begin >= 5)
{
// 切换到状态ID 1001
ToNext(1001);
}
// 检查是否存在攻击目标
if (atk_target != null)
{
// 使AI面向攻击目标
LookAtkTarget();
// 检查与目标的距离是否≤3个单位
if (GetDst() <= 3)
{
// 执行攻击行为
AIAtk();
// 立即结束本次状态更新
return;
}
}
}
这是一个AI行为的状态更新方法,主要处理两个逻辑:
状态超时切换:如果当前状态持续超过5秒,则切换到新状态
目标攻击处理:如果有攻击目标,处理面向目标和攻击逻辑
OnMoveToPoint方法
float _OnMoveToPoint_CheckTime;
private void OnMoveToPoint()
{
//每0.1秒执行一次核心逻辑
if (GameTime.time - _OnMoveToPoint_CheckTime > 0.1)
{
_OnMoveToPoint_CheckTime = GameTime.time;
//预定要执行的攻击技能ID和攻击目标都存在时
if (next_atk != 0 && atk_target != null)
{
var dst = GetDst();//获取目标与自身的距离
if (dst <= stateData[next_atk].skill.atk_distance)//判断是否在施法范围内
{
navigationService.Stop();//停止导航移动
this._transform.LookTarget(atk_target._transform);//看向目标
ToNext(next_atk);//切换到攻击状态
next_atk = 0;//清空预定攻击
}
else
{
//寻路到终点了 或者移动超时5f
if (navigationService.IsEnd() || GameTime.time - currentState.begin >= 5f)
{
navigationService.Stop();//停止移动
/*ToNext(1001);*/
AIAtk();//进行攻击
next_atk = 0;
}
}
}
}
}
1、时间差定时器
if (GameTime.time - _OnMoveToPoint_CheckTime > 0.1) { _OnMoveToPoint_CheckTime = GameTime.time; }
GameTime.time
:当前游戏运行的总时间(秒)
_OnMoveToPoint_CheckTime
:上次执行逻辑时记录的时间戳两者相减得到从上一次执行到现在经过的时间
检查时间差是否超过0.1秒(100毫秒),如果条件成立,说明距离上次执行已超过0.1秒
将当前时间赋值给
_OnMoveToPoint_CheckTime,即重置记录的时间,
为下一次检查做准备时间差计时器的使用案例:
t=0.000s(游戏开始): GameTime.time = 0.0 _OnMoveToPoint_CheckTime = 0.0 差值 = 0.0 < 0.1 → 不执行 t=0.100s: GameTime.time ≈ 0.100 差值 = 0.100 - 0.000 = 0.100 > 0.1 → 执行逻辑 更新 _OnMoveToPoint_CheckTime = 0.100 t=0.116s: 差值 = 0.116 - 0.100 = 0.016 < 0.1 → 不执行 t=0.200s: 差值 = 0.200 - 0.100 = 0.100 > 0.1 → 再次执行 更新 _OnMoveToPoint_CheckTime = 0.200
if (navigationService.IsEnd() || GameTime.time - currentState.begin >= 5f)
currentState.begin
:
表示当前状态开始执行的时间点
通过
SetBeginTime()
方法设置:public bool ToNext(int Next) { if (stateData.ContainsKey(Next))//先检查stateData是否含有此状态 { currentState = stateData[Next]; currentState.SetBeginTime();//currentState.begin:进入新状态的开始计时的计时器 } return false; }
GameTime.time
:
表示当前游戏运行的总时间(秒)
在
GameTime.Update()
中每帧更新
GameTime.time - currentState.begin
:
计算从状态开始到现在经过的时间
相当于:
当前时间 - 状态开始时间 = 状态持续时间
TriggerPacing方法
private void TriggerPacing()
{
if (IsDead())//如果自己已经死亡,直接退出,什么都不做
{
return;
}
if (unitEntity.pacing_probability > 0 )//当单位配置里的pacing_probability大于0
{
if (unitEntity.pacing_probability.InRange())//判断是否踱步(使用InRange随机生成数看是否触发当前概率)
{
if (atk_target == null)//如果当前没有攻击目标(atk_target == null),就测量一下自己跟玩家的距离;
{
var dst = Vector3.Distance(_transform.position, UnitManager.Instance.player._transform.position);
if (dst < 10)//距离小于 10 就把玩家设成目标,否则直接退出。
{
atk_target = UnitManager.Instance.player;
}
else
{
return;
}
}
if (currentState.excel_config.tag == 4)
{
//如果正处于待机状态,必须等 3 到 6 秒的随机时间后才能继续,防止动作切换太频繁。
if (GameTime.time - currentState.begin >= IntEx.Range(3, 6))
{
var next = IntEx.Range(1036, 1041);//随机选一个攻击动作:从 1036 到 1041 之间随机挑一个动作 ID
_transform.LookTarget(atk_target._transform);
ToNext(next);
}
}
else//如果不处于待机状态,则直接发动攻击
{
var next = IntEx.Range(1036, 1041);
_transform.LookTarget(atk_target._transform);
ToNext(next);
}
}
}
OnPlayerAtk方法
private void OnPlayerAtk(FSM atk, SkillEntity arg2)
{
if (att_crn.hp <= 0)
{
return;
}
//5米距离内且玩家在AI面前
if (GetDst(atk._transform) <= 5
&& _transform.ForwardOrBack(atk._transform.position) > 0)//这里是AI为当前位置,目标位置是玩家(即攻击方)
{
if (unitEntity.block_probability.InRange())//随机生成数判定是否成功格挡
{
//检查当前动画进度是否处于配置允许触发防御动作的时间区间内,确保防御动作只能在角色动画合适的阶段被激活。
if (CheckConfig(currentState.excel_config.on_defense))//
{
_transform.LookTarget(atk._transform);AI朝向攻击方
var result = ToNext((int)currentState.excel_config.on_defense[2]);AI进入格挡状态
if (result)
{
return;//进入了格挡状态后,直接退出OnPlayerAtk方法
}
}
}
else if (unitEntity.dodge_probability.InRange())//随机生成数判定是否成功闪避
{
//trigger_dodge在表中只有1和0,为1时当前状态可以躲避
if (currentState.excel_config.trigger_dodge > 0)
{
_transform.LookTarget(atk._transform);
var next = IntEx.Range(1032, 1035);//随机从1032-1035生成一个状态id
var result = ToNext(next);
if (result)
{
return;
}
}
}
else if (unitEntity.atk_probability.InRange())//随机生成数判定是否触发攻击
{ //抢手攻击的检测
if (currentState.excel_config.first_strike > 0)first_strike
{
TriggerAtk_AI();
}
}
}
}
触发躲闪,触发抢攻,即调度攻击决策的表格如下:
TriggerAtk_AI方法
private void TriggerAtk_AI()
{
//当前动画进度大于表格中攻击参数触发点,调用AIAtk();
if (animationService.normalizedTime >= currentState.excel_config.trigger_atk)
{
AIAtk();
}
}
AIAtk方法
int next_atk;
private void AIAtk()
{
if (IsDead())
{
return;
}
next_atk = IntEx.Range(1005, 1012);//随机从1005~1012中挑一个出来
//挑选攻击的决策
//根据对方的技能ID 自己做出对应的技能ID
if (stateData[next_atk].skill.atk_distance > 0)//当前状态的施法距离是否大于0
{
if (atk_target == null)//若攻击目标为空,则添加UnitManager.Instance中的player实例
{
atk_target = UnitManager.Instance.player;
}
var dst =Vector3.Distance(_transform.position,atk_target._transform.position);//取AI与目标的距离
if (dst >= stateData[next_atk].skill.atk_distance)//若二者距离大于等于施法距离
{
this._transform.LookTarget(atk_target._transform);//看向玩家
if (IntEx.Range(0, 100) <= 50)//取一半概率,若此时寻路状态空闲,则寻路至玩家位置
{
if (navigationService.state == 0)
{
navigationService.Move(atk_target._transform.position, MoveToPoint);
}
}
else
{
ToNext(1014); //突进冲刺
}
}
else//二者距离小于施法距离,随机进入一种攻击状态
{
this._transform.LookTarget(atk_target._transform);
ToNext(next_atk);
}
}
}
OnBash方法
//fb: 整数参数,可能是击退类型或强度标识
//atk: FSM类型对象(有限状态机),表示攻击者
//add_fly: 浮点数组,包含击退附加力的XYZ分量
//point: Vector3类型,表示击中的位置坐标
public void OnBash(int fb, FSM atk, float[] add_fly, Vector3 point)
{
atk_target = atk;
bash_add_fly = new Vector3(add_fly[0], add_fly[1], add_fly[2]);//将技能表中的add_fly数据作为坐标点
bash_fly_dir = (this._transform.position - atk.transform.position).normalized;//计算从攻击者位置指向当前对象位置的向量,归一化(normalized)后得到单位方向向量
if (currentState.excel_config.on_bash != null)//切换到击飞状态
{
ToNext(currentState.excel_config.on_bash[fb]);
}
}
附加击飞效果的数据表如下:
OnBashUpdate
//这俩变量是全局变量
bash_add_fly = new Vector3(add_fly[0], add_fly[1], add_fly[2]);//将技能表中的add_fly数据作为坐标点(即每个轴上的期望的位移距离)
bash_fly_dir = (this._transform.position - atk.transform.position).normalized;//计算从攻击者位置指向当前对象位置的向量,归一化(normalized)后得到单位方向向量
public void OnBashUpdate()
{
Debug.Log("OnBash");
//时长 0.2f (0.1)上升 (0.1)下降的过程
var f = GameTime.time - currentState.begin;//状态持续时间
if (f <= 0.1f)//在状态持续前0.1秒前
{
var d = bash_fly_dir * (bash_add_fly.z / 0.2f);// // 水平方向:按击退方向移动,除以0.2f即是在该Z轴的速度
d.y = bash_add_fly.y / 0.2f;// 垂直方向:固定上升速度
Move(d, false, _add_gravity: false, _do_ground_check: false);// 应用移动(禁用重力和地面检测)
}
else if (f <= 0.2f)
{
var d = bash_fly_dir * (bash_add_fly.z / 0.2f);//保持与上升阶段相同的水平速度
d.y = -(bash_add_fly.y / 0.2f * 2);//以两倍速度下降(实现快速坠落效果)
Move(d, false, _add_gravity: false, _do_ground_check: false);//_add_gravity:false禁用重力,完全由代码控制运动
}
}
y轴为0时只是简单的击退击倒动作,y轴为正值则为击飞动作
1、向量的帧位移计算// 结果向量d的组成: d.x = bash_fly_dir.x * (bash_add_fly.z / 0.2f) d.y = bash_add_fly.y / 0.2f d.z = bash_fly_dir.z * (bash_add_fly.z / 0.2f)
计算过程如下:
bash_add_fly = new Vector3(0, 5, 10); // X未使用,Y=5(上升高度),Z=10(水平距离) bash_fly_dir = (0.707, 0, 0.707); // 45度方向(归一化) 假设: 水平速度:10 / 0.2 = 50 单位/秒 垂直速度:5 / 0.2 = 25 单位/秒 帧位移(假设60FPS,deltaTime=0.0167秒): X位移:0.707 * 50 * 0.0167 ≈ 0.59 Y位移:25 * 0.0167 ≈ 0.42 Z位移:0.707 * 50 * 0.0167 ≈ 0.59
OnBashEnd方法
public void OnBashEnd()
{
ground_check = true;
}
TriggerPatrol方法
private void TriggerPatrol()
{
if (atk_target == null || GetDst() > 10)
{
if (GameTime.time - currentState.begin >= currentState.excel_config.trigger_patrol)//若是状态时间超过巡逻触发事件
{
//进入巡逻
//ToNext(1043);
//自己周边3-6米的位置
var r = IntEx.Range(3, 6);
//从0-360度旋转
var a = IntEx.Range(0, 359);
//用于计算以物体为中心、基于给定半径和角度的偏移位置点(以Vector类型)
var target = _transform.GetOffsetPoint(r, a);
//导航到目标点位置,触发巡逻事件
navigationService.Move(target, ToPatrol);
}
}
}
血条更新方法:UpdateHP_OnHit
internal void UpdateHP_OnHit(int damage)
{
this.att_crn.hp -= damage;
if (this.att_crn.hp < 0)
{
this.att_crn.hp = 0;
}
if (AI)
{
//更新敌人血条
if (unitEntity.type == 3)
{
//更新Boss的血条
}
else
{
//更新小兵的血条
UpdateEnemyHUD();
}
}
else
{
//更新主角的血条
}
}
UpdateEnemyHUD方法
private void UpdateEnemyHUD()
{
if (AI)
{
if (unitEntity.type == 1 || unitEntity.type == 2 || unitEntity.type == 0)
{
if (enemyHUD == null)
{
enemyHUD = ResourcesManager.Instance.CreateEnemyHUD();//使血条实例赋值
}
//血条更新
enemyHUD.UpdateHP(att_crn.hp / att_base.hp, this._transform, unitEntity.info);
}
}
}
Resource新加入的方法
Stack<EnemyHUD> hud = new Stack<EnemyHUD>(50);
internal EnemyHUD CreateEnemyHUD()
{
if (hud.Count > 0)//若hud存储栈中有数据存在则取用
{
return hud.Pop();
}
else
{
//在Resource文件夹中遵循该路径找到此物体并获取该组件
var go = Instantiate<GameObject>("UI/HUD/Enemy_HUD");
return go.GetComponent<EnemyHUD>();
}
}
public void DestroyEnemyHUD(EnemyHUD enemyHUD)
{
enemyHUD.gameObject.SetActive(false);
hud.Push(enemyHUD);
}
}
EnemyHUD类:血条
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class EnemyHUD : MonoBehaviour
{
public Image hp_top;//最顶层
public Image hp_middle;//中间部分的血条
public float middle_speed = 1;//插值速度
public float hp = -1;//血量
public Transform target;
public Text name_text;//昵称
public Vector3 offset = new Vector3(0, 1.8f, 0);
void Awake()
{
hp_top = transform.Find("HealthBar/HP_Top").GetComponent<Image>();
hp_middle = transform.Find("HealthBar/HP_Middle").GetComponent<Image>();
name_text = transform.Find("Name").GetComponent<Text>();
}
void Update()
{
DOUpdateHP();
if (hp <= 0)
{
if (hp_top.fillAmount == 0 && hp_middle.fillAmount == 0)
{
this.gameObject.SetActive(false);
ResourcesManager.Instance.DestroyEnemyHUD(this);
}
}
if (target != null)
{
this.transform.position = target.position + offset;
this.transform.rotation = GameDefine._Camera.transform.rotation;
}
}
bool _do_update;
public void UpdateHP(float v, Transform target, string name)
{
_do_update = true;
hp = v;
this.target = target;
name_text.text = name;
if (v > 0) { this.gameObject.SetActive(true); }
}
private void DOUpdateHP()
{
if (hp == -1 || _do_update == false)
{
return;
}
if (hp_top.fillAmount > hp)
{
//减少
SetFillAmount(hp_top, hp, middle_speed * 5);
}
else if (hp_top.fillAmount < hp)
{
//增加它
SetFillAmount(hp_top, hp, middle_speed);
}
if (hp_middle.fillAmount > hp)
{
SetFillAmount(hp_middle, hp, middle_speed);
}
else if (hp_middle.fillAmount < hp)
{
SetFillAmount(hp_middle, hp, middle_speed * 5);
}
if (_do_update)
{
if (hp_top.fillAmount == hp && hp_middle.fillAmount == hp)
{
_do_update = false;
}
}
}
public bool SetFillAmount(Image image, float v, float speed)
{
if (image.fillAmount > v)
{
var temp = image.fillAmount - GameTime.deltaTime * speed;
if (temp < v)
{
temp = v;
}
image.fillAmount = v;
}
else if (image.fillAmount < v)
{
var temp = image.fillAmount + GameTime.deltaTime * speed;
if (temp > v)
{
temp = v;
}
image.fillAmount = v;
}
return image.fillAmount == v;
}
}
这个方法一般挂载在血条物体上如下:
Awake方法
void Awake()
{
hp_top = transform.Find("HealthBar/HP_Top").GetComponent<Image>();
hp_middle = transform.Find("HealthBar/HP_Middle").GetComponent<Image>();
name_text = transform.Find("Name").GetComponent<Text>();
}
获取该物体中的子物体上的组件
Update方法
public Image hp_top;//最顶层
public Image hp_middle;//中间部分的血条
public float middle_speed = 1;//插值速度
public float hp = -1;//血量
public Transform target;
public Text name_text;//昵称
public Vector3 offset = new Vector3(0, 1.8f, 0);
void Update()
{
DOUpdateHP();//更新血条显示(如填充值变化)
if (hp <= 0)//检查生命值是否≤0
{
// 当顶部血条和中部血条均已归零(动画播完)
if (hp_top.fillAmount == 0 && hp_middle.fillAmount == 0)
{
this.gameObject.SetActive(false);// 隐藏对象
ResourcesManager.Instance.DestroyEnemyHUD(this);// 通知资源管理器销毁HUD
}
}
if (target != null)// 确保目标存在
{
// 位置:固定在目标对象的指定偏移位置
this.transform.position = target.position + offset;
// 旋转:与主摄像机保持相同朝向(实现Billboard效果)
this.transform.rotation = GameDefine._Camera.transform.rotation;
}
}
UpdateHP方法
public void UpdateHP(float v, Transform target, string name)
{
// 1. 设置更新标志
_do_update = true; // 触发血条视觉更新(可能在DOUpdateHP()中使用)
// 2. 更新核心数据
hp = v; // 存储当前生命值(用于Update()中的死亡判断)
this.target = target; // 更新血条跟随的目标对象
name_text.text = name;// 更新名称显示(如显示敌人名称)
// 3. 控制血条显示
if (v > 0) {
this.gameObject.SetActive(true); // 生命值>0时激活血条
}
}
血条激活条件
仅当
v > 0
(生命值未耗尽)时激活血条对象若敌人复活(生命值从0恢复),血条会重新显示
不处理死亡隐藏:死亡时的隐藏逻辑在
Update()
中处理(需等待血条动画归零)数据同步机制
hp
变量在Update()
中用于死亡检测(if (hp <= 0)
)
target
更新确保血条位置正确跟随新目标
name_text
实时更新敌人名称(如精英怪特殊标识)更新触发标志
_do_update = true
可能用于:
控制
DOUpdateHP()
中的血条动画(如缓动填充效果)避免无变化时的冗余计算(性能优化)
DOUpdateHP方法
private void DOUpdateHP()
{
if (hp == -1 || _do_update == false)//hp == -1未初始化状态||_do_update:更新标志(由UpdateHP()方法设置)
{
return;//确保只在需要更新时执行(性能优化)
}
if (hp_top.fillAmount > hp)//血量减少(受伤)
{
//受伤特效:血条快速下降(5倍速度),增强打击感
SetFillAmount(hp_top, hp, middle_speed * 5);
}
else if (hp_top.fillAmount < hp)//血量增加(治疗)
{
//治疗特效:血条匀速上升,表现平缓恢复
SetFillAmount(hp_top, hp, middle_speed);
}
if (hp_middle.fillAmount > hp)// 延迟血条高于实际血量(需下降)
{
SetFillAmount(hp_middle, hp, middle_speed);// 慢速下降
}
else if (hp_middle.fillAmount < hp)// 延迟血条低于实际血量(需上升)
{
SetFillAmount(hp_middle, hp, middle_speed * 5); // 快速追平
}
if (_do_update)
{
if (hp_top.fillAmount == hp && hp_middle.fillAmount == hp)当两个血条都达到目标值hp
{
_do_update = false;// 停止更新
}
}
}
1. 更新条件检查
csharp if (hp == -1 || _do_update == false) return;
作用:确保只在需要更新时执行(性能优化)
2. 实时血条(
hp_top
)控制csharp // 血量减少(受伤) if (hp_top.fillAmount > hp) { SetFillAmount(hp_top, hp, middle_speed * 5); // 快速下降 } // 血量增加(治疗) else if (hp_top.fillAmount < hp) { SetFillAmount(hp_top, hp, middle_speed); // 正常速度上升 }
受伤特效:血条快速下降(5倍速度),增强打击感
治疗特效:血条匀速上升,表现平缓恢复
设计意图:突出"受伤"的视觉反馈强于"治疗"
3. 延迟血条(
hp_middle
)控制csharp // 延迟血条高于实际血量(需下降) if (hp_middle.fillAmount > hp) { SetFillAmount(hp_middle, hp, middle_speed); // 慢速下降 } // 延迟血条低于实际血量(需上升) else if (hp_middle.fillAmount < hp) { SetFillAmount(hp_middle, hp, middle_speed * 5); // 快速追平 }
经典延迟效果:受伤后白色血条缓慢追平(
middle_speed
)快速复位:治疗时立即追上实时血条(5倍速度)
视觉意义:延迟血条代表"潜在伤害",增强战斗紧张感
4. 更新状态检测
csharp if (_do_update) { if (hp_top.fillAmount == hp && hp_middle.fillAmount == hp) { _do_update = false; // 停止更新 } }
完成条件:当两个血条都达到目标值
hp
自动停止:重置
_do_update
避免持续计算性能优化:确保动画完成后不再消耗资源
使用场景:玩家从满血(1.0)受到伤害到半血(0.5)
初始化状态:
hp_top.fillAmount = 1.0 hp_middle.fillAmount = 1.0 hp = 0.5(新血量) _do_update = true(由UpdateHP()设置)
第一帧处理:
csharp // hp_top(1.0) > hp(0.5) → 需要减少 SetFillAmount(hp_top, 0.5, middle_speed * 5); // 5倍速下降 // hp_middle(1.0) > hp(0.5) → 需要减少 SetFillAmount(hp_middle, 0.5, middle_speed); // 正常速度下降
视觉效果:
红色血条(
hp_top
)快速下降到0.5(middle_speed * 5)白色血条(
hp_middle
)开始缓慢下降(middle_speed)
3、后续帧处理:
假设middle_speed = 1,每帧下降0.1 第2帧:hp_top=0.5,hp_middle=0.9 第3帧:hp_top=0.5,hp_middle=0.8 ... 第6帧:hp_middle=0.5
完成检测:
csharp if (hp_top.fillAmount == hp && hp_middle.fillAmount == hp) { _do_update = false; // 第6帧满足条件,停止更新 }
最终效果:
红色血条立即显示当前血量(0.5)
白色血条缓慢跟进,形成"伤害残留"效果
SetFillAmount方法
public bool SetFillAmount(Image image, float v, float speed)
{
if (image.fillAmount > v)
{
// 当前值 > 目标值 → 递减
float temp = image.fillAmount - GameTime.deltaTime * speed;
if (temp < v)
{
temp = v;
}
image.fillAmount = temp; // ✅ 修正:使用 temp
return false; // 还没到目标值
}
else if (image.fillAmount < v)
{
// 当前值 < 目标值 → 递增
float temp = image.fillAmount + GameTime.deltaTime * speed;
if (temp > v)
{
temp = v;
}
image.fillAmount = temp; // ✅ 修正:使用 temp
return false;
}
// 已经等于目标值
return true;
}
这段代码
SetFillAmount()
是一个血条填充动画的实现方法:设计意图(理想功能)
平滑过渡:使血条图像(
Image
)从当前填充值逐渐变化到目标值(v
)速度控制:通过
speed
参数控制变化速度状态返回:返回布尔值表示是否已完成动画
修正后效果举例(1.0→0.5,速度=2,帧时间=0.1s):
如果游戏运行在60帧每秒(FPS)的情况下,每帧的时间大约是0.0167秒(1/60≈0.0167)。在此使用0.1秒是为了计算方便
帧数 计算值 实际显示 完成状态 1 1.0 - 0.2 = 0.8 0.8 false 2 0.8 - 0.2 = 0.6 0.6 false 3 0.6 - 0.2 = 0.4 < 0.5 → 0.5 0.5 true
更多推荐
所有评论(0)