332 lines
11 KiB
C#
332 lines
11 KiB
C#
using UnityEngine;
|
||
|
||
namespace AibisDream.MiniGame.HuoShan
|
||
{
|
||
/// <summary>
|
||
/// 辉光管粒子效果 — 在指定区域内用粒子模拟等离子体发光
|
||
/// 移植自 Web_EmotionWave glow-tube 分支的渲染逻辑
|
||
/// 由 KnobController 通过 SetIntensity / TriggerPulse 驱动
|
||
/// </summary>
|
||
public class GlowTubeEffect : MonoBehaviour
|
||
{
|
||
[Header("区域")]
|
||
[Tooltip("效果区域参考(取其 SpriteRenderer bounds),不填则用自身")]
|
||
[SerializeField] private SpriteRenderer areaReference;
|
||
|
||
[Header("粒子")]
|
||
[Tooltip("粒子数量")]
|
||
[SerializeField] private int particleCount = 80;
|
||
[Tooltip("粒子基础大小")]
|
||
[SerializeField] private float particleSize = 0.06f;
|
||
[Tooltip("粒子材质(Additive 发光),不赋值则自动创建")]
|
||
[SerializeField] private Material particleMaterial;
|
||
[Tooltip("粒子纹理(软圆),不赋值则自动生成")]
|
||
[SerializeField] private Texture2D particleTexture;
|
||
|
||
[Header("Sorting")]
|
||
[CustomSortingLayer]
|
||
[SerializeField] private int sortingLayerId;
|
||
[SerializeField] private int sortingOrder = 55;
|
||
|
||
[Header("颜色")]
|
||
[SerializeField] private Color lowColor = new Color(0.31f, 0.08f, 0.16f, 1f);
|
||
[SerializeField] private Color midColor = new Color(1f, 0.47f, 0.31f, 1f);
|
||
[SerializeField] private Color highColor = new Color(1f, 0.78f, 0.67f, 1f);
|
||
|
||
[Header("闪烁")]
|
||
[SerializeField] private float flickerRate = 8f;
|
||
[SerializeField] private float flickerAmplitude = 0.15f;
|
||
|
||
[Header("噪声流动")]
|
||
[SerializeField] private float noiseScale = 3f;
|
||
[SerializeField] private float noiseSpeed = 1.2f;
|
||
|
||
private ParticleSystem _ps;
|
||
private ParticleSystem.Particle[] _particles;
|
||
private float[] _phases;
|
||
private Material _matInstance;
|
||
private Texture2D _generatedTex;
|
||
|
||
private float _intensity = 1f;
|
||
private float _resistancePulse;
|
||
private float _nextPulseTime;
|
||
private float _time;
|
||
|
||
private float _seedX;
|
||
private float _seedY;
|
||
|
||
private Vector3 _areaCenter;
|
||
private Vector2 _areaHalfSize;
|
||
|
||
/// <summary>当前显示强度(含脉冲)</summary>
|
||
public float DisplayIntensity { get; private set; }
|
||
|
||
#region Lifecycle
|
||
|
||
private void Awake()
|
||
{
|
||
_seedX = Random.Range(0f, 1000f);
|
||
_seedY = Random.Range(0f, 1000f);
|
||
CacheAreaBounds();
|
||
EnsureParticleSystem();
|
||
}
|
||
|
||
private void OnDestroy()
|
||
{
|
||
if (_matInstance != null) Destroy(_matInstance);
|
||
if (_generatedTex != null) Destroy(_generatedTex);
|
||
}
|
||
|
||
private void Update()
|
||
{
|
||
float dt = Time.deltaTime;
|
||
_time += dt;
|
||
UpdateResistancePulse(dt);
|
||
UpdateParticles();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Public API
|
||
|
||
/// <summary>设置基础强度(0=熄灭, 1=最亮, 可超过 1)</summary>
|
||
public void SetIntensity(float value)
|
||
{
|
||
_intensity = Mathf.Max(0f, value);
|
||
}
|
||
|
||
/// <summary>触发一次反抗脉冲爆亮</summary>
|
||
public void TriggerPulse(float strength = 0.5f)
|
||
{
|
||
_resistancePulse = Mathf.Max(_resistancePulse, Mathf.Clamp01(strength));
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Particle System Setup
|
||
|
||
private void CacheAreaBounds()
|
||
{
|
||
if (areaReference != null)
|
||
{
|
||
var b = areaReference.bounds;
|
||
_areaCenter = b.center;
|
||
_areaHalfSize = new Vector2(b.extents.x, b.extents.y);
|
||
}
|
||
else
|
||
{
|
||
_areaCenter = transform.position;
|
||
_areaHalfSize = new Vector2(0.5f, 0.25f);
|
||
}
|
||
}
|
||
|
||
private void EnsureParticleSystem()
|
||
{
|
||
var go = new GameObject("GlowTubeParticles");
|
||
go.transform.SetParent(transform);
|
||
go.transform.localPosition = Vector3.zero;
|
||
go.transform.localScale = Vector3.one;
|
||
|
||
_ps = go.AddComponent<ParticleSystem>();
|
||
|
||
var main = _ps.main;
|
||
main.simulationSpace = ParticleSystemSimulationSpace.World;
|
||
main.loop = false;
|
||
main.startLifetime = 999f;
|
||
main.startSize = particleSize;
|
||
main.maxParticles = particleCount;
|
||
main.playOnAwake = false;
|
||
main.startSpeed = 0f;
|
||
main.startColor = midColor;
|
||
|
||
var emission = _ps.emission;
|
||
emission.enabled = false;
|
||
var vel = _ps.velocityOverLifetime;
|
||
vel.enabled = false;
|
||
var col = _ps.colorOverLifetime;
|
||
col.enabled = false;
|
||
var sol = _ps.sizeOverLifetime;
|
||
sol.enabled = false;
|
||
|
||
var psr = go.GetComponent<ParticleSystemRenderer>();
|
||
if (psr != null)
|
||
{
|
||
psr.renderMode = ParticleSystemRenderMode.Billboard;
|
||
ApplyMaterial(psr);
|
||
int lid = SortingLayer.IsValid(sortingLayerId) ? sortingLayerId : SortingLayer.layers[0].id;
|
||
psr.sortingLayerID = lid;
|
||
psr.sortingOrder = sortingOrder;
|
||
}
|
||
|
||
_particles = new ParticleSystem.Particle[particleCount];
|
||
_phases = new float[particleCount];
|
||
for (int i = 0; i < particleCount; i++)
|
||
_phases[i] = Random.value;
|
||
|
||
_ps.Play();
|
||
_ps.Emit(particleCount);
|
||
}
|
||
|
||
private void ApplyMaterial(ParticleSystemRenderer psr)
|
||
{
|
||
var tex = particleTexture != null ? particleTexture : GenerateSoftCircle();
|
||
if (tex == null) return;
|
||
|
||
Material mat;
|
||
if (particleMaterial != null)
|
||
{
|
||
mat = new Material(particleMaterial);
|
||
}
|
||
else
|
||
{
|
||
var shader = Shader.Find("Legacy Shaders/Particles/Additive")
|
||
?? Shader.Find("Particles/Additive");
|
||
if (shader == null) return;
|
||
mat = new Material(shader);
|
||
mat.SetColor("_TintColor", Color.white);
|
||
}
|
||
mat.mainTexture = tex;
|
||
_matInstance = mat;
|
||
psr.material = mat;
|
||
}
|
||
|
||
private Texture2D GenerateSoftCircle()
|
||
{
|
||
if (_generatedTex != null) return _generatedTex;
|
||
const int size = 64;
|
||
_generatedTex = new Texture2D(size, size)
|
||
{
|
||
wrapMode = TextureWrapMode.Clamp,
|
||
filterMode = FilterMode.Bilinear
|
||
};
|
||
var px = new Color[size * size];
|
||
float c = (size - 1) * 0.5f;
|
||
for (int y = 0; y < size; y++)
|
||
for (int x = 0; x < size; x++)
|
||
{
|
||
float dx = x - c, dy = y - c;
|
||
float d = Mathf.Sqrt(dx * dx + dy * dy) / c;
|
||
float a = 1f - Mathf.Clamp01(d);
|
||
a *= a;
|
||
px[y * size + x] = new Color(1f, 1f, 1f, a);
|
||
}
|
||
_generatedTex.SetPixels(px);
|
||
_generatedTex.Apply();
|
||
return _generatedTex;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Per-frame Update
|
||
|
||
private void UpdateResistancePulse(float dt)
|
||
{
|
||
if (_intensity < 0.5f && _intensity > 0.03f)
|
||
{
|
||
float suppression = 1f - _intensity;
|
||
if (_time > _nextPulseTime)
|
||
{
|
||
_resistancePulse = Mathf.Max(_resistancePulse,
|
||
suppression * (0.25f + Random.value * 0.5f));
|
||
_nextPulseTime = _time + 0.2f + Random.value * (2.5f - suppression * 2f);
|
||
}
|
||
}
|
||
|
||
_resistancePulse *= Mathf.Exp(-dt * 9f);
|
||
if (_resistancePulse < 0.008f) _resistancePulse = 0f;
|
||
}
|
||
|
||
private void UpdateParticles()
|
||
{
|
||
if (_ps == null || _particles == null) return;
|
||
|
||
CacheAreaBounds();
|
||
|
||
float di = Mathf.Clamp(_intensity + _resistancePulse, 0f, 1.5f);
|
||
DisplayIntensity = di;
|
||
|
||
int alive = _ps.GetParticles(_particles);
|
||
if (alive == 0)
|
||
{
|
||
_ps.Emit(particleCount);
|
||
return;
|
||
}
|
||
|
||
if (di < 0.01f)
|
||
{
|
||
for (int i = 0; i < particleCount && i < alive; i++)
|
||
{
|
||
_particles[i].startColor = new Color(0, 0, 0, 0);
|
||
_particles[i].startSize = 0f;
|
||
}
|
||
_ps.SetParticles(_particles, alive);
|
||
return;
|
||
}
|
||
|
||
float baseFlicker = 1f + Mathf.Sin(_time * flickerRate) * flickerAmplitude * di;
|
||
float burstFlicker = 1f;
|
||
if (di > 0.7f)
|
||
{
|
||
float bc = Mathf.PerlinNoise(_time * 2f + _seedX, _seedY) - 0.5f;
|
||
if (bc > 0.35f) burstFlicker = 1.3f + bc * 0.4f;
|
||
}
|
||
float totalFlicker = baseFlicker * burstFlicker;
|
||
|
||
float z = _areaCenter.z;
|
||
|
||
for (int i = 0; i < particleCount && i < alive; i++)
|
||
{
|
||
_phases[i] += noiseSpeed * Time.deltaTime * 0.3f;
|
||
if (_phases[i] > 100f) _phases[i] -= 100f;
|
||
|
||
float nx = _phases[i] * noiseScale + _seedX;
|
||
float ny = i * 0.37f + _seedY;
|
||
float noiseX = Mathf.PerlinNoise(nx, ny) * 2f - 1f;
|
||
float noiseY = Mathf.PerlinNoise(ny, nx) * 2f - 1f;
|
||
|
||
float px = _areaCenter.x + noiseX * _areaHalfSize.x * 0.85f;
|
||
float py = _areaCenter.y + noiseY * _areaHalfSize.y * 0.85f;
|
||
|
||
// Elliptical edge fade
|
||
float ex = (px - _areaCenter.x) / Mathf.Max(0.01f, _areaHalfSize.x);
|
||
float ey = (py - _areaCenter.y) / Mathf.Max(0.01f, _areaHalfSize.y);
|
||
float ellipse = ex * ex + ey * ey;
|
||
float edgeFade = Mathf.Clamp01(1f - Mathf.Pow(Mathf.Clamp01(ellipse), 1.5f));
|
||
|
||
float val = edgeFade * di * totalFlicker;
|
||
|
||
Color c;
|
||
if (val < 0.35f)
|
||
{
|
||
c = Color.Lerp(new Color(0.05f, 0.02f, 0.03f), lowColor, val / 0.35f);
|
||
}
|
||
else if (val < 0.7f)
|
||
{
|
||
c = Color.Lerp(lowColor, midColor, (val - 0.35f) / 0.35f);
|
||
}
|
||
else
|
||
{
|
||
c = Color.Lerp(midColor, highColor, (val - 0.7f) / 0.3f);
|
||
}
|
||
c.a = Mathf.Clamp01(val * 1.2f) * di;
|
||
|
||
if (_resistancePulse > 0.05f)
|
||
{
|
||
float flash = _resistancePulse * 0.5f * edgeFade;
|
||
c = Color.Lerp(c, highColor, Mathf.Min(0.6f, flash));
|
||
}
|
||
|
||
_particles[i].position = new Vector3(px, py, z);
|
||
_particles[i].startColor = c;
|
||
_particles[i].startSize = particleSize * (0.6f + val * 0.8f);
|
||
_particles[i].remainingLifetime = 999f;
|
||
_particles[i].startLifetime = 999f;
|
||
}
|
||
|
||
_ps.SetParticles(_particles, Mathf.Min(alive, particleCount));
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
}
|