Saving objects using BasicSaveObject - Enhanced
The initial implementation made use of a Kismet event to
restore the level’s state which wasn’t too ideal especially from a maintenance
point of view. This has been removed and in its place I’m making use of
CheckpointRecord structures that are define in some UDK classes e.g. The
Trigger has this definition.
struct CheckpointRecord
{
var bool
bCollideActors;
};
And these functions
function bool ShouldSaveForCheckpoint()
{
return (bStatic || bNoDelete);
}
function CreateCheckpointRecord(out CheckpointRecord
Record)
{
// actor collision is the primary method of toggling
triggers apparently
Record.bCollideActors =
bCollideActors;
}
function ApplyCheckpointRecord(const out CheckpointRecord Record)
{
SetCollision(Record.bCollideActors,bBlockActors,bIgnoreEncroachers);
ForceNetRelevant();
}
So basically, to restore a Trigger actor, all you want to
know is whether its set to collide actors or not.
The idea is this, when a save is requested, we go through
all actors in the level that we consider savable, creating a CheckpointRecord
for each and calling the actor’s CreateCheckpointRecord.
When the level is loaded, we again iterate through the
actor’s we consider savable and look them up in our saved data, if we find a
record then we use the actor’s ApplyCheckpointRecord to restore the actor.
First things first, we need to know what actor’s we want to
save. In the game type class, define an array of actor classes; this will hold
the actor classes we deem as
savable. Also add an array of actual actors that we want to save.
var array<class> SavableActorClasses;
/** Array of actors in the current
level that we consider savable. */
var array<Actor> SavableActors;
In the game’s defaultproperties block we add the classes
that we would like to have saved. I only added Trigger and
FracturedStaticMeshActor for testing.
SavableActorClasses.Empty
SavableActorClasses(0)=class'FracturedStaticMeshActor'
SavableActorClasses(1)=class'Trigger'
Override the StartMatch function so that we can use it to
create an array of savable actors for use when we are saving.
function StartMatch()
{
local Actor A;
if (
MyAutoTestManager != None
)
{
MyAutoTestManager.StartMatch();
}
// tell all actors the game is starting
ForEach AllActors(class'Actor', A)
{
A.MatchStarting();
/// check if actor is savable and add to array
if(CheckSavable(A))
{
SavableActors.AddItem(A);
///
try to restore from checkpoint data only if Save Data is valid and we're not
forcing a new game
if(bSaveDataValid &&
!bForceNewGame)
SaveData.ApplyCheckpointRecord(A);
}
}
// start human players first
StartHumans();
// start AI players
StartBots();
bWaitingToStartMatch
= false;
StartOnlineGame();
// fire off any level startup events
WorldInfo.NotifyMatchStarted();
}
Start by calling a CheckSavable on the actor to check if the
actor belongs to any of the classes we have defined.
/**
* Check if the actor belongs to one of our
savable actor classes and add to array if it does.
*/
protected function bool CheckSavable(Actor A)
{
local Class
CurrentClass;
local bool bIsSavable;
bIsSavable
= false;
/// check if actor belongs to any of our savable
classes.
foreach SavableActorClasses(CurrentClass)
{
if(A.IsA(CurrentClass.Name))
{
bIsSavable
= true;
break;
}
}
return bIsSavable;
}
If it is, we add it to our array and then we try to restore
it from the saved data. The SaveData object is an instance of a class that we
use to save and load checkpoint records. You can ignore the if(bSaveDataValid && !bForceNewGame) for now.
The ApplyCheckpointRecord looks like this.
/**
* Apply checkpoint record onto actor.
*/
function ApplyCheckpointRecord(Actor A)
{
switch( A.Class )
{
case class'FracturedStaticMeshActor':
case class'PlayerBreakableFracturedMeshActor':
ApplyFracturedStaticMeshActorRecord(FracturedStaticMeshActor(A));
break;
case class'Trigger':
ApplyTriggerRecord(Trigger(A));
break;
default:
break;
}
}
It calls a specific function for each class. This means that
you will have to enhance the class with a function for every different class
you would like to save.
This class also has a structure and an array for every class
that you would like to save. In my case I have:
struct FracturedStaticMeshActorRecord
{
var Name ActorName;
var FracturedStaticMeshActor.CheckpointRecord
Record;
};
struct TriggerRecord
{
var Name ActorName;
var Trigger.CheckpointRecord
Record;
};
var Array<FracturedStaticMeshActorRecord> FracturedStaticMeshActorRecords;
var Array<TriggerRecord> TriggerRecords;
Each record has a name, which we will use to lookup the
actor. You will also need to add additional structures for any additional class
you would like to save.
To load a Trigger for example, this is what we have
protected function
ApplyTriggerRecord(Trigger
A)
{
local int Index;
local TriggerRecord
CheckpointRecord;
Index = TriggerRecords.Find('ActorName', A.Name);
if(Index > -1)
{
CheckpointRecord
= TriggerRecords[Index];
A.ApplyCheckpointRecord(CheckpointRecord.Record);
}
else
`warn("Couldn't find checkpoint record for" @ A);
}
To save the level, we use this bit of code in our game
class. It basically iterates over the actor’s array we created and calls
CreateCheckpointRecord on each of them. We again we make use of our SaveData
object.
foreach SavableActors(A)
{
SaveData.CreateCheckpointRecord(A);
}
class'Engine'.static.BasicSaveObject(SaveData, class'CheckpointSaveData'.const.SaveFilename, true, class'CheckpointSaveData'.const.SaveVersionNo);
The CreateCheckpointRecord
function looks a lot like the ApplyCheckpointRecord function we looked at
earlier.
function CreateCheckpointRecord(Actor A)
{
switch( A.Class )
{
case class'FracturedStaticMeshActor':
case class'PlayerBreakableFracturedMeshActor':
CreateFracturedStaticMeshActorRecord(FracturedStaticMeshActor(A));
break;
case class'Trigger':
CreateTriggerRecord(Trigger(A));
break;
default:
break;
}
}
And the specific
function that we use to create a trigger record looks like this.
protected function
CreateTriggerRecord(Trigger
A)
{
local TriggerRecord
CheckpointRecord;
if(A.ShouldSaveForCheckpoint())
{
CheckpointRecord.ActorName = A.Name;
A.CreateCheckpointRecord(CheckpointRecord.Record);
TriggerRecords.AddItem(CheckpointRecord);
}
}
Also note that the
TriggerRecords array (and any other record array) should be empty before calling
this function because if you have existing records, this one will just be
appended onto the array. This will result in the older records being used when
loading.
The saving process as
before is triggered from the kismet action we had in the previous tutorial.
To get this to work in a
game menu, I defined two functions, NewGame and ResumeGame. The former will
ignore any saved data and start a new game while the latter will try to load
the checkpoint data and if it fails, it will revert to new game behaviour.
The functions are quite
simple and look like this.
/**
* Start a new game and ignore any saved
checkpoints.
*/
exec function NewGame()
{
StartGame(True);
}
/**
* Resume the game from the saved checkpoint
file.
*/
exec function ResumeGame()
{
StartGame(False);
}
The StartGame function
is where the magic happens, it takes a Boolean argument to specify whether to
start a new game or load form a checkpoint. Here is the entire function.
/**
* Start the game using open console command to
load the required map.
*/
private function StartGame(bool bNewGame)
{
local string GameExec, MapName;
// if a new game was NOT requested and we have valid
checkpoint data
bForceNewGame
= (!bNewGame && bSaveDataValid)?False:True;
MapName
= FirstMapName;
if(!bForceNewGame && bSaveDataValid)
MapName
= SaveData.MapName;
GameExec
= "open "
$ MapName $
GetCommonOpenOptions() $ "?ForceNewGame=" $ (bForceNewGame?1:0);
ConsoleCommand(GameExec);
}
We first check whether
we should force loading a new game. This happens if:
- A new game was requested
or
- The checkpoint data was not successfully loaded e.g. the file didn’t exist.
We then get the map name form a config variable I created that contains the
name of the game’s first level. If a resume was requested and we have valid
checkpoint data, we set the map name to the name saved in the checkpoint.
We then make use of the
open console command to load the specified map, specifying whether we want to
resume or start a new game. This is necessary because the open command will
result in the game initialization process being run again. We used this earlier
in the StartMatch function to figure out whether we should restore actors from
the checkpoint data.
function StartMatch()
{
. . .
/// load save data
LoadCheckpointData();
// tell all actors the game is starting
ForEach AllActors(class'Actor', A)
{
A.MatchStarting();
/// check if actor is savable and add to array
if(CheckSavable(A))
{
SavableActors.AddItem(A);
/// try to restore from checkpoint data only if Save
Data is valid and we're not forcing a new game
if(bSaveDataValid && !bForceNewGame)
SaveData.ApplyCheckpointRecord(A);
}
}
. . .
}
That’s about it so here
are the code listings.
SeqAct_SaveCheckpoint.uc
/**
* Sequence action used to save checkpoints
*
* Copyright 2011 Larry Weya.
*/
class SeqAct_SaveCheckpoint
extends SequenceAction;
/**
* Return the version number for this
class. Child classes should increment
this method by calling Super then adding
* a individual class version to the
result. When a class is first created,
the number should be 0; each time one of the
* link arrays is modified (VariableLinks,
OutputLinks, InputLinks, etc.), the number that is added to the result of
* Super.GetObjClassVersion() should be
incremented by 1.
*
* @return the
version number for this specific class.
*/
static event int GetObjClassVersion()
{
return Super.GetObjClassVersion()
+ 1;
}
DefaultProperties
{
ObjName="Save
Checkpoint"
ObjCategory="Level"
// this is the LIST(S) object that we are going to do
stuff too (i.e. add, remove, empty, call actions on all of the objects)
VariableLinks(0)=(ExpectedType=class'SeqVar_Object',LinkDesc="Checkpoint
PlayerStart",MinVars=1,MaxVars=1)
}
CheckpointPlayerStart.uc
class CheckpointPlayerStart
extends PlayerStart;
function OnSaveCheckpoint(SeqAct_SaveCheckpoint Action)
{
SaveGame(WorldInfo.Game).SaveCheckpointData(Self);
}
defaultproperties
{
bPrimaryStart=False
}
CheckpointSaveData.uc
class CheckpointSaveData extends Object;
const SaveFilename="SaveGame.sav";
const SaveVersionNo=1;
var Name PlayerStartName;
/** the name of the map the player
is currently playing in. */
var String MapName;
struct FracturedStaticMeshActorRecord
{
var Name ActorName;
var FracturedStaticMeshActor.CheckpointRecord
Record;
};
struct TriggerRecord
{
var Name ActorName;
var Trigger.CheckpointRecord
Record;
};
var Array<FracturedStaticMeshActorRecord> FracturedStaticMeshActorRecords;
var Array<TriggerRecord> TriggerRecords;
function ClearArrays()
{
FracturedStaticMeshActorRecords.Remove(0,
FracturedStaticMeshActorRecords.Length);
TriggerRecords.Remove(0,
TriggerRecords.Length);
}
/**
* Create a checkpoint record from the actor.
*/
function CreateCheckpointRecord(Actor A)
{
switch( A.Class )
{
case class'FracturedStaticMeshActor':
case class'PlayerBreakableFracturedMeshActor':
CreateFracturedStaticMeshActorRecord(FracturedStaticMeshActor(A));
break;
case class'Trigger':
CreateTriggerRecord(Trigger(A));
break;
default:
break;
}
}
protected function
CreateFracturedStaticMeshActorRecord(FracturedStaticMeshActor A)
{
local int Index;
local FracturedStaticMeshActorRecord
CheckpointRecord;
if(A.ShouldSaveForCheckpoint())
{
CheckpointRecord.ActorName = A.Name;
A.CreateCheckpointRecord(CheckpointRecord.Record);
FracturedStaticMeshActorRecords.AddItem(CheckpointRecord);
}
}
protected function
CreateTriggerRecord(Trigger
A)
{
local TriggerRecord
CheckpointRecord;
if(A.ShouldSaveForCheckpoint())
{
CheckpointRecord.ActorName = A.Name;
A.CreateCheckpointRecord(CheckpointRecord.Record);
TriggerRecords.AddItem(CheckpointRecord);
}
}
/**
* Apply checkpoint record onto actor.
*/
function ApplyCheckpointRecord(Actor A)
{
switch( A.Class )
{
case class'FracturedStaticMeshActor':
case class'PlayerBreakableFracturedMeshActor':
ApplyFracturedStaticMeshActorRecord(FracturedStaticMeshActor(A));
break;
case class'Trigger':
ApplyTriggerRecord(Trigger(A));
break;
default:
break;
}
}
protected function
ApplyFracturedStaticMeshActorRecord(FracturedStaticMeshActor A)
{
local int Index;
local FracturedStaticMeshActorRecord
CheckpointRecord;
Index = FracturedStaticMeshActorRecords.Find('ActorName', A.Name);
if(Index > -1)
{
CheckpointRecord
= FracturedStaticMeshActorRecords[Index];
A.ApplyCheckpointRecord(CheckpointRecord.Record);
}
else
`warn("Couldn't find checkpoint record for" @ A);
}
protected function
ApplyTriggerRecord(Trigger
A)
{
local int Index;
local TriggerRecord
CheckpointRecord;
Index = TriggerRecords.Find('ActorName', A.Name);
if(Index > -1)
{
CheckpointRecord
= TriggerRecords[Index];
A.ApplyCheckpointRecord(CheckpointRecord.Record);
}
else
`warn("Couldn't find checkpoint record for" @ A);
}
defaultproperties
{
}
SaveGame.uc
class SaveGame extends UDKGame;
/** checkpint sava data */
var CheckpointSaveData
SaveData;
var array<class> SavableActorClasses;
/** name of the first map in the
game */
var config string FirstMapName;
/** Array of actors in the current
level that we consider savable. */
var array<Actor> SavableActors;
/** Whether we should ignore the
saved checkpoint data and force start a fresh game */
var bool bForceNewGame;
/** only valid if a load was tried
and successful */
var private bool bSaveDataValid;
/* 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.
Warning: this is called before actors'
PreBeginPlay.
*/
event InitGame( string Options, out string
ErrorMessage )
{
local int
ForceNewGame;
// get ForceNewGame option and set bForceNewGame (False
by default)
ForceNewGame
= GetIntOption(Options, "ForceNewGame", 0);
bForceNewGame
= ForceNewGame==1?True:False;
Super.InitGame( Options,
ErrorMessage );
}
/* StartMatch()
Start the game - inform all actors
that the match is starting, and spawn player pawns
*/
function StartMatch()
{
local Actor A;
if (
MyAutoTestManager != None
)
{
MyAutoTestManager.StartMatch();
}
/// load save data
LoadCheckpointData();
// tell all actors the game is starting
ForEach AllActors(class'Actor', A)
{
A.MatchStarting();
/// check if
actor is savable and add to array
if(CheckSavable(A))
{
SavableActors.AddItem(A);
///
try to restore from checkpoint data only if Save Data is valid and we're not
forcing a new game
if(bSaveDataValid &&
!bForceNewGame)
SaveData.ApplyCheckpointRecord(A);
}
}
// start human players first
StartHumans();
// start AI players
StartBots();
bWaitingToStartMatch
= false;
StartOnlineGame();
// fire off any level startup events
WorldInfo.NotifyMatchStarted();
}
/** FindPlayerStart()
* Return the 'best' player start
for this player to start from.
PlayerStarts are rated by RatePlayerStart().
* @param Player is the controller
for whom we are choosing a playerstart
* @param InTeam specifies the
Player's team (if the player hasn't joined a team yet)
* @param IncomingName specifies the
tag of a teleporter to use as the Playerstart
* @returns NavigationPoint chosen
as player start (usually a PlayerStart)
*/
function NavigationPoint
FindPlayerStart( Controller
Player, optional
byte InTeam, optional string
IncomingName )
{
local NavigationPoint
N, BestStart;
local Teleporter
Tel;
local CheckpointPlayerStart
CheckPoint;
// allow GameRulesModifiers to override playerstart
selection
if (BaseMutator != None)
{
N
= BaseMutator.FindPlayerStart(Player, InTeam, IncomingName);
if (N != None)
{
return N;
}
}
// if incoming start is specified, then just use it
if( incomingName!="" )
{
ForEach WorldInfo.AllNavigationPoints( class 'Teleporter', Tel )
if( string(Tel.Tag)~=incomingName
)
return Tel;
}
/// since both bots and humans use this, we need to
make sure that we only load the spawn point for the human player
/// NOTE: this assumes a single player game
/// his stops the "playe form here"
functionality from working, perhaps we should overide FindPlayerStart to suit
us
if(!bForceNewGame &&
bSaveDataValid && PlayerController(Player) != None /*&&
StartSpot == None*/)
{
ForEach
WorldInfo.AllNavigationPoints( class 'CheckpointPlayerStart', CheckPoint )
{
if( CheckPoint.Name == SaveData.PlayerStartName
)
return Checkpoint;
}
}
// always pick StartSpot at start of match
if (
ShouldSpawnAtStartSpot(Player) &&
(PlayerStart(Player.StartSpot) == None ||
RatePlayerStart(PlayerStart(Player.StartSpot), InTeam, Player) >= 0.0) )
{
return Player.StartSpot;
}
BestStart
= ChoosePlayerStart(Player, InTeam);
if ( (BestStart == None) && (Player == None) )
{
// no playerstart found, so pick any NavigationPoint to
keep player from failing to enter game
`log("Warning - PATHS NOT DEFINED or NO PLAYERSTART with
positive rating");
ForEach AllActors( class 'NavigationPoint',
N )
{
BestStart
= N;
break;
}
}
return BestStart;
}
/**
* Load checkpoint sata form the saved file. If
successfule, this will
* set the AdvGame.bSaveDataValid to true for
use later in RestartPlayer.
* Keep this here in-case we change the
logic/filename/version etc
*/
protected function
LoadCheckpointData()
{
/// load checkpoint data from file if any
SaveData = new(self) class'CheckpointSaveData';
if(class'Engine'.static.BasicLoadObject(SaveData, class'CheckpointSaveData'.const.SaveFilename, true, class'CheckpointSaveData'.const.SaveVersionNo))
{
bSaveDataValid
= True;
}
else
`warn("Failed to load checkpoint data.");
}
/**
* Save checkpoint data into the save file.
* Keep this here in-case we change the
logic/filename/version etc
*/
function SaveCheckpointData(CheckpointPlayerStart Checkpoint)
{
local Actor A;
/// make sure our checkpoint record arrays are empty
SaveData.ClearArrays();
SaveData.PlayerStartName =
Checkpoint.Name;
SaveData.MapName =
GetURLMap();
foreach SavableActors(A)
{
SaveData.CreateCheckpointRecord(A);
}
class'Engine'.static.BasicSaveObject(SaveData, class'CheckpointSaveData'.const.SaveFilename, true, class'CheckpointSaveData'.const.SaveVersionNo);
}
/**
* Check if the actor belongs to one of our
savable actor classes and add to array if it does.
*/
protected function bool CheckSavable(Actor A)
{
local Class
CurrentClass;
local bool bIsSavable;
bIsSavable
= false;
/// check if actor belongs to any of our savable
classes.
foreach SavableActorClasses(CurrentClass)
{
if(A.IsA(CurrentClass.Name))
{
bIsSavable
= true;
break;
}
}
return bIsSavable;
}
/**
* Start the game using open console command to
load the required map.
*/
private function StartGame(bool bNewGame)
{
local string GameExec, MapName;
// if a new game was NOT requested and we have valid
checkpoint data
bForceNewGame
= (!bNewGame && bSaveDataValid)?False:True;
MapName
= FirstMapName;
if(!bForceNewGame && bSaveDataValid)
MapName
= SaveData.MapName;
GameExec
= "open "
$ MapName $ "?ForceNewGame=" $
(bForceNewGame?1:0);
ConsoleCommand(GameExec);
}
/**
* Start a new game and ignore any saved
checkpoints.
*/
exec function NewGame()
{
StartGame(True);
}
/**
* Resume the game from the saved checkpoint
file.
*/
exec function ResumeGame()
{
StartGame(False);
}
defaultproperties
{
bSaveDataValid=false
SavableActorClasses.Empty
SavableActorClasses(0)=class'FracturedStaticMeshActor'
SavableActorClasses(1)=class'Trigger'
}