保存与读取
把场景状态的字典以 JSON 写入 user://,文件内容就显示在实时数值旁边。
上方的编辑器正在运行这个项目。改一行代码,它就会重新加载。
用到的 Godot 类
FileAccessJSONDirAccess
代码
scripts/main.gd
extends Node
const SAVE_PATH: String = 'user://save_and_load.json'
const PERKS: Array = ['dash', 'glide', 'grapple', 'shield']
const TICK: float = 0.3
var state: Dictionary = {}
# key -> the Label that shows it, plus 'bar' -> ProgressBar
var fields: Dictionary = {}
var json_view: Label
var status: Label
var elapsed: float = 0.0
func _ready() -> void:
var row: HBoxContainer = _root()
row.add_child(_state_panel())
row.add_child(_file_panel())
if _load():
status.text = 'a file was already on disk, this run started from it'
else:
state = {player = 'nova', level = 1, coins = 0, best_time = 12.5, perks = ['dash']}
status.text = 'no file yet, this run started from the defaults'
_save()
print('the state drifts and autosaves, the buttons load it back or wipe the file')
func _process(delta: float) -> void:
state.coins = int(state.coins) + int(delta * 240.0)
elapsed += delta
_refresh()
if elapsed < TICK:
return
elapsed = 0.0
state.level = int(state.level) % 9 + 1
state.best_time = snappedf(randf_range(8.0, 30.0), 0.01)
state.perks = PERKS.slice(0, int(state.level) % PERKS.size() + 1)
_save()
func _save() -> void:
var file: FileAccess = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
if file == null:
status.text = 'could not open the file for writing'
return
file.store_string(JSON.stringify(state, ' '))
file.close()
_refresh_file()
func _load() -> bool:
if not FileAccess.file_exists(SAVE_PATH):
return false
var parsed: Variant = JSON.parse_string(_read())
if not parsed is Dictionary:
return false
state = parsed
return true
func _read() -> String:
if not FileAccess.file_exists(SAVE_PATH):
return ''
var file: FileAccess = FileAccess.open(SAVE_PATH, FileAccess.READ)
if file == null:
return ''
var text: String = file.get_as_text()
file.close()
return text
func _on_load() -> void:
status.text = 'loaded from disk' if _load() else 'nothing on disk to load'
_refresh()
func _on_clear() -> void:
DirAccess.remove_absolute(SAVE_PATH)
status.text = 'file removed, the next save writes it again'
_refresh_file()
func _refresh() -> void:
# JSON has a single number type, so every int comes back from the file as a float
fields.player.text = str(state.player)
fields.level.text = '%d' % int(state.level)
fields.coins.text = '%d' % int(state.coins)
fields.best_time.text = '%.2f s' % float(state.best_time)
fields.perks.text = ', '.join(state.perks)
fields.bar.value = int(state.level)
func _refresh_file() -> void:
var text: String = _read()
json_view.text = text if text != '' else 'the file does not exist'
func _state_panel() -> Control:
var panel: Dictionary = _panel('live scene state', Color('#8da5f3'))
var column: VBoxContainer = panel.body
for entry in [{key = 'player', label = 'player'}, {key = 'level', label = 'level'}, {key = 'coins', label = 'coins'}, {key = 'best_time', label = 'best time'}, {key = 'perks', label = 'perks'}]:
var line: HBoxContainer = HBoxContainer.new()
line.add_theme_constant_override('separation', 12)
column.add_child(line)
var name_label: Label = Label.new()
name_label.text = entry.label
name_label.custom_minimum_size = Vector2(130, 0)
name_label.add_theme_color_override('font_color', Color('#8a90a0'))
line.add_child(name_label)
var value: Label = Label.new()
value.add_theme_font_size_override('font_size', 20)
line.add_child(value)
fields[entry.key] = value
var bar: ProgressBar = ProgressBar.new()
bar.max_value = 9
bar.show_percentage = false
bar.custom_minimum_size = Vector2(0, 18)
bar.add_theme_stylebox_override('background', _bar_style(Color('#12141a')))
bar.add_theme_stylebox_override('fill', _bar_style(Color('#8da5f3')))
column.add_child(bar)
fields['bar'] = bar
var buttons: HBoxContainer = HBoxContainer.new()
buttons.add_theme_constant_override('separation', 10)
column.add_child(buttons)
for entry in [{text = 'save now', action = _save}, {text = 'load', action = _on_load}, {text = 'clear file', action = _on_clear}]:
var button: Button = Button.new()
button.text = entry.text
button.pressed.connect(entry.action)
buttons.add_child(button)
status = Label.new()
status.add_theme_color_override('font_color', Color('#8eef97'))
status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
column.add_child(status)
var hint: Label = Label.new()
hint.text = 'JSON.stringify turns the dictionary into text, FileAccess writes it and user:// survives a reload of the page.'
hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
hint.add_theme_color_override('font_color', Color('#8a90a0'))
column.add_child(hint)
return panel.root
func _file_panel() -> Control:
var panel: Dictionary = _panel(SAVE_PATH, Color('#f3d48d'))
json_view = Label.new()
json_view.text = 'the file does not exist'
json_view.add_theme_font_size_override('font_size', 18)
json_view.add_theme_color_override('font_color', Color('#8eef97'))
panel.body.add_child(json_view)
return panel.root
func _panel(heading: String, tint: Color) -> Dictionary:
var outer: VBoxContainer = VBoxContainer.new()
outer.custom_minimum_size = Vector2(500, 0)
outer.add_theme_constant_override('separation', 10)
var title: Label = Label.new()
title.text = heading
title.add_theme_font_size_override('font_size', 22)
title.add_theme_color_override('font_color', tint)
outer.add_child(title)
var style: StyleBoxFlat = StyleBoxFlat.new()
style.bg_color = Color('#2a2f3d')
style.set_corner_radius_all(12)
style.set_content_margin_all(20)
var box: PanelContainer = PanelContainer.new()
box.size_flags_vertical = Control.SIZE_EXPAND_FILL
box.add_theme_stylebox_override('panel', style)
outer.add_child(box)
var column: VBoxContainer = VBoxContainer.new()
column.add_theme_constant_override('separation', 10)
box.add_child(column)
return {root = outer, body = column}
func _bar_style(color: Color) -> StyleBoxFlat:
var style: StyleBoxFlat = StyleBoxFlat.new()
style.bg_color = color
style.set_corner_radius_all(6)
return style
func _root() -> HBoxContainer:
var background: ColorRect = ColorRect.new()
background.color = Color('#12141a')
add_child(background)
background.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
var margin: MarginContainer = MarginContainer.new()
for side in ['margin_left', 'margin_right', 'margin_top', 'margin_bottom']:
margin.add_theme_constant_override(side, 56)
add_child(margin)
margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
var row: HBoxContainer = HBoxContainer.new()
row.add_theme_constant_override('separation', 32)
margin.add_child(row)
return row