GMS 2 What type of save system?

Discussion in 'Programming' started by samspade, Mar 17, 2019.

  1. samspade

    samspade Member

    Joined:
    Feb 26, 2017
    Posts:
    1,589
    I'm not really sure if this is a programming question or a design question, so I'm just picking one.

    I'm working on a project that is pretty small, but basically needs to save at least some information from every instance of every object in every room. Saving is necessary for both exiting the game and returning but also for just moving from room to room.

    Essentially it is a puzzle game so switches, doors, puzzle information, inventories, items held by things, and so on need to be saved.

    For those of you who have done more elaborate save systems, how would you go about this and what things would you consider? What are good design practices?

    For example, should the save system be entirely independent of the instances themselves? Or should the instances themselves have code to save and load their states? Should saved variables all be globals that instances pull from or should I pull the varaibles to save from those instances? Should I use JSON or something else?

    Code examples would be great, but just plain English explanations of what people have done would also be great.
     
  2. Tsa05

    Tsa05 Member

    Joined:
    Jun 21, 2016
    Posts:
    527
    Personally, I'd go json for the future-proofing possibilities it provides.
    Basically, you create a script that runs at the start of your game, and it creates a global variable pointing to a ds_map.

    Then, you populate that map with the default state of "things."

    As your game goes on, rooms, puzzles, objects create themselves with all of their normal values, and then they check the ds_map for the "saveable" pieces. As those bits of info change, they are updated in the map.

    Once that is all set up, saving and loading are easy and can be done at any time. To save, you json_encode your map, obfuscate it if you wish, and write the string to a save file. To load, you'd pull in the string, decode the json, and put the result into your global variable (and then reload the room if you were in a level already).

    The advantage of using a map structure is that you can do things like this:
    Code:
    {
         "red_lever": 0,
         "red_key": 1,
         "red_door": 0,
         "danger_tile": 4
    }
    Basically, all of your saved data is clearly named and hard to confuse. Adding more saved data to the map doesn't break anything that's already there.

    But the advantage of using *json* goes much farther. First, imagine that you change your mind in the above example, and you want to have "danger_tile" become a list of floor tile states, instead of a single number. Not a problem; Maps and lists in GMS can contain maps and lists:

    Code:
    {
         "red_lever": 0,
         "red_key": 1,
         "red_door": 0,
         "danger_tile": [3, 2, 4, 0, 0, 1]
    }
    And this is where json comes into play. By using json_encode to save your map, and by using json_decode to load your map, the data structures are all restored to their proper place.

    This is powerful! Remember, in GameMaker, ds_maps and ds_lists are "referred to" by number. So, if you create a list of floor tile states, what you've really got is a single number. When you use that number with a ds_list function, GMS knows how to find data in the list. But if you simply tried to save "the list" you'd just be saving a number. So your save mechanism would need to either "take into account every list you want to save and specifically be written to save them" or...use json and it's automatically done for you.

    The difference occurs one time, in code, during that initial script that sets up your game's save-map.

    Code:
    global.saveMap = ds_map_create();
    
    saveMap[?"red_lever"] = 0;
    saveMap[?"red_key"] = 1;
    saveMap[?"red_door"] = 0;
    
    var tileList = ds_list_create();
    ds_list_add(tileList, 3, 2, 4, 0, 0, 1);
    
    ds_map_add_list(global.saveMap, "danger_tile", tileList);
    That last line is the difference-maker. Rather than simply adding the list to the map, we add the list to the map "as a list." Tiny change in verbiage, big change in behavior. GMS now knows to include all of the list values in the json_encode() rather than simply writing down the index number of the list.

    Later, in your game, you could use the list simply by looking it up:
    Code:
    var list_to_check = global.saveMap[?"danger_tile"];
    
    // Now, look up any data in the list as you normally would...for example
    for(var i=0; i<ds_list_size(list_to_check); ++i){
         var tileState = list_to_check[| i];
         // Code to do things with that state info
    }
    
    It gets better. Really!
    Maps can store maps, too. Check this out:
    Code:
    {
         "level_1": {
              "red_lever": 0,
              "red_key": 1,
              "red_door": 0,
              "danger_tile": [3, 2, 4, 0, 0, 1]
         },
         "level_2": {
              "red_lever": 1,
              "red_key": 0,
              "blue_key": 1,
              "red_door": 1,
              "blue_door": 0
         }
    }
    Woooo, I made levels! They even share some variable names, but GMS doesn't care because they're in separate maps.
    Just need one change to make this work. We've got to place maps inside of maps, just like with lists.
    Code:
    global.saveMap = ds_map_create();
    
    // Level 1 setup
    var levelMap = ds_map_create();
    levelMap[?"red_lever"] = 0;
    levelMap[?"red_key"] = 1;
    levelMap[?"red_door"] = 0;
    
    var tileList = ds_list_create();
    ds_list_add(tileList, 3, 2, 4, 0, 0, 1);
    
    ds_map_add_list(levelMap, "danger_tile", tileList);
    
    ds_map_add_map(global.saveMap, "level_1", levelMap);
    
    // Level 2 setup
    var levelMap = ds_map_create();
    levelMap[?"red_lever"] = 1;
    levelMap[?"red_key"] = 0;
    levelMap[?"blue_key"] = 1;
    levelMap[?"red_door"] = 1;
    levelMap[?"blue_door"] = 0;
    
    ds_map_add_map(global.saveMap, "level_2", levelMap);
    By simply adding each map to an overarching map, you get the benefit of having the whole structure save and load automatically as json.

    When you load a level, you simply locate the correct map first, then load from it.
    So, instead of something like this for an object to get the status of its lever:
    Code:
    myLeverState = global.saveMap[?"red_lever"];
    You'd do this:
    Code:
    levelMap = global.saveMap[?"level_1"];
    myLeverState = levelMap[?"red_lever"];
    Simple change, really--since the state of the lever is not in the root save map, but rather, one map deeper in a level map, you get that level map and then look things up as usual.

    In this way, any objects in any level can quickly look up the correct level map from the overall saveMap, and can then look up the values they need. They can even look up previous or next levels, or any level at all, and find values in there, if needed. Since everything is loading from a map, you can evennnnnnn have the current level's objects look up the map for something 10 levels later and alter a value in there...causing the future state of the game to be altered when it loads, based on the present interaction.
     
    Last edited: Mar 17, 2019
  3. samspade

    samspade Member

    Joined:
    Feb 26, 2017
    Posts:
    1,589
    I went with something similar. But I have a few questions. Here are a couple of the things I wanted.
    • I wanted the saving to happen completely independent of any object. In other words, I decided that no object should have code which references or otherwise interacts with the save system.
    • I wanted to use the JSON format.
    • Every level would need their own save object, as the types of information necessary to save would be different in each level. And as a result, hard coding what is saved per level simply seemed to be the fastest way to handle it.
    Here is my first pass. The level isn't fully built (or fully saved at this point) so right now I'm only saving the state of doors and switches.

    Code:
    
    hello_world_saved_list = ds_list_create();
    hello_world_saved_map = ds_map_create();
    ds_list_add(hello_world_saved_list, hello_world_saved_map);
    ds_list_mark_as_map(hello_world_saved_list, 0);
    hw_switch_list = ds_list_create();
    ds_map_add_list(hello_world_saved_map, "switches", hw_switch_list);
    hw_door_list = ds_list_create();
    ds_map_add_list(hello_world_saved_map, "doors", hw_door_list);
    
    

    Code:
    
    var cur_list = hw_switch_list;
    with (switch_parent) {
        var pos = ds_list_find_index(cur_list, id);  
        if (pos >= 0) {
            var saved_map = ds_list_find_value(cur_list, pos);
            ds_map_replace(saved_map, "switch id",      id);      
            ds_map_replace(saved_map, "switch state", state);
        } else {
            var new_map = ds_map_create();
            ds_map_add(new_map, "switch id",    id);
            ds_map_add(new_map, "switch state", state);
            ds_list_add(cur_list, new_map);
            pos = ds_list_find_index(cur_list, new_map);  
            ds_list_mark_as_map(cur_list, pos);
        }
    }
    
    var cur_list = hw_door_list;
    with (object_of_switch_parent) {
        var pos = ds_list_find_index(cur_list, id);  
        if (pos >= 0) {
            var saved_map = ds_list_find_value(cur_list, pos);
            ds_map_replace(saved_map, "door id",      id);      
            ds_map_replace(saved_map, "door state", state);
        } else {
            var new_map = ds_map_create();
            ds_map_add(new_map, "door id",    id);
            ds_map_add(new_map, "door state", state);
            ds_list_add(cur_list, new_map);
            pos = ds_list_find_index(cur_list, new_map);  
            ds_list_mark_as_map(cur_list, pos);
        }
    }
    
    

    Code:
    
    var list_size = ds_list_size(hw_switch_list);
    for (var i = 0; i < list_size; i += 1) {
        var cur_map = hw_switch_list[| i];
        var switch_id = ds_map_find_value(cur_map, "switch id");
        with (switch_id) {
            state = ds_map_find_value(cur_map, "switch state");
        }
    }
    
    var list_size = ds_list_size(hw_door_list);
    for (var i = 0; i < list_size; i += 1) {
        var cur_map = hw_door_list[| i];
        var switch_id = ds_map_find_value(cur_map, "door id");
        with (switch_id) {
            state = ds_map_find_value(cur_map, "door state");
        }
    }
    
    

    Code:
    
    ds_list_destroy(hello_world_saved_list);
    
    

    my save and load scripts just look like this (load just uses a different enum):

    Code:
    
    with (saver_parent) {
        event_user(saver_user_event.save);
      
        print_start();
        print("LEVEL SAVED");
        print_end();
        return true;
    }
    print_start();
    print("NO SAVE OBJECT! LEVEL NOT SAVED!");
    print_end();
    return false;
    
    

    I've tested all of this and it appears to work. But there are two specific things I have questions about.

    First, I don't know how to tell if all the scripts and maps are being deleted correctly. I think I did it right, but I'm not sure how to check because once I destroy the save object, the debugger doesn't have any variables to check.

    Second, related to the first, while I'm nesting all my maps and lists, I'm also directly accessing them without going through the list structure, including inside of with statements. I don't know why this would be a problem, and in watching the debugger it doesn't seem to mess up the structure, but is this acceptable in GML?
     
  4. Tsa05

    Tsa05 Member

    Joined:
    Jun 21, 2016
    Posts:
    527
    @samspade
    All of the maps and lists are being deleted properly. Technically, if you really, really wanted to check, here's a silly thing you can do:

    Code:
    if(keyboard_check(vk_control)){
         var m = ds_map_create();
         show_debug_message(m);
         ds_map_destroy(m);
    }
    Really silly, but... basically, the data structure system starts at zero, counts up as new structures are created, and recycles numbers as structures are destroyed. So you do something like this:

    Load a level, check the id number of a new map (via the code above).
    Exit that level.
    Come back to the same level and check again.

    In theory you've got a pretty darned similar number (or, hopefully, same number).
    That would indicate that both levels you just loaded/exited successfully cleaned up all maps.

    But that's the fun beauty of doing ds_map_add_list() and ds_map_add_list(). By indicating that the value being added to the map is a data structure, you ensure that GMS destroys it whenever the parent structure is destroyed. As long as you destroy any existing save map before loading a new one to replace it, you'll definitely clean up the structure.

    Direct access is another fun perk, and I use it extensively in my dialog system too. Once you've got the ID of a list or map inside of a nested structure, you can read and write to that spot as you please, and the overall structure is preserved and up to date.
     

Share This Page

  1. This site uses cookies to help personalise content, tailor your experience and to keep you logged in if you register.
    By continuing to use this site, you are consenting to our use of cookies.
    Dismiss Notice