Files
aibis-dream/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs
T

762 lines
28 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 System.Collections;
using DG.Tweening;
using UnityEngine;
using AibisDream.FixSystem;
using AibisDream.MiniGame.Language;
using System.Collections.Generic;
namespace AibisDream
{
/// <summary>
/// 表达系统顶层管理器(原 ExpressionManager,现改为语言粒子系统)
/// </summary>
public class ExpressionManager : MonoBehaviour
{
[Header("系统组件")]
[SerializeField] private LanguageParticleManager particleManager;
[SerializeField] private GameObject expressionView;
[SerializeField] private LogReleasePresentationController logReleasePresentation;
[Header("默认配置")]
[SerializeField] private List<string> defaultAnxietyPhrases = new List<string>
{
"哈哈",
"嘿嘿",
"呵呵"
};
[SerializeField] private string defaultTargetSentence = "这是一个测试示例";
[Header("Sprite 淡入淡出")]
[SerializeField] private SpriteRenderer screenOverlayRenderer;
[SerializeField] private SpriteRenderer volcanoOverlayRenderer;
[SerializeField, Range(0f, 1f)] private float spriteOverlayFadeInAlpha = 1f;
[Tooltip("屏幕淡入时的亮度倍率。大于 1 的部分由 Bloom 提取,Sprite 原本的绿色保持不变。")]
[SerializeField, Min(1f)] private float screenBloomBrightness = 1.35f;
[Header("Glitch 噪波效果")]
[Tooltip("记忆/对白阶段的透明噪波叠层(glitch_transition)。expression_screen_glitch 已改走屏幕 Shader。")]
[SerializeField] private SpriteNoiseGlitchController glitchController;
[Header("屏幕 Glitch Shader")]
[Tooltip("仅在屏幕淡入 / expression_screen_glitch 时套用;小游戏阶段保持默认 Sprite 材质,避免进 VolFx 盖住粒子")]
[SerializeField] private Material screenGlitchMaterial;
// 观感全部在材质 M_HuoshanScreenGlitch 上调(只有 6 个宏观旋钮:Scale / ColorDamage /
// Warp / Chaos / Grit / SurgeRate)。代码这边只驱动 _GlitchIntensity 档位。
// 不要再把观感参数搬回 Inspector——两处都能改就等于没有真相源,上一版 40 个属性
// 分散在材质和组件两边,结果谁也调不动。
public LogReleasePresentationController LogReleasePresentation => logReleasePresentation;
/// <summary>当前屏幕 Glitch 档位(01),由 expression_screen_glitch 持续保持。</summary>
public float ScreenGlitchLevel => screenGlitchIntensity;
private static readonly int ScreenGlitchIntensityId = Shader.PropertyToID("_GlitchIntensity");
private Material screenDefaultMaterial;
private Material screenFallbackMaterial;
private Material screenGlitchMaterialInstance;
private Coroutine screenGlitchRampRoutine;
private float screenGlitchIntensity;
private bool screenGlitchMaterialActive;
void Start()
{
// 注册到系统字典
FixSystemCenter.SystemDic.Register(this);
// 查找并初始化表达视图
if (expressionView == null)
{
expressionView = transform.Find("Expression View")?.gameObject;
}
if (expressionView != null)
{
expressionView.SetActive(false);
}
// 查找粒子管理器
if (particleManager == null)
{
particleManager = GetComponentInChildren<LanguageParticleManager>(true);
}
if (glitchController == null)
{
glitchController = GetComponentInChildren<SpriteNoiseGlitchController>(true);
}
// glitch_transition 用的 noise:必须停在 Default,绝不能进 HuoshanScreen(否则会叠进 VolFx 盖住粒子)。
PrepareNoiseOverlayForGlitchTransition(false);
CacheScreenDefaultMaterial();
// 小游戏阶段绝不能提前套 Glitch 材质:其 SRPDefaultUnlit 会进 VolFx huoshan 合成并盖住粒子。
RestoreScreenDefaultMaterial();
if (logReleasePresentation == null)
{
logReleasePresentation = GetComponent<LogReleasePresentationController>();
}
if (particleManager != null)
{
particleManager.SetPresentationController(logReleasePresentation);
}
}
/// <summary>
/// 打开表达视图(只显示美术背景,不启动粒子系统)
/// 先设置所有初始状态,再打开视图
/// </summary>
public void OpenView()
{
// 先设置所有初始状态再打开
if (particleManager != null)
{
particleManager.SetInitialStatesBeforeOpen();
}
if (expressionView != null)
{
expressionView.SetActive(true);
}
}
/// <summary>
/// 启动粒子系统(在打开视图后调用,通常通过 Yarn 对话控制)
/// </summary>
/// <param name="anxietyPhrases">干扰短句列表(笑话等,非目标粒子从中随机取字符)</param>
/// <param name="targetSentence">目标句子</param>
/// <param name="completionNodeName">完成时触发的对话节点名称(可选)</param>
/// <param name="nonTargetParticleTotal">非目标候选粒子总数;&lt;0 时使用 Inspector 默认候选池大小</param>
public void StartSystem(List<string> anxietyPhrases, string targetSentence, string completionNodeName = null, int nonTargetParticleTotal = -1)
{
HideSpriteOverlaysForMinigame();
if (particleManager != null)
{
particleManager.SetPresentationController(logReleasePresentation);
// 使用传入的配置,如果没有则使用默认配置
var phrases = anxietyPhrases ?? defaultAnxietyPhrases;
var sentence = targetSentence ?? defaultTargetSentence;
particleManager.InitializeSystem(phrases, sentence, completionNodeName, nonTargetParticleTotal);
}
}
private void HideSpriteOverlaysForMinigame()
{
if (screenGlitchRampRoutine != null)
{
StopCoroutine(screenGlitchRampRoutine);
screenGlitchRampRoutine = null;
}
screenGlitchIntensity = 0f;
// 粒子玩法必须回到默认 Sprite 材质,否则屏幕会进 VolFx 层把字盖掉。
RestoreScreenDefaultMaterial();
List<SpriteRenderer> renderers = CollectSpriteOverlayRenderers();
for (int i = 0; i < renderers.Count; i++)
{
SpriteRenderer sr = renderers[i];
if (sr == null) continue;
DOTween.Kill(sr);
if (IsScreenOverlayRenderer(sr))
{
LockScreenOverlayRgb(sr, 0f);
}
else
{
Color color = sr.color;
color.a = 0f;
sr.color = color;
}
sr.enabled = false;
sr.gameObject.SetActive(false);
}
}
/// <summary>
/// 启动粒子系统(字符串数组版本,方便 Yarn 调用)
/// </summary>
public void StartSystem(string[] anxietyPhrases, string targetSentence, string completionNodeName = null, int nonTargetParticleTotal = -1)
{
List<string> phrasesList = anxietyPhrases != null ? new List<string>(anxietyPhrases) : null;
StartSystem(phrasesList, targetSentence, completionNodeName, nonTargetParticleTotal);
}
/// <summary>
/// 若火山释放 log 界面已淡出,则先淡入再返回(start_expression 时调用)
/// </summary>
/// <param name="duration">淡入时长(秒)</param>
public IEnumerator EnsureReleaseLogFadedIn(float duration = 1f)
{
if (particleManager == null) yield break;
yield return particleManager.FadeInReleaseLogIfNeeded(duration);
}
/// <summary>
/// 再次 <c>start_expression</c> 前淡出上一轮粒子与连线(若尚未初始化则立即结束)。
/// duration≤0 时使用 LanguageParticleManager 上的默认时长。
/// </summary>
public IEnumerator FadeOutParticlesBeforeNewRound(float duration = 0f)
{
if (particleManager == null) yield break;
yield return StartCoroutine(particleManager.FadeOutBeforeNewExpressionRound(duration));
}
public IEnumerator PreparePresentationForNextRound()
{
if (logReleasePresentation == null) yield break;
yield return logReleasePresentation.PrepareForNextRound();
}
/// <summary>
/// 等待表达流程就绪(开场表现 + 火山表达panel 播完)
/// </summary>
public IEnumerator WaitUntilExpressionFlowReady()
{
if (particleManager == null) yield break;
yield return particleManager.WaitUntilExpressionFlowReady();
}
/// <summary>
/// 从聚焦保持态触发稳定化(概率递增 → 文字锁定 → 排列)
/// </summary>
public void ResolveFocusSequence()
{
if (particleManager != null)
{
particleManager.ResolveFocusSequence();
}
}
/// <summary>
/// 触发聚焦稳定化并等待全流程完成
/// </summary>
public IEnumerator ResolveFocusAndWait()
{
if (particleManager == null) yield break;
particleManager.ResolveFocusSequence();
yield return particleManager.WaitUntilFocusFinished();
}
public IEnumerator PlayFocusInterferenceAndWait(
string direction,
float duration,
string characterPool,
string colorHex,
float shakeAmplitude,
float shakeSpeed,
float changeInterval,
float transitionRatio)
{
if (particleManager == null) yield break;
yield return particleManager.PlayFocusInterferenceAndWait(
direction,
duration,
characterPool,
colorHex,
shakeAmplitude,
shakeSpeed,
changeInterval,
transitionRatio);
}
/// <summary>
/// 开发预览:跳过玩法,直接进入 FocusHolding(目标字已就位)。
/// </summary>
public IEnumerator SkipToFocusHoldingPreview()
{
if (particleManager == null)
yield break;
if (!particleManager.IsInitialized)
yield break;
yield return particleManager.WaitUntilExpressionFlowReady();
particleManager.DebugCompleteTargets();
float timeout = 8f;
float elapsed = 0f;
while (elapsed < timeout &&
particleManager.CurrentCompletionPhase != CompletionPhase.FocusHolding &&
particleManager.CurrentCompletionPhase != CompletionPhase.Focusing)
{
elapsed += Time.deltaTime;
yield return null;
}
while (particleManager.CurrentCompletionPhase == CompletionPhase.Focusing)
yield return null;
}
/// <summary>
/// 关闭表达系统
/// </summary>
public void CloseView()
{
if (screenGlitchRampRoutine != null)
{
StopCoroutine(screenGlitchRampRoutine);
screenGlitchRampRoutine = null;
}
SetScreenGlitchIntensity(0f);
if (logReleasePresentation != null)
{
logReleasePresentation.ForceCleanupImmediate(true);
}
if (expressionView != null)
{
expressionView.SetActive(false);
}
if (particleManager != null)
{
particleManager.StopSystem();
}
}
/// <summary>
/// 重置系统(保持当前配置)
/// </summary>
public void ResetSystem(bool playEntranceEffect = true)
{
if (logReleasePresentation != null)
{
logReleasePresentation.ForceCleanupImmediate(true);
}
if (particleManager != null)
{
particleManager.ResetGame(playEntranceEffect);
}
}
/// <summary>
/// 淡入 Sprite 图片(屏幕、火山同步)
/// </summary>
/// <param name="duration">淡入时长(秒)</param>
public IEnumerator FadeInSpriteOverlay(float duration = 1f)
{
List<SpriteRenderer> renderers = CollectSpriteOverlayRenderers();
if (renderers.Count == 0) yield break;
float fadeDuration = Mathf.Max(0.01f, duration);
float targetAlpha = Mathf.Clamp01(spriteOverlayFadeInAlpha);
Sequence sequence = DOTween.Sequence().SetTarget(this);
for (int i = 0; i < renderers.Count; i++)
{
SpriteRenderer sr = renderers[i];
DOTween.Kill(sr);
PrepareOverlayRendererForFade(sr, 0f);
sr.enabled = true;
sr.gameObject.SetActive(true);
sequence.Join(sr.DOFade(targetAlpha, fadeDuration).SetEase(Ease.OutQuad));
}
// 淡入演出屏幕时才启用 Glitch 材质;强度保持 0,外观与普通屏幕一致。
ActivateScreenGlitchMaterial();
ApplyScreenGlitchProperties();
yield return sequence.WaitForCompletion();
for (int i = 0; i < renderers.Count; i++)
LockScreenOverlayRgb(renderers[i], renderers[i] != null ? renderers[i].color.a : 0f);
}
/// <summary>
/// 淡出 Sprite 图片(屏幕、火山同步)
/// </summary>
/// <param name="duration">淡出时长(秒)</param>
public IEnumerator FadeOutSpriteOverlay(float duration = 1f)
{
List<SpriteRenderer> renderers = CollectSpriteOverlayRenderers();
if (renderers.Count == 0) yield break;
float fadeDuration = Mathf.Max(0.01f, duration);
// Glitch 档位是保持型的:屏幕淡出时一并收掉,免得下一段淡入时带着上一段的故障。
SetScreenGlitchLevel(0f, fadeDuration);
Sequence sequence = DOTween.Sequence().SetTarget(this);
for (int i = 0; i < renderers.Count; i++)
{
SpriteRenderer sr = renderers[i];
DOTween.Kill(sr);
LockScreenOverlayRgb(sr, sr.color.a);
sr.enabled = true;
sr.gameObject.SetActive(true);
sequence.Join(sr.DOFade(0f, fadeDuration).SetEase(Ease.InQuad));
}
yield return sequence.WaitForCompletion();
for (int i = 0; i < renderers.Count; i++)
{
SpriteRenderer sr = renderers[i];
if (sr == null) continue;
LockScreenOverlayRgb(sr, 0f);
sr.enabled = false;
sr.gameObject.SetActive(false);
}
}
private List<SpriteRenderer> CollectSpriteOverlayRenderers()
{
var result = new List<SpriteRenderer>();
if (screenOverlayRenderer != null)
result.Add(screenOverlayRenderer);
if (volcanoOverlayRenderer != null && volcanoOverlayRenderer != screenOverlayRenderer)
result.Add(volcanoOverlayRenderer);
return result;
}
private bool IsScreenOverlayRenderer(SpriteRenderer sr)
{
return sr != null && sr == screenOverlayRenderer;
}
private void PrepareOverlayRendererForFade(SpriteRenderer sr, float alpha)
{
if (sr == null) return;
if (IsScreenOverlayRenderer(sr))
{
LockScreenOverlayRgb(sr, alpha);
return;
}
Color color = sr.color;
color.a = alpha;
sr.color = color;
}
private void LockScreenOverlayRgb(SpriteRenderer sr, float alpha)
{
if (!IsScreenOverlayRenderer(sr)) return;
Color locked = Color.white * screenBloomBrightness;
locked.a = Mathf.Clamp01(alpha);
sr.color = locked;
}
private void CacheScreenDefaultMaterial()
{
if (screenOverlayRenderer == null || screenDefaultMaterial != null)
return;
var current = screenOverlayRenderer.sharedMaterial;
// 若场景里误挂了 Glitch 材质,仍回退到 URP 默认 Sprite,保证小游戏可见。
if (current != null && current.shader != null &&
current.shader.name == "AibisDream/HuoshanScreenGlitch")
{
screenDefaultMaterial = null;
return;
}
screenDefaultMaterial = current;
}
private void RestoreScreenDefaultMaterial()
{
if (screenOverlayRenderer == null)
return;
CacheScreenDefaultMaterial();
screenGlitchMaterialActive = false;
screenGlitchIntensity = 0f;
if (screenDefaultMaterial != null)
{
screenOverlayRenderer.sharedMaterial = screenDefaultMaterial;
return;
}
if (screenOverlayRenderer.sharedMaterial != null &&
screenOverlayRenderer.sharedMaterial.shader != null &&
screenOverlayRenderer.sharedMaterial.shader.name == "AibisDream/HuoshanScreenGlitch")
{
// 场景直接挂了 Glitch 材质时要换掉,否则它的 SRPDefaultUnlit 会进 VolFx 合成盖住粒子。
// 绝不能置 null——SpriteRenderer 的 material 为 null 会渲染成品红方块,
// 表现为 switch_fix_system_to Expression 后闪一下粉色。
screenOverlayRenderer.sharedMaterial = GetScreenFallbackMaterial();
}
}
/// <summary>
/// 场景没有提供干净的屏幕材质时的兜底:标准 Sprite 材质。
/// </summary>
private Material GetScreenFallbackMaterial()
{
if (screenFallbackMaterial != null)
return screenFallbackMaterial;
Shader spriteShader = Shader.Find("Sprites/Default");
if (spriteShader == null)
{
Debug.LogError("[ExpressionManager] 找不到 Sprites/Default;屏幕材质无法回退。", this);
return null;
}
screenFallbackMaterial = new Material(spriteShader) { name = "Screen Sprite Fallback" };
return screenFallbackMaterial;
}
private void ActivateScreenGlitchMaterial()
{
if (screenOverlayRenderer == null)
return;
CacheScreenDefaultMaterial();
if (screenGlitchMaterial == null)
{
Debug.LogError(
"[ExpressionManager] 未找到屏幕 Glitch 材质;请把 M_HuoshanScreenGlitch 赋给 screenGlitchMaterial。",
this);
return;
}
if (screenGlitchMaterialInstance == null)
{
screenGlitchMaterialInstance = new Material(screenGlitchMaterial)
{
name = screenGlitchMaterial.name + " (Screen Instance)"
};
}
screenOverlayRenderer.sharedMaterial = screenGlitchMaterialInstance;
screenGlitchMaterialActive = true;
ApplyScreenGlitchProperties();
}
private void ApplyScreenGlitchProperties()
{
if (!screenGlitchMaterialActive || screenGlitchMaterialInstance == null)
return;
// 只驱动强度档位;其余观感是材质的事,见 lookLivesOnMaterial。
screenGlitchMaterialInstance.SetFloat(ScreenGlitchIntensityId, screenGlitchIntensity);
}
private void SetScreenGlitchIntensity(float value)
{
screenGlitchIntensity = Mathf.Clamp01(value);
if (screenGlitchIntensity > 0f && !screenGlitchMaterialActive)
ActivateScreenGlitchMaterial();
ApplyScreenGlitchProperties();
}
/// <summary>
/// 恢复 glitch_transition 用的 noise 叠层到原始层级关系:
/// Default 层 + sortingOrder 10 + OpaqueBackground。
/// 不要对齐/改到 HuoshanScreen,否则会进 VolFx 合成叠在粒子上面。
/// </summary>
private void PrepareNoiseOverlayForGlitchTransition(bool activateForUse)
{
if (glitchController == null)
return;
const int defaultLayer = 0;
glitchController.gameObject.layer = defaultLayer;
var noiseRenderer = glitchController.GetComponent<SpriteRenderer>();
if (noiseRenderer != null)
{
noiseRenderer.sortingLayerID = 0;
noiseRenderer.sortingOrder = 10;
if (activateForUse)
noiseRenderer.enabled = true;
}
// 与场景原始配置一致:不透明黑底噪波(强度 0 时几乎全黑底,由上层 HuoshanScreen 内容盖住中心)。
glitchController.SetCompositeMode(SpriteNoiseGlitchController.CompositeMode.OpaqueBackground);
if (activateForUse && !glitchController.gameObject.activeSelf)
glitchController.gameObject.SetActive(true);
}
/// <summary>
/// Glitch 噪波过渡:驱动 ExpressView/noiseSpriteNoiseGlitch),非屏幕 Shader。
/// </summary>
public void TransitionGlitch(float to, float duration = 0f)
{
if (glitchController == null) return;
PrepareNoiseOverlayForGlitchTransition(true);
glitchController.TransitionTo(to, duration);
}
/// <summary>
/// 把「屏幕」Sprite 的故障档位推到 <paramref name="level"/> 并保持住(非阻塞)。
/// 从当前档位平滑过渡,<paramref name="duration"/> 是到达该档位的时间;到位后不会自动回落,
/// 需要再调一次 level=0 才收掉。Shader 内部自带「憋气→用力→泄掉」的循环,
/// 所以保持档位时屏幕依然在持续挣扎,不是一张静止的花屏。
/// </summary>
public void SetScreenGlitchLevel(float level, float duration)
{
if (screenOverlayRenderer == null)
return;
float target = Mathf.Clamp01(level);
if (target > 0f)
{
ActivateScreenGlitchMaterial();
if (!screenGlitchMaterialActive)
return;
// 确保屏幕可见(否则材质在动但物体是关的)。
screenOverlayRenderer.enabled = true;
screenOverlayRenderer.gameObject.SetActive(true);
}
else if (!screenGlitchMaterialActive)
{
screenGlitchIntensity = 0f;
return;
}
if (screenGlitchRampRoutine != null)
{
StopCoroutine(screenGlitchRampRoutine);
screenGlitchRampRoutine = null;
}
float ramp = Mathf.Max(0f, duration);
if (ramp <= 0.02f || !isActiveAndEnabled)
{
SetScreenGlitchIntensity(target);
return;
}
screenGlitchRampRoutine = StartCoroutine(RampScreenGlitch(target, ramp));
}
private IEnumerator RampScreenGlitch(float target, float duration)
{
float start = screenGlitchIntensity;
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = Mathf.SmoothStep(0f, 1f, elapsed / duration);
SetScreenGlitchIntensity(Mathf.Lerp(start, target, t));
yield return null;
}
SetScreenGlitchIntensity(target);
screenGlitchRampRoutine = null;
}
/// <summary>
/// Glitch 噪波过渡并等待完成:从当前强度平滑过渡到 to。
/// </summary>
public IEnumerator TransitionGlitchAndWait(float to, float duration = 0f)
{
if (glitchController == null) yield break;
PrepareNoiseOverlayForGlitchTransition(true);
yield return glitchController.StartCoroutine(glitchController.TransitionToAndWait(to, duration));
}
/// <summary>
/// Glitch 噪波过渡并等待完成:从当前强度平滑过渡到 to(from 忽略,保持丝滑连续)。
/// </summary>
public IEnumerator TransitionGlitchAndWait(float from, float to, float duration)
{
if (glitchController == null) yield break;
PrepareNoiseOverlayForGlitchTransition(true);
yield return glitchController.StartCoroutine(glitchController.TransitionToAndWait(to, duration));
}
public IEnumerator BeginExpressionMemory(
string memoryKey,
float fadeDuration = -1f,
float pushSpeed = -1f,
float horizontalSpeed = -1f,
bool preserveGlitch = false,
float targetOpacity = 1f)
{
if (logReleasePresentation == null)
{
Debug.LogError("[ExpressionManager] LogReleasePresentationController 未配置;记忆命令安全结束。");
yield break;
}
yield return logReleasePresentation.BeginMemory(
memoryKey,
fadeDuration,
pushSpeed,
horizontalSpeed,
preserveGlitch,
targetOpacity);
}
public IEnumerator BeginExpressionLie(string keyword, string preset)
{
if (logReleasePresentation == null)
{
Debug.LogError("[ExpressionManager] LogReleasePresentationController 未配置;谎话命令安全结束。");
yield break;
}
yield return logReleasePresentation.BeginLie(keyword, preset);
}
public IEnumerator BreakExpressionLie()
{
if (logReleasePresentation == null) yield break;
yield return logReleasePresentation.BreakLie();
}
public IEnumerator RevealExpressionTruth()
{
if (logReleasePresentation == null) yield break;
yield return logReleasePresentation.RevealTruth();
}
public IEnumerator EndExpressionMemory()
{
if (logReleasePresentation == null) yield break;
yield return logReleasePresentation.EndMemory();
}
public void ImpactExpressionTruth()
{
logReleasePresentation?.TruthImpact();
}
private void OnDisable()
{
if (screenGlitchRampRoutine != null)
{
StopCoroutine(screenGlitchRampRoutine);
screenGlitchRampRoutine = null;
}
SetScreenGlitchIntensity(0f);
if (logReleasePresentation != null)
logReleasePresentation.ForceCleanupImmediate(true);
}
private void OnDestroy()
{
if (screenGlitchMaterialInstance != null)
{
if (Application.isPlaying)
Destroy(screenGlitchMaterialInstance);
else
DestroyImmediate(screenGlitchMaterialInstance);
screenGlitchMaterialInstance = null;
}
if (screenFallbackMaterial != null)
{
if (Application.isPlaying)
Destroy(screenFallbackMaterial);
else
DestroyImmediate(screenFallbackMaterial);
screenFallbackMaterial = null;
}
}
}
}