一、前言:
在前兩天(DAY 12 - 13),我們實裝了開局自訂暱稱與黑屏旁白打字機效果。今天我們要邁入遊戲重要的玩法——「辦公桌互動與決策介面」!我們將實裝兩支關鍵腳本:
1. PhoneUIController.cs: 處理電話 UI 面板的開啟、置頂層級管理與 Escape 鍵安全關閉。
2. PaperManager.cs: 處理陳情表單(Consent Form)的彈出、桌面互動按鈕遮擋,以及 ⭕ / ❌ 簽核決策對接 NewPlayerStatsManager 的數值計算與分歧轉場!
二、 辦公桌 UI 互動與決策流程需求
參考已上市2D遊戲《Papers, Please》的介面,了解到系統需要具備極佳的 UI 互動與狀態防護:
1. 電話視窗層級與彈性關閉 (PhoneUIController):
三、核心程式實作:PhoneUIController.cs 與 PaperManager.cs
1. 電話 UI 面板與層級控制 (PhoneUIController.cs)
PhoneUIController 負責管理電話視窗的顯隱與 UI 層級排序,並相容 Unity New/Old Input System 的 Escape 鍵快捷關閉:
using UnityEngine;
using UnityEngine.UI;
public class PhoneUIController : MonoBehaviour {
[Header("UI 元件")]
public GameObject phonePanel; // 指向 Phone Panel
public Button phoneButton; // 點擊開啟 Phone 的按鈕
public Button closeButton; // Phone Panel 裡的關閉按鈕
void Start() {
phonePanel.SetActive(false); // 起始隱藏
phoneButton.onClick.AddListener(OpenPanel);
if (closeButton != null)
closeButton.onClick.AddListener(ClosePanel);
}
void Update() {
// 支援按 Escape 鍵關閉最上層視窗
#if ENABLE_INPUT_SYSTEM
if (phonePanel.activeSelf && IsTopPanel() &&
UnityEngine.InputSystem.Keyboard.current.escapeKey.wasPressedThisFrame) {
ClosePanel();
}
#else
if (phonePanel.activeSelf && IsTopPanel() &&
Input.GetKeyDown(KeyCode.Escape)) {
ClosePanel();
}
#endif
}
public void OpenPanel() {
phonePanel.SetActive(true);
phonePanel.transform.SetAsLastSibling(); // 將視窗推至最上層置頂
}
public void ClosePanel() {
phonePanel.SetActive(false);
// 關閉後,若同父物件下有其他開啟中的 Panel,維持最上層視窗置頂
Transform parent = phonePanel.transform.parent;
for (int i = parent.childCount - 1; i >= 0; i--) {
GameObject child = parent.GetChild(i).gameObject;
if (child.activeSelf && child != phonePanel) {
child.transform.SetAsLastSibling();
break;
}
}
}
private bool IsTopPanel() {
return phonePanel.transform.GetSiblingIndex() == phonePanel.transform.parent.childCount - 1;
}
}
2. 陳情表單決策與數值結算 (PaperManager.cs)
PaperManager 負責控制陳情文件的開啟/遮擋、蓋章決策,並呼叫 NewPlayerStatsManager 進行數據結算與場景跳轉:
using UnityEngine;
using UnityEngine.SceneManagement;
public class PaperManager : MonoBehaviour {
[Header("UI 連接")]
public GameObject documentPanel; // 文件決策面板 (Yes/No 按鈕的父物件)
public GameObject interactionButtonsContainer; // 桌上互動按鈕容器 (用於隱藏遮擋)
[Header("當前決策數值設定")]
public int moneyChangeYes = -100000;
public int reputationChangeYes = 2;
public int moneyChangeNo = 0;
public int reputationChangeNo = -3;
[Header("分歧場景設定")]
public string sceneIfYes = "SuccessScene";
public string sceneIfNo = "FailureScene";
[Header("決策處理引用")]
public NewPlayerStatsManager decisionHandler;
private NewPlayerStatsManager statsManagerCache;
void Start() {
if (documentPanel != null) documentPanel.SetActive(false);
FindStatsManager();
}
private void FindStatsManager() {
if (decisionHandler != null) {
statsManagerCache = decisionHandler;
return;
}
statsManagerCache = FindObjectOfType<NewPlayerStatsManager>();
if (statsManagerCache == null)
Debug.LogError("FATAL ERROR: NewPlayerStatsManager 在場景中找不到!");
}
public void ShowDocumentDecision() {
if (documentPanel == null) return;
documentPanel.SetActive(true);
// 開啟陳情表單時,隱藏桌上的其他互動按鈕防止誤觸
if (interactionButtonsContainer != null)
interactionButtonsContainer.SetActive(false);
}
public void HideDocumentDecision() {
if (documentPanel != null) {
documentPanel.SetActive(false);
if (interactionButtonsContainer != null)
interactionButtonsContainer.SetActive(true);
}
}
// 供 Document ⭕ 按鈕呼叫
public void OnYesDecisionClick() {
ProcessDecision(moneyChangeYes, reputationChangeYes, sceneIfYes, "Yes");
}
// 供 Document ❌ 按鈕呼叫
public void OnNoDecisionClick() {
ProcessDecision(moneyChangeNo, reputationChangeNo, sceneIfNo, "No");
}
/// 統一決策處理:數值更新 -> 關閉 UI -> 場景跳轉
private void ProcessDecision(int moneyChange, int reputationChange, string targetScene, string debugTag) {
if (statsManagerCache == null) FindStatsManager();
if (statsManagerCache != null) {
// 1. 執行數值變動
statsManagerCache.HandleDecision(moneyChange, reputationChange);
Debug.Log($"{debugTag} 決策執行:金錢 {moneyChange}, 聲譽 {reputationChange}");
// 2. 關閉 UI
HideDocumentDecision();
// 3. 切換至對應分歧場景
if (!string.IsNullOrEmpty(targetScene)) {
SceneManager.LoadScene(targetScene);
}
}
}
}
四、Unity Inspector 設定與實測驗收
在 Unity 介面中將元件拖曳連結:
1. 辦公桌 UI 視窗與發光點擊配置 (以DeskScene 9幕為例)
點擊桌面上的電話開啟 PhonePanel,PhoneUIController 會調用 SetAsLastSibling() 確保電話視窗置頂在最上層,且可隨時按下 Escape 關閉視窗。
圖一、辦公桌場警-電話黃框點擊

