Godot 通用教程:如何动态加载列表到指定组件

一、这篇教程解决什么问题

在游戏开发里,经常会遇到这样的需求:

  • 把一组数据动态显示到下拉框里
  • 把一组数据动态显示到列表组件里
  • 把配置项动态生成到按钮组里
  • 根据保存的配置,自动回填当前选中项
  • 点击保存时,把用户选择重新写回配置

这些需求看起来不同,但本质其实是同一件事:

把一份数据,动态映射到一个 UI 组件上

二、先理解核心思想

动态加载列表,不是“往组件里塞内容”这么简单,它通常分成 4 步:

1. 准备数据源

例如分辨率列表:

const RESOLUTION_LIST := [
"1280x720",
"1366x768",
"1600x900",
"1920x1080",
"2560x1440"
]

这就是数据源。

2. 找到目标组件

例如你的分辨率下拉框:

@onready var opt_resolution: OptionButton = $CenterContainer/PanelRoot/Root/VBox/ContentPanel/Pages/PageVideo/RowResolution/OptResolution

这就是目标组件。

3. 把数据源循环写入组件

例如:

func _setup_resolution_options() -> void:
opt_resolution.clear()
for item in RESOLUTION_LIST:
opt_resolution.add_item(item)

这一步叫:渲染列表。

4. 根据当前配置回填选中项

例如:

func _load_current_resolution() -> void:
var current_resolution: String = str(SettingsManager.get_value("display", "resolution", "1600x900"))
_select_resolution(current_resolution)

这一步叫:状态回显。

所以你要记住,动态加载列表通常不是一步,而是这 4 步:

数据源 → 目标组件 → 动态写入 → 回填当前值

三、最通用的应用场景有哪些

这套方法可以用于很多地方。

1. 下拉框

例如:

  • 分辨率
  • 显示模式
  • 语言
  • 难度
  • 音质档位

常用组件:OptionButton

2. 列表框

例如:

  • 存档列表
  • 任务列表
  • 卡牌列表
  • 装备列表

常用组件:ItemList

3. 动态按钮组

例如:

  • 技能分类按钮
  • 章节选择按钮
  • 商店页签按钮

常用组件:HBoxContainer / VBoxContainer + Button

4. 设置项面板

例如:

  • 音频设置列表
  • 图像设置列表
  • 键位设置列表

常用方式:数据驱动生成多行 UI

四、先从最简单的例子开始:动态加载分辨率到 OptionButton

这是最适合入门的例子。

第一步:准备数据源

const RESOLUTION_LIST := [
"1280x720",
"1366x768",
"1600x900",
"1920x1080",
"2560x1440"
]

这是一组字符串数组。 每一项就是一个分辨率。

第二步:拿到组件引用

@onready var opt_resolution: OptionButton = $CenterContainer/PanelRoot/Root/VBox/ContentPanel/Pages/PageVideo/RowResolution/OptResolution

这里的意思是:

  • 当节点准备完成后
  • 取到这个路径上的 OptionButton
  • 后面就能直接操作它

第三步:写一个初始化方法

func _setup_resolution_options() -> void:
opt_resolution.clear()
for item in RESOLUTION_LIST:
opt_resolution.add_item(item)

这里做了两件事:

clear():先清空旧内容。

这是一个很重要的习惯。 因为如果你重复打开界面、重复执行初始化,而不先清空,列表会越加越多,变成重复项。

add_item(item) 把数组里的每一项加入到下拉框。

第四步:在 _ready() 里调用

func _ready() -> void:
_setup_resolution_options()

这样场景一加载,分辨率列表就会被动态写进去。

五、为什么要“动态加载”,而不是在编辑器里手动填

很多初学者会问:

“我直接在编辑器里把分辨率一个一个写进去不行吗?”

可以,但不推荐,原因有三个。

1. 后续维护麻烦

假如你后面要增加一个 3840x2160,手动改 UI 很慢。

2. 不能复用

如果多个场景都要用同一组数据,手填会重复劳动。

3. 不方便和配置、逻辑联动

动态加载后,你可以很方便根据平台、系统、配置文件去生成不同列表。

所以在稍微正式一点的项目里,列表数据尽量代码驱动。

六、回填当前选中值:这是动态列表最关键的一步

很多人会做“显示列表”,但不会做“回填状态”。

比如:

  • 你保存过 1920x1080
  • 再打开设置界面
  • 下拉框应该自动选中 1920x1080

这就需要回填逻辑。

第一步:从配置里取当前值

