SOLVED Making a stable save system.

In save systems, you'll often refer to assets.
But if you try storing sprite_index : _char.sprite_index you are just writing down the number reference for sprite_index, and that number can change when you add new sprites to the game.
I right now have a robust save system that records sprites, objects, rooms, and functions! It works great until I update the game's asset list.
I would like to find a way to make a stable save system before I release the game publicly, so I don't have to force players to lose save data whenever I release an update.
What's the best practice for doing so? I've never seen this subject discussed anywhere.

I'm guessing I need to record assets as strings, using
sprite_get_name() script_get_name() etc.
and then load them using asset_get_index().
But this doesn't work for sequences (there is no sequence_get_name)
and this can make arrays of data with assets in them complicated.
Do I need to before saving loop through the array checking each line and converting assets to strings?
I worry I'm overlooking a more straightforward method.
Update- using sprite_get_name() script_get_name() etc.
and then load them using asset_get_index() works great.
Problem solved.
 
Last edited:

Mr Magnus

Viking King
A few ways. While I'd recommend you'd not refer to assets to begin with because they can be volatile if you can avoid it for your specific case I'd recommend writing your own asset-id system that is constant.

So, you'd have some data structure (function, struct, map, whatever) that maps your keys to the asset index. You save the keys to the file, and when you load you use your map to translate the keys back in to assets. Because the keys don't change and the map is re-evaluated whenever the game starts it doesn't matter if the assets change, the key is the same.


So something like

GML:
function get_asset_key(asset){
    return asset_key_storage[$ asset];
}

function get_key_asset(key){
    return asset_key_storage[$ key];
}

get_asset_key(obj_player); /// Returns "Oplay" or whatever. "Oplay" is set by you and never changes. You can save that to file.

get_key_asset("oPlay") /// Returns the index of obj_player. The index can change all it wants, but you've permanently mapped "oPlay" to that constant.
 
Last edited:
Maybe I misunderstanding what you're saying but are you suggesting manually giving a unique name to every asset?
That's not going to work for me that would take too much time.
 

Mr Magnus

Viking King
Doesn't have to be manually assigned, you can automatically generate it once by looping trough all the assets and spitting out some auto-generated name that you can then use to construct your translation map. You can loop trough assets easily enough and they're already constants.

The unique name doesn't have to be important or thought out, it can literally be a random number. All it has to do is not change.

But generally you're fast running out of options: you can't rely on object indexes that could for all intents and purposes are random from build to build. A good save system is something that is hard to retroactively fit half-way trough the project. Your robust save system isn't robust if it's sensitive to the whims of a compiler.
 
I don't know how you would loop through all assets or generate a unique number for each one.
I'd love to hear more about how you'd do this easily!
 

kupo15

Member
Maybe I misunderstanding what you're saying but are you suggesting manually giving a unique name to every asset?
That's not going to work for me that would take too much time.
Surely you are checking the object_name and have built up a list of asset names in order to determine which things to save in each object, right? Like if object_name == obj_player save x, y, state...etc..? Or are you purely using index numbers?

Does adding a new asset break the save file because the index numbers don't line up anymore or are you changing the format entirely? I'm having a hard time understanding how you built your save system and how its formatted to be able to offer suggestions. Would be easier to see what the save file looks like
 

Mr Magnus

Viking King
I don't know how you would loop through all assets or generate a unique number for each one.
I'd love to hear more about how you'd do this easily!

It's not "easily" per say, takes a bit of tinkering to fit your specific use case or project specifications, but it all fits neatly in to a single script file and is easily built upon if you want it sorted or in a different format or whatever suits your needs.

The idea is simple: at the root of your project directory is a .yyp file. This file is how GameMaker knows what resources your project contains and what they are called and where they are. It also happens to just be a normal .json file.
We open that file, read out all the assets, find out their types, and use that to make a unique key.
Those key mappings are then saved to %localappdata%/project (or wherever save files go).

You can then use standard find/replace or json prettifiers or whatnot to turn that in to a valid gml code snippet or data structure or whatever you can paste in to a script somewhere and keep updated when you add or remove assets from your save system.

The "map" that ends up in assetMap.json will be of the format

Code:
//After prettification to add in indents and spaces and whatnot
{
    key_to_asset:{
        "OB0": "obj_stuff",
        "OB1": "obj_stuff1",
        "FN5": "font_consolas",
        //Etc...
    }
    asset_to_key:{
        "obj_stuff":"OB0",
        "obj_stuff1":"OB1",
        "font_consolas":"FN5",
        //...
    }
}
This is a valid struct for GML if you're just after mapping asset names to the keys, but you can manipulate this to your hearts content to build some data structure that can keep track of your keys and assets or id's that currently exist, or load it in to GameMaker and parse it there, or do whatever you want. This is just a rough foundation because it's 2AM and I should head to bed, but this may push you in to the right direction.

Example script in spoiler
GML:
//Step one, asset types get to be differentiated with a two letter code. All object will have a code starting with "OB", all sprites "SP", e.t.c.
var asset_letter = {};
asset_letter[$ asset_object] =  "OB";
asset_letter[$ asset_sprite] =  "SP";
asset_letter[$ asset_sound] =  "SN";
asset_letter[$ asset_room] =  "RM";
asset_letter[$ asset_tiles] =  "TI";
asset_letter[$ asset_path] =  "PA";
asset_letter[$ asset_script] = "SC";
asset_letter[$ asset_font] =  "FN";
asset_letter[$ asset_timeline] = "TL";
asset_letter[$ 4] = "TX"; ///There is some bug going on here. Remind me to contact Yoyo.
asset_letter[$ asset_shader] = "SH";
asset_letter[$ asset_animationcurve] =  "AC";
asset_letter[$ asset_sequence] = "SE";
asset_letter[$ asset_unknown] =  "??";

//A function to load our asset master list
function load_json(file){
    var existing_buffer = buffer_load(file);
    if (existing_buffer != -1) {
        var json = buffer_read(existing_buffer, buffer_string);
        buffer_delete(existing_buffer)
        return json_parse(json);
    } else{
        return false;
    }
}

//A function to save our converted map
function save_json(file, structure){
    var json = json_stringify(structure);
    var buffer = buffer_create(1, buffer_grow, 1);
    buffer_write(buffer, buffer_string, json);
    buffer_save(buffer, file);
    buffer_delete(buffer);
}

// When you run this function a file dialogue opens. Point it to the .yyp file that's at the root of your project directory.
function create_asset_keys(){
    ///Load the .yyp file
    resources = load_json(get_open_filename("YoYoGames Project folder|*.yyp",""));
    var _r = resources.resources
    //Prepare our resulting map
    var map = {
        key_to_asset:{},
        asset_to_key:{},
    };

    //Loop trough all the resources that the .yyp file lists, which should be all the resources that appear in the asset browser in GameMaker (this is where gameMaker gets those resources from)
    for(var i = 0; i < array_length(_r); i++){
        var asset_name = _r[i].id.name; //The asset name as it appears in the tree
        var asset_type = asset_get_type(asset_name); // The type of resource
        var asset_index = asset_get_index(asset_name); // The index of that asset right now
        var unique_key = asset_letter[$ asset_type] + string(asset_index); // A unique key that can be used to identify this resource after the fact
        map.key_to_asset[$ unique_key] = asset_name; // key => asset_name
        map.asset_to_key[$ asset_name] = unique_key; //asset_name => key
    }
    save_json("assetMap.json",map); //save
}
 
Last edited:
I've made great progress on my save system!
I've been saving to a struct. I just on save convert the names to strings with get_name
sprite_index : sprite_get_name(_char.sprite_index),
dialog : script_get_name(_choice.dialog),
and when loading them I convert them back with asset_get_index!
_char.sprite_index = asset_get_index(_charData.sprite_index)
_char.dialog = asset_get_index(_choiceData.dialog)

for an array I loop through it and converted indexes over before passing the string converted array back.
Whenever I converted something in the array I made a note of what row it was in, and convert them back.

This has handled everything except for sequences, I've put in a request for sequence_get_name() to be added as a feature.

Mr Magnus thank you for the help! This might be the solution for me until yoyogames implements a sequence_get_name()
 

kupo15

Member
asset_letter[$ 4] = "TX"; ///There is some bug going on here. Remind me to contact Yoyo.
I remember the manual saying that struct keys cannot start with a numeral, even a string form. I've had an error when I tried to make one using a literal but somehow no error and it works when I did it this way
 

Mr Magnus

Viking King
I remember the manual saying that struct keys cannot start with a numeral, even a string form. I've had an error when I tried to make one using a literal but somehow no error and it works when I did it this way
That's not the error (albeit the fact the manual says structs can't start with a number is interesting given it accepts this well enough). It's that "get_asset_type" seems to return 4 for tilesets instead of what asset_tiles returns ( a 9).
 

SnoutUp

Member
Not sure I understand the benefits of this approach. Saving objects and their properties - sure. But why sprites and functions? Granted, I haven't written very advanced saving systems, but I usually give game objects readable IDs, save those with object properties and on load recreate the game from that data.
 

Yal

šŸ§ *penguin noises*
GMC Elder
object_get_name / asset_get_index handles index/name translations without you needing to set up your own system. (Though if you use this, you need to never rename an object that can be stored in a save file). I usually only use sequences for cutscenes, and you can't save in the middle of a cutscene, so there's no need to figure out a way to save them.

In general, it's best to save as little data as possible for maximal flexibility for updating the game - e.g. if you can only save at certain save points, you could have them have special index numbers which determines which room and which coordinates the player is loaded in to - even if you move it around a lot, the player won't load into a wall after a patch. Instead of saving which sprite something is using, have that sprite be based on their equipment / powerup and save that instead. The built-in save system is notoriously useless once you start using global variables, because it will use the saved version of those, making changes to static data like the map layout not show up in savefiles.
 

Mr Magnus

Viking King
In general: agreed. While I won't question your method as I don't know why you're saving all these things I'd recommend taking a hard look why you need to save all these assets. What do you really need saved instead of reconstructing it after the fact or represent it via more efficient means.
 
Howdy! My save system works great, I solved the sequence issue by having them be recorded as strings and just translated before processing them.
Normally, you wouldn't need a save system that records so much. It was a poor choice of words for me to start my post saying recording assets was typical.

I'm making a story-focused visual novel sort of game.
I've made it so I can write a text script that then gets parsed into code.
I've been focusing on making things as easy as possible for me while writing.
So for example in my script I might write:

Empty Room... Let's load a friend!
#spawnC: "John",sJohn,"left",160
#wait: 15
John: Why here, this is an example
John: Goodbye now
#moveC: "John","right"
#end

This would create john, have him walk in from the left, set his sprite, stop him at pos160, he'd say hello, and then walk off-screen to the right.
It was a trivial matter to save the array that was holding all this information.
Being able to save and reload at any point in the story saves a lot of time when polishing.
And now I can use that same save system to record checkpoints for players.
 
Last edited:
Top