using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using DG.Tweening;
using TMPro;
using UnityEngine.UI;
using UnityEngine.Localization;
using UnityEngine.Localization.Settings;
using AibisDream;
using AibisDream.Utility;
namespace AibisDream.MiniGame.Language
{
///
/// 粒子排布区域形状
///
public enum LayoutShape
{
Rectangle, // 矩形
Circle // 圆形
}
///
/// 游戏完成阶段
///
public enum CompletionPhase
{
None, // 未完成
Completed, // 已完成但尚未进入聚焦流程
FocusHolding, // 聚焦位置保持态(文字飘动/变换,等待外部命令触发下一步)
Focusing, // 正在聚焦放大并收敛
Finished // 全流程完成
}
///
/// 语言粒子系统主管理器
///
public class LanguageParticleManager : MonoBehaviour
{
[Header("Canvas设置")]
[SerializeField] private Canvas worldCanvas;
[SerializeField] private float canvasSize = 10f;
[SerializeField] private LayoutShape layoutShape = LayoutShape.Rectangle;
[Tooltip("圆形布局时的半径(留空则自动使用 canvasSize/2 - textMargin)")]
[SerializeField] private float circleRadiusOverride = -1f;
[Header("字体设置")]
[SerializeField] private TMP_FontAsset chineseFontAsset;
[Tooltip("可选:基于 chineseFontAsset 的 Material Preset(如 huoshantextmaterial)")]
[SerializeField] private Material chineseFontMaterial;
[Header("粒子预制体")]
[SerializeField] private GameObject floatingParticlePrefab;
[SerializeField] private GameObject candidateParticlePrefab;
[Header("粒子数量")]
[SerializeField] private int floatingCount = 100;
[SerializeField] private int candidateCount = 50;
[SerializeField] private int redParticleCount = 8;
[Header("游戏参数")]
[SerializeField] private float connectionDistance = 2f; // Unity单位
[SerializeField] private float textMargin = 1f;
[Header("游戏配置")]
private ExpressionRoundTextSnapshot activeSnapshot;
private IReadOnlyList defaultUnitPool = new List().AsReadOnly();
private IReadOnlyList interferenceUnitPool = new List().AsReadOnly();
[SerializeField] private string completionDialogNode = ""; // 完成时触发的对话节点
[Header("波形参数")]
[SerializeField] private float waveformAmplitude = 0.3f;
[SerializeField] private float interactionRadius = 0.5f;
[Header("鼠标交互参数")]
[SerializeField] private float mouseInteractionRadius = 1.5f;
[SerializeField] private float repulsionForce = 2f;
[SerializeField] private float attractionForce = 1.5f;
[Header("颜色配置(用于粒子/字的颜色,与目标/非目标解绑,可自由搭配)")]
[SerializeField] private Color targetParticleColor = new Color(0.627451f, 1f, 0.8470588f, 1f); // 目标粒子 #A0FFD8
[SerializeField] private Color nonTargetParticleColor = new Color(1f, 0.1882353f, 0.3019608f, 1f); // 干扰粒子固定红色 #FF304D
[SerializeField] private Color floatingTextColor = new Color(0.7f, 0.78f, 1f, 1f); // 背景浮动文字颜色
[SerializeField] private Color targetCalmColor = new Color(0.3607843f, 0.945098f, 0.8156863f, 1f); // 聚拢后的固定目标色 #5CF1D0
[Header("连接线颜色(用于连线的颜色,与上面可分开调节)")]
[SerializeField] private Color targetConnectionColor = new Color(0.627451f, 1f, 0.8470588f, 0.49f); // 目标连线 #A0FFD8
[SerializeField] private Color nonTargetConnectionColor = new Color(1f, 0.3137255f, 0.4392157f, 0.49f); // 干扰连线 #FF5070
[Header("文字样式配置")]
[SerializeField] private float targetParticleFontSize = 4.5f; // 目标粒子字体大小
[SerializeField] private float nonTargetParticleFontSize = 3.7f; // 非目标粒子字体大小
[SerializeField] private float floatingTextFontSize = 3f; // 背景浮动文字大小
[SerializeField] private bool targetParticleBold = true; // 目标粒子是否加粗
[SerializeField] private bool nonTargetParticleBold = false; // 非目标粒子是否加粗
[SerializeField, Range(0.1f, 2f)] private float whitespaceSpacingMultiplier = 0.6f;
[Header("非目标粒子干扰效果")]
[SerializeField, Range(0f, 0.1f)] private float nonTargetShakeIntensity = 0.02f; // 非目标粒子抖动强度
[SerializeField, Range(1f, 20f)] private float nonTargetShakeSpeed = 8f; // 非目标粒子抖动速度
[SerializeField, Range(0.1f, 1f)] private float nonTargetChangeInterval = 0.4f; // 非目标粒子变字间隔(越小越快)
[SerializeField, Range(0f, 0.03f)] private float nonTargetFloatAmplitude = 0.015f; // 非目标粒子漂浮幅度
[SerializeField] private Color[] nonTargetMarqueeColors =
{
// 保留单色数组以兼容旧的 "rainbow" 演出参数;屏幕粒子不再循环彩虹色。
new Color(1f, 0.1882353f, 0.3019608f, 1f) // #FF304D
};
[SerializeField, Min(0.02f)] private float nonTargetMarqueeStepDuration = 0.18f;
[Header("连接线粗细")]
[SerializeField] private float targetConnectionThickness = 0.04f; // 目标连线粗细
[SerializeField] private float nonTargetConnectionThickness = 0.02f; // 非目标连线粗细
[Header("连接线 Bloom 发光效果")]
[SerializeField] private bool enableBloomGlow = true; // 是否启用连接线 HDR 发光
[SerializeField, Range(1f, 5f)] private float lineGlowIntensity = 2f; // 连接线发光强度
[SerializeField, Range(1f, 5f)] private float targetLineGlowIntensity = 2.5f; // 目标连接线发光强度
[Header("目标 UI")]
[SerializeField] private TMP_Text integrationProgressText;
[SerializeField] private TMP_Text interferenceCountText;
[Header("完成/确认阶段 — 屏幕内显示范围")]
[Tooltip("为真时,完成连线后(含确认按钮、聚焦、稳定化)目标字符位置会被限制在裁剪区域内,避免超出电视机屏幕可视区")]
[SerializeField] private bool clampCompletionPhasePositions = true;
[Tooltip("可选:场景中一块与屏幕内框对齐的 RectTransform(World Space Canvas 子物体即可)。不指定则用粒子用的 movementBounds(canvasSize - textMargin)")]
[SerializeField] private RectTransform completionScreenClipRect;
[Tooltip("在裁剪区基础上再向内缩进的世界单位距离,避免放大后的字半个挂在边外")]
[SerializeField] private float completionClipEdgePadding = 0.35f;
[Tooltip("屏幕内容的最小安全边距;最终边距还会根据字形、光晕、圆环和晃动幅度动态增大。")]
[SerializeField, Min(0f)] private float minimumScreenMargin = 0.2f;
[Tooltip("将目标粒子拉回屏幕裁剪区时的平滑时间(越大越柔和,略增加拖尾感)")]
[SerializeField, Range(0.02f, 0.55f)] private float completionPositionSmoothTime = 0.2f;
[Header("完成阶段 UI")]
[SerializeField] private GameObject languageDeepPanel1;
[SerializeField] private GameObject languageDeepPanel2;
[SerializeField] private Button focusSequenceButton;
[Tooltip("火山释放log界面,触发 focus 时对其 SpriteRenderer 做淡出,重新开始时恢复")]
[SerializeField] private SpriteRenderer releaseLogSpriteRenderer;
[SerializeField] private ExpressionScreenPresentationController screenPresentation;
[Tooltip("开场表达panel Timeline 名称(DirectorName 或 DirectorName/AddressableKey),播完才算表达流程完成")]
[SerializeField] private string expressionPanelTimelineName = "火山表达模块/火山表达panel";
[Tooltip("火山表达确认 Timeline 名称,播完才解锁按钮交互")]
[SerializeField] private string expressionConfirmTimelineName = "火山表达模块/火山表达确认";
[SerializeField] private float focusZoomScale = 1.6f;
[SerializeField] private float focusMoveDuration = 1.2f;
[SerializeField] private float focusFadeDuration = 1f;
[SerializeField] private float logOperationFadeDuration = 0.6f;
[Tooltip("再次 start_expression 时,若上一轮粒子/UI 仍在,先淡出再重开;协程 duration≤0 时用此值(秒)")]
[SerializeField] private float restartExpressionFadeDuration = 0.55f;
[SerializeField] private float probabilityDuration = 3.5f;
[SerializeField] private float predictionSequenceGap = 0.4f;
[SerializeField] private float spreadDistance = 0.35f;
[Header("聚焦表现")]
[SerializeField] private float statusFadeOutDuration = 0.45f;
[SerializeField] private float shuffleDistance = 0.35f;
[SerializeField] private float shuffleFrequency = 3.5f;
[SerializeField] private float shuffleVerticalScale = 0.6f;
[Header("聚焦干扰表现")]
[SerializeField] private Color focusInterferenceColor = new Color(1f, 0.1882353f, 0.3019608f, 1f); // #FF304D
[SerializeField, Range(0.02f, 0.5f)] private float focusInterferenceShakeAmplitude = 0.16f;
[SerializeField, Range(1f, 60f)] private float focusInterferenceShakeSpeed = 24f;
[Header("真话攻击表现")]
[SerializeField] private Color truthAttackColor = new Color(1f, 0.1882353f, 0.3019608f, 1f); // #FF304D
[SerializeField, Min(0.1f)] private float truthAttackCharacterSpacing = 0.9f;
[SerializeField, Min(0f)] private float truthAttackTravelDistance = 0.75f;
[Header("开场表现")]
[SerializeField] private float entranceDelay = 0.45f;
[SerializeField] private int entranceCoreCount = 8;
[SerializeField] private float entranceCoreRadius = 0.55f;
[SerializeField] private float entranceInitialScale = 0.25f;
[SerializeField] private float entranceCoreBurstDuration = 1f;
[SerializeField] private float entranceCoreStagger = 0.08f;
[SerializeField] private float entranceCascadeDuration = 0.8f;
[SerializeField] private float entranceCascadeStagger = 0.03f;
[SerializeField] private float entranceSearchRadius = 2f;
[Header("渲染层设置")]
[SerializeField] private string renderLayerName = "HuoshanScreen";
private int renderLayer = 0;
private bool renderLayerResolved;
[Header("布局优化")]
[SerializeField] private bool enableSeparation = true; // 是否启用粒子分离力
[SerializeField] private float minParticleSpacing = 0.6f; // 粒子最小间距
[SerializeField] private float separationForce = 2.5f; // 分离力度
[SerializeField] private bool useGlyphBoundsForSeparation = true; // 字号较大时按实际字形扩大间距
[SerializeField, Min(0f)] private float separationPadding = 0.08f; // 字形外额外留白
[SerializeField, Min(1f)] private float differentTypeSpacingMultiplier = 1.2f;
[SerializeField, Min(1f)] private float differentTypeForceMultiplier = 1.75f;
[SerializeField, Range(0f, 1f)] private float separationPositionCorrection = 0.45f;
[SerializeField, Min(0.01f)] private float maxSeparationCorrectionPerFrame = 0.25f;
[SerializeField] private float lineAlphaMultiplier = 0.65f; // 连接线透明度缩放
// 粒子列表
private List floatingParticles = new List();
private List candidateParticles = new List();
private List targetParticles = new List(); // 目标粒子列表(原 redParticles)
// 效果系统(传播效果已移除,保留空列表供渲染器使用)
private static readonly List s_emptyPropagations = new List();
private Dictionary previousConnections = new Dictionary();
private bool initializationComplete = false;
private int initializationFrames = 0;
// 游戏状态
private bool isCompleted = false;
private CompletionPhase completionPhase = CompletionPhase.None;
private float completionTimer = 0f;
private float fadeOutAlpha = 1f;
private List orderedRedParticles = new List();
private bool allowInput = true;
private float focusTimer = 0f;
private Dictionary focusVisuals = new Dictionary();
/// 完成阶段屏幕钳制用 SmoothDamp 速度缓存(按粒子)
private readonly Dictionary completionClampSmoothVelocity = new Dictionary();
private Vector3 focusCentroid;
private List finalTargetUnits = new List();
private List finalTargetPositions = new List();
private bool interferenceLogicSuspended = false;
private bool statusUIHidden = false;
private float integrationTextBaseAlpha = 1f;
private float interferenceTextBaseAlpha = 1f;
private Coroutine entranceRoutine;
private Coroutine expressionPanelRoutine;
private Coroutine confirmTimelineRoutine;
private Coroutine focusTransitionRoutine;
private Coroutine slotMachineRoutine;
private readonly Dictionary entranceTargets = new Dictionary();
private bool pendingCompletionDialog;
private class TargetFocusVisual
{
public CandidateParticle particle;
public float startShakeIntensity;
public string finalUnitText;
public bool stabilized;
public Vector3 focusPosition;
public Vector3 finalPosition;
public Vector3 lieEdgePosition;
public int orderIndex;
public bool gatherLocked;
public Vector3 scatterOffset;
}
private bool focusTweensCompleted = false;
private bool focusMoveTweensCompleted;
private float resolveTotalDurationOverride = -1f;
private float lieEdgeCompression;
private float lieEdgeJitter;
private float truthGatherProgress;
private LogReleasePresentationController presentationController;
private const string IntegrationLocalizationKey = "huoshan_expression_integration";
private const string InterferenceLocalizationKey = "huoshan_expression_interference";
private string integrationStatusFormat = "{0}% 语言整合完成度";
private string interferenceStatusFormat = "{0} 处异常思绪仍在干扰表达";
private Coroutine statusLocalizationRoutine;
private const string LieCompressionTweenId = "HuoshanLieEdgeCompression";
private const string TruthGatherTweenId = "HuoshanTruthGather";
private const string FocusInterferenceTweenId = "HuoshanFocusInterference";
private int truthAttackSequenceIndex;
private float activePhraseScale = 1f;
private ExpressionParticleUnitMode ActiveUnitMode =>
activeSnapshot?.Profile?.UnitMode ?? ExpressionParticleUnitMode.Grapheme;
private float ResolvedFocusScale => focusZoomScale * activePhraseScale;
// 边界
private Bounds movementBounds;
private Vector3 boundsCenter;
private float boundsRadius; // 圆形时有效
// 连接渲染(使用Shapes)
private ConnectionRenderer connectionRenderer;
// 噪点叠加渲染器(复古显示器效果)
private NoiseOverlayRenderer noiseOverlayRenderer;
[Header("复古显示器效果")]
[SerializeField] private bool enableRetroEffect = true;
[SerializeField, Range(0f, 1f)] private float retroEffectIntensity = 0.5f;
// 是否已经初始化
private bool isInitialized = false;
/// Inspector 中的候选粒子总数,用于未在 Yarn 中指定非目标数量时恢复默认
private int defaultCandidateCountFromInspector;
private bool hasCapturedDefaultCandidateCount;
private float defaultTargetFontSizeFromInspector;
private float defaultNonTargetFontSizeFromInspector;
private float defaultFloatingFontSizeFromInspector;
private void CaptureDefaultCandidateCountFromInspector()
{
if (hasCapturedDefaultCandidateCount) return;
defaultCandidateCountFromInspector = candidateCount;
defaultTargetFontSizeFromInspector = targetParticleFontSize;
defaultNonTargetFontSizeFromInspector = nonTargetParticleFontSize;
defaultFloatingFontSizeFromInspector = floatingTextFontSize;
hasCapturedDefaultCandidateCount = true;
}
private void Start()
{
ResolveScreenPresentation();
LocalizationSettings.SelectedLocaleChanged += HandleLocaleChanged;
statusLocalizationRoutine = StartCoroutine(RefreshStatusLocalization());
// Start 中不再自动初始化,等待外部调用 InitializeSystem
}
private void ResolveScreenPresentation()
{
if (screenPresentation == null)
screenPresentation = GetComponentInParent(true);
}
private void HandleLocaleChanged(Locale locale)
{
if (!isActiveAndEnabled)
return;
if (statusLocalizationRoutine != null)
StopCoroutine(statusLocalizationRoutine);
statusLocalizationRoutine = StartCoroutine(RefreshStatusLocalization());
}
private IEnumerator RefreshStatusLocalization()
{
yield return LocalizationSettings.InitializationOperation;
var integrationOperation = LocalizationSettings.StringDatabase.GetLocalizedStringAsync(
ConstRef.UITextTable,
IntegrationLocalizationKey);
yield return integrationOperation;
if (!string.IsNullOrEmpty(integrationOperation.Result))
integrationStatusFormat = integrationOperation.Result;
var interferenceOperation = LocalizationSettings.StringDatabase.GetLocalizedStringAsync(
ConstRef.UITextTable,
InterferenceLocalizationKey);
yield return interferenceOperation;
if (!string.IsNullOrEmpty(interferenceOperation.Result))
interferenceStatusFormat = interferenceOperation.Result;
statusLocalizationRoutine = null;
if (isInitialized)
UpdateStatusUI();
}
///
/// 初始化系统(可以由外部调用,用于配置游戏参数)
///
/// 当前 Locale 已完成粒子单位解析的轮次快照
/// 完成时触发的对话节点
///
/// 轮次配置中的基础非目标粒子数。≥0 时先应用 Locale Profile 数量倍率;
/// <0 时使用 Inspector 的 Candidate Count。
///
public void InitializeSystem(
ExpressionRoundTextSnapshot snapshot,
string dialogNode = null,
int baseNonTargetParticleCount = -1)
{
CaptureDefaultCandidateCountFromInspector();
if (snapshot == null ||
snapshot.TargetUnits == null ||
snapshot.TargetUnits.Count == 0 ||
snapshot.DefaultPoolUnits == null ||
snapshot.DefaultPoolUnits.Count == 0 ||
snapshot.InterferencePoolUnits == null ||
snapshot.InterferencePoolUnits.Count == 0)
{
Debug.LogError(
"[LanguageParticleManager] 粒子单位快照无效;初始化已中止。");
return;
}
ExpressionParticleLocaleSettings profile =
snapshot.Profile ?? ExpressionParticleLocaleSettings.CreateFallback();
int targetUnitCount = snapshot.TargetUnits.Count;
int scaledNonTargetCount =
profile.ScaleNonTargetCount(baseNonTargetParticleCount);
if (scaledNonTargetCount >= 0)
{
candidateCount = Mathf.Max(
targetUnitCount + scaledNonTargetCount,
targetUnitCount);
}
else
{
candidateCount = defaultCandidateCountFromInspector;
}
activeSnapshot = snapshot;
completionDialogNode = dialogNode ?? "";
defaultUnitPool = snapshot.DefaultPoolUnits;
interferenceUnitPool = snapshot.InterferencePoolUnits;
targetParticleFontSize =
defaultTargetFontSizeFromInspector * profile.TargetFontScale;
nonTargetParticleFontSize =
defaultNonTargetFontSizeFromInspector * profile.NonTargetFontScale;
floatingTextFontSize =
defaultFloatingFontSizeFromInspector * profile.FloatingFontScale;
// 如果是第一次初始化,先执行基础初始化
if (!isInitialized)
{
InitializeCanvas();
InitializeParticles();
InitializeRenderers();
if (integrationProgressText != null)
{
integrationTextBaseAlpha = integrationProgressText.color.a;
}
if (interferenceCountText != null)
{
interferenceTextBaseAlpha = interferenceCountText.color.a;
}
if (focusSequenceButton != null)
{
focusSequenceButton.interactable = false;
focusSequenceButton.gameObject.SetActive(false);
focusSequenceButton.onClick.RemoveListener(HandleFocusButtonClicked);
focusSequenceButton.onClick.AddListener(HandleFocusButtonClicked);
}
if (languageDeepPanel1 != null)
languageDeepPanel1.SetActive(false);
if (languageDeepPanel2 != null)
languageDeepPanel2.SetActive(false);
isInitialized = true;
}
else
{
EnsureCandidatePoolSize(candidateCount);
ApplyUnitConfigurationToParticles();
}
redParticleCount = Mathf.Min(targetUnitCount, candidateCount);
// 重新初始化游戏状态并播放入场动画
ResetGame(true);
}
///
/// 在再次 start_expression 前调用:若系统已初始化过,则淡出当前粒子、连线与相关 UI,避免与新一轮重叠。
/// duration≤0 时使用 。
///
public IEnumerator FadeOutBeforeNewExpressionRound(float duration)
{
if (!isInitialized)
yield break;
ResolveScreenPresentation();
screenPresentation?.PrepareForNewRound();
StopFocusTransitionRoutine();
KillCompletionUiTweens();
resolveTotalDurationOverride = -1f;
float d = duration > 0f ? duration : restartExpressionFadeDuration;
// 火山释放 log 与文字同时叠在屏幕上时,先把 log 藏起来,等文字淡出结束后再由 FadeInReleaseLogIfNeeded 出现
HideReleaseLogImmediate();
if (entranceRoutine != null)
{
StopCoroutine(entranceRoutine);
entranceRoutine = null;
}
if (expressionPanelRoutine != null)
{
StopCoroutine(expressionPanelRoutine);
expressionPanelRoutine = null;
}
if (confirmTimelineRoutine != null)
{
StopCoroutine(confirmTimelineRoutine);
confirmTimelineRoutine = null;
}
DOTween.Kill(this);
foreach (var p in candidateParticles)
{
if (p != null)
{
DOTween.Kill(p);
p.transform.DOKill();
p.velocity = Vector2.zero;
p.isStatic = true;
}
}
foreach (var f in floatingParticles)
{
if (f != null)
{
DOTween.Kill(f);
f.transform.DOKill();
f.velocity = Vector2.zero;
f.isCalmed = true;
}
}
interferenceLogicSuspended = true;
allowInput = false;
isCompleted = false;
completionPhase = CompletionPhase.None;
DestroyFocusVisualDecorationsAndClear();
DOTween.To(() => fadeOutAlpha, v => fadeOutAlpha = v, 0f, d)
.SetEase(Ease.OutQuad)
.SetTarget(this);
if (connectionRenderer != null)
{
DOTween.To(() => connectionRenderer.NonTargetAlphaScale, v => connectionRenderer.NonTargetAlphaScale = v, 0f, d)
.SetEase(Ease.OutQuad)
.SetTarget(this);
DOTween.To(() => connectionRenderer.TargetAlphaScale, v => connectionRenderer.TargetAlphaScale = v, 0f, d)
.SetEase(Ease.OutQuad)
.SetTarget(this);
}
foreach (var p in candidateParticles)
{
if (p != null)
FadeOutTextParticle(p, d);
}
foreach (var f in floatingParticles)
{
if (f != null)
FadeOutTextParticle(f, d);
}
SetStatusUIVisibility(false, d);
if (focusSequenceButton != null && focusSequenceButton.gameObject.activeSelf)
FadeOutUIElement(focusSequenceButton.gameObject, d);
FadeOutUIElement(languageDeepPanel1, d);
FadeOutUIElement(languageDeepPanel2, d);
yield return new WaitForSeconds(d);
fadeOutAlpha = 0f;
}
///
/// 在打开视图前设置所有初始状态(panels、button 等不激活)
///
public void SetInitialStatesBeforeOpen()
{
RestoreCompletionUiAlpha();
if (languageDeepPanel1 != null)
languageDeepPanel1.SetActive(false);
if (languageDeepPanel2 != null)
languageDeepPanel2.SetActive(false);
if (focusSequenceButton != null)
{
focusSequenceButton.gameObject.SetActive(false);
focusSequenceButton.interactable = false;
}
}
///
/// 等待表达流程就绪(粒子字入场完成即可交互;panel 动效可并行、不阻塞)
///
public IEnumerator WaitUntilExpressionFlowReady()
{
if (!isInitialized) yield break;
while (!allowInput)
{
yield return null;
}
}
///
/// 停止系统(关闭时调用,但保持初始化状态,可以重新开启)
///
public void StopSystem()
{
// 如果未初始化,直接返回
if (!isInitialized)
return;
StopFocusTransitionRoutine();
KillCompletionUiTweens();
RestoreTargetPresentationDefaults();
if (entranceRoutine != null)
{
StopCoroutine(entranceRoutine);
entranceRoutine = null;
}
if (confirmTimelineRoutine != null)
{
StopCoroutine(confirmTimelineRoutine);
confirmTimelineRoutine = null;
}
if (expressionPanelRoutine != null)
{
StopCoroutine(expressionPanelRoutine);
expressionPanelRoutine = null;
}
// 停止所有 DOTween 动画
DOTween.Kill(this);
foreach (var particle in candidateParticles)
{
if (particle != null)
{
DOTween.Kill(particle);
particle.transform.DOKill();
}
}
foreach (var particle in floatingParticles)
{
if (particle != null)
{
DOTween.Kill(particle);
particle.transform.DOKill();
}
}
// 重置游戏状态(但不重置 isInitialized,这样可以重新开启)
isCompleted = false;
completionPhase = CompletionPhase.None;
allowInput = false;
ClearCompletionClampSmoothVelocity();
ReleaseScreenClipMaterials();
}
private void ResolveRenderLayer()
{
renderLayer = LayerMask.NameToLayer(renderLayerName);
if (renderLayer == -1)
{
Debug.LogWarning($"Layer '{renderLayerName}' not found, using default layer.");
renderLayer = 0;
}
renderLayerResolved = true;
}
private static void ApplyLayerRecursively(GameObject root, int layer)
{
if (root == null) return;
root.layer = layer;
var transforms = root.GetComponentsInChildren(true);
for (int i = 0; i < transforms.Length; i++)
transforms[i].gameObject.layer = layer;
}
private void ApplyParticleRenderLayer(GameObject particleRoot)
{
ApplyLayerRecursively(particleRoot, renderLayer);
}
private void InitializeCanvas()
{
ResolveRenderLayer();
// 管理器自身也进 HuoshanScreen,避免子物体(连线/噪点)继承 Default
gameObject.layer = renderLayer;
// 如果没有指定canvas,创建一个
if (worldCanvas == null)
{
GameObject canvasObj = new GameObject("Language Canvas");
canvasObj.transform.SetParent(transform);
canvasObj.transform.localPosition = Vector3.zero;
ApplyLayerRecursively(canvasObj, renderLayer);
worldCanvas = canvasObj.AddComponent