Creating a Side-scrolling Game with UDK
I wanted to find a way to introduce others to UDK in a way that is fun and simple. I discovered some notes I took while prototyping a simple UDK side-scroller, and turned them into this little guide. This guide attempts to lay out the groundwork for developing a simple game with the Unreal Development Kit. It will help readers understand the structure of a basic UDK game, its elements and the relationship between them. It does not, however, detail creating levels or assets. For that, please refer to the excellent 3D Buzz video tutorials available on the UDK website.
Update:
The complete source code and level file created during this guide is available here: http://labs.vectorform.com/files/Tomato.zip
- Introduction to the UDK environment
- Creating a Side-scroller
UDK, Unreal Script and Unreal Ed
Development with UDK consists of two parts:
- Writing Unreal Script code to create game objects and low-level game logic — such as AI, multiplayer behavior, game rules, and so on.
- Using Unreal Ed to design the game visually, using the Unreal Script objects you wrote. Unreal Ed is a powerful tool for designing levels and UI, managing assets, adding level-specific logic (switches, portals), and much more.
I highly recommend that you learn Unreal Ed before jumping into code — it will help you understand the terminology used throughout UDK. I recommend watching the 3DBuzz video tutorials on using the level editor and Kismet (a visual editor for high-level gameplay logic), which can be found here: http://udn.epicgames.com/Three/VideoTutorials.html. You will even learn to create a 3rd person game as you follow along!
UDK comes with the Unreal Engine codebase and the full source code for the Unreal Tournament 3 demo game. If you have experience with C++, C#, Objective C, Java or similar, I find that reading the codebase is the fastest way to learn Unreal Script.
Getting UDK
Head over to http://www.udk.com/download and grab the latest release. I have the September 2011 beta but yours may be newer. Install UDK.
Additionally, you will need the following:
- A text editor. Unreal Script (.uc) source files can be edited with any text editor. I recommend ConTEXT with the UnrealScript language file, or jEdit because you will get the benefit of syntax highlighting right out of the box.
- UnCodeX. It is a stand-alone project navigator for UDK source files — it scans all the Unreal Script files you have, and presents you with a well-organized class tree. You get a major benefit of having an IDE for free. Get it here: http://udn.epicgames.com/Three/UnCodeX.html (make sure you carefully follow the instructions under the “Installation” heading)
Third-party IDEs are also available for Unreal Script development, but will not be covered here.
The Directory Structure
Now that you have UDK installed, let’s look at the various files and directories that make up UDK. Open up the UDK install directory (usually something like C:/UDK/UDK-2011-09). The two directories that are important for us are Development and UDKGame:
[UDK install directory]/Development/Src contains the Unreal Script files that make up the UDK code base (more on this below).
[UDK install directory]/UDKGame contains the game’s levels, textures, meshes, sounds and scripts — all compiled into packages. Levels have a .udk extension, assets are .upk and the script packages are .u.
More details about the directory structure can be found on Epic’s Unreal Development Network website: http://udn.epicgames.com/Three/DevelopmentKitFirstScriptProject.html#UDK directory layout
The Code Base
This code base is a large and complex hierarchy of Unreal Script classes. Networking, vehicles, AI behavior, lights, UI elements — are just some of the examples of classes available to you. Each script in the code base (file with .uc extension) bears the name of the class it defines. Each class derives from another class, and eventually from the base class Object. The scripts are organized into directories bearing the name of the package to which they belong (Engine, Core, UTGame, etc). A package is an assembly created when you compile your scripts.
It is important to note at this point that all objects in the game world are derived from the class Actor. Actors include light sources, players, weapons, rocks, switches, waypoints. Actors do not need to be visible. In a typical game, most of your development time will be spent on creating Actor classes.
Creating a New Game Type
I will name the game Total Maximum Torque, or “Tomato” for short. Create the following directories for your source files:
- Create [UDK install directory]/Development/Src/Tomato
- Create [UDK install directory]/Development/Src/Tomato/Classes
We will create a custom game type by defining our own GameInfo subclass. GameInfo is just an Actor that is created once when we load a map, and handles the game rules (spawning players, time limits, scoring, etc). You might think of it as an entry point to your game, because it determines what other objects will be used in the game. How does UDK know which GameInfo class to load? We have to tell it either in the URL or in the game’s .ini file (more on all this later).
To save some work, we will reuse Unreal Tournament 3′s GameInfo class. It is called UTGame, and it eventually extends GameInfo. If the game were drastically different from UT, we would subclass GameInfo directly, but our game will use a lot of Unreal Tournament’s existing functionality. Create and open [UDK install directory]/Development/Src/Tomato/Classes/TomatoGameInfo.uc. Add the following code:
class TomatoGameInfo extends UTGame;
DefaultProperties
{
MapPrefixes(0)="TMT"
DefaultPawnClass=Class'Tomato.TomatoPawn'
PlayerControllerClass=Class'Tomato.TomatoPlayerController'
BotClass=class'Tomato.TomatoBot'
HUDType=class'Tomato.TomatoHUD'
bUseClassicHUD = true
DefaultInventory(0)=none
DefaultInventory(1)=none
}
First, we declared our class name, and the name of the class it’s extending.
The DefaultProperties block contains default/initial values for class properties. We are subclassing, and these class properties already exist — we are just changing their default values. Our game will have its own HUD, bot class, controls, and pawn class — in GameInfo we specify that we want to use our classes instead of the classes defined in UTGame.
- • A Pawn is an actor that can be controlled by players or AI. Typically, it is some kind of a mesh that can collide with the world, pick up items, take damage, and use weapons. We want to have our own pawn that can only move in two dimensions, so we tell our GameInfo to use it as the standard pawn class.
- • The PlayerController provides a way for human players to control pawns. We want to define our own 2D controls, so we will have GameInfo use our class.
- • The Bot class is similar to the PlayerController in that it controls pawns. However, bot use AI instead of player input.
- • A custom HUD is needed to display our score
Creating 2D Player Controls
We’re going to take a little shortcut. Epic provided us with excellent code for a side-scrolling camera on the UDN website.
http://udn.epicgames.com/Three/CameraTechnicalGuide.html#Example Side-Scrolling Camera
Go ahead and copy the side-scrolling camera code Epic provided, and put it into the respective files (UDNPawn.uc and UDNPlayerController.uc in the Tomato source directory).
UDNPawn derives from UTPawn. It overrides CalcCamera to fix camera position to the plane Y=CamOffsetDistance (0 by default), and to restrict camera rotation to face in the direction of the positive Y-axis. It also overrides GetBaseAimRotation to restrict the direction of aiming. UDNPlayerController modifies the player controls to only rotate and move the player’s pawn within the 2D plane.
UDNPawn.uc:
class UDNPawn extends UTPawn;
var float CamOffsetDistance; //Position on Y-axis to lock camera to
//override to make player mesh visible by default
simulated event BecomeViewTarget( PlayerController PC )
{
local UTPlayerController UTPC;
Super.BecomeViewTarget(PC);
if (LocalPlayer(PC.Player) != None)
{
UTPC = UTPlayerController(PC);
if (UTPC != None)
{
//set player controller to behind view and make mesh visible
UTPC.SetBehindView(true);
SetMeshVisibility(UTPC.bBehindView);
UTPC.bNoCrosshair = true;
}
}
}
simulated function bool CalcCamera( float fDeltaTime, out vector out_CamLoc, out rotator out_CamRot, out float out_FOV )
{
out_CamLoc = Location;
out_CamLoc.Y = CamOffsetDistance;
out_CamRot.Pitch = 0;
out_CamRot.Yaw = 16384;
out_CamRot.Roll = 0;
return true;
}
simulated singular event Rotator GetBaseAimRotation()
{
local rotator POVRot;
POVRot = Rotation;
if( (Rotation.Yaw % 65535 > 16384 && Rotation.Yaw % 65535 < 49560) ||
(Rotation.Yaw % 65535 < -16384 && Rotation.Yaw % 65535 > -49560) )
{
POVRot.Yaw = 32768;
}
else
{
POVRot.Yaw = 0;
}
if( POVRot.Pitch == 0 )
{
POVRot.Pitch = RemoteViewPitch << 8;
}
return POVRot;
}
defaultproperties
{
CamOffsetDistance=0.0
}
UDNPlayerController.uc:
class UDNPlayerController extends UTPlayerController;
state PlayerWalking
{
ignores SeePlayer, HearNoise, Bump;
function ProcessMove(float DeltaTime, vector NewAccel, eDoubleClickDir DoubleClickMove, rotator DeltaRot)
{
local Rotator tempRot;
if( Pawn == None )
{
return;
}
if (Role == ROLE_Authority)
{
// Update ViewPitch for remote clients
Pawn.SetRemoteViewPitch( Rotation.Pitch );
}
Pawn.Acceleration.X = -1 * PlayerInput.aStrafe * DeltaTime * 100 * PlayerInput.MoveForwardSpeed;
Pawn.Acceleration.Y = 0;
Pawn.Acceleration.Z = 0;
tempRot.Pitch = Pawn.Rotation.Pitch;
tempRot.Roll = 0;
if(Normal(Pawn.Acceleration) Dot Vect(1,0,0) > 0)
{
tempRot.Yaw = 0;
Pawn.SetRotation(tempRot);
}
else if(Normal(Pawn.Acceleration) Dot Vect(1,0,0) < 0)
{
tempRot.Yaw = 32768;
Pawn.SetRotation(tempRot);
}
CheckJumpOrDuck();
}
}
function UpdateRotation( float DeltaTime )
{
local Rotator DeltaRot, ViewRotation;
ViewRotation = Rotation;
// Calculate Delta to be applied on ViewRotation
DeltaRot.Yaw = Pawn.Rotation.Yaw;
DeltaRot.Pitch = PlayerInput.aLookUp;
ProcessViewRotation( DeltaTime, ViewRotation, DeltaRot );
SetRotation(ViewRotation);
}
defaultproperties
{
}
We now have UDNPlayerController, UDNPawn and TomatoGameInfo classes. In TomatoGameInfo we specified that our player controller will be TomatoPlayerController and the pawn class will be TomatoPawn. Let’s create those classes, and base them on the UDN classes we created — this will later allow us to add our own functionality without becoming distracted by Epic’s sidescroller camera code.
Create the file TomatoPlayerController.uc with the following code:
class TomatoPlayerController extends UDNPlayerController dependson(UDNPlayerController);
Likewise, create TomatoPawn.uc with the following:
class TomatoPawn extends UDNPawn
dependson(UDNPawn);
simulated function Tick(float DeltaTime)
{
local vector tmpLocation;
super.Tick(DeltaTime);
tmpLocation = Location;
tmpLocation.Y = 500;
SetLocation(tmpLocation);
}
function bool Dodge(eDoubleClickDir DoubleClickMove)
{
return false;
}
defaultproperties
{
ControllerClass=class'Tomato.TomatoBot'
bCanStrafe=false
MaxStepHeight=50.0
MaxJumpHeight=96
JumpZ=550
}
In the code above, we are forcing any of our pawns in the game (players and bots) to be on the Y=500 plane. We are also increasing the jump and step heights to make terrain easier to navigate and more similar to a platformer.
Create TomatoBot.uc with this code:
class TomatoBot extends UTBot;
We will leave TomatoBot empty for now. We will create bot AI at a later time.
Create TomatoHUD.uc with this code:
class TomatoHUD extends UTHUD;
We’ve now added all the classes that TomatoGameInfo references. Let’s compile them!
Compiling Your Game
We must compile our UnrealScript classes into a package in order to use them in the game. UDK maintains a list of all packages that need to be compiled in an .ini file — so we must add our package to that list. Open [UDK install directory]/UDKGame/Config/DefaultEngine.ini. In the section [UnrealEd.EditorEngine], add the following line, and save the file:
+ModEditPackages=Tomato
Run the Unreal Frontend tool ([UDK install directory]/Binaries/UnrealFrontend.exe). We will be using this tool to invoke the compiler. Ensure that all your Unreal Script files are saved, and close UDK Editor if it is open. Select “Compile Scripts” from the ”Scripts” menu.
You should not receive any errors or warnings related to the Tomato scripts.
See the Compiling section on the UDN website if you need additional details.
Creating A Level and Running The Game
Save your level as TMT-Zone1.udk
Adding Coin Pickups
Now that we have movement and camera controls working, it’s time to give the player something to do. We will add some coins that can be collected. Once all coins are collected, the game is won.
Adding a Coin Mesh and Making Coins Disappear When Collected
Save your level (File->Save All)
We need to add the ability to track the number of coins collected, and to set the total number of coins needed to win the level. This can all be done in Unreal Kismet, but we would have to re-create this logic for each level in our game. Additionally, it does not serve as a good demonstration of a flexible architecture for a complete game. I am going to show you how to create communication between your level and your Unreal Script code.
What should be the responsibilities of the level (or level designer) in our game?
- The level should tell our script how many coins the player needs to complete this level.
- The level should notify our script when a player collected a coin.
- The level should tell our script if a player earned points.
In the previous section we used Unreal Kismet to call the Destroy action when a coin was touched. Essentially, we want to do the same thing, but instead of Destroy we want to create our own actions. When the action is performed, we’d like to call some function in our Unreal Script. To accomplish this, we will use SequenceActions.
Create a new Unreal Script file named SeqAct_TMTCollectCoin.uc in our source directory. The code is very simple:
class SeqAct_TMTCollectCoin extends SequenceAction;
DefaultProperties
{
ObjName="Collect Coin"
ObjCategory="Tomato"
HandlerName="CollectCoin"
}
We are creating an action with the readable name “Collect Coin” in the category “Tomato” (defining a category allows us to group together all “Tomato” actions in Kismet’s context menus). We are also defining the name of the handler function for this action.
Let’s create the rest of the actions first, and then move on to creating handlers for them. Create SeqAct_TMTSetCoinsNeeded.uc with the following:
class SeqAct_TMTSetCoinsNeeded extends SequenceAction;
var() int NumCoins;
DefaultProperties
{
ObjName="Set Coins Needed"
ObjCategory="Tomato"
HandlerName="SetCoinsNeeded"
NumCoins = 0
VariableLinks(1)=(ExpectedType=class'SeqVar_Int', LinkDesc="NumCoins", bWriteable=true, PropertyName=NumCoins)
}
The above-action is an action that takes a parameter. We first declare a local variable NumCoins. We then set a VariableLink for the NumCoins variable — this creates a writable node in Kismet to which an integer value can be attached. You will see how to pass values later.
Create SeqAct_TMTGivePoints.uc with the following code:
class SeqAct_TMTGivePoints extends SequenceAction;
var() int NumPoints;
DefaultProperties
{
ObjName="Add Points"
ObjCategory="Tomato"
HandlerName="AddPoints"
NumPoints = 0
VariableLinks(1)=(ExpectedType=class'SeqVar_Int', LinkDesc="NumPoints", bWriteable=true, PropertyName=NumPoints)
}
The “Add Points” action also takes an integer parameter — we use it to specify the number of points to add.
Time to create handlers for our new actions. In TomatoPlayerController.uc, modify the class definition in the beginning of the file to add three dependson directives for our actions. This will ensure that the actions are compiled before our player controller, so our player controller can use them. Modify TomatoPlayerController as follows:
class TomatoPlayerController extends UDNPlayerController
dependson(UDNPlayerController)
dependson(SeqAct_TMTGivePoints)
dependson(SeqAct_TMTSetCoinsNeeded)
dependson(SeqAct_TMTCollectCoin);
Next, we want to keep track of the number of points, the number of coins collected and the required number of coins — so we need some local variables. Right below, declare the following three integer variables:
var() int TMTPoints; var() int TMTCoinsCollected; var() int TMTCoinsNeededThisLevel;
Before we jump into creating the handlers, let’s create a function to test if the player has enough coins to win the game:
exec function TestWinningConditions()
{
`log("TestWinningConditions() : Checking winning conditions");
`log("TestWinningConditions() : Collected: "$TMTCoinsCollected);
`log("TestWinningConditions() : Needed: "$TMTCoinsNeededThisLevel);
if(TMTCoinsCollected > 0 && TMTCoinsNeededThisLevel > 0 && TMTCoinsCollected >= TMTCoinsNeededThisLevel)
{
`log("TestWinningConditions() : Victory!");
}
else
{
`log("TestWinningConditions() : Go collect more coins!");
}
}
The above-code should be self-explanatory, with the exception of exec function. An exec function can be invoked by typing its name in the console. Executing the function directly from console will make debugging winning conditions a bit easier.
Time to create handlers. To create a handler for an action, we must declare a method named with the value of the action’s HandlerName property. Also, the method will take the action as a parameter. Create the following handler functions in TomatoPlayerController:
function CollectCoin(SeqAct_TMTCollectCoin MyAction)
{
`log("CollectCoin() : Collected coin");
TMTCoinsCollected += 1;
TestWinningConditions();
}
function SetCoinsNeeded(SeqAct_TMTSetCoinsNeeded MyAction)
{
`log("SetCoinsNeeded() : Set needed coins to "$MyAction.NumCoins);
TMTCoinsNeededThisLevel = MyAction.NumCoins;
}
function AddPoints(SeqAct_TMTGivePoints MyAction)
{
`log("AddPoints() : Adding points: "$MyAction.NumPoints);
TMTPoints += MyAction.NumPoints;
}
Finally, let’s properly initialize our properties:
defaultproperties
{
TMTPoints = 0
TMTCoinsCollected = 0
TMTCoinsNeededThisLevel = 1
}
Expand to see the complete TomatoPlayerController.uc file:
class TomatoPlayerController extends UDNPlayerController
dependson(UDNPlayerController)
dependson(SeqAct_TMTGivePoints)
dependson(SeqAct_TMTSetCoinsNeeded)
dependson(SeqAct_TMTCollectCoin);
var() int TMTPoints;
var() int TMTCoinsCollected;
var() int TMTCoinsNeededThisLevel;
exec function TestWinningConditions()
{
`log("TestWinningConditions() : Checking winning conditions");
`log("TestWinningConditions() : Collected: "$TMTCoinsCollected);
`log("TestWinningConditions() : Needed: "$TMTCoinsNeededThisLevel);
if(TMTCoinsCollected > 0 && TMTCoinsNeededThisLevel > 0 && TMTCoinsCollected >= TMTCoinsNeededThisLevel)
{
`log("TestWinningConditions() : Victory!");
}
else
{
`log("TestWinningConditions() : Go collect more coins!");
}
}
function CollectCoin(SeqAct_TMTCollectCoin MyAction)
{
`log("CollectCoin(): Collected coin");
self.TMTCoinsCollected += 1;
TestWinningConditions();
}
function SetCoinsNeeded(SeqAct_TMTSetCoinsNeeded MyAction)
{
`log("SetCoinsNeeded() : Set needed coins to "$MyAction.NumCoins);
TMTCoinsNeededThisLevel = MyAction.NumCoins;
}
function AddPoints(SeqAct_TMTGivePoints MyAction)
{
`log("AddPoints() : Adding points: "$MyAction.NumPoints);
TMTPoints += MyAction.NumPoints;
}
defaultproperties
{
TMTPoints = 0
TMTCoinsCollected = 0
TMTCoinsNeededThisLevel = 1
}
How does the engine know to call the handlers for each action in the Player Controller class, and not in some other class? We will later need to specify, in Kismet, that the Target of the action is the Player Controller — just like we set the coin as a target of the Destroy action.
Things seem to finally be coming together. Ensure that UDK Editor is closed, save all files, and compile the scripts using Unreal Frontend (select Script->Compile scripts).
Enabling Debug Output
Before we fire up UDK, we need to enable the debug console. This will allow us to see the `log() output. The idea is to add a “-log” command-line parameter when launching the UDK editor. I set it up this way:
- Right-click UDKLift.exe (it’s in C:\UDK\UDK-2011-09\Binaries) and choose “Create shortcut”.
- In the Properties of the short-cut, modify the “Target” as follows:
C:\UDK\UDK-2011-09\Binaries\UDKLift.exe editor -log
You can now use this shortcut to launch the UDK editor with a debug log window.
Using Actions from Unreal Kismet
Once all scripts have compiled successfully, run UDK Editor with the debug log window enabled. Open up the level we worked on previously, TMT-Zone1.udk.
Take your time and design a level to your liking (if you haven’t already done so). Have a look at the 3D Buzz video tutorials for information on the various level design features available in UDK — things like destructible objects, switches, doors and terrain can be a great addition to your game! And add some more coins while you’re at it.
Creating a Custom HUD
We will be creating a very basic HUD. Modify TomatoHUD.uc as follows:
class TomatoHUD extends UTHUD;
function DrawHealthBar(float percentFull)
{
local int currentBarWidth;
local int maxBarWidth;
local int barHeight;
local int barPositionX;
local int barPositionY;
local int healthPct;
maxBarWidth = 300;
barHeight = 20;
barPositionX = 20;
barPositionY = 20;
healthPct = percentFull * 100;
currentBarWidth = maxBarWidth * percentFull;
// Draw the "filled" part of the bar
Canvas.SetPos(barPositionX, barPositionY); // X, Y position
Canvas.SetDrawColor(80, 200, 80, 200); // R, G, B, A for bar rectangle
Canvas.DrawRect(currentBarWidth, barHeight);
// Draw the empty part of the bar
Canvas.SetPos(barPositionX + currentBarWidth, barPositionY);
Canvas.SetDrawColor(200, 255, 200, 110);
Canvas.DrawRect(maxBarWidth - currentBarWidth, barHeight);
// Draw some text next to the bar
Canvas.SetPos(barPositionX + maxBarWidth + 10, barPositionY);
Canvas.SetDrawColor(0, 200, 0, 200);
Canvas.Font = class'Engine'.static.GetMediumFont();
Canvas.DrawText("Health: "$healthPct$"%");
}
function DrawString(string text, int X, int Y, int R, int G, int B, int A)
{
Canvas.SetPos(X, Y);
Canvas.SetDrawColor(R, G, B, A);
Canvas.Font = class'Engine'.static.GetMediumFont();
Canvas.DrawText(text);
}
function DrawGameHud()
{
local TomatoPlayerController PC;
// Type cast the PlayerOwner property of the HUD to TomatoPlayerController
PC = TomatoPlayerController(PlayerOwner);
if (!PlayerOwner.IsDead())
{
// Display health bar only when the player hasn't died
if(PlayerOwner.Pawn.HealthMax > 0)
{
DrawHealthBar(float(PlayerOwner.Pawn.Health) / float(PlayerOwner.Pawn.HealthMax));
}
}
// Always display other information
DrawString("Coins: "$PC.TMTCoinsCollected$"/"$PC.TMTCoinsNeededThisLevel,20,40,140,80,0,200);
DrawString("Score: "$PC.TMTPoints,20,60,0,80,160,200);
}
defaultproperties
{
bCrosshairShow=false
}
For this basic HUD, we are using Canvas to draw text, and rectangles to represent our health bar. For a description of what each Canvas method does, refer to this tutorial at UDK Central. Ensure that UDK is closed, and compile your scripts. When you load and play TMT-Zone1.udk, you should have a fancy new HUD!
If you wish to create an advanced HUD, refer to this complete series of video tutorials on using Scaleform GFx to create a HUD.
Creating Bots
Creating Bot Behavior in UnrealScript
Bots are Controllers of Pawns, just like a Player Controller. We will modify the Unreal Tournament bots to make them better side-scroller opponents. The bots should begin attacking when they are very close to the player, since AI designed for 3D has a large sight radius. Additionally, they should patrol between two nodes (just like in the 3DBuzz video tutorials, but we will use Unreal Script instead of just Kismet to achieve this).
The way in which I designed the bots is probably not ideal, but it works and is really easy. Essentially, the bot will patrol between two nodes (these are just Actors). While that is happening, the bot will constantly check if the player is nearby and if there is a line of sight to the player. If there is, the bot will begin to attack until the target dies. I started creating this behavior by copy-pasting the Defending state from UTBot.uc, and modifying it. This is always a great starting point for learn the code base and creating your own behavior.
Since the bots require two path nodes, we need a way to tell the bot script what path nodes to use through Kismet. To achieve this, we will create a sequence action to configure our bot.
Create the file SeqAct_TMTConfigureBot.uc with the following code:
class SeqAct_TMTConfigureBot extends SequenceAction;
var() object Target;
var() float MaxFireRange;
var() object PatrolStart;
var() object PatrolEnd;
defaultproperties
{
ObjName="Configure a TomatoBot"
ObjCategory="Tomato"
HandlerName = "TMTConfigureBot"
target = None
MaxFireRange = 256
PatrolStart = None
PatrolEnd = None
VariableLinks(1)=(ExpectedType=class'SeqVar_Object', LinkDesc="Firing Target", bWriteable=true, PropertyName=Target)
VariableLinks(2)=(ExpectedType=class'SeqVar_Float', LinkDesc="Max. Fire Range", bWriteable=true, PropertyName=MaxFireRange)
VariableLinks(3)=(ExpectedType=class'SeqVar_Object', LinkDesc="Patrol Start", bWriteable=true, PropertyName=PatrolStart)
VariableLinks(4)=(ExpectedType=class'SeqVar_Object', LinkDesc="Patrol End", bWriteable=true, PropertyName=PatrolEnd)
}
The Target variable link is the target that the bot will attack (this will be the player). MaxFireRange is how close to the player the bot needs to be to start attacking. PatrolStart and PatrolEnd are the patrol path nodes, the bot will walk between them.
Open the file TomatoBot.uc and edit it as follows:
class TomatoBot extends UTBot;
var() actor TMTPatrolStart;
var() actor TMTPatrolEnd;
var() actor TMTTarget;
var() float TMTMaxFireRange;
function TMTConfigureBot(SeqAct_TMTConfigureBot MyAction)
{
TMTPatrolStart = actor(MyAction.PatrolStart);
TMTPatrolEnd = actor(MyAction.PatrolEnd);
TMTTarget = actor(MyAction.Target);
TMTMaxFireRange = MyAction.MaxFireRange;
SetTimer(0.3, true, 'ScanForPlayers');
}
function bool ShouldStrafeTo(Actor WayPoint)
{
return false;
}
function ScanForPlayers()
{
if(TMTTarget != none && VSize(Controller(TMTTarget).Pawn.Location - Pawn.Location) < TMTMaxFireRange && FastTrace(Controller(TMTTarget).Pawn.Location, Pawn.Location,, true))
{
Focus = TMTTarget;
Enemy = Controller(TMTTarget).Pawn;
VisibleEnemy = Enemy;
EnemyVisibilityTime = WorldInfo.TimeSeconds;
bEnemyIsVisible = true;
`log("TomatoBot::ScanForPlayers() : I see the player!");
if(!IsInState('ChargingNoStrafe'))
{
`log("TomatoBot::ScanForPlayers() : Going to CharginNoStrafe state");
GotoState('ChargingNoStrafe');
}
}
}
state Defending
{
ignores EnemyNotVisible;
function Restart( bool bVehicleTransition ) {}
function bool IsDefending()
{
return true;
}
function EnableBumps()
{
enable('NotifyBump');
}
event MonitoredPawnAlert()
{
WhatToDoNext();
}
function ClearPathFor(Controller C)
{
}
function SetRouteGoal()
{
local Actor NextMoveTarget;
local bool bCanDetour;
bCanDetour = (Vehicle(Pawn) == None) || (UTVehicle_Hoverboard(Pawn) != None);
if ( ActorReachable(RouteGoal) )
NextMovetarget = RouteGoal;
else
NextMoveTarget = FindPathToward(RouteGoal, bCanDetour);
if ( NextMoveTarget == None )
{
NextMoveTarget = FindPathToward(RouteGoal, bCanDetour);
}
if ( NextMoveTarget == None )
{
CampTime = 3;
// No target
GotoState('Defending','Pausing');
}
Focus = NextMoveTarget;
MoveTarget = NextMoveTarget;
}
function EndState(Name NextStateName)
{
MonitoredPawn = None;
SetCombatTimer();
bShortCamp = false;
}
function BeginState(Name PreviousStateName) { }
Begin:
`log("BEGIN DEFENDING");
WaitForLanding();
CampTime = bShortCamp ? 0.3 : 3.0;
bShortCamp = false;
SetRouteGoal();
if (Pawn.ReachedDestination(RouteGoal) )
{
Goto('Pausing');
}
else
{
Moving:
`log("MOVE DEFENDING");
Pawn.bWantsToCrouch = false;
MoveToward(MoveTarget, MoveTarget, 20, false, true);
if (Pawn.ReachedDestination(RouteGoal))
{
goto('Pausing');
}
}
LatentWhatToDoNext();
Pausing:
`log("PAUSE DEFENDING");
StopMovement();
Pawn.bWantsToCrouch = IsSniping() && !WorldInfo.bUseConsoleInput;
SetFocalPoint( Pawn.Location + vector(MoveTarget.Rotation) * 10.0 );
SwitchToBestWeapon();
Sleep(0.5);
if (UTWeapon(Pawn.Weapon) != None && UTWeapon(Pawn.Weapon).ShouldFireWithoutTarget())
Pawn.BotFire(false);
Sleep(FMax(0.1,CampTime - 0.5));
// Paused at one destination, select the other destination
if(RouteGoal != none && RouteGoal == TMTPatrolEnd)
{
RouteGoal = TMTPatrolStart;
}
else
{
RouteGoal = TMTPatrolEnd;
}
LatentWhatToDoNext();
}
state ChargingNoStrafe extends Charging
{
function bool StrafeFromDamage(float Damage, class<DamageType> DamageType, bool bFindDest)
{
return false;
}
function bool TryStrafe(vector sideDir)
{
return false;
}
}
defaultproperties
{
TMTMaxFireRange = 500
TMTPatrolEnd = None
TMTPatrolStart = None
TMTTarget = None
}
When the bot is configured using our action, it begins calling ScanForPlayers on a timer, which checks if the TMTTarget (our player) is in close enough. If that’s the case, it enters the ChargingNoStrafe state, which is almost entirely defined in UTBot.uc. If it does not find anything, it will be in a Defending state, walking between two path nodes defined by TMTPatrolStart and TMTPatrolEnd.
Save both files and re-compile the scripts.
Configuring, and Adding Bots to the Level
Game Over!
The last element to add to our game will be a “game over” screen. Ideally, you would use Scaleform to create such UI screens, but it is beyond the scope of this guide. We will simply use Kismet to create a sequence of actions that display an existing Scaleform GFx movie. To begin this sequence, we will need an event. This event will be triggered by our TestWinningConditions() method when the game is won. To begin, create a new class file named SeqEvent_TMTGameWon.uc with the following code:
class SeqEvent_TMTGameWon extends SequenceEvent;
event Activated(){
`log("IN SeqEvent_TMTGameWon Activated()");
}
defaultproperties
{
ObjName="Tomato Game Won"
ObjCategory="Tomato"
bPlayerOnly = false
}
Next, let’s modify TestWinningConditions() in TomatoPlayerController.uc, to activate this event. Update the method as follows:
exec function TestWinningConditions()
{
local int i;
local Sequence GameSeq;
local array<SequenceObject> AllSeqEvents;
`log("TestWinningConditions() : Checking winning conditions");
`log("TestWinningConditions() : Collected: "$TMTCoinsCollected);
`log("TestWinningConditions() : Needed: "$TMTCoinsNeededThisLevel);
if(TMTCoinsCollected > 0 && TMTCoinsNeededThisLevel > 0 && TMTCoinsCollected >= TMTCoinsNeededThisLevel)
{
`log("TestWinningConditions() : Victory!");
GameSeq = WorldInfo.GetGameSequence();
if(GameSeq != None)
{
GameSeq.FindSeqObjectsByClass(class'SeqEvent_TMTGameWon', true, AllSeqEvents);
for(i=0; i<AllSeqEvents.Length; i++)
SeqEvent_TMTGameWon(AllSeqEvents[i]).CheckActivate(WorldInfo, None);
}
}
else
{
`log("TestWinningConditions() : Go collect more coins!");
}
}
In the code above, we are simply getting and activating all sequence events of type SeqEvent_TMTGameWon.
Save and compile all scripts.
Open up UDK, and load TMT-Zone1.udk.
The complete source code and level file created during this guide is available here: http://labs.vectorform.com/files/Tomato.zip
-
Moaaz
-
Vasiliy Deych
-
Alex Viana
-
Will
-
Vasiliy Deych
-
Will
-
Sowa125
-
Vasiliy Deych
-
B_dobbs
-
B_dobbs
-
B_dobbs
-
http://www.facebook.com/profile.php?id=551044477 Justin Piatt
-
http://www.facebook.com/profile.php?id=551044477 Justin Piatt
-
Vasiliy Deych
-
Fabi
-
Will
-
Kronoix
-
Vasiliy Deych
-
Sowa125
-
Explodi
-
http://www.facebook.com/srinathupadhyayula Srinath Upadhyayula
-
Terryburgenheim
-
Vasiliy Deych
-
Terry
-
son1c
-
Karthikk2404
-
Will
-
http://www.facebook.com/srinathupadhyayula Srinath Upadhyayula
-
Will
-
Feinstein Matt
-
Cheatachild
-
Will
-
Will
-
Thazzrill
-
http://www.facebook.com/srinathupadhyayula Srinath Upadhyayula
-
Thazzrill
-
Young Shakey
-
Cheatachild
-
http://www.facebook.com/srinathupadhyayula Srinath Upadhyayula
-
al
-
Will
-
Xenogray
-
http://www.facebook.com/menghui0330 Tan Meng Hui
-
http://www.facebook.com/levjski Lee Gibson
-
http://www.facebook.com/levjski Lee Gibson
-
http://www.facebook.com/srinathupadhyayula Srinath Upadhyayula
-
http://www.facebook.com/people/Ochir-Sanjaadorj/605272431 Ochir Sanjaadorj
-
Pl4y0n
-
enak
-
Aiman
-
Nathian Myers
-
Will
-
Will
-
Timelog
-
Dark_Dake
-
Dark_Dake
-
Timelog
-
Timelog
-
Bhargava Surya Vemuri
-
Timelog
-
Guest
-
Hobo
-
GuentherJanson
-
http://www.facebook.com/levjski Lee Gibson
-
iSoundgear
-
Ajfox
-
A_okay5175
-
Nerftacular
-
shog768
-
http://www.facebook.com/profile.php?id=100001736915404 Douglas Gomes
-
Hextrix
-
POW
-
Daredha
-
http://www.facebook.com/people/Guillermo-Hidan-Gomez/797968738 Guillermo Hidan Gomez
-
Alok
-
Marko
-
Drew
-
KK
-
Peripherus
-
Alok
-
KelSquared
-
KelSquared
-
Julian
-
Al
-
NAiLz
-
http://profile.yahoo.com/KKULYG4FYHZKASYAFYMJT7AHNA Joe Bloggingtonfield
-
http://profile.yahoo.com/KKULYG4FYHZKASYAFYMJT7AHNA Joe Bloggingtonfield
-
Emailingme
-
Beef442
-
NAiLz
-
Azat
-
Kittyjenta
-
Mr. Bingolino
-
Mr. Bingolino
-
nikotech
-
Aaronstone628
-
Du1997du
-
Du1997du
-
Borisss
-
Nicolas DEJEANS
-
Dashort22
-
Kris
-
Kris
-
Unknown
-
http://www.youtube.com/user/Sizza47?feature=mhee Sizza147
-
B. Cullen
-
B. Cullen
-
Jasper
-
Marko Alerić
-
Chris
-
god++
-
Brett C.
-
rand
-
Hrvat
-
Hrvat
-
frog
-
Hrvat
-
Gypsy
-
John
-
B
-
Alex
-
http://en-gb.facebook.com/people/Dean-Gvozdic/582245361 Dean Gvozdic
-
Reinout
-
Reinout
-
João Marcos
-
http://www.facebook.com/kholio.bungholio VlOlz Elxenoanizd
-
adi

