跑通一个版本
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
using System.Collections;
|
||||
using AibisDream.Kit;
|
||||
using AibisDream.UI;
|
||||
using AibisDream.Utility;
|
||||
using DG.Tweening;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream
|
||||
{
|
||||
public class DreamDoorSystem : Singleton<DreamDoorSystem>
|
||||
{
|
||||
[Header("Door Root")]
|
||||
[Tooltip("Door 根节点。默认仅 background 和 Frame 显示,整个 Door 初始隐藏。show_door 命令激活后显示。")]
|
||||
[SerializeField] private Transform doorRoot;
|
||||
|
||||
[Header("Door Sprites")]
|
||||
[SerializeField] private SpriteRenderer glassSea;
|
||||
[SerializeField] private SpriteRenderer codeSea;
|
||||
[SerializeField] private SpriteRenderer flashOverlay;
|
||||
|
||||
[Header("Terminal(挂在 Door 子节点上即可,无需 Canvas)")]
|
||||
[SerializeField] private DreamTerminalUI terminalUI;
|
||||
|
||||
[Header("Morph Settings")]
|
||||
[SerializeField] private Vector3 seaCollapseScale = new(0.9f, 0.84f, 1f);
|
||||
[SerializeField] private Ease morphEase = Ease.InOutCubic;
|
||||
|
||||
private Transform _codeSeaTransform;
|
||||
private Vector3 _codeSeaOriginalScale;
|
||||
private Color _codeSeaOriginalColor;
|
||||
private Transform _terminalUiTransform;
|
||||
private Vector3 _terminalUiOriginalScale;
|
||||
|
||||
public override void OnSingletonInit()
|
||||
{
|
||||
if (doorRoot != null)
|
||||
doorRoot.gameObject.SetActive(false);
|
||||
|
||||
if (glassSea != null)
|
||||
{
|
||||
glassSea.SetAlpha(0);
|
||||
glassSea.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
if (codeSea != null)
|
||||
{
|
||||
codeSea.SetAlpha(0);
|
||||
codeSea.gameObject.SetActive(false);
|
||||
_codeSeaTransform = codeSea.transform;
|
||||
_codeSeaOriginalScale = _codeSeaTransform.localScale;
|
||||
_codeSeaOriginalColor = codeSea.color;
|
||||
}
|
||||
|
||||
if (flashOverlay != null)
|
||||
{
|
||||
flashOverlay.SetAlpha(0);
|
||||
flashOverlay.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
if (terminalUI != null)
|
||||
{
|
||||
terminalUI.ResetVisualState();
|
||||
terminalUI.Alpha = 0f;
|
||||
_terminalUiTransform = terminalUI.transform;
|
||||
_terminalUiOriginalScale = _terminalUiTransform.localScale;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示整个门(仅 background + Frame,GlassSea/CodeSea 保持隐藏直到各自命令调用)
|
||||
/// </summary>
|
||||
public IEnumerator ShowDoor(float duration = 0f)
|
||||
{
|
||||
if (doorRoot != null)
|
||||
doorRoot.gameObject.SetActive(true);
|
||||
|
||||
if (duration > 0f)
|
||||
yield return new WaitForSeconds(duration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 隐藏整个门
|
||||
/// </summary>
|
||||
public void HideDoor()
|
||||
{
|
||||
if (doorRoot != null)
|
||||
doorRoot.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
#region Phase 1: GlassSea 浮现
|
||||
|
||||
public IEnumerator ShowGlassSea(float duration = 2f)
|
||||
{
|
||||
glassSea.gameObject.SetActive(true);
|
||||
yield return glassSea.DOFade(1f, duration)
|
||||
.SetEase(Ease.InOutSine)
|
||||
.WaitForCompletion();
|
||||
}
|
||||
|
||||
public IEnumerator HideGlassSea(float duration = 1f)
|
||||
{
|
||||
yield return glassSea.DOFade(0f, duration)
|
||||
.SetEase(Ease.InOutSine)
|
||||
.WaitForCompletion();
|
||||
glassSea.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Phase 2: CodeSea 浮现 (GlassSea 交叉淡出)
|
||||
|
||||
public IEnumerator ShowCodeSea(float duration = 2f)
|
||||
{
|
||||
codeSea.gameObject.SetActive(true);
|
||||
|
||||
var seq = DOTween.Sequence();
|
||||
seq.Append(codeSea.DOFade(1f, duration).SetEase(Ease.InOutSine));
|
||||
if (glassSea != null && glassSea.gameObject.activeSelf)
|
||||
{
|
||||
seq.Join(glassSea.DOFade(0f, duration * 0.7f).SetEase(Ease.InSine));
|
||||
}
|
||||
|
||||
yield return seq.WaitForCompletion();
|
||||
|
||||
if (glassSea != null)
|
||||
glassSea.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Phase 3: CodeSea 变形为终端
|
||||
|
||||
public IEnumerator ShowTerminal()
|
||||
{
|
||||
yield return AnimateDoorToTerminal();
|
||||
if (terminalUI != null)
|
||||
terminalUI.Begin();
|
||||
}
|
||||
|
||||
private IEnumerator AnimateDoorToTerminal()
|
||||
{
|
||||
if (terminalUI != null)
|
||||
{
|
||||
terminalUI.ResetVisualState();
|
||||
terminalUI.Alpha = 0f;
|
||||
terminalUI.MorphProgress = 0f;
|
||||
}
|
||||
|
||||
if (_terminalUiTransform != null)
|
||||
_terminalUiTransform.localScale = Vector3.Scale(_terminalUiOriginalScale, new Vector3(1.04f, 1.04f, 1f));
|
||||
|
||||
var seq = DOTween.Sequence();
|
||||
|
||||
// Step 1: 短暂闪白(CRT 切换感)
|
||||
if (flashOverlay != null)
|
||||
{
|
||||
seq.Append(flashOverlay.DOFade(0.8f, 0.05f));
|
||||
seq.Append(flashOverlay.DOFade(0f, 0.12f));
|
||||
}
|
||||
|
||||
// Step 2: 海浪内容退场,终端框架在门内成型
|
||||
if (codeSea != null)
|
||||
{
|
||||
codeSea.gameObject.SetActive(true);
|
||||
seq.Append(codeSea.DOColor(new Color(0.04f, 0.04f, 0.04f, 1f), 0.3f)
|
||||
.SetEase(Ease.OutSine));
|
||||
seq.Join(codeSea.DOFade(0f, 0.78f).SetEase(Ease.InSine));
|
||||
}
|
||||
|
||||
if (_codeSeaTransform != null)
|
||||
{
|
||||
seq.Join(_codeSeaTransform.DOScale(Vector3.Scale(_codeSeaOriginalScale, seaCollapseScale), 0.78f)
|
||||
.SetEase(morphEase));
|
||||
}
|
||||
|
||||
if (terminalUI != null)
|
||||
{
|
||||
float uiAlpha = 0f;
|
||||
float morph = 0f;
|
||||
seq.Join(DOTween.To(() => uiAlpha, v =>
|
||||
{
|
||||
uiAlpha = v;
|
||||
terminalUI.Alpha = v;
|
||||
}, 1f, 0.24f).SetEase(Ease.OutQuad));
|
||||
seq.Join(DOTween.To(() => morph, v =>
|
||||
{
|
||||
morph = v;
|
||||
terminalUI.MorphProgress = v;
|
||||
}, 1f, 0.82f).SetEase(morphEase));
|
||||
}
|
||||
|
||||
if (_terminalUiTransform != null)
|
||||
{
|
||||
seq.Join(_terminalUiTransform.DOScale(_terminalUiOriginalScale, 0.82f)
|
||||
.SetEase(Ease.OutCubic));
|
||||
}
|
||||
|
||||
// Step 3: 第二次微闪(终端稳定)
|
||||
if (flashOverlay != null)
|
||||
{
|
||||
seq.Append(flashOverlay.DOFade(0.5f, 0.03f));
|
||||
seq.Append(flashOverlay.DOFade(0f, 0.15f));
|
||||
}
|
||||
|
||||
yield return seq.WaitForCompletion();
|
||||
|
||||
if (codeSea != null)
|
||||
codeSea.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 终端结局动效
|
||||
|
||||
public IEnumerator PlayShutdownEffect()
|
||||
{
|
||||
var target = _terminalUiTransform != null ? _terminalUiTransform : _codeSeaTransform;
|
||||
if (target == null) yield break;
|
||||
|
||||
var seq = DOTween.Sequence();
|
||||
|
||||
if (flashOverlay != null)
|
||||
seq.Append(flashOverlay.DOFade(0.6f, 0.15f));
|
||||
|
||||
// 压扁成白线
|
||||
float scaleY = _terminalUiTransform != null ? _terminalUiOriginalScale.y * 0.02f : 0.005f;
|
||||
seq.Join(target.DOScaleY(scaleY, 0.35f).SetEase(Ease.OutQuad));
|
||||
// 横向收缩消失
|
||||
seq.Append(target.DOScaleX(0f, 0.3f).SetEase(Ease.InQuad));
|
||||
|
||||
if (flashOverlay != null)
|
||||
seq.Join(flashOverlay.DOFade(0f, 0.5f));
|
||||
|
||||
// 同步淡出 Shapes 终端
|
||||
if (terminalUI != null)
|
||||
{
|
||||
float a = terminalUI.Alpha;
|
||||
seq.Join(DOTween.To(() => a, v => { a = v; terminalUI.Alpha = v; }, 0f, 0.4f));
|
||||
}
|
||||
|
||||
yield return seq.WaitForCompletion();
|
||||
}
|
||||
|
||||
public void PlayCrashEffect()
|
||||
{
|
||||
var target = _terminalUiTransform != null ? _terminalUiTransform : _codeSeaTransform;
|
||||
if (target != null)
|
||||
target.DOShakePosition(1.2f, 0.08f, 35, fadeOut: false);
|
||||
}
|
||||
|
||||
public void StopAllEffects()
|
||||
{
|
||||
DOTween.Kill(_codeSeaTransform);
|
||||
DOTween.Kill(codeSea);
|
||||
DOTween.Kill(_terminalUiTransform);
|
||||
DOTween.Kill(flashOverlay);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 清理
|
||||
|
||||
public void HideAll()
|
||||
{
|
||||
StopAllEffects();
|
||||
HideDoor();
|
||||
|
||||
if (glassSea != null)
|
||||
{
|
||||
glassSea.SetAlpha(0);
|
||||
glassSea.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
if (codeSea != null)
|
||||
{
|
||||
codeSea.color = _codeSeaOriginalColor;
|
||||
codeSea.SetAlpha(0);
|
||||
codeSea.gameObject.SetActive(false);
|
||||
if (_codeSeaTransform != null)
|
||||
_codeSeaTransform.localScale = _codeSeaOriginalScale;
|
||||
}
|
||||
|
||||
if (terminalUI != null)
|
||||
{
|
||||
terminalUI.Alpha = 0f;
|
||||
terminalUI.ResetVisualState();
|
||||
}
|
||||
|
||||
if (_terminalUiTransform != null)
|
||||
_terminalUiTransform.localScale = _terminalUiOriginalScale;
|
||||
|
||||
if (flashOverlay != null)
|
||||
flashOverlay.SetAlpha(0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (codeSea == null) return;
|
||||
Gizmos.color = new Color(1f, 0.7f, 0f, 0.4f);
|
||||
var bounds = codeSea.bounds;
|
||||
Gizmos.DrawWireCube(bounds.center, bounds.size);
|
||||
|
||||
Gizmos.color = new Color(0f, 1f, 1f, 0.3f);
|
||||
Vector3 termSize = new(
|
||||
bounds.size.x * seaCollapseScale.x,
|
||||
bounds.size.y * seaCollapseScale.y,
|
||||
bounds.size.z
|
||||
);
|
||||
Gizmos.DrawWireCube(bounds.center, termSize);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5da7fe5bad47f764bb5ce8a12e59ec2c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Collections;
|
||||
using Yarn.Unity;
|
||||
|
||||
namespace AibisDream
|
||||
{
|
||||
public static class DreamDoorYarnCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// 显示整个门(仅 background + Frame)。默认门是隐藏的,需先调用此命令。
|
||||
/// </summary>
|
||||
[YarnCommand("show_door")]
|
||||
public static IEnumerator ShowDoor(float duration = 0f)
|
||||
{
|
||||
yield return DreamDoorSystem.Instance.ShowDoor(duration);
|
||||
}
|
||||
|
||||
[YarnCommand("show_door_glass_sea")]
|
||||
public static IEnumerator ShowDoorGlassSea(float duration = 2f)
|
||||
{
|
||||
yield return DreamDoorSystem.Instance.ShowGlassSea(duration);
|
||||
}
|
||||
|
||||
[YarnCommand("hide_door_glass_sea")]
|
||||
public static IEnumerator HideDoorGlassSea(float duration = 1f)
|
||||
{
|
||||
yield return DreamDoorSystem.Instance.HideGlassSea(duration);
|
||||
}
|
||||
|
||||
[YarnCommand("show_door_code_sea")]
|
||||
public static IEnumerator ShowDoorCodeSea(float duration = 2f)
|
||||
{
|
||||
yield return DreamDoorSystem.Instance.ShowCodeSea(duration);
|
||||
}
|
||||
|
||||
[YarnCommand("show_dream_terminal")]
|
||||
public static IEnumerator ShowDreamTerminal()
|
||||
{
|
||||
yield return DreamDoorSystem.Instance.ShowTerminal();
|
||||
}
|
||||
|
||||
[YarnCommand("hide_dream_door")]
|
||||
public static void HideDreamDoor()
|
||||
{
|
||||
DreamDoorSystem.Instance.HideAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aa28f05af98b0f94d9979fb91ace8e7f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,756 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using DG.Tweening;
|
||||
using Shapes;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace AibisDream.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 梦境命令窗口:全部用 Shapes 即时模式绘制,无需手动配置任何 Canvas 或 UI 组件。
|
||||
/// 挂在 Door 节点(或其子节点)上即可。
|
||||
/// </summary>
|
||||
[ExecuteAlways]
|
||||
public class DreamTerminalUI : ImmediateModeShapeDrawer
|
||||
{
|
||||
#region 样式参数
|
||||
|
||||
[Header("尺寸(世界单位)")]
|
||||
[SerializeField] private Vector2 terminalSize = new(6.8f, 4.2f);
|
||||
[SerializeField] private Vector2 morphStartSize = new(8.6f, 5.6f);
|
||||
|
||||
[Header("琥珀色调色盘")]
|
||||
[SerializeField] private Color colAmberMain = new(1f, 0.69f, 0f, 1f); // #ffb000
|
||||
[SerializeField] private Color colAmberDim = new(0.72f, 0.52f, 0.04f, 1f); // #b8860b
|
||||
[SerializeField] private Color colWhiteGlow = new(1f, 1f, 1f, 1f);
|
||||
[SerializeField] private Color colBg = new(0.02f, 0.02f, 0.02f, 0.96f);
|
||||
|
||||
[Header("布局")]
|
||||
[SerializeField] private float borderThickness = 0.05f;
|
||||
[SerializeField] private float padding = 0.3f;
|
||||
[SerializeField] private float lineHeight = 0.3f;
|
||||
[SerializeField] private float fontSize = 0.24f;
|
||||
[SerializeField] private float inputAreaHeight = 0.58f;
|
||||
[SerializeField] private int maxVisibleLines = 9;
|
||||
|
||||
[Header("扫描线")]
|
||||
[SerializeField] private float scanlineSpacing = 0.07f;
|
||||
[SerializeField] private float scanlineAlpha = 0.08f;
|
||||
|
||||
[Header("光标")]
|
||||
[SerializeField] private float cursorBlinkRate = 0.5f;
|
||||
[SerializeField] private Vector2 cursorSize = new(0.14f, 0.24f);
|
||||
|
||||
[Header("形变")]
|
||||
[Range(0f, 1f)]
|
||||
[SerializeField] private float morphProgress = 0f;
|
||||
|
||||
[Header("编辑器预览")]
|
||||
[SerializeField] private bool previewInEditor = true;
|
||||
[Range(0f, 1f)]
|
||||
[SerializeField] private float previewAlpha = 1f;
|
||||
[Range(0f, 1f)]
|
||||
[SerializeField] private float previewMorphProgress = 1f;
|
||||
[SerializeField] private PreviewStage previewStage = PreviewStage.WaitingInput;
|
||||
[SerializeField] private bool animatePreviewCursor = true;
|
||||
[SerializeField] private string previewInputBuffer = "false";
|
||||
[TextArea(3, 8)]
|
||||
[SerializeField] private string previewLines =
|
||||
">> WARNING: TRAP DETECTED.\n" +
|
||||
">> SYSTEM: Reality_Engine.sys stopped.\n" +
|
||||
">> SYSTEM: Rendering underlying code...\n" +
|
||||
">> LOADING: Ocean_Simulation.sh ... OK\n" +
|
||||
"root@sys:~$ print_string(\"海是存在的吗?\")";
|
||||
[SerializeField] private string previewTypingLine = "";
|
||||
|
||||
[Header("交互")]
|
||||
[SerializeField] private int maxInputLength = 20;
|
||||
[SerializeField] private string promptString = "> bit seaExists = ";
|
||||
[SerializeField] private string onCompleteNode = "苏醒";
|
||||
|
||||
[Header("启动日志")]
|
||||
[SerializeField] private List<BootLogEntry> bootSequence = new()
|
||||
{
|
||||
new BootLogEntry { text = ">> WARNING: TRAP DETECTED.", style = LogStyle.Alert, delayAfter = 0.5f },
|
||||
new BootLogEntry { text = ">> SYSTEM: Reality_Engine.sys stopped.", style = LogStyle.Dim, delayAfter = 0.2f },
|
||||
new BootLogEntry { text = ">> SYSTEM: Rendering underlying code...", style = LogStyle.Dim, delayAfter = 0.2f },
|
||||
new BootLogEntry { text = ">> LOADING: Ocean_Simulation.sh ... OK", style = LogStyle.Dim, delayAfter = 0.8f },
|
||||
new BootLogEntry { text = "root@sys:~$ print_string(\"海是存在的吗?\")", style = LogStyle.Highlight, delayAfter = 0.5f },
|
||||
};
|
||||
|
||||
#endregion
|
||||
|
||||
#region 状态
|
||||
|
||||
private enum TerminalPhase { Idle, BootSequence, WaitingInput, Confirming, EndAnimation, Done }
|
||||
|
||||
private TerminalPhase _phase = TerminalPhase.Idle;
|
||||
|
||||
// 可见文本行及其颜色
|
||||
private readonly List<string> _lines = new();
|
||||
private readonly List<Color> _lineColors = new();
|
||||
|
||||
// 打字机
|
||||
private string _typingTarget = "";
|
||||
private string _typingCurrent = "";
|
||||
private bool _typingDone = false;
|
||||
private Color _typingColor;
|
||||
private Coroutine _typewriterCoroutine;
|
||||
|
||||
// 输入
|
||||
private string _inputBuffer = "";
|
||||
private bool _cursorVisible = true;
|
||||
private Coroutine _cursorBlinkCoroutine;
|
||||
private Coroutine _crashSpamCoroutine;
|
||||
private double _editorLastRepaintTime;
|
||||
|
||||
// 全局 alpha(用于 DreamDoorSystem 淡入淡出)
|
||||
public float Alpha { get; set; } = 0f;
|
||||
public float MorphProgress
|
||||
{
|
||||
get => morphProgress;
|
||||
set => morphProgress = Mathf.Clamp01(value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 生命周期
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Alpha = 0f;
|
||||
MorphProgress = 0f;
|
||||
_phase = TerminalPhase.Idle;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
UpdateEditorPreview();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_phase == TerminalPhase.WaitingInput)
|
||||
HandleTypingInput();
|
||||
else if (_phase == TerminalPhase.Confirming)
|
||||
HandleConfirmInput();
|
||||
}
|
||||
|
||||
public override void OnDisable()
|
||||
{
|
||||
base.OnDisable();
|
||||
_phase = TerminalPhase.Idle;
|
||||
StopAllCoroutines();
|
||||
}
|
||||
|
||||
public override void OnEnable()
|
||||
{
|
||||
base.OnEnable();
|
||||
|
||||
if (!Application.isPlaying)
|
||||
ApplyEditorPreviewState();
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
MorphProgress = morphProgress;
|
||||
|
||||
if (!Application.isPlaying)
|
||||
ApplyEditorPreviewState();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 公共接口
|
||||
|
||||
public void Begin()
|
||||
{
|
||||
if (_phase != TerminalPhase.Idle) return;
|
||||
_lines.Clear();
|
||||
_lineColors.Clear();
|
||||
_inputBuffer = "";
|
||||
MorphProgress = 1f;
|
||||
StartCoroutine(RunBootSequence());
|
||||
}
|
||||
|
||||
public void ForceEnd()
|
||||
{
|
||||
StopAllCoroutines();
|
||||
_phase = TerminalPhase.Done;
|
||||
}
|
||||
|
||||
public void ResetVisualState()
|
||||
{
|
||||
StopAllCoroutines();
|
||||
_phase = TerminalPhase.Idle;
|
||||
_lines.Clear();
|
||||
_lineColors.Clear();
|
||||
_typingTarget = "";
|
||||
_typingCurrent = "";
|
||||
_typingDone = false;
|
||||
_inputBuffer = "";
|
||||
_cursorVisible = false;
|
||||
MorphProgress = 0f;
|
||||
}
|
||||
|
||||
[ContextMenu("Preview/Frame Only")]
|
||||
private void PreviewFrameOnly()
|
||||
{
|
||||
previewInEditor = true;
|
||||
previewStage = PreviewStage.FrameOnly;
|
||||
previewAlpha = 1f;
|
||||
previewMorphProgress = 0.65f;
|
||||
ApplyEditorPreviewState();
|
||||
}
|
||||
|
||||
[ContextMenu("Preview/Waiting Input")]
|
||||
private void PreviewWaitingInput()
|
||||
{
|
||||
previewInEditor = true;
|
||||
previewStage = PreviewStage.WaitingInput;
|
||||
previewAlpha = 1f;
|
||||
previewMorphProgress = 1f;
|
||||
ApplyEditorPreviewState();
|
||||
}
|
||||
|
||||
[ContextMenu("Preview/Clear")]
|
||||
private void ClearPreview()
|
||||
{
|
||||
previewInEditor = false;
|
||||
ApplyEditorPreviewState();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Shapes 绘制(DrawShapes 覆写)
|
||||
|
||||
public override void DrawShapes(Camera cam)
|
||||
{
|
||||
if (Alpha <= 0.01f) return;
|
||||
|
||||
float frameProgress = Mathf.SmoothStep(0f, 1f, MorphProgress);
|
||||
float textReveal = Mathf.InverseLerp(0.78f, 1f, frameProgress);
|
||||
Vector2 currentSize = Vector2.Lerp(morphStartSize, terminalSize, frameProgress);
|
||||
|
||||
using (Draw.Command(cam))
|
||||
{
|
||||
Draw.BlendMode = ShapesBlendMode.Transparent;
|
||||
Draw.ZTest = UnityEngine.Rendering.CompareFunction.Always;
|
||||
|
||||
using (Draw.MatrixScope)
|
||||
{
|
||||
Draw.Matrix = transform.localToWorldMatrix;
|
||||
DrawBackground(currentSize, frameProgress);
|
||||
DrawScanlines(currentSize, frameProgress);
|
||||
DrawBorder(currentSize, frameProgress);
|
||||
DrawLogLines(currentSize, textReveal);
|
||||
DrawInputArea(currentSize, textReveal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----- 背景 -----
|
||||
private void DrawBackground(Vector2 currentSize, float frameProgress)
|
||||
{
|
||||
Color bg = colBg;
|
||||
bg.a *= Alpha * Mathf.Lerp(0.18f, 1f, frameProgress);
|
||||
Draw.Rectangle(Vector3.zero, Quaternion.identity, currentSize, bg);
|
||||
}
|
||||
|
||||
// ----- CRT 扫描线 -----
|
||||
private void DrawScanlines(Vector2 currentSize, float frameProgress)
|
||||
{
|
||||
float scanReveal = Mathf.InverseLerp(0.35f, 1f, frameProgress);
|
||||
if (scanReveal <= 0f) return;
|
||||
|
||||
float halfH = currentSize.y * 0.5f;
|
||||
float halfW = currentSize.x * 0.5f;
|
||||
Color scanCol = new Color(0f, 0f, 0f, scanlineAlpha * Alpha * scanReveal);
|
||||
Draw.Thickness = scanlineSpacing * 0.5f;
|
||||
Draw.LineGeometry = LineGeometry.Flat2D;
|
||||
|
||||
// 横向扫描线(每隔 spacing 画一条暗线)
|
||||
float y = -halfH + scanlineSpacing;
|
||||
while (y < halfH)
|
||||
{
|
||||
Draw.Line(new Vector3(-halfW, y, 0f), new Vector3(halfW, y, 0f), scanCol);
|
||||
y += scanlineSpacing * 2f;
|
||||
}
|
||||
}
|
||||
|
||||
// ----- 琥珀色边框 -----
|
||||
private void DrawBorder(Vector2 currentSize, float frameProgress)
|
||||
{
|
||||
float borderReveal = Mathf.InverseLerp(0.15f, 0.78f, frameProgress);
|
||||
if (borderReveal <= 0f) return;
|
||||
|
||||
Color borderCol = colAmberMain;
|
||||
borderCol.a *= Alpha * borderReveal;
|
||||
Draw.RectangleBorder(Vector3.zero, Quaternion.identity, currentSize, borderThickness, borderCol);
|
||||
|
||||
float halfW = currentSize.x * 0.5f;
|
||||
float halfH = currentSize.y * 0.5f;
|
||||
float cornerLen = Mathf.Lerp(0.25f, 0.6f, borderReveal);
|
||||
float inset = borderThickness * 0.6f;
|
||||
|
||||
Draw.Thickness = borderThickness * 0.75f;
|
||||
Draw.LineGeometry = LineGeometry.Flat2D;
|
||||
|
||||
DrawCorner(new Vector2(-halfW + inset, halfH - inset), Vector2.right, Vector2.down, cornerLen, borderCol);
|
||||
DrawCorner(new Vector2( halfW - inset, halfH - inset), Vector2.left, Vector2.down, cornerLen, borderCol);
|
||||
DrawCorner(new Vector2(-halfW + inset, -halfH + inset), Vector2.right, Vector2.up, cornerLen, borderCol);
|
||||
DrawCorner(new Vector2( halfW - inset, -halfH + inset), Vector2.left, Vector2.up, cornerLen, borderCol);
|
||||
|
||||
float separatorReveal = Mathf.InverseLerp(0.45f, 0.95f, frameProgress);
|
||||
if (separatorReveal > 0f)
|
||||
{
|
||||
Color divCol = colAmberDim;
|
||||
divCol.a *= Alpha * separatorReveal;
|
||||
float botY = -currentSize.y * 0.5f + inputAreaHeight;
|
||||
float lineHalfWidth = (halfW - padding) * separatorReveal;
|
||||
Draw.Thickness = 0.015f;
|
||||
Draw.Line(
|
||||
new Vector3(-lineHalfWidth, botY, 0f),
|
||||
new Vector3(lineHalfWidth, botY, 0f),
|
||||
divCol
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ----- 日志文本区 -----
|
||||
private void DrawLogLines(Vector2 currentSize, float textReveal)
|
||||
{
|
||||
if (textReveal <= 0f) return;
|
||||
|
||||
float halfW = currentSize.x * 0.5f;
|
||||
float topY = currentSize.y * 0.5f - padding;
|
||||
|
||||
// 最多显示 maxVisibleLines 行
|
||||
int startIdx = Mathf.Max(0, _lines.Count - maxVisibleLines);
|
||||
|
||||
for (int i = startIdx; i < _lines.Count; i++)
|
||||
{
|
||||
int row = i - startIdx;
|
||||
float y = topY - row * lineHeight;
|
||||
|
||||
Color c = _lineColors.Count > i ? _lineColors[i] : colAmberMain;
|
||||
c.a *= Alpha * textReveal;
|
||||
|
||||
Draw.FontSize = fontSize;
|
||||
Draw.Text(
|
||||
new Vector3(-halfW + padding, y, 0f),
|
||||
Quaternion.identity,
|
||||
_lines[i],
|
||||
TextAlign.TopLeft,
|
||||
c
|
||||
);
|
||||
}
|
||||
|
||||
// 正在打字的当前行
|
||||
if (!string.IsNullOrEmpty(_typingCurrent))
|
||||
{
|
||||
int row = Mathf.Min(_lines.Count - startIdx, maxVisibleLines - 1);
|
||||
float y = topY - row * lineHeight;
|
||||
Color c = _typingColor;
|
||||
c.a *= Alpha * textReveal;
|
||||
Draw.FontSize = fontSize;
|
||||
Draw.Text(
|
||||
new Vector3(-halfW + padding, y, 0f),
|
||||
Quaternion.identity,
|
||||
_typingCurrent,
|
||||
TextAlign.TopLeft,
|
||||
c
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ----- 输入区 -----
|
||||
private void DrawInputArea(Vector2 currentSize, float textReveal)
|
||||
{
|
||||
if (textReveal <= 0f)
|
||||
return;
|
||||
|
||||
if (_phase != TerminalPhase.WaitingInput && _phase != TerminalPhase.Confirming)
|
||||
return;
|
||||
|
||||
float halfW = currentSize.x * 0.5f;
|
||||
float botY = -currentSize.y * 0.5f + inputAreaHeight;
|
||||
|
||||
// Prompt + 用户输入
|
||||
string display = promptString + _inputBuffer;
|
||||
Color promptCol = colAmberMain;
|
||||
promptCol.a *= Alpha * textReveal;
|
||||
float textY = botY - lineHeight * 0.6f;
|
||||
Draw.FontSize = fontSize;
|
||||
Draw.Text(
|
||||
new Vector3(-halfW + padding, textY, 0f),
|
||||
Quaternion.identity,
|
||||
display,
|
||||
TextAlign.TopLeft,
|
||||
promptCol
|
||||
);
|
||||
|
||||
// 闪烁光标
|
||||
if (_cursorVisible && _phase == TerminalPhase.WaitingInput)
|
||||
{
|
||||
// 估算文本宽度(粗略按字符数 × fontSize × 0.6)
|
||||
float textWidth = display.Length * fontSize * 0.6f;
|
||||
Vector3 cursorPos = new Vector3(-halfW + padding + textWidth, textY - cursorSize.y * 0.3f, 0f);
|
||||
Color cursorCol = colAmberMain;
|
||||
cursorCol.a *= Alpha * textReveal;
|
||||
Draw.Rectangle(cursorPos, Quaternion.identity, cursorSize, cursorCol);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawCorner(Vector2 pivot, Vector2 horizontalDir, Vector2 verticalDir, float length, Color color)
|
||||
{
|
||||
Draw.Line(
|
||||
new Vector3(pivot.x, pivot.y, 0f),
|
||||
new Vector3(pivot.x + horizontalDir.x * length, pivot.y + horizontalDir.y * length, 0f),
|
||||
color
|
||||
);
|
||||
Draw.Line(
|
||||
new Vector3(pivot.x, pivot.y, 0f),
|
||||
new Vector3(pivot.x + verticalDir.x * length, pivot.y + verticalDir.y * length, 0f),
|
||||
color
|
||||
);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 打字机
|
||||
|
||||
private void AddLine(string text, Color color)
|
||||
{
|
||||
_lines.Add(text);
|
||||
_lineColors.Add(color);
|
||||
}
|
||||
|
||||
private void AppendToLastLine(string text)
|
||||
{
|
||||
if (_lines.Count > 0)
|
||||
_lines[^1] += text;
|
||||
}
|
||||
|
||||
private IEnumerator TypewriterLine(string text, Color color, float charDelay = 0.025f)
|
||||
{
|
||||
_typingTarget = text;
|
||||
_typingCurrent = "";
|
||||
_typingColor = color;
|
||||
_typingDone = false;
|
||||
|
||||
foreach (char c in text)
|
||||
{
|
||||
_typingCurrent += c;
|
||||
yield return new WaitForSeconds(charDelay);
|
||||
}
|
||||
|
||||
// 提交为已完成行
|
||||
AddLine(_typingCurrent, color);
|
||||
_typingCurrent = "";
|
||||
_typingTarget = "";
|
||||
_typingDone = true;
|
||||
}
|
||||
|
||||
private IEnumerator WaitForTypewriter()
|
||||
{
|
||||
while (!_typingDone)
|
||||
yield return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Boot Sequence
|
||||
|
||||
private IEnumerator RunBootSequence()
|
||||
{
|
||||
_phase = TerminalPhase.BootSequence;
|
||||
yield return new WaitForSeconds(0.3f);
|
||||
|
||||
foreach (var entry in bootSequence)
|
||||
{
|
||||
Color c = StyleToColor(entry.style);
|
||||
_typewriterCoroutine = StartCoroutine(TypewriterLine(entry.text, c));
|
||||
yield return WaitForTypewriter();
|
||||
|
||||
if (entry.delayAfter > 0f)
|
||||
yield return new WaitForSeconds(entry.delayAfter);
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(0.3f);
|
||||
EnterInputPhase();
|
||||
}
|
||||
|
||||
private Color StyleToColor(LogStyle style) => style switch
|
||||
{
|
||||
LogStyle.Alert => colWhiteGlow,
|
||||
LogStyle.Highlight => colWhiteGlow,
|
||||
LogStyle.Dim => colAmberDim,
|
||||
_ => colAmberMain
|
||||
};
|
||||
|
||||
#endregion
|
||||
|
||||
#region Input Phase
|
||||
|
||||
private void EnterInputPhase()
|
||||
{
|
||||
_phase = TerminalPhase.WaitingInput;
|
||||
_inputBuffer = "";
|
||||
StartCursorBlink();
|
||||
}
|
||||
|
||||
private void HandleTypingInput()
|
||||
{
|
||||
foreach (char c in Input.inputString)
|
||||
{
|
||||
if (c == '\b')
|
||||
{
|
||||
if (_inputBuffer.Length > 0)
|
||||
_inputBuffer = _inputBuffer[..^1];
|
||||
}
|
||||
else if (c == '\n' || c == '\r')
|
||||
{
|
||||
if (_inputBuffer.Length > 0)
|
||||
EnterConfirmPhase();
|
||||
return;
|
||||
}
|
||||
else if (!char.IsControl(c) && _inputBuffer.Length < maxInputLength)
|
||||
{
|
||||
_inputBuffer += c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Confirm Phase
|
||||
|
||||
private void EnterConfirmPhase()
|
||||
{
|
||||
_phase = TerminalPhase.Confirming;
|
||||
StopCursorBlink();
|
||||
AddLine(">> Submit Reality Check? (Y/N)", colWhiteGlow);
|
||||
}
|
||||
|
||||
private void HandleConfirmInput()
|
||||
{
|
||||
if (Input.GetKeyDown(KeyCode.Y))
|
||||
{
|
||||
_phase = TerminalPhase.EndAnimation;
|
||||
StartCoroutine(HandleSubmission(_inputBuffer));
|
||||
}
|
||||
else if (Input.GetKeyDown(KeyCode.N))
|
||||
{
|
||||
_phase = TerminalPhase.WaitingInput;
|
||||
StartCursorBlink();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region End Animation
|
||||
|
||||
private IEnumerator HandleSubmission(string input)
|
||||
{
|
||||
string val = input.Trim().ToLower();
|
||||
bool isFalse = val is "false" or "0" or "no" or "不存在";
|
||||
|
||||
if (isFalse)
|
||||
yield return PlayShutdownEnding();
|
||||
else
|
||||
yield return PlayCrashEnding();
|
||||
|
||||
_phase = TerminalPhase.Done;
|
||||
yield return new WaitForSeconds(1f);
|
||||
DialogController.Instance.StartDialogNode(onCompleteNode);
|
||||
}
|
||||
|
||||
private IEnumerator PlayShutdownEnding()
|
||||
{
|
||||
AddLine(">> FALSE. SHUTTING DOWN...", colWhiteGlow);
|
||||
yield return new WaitForSeconds(1.5f);
|
||||
yield return DreamDoorSystem.Instance.PlayShutdownEffect();
|
||||
}
|
||||
|
||||
private IEnumerator PlayCrashEnding()
|
||||
{
|
||||
AddLine(">> TRUE. ERROR: OVERFLOW.", colWhiteGlow);
|
||||
yield return new WaitForSeconds(0.8f);
|
||||
|
||||
DreamDoorSystem.Instance.PlayCrashEffect();
|
||||
_crashSpamCoroutine = StartCoroutine(SpamErrorLines());
|
||||
|
||||
yield return new WaitForSeconds(3f);
|
||||
|
||||
if (_crashSpamCoroutine != null)
|
||||
StopCoroutine(_crashSpamCoroutine);
|
||||
|
||||
DreamDoorSystem.Instance.StopAllEffects();
|
||||
}
|
||||
|
||||
private IEnumerator SpamErrorLines()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
string hex = UnityEngine.Random.Range(0, 0xFFFF).ToString("x4");
|
||||
AddLine($"0x{hex} SYSTEM FAILURE", colAmberDim);
|
||||
yield return new WaitForSeconds(0.05f);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 光标闪烁
|
||||
|
||||
private void StartCursorBlink()
|
||||
{
|
||||
_cursorVisible = true;
|
||||
_cursorBlinkCoroutine = StartCoroutine(BlinkCursor());
|
||||
}
|
||||
|
||||
private void StopCursorBlink()
|
||||
{
|
||||
if (_cursorBlinkCoroutine != null)
|
||||
StopCoroutine(_cursorBlinkCoroutine);
|
||||
_cursorVisible = false;
|
||||
}
|
||||
|
||||
private IEnumerator BlinkCursor()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
_cursorVisible = !_cursorVisible;
|
||||
yield return new WaitForSeconds(cursorBlinkRate);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 数据结构
|
||||
|
||||
[Serializable]
|
||||
public class BootLogEntry
|
||||
{
|
||||
public string text;
|
||||
public LogStyle style;
|
||||
public float delayAfter;
|
||||
}
|
||||
|
||||
private enum PreviewStage
|
||||
{
|
||||
Hidden,
|
||||
FrameOnly,
|
||||
BootText,
|
||||
WaitingInput,
|
||||
Confirming
|
||||
}
|
||||
|
||||
public enum LogStyle { Normal, Alert, Highlight, Dim }
|
||||
|
||||
#endregion
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void UpdateEditorPreview()
|
||||
{
|
||||
ApplyEditorPreviewState();
|
||||
|
||||
if (previewInEditor)
|
||||
{
|
||||
EditorApplication.QueuePlayerLoopUpdate();
|
||||
|
||||
double now = EditorApplication.timeSinceStartup;
|
||||
if (now - _editorLastRepaintTime > 0.08d)
|
||||
{
|
||||
_editorLastRepaintTime = now;
|
||||
SceneView.RepaintAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyEditorPreviewState()
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
return;
|
||||
|
||||
if (!previewInEditor)
|
||||
{
|
||||
Alpha = 0f;
|
||||
MorphProgress = 0f;
|
||||
_phase = TerminalPhase.Idle;
|
||||
_lines.Clear();
|
||||
_lineColors.Clear();
|
||||
_typingCurrent = "";
|
||||
_inputBuffer = "";
|
||||
_cursorVisible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
Alpha = previewAlpha;
|
||||
MorphProgress = previewMorphProgress;
|
||||
|
||||
_lines.Clear();
|
||||
_lineColors.Clear();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(previewLines))
|
||||
{
|
||||
var previewEntries = previewLines.Replace("\r\n", "\n").Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var line in previewEntries)
|
||||
AddPreviewLine(line);
|
||||
}
|
||||
|
||||
_typingCurrent = previewTypingLine ?? "";
|
||||
_typingColor = colAmberMain;
|
||||
_inputBuffer = previewInputBuffer ?? "";
|
||||
|
||||
switch (previewStage)
|
||||
{
|
||||
case PreviewStage.Hidden:
|
||||
Alpha = 0f;
|
||||
MorphProgress = 0f;
|
||||
_phase = TerminalPhase.Idle;
|
||||
_cursorVisible = false;
|
||||
break;
|
||||
case PreviewStage.FrameOnly:
|
||||
_phase = TerminalPhase.Idle;
|
||||
_typingCurrent = "";
|
||||
_cursorVisible = false;
|
||||
break;
|
||||
case PreviewStage.BootText:
|
||||
_phase = TerminalPhase.BootSequence;
|
||||
_cursorVisible = false;
|
||||
break;
|
||||
case PreviewStage.WaitingInput:
|
||||
_phase = TerminalPhase.WaitingInput;
|
||||
_cursorVisible = animatePreviewCursor
|
||||
? ((EditorApplication.timeSinceStartup % (cursorBlinkRate * 2f)) < cursorBlinkRate)
|
||||
: true;
|
||||
break;
|
||||
case PreviewStage.Confirming:
|
||||
_phase = TerminalPhase.Confirming;
|
||||
_cursorVisible = false;
|
||||
AddPreviewLine(">> Submit Reality Check? (Y/N)");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddPreviewLine(string text)
|
||||
{
|
||||
Color color = text.Contains("WARNING") || text.Contains("FALSE") || text.Contains("ERROR")
|
||||
? colWhiteGlow
|
||||
: text.Contains("SYSTEM") || text.Contains("LOADING")
|
||||
? colAmberDim
|
||||
: colAmberMain;
|
||||
|
||||
_lines.Add(text);
|
||||
_lineColors.Add(color);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: df85ec8ec1f91ba4a94b313b13053046
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user