func _load_current_resolution() -> void:
var current_resolution: String = str(SettingsManager.get_value("display", "resolution", "1600x900"))
_select_resolution(current_resolution)

这里做了两件事:

  • SettingsManager.get_value(...)

从设置管理器里取当前保存的分辨率。

  • str(...)

把返回值强制转成字符串,避免类型推断问题。

第二步:根据文本匹配下拉项

func _select_resolution(target_text: String) -> void:
for i in range(opt_resolution.item_count):
if opt_resolution.get_item_text(i) == target_text:
opt_resolution.select(i)
return
opt_resolution.select(0)

逻辑很简单:

  • 遍历下拉框里的所有选项
  • 找到和当前配置相同的那一项
  • 选中它
  • 如果没找到,就默认选第一个

这是一种非常通用的写法,不只适用于分辨率。

七、点击保存时,怎么把用户选择写回配置

动态列表不是只显示出来,还要能保存。

例如分辨率保存:

func _on_save_pressed() -> void:
var selected_resolution: String = opt_resolution.get_item_text(opt_resolution.selected)
SettingsManager.set_value("display", "resolution", selected_resolution)
SettingsManager.apply_all()
SettingsManager.save_settings()
close_popup()

这里分成 4 步:

1. 取用户当前选中的项

var selected_resolution: String = opt_resolution.get_item_text(opt_resolution.selected)

2. 写入配置数据

SettingsManager.set_value("display", "resolution", selected_resolution)

3. 立即应用到游戏

SettingsManager.apply_all()

4. 保存到本地

SettingsManager.save_settings()

这也是一套非常通用的流程:

从组件读取 → 写回数据 → 应用 → 持久化

八、把这套思路抽象成通用模板

如果你以后不是做分辨率,而是做语言、画质、显示模式,结构都是一样的。

你可以把它记成下面这套模板。

1. 定义数据源

const DATA_LIST := [
"选项A",
"选项B",
"选项C"
]

2. 获取组件

@onready var target_option: OptionButton = $YourPath/OptionButton

3. 初始化列表

func _setup_options() -> void:
target_option.clear()
for item in DATA_LIST:
target_option.add_item(item)

4. 回填当前值

func _load_current_value() -> void:
var current_value: String = str(SomeManager.get_value("section", "key", "默认值"))
_select_option(current_value)

5. 选中匹配项

func _select_option(target_text: String) -> void:
for i in range(target_option.item_count):
if target_option.get_item_text(i) == target_text:
target_option.select(i)
return
target_option.select(0)

6. 点击保存

func _on_save_pressed() -> void:
var selected_value: String = target_option.get_item_text(target_option.selected)
SomeManager.set_value("section", "key", selected_value)
SomeManager.save()

九、如果列表项不是字符串,而是“显示名 + 实际值”,怎么办

这是非常重要的一步。 实际开发里,很多时候你不能只存字符串,而要区分:

  • 给玩家看的文本
  • 真正保存的值

例如显示模式:

  • 给玩家看:窗口 / 无边框 / 全屏
  • 实际保存:windowed / borderless / fullscreen

这时就不要用纯字符串数组,而要用字典数组。

数据源写法

const DISPLAY_MODE_LIST := [
{"text": "窗口", "value": "windowed"},
{"text": "无边框", "value": "borderless"},
{"text": "全屏", "value": "fullscreen"}
]

渲染到下拉框

func _setup_display_mode_options() -> void:
opt_display_mode.clear()
for item in DISPLAY_MODE_LIST:
opt_display_mode.add_item(item["text"])

这里显示的是 text。

回填当前值

func _select_display_mode(mode_value: String) -> void:
for i in range(DISPLAY_MODE_LIST.size()):
if DISPLAY_MODE_LIST[i]["value"] == mode_value:
opt_display_mode.select(i)
return
opt_display_mode.select(0)

这里比较的是 value。

保存时取实际值

func _get_selected_display_mode_value() -> String:
var idx := opt_display_mode.selected
if idx < 0 or idx >= DISPLAY_MODE_LIST.size():
return "windowed"
return DISPLAY_MODE_LIST[idx]["value"]

这就是更通用、更真实项目里的写法。

十、通用设计原则:UI 不保存业务值,UI 只负责展示和选择

你以后做列表时,尽量遵守这个原则:

  • UI 组件负责显示
  • 数据源负责提供内容
  • 配置管理器负责保存
  • 应用逻辑负责生效

例如分辨率:

  • UI 组件 :OptionButton
  • 数据源 :RESOLUTION_LIST
  • 配置存储 :SettingsManager.data["display"]["resolution"]
  • 应用逻辑 :DisplayServer.window_set_size(...)

