1083 lines
42 KiB
C#
1083 lines
42 KiB
C#
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();
|
||
}
|
||
}
|