How to organize JSON Save File

Godot Version

4.3

Question

I am working on creating a save system for my project and I have created a basic save system to save my players inventory.

extends Node

const SAVE_PATH: String = "res://savegame.bin"

func save_inventory() -> void:
	var file = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
	var inventoryData: Dictionary = {}
	var inventoryGrid = GUI.inventoryMenu.get_child(0).get_child(1)
	for i in inventoryGrid.get_child_count():
		var slot_index = i
		var item_data
		var item_data_count
		if inventoryGrid.get_child(i).get_child_count() <= 0:
			item_data = null
			item_data_count = 0
			inventoryData[slot_index] = {item_data: item_data_count}
		elif inventoryGrid.get_child(i).get_child_count() > 0:
			#slot has an item
			item_data = inventoryGrid.get_child(i).get_child(0).data
			item_data_count = inventoryGrid.get_child(i).get_child(0).itemCount
			inventoryData[slot_index] = {item_data.itemName: item_data_count}
	var inventoryJSTR = JSON.stringify(inventoryData)
	file.store_line(inventoryJSTR)
	print("saved")

func load_inventory() -> void:
	var file = FileAccess.open(SAVE_PATH, FileAccess.READ)
	if !file:
		return
	if file == null:
		return
	if FileAccess.file_exists(SAVE_PATH) == true:
		if not file.eof_reached():
			var current_line = JSON.parse_string(file.get_line())
			if current_line:
				var inventoryGrid = GUI.inventoryMenu.get_child(0).get_child(1)
				for index in current_line:
					for item in current_line[index]:
						if item != str("<null>"):
							var inventoryItem = InventoryItem.new()
							inventoryItem.init(Game.items[item])
							if inventoryGrid.get_child(int(index)).get_child_count() <= 0:
								inventoryGrid.get_child(int(index)).add_child(inventoryItem)
								inventoryGrid.get_child(int(index)).get_child(0).itemCount = int(current_line[index].get(item))
				print("loaded")

Example of Save File:

{β€œ0”:{β€œβ€:0},β€œ1”:{β€œβ€:0},β€œ2”:{β€œβ€:0},β€œ3”:{β€œβ€:0},β€œ4”:{β€œPurple Berry”:5},β€œ5”:{β€œβ€:0},β€œ6”:{β€œβ€:0},β€œ7”:{β€œβ€:0},β€œ8”:{β€œβ€:0},β€œ9”:{β€œβ€:0},β€œ10”:{β€œβ€:0},β€œ11”:{β€œβ€:0},β€œ12”:{β€œβ€:0},β€œ13”:{β€œPurple Berry”:4},β€œ14”:{β€œβ€:0},β€œ15”:{β€œβ€:0},β€œ16”:{β€œβ€:0},β€œ17”:{β€œβ€:0},β€œ18”:{β€œβ€:0},β€œ19”:{β€œβ€:0},β€œ20”:{β€œβ€:0},β€œ21”:{β€œβ€:0},β€œ22”:{β€œβ€:0},β€œ23”:{β€œβ€:0},β€œ24”:{β€œβ€:0},β€œ25”:{β€œβ€:0},β€œ26”:{β€œPurple Berry”:6},β€œ27”:{β€œβ€:0},β€œ28”:{β€œβ€:0},β€œ29”:{β€œβ€:0}}

My question is, if I wanted to store more lines of information to be saved; for example to save the hotbar inventory or equipment slots, what is the best way to store multiple lines in a JSON file? Or how should I organize the save file so i can easily get the line i need to load and not need to keep track which line is what in the JSON? Can I create a label or a tag for each line to easily get what i need?

The way I do it personally is with ConfigFile and sections

extends Node


var _file: ConfigFile = ConfigFile.new()
const _filename: String = "user://file.cfg"


var _data_array: Array = []


func _ready():
	_file.load(_filename)
	
	_init_data_array()


# _data_array
func _init_data_array():
	set_data_array(_file.get_value("SECTION_NAME", "data_array", []))

func get_data_array() -> Array:
	return _data_array

