using UnityEngine; using UnityEngine.Serialization; namespace AibisDream.MiniGame.HuoShan { /// /// 笑话发生器 · 像素火花管 — 逐像素写 Texture2D,量化色阶呈现粗颗粒像素风 /// 由 KnobController 通过 SetFill / SetIntensity / TriggerPulse 驱动 /// 表现:管体兼作进度条,右端随抑制推进往左退;管内每格独立跳动并不断迸出火花, /// 抑制越深越稀越暗,但反抗脉冲会把整管重新炸亮 —— 读作"抑制在进行,但没能成功" /// public class GlowTubeEffect : MonoBehaviour { [Header("区域")] [Tooltip("效果区域参考(取其 SpriteRenderer bounds),不填则用自身")] [SerializeField] private SpriteRenderer areaReference; [Tooltip("相对参考区域向内收缩比例,避免辉光贴满边框轻微溢出")] [Range(0f, 0.2f)] [SerializeField] private float areaInset = 0.04f; [Header("像素网格")] [Tooltip("横向像素格数(纵向按区域纵横比自动推算)")] [SerializeField] private int pixelColumns = 72; [Tooltip("渲染用材质(Unlit,不受 2D 灯光影响),不填则回退到 areaReference 的材质")] [FormerlySerializedAs("particleMaterial")] [SerializeField] private Material pixelMaterial; [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("进度条(管体长度 = 剩余量)")] [Tooltip("剩余量跟随速度:越大越跟手")] [SerializeField] private float fillLerpSpeed = 16f; [Tooltip("前沿热区宽度(占全管比例):越大前端亮带越长")] [SerializeField] private float edgeHotWidth = 0.1f; [Tooltip("前沿抖动幅度(占全管比例):让右端边界像火焰一样翻腾而不是一刀切")] [SerializeField] private float edgeBoil = 0.025f; [Header("笑声节奏")] [Tooltip("笑声脉冲串的基础频率(次/秒),强度越高笑得越快")] [SerializeField] private float laughRate = 2.2f; [Tooltip("笑声包络对亮度/密度的影响强度")] [SerializeField] private float laughAmplitude = 0.85f; [Header("噪声流动")] [SerializeField] private float noiseScale = 3f; [SerializeField] private float noiseSpeed = 2.6f; [Header("逐格跳动")] [Tooltip("每格独立闪烁的基础速率(次/秒),越大越躁")] [SerializeField] private float twinkleRate = 11f; [Tooltip("跳动锐度:越大越接近整格啪嗒开关,越小越像呼吸")] [Range(1f, 6f)] [SerializeField] private float twinkleSharpness = 2.4f; [Header("火花")] [Tooltip("同屏最大火花数")] [SerializeField] private int maxSparks = 96; [Tooltip("满强度时每秒生成的火花数")] [SerializeField] private float sparkRate = 45f; [Tooltip("火花基础存活时长 (秒)")] [SerializeField] private float sparkLife = 0.32f; [Tooltip("火花从前沿迸出的比例(其余散布在整段管体内)")] [Range(0f, 1f)] [SerializeField] private float sparkEdgeBias = 0.55f; [Header("闪烁")] [SerializeField] private float flickerRate = 8f; [SerializeField] private float flickerAmplitude = 0.15f; [Header("量化阈值(值→档位,制造啪嗒式整块亮灭)")] [SerializeField] private float thresholdLow = 0.16f; [SerializeField] private float thresholdMid = 0.42f; [SerializeField] private float thresholdHigh = 0.72f; private SpriteRenderer _sr; private Texture2D _tex; private Sprite _sprite; private Color32[] _pixels; private Material _matInstance; private int _texWidth; private int _texHeight; private float _intensity = 1f; private float _resistancePulse; private float _nextPulseTime; private float _time; private float _seedX; private float _seedY; private float _laughSeed; private float _fillTarget = 1f; private float _fill = 1f; private float _activity = 1f; private float _laugh; private Spark[] _sparks; private int _sparkCount; private float _sparkAccumulator; private Vector2 _areaHalfSize = new Vector2(0.5f, 0.25f); private struct Spark { public float X; public float Y; public float VX; public float VY; public float Life; public float MaxLife; } /// 当前显示强度(含脉冲) public float DisplayIntensity { get; private set; } /// 当前管体剩余长度(0-1),已平滑 public float Fill => _fill; #region Lifecycle private void Awake() { _seedX = Random.Range(0f, 1000f); _seedY = Random.Range(0f, 1000f); _laughSeed = Random.Range(0f, 1000f); _sparks = new Spark[Mathf.Max(1, maxSparks)]; CacheAreaBounds(); EnsurePixelRenderer(); } private void OnDestroy() { if (_matInstance != null) Destroy(_matInstance); if (_tex != null) Destroy(_tex); if (_sprite != null) Destroy(_sprite); } private void Update() { float dt = Time.deltaTime; _time += dt; UpdateResistancePulse(dt); _fill += (_fillTarget - _fill) * Mathf.Min(1f, dt * fillLerpSpeed); UpdateSparks(dt); RenderPixels(); } #endregion #region Public API /// 设置基础强度(0=熄灭, 1=最亮, 可超过 1) public void SetIntensity(float value) { _intensity = Mathf.Max(0f, value); } /// /// 设置管体剩余长度(1=满管,0=空管)。管体从右端往左退,兼作旋钮进度条。 /// public void SetFill(float value) { _fillTarget = Mathf.Clamp01(value); } /// 立即把管长设为指定值(重置用,不做平滑) public void SetFillImmediate(float value) { _fillTarget = Mathf.Clamp01(value); _fill = _fillTarget; } /// 触发一次反抗脉冲爆亮 public void TriggerPulse(float strength = 0.5f) { _resistancePulse = Mathf.Max(_resistancePulse, Mathf.Clamp01(strength)); BurstSparks(Mathf.RoundToInt(strength * 24f)); } #endregion #region Setup private void CacheAreaBounds() { var reference = areaReference != null ? areaReference : GetComponent(); if (reference != null) { var b = reference.bounds; float inset = 1f - Mathf.Clamp01(areaInset); _areaHalfSize = new Vector2( Mathf.Max(0.01f, b.extents.x * inset), Mathf.Max(0.01f, b.extents.y * inset)); } } private void EnsurePixelRenderer() { var go = new GameObject("GlowTubePixels"); go.transform.SetParent(transform); go.transform.localPosition = Vector3.zero; go.transform.localRotation = Quaternion.identity; go.transform.localScale = Vector3.one; _sr = go.AddComponent(); _texWidth = Mathf.Max(4, pixelColumns); float aspect = _areaHalfSize.y / Mathf.Max(0.001f, _areaHalfSize.x); _texHeight = Mathf.Max(2, Mathf.RoundToInt(_texWidth * aspect)); _tex = new Texture2D(_texWidth, _texHeight, TextureFormat.RGBA32, false) { filterMode = FilterMode.Point, wrapMode = TextureWrapMode.Clamp }; _pixels = new Color32[_texWidth * _texHeight]; float ppu = _texWidth / (_areaHalfSize.x * 2f); _sprite = Sprite.Create(_tex, new Rect(0, 0, _texWidth, _texHeight), new Vector2(0.5f, 0.5f), ppu); _sr.sprite = _sprite; var mat = pixelMaterial != null ? new Material(pixelMaterial) : (areaReference != null && areaReference.sharedMaterial != null ? new Material(areaReference.sharedMaterial) : null); if (mat != null) { _matInstance = mat; _sr.sharedMaterial = mat; } int lid = SortingLayer.IsValid(sortingLayerId) ? sortingLayerId : SortingLayer.layers[0].id; _sr.sortingLayerID = lid; _sr.sortingOrder = sortingOrder; } #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 float GetLaughEnvelope(float activity) { float rate = laughRate * (0.4f + activity * 1.1f); float phase = _time * rate + _laughSeed; float cycle = phase - Mathf.Floor(phase); // 每个周期内是一串递减的尖峰(哈-哈-哈),而不是单个正弦波 float burst = 0f; for (int i = 0; i < 3; i++) { float spikeCenter = i * 0.28f; float d = cycle - spikeCenter; float spike = Mathf.Exp(-d * d * 220f) * (1f - i * 0.28f); burst += Mathf.Max(0f, spike); } return Mathf.Clamp01(burst) * activity; } #endregion #region Sparks private void UpdateSparks(float dt) { if (_sparks == null || _sparks.Length != Mathf.Max(1, maxSparks)) { _sparks = new Spark[Mathf.Max(1, maxSparks)]; _sparkCount = 0; } // 存活推进 int write = 0; for (int i = 0; i < _sparkCount; i++) { var s = _sparks[i]; s.Life -= dt; if (s.Life <= 0f) continue; s.X += s.VX * dt; s.Y += s.VY * dt; s.VY *= 1f - Mathf.Min(0.9f, dt * 2.2f); // 轻微空气阻力,火花往外冲后停住 if (s.X < -0.05f || s.X > 1.05f || s.Y < -0.4f || s.Y > 1.4f) continue; _sparks[write++] = s; } _sparkCount = write; if (_fill <= 0.02f) return; float spawnPerSec = sparkRate * (0.2f + _activity * 1.1f) * (1f + _laugh * 2.5f + _resistancePulse * 3f); _sparkAccumulator += dt * spawnPerSec; int budget = 24; while (_sparkAccumulator >= 1f && budget-- > 0) { _sparkAccumulator -= 1f; SpawnSpark(); } if (_sparkAccumulator > 4f) _sparkAccumulator = 4f; } private void BurstSparks(int count) { for (int i = 0; i < count; i++) SpawnSpark(); } private void SpawnSpark() { if (_sparks == null || _sparkCount >= _sparks.Length) return; float x = Random.value < sparkEdgeBias ? Mathf.Max(0f, _fill - Random.value * edgeHotWidth * 0.8f) // 前沿迸溅 : Random.value * _fill; // 略收纵向散布,避免火花贴边后冲出管体外框 float y = 0.5f + (Random.value - 0.5f) * 0.62f; float outward = y >= 0.5f ? 1f : -1f; float life = Mathf.Max(0.05f, sparkLife * (0.55f + Random.value * 0.9f)); _sparks[_sparkCount++] = new Spark { X = x, Y = y, // 略偏左:被抑制的方向 VX = (Random.value - 0.5f) * 0.55f - 0.12f, VY = outward * (0.22f + Random.value * 0.85f), Life = life, MaxLife = life }; } private void DrawSparks() { int w = _texWidth; int h = _texHeight; for (int i = 0; i < _sparkCount; i++) { var s = _sparks[i]; int px = Mathf.RoundToInt(s.X * (w - 1)); int py = Mathf.RoundToInt(s.Y * (h - 1)); if (px < 0 || px >= w || py < 0 || py >= h) continue; float t = s.MaxLife > 0f ? Mathf.Clamp01(s.Life / s.MaxLife) : 0f; Color c = t > 0.5f ? highColor : Color.Lerp(midColor, highColor, t * 2f); c.a = 1f; _pixels[py * w + px] = c; // 拖尾一格,让火花有方向感 int tx = px - (s.VX >= 0f ? 1 : -1); int ty = py - (s.VY >= 0f ? 1 : -1); if (t < 0.75f && tx >= 0 && tx < w && ty >= 0 && ty < h) { var tc = midColor; tc.a = 1f; _pixels[ty * w + tx] = tc; } } } #endregion #region Render private void RenderPixels() { if (_tex == null) return; float di = Mathf.Clamp(_intensity + _resistancePulse, 0f, 1.5f); DisplayIntensity = di; // 活动度:强度越高越活跃,且用陡峭曲线让压制过程变化明显 _activity = Mathf.Pow(Mathf.Clamp01(_intensity), 1.8f); _laugh = GetLaughEnvelope(_activity); float baseFlicker = 1f + Mathf.Sin(_time * flickerRate) * flickerAmplitude * di; float laughBoost = 1f + _laugh * laughAmplitude; float totalBoost = baseFlicker * laughBoost; // 点亮密度随活动度收缩:功率越低,同一时刻亮着的格子越少(云变稀、跳得越碎) float density = Mathf.Clamp01(0.28f + _activity * 0.72f + _resistancePulse * 0.6f + _laugh * 0.35f); float gate = 1f - density; int w = _texWidth; int h = _texHeight; var clear = new Color32(0, 0, 0, 0); for (int i = 0; i < _pixels.Length; i++) _pixels[i] = clear; if (di < 0.01f || _fill < 0.005f) { _tex.SetPixels32(_pixels); _tex.Apply(false); return; } float nt = _time * noiseSpeed; float invW = 1f / Mathf.Max(1, w - 1); float invH = 1f / Mathf.Max(1, h - 1); for (int py = 0; py < h; py++) { float cy = (py * invH - 0.5f) * 2f; // 每行前沿单独抖动:右端像火焰一样翻腾,而不是一条硬边 float boilN = Mathf.PerlinNoise(py * 0.35f + _seedY, _time * 6f) - 0.5f; float fillEdge = Mathf.Min(1f, _fill + boilN * edgeBoil * 2f); for (int px = 0; px < w; px++) { int idx = py * w + px; float u = px * invW; // 管体只画到前沿为止 —— 抑制推进时整条管从右往左变短 float behindEdge = fillEdge - u; if (behindEdge < 0f) continue; float cx = (u - 0.5f) * 2f; // 超椭圆遮罩:只裁掉四角,让噪点云基本铺满整个管体 float cx2 = cx * cx; float cy2 = cy * cy; float super = cx2 * cx2 * cx2 + cy2 * cy2 * cy2; if (super > 1f) continue; float edgeFade = 1f - super * super; // 前沿热区:越靠近右端越亮,读作"还在往回顶的火头" float edgeHot = Mathf.Exp(-behindEdge / Mathf.Max(0.01f, edgeHotWidth)); float nx = px * (noiseScale * invW) + _seedX; float ny = py * (noiseScale * invW) + _seedY; float n = Mathf.PerlinNoise(nx + nt, ny - nt * 0.6f); // 每格固定 hash 决定自己的闪烁相位与速率 → 逐格独立跳动,不再像一张静态贴图 float hash = Frac(Mathf.Sin(px * 12.9898f + py * 78.233f + _seedX) * 43758.5453f); float twinkle = 0.5f + 0.5f * Mathf.Sin(_time * twinkleRate * (0.45f + hash * 1.7f) + hash * 63f); twinkle = Mathf.Pow(twinkle, twinkleSharpness); float alive = twinkle * Mathf.Lerp(0.7f, 1.35f, hash) + edgeHot * 0.7f + _resistancePulse * 0.5f + _laugh * 0.35f; if (alive < gate) continue; float val = (0.22f + n * 0.7f + twinkle * 0.4f) * edgeFade * totalBoost * di; val *= 1f + edgeHot * 1.2f; Color32 c; if (val < thresholdLow) continue; if (val < thresholdMid) c = lowColor; else if (val < thresholdHigh) c = midColor; else c = highColor; if (edgeHot > 0.65f || (_resistancePulse > 0.05f && hash > 1f - _resistancePulse)) c = highColor; _pixels[idx] = c; } } DrawSparks(); _tex.SetPixels32(_pixels); _tex.Apply(false); } private static float Frac(float v) => v - Mathf.Floor(v); #endregion } }