Files
bansonic_beta_main/Assets/scripts/gamePlay_gameplay/gfxController.cs
T

1083 lines
42 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using UnityEngine;
using UnityEngine.UI;
using System;
using System.Collections;
using SmoothShakeFree;
using Random = UnityEngine.Random;
/// <summary>
/// 控制游戏过程中的打击特效及其他图形效果
/// </summary>
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);
}
}
/// <summary>
/// 在指定目标位置播放受击特效
/// </summary>
/// <param name="target">受击者物体</param>
/// <param name="parent">父物体(特效将生成在该物体下)</param>
/// <param name="isEnemy">是否为敌人受击</param>
/// <param name="customWorldPos">可选:自定义世界坐标播放(如匹配弹道终点)</param>
/// <param name="scaleOverride">可选:强制指定特效缩放倍率</param>
/// <param name="damageInfo">可选:传入伤害信息以判断是否触发爆炸特效 (damage, maxHealth)</param>
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<AllyCombatant>();
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<AllyCombatant>();
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<RectTransform>();
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<RectTransform>().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<ParticleSystem>();
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);
}
}
/// <summary>
/// 从发射源向目标发射一个拖尾特效
/// </summary>
/// <param name="source">发射源逻辑物体</param>
/// <param name="target">目标逻辑物体</param>
/// <param name="onComplete">到达终点后的回调(可选,返回弹道终点的世界坐标)</param>
public void PlayProjectile(GameObject source, GameObject target, Action<Vector3> 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));
}
/// <summary>
/// 触发友军技能弹道:从施法者飞向目标友军
/// </summary>
public void PlayAllySkillProjectile(GameObject source, GameObject target, Action<Vector3> 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<AllyCombatant>();
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<AllyCombatant>();
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));
}
/// <summary>
/// 将逻辑战斗物体(Ally/Enemy)映射到 GfxController 中定义的 UI 挂载点
/// </summary>
private GameObject ResolveGfxMountPoint(GameObject logicObj)
{
if (logicObj == null) return null;
// 检查是否是敌人
if (logicObj.name == "thisEnemy" || logicObj.GetComponent<EnemyCombatant>() != null)
{
return enemyObject;
}
// 检查是否是盟友 (ally_01 到 ally_05)
var ally = logicObj.GetComponent<AllyCombatant>();
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<Vector3> 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<ParticleSystem>();
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<Vector3> 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<ParticleSystem>();
if (ps.Length > 0)
{
foreach (var p in ps) p.Stop(true, ParticleSystemStopBehavior.StopEmitting);
Destroy(projectile, 1.0f); // 给一点时间让残余粒子消失
}
else
{
Destroy(projectile);
}
}
}
/// <summary>
/// 为特效物体及其子物体中的 TrailRenderer 和 ParticleSystem 应用颜色渐变
/// </summary>
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<TrailRenderer>(true);
foreach (var trail in trails)
{
trail.colorGradient = gradient;
}
// 应用于 ParticleSystem (主要影响起始颜色)
var particles = effectObj.GetComponentsInChildren<ParticleSystem>(true);
foreach (var ps in particles)
{
var main = ps.main;
main.startColor = new ParticleSystem.MinMaxGradient(gradient);
}
}
/// <summary>
/// 为特效物体及其子物体中的渲染器应用透明度和层级
/// </summary>
private void ApplyTransparencyAndRender(GameObject fx, float transparency, int sortingOrder)
{
// 1. 处理所有普通渲染器 (MeshRenderer, SpriteRenderer 等)
Renderer[] renderers = fx.GetComponentsInChildren<Renderer>();
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<ParticleSystem>();
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<TrailRenderer>();
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<RectTransform>();
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<RectTransform>();
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<Renderer>(true);
foreach (var r in renderers)
{
r.sortingOrder = koFxSortingOrder;
}
Canvas[] canvases = fx.GetComponentsInChildren<Canvas>(true);
foreach (var c in canvases)
{
c.sortingOrder = koFxSortingOrder;
}
// 动画播放完后销毁
Destroy(fx, 3.0f);
}
/// <summary>
/// 当 Miss 发生时,从敌人向指定角色发射弹道,并在到达时触发受击效果及实际扣血
/// </summary>
/// <returns>是否成功启动弹道逻辑。如果返回 false,调用方应执行保底扣血。</returns>
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<ParticleSystem>();
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<AllyCombatant>();
if (ally != null) ally.ReceiveDamage(damageAmount, null);
}
}
}
/// <summary>
/// 根据槽位索引获取盟友特效挂载点
/// </summary>
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;
}
}
/// <summary>
/// 获取盟友/敌人的移动父物体(用于震动)
/// </summary>
public GameObject GetMoveObject(GameObject targetObj)
{
if (targetObj == null) return null;
// 1. 检查是否是敌人(逻辑物体或受击挂载点)
if (targetObj.name == "thisEnemy" ||
targetObj == enemyObject ||
targetObj.GetComponent<EnemyCombatant>() != null)
{
return enemyMoveObject;
}
// 2. 检查是否是盟友逻辑物体
var ally = targetObj.GetComponent<AllyCombatant>();
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;
}
}
/// <summary>
/// 触发指定目标的震动效果
/// </summary>
private void TriggerShake(GameObject target, bool isEnemy)
{
GameObject moveObj = GetMoveObject(target);
if (moveObj == null) return;
SmoothShake ss = moveObj.GetComponent<SmoothShake>();
if (ss == null) ss = moveObj.AddComponent<SmoothShake>();
// 修复:确保 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();
}
}