圖二、電話 Panel 彈出
2. 陳情表單放大面板與 ⭕ / ❌ 決策
點擊桌面文件後呼叫 ShowDocumentDecision(),桌面互動按鈕容器自動隱藏。點擊 ⭕ 扣除 $10,000 並增加 2 點聲譽,同時自動切換至 SuccessScene;點擊 ❌ 扣除 1 點聲譽並自動切換至 FailureScene!
圖三、接完電話後介面示意圖(此時可以選擇要重看對話或進行決策)

圖四、點擊桌面文件後介面示意圖

圖五、if點擊⭕介面示意圖(金錢從980,000變970,000;聲譽17變19)

圖六、if點擊❌介面示意圖(金錢j維持980,000;聲譽17變16)
五、開發實戰經驗:UI 防錯與數值架構設計
1. interactionButtonsContainer 視窗互斥機制: 在辦公桌這種多物件場景中,當玩家彈出大尺寸的決策表單(documentPanel)時,將背景的點擊按鈕容器暫時隱藏,是避免玩家連點誤觸非常關鍵的 UI/UX 防呆思維。
2. SetAsLastSibling() 多視窗疊加管理: 在 Unity Canvas 中,渲染順序完全取決於 Hierarchy 上的順序。使用 SetAsLastSibling() 能以最少的程式碼實現「後開啟的視窗永遠蓋在最上面」的效果,極度適合多視窗互動遊戲。
六、今日成果與小結
因為這已經開始進到辦公桌互動玩法與桌面物件管理的階段,今天我們依照遊戲階段開新分支,將這個部分歸類在feature/desk-interaction分支:
1.檢查修改狀態
git status
2.加入暫存區並提交 Commit
git add .
git commit -m "feat: 實作辦公桌 PhoneUIController 視窗置頂與 PaperManager 決策結算機制"
3.推送到遠端開發分支
git push origin feature/desk-interaction