三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Godot引擎径向菜单开发指南:模块化设计与数据驱动实现

Godot引擎径向菜单开发指南:模块化设计与数据驱动实现

1. 项目概述与核心价值

如果你正在用Godot引擎开发游戏,尤其是那些需要快速、直观操作的游戏,比如RTS、模拟经营或者带有技能轮盘的ARPG,那么一个顺滑好用的径向菜单绝对是提升玩家体验的利器。这个项目,就是带你从零开始,在Godot里打造一个功能完整、性能优秀且易于维护的径向菜单系统。它绝不仅仅是几个按钮围成一个圈那么简单。

回想一下早期或者一些简单的实现方式:用一堆Control节点(比如TextureRect)手动摆位置,然后写一堆三角函数去计算鼠标位置和角度,再判断选中了哪个扇区。代码里充斥着sincosatan2,还要自己处理鼠标进入、离开、点击的状态,想加个动画更是头疼。正如一些开发者吐槽的,这种做法会让代码迅速“膨胀”且“难以维护”。我们这个指南的目的,就是彻底告别这种“硬拼”的原始阶段。

我们将采用一种更符合Godot设计哲学、更模块化、更数据驱动的方法。核心思路是:将菜单的视觉表现(布局、样式)与逻辑控制(输入、选择、回调)分离,并通过配置文件或资源来定义菜单结构。这样,美术或策划调整菜单项的数量、图标、文字,你不需要动一行代码;你想把这个菜单复用到项目的其他地方,也几乎是即拿即用。最终,你会得到一个这样的系统:它可能由一个主场景(RadialMenu.tscn)构成,内部通过脚本动态生成菜单项,响应输入事件,并触发你定义好的回调函数,整个过程清晰、高效。

2. 系统架构设计与核心思路拆解

2.1 为什么不用“硬拼”法?

在深入我们的方案之前,先明确一下“硬拼”法的主要问题,这能帮你更好地理解我们设计决策的初衷。

  1. 布局与计算耦合:菜单项的位置、角度计算直接写在生成或控制逻辑里。一旦需要改变菜单半径、起始角度或项数,就要深入代码修改计算公式,容易出错。
  2. 状态管理混乱:每个菜单项的高亮、选中、禁用状态,需要自己通过修改纹理、颜色或添加/移除子节点来控制,逻辑分散。
  3. 数据与表现强绑定:菜单项的内容(图标、文字、关联的动作)直接写在场景树里或脚本的硬编码中。要添加一个新技能,就得复制粘贴节点并修改一堆属性。
  4. 复用性差:做好了一个八方向的技能轮盘,想做一个四方向的工具选择轮盘?几乎要重写一遍。
  5. 动画实现困难:实现菜单弹出、项高亮、确认选择等动画,需要在代码里手动插值(interpolate)每个项的位置、旋转和缩放,代码量巨大。

2.2 我们的模块化架构

