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