feat(fix-system): 添加维修模块运行状态表现
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
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(-1f, -1f), new(0f, -1f), new(1f, -1f),
|
||||
new(-1f, 0f), new(1f, 0f),
|
||||
new(-1f, 1f), new(0f, 1f), new(1f, 1f)
|
||||
};
|
||||
|
||||
[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.35f;
|
||||
[SerializeField, Min(0.01f)] private float sparkMaxInterval = 1.2f;
|
||||
[SerializeField, Min(0f)] private float sparkBoundsPadding = 0.06f;
|
||||
[SerializeField] private Vector2 sparkBoundsScale = Vector2.one;
|
||||
[SerializeField] private Vector2 sparkCenterOffset = Vector2.zero;
|
||||
[SerializeField] private Vector2 sparkBurstCount = new(2f, 5f);
|
||||
[SerializeField] private Vector2 sparkLifetime = new(0.08f, 0.22f);
|
||||
[SerializeField] private Vector2 sparkSizeFactor = new(0.025f, 0.055f);
|
||||
[SerializeField, Min(0f)] private float sparkVelocityFactor = 1.5f;
|
||||
|
||||
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;
|
||||
|
||||
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.HasVector3("Initial Velocity_vector"))
|
||||
{
|
||||
var speed = sizeBase * sparkVelocityFactor;
|
||||
effect.SetVector3("Initial Velocity_vector", new Vector3(direction.x, direction.y, 0f) * speed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user