247 lines
9.0 KiB
C#
247 lines
9.0 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.VFX;
|
|
|
|
namespace AibisDream.FixSystem
|
|
{
|
|
/// <summary>
|
|
/// 模块运行状态的纯表现层:Module Pic 始终显示,文字负责常亮/闪烁/熄灭,
|
|
/// 火花依据 Module Pic 的世界包围盒收束在底边,并渲染在本体下层。
|
|
/// </summary>
|
|
public class ModuleConditionVisual : MonoBehaviour
|
|
{
|
|
private static readonly Vector2[] SparkDirections =
|
|
{
|
|
new(-0.5f, -0.86f),
|
|
new(0f, -0.86f),
|
|
new(0.5f, -0.86f)
|
|
};
|
|
|
|
[Header("模块引用")]
|
|
[SerializeField] private SpriteRenderer modulePicRenderer;
|
|
[SerializeField] private SpriteRenderer moduleTextRenderer;
|
|
|
|
[Header("损坏文字闪烁")]
|
|
[SerializeField, Min(0.01f)] private float flickerMinInterval = 0.06f;
|
|
[SerializeField, Min(0.01f)] private float flickerMaxInterval = 0.18f;
|
|
|
|
[Header("损坏火花")]
|
|
[SerializeField] private VisualEffectAsset sparkAsset;
|
|
[SerializeField, Min(0.01f)] private float sparkMinInterval = 0.3f;
|
|
[SerializeField, Min(0.01f)] private float sparkMaxInterval = 1f;
|
|
[SerializeField, Min(0f)] private float sparkBoundsPadding = 0.01f;
|
|
[SerializeField] private Vector2 sparkBoundsScale = new(0.9f, 0.92f);
|
|
[SerializeField] private Vector2 sparkCenterOffset = Vector2.zero;
|
|
[SerializeField] private Vector2 sparkBurstCount = new(3f, 6f);
|
|
[SerializeField] private Vector2 sparkLifetime = new(0.1f, 0.24f);
|
|
[SerializeField] private Vector2 sparkSizeFactor = new(0.032f, 0.065f);
|
|
[SerializeField, Min(0.01f)] private float sparkWidth = 0.45f;
|
|
[SerializeField, Min(0f)] private float sparkVelocityFactor = 0.9f;
|
|
|
|
private readonly List<VisualEffect> _sparkEmitters = new();
|
|
private Coroutine _textFlickerCoroutine;
|
|
private Coroutine _sparkCoroutine;
|
|
private Color _originalTextColor = Color.white;
|
|
private ModuleCondition _condition = ModuleCondition.Normal;
|
|
private bool _initialized;
|
|
private bool _hasAppliedCondition;
|
|
|
|
public bool IsConfigured => modulePicRenderer != null && moduleTextRenderer != null;
|
|
|
|
/// <summary>
|
|
/// 文字当前的亮/灭状态(含 Damaged 闪烁中的瞬时相位),供切割视图入场时对齐。
|
|
/// </summary>
|
|
public bool IsTextCurrentlyVisible => moduleTextRenderer != null && moduleTextRenderer.enabled;
|
|
|
|
private void Awake()
|
|
{
|
|
Initialize();
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
if (_initialized && _hasAppliedCondition)
|
|
ApplyConditionInternal(_condition, true);
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
StopRunningEffects();
|
|
}
|
|
|
|
public void ApplyCondition(ModuleCondition condition)
|
|
{
|
|
Initialize();
|
|
ApplyConditionInternal(condition, false);
|
|
}
|
|
|
|
private void Initialize()
|
|
{
|
|
if (_initialized) return;
|
|
_initialized = true;
|
|
|
|
if (moduleTextRenderer != null)
|
|
_originalTextColor = moduleTextRenderer.color;
|
|
|
|
CreateSparkEmitters();
|
|
}
|
|
|
|
private void ApplyConditionInternal(ModuleCondition condition, bool force)
|
|
{
|
|
if (!force && _hasAppliedCondition && _condition == condition)
|
|
return;
|
|
|
|
_condition = condition;
|
|
_hasAppliedCondition = true;
|
|
StopRunningEffects();
|
|
|
|
if (!IsConfigured)
|
|
return;
|
|
|
|
switch (condition)
|
|
{
|
|
case ModuleCondition.Normal:
|
|
SetTextVisible(true);
|
|
break;
|
|
case ModuleCondition.Damaged:
|
|
SetTextVisible(true);
|
|
if (isActiveAndEnabled)
|
|
{
|
|
_textFlickerCoroutine = StartCoroutine(FlickerText());
|
|
if (_sparkEmitters.Count > 0)
|
|
_sparkCoroutine = StartCoroutine(PlayIntermittentSparks());
|
|
}
|
|
break;
|
|
case ModuleCondition.PoweredOff:
|
|
SetTextVisible(false);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void StopRunningEffects()
|
|
{
|
|
if (_textFlickerCoroutine != null)
|
|
{
|
|
StopCoroutine(_textFlickerCoroutine);
|
|
_textFlickerCoroutine = null;
|
|
}
|
|
|
|
if (_sparkCoroutine != null)
|
|
{
|
|
StopCoroutine(_sparkCoroutine);
|
|
_sparkCoroutine = null;
|
|
}
|
|
|
|
foreach (var effect in _sparkEmitters)
|
|
{
|
|
if (effect == null) continue;
|
|
effect.Stop();
|
|
effect.gameObject.SetActive(false);
|
|
}
|
|
}
|
|
|
|
private IEnumerator FlickerText()
|
|
{
|
|
while (_condition == ModuleCondition.Damaged)
|
|
{
|
|
SetTextVisible(Random.value > 0.4f);
|
|
yield return new WaitForSeconds(Random.Range(
|
|
Mathf.Min(flickerMinInterval, flickerMaxInterval),
|
|
Mathf.Max(flickerMinInterval, flickerMaxInterval)));
|
|
}
|
|
}
|
|
|
|
private IEnumerator PlayIntermittentSparks()
|
|
{
|
|
yield return new WaitForSeconds(Random.Range(0.05f, 0.25f));
|
|
|
|
while (_condition == ModuleCondition.Damaged)
|
|
{
|
|
PlaySparkBurst();
|
|
yield return new WaitForSeconds(Random.Range(
|
|
Mathf.Min(sparkMinInterval, sparkMaxInterval),
|
|
Mathf.Max(sparkMinInterval, sparkMaxInterval)));
|
|
}
|
|
}
|
|
|
|
private void PlaySparkBurst()
|
|
{
|
|
if (_sparkEmitters.Count == 0) return;
|
|
|
|
var effect = _sparkEmitters[Random.Range(0, _sparkEmitters.Count)];
|
|
if (effect == null) return;
|
|
|
|
effect.gameObject.SetActive(true);
|
|
effect.Reinit();
|
|
effect.Play();
|
|
}
|
|
|
|
private void SetTextVisible(bool visible)
|
|
{
|
|
if (moduleTextRenderer == null) return;
|
|
|
|
moduleTextRenderer.color = _originalTextColor;
|
|
moduleTextRenderer.enabled = visible;
|
|
}
|
|
|
|
private void CreateSparkEmitters()
|
|
{
|
|
if (sparkAsset == null || modulePicRenderer == null)
|
|
return;
|
|
|
|
var bounds = modulePicRenderer.bounds;
|
|
var scaledExtents = new Vector2(
|
|
bounds.extents.x * Mathf.Max(0.01f, sparkBoundsScale.x),
|
|
bounds.extents.y * Mathf.Max(0.01f, sparkBoundsScale.y));
|
|
var sizeBase = Mathf.Max(0.01f, Mathf.Min(bounds.size.x, bounds.size.y));
|
|
|
|
foreach (var rawDirection in SparkDirections)
|
|
{
|
|
var direction = rawDirection.normalized;
|
|
var worldPosition = (Vector2)bounds.center + sparkCenterOffset + new Vector2(
|
|
rawDirection.x * scaledExtents.x,
|
|
rawDirection.y * scaledExtents.y) + direction * sparkBoundsPadding;
|
|
|
|
var emitterObject = new GameObject($"Damage Spark {rawDirection.x:0},{rawDirection.y:0}");
|
|
emitterObject.transform.SetParent(transform, false);
|
|
emitterObject.transform.localPosition = transform.InverseTransformPoint(worldPosition);
|
|
|
|
var effect = emitterObject.AddComponent<VisualEffect>();
|
|
effect.visualEffectAsset = sparkAsset;
|
|
ConfigureSparkEffect(effect, direction, sizeBase);
|
|
|
|
var renderer = emitterObject.GetComponent<Renderer>();
|
|
if (renderer != null)
|
|
{
|
|
renderer.sortingLayerID = modulePicRenderer.sortingLayerID;
|
|
renderer.sortingOrder = modulePicRenderer.sortingOrder - 1;
|
|
}
|
|
|
|
effect.Stop();
|
|
emitterObject.SetActive(false);
|
|
_sparkEmitters.Add(effect);
|
|
}
|
|
}
|
|
|
|
private void ConfigureSparkEffect(VisualEffect effect, Vector2 direction, float sizeBase)
|
|
{
|
|
if (effect.HasFloat("Spawn Rate"))
|
|
effect.SetFloat("Spawn Rate", 0f);
|
|
if (effect.HasVector2("Burst Count Min/Max"))
|
|
effect.SetVector2("Burst Count Min/Max", sparkBurstCount);
|
|
if (effect.HasVector2("Lifetime Min/Max"))
|
|
effect.SetVector2("Lifetime Min/Max", sparkLifetime);
|
|
if (effect.HasVector2("Size Min/Max"))
|
|
effect.SetVector2("Size Min/Max", sparkSizeFactor * sizeBase);
|
|
if (effect.HasFloat("Sparkle Width"))
|
|
effect.SetFloat("Sparkle Width", sparkWidth);
|
|
if (effect.HasVector3("Initial Velocity_vector"))
|
|
{
|
|
var speed = sizeBase * sparkVelocityFactor;
|
|
effect.SetVector3("Initial Velocity_vector", new Vector3(direction.x, direction.y, 0f) * speed);
|
|
}
|
|
}
|
|
}
|
|
}
|