func set_data_array(val: Array = []):
	_data_array = val
	_file.set_value("SECTION_NAME", "data_array", val)
	_file.save(_filename)

func append_data_array(val: String):
	_data_array.append(val)
	set_data_array(_data_array)

This way when the node is loaded it updates _data_array and by using the get_ set_ append_ functions keep both the variable and the file up to date.

The _data_array here is an example of course, it works for basic data types, here is what the file looks like:

[Creatures]

discovered=["mob_1", "mob_2"]

[Tutorial]

finished=true

[Inventory]

items=["item_1", "item_2", "item_3"]
hotbar=["item_1"]

To be clear for the Inventory section the code would be

# INVENTORY
# _items_array
func _init_items_array():
	set_items_array(_file.get_value("Inventory", "items", []))

func get_items_array() -> Array:
	return _items_array

func set_items_array(val: Array = []):
	_items_array = val
	_file.set_value("Inventory", "items", val)
	_file.save(_filename)

func append_items_array(val: String):
	_items_array.append(val)
	set_items_array(_items_array)

# _hotbar_array
func _init_hotbar_array():
	set_hotbar_array(_file.get_value("Inventory", "hotbar", []))

func get_hotbar_array() -> Array:
	return _hotbar_array

func set_hotbar_array(val: Array = []):
	_hotbar_array = val
	_file.set_value("Inventory", "hotbar", val)
	_file.save(_filename)

func append_hotbar_array(val: String):
	_hotbar_array.append(val)
	set_hotbar_array(_hotbar_array)

This needs a lot of boilerplate code, but once it’s done it’s quite robust and the auto-complete in-editor is quite nice.

This also allows to add custom code for some sections (for exemple in the get_items_array() to return the actual items instead of a String array).

1 Like

If You want to keep the JSON format, calling get_line() again on a FileAccess:file will get you the next line.


func load_inventory() -> void:
	var file = FileAccess.open(SAVE_PATH, FileAccess.READ)
	if !file:
		return
	if file == null:
		return
	if FileAccess.file_exists(SAVE_PATH) == true:
		if not file.eof_reached():
			var current_line = JSON.parse_string(file.get_line())
			if current_line:
				pass #INVENTORY
			current_line = JSON.parse_string(file.get_line())
			if current_line:
				pass #HOTBAR
			current_line = JSON.parse_string(file.get_line())
			if current_line:
				pass #EQUIPMENT_SLOTS

The reason I mentioned the section system in the other post is that the system you have seem like it would be a little hard to update, but I am sure with a bit of structuring you will also be able to manage :grinning:


Also: you can store it all in a greater dictionary, so

{"inventory": {β€œ0”:{β€œβ€:0},β€œ1”:{β€œβ€:0},...},"hotbar": {...},"equipment": {...}}

then

func load_save() -> void:
	var file = FileAccess.open(SAVE_PATH, FileAccess.READ)

	if !file or file == null:
		return

	var json_save = JSON.parse_string(file.get_line())
	
	if !json_save:
		return

	load_inventory(json_save["inventory"])
	load_hotbar(json_save["hotbar"])
	load_equipment(json_save["equipment"])
1 Like

Please don’t use res for saving, since is not accesible at runtime.

Organize the save file in a way that’s readable for you using a Dictionary:

extends Node

const SAVE_PATH: String = "user://savegame.json"


# Called when the node enters the scene tree for the first time.
func _ready() -> void:
	var count = 1
	var data := {
		"item_data": [
			{
				"count": count,
				"index": 0
			},
			{
				"feature1": "gun",
				"feature2": "fire",
				"damage": 1
			}
		],
		"two": 2,
		"three": 3
	}
	
	var json_string = JSON.stringify(data, "\t")
	
	var file = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
	file.store_string(json_string)
	file.close()
	print_debug(json_string)

If you go to AppData\Roaming\Godot\app_userdata\yourgame you should see the file saved.

The savefile should look like this:

{
	"item_data": [
		{
			"count": 1,
			"index": 0
		},
		{
			"damage": 1,
			"feature1": "gun",
			"feature2": "fire"
		}
	],
	"three": 3,
	"two": 2
}
1 Like