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.

Sunday, April 18, 2010

Walk your Pawn - Follow Up

If like me or Droganis from the UDK forums you would like your pawn to walk by default and only run on a key press, you can use this.

First follow the "Walk Your Pawn" tutorial if you haven't, then in your PlayerController's HandleWalking function, use this so that we only ask the Pawn to run when bRun is set i.e. when Shift is down.

function HandleWalking()
{
if ( Pawn != None )
Pawn.SetWalking( bRun == 0 );
}

Tuesday, March 2, 2010

Tutorial: Replicating a flashlight's state

I've been trying to understand replication so based on a question on the UDK forums I decided to implement a flash light whose on/off state can be replicated.

First, create a subclass of a SpotLightMovable which will act as the flash light.

class MyFlashLight extends SpotLightMovable
notplaceable;

defaultproperties
{
Begin Object name=SpotLightComponent0
LightColor=(R=255,G=0,B=0)
End Object
bNoDelete=FALSE
}



Next, we will modify our custom pawn by adding the flashlight as a variable.

var MyFlashLight FlashLight;

We also need a variable that we will replicate the flashlight's on/off state.

var repnotify bool bIsFlashlightOn; /// whether the flashlight is on or not

Then in the pawn's PostBeginPlay, we will attach the flash light to the pawn.

simulated function PostBeginPlay()
{
FlashLight = Spawn(class'MyFlashLight', self);
FlashLight.SetBase(self);
FlashLight.LightComponent.SetEnabled(self.default.bIsFlashlightOn);
super.PostBeginPlay();
}


And set the variable's default value in the pawn's default properties.

defaultproperties
{
...
bIsFlashlightOn=true
}


Now we need a way to switch the flashlight on and off. We will set this up via an exec function that will be called when the "R" key is pressed. I added my key bind to the DefaultInput.ini (You might need to make the file writable as its read-only by default).

Add this line at the end of the section marked as Primary default bindings

.Bindings=(Name="R",Command="ToggleFlashlight")

This says that when the R key is pressed the exec function ToggleFlashlight will be called. We will define the function in our cutsom PlayerController subclass.

exec function ToggleFlashlight()
{
MyPawn(Pawn).ToggleFlashlight();
}


This calls the PlayerController's Pawn's ToggleFlashlight function (a mouth-full I know). Now this is where the magic happens. Since we essentially have all the information we need to determine whether the flash light can be turned on or not (without consulting the server) we can do what is nessecary to switch on the flashlight. To do this, we need to mark the funtion with the simulated keyword.

simulated function ToggleFlashlight()
{
bIsFlashlightOn = !bIsFlashlightOn;
FlashLightToggled();
// if we are a remote client, make sure the Server toggles the flashlight
if( Role < Role_Authority )
{
ServerToggleFlashlight();
}
}

We start by changing the value of our variable that keeps track of the flashlight's state.

Next we check whether we are a client - in which case we need to tell the server what we have done by calling a server function. The server function essentially does the same thing as the client version, but on the server.

