Announcement

Collapse
No announcement yet.

[Video] Instant Save System for 3D Game State, Even After Complete Computer Restart

Collapse
X
 
  • Filter
  • Time
  • Show
Clear All
new posts

    [Video] Instant Save System for 3D Game State, Even After Complete Computer Restart

    Dear Community,

    In this tutorial I show you the basic structure that I've used to be able to:

    1. save dynamically created levels to hard disk

    2. save in-game playing state of the entire 3D game world (including flying projectiles and moving platforms), and save/reload as often as wanted

    3. be able to even turn the computer off, restart it, and then load the user-created level, and then load the in-game save state from a previous gaming session within that level before computer was turned off.



    All of this I do with just two Unrealscript functions at the core of it all:

    Code:
    class'Engine'.static.BasicLoadObject(loadedFile, filename, true, 1);
    class'Engine'.static.BasicSaveObject(saveFile, filename, true, 1);
    ~~~

    How to Save Dynamically Generated Levels

    The biggest probem I encountered when trying to make the save system for my game was that my levels are dynamically generated by the user, and I can never know how many objects the user is creating. I make new levels all the time using my game engine, and add to them spontaneously, and using my current save system, I can permanently save my dynamically generated / updated levels. I show this in the video where I add a moving platform to a level from a pervious video of mine.

    So there are two main problems I had to tackle.

    1. I never know how many objects the user will create for a levels, so how I can I generate a save file for unknown amount of data?

    2. BasicSaveObject cannot store dynamically generated objects, only simple variable types and structures.


    Here's what a sample save file for one of my levels might look like:

    Code:
    class saveFile extends object;
    
    var bool levelUsesSun;
    var wallDataStruct walls[1001];
    var wallDataStruct walls2[1001];
    var wallDataStruct walls3[1001];
    var 3DScriptingStructData scripts[200];
    Please note, your actual save file should be a CLASS, all by itself, composed ONLY of variable declarations.

    Then write two other classes, one to load data and one to save data for that simple save file of just variable declarations.

    Notice I am storing a huge amount of custom struct variable space.

    My save file for my actual game engine is storing space for over 15,000 custom struct variables.

    But this is not a problem, because it is just a big storage bank.

    Not all of the variables need to get accessed by my game engine.

    And the save and load classes will ONLY access the data needed, so saving 5 walls vs. 3000 IS much faster, because the save engine I wrote knows to stop after 5 walls are done and not check all 3000 variables.

    So the actual save file of variables can be huge, but then you write separate classes for save and load engines to make things efficient.

    Note I am only setting aside data for 3003 walls, which are the dynamically generated user-created objects in-game

    Code:
    var wallDataStruct walls[1001];
    var wallDataStruct walls2[1001];
    var wallDataStruct walls3[1001];
    So there is a limit in this way for my way of doing saving, because the user cannot create more than 3000 walls for example, or currently more than 200 3dscripting triggers.

    But for my game that is a reasonable limit.

    You could easily increase the number to 6000 or 18000 walls depending on your game's needs, but I am just letting you know:

    It's okay to have a huuuge amount of data available to be saved to in your game's save file.

    As you saw in my video, saving and loading can be VERY fast if you do the code well.

    So how big are the save files for my game engine?

    ~~~

    My Current Save File Size on Disk

    Well!

    Take a look at this!

    The first time I was testing my save engine and saw how big my save files were, even after 15,000 custom struct variables, I was SHOCKED.

    This is the file size on my hard drive for the the first level you see me play in the video.

    Please understand, this is a fixed file save size, it would not grow if I added more complexity to the level, because I am ALREADY setting aside the space for 3000 walls and 200 3dscripting triggers etc.

    File size for level “d5” from the video:


    So the entire level you see me load in the game engine, and the save games you saw me make of the ball shooting various parts of the wall structure to pieces, all of that entire data in 3D engine is stored in mere KBs! Less than 1 mb ! per level with all data!

    In other words, the file size for my entire dynamically generated levels, for 1 level, is TINY.

    Insignificant file size these days.

    Also note, that the level was originally created on the 11th, around the time of that earlier tutorial, but now it has been updated with today's date. You really can use unreal engine to store level and save data over weeks or years.

    And so the method I am showing you in this tutorial is very viable, for the following

    1. speed of loading and saving while in game
    2. quantity of data stored (can be easily increase)
    3. saving of dynamically generated content including playthrough saves
    4. file size is tiny



    So once I verified all this in my testing I went full speed ahead with my save system, the basic of which I am showing you in this tutorial

    ~~~

    How to Save Dynamically Generated Objects

    Since BasicSaveObject cannot save dynamically generated objects, you must save all the critical data as a Struct composed only of basic variable types

    BasicSaveObject CAN save each of these variables:

    Code:
    var bool aBool;
    var vector v;
    var rotator r;
    var string s;
    
    struct wallData{
      var vector location;
      var rotator rotation;
      var string curMaterial;
      var string curMesh;
    };
    
    var wallData wall1Data;
    BasicSaveObject CANNOT save these types

    Code:
    var myObjClass newobj;
    
    newobj = spawn(class'myObjClass'...);
    
    savefile.obj1 = newObj;
    The above does not work, you cannot save dynamically generated class data like that, only simple variables.

    ~~~

    Saving Material and Mesh Data as Strings

    Code:
    struct wallData{
      var vector location;
      var rotator rotation;
      var string curMaterial;
      var string curMesh;
    };
    In the wallData struct, notice I save the material and mesh as strings, and then you can use DynamicLoadObject to rebuild the level wall's entire state including user-specified mesh and material.

    Code:
    RecreatedWall.StaticMeshComponent.SetMaterial(0,
    Material(DynamicLoadObject(
      thisIsaStringPointingtoMaterialLocation, class'Material')));
    To GET the file path / location of a user-specified material or mesh as a string you can use something like:

    Code:
    local string materialPath;
    materialPath = PathName(preSaveWall.StaticMeshComponent.GetMaterial(0));
    PathName yields a string (thank you for telling me this Spoof), and since a string is a simple variable type, you can easily save that in your savefile for your game.

    For meshes you can use:

    Code:
    local string meshPath;
    meshPath = PathName(StaticMeshActor(theActor).StaticMeshComponent.staticmesh);
    ~~~

    So Why Use Structs?

    So let's suppose you create a really complicated object where you need to store 17 different types of data PER object.

    Code:
    Struct complicatedStructData {
    
    //15 vectors
    var vector v1;
    …
    var vector v15;
    
    var bool isOn;
    var bool IsExcited;
    };
    
    var complicatedStructData data;
    In your save file, remember, the critical fact is:


    BasicSaveObject CAN save Structs of infinite simple variable types


    so you can store alll your required data in a single struct

    and to copy the data from your save file into your dynamically generated in-game object, when you are re-creating the object after loading, it as simple as

    Code:
    recreatedObject.data = loadfile.data1;
    you've just compied all the values of all 17 variables.

    And note that vectors are structs by themselves, so you are actually copying 15 x 3 + 2 = 47 simple variable types of information, 45 floats and 2 bools.

    And all of that can be copied with 1 line:

    Code:
    recreatedObject.data = loadfile.data1;
    where data is your custom made Struct.


    So my secret key to storing vasts amounts of data to recreate a 3D game world with instant saving and loading is this awesome fact:

    UnrealEngine's BasicSaveObject can store custom-made structs of composed of infinite simple variable types.

    ~~~

    So How Does This All Come Together?

    So here's what you need to be able to do:

    1. Store all required data about a 3D dynamically generated object into a single struct.
    2. set aside space in your save file for a sufficient number of structs of this custom struct type.
    3. Write a function to recreate your dynamically generated object from the struct after accessing the saved file data.



    So for each of my classes of objects in my game engine I use this format:

    Code:
    class 3DObjectType1 extends Actor;
    
    struct 3DObjectType1Data {
      var vector location;
      var rotator rotation;
      var rotator startingRotation;
      var vector starting Location;
      var string meshPath;
      var string materialPath;
      var float customData1;
      var bool startsOn;
      //….
      //etc, only simple variable typles
    };
    
    var 3DObjectType1Data data;
    
    //called just prior to saving
    //to make sure all data is accurate
    //to 3D world
    updateStruct(){
      data.location = self.Location;
      data.rotation = self.Rotation;
      data.customData1 = whatever happened ingame prior to saving;
    }
    
    other functions(){}
    
    defaultproperties{};

    So notice, each of my classes has a custom struct of ONLY simple variable types that stores all required data.

    And each class has a single variable of that struct type, the actual instance of the struct definition.

    And each class has an updateStruct() function to ensure all data is up to date when the game is being saved.

    ~~~

    Turning a Custom Struct of simple Variables Back Into an Object

    So once you can save all important data of a custom object class as a series of simple variable types like vectors and floats in a custom Struct for just that class.

    Now in the main game engine, your game engine needs to know how to turn a struct of that special type back into a dynamic object after loading.

    Code:
    Function createObjFromStruct(3DObjectType1Data loadedData){
      local newObj 3DObjectType1;
    
      newObj = Spawn(class'3DObjectType1',,,loadedData.Location, loadedData.Rotation, ,true );
    
      //CRITICAL LINE and so simple
      newObj.data = loadedData;
    
      //set material
    newObj.StaticMeshComponent.SetMaterial(  0, Material(DynamicLoadObject(
      loadedData.materialPath, class'Material')));
    
      //set mesh
    newObj.StaticMeshComponent.SetStaticMes  h(
      StaticMesh(DynamicLoadObject(
          loadedData.meshPath, class'StaticMesh')));
    
      //do any other important things
      //with struct data
      //during object re-creation process
    }
    Notice the critical line:

    Code:
    newObj.data = loadedData;
    Using a custom struct of any number of simple variable types, with the one line above, you just saved yourself so much work and utlized the power of Unreal' BasicSaveObject. You could have had 300 vectors and rotators in that struct.

    But the command

    Code:
    struct1 = struct2
    copies all the data from struct2 to struct1 as long as each member of both structs is a simple variable type or another struct, like a vector, and of course both structs have the same type.

    So this is complete and total data copying.

    Combine this with fact that BasicSaveObject can save custom structs of any length, and you can see how I was able to write a save/load engine for completely dynamically generated levels with custom object types.

    ~~~

    How to Use BasicSave and BasicLoad Object

    So now that you understand how to

    1. store all required data as simple variable types in a single struct
    2. update that struct prior to saving
    3. recreate an object from saved struct data


    Now we can talk about actually saving and loading files.

    You need 3 classes

    ~~~

    First Class: The save file

    This should only contain the variable declarations, nothing else, and a sufficient number of variables for each type of object in your level, as per my example earlier.

    So using the code in this tutorial:

    Let's say you need to store a maximum of 3000 objects of this class:

    Code:
    class 3DObjectType1 extends Actor;
    
    struct 3DObjectType1Data {
      var vector location;
      var rotator rotation;
      var rotator startingRotation;
      var vector starting Location;
      var string meshPath;
      var string materialPath;
      var float customData1;
      var bool startsOn;
      //….
      //etc, only simple variable typles
    };
    
    var 3DObjectType1Data data;
    In your save file you would only have the following:

    Code:
    class saveFile extends object;
    
    //very important
    var int totalObjectType1s;
    
    var 3DObjectType1Data objs[1001];
    var 3DObjectType1Data objs2[1001];
    var 3DObjectType1Data objs3[1001];

    Notice, you are not storing the class, you are storing the STRUCT type (3DObjectType1Data), which is composed only of other simple variable type structs (like vector) or simple variables.

    The reason I did it this way is because BasicSaveObject can only store data as complicated as this format that I am describing.

    But for you the programmer it is easy! 1 custom struct per dynamic object that you want to save

    ~~~

    Class 2, the SaveEngine

    Now you need to write a save engine, to actually make an instance of your savefile type and copy the required data

    Code:
    class saveEngine extends Object;
    
    var saveFile saveFileObj;
    
    //make an instance of all the variable data
    //only some of which will be used
    function init(){
    saveFileObj = new class'saveFile';
    }
    
    function saveObjectType1(){
    local int v;
    
    saveFileObj.totalObjectType1s = levelArrayObjectType1.length;
    
      //Walls	
      if(levelArrayObjectType1.length> 3000){
    	`log("too many walls, > 3000"); return;	
       }
    	
    for(v=0; v < levelArrayObjectType1.length; v++ ){
    	if(v >= 1000) break;
    	levelArrayObjectType1[v].updateStruct();
    	saveFileObj.objs[v] = levelArrayObjectType1[v].data;	
    }
    
    if (saveCount >= 1000){
    for(v=0; v < levelArrayObjectType1.length; v++ ){
    	if(v >= 1000) break;
    	levelArrayObjectType1[v].updateStruct();
    	saveFileObj.objs2[v] =levelArrayObjectType1[v].data;	
    }
    }
    if (saveCount >= 2000){
    for(v=0; v < levelArrayObjectType1.length; v++ ){
    	if(v >= 1000) break;
    	levelArrayObjectType1[v].updateStruct();
    	saveFileObj.objs3[v] = levelArrayObjectType1[v].data;	
    }
    }
      }
    
    
    function finalSave(string filename){
    //save file at very end of all loops
    //just before finishing
    //make sure to saaaave
    class'Engine'.static.BasicSaveObject(saveFile, filename, true, 1);
     }
    levelArrayObjectType1 is a dynamic array that has stored all the objects created dynamically during game time.

    Dynamic lists cannot be saved so we must split the dynamic array of expected max size 3000 into 3 arrays of size 1001, because the max static array size is 1024.

    The extra 1 in 1001 should never get used, it's just a precaution on my part to avoid crashes.
    ~~~

    Class 3, Loading

    For loading you do something similar to saving, but you are calling that function createObjFromStruct and passing in the structs that were saved from

    Code:
    class LoadEngine extends Object;
    
    var saveFile loadFileObj;
    
    function init(string filename){
      loadFileObj = new class'saveFile';
      if (!class'Engine'.static.BasicLoadObject(loadedFile, filename, true, 1)){
        `log(“LoadEngine:>> Invalid Filename”);
      }
    }
    
    function loadObjectType1s(){
      for(v = 0;v < loadFileObj.totalObjectType1s; v++){
    	if(v >= 1000) break;
    	createObjFromStruct(loadFileObj.objs[v]);
      }
      if(loadFileObj.totalObjectType1s >= 1000){
      for(v = 0;v < loadFileObj.totalObjectType1s; v++){
    	if(v >= 1000) break;
    	createObjFromStruct(loadFileObj.objs2[v]);
      }
      }
      if(loadFileObj.totalObjectType1s >= 2000){
      for(v = 0;v < loadFileObj.totalObjectType1s; v++){
    	if(v >= 1000) break;
    	createObjFromStruct(loadFileObj.objs3[v]);
      }
      }
    }
    ~~~

    Instancing Save and Load Engine

    When your game engine first starts from the command prompt, you need only make a single instance of your save and load engine to then call their functions.

    making them separate objects is mainly an organizational feature, to avoid enormous confusions between save and load functions and when to save and load the object etc etc

    Code:
    //after your game .exe runs
    //maybe in playercontroller class 
    
    var LoadEngine LoadEngineOBJ;
    var SaveEngine SaveEngineOBJ;
    
    //maybe your dynamic array is stored
    //in your controller class
    //adjust save/load engine code accordingly
    var array<3DObjectType1> levelArrayObjectType1;
    
    Simulated Event PostBeginPlay() {
    
      //no forget this one plz	
      super.postbeginplay();
    
      LoadEngineOBJ = new class'LoadEngine';
      SaveEngineOBJ = new class'SaveEngine';
    }
    
    
    //and call your engines like this:
    
    load(string s){
      LoadEngineOBJ.init(s);
      LoadengineOBJ.loadObjectType1s();
    }
    
    save(string s){
      SaveEngineOBJ.init();
      SaveEngineOBJ.saveObjectType1();
      //other save functions   
    
    
       //please remember this line
       //and keep it as always LAST
      SaveEngineOBJ.FinalSave(s);
    
    }
    ~~~

    Summary

    In this tutorial I”ve covered all the core concepts of how I got my in-game save feature as well as my level saving feature working using only Basic Save Object

    Core concepts:

    1. Unreal BasicSave/Load Object can only store simple variable types
    2. You can make structs of an infinite number of simple variable types for simple copying of data.
    3. Write a function to transform simple variable data back into your dynamic object
    4. Make a save file storing only your custom struct variable types.
    5. Write save/load engines to utlize the save file and save dynamic objects to simple data or convert simple data back into dynamic objects


    That's the basics of how I created my turn-the-computer-off-for-a-year and the restart it had load a level you made a long time ago and even load an in-game save that occurred during game-time!



    Rama

    #2
    Bumping as new tutorial and community resource

    Rama

    Comment


      #3
      probably not ever gonna use this myself, but I must say good job and thanks for sharing!

      Comment


        #4
        Thanks Chosker!



        Rama

        Comment


          #5
          Looks awesome rama, good job.

          I would love to use this system for ghostship if i can clear up a few things.

          1. The save saves instantly, i need it to save the map name the player is on but i also need to save players inventory (ammo,weapons,health,shields) and number of kills. Can it do this?

          2.Can the save load be used in kismet to connect to my scaleform menu?

          3.By the looks of it, i think every custom class i have created (which are quite a few upto now lol) has to be updated with some extra code, most of my level is built with static meshes not bsp is this ok for the system?

          Cheers Rama

          Comment


            #6
            Originally posted by madaboutagmes View Post
            Looks awesome rama, good job.

            I would love to use this system for ghostship if i can clear up a few things.

            1. The save saves instantly, i need it to save the map name the player is on but i also need to save players inventory (ammo,weapons,health,shields) and number of kills. Can it do this?

            2.Can the save load be used in kismet to connect to my scaleform menu?

            3.By the looks of it, i think every custom class i have created (which are quite a few upto now lol) has to be updated with some extra code, most of my level is built with static meshes not bsp is this ok for the system?

            Cheers Rama

            1. yes this is not hard, just obtain the player stats and store them as floats, then re-apply the player stats to the player upon loading

            2. You can call your save/load functions from kismet by making custom kismet nodes that simply call the functions, and you could even have a string input if you want to be able to choose the savefilename from kismet. My custom kismet node tutorial

            3. BSP would not work with my system, but your levels are constant geometry right? Do your levels have moving parts or is it static scenery? If you want to clear a level and load in other level geometry you need to make a class where your objects can be deleted, that would be a bit of work if your levels are already done.

            I'd stay start with saving/loading the player state to get a sense of things, that will be pretty fast to implement.

            If that goes well, and you DO need to clear/remake levels or save the state of moving doors/bridges that will require a lot more work.


            so start small, loading/reloading just the player stats and the player position, and see how it goes

            Rama

            Comment


              #7
              Amazing stuff ! i can learn 2 things or 3 here ! prbbly going to use it .

              Comment


                #8
                Yay! Glad you like!



                Allegria!

                Rama

                Comment


                  #9
                  No alegria without postProcessChain working : / .

                  Comment


                    #10
                    did you see my questions about that?

                    I can help you better if I know what your goal is, check 2nd page of your thread

                    Rama

                    Comment


                      #11
                      Originally posted by evernewjoy View Post
                      1. yes this is not hard, just obtain the player stats and store them as floats, then re-apply the player stats to the player upon loading

                      2. You can call your save/load functions from kismet by making custom kismet nodes that simply call the functions, and you could even have a string input if you want to be able to choose the savefilename from kismet. My custom kismet node tutorial

                      3. BSP would not work with my system, but your levels are constant geometry right? Do your levels have moving parts or is it static scenery? If you want to clear a level and load in other level geometry you need to make a class where your objects can be deleted, that would be a bit of work if your levels are already done.

                      I'd stay start with saving/loading the player state to get a sense of things, that will be pretty fast to implement.

                      If that goes well, and you DO need to clear/remake levels or save the state of moving doors/bridges that will require a lot more work.


                      so start small, loading/reloading just the player stats and the player position, and see how it goes

                      Rama
                      Yes ill be starting small and working it up, thats been my approach with everything in Ghostship I have not been to sure about. Saving the players data is for first and most important step, ill check out your kismet tutorial and let you know how i get on Cheers

                      Comment


                        #12
                        for your project, just have a reference to the class that has your save/load functions in your gameinfo class.

                        then you can use the code I have for the MeshToAdd node (in my kisme ttutorial), and just eliminate the input cause you are just running one of your functions.

                        and you have to cast the gameinfo var to your custom gameinfo class to access the reference



                        Rama

                        Comment


                          #13
                          Sorry forgot to mention about my levels, yes i have 10 maps done on the game now (each one being a deck of the ships or a large area such as Hangars, main access corridors etc. As the player finishes a map another map will load in, not dynamically but relevent to to where the player is on the ship. So each map is a seperate level and when saved the player will start from the mapname he or she was on. There are a few doors and other envirnmental actors that will need to be saved, such as if the player gets a keycard to enter the small arms vault in the armoury then after the player has opened the door with the card : each time the player revisits the map the door would be open. So saving these states would be vital by the time i get to the beta stage, I just need to concentrate on saving the players inventry and such and map for now.

                          Comment


                            #14
                            saving the state of a few doors will be eaaaasy

                            you can just have that info for each map in your save file

                            Code:
                            class saveFile extends object;
                            
                            var bool level1Door1isOpen;
                            var bool level1Door2isOpen;
                            var bool level3Door1isOpen;
                            and then

                            upon loading that level and opening the save file

                            your level can check its appropriate bools

                            and then open the door if the door was open, or close it (if the level default is to have door open)


                            very easy

                            Rama

                            PS: so you store all the bools for all doors and player stats and everything in ONE save file, and during each level load your main player controller class opens the main savefile and looks at the relevant bools for the current level.

                            Comment


                              #15
                              Very interesting.
                              I wonder if the way you save could be used to persist data from one level to another ...
                              Imagine you're at a level that has 3 doors locked and the player opens them, then when you change to a different level and return to your level again, the 3-door should be kept open.
                              It's not like saving specifically, as when you get to a checkpoint and saved the whole game, I mean keep information between levels only.
                              Even when you turn off the PC if you have not saved, then the level is reset to its original state.
                              Any idea about any of this?

                              Comment

                              Working...
                              X