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 } }