500 lines
18 KiB
C#
500 lines
18 KiB
C#
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
|
||
}
|
||
}
|