Sunday, September 16, 2012

Side-scroller camera in UDK

I’m currently working on a side-scroller game and just want to share how I implemented my side-scroller camera.

 NOTE: This all assumes you already have a PlayerController and a Pawn with a skeletal mesh already set-up.

 According to the UDK documentation here - http://udn.epicgames.com/Three/CameraTechnicalGuide.html, you can implement a camera in one of 2 ways.

 The first and also quickest is to override your Pawn’s CalcCamera function and calculate the views rotation and location.

 The second is by overriding the camera class which is said to be more flexible and allows you to have fancy things like camera animations and effects. This is what I did by sub-classing the Camera class.

Lets start by creating this sub-class. We need to figure out the camera's location.

1. Create a vector that points to the right of the characters position, this is basically a vector pointing down Y in world coordinates.

2. Offset this unit vector by multiplying it by the number of units we want. This moves the camera's position to the right of the character by said units.

3. Add any other offsets we want in the X and Z directions, in my case I want my camera slightly moved up.

This is achieved by defining a vector with these offsets - FollowCamDist which is initialised as (X=0.f,Y=400.f,Z=100.f)

Once we have this position, we need to rotate the camera to look at the character. This is done by getting the direction from the camera's position to the character's position like so:

 CamDir = (OutVT.Target.Location - CamLocation);

Then converting this to a Rotator and assigning it as the camera's rotation.

And here is the complete camera class. The stuff discussed above is what is within the highlighted block. The rest of the function is picked from the base Camera class.


class DecadePlayerCamera extends Camera;

var Vector FollowCamDist;

/**
 * Query ViewTarget and outputs Point Of View.
 *
 * @param   OutVT      ViewTarget to use.
 * @param   DeltaTime   Delta Time since last camera update (in seconds).
 */
function UpdateViewTarget(out TViewTarget OutVT, float DeltaTime)
{
 local vector  HitLocation, HitNormal, CamDir, CamLocation;
 local Actor   HitActor;
 local CameraActor CamActor;
 local TPOV   OrigPOV;
 local Pawn          TPawn;

 // Don't update outgoing viewtarget during an interpolation
 if( PendingViewTarget.Target != None &&
        OutVT == ViewTarget && BlendParams.bLockOutgoing )
 {
  return;
 }

 // store previous POV, in case we need it later
 OrigPOV = OutVT.POV;

 // Default FOV on viewtarget
 OutVT.POV.FOV = DefaultFOV;

 // Viewing through a camera actor.
 CamActor = CameraActor(OutVT.Target);
 if( CamActor != None )
 {
  CamActor.GetCameraView(DeltaTime, OutVT.POV);

  // Grab aspect ratio from the CameraActor.
  bConstrainAspectRatio = bConstrainAspectRatio ||
                               CamActor.bConstrainAspectRatio;
  OutVT.AspectRatio  = CamActor.AspectRatio;

  // See if the CameraActor wants to override the PostProcess settings used.
  CamOverridePostProcessAlpha = CamActor.CamOverridePostProcessAlpha;
  CamPostProcessSettings = CamActor.CamOverridePostProcess;
 }
 else
 {
  TPawn = Pawn(OutVT.Target);
  // Give Pawn Viewtarget a chance to dictate the camera position.
  // If Pawn doesn't override the camera view, then we proceed with our own defaults
  if( TPawn == None || !TPawn.CalcCamera(DeltaTime, OutVT.POV.Location,
            OutVT.POV.Rotation, OutVT.POV.FOV) )
  {

   /// make a unit vector that points down the Y/left axis seems that for the camrea, X is forward - perpediculr to pawn rotation
            CamDir = vect(0.f, 1.f, 0.f);

            /// displace vector by Z offset
            CamDir *= FollowCamDist.Y;

            /// offset direction by follow distance
            CamLocation = CamDir + OutVT.Target.Location;

            CamLocation.X += FollowCamDist.X;
            CamLocation.Z += FollowCamDist.Z;

            /// handle collisions @todo: fix player mesh intersection
            HitActor = Trace(HitLocation, HitNormal, CamLocation,
                OutVT.Target.Location, false, vect(12,12,12));

            //if (HitActor != none)
            //    CamLocation = HitLocation;

            /// set view position and location
            OutVT.POV.Location = CamLocation;

            /// get the new direction
            CamDir = (OutVT.Target.Location - CamLocation);
            OutVT.POV.Rotation = Rotator(CamDir);
  }
 }

    ApplyCameraModifiers(DeltaTime, OutVT.POV);
 //`log( WorldInfo.TimeSeconds  @ GetFuncName() @ OutVT.Target @ OutVT.POV.Location @ OutVT.POV.Rotation @ OutVT.POV.FOV );
}

