feat(huoshan): 情感波浪 EmotionWave 模块

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-25 16:19:28 +08:00
co-authored by Cursor
parent e0cb152950
commit a2a34825bf
18 changed files with 2136 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d88539d912cafbe489fa8efdd1fdf0b9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d558d580689c5ba4c9981eee8e436b82
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,375 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e4cbad02f49607e4c873f5e76ed20513, type: 3}
m_Name:
m_EditorClassIdentifier:
anchors:
- position: 0
name: Raw Signal
stage: BUILD
waveParams:
waveMode: 0
timeSpread: 0.94
omegaX: 1.2
omegaY: 1
phaseSpeed: 0.07
numCycles: 2
amplitude: 0.36
amplitudeY: 0.2
waveformType: 0
spikiness: 0
waveshapeGain: 1
harmonicStrength: 0
harmonicFreq: 2
noiseAmount: 0
noiseScale: 0.5
noiseSpeed: 0.12
jitter: 0
weierstrassAmount: 0
lineWidth: 0.88
persistence: 0.86
fragmentation: 0
hue: 128
saturation: 52
lightness: 32
glowIntensity: 0.45
breathRate: 0.12
breathDepth: 0.008
reviewIntensity: 0
featureBoxRate: 0
thresholdLevel: 0
cursorSpeed: 0
spikeIntensity: 0
spikeFreq: 0
spikeSharpness: 0
envelopeDecay: 0
tipLayer: 0
- position: 0.15
name: Harmonic Stack
stage: BUILD
waveParams:
waveMode: 0.32
timeSpread: 0.76
omegaX: 2
omegaY: 1.5
phaseSpeed: 0.24
numCycles: 2
amplitude: 0.4
amplitudeY: 0.38
waveformType: 0
spikiness: 0
waveshapeGain: 1
harmonicStrength: 0.04
harmonicFreq: 2.6
noiseAmount: 0.008
noiseScale: 0.7
noiseSpeed: 0.28
jitter: 0.003
weierstrassAmount: 0
lineWidth: 1.15
persistence: 0.88
fragmentation: 0
hue: 128
saturation: 55
lightness: 36
glowIntensity: 0.58
breathRate: 0.28
breathDepth: 0.022
reviewIntensity: 0
featureBoxRate: 0
thresholdLevel: 0
cursorSpeed: 0
spikeIntensity: 0
spikeFreq: 0
spikeSharpness: 0
envelopeDecay: 0
tipLayer: 0
- position: 0.25
name: Transition
stage: BUILD
waveParams:
waveMode: 0.55
timeSpread: 0.62
omegaX: 1.85
omegaY: 1.15
phaseSpeed: 0.26
numCycles: 2
amplitude: 0.43
amplitudeY: 0.42
waveformType: 0
spikiness: 0
waveshapeGain: 1
harmonicStrength: 0.022
harmonicFreq: 2.2
noiseAmount: 0.004
noiseScale: 0.65
noiseSpeed: 0.24
jitter: 0.001
weierstrassAmount: 0
lineWidth: 1.22
persistence: 0.875
fragmentation: 0
hue: 128
saturation: 55
lightness: 37
glowIntensity: 0.6
breathRate: 0.3
breathDepth: 0.024
reviewIntensity: 0
featureBoxRate: 0
thresholdLevel: 0
cursorSpeed: 0
spikeIntensity: 0
spikeFreq: 0
spikeSharpness: 0
envelopeDecay: 0
tipLayer: 0
- position: 0.35
name: Language Formed
stage: BUILD
waveParams:
waveMode: 1
timeSpread: 0.5
omegaX: 2
omegaY: 1
phaseSpeed: 0.3
numCycles: 2
amplitude: 0.45
amplitudeY: 0.45
waveformType: 0
spikiness: 0
waveshapeGain: 1
harmonicStrength: 0
harmonicFreq: 2
noiseAmount: 0
noiseScale: 0.8
noiseSpeed: 0.3
jitter: 0
weierstrassAmount: 0
lineWidth: 1.3
persistence: 0.87
fragmentation: 0
hue: 128
saturation: 55
lightness: 38
glowIntensity: 0.6
breathRate: 0.32
breathDepth: 0.025
reviewIntensity: 0
featureBoxRate: 0
thresholdLevel: 0
cursorSpeed: 0
spikeIntensity: 0
spikeFreq: 0
spikeSharpness: 0
envelopeDecay: 0
tipLayer: 0
- position: 0.5
name: Deep Review
stage: REVIEW
waveParams:
waveMode: 1
timeSpread: 0.5
omegaX: 2
omegaY: 1
phaseSpeed: 0.5
numCycles: 2
amplitude: 0.48
amplitudeY: 0.48
waveformType: 0
spikiness: 0
waveshapeGain: 1
harmonicStrength: 0
harmonicFreq: 2
noiseAmount: 0
noiseScale: 1
noiseSpeed: 0.3
jitter: 0
weierstrassAmount: 0
lineWidth: 1.3
persistence: 0.87
fragmentation: 0
hue: 128
saturation: 55
lightness: 40
glowIntensity: 0.65
breathRate: 0.38
breathDepth: 0.022
reviewIntensity: 0
featureBoxRate: 0
thresholdLevel: 0
cursorSpeed: 0
spikeIntensity: 0
spikeFreq: 0
spikeSharpness: 0
envelopeDecay: 0
tipLayer: 0
- position: 0.65
name: Review Pass
stage: REVIEW
waveParams:
waveMode: 1
timeSpread: 0.5
omegaX: 2
omegaY: 1
phaseSpeed: 0.35
numCycles: 2
amplitude: 0.48
amplitudeY: 0.48
waveformType: 0
spikiness: 0
waveshapeGain: 1
harmonicStrength: 0
harmonicFreq: 2
noiseAmount: 0
noiseScale: 0.9
noiseSpeed: 0.25
jitter: 0
weierstrassAmount: 0
lineWidth: 1.3
persistence: 0.87
fragmentation: 0
hue: 128
saturation: 55
lightness: 40
glowIntensity: 0.6
breathRate: 0.32
breathDepth: 0.02
reviewIntensity: 0
featureBoxRate: 0
thresholdLevel: 0
cursorSpeed: 0
spikeIntensity: 0
spikeFreq: 0
spikeSharpness: 0
envelopeDecay: 0
tipLayer: 0
- position: 0.75
name: Tone Tweak
stage: ADJUST
waveParams:
waveMode: 1
timeSpread: 0.5
omegaX: 2
omegaY: 1
phaseSpeed: 0.35
numCycles: 2
amplitude: 0.48
amplitudeY: 0.48
waveformType: 0
spikiness: 0
waveshapeGain: 1
harmonicStrength: 0
harmonicFreq: 2
noiseAmount: 0
noiseScale: 0.9
noiseSpeed: 0.25
jitter: 0
weierstrassAmount: 0
lineWidth: 1.3
persistence: 0.87
fragmentation: 0
hue: 128
saturation: 55
lightness: 40
glowIntensity: 0.6
breathRate: 0.3
breathDepth: 0.02
reviewIntensity: 0
featureBoxRate: 0
thresholdLevel: 0
cursorSpeed: 0
spikeIntensity: 0
spikeFreq: 0
spikeSharpness: 0
envelopeDecay: 0
tipLayer: 0
- position: 0.85
name: Error Compress
stage: ADJUST
waveParams:
waveMode: 1
timeSpread: 0.5
omegaX: 2.5
omegaY: 1.5
phaseSpeed: 0.45
numCycles: 2
amplitude: 0.48
amplitudeY: 0.48
waveformType: 1
spikiness: 0
waveshapeGain: 3
harmonicStrength: 0.04
harmonicFreq: 2
noiseAmount: 0.012
noiseScale: 1
noiseSpeed: 0.4
jitter: 0.006
weierstrassAmount: 0.22
lineWidth: 1.35
persistence: 0.84
fragmentation: 0
hue: 50
saturation: 56
lightness: 46
glowIntensity: 0.7
breathRate: 0.42
breathDepth: 0.025
reviewIntensity: 0
featureBoxRate: 0
thresholdLevel: 0
cursorSpeed: 0
spikeIntensity: 0
spikeFreq: 0
spikeSharpness: 0
envelopeDecay: 0
tipLayer: 0
- position: 1
name: Joke Spike
stage: ADJUST
waveParams:
waveMode: 1
timeSpread: 0.5
omegaX: 3
omegaY: 2
phaseSpeed: 0.5
numCycles: 2
amplitude: 0.48
amplitudeY: 0.48
waveformType: 2
spikiness: 0
waveshapeGain: 5
harmonicStrength: 0.08
harmonicFreq: 2
noiseAmount: 0.02
noiseScale: 1.2
noiseSpeed: 0.5
jitter: 0.01
weierstrassAmount: 0.42
lineWidth: 1.4
persistence: 0.82
fragmentation: 0
hue: 0
saturation: 58
lightness: 50
glowIntensity: 0.78
breathRate: 0.52
breathDepth: 0.03
reviewIntensity: 0
featureBoxRate: 0
thresholdLevel: 0
cursorSpeed: 0
spikeIntensity: 0
spikeFreq: 0
spikeSharpness: 0
envelopeDecay: 0
tipLayer: 0
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9c046a1bc8b00d149b08de4b96288243
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,37 @@
using UnityEngine;
namespace AibisDream.MiniGame.HuoShan.EmotionWave
{
public static class ColorUtil
{
/// <summary>
/// HSL to RGB conversion matching CSS hsla(h, s%, l%, a).
/// h: 0~360, s: 0~100, l: 0~100, a: 0~1
/// </summary>
public static Color HslToRgb(float h, float s, float l, float a = 1f)
{
h = ((h % 360f) + 360f) % 360f;
float sNorm = Mathf.Clamp01(s / 100f);
float lNorm = Mathf.Clamp01(l / 100f);
float c = (1f - Mathf.Abs(2f * lNorm - 1f)) * sNorm;
float x = c * (1f - Mathf.Abs((h / 60f) % 2f - 1f));
float m = lNorm - c * 0.5f;
float r, g, b;
if (h < 60f) { r = c; g = x; b = 0f; }
else if (h < 120f) { r = x; g = c; b = 0f; }
else if (h < 180f) { r = 0f; g = c; b = x; }
else if (h < 240f) { r = 0f; g = x; b = c; }
else if (h < 300f) { r = x; g = 0f; b = c; }
else { r = c; g = 0f; b = x; }
return new Color(
Mathf.Clamp01(r + m),
Mathf.Clamp01(g + m),
Mathf.Clamp01(b + m),
Mathf.Clamp01(a)
);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: da528dd9fdd5afb4093ea165b5ec4a55
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,209 @@
using UnityEngine;
namespace AibisDream.MiniGame.HuoShan.EmotionWave
{
[CreateAssetMenu(fileName = "EmotionWaveConfig", menuName = "AibisDream/EmotionWave Config")]
public class EmotionWaveConfig : ScriptableObject
{
[System.Serializable]
public struct WaveParams
{
[Header("Trajectory Shape")]
[Range(0f, 1f)] public float waveMode;
[Range(0.1f, 1f)] public float timeSpread;
public float omegaX;
public float omegaY;
public float phaseSpeed;
[Min(1)] public int numCycles;
[Header("Amplitude & Waveform")]
public float amplitude;
public float amplitudeY;
[Range(0f, 2f)] public float waveformType;
[Range(0f, 1f)] public float spikiness;
[Range(1f, 12f)] public float waveshapeGain;
public float harmonicStrength;
public float harmonicFreq;
[Header("Noise & Glitch")]
public float noiseAmount;
public float noiseScale;
public float noiseSpeed;
public float jitter;
[Range(0f, 1f)] public float weierstrassAmount;
[Header("Line")]
public float lineWidth;
[Range(0.5f, 0.98f)] public float persistence;
[Range(0f, 1f)] public float fragmentation;
[Header("Color (HSL)")]
public float hue;
[Range(0f, 100f)] public float saturation;
[Range(5f, 80f)] public float lightness;
[Range(0f, 1.2f)] public float glowIntensity;
[Header("Dynamics")]
public float breathRate;
public float breathDepth;
[Header("Review (injected at runtime)")]
[HideInInspector] public float reviewIntensity;
[HideInInspector] public float featureBoxRate;
[HideInInspector] public float thresholdLevel;
[HideInInspector] public float cursorSpeed;
[Header("Spike")]
[HideInInspector] public float spikeIntensity;
[HideInInspector] public float spikeFreq;
[HideInInspector] public float spikeSharpness;
[HideInInspector] public float envelopeDecay;
[HideInInspector] public float tipLayer;
public static WaveParams Lerp(WaveParams a, WaveParams b, float t)
{
return new WaveParams
{
waveMode = Mathf.Lerp(a.waveMode, b.waveMode, t),
timeSpread = Mathf.Lerp(a.timeSpread, b.timeSpread, t),
omegaX = Mathf.Lerp(a.omegaX, b.omegaX, t),
omegaY = Mathf.Lerp(a.omegaY, b.omegaY, t),
phaseSpeed = Mathf.Lerp(a.phaseSpeed, b.phaseSpeed, t),
numCycles = Mathf.RoundToInt(Mathf.Lerp(a.numCycles, b.numCycles, t)),
amplitude = Mathf.Lerp(a.amplitude, b.amplitude, t),
amplitudeY = Mathf.Lerp(a.amplitudeY, b.amplitudeY, t),
waveformType = Mathf.Lerp(a.waveformType, b.waveformType, t),
spikiness = Mathf.Lerp(a.spikiness, b.spikiness, t),
waveshapeGain = Mathf.Lerp(a.waveshapeGain, b.waveshapeGain, t),
harmonicStrength = Mathf.Lerp(a.harmonicStrength, b.harmonicStrength, t),
harmonicFreq = Mathf.Lerp(a.harmonicFreq, b.harmonicFreq, t),
noiseAmount = Mathf.Lerp(a.noiseAmount, b.noiseAmount, t),
noiseScale = Mathf.Lerp(a.noiseScale, b.noiseScale, t),
noiseSpeed = Mathf.Lerp(a.noiseSpeed, b.noiseSpeed, t),
jitter = Mathf.Lerp(a.jitter, b.jitter, t),
weierstrassAmount = Mathf.Lerp(a.weierstrassAmount, b.weierstrassAmount, t),
lineWidth = Mathf.Lerp(a.lineWidth, b.lineWidth, t),
persistence = Mathf.Lerp(a.persistence, b.persistence, t),
fragmentation = Mathf.Lerp(a.fragmentation, b.fragmentation, t),
hue = Mathf.Lerp(a.hue, b.hue, t),
saturation = Mathf.Lerp(a.saturation, b.saturation, t),
lightness = Mathf.Lerp(a.lightness, b.lightness, t),
glowIntensity = Mathf.Lerp(a.glowIntensity, b.glowIntensity, t),
breathRate = Mathf.Lerp(a.breathRate, b.breathRate, t),
breathDepth = Mathf.Lerp(a.breathDepth, b.breathDepth, t),
};
}
public void Clamp()
{
numCycles = Mathf.Max(2, numCycles);
amplitude = Mathf.Max(0.05f, amplitude);
amplitudeY = Mathf.Max(0.05f, amplitudeY);
lineWidth = Mathf.Max(0.3f, lineWidth);
persistence = Mathf.Clamp(persistence, 0.5f, 0.98f);
glowIntensity = Mathf.Clamp(glowIntensity, 0f, 1.2f);
noiseAmount = Mathf.Max(0f, noiseAmount);
fragmentation = Mathf.Clamp01(fragmentation);
hue = ((hue % 360f) + 360f) % 360f;
saturation = Mathf.Clamp(saturation, 0f, 100f);
lightness = Mathf.Clamp(lightness, 5f, 80f);
waveMode = Mathf.Clamp01(waveMode);
spikiness = Mathf.Clamp01(spikiness);
waveformType = Mathf.Clamp(waveformType, 0f, 2f);
timeSpread = Mathf.Clamp(timeSpread, 0.1f, 1f);
waveshapeGain = Mathf.Clamp(waveshapeGain, 1f, 12f);
weierstrassAmount = Mathf.Clamp01(weierstrassAmount);
}
}
[System.Serializable]
public struct StageAnchor
{
public float position;
public string name;
public string stage;
public WaveParams waveParams;
}
public StageAnchor[] anchors = new StageAnchor[9];
static float Smoothstep(float t)
{
t = Mathf.Clamp01(t);
return t * t * (3f - 2f * t);
}
public WaveParams Interpolate(float value)
{
value = Mathf.Clamp01(value);
int n = anchors.Length;
if (n == 0) return default;
if (n == 1) return anchors[0].waveParams;
int idx = 0;
for (int i = 0; i < n - 1; i++)
{
if (value <= anchors[i + 1].position)
{
idx = i;
break;
}
if (i == n - 2) idx = n - 2;
}
int i1 = idx;
int i2 = Mathf.Min(n - 1, idx + 1);
float segStart = anchors[i1].position;
float segEnd = anchors[i2].position;
float segLen = segEnd - segStart;
float tLinear = segLen > 0f ? (value - segStart) / segLen : 0f;
float t = Smoothstep(tLinear);
var result = WaveParams.Lerp(anchors[i1].waveParams, anchors[i2].waveParams, t);
result.Clamp();
return result;
}
public string GetStageName(float value)
{
value = Mathf.Clamp01(value);
if (anchors == null || anchors.Length == 0) return "";
StageAnchor closest = anchors[0];
float minDist = float.MaxValue;
foreach (var a in anchors)
{
float d = Mathf.Abs(value - a.position);
if (d < minDist) { minDist = d; closest = a; }
}
if (minDist < 0.03f) return closest.name;
StageAnchor lower = anchors[0];
StageAnchor upper = anchors[anchors.Length - 1];
for (int i = 0; i < anchors.Length - 1; i++)
{
if (value >= anchors[i].position && value <= anchors[i + 1].position)
{
lower = anchors[i];
upper = anchors[i + 1];
break;
}
}
float blend = (upper.position - lower.position) > 0.001f
? (value - lower.position) / (upper.position - lower.position)
: 0f;
if (blend < 0.35f) return lower.name;
if (blend > 0.65f) return upper.name;
return $"{lower.name} -> {upper.name}";
}
public string GetStagePhase(float value)
{
if (value < 0.35f) return "BUILD";
if (value < 0.65f) return "REVIEW";
return "ADJUST";
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e4cbad02f49607e4c873f5e76ed20513
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,134 @@
using UnityEngine;
namespace AibisDream.MiniGame.HuoShan.EmotionWave
{
/// <summary>
/// Controls the EmotionWave visualization.
/// Drag the progress slider in the Inspector to drive the 3-act narrative.
/// Ported from Web_EmotionWave/js/app.js.
/// </summary>
[ExecuteAlways]
public class EmotionWaveController : MonoBehaviour
{
[Header("References")]
[SerializeField] private EmotionWaveConfig config;
[SerializeField] private EmotionWaveRenderer waveRenderer;
[Header("Progress Control")]
[Range(0f, 1f)]
[SerializeField] private float progress = 0f;
[Header("Smooth Tracking")]
[SerializeField] private float lerpSpeed = 4f;
[Header("Tweak (active in 0.75~1.0 range)")]
[Range(0f, 1f)] [SerializeField] private float tweakFuzz = 0f;
[Range(0f, 1f)] [SerializeField] private float tweakSpin = 0.5f;
[Range(0f, 1f)] [SerializeField] private float tweakHue = 0f;
[Header("CRT Startup")]
[SerializeField] private float startupDuration = 1.8f;
[Header("Debug")]
[SerializeField] [ReadOnlyField] private string currentStage = "";
[SerializeField] [ReadOnlyField] private string currentStageName = "";
[SerializeField] [ReadOnlyField] private float smoothedProgress;
private float _currentValue;
private float _elapsedTime;
public float Progress
{
get => progress;
set => progress = Mathf.Clamp01(value);
}
void Update()
{
if (config == null || waveRenderer == null) return;
float dt = Time.deltaTime;
_elapsedTime += dt;
// CRT startup ease
float startupFactor = Mathf.Clamp01(_elapsedTime / startupDuration);
float startupEase = startupFactor * startupFactor;
// Smooth tracking
_currentValue += (progress - _currentValue) * Mathf.Min(1f, dt * lerpSpeed);
smoothedProgress = _currentValue;
// Interpolate wave params
var waveParams = config.Interpolate(_currentValue);
// Inject review parameters based on stage phase
string phase = config.GetStagePhase(_currentValue);
currentStage = phase;
currentStageName = config.GetStageName(_currentValue);
if (phase == "REVIEW")
{
float r0 = 0.35f, r1 = 0.5f, r2 = 0.65f;
if (_currentValue <= r1)
waveParams.reviewIntensity = Smoothstep((_currentValue - r0) / (r1 - r0));
else
waveParams.reviewIntensity = 0.15f + 0.85f * Smoothstep((r2 - _currentValue) / (r2 - r1));
waveParams.featureBoxRate = 3f;
waveParams.thresholdLevel = 0.75f;
waveParams.cursorSpeed = 0.5f;
}
else
{
waveParams.reviewIntensity = 0f;
waveParams.featureBoxRate = 0f;
waveParams.thresholdLevel = 0.8f;
waveParams.cursorSpeed = 0f;
}
// End-stage tweaks (smoothstep fade-in from 0.75)
float tweakLinear = Mathf.Clamp01((_currentValue - 0.75f) / 0.1f);
float tweakWeight = tweakLinear * tweakLinear * (3f - 2f * tweakLinear);
if (tweakWeight > 0.001f)
{
float fuzz = tweakFuzz * 0.8f;
float spin = 0.2f + tweakSpin * 1f;
float hueVal = tweakHue * 128f;
waveParams.weierstrassAmount = Mathf.Lerp(waveParams.weierstrassAmount, fuzz, tweakWeight);
waveParams.phaseSpeed = Mathf.Lerp(waveParams.phaseSpeed, spin, tweakWeight);
waveParams.hue = Mathf.Lerp(waveParams.hue, hueVal, tweakWeight);
}
// CRT startup fade
waveParams.glowIntensity *= startupEase;
waveParams.lightness *= startupEase;
// Push to renderer
waveRenderer.currentParams = waveParams;
waveRenderer.time = _elapsedTime;
}
static float Smoothstep(float t)
{
t = Mathf.Clamp01(t);
return t * t * (3f - 2f * t);
}
}
/// <summary>
/// Makes a field read-only in the Inspector.
/// </summary>
public class ReadOnlyFieldAttribute : PropertyAttribute { }
#if UNITY_EDITOR
[UnityEditor.CustomPropertyDrawer(typeof(ReadOnlyFieldAttribute))]
public class ReadOnlyFieldDrawer : UnityEditor.PropertyDrawer
{
public override void OnGUI(Rect position, UnityEditor.SerializedProperty property, GUIContent label)
{
UnityEditor.EditorGUI.BeginDisabledGroup(true);
UnityEditor.EditorGUI.PropertyField(position, property, label);
UnityEditor.EditorGUI.EndDisabledGroup();
}
}
#endif
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 10373e9ad77e7df4eaf482f687e678d0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,226 @@
using System.Collections;
using UnityEngine;
using DG.Tweening;
using AibisDream.FixSystem;
namespace AibisDream.MiniGame.HuoShan.EmotionWave
{
/// <summary>
/// 情绪波形管理器 - 用于控制情绪波形的显示、隐藏和进度动画
///
/// 功能:
/// - FixSystemCenter 注册/注销
/// - DOTween 显示/隐藏动画
/// - 进度控制:立即设置 + 平滑动画
/// - 平滑追踪:smoothstep lerp 追踪目标进度
/// </summary>
public class EmotionWaveManager : MonoBehaviour
{
#region
[Header("References")]
[SerializeField] private EmotionWaveController controller;
[SerializeField] private Transform containerTransform;
[Header("Slider Sync")]
[Tooltip("可选:关联 SliderController 以自动同步进度")]
[SerializeField] private SliderController sliderController;
[Tooltip("是否启用滑片同步(勾选后滑片进度会自动驱动情绪波形进度)")]
[SerializeField] private bool enableSliderSync = true;
[Header("动画设置")]
[SerializeField] private Vector2 hiddenPosition = new Vector2(0f, -600f);
[SerializeField] private Vector2 shownPosition = new Vector2(0f, 0f);
[SerializeField] private float showDuration = 0.5f;
[SerializeField] private float hideDuration = 0.3f;
[SerializeField] private Ease showEase = Ease.OutCubic;
[SerializeField] private Ease hideEase = Ease.InCubic;
[Header("Progress Settings")]
[SerializeField] private float lerpSpeed = 4f;
[Header("Debug")]
[SerializeField] [ReadOnly] private float currentProgress;
[SerializeField] [ReadOnly] private float targetProgress;
[SerializeField] [ReadOnly] private bool isShown;
#endregion
#region
private Tweener _progressTweener;
private AudioSource _audioSource;
#endregion
#region
public bool IsShown => isShown;
public float CurrentProgress => currentProgress;
public float TargetProgress => targetProgress;
#endregion
#region
private void Awake()
{
_audioSource = GetComponent<AudioSource>();
if (_audioSource == null)
{
_audioSource = gameObject.AddComponent<AudioSource>();
}
// 初始化位置为隐藏状态(不控制 scale,由场景/Prefab 决定)
if (containerTransform != null)
{
containerTransform.localPosition = hiddenPosition;
}
// 注册到 FixSystemCenter
FixSystemCenter.SystemDic.Register(this);
// 订阅 SliderController 的进度事件
if (sliderController != null && enableSliderSync)
{
sliderController.onSliderPositionChanged.AddListener(OnSliderPositionChanged);
}
}
private void OnDestroy()
{
// 清理 DOTween
_progressTweener?.Kill();
// 取消订阅
if (sliderController != null)
{
sliderController.onSliderPositionChanged.RemoveListener(OnSliderPositionChanged);
}
// 注销
FixSystemCenter.SystemDic.Unregister<EmotionWaveManager>();
}
private void Update()
{
if (controller == null) return;
// smoothstep lerp 追踪目标进度(和 Web 版 lerpSpeed = 4.0 一致)
float dt = Time.deltaTime;
currentProgress += (targetProgress - currentProgress) * Mathf.Min(1f, dt * lerpSpeed);
// 将当前进度传递给 Controller
controller.Progress = currentProgress;
}
#endregion
#region /
/// <summary>
/// 显示情绪波形(DOTween 动画)
/// </summary>
public IEnumerator Show(float duration = -1f)
{
if (isShown) yield break;
float actualDuration = duration > 0 ? duration : showDuration;
isShown = true;
if (containerTransform != null)
{
yield return containerTransform.DOLocalMove(shownPosition, actualDuration)
.SetEase(showEase)
.WaitForCompletion();
}
}
/// <summary>
/// 隐藏情绪波形(DOTween 动画)
/// </summary>
public IEnumerator Hide(float duration = -1f)
{
if (!isShown) yield break;
float actualDuration = duration > 0 ? duration : hideDuration;
isShown = false;
// 停止所有进度动画
_progressTweener?.Kill();
if (containerTransform != null)
{
yield return containerTransform.DOLocalMove(hiddenPosition, actualDuration)
.SetEase(hideEase)
.WaitForCompletion();
}
}
#endregion
#region
/// <summary>
/// 立即设置进度(不等待)
/// </summary>
public void SetProgress(float value)
{
_progressTweener?.Kill();
targetProgress = Mathf.Clamp01(value);
currentProgress = targetProgress;
}
/// <summary>
/// 动画到指定进度(DOTween 驱动 targetProgress
/// </summary>
public IEnumerator AnimateToProgress(float target, float duration)
{
_progressTweener?.Kill();
target = Mathf.Clamp01(target);
// 使用 DOTween.To 驱动 targetProgress
_progressTweener = DOTween.To(
() => targetProgress,
x => targetProgress = x,
target,
duration
).SetEase(Ease.InOutCubic);
yield return _progressTweener.WaitForCompletion();
}
#endregion
#region Slider
/// <summary>
/// 响应 SliderController 的位置变化事件
/// </summary>
private void OnSliderPositionChanged(float normalizedPosition)
{
if (!enableSliderSync) return;
// 滑片进度直接驱动 targetProgress(平滑追踪会在 Update 中处理)
targetProgress = Mathf.Clamp01(normalizedPosition);
}
/// <summary>
/// 启用/禁用滑片同步
/// </summary>
public void SetSliderSyncEnabled(bool enabled)
{
enableSliderSync = enabled;
}
#endregion
#region
[System.Serializable]
private class ReadOnlyAttribute : PropertyAttribute { }
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 55e8be7c583c88f44a97e953a6d806e1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,805 @@
using UnityEngine;
using Shapes;
namespace AibisDream.MiniGame.HuoShan.EmotionWave
{
/// <summary>
/// Emotion Wave renderer using Shapes immediate mode.
/// Ported from Web_EmotionWave/js/renderer.js (OscilloscopeRenderer).
/// </summary>
[ExecuteAlways]
public class EmotionWaveRenderer : ImmediateModeShapeDrawer
{
[Header("Drawing Area")]
[Tooltip("绘制区域参考。若挂载了 RectTransform 则用 rect 尺寸转世界坐标,否则用 lossyScale")]
[SerializeField] private Transform waveScreen;
[Tooltip("波形在绘制区域内的填充比例,1=贴满边线,0.82=默认留边距")]
[Range(0.5f, 1f)]
[SerializeField] private float fillAmount = 0.82f;
[SerializeField] private float thicknessScale = 0.01f;
[Header("Sampling")]
[SerializeField] private int numPoints = 600;
internal EmotionWaveConfig.WaveParams currentParams;
internal float time;
struct WavePoint
{
public Vector3 worldPos;
public float t;
public float tNorm;
public float tipIntensity;
public float yNorm;
public float rawX, rawY;
}
private WavePoint[] _points;
private float _scanLineX;
private float _cursorX;
// Blob tracker state
private struct BlobTracker
{
public int pointIndex;
public float birth;
public float life;
}
private BlobTracker[] _trackers = new BlobTracker[0];
private int _trackerCount;
private float _lastTrackerTime;
// Peak markers
private struct PeakMarker
{
public Vector3 pos;
public bool isMax;
}
private PeakMarker[] _peakMarkers = new PeakMarker[12];
private int _peakCount;
private int _spectrumScanIdx;
#region Math helpers
static float Smoothstep(float x)
{
x = Mathf.Clamp01(x);
return x * x * (3f - 2f * x);
}
static float SpikyWave(float t, float spikiness)
{
float s = Mathf.Sin(t);
if (spikiness < 0.01f) return s;
float exp = 1f - spikiness * 0.85f;
return Mathf.Sign(s) * Mathf.Pow(Mathf.Abs(s) + 0.0001f, exp);
}
static float Waveshape(float x, float gain)
{
if (gain < 1.05f) return x;
float tg = (float)System.Math.Tanh(gain);
return (float)System.Math.Tanh(gain * x) / tg;
}
static float PeriodicWave(float phase, float type)
{
float s = Mathf.Sin(phase);
if (type <= 0f) return s;
float TWO_PI = Mathf.PI * 2f;
float p = (phase / TWO_PI) % 1f;
if (p < 0f) p += 1f;
float tri = 4f * Mathf.Abs(p - 0.5f) - 1f;
float sq = s >= 0f ? 1f : -1f;
if (type >= 2f) return sq;
if (type >= 1f) return (2f - type) * tri + (type - 1f) * sq;
return (1f - type) * s + type * tri;
}
static float Weierstrass(float phase, int layers = 5, float a = 0.55f, float b = 5f)
{
float sum = 0f;
float norm = 0f;
for (int k = 0; k <= layers; k++)
{
float coef = Mathf.Pow(a, k);
sum += coef * Mathf.Sin(Mathf.Pow(b, k) * phase);
norm += coef;
}
return norm > 0.001f ? sum / norm : 0f;
}
static float Noise2D(float x, float y)
{
return Mathf.PerlinNoise(x + 1000f, y + 1000f) * 2f - 1f;
}
#endregion
void EnsureArrays()
{
if (_points == null || _points.Length != numPoints + 1)
_points = new WavePoint[numPoints + 1];
}
public override void DrawShapes(Camera cam)
{
if (waveScreen == null) return;
EnsureArrays();
var p = currentParams;
Vector3 center = waveScreen.position;
Vector3 size = waveScreen.lossyScale;
float halfW = size.x * 0.5f;
float halfH = size.y * 0.5f;
float scaleX = halfW * 0.9f;
float scaleY = halfH * 0.9f;
float z = center.z;
float dt = Time.deltaTime;
// Breath
float breathPhase = time * p.breathRate * Mathf.PI * 2f;
float breath = 1f + Mathf.Sin(breathPhase) * p.breathDepth;
float subBreath = 1f + Mathf.Sin(breathPhase * 2.7f + 1.3f) * p.breathDepth * 0.35f;
float totalBreath = breath * subBreath;
// Review pulse
float ri = p.reviewIntensity;
float reviewPulse = 1f;
if (ri > 0.05f)
{
float pulsePhase = time * 1.5f;
reviewPulse = 1f + Mathf.Sin(pulsePhase * Mathf.PI * 2f) * 0.2f * ri;
}
// Scan line position
if (ri > 0.05f)
_scanLineX = (_scanLineX + dt * 0.3f) % 1f;
// Cursor position
float cursorSpeed = p.cursorSpeed;
if (cursorSpeed > 0.001f)
_cursorX = (_cursorX + dt * cursorSpeed * 0.25f) % 1f;
float waveshapeGain = Mathf.Max(1f, p.waveshapeGain);
float envelopeDecay = p.envelopeDecay;
float spikeIntensity = p.spikeIntensity;
float spikeFreq = p.spikeFreq > 0.01f ? p.spikeFreq : 3f;
float spikeSharpness = p.spikeSharpness > 0.01f ? p.spikeSharpness : 1f;
float weierstrassAmount = p.weierstrassAmount;
float totalT = Mathf.PI * 2f * Mathf.Max(2, p.numCycles);
float waveMode = p.waveMode;
float spikiness = p.spikiness;
float waveformType = p.waveformType;
float eased = Smoothstep(waveMode);
// --- Generate wave points ---
for (int i = 0; i <= numPoints; i++)
{
float tParam = (float)i / numPoints * totalT;
float tNorm = (float)i / numPoints;
// X: sweep + Lissajous oscillation + transition bend
float sweepX = (tNorm * 2f - 1f) * p.timeSpread;
float lissPhase = p.omegaX * tParam + time * p.phaseSpeed;
float blendSpiky = (spikiness > 0.01f) ? Smoothstep(1f - Mathf.Min(1f, waveformType / 0.5f)) : 0f;
float spikyX = SpikyWave(lissPhase, spikiness);
float periodicX = PeriodicWave(lissPhase, waveformType);
float baseX = blendSpiky * spikyX + (1f - blendSpiky) * periodicX;
float lissX = p.amplitude * baseX;
float baseSweep = sweepX * (1f - eased);
float oscillation = lissX * eased;
float transitionBend = Mathf.Sin(lissPhase) * p.amplitude * 4f * eased * (1f - eased);
float x = baseSweep + oscillation + transitionBend;
// Y: waveform amplitude
float yPhase = p.omegaY * tParam + 0.3f;
float phaseBlend = Smoothstep((0.25f - eased) / 0.08f);
yPhase += time * p.phaseSpeed * phaseBlend;
float spikyY = SpikyWave(yPhase, spikiness);
float periodicY = PeriodicWave(yPhase, waveformType);
float yRaw = blendSpiky * spikyY + (1f - blendSpiky) * periodicY;
// tanh waveshaping
float wsBlend = Smoothstep((waveshapeGain - 1f) / 0.12f);
float y = p.amplitudeY * ((1f - wsBlend) * yRaw + wsBlend * Waveshape(yRaw, Mathf.Max(1.01f, waveshapeGain)));
// Envelope decay
if (envelopeDecay > 0.01f)
{
float envelope = Mathf.Max(0.1f, Mathf.Exp(-tNorm * envelopeDecay * 5f));
y *= envelope;
x *= Mathf.Max(0.5f, Mathf.Exp(-tNorm * envelopeDecay * 1.5f));
}
// Harmonics
if (p.harmonicStrength > 0.001f)
{
float hf = p.harmonicFreq;
float hPhaseX = hf * p.omegaX * tParam + time * p.phaseSpeed * 1.7f;
float hPhaseY = hf * p.omegaY * tParam + time * p.phaseSpeed * 0.6f;
float hx = p.harmonicStrength * PeriodicWave(hPhaseX, waveformType);
float hy = p.harmonicStrength * PeriodicWave(hPhaseY, waveformType);
x += hx * eased;
y += hy;
if (eased < 0.9f) y += hx * (1f - eased) * 0.5f;
}
// Weierstrass fractal
if (weierstrassAmount > 0.001f)
{
int wLayers = Mathf.Clamp(5, 3, 6);
float wx = Weierstrass(lissPhase * 2.1f + time * 0.4f, wLayers);
float wy = Weierstrass(yPhase * 2.3f + time * 0.3f, wLayers);
x += p.amplitude * wx * weierstrassAmount * 0.5f;
y += p.amplitudeY * wy * weierstrassAmount * 0.5f;
}
// Noise
if (p.noiseAmount > 0.001f)
{
float ng = 0f;
if (waveformType > 0.35f)
{
float gt = Mathf.Clamp01((waveformType - 0.35f) / 0.25f);
ng = gt * gt * (3f - 2f * gt);
}
else if (waveMode > 0.2f && waveMode < 0.95f)
{
float raw = (waveMode - 0.2f) * (0.95f - waveMode) * 4.5f;
ng = raw * raw * (3f - 2f * Mathf.Min(1f, raw));
}
if (ng > 0.001f)
{
float nt = tParam * p.noiseScale * 0.3f;
float nTime = time * p.noiseSpeed;
float nx = Noise2D(nt, nTime);
float ny = Noise2D(nt + 37.7f, nTime + 19.3f);
x += nx * p.noiseAmount * ng;
y += ny * p.noiseAmount * ng;
}
}
// Jitter
if (p.jitter > 0.001f)
{
float jg = 0f;
if (waveformType > 0.35f)
{
float jt = Mathf.Clamp01((waveformType - 0.35f) / 0.25f);
jg = jt * jt * (3f - 2f * jt);
}
else if (waveMode > 0.2f && waveMode < 0.95f)
{
float rawJ = (waveMode - 0.2f) * (0.95f - waveMode) * 4.5f;
jg = rawJ * rawJ * (3f - 2f * Mathf.Min(1f, rawJ));
}
if (jg > 0.001f)
{
x += (Random.value - 0.5f) * p.jitter * 2f * jg;
y += (Random.value - 0.5f) * p.jitter * 2f * jg;
}
}
// Breath
x *= totalBreath;
y *= totalBreath;
// Ferrofluid spikes
if (spikeIntensity > 0.01f)
{
float r = Mathf.Sqrt(x * x + y * y);
if (r > 0.001f)
{
float spikeRaw = Mathf.Sin(spikeFreq * tParam + time * 0.5f);
float spike = spikeIntensity * Mathf.Pow(Mathf.Max(0f, spikeRaw), spikeSharpness);
float rNew = r + spike * 0.3f;
x *= rNew / r;
y *= rNew / r;
}
}
// Tip intensity (red highlight)
float tipIntensity = 0f;
if (spikeIntensity > 0.2f)
{
float spikeRawTip = Mathf.Sin(spikeFreq * tParam + time * 0.5f);
float spikeValTip = Mathf.Pow(Mathf.Max(0f, spikeRawTip), spikeSharpness);
if (spikeValTip > 0.5f)
{
tipIntensity = (spikeValTip - 0.5f) / 0.5f;
tipIntensity *= Mathf.Clamp01((spikeIntensity - 0.2f) / 0.4f);
tipIntensity = Mathf.Clamp01(tipIntensity);
}
}
float wsTipBlend = waveshapeGain > 1.8f ? Mathf.Clamp01((waveshapeGain - 1.8f) / 0.4f) : 0f;
wsTipBlend = wsTipBlend * wsTipBlend * (3f - 2f * wsTipBlend);
if (wsTipBlend > 0.001f)
{
float shapedY = Mathf.Abs(Waveshape(yRaw, waveshapeGain));
float wsThreshold = 0.82f;
if (shapedY > wsThreshold)
{
float wsTip = (shapedY - wsThreshold) / (1f - wsThreshold);
wsTip *= Mathf.Clamp01((waveshapeGain - 1.8f) / 2.2f);
wsTip = Mathf.Min(1f, wsTip * wsTipBlend);
tipIntensity = Mathf.Max(tipIntensity, wsTip);
}
}
if (spikiness > 0.2f)
{
float rawAbsY = Mathf.Abs(PeriodicWave(p.omegaY * tParam + 0.3f, waveformType));
float threshold = 0.85f;
if (rawAbsY > threshold)
{
float classicTip = (rawAbsY - threshold) / (1f - threshold);
classicTip *= (spikiness - 0.2f) / 0.8f;
tipIntensity = Mathf.Max(tipIntensity, Mathf.Clamp01(classicTip));
}
}
float yNormVal = y / (p.amplitudeY > 0.001f ? p.amplitudeY : 1f);
_points[i] = new WavePoint
{
worldPos = new Vector3(center.x + x * scaleX, center.y + y * scaleY, z),
t = tParam,
tNorm = tNorm,
tipIntensity = tipIntensity,
yNorm = yNormVal,
rawX = x,
rawY = y
};
}
// --- Draw ---
using (Draw.Command(cam))
{
Draw.ResetAllDrawStates();
Draw.LineGeometry = LineGeometry.Flat2D;
Draw.ThicknessSpace = ThicknessSpace.Meters;
float hue = p.hue;
float sat = p.saturation;
float lit = p.lightness;
float gi = p.glowIntensity * reviewPulse;
float frag = p.fragmentation;
// 6 glow layers (additive)
Draw.BlendMode = ShapesBlendMode.Additive;
float[] glowWMul = { 10f, 6f, 3.5f, 2f, 1f, 0.5f };
float[] glowAMul = { 0.015f, 0.03f, 0.08f, 0.18f, 0.55f, 0.85f };
for (int la = 0; la < glowWMul.Length; la++)
{
float alpha = glowAMul[la] * gi;
if (alpha < 0.003f) continue;
float lw = p.lineWidth * glowWMul[la] * thicknessScale;
float layerLit = glowWMul[la] > 2f ? Mathf.Min(90f, lit + glowWMul[la] * 3f) : lit;
Color layerColor = ColorUtil.HslToRgb(hue, sat, layerLit, alpha);
Draw.Thickness = lw;
for (int i = 0; i < numPoints; i++)
{
// Fragmentation
if (frag > 0.01f)
{
float fragVal = Noise2D(_points[i].t * 1.5f + 5.5f, time * 0.3f + 100f);
if (fragVal > (1f - frag * 1.8f)) continue;
}
// Scan line boost
Color c = layerColor;
if (ri > 0.05f)
{
float distToScan = Mathf.Abs(_points[i].tNorm - _scanLineX);
if (distToScan < 0.03f)
{
float boost = (1f - distToScan / 0.03f) * 0.8f * ri;
c = new Color(
Mathf.Clamp01(c.r * (1f + boost)),
Mathf.Clamp01(c.g * (1f + boost)),
Mathf.Clamp01(c.b * (1f + boost)),
Mathf.Clamp01(c.a * (1f + boost * 0.5f))
);
}
}
Draw.Line(_points[i].worldPos, _points[i + 1].worldPos, c);
}
}
// --- Tip layer (red highlights) ---
float tipLayer = p.tipLayer;
float redTipIntensity = Mathf.Clamp01((tipLayer - 0.4f) / 0.2f);
redTipIntensity = redTipIntensity * redTipIntensity * (3f - 2f * redTipIntensity);
float redCond = Mathf.Clamp01(
Mathf.Max(0f, (spikiness - 0.08f) / 0.2f) +
Mathf.Max(0f, (waveshapeGain - 1.5f) / 0.8f) +
Mathf.Max(0f, (spikeIntensity - 0.08f) / 0.2f));
redTipIntensity *= Mathf.Clamp01(redCond);
if (redTipIntensity > 0.005f)
{
float[] tipWMul = { 4f, 2f, 1f, 0.5f };
float[] tipAMul = { 0.06f, 0.15f, 0.5f, 0.8f };
for (int rl = 0; rl < tipWMul.Length; rl++)
{
Draw.Thickness = p.lineWidth * tipWMul[rl] * thicknessScale;
for (int i = 0; i < numPoints; i++)
{
float tip = Mathf.Max(_points[i].tipIntensity, _points[i + 1].tipIntensity);
if (tip < 0.05f) continue;
float tipHue = (hue <= 30f || hue >= 330f) ? hue : 128f - tip * 128f;
float tipLit = lit + tip * 15f;
float tipAlpha = tipAMul[rl] * gi * tip * redTipIntensity;
Color tipColor = ColorUtil.HslToRgb(tipHue, sat + tip * 20f, tipLit, tipAlpha);
Draw.Line(_points[i].worldPos, _points[i + 1].worldPos, tipColor);
}
}
}
// --- Review overlay ---
if (ri > 0.05f)
{
Draw.BlendMode = ShapesBlendMode.Transparent;
DrawReviewOverlay(cam, center, size, scaleY, z, p, ri, hue, sat, lit, gi, dt);
}
// --- Beam spot ---
Draw.BlendMode = ShapesBlendMode.Additive;
float beamSpeed = p.phaseSpeed * 8f;
float beamPhase = (time * beamSpeed) % totalT;
int beamIdx = Mathf.FloorToInt((beamPhase / totalT) * numPoints);
if (beamIdx >= 0 && beamIdx < _points.Length)
{
float spotR = p.lineWidth * 2.5f * thicknessScale;
float spotLit = Mathf.Min(95f, lit + 35f);
Color spotColor = ColorUtil.HslToRgb(hue, Mathf.Max(0f, sat - 15f), spotLit, 0.9f);
Draw.Disc(_points[beamIdx].worldPos, spotR, DiscColors.Radial(spotColor, Color.clear));
}
}
}
void DrawReviewOverlay(Camera cam, Vector3 center, Vector3 size, float scale,
float z, EmotionWaveConfig.WaveParams p, float ri,
float hue, float sat, float lit, float gi, float dt)
{
float halfW = size.x * 0.5f;
float halfH = size.y * 0.5f;
float trackerRate = p.featureBoxRate;
float thresholdLevel = p.thresholdLevel;
float cursorSpeed = p.cursorSpeed;
// --- Threshold / Limiter lines ---
if (thresholdLevel > 0f && ri > 0.1f)
{
float threshYTop = center.y + thresholdLevel * p.amplitudeY * scale;
float threshYBot = center.y - thresholdLevel * p.amplitudeY * scale;
float threshAlpha = 0.3f * ri;
Color threshColor = new Color(1f, 0.8f, 0.24f, threshAlpha);
Draw.Thickness = 0.008f;
// Dashed lines (simulated)
float dashLen = halfW * 0.03f;
float gapLen = dashLen * 0.67f;
float xStart = center.x - halfW;
float xEnd = center.x + halfW;
for (float dx = xStart; dx < xEnd; dx += dashLen + gapLen)
{
float dxEnd = Mathf.Min(dx + dashLen, xEnd);
Draw.Line(new Vector3(dx, threshYTop, z), new Vector3(dxEnd, threshYTop, z), threshColor);
Draw.Line(new Vector3(dx, threshYBot, z), new Vector3(dxEnd, threshYBot, z), threshColor);
}
// Arrow indicators
float arrowSize = 0.02f;
Color arrowColor = new Color(1f, 0.8f, 0.24f, threshAlpha * 1.5f);
Vector3 arrowLeft = new Vector3(center.x - halfW + 0.01f, 0, z);
// Top arrow
arrowLeft.y = threshYTop;
Draw.Triangle(
arrowLeft + new Vector3(0, arrowSize, 0),
arrowLeft + new Vector3(arrowSize * 2f, 0, 0),
arrowLeft + new Vector3(0, -arrowSize, 0),
arrowColor);
// Bottom arrow
arrowLeft.y = threshYBot;
Draw.Triangle(
arrowLeft + new Vector3(0, arrowSize, 0),
arrowLeft + new Vector3(arrowSize * 2f, 0, 0),
arrowLeft + new Vector3(0, -arrowSize, 0),
arrowColor);
// Highlight wave segments exceeding threshold
for (int i = 0; i < numPoints; i++)
{
float py = _points[i].worldPos.y;
bool exceeds = py > threshYTop || py < threshYBot;
if (!exceeds) continue;
float exceedAmount;
if (py > threshYTop)
exceedAmount = (py - threshYTop) / Mathf.Max(0.001f, threshYTop - center.y);
else
exceedAmount = (threshYBot - py) / Mathf.Max(0.001f, center.y - threshYBot);
exceedAmount = Mathf.Clamp01(exceedAmount);
float exHue = 40f - exceedAmount * 40f;
float exAlpha = (0.25f + exceedAmount * 0.45f) * ri;
Color exColor = ColorUtil.HslToRgb(exHue, 90f, 55f, exAlpha);
Draw.Thickness = p.lineWidth * 1.8f * thicknessScale;
Draw.Line(_points[i].worldPos, _points[i + 1].worldPos, exColor);
}
}
// --- Scan line (vertical) ---
if (ri > 0.1f)
{
float scanX = center.x - halfW + _scanLineX * size.x;
float scanAlpha = 0.25f * ri;
Color scanColor = ColorUtil.HslToRgb(128f, 80f, 60f, scanAlpha);
Draw.Thickness = 0.005f;
Draw.Line(new Vector3(scanX, center.y - halfH, z), new Vector3(scanX, center.y + halfH, z), scanColor);
// Scan line glow (wider, fainter)
Color scanGlow = ColorUtil.HslToRgb(128f, 80f, 60f, scanAlpha * 0.3f);
Draw.Thickness = 0.04f;
Draw.Line(new Vector3(scanX, center.y - halfH, z), new Vector3(scanX, center.y + halfH, z), scanGlow);
}
// --- Measurement cursor ---
if (cursorSpeed > 0.001f && ri > 0.1f)
{
float cursorScreenX = center.x - halfW + _cursorX * size.x;
float cursorAlpha = 0.25f * ri;
Color cursorColor = new Color(0.4f, 1f, 0.7f, cursorAlpha);
// Vertical dashed line
Draw.Thickness = 0.004f;
float dashLen = halfH * 0.04f;
float gapLen = dashLen * 1.5f;
for (float dy = center.y - halfH; dy < center.y + halfH; dy += dashLen + gapLen)
{
float dyEnd = Mathf.Min(dy + dashLen, center.y + halfH);
Draw.Line(new Vector3(cursorScreenX, dy, z), new Vector3(cursorScreenX, dyEnd, z), cursorColor);
}
// Find closest wave point
int closestIdx = -1;
float closestDist = float.MaxValue;
for (int i = 0; i <= numPoints; i++)
{
float dist = Mathf.Abs(_points[i].worldPos.x - cursorScreenX);
if (dist < closestDist) { closestDist = dist; closestIdx = i; }
}
if (closestIdx >= 0 && closestDist < size.x * 0.05f)
{
Vector3 cp = _points[closestIdx].worldPos;
float crossSize = 0.02f;
Color crossColor = new Color(0.4f, 1f, 0.7f, cursorAlpha * 2.5f);
Draw.Thickness = 0.003f;
Draw.Line(cp + Vector3.left * crossSize, cp + Vector3.right * crossSize, crossColor);
Draw.Line(cp + Vector3.down * crossSize, cp + Vector3.up * crossSize, crossColor);
Draw.Disc(cp, 0.006f, crossColor);
}
}
// --- Blob Trackers ---
if (trackerRate > 0.01f)
{
float trackerInterval = 1f / trackerRate;
if (time - _lastTrackerTime > trackerInterval && _trackerCount < 8)
{
// Find high-curvature point
int bestIdx = numPoints / 2;
float bestCurv = 0f;
for (int ci = 2; ci < numPoints - 1; ci++)
{
float curvX = Mathf.Abs(_points[ci - 1].worldPos.x + _points[ci + 1].worldPos.x - 2f * _points[ci].worldPos.x);
float curvY = Mathf.Abs(_points[ci - 1].worldPos.y + _points[ci + 1].worldPos.y - 2f * _points[ci].worldPos.y);
float curv = curvX + curvY;
if (curv > bestCurv) { bestCurv = curv; bestIdx = ci; }
}
if (_trackers.Length < 8) _trackers = new BlobTracker[8];
_trackers[_trackerCount] = new BlobTracker
{
pointIndex = bestIdx,
birth = time,
life = 1.5f + Random.value
};
_trackerCount++;
_lastTrackerTime = time;
}
// Draw trackers
int writeIdx = 0;
for (int ti = 0; ti < _trackerCount; ti++)
{
var tr = _trackers[ti];
float age = time - tr.birth;
if (age >= tr.life) continue;
float lifeRatio = age / tr.life;
float tAlpha = 1f;
if (age < 0.15f) tAlpha = age / 0.15f;
else if (lifeRatio > 0.75f) tAlpha = 1f - (lifeRatio - 0.75f) / 0.25f;
tAlpha *= ri;
tAlpha = Mathf.Clamp01(tAlpha);
int pIdx = Mathf.Min(tr.pointIndex, numPoints);
Vector3 px = _points[pIdx].worldPos;
float flashBoost = age < 0.1f ? 1.5f : 1f;
// Glow disc
Color dotGlow = new Color(0f, 1f, 0.47f, 0.15f * tAlpha);
Draw.Disc(px, 0.03f, DiscColors.Radial(new Color(0f, 1f, 0.47f, 0.7f * tAlpha * flashBoost), Color.clear));
// Core dot
Color dotCore = new Color(0f, 1f, 0.47f, 0.9f * tAlpha * flashBoost);
Draw.Disc(px, 0.008f, dotCore);
// Leader line
float lineDir = px.y < center.y ? -1f : 1f;
float lineLen = 0.04f;
Color lineColor = new Color(0f, 1f, 0.47f, 0.25f * tAlpha);
Draw.Thickness = 0.002f;
Draw.Line(px + Vector3.up * lineDir * 0.01f, px + Vector3.up * lineDir * lineLen, lineColor);
_trackers[writeIdx++] = tr;
}
_trackerCount = writeIdx;
}
// --- Peak markers ---
if (ri > 0.15f)
{
_peakCount = 0;
for (int i = 2; i < numPoints - 1 && _peakCount < 12; i++)
{
bool isMax = _points[i].worldPos.y > _points[i - 1].worldPos.y && _points[i].worldPos.y > _points[i + 1].worldPos.y;
bool isMin = _points[i].worldPos.y < _points[i - 1].worldPos.y && _points[i].worldPos.y < _points[i + 1].worldPos.y;
if (!isMax && !isMin) continue;
float peakDist = Mathf.Abs(_points[i].worldPos.y - center.y);
if (peakDist < scale * 0.15f) continue;
_peakMarkers[_peakCount++] = new PeakMarker { pos = _points[i].worldPos, isMax = isMax };
}
float peakAlpha = 0.35f * ri;
Color peakColor = new Color(0f, 1f, 0.47f, peakAlpha);
float triSize = 0.012f;
for (int pi = 0; pi < _peakCount; pi++)
{
var pk = _peakMarkers[pi];
if (pk.isMax)
{
Draw.Triangle(
pk.pos + new Vector3(-triSize, triSize * 2f + 0.005f, 0),
pk.pos + new Vector3(triSize, triSize * 2f + 0.005f, 0),
pk.pos + new Vector3(0, 0.005f, 0),
peakColor);
}
else
{
Draw.Triangle(
pk.pos + new Vector3(-triSize, -triSize * 2f - 0.005f, 0),
pk.pos + new Vector3(triSize, -triSize * 2f - 0.005f, 0),
pk.pos + new Vector3(0, -0.005f, 0),
peakColor);
}
}
}
// --- Frequency spectrum bars ---
if (ri > 0.05f)
DrawFrequencyBars(center, size, z, hue, sat, lit, ri);
}
void DrawFrequencyBars(Vector3 center, Vector3 size, float z,
float hue, float sat, float lit, float ri)
{
int numFreqs = 8;
float[] amps = new float[numFreqs];
int step = Mathf.Max(1, numPoints / 128);
int sampleN = numPoints / step;
for (int f = 0; f < numFreqs; f++)
{
float cosSum = 0f, sinSum = 0f;
int freq = f + 1;
for (int si = 0; si < sampleN; si++)
{
int idx = si * step;
if (idx > numPoints) break;
float val = _points[idx].rawY;
float angle = 2f * Mathf.PI * freq * si / sampleN;
cosSum += val * Mathf.Cos(angle);
sinSum += val * Mathf.Sin(angle);
}
cosSum /= sampleN;
sinSum /= sampleN;
amps[f] = 2f * Mathf.Sqrt(cosSum * cosSum + sinSum * sinSum);
}
float maxAmp = 0f;
for (int i = 0; i < numFreqs; i++)
if (amps[i] > maxAmp) maxAmp = amps[i];
if (maxAmp < 0.001f) return;
_spectrumScanIdx = Mathf.FloorToInt(time * 1.2f) % numFreqs;
float halfW = size.x * 0.5f;
float halfH = size.y * 0.5f;
float chartW = Mathf.Min(halfW * 0.4f, 0.15f);
float chartH = Mathf.Min(halfH * 0.3f, 0.06f);
float chartX = center.x + halfW - chartW - 0.02f;
float chartY = center.y - halfH + 0.02f;
// Background
Color bgColor = new Color(0, 0, 0, 0.5f * ri);
Draw.Rectangle(new Vector3(chartX + chartW * 0.5f, chartY + chartH * 0.5f, z),
new Vector2(chartW + 0.01f, chartH + 0.01f), bgColor);
// Border
Draw.Thickness = 0.001f;
Color borderColor = ColorUtil.HslToRgb(hue, sat, lit, 0.2f * ri);
float bx0 = chartX - 0.005f;
float by0 = chartY - 0.005f;
float bx1 = chartX + chartW + 0.005f;
float by1 = chartY + chartH + 0.005f;
Draw.Line(new Vector3(bx0, by0, z), new Vector3(bx1, by0, z), borderColor);
Draw.Line(new Vector3(bx1, by0, z), new Vector3(bx1, by1, z), borderColor);
Draw.Line(new Vector3(bx1, by1, z), new Vector3(bx0, by1, z), borderColor);
Draw.Line(new Vector3(bx0, by1, z), new Vector3(bx0, by0, z), borderColor);
float barGap = 0.002f;
float barW = Mathf.Max(0.005f, (chartW - (numFreqs - 1) * barGap) / numFreqs);
for (int i = 0; i < numFreqs; i++)
{
float normAmp = amps[i] / maxAmp;
float barH = normAmp * chartH * 0.9f;
float bx = chartX + i * (barW + barGap);
float by = chartY;
bool isScanned = i == _spectrumScanIdx;
float barAlpha = isScanned ? 0.8f * ri : 0.35f * ri;
float barLit = isScanned ? lit + 15f : lit;
Color barColor = ColorUtil.HslToRgb(hue, sat, barLit, barAlpha);
Draw.Rectangle(new Vector3(bx + barW * 0.5f, by + barH * 0.5f, z),
new Vector2(barW, barH), barColor);
if (isScanned)
{
Color markColor = ColorUtil.HslToRgb(hue, sat, lit + 25f, 0.7f * ri);
float mx = bx + barW * 0.5f;
float my = by + barH + 0.005f;
Draw.Triangle(
new Vector3(mx - 0.005f, my + 0.008f, z),
new Vector3(mx + 0.005f, my + 0.008f, z),
new Vector3(mx, my, z),
markColor);
}
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c4c0a33f1933d714aa92cba0bff102bd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,159 @@
using System.Collections;
using AibisDream.FixSystem;
using UnityEngine;
using Yarn.Unity;
namespace AibisDream.MiniGame.HuoShan.EmotionWave
{
/// <summary>
/// 情绪波形系统的 Yarn 命令接口
///
/// 功能:
/// - 显示/隐藏:show_emotion_wave, hide_emotion_wave
/// - 进度控制:emotion_wave_to(动画到指定进度),set_emotion_wave(立即设置)
/// - 状态查询:is_emotion_wave_shown()
/// </summary>
public static class EmotionWaveYarnCommand
{
private static EmotionWaveManager GetManager()
{
return FixSystemCenter.SystemDic.Get<EmotionWaveManager>();
}
#region /
/// <summary>
/// 显示情绪波形
/// 用法: <<show_emotion_wave>>
/// <<show_emotion_wave 0.8>>
/// </summary>
[YarnCommand("show_emotion_wave")]
public static IEnumerator ShowEmotionWave(float duration = -1f)
{
var manager = GetManager();
if (manager != null)
{
yield return manager.Show(duration);
}
}
/// <summary>
/// 隐藏情绪波形
/// 用法: <<hide_emotion_wave>>
/// <<hide_emotion_wave 0.5>>
/// </summary>
[YarnCommand("hide_emotion_wave")]
public static IEnumerator HideEmotionWave(float duration = -1f)
{
var manager = GetManager();
if (manager != null)
{
yield return manager.Hide(duration);
}
}
#endregion
#region
/// <summary>
/// 动画到指定进度(以指定时间动画到指定进度值,完成后返回给 Yarn)
/// 用法: <<emotion_wave_to 0.5 2>> -- 2秒内动画到进度0.5
/// <<emotion_wave_to 1.0>> -- 默认1秒内动画到进度1.0
/// </summary>
[YarnCommand("emotion_wave_to")]
public static IEnumerator EmotionWaveTo(float progress, float duration = 1f)
{
var manager = GetManager();
if (manager != null)
{
yield return manager.AnimateToProgress(progress, duration);
}
}
/// <summary>
/// 立即设置进度(不等待)
/// 用法: <<set_emotion_wave 0.5>>
/// </summary>
[YarnCommand("set_emotion_wave")]
public static void SetEmotionWave(float progress)
{
var manager = GetManager();
if (manager != null)
{
manager.SetProgress(progress);
}
}
#endregion
#region
/// <summary>
/// 检查情绪波形是否已显示
/// 用法: <<if is_emotion_wave_shown()>>
/// </summary>
[YarnFunction("is_emotion_wave_shown")]
public static bool IsEmotionWaveShown()
{
var manager = GetManager();
return manager != null && manager.IsShown;
}
/// <summary>
/// 获取当前进度
/// 用法: <<set $progress to get_emotion_wave_progress()>>
/// </summary>
[YarnFunction("get_emotion_wave_progress")]
public static float GetEmotionWaveProgress()
{
var manager = GetManager();
return manager != null ? manager.CurrentProgress : 0f;
}
/// <summary>
/// 获取目标进度
/// 用法: <<set $target to get_emotion_wave_target()>>
/// </summary>
[YarnFunction("get_emotion_wave_target")]
public static float GetEmotionWaveTarget()
{
var manager = GetManager();
return manager != null ? manager.TargetProgress : 0f;
}
#endregion
#region Slider
/// <summary>
/// 启用滑片同步(情绪波形进度跟随 SliderController
/// 用法: <<enable_emotion_wave_slider_sync>>
/// </summary>
[YarnCommand("enable_emotion_wave_slider_sync")]
public static void EnableSliderSync()
{
var manager = GetManager();
if (manager != null)
{
manager.SetSliderSyncEnabled(true);
}
}
/// <summary>
/// 禁用滑片同步(情绪波形进度由 Yarn 命令控制)
/// 用法: <<disable_emotion_wave_slider_sync>>
/// </summary>
[YarnCommand("disable_emotion_wave_slider_sync")]
public static void DisableSliderSync()
{
var manager = GetManager();
if (manager != null)
{
manager.SetSliderSyncEnabled(false);
}
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1afa5722e9c3ef647b6b4461f2e959f2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,94 @@
# EmotionWave 与 SliderController 同步使用说明
## 概述
`EmotionWaveManager` 可以自动同步 `SliderController` 的进度,实现滑片位置驱动情绪波形进度的效果。
## 设置方法
### 1. Inspector 配置
`EmotionWaveManager` 组件的 Inspector 中:
```
[Header("Slider Sync")]
- Slider Controller: 拖入场景中的 SliderController 组件
- Enable Slider Sync: 勾选启用滑片同步(默认启用)
```
### 2. 工作原理
`Enable Slider Sync` 勾选时:
- `SliderController``onSliderPositionChanged` 事件会自动触发
- `EmotionWaveManager` 接收到事件后更新 `targetProgress`
- `Update` 中的 smoothstep lerp 会平滑追踪到新进度
- 最终传递给 `EmotionWaveController` 进行渲染
### 3. Yarn 脚本控制
#### 启用/禁用同步
```yarn
// 启用滑片同步(情绪波形跟随滑片)
<<enable_emotion_wave_slider_sync>>
// 禁用滑片同步(情绪波形由 Yarn 命令独立控制)
<<disable_emotion_wave_slider_sync>>
```
#### 典型使用场景
```yarn
// 场景1: 显示情绪波形,同时启用滑片同步
<<show_emotion_wave>>
<<enable_emotion_wave_slider_sync>>
// 玩家操作滑片时,情绪波形自动跟随
// ...
// 场景2: 禁用同步,手动控制进度
<<disable_emotion_wave_slider_sync>>
<<emotion_wave_to 0.5 2>> // 2秒动画到进度0.5
// 场景3: 立即设置进度(跳过动画)
<<set_emotion_wave 0.75>>
```
## 事件流程
```
SliderController.SlideToNormalized(0.5f)
SliderController._currentAngle 更新
SliderController.onSliderPositionChanged.Invoke(0.5f)
EmotionWaveManager.OnSliderPositionChanged(0.5f)
EmotionWaveManager.targetProgress = 0.5f
EmotionWaveManager.Update() - smoothstep lerp 追踪
EmotionWaveManager.currentProgress 平滑到 0.5f
EmotionWaveController.Progress = currentProgress
EmotionWaveRenderer 渲染对应阶段的波形
```
## 注意事项
1. **优先级**:当滑片同步启用时,Yarn 命令的 `emotion_wave_to``set_emotion_wave` 仍然有效,但会与滑片进度产生竞争。建议:
- 滑片控制时:启用同步
- Yarn 自动演出时:禁用同步
2. **平滑追踪**:无论进度来源(滑片/Yarn),都会经过 smoothstep lerp 平滑处理(lerpSpeed = 4.0
3. **性能**:事件机制确保只在滑片位置实际改变时触发,不会每帧都更新
## 调试
在 Inspector 的 Debug 区域可以实时查看:
- `Current Progress`:当前平滑后的进度
- `Target Progress`:目标进度(来自滑片或 Yarn
- `Is Shown`:是否显示状态
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 028c214d27760e64a9796858c85940fa
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: