• Hey Guest! Ever feel like entering a Game Jam, but the time limit is always too much pressure? We get it... You lead a hectic life and dedicating 3 whole days to make a game just doesn't work for you! So, why not enter the GMC SLOW JAM? Take your time! Kick back and make your game over 4 months! Interested? Then just click here!

GML Save/Load System on Consoles - JSON vs DS Map System

B

Biddum

Guest
Hello,

I am working on a project that may appear on consoles (XBox, PS4, and potential Nintendo Switch). I am looking into the various ways of saving/loading game data and came across tutorials using JSON and another tutorial that uses DS Map based save systems. I am wondering if consoles need to have a certain save/load system format in order to work correctly?

Is there any advantage using JSON vs. ds_map saving for console-based games?


Thanks.
 
M

MishMash

Guest
JSON has the added benefit of automatically saving and re-constructing nested data structures. Both formats are generated off of ds_maps:
json_encode(ds map index), vs ds_map_write(ds map index). Both of which also do return strings, however json_encode is almost always better to use.

There are a couple of major benefits of using Json:
- You can create nested datastructures: lists or maps inside maps using ds_map_add_map or ds_map_add_list. These internal datastructures are automatically re-constructed when you load back in using json_decode.
- You can more easily visually debug the output if you have any problems saving or loading, bceause JSON is a visually readable format, whereas ds_map_write simply dumps the contents of the map from memory into a string.

The format should be irrelevant as ultimately, in the end, you'll need to use some form of file function, whether it be text file writing, ini files or saving buffers. Both ds_map_write and json_encode both just return strings which would need a separate scheme for saving anyway. The only disadvantage of json_encode is that it'll take up slightly more space with its formatting characters. It also seems to break if the map key is not a string, but that's easy to resolve.

Personally, my favourite approach for saving and loading is to use buffers in conjunction with json_encode and various other functions for encoding datastructures. Buffers allow you to easily and compactly write out data of different formats and are easy to load in/save.
 
B

Biddum

Guest
@MishMash thank you or the advice! I watched this tutorial and it blends JSON with buffers and I am going to plan to design my save system that way. One (maybe obvious) question is saving global variables to a ds_map. I'm not sure if you ever watched the tutorial but ideally it takes all instances and their attributes (x cord, y cord, sprite, etc..) and saves them and loads them back to the screen. What if I have a few controller objects (obj_score, obj_achievements, obj_settings) that stores global variables and I want to save their values? Do I save the instance of the object and their attributes/variables and load them or do I just create a key-value pair for the variable and plop it in the ds_map?
 
M

MishMash

Guest
I've not watched that specific tutorial, however my own personal solution to this problem was to abstract away the saving and loading, and give each object two custom user events (6 for saving, 7 for loading).

Any object that wished to save its information would do so in that event, writing out to a global buffer. The loading process was the same in reverse.

The backend i created looks something like this (this is a simplified version, but encapsulates the core idea):

Global Save routine:
Code:
global.SAVE_BUFFER = buffer_create(...);
with(all){
       // save core object properties
       buffer_write(global.SAVE_BUFFER, buffer_u32, object_index);
       buffer_write(global.SAVE_BUFFER, buffer_s32, x);
       buffer_write(global.SAVE_BUFFER, buffer_s32, y);

       // save custom object properties
       event_user(6);
}
buffer_save(global.SAVE_BUFFER, "save_data.save");
buffer_delete(global.SAVE_BUFFER);
Global Load routine:
Code:
global.LOAD_BUFFER = buffer_load("save_data.save");
while(buffer_tell(global.LOAD_BUFFER) < buffer_size(global.LOAD_BUFFER)){
       // save core object properties
       var o_index = buffer_read(global.LOAD_BUFFER, buffer_u32);
       var o_x = buffer_read(global.LOAD_BUFFER, buffer_s32);
       var o_y = buffer_read(global.LOAD_BUFFER, buffer_s32);

       // Create instance
       var inst = instance_create(o_x, o_y, o_index);

       // load custom object properties
       with(inst){
           event_user(7);
       }
}
buffer_delete(global.LOAD_BUFFER);
Code:
// Example object event user 6
buffer_write(global.SAVE_BUFFER, buffer_u8, my_health);
buffer_write(global.SAVE_BUFFER, buffer_u8, my_level);

// Example object event user 7
my_health = buffer_read(global.LOAD_BUFFER, buffer_u8);
my_level   = buffer_read(global.LOAD_BUFFER, buffer_u8);
Then inside event_user 6 and 7 of each object, they would have their own buffer_writes and buffer_reads (writing to global.SAVE_BUFFER, and loading from global.LOAD_BUFFER). This is a nice way of encapsulating saving and loading on a per-object basis. Regarding global variables, my personal approach is to make each controller responsible for the global variables they care about. Though in practise, most things end up not being global, but accessible through a series of functions that interface with the controller objects.
But a system like this basically allows you to give each instance its own custom saving and loading.

Some general notes/tips on how you can extend something like this:
- Doesn't need to be done with "all" instances, but perhaps you could create a list of object indices which need saving.
- You can keep track of the amount of data each object has written to the buffer, by measuring writing position before and after writing custom data. This allows your read routine to be a bit smarter and know how much data to expect in each instance, as a common problem here is if you make a mistake and end up reading more/less than you should, all sorts of nonsense arises.
- You can also assign objects a unique index on creation. If for example instances need to reference each other, GM instance id's will change between different runs, so it is useful to have a "global" array of all instances that need saving and loading and assign then a singular unique system ID that will remain the same between loads, along with a series of functions for fetching an instance from an ID and so on.

-- In this case, the only "core" properties I would bother to save are object index, x and y. This is because anything else is superfluous and could change for different instances. Theres no point saving information you dont need to save.

Hope this helps!
 
Top