Tutorials‎ > ‎UDK Save Game System‎ > ‎

UDK Basic Save Game Tutorial


Introduction

In this tutorial, I'll go over how to create a system for saving the game state in a UDK game. As many of you probably already know, there is a gem available on the UDN that already does this. So why am I writing a tutorial that already exists? First of all, the save gem didn't work for me out of the box - I had to dissect it piece by piece until I understood what every line of code was doing so I could find out why it wasn't working for me. This lead me to understand something which is the second reason I'm writing this tutorial: the save gem is designed to work specifically with a UT type game. For anyone else like me who is writing their own custom game that isn't a first person shooter like UT, it simply doesn't work.

The third reason is, the save gem is fairly complex and includes a few components that aren't entirely necessary for a basic save system. True, some of them will most likely need to be implemented eventually, but are a little excessive for a basic starting point. My goal for this tutorial is to make it as simple as possible, explaining the concepts behind the code so that you understand what's happening and can write the code for yourself rather than just copy-pasting.

In this part of the tutorial, I'll start by talking about how the system works and what it does. Note that most of the code I'll show comes from the save gem, but I've modified some of it to make it simpler and more versatile. I've provided the modified code here, as well as the Visual Studio project for the simple text mangling dll, which I'll cover in the next part of the tutorial. I also recommend that you go download the save gem so that you can study it and later implement the pieces I'm leaving out. It might also help you to understand why I cut out what I did. The level included with the content for this tutorial is taken directly from the save gem with no modifications whatsoever. Since this isn't a tutorial on level building, you should just use that level to follow along.

Setup

To set up the project, I recommend using Pixel Mine’s nFringe plugin for Visual Studio and Mavrik Games' Project Suite Management Utility. If you’re never used those tools before and you don’t want to, you can still follow this tutorial as long as you know how to set up your environment. I’m going to go through this tutorial assuming you’re using them though.