reliable server function ServerToggleFlashlight()
{
bIsFlashlightOn = !bIsFlashlightOn;
`log("ServerToggleFlashlight: " $ bIsFlashlightOn);
FlashLightToggled();
}


You'll notice that both functions call the FlashLightToggled function. This does the actual state switch.

simulated function FlashLightToggled()
{
if(bIsFlashlightOn)
{
FlashLight.LightComponent.SetEnabled(true);
}
else
{
FlashLight.LightComponent.SetEnabled(false);
}
}


This has taken care of the client that switched the flashlight on/off and the server, but what about the other clients. If you look at the definition of the bIsFlashlightOn variable, its defined as repnotify. What this does is whenever the variable changes, a special function ReplicatedEvent is called.

simulated event ReplicatedEvent(name VarName)
{
if (VarName == 'bIsFlashlightOn')
{
FlashLightToggled();
}
else
{
Super.ReplicatedEvent(VarName);
}
}


This function will be called on all the clients including the client that initiated the whole thing.

We also need a way to tell the server when to send the variable to its clients, this is done through a replication condition in our pawn class like this.

replication
{
// replicated properties
if ( bNetDirty )
bIsFlashlightOn;
}


That about covers it. Here are the full sources for each of the 3 classes we have touched on.

MyFlashlight.uc

class MFlashLight extends SpotLightMovable
notplaceable;


defaultproperties
{
Begin Object name=SpotLightComponent0
LightColor=(R=255,G=0,B=0) /// red so we can see the change
End Object
bNoDelete=FALSE
}

MyPawn.uc

class MyPawn extends UTPawn
config(Game)
notplaceable;

var MyFlashLight FlashLight;
var repnotify bool bIsFlashlightOn; /// whether the flashlight is on or not

replication
{
// replicated properties
if ( bNetDirty )
bIsFlashlightOn;
}

simulated function PostBeginPlay()
{
FlashLight = Spawn(class'KDFlashLight', self);
FlashLight.SetBase(self);
FlashLight.LightComponent.SetEnabled(self.default.bIsFlashlightOn);
super.PostBeginPlay();
}

/**
* Check on various replicated data and act accordingly.
*/
simulated event ReplicatedEvent(name VarName)
{
`log(VarName @ "replicated");
if (VarName == 'bIsFlashlightOn')
{
FlashLightToggled();
`log("bIsFlashlightOn replicated");
}
else
{
Super.ReplicatedEvent(VarName);
}
}

simulated function ToggleFlashlight()
{
bIsFlashlightOn = !bIsFlashlightOn;
`log("ToggleFlashlight: " $ bIsFlashlightOn);
FlashLightToggled();
// if we are a remote client, make sure the Server Set's toggles the flashlight
`log("Role:" @ Role);
if( Role < Role_Authority )
{
ServerToggleFlashlight();
}
}

reliable server function ServerToggleFlashlight()
{
bIsFlashlightOn = !bIsFlashlightOn;
`log("ServerToggleFlashlight: " $ bIsFlashlightOn);
FlashLightToggled(!bIsFlashlightOn);
}

simulated function FlashLightToggled()
{
if(bIsFlashlightOn)
{
FlashLight.LightComponent.SetEnabled(true);
}
else
{
FlashLight.LightComponent.SetEnabled(false);
}
}

defaultproperties
{
bIsFlashlightOn=true
}

MyPlayerController.uc

class MyPlayerController extends UTPlayerController;

exec function ToggleFlashlight()
{
MyPawn(Pawn).ToggleFlashlight();
}

state Dead
{
function EndState(name NextStateName)
{
SetBehindView(default.bBehindView);
}
}

defaultproperties
{
bBehindView = true
}

Tuesday, February 9, 2010

Walk your Pawn

So UTPawn, which many UDK users are subclassing does not walk, it only runs - has some place to be I guess. This short tutorial will show you how to make your Pawn walk.

When the PlayerController is calculating its movement, it calls its HandleWalking function to check if the Pawn wants to walk. In its definition, you will notice a check against a variable bRun.

function HandleWalking()
{
if ( Pawn != None )
Pawn.SetWalking( bRun != 0 );
}


bRun is an input variable whose value is set when you hold down the Left Shift key. A search for bRun in your UTInput.ini will give you this.

Bindings=(Name="Walking",Command="Button bRun")

and a search for walking will give you this.

Bindings=(Name="LeftShift",Command="Walking")

If (bRun != 0) i.e. Pawn wants to walk, the Pawn's SetWalking funtion is called. Looking at this function's definition in UTPawn, you will notice that its blank and doesnt do anything. Assuming you have a subclass of UTPawn as your game's default pawn, you need to override the SetWalking function. You should end up with something similar to this.

class Mypawn extends UTPawn;

event SetWalking( bool bNewIsWalking )
{
super(Pawn).SetWalking(bNewIsWalking);
}

defaultproperties
{
}


Super refers to our parent class which in this case is UTPawn but by adding Pawn in parenthesis, we're actually calling the the SetWalking function in the Pawn class and not in UTPawn.

If you try this out, you will notice that what we have done is just make the character move slower but he still looks like he is running. To fix this, we have to play around with the AnimTree so fire up the editor. What we want to do is use a different animation when the pawn is walking. Walking reduces the pawn's speed/velocity.

Assuming you are using the default Anim Tree, add an UTAnimBlendBySpeed node like in the image below.



You will notice that I've set the minimum speed to 220 and the maximum speed to 440. By default, the UTPawn's maximum speed is 440 (GroundSpeed=440.0 in UTPawn.uc default properties) and its walking speed is half of this - so 220 (WalkingPct=+0.5 in Pawn.uc). So basically when the pawn is at full speed, the fast branch of the anim node is used entirelyand when walking the slow branch is used and when in the middle, the animations are blended together.

Thats it, enjoy walking.

Friday, December 18, 2009

Scripting a simple Game in UDK

Let’s call our new game SGGame – with SG standing for standing for Simple Game. Prefix your game as you please.

First create a directory in your \Development\Src\ and give it the same name i.e. SGGame. In it, create a directory called Classes.

Next, let’s create a file in the Classes directory and call it SGGame.uc. Open the file in your favorite text editor and type in the code below.

class SGGame extends UTGame

config(Game);

defaultproperties

{

}

This basically defines a class called SGGame that extends the UTGame class. Generally, your game will extend a subclass of the GameInfo class. By extending the UTGame (also a subclass of GameInfo) class we gain a lot of functionality that we would require for an FPS game.

Now let’s compile our game just to make sure everything is fine so far. To do this, we want to run the UDK executable with a special command line argument – make. To make this easier as we will be using it a lot, we will make a shortcut on the desktop for this.

Go into the \Binaries\Win32 directory and copy the UDK.exe file. Right click on your desktop and click on Paste Shortcut. Rename the shortcut to Compile UDK Scripts. Right click on the shortcut and go to its properties. In the target section, add make.

Mine looks like this - X:\UDK-2009-12\Binaries\Win32\UDK.exe make

Running this right now will not compile your new game’s scripts because UDK does not yet know about our new game. To do this, we need to modify one of the .ini files. Go to \UTGame\Config and open DefaultEngine.ini in your text editor.

NOTE: By default, the file is read only; you need to make it writable from its properties to make these changes.

Scroll down to the [UnrealEd.EditorEngine] section and add the following line.

+EditPackages=SGGame

The UnrealEd.EditorEnginesection is used by the compiler to determine which packages exist and need to be compiled when they change.

The + (plus) basically tells the engine to add the line in the generated UTEngine.ini file.

NOTE: Whenever you make a change to the DefaultEngine.ini the engine will generate a new UTEngine.ini file on the next run. More information on this can be found in the Configuration File section of UDN.

Double click on the compiler shortcut to compile our game’s scripts. If there are any syntax errors in the file, you will get the warnings/errors in the compilation window. It should however compile without any problems.

It’s time to see what we’ve done so far. We will load up the examplemap map for this. We’ll use another shortcut and command line argument to run the map with our game. Copy and paste a UDK shortcut like we did before. Call this Simple Game(or whatever else your prefer). Open its properties and add ExampleMap?Game=SGGame.SGGame

This basically tells UDK to load the map called ExampleMap and run the game called SGGame in the SGGame package.

There you go; you have a basic game up and running.

The UDK bandwagon

If you are a game development enthusiast/hobbyist/indie like me, you've probably heard of UDK. If not, get out from under that rock and head staright to http://www.udk.com. It's pretty much an Indies dream come true.

I've been using it since early November and its a hell of a steep learning curve but at the end of the day, its Unreal. I've joined a small indie team as an Unreal Scripter to work on a project called Nothern Island 1983.

The feature list is very impressive, including speed tree, face fx, the unreal editor in all its glory, documentation via the UDN and recently released video tutorials. Christmas really did come early this year and the Indie(read me) has to stop making excuses.

Tuesday, April 8, 2008

Brute Terrain texture splatting

After getting texture splatting working I'm seriously re-thinking implementing my "Brute force chunk NO-LOD terrain." I was in such a rush to do it that I didn't really think about it.

Using nebula's fixed function render path, I need a pass for each detail texture plus an additional pass for the base texture and the performance is not too bad with 3 detail textures and a base texture. Since I was developing using the fixed function render path, it didn't occur to me that on shader model 2.0 hardware, I only need 1 pass to blend all my detail textures (4 so far) plus my base texture. Since the whole idea was to improve the look of distant geometry it really doesn't make sense anymore (see below). Plus I can use a single image to store alpha maps for 4 detail textures in each of its 4 channels.

About my splatting implementation; Initially, I wanted to blend the base texture based on the camera's distance from the particular point being rendered. As a first step to that, I tried a fixed blend value of 0.5 which looked really good. I then used the camera's distance method and distant geometry didn't look as good as with a fixed value. So I decided to go with the fixed value version.

See for yourself





I'm pretty happy with the results but any criticism/comments are welcome