Saving & Loading Data

Modes: READ, WRITE (truncates), READ_WRITE, WRITE_READ.

27.1 File paths

res:// — project files (read-only after export).

user:// — writable app data (saves, settings, logs). OS.get_user_data_dir() returns the real path.

27.2 FileAccess (text and binary)

Modes: READ, WRITE (truncates), READ_WRITE, WRITE_READ. Encrypted files: FileAccess.open_encrypted_with_pass(path, mode, "password"). Other methods: store_line, get_line, eof_reached, store_32, get_32, store_float, get_buffer.

const SAVE_PATH := "user://save.dat"

func save_text(path: String, text: String) -> void:
	var f := FileAccess.open(path, FileAccess.WRITE)
	if f == null:
		push_error("Cannot open %s: %s" % [path, error_string(FileAccess.get_open_error())])
		return
	f.store_string(text)
	# file closes automatically when 'f' goes out of scope (or call f.close())

func load_text(path: String) -> String:
	if not FileAccess.file_exists(path):
		return ""
	return FileAccess.get_file_as_string(path)

func save_binary(data: Dictionary) -> void:
	var f := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
	f.store_var(data)            # serializes Variants (objects not allowed by default)

func load_binary() -> Dictionary:
	if not FileAccess.file_exists(SAVE_PATH):
		return {}
	var f := FileAccess.open(SAVE_PATH, FileAccess.READ)
	return f.get_var()

27.3 JSON

JSON caveats: all numbers parse as float; vectors/colors must be converted manually (e.g., store [x, y] or use var_to_str/str_to_var, or JSON.from_native()/JSON.to_native() in 4.4+).

func save_json(data: Dictionary) -> void:
	var f := FileAccess.open("user://save.json", FileAccess.WRITE)
	f.store_string(JSON.stringify(data, "\t"))

func load_json() -> Dictionary:
	var text := FileAccess.get_file_as_string("user://save.json")
	var parsed = JSON.parse_string(text)      # null on error
	return parsed if parsed is Dictionary else {}

func load_json_with_errors(text: String) -> Variant:
	var json := JSON.new()
	var err := json.parse(text)
	if err != OK:
		push_error("JSON error at line %d: %s" % [json.get_error_line(), json.get_error_message()])
		return null
	return json.data

27.4 ConfigFile (settings)

Produces an INI-like file:

const SETTINGS := "user://settings.cfg"

func save_settings() -> void:
	var cfg := ConfigFile.new()
	cfg.set_value("audio", "music_volume", 0.8)
	cfg.set_value("video", "fullscreen", true)
	cfg.set_value("controls", "jump", InputMap.action_get_events("jump"))
	cfg.save(SETTINGS)

func load_settings() -> void:
	var cfg := ConfigFile.new()
	if cfg.load(SETTINGS) != OK:
		return
	var vol: float = cfg.get_value("audio", "music_volume", 1.0)
	var fs: bool = cfg.get_value("video", "fullscreen", false)
	DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN if fs else DisplayServer.WINDOW_MODE_WINDOWED)
[audio]
music_volume=0.8
[video]
fullscreen=true

27.5 A save system with groups

Always include a version field so you can migrate old saves.

# save_manager.gd (autoload)
extends Node

const PATH := "user://savegame.json"

func save_game() -> void:
	var nodes := []
	for node in get_tree().get_nodes_in_group("persist"):
		if node.has_method("save"):
			var d: Dictionary = node.save()
			d["scene"] = node.scene_file_path
			d["parent"] = str(node.get_parent().get_path())
			nodes.append(d)
	var f := FileAccess.open(PATH, FileAccess.WRITE)
	f.store_string(JSON.stringify({"version": 1, "nodes": nodes}))

func load_game() -> void:
	if not FileAccess.file_exists(PATH):
		return
	var data = JSON.parse_string(FileAccess.get_file_as_string(PATH))
	if not data is Dictionary:
		return
	for node in get_tree().get_nodes_in_group("persist"):
		node.queue_free()
	await get_tree().process_frame
	for d in data.nodes:
		var obj: Node = load(d.scene).instantiate()
		get_node(d.parent).add_child(obj)
		obj.load_state(d)
# in a persistent object (added to group "persist")
func save() -> Dictionary:
	return {"x": position.x, "y": position.y, "hp": health}

func load_state(d: Dictionary) -> void:
	position = Vector2(d.x, d.y)
	health = int(d.hp)

27.6 Directories

Godot 3 note: File and Directory classes became FileAccess and DirAccess; JSON.print/JSON.parse became JSON.stringify/JSON.parse_string.

DirAccess.make_dir_recursive_absolute("user://saves/slot1")
var dir := DirAccess.open("user://saves")
if dir:
	for file in dir.get_files():
		print(file)
DirAccess.remove_absolute("user://saves/old.json")