保存与读取

把场景状态的字典以 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

更多数据示例