Tuesday, December 27, 2011

Simple UDK Checkpoint System Using PlayerStarts

Abstract
PlayerStarts determine where the player will start a level. This checkpoint implementation aims to use a specialized sub-class of player start as checkpoints.

A kismet action will also be implemented that will allow the level designers to save a checkpoint after a certain event. E.g. after all enemies in a room are cleared the level designer will call the action to save a checkpoint.

A kismet event will also be implemented that we will use to setup a level when the game is resumed from a checkpoint. This is to allow the level designer to run actions that will put the level is the required state. E.g. Assume that a trigger was used before the current checkpoint but the trigger is still accessible, the trigger can be disabled using this event.

Implementation
As mentioned, the idea is that when a player triggers a certain event, a kismet action will be called to save a checkpoint so let’s start by creating the Kismet action. We’ll call it SeqAct_SaveCheckpoint. Create a new class that looks like this.

/**
* Sequence action used to save checkpoints
*/
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.Empty
VariableLinks(0)=(ExpectedType=class'SeqVar_Object',LinkDesc="Checkpoint PlayerStart",MinVars=1,MaxVars=1)
}


Looking at the defaultproperties block, we have an ObjCategory variable that indicates the name of the category our action will appear under in kismet. And ObjName is offcourse its name. We then add an object variable that will indicate which PlayerStart to use as our checkpoint. We add that as the first object in the VariableLinks array.

Next, we’ll create our specialized PlayerStart class which will look like this.

class CheckpointPlayerStart extends PlayerStart;

function OnSaveCheckpoint(SeqAct_SaveCheckpoint Action)
{
local CheckPointSaveData SaveData;

SaveData = new(self) class'CheckpointSaveData';
SaveData.PlayerStartName = Self.Name;
class'Engine'.static.BasicSaveObject(SaveData, "SaveGame.sav", true, 1);
}

defaultproperties
{
bPrimaryStart=False
}


When our kismet action is activated, the object attached to it will have its OnSaveCheckpoint function called. This is where we will do our saving. We are using the BasicSaveObject static function of the Engine class to save an object that contains the name of this player start. The object is another special class that looks like this.

class CheckpointSaveData extends Object;

var Name PlayerStartName;

defaultproperties
{

}


This class should be extended to include any other data that you need to save e.g. the players health.
We’re now ready to try this, run the compiler then open up the editor.

In your level, create a normal PlayerStart where the game should start and then create 2 CheckpointPlayerStart objects randomly in the level. Create 2 triggers and place each under a CheckpointPlayerStart.

Open up Kismet and add a touch event for each trigger. Select the first CheckpointPlayerStart and add and create a new Object Var for it in kismet. Add the SaveCheckpoint action and hook the trigger’s touch output to the SaveCheckpoint’s input and the SaveCheckpoint’s Player Start to the object variable we created using the CheckpointPlayerStart. It should now look like this.



Repeat the same to setup the 2nd trigger and checkpoint.

Now whenever you touch any of the triggers, the checkpoints name is saved into the SaveGame.sav file, now we need to implement the load feature. This needs to be done in your custom GameInfo sub-class. I did it by overriding its RestartPlayer function and adding anew function to load the saved data, which looks like this.

function CheckpointSaveData LoadCheckpointSaveData()
{
local CheckpointSaveData SaveData;

/// load checkpoint data from file if any
SaveData = new(self) class'CheckpointSaveData';
if(class'Engine'.static.BasicLoadObject(SaveData, "SaveGame.sav", true, 1))
{
`log("SaveData.PlayerStartName" @ SaveData.PlayerStartName);
return SaveData;
}
else
{
`warn("SaveData Couldn't load checkpoint, resuming default.");
return None;
}
}