The Project Suite Management Utility takes care of all the required setup by creating a Visual Studio solution for your suite and a Visual Studio project for your game, as well as setting up the configuration files and content directories. If this is the first time you’re running the PSMU, it will ask you to select the root directory of your UDK installation. Once you’ve selected the folder, create a project suite. You can call it whatever you want, but mine is called “Tutorial”. Add a game to the suite (I'm using SaveGameState - the same name as the save gem) and select it in the list. Leave the settings at default and click the execute button.

How the System Works

Before we start digging through code, I thought it would be helpful to know what the system does and how it's doing it. In any given level, there are a number of dynamic actors that can have any of their properties set to different values according to what has happened in the course of game play. The player, for example, will probably have moved from the start position and maybe picked up a weapon or other inventory item. This save system basically saves each of those values so that they can be reassigned later when the game is loaded. During the save operation, it basically loops through all the dynamic actors in the level and stores their properties' values in an array of JSON formatted strings, which is basically just a series of key/value pairs. Then, the whole save object (which contains this JSON array) is saved to a file using the Unreal engine's BasicSaveObject function. During the load operation, it goes through the save file and reassigns the values stored in the save object's JSON array to each dynamic actor in the level. If the actor is one that's generated during runtime and not actually placed in the level beforehand (as is the case with the player), the system spawns the actor based on its template/archetype and then assigns the saved values.

If you think about how many different types of actors could be in your level, each having their own unique sets of properties and values, this can seem like a pretty daunting task. How do you tell one actor to save its set of properties which are completely different than another actor's set of properties? The solution comes from an object-oriented programming principle called the Law of Demeter. This law says basically that a class that is operating on any instance objects should assume as little as possible about how those objects are structured. So, instead of going through each different type of actor and telling it which properties to save, tell the actor to save its own properties, whatever properties those may be. This means that the only thing the system will assume about each actor is that it has a way of saving its own properties, and to ensure that each actor has this capability, we create an interface.

Save Game State Interface

If you've never used an interface before, the basic explanation is that an interface lists function declarations but no definitions. It is up to the individual classes that implement the interface to provide definitions for those functions. If a class implements an interface, it's basically making a promise to the compiler that it will have a definition for each of the functions declared in the interface. We can cast any object that implements the interface as an instance of the interface class itself, and by calling a declared function, the object's defined function executes. This might be better understood as we look at some of the code involved. Create a new class called SaveGameStateInterface, and write this code to it:

interface SaveGameStateInterface;

function string Serialize();
function Deserialize(JSonObject Data);

The SaveGameStateInterface has only two functions: Serialize and Deserialize. To serialize an object means to convert it to a string format. Each actor we want to save will serialize itself by writing out its properties to a JSON formatted string. The system will then save each of these strings to the save object's JSON array. It's up to each actor to define how these two functions work for that actor - ie: which properties to save/load and how to save/load them. Now the system knows that if an actor implements this interface, it has these two functions defined, so it can call these functions on that actor.

Serialized KActor

Now let's look at how a KActor might serialize itself. Remember that each actor will save the properties it needs in a JSON format. The Unreal engine provides a JSonObject class that we can use to write out our key/value pairs to a string, which is much easier than trying to create the string manually. Create a class called SaveGameStateKActor and write this code to it:

class SaveGameStateKActor extends KActor implements(SaveGameStateInterface);

function String Serialize()
{
    local JSonObject KJSonObject;

    // Instance the JSonObject, abort if one could not be created
    KJSonObject = new class'JSonObject';

    if (KJSonObject == None)
    {
        `Warn(Self$" could not be serialized for saving the game state.");
	return "";
    }

    // Save the location
    KJSonObject.SetFloatValue("Location_X", Location.X);
    KJSonObject.SetFloatValue("Location_Y", Location.Y);
    KJSonObject.SetFloatValue("Location_Z", Location.Z);

    // Save the rotation
    KJSonObject.SetIntValue("Rotation_Pitch", Rotation.Pitch);
    KJSonObject.SetIntValue("Rotation_Yaw", Rotation.Yaw);
    KJSonObject.SetIntValue("Rotation_Roll", Rotation.Roll);

    // Send the encoded JSonObject
    return class'JSonObject'.static.EncodeJson(KJSonObject);
}

The first thing to notice is that we're telling the engine that this class "implements(SaveGameStateInterface)", so our save system can trust that any instance of this class will have the Serialize and Deserialize functions defined. If we fail to define these functions, the compiler will complain.

In the Serialize function, we declare and create a local JSonObject (I don't like to give my instance variables the same name as their class, so I added a "K" to the beginning of the variable name). If for any reason it didn't initialize correctly, we have to abort. There are several functions available to a JSonObject used for setting key/value pairs. The parameters for the SetFloatValue function are a string key by which the value will be referenced and a float value to store with that key. The same goes for the SetIntValue, except the value is an integer. Pretty simple right? :) Finally, we use the static EncodeJson function to get a JSON structured string out of all this.

Next, let's add the Deserialize function:

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 (self.StaticMeshComponent != None)
    {
        self.StaticMeshComponent.SetRBPosition(SavedLocation);
        self.StaticMeshComponent.SetRBRotation(SavedRotation);
    }
}

This function takes in a JSonObject called Data, and we use some getter functions to get the values out of the data by passing in the keys. We use these values to set the appropriate properties on our actor. That's it! The rest of the system doesn't have to worry about which values to store/restore because each actor decides that for itself with these two functions.

Save Game State

OK, now it's time for the main part of the system. The SaveGameState class is the one that tells each dynamic actor in the level to save itself. It's also the object that actually gets saved to the file, so when we load this object from the file, all of its properties and variables will contain the values they contained when we saved the object. The save gem version of this class is huge, but I've trimmed it down to what I think are the most important functions: SaveGameState and LoadGameState. I've cut out the functions for saving/loading Kimet and Matinee only because these really just use the same ideas but specifically target Kismet and Matinee objects in the level. You will most likely need this functionality, but it's not necessary to get started so I decided to leave it out.

class SaveGameState extends Object;

const VERSION = 1;                                          // SaveGameState revision number
const SAVE_LOCATION = "..\\..\\..\\Saves\\SaveGameState\\"; // Folder for saved game files

var string PersistentMapFileName;                           // File name of the map that this save game state is associated with
var array<name> StreamingMapFileNames;	                    // File names of the streaming maps that this save game state is associated with
var array<string> WorldData;                                // Serialized world data

function SaveGameState()
{
    local WorldInfo SWorldInfo;
    local JsonObject SJsonObject;
    local SaveGameStateInterface SaveGameInterface;
    local LevelStreaming Level;
    local Actor CurrentActor;
    local string ActorData;

    // Get the world info, abort if the world info could not be found
    SWorldInfo = class'WorldInfo'.static.GetWorldInfo();

    if (SWorldInfo == none)
    {
        return;
    }

    // Save the persistent map file name
    PersistentMapFileName = String(SWorldInfo.GetPackageName());

    // Save the currently streamed in map file names
    foreach SWorldInfo.StreamingLevels(Level)
    {
	// Levels that are visible and have a load request pending should be included in the streaming levels list
        if (Level != none && (Level.bIsVisible || Level.bHasLoadRequestPending))
	{				
            StreamingMapFileNames.AddItem(Level.PackageName);
	}
    }

    // Iterate through all of the actors that implement SaveGameStateInterface and ask them to serialize themselves
    foreach SWorldInfo.DynamicActors(class'Actor', CurrentActor, class'SaveGameStateInterface')
    {
	// Type cast to the SaveGameStateInterface
	SaveGameInterface = SaveGameStateInterface(CurrentActor);

	if (SaveGameInterface != none)
	{
	    // Serialize properties that are common to every serializable actor to avoid repetition in the actor classes
	    SJsonObject = class'JsonObject'.static.DecodeJson(SaveGameInterface.Serialize());
	    SJsonObject.SetStringValue("Name", PathName(CurrentActor));
	    SJsonObject.SetStringValue("ObjectArchetype", PathName(CurrentActor.ObjectArchetype));

	    ActorData = class'JsonObject'.static.EncodeJson(SJsonObject);

	    // If the serialzed actor data is valid, then add it to the serialized world data array
	    if (ActorData != "")
	    {
		WorldData.AddItem(ActorData);
	    }
	}
    }
}

There's a lot going on here, but let's pick it apart to understand it. First, the VERSION constant is necessary for the Unreal engine's BasicSaveObject and BasicLoadObject static functions, which we'll be using later to actually write the file. The SAVE_LOCATION is where we want to save the files. Since the UDK gets updated every month, I opted for a location just outside of the current UDK version folder. For a packaged game though, I would change this to something more reasonable. For organization, I put it inside a Saves folder under a folder named after the current game project (in this case, SaveGameState). The comments on the other variables should be enough of an explanation for now, so we'll move on to the SaveGameState function.

The first loop goes through the level and stores any streaming levels because it will need to load these levels and stream them in. The next loop is the important one. The WorldInfo.DynamicActors function loops through all the dynamic actors in the level. The first parameter restricts the actors that will be looped over to only the Actor class actors (which is to say, all of them :)). The second parameter is an actor to assign the current actor to. The third parameter is an interface class, meaning that only actors that implement this interface will be included in the loop. This way, we know that our CurrentActor will implement the SaveGameStateInterface interface, and therefore, we know that it will have defined the Serialize and Deserialize functions.

Next we cast the CurrentActor to a SaveGameSateInterface, and if the cast was successful, we serialize the CurrentActor by calling its Serialize function. Note here a difference between my code and the save gem's. I noticed that all actors need to store a "Name" and an "ObjectArchetype" key value pair, so rather than copy and paste those two calls to SetStringValue into every actor's Serialize function, I simply tack them on here. The Serialize function returns a JSON encoded string, so I decode the string back into a JSonObject, add the two key value pairs, and then re-encode the object into a string. Finally, we store the string in the WorldData array, which is the "JSON array" I mentioned earlier. It's the array of all the JSON encoded data for every dynamic actor in the level.

Now for the LoadGameState function:

function LoadGameState()
{
    local WorldInfo SWorldInfo;
    local JSonObject SJSonObject;
    local SaveGameStateInterface SaveGameInterface;
    local Actor CurrentActor, ActorArchetype;
    local string ObjectData, ObjectName;

    // No serialized world data to load
    if (WorldData.Length <= 0)
    {
        return;
    }

    // Grab the world info, abort if no valid world info
    SWorldInfo = class'WorldInfo'.static.GetWorldInfo();

    if (SWorldInfo == none)
    {
        return;
    }

    // Iterate through each serialized data object
    foreach WorldData(ObjectData)
    {
        if (ObjectData != "")
        {
            // Decode the JSonObject from the encoded string
            SJSonObject = class'JSonObject'.static.DecodeJson(ObjectData);

            if (SJSonObject != none)
            {
                ObjectName = SJSonObject.GetStringValue("Name");            // Get the object name
                CurrentActor = Actor(FindObject(ObjectName, class'Actor')); // Try to find the persistent level actor

                // If the actor was not in the persistent level, then it must have been dynamic so attempt to spawn it
                if (CurrentActor == none)
                {
                    ActorArchetype = Actor(DynamicLoadObject(SJSonObject.GetStringValue("ObjectArchetype"), class'Actor'));
                    if (ActorArchetype != none)
		    {
		        CurrentActor = SWorldInfo.Spawn(ActorArchetype.Class,,,,, ActorArchetype, true);
		    }
                }

                if (CurrentActor != none)
                {
		    // Type cast to the SaveGameStateInterface
		    SaveGameInterface = SaveGameStateInterface(CurrentActor);

		    if (SaveGameInterface != none)
		    {
        	        // Deserialize the actor
			SaveGameInterface.Deserialize(SJSonObject);
		    }
		}
            }
        }
    }
}

This is basically the reverse of the SaveGameState function. We loop through our WorldData array (which contains all the JSON encoded serialized dynamic actors) and first try to get the actor from the persistent actors in the level. If it's not a persistent actor, we get the archetype from the JSonObject and spawn one. Once we have an actor, we cast it as a SaveGameStateInterface object and tell it to restore it's properties' values by calling its Deserialize function.

Save Game State Pawn

Now that we can save actors, we'll want the ability to store our player's information. For that, we need our pawn class to implement the SaveGameStateInterface and define the Serialize and Deserialize functions:

class SaveGameStatePawn extends UDKPawn implements(SaveGameStateInterface);

function String Serialize()
{
    local JSonObject PJSonObject;

    // Instance the JSonObject, abort if one could not be created
    PJSonObject = new class'JSonObject';

    if (PJSonObject == None)
    {
	`Warn(Self$" could not be serialized for saving the game state.");
	return "";
    }

    // Save the location
    PJSonObject.SetFloatValue("Location_X", Location.X);
    PJSonObject.SetFloatValue("Location_Y", Location.Y);
    PJSonObject.SetFloatValue("Location_Z", Location.Z);

    // Save the rotation
    PJSonObject.SetIntValue("Rotation_Pitch", Rotation.Pitch);
    PJSonObject.SetIntValue("Rotation_Yaw", Rotation.Yaw);
    PJSonObject.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
    PJSonObject.SetBoolValue("IsPlayer", SaveGameStatePlayerController(self.Controller) != none);

    // Send the encoded JSonObject
    return class'JSonObject'.static.EncodeJson(PJSonObject);
}

function Deserialize(JSonObject Data)
{
    local Vector SavedLocation;
    local Rotator SavedRotation;
    local SaveGameStateGameInfo SGameInfo;

    // 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.GetBoolValue("IsPlayer"))
    {
	SGameInfo = SaveGameStateGameInfo(self.WorldInfo.Game);

	if (SGameInfo != none)
	{
	    SGameInfo.PendingPlayerPawn = self;
	}
    }
}

Hopefully, this should all look pretty familiar. The only difference between this and the KActor is we want to know if this pawn is player controlled. If it is, we'll spawn the player from this pawn's saved information instead of spawning a new pawn when we load the saved game. Notice how we assign the SGameInfo.PendingPlayerPawn to this pawn in the Deserialize function. This is the variable that we'll use at load time to assign the pawn rather than spawn it.

So, for any other actors you want to save, just have them implement the SaveGameStateInterface and define the Serialize and Deserialize functions. Within these functions, you just decide what properties are important to that actor and store them in a JSonObject.

Save Game State Player Controller

Now that we have the system in place, we need a way to actually activate it from the game. I like to keep all my exec commands in the PlayerController class, and this is where the save gem does it too:

class SaveGameStatePlayerController extends UDKPlayerController;

exec function SaveGame(string FileName)
{
    local SaveGameState GameSave;

    // Instance the save game state
    GameSave = new class'SaveGameState';

    if (GameSave == None)
    {
	return;
    }

    ScrubFileName(FileName);    // Scrub the file name
    GameSave.SaveGameState();   // Ask the save game state to save the game

    // Serialize the save game state object onto disk
    if (class'Engine'.static.BasicSaveObject(GameSave, FileName, true, class'SaveGameState'.const.VERSION))
    {
        // If successful then send a message
	ClientMessage("Saved game state to " $ FileName $ ".", 'System');
    }
}

exec function LoadGame(string FileName)
{
    local SaveGameState GameSave;

    // Instance the save game state
    GameSave = new class'SaveGameState';

    if (GameSave == None)
    {
	return;
    }

    // Scrub the file name
    ScrubFileName(FileName);

    // Attempt to deserialize the save game state object from disk
    if (class'Engine'.static.BasicLoadObject(GameSave, FileName, true, class'SaveGameState'.const.VERSION))
    {
        // Start the map with the command line parameters required to then load the save game state
	ConsoleCommand("start " $ GameSave.PersistentMapFileName $ "?Game=SaveGameState.SaveGameStateGameInfo?SaveGameState=" $ FileName);
    }
}

function ScrubFileName(out string FileName)
{
    // Add the extension if it does not exist
    if (InStr(FileName, ".sav",, true) == INDEX_NONE)
    {
	FileName $= ".sav";
    }

    FileName = Repl(FileName, " ", "_");                            // If the file name has spaces, replace then with under scores
    FileName = class'SaveGameState'.const.SAVE_LOCATION $ FileName; // Prepend the filename with the save folder location
}

Hopefully, this is all pretty straightforward. You save a game by typing in the console "SaveGame" followed by the name of the file you want to save it as, and load a game by typing in "LoadGame" followed by the name of the file you want to load. The ScrubFileName function makes sure there are no illegal characters and that the ".sav" file extension is on the end. It also adds the path to the file name (the SaveGameState's SAVE_LOCATION constant) so that it gets saved in the right folder.

The SaveGame function tells the SaveGameState object - called GameSave here - to save all the actors in the level by calling its SaveGameState function. Then it uses the BasicSaveObject function to save the GameSave object to the file we specified. The LoadGame function calls the BasicLoadObject function to load the SaveGameState object and store it in the GameSave variable. It grabs the map name out of the object (the PersistentMapFileName), and then executes a console command to start that map. Notice that it doesn't actually load any of the actors by calling the GameSave's LoadGameState function, but it passes in the file name where this SaveGameState object is stored under the SaveGameState command line parameter.

So how do the actors actually get loaded in? We know that a call to LoadGameState has to happen because that's where all the actors call their Deserialize function. Well, when you use the console command "start", a new map is loaded and basically a new game is started. If we tried to load the actors from here, their data would just be wiped out as soon as the map loaded, resetting all their values to default. That's why we pass in the file name as a command line argument, so that when the map loads, it knows where to find the file, and it will load the actors from that saved SaveGameState object after the map has finished loading. So where do the command line arguments get parsed? The GameInfo class of course!

Game Info

This is another fairly hefty class in the save gem, and I only trimmed out one function (the RestartPlayer function) and added an overridden SpawnDefaultPawnFor function:

class SaveGameStateGameInfo extends UDKGame;

var private string PendingSaveGameFileName; // Pending save game state file name
var Pawn PendingPlayerPawn;                 // Pending player pawn for the player controller to spawn when loading a game state
var SaveGameState StreamingSaveGameState;   // Save game state used for when streaming levels are waiting to be loaded

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 = "";
    }
}

