236 lines
23 KiB
Markdown
236 lines
23 KiB
Markdown
# UI 框架开发指南
|
||
|
||
**1. 简介**
|
||
|
||
本文档旨在为使用本 UI 框架的开发人员提供指导,帮助大家快速、规范地创建和管理游戏中的 UI 页面及工作流。本框架的核心是通过一个中央控制器 (`MainController`) 来管理各个 UI 页面 (`UIPage` 的派生类) 的显示、隐藏、导航以及数据共享。
|
||
|
||
核心组件:
|
||
* **`main_controller.gd` (`MainController`)**: 全局 UI 工作流管理器。负责页面切换、维护工作流共享数据、处理工作流的开始与结束。
|
||
* **`ui_base.gd` (`UIPage`)**: 所有具体 UI 页面的基类。提供了基础的导航逻辑、与 `MainController` 的通信接口以及页面生命周期管理。
|
||
* **具体页面脚本 **: 继承自 `UIPage`,实现特定页面的内容展示和交互逻辑。
|
||
|
||
**2. 核心概念**
|
||
|
||
* **MainController (`main_controller.gd`)**
|
||
* **职责**:
|
||
* 管理当前激活的 UI 页面。
|
||
* 根据 `UIPage` 发出的请求进行页面切换,并处理页面间的过渡逻辑(如隐藏旧页面、显示新页面及其父页面)。
|
||
* 持有一个 `shared_workflow_data` 字典,用于在工作流中的不同页面间共享数据。此数据可在启动时从全局状态管理(例如 `GameState`)初始化。
|
||
* 定义工作流的起点 (`initial_page_path`)。
|
||
* 发出工作流结束信号 (`workflow_confirmed`, `workflow_cancelled`)。
|
||
* **导出变量**:
|
||
* `initial_page_path: NodePath`: 指向工作流初始页面的节点路径。
|
||
* `gamestate_data_key: String`: 用于从全局状态管理(如 `GameState`)中读取/初始化 `shared_workflow_data` 的键名。
|
||
* **启动与显示工作流**:
|
||
* `_ready()`: 控制器准备就绪时,会尝试从 `GameState` 使用 `gamestate_data_key` 加载共享数据 (如果 `gamestate_data_key` 已配置)。然后,它会遍历其直接子节点:如果是 `UIPage` 类型,则隐藏它们并连接必要的信号。接着,对于每个作为 `UIPage` 的直接子节点,它还会遍历该 `UIPage` 的直接子节点(即 `MainController` 的孙子节点):如果这些孙子节点也是 `UIPage` 类型,则同样会隐藏它们并连接信号。**注意**:`show_up()` 方法需要被外部调用以启动工作流,它不会在 `_ready()` 中自动调用。
|
||
* `show_up()`: 此方法用于显示和启动 UI 工作流。它首先会调用 `GameState.pause_game()` 来暂停游戏,然后重新从 `GameState` 使用 `gamestate_data_key` 获取最新的共享数据,并根据 `initial_page_path` 找到初始页面,然后调用 `start_new_workflow` 并将获取的数据传递给新工作流。如果初始页面路径无效或未找到对应节点,则控制器自身将保持不可见。
|
||
* `start_new_workflow(start_page: UIPage, initial_data: Dictionary = {})` 方法启动一个新的 UI 工作流。它会复制 `initial_data` (通常是从 `GameState` 或 `show_up()` 传递的) 到 `shared_workflow_data`,隐藏所有非起始的顶级页面及其子 `UIPage`(具体来说,它遍历 `MainController` 的直接子节点,如果子节点是 `UIPage` 且不是 `start_page`,则隐藏它;然后遍历该子节点的子节点,如果是 `UIPage`,也隐藏它),然后调用 `_switch_to_page` 切换到指定的 `start_page` 并使控制器可见。
|
||
* **共享数据**:
|
||
* `get_shared_data(key: String, default = null)`: 从共享数据字典中获取值。
|
||
* `set_shared_data(key: String, value)`: 设置共享数据字典中的值。
|
||
* **页面切换逻辑 (`_switch_to_page`)**:
|
||
* **处理前一页面**:
|
||
* 如果存在前一个活动页面 (`previous_page`),控制器会决定是否隐藏它。默认行为是隐藏 (`should_hide_previous_page = true`)。控制器会按以下顺序检查 `previous_page` 的 `hide_on_next` 行为:
|
||
1. 尝试通过元数据 `previous_page.get_meta("hide_on_next")` 获取布尔值。
|
||
2. 如果元数据不存在,并且 `previous_page` 具有名为 "about_to_show" 的信号 (这是一个特定条件,可能用于特定页面类型),则会尝试直接访问 `previous_page.hide_on_next` 脚本变量(`UIPage` 基类将 `hide_on_next` 定义为一个导出变量,默认为 `true`)。如果此时 `hide_on_next` 属性未找到,会打印错误并默认隐藏。
|
||
3. 如果上述条件都不满足,则使用默认行为(隐藏)。
|
||
* 如果最终决定需要隐藏前一页面,则调用 `previous_page.hide_page()`。
|
||
* **父页面清理**: 如果前一页面 (`previous_page`) 是一个子页面(即其 `parent_page_node_path` 已配置),并且导航的目标页面 (`target_page`) 满足以下两个条件:1) `target_page` 不是 `previous_page` 的逻辑父页面 (`old_page_parent_node`),并且 2) `target_page` 的场景树父节点不是 `old_page_parent_node`(即 `target_page` 不是 `old_page_parent_node` 的直接子节点,意味着它不是 `previous_page` 在该逻辑父页面下的兄弟页面),则前一子页面的逻辑父页面 (`old_page_parent_node`) 将被隐藏,以清理不再相关的上下文。
|
||
* **激活新页面**: 将 `current_active_page` 更新为目标页面。调用 `current_active_page.show_page()`(此方法在 `UIPage` 中仅负责设置 `visible = true`),然后关键地,`MainController` 直接调用 `current_active_page._on_page_activated()` 来确保页面的激活逻辑被执行。
|
||
* **确保新子页面的父页面可见并已激活**: 如果新的当前活动页面 (`current_active_page`) 是一个子页面(配置了 `parent_page_node_path`),控制器会确保其在 `parent_page_node_path` 中指定的父 `UIPage` 节点 (`new_page_parent_node`) 也是可见且(如果需要)已激活的。
|
||
* 如果该父页面 (`new_page_parent_node`) 之前不可见,则会调用其 `show_page()` 方法(使其可见),然后调用其 `_on_page_activated()` 方法(激活它)。
|
||
* 如果父页面已经可见,则仍会调用其 `show_page()` 方法(尽管它可能仅确认可见性)。
|
||
* **信号处理器**:
|
||
* `_on_page_navigate_requested(target_page_node: Control)`: 当 `UIPage` 发出 `navigate_requested` 信号时调用。如果 `target_page_node` 是 `UIPage` 的实例,则负责执行到目标页面的切换(通过 `_switch_to_page`)。否则会打印错误。
|
||
* `_on_page_workflow_end_requested(is_confirm_and_trigger_action: bool)`: 当 `UIPage` 发出 `workflow_end_requested` 信号时调用。
|
||
* 如果 `is_confirm_and_trigger_action` 为 `true`,则发出 `workflow_confirmed` 信号并携带最终的 `shared_workflow_data`。
|
||
* 否则,发出 `workflow_cancelled` 信号。
|
||
* 之后,如果存在当前活动页面 (`current_active_page`),则调用其 `hide_page()`。如果当前活动页面配置了 `parent_page_node_path`,其对应的父页面也会被调用 `hide_page()`。
|
||
* `current_active_page` 被设为 `null`。
|
||
* 当前的 `shared_workflow_data` 会使用 `GameState.set_value(gamestate_data_key, shared_workflow_data)` 保存到全局状态。注意:`MainController` 内部的 `shared_workflow_data` 不会被清空。随后调用 `GameState.resume_game()` 来恢复游戏。
|
||
* 最后,`MainController` 自身设置为不可见。
|
||
|
||
* **UIPage (`ui_base.gd`)**
|
||
* **页面基类**: 所有具体的 UI 页面场景的根节点所附加的脚本都应继承自此类。
|
||
* **初始化 (`_ready`)**:
|
||
* `UIPage` 在其 `_ready` 方法中会尝试获取 `MainController` 的引用。它会检查其 `owner` 节点是否为 `MainController`。这是通过判断 `owner` 是否存在并且是否拥有一个名为 `get_shared_data` 的方法来实现的。如果条件满足,则将 `owner` 赋值给 `main_controller` 变量。
|
||
* **输入处理 (`_input`)**:
|
||
* 如果页面可见,它会监听鼠标右键点击事件 (`MOUSE_BUTTON_RIGHT`)。当右键被按下时,会尝试执行 `go_back()` 操作,并调用 `get_viewport().set_input_as_handled()` 来消费该输入事件。
|
||
* **导航功能**:
|
||
* `go_next()`: 尝试导航到 `next_ui_node_path` 配置的页面。若路径有效且目标节点是 `Control` 类型,则发出 `navigate_requested` 信号。若路径为空或目标节点无效(或不是 `Control`),则打印错误并发出 `workflow_end_requested` 信号并传递 `true` (表示确认结束)。
|
||
* `go_back()`: 优先尝试导航到 `back_ui_node_path` 配置的页面。如果 `back_ui_node_path` 为空,则尝试导航到 `parent_page_node_path` 配置的父页面。若选中的路径有效且目标节点是 `Control` 类型,则发出 `navigate_requested` 信号。若两个路径均为空或目标节点无效(或不是 `Control`),则打印错误并发出 `workflow_end_requested` 信号并传递 `false` (表示取消结束)。
|
||
* `go_to_child_page(child_page_node: Control)`: 导航到指定的子页面。此方法会进行检查:
|
||
* 确保 `child_page_node` 不为 null。
|
||
* 确保 `child_page_node` 不是当前页面的祖先节点 (使用 `_is_ancestor_of` 辅助函数判断,以防止循环导航到父级)。
|
||
* 确保 `child_page_node` 是一个 `Control` 节点。
|
||
如果检查通过,则发出 `navigate_requested` 信号。否则打印错误。
|
||
* **Inspector 配置项**:
|
||
* `next_ui_node_path: NodePath`: “下一个”页面节点的路径。
|
||
* `back_ui_node_path: NodePath`: “上一个”页面节点的路径。
|
||
* `parent_page_node_path: NodePath`: (主要用于子页面)父页面节点的路径,当 `back_ui_node_path` 为空时,子页面会尝试通过 `go_back()` 返回此父页面。
|
||
* `hide_on_next: bool`: (默认值: `true`) 如果此属性为 `true`,当通过 `go_next()` 方法成功导航到 `next_ui_node_path` 所配置的页面时,`MainController` 在处理前一页面(即当前页面)时,会倾向于隐藏当前页面(具体逻辑见 `MainController._switch_to_page` 中对 `hide_on_next` 的处理)。如果设置为 `false`,当前页面在导航后有更大可能性保持可见。
|
||
* **生命周期方法 (可覆写)**:
|
||
* `show_page()`: 公开方法,用于显示页面。它**仅**负责将节点的 `visible` 属性设为 `true` (如果之前是 `false`)。**此方法本身不调用 `_on_page_activated()`**。
|
||
* `hide_page()`: 公开方法,用于隐藏页面。**仅当页面之前是可见状态时**,此方法会先调用 `_on_page_deactivated()`,然后再将节点的 `visible` 属性设为 `false`。
|
||
* `_on_page_activated()`: 当页面被 `MainController` 激活时调用(通常在 `_switch_to_page` 期间)。用于加载数据、更新UI、启动动画等。基类实现会打印一条日志,如 `UIPage activated: [PageName]`。
|
||
* `_on_page_deactivated()`: 当页面通过 `hide_page()` **即将变为不可见** (即其 `visible` 属性从 `true` 变为 `false`) 之前调用。用于保存临时状态、清理资源或停止动画等。如果页面已不可见,再次调用 `hide_page()` 不会重复触发此方法。基类实现会打印一条日志,如 `UIPage deactivated: [PageName]`。
|
||
* **与 MainController 通信**:
|
||
* 信号 `navigate_requested(target_page_node: Control)`: 请求 `MainController` 切换到目标页面。
|
||
* 信号 `workflow_end_requested(is_confirm_and_trigger_action: bool)`: 请求 `MainController` 结束当前工作流。
|
||
* 方法 `get_main_data(key: String, default = null)` 和 `set_main_data(key: String, value)`: 分别用于从 `main_controller` 获取和设置共享数据。如果 `main_controller` 未被正确引用,会发出警告并返回 `default` 值 (对于 `get_main_data`) 或不设置数据 (对于 `set_main_data`)。
|
||
|
||
**3. 创建新的 UI 页面**
|
||
|
||
以下步骤详细说明了如何创建一个新的 UI 页面并将其集成到框架中。
|
||
|
||
**3.1. 创建场景文件 (.tscn)**
|
||
|
||
1. 在 Godot 编辑器的文件系统中,右键点击目标文件夹,选择“新建” -> “场景”。
|
||
2. **根节点类型**: 选择 `Control` 作为根节点类型。你也可以根据需要选择 `PanelContainer` 等 `Control` 的派生类。
|
||
3. **命名规范**: 场景文件建议使用清晰的命名,例如 `staff_management_screen.tscn` 或 `project_details_dialog.tscn`。
|
||
4. 保存场景。
|
||
|
||
**3.2. 场景节点结构与命名规范**
|
||
|
||
* **页面根节点 (即场景的根 Control 节点)**:
|
||
* 此节点将附加一个继承自 `UIPage` 的特定页面脚本 (见 3.3)。
|
||
* 在 `Main.tscn` 场景中(或包含 `MainController` 的场景),此页面场景的实例将作为 `MainController` 节点的直接子节点(如果是顶级页面)或另一个 `UIPage` 实例的子节点(如果是子页面)。
|
||
|
||
* **内部 UI 控件**:
|
||
* 在页面根节点下,根据需求添加各种 Godot UI 控件,如 `Label`, `Button`, `LineEdit`, `TextureRect`, `ProgressBar`, `ItemList`, `HBoxContainer`, `VBoxContainer`, `GridContainer` 等。
|
||
* **命名规范**:
|
||
* 为重要的、需要在脚本中引用的控件赋予清晰、一致的名称。
|
||
* 建议使用驼峰式命名或下划线命名,并能体现控件类型和功能,例如:
|
||
* `PlayerNameLabel`
|
||
* `ConfirmButton` 或 `confirm_button`
|
||
* `ProjectListVBoxContainer`
|
||
* `CancelChangesButton`
|
||
* **布局**: 强烈建议使用 Godot 的容器节点 (`HBoxContainer`, `VBoxContainer`, `GridContainer`, `MarginContainer`, `PanelContainer`, `ScrollContainer` 等) 来组织和布局内部 UI 控件,以实现响应式和易于维护的界面。
|
||
|
||
**3.3. 创建页面脚本 (.gd)**
|
||
|
||
1. 在 Godot 编辑器的文件系统中,右键点击目标文件夹,选择“新建” -> “脚本”。
|
||
2. **命名规范**: 脚本名称应与其管理的页面场景相对应,例如 `staff_management_page.gd`。
|
||
3. **继承 `UIPage`**:
|
||
```gdscript
|
||
# staff_management_page.gd
|
||
class_name StaffManagementPage # 使用 class_name 方便类型提示
|
||
extends UIPage
|
||
```
|
||
4. **`@onready` 变量获取控件引用**:
|
||
在脚本顶部,使用 `@onready` 关键字获取场景中需要交互的 UI 控件的引用。
|
||
```gdscript
|
||
@onready var employee_list: ItemList = $ScrollContainer/VBoxContainer/EmployeeList
|
||
@onready var hire_button: Button = $ControlsHBox/HireButton
|
||
@onready var details_panel: PanelContainer = $DetailsPanel/ContentPanel
|
||
@onready var employee_name_label: Label = $DetailsPanel/ContentPanel/NameLabel
|
||
```
|
||
5. **实现 `_ready()` 方法**:
|
||
* **必须调用 `super._ready()`**: 确保基类 `UIPage` 的 `_ready()` 逻辑(如查找 `main_controller`)被执行。
|
||
* **连接页面内部控件的信号**: 将按钮的 `pressed` 信号、`LineEdit` 的 `text_changed` 信号等连接到此脚本中的处理函数。
|
||
```gdscript
|
||
func _ready():
|
||
super._ready() # !! 非常重要 !!确保 main_controller 被初始化
|
||
|
||
if hire_button: # 良好的习惯是检查节点是否存在
|
||
hire_button.pressed.connect(_on_hire_button_pressed)
|
||
if employee_list:
|
||
employee_list.item_selected.connect(_on_employee_list_item_selected)
|
||
# ... 其他信号连接
|
||
```
|
||
6. **覆写 `_on_page_activated()` 方法**:
|
||
此方法在页面被 `MainController` 激活时调用。
|
||
* **建议调用 `super._on_page_activated()`**: 基类实现会打印日志 (`UIPage activated: [PageName]`), 方便调试。
|
||
* **加载数据**: 使用 `get_main_data("data_key", default_value)` 从 `MainController` 的共享数据中获取当前工作流所需的数据。
|
||
* **更新 UI**: 根据获取的数据,更新页面上的 `Label.text`, `LineEdit.text`, `ProgressBar.value`,填充列表,设置按钮的 `disabled` 状态等。
|
||
```gdscript
|
||
func _on_page_activated():
|
||
super._on_page_activated() # 基类打印日志
|
||
|
||
var current_staff = get_main_data("company_staff", [])
|
||
_populate_employee_list(current_staff)
|
||
|
||
var can_hire = get_main_data("can_afford_new_hire", true)
|
||
if hire_button:
|
||
hire_button.disabled = not can_hire
|
||
```
|
||
7. **覆写 `_on_page_deactivated()` 方法 (可选)**:
|
||
此方法在页面每次通过 `hide_page()` 即将变为非活动状态(隐藏)之前调用。
|
||
* **建议调用 `super._on_page_deactivated()`**: 基类实现会打印日志 (`UIPage deactivated: [PageName]`)。
|
||
* 用于执行页面隐藏前的清理工作,例如保存未提交的临时输入到 `shared_workflow_data`,或者重置页面状态以便下次激活时重新初始化。
|
||
```gdscript
|
||
func _on_page_deactivated():
|
||
super._on_page_deactivated() # 基类打印日志
|
||
# 如果有临时输入未保存,可以在这里处理
|
||
# if name_input_field.text != get_main_data("current_editing_name"):
|
||
# set_main_data("temp_unsaved_name", name_input_field.text)
|
||
```
|
||
8. **实现自定义方法**:
|
||
编写处理页面特定交互逻辑的函数(例如上面 `_ready` 中连接的 `_on_hire_button_pressed`)。
|
||
* 这些方法可能会读取页面输入、执行计算、调用 `set_main_data(key, value)` 来更新 `MainController` 中的共享数据。
|
||
```gdscript
|
||
func _on_hire_button_pressed():
|
||
print("Hire button pressed on " + name)
|
||
# 可能需要更新一些共享数据,表明要进入雇佣流程
|
||
set_main_data("entering_hire_mode", true)
|
||
# 然后导航到下一个页面(例如一个详细的雇佣信息填写页面)
|
||
go_next() # go_next() 是从 UIPage 继承的方法
|
||
|
||
func _populate_employee_list(staff_data: Array):
|
||
if employee_list:
|
||
employee_list.clear()
|
||
for employee_info in staff_data:
|
||
employee_list.add_item(employee_info.get("name", "Unknown"))
|
||
# 可以存储更复杂的数据
|
||
# employee_list.set_item_metadata(employee_list.get_item_count() - 1, employee_info)
|
||
```
|
||
9. **调用导航方法**:
|
||
在适当的时候(例如,用户点击确认按钮后),调用从 `UIPage` 继承的导航方法:
|
||
* `go_next()`: 前往流程中的下一个页面或确认结束工作流。
|
||
* `go_back()`: 返回上一个页面、父页面或取消结束工作流。
|
||
* `go_to_child_page(child_node)`: 打开当前页面的一个子页面。
|
||
|
||
10. **附加脚本**: 将创建好的页面脚本附加到对应 UI 场景的根节点上。
|
||
|
||
**3.4. 配置页面导航 (Inspector)**
|
||
|
||
1. 打开包含 `MainController` 和 UI 页面实例的场景(例如 `Main.tscn`)。
|
||
2. 确保你的新 UI 页面场景已经被实例化并添加为 `MainController` 节点的子节点(或相应父页面的子节点)。
|
||
3. 选中新页面实例。
|
||
4. 在 Godot 编辑器的 **Inspector** 面板中,找到并配置从 `UIPage` 继承来的导出变量:
|
||
* **Next Ui Node Path**: 拖拽流程中的下一个页面节点到此属性。如果这是流程的最后一步(确认并结束),则留空 (使其 `NodePath` 为空)。
|
||
* **Back Ui Node Path**: 拖拽流程中的上一个页面节点到此属性。如果这是流程的起点或返回即取消工作流,则留空。
|
||
* **Parent Page Node Path**: **仅当此页面是作为另一个页面的子页面时配置**。拖拽其直接的父 `UIPage` 节点到此属性。这用于子页面在没有特定 `Back Ui Node Path` 时,通过 `go_back()` 返回其父级。
|
||
* **Hide On Next**: (布尔值,默认为 `true`) 勾选此项(或设置为 `true`)表示当通过 `go_next()` 导航到下一个页面时,`MainController` 更倾向于隐藏此页面。取消勾选(或设置为 `false`)则此页面在导航后有更大可能性保持可见(具体行为取决于 `MainController` 中 `_switch_to_page` 的逻辑)。
|
||
|
||
**4. 将新页面集成到工作流**
|
||
|
||
* **4.1. 作为顶级页面 (例如 A, B, C)**:
|
||
1. 将新创建的 UI 页面场景实例化,并使其成为 `MainController` 节点的直接子节点。
|
||
2. `MainController` 的 `_ready()` 函数会自动查找并处理其场景树下两层内的 UI 页面(即 `MainController` 的直接子 `UIPage`,以及这些 `UIPage` 的直接子 `UIPage`)。它会隐藏这些页面并连接它们的信号。确保你的新页面是 `UIPage` 类型(即附加了继承自 `UIPage` 的脚本)。
|
||
3. 根据工作流逻辑,配置 `MainController` 的 `Initial Page Path` 指向此新页面(如果它是起始页),或者修改其他页面的 `Next Ui Node Path` 或 `Back Ui Node Path` 以包含此新页面。
|
||
|
||
* **4.2. 作为子页面 (例如 A_1 是 A 的子页面)**:
|
||
1. 将新创建的 UI 页面场景实例化,并使其成为其父 `UIPage` 节点的子节点。例如,`PageA1.tscn` 的实例是 `PageA` 节点的子节点。
|
||
2. 在父页面 (例如 `PageA.gd`) 的脚本中,你需要:
|
||
* 添加一个按钮或交互元素来触发打开此子页面。
|
||
* 在该按钮的响应函数中,获取子页面节点的引用,并调用 `go_to_child_page(get_node("Path/To/YourChildPageNode"))`。
|
||
3. 配置新子页面的 `Parent Page Node Path` (在 Inspector 中) 指向其父 `UIPage` 节点。
|
||
4. `MainController` 的 `_ready()` 中的信号连接和初始隐藏逻辑会处理 `MainController` 的直接子 `UIPage` 以及这些 `UIPage` 的直接子 `UIPage`。如果你的子页面嵌套更深(例如,一个 `UIPage` 的孙子节点也是 `UIPage`,即相对于 `MainController` 三层或更深),则需要调整 `main_controller.gd` 中的 `_ready()` 循环以遍历更深层级,或者手动为更深层级的 `UIPage` 调用 `hide_page()` 并使用 `_connect_page_signals()` 连接其信号到 `MainController` 的相应处理方法。
|
||
|
||
**5. 注意事项与最佳实践**
|
||
|
||
* **单一职责**: 保持 `UIPage` 基类脚本的通用性。特定于某个页面的复杂逻辑应该放在该页面的派生脚本中。
|
||
* **命名一致性**: 对场景、节点和脚本使用清晰、一致的命名约定。
|
||
* **响应式布局**: 优先使用 Godot 的容器节点进行布局,以适应不同的屏幕尺寸和分辨率。
|
||
* **数据流**: 所有跨页面的工作流数据都应通过 `MainController` 的 `shared_workflow_data` 进行管理。避免页面脚本之间直接相互引用以获取或修改状态。
|
||
* **`_ready()` vs `_on_page_activated()`**:
|
||
* `_ready()`: 用于一次性的初始化设置,如信号连接、节点引用获取、`main_controller` 的查找。它在节点进入场景树时调用一次。
|
||
* `_on_page_activated()`: 由 `MainController` 在页面成为当前活动页面时调用。用于执行每次页面激活时需要执行的逻辑,如从 `MainController` 拉取最新数据并刷新 UI 显示。
|
||
* **错误处理与健壮性**: 在获取节点引用 (`get_node_or_null`) 或处理路径时,考虑添加空检查,以避免运行时错误。`UIPage` 和 `MainController` 中的导航方法已经包含了一些基本的错误打印。
|
||
* **测试**: 彻底测试所有导航路径,包括前进、后退、进入/退出子页面以及各种工作流结束条件(确认或取消)。
|
||
* **处理多页面同时可见的情况**:
|
||
* 当使用 `hide_on_next = false` 使得多个页面同时显示时,需要特别注意UI布局,确保它们不会相互不当重叠或干扰用户操作。
|
||
* 考虑输入焦点管理。Godot的事件系统通常会将输入传递给最上层(渲染顺序)且可见的控件。如果多个可见页面都有可交互元素,请仔细规划和测试以确保行为符合预期。
|
||
* 请记住,`MainController` 中的 `current_active_page` 始终指向最新导航到的(即“最活动的”)页面。其他因 `hide_on_next = false` (或类似逻辑)而保持可见的页面虽然显示在屏幕上,但它们不是 `MainController` 当前主要管理的页面。**重要:当从一个设置了 `hide_on_next = false` 的页面导航离开时,如果 `MainController` 决定不隐藏该页面,那么该页面的 `hide_page()` 方法将不会被调用,因此其 `_on_page_deactivated()` 方法也不会被调用。**
|