using Opsive.BehaviorDesigner.Runtime; using Opsive.BehaviorDesigner.Runtime.Tasks; using Opsive.GraphDesigner.Runtime.Variables; using Opsive.Shared.Utility; using UnityEngine; using UnityEngine.AI; namespace Cielonos.MainGame.Characters.AI { [Description("计算跳跃的目标落点坐标,并将计算结果写入行为树的 Vector3 变量中。支持多种跳跃策略(接近、后撤、侧闪、出死角等)。")] [Category("Cielonos/Movement")] public class CalculateJumpTarget : AutomataActionBase { public enum JumpStrategy { [Tooltip("快速接近玩家:直接指向玩家位置(或预测位置)。")] CloseIn, [Tooltip("战术后撤:向自身后方跳跃拉开距离,用于准备释放远程攻击。")] Retreat, [Tooltip("侧向侧袭(Flanking):跳到玩家侧方90度,用于绕过正面防守/激光。")] Flank, [Tooltip("挣脱死角(Escape Corner):当靠近场景边缘/墙壁时,计算指向场地中央的跳跃点。")] EscapeCorner, [Tooltip("避险跳跃(Escape Hazard):远离最近的玩家危险区域/地裂伤害。")] EscapeHazard, [Tooltip("原地原跳(Self Slam):以自身当前位置为中心,通常用于大招落地原地震击。")] SelfSlam } [Header("Strategy Config")] [Tooltip("跳跃落点策略模式")] public JumpStrategy strategy = JumpStrategy.CloseIn; [Tooltip("输出的目标位置变量名称(写入行为树,供 ManualMove 读取)。")] public SharedVariable jumpLandPosition = new SharedVariable { Value = Vector3.zero }; [Header("Distance Parameters")] [Tooltip("后撤跳跃的距离(米)。")] public float retreatDistance = 8f; [Tooltip("接近跳跃时,期望落在距离玩家多少米的位置(用于巨斧攻击范围适配)。")] public float closeInArrivalRadius = 2.0f; [Tooltip("战斗场地/房间的中心点。当从死角需要跃回场地中央时使用。")] public SharedVariable arenaCenter = new SharedVariable { Value = Vector3.zero }; [Tooltip("侧向跳跃相对于玩家的偏置距离(米)。")] public float flankDistance = 5f; [Tooltip("侧向跳跃的方向选择:1 = 玩家右侧,-1 = 玩家左侧,0 = 随机。")] public int flankDirection = 0; [Header("Prediction")] [Tooltip("是否在 CloseIn 模式下启用玩家位置预测?")] public bool usePlayerPrediction = true; [Tooltip("预测玩家未来的提前量时间(秒)。")] public float predictionLeadTime = 1.0f; [Header("NavMesh Validation")] [Tooltip("是否对计算出的落点在 NavMesh 上进行合法性采样?(防止跳出地图边缘/掉落悬崖)")] public bool validateOnNavMesh = true; [Tooltip("NavMesh 采样半径。")] public float navMeshSampleRadius = 2f; public override TaskStatus OnUpdate() { if (self == null || self.player == null) return TaskStatus.Failure; Vector3 computedTarget = self.transform.position; Vector3 playerPos = self.player.transform.position; Vector3 selfPos = self.transform.position; switch (strategy) { case JumpStrategy.CloseIn: Vector3 basePlayerPos = usePlayerPrediction ? self.PredictPlayerPosition(predictionLeadTime) : playerPos; Vector3 toPlayer = basePlayerPos - selfPos; toPlayer.y = 0f; if (toPlayer.magnitude > closeInArrivalRadius) { // 敌人跳到连线与设定半径的交点上 computedTarget = basePlayerPos - toPlayer.normalized * closeInArrivalRadius; } else { computedTarget = basePlayerPos; } break; case JumpStrategy.Retreat: // 计算 Boss 朝向的后方 Vector3 backDir = -self.transform.forward; backDir.y = 0f; backDir.Normalize(); computedTarget = selfPos + backDir * retreatDistance; break; case JumpStrategy.Flank: // 计算从玩家指向 Boss 的方向 Vector3 toBoss = selfPos - playerPos; toBoss.y = 0f; toBoss.Normalize(); // 计算垂直的切线方向 (左右) Vector3 tangent = new Vector3(-toBoss.z, 0f, toBoss.x); // 玩家右侧 int dir = flankDirection; if (dir == 0) { dir = Random.value > 0.5f ? 1 : -1; } computedTarget = playerPos + tangent * (flankDistance * dir); break; case JumpStrategy.EscapeCorner: Vector3 toCenter = arenaCenter.Value - selfPos; toCenter.y = 0f; toCenter.Normalize(); computedTarget = selfPos + toCenter * retreatDistance; break; case JumpStrategy.EscapeHazard: // 默认向远离玩家方向跳跃,作为基础避险 Vector3 awayDir = selfPos - playerPos; awayDir.y = 0f; awayDir.Normalize(); computedTarget = selfPos + awayDir * retreatDistance; break; case JumpStrategy.SelfSlam: computedTarget = selfPos; break; } // 对计算出的落点在 NavMesh 上进行合法性采样,防止跳出地图 if (validateOnNavMesh) { if (NavMesh.SamplePosition(computedTarget, out NavMeshHit hit, navMeshSampleRadius, NavMesh.AllAreas)) { computedTarget = hit.position; } else { // 如果采样失败,尝试向玩家位置收缩 Vector3 dirToPlayer = playerPos - computedTarget; if (NavMesh.SamplePosition(computedTarget + dirToPlayer * 0.5f, out hit, navMeshSampleRadius, NavMesh.AllAreas)) { computedTarget = hit.position; } } } // 写入行为树变量与调试缓存 debugComputedTarget = computedTarget; jumpLandPosition.SetValue(computedTarget); return TaskStatus.Success; } private Vector3 debugComputedTarget; protected override void OnDrawGizmos() { #if UNITY_EDITOR if (debugComputedTarget != Vector3.zero) { Gizmos.color = Color.cyan; Gizmos.DrawWireSphere(debugComputedTarget, 0.4f); Gizmos.color = new Color(0.2f, 0.8f, 1.0f, 0.25f); Gizmos.DrawSphere(debugComputedTarget, 0.4f); if (self != null) { Gizmos.color = Color.cyan; Gizmos.DrawLine(self.transform.position, debugComputedTarget); } } #endif } } }