这样结构清晰,不容易乱。

十一、除了 OptionButton,还能怎么动态加载

虽然你现在是分辨率下拉框,但通用方法并不只限于 OptionButton。

1. 动态加载到 ItemList

例如存档列表:

func _setup_save_list(save_list: Array[String]) -> void:
item_list.clear()
for save_name in save_list:
item_list.add_item(save_name)

2. 动态生成按钮

例如章节选择:

func _setup_chapter_buttons(chapter_list: Array[String]) -> void:
for child in button_container.get_children():
child.queue_free()
for chapter_name in chapter_list:
var btn := Button.new()
btn.text = chapter_name
button_container.add_child(btn)

3. 动态生成复杂条目

例如卡牌列表、任务条目、装备列表,通常会实例化一个预制场景:

func _setup_card_list(card_data_list: Array) -> void:
for child in card_container.get_children():
child.queue_free()
for card_data in card_data_list:
var item = preload("res://scenes/ui/card_item.tscn").instantiate()
item.set_card_data(card_data)
card_container.add_child(item)

这其实和分辨率下拉框是一个思路:

遍历数据 → 创建/写入 UI → 显示

十二、最常见的 5 个错误

下面这些是初学者最容易踩的坑。

1. 忘了 clear()

结果每次打开界面,列表重复追加。

2. 列表加载了,但没有回填当前值

导致玩家明明保存过设置,打开界面却显示默认值。

3. 保存时保存了显示文本,而不是实际值

例如显示模式只保存了“窗口”,但真正应用逻辑需要的是 windowed。

4. 用 := 推断类型时,返回值类型不稳定

例如:

var current_value := SettingsManager.get_value(...)

如果返回 Variant,Godot 可能推断失败。 更稳的写法是:

var current_value: String = str(SettingsManager.get_value(...))

5. UI 组件路径写错

动态列表代码本身没问题,但引用的节点路径不对,导致根本没把内容写进去。

所以你每次做动态加载前,都先确认:

  • 节点真的存在
  • 路径真的正确
  • 方法真的被调用了

十三、以分辨率为例,一份完整可运行的最小代码

下面是一份最小可运行逻辑,专门演示“动态加载 + 回填 + 保存”。

extends Control
const RESOLUTION_LIST := [
"1280x720",
"1366x768",
"1600x900",
"1920x1080",
"2560x1440"
]
@onready var opt_resolution: OptionButton = $OptResolution
@onready var btn_save: Button = $BtnSave
func _ready() -> void:
_setup_resolution_options()
_load_current_resolution()
btn_save.pressed.connect(_on_save_pressed)
func _setup_resolution_options() -> void:
opt_resolution.clear()
for item in RESOLUTION_LIST:
opt_resolution.add_item(item)
func _load_current_resolution() -> void:
var current_resolution: String = str(SettingsManager.get_value("display", "resolution", "1600x900"))
_select_resolution(current_resolution)
func _select_resolution(target_text: String) -> void:
for i in range(opt_resolution.item_count):
if opt_resolution.get_item_text(i) == target_text:
opt_resolution.select(i)
return
opt_resolution.select(0)
func _on_save_pressed() -> void:
var selected_resolution: String = opt_resolution.get_item_text(opt_resolution.selected)
SettingsManager.set_value("display", "resolution", selected_resolution)
SettingsManager.apply_all()
SettingsManager.save_settings()

这就是一份最标准、最通用的动态下拉列表写法。

十四、这套方法你以后怎么复用

你以后要做这些时,都可以直接照这个思路套:

  • 语言列表
  • 显示模式列表
  • 难度选择列表
  • 音质列表
  • 存档列表
  • 章节列表
  • 卡牌筛选条件
  • 商店分类按钮

你只需要替换 3 个东西:

  • 数据源
  • 目标组件
  • 保存字段

其它结构几乎不变。

十五、最后总结

动态加载列表到指定组件,本质不是某个组件的技巧,而是一套通用 UI 数据绑定思路:

先准备数据源,再渲染到组件,再回填当前状态,最后把用户选择保存回数据层。

以分辨率为例,你需要记住的完整流程是:

  • 定义分辨率数组

  • 获取 OptionButton

  • clear() 后循环 add_item()

  • 打开界面时根据当前配置自动选中

  • 点击保存时读取当前选项

  • 写入 SettingsManager 并应用

只要你掌握了这套流程,以后任何“动态列表”都能做。