为了解决上述问题,我们采用“数据驱动”和“组件分离”的架构。整个系统可以划分为四个核心层:

  1. 数据层(Data Layer)

    • 角色:定义菜单“是什么”。它包含所有菜单项的静态信息。
    • 实现:我们创建一个自定义资源类型,比如RadialMenuData,它继承自Resource。这个资源里可以定义:
      • items: 一个数组,每个元素是一个字典或自定义的RadialMenuItemData资源,包含icon_texture(图标)、label_text(标签)、action_id(动作标识符)、enabled(是否启用)等字段。
      • radius: 菜单的半径。
      • start_angle: 起始角度(例如,从正上方开始是-90度)。
      • item_scene: 一个PackedScene引用,指向单个菜单项的表现层预设。这实现了表现与数据的解耦。
  2. 表现层(Presentation Layer)

    • 角色:定义菜单“看起来怎么样”。负责将数据层的抽象信息渲染到屏幕上。
    • 实现
      • 菜单项预设(RadialMenuItem.tscn:这是一个小的场景,可能包含一个TextureRect(图标)、一个Label(文字),以及用于高亮效果的ColorRect或另一个TextureRect。它有自己的脚本(RadialMenuItem.gd),提供set_data(data)set_highlighted(is_highlighted)set_selected(is_selected)等方法,用于根据状态更新视觉。
      • 菜单容器(RadialMenu.tscn:这是主场景。它包含一个中心点(Node2D)作为旋转和布局的参考,以及一个用于容纳所有生成的菜单项实例的容器节点(比如Node2DControl)。它的脚本(RadialMenu.gd)负责根据RadialMenuData动态实例化并排列RadialMenuItem
  3. 控制层(Control Layer)

    • 角色:定义菜单“如何工作”。处理输入事件,管理状态机,并协调表现层。
    • 实现:这部分逻辑主要写在RadialMenu.gd中。它包括:
      • 输入处理:监听鼠标移动或摇杆输入,计算输入向量相对于菜单中心的角度和距离。
      • 选择逻辑:根据输入角度,确定当前高亮的菜单项索引。这里会用到一些向量数学,但逻辑被封装得很好。
      • 状态管理:管理菜单的几种状态:HIDDEN(隐藏)、OPENING(打开中)、IDLE(打开待命)、HIGHLIGHTING(高亮某项)、CONFIRMING(确认选择)、CLOSING(关闭中)。状态机使动画和逻辑流转更清晰。
      • 事件派发:当用户确认选择(如松开鼠标按键)时,根据当前高亮项的action_id,触发一个信号(如item_selected(action_id))或调用一个回调函数。
  4. 动画层(Animation Layer)

    • 角色:让菜单的交互“感觉流畅”。我们利用Godot强大的动画系统,而不是手动插值。
    • 实现
      • RadialMenu场景中创建一个AnimationPlayer节点。
      • 设计动画:open(菜单项从中心缩放弹出)、close(菜单项缩回中心消失)、highlight_item_[N](高亮第N项,例如放大和变亮)、select_item(被选中项的反馈动画)。
      • 在控制层的状态转换中,播放对应的动画。例如,从HIDDEN进入OPENING状态时,播放open动画,并在动画播放完毕后切换到IDLE状态。

提示:这种架构的关键优势在于,当你需要修改菜单外观时,你只需调整RadialMenuItem.tscn或动画;修改菜单内容时,只需编辑RadialMenuData资源;修改交互逻辑时,则专注于RadialMenu.gd的控制部分。三者互不干扰。

2.3 核心数学原理:从输入到选择

无论架构如何优雅,径向菜单的核心逻辑都绕不开一点数学:如何将屏幕上的一个点(鼠标位置)映射到菜单的一个扇区?

  1. 获取相对向量:首先,获取输入点(鼠标全局位置)与菜单中心点(全局位置)的差值向量。input_vector = input_global_position - menu_global_position
  2. 计算角度:使用atan2函数计算该向量的角度(弧度制)。angle_rad = atan2(input_vector.y, input_vector.x)atan2返回的范围是(-PI, PI],对应(-180°, 180°],其中x轴正方向为0度。
  3. 角度归一化:为了便于处理,我们通常将角度转换到[0, 2PI)的范围。if angle_rad < 0: angle_rad += 2 * PI
  4. 调整起始角:根据菜单定义的start_angle(通常我们以弧度制在代码中处理),调整计算出的角度。adjusted_angle = angle_rad - start_angle。同样,确保adjusted_angle[0, 2PI)范围内。
  5. 计算扇区索引:如果菜单有n个项,那么每个项占据的弧度是sector_angle = 2 * PI / n。当前输入对应的项索引就是:index = floor(adjusted_angle / sector_angle)floor是向下取整,确保索引在[0, n-1]
  6. 距离判断:通常,只有当输入点与菜单中心的距离在有效半径范围内(比如,大于内径inner_radius且小于外径outer_radius),才认为是在操作菜单,否则可能视为无选择或取消操作。

这个计算过程会在_process_input事件中频繁执行,因此要确保高效。在我们的架构中,这部分逻辑被封装在控制层的一个函数(例如_update_highlight_from_input)里,清晰且独立。

3. 核心模块实现详解

3.1 创建数据资源(RadialMenuData)

这是我们的“单点真理”。在Godot中创建自定义资源非常简单。

  1. 创建脚本:新建一个GDScript文件,命名为radial_menu_data.gd
    # radial_menu_data.gd extends Resource class_name RadialMenuData @export var menu_name: String = "MyRadialMenu" # 菜单项数据数组,建议导出时使用自定义资源数组,但Godot 4对导出Resource数组支持更友好,这里先用字典数组示意 @export var items: Array[Dictionary] = [] # 或者,更优雅的方式是定义一个RadialMenuItemData资源类,然后导出Array[RadialMenuItemData] # @export var items: Array[RadialMenuItemData] = [] @export var radius: float = 150.0 @export_range(0, 360) var start_angle_degrees: float = -90.0 # 从正上方开始 @export var item_scene: PackedScene = null # 指向RadialMenuItem.tscn # 一个便捷的方法,用于在编辑器中快速添加测试项 func _init(): if items.is_empty(): items.append({"id": "item_0", "icon": null, "label": "Option 1"}) items.append({"id": "item_1", "icon": null, "label": "Option 2"})
  2. 创建与使用资源:在Godot编辑器的文件系统面板中右键,选择“新建资源...”,搜索并选择你刚创建的RadialMenuData。将其保存为.tres文件(例如skill_menu_data.tres)。然后你可以在Inspector面板中直观地编辑它:添加、删除菜单项,修改半径,关联图标纹理和项预设场景。

实操心得:使用@export注解将关键参数暴露到编辑器面板,是Godot提升开发效率的利器。这让策划和美术能直接在友好的界面里配置内容,无需程序员介入。对于items数组,虽然直接导出字典数组可以工作,但为了更好的类型检查和编辑器支持,强烈建议为菜单项也创建一个自定义Resource(如RadialMenuItemData)。这样在Inspector中每个项都有独立的折叠栏,编辑体验更好。

3.2 构建菜单项预设(RadialMenuItem.tscn)

这个场景代表菜单中的一个“格子”。

  1. 场景结构

    • RadialMenuItem(Node2D) - 根节点,方便整体旋转和定位。
      • Background(TextureRect) - 背景板,可能是一个扇形或圆角矩形纹理。
      • Icon(TextureRect) - 显示图标,居中。
      • Label(Label) - 显示文字描述,可以放在图标下方或扇区外侧。
      • Highlight(ColorRect 或 TextureRect) - 高亮覆盖层,初始状态为隐藏或透明。用于鼠标悬停时的视觉反馈。
  2. 附加脚本:为根节点附加脚本RadialMenuItem.gd

    # RadialMenuItem.gd extends Node2D @onready var icon_texture: TextureRect = $Icon @onready var label_node: Label = $Label @onready var highlight_node: ColorRect = $Highlight var item_data: Dictionary = {} func set_data(data: Dictionary) -> void: item_data = data if icon_texture and data.has("icon") and data["icon"] is Texture2D: icon_texture.texture = data["icon"] if label_node and data.has("label"): label_node.text = data["label"] # 根据data中的“enabled”字段,可以初始设置节点禁用样式 set_highlighted(false) func set_highlighted(is_highlighted: bool) -> void: if highlight_node: highlight_node.visible = is_highlighted # 可以添加更丰富的效果,如图标缩放、颜色变化 if is_highlighted: modulate = Color(1.2, 1.2, 1.2, 1.0) # 变亮 else: modulate = Color.WHITE func set_selected(is_selected: bool) -> void: # 被选中时的瞬时反馈,例如一个缩放动画或闪光 if is_selected: var tween = create_tween() tween.tween_property(self, "scale", Vector2(1.3, 1.3), 0.1) tween.tween_property(self, "scale", Vector2(1.0, 1.0), 0.1)

这个预设场景只关心自己如何被渲染和表现,不关心自己在菜单中的位置,也不处理输入。位置将由父容器(RadialMenu)统一计算和设置。

3.3 实现主菜单控制器(RadialMenu.gd)

这是系统的大脑,也是最复杂的部分。我们将分步实现。

第一步:基础属性和节点引用。

# RadialMenu.gd extends Node2D # 导出自定义数据资源 @export var menu_data: RadialMenuData # 状态枚举 enum MenuState { HIDDEN, OPENING, IDLE, HIGHLIGHTING, CONFIRMING, CLOSING } # 节点引用 @onready var items_container: Node2D = $ItemsContainer @onready var animation_player: AnimationPlayer = $AnimationPlayer var current_state: MenuState = MenuState.HIDDEN var current_highlighted_index: int = -1 # -1表示无高亮 var menu_items: Array[RadialMenuItem] = [] var is_active: bool = false # 是否接受输入 # 计算用的缓存变量 var _center_position: Vector2 var _item_count: int var _sector_angle: float var _start_angle_rad: float

第二步:初始化与菜单构建。_ready函数中,我们根据menu_data动态构建菜单。

func _ready() -> void: _center_position = global_position if not menu_data or not menu_data.item_scene: push_error("RadialMenu: menu_data or item_scene is not set!") return _item_count = menu_data.items.size() if _item_count == 0: return _sector_angle = TAU / _item_count # TAU = 2 * PI _start_angle_rad = deg_to_rad(menu_data.start_angle_degrees) # 清空容器并实例化所有菜单项 for child in items_container.get_children(): child.queue_free() menu_items.clear() for i in range(_item_count): var item_instance: RadialMenuItem = menu_data.item_scene.instantiate() as RadialMenuItem if item_instance: items_container.add_child(item_instance) menu_items.append(item_instance) # 设置项的数据 item_instance.set_data(menu_data.items[i]) # 初始位置设为中心,后续由动画弹出 item_instance.position = Vector2.ZERO item_instance.visible = false # 初始隐藏

第三步:状态机与输入处理。我们通过_process函数和状态机来驱动菜单。

func _process(delta: float) -> void: if not is_active: return match current_state: MenuState.IDLE, MenuState.HIGHLIGHTING: _handle_input() # 其他状态如OPENING, CLOSING通常由动画信号驱动切换 func _handle_input() -> void: # 获取鼠标相对于菜单中心的位置 var mouse_pos = get_global_mouse_position() var input_vector = mouse_pos - _center_position var distance = input_vector.length() # 定义一个有效操作半径(例如,菜单半径的0.8到1.2倍之间) var inner_deadzone = menu_data.radius * 0.3 var outer_limit = menu_data.radius * 1.5 if distance < inner_deadzone: # 鼠标在中心死区内,取消高亮 _set_highlighted_index(-1) return elif distance > outer_limit: # 鼠标超出外圈太远,可以视为取消操作(可选) # _set_highlighted_index(-1) # close_menu() return # 计算角度并找到高亮索引 var angle_rad = atan2(input_vector.y, input_vector.x) # 归一化到[0, TAU) if angle_rad < 0: angle_rad += TAU # 调整起始角 var adjusted_angle = angle_rad - _start_angle_rad if adjusted_angle < 0: adjusted_angle += TAU var index = int(floor(adjusted_angle / _sector_angle)) % _item_count _set_highlighted_index(index) func _set_highlighted_index(new_index: int) -> void: if new_index == current_highlighted_index: return # 取消旧项的高亮 if current_highlighted_index >= 0 and current_highlighted_index < menu_items.size(): menu_items[current_highlighted_index].set_highlighted(false) # 设置新项的高亮 current_highlighted_index = new_index if current_highlighted_index >= 0 and current_highlighted_index < menu_items.size(): menu_items[current_highlighted_index].set_highlighted(true) current_state = MenuState.HIGHLIGHTING # 可以在这里播放一个轻微的高亮动画,比如图标微颤 else: current_state = MenuState.IDLE

第四步:菜单的打开、关闭与选择确认。我们需要提供API供外部调用,并处理确认选择逻辑。

func open_menu(at_position: Vector2) -> void: if current_state != MenuState.HIDDEN: return global_position = at_position _center_position = at_position is_active = true current_state = MenuState.OPENING # 显示所有项(目前还在中心位置) for item in menu_items: item.visible = true # 播放弹出动画 animation_player.play("open") # 假设动画结尾有调用 _on_open_animation_finished 的信号 func close_menu() -> void: if current_state == MenuState.HIDDEN or current_state == MenuState.CLOSING: return is_active = false current_state = MenuState.CLOSING _set_highlighted_index(-1) animation_player.play("close") # 假设动画结尾有调用 _on_close_animation_finished 的信号 func _on_open_animation_finished() -> void: current_state = MenuState.IDLE func _on_close_animation_finished() -> void: for item in menu_items: item.visible = false current_state = MenuState.HIDDEN # 在_input函数中监听确认动作(如鼠标左键释放) func _input(event: InputEvent) -> void: if not is_active or current_state != MenuState.HIGHLIGHTING: return if event.is_action_released("ui_accept") or event.is_action_released("left_mouse_button"): # 假设"ui_accept"是游戏中的确认键 if current_highlighted_index >= 0: _confirm_selection(current_highlighted_index) func _confirm_selection(index: int) -> void: current_state = MenuState.CONFIRMING is_active = false # 确认后立即停止接受输入,防止重复触发 var selected_item = menu_items[index] selected_item.set_selected(true) # 发出信号,通知外部哪个项被选中了 emit_signal("item_selected", menu_data.items[index]["id"]) # 短暂延迟后关闭菜单,让选中动画有时间播放 var close_timer = get_tree().create_timer(0.3) close_timer.timeout.connect(close_menu)

第五步:定义信号。在脚本顶部添加信号声明,以便其他节点可以连接并响应菜单选择。

signal item_selected(item_id: String)

3.4 设计动画(AnimationPlayer)

RadialMenu场景中,为AnimationPlayer节点创建以下动画:

  1. open动画

    • 选中items_container下的所有RadialMenuItem实例(在动画编辑器中通过路径选择)。
    • 在第0帧,设置它们的position(0, 0)scale(0, 0)modulate.a(透明度)为0。
    • 在第20帧(约0.3秒),设置它们的position为各自的目标位置(这需要脚本计算,但动画可以驱动一个由脚本暴露的属性,更简单的方法是让脚本在播放动画前先设置好最终位置,动画只负责从中心移动到那里。或者,使用Call Method Track调用一个设置位置的方法)。
    • 更实用的方法是:open动画中只处理弹出效果(缩放和淡入),而项的位置在_readyopen_menu时由脚本通过计算直接设置好。这样动画逻辑更清晰。
    • 因此,open动画可以只是将所有子项从scale=0动画到scale=1,同时modulate.a从0到1。位置保持不变(已是计算好的位置)。
  2. close动画:与open相反,将scale动画到0modulate.a0

  3. highlight动画(可选):可以为每个菜单项或高亮层制作一个简单的脉冲或缩放动画,在_set_highlighted_index时触发。

注意事项:在Godot中,动态生成的子节点在动画编辑器中可能无法直接选中。一个常见的技巧是:在_ready中生成菜单项后,为AnimationPlayer添加一个call_method轨道,调用一个如_setup_animation_targets的方法,该方法将动态生成的菜单项节点路径注册到动画中。或者,更简单粗暴但有效的方法是:动画不直接控制每个项,而是控制items_container的某个属性,然后由脚本在_process中根据这个属性去更新每个项。例如,定义一个open_progress变量(0到1),动画通过Call Method修改它,然后在_process中根据open_progress为每个项计算当前的位置和缩放。这给了你最大的灵活性,但代码量稍多。对于初学者,建议先使用“先摆好位置,再播放弹出动画”的简化方案。

4. 集成使用与高级技巧

4.1 在游戏中使用径向菜单

假设你有一个玩家角色,按下某个键(如Tab键)时在鼠标位置打开技能轮盘。

  1. 实例化菜单:在你的游戏主场景或UI层中,预先加载RadialMenu场景。

    # 例如在游戏的UI控制器中 var radial_menu_scene = preload("res://ui/radial_menu/RadialMenu.tscn") var radial_menu_instance: RadialMenu func _ready(): radial_menu_instance = radial_menu_scene.instantiate() add_child(radial_menu_instance) radial_menu_instance.item_selected.connect(_on_radial_menu_item_selected) # 加载你的技能数据资源 var skill_data = preload("res://data/skill_menu_data.tres") radial_menu_instance.menu_data = skill_data radial_menu_instance.hide() # 初始隐藏
  2. 触发打开

    func _input(event): if event.is_action_pressed("open_radial_menu"): # 例如"ui_tab" var mouse_pos = get_global_mouse_position() radial_menu_instance.open_menu(mouse_pos)
  3. 处理选择结果

    func _on_radial_menu_item_selected(item_id: String): print("Selected item: ", item_id) # 根据item_id执行对应的游戏逻辑,如释放技能、使用道具 match item_id: "fireball": player.cast_fireball() "heal": player.cast_heal() _: pass

4.2 性能优化与注意事项

  1. 避免每帧计算所有项的位置:在_ready或菜单数据改变时,一次性计算好所有菜单项的目标位置并存储起来。动画或状态更新时直接使用这些预计算的位置。

    var _item_target_positions: Array[Vector2] = [] func _calculate_target_positions(): _item_target_positions.clear() for i in range(_item_count): var angle = _start_angle_rad + i * _sector_angle var pos = Vector2(cos(angle), sin(angle)) * menu_data.radius _item_target_positions.append(pos)

    open动画或直接设置时,将项的位置设为目标位置。

  2. 对象池(Object Pooling):如果你的游戏需要频繁打开/关闭不同类型的径向菜单(如不同的技能栏、武器轮盘),考虑使用对象池管理菜单实例,而不是每次都实例化和释放。

  3. 输入处理优化:在_handle_input中,get_global_mouse_position()和向量运算每帧都在调用。确保它们只在菜单激活时进行。对于主机或移动端,你需要适配手柄摇杆或触摸输入,原理相同(获取输入向量)。

  4. 处理UI遮挡:确保你的RadialMenu在UI层的最上层,或者正确处理z_indexmouse_filter,防止被其他UI元素挡住输入。

4.3 常见问题与排查技巧

  1. 菜单项位置不对或重叠在中心

    • 检查_calculate_target_positions中的角度计算是否正确。确认start_angle_degreesradius的值。
    • 检查items_container的节点路径是否正确,确保menu_items数组中的实例顺序与计算位置顺序一致。
    • 检查:动画是否覆盖了最终位置?确保动画播放前或播放后,项的位置被正确设置为目标位置。
  2. 鼠标高亮不准确或跳动

    • 检查:角度归一化和索引计算逻辑。打印出angle_radadjusted_angle和计算出的index,看是否符合预期。
    • 检查inner_deadzoneouter_limit的设置是否合理。死区太小可能导致在中心点附近轻微晃动就触发高亮切换。
    • 检查:菜单的global_position(中心点)获取是否准确。确保在open_menu时更新了_center_position
  3. 选择后信号未触发

    • 检查_input函数中的动作名称"ui_accept""left_mouse_button"是否与项目输入映射中定义的一致。
    • 检查_confirm_selection函数是否被调用。可以在函数开始处加print调试。
    • 检查:信号连接是否正确。确保接收信号的函数(如_on_radial_menu_item_selected)签名匹配(参数类型和数量)。
  4. 动画播放异常

    • 检查AnimationPlayer中的轨道路径是否指向正确的节点。动态生成的节点可能需要通过相对路径或%唯一名称来访问。
    • 检查:动画是否被意外中断。确保状态机转换逻辑严谨,不会在动画播放中强行切换到其他状态。
  5. 在复杂场景中菜单被遮挡

    • 解决:将RadialMenu场景放在一个专用的CanvasLayer上,并设置较高的layer属性,确保它渲染在最前面。
    • 解决:在RadialMenu_input函数中,在处理完事件后调用get_viewport().set_input_as_handled(),可以阻止输入事件继续向下传递。

5. 功能扩展与变体思路

一个基础系统建成后,你可以根据游戏需求进行丰富扩展:

  1. 嵌套子菜单:当选中某个主菜单项时,可以以其为圆心,弹出另一个RadialMenu作为子菜单。这需要修改数据结构和控制逻辑,让RadialMenuItemData可以包含一个子RadialMenuData引用。

  2. 图标与标签动态加载:对于大量菜单项,图标纹理可能占用大量内存。可以实现异步加载,当菜单打开时才开始加载图标,并使用占位符。

  3. 手柄与触摸支持:除了鼠标,完整支持游戏手柄右摇杆的向量输入。对于触摸设备,可以检测手指滑动轨迹来模拟向量输入。

  4. 视觉主题与样式化:将颜色、字体、纹理等视觉元素抽取到另一个ThemeStyleBox资源中,使菜单换肤变得容易。

  5. 声音反馈:在不同状态(打开、高亮、选择、关闭)添加对应的音效,提升操作质感。

  6. 数据绑定与动态更新:菜单项的状态(如技能冷却、道具数量)可以与游戏数据实时绑定。在RadialMenuItem.gd中增加update_from_game_data(data)方法,并在游戏数据变化时通知菜单更新。

实现一个健壮的径向菜单系统,关键在于理解其数据、表现、控制分离的核心思想,并充分利用Godot引擎的资源系统、节点场景和信号机制。从这个小系统出发,你能更深入地掌握Godot的游戏UI架构模式,并将其应用到更复杂的游戏界面开发中去。

← 返回列表