762 lines
28 KiB
C#
762 lines
28 KiB
C#
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 / Color /
|
||
// Warp / Chaos / Grit / SurgeRate)。代码这边只驱动 _GlitchIntensity 档位。
|
||
// 不要再把观感参数搬回 Inspector——两处都能改就等于没有真相源,上一版 40 个属性
|
||
// 分散在材质和组件两边,结果谁也调不动。
|
||
|
||
public LogReleasePresentationController LogReleasePresentation => logReleasePresentation;
|
||
|
||
/// <summary>当前屏幕 Glitch 档位(0–1),由 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">非目标候选粒子总数;<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/noise(SpriteNoiseGlitch),非屏幕 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;
|
||
}
|
||
}
|
||
|
||
}
|
||
}
|