function StartMatch()
{
    local SaveGameState SaveGame;
    local SaveGameStatePlayerController SPlayerController;
    local name CurrentStreamingMap;

    // Check if we need to load the game or not
    if (PendingSaveGameFileName != "")
    {
	// Instance the save game state
	SaveGame = new class'SaveGameState';

	if (SaveGame == none)
	{
	    return;
	}

	// Attempt to deserialize the save game state object from disk
	if (class'Engine'.static.BasicLoadObject(SaveGame, PendingSaveGameFileName, true, class'SaveGameState'.const.VERSION))
	{
	    // Synchrously load in any streaming levels
	    if (SaveGame.StreamingMapFileNames.Length > 0)
	    {
	        // Ask every player controller to load up the streaming map
	        foreach self.WorldInfo.AllControllers(class'SaveGameStatePlayerController', SPlayerController)
	        {
	            // Stream map files now
		    foreach SaveGame.StreamingMapFileNames(CurrentStreamingMap)
		    {												
		        SPlayerController.ClientUpdateLevelStreamingStatus(CurrentStreamingMap, true, true, true);
		    }

		    // Block everything until pending loading is done
		    SPlayerController.ClientFlushLevelStreaming();
	        }

	        StreamingSaveGameState = SaveGame;                              // Store the save game state in StreamingSaveGameState
	        SetTimer(0.05f, true, NameOf(WaitingForStreamingLevelsTimer));  // Wait for all streaming levels to finish loading

	        return;
	    }

	    // Load the game state
	    SaveGame.LoadGameState();
	}

	// Send a message to all player controllers that we've loaded the save game state
	foreach self.WorldInfo.AllControllers(class'SaveGameStatePlayerController', SPlayerController)
	{
	    SPlayerController.ClientMessage("Loaded save game state from " $ PendingSaveGameFileName $ ".", 'System');
	}
    }

    super.StartMatch();
}

