Files
aibis-dream/Assets/Scripts/MiniGame/HuoShan/SalesSystem/GlowTubeEffect.cs
T
2026-07-25 15:57:03 +08:00

500 lines
18 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using UnityEngine;
using UnityEngine.Serialization;
namespace AibisDream.MiniGame.HuoShan
{
/// <summary>
/// 笑话发生器 · 像素火花管 — 逐像素写 Texture2D,量化色阶呈现粗颗粒像素风
/// 由 KnobController 通过 SetFill / SetIntensity / TriggerPulse 驱动
/// 表现:管体兼作进度条,右端随抑制推进往左退;管内每格独立跳动并不断迸出火花,
/// 抑制越深越稀越暗,但反抗脉冲会把整管重新炸亮 —— 读作"抑制在进行,但没能成功"
/// </summary>
public class GlowTubeEffect : MonoBehaviour
{
[Header("区域")]
[Tooltip("效果区域参考(取其 SpriteRenderer bounds),不填则用自身")]
[SerializeField] private SpriteRenderer areaReference;
[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;
}
/// <summary>当前显示强度(含脉冲)</summary>
public float DisplayIntensity { get; private set; }
/// <summary>当前管体剩余长度(0-1),已平滑</summary>
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
/// <summary>设置基础强度(0=熄灭, 1=最亮, 可超过 1</summary>
public void SetIntensity(float value)
{
_intensity = Mathf.Max(0f, value);
}
/// <summary>
/// 设置管体剩余长度(1=满管,0=空管)。管体从右端往左退,兼作旋钮进度条。
/// </summary>
public void SetFill(float value)
{
_fillTarget = Mathf.Clamp01(value);
}
/// <summary>立即把管长设为指定值(重置用,不做平滑)</summary>
public void SetFillImmediate(float value)
{
_fillTarget = Mathf.Clamp01(value);
_fill = _fillTarget;
}
/// <summary>触发一次反抗脉冲爆亮</summary>
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<SpriteRenderer>();
if (reference != null)
{
var b = reference.bounds;
_areaHalfSize = new Vector2(Mathf.Max(0.01f, b.extents.x), Mathf.Max(0.01f, b.extents.y));
}
}
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<SpriteRenderer>();
_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;
}
/// <summary>成组尖峰包络:"哈-哈-哈"式快攻击、弹跳衰减的脉冲串</summary>
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.75f;
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.3f + Random.value * 1.1f),
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 = _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
}
}