feat(ui): 重构终端面板,拆分主菜单与局内终端

This commit is contained in:
2026-07-02 02:44:02 +08:00
parent 73e6b86579
commit 3e2d1e931a
22 changed files with 896 additions and 214 deletions
@@ -3,7 +3,6 @@ using System.Collections;
using System.Collections.Generic;
using AibisDream.Framework;
using AibisDream.UI;
using Newtonsoft.Json.Linq;
using UnityEngine;
namespace AibisDream.SaveSystem
@@ -277,6 +276,7 @@ namespace AibisDream.SaveSystem
if (ui != null)
{
ui.HideTerminal();
ui.GetPanel<InGameTerminalPanel>()?.Close();
ui.ShowPanel<MainPanel>();
ui.HidePanel<SavesPanel>();
ui.HidePanel<EndPanel>();
@@ -1,7 +1,6 @@
using AibisDream.Framework;
using AibisDream.Kit;
using AibisDream.UI;
using AibisDream.UI.Terminal;
using TMPro;
using UnityEngine;
@@ -36,12 +35,14 @@ namespace AibisDream
return;
}
UIManager.Instance.GetPanel<TerminalPanel>()?.AppendDialogLine(line);
var panel = UIManager.Instance.GetPanel<InGameTerminalPanel>();
panel?.AppendDialogLine(line);
}
private void CleanDialogHistory()
{
UIManager.Instance.GetPanel<TerminalPanel>()?.ClearDialog();
var panel = UIManager.Instance.GetPanel<InGameTerminalPanel>();
panel?.ClearDialog();
}
public void CloseCanvas()
@@ -0,0 +1,11 @@
namespace AibisDream.UI
{
public interface ITerminalPanelHost
{
void SetHeader(string title, string status = "");
void Back();
void Close();
void CloseToMainMenu();
bool IsInGameContext { get; }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b7c6d5e4f3a2918071625344a5b6c7d8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,212 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace AibisDream.UI
{
public enum InGameTerminalPage
{
None,
Setting,
Record,
}
/// <summary>
/// 局内终端 Panel 壳:管理 Setting / Record 两个 Page。
/// Setting / Record 需在场景中分别挂独立的 Prefab 实例,并在 Inspector 中绑定。
/// </summary>
public class InGameTerminalPanel : MonoBehaviour, IUIPanel, ITerminalPanelHost
{
[SerializeField] private TerminalHeaderView header;
[SerializeField] private TerminalSettingPanel settingPage;
[SerializeField] private TerminalRecordPanel recordPage;
private readonly Stack<InGameTerminalPage> _backStack = new();
private InGameTerminalPage _currentPage = InGameTerminalPage.None;
private bool _pausedByPanel;
public bool IsOpen => gameObject.activeSelf && _currentPage != InGameTerminalPage.None;
public bool IsCloseable => true;
public InGameTerminalPage CurrentPage => _currentPage;
public bool IsInGameContext => true;
public void Initialize()
{
var root = transform;
settingPage ??= root.GetComponentInChildren<TerminalSettingPanel>(true);
recordPage ??= root.GetComponentInChildren<TerminalRecordPanel>(true);
settingPage?.SetOwner(this);
recordPage?.SetOwner(this);
Close();
}
public bool HasRequiredPages()
{
return settingPage != null && recordPage != null;
}
public void Show()
{
Open(InGameTerminalPage.Setting);
}
public void Hide()
{
Close();
}
public void Open(InGameTerminalPage page)
{
if (!HasRequiredPages() || page == InGameTerminalPage.None)
{
return;
}
if (!IsOpen)
{
_backStack.Clear();
}
gameObject.SetActive(true);
PauseIfNeeded();
SwitchTo(page, pushCurrent: false);
}
public void SwitchTo(InGameTerminalPage page, bool pushCurrent = true)
{
if (page == InGameTerminalPage.None || page == _currentPage)
{
return;
}
if (pushCurrent && _currentPage != InGameTerminalPage.None)
{
_backStack.Push(_currentPage);
}
var previous = _currentPage;
_currentPage = page;
if (previous == InGameTerminalPage.None)
{
ShowPage(_currentPage);
return;
}
HidePage(previous, () => ShowPage(_currentPage));
}
public void Back()
{
if (_backStack.Count > 0)
{
var previous = _backStack.Pop();
var current = _currentPage;
_currentPage = previous;
HidePage(current, () => ShowPage(_currentPage));
return;
}
Close();
}
public bool HandleEscape()
{
if (!IsOpen)
{
return false;
}
Back();
return true;
}
public void Close()
{
HideAllPages();
_backStack.Clear();
_currentPage = InGameTerminalPage.None;
ContinueIfPaused();
gameObject.SetActive(false);
}
public void CloseToMainMenu()
{
Close();
GameManager.Instance.QuitGame();
}
public void SetHeader(string title, string status = "")
{
header?.SetText(title, status);
}
public void AppendDialogLine(LineInfo lineInfo)
{
recordPage?.AppendDialogLine(lineInfo);
}
public void ClearDialog()
{
recordPage?.ClearDialog();
}
private void PauseIfNeeded()
{
if (GameManager.Instance == null || !GameManager.Instance.state.isInGame || _pausedByPanel)
{
return;
}
GameManager.Instance.PauseGame();
_pausedByPanel = true;
}
private void ContinueIfPaused()
{
if (!_pausedByPanel)
{
return;
}
GameManager.Instance.ContinueGame();
_pausedByPanel = false;
}
private void ShowPage(InGameTerminalPage page)
{
switch (page)
{
case InGameTerminalPage.Setting:
settingPage?.Show();
break;
case InGameTerminalPage.Record:
recordPage?.Show();
break;
}
}
private void HidePage(InGameTerminalPage page, Action onComplete = null)
{
switch (page)
{
case InGameTerminalPage.Setting:
settingPage?.Hide(onComplete);
break;
case InGameTerminalPage.Record:
recordPage?.Hide(onComplete);
break;
default:
onComplete?.Invoke();
break;
}
}
private void HideAllPages()
{
settingPage?.Hide();
recordPage?.Hide();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e8f4a2b1c3d54e6f9a0b1c2d3e4f5a6b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,144 @@
using System;
using AibisDream;
using AibisDream.Utility;
using TMPro;
using UnityEngine;
using UnityEngine.Localization.Components;
using UnityEngine.UI;
namespace AibisDream.UI
{
[RequireComponent(typeof(BaseButton))]
public class TerminalButton : MonoBehaviour
{
[SerializeField] private BaseButton button;
[SerializeField] private Image buttonImage;
[SerializeField] private Image backgroundImage;
[SerializeField] private TMP_Text labelText;
[SerializeField] private LocalizeStringEvent labelLocalize;
[Header("文字")]
[SerializeField] private string defaultLabel = string.Empty;
[Header("前景色彩(文字 + 按钮图)")]
[SerializeField] private Color normalForegroundColor = Color.white;
[SerializeField] private Color hoverForegroundColor = new Color(0.02f, 0.02f, 0.04f);
[Header("底图色彩")]
[SerializeField] private Color normalBackgroundColor = new Color(1f, 1f, 1f, 0f);
[SerializeField] private Color hoverBackgroundColor = Color.white;
[Header("禁用")]
[SerializeField] private Color disabledForegroundColor = new Color(0.78431374f, 0.78431374f, 0.78431374f, 0.5f);
[SerializeField] private Color disabledBackgroundColor = new Color(1f, 1f, 1f, 0.3f);
public BaseButton Button => button != null ? button : (button = GetComponent<BaseButton>());
private void Awake()
{
button = GetComponent<BaseButton>();
if (buttonImage == null)
{
buttonImage = GetComponent<Image>();
}
if (labelLocalize == null && labelText != null)
{
labelLocalize = labelText.GetComponent<LocalizeStringEvent>();
}
button.onStateTransition.AddListener(OnStateTransition);
if (!string.IsNullOrEmpty(defaultLabel))
{
SetLabel(defaultLabel);
}
}
private void OnEnable()
{
if (button != null)
{
OnStateTransition(button.CurSelectionState, true);
}
}
private void OnDestroy()
{
if (button != null)
{
button.onStateTransition.RemoveListener(OnStateTransition);
}
}
public void SetLabel(string value)
{
if (labelLocalize != null && IsLocalizationKey(value))
{
labelLocalize.enabled = true;
labelLocalize.SetTable(ConstRef.UITextTable);
labelLocalize.SetEntry(value);
return;
}
if (labelLocalize != null)
{
labelLocalize.enabled = false;
}
if (labelText != null)
{
labelText.text = value;
}
}
private void OnStateTransition(SelectionType state, bool instant)
{
switch (state)
{
case SelectionType.Disabled:
ApplyForegroundColor(disabledForegroundColor);
ApplyBackgroundColor(disabledBackgroundColor);
break;
case SelectionType.Highlighted:
case SelectionType.Pressed:
case SelectionType.Selected:
ApplyForegroundColor(hoverForegroundColor);
ApplyBackgroundColor(hoverBackgroundColor);
break;
default:
ApplyForegroundColor(normalForegroundColor);
ApplyBackgroundColor(normalBackgroundColor);
break;
}
}
private void ApplyForegroundColor(Color color)
{
if (buttonImage != null)
{
buttonImage.color = color;
}
if (labelText != null)
{
labelText.color = color;
}
}
private void ApplyBackgroundColor(Color color)
{
if (backgroundImage != null)
{
backgroundImage.color = color;
}
}
private static bool IsLocalizationKey(string value)
{
return !string.IsNullOrEmpty(value) && value.Contains("_", StringComparison.Ordinal);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: febed96817a804e47ace59be0d7c3995
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -3,7 +3,7 @@ using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace AibisDream.UI.Terminal
namespace AibisDream.UI
{
[RequireComponent(typeof(BaseButton))]
public class TerminalChapterItemView : MonoBehaviour
@@ -14,8 +14,8 @@ namespace AibisDream.UI.Terminal
[SerializeField] private TMP_Text statusText;
[SerializeField] private Image selectionFrame;
[SerializeField] private CanvasGroup canvasGroup;
[SerializeField] private float selectedScale = 1.05f;
[SerializeField] private float previewScale = 1f;
[SerializeField] private float selectedScale = 1f;
[SerializeField] private float previewScale = 0.45f;
private BaseButton _button;
private ChapterVo _chapterVo;
@@ -52,7 +52,9 @@ namespace AibisDream.UI.Terminal
if (coverImage != null)
{
coverImage.sprite = chapterVo.pic;
coverImage.enabled = chapterVo.pic != null;
var hasCover = chapterVo.pic != null;
coverImage.enabled = hasCover;
coverImage.preserveAspect = true;
}
if (titleText != null)
@@ -1,9 +1,10 @@
using System;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace AibisDream.UI.Terminal
namespace AibisDream.UI
{
public class TerminalChapterPanel : MonoBehaviour
{
@@ -47,31 +48,37 @@ namespace AibisDream.UI.Terminal
public void Show()
{
_owner?.SetHeader("<选择章节>");
LoadChapters();
if (panelAnimator != null)
{
panelAnimator.Show(RefreshViews);
panelAnimator.Show(prepare: PrepareContent);
}
else
{
PrepareContent();
gameObject.SetActive(true);
RefreshViews();
}
}
public void Hide()
public void Hide(Action onComplete = null)
{
if (panelAnimator != null)
{
panelAnimator.Hide();
panelAnimator.Hide(onComplete);
}
else
{
gameObject.SetActive(false);
onComplete?.Invoke();
}
}
private void PrepareContent()
{
_owner?.SetHeader("terminal_header_chapter", "REPAIR_STATION_07 - AUTH_VERIFIED");
LoadChapters();
RefreshViews();
}
public void Close()
{
_owner.Back();
@@ -1,24 +1,59 @@
using System;
using AibisDream.Utility;
using TMPro;
using UnityEngine;
using UnityEngine.Localization.Components;
namespace AibisDream.UI.Terminal
namespace AibisDream.UI
{
public class TerminalHeaderView : MonoBehaviour
{
[SerializeField] private TMP_Text titleText;
[SerializeField] private LocalizeStringEvent titleLocalize;
[SerializeField] private TMP_Text statusText;
private void Awake()
{
if (titleLocalize == null && titleText != null)
{
titleLocalize = titleText.GetComponent<LocalizeStringEvent>();
}
}
public void SetText(string title, string status = "")
{
if (titleText != null)
{
titleText.text = title;
}
SetTitle(title);
if (statusText != null)
{
statusText.text = status;
}
}
public void SetTitle(string title)
{
if (titleLocalize != null && IsLocalizationKey(title))
{
titleLocalize.enabled = true;
titleLocalize.SetTable(ConstRef.UITextTable);
titleLocalize.SetEntry(title);
return;
}
if (titleLocalize != null)
{
titleLocalize.enabled = false;
}
if (titleText != null)
{
titleText.text = title;
}
}
private static bool IsLocalizationKey(string value)
{
return !string.IsNullOrEmpty(value) && value.Contains("_", StringComparison.Ordinal);
}
}
}
@@ -1,8 +1,9 @@
using System;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace AibisDream.UI.Terminal
namespace AibisDream.UI
{
public class TerminalLoadingPanel : MonoBehaviour, IUIPanel
{
@@ -18,6 +19,32 @@ namespace AibisDream.UI.Terminal
public bool IsCloseable => false;
public void Show()
{
if (panelAnimator != null)
{
panelAnimator.Show(prepare: PrepareContent);
}
else
{
PrepareContent();
gameObject.SetActive(true);
}
}
public void Hide(Action onComplete = null)
{
if (panelAnimator != null)
{
panelAnimator.Hide(onComplete);
}
else
{
gameObject.SetActive(false);
onComplete?.Invoke();
}
}
private void PrepareContent()
{
SetText(companyText, "SATISFYING INC");
SetText(deviceText, "Device Model: Dell P5999 DX130\nDevice Serial Number: SN46456546DAS");
@@ -29,27 +56,6 @@ namespace AibisDream.UI.Terminal
{
avatarImage.gameObject.SetActive(avatarImage.sprite != null);
}
if (panelAnimator != null)
{
panelAnimator.Show();
}
else
{
gameObject.SetActive(true);
}
}
public void Hide()
{
if (panelAnimator != null)
{
panelAnimator.Hide();
}
else
{
gameObject.SetActive(false);
}
}
public void SetProgress(float progress)
@@ -1,18 +1,22 @@
using System;
using AibisDream.Utility;
using TMPro;
using UnityEngine;
using UnityEngine.Localization.Components;
using UnityEngine.UI;
namespace AibisDream.UI.Terminal
namespace AibisDream.UI
{
[RequireComponent(typeof(BaseButton))]
public class TerminalMenuItemView : MonoBehaviour
{
[SerializeField] private TMP_Text labelText;
[SerializeField] private LocalizeStringEvent labelLocalize;
[SerializeField] private TMP_Text statusText;
[SerializeField] private TMP_Text detailText;
[SerializeField] private TMP_Text cursor;
[SerializeField] private Image selectionBar;
[SerializeField] private string defaultLabelKey = string.Empty;
[SerializeField] private Color normalTextColor = Color.white;
[SerializeField] private Color selectedTextColor = new Color(0.02f, 0.02f, 0.04f);
[SerializeField] private Color dangerTextColor = new Color(1f, 0.18f, 0.2f);
@@ -35,9 +39,19 @@ namespace AibisDream.UI.Terminal
_canvasGroup = gameObject.AddComponent<CanvasGroup>();
}
if (labelLocalize == null && labelText != null)
{
labelLocalize = labelText.GetComponent<LocalizeStringEvent>();
}
_button.onClick.AddListener(HandleClick);
_button.onStateTransition.AddListener(OnButtonStateTransition);
SetSelected(false);
if (!string.IsNullOrEmpty(defaultLabelKey))
{
SetLabel(defaultLabelKey);
}
}
private void OnDestroy()
@@ -49,13 +63,9 @@ namespace AibisDream.UI.Terminal
}
}
public void SetContent(string label, string status = "", string detail = "", bool isDanger = false)
public void SetContent(string label, string status = "", string detail = "")
{
_isDanger = isDanger;
if (labelText != null)
{
labelText.text = label;
}
SetLabel(label);
if (statusText != null)
{
@@ -70,6 +80,27 @@ namespace AibisDream.UI.Terminal
RefreshVisual();
}
public void SetLabel(string value)
{
if (labelLocalize != null && IsLocalizationKey(value))
{
labelLocalize.enabled = true;
labelLocalize.SetTable(ConstRef.UITextTable);
labelLocalize.SetEntry(value);
return;
}
if (labelLocalize != null)
{
labelLocalize.enabled = false;
}
if (labelText != null)
{
labelText.text = value;
}
}
public void SetInteractable(bool interactable)
{
if (_button != null)
@@ -135,5 +166,10 @@ namespace AibisDream.UI.Terminal
text.color = color;
}
}
private static bool IsLocalizationKey(string value)
{
return !string.IsNullOrEmpty(value) && value.Contains("_", StringComparison.Ordinal);
}
}
}
@@ -3,7 +3,7 @@ using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace AibisDream.UI.Terminal
namespace AibisDream.UI
{
public class TerminalOptionItemView : MonoBehaviour
{
+61 -85
View File
@@ -1,8 +1,9 @@
using System;
using System;
using System.Collections.Generic;
using UnityEngine;
namespace AibisDream.UI.Terminal
namespace AibisDream.UI
{
public enum TerminalPage
{
@@ -10,56 +11,47 @@ namespace AibisDream.UI.Terminal
Start,
Setting,
Chapter,
Record
}
public enum TerminalOpenSource
{
None,
MainMenu,
InGame
}
public class TerminalPanel : MonoBehaviour, IUIPanel
/// <summary>
/// 主菜单终端 Panel 壳:管理 Start / Chapter / Setting 三个 Page。
/// </summary>
public class TerminalPanel : MonoBehaviour, IUIPanel, ITerminalPanelHost
{
[SerializeField] private TerminalHeaderView header;
[SerializeField] private TerminalStartPanel startPage;
[SerializeField] private TerminalSettingPanel settingPage;
[SerializeField] private TerminalChapterPanel chapterPage;
[SerializeField] private TerminalRecordPanel recordPage;
private readonly Stack<TerminalPage> _backStack = new();
private TerminalPage _currentPage = TerminalPage.None;
private TerminalOpenSource _source = TerminalOpenSource.None;
private bool _pausedByTerminal;
public bool IsOpen => gameObject.activeSelf && _currentPage != TerminalPage.None;
public bool IsCloseable => _source == TerminalOpenSource.InGame;
public bool IsCloseable => false;
public TerminalPage CurrentPage => _currentPage;
public TerminalOpenSource Source => _source;
public bool IsInGameContext => false;
public void Initialize(RectTransform searchRoot)
public void Initialize()
{
startPage ??= searchRoot.GetComponentInChildren<TerminalStartPanel>(true);
settingPage ??= searchRoot.GetComponentInChildren<TerminalSettingPanel>(true);
chapterPage ??= searchRoot.GetComponentInChildren<TerminalChapterPanel>(true);
recordPage ??= searchRoot.GetComponentInChildren<TerminalRecordPanel>(true);
var root = transform;
startPage ??= root.GetComponentInChildren<TerminalStartPanel>(true);
settingPage ??= root.GetComponentInChildren<TerminalSettingPanel>(true);
chapterPage ??= root.GetComponentInChildren<TerminalChapterPanel>(true);
startPage?.SetOwner(this);
settingPage?.SetOwner(this);
chapterPage?.SetOwner(this);
recordPage?.SetOwner(this);
Close();
}
public bool HasRequiredPages()
{
return startPage != null && settingPage != null && chapterPage != null && recordPage != null;
return startPage != null && settingPage != null && chapterPage != null;
}
public void Show()
{
Open(TerminalPage.Start, TerminalOpenSource.MainMenu);
Open(TerminalPage.Start);
}
public void Hide()
@@ -67,21 +59,19 @@ namespace AibisDream.UI.Terminal
Close();
}
public void Open(TerminalPage page, TerminalOpenSource source)
public void Open(TerminalPage page)
{
if (!HasRequiredPages())
if (!HasRequiredPages() || page == TerminalPage.None)
{
return;
}
if (!IsOpen || _source != source)
if (!IsOpen)
{
_backStack.Clear();
}
gameObject.SetActive(true);
_source = source;
PauseIfNeeded(page, source);
SwitchTo(page, pushCurrent: false);
}
@@ -97,25 +87,26 @@ namespace AibisDream.UI.Terminal
_backStack.Push(_currentPage);
}
HidePage(_currentPage);
var previous = _currentPage;
_currentPage = page;
ShowPage(_currentPage);
if (previous == TerminalPage.None)
{
ShowPage(_currentPage);
return;
}
HidePage(previous, () => ShowPage(_currentPage));
}
public void Back()
{
if (_source == TerminalOpenSource.InGame)
{
Close();
return;
}
if (_backStack.Count > 0)
{
var previous = _backStack.Pop();
HidePage(_currentPage);
var current = _currentPage;
_currentPage = previous;
ShowPage(_currentPage);
HidePage(current, () => ShowPage(_currentPage));
return;
}
@@ -152,8 +143,6 @@ namespace AibisDream.UI.Terminal
HideAllPages();
_backStack.Clear();
_currentPage = TerminalPage.None;
_source = TerminalOpenSource.None;
ContinueIfPaused();
gameObject.SetActive(false);
}
@@ -163,46 +152,11 @@ namespace AibisDream.UI.Terminal
GameManager.Instance.QuitGame();
}
public void AppendDialogLine(LineInfo lineInfo)
{
recordPage?.AppendDialogLine(lineInfo);
}
public void ClearDialog()
{
recordPage?.ClearDialog();
}
public void SetHeader(string title, string status = "")
{
header?.SetText(title, status);
}
private void PauseIfNeeded(TerminalPage page, TerminalOpenSource source)
{
if (source != TerminalOpenSource.InGame || page is not (TerminalPage.Setting or TerminalPage.Record))
{
return;
}
if (!_pausedByTerminal)
{
GameManager.Instance.PauseGame();
_pausedByTerminal = true;
}
}
private void ContinueIfPaused()
{
if (!_pausedByTerminal)
{
return;
}
GameManager.Instance.ContinueGame();
_pausedByTerminal = false;
}
private void ShowPage(TerminalPage page)
{
switch (page)
@@ -216,27 +170,50 @@ namespace AibisDream.UI.Terminal
case TerminalPage.Chapter:
chapterPage?.Show();
break;
case TerminalPage.Record:
recordPage?.Show();
break;
}
}
private void HidePage(TerminalPage page)
private void HidePage(TerminalPage page, Action onComplete = null)
{
switch (page)
{
case TerminalPage.Start:
startPage?.Hide();
HidePage(startPage, onComplete);
break;
case TerminalPage.Setting:
settingPage?.Hide();
HidePage(settingPage, onComplete);
break;
case TerminalPage.Chapter:
chapterPage?.Hide();
HidePage(chapterPage, onComplete);
break;
case TerminalPage.Record:
recordPage?.Hide();
default:
onComplete?.Invoke();
break;
}
}
private static void HidePage(MonoBehaviour page, Action onComplete)
{
if (page == null)
{
onComplete?.Invoke();
return;
}
switch (page)
{
case TerminalStartPanel startPanel:
startPanel.Hide(onComplete);
break;
case TerminalSettingPanel settingPanel:
settingPanel.Hide(onComplete);
break;
case TerminalChapterPanel chapterPanel:
chapterPanel.Hide(onComplete);
break;
default:
page.gameObject.SetActive(false);
onComplete?.Invoke();
break;
}
}
@@ -246,7 +223,6 @@ namespace AibisDream.UI.Terminal
startPage?.Hide();
settingPage?.Hide();
chapterPage?.Hide();
recordPage?.Hide();
}
}
}
@@ -1,15 +1,20 @@
using System;
using System;
using DG.Tweening;
using UnityEngine;
namespace AibisDream.UI.Terminal
namespace AibisDream.UI
{
public class TerminalPanelAnimator : MonoBehaviour
{
[SerializeField] private CanvasGroup canvasGroup;
[SerializeField] private RectTransform sweep;
[SerializeField] private float fadeDuration = 0.18f;
[SerializeField] private float sweepDuration = 0.24f;
[Header("终端风格")]
[SerializeField, Range(0f, 1f)] private float visibleAlpha = 0.88f;
[SerializeField] private float fadeDuration = 0.06f;
[SerializeField] private Ease showEase = Ease.Linear;
[SerializeField] private Ease hideEase = Ease.Linear;
[SerializeField] private float sweepDuration = 0.08f;
private Tween _activeTween;
@@ -21,10 +26,23 @@ namespace AibisDream.UI.Terminal
}
}
public void Show(Action onComplete = null)
/// <param name="onComplete">淡入完成回调。</param>
/// <param name="prepare">在 SetActive 之前执行的内容准备(赋值、布局等)。</param>
public void Show(Action onComplete = null, Action prepare = null)
{
gameObject.SetActive(true);
_activeTween?.Kill();
prepare?.Invoke();
if (canvasGroup != null)
{
canvasGroup.alpha = 0f;
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
}
PrepareSweep();
gameObject.SetActive(true);
Canvas.ForceUpdateCanvases();
if (canvasGroup == null)
{
@@ -32,16 +50,11 @@ namespace AibisDream.UI.Terminal
return;
}
canvasGroup.alpha = 0f;
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
PrepareSweep();
var sequence = DOTween.Sequence().SetUpdate(true);
sequence.Join(canvasGroup.DOFade(1f, fadeDuration));
sequence.Join(canvasGroup.DOFade(visibleAlpha, fadeDuration).SetEase(showEase));
if (sweep != null)
{
sequence.Join(sweep.DOAnchorPosX(0f, sweepDuration).SetEase(Ease.OutQuad));
sequence.Join(sweep.DOAnchorPosX(0f, sweepDuration).SetEase(Ease.Linear));
}
sequence.OnComplete(() =>
@@ -69,6 +82,7 @@ namespace AibisDream.UI.Terminal
_activeTween = canvasGroup
.DOFade(0f, fadeDuration)
.SetEase(hideEase)
.SetUpdate(true)
.OnComplete(() =>
{
@@ -1,7 +1,7 @@
using TMPro;
using UnityEngine;
namespace AibisDream.UI.Terminal
namespace AibisDream.UI
{
public class TerminalRecordItemView : MonoBehaviour
{
@@ -13,11 +13,13 @@ namespace AibisDream.UI.Terminal
if (actorNameText != null)
{
actorNameText.text = lineInfo.character.GetActorName();
actorNameText.ForceMeshUpdate();
}
if (lineText != null)
{
lineText.text = lineInfo.GetRecordLine();
lineText.ForceMeshUpdate();
}
gameObject.SetActive(true);
+105 -12
View File
@@ -1,11 +1,15 @@
using System;
using System.Collections.Generic;
using AibisDream.Utility;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace AibisDream.UI.Terminal
namespace AibisDream.UI
{
/// <summary>
/// 终端 Record Page(遗留类名 Panel)。挂在局内 InGameTerminalPanel 壳下。
/// </summary>
public class TerminalRecordPanel : MonoBehaviour
{
[SerializeField] private TerminalPanelAnimator panelAnimator;
@@ -16,11 +20,11 @@ namespace AibisDream.UI.Terminal
[SerializeField] private TMP_Text emptyText;
private readonly List<LineInfo> _recordDataList = new List<LineInfo>();
private InGameTerminalPanel _owner;
public bool IsOpen => gameObject.activeSelf;
private TerminalPanel _owner;
public void SetOwner(TerminalPanel owner)
public void SetOwner(InGameTerminalPanel owner)
{
_owner = owner;
}
@@ -28,6 +32,39 @@ namespace AibisDream.UI.Terminal
private void Awake()
{
backButton?.onClick.AddListener(ReturnGame);
EnsureScrollViewportMask();
}
private void EnsureScrollViewportMask()
{
if (scrollRect == null)
{
return;
}
var legacyMask = scrollRect.GetComponent<Mask>();
if (legacyMask != null)
{
legacyMask.enabled = false;
}
if (scrollRect.TryGetComponent<Image>(out var scrollImage))
{
var color = scrollImage.color;
color.a = 0f;
scrollImage.color = color;
scrollImage.raycastTarget = true;
}
if (scrollRect.viewport == null)
{
return;
}
if (scrollRect.viewport.GetComponent<RectMask2D>() == null)
{
scrollRect.viewport.gameObject.AddComponent<RectMask2D>();
}
}
private void OnDestroy()
@@ -37,30 +74,34 @@ namespace AibisDream.UI.Terminal
public void Show()
{
_owner?.SetHeader("<诊疗记录>", "EMR 511/42 - Clinical Record");
DrawRecords();
_owner?.SetHeader("termnial_header_record", "REPAIR_STATION_07 - AUTH_VERIFIED");
if (panelAnimator != null)
{
panelAnimator.Show();
panelAnimator.Show(onComplete: () => StartCoroutine(DrawRecordsWhenReady()));
}
else
{
gameObject.SetActive(true);
StartCoroutine(DrawRecordsWhenReady());
}
}
public void Hide()
public void Hide(Action onComplete = null)
{
ClearRecordItems();
if (panelAnimator != null)
{
panelAnimator.Hide();
panelAnimator.Hide(() =>
{
ClearRecordItems();
onComplete?.Invoke();
});
}
else
{
ClearRecordItems();
gameObject.SetActive(false);
onComplete?.Invoke();
}
}
public void AppendDialogLine(LineInfo lineInfo)
@@ -73,10 +114,50 @@ namespace AibisDream.UI.Terminal
_recordDataList.Clear();
}
private System.Collections.IEnumerator DrawRecordsWhenReady()
{
yield return null;
DrawRecords();
}
private void ConfigureContentRect()
{
if (recordRoot is not RectTransform content)
{
return;
}
content.anchorMin = new Vector2(0f, 1f);
content.anchorMax = new Vector2(1f, 1f);
content.pivot = new Vector2(0.5f, 1f);
content.anchoredPosition = Vector2.zero;
content.sizeDelta = Vector2.zero;
}
private static void ConfigureRecordItemRect(RectTransform itemRect)
{
itemRect.anchorMin = new Vector2(0f, 1f);
itemRect.anchorMax = new Vector2(1f, 1f);
itemRect.pivot = new Vector2(0.5f, 1f);
itemRect.sizeDelta = Vector2.zero;
}
private static void SyncUiLayer(GameObject target, int layer)
{
target.layer = layer;
for (var i = 0; i < target.transform.childCount; i++)
{
SyncUiLayer(target.transform.GetChild(i).gameObject, layer);
}
}
private void DrawRecords()
{
ClearRecordItems();
ConfigureContentRect();
var hasRecords = _recordDataList.Count > 0;
if (emptyText != null)
{
emptyText.gameObject.SetActive(!hasRecords);
@@ -85,12 +166,24 @@ namespace AibisDream.UI.Terminal
foreach (var recordData in _recordDataList)
{
var item = Instantiate(recordItemPrefab, recordRoot);
SyncUiLayer(item.gameObject, recordRoot.gameObject.layer);
item.Init(recordData);
if (item.transform is RectTransform itemRect)
{
ConfigureRecordItemRect(itemRect);
}
}
if (recordRoot is RectTransform contentRect)
{
LayoutRebuilder.ForceRebuildLayoutImmediate(contentRect);
}
if (scrollRect != null)
{
scrollRect.verticalNormalizedPosition = 0f;
Canvas.ForceUpdateCanvases();
scrollRect.verticalNormalizedPosition = 1f;
}
}
@@ -104,7 +197,7 @@ namespace AibisDream.UI.Terminal
private void ReturnGame()
{
_owner.Back();
_owner?.Back();
}
}
}
@@ -1,8 +1,12 @@
using UnityEngine;
using UnityEngine.UI;
using System;
using AibisDream;
using UnityEngine;
namespace AibisDream.UI.Terminal
namespace AibisDream.UI
{
/// <summary>
/// 终端 Setting Page(遗留类名 Panel)。可挂在主菜单或局内 Panel 壳下。
/// </summary>
public class TerminalSettingPanel : MonoBehaviour
{
[SerializeField] private TerminalPanelAnimator panelAnimator;
@@ -10,12 +14,15 @@ namespace AibisDream.UI.Terminal
[SerializeField] private SelectItem textSpeedOption;
[SerializeField] private SelectItem windowModeOption;
[SerializeField] private SelectItem languageOption;
[SerializeField] private Button continueButton;
[SerializeField] private Button backToMainButton;
[SerializeField] private Button quitButton;
[SerializeField] private TerminalButton continueButton;
[SerializeField] private TerminalButton backToMainButton;
[SerializeField] private TerminalButton quitButton;
public bool IsOpen => gameObject.activeSelf;
private TerminalPanel _owner;
private ITerminalPanelHost _owner;
private RectTransform _continueButtonRect;
private Vector2 _continueButtonDefaultPosition;
private bool _hasContinueButtonDefaultPosition;
private static readonly UIFormOption[] WindowModeOptions =
{
@@ -37,59 +44,85 @@ namespace AibisDream.UI.Terminal
new UIFormOption { prop = "3.0", label = "setting_textSpeed_fast" },
};
public void SetOwner(TerminalPanel owner)
public void SetOwner(ITerminalPanelHost owner)
{
_owner = owner;
}
private void Awake()
{
continueButton?.onClick.AddListener(BackToGame);
backToMainButton?.onClick.AddListener(BackToMain);
quitButton?.onClick.AddListener(QuitApp);
BindButton(continueButton, BackToGame);
BindButton(backToMainButton, BackToMain);
BindButton(quitButton, QuitApp);
if (continueButton != null)
{
_continueButtonRect = continueButton.transform as RectTransform;
if (_continueButtonRect != null)
{
_continueButtonDefaultPosition = _continueButtonRect.anchoredPosition;
_hasContinueButtonDefaultPosition = true;
}
}
}
private void OnDestroy()
{
continueButton?.onClick.RemoveListener(BackToGame);
backToMainButton?.onClick.RemoveListener(BackToMain);
quitButton?.onClick.RemoveListener(QuitApp);
UnbindButton(continueButton, BackToGame);
UnbindButton(backToMainButton, BackToMain);
UnbindButton(quitButton, QuitApp);
}
private static void BindButton(TerminalButton terminalButton, UnityEngine.Events.UnityAction action)
{
terminalButton?.Button?.onClick.AddListener(action);
}
private static void UnbindButton(TerminalButton terminalButton, UnityEngine.Events.UnityAction action)
{
terminalButton?.Button?.onClick.RemoveListener(action);
}
public void Show()
{
_owner?.SetHeader("<设置>", "MUSIC 50/100 - AUDIO INPUT 001");
InitControls();
RefreshControls();
if (panelAnimator != null)
{
panelAnimator.Show();
panelAnimator.Show(prepare: PrepareContent);
}
else
{
PrepareContent();
gameObject.SetActive(true);
}
}
public void Hide()
public void Hide(Action onComplete = null)
{
if (panelAnimator != null)
{
panelAnimator.Hide();
panelAnimator.Hide(onComplete);
}
else
{
gameObject.SetActive(false);
onComplete?.Invoke();
}
}
private void PrepareContent()
{
_owner?.SetHeader("terminal_header_setting", "REPAIR_STATION_07 - AUTH_VERIFIED");
InitControls();
RefreshControls();
RefreshFooterButtons();
}
private void InitControls()
{
volumeSlider?.Init("Volume", "音量.....", 0f, 1f, OnSettingChanged);
textSpeedOption?.Init("TextSpeed", "文字速度", TextSpeedOptions, OnSettingChanged);
windowModeOption?.Init("WindowMode", "屏幕模式", WindowModeOptions, OnSettingChanged);
languageOption?.Init("Language", "语言.....", LanguageOptions, OnSettingChanged);
volumeSlider?.Init("Volume", "setting_volume", 0f, 1f, OnSettingChanged);
textSpeedOption?.Init("TextSpeed", "setting_textSpeed", TextSpeedOptions, OnSettingChanged);
windowModeOption?.Init("WindowMode", "setting_window", WindowModeOptions, OnSettingChanged);
languageOption?.Init("Language", "setting_language", LanguageOptions, OnSettingChanged);
}
private void RefreshControls()
@@ -121,9 +154,37 @@ namespace AibisDream.UI.Terminal
SettingLoader.Instance.Write(propName, value);
}
private void RefreshFooterButtons()
{
var inGame = _owner != null && _owner.IsInGameContext;
continueButton?.gameObject.SetActive(true);
backToMainButton?.gameObject.SetActive(inGame);
quitButton?.gameObject.SetActive(inGame);
ApplyContinueButtonLayout(inGame);
}
private void ApplyContinueButtonLayout(bool inGame)
{
if (!_hasContinueButtonDefaultPosition || _continueButtonRect == null)
{
return;
}
_continueButtonRect.anchoredPosition = inGame
? _continueButtonDefaultPosition
: new Vector2(0f, _continueButtonDefaultPosition.y);
}
private void BackToGame()
{
if (_owner.Source == TerminalOpenSource.InGame)
if (_owner == null)
{
return;
}
if (_owner.IsInGameContext)
{
_owner.Close();
}
@@ -135,7 +196,7 @@ namespace AibisDream.UI.Terminal
private void BackToMain()
{
_owner.CloseToMainMenu();
_owner?.CloseToMainMenu();
}
private static void QuitApp()
@@ -4,7 +4,7 @@ using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace AibisDream.UI.Terminal
namespace AibisDream.UI
{
public class TerminalSliderView : MonoBehaviour
{
@@ -1,7 +1,7 @@
using System;
using UnityEngine;
namespace AibisDream.UI.Terminal
namespace AibisDream.UI
{
public class TerminalStartPanel : MonoBehaviour
{
@@ -23,6 +23,7 @@ namespace AibisDream.UI.Terminal
private void Awake()
{
InitMenuItems();
RegisterItem(newGameItem, StartNewGame);
RegisterItem(chapterItem, OpenChapter);
RegisterItem(settingItem, OpenSetting);
@@ -39,29 +40,38 @@ namespace AibisDream.UI.Terminal
public void Show()
{
_owner?.SetHeader("<选择章节>", "CHAPER xx/xxx - LOADING - DT");
if (panelAnimator != null)
{
panelAnimator.Show();
panelAnimator.Show(prepare: () => _owner?.SetHeader("termnial_header_start", "REPAIR_STATION_07 - AUTH_VERIFIED"));
}
else
{
_owner?.SetHeader("termnial_header_start", "REPAIR_STATION_07 - AUTH_VERIFIED");
gameObject.SetActive(true);
}
}
public void Hide()
public void Hide(Action onComplete = null)
{
if (panelAnimator != null)
{
panelAnimator.Hide();
panelAnimator.Hide(onComplete);
}
else
{
gameObject.SetActive(false);
onComplete?.Invoke();
}
}
private void InitMenuItems()
{
newGameItem?.SetContent("start_newGame", "SESSION CREATE", "STATUS: ACTIVE");
chapterItem?.SetContent("start_selcetLevel", "HISTORY QUERY", "RANGE: 04/533");
settingItem?.SetContent("start_setting", "USER CONFIG", "AUTH: LOCAL");
quitItem?.SetContent("start_quit", "SESSION CLOSE", "LOG: SAVED");
}
private void RegisterItem(TerminalMenuItemView item, Action action)
{
if (item == null)
+59 -20
View File
@@ -1,9 +1,8 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using AibisDream.Framework;
using AibisDream.UI;
using AibisDream.UI.Terminal;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
@@ -25,6 +24,7 @@ namespace AibisDream
private GraphicRaycaster _raycaster;
private TerminalPanel _terminalPanel;
private InGameTerminalPanel _inGameTerminalPanel;
#endregion
@@ -37,6 +37,11 @@ namespace AibisDream
{
if (Input.GetKeyDown(KeyCode.Escape))
{
if (_inGameTerminalPanel != null && _inGameTerminalPanel.HandleEscape())
{
return;
}
if (_terminalPanel != null && _terminalPanel.HandleEscape())
{
return;
@@ -78,6 +83,7 @@ namespace AibisDream
}
RegisterTerminalPanel();
RegisterInGameTerminalPanel();
EnumEventSystem.Global.Register(GameLoopEnum.AppStart, OnAppStart);
EnumEventSystem.Global.Register(GameLoopEnum.GameStart, OnGameStart);
@@ -101,7 +107,7 @@ namespace AibisDream
return;
}
_terminalPanel.Initialize(Canvas);
_terminalPanel.Initialize();
if (_terminalPanel.HasRequiredPages())
{
_panelPool[typeof(TerminalPanel)] = _terminalPanel;
@@ -112,6 +118,35 @@ namespace AibisDream
}
}
private void RegisterInGameTerminalPanel()
{
if (_panelPool.TryGetValue(typeof(InGameTerminalPanel), out var registeredPanel))
{
_inGameTerminalPanel = registeredPanel as InGameTerminalPanel;
}
else
{
_inGameTerminalPanel = Canvas.GetComponentInChildren<InGameTerminalPanel>(true);
}
if (_inGameTerminalPanel == null)
{
Debug.LogError("[UIManager] InGameTerminalPanel not found under UI Canvas. Add the in-game terminal shell and assign its Setting/Record prefab pages in the scene.");
return;
}
_inGameTerminalPanel.Initialize();
var hasPages = _inGameTerminalPanel.HasRequiredPages();
if (hasPages)
{
_panelPool[typeof(InGameTerminalPanel)] = _inGameTerminalPanel;
}
else
{
Debug.LogError("[UIManager] InGameTerminalPanel is missing required pages. Assign TerminalSettingPanel and TerminalRecordPanel on the in-game shell.");
}
}
private void OnAppStart()
{
// 游戏打开
@@ -121,6 +156,7 @@ namespace AibisDream
HidePanel<MainPanel>();
HideTerminalSetting();
HideTerminalRecord();
_inGameTerminalPanel?.Close();
HidePanel<EndPanel>();
}
@@ -128,6 +164,7 @@ namespace AibisDream
{
// 开始游戏
HideTerminal();
_inGameTerminalPanel?.Close();
ShowPanel<MainPanel>();
}
@@ -213,7 +250,7 @@ namespace AibisDream
public void ShowTerminalStart()
{
GetPanel<TerminalPanel>()?.Open(TerminalPage.Start, TerminalOpenSource.MainMenu);
GetPanel<TerminalPanel>()?.Open(TerminalPage.Start);
}
public void HideTerminal()
@@ -223,25 +260,26 @@ namespace AibisDream
public void ShowTerminalSetting()
{
var source = GameManager.Instance != null && GameManager.Instance.state.isInGame
? TerminalOpenSource.InGame
: TerminalOpenSource.MainMenu;
GetPanel<TerminalPanel>()?.Open(TerminalPage.Setting, source);
if (GameManager.Instance != null && GameManager.Instance.state.isInGame)
{
GetPanel<InGameTerminalPanel>()?.Open(InGameTerminalPage.Setting);
return;
}
GetPanel<TerminalPanel>()?.Open(TerminalPage.Setting);
}
public void HideTerminalSetting()
{
var terminal = GetPanel<TerminalPanel>();
if (terminal == null || !terminal.IsOpen || terminal.CurrentPage != TerminalPage.Setting)
var inGame = GetPanel<InGameTerminalPanel>();
if (inGame != null && inGame.IsOpen && inGame.CurrentPage == InGameTerminalPage.Setting)
{
inGame.Close();
return;
}
if (terminal.Source == TerminalOpenSource.InGame)
{
terminal.Close();
}
else
var terminal = GetPanel<TerminalPanel>();
if (terminal != null && terminal.IsOpen && terminal.CurrentPage == TerminalPage.Setting)
{
terminal.Back();
}
@@ -249,7 +287,7 @@ namespace AibisDream
public void ShowTerminalChapter()
{
GetPanel<TerminalPanel>()?.Open(TerminalPage.Chapter, TerminalOpenSource.MainMenu);
GetPanel<TerminalPanel>()?.Open(TerminalPage.Chapter);
}
public void HideTerminalChapter()
@@ -263,15 +301,16 @@ namespace AibisDream
public void ShowTerminalRecord()
{
GetPanel<TerminalPanel>()?.Open(TerminalPage.Record, TerminalOpenSource.InGame);
var panel = GetPanel<InGameTerminalPanel>();
panel?.Open(InGameTerminalPage.Record);
}
public void HideTerminalRecord()
{
var terminal = GetPanel<TerminalPanel>();
if (terminal != null && terminal.IsOpen && terminal.CurrentPage == TerminalPage.Record)
var inGame = GetPanel<InGameTerminalPanel>();
if (inGame != null && inGame.IsOpen && inGame.CurrentPage == InGameTerminalPage.Record)
{
terminal.Back();
inGame.Close();
}
}