defaultproperties
{
 FollowCamDist=(X=0.f,Y=400.f,Z=100.f)
}

Now lets look at how to make the character only walk along the X axis. Input is translated into movement in the PlayerWalking state's PlayerMove function.



...
// Update acceleration.
NewAccel = PlayerInput.aForward*X + PlayerInput.aStrafe*Y;
NewAccel.Z = 0;
NewAccel = Pawn.AccelRate * Normal(NewAccel);
...

What we want is to use the strafe input to move the player. We'll take the player's X direction and multiply it by the strafe input. Then we'll zero out the Y movement (Z is already zeroed out)


...
// Update acceleration.
NewAccel = PlayerInput.aForward*X;
NewAccel.Z = 0;
NewAccel.Y = 0;
NewAccel = Pawn.AccelRate * Normal(NewAccel);
...

We also need to prevent the player from turning around the Z axis, this we do in the PlayerController's UpdateRotation function by removing the line that uses the aTurn input to update the Yaw rotation.


...
DeltaRot.Yaw = PlayerInput.aTurn; // turning is not allowed
...

And here is the full code listing for the PlaeyrController class

NOTE: the character runs backwards but likely what we want is to have the player change direction when the A key or equivalent is used. I'm still working on this and will post an update when I can.

As always questions, comments and suggestions are welcome.




class DecadePlayerController extends AcventurePlayerController;

function UpdateRotation( float DeltaTime )
{
 local Rotator DeltaRot, newRotation, ViewRotation;

 ViewRotation = Rotation;
 if (Pawn!=none)
 {
  Pawn.SetDesiredRotation(ViewRotation);
 }

 // Calculate Delta to be applied on ViewRotation
 DeltaRot.Yaw = 0; // turning is not allowed
 DeltaRot.Pitch = PlayerInput.aLookUp;

 ProcessViewRotation( DeltaTime, ViewRotation, DeltaRot );
 SetRotation(ViewRotation);

 ViewShake( deltaTime );

 NewRotation = ViewRotation;
 NewRotation.Roll = Rotation.Roll;

 if ( Pawn != None )
  Pawn.FaceRotation(NewRotation, deltatime);
}

// Player movement.
// Player Standing, walking, running, falling.
state PlayerWalking
{
    ignores SeePlayer, HearNoise, Bump;

 function PlayerMove( float DeltaTime )
 {
  local vector   X,Y,Z, NewAccel;
  local eDoubleClickDir DoubleClickMove;
  local rotator   OldRotation;
  local bool    bSaveJump;

  if( Pawn == None )
  {
   GotoState('Dead');
  }
  else
  {
   GetAxes(Pawn.Rotation,X,Y,Z);

   // Update acceleration.
            //@TODO: use aStrafe * X and figure out how to change pawns direction
   NewAccel = PlayerInput.aStrafe * X;
   NewAccel.Z = 0;
            NewAccel.Y = 0;
   NewAccel = Pawn.AccelRate * Normal(NewAccel);

   if (IsLocalPlayerController())
   {
    AdjustPlayerWalkingMoveAccel(NewAccel);
   }

   DoubleClickMove = PlayerInput.CheckForDoubleClickMove( DeltaTime/WorldInfo.TimeDilation );

   // Update rotation.
   OldRotation = Rotation;
   UpdateRotation( DeltaTime );
   bDoubleJump = false;

   if( bPressedJump && Pawn.CannotJumpNow() )
   {
    bSaveJump = true;
    bPressedJump = false;
   }
   else
   {
    bSaveJump = false;
   }

   if( Role < ROLE_Authority ) // then save this move and replicate it
   {
    ReplicateMove(DeltaTime, NewAccel, DoubleClickMove, OldRotation - Rotation);
   }
   else
   {
    ProcessMove(DeltaTime, NewAccel, DoubleClickMove, OldRotation - Rotation);
   }
   bPressedJump = bSaveJump;
  }
 }
Begin:
}

defaultproperties
{
    CameraClass=class'SideScrollerCamera'
}

Wednesday, January 11, 2012

Saving objects using BasicSaveObject - Enhanced


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:
  1. A new game was requested or 
  2. 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'
}