UDN
Search public documentation:
DevelopmentKitGemsSaveGameStates
日本語訳
中国翻译
한국어
Interested in the Unreal Engine?
Visit the Unreal Technology site.
Looking for jobs and company info?
Check out the Epic games site.
Questions about support via UDN?
Contact the UDN Staff
中国翻译
한국어
Interested in the Unreal Engine?
Visit the Unreal Technology site.
Looking for jobs and company info?
Check out the Epic games site.
Questions about support via UDN?
Contact the UDN Staff
UE3 Home > Unreal Development Kit Gems > Save Game States
UE3 Home > Input / Output > Save Game States
UE3 Home > Input / Output > Save Game States
Save Game States
Last tested against UDK March, 2012 PC compatible
Mac compatible
iOS compatible
- Save Game States
- Overview
- Developing with saved game states in mind
- General flow of how saved game states work
- Player saves the game after playing a level for a while
- Serialize the level name
- Serialize all actors that implement the SaveGameStateInterface as JSon
- Serialize Kismet and Matinee as JSon
- Save the data using BasicSaveObject
- Player loads a game from a saved game state
- Load the saved game state object
- Load the map appending a command line to store saved game state file name
- When the map has finished loading, reload the saved game state object
- Iterate over the JSon data and deserialize the data on the actors and objects within the level
- KActor example
- Player controlled pawn example
- Game State Loaded Kismet Event
- Questions
- Related Topics
- Downloads
Overview
Unreal Engine 1 and Unreal Engine 2 supported save games by simply saving the entire level from memory to disk. While this method worked well in production, unfortunately any changes content developers made to the levels later on would not reflect over. To solve this problem, another way to represent saved games is to store just enough data, that the saved game is able to be restored by first loading the level and then applying saved game state data to all of the actors and objects in the level.
Developing with saved game states in mind
Due to the way saved game states work, you have to be very careful about what you destroy within the world. Once an actor is destroyed, it can no longer be picked up by the saved game state serializer because the actor is now gone. If the actor is transient, then it is generally not a problem. However, if the actor is level designer placed then when the level is reloaded and the saved game state data is applied, the level designer placed actor will not be affected as there is no data for it!
General flow of how saved game states work
- Player saves the game after playing a level for a while.
- Player loads a game from a saved game state.
Player saves the game after playing a level for a while
For example purposes, only a console command has been added to this development kit gem. Obviously, your game will have a graphical user interface attached to it, however you can always call the same console command with the file name parameter anyways. Or the console command function could be made static as it is not dependent on executing within a particular instance of an actor or object (It calls PlayerController::ClientMessage(), but you can always get the local player controller by using Actor::GetALocalPlayerController()). When the console command is executed, it kick starts the save game state process. First the SaveGameState object is instanced. The SaveGameState object handles iterating and serializing Actors, Kismet and Matinee. We then "scrub" the file name. Scrubbing the file name just ensures that there are no illegal characters added, although in this case only spaces were checked for. For a more robust scrubbing, you may want to consider ensuring that characters such as \, /, ?, ! are not in the file name. The scrubbing function also ensure that the file extension "sav" is also added if it hasn't been already. The SaveGameState is then asked to iterate and serialize Actors, Kismet and Matinee. Finally, if the SaveGameState was successfully saved to disk by BasicSaveObject(), then a message is sent to the player stating that the game was saved./** * This exec function will save the game state to the file name provided. * * @param FileName File name to save the SaveGameState to */ exec function SaveGameState(string FileName) { local SaveGameState SaveGameState; // Instance the save game state SaveGameState = new () class'SaveGameState'; if (SaveGameState == None) { return; } // Scrub the file name FileName = ScrubFileName(FileName); // Ask the save game state to save the game SaveGameState.SaveGameState(); // Serialize the save game state object onto disk if (class'Engine'.static.BasicSaveObject(SaveGameState, FileName, true, class'SaveGameState'.const.SAVEGAMESTATE_REVISION)) { // If successful then send a message ClientMessage("Saved game state to "$FileName$".", 'System'); } }
Serialize the level name
The saved game state serializes the level name (or map file name) so that the saved game state knows which map it is to load when it, itself is loaded. Rather than storing this in another file such as the configuration file, it makes more sense to simply store it within the saved game state. The saved game state only needs to set the variables it wants saved, as BasicSaveObject() will perform the actual saving to disk for you. If any streaming levels are visible or have a load request pending, then they are saved into an array so that when the save game state is reloaded the streaming levels will be loaded straight away. This step also saves the current GameInfo class./** * Saves the game state by serializing all of the actors that implement the SaveGameStateInterface, Kismet and Matinee. */ function SaveGameState() { local WorldInfo WorldInfo; // Get the world info, abort if the world info could not be found WorldInfo = class'WorldInfo'.static.GetWorldInfo(); if (WorldInfo == None) { return; } // Save the map file name PersistentMapFileName= String(WorldInfo.GetPackageName()); // Save the currently streamed in map file names if (WorldInfo.StreamingLevels.Length > 0) { // Iterate through the streaming levels for (i = 0; i < WorldInfo.StreamingLevels.Length; ++i) { // Levels that are visible and has a load request pending should be included in the streaming levels list if (WorldInfo.StreamingLevels[i] != None && (WorldInfo.StreamingLevels[i].bIsVisible || WorldInfo.StreamingLevels[i].bHasLoadRequestPending)) { StreamingMapFileNames.AddItem(String(WorldInfo.StreamingLevels[i].PackageName)); } } } // Save the game info class GameInfoClassName = PathName(WorldInfo.Game.Class); }
Serialize all actors that implement the SaveGameStateInterface as JSon
Only dynamic actors need to be serialized, so the iterator of choice here was DynamicActors. A filter for SaveGameStateInterface was also added, as that allows you to decide which dynamic actors need to be serialized and which do not. An interface is used here as it is easier to extend the save game state later on, since it is the actor which will serialize and deserialize the JSon data later on. When the Actor implementing SaveGameStateInterface is asked to serialize itself, it returns the encoded JSon string. The string is added the SerializedActorData array, which is then saved by BasicSaveObject()./** * Saves the game state by serializing all of the actors that implement the SaveGameStateInterface, Kismet and Matinee. */ function SaveGameState() { local WorldInfo WorldInfo; local Actor Actor; local String SerializedActorData; local SaveGameStateInterface SaveGameStateInterface; local int i; // Get the world info, abort if the world info could not be found WorldInfo = class'WorldInfo'.static.GetWorldInfo(); if (WorldInfo == None) { return; } // Save the persistent map file name PersistentMapFileName = String(WorldInfo.GetPackageName()); // Save the currently streamed in map file names if (WorldInfo.StreamingLevels.Length > 0) { // Iterate through the streaming levels for (i = 0; i < WorldInfo.StreamingLevels.Length; ++i) { // Levels that are visible and has a load request pending should be included in the streaming levels list if (WorldInfo.StreamingLevels[i] != None && (WorldInfo.StreamingLevels[i].bIsVisible || WorldInfo.StreamingLevels[i].bHasLoadRequestPending)) { StreamingMapFileNames.AddItem(String(WorldInfo.StreamingLevels[i].PackageName)); } } } // Save the game info class GameInfoClassName = PathName(WorldInfo.Game.Class); // Iterate through all of the actors that implement SaveGameStateInterface and ask them to serialize themselves ForEach WorldInfo.DynamicActors(class'Actor', Actor, class'SaveGameStateInterface') { // Type cast to the SaveGameStateInterface SaveGameStateInterface = SaveGameStateInterface(Actor); if (SaveGameStateInterface != None) { // Serialize the actor SerializedActorData = SaveGameStateInterface.Serialize(); // If the serialzed actor data is valid, then add it to the serialized world data array if (SerializedActorData != "") { SerializedWorldData.AddItem(SerializedActorData); } } } }
interface SaveGameStateInterface; /** * Serializes the actor's data into JSon * * @return JSon data representing the state of this actor */ function String Serialize(); /** * Deserializes the actor from the data given * * @param Data JSon data representing the differential state of this actor */ function Deserialize(JSonObject Data);
Serialize Kismet and Matinee as JSon
The save game state is also able to serialize Kismet Events and Kismet Variables. This allows game designers to implement a portion of the game using Kismet. This is done by iterating though the level's Kismet Events and Kismet variables and serializing each one. Kismet Events have their ActivationTime calculated as offsets. When the saved game state is reloaded, the WorldInfo.TimeSeconds is usually at zero or a very small number. This is unlikely to be the time when the game was saved previously. ActivationTime is mostly important if the Kismet Event has set its ReTriggerDelay variable. Thus to prevent the bug where a Kismet Event is retriggered too quickly by saving and loading, it is required to calculate the time remaining from ActivationTime with ReTriggerDelay in consideration. This way, when the Kismet Event is reloaded the ActivationTime is usually set in the future, if it had been triggered. The other value that is saved is the TriggerCount. This is usually required for triggers that have their MaxTriggerCount values set to something other than zero. Kismet Variables are detected using a typecasting trial and error method. Another option would have been to iterate over the Kismet Sequence Objects looking for each type of Kismet Variable. Either approach is fine. Once a Kismet Variable has been detected, its value is then serialized./** * Saves the Kismet game state */ protected function SaveKismetState() { local WorldInfo WorldInfo; local array<Sequence> RootSequences; local array<SequenceObject> SequenceObjects; local SequenceEvent SequenceEvent; local SeqVar_Bool SeqVar_Bool; local SeqVar_Float SeqVar_Float; local SeqVar_Int SeqVar_Int; local SeqVar_Object SeqVar_Object; local SeqVar_String SeqVar_String; local SeqVar_Vector SeqVar_Vector; local int i, j; local JSonObject JSonObject; // Get the world info, abort if it does not exist WorldInfo = class'WorldInfo'.static.GetWorldInfo(); if (WorldInfo == None) { return; } // Get all of the root sequences within the world, abort if there are no root sequences RootSequences = WorldInfo.GetAllRootSequences(); if (RootSequences.Length <= 0) { return; } // Serialize all SequenceEvents and SequenceVariables for (i = 0; i < RootSequences.Length; ++i) { if (RootSequences[i] != None) { // Serialize Kismet Events RootSequences[i].FindSeqObjectsByClass(class'SequenceEvent', true, SequenceObjects); if (SequenceObjects.Length > 0) { for (j = 0; j < SequenceObjects.Length; ++j) { SequenceEvent = SequenceEvent(SequenceObjects[j]); if (SequenceEvent != None) { JSonObject = new () class'JSonObject'; if (JSonObject != None) { // Save the path name of the SequenceEvent so it can found later JSonObject.SetStringValue("Name", PathName(SequenceEvent)); // Calculate the activation time of what it should be when the saved game state is loaded. This is done as the retrigger delay minus the difference between the current world time // and the last activation time. If the result is negative, then it means this was never triggered before, so always make sure it is larger or equal to zero. JsonObject.SetFloatValue("ActivationTime", FMax(SequenceEvent.ReTriggerDelay - (WorldInfo.TimeSeconds - SequenceEvent.ActivationTime), 0.f)); // Save the current trigger count JSonObject.SetIntValue("TriggerCount", SequenceEvent.TriggerCount); // Encode this and append it to the save game data array SerializedWorldData.AddItem(class'JSonObject'.static.EncodeJson(JSonObject)); } } } } // Serialize Kismet Variables RootSequences[i].FindSeqObjectsByClass(class'SequenceVariable', true, SequenceObjects); if (SequenceObjects.Length > 0) { for (j = 0; j < SequenceObjects.Length; ++j) { // Attempt to serialize as a boolean variable SeqVar_Bool = SeqVar_Bool(SequenceObjects[j]); if (SeqVar_Bool != None) { JSonObject = new () class'JSonObject'; if (JSonObject != None) { // Save the path name of the SeqVar_Bool so it can found later JSonObject.SetStringValue("Name", PathName(SeqVar_Bool)); // Save the boolean value JSonObject.SetIntValue("Value", SeqVar_Bool.bValue); // Encode this and append it to the save game data array SerializedWorldData.AddItem(class'JSonObject'.static.EncodeJson(JSonObject)); } // Continue to the next one within the array as we're done with this array index continue; } // Attempt to serialize as a float variable SeqVar_Float = SeqVar_Float(SequenceObjects[j]); if (SeqVar_Float != None) { JSonObject = new () class'JSonObject'; if (JSonObject != None) { // Save the path name of the SeqVar_Float so it can found later JSonObject.SetStringValue("Name", PathName(SeqVar_Float)); // Save the float value JSonObject.SetFloatValue("Value", SeqVar_Float.FloatValue); // Encode this and append it to the save game data array SerializedWorldData.AddItem(class'JSonObject'.static.EncodeJson(JSonObject)); } // Continue to the next one within the array as we're done with this array index continue; } // Attempt to serialize as an int variable SeqVar_Int = SeqVar_Int(SequenceObjects[j]); if (SeqVar_Int != None) { JSonObject = new () class'JSonObject'; if (JSonObject != None) { // Save the path name of the SeqVar_Int so it can found later JSonObject.SetStringValue("Name", PathName(SeqVar_Int)); // Save the int value JSonObject.SetIntValue("Value", SeqVar_Int.IntValue); // Encode this and append it to the save game data array SerializedWorldData.AddItem(class'JSonObject'.static.EncodeJson(JSonObject)); } // Continue to the next one within the array as we're done with this array index continue; } // Attempt to serialize as an object variable SeqVar_Object = SeqVar_Object(SequenceObjects[j]); if (SeqVar_Object != None) { JSonObject = new () class'JSonObject'; if (JSonObject != None) { // Save the path name of the SeqVar_Object so it can found later JSonObject.SetStringValue("Name", PathName(SeqVar_Object)); // Save the object value JSonObject.SetStringValue("Value", PathName(SeqVar_Object.GetObjectValue())); // Encode this and append it to the save game data array SerializedWorldData.AddItem(class'JSonObject'.static.EncodeJson(JSonObject)); } // Continue to the next one within the array as we're done with this array index continue; } // Attempt to serialize as a string variable SeqVar_String = SeqVar_String(SequenceObjects[j]); if (SeqVar_String != None) { JSonObject = new () class'JSonObject'; if (JSonObject != None) { // Save the path name of the SeqVar_String so it can found later JSonObject.SetStringValue("Name", PathName(SeqVar_String)); // Save the string value JSonObject.SetStringValue("Value", SeqVar_String.StrValue); // Encode this and append it to the save game data array SerializedWorldData.AddItem(class'JSonObject'.static.EncodeJson(JSonObject)); } // Continue to the next one within the array as we're done with this array index continue; } // Attempt to serialize as a vector variable SeqVar_Vector = SeqVar_Vector(SequenceObjects[j]); if (SeqVar_Vector != None) { JSonObject = new () class'JSonObject'; if (JSonObject != None) { // Save the path name of the SeqVar_Vector so it can found later JSonObject.SetStringValue("Name", PathName(SeqVar_Vector)); // Save the vector value JSonObject.SetFloatValue("Value_X", SeqVar_Vector.VectValue.X); JSonObject.SetFloatValue("Value_Y", SeqVar_Vector.VectValue.Y); JSonObject.SetFloatValue("Value_Z", SeqVar_Vector.VectValue.Z); // Encode this and append it to the save game data array SerializedWorldData.AddItem(class'JSonObject'.static.EncodeJson(JSonObject)); } // Continue to the next one within the array as we're done with this array index continue; } } } } } }
/** * Saves the Matinee game state */ protected function SaveMatineeState() { local WorldInfo WorldInfo; local array<Sequence> RootSequences; local array<SequenceObject> SequenceObjects; local SeqAct_Interp SeqAct_Interp; local int i, j; local JSonObject JSonObject; // Get the world info, abort if it does not exist WorldInfo = class'WorldInfo'.static.GetWorldInfo(); if (WorldInfo == None) { return; } // Get all of the root sequences within the world, abort if there are no root sequences RootSequences = WorldInfo.GetAllRootSequences(); if (RootSequences.Length <= 0) { return; } // Serialize all SequenceEvents and SequenceVariables for (i = 0; i < RootSequences.Length; ++i) { if (RootSequences[i] != None) { // Serialize Matinee Kismet Sequence Actions RootSequences[i].FindSeqObjectsByClass(class'SeqAct_Interp', true, SequenceObjects); if (SequenceObjects.Length > 0) { for (j = 0; j < SequenceObjects.Length; ++j) { SeqAct_Interp = SeqAct_Interp(SequenceObjects[j]); if (SeqAct_Interp != None) { // Attempt to serialize the data JSonObject = new () class'JSonObject'; if (JSonObject != None) { // Save the path name of the SeqAct_Interp so it can found later JSonObject.SetStringValue("Name", PathName(SeqAct_Interp)); // Save the current position of the SeqAct_Interp JSonObject.SetFloatValue("Position", SeqAct_Interp.Position); // Save if the SeqAct_Interp is playing or not JSonObject.SetIntValue("IsPlaying", (SeqAct_Interp.bIsPlaying) ? 1 : 0); // Save if the SeqAct_Interp is paused or not JSonObject.SetIntValue("Paused", (SeqAct_Interp.bPaused) ? 1 : 0); // Encode this and append it to the save game data array SerializedWorldData.AddItem(class'JSonObject'.static.EncodeJson(JSonObject)); } } } } } } }
Save the data using BasicSaveObject
As shown earlier, the save game state data is saved by BasicSaveObject(). BasicSaveObject() returns true or false depending if the file was written successfully or not. This allows you to display a message if the saved game was saved successfully or not./** * This exec function will save the game state to the file name provided. * * @param FileName File name to save the SaveGameState to */ exec function SaveGameState(string FileName) { local SaveGameState SaveGameState; // Instance the save game state SaveGameState = new () class'SaveGameState'; if (SaveGameState == None) { return; } // Scrub the file name FileName = ScrubFileName(FileName); // Ask the save game state to save the game SaveGameState.SaveGameState(); // Serialize the save game state object onto disk if (class'Engine'.static.BasicSaveObject(SaveGameState, FileName, true, class'SaveGameState'.const.SAVEGAMESTATE_REVISION)) { // If successful then send a message ClientMessage("Saved game state to "$FileName$".", 'System'); } }
Player loads a game from a saved game state
LoadGameState() is the entry point from where saved game states are loaded. Again, this function may be made a static function as it is not really dependent on any class instances./** * This exec function will load the game state from the file name provided * * @param FileName File name of load the SaveGameState from */ exec function LoadGameState(string FileName);
Load the saved game state object
The saved game state object is first loaded from disk using BasicLoadObject()./** * This exec function will load the game state from the file name provided * * @param FileName File name of load the SaveGameState from */ exec function LoadGameState(string FileName) { local SaveGameState SaveGameState; // Instance the save game state SaveGameState = new () class'SaveGameState'; if (SaveGameState == None) { return; } // Scrub the file name FileName = ScrubFileName(FileName); // Attempt to deserialize the save game state object from disk if (class'Engine'.static.BasicLoadObject(SaveGameState, FileName, true, class'SaveGameState'.const.SAVEGAMESTATE_REVISION)) { } }
Load the map appending a command line to store saved game state file name
If the saved game state object was loaded successfully, then the serialized map is loaded with command line parameters stating that when the map has finished loading, it should continue loading up the saved game state defined. If you decide to make this function a static function, you can call ConsoleCommand() from other global referenceable Actors. Note: The console command 'start' is used here instead of 'open' because 'start' always resets the command line parameters; where as 'open' appends command line parameters. This is very important, otherwise the command line parameter "SaveGameState" will be appended multiple times which will lead to incorrect loading of the save game state!/** * This exec function will load the game state from the file name provided * * @param FileName File name of load the SaveGameState from */ exec function LoadGameState(string FileName) { local SaveGameState SaveGameState; // Instance the save game state SaveGameState = new () class'SaveGameState'; if (SaveGameState == None) { return; } // Scrub the file name FileName = ScrubFileName(FileName); // Attempt to deserialize the save game state object from disk if (class'Engine'.static.BasicLoadObject(SaveGameState, FileName, true, class'SaveGameState'.const.SAVEGAMESTATE_REVISION)) { // Start the map with the command line parameters required to then load the save game state ConsoleCommand("start "$SaveGameState.PersistentMapFileName$"?Game="$SaveGameState.GameInfoClassName$"?SaveGameState="$FileName); } }
When the map has finished loading, reload the saved game state object
When the map has loaded, SaveStateGameInfo::InitGame() picks out whether or not a save game state command line parameter exists or not. If it does then it saves the value within PendingSaveGameFileName. Then when the match is started, the save game state object is loaded from disk again and is asked to load the game state. When the saved game state is loaded, a message is sent to the player to inform him / her that the saved game has loaded. If there are any streaming levels, then SaveStateGameInfo::StartMatch() will ask all player controllers streaming in the other maps. However, because streaming in the other maps will not be finished in the same tick, a looping timer called SaveStateGameInfo::WaitingForStreamingLevelsTimer() is setup to watch for when all streaming levels have finished loading. When the streaming maps have finished loading, then the match is started by calling Super.StartMatch() [UTGame::StartMatch()].class SaveGameStateGameInfo extends UTGame; // Pending save game state file name var private string PendingSaveGameFileName; /* * Initialize the game. The GameInfo's InitGame() function is called before any other scripts (including PreBeginPlay()), and is used by the GameInfo to initialize parameters and spawn its helper classes. * * @param Options Passed options from the command line * @param ErrorMessage Out going error messages */ event InitGame(string Options, out string ErrorMessage) { Super.InitGame(Options, ErrorMessage); // Set the pending save game file name if required if (HasOption(Options, "SaveGameState")) { PendingSaveGameFileName = ParseOption(Options, "SaveGameState"); } else { PendingSaveGameFileName = ""; } } /** * Start the match - inform all actors that the match is starting, and spawn player pawns */ function StartMatch() { local SaveGameState SaveGameState; local PlayerController PlayerController; local int i; // Check if we need to load the game or not if (PendingSaveGameFileName != "") { // Instance the save game state SaveGameState = new () class'SaveGameState'; if (SaveGameState == None) { return; } // Attempt to deserialize the save game state object from disk if (class'Engine'.static.BasicLoadObject(SaveGameState, PendingSaveGameFileName, true, class'SaveGameState'.const.SAVEGAMESTATE_REVISION)) { // Synchrously load in any streaming levels if (SaveGameState.StreamingMapFileNames.Length > 0) { // Ask every player controller to load up the streaming map ForEach WorldInfo.AllControllers(class'PlayerController', PlayerController) { // Stream map files now for (i = 0; i < SaveGameState.StreamingMapFileNames.Length; ++i) { PlayerController.ClientUpdateLevelStreamingStatus(Name(SaveGameState.StreamingMapFileNames[i]), true, true, true); } // Block everything until pending loading is done PlayerController.ClientFlushLevelStreaming(); } // Store the save game state in StreamingSaveGameState StreamingSaveGameState = SaveGameState; // Start the looping timer which waits for all streaming levels to finish loading SetTimer(0.05f, true, NameOf(WaitingForStreamingLevelsTimer)); return; } // Load the game state SaveGameState.LoadGameState(); } // Send a message to all player controllers that we've loaded the save game state ForEach WorldInfo.AllControllers(class'PlayerController', PlayerController) { PlayerController.ClientMessage("Loaded save game state from "$PendingSaveGameFileName$".", 'System'); } } Super.StartMatch(); } function WaitingForStreamingLevelsTimer() { local int i; local PlayerController PlayerController; for (i = 0; i < WorldInfo.StreamingLevels.Length; ++i) { // If any levels still have the load request pending, then return if (WorldInfo.StreamingLevels[i].bHasLoadRequestPending) { return; } } // Clear the looping timer ClearTimer(NameOf(WaitingForStreamingLevelsTimer)); // Load the save game state StreamingSaveGameState.LoadGameState(); // Clear it for garbage collection StreamingSaveGameState = None; // Send a message to all player controllers that we've loaded the save game state ForEach WorldInfo.AllControllers(class'PlayerController', PlayerController) { PlayerController.ClientMessage("Loaded save game state from "$PendingSaveGameFileName$".", 'System'); } // Start the match Super.StartMatch(); }
Iterate over the JSon data and deserialize the data on the actors and objects within the level
Now that the saved game state object has been loaded, it is now possible to iterate over the Actors that implement SaveGameStateInterface, Kismet and Matinee and restore them based on the data stored in SerializedWorldData array (which is now encoded as JSon). As SerializedWorldData is iterated over, each entry is decoded as a JSonObject. Retrieving the Name will provide some insight as to what the JSonObject data is relevant to. Testing for SeqAct_Interp will reveal that the data is relevant for a Matinee Object, SeqEvent or SeqVar for either Kismet Event or a Kismet Variables. If those three fail, then it must be for an Actor in the world. If the JSonObject data is for an Actor in the world, then the actor is retrieved by using FindObject(). As the full path name of the Actor is stored, FindObject() should be able to find any Actor that was placed by the level designer. If FindObject() fails, then it must be for an Actor that was instanced during play. This is why it is often useful to store the ObjectArchetype too, so that it can be reinstanced by the saved game state if required. Once the Actor is found or instanced, the Actor is then casted to SaveGameStateInterface and is then asked to deserialize itself based on the data stored within the JSonObject./** * Loads the game state by deserializing all of the serialized data and applying the data to the actors that implement the SaveGameStateInterface, Kisment and Matinee. */ function LoadGameState() { local WorldInfo WorldInfo; local int i; local JSonObject JSonObject; local String ObjectName; local SaveGameStateInterface SaveGameStateInterface; local Actor Actor, ActorArchetype; // No serialized world data to load! if (SerializedWorldData.Length <= 0) { return; } // Grab the world info, abort if no valid world info WorldInfo = class'WorldInfo'.static.GetWorldInfo(); if (WorldInfo == None) { return; } // For each serialized data object for (i = 0; i < SerializedWorldData.Length; ++i) { if (SerializedWorldData[i] != "") { // Decode the JSonObject from the encoded string JSonObject = class'JSonObject'.static.DecodeJson(SerializedWorldData[i]); if (JSonObject != None) { // Get the object name ObjectName = JSonObject.GetStringValue("Name"); // Check if the object name contains SeqAct_Interp, if so deserialize Matinee if (InStr(ObjectName, "SeqAct_Interp",, true) != INDEX_NONE) { LoadMatineeState(ObjectName, JSonObject); } // Check if the object name contains SeqEvent or SeqVar, if so deserialize Kismet else if (InStr(ObjectName, "SeqEvent",, true) != INDEX_NONE || InStr(ObjectName, "SeqVar",, true) != INDEX_NONE) { LoadKismetState(ObjectName, JSonObject); } // Otherwise it is some other type of actor else { // Try to find the persistent level actor Actor = Actor(FindObject(ObjectName, class'Actor')); // If the actor was not in the persistent level, then it must have been transient then attempt to spawn it if (Actor == None) { // Spawn the actor ActorArchetype = GetActorArchetypeFromName(JSonObject.GetStringValue("ObjectArchetype")); if (ActorArchetype != None) { Actor = WorldInfo.Spawn(ActorArchetype.Class,,,,, ActorArchetype, true); } } if (Actor != None) { // Cast to the save game state interface SaveGameStateInterface = SaveGameStateInterface(Actor); if (SaveGameStateInterface != None) { // Deserialize the actor SaveGameStateInterface.Deserialize(JSonObject); } } } } } } } /** * Returns an actor archetype from the name * * @return Returns an actor archetype from the string representation */ function Actor GetActorArchetypeFromName(string ObjectArchetypeName) { local WorldInfo WorldInfo; WorldInfo = class'WorldInfo'.static.GetWorldInfo(); if (WorldInfo == None) { return None; } // Use static look ups if on the console, for static look ups to work // * Force cook the classes or packaged archetypes to the maps // * Add packaged archetypes to the StartupPackage list // * Reference the packages archetypes somewhere within Unrealscript if (WorldInfo.IsConsoleBuild()) { return Actor(FindObject(ObjectArchetypeName, class'Actor')); } else // Use dynamic look ups if on the PC { return Actor(DynamicLoadObject(ObjectArchetypeName, class'Actor')); } }
/** * Loads the Kismet Sequence state based on the data provided * * @param ObjectName Name of the Kismet object in the level * @param Data Data as JSon for the Kismet object */ function LoadKismetState(string ObjectName, JSonObject Data) { local SequenceEvent SequenceEvent; local SeqVar_Bool SeqVar_Bool; local SeqVar_Float SeqVar_Float; local SeqVar_Int SeqVar_Int; local SeqVar_Object SeqVar_Object; local SeqVar_String SeqVar_String; local SeqVar_Vector SeqVar_Vector; local Object SequenceObject; local WorldInfo WorldInfo; // Attempt to find the sequence object SequenceObject = FindObject(ObjectName, class'Object'); // Could not find sequence object, so abort if (SequenceObject == None) { return; } // Deserialize Kismet Event SequenceEvent = SequenceEvent(SequenceObject); if (SequenceEvent != None) { WorldInfo = class'WorldInfo'.static.GetWorldInfo(); if (WorldInfo != None) { SequenceEvent.ActivationTime = WorldInfo.TimeSeconds + Data.GetFloatValue("ActivationTime"); } SequenceEvent.TriggerCount = Data.GetIntValue("TriggerCount"); return; } // Deserialize Kismet Variable Bool SeqVar_Bool = SeqVar_Bool(SequenceObject); if (SeqVar_Bool != None) { SeqVar_Bool.bValue = Data.GetIntValue("Value"); return; } // Deserialize Kismet Variable Float SeqVar_Float = SeqVar_Float(SequenceObject); if (SeqVar_Float != None) { SeqVar_Float.FloatValue = Data.GetFloatValue("Value"); return; } // Deserialize Kismet Variable Int SeqVar_Int = SeqVar_Int(SequenceObject); if (SeqVar_Int != None) { SeqVar_Int.IntValue = Data.GetIntValue("Value"); return; } // Deserialize Kismet Variable Object SeqVar_Object = SeqVar_Object(SequenceObject); if (SeqVar_Object != None) { SeqVar_Object.SetObjectValue(FindObject(Data.GetStringValue("Value"), class'Object')); return; } // Deserialize Kismet Variable String SeqVar_String = SeqVar_String(SequenceObject); if (SeqVar_String != None) { SeqVar_String.StrValue = Data.GetStringValue("Value"); return; } // Deserialize Kismet Variable Vector SeqVar_Vector = SeqVar_Vector(SequenceObject); if (SeqVar_Vector != None) { SeqVar_Vector.VectValue.X = Data.GetFloatValue("Value_X"); SeqVar_Vector.VectValue.Y = Data.GetFloatValue("Value_Y"); SeqVar_Vector.VectValue.Z = Data.GetFloatValue("Value_Z"); return; } }
/** * Loads up the Matinee state based on the data * * @param ObjectName Name of the Matinee Kismet object * @param Data Saved Matinee Kismet data */ function LoadMatineeState(string ObjectName, JSonObject Data) { local SeqAct_Interp SeqAct_Interp; local float OldForceStartPosition; local bool OldbForceStartPos; // Find the matinee kismet object SeqAct_Interp = SeqAct_Interp(FindObject(ObjectName, class'Object')); if (SeqAct_Interp == None) { return; } if (Data.GetIntValue("IsPlaying") == 1) { OldForceStartPosition = SeqAct_Interp.ForceStartPosition; OldbForceStartPos = SeqAct_Interp.bForceStartPos; // Play the matinee at the forced position SeqAct_Interp.ForceStartPosition = Data.GetFloatValue("Position"); SeqAct_Interp.bForceStartPos = true; SeqAct_Interp.ForceActivateInput(0); // Reset the start position and start pos SeqAct_Interp.ForceStartPosition = OldForceStartPosition; SeqAct_Interp.bForceStartPos = OldbForceStartPos; } else { // Set the position of the matinee SeqAct_Interp.SetPosition(Data.GetFloatValue("Position"), true); } // Set the paused SeqAct_Interp.bPaused = (Data.GetIntValue("Paused") == 1) ? true : false; }
KActor example
This example shows how you would setup a KActor to serialize and deserialize itself using the Save Game State System. Remember that for any Actor class that you want the Save Game System to automatically pick up upon loading or saving, you need to implement the SaveGameStateInterface.
class SaveGameStateKActor extends KActor Implements(SaveGameStateInterface);
Serializing the KActor
Only the location and rotation values are saved here. The path name and object archetype are required data; otherwise the Save Game State System will not know what Actor or Object to apply the data to and or if the Actor or Object is required to be instanced the Save Game State System will not know what Actor or Object archetype to instance. So the location is saved as three floats and the rotation is saved as three integers. You can of course save more variables as required. One reason why JSon was chosen, was that you can create parent - child structures using the JSonObject::SetObject() function. Thus you can also have child Actors or Objects serialize themselves within this step (ensure that these Actors or Objects have a way of keeping track if they have been serialized or not; as you do not want these Actors or Objects being serialized and deserialized more than once) and saved together with the parent data set. This naturally creates a very easy method to handle attached Actors or Objects, without having to tweak the base Save Game State System code base./** * Serializes the actor's data into JSon * * @return JSon data representing the state of this actor */ function String Serialize() { local JSonObject JSonObject; // Instance the JSonObject, abort if one could not be created JSonObject = new () class'JSonObject'; if (JSonObject == None) { `Warn(Self$" could not be serialized for saving the game state."); return ""; } // Serialize the path name so that it can be looked up later JSonObject.SetStringValue("Name", PathName(Self)); // Serialize the object archetype, in case this needs to be spawned JSonObject.SetStringValue("ObjectArchetype", PathName(ObjectArchetype)); // Save the location JSonObject.SetFloatValue("Location_X", Location.X); JSonObject.SetFloatValue("Location_Y", Location.Y); JSonObject.SetFloatValue("Location_Z", Location.Z); // Save the rotation JSonObject.SetIntValue("Rotation_Pitch", Rotation.Pitch); JSonObject.SetIntValue("Rotation_Yaw", Rotation.Yaw); JSonObject.SetIntValue("Rotation_Roll", Rotation.Roll); // Send the encoded JSonObject return class'JSonObject'.static.EncodeJson(JSonObject); }
Deserializing the KActor
When the KActor is asked to deserialize itself, it is given the JSon data that it had serialized itself. Thus simply performing the opposite should restore the KActor to its state that it was when the game state was saved. As mentioned above, if you required child Actors or Objects to be serialized; then here would be the appropriate place to deserialize that data./** * Deserializes the actor from the data given * * @param Data JSon data representing the differential state of this actor */ function Deserialize(JSonObject Data) { local Vector SavedLocation; local Rotator SavedRotation; // Deserialize the location and set it SavedLocation.X = Data.GetFloatValue("Location_X"); SavedLocation.Y = Data.GetFloatValue("Location_Y"); SavedLocation.Z = Data.GetFloatValue("Location_Z"); // Deserialize the rotation and set it SavedRotation.Pitch = Data.GetIntValue("Rotation_Pitch"); SavedRotation.Yaw = Data.GetIntValue("Rotation_Yaw"); SavedRotation.Roll = Data.GetIntValue("Rotation_Roll"); if (StaticMeshComponent != None) { StaticMeshComponent.SetRBPosition(SavedLocation); StaticMeshComponent.SetRBRotation(SavedRotation); } }
Player controlled pawn example
The player controlled pawn is an interesting example where none of the Actors involved are placed by the level designers; that is neither the PlayerController or the Pawn classes were placed in the map. However, Pawns may be placed by the level designer for different purposes such as place enemy monsters in the map for a single player game. Thus the method that was done here was to save an extra flag called IsPlayerControlled. Thus when the pawn is instanced and deserialized by the Save Game System, if IsPlayerControlled is set to 1 then the deserializing code will tell the GameInfo about that.
/** * Serializes the actor's data into JSon * * @return JSon data representing the state of this actor */ function String Serialize() { local JSonObject JSonObject; // Instance the JSonObject, abort if one could not be created JSonObject = new () class'JSonObject'; if (JSonObject == None) { `Warn(Self$" could not be serialized for saving the game state."); return ""; } // Serialize the path name so that it can be looked up later JSonObject.SetStringValue("Name", PathName(Self)); // Serialize the object archetype, in case this needs to be spawned JSonObject.SetStringValue("ObjectArchetype", PathName(ObjectArchetype)); // Save the location JSonObject.SetFloatValue("Location_X", Location.X); JSonObject.SetFloatValue("Location_Y", Location.Y); JSonObject.SetFloatValue("Location_Z", Location.Z); // Save the rotation JSonObject.SetIntValue("Rotation_Pitch", Rotation.Pitch); JSonObject.SetIntValue("Rotation_Yaw", Rotation.Yaw); JSonObject.SetIntValue("Rotation_Roll", Rotation.Roll); // If the controller is the player controller, then saved a flag to say that it should be repossessed by the player when we reload the game state JSonObject.SetIntValue("IsPlayerControlled", (PlayerController(Controller) != None) ? 1 : 0); // Send the encoded JSonObject return class'JSonObject'.static.EncodeJson(JSonObject); } /** * Deserializes the actor from the data given * * @param Data JSon data representing the differential state of this actor */ function Deserialize(JSonObject Data) { local Vector SavedLocation; local Rotator SavedRotation; local SaveGameStateGameInfo SaveGameStateGameInfo; // Deserialize the location and set it SavedLocation.X = Data.GetFloatValue("Location_X"); SavedLocation.Y = Data.GetFloatValue("Location_Y"); SavedLocation.Z = Data.GetFloatValue("Location_Z"); SetLocation(SavedLocation); // Deserialize the rotation and set it SavedRotation.Pitch = Data.GetIntValue("Rotation_Pitch"); SavedRotation.Yaw = Data.GetIntValue("Rotation_Yaw"); SavedRotation.Roll = Data.GetIntValue("Rotation_Roll"); SetRotation(SavedRotation); // Deserialize if this was a player controlled pawn, if it was then tell the game info about it if (Data.GetIntValue("IsPlayerControlled") == 1) { SaveGameStateGameInfo = SaveGameStateGameInfo(WorldInfo.Game); if (SaveGameStateGameInfo != None) { SaveGameStateGameInfo.PendingPlayerPawn = Self; } } }
/** * Restarts a controller * * @param NewPlayer Player to restart */ function RestartPlayer(Controller NewPlayer) { local LocalPlayer LP; local PlayerController PC; // Ensure that we have a controller if (NewPlayer == None) { return; } // If we have a pending player pawn, then just possess that one if (PendingPlayerPawn != None) { // Assign the pending player pawn as the new player's pawn NewPlayer.Pawn = PendingPlayerPawn; // Initialize and start it up if (PlayerController(NewPlayer) != None) { PlayerController(NewPlayer).TimeMargin = -0.1; } NewPlayer.Pawn.LastStartTime = WorldInfo.TimeSeconds; NewPlayer.Possess(NewPlayer.Pawn, false); NewPlayer.ClientSetRotation(NewPlayer.Pawn.Rotation, true); if (!WorldInfo.bNoDefaultInventoryForPlayer) { AddDefaultInventory(NewPlayer.Pawn); } SetPlayerDefaults(NewPlayer.Pawn); // Clear the pending pawn PendingPlayerPawn = None; } else // Otherwise spawn a new pawn for the player to possess { Super.RestartPlayer(NewPlayer); } // To fix custom post processing chain when not running in editor or PIE. PC = PlayerController(NewPlayer); if (PC != none) { LP = LocalPlayer(PC.Player); if (LP != None) { LP.RemoveAllPostProcessingChains(); LP.InsertPostProcessingChain(LP.Outer.GetWorldPostProcessChain(), INDEX_NONE, true); if (PC.myHUD != None) { PC.myHUD.NotifyBindPostProcessEffects(); } } } }
Game State Loaded Kismet Event
Sometimes it may be necessary to perform some Kismet Actions to ensure that the game world is fully restored. This is done by making a custom Sequence Event.
class SaveGameState_SeqEvent_SavedGameStateLoaded extends SequenceEvent; defaultproperties { ObjName="Saved Game State Loaded" MaxTriggerCount=0 VariableLinks.Empty OutputLinks(0)=(LinkDesc="Loaded") bPlayerOnly=false }
/** * Start the match - inform all actors that the match is starting, and spawn player pawns */ function StartMatch() { local SaveGameState SaveGameState; local PlayerController PlayerController; local int Idx; local array<SequenceObject> Events; local SaveGameState_SeqEvent_SavedGameStateLoaded SavedGameStateLoaded; // Check if we need to load the game or not if (PendingSaveGameFileName != "") { // Instance the save game state SaveGameState = new () class'SaveGameState'; if (SaveGameState == None) { return; } // Attempt to deserialize the save game state object from disk if (class'Engine'.static.BasicLoadObject(SaveGameState, PendingSaveGameFileName, true, class'SaveGameState'.const.SAVEGAMESTATE_REVISION)) { // Load the game state SaveGameState.LoadGameState(); } // Send a message to all player controllers that we've loaded the save game state ForEach WorldInfo.AllControllers(class'PlayerController', PlayerController) { PlayerController.ClientMessage("Loaded save game state from "$PendingSaveGameFileName$".", 'System'); // Activate saved game state loaded events if (WorldInfo.GetGameSequence() != None) { WorldInfo.GetGameSequence().FindSeqObjectsByClass(class'SaveGameState_SeqEvent_SavedGameStateLoaded', true, Events); for (Idx = 0; Idx < Events.Length; Idx++) { SavedGameStateLoaded = SaveGameState_SeqEvent_SavedGameStateLoaded(Events[Idx]); if (SavedGameStateLoaded != None) { SavedGameStateLoaded.CheckActivate(PlayerController, PlayerController); } } } } } Super.StartMatch(); }
Questions
How do a handle child Actors or Objects?
One reason why JSon was chosen, was that you can create parent - child structures using the JSonObject::SetObject() function. Thus you can also have child Actors or Objects serialize themselves within this step (ensure that these Actors or Objects have a way of keeping track if they have been serialized or not; as you do not want these Actors or Objects being serialized and deserialized more than once) and saved together with the parent data set. This naturally creates a very easy method to handle attached Actors or Objects, without having to tweak the base Save Game State System code base. When the Actor or Object is asked to be deserialized, then you can iterate through the inner JSonObjects and perform the same kind of deserialization.The saved game state is stored as plain text! How would I prevent players from cheating?
Another reason why JSon was chosen, was that it would be very easy to debug the saved game state files by simply opening them up in Notepad or some other kind of text editing software. However, it is understandable that not storing it as binary may lead to some fears about cheating. There are a few trains of thought on this. You could obfuscate the data by passing the encoded JSon through a text mangler function. However, even that would eventually get decoded by people who really want to hack your saved games. Even binary would not be immune to this. Therefore, at the end of the day; there is very little you can do to prevent cheating; unless you can verify the source of the information and verify where the save data is being stored (online saves).Is it possible to store the JSon data online?
Yes. The nice thing about using JSon for this, is that it is a plain text interchangable data format that can be sent to a server via TCPLink. Thus save games can be stored online some where and the client could retrieve them on a different machine... or even on a different device. Or you could even have a website which reads the JSon data and displays the player's progress to them. The possibilities are practically endless.How do I integrate this Development Kit Gem!?
You can either subclass from SaveGameState classes (easiest) or you can shift the code within SaveGameState classes into your own game. Remember, you must be running the correct game type so that the correct PlayerController is being used by the game; otherwise none of the code will work because the incorrect classes are being used. To check which GameInfo and which PlayerController is currently being used, used the "showdebug" console command. This will print on screen in the top left corner which GameInfo and which PlayerController are currently being used.I've integrated, but when I load a map nothing happens!
Remember that by default, the example code uses SaveGameStateGameInfo::StartMatch() and a delayed called to Super.StartMatch() [UTGame::StartMatch()] when the Save Game State has streaming levels. GameInfo::StartMatch() is automatically called when bDelayedStart is false and bWaitingToStartMatch is true by default. However, if this does not fit with your game; then remember to call SaveGameStateGameInfo::StartMatch(). You can also move the contents of the SaveGameStateGameInfo::StartMatch(), as the main reason why it is in there is because save game state requires the PlayerController to be instanced before the save game state is loaded.Related Topics
Downloads
- Download the script and example map.