using UnityEngine; using UnityEngine.UI; using System; using System.Collections; using SmoothShakeFree; using Random = UnityEngine.Random; /// /// 控制游戏过程中的打击特效及其他图形效果 /// public class GfxController : MonoBehaviour { public static GfxController Instance { get; private set; } // 复用的属性块:命中特效透明度覆写用,避免每次命中访问 r.materials 克隆材质。 private MaterialPropertyBlock _fxPropertyBlock; [Header("Combatant Objects")] public GameObject enemyMoveObject; public GameObject ally01MoveObject; public GameObject ally02MoveObject; public GameObject ally03MoveObject; public GameObject ally04MoveObject; public GameObject ally05MoveObject; [Header("Hit Effects - Enemy")] public GameObject enemyObject; [Header("Hit Effects - Allies")] public GameObject ally01Object; public GameObject ally02Object; public GameObject ally03Object; public GameObject ally04Object; public GameObject ally05Object; [Header("Prefabs")] public GameObject hitFX; public GameObject projectileFX; public GameObject explosionFX; public GameObject k_o_fx; public GameObject koFatherObject; public float koFxScale = 1f; public int koFxSortingOrder = 31; [Header("Explosion Effect Settings")] [Tooltip("当单次伤害超过敌人最大生命值的百分比时触发爆炸 (0.2 = 20%)")] public float explosionDamageThresholdRatio = 0.2f; [Tooltip("爆炸特效是否使用独立的缩放设置 (否则跟随Hit特效缩放)")] public bool useExplosionSeparateScale = false; public float explosionFxScale = 1.5f; [Header("Hit Effect Settings - Enemy")] public float enemyOffsetX = 0f; public float enemyOffsetY = 0f; public float enemyFxScale = 1f; public bool useEnemyRandomScale = false; public float enemyMinFxScale = 0.8f; public float enemyMaxFxScale = 1.2f; [Range(0f, 1f)] public float enemyFxTransparency = 1f; public int enemyFxSortingOrder = 10; [Header("Hit Effect Settings - Ally")] public float allyOffsetX = 0f; public float allyOffsetY = 0f; public float allyFxScale = 1f; public bool useAllyRandomScale = false; public float allyMinFxScale = 0.8f; public float allyMaxFxScale = 1.2f; [Range(0f, 1f)] public float allyFxTransparency = 1f; public int allyFxSortingOrder = 10; [Header("Ally Attack Projectile")] public float projectileDuration = 0.5f; public AnimationCurve projectileSpeedCurve = AnimationCurve.Linear(0, 0, 1, 1); [Range(0f, 1f)] public float projectileTransparency = 1f; public float projectileScale = 1f; [Tooltip("弹道颜色控制 (影响 Trail 和 Particle)")] public Gradient projectileColor; [Tooltip("弹道到达终点时的随机偏移范围 - X轴")] public float projectileRandomOffsetX = 0f; [Tooltip("弹道到达终点时的随机偏移范围 - Y轴")] public float projectileRandomOffsetY = 0f; [Tooltip("弹道到达终点时的随机偏移范围 - Z轴")] public float projectileRandomOffsetZ = 0f; public int projectileSortingOrder = 10; [Header("敌人攻击控件")] [Tooltip("Miss时敌人攻击角色的弹道特效")] public GameObject enemyMissProjectilePrefab; [Tooltip("Miss弹道颜色控制")] public Gradient enemyAttackProjectileColor; [Tooltip("Miss弹道移动时间")] public float enemyAttackProjectileDuration = 0.4f; [Tooltip("Miss弹道缩放")] public float enemyAttackProjectileScale = 1.0f; [Tooltip("Miss弹道层级")] public int enemyAttackProjectileSortingOrder = 15; [Tooltip("Miss弹道到达时的Hit特效缩放")] public float enemyAttackHitFxScale = 1.0f; [Tooltip("Miss弹道速度曲线")] public AnimationCurve enemyAttackSpeedCurve = AnimationCurve.Linear(0, 0, 1, 1); [Header("Ally Skill Projectile")] [Tooltip("友军技能释放的弹道特效")] public GameObject allySkillProjectilePrefab; [Tooltip("友军技能弹道颜色控制")] public Gradient allySkillProjectileColor; [Tooltip("友军技能弹道移动时间")] public float allySkillProjectileDuration = 0.5f; [Tooltip("友军技能弹道缩放")] public float allySkillProjectileScale = 1.0f; [Tooltip("友军技能弹道层级")] public int allySkillProjectileSortingOrder = 15; [Tooltip("友军技能弹道速度曲线")] public AnimationCurve allySkillSpeedCurve = AnimationCurve.Linear(0, 0, 1, 1); [Tooltip("友军技能弹道到达时的随机偏移范围 - X轴")] public float allySkillRandomOffsetX = 0f; [Tooltip("友军技能弹道到达时的随机偏移范围 - Y轴")] public float allySkillRandomOffsetY = 0f; [Tooltip("友军技能弹道到达时的随机偏移范围 - Z轴")] public float allySkillRandomOffsetZ = 0f; [Header("Enemy Hurt Shake Settings")] public Vector3 hitShakeAmplitude = new Vector3(10f, 10f, 0f); public Vector3 hitShakeFrequency = new Vector3(20f, 20f, 0f); public float hitShakeDuration = 0.2f; [Header("Ally Hurt Shake Settings")] public Vector3 allyHitShakeAmplitude = new Vector3(8f, 8f, 0f); public Vector3 allyHitShakeFrequency = new Vector3(25f, 25f, 0f); public float allyHitShakeDuration = 0.15f; private void Awake() { if (Instance == null) { Instance = this; } else { Destroy(gameObject); } } /// /// 在指定目标位置播放受击特效 /// /// 受击者物体 /// 父物体(特效将生成在该物体下) /// 是否为敌人受击 /// 可选:自定义世界坐标播放(如匹配弹道终点) /// 可选:强制指定特效缩放倍率 /// 可选:传入伤害信息以判断是否触发爆炸特效 (damage, maxHealth) public void PlayHitFX(GameObject target, GameObject parent = null, bool isEnemy = false, Vector3? customWorldPos = null, float? scaleOverride = null, (float damage, float maxHealth)? damageInfo = null) { // --- Check if the target ally slot is active before playing VFX --- if (!isEnemy && teamUIController.Instance != null) { int slot = -1; var ally = target.GetComponent(); if (ally != null) slot = ally.slotIndex; else { string n = target.name.ToLower(); if (n.Contains("ally_01")) slot = 0; else if (n.Contains("ally_02")) slot = 1; else if (n.Contains("ally_03")) slot = 2; else if (n.Contains("ally_04")) slot = 3; else if (n.Contains("ally_05")) slot = 4; } if (slot != -1) { bool isActive = false; switch (slot) { case 0: isActive = teamUIController.Instance.isAlly01_active; break; case 1: isActive = teamUIController.Instance.isAlly02_active; break; case 2: isActive = teamUIController.Instance.isAlly03_active; break; case 3: isActive = teamUIController.Instance.isAlly04_active; break; case 4: isActive = teamUIController.Instance.isAlly05_active; break; } if (!isActive) return; // Skip VFX for inactive ally } } // Determine which FX prefab to use (Hit or Explosion) GameObject fxPrefab = hitFX; bool isExplosion = false; if (isEnemy && damageInfo.HasValue && explosionFX != null) { float ratio = damageInfo.Value.damage / damageInfo.Value.maxHealth; if (ratio >= explosionDamageThresholdRatio) { fxPrefab = explosionFX; isExplosion = true; } } if (target == null || fxPrefab == null) { if (fxPrefab == null) Debug.LogWarning($"[GfxController] {(isExplosion ? "ExplosionFX" : "HitFX")} Prefab 未在 Inspector 中分配!"); return; } // 触发受击震动效果 TriggerShake(target, isEnemy); GameObject visualParent = null; if (isEnemy && enemyObject != null) { visualParent = enemyObject; } else if (!isEnemy) { visualParent = ResolveGfxMountPoint(target); } if (visualParent == null) { visualParent = parent != null ? parent : ResolveGfxMountPoint(target); } // 如果是敌人,触发闪红效果 (使用 teamUIController 已有的逻辑) if (isEnemy && teamUIController.Instance != null) { teamUIController.Instance.TriggerEnemyHurtFlash(); } else if (!isEnemy && teamUIController.Instance != null) { // 尝试从物体名称或组件中识别槽位并触发闪红 int slot = -1; var ally = target.GetComponent(); if (ally != null) slot = ally.slotIndex; else { string n = target.name.ToLower(); if (n.Contains("ally_01")) slot = 0; else if (n.Contains("ally_02")) slot = 1; else if (n.Contains("ally_03")) slot = 2; else if (n.Contains("ally_04")) slot = 3; else if (n.Contains("ally_05")) slot = 4; } if (slot != -1) { teamUIController.Instance.TriggerAllyHurtFlash(slot); } } // 根据身份选择参数 float offsetX = isEnemy ? enemyOffsetX : allyOffsetX; float offsetY = isEnemy ? enemyOffsetY : allyOffsetY; // 计算缩放逻辑 float fxScale; if (scaleOverride.HasValue) { fxScale = scaleOverride.Value; } else if (isEnemy) { if (isExplosion && useExplosionSeparateScale) { fxScale = explosionFxScale; } else { fxScale = useEnemyRandomScale ? Random.Range(enemyMinFxScale, enemyMaxFxScale) : enemyFxScale; } } else { fxScale = useAllyRandomScale ? Random.Range(allyMinFxScale, allyMaxFxScale) : allyFxScale; } float transparency = isEnemy ? enemyFxTransparency : allyFxTransparency; GameObject fx; Vector3 offsetVector = new Vector3(offsetX, offsetY, 0f); if (visualParent != null) { // 将缩放应用到挂载点物体(visualParent)本身 visualParent.transform.localScale = Vector3.one * fxScale; // 作为子物体生成 fx = Instantiate(fxPrefab, visualParent.transform); // 确保特效在 UI 层级中显示在最前方 fx.transform.SetAsLastSibling(); // 检查是否是 UI 元素 (RectTransform) RectTransform rt = fx.GetComponent(); if (rt != null) { if (customWorldPos.HasValue) { // 如果有自定义世界坐标,将其转换为本地 UI 坐标 rt.position = customWorldPos.Value; } else { rt.anchoredPosition = new Vector2(offsetX, offsetY); } rt.localRotation = Quaternion.identity; // 确保 UI 特效的原始大小正确,且 localScale 为 1(继承 parent 的缩放) rt.sizeDelta = fxPrefab.GetComponent().sizeDelta; rt.localScale = Vector3.one; } else { if (customWorldPos.HasValue) { fx.transform.position = customWorldPos.Value; } else { fx.transform.localPosition = offsetVector; } fx.transform.localRotation = Quaternion.identity; fx.transform.localScale = Vector3.one; } } else { Vector3 spawnPos = customWorldPos.HasValue ? customWorldPos.Value : (target.transform.position + offsetVector); fx = Instantiate(fxPrefab, spawnPos, Quaternion.identity); fx.transform.localScale = Vector3.one * fxScale; } // 针对粒子系统的特殊处理 ParticleSystem[] allParticles = fx.GetComponentsInChildren(); foreach (var ps in allParticles) { var main = ps.main; // 强制修复缩放:设置 Scaling Mode 为 Hierarchy,使粒子大小跟随 Transform 缩放 main.scalingMode = ParticleSystemScalingMode.Hierarchy; } // 应用透明度并处理渲染 ApplyTransparencyAndRender(fx, transparency, isEnemy ? enemyFxSortingOrder : allyFxSortingOrder); if (allParticles.Length > 0) { foreach (var ps in allParticles) { // 确保粒子在生成时立即播放 ps.Play(true); // 设置停止动作,如果是根节点粒子,设置自动销毁 if (ps.gameObject == fx) { var m = ps.main; m.stopAction = ParticleSystemStopAction.Destroy; } } } else { // 如果不是粒子系统(例如动画 Prefab),在固定时间后销毁(默认 2 秒) Destroy(fx, 2.0f); } } /// /// 从发射源向目标发射一个拖尾特效 /// /// 发射源逻辑物体 /// 目标逻辑物体 /// 到达终点后的回调(可选,返回弹道终点的世界坐标) public void PlayProjectile(GameObject source, GameObject target, Action onComplete = null) { if (source == null || target == null || projectileFX == null) { if (projectileFX == null) Debug.LogWarning("[GfxController] projectileFX Prefab 未分配!"); onComplete?.Invoke(target != null ? target.transform.position : Vector3.zero); return; } // --- 核心修正:将逻辑物体映射到 UI 挂载点 --- GameObject visualSource = ResolveGfxMountPoint(source); GameObject visualTarget = ResolveGfxMountPoint(target); // 如果映射失败,则退而求其次使用原始物体的 Transform Transform startTransform = visualSource != null ? visualSource.transform : source.transform; Transform endTransform = visualTarget != null ? visualTarget.transform : target.transform; // 在源物体位置实例化 GameObject projectile = Instantiate(projectileFX, startTransform.position, Quaternion.identity); // --- 核心修正:挂载到 GfxController (即当前脚本所在的 Canvas 层级物体) --- projectile.transform.SetParent(this.transform, true); projectile.transform.localScale = Vector3.one * projectileScale; // 应用透明度 ApplyTransparencyAndRender(projectile, projectileTransparency, projectileSortingOrder); // 应用颜色 ApplyColorToEffect(projectile, projectileColor); // 启动移动协程 Vector3 targetRandomOffset = Vector3.zero; if (projectileRandomOffsetX > 0f || projectileRandomOffsetY > 0f || projectileRandomOffsetZ > 0f) { targetRandomOffset = new Vector3( Random.Range(-projectileRandomOffsetX, projectileRandomOffsetX), Random.Range(-projectileRandomOffsetY, projectileRandomOffsetY), Random.Range(-projectileRandomOffsetZ, projectileRandomOffsetZ) ); } StartCoroutine(MoveProjectileCoroutine(projectile, startTransform, endTransform, targetRandomOffset, onComplete)); } /// /// 触发友军技能弹道:从施法者飞向目标友军 /// public void PlayAllySkillProjectile(GameObject source, GameObject target, Action onComplete = null) { if (source == null || target == null) return; // --- Check if the source or target ally slot is active before playing VFX --- if (teamUIController.Instance != null) { // Check source var sourceAlly = source.GetComponent(); if (sourceAlly != null) { bool isActive = false; switch (sourceAlly.slotIndex) { case 0: isActive = teamUIController.Instance.isAlly01_active; break; case 1: isActive = teamUIController.Instance.isAlly02_active; break; case 2: isActive = teamUIController.Instance.isAlly03_active; break; case 3: isActive = teamUIController.Instance.isAlly04_active; break; case 4: isActive = teamUIController.Instance.isAlly05_active; break; } if (!isActive) { onComplete?.Invoke(target.transform.position); return; } } // Check target var targetAlly = target.GetComponent(); if (targetAlly != null) { bool isActive = false; switch (targetAlly.slotIndex) { case 0: isActive = teamUIController.Instance.isAlly01_active; break; case 1: isActive = teamUIController.Instance.isAlly02_active; break; case 2: isActive = teamUIController.Instance.isAlly03_active; break; case 3: isActive = teamUIController.Instance.isAlly04_active; break; case 4: isActive = teamUIController.Instance.isAlly05_active; break; } if (!isActive) { onComplete?.Invoke(target.transform.position); return; } } } GameObject prefab = allySkillProjectilePrefab != null ? allySkillProjectilePrefab : projectileFX; if (prefab == null) { Debug.LogWarning("[GfxController] allySkillProjectilePrefab 未分配且无默认 projectileFX!"); onComplete?.Invoke(target.transform.position); return; } // 核心修正:将逻辑物体映射到 UI 挂载点 GameObject visualSource = ResolveGfxMountPoint(source); GameObject visualTarget = ResolveGfxMountPoint(target); Transform startTransform = visualSource != null ? visualSource.transform : source.transform; Transform endTransform = visualTarget != null ? visualTarget.transform : target.transform; GameObject projectile = Instantiate(prefab, startTransform.position, Quaternion.identity); projectile.transform.SetParent(this.transform, true); projectile.transform.localScale = Vector3.one * allySkillProjectileScale; // 应用层级(复用 projectileTransparency) ApplyTransparencyAndRender(projectile, projectileTransparency, allySkillProjectileSortingOrder); // 应用颜色 ApplyColorToEffect(projectile, allySkillProjectileColor); // 计算随机偏移 Vector3 targetRandomOffset = Vector3.zero; if (allySkillRandomOffsetX > 0f || allySkillRandomOffsetY > 0f || allySkillRandomOffsetZ > 0f) { targetRandomOffset = new Vector3( UnityEngine.Random.Range(-allySkillRandomOffsetX, allySkillRandomOffsetX), UnityEngine.Random.Range(-allySkillRandomOffsetY, allySkillRandomOffsetY), UnityEngine.Random.Range(-allySkillRandomOffsetZ, allySkillRandomOffsetZ) ); } StartCoroutine(MoveAllySkillProjectileCoroutine(projectile, startTransform, endTransform, targetRandomOffset, onComplete)); } /// /// 将逻辑战斗物体(Ally/Enemy)映射到 GfxController 中定义的 UI 挂载点 /// private GameObject ResolveGfxMountPoint(GameObject logicObj) { if (logicObj == null) return null; // 检查是否是敌人 if (logicObj.name == "thisEnemy" || logicObj.GetComponent() != null) { return enemyObject; } // 检查是否是盟友 (ally_01 到 ally_05) var ally = logicObj.GetComponent(); if (ally != null) { return GetAllyObject(ally.slotIndex); } // 如果名字符合模式也可以识别 string name = logicObj.name.ToLower(); if (name.Contains("ally_01")) return ally01Object; if (name.Contains("ally_02")) return ally02Object; if (name.Contains("ally_03")) return ally03Object; if (name.Contains("ally_04")) return ally04Object; if (name.Contains("ally_05")) return ally05Object; return null; } private IEnumerator MoveProjectileCoroutine(GameObject projectile, Transform start, Transform end, Vector3 targetOffset, Action onComplete) { float elapsed = 0f; Vector3 startPos = start.position; Vector3 lastEndPos = startPos; while (elapsed < projectileDuration) { if (projectile == null) { onComplete?.Invoke(lastEndPos); yield break; } elapsed += Time.deltaTime; float normalizedTime = Mathf.Clamp01(elapsed / projectileDuration); // 使用动画曲线计算插值比例 float t = projectileSpeedCurve.Evaluate(normalizedTime); // 如果目标还在,更新目标位置(支持动态移动的目标) Vector3 currentEndPos = end != null ? end.position + targetOffset : projectile.transform.position; lastEndPos = currentEndPos; projectile.transform.position = Vector3.Lerp(startPos, currentEndPos, t); // 可选:让拖尾朝向移动方向 if (t > 0) { Vector3 direction = currentEndPos - startPos; if (direction != Vector3.zero) { projectile.transform.rotation = Quaternion.LookRotation(Vector3.forward, direction); } } yield return null; } // 到达终点,触发回调并传回终点坐标 onComplete?.Invoke(lastEndPos); // 到达后销毁 if (projectile != null) { // 如果有粒子系统,先停止发射让残余拖尾自然消失,或者直接销毁 ParticleSystem[] ps = projectile.GetComponentsInChildren(); if (ps.Length > 0) { foreach (var p in ps) p.Stop(true, ParticleSystemStopBehavior.StopEmitting); Destroy(projectile, 1.0f); // 给一点时间让残余粒子消失 } else { Destroy(projectile); } } } private IEnumerator MoveAllySkillProjectileCoroutine(GameObject projectile, Transform start, Transform end, Vector3 targetOffset, Action onComplete) { float elapsed = 0f; Vector3 startPos = start.position; Vector3 lastEndPos = startPos; while (elapsed < allySkillProjectileDuration) { if (projectile == null) { onComplete?.Invoke(lastEndPos); yield break; } elapsed += Time.deltaTime; float normalizedTime = Mathf.Clamp01(elapsed / allySkillProjectileDuration); // 使用动画曲线计算插值比例 float t = allySkillSpeedCurve.Evaluate(normalizedTime); // 如果目标还在,更新目标位置(支持动态移动的目标) Vector3 currentEndPos = end != null ? end.position + targetOffset : projectile.transform.position; lastEndPos = currentEndPos; projectile.transform.position = Vector3.Lerp(startPos, currentEndPos, t); // 可选:让拖尾朝向移动方向 if (t > 0) { Vector3 direction = currentEndPos - startPos; if (direction != Vector3.zero) { projectile.transform.rotation = Quaternion.LookRotation(Vector3.forward, direction); } } yield return null; } // 到达终点,触发回调并传回终点坐标 onComplete?.Invoke(lastEndPos); // 到达后销毁 if (projectile != null) { ParticleSystem[] ps = projectile.GetComponentsInChildren(); if (ps.Length > 0) { foreach (var p in ps) p.Stop(true, ParticleSystemStopBehavior.StopEmitting); Destroy(projectile, 1.0f); // 给一点时间让残余粒子消失 } else { Destroy(projectile); } } } /// /// 为特效物体及其子物体中的 TrailRenderer 和 ParticleSystem 应用颜色渐变 /// private void ApplyColorToEffect(GameObject effectObj, Gradient gradient) { if (effectObj == null || gradient == null) return; // 检查 Gradient 是否有效(至少有一个 alpha key 或 color key 且不全是透明) // 如果用户没有设置颜色,则保持原样 if (gradient.colorKeys.Length <= 1 && gradient.alphaKeys.Length <= 1 && gradient.colorKeys[0].color == Color.white && gradient.alphaKeys[0].alpha == 0) { // 简单的空检查,如果 Gradient 看起来是默认未设置状态则跳过 // 注意:Unity 默认 Gradient 通常是全白不透明,这里可以根据需要调整判断 } // 应用于 TrailRenderer var trails = effectObj.GetComponentsInChildren(true); foreach (var trail in trails) { trail.colorGradient = gradient; } // 应用于 ParticleSystem (主要影响起始颜色) var particles = effectObj.GetComponentsInChildren(true); foreach (var ps in particles) { var main = ps.main; main.startColor = new ParticleSystem.MinMaxGradient(gradient); } } /// /// 为特效物体及其子物体中的渲染器应用透明度和层级 /// private void ApplyTransparencyAndRender(GameObject fx, float transparency, int sortingOrder) { // 1. 处理所有普通渲染器 (MeshRenderer, SpriteRenderer 等) Renderer[] renderers = fx.GetComponentsInChildren(); foreach (var r in renderers) { // 设置排序层级 r.sortingOrder = sortingOrder; // 处理材质透明度:用 MaterialPropertyBlock 覆写 alpha,避免访问 r.materials // 触发材质实例化(每次命中都会克隆并泄漏材质,产生 GC)。视觉结果等价。 if (transparency < 1f) { var sharedMat = r.sharedMaterial; if (sharedMat != null) { if (_fxPropertyBlock == null) _fxPropertyBlock = new MaterialPropertyBlock(); r.GetPropertyBlock(_fxPropertyBlock); if (sharedMat.HasProperty("_Color")) { Color c = sharedMat.color; c.a *= transparency; _fxPropertyBlock.SetColor("_Color", c); } else if (sharedMat.HasProperty("_BaseColor")) // URP 命名 { Color c = sharedMat.GetColor("_BaseColor"); c.a *= transparency; _fxPropertyBlock.SetColor("_BaseColor", c); } r.SetPropertyBlock(_fxPropertyBlock); } } } // 2. 处理粒子系统颜色 (考虑不同模式) ParticleSystem[] particles = fx.GetComponentsInChildren(); foreach (var ps in particles) { var main = ps.main; // 处理 Start Color var startColor = main.startColor; if (startColor.mode == ParticleSystemGradientMode.Color) { Color c = startColor.color; c.a *= transparency; main.startColor = c; } else if (startColor.mode == ParticleSystemGradientMode.TwoColors) { Color c1 = startColor.colorMin; Color c2 = startColor.colorMax; c1.a *= transparency; c2.a *= transparency; main.startColor = new ParticleSystem.MinMaxGradient(c1, c2); } } // 3. 处理 TrailRenderer (拖尾特效的核心组件) TrailRenderer[] trails = fx.GetComponentsInChildren(); foreach (var trail in trails) { if (transparency < 1f) { Gradient gradient = trail.colorGradient; GradientAlphaKey[] alphaKeys = gradient.alphaKeys; for (int i = 0; i < alphaKeys.Length; i++) { alphaKeys[i].alpha *= transparency; } gradient.SetKeys(gradient.colorKeys, alphaKeys); trail.colorGradient = gradient; } } } public void PlayKOFX(GameObject target) { if (target == null || k_o_fx == null) { if (k_o_fx == null) Debug.LogWarning("[GfxController] k_o_fx Prefab 未分配!"); return; } GameObject fx; // 优先使用指定的 KO 父物体 if (koFatherObject != null) { // 控制父物体的缩放 koFatherObject.transform.localScale = Vector3.one * koFxScale; fx = Instantiate(k_o_fx, koFatherObject.transform); RectTransform rt = fx.GetComponent(); if (rt != null) { rt.anchoredPosition = Vector2.zero; rt.localRotation = Quaternion.identity; rt.localScale = Vector3.one; // 预设物体缩放回归 1,由父物体控制 } else { fx.transform.localPosition = Vector3.zero; fx.transform.localRotation = Quaternion.identity; fx.transform.localScale = Vector3.one; } } else { // 备份逻辑:映射到 UI 挂载点 GameObject visualParent = ResolveGfxMountPoint(target); if (visualParent != null) { fx = Instantiate(k_o_fx, visualParent.transform); fx.transform.SetAsLastSibling(); RectTransform rt = fx.GetComponent(); if (rt != null) { rt.anchoredPosition = Vector2.zero; rt.localRotation = Quaternion.identity; rt.localScale = Vector3.one * koFxScale; } else { fx.transform.localPosition = Vector3.zero; fx.transform.localRotation = Quaternion.identity; fx.transform.localScale = Vector3.one * koFxScale; } } else { fx = Instantiate(k_o_fx, target.transform.position, Quaternion.identity); fx.transform.localScale = Vector3.one * koFxScale; } } // 应用层级 Order In Layer Renderer[] renderers = fx.GetComponentsInChildren(true); foreach (var r in renderers) { r.sortingOrder = koFxSortingOrder; } Canvas[] canvases = fx.GetComponentsInChildren(true); foreach (var c in canvases) { c.sortingOrder = koFxSortingOrder; } // 动画播放完后销毁 Destroy(fx, 3.0f); } /// /// 当 Miss 发生时,从敌人向指定角色发射弹道,并在到达时触发受击效果及实际扣血 /// /// 是否成功启动弹道逻辑。如果返回 false,调用方应执行保底扣血。 public bool PlayEnemyAttackOnMiss(int allySlotIndex, float damageAmount, AllyCombatant allyInstance) { if (enemyObject == null || !enemyObject.activeInHierarchy) { Debug.LogWarning("[GfxController] enemyObject 为空或未激活,无法播放 Miss 攻击特效。"); return false; } // --- Check if the target ally slot is active before playing VFX --- if (teamUIController.Instance != null) { bool isActive = false; switch (allySlotIndex) { case 0: isActive = teamUIController.Instance.isAlly01_active; break; case 1: isActive = teamUIController.Instance.isAlly02_active; break; case 2: isActive = teamUIController.Instance.isAlly03_active; break; case 3: isActive = teamUIController.Instance.isAlly04_active; break; case 4: isActive = teamUIController.Instance.isAlly05_active; break; } if (!isActive) { // If inactive, skip VFX and return false so SkillBuilder applies immediate (but blocked) damage return false; } } GameObject targetAllyObj = GetAllyObject(allySlotIndex); if (targetAllyObj == null) { Debug.LogWarning($"[GfxController] 找不到槽位 {allySlotIndex} 的盟友物体。"); return false; } GameObject projectilePrefab = enemyMissProjectilePrefab != null ? enemyMissProjectilePrefab : projectileFX; if (projectilePrefab == null) { Debug.LogWarning("[GfxController] 未分配任何弹道 Prefab。"); return false; } // 生成弹道 GameObject projectile = Instantiate(projectilePrefab, enemyObject.transform.position, Quaternion.identity); projectile.transform.SetParent(enemyObject.transform, true); projectile.transform.localScale = Vector3.one * enemyAttackProjectileScale; // 应用层级 ApplyTransparencyAndRender(projectile, projectileTransparency, enemyAttackProjectileSortingOrder); // 应用颜色 ApplyColorToEffect(projectile, enemyAttackProjectileColor); // 启动移动协程 StartCoroutine(MoveEnemyAttackProjectileCoroutine(projectile, enemyObject.transform, targetAllyObj.transform, allySlotIndex, damageAmount, allyInstance)); return true; } private IEnumerator MoveEnemyAttackProjectileCoroutine(GameObject projectile, Transform start, Transform end, int allySlotIndex, float damageAmount, AllyCombatant allyInstance) { float elapsed = 0f; Vector3 startPos = start.position; Vector3 lastEndPos = startPos; while (elapsed < enemyAttackProjectileDuration) { if (projectile == null) yield break; elapsed += Time.deltaTime; float normalizedTime = Mathf.Clamp01(elapsed / enemyAttackProjectileDuration); float t = enemyAttackSpeedCurve.Evaluate(normalizedTime); Vector3 currentEndPos = end != null ? end.position : lastEndPos; lastEndPos = currentEndPos; projectile.transform.position = Vector3.Lerp(startPos, currentEndPos, t); // 朝向目标 Vector3 direction = currentEndPos - projectile.transform.position; if (direction != Vector3.zero) { projectile.transform.rotation = Quaternion.LookRotation(Vector3.forward, direction); } yield return null; } // 到达终点 if (projectile != null) { // 停止粒子系统 ParticleSystem[] ps = projectile.GetComponentsInChildren(); foreach (var p in ps) { var m = p.main; m.loop = false; } Destroy(projectile, 0.5f); } // 播放 Hit 特效、闪红,并执行实际扣血 GameObject targetAllyObj = GetAllyObject(allySlotIndex); if (targetAllyObj != null) { // 1. 播放 Hit FX 并触发闪红(PlayHitFX 内部已处理闪红) PlayHitFX(targetAllyObj, null, false, lastEndPos, enemyAttackHitFxScale); // 2. 执行实际扣血逻辑 (使用传入的实例,更准确) if (allyInstance != null) { allyInstance.ReceiveDamage(damageAmount, null); } else { // 备选方案:尝试从物体获取 var ally = targetAllyObj.GetComponent(); if (ally != null) ally.ReceiveDamage(damageAmount, null); } } } /// /// 根据槽位索引获取盟友特效挂载点 /// public GameObject GetAllyObject(int slotIndex) { switch (slotIndex) { case 0: return ally01Object; case 1: return ally02Object; case 2: return ally03Object; case 3: return ally04Object; case 4: return ally05Object; default: return null; } } /// /// 获取盟友/敌人的移动父物体(用于震动) /// public GameObject GetMoveObject(GameObject targetObj) { if (targetObj == null) return null; // 1. 检查是否是敌人(逻辑物体或受击挂载点) if (targetObj.name == "thisEnemy" || targetObj == enemyObject || targetObj.GetComponent() != null) { return enemyMoveObject; } // 2. 检查是否是盟友逻辑物体 var ally = targetObj.GetComponent(); if (ally != null) { return GetAllyMoveObject(ally.slotIndex); } // 3. 检查是否是盟友受击挂载点 (ally01Object - ally05Object) if (targetObj == ally01Object) return ally01MoveObject; if (targetObj == ally02Object) return ally02MoveObject; if (targetObj == ally03Object) return ally03MoveObject; if (targetObj == ally04Object) return ally04MoveObject; if (targetObj == ally05Object) return ally05MoveObject; // 4. 通过名称模式兜底识别 string n = targetObj.name.ToLower(); if (n.Contains("ally_01") || n.Contains("ally01")) return ally01MoveObject; if (n.Contains("ally_02") || n.Contains("ally02")) return ally02MoveObject; if (n.Contains("ally_03") || n.Contains("ally03")) return ally03MoveObject; if (n.Contains("ally_04") || n.Contains("ally04")) return ally04MoveObject; if (n.Contains("ally_05") || n.Contains("ally05")) return ally05MoveObject; return null; } private GameObject GetAllyMoveObject(int slotIndex) { switch (slotIndex) { case 0: return ally01MoveObject; case 1: return ally02MoveObject; case 2: return ally03MoveObject; case 3: return ally04MoveObject; case 4: return ally05MoveObject; default: return null; } } /// /// 触发指定目标的震动效果 /// private void TriggerShake(GameObject target, bool isEnemy) { GameObject moveObj = GetMoveObject(target); if (moveObj == null) return; SmoothShake ss = moveObj.GetComponent(); if (ss == null) ss = moveObj.AddComponent(); // 修复:确保 Shaker 实例已初始化,避免 NullReferenceException if (ss.positionShake == null) ss.positionShake = new Shaker(); if (ss.rotationShake == null) ss.rotationShake = new Shaker(); // 选择震动参数 Vector3 amplitude = isEnemy ? hitShakeAmplitude : allyHitShakeAmplitude; Vector3 frequency = isEnemy ? hitShakeFrequency : allyHitShakeFrequency; float duration = isEnemy ? hitShakeDuration : allyHitShakeDuration; if (isEnemy && teamUIController.Instance != null) { teamUIController.Instance.TriggerEnemySpineShake(amplitude, frequency, duration); } if (isEnemy) amplitude *= 0.5f; // 配置震动参数 ss.positionShake.noiseType = Shaker.NoiseType.SineWave; ss.positionShake.amplitude = amplitude; ss.positionShake.frequency = frequency; // 旋转震动重置为0,防止干扰 ss.rotationShake.amplitude = Vector3.zero; // 配置时间参数 ss.timeSettings.constantShake = false; ss.timeSettings.fadeInDuration = 0.05f; ss.timeSettings.holdDuration = duration * 0.4f; ss.timeSettings.fadeOutDuration = duration * 0.55f; // 确保动画曲线有值(否则震动无法淡出) if (ss.timeSettings.fadeInCurve == null || ss.timeSettings.fadeInCurve.length == 0) ss.timeSettings.fadeInCurve = AnimationCurve.Linear(0, 0, 1, 1); if (ss.timeSettings.fadeOutCurve == null || ss.timeSettings.fadeOutCurve.length == 0) ss.timeSettings.fadeOutCurve = AnimationCurve.Linear(0, 1, 1, 0); // 初始化内部数组(插件在 AddComponent 时 Awake 已经运行,由于当时字段为空,需要重新同步数组) ss.shakers = new Shaker[] { ss.positionShake, ss.rotationShake }; ss.sum = new Vector3[2]; // 启动震动 ss.StartShake(); } }