function WaitingForStreamingLevelsTimer()
{
    local LevelStreaming Level;
    local SaveGameStatePlayerController SPlayerController;

    foreach self.WorldInfo.StreamingLevels(Level)
    {
	// If any levels still have the load request pending, then return
	if (Level.bHasLoadRequestPending)
	{
            return;
	}
    }

    ClearTimer(NameOf(WaitingForStreamingLevelsTimer)); // Clear the looping timer
    StreamingSaveGameState.LoadGameState();             // Load the save game state
    StreamingSaveGameState = none;                      // Clear it for garbage collection

    // Send a message to all player controllers that we've loaded the save game state
    foreach self.WorldInfo.AllControllers(class'SaveGameStatePlayerController', SPlayerController)
    {
	SPlayerController.ClientMessage("Loaded save game state from " $ PendingSaveGameFileName $ ".", 'System');
    }

    // Start the match
    super.StartMatch();
}

function Pawn SpawnDefaultPawnFor(Controller NewPlayer, NavigationPoint StartSpot)
{
    local Pawn SpawnedPawn;

    if (NewPlayer == none || StartSpot == none)
    {
	return none;
    }

    // If there's a pending player pawn, return it, but if not, spawn a new one
    SpawnedPawn = PendingPlayerPawn == none ? Spawn(class'SaveGameStatePawn',,, StartSpot.Location) : PendingPlayerPawn;
    PendingPlayerPawn = none;

    return SpawnedPawn;
}

