using UnityEngine;
namespace AibisDream.MiniGame.HuoShan
{
///
/// 辉光管粒子效果 — 在指定区域内用粒子模拟等离子体发光
/// 移植自 Web_EmotionWave glow-tube 分支的渲染逻辑
/// 由 KnobController 通过 SetIntensity / TriggerPulse 驱动
///
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;
/// 当前显示强度(含脉冲)
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
/// 设置基础强度(0=熄灭, 1=最亮, 可超过 1)
public void SetIntensity(float value)
{
_intensity = Mathf.Max(0f, value);
}
/// 触发一次反抗脉冲爆亮
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();
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();
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
}
}