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.data27.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=true27.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")