DefaultProperties
{
    bDelayedStart=false
    bWaitingToStartMatch=true
    PlayerControllerClass=class'SaveGameStatePlayerController'
    DefaultPawnClass=class'SaveGameStatePawn'
}

The InitGame function is where we parse the command line parameters that got sent in from the LoadGame function. It looks up the SaveGameState parameter and stores its value in the PendingSaveGameFileName variable, which will be the name of the file to load. We don't just go ahead and load the file yet though because we need to wait until the level has finished loading before we start loading actors.

When the StartMatch function executes, it will check the PendingSaveGameFileName variable and if it has a value, we will load our save game object using the file name stored there with the BasicLoadObject function. Next, we check to see if there are any streaming levels that need to be loaded. If there aren't, we tell the loaded SaveGameState object to load the saved actors by calling its LoadGameState function. If there are streaming levels that need to be loaded, we need to wait for them all to load so we don't try to load an actor that actually belongs to one of those streaming levels. We call the looping function WaitingForStreamingLevelsTimer to periodically check the streaming levels to see if they've loaded. When they have, this function will break its loop and load the saved actors by calling the LoadGameState function.

Finally, we override the SpawnDefaultPawnFor function so that if there's a saved pawn (the PendingPlayerPawn we assigned in out Pawn class' Deserialize function), we return it instead of spawning a new one.

One more thing before we test out the system. If you launch the level, you'll notice some barrels floating in the air, but you have no way of affecting them. When you extend UTGame and UTPawn, you get some default weapons, but UDKGame and UDKPawn extended classes don't give you those things. I could add in a weapon class, but this tutorial is already long enough, so let's just add a simple exec function to the SaveGameStatePlayerController class just so we can knock those barrels down:

exec function Pulse()
{
    local SaveGameStateKActor Barrel;

    foreach self.Pawn.VisibleCollidingActors(class'SaveGameStateKActor', Barrel, 10000, self.Pawn.Location)
    {
	Barrel.ApplyImpulse(Normal(Barrel.Location - self.Pawn.Location), 5000, self.Pawn.Location);
    }
}

OK, time to test the system. Launch the level and you should see a barrel floating above the ground and a cube with a trigger in front of it. Run over the trigger and you'll see the streaming levels load along with two more barrels. Open up your console, and type in SaveGame Test. Type in Pulse to move the barrels and then type in LoadGame Test to see that they are restored to their original positions.

Now you have a working beginning to a save system. You should look at the save gem's Kismet and Matinee saving/loading functions and study how they work, because eventually you're going to need them. If you open up the save file in a text editor, you'll notice that it's mostly just plain text, and if someone wanted to cheat in your game, all they would need to do is edit a few values. Maybe you don't mind, but it's been my experience that I don't have nearly as much fun with a game if I can cheat at it. If you want to prevent this, there are many ways of doing it, and in the next part of this tutorial, I'll show you how to implement a very simple text mangling function using C# and DllBind.