//
// Restart a player.
//
function RestartPlayer(Controller NewPlayer)
{
local NavigationPoint startSpot;
local int TeamNum, Idx;
local array Events;
local SeqEvent_PlayerSpawned SpawnedEvent;
local CheckpointSaveData SaveData;
local CheckpointPlayerStart Checkpoint, SpawnedFromCheckpoint;
local SeqEvent_CheckpointSpawnedFrom SpawnedFromEvent;
local SeqVar_Object SpawnedFromSeqVarObject;

if( bRestartLevel && WorldInfo.NetMode!=NM_DedicatedServer && WorldInfo.NetMode!=NM_ListenServer )
{
`warn("bRestartLevel && !server, abort from RestartPlayer"@WorldInfo.NetMode);
return;
}
// figure out the team number and find the start spot
TeamNum = ((NewPlayer.PlayerReplicationInfo == None) || (NewPlayer.PlayerReplicationInfo.Team == None)) ? 255 : NewPlayer.PlayerReplicationInfo.Team.TeamIndex;

/// load save data
SaveData = LoadCheckpointSaveData();

/// 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
if(SaveData != None && PlayerController(NewPlayer) != None)
{
ForEach WorldInfo.AllNavigationPoints( class 'CheckpointPlayerStart', CheckPoint )
if( string(CheckPoint.Name) ~= string(SaveData.PlayerStartName) )
StartSpot = Checkpoint;
}


/// if we dont have a start spot from our save data, find one
if(StartSpot == None)
StartSpot = FindPlayerStart(NewPlayer, TeamNum);

// if a start spot wasn't found,
if (StartSpot == None)
{
// check for a previously assigned spot
if (NewPlayer.StartSpot != None)
{
StartSpot = NewPlayer.StartSpot;
`warn("Player start not found, using last start spot");
}
else
{
// otherwise abort
`warn("Player start not found, failed to restart player");
return;
}
}
// try to create a pawn to use of the default class for this player
if (NewPlayer.Pawn == None)
{
NewPlayer.Pawn = SpawnDefaultPawnFor(NewPlayer, StartSpot);
}
if (NewPlayer.Pawn == None)
{
`log("failed to spawn player at "$StartSpot);
NewPlayer.GotoState('Dead');
if ( PlayerController(NewPlayer) != None )
{
PlayerController(NewPlayer).ClientGotoState('Dead','Begin');
}
}
else
{
// initialize and start it up
NewPlayer.Pawn.SetAnchor(startSpot);
if ( PlayerController(NewPlayer) != None )
{
PlayerController(NewPlayer).TimeMargin = -0.1;
startSpot.AnchoredPawn = None; // SetAnchor() will set this since IsHumanControlled() won't return true for the Pawn yet
}
NewPlayer.Pawn.LastStartSpot = PlayerStart(startSpot);
NewPlayer.Pawn.LastStartTime = WorldInfo.TimeSeconds;
NewPlayer.Possess(NewPlayer.Pawn, false);
NewPlayer.Pawn.PlayTeleportEffect(true, true);
NewPlayer.ClientSetRotation(NewPlayer.Pawn.Rotation, TRUE);

if (!WorldInfo.bNoDefaultInventoryForPlayer)
{
AddDefaultInventory(NewPlayer.Pawn);
}
SetPlayerDefaults(NewPlayer.Pawn);

// activate spawned events
if (WorldInfo.GetGameSequence() != None)
{
WorldInfo.GetGameSequence().FindSeqObjectsByClass(class'SeqEvent_PlayerSpawned',TRUE,Events);
for (Idx = 0; Idx < Events.Length; Idx++)
{
SpawnedEvent = SeqEvent_PlayerSpawned(Events[Idx]);
if (SpawnedEvent != None &&
SpawnedEvent.CheckActivate(NewPlayer,NewPlayer))
{
SpawnedEvent.SpawnPoint = startSpot;
SpawnedEvent.PopulateLinkedVariableValues();
}
}

if(PlayerController(NewPlayer) != None)
{
/// activate events that are attached to the startpoint we just spawned from
WorldInfo.GetGameSequence().FindSeqObjectsByClass(class'SeqEvent_CheckpointSpawnedFrom',TRUE,Events);
for (Idx = 0; Idx < Events.Length; Idx++)
{
SpawnedFromEvent = SeqEvent_CheckpointSpawnedFrom(Events[Idx]);
if (SpawnedFromEvent != None)
{
SpawnedFromSeqVarObject = SeqVar_Object(SpawnedFromEvent.VariableLinks[0].LinkedVariables[0]);
SpawnedFromCheckpoint = CheckpointPlayerStart(SpawnedFromSeqVarObject.GetObjectValue());
if(SpawnedFromCheckpoint != None && SpawnedFromCheckpoint == StartSpot)
{
SpawnedFromEvent.CheckActivate(NewPlayer,NewPlayer);
SpawnedFromEvent.PopulateLinkedVariableValues();
}
}
}
}

}
}
}


The highlighted portions are the only ones that were changed. The LoadCheckpointSaveData function uses the BasicLoadObject function to load the file and populate the passed CheckpointSaveData object which will then contain the name if the CheckpointPlayerStart.

Since both bot spawning and human player spawning use this function, we only use the checkpoint for human players. We then iterate through all checkpoints in the current level and try to find one matching the name we just loaded. If we find one, we set it as the StartSpot and we’re good to go. The last highlighted portion will be explained below. We can now go ahead and test out out.

So now there is a bit of a problem, you can always go back to a previous checkpoint, which means the game could run actions that were meant for a previous checkpoint which I assume is not what we want. To fix that, we need a way of knowing when a checkpoint is used to resume the game. To do this, lets create a kismet event object that will be fired when a player is spawned from a checkpoint. It looks like this.

class SeqEvent_CheckpointSpawnedFrom extends SequenceEvent;

defaultproperties
{
ObjName="Checkpoint Used"
VariableLinks(1)=(ExpectedType=class'SeqVar_Object',LinkDesc="Checkpoint",bWriteable=False,MinVars=1,MaxVars=1)
}


We extend the SequenceEvent class and add a variable that will point to our CheckpointPlayerStart. The last highlighted portion of our RestartPlayer code takes care of activating the event only if it has the right CheckpointPlayerStart attached to it. It iterates through each sequence objects of type SeqEvent_CheckpointSpawnedFrom checking if the CheckpointPlayerStart that is attached to it is the same one that the player just spawned from.

We could then for example disable the first trigger by using this event on the second checkpoint which could look like this.



It’s worth noting that using this event to setup the level can get quite complex because with each checkpoint you need to take care of all preceding checkpoints. Perhaps a simpler solution would be to make sure previous checkpoints are inaccessible to the player.

No comments: