Synopsis
This is the first part of four to help people create their own moves. This part details how to set things up and get started on creating basic parts of a functional move. By the end you should be able to create your own basic moves. Note, some of the code requires reformatting due to the 80 character limit on words.
Starting out
- Create new folder 'ExtraPsiMoves'
- Create 'Classes' folder within that folder
- Also create a 'Textures' folder here for later
- Create a new 'PsychicMoveHeal.uc' file to represent your new move
Setting Up UDE
Open UDE and your file 'PsychicMoveHeal.uc'
Open EditPackages and add the 'ExtraPsiMoves' class package to it
Starting Structure
Now we will create the barebones structure, firstly we will extend PsychicMove and then set default properties
You can ignore typing the comments for speeds sake
//========
//Heal those who are friendly
//
//Author:
// [Add your name here]
//Date:
// [Add the date here]
//==========
class PsychicMoveHeal extends PsychicMove;
defaultproperties
{
//Some information
thename="Heal"
description="Heal your buddies"
moveon=false
//For the Description tab panel, not all that important
levelDescription[0]="Heal only those who are very close to you who you are looking at"
levelDescription[1]="Heal at a further distance who you are looking at"
levelDescription[2]="Heal at an even further distance who you are looking at"
levelDescription[3]="Heal anyone within a radius of you up to 75% health"
levelDescription[4]="Heal anyone within a radius of you up to full health"
//What it costs to use, if we don't have this much energy the move won't start
// In this case we want it to be free
levelInitialCost[0]=0
levelInitialCost[1]=0
levelInitialCost[2]=0
levelInitialCost[3]=0
levelInitialCost[4]=0
//What it costs to use per second, in this case we only drain energy when it is in use
// so set to 0
levelUsingCost[0]=0
levelUsingCost[1]=0
levelUsingCost[2]=0
levelUsingCost[3]=0
levelUsingCost[4]=0
binstant = false //As this move works over a period of time it is not instant (Blast is instant)
botuseful = true //We will add some bot support to this so set this to true
Icon=Texture'HealIcon' //The icon, we'll set it later
}
Icon
Now we will set the icon so that we know which one our move is when selected
Create an icon in your favorite graphics program (i.e. photoshop)
The icon must be 64x64, for best effects use a 32-bit tga (with alpha)
Save this icon into the 'Textures' folder of your package
Add the following line just below the class declaration:
#exec TEXTURE IMPORT NAME=HealIcon FILE=textures\Heal.tga GROUP="HudIcons" MIPS=OFF Flags=2
This will import the texture, notice we set the NAME to the same as the one used in the default properties
Icon=Texture'HealIcon'
The FILE component is obviously the image, just leave the rest the same for any moves
Functionality
Now for some functionality
Add this under the pre-processor statement
function bool startMove(int plevel, PsychicLinkedReplicationInfo _plri)
{
if(Super.startMove(plevel, _plri)) //If the move starts correctly
{
moveon = true; //Turn the move on
return true; //Return all good
}
return false; //Return not good
}
This starts the move if we don't have any problems (such as a backfire)
function moveTick(float DeltaTime)
{
local vector HitLoc, HitNorm, endloc; //Sate the trace function
local Actor HitActor;
local Pawn HitPawn;
bDidAHeal = false;
if(levelUsed < 3) //Line of sight move
{
endloc = user.Pawn.Location + (vector(user.GetViewRotation()) * 500 * (levelUsed+1)); //Set trace end based on level used
HitActor = user.Pawn.Trace(HitLoc, HitNorm, endloc, user.Pawn.Location, true); //Find an actor
HitPawn = Pawn(HitActor); //Cast it to a pawn as we only want to deal with pawns
//Check if pawn is valid and on same team
if(HitPawn != None && HitPawn.PlayerReplicationInfo != None && HitPawn.PlayerReplicationInfo.Team != None
&& HitPawn.PlayerReplicationInfo.Team.TeamIndex == user.PlayerReplicationInfo.Team.TeamIndex)
{
targetPawn = HitPawn;
//If so heal them and take away energy
if(HitPawn.Health < 95)
{
HitPawn.Health += levelUsed;
plri.energy -= levelUsed;
bDidAHeal = true;
}
}
}
else //Radius move
{
//With upper levels we use a radius based approach
foreach user.Pawn.RadiusActors(class'Pawn', HitPawn, levelUsed * 250, user.Pawn.Location)
{
//Check if pawn is valid and on same team
if(HitPawn != None && HitPawn.PlayerReplicationInfo != None && HitPawn.PlayerReplicationInfo.Team != None
&& HitPawn.PlayerReplicationInfo.Team.TeamIndex == user.PlayerReplicationInfo.Team.TeamIndex)
{
//If so heal them and take away energy
if(HitPawn.Health < 75 + ((levelused - 3)*25)) //Level 4 only goes to 75%
{
HitPawn.Health += levelUsed; //Use dam type blast as I can't be bothered making a new one
plri.energy -= levelUsed/2;
}
}
}
}
//If energy is depleated then turn off the move
if(plri.energy < 0)
{
plri.energy = 0;
plri.endCurMove();
moveon = false;
}
}
This is the meat and potatoes of the move, read the comments as they explain it pretty easily.
function backfire(int level)
{
//If it stuffs up then cause some damage to us
user.Pawn.TakeDamage(level*10, user.Pawn, user.Pawn.Location, vect(0,0,0), class'DamTypeBlast'); //Use dam type blast as I can't be bothered making a new one
}
The backfire function will trigger if we activate when too hot, we just set it to do some damage to us
Testing
- Now for a quick test, compile the package (remember if using UDE to refresh package tree, etc.) and make sure it's all good
- Now we need to go to the root UT2004 folder and then into system
- Open 'MutPsychicPowers.int' in your favorite text editor
- Add the following line ot the bottom of the list of similar ones
- Object=(Name=ExtraPsiMoves.PsychicMoveHeal, Class=Class,MetaClass=MutPsychicPowers.PsychicMove)
- Now run UT2004 and you should be able to select your new move.
Next Up
The next tutorial will add extra code to render to the HUD, just to show how it is done.
Part 2
Now we have our basic move set up we can add in both a secondary function and some rendering just for kicks. Our secondary function will be an all or nothing health dump that increases the targets health by ours, but kills us, only under level 3. The rendering will draw a box around the person we are healing, the colour dependent upon their health.
Secondary Function
The secondary is very easy to implement.
Firstly we need to store the Pawn we are looking at so we don't have to trace again, add this code to the top of your your ‘PsychicMoveHeal.uc' file (just under the pre-processor statement):
var Pawn targetPawn;
Now within the moveTick function, inside the if loop that confirms our pawn is valid type the top line:
targetPawn = HitPawn;
//If so heal them and take away energy
if(HitPawn.Health < 95)
Now add this function anywhere in the file.
function bool secondaryFunction()
{
//If we have a target
if(targetPawn != None)
{
//Add health to the target
targetPawn.Health += user.Pawn.Health;
//Kill us off
user.Pawn.TakeDamage(user.Pawn.Health*2, user.Pawn, user.Pawn.Location, vect(0,0,0), class'DamTypeBlast');
return true;
}
//Do nothing
return false;
}
Rendering
Okay, now we have a rather pathetic secondary move, we will now draw an even more pathetic box around the player we are healing. For us this means we need to deal with replication (YAAAYYYYY.) Firstly add this variable declaration below the targetPawn one:
var bool bDidAHeal;
Now below that type the following:
replication
{
reliable if(Role == ROLE_Authority)
bDidAHeal;
unreliable if(Role == ROLE_Authority)
targetPawn;
}
If you don't understand this part then don't worry about it and skip the rest of this paragraph. If you do understand replication, the reason I added the bDidAHeal Boolean was because I set the targetPawn to unreliable. This is simply helping to save net bandwidth, as sending the pawn reliably is very costly as compared to sending the single byte Boolean. Replicating the pawn properly isn't all that important in the grand scheme of things.
Righty'o then, time to add the big cahoona
simulated function moveRender(Canvas C, PlayerController owner)
{
local vector screenloc, tempto;
local color targetcol;
//Log("Target pawn is: " $ targetpawn);
//Don't do if this didn't do a heal or no target or no playercontroller
if(!bDidAHeal || targetpawn == None || owner == None)
return;
//Make sure the player is actually in front of us, stops some stupid bug in unreal
tempto = targetpawn.Location - owner.Pawn.Location;
if(targetpawn.Location Dot vector(owner.GetViewRotation()) < 0)
return;
//Get the player's location on screen based on world, uses a special function in the Interaction class
screenloc = owner.Player.InteractionMaster.GlobalInteractions[ 0].WorldToScreen(targetpawn.Location);
//Choose colour using a simple rule system
if(targetpawn.Health < 30)
targetcol = C.MakeColor(255, 0, 0); //RED
else if(targetpawn.Health < 70)
targetcol = C.MakeColor(255, 255, 0); //YELLOW
else
targetcol = C.MakeColor(0, 255, 0); //GREEN
C.DrawColor = targetcol;
//Set box position to center and draw
C.SetPos(screenloc.X - 25, screenloc.Y - 25);
C.DrawBox(C, 50, 50);
//Set font to small and write the pawns health
C.Font = C.SmallFont;
C.DrawText(targetpawn.Health);
}
GO, GO, GO
Hit your compile button and jump into a game and test your new toy.
Next up
We'll add some client side configuration just for the hell of it.
Part 3
The following will allow you to place configurable variables into the Psionics mutators move configuration menu. This will allow the player to change the way the move works to suit their play style. For this tut we will add the option to turn off HUD rendering and also set how health is drawn. For a more in depth look, check out the Perception and Sight classes as these have configurable values.
Our variables
Add the following variable declarations to the top of the file:
//Config vars
var config string healthdrawtype;
var config bool bDrawOnHud;
And it is good practice to set their default values so within the defaultproperties block add:
healthdrawtype="honly"
bDrawOnHud=true
The functionality
Before we get down to how to set config values, lets just add in the functionality quickly. Add the following to the top of the moveRender() function (below the variable declarations):
//Hud Drawing option
if(!bDrawOnHUD)
return;
And convert the following part of moveRender() (is at the very bottom)
//Set font to small and write the pawns health
C.Font = C.SmallFont;
C.DrawText(targetPawn.Health);
to
//Set font to small and write the pawns health
C.Font = C.SmallFont;
switch(healthdrawtype)
{
case "none":
break;
case "honly":
C.DrawText(targetpawn.Health);
break;
case "hmax":
C.DrawText(targetpawn.Health $ "/" $ targetpawn.HealthMax);
break;
}
Putting it on the menu
Now it is time to allow our user to change the values. We do this using the FillPlayInfo(PlayInfo PlayInfo) function, which astute readers will note is the exact same as how to add mutator configuration variables. Essentially the system works exactly the same way so check tutorials on how to do mutators for more information on how to do this. Okay now add this function somewhere:
static function FillPlayInfo(PlayInfo PlayInfo)
{
local String Options;
Super.FillPlayInfo(PlayInfo); //Call the super to do some setup (VERY IMPORTANT)
Options = "none;No Health;honly;Health Only;hmax;Health/Max"; //OPtions for the combo box, not semi-colon delimited ;
//Add the options combo box
PlayInfo.AddSetting("PsychicMoveHeal", "healthdrawtype", "Health Draw Type", 0, 0, "Select", Options);
//Add the HUD switch
PlayInfo.AddSetting("PsychicMoveHeal", "bDrawOnHud", "Draw to HUD", 0, 0, "Check");
}
This is where all the information is added to the menu; in this case we are adding a combo box for the type of health and a checkbox to turn on/off HUD rendering. Just to round off the menu system we need to add our description setter function of:
static event string GetDescriptionText(string PropName)
{
switch(PropName)
{
case "healthdrawtype": return "What numbers to display for health";
case "bDrawOnHud": return "Enables/Disables drawing to the HUD";
}
return Super.GetDescriptionText(PropName);
}
This simply sets the tool tip for each variable modifier in the menu.
And move out...
- Compile...Run...
- Open the client-side mutator config window and select the psionics mutator button
- On the smaller move selector, hit the configuration button
- Scroll down until you find the heal move
- Modify and test
Next up
For the last part we will add some bot support so that our bots can use the move.
Part 4
This fourth and final part of the tutorial focuses on allowing bots to use your move in game. The AI implemented in the tutorial will be very basic; it will just serve to highlight what the functions do as opposed to being in depth AI simulation. As a challenge at the end, write the AI to be super smart and useful.
The functions
Before we begin I will quickly explain the purpose of the bot functions:
- getBotWeight(level, energy) - Return a weight (0.0 - 1.0 generally) of how useful this move would be if used at this particular time. The level is the current level of the user and the energy is the amount of energy they have left.
- getBestLevel(maxlevel, bplri) - Called if move is to be used, return the best level to use at this time. maxlevel is the maximum level we can use for this move while bplri is a link to the bot replication info so we can access any info in that (energy, level, etc.)
- botUsing(bplri) - Called when a move is on, return true to turn the move off. bplri is a link to the bot replication info so we can access any info in that (energy, level, etc.)
Implementation
Firstly we will write the weighting function. The way I did it was to search all pawns within the affected radius and count the ones that needed healing. The weight is then based on how many need healing:
function float getBotWeight(int level, float energy)
{
local Pawn HitPawn;
local int numwecanheal;
//In order to not get too deep into AI, we will make it easy and only use the radius ones
if(level < 3)
return 0;
//Set our variable to 0
numwecanheal = 0;
foreach user.Pawn.RadiusActors(class'Pawn', HitPawn, levelUsed * 250)
{
//Make sure this person is on our team
if(HitPawn != None && HitPawn.PlayerReplicationInfo != None && HitPawn.PlayerReplicationInfo.Team != None
&& HitPawn.PlayerReplicationInfo.Team.TeamIndex == user.PlayerReplicationInfo.Team.TeamIndex)
{
//If they need healing then update counter
if(HitPawn.Health < 70)
{
numwecanheal++;
}
}
}
//Return a weight, as 1.0 is pretty much the top then any more than
// 4 people need healing will make this a priority
return numwecanheal / 0.25;
}
The next function is easy for this move, we just return the highest level we can use:
function int getBestLevel(int maxlevel, BotPsychicLinkedReplicationInfo bplri)
{
//We want the best healing we can get
return maxlevel;
}
And the final one does the same checking as the weighting function, but will end the move when less than 2 people need healing (i.e. 0 or 1):
function bool botUsing(BotPsychicLinkedReplicationInfo bplri)
{
local Pawn HitPawn;
local int numwecanheal;
//Set our variable to 0
numwecanheal = 0;
foreach user.Pawn.RadiusActors(class'Pawn', HitPawn, levelUsed * 250)
{
//Make sure this person is on our team
if(HitPawn != None && HitPawn.PlayerReplicationInfo != None && HitPawn.PlayerReplicationInfo.Team != None
&& HitPawn.PlayerReplicationInfo.Team.TeamIndex == user.PlayerReplicationInfo.Team.TeamIndex)
{
//If they need healing then update counter
if(HitPawn.Health < 70)
{
numwecanheal++;
}
}
}
//If less than 2 people need healing then we exit the move
if(numwecanheal < 2)
return true;
//Else stay in the move
return false;
}
That's all folks
So there you have it, all the knowledge you need to create your own moves. The next page has a complete code dump of the above example.
Enjoy!!!
//=========================================================
//Heal those who are friendly
//
//Author:
//
//Date:
//
//=========================================================
class PsychicMoveHeal extends PsychicMove;
#exec TEXTURE IMPORT NAME=HealIcon FILE=textures\Heal.tga GROUP="HudIcons" MIPS=OFF Flags=2
var Pawn targetPawn;
var bool bDidAHeal;
//Config vars
var config string healthdrawtype;
var config bool bDrawOnHud;
replication
{
reliable if(Role == ROLE_Authority)
bDidAHeal;
unreliable if(Role == ROLE_Authority)
targetPawn;
}
function bool startMove(int plevel, PsychicLinkedReplicationInfo _plri)
{
if(Super.startMove(plevel, _plri)) //If the move starts correctly
{
moveon = true; //Turn the move on
return true; //Return all good
}
return false; //Return not good
}
function bool secondaryFunction()
{
//If we have a target
if(targetPawn != None)
{
//Add health to the target
targetPawn.Health += user.Pawn.Health;
//Kill us off
user.Pawn.TakeDamage(user.Pawn.Health*2, user.Pawn, user.Pawn.Location, vect(0,0,0), class'DamTypeBlast');
return true;
}
//Do nothing
return false;
}
function moveTick(float DeltaTime)
{
local vector HitLoc, HitNorm, endloc; //Sate the trace function
local Actor HitActor;
local Pawn HitPawn;
bDidAHeal = false;
if(levelUsed < 3) //Line of sight move
{
endloc = user.Pawn.Location + (vector(user.GetViewRotation()) * 500 * (levelUsed+1)); //Set trace end based on level used
HitActor = user.Pawn.Trace(HitLoc, HitNorm, endloc, user.Pawn.Location, true); //Find an actor
HitPawn = Pawn(HitActor); //Cast it to a pawn as we only want to deal with pawns
//Check if pawn is valid and on same team
if(HitPawn != None && HitPawn.PlayerReplicationInfo != None && HitPawn.PlayerReplicationInfo.Team != None
&& HitPawn.PlayerReplicationInfo.Team.TeamIndex == user.PlayerReplicationInfo.Team.TeamIndex)
{
targetPawn = HitPawn;
//If so heal them and take away energy
if(HitPawn.Health < 95)
{
HitPawn.Health += levelUsed;
plri.energy -= levelUsed;
bDidAHeal = true;
}
}
}
else //Radius move
{
//With upper levels we use a radius based approach
foreach user.Pawn.RadiusActors(class'Pawn', HitPawn, levelUsed * 250, user.Pawn.Location)
{
//Check if pawn is valid and on same team
if(HitPawn != None && HitPawn.PlayerReplicationInfo != None && HitPawn.PlayerReplicationInfo.Team != None
&& HitPawn.PlayerReplicationInfo.Team.TeamIndex == user.PlayerReplicationInfo.Team.TeamIndex)
{
//If so heal them and take away energy
if(HitPawn.Health < 75 + ((levelused - 3)*25)) //Level 4 only goes to 75%
{
HitPawn.Health += levelUsed; //Use dam type blast as I can't be bothered making a new one
plri.energy -= levelUsed/2;
}
}
}
}
//If energy is depleated then turn off the move
if(plri.energy < 0)
{
plri.energy = 0;
plri.endCurMove();
moveon = false;
}
}
simulated function moveRender(Canvas C, PlayerController owner)
{
local vector screenloc, tempto;
local color targetcol;
//Hud Drawing option
if(!bDrawOnHUD)
return;
//Don't do if this didn't do a heal or no target or no playercontroller
if(!bDidAHeal || targetpawn == None || owner == None)
return;
//Make sure the player is actually in front of us, stops some stupid bug in unreal
tempto = targetpawn.Location - owner.Pawn.Location;
if(targetpawn.Location Dot vector(owner.GetViewRotation()) < 0)
return;
//Get the player's location on screen based on world, uses a special function in the Interaction class
screenloc = owner.Player.InteractionMaster.GlobalInteractions[0]. WorldToScreen(targetpawn.Location);
//Choose colour using a simple rule system
if(targetpawn.Health < 30)
targetcol = C.MakeColor(255, 0, 0); //RED
else if(targetpawn.Health < 70)
targetcol = C.MakeColor(255, 255, 0); //YELLOW
else
targetcol = C.MakeColor(0, 255, 0); //GREEN
C.DrawColor = targetcol;
//Set box position to center and draw
C.SetPos(screenloc.X - 25, screenloc.Y - 25);
C.DrawBox(C, 50, 50);
//Set font to small and write the pawns health
C.Font = C.SmallFont;
switch(healthdrawtype)
{
case "none":
break;
case "honly":
C.DrawText(targetpawn.Health);
break;
case "hmax":
C.DrawText(targetpawn.Health $ "/" $ targetpawn.HealthMax);
break;
}
}
function backfire(int level)
{
//If it stuffs up then cause some damage to us
user.Pawn.TakeDamage(level*10, user.Pawn, user.Pawn.Location, vect(0,0,0), class'DamTypeBlast'); //Use dam type blast as I can't be bothered making a new one
}
static function FillPlayInfo(PlayInfo PlayInfo)
{
local String Options;
Super.FillPlayInfo(PlayInfo); //Call the super to do some setup (VERY IMPORTANT)
Options = "none;No Health;honly;Health Only;hmax;Health/Max"; //OPtions for the combo box, not semi-colon delimited ;
//Add the options combo box
PlayInfo.AddSetting("PsychicMoveHeal", "healthdrawtype", "Health Draw Type", 0, 0, "Select", Options);
//Add the HUD switch
PlayInfo.AddSetting("PsychicMoveHeal", "bDrawOnHud", "Draw to HUD", 0, 0, "Check");
}
static event string GetDescriptionText(string PropName)
{
switch(PropName)
{
case "healthdrawtype": return "What numbers to display for health";
case "bDrawOnHud": return "Enables/Disables drawing to the HUD";
}
return Super.GetDescriptionText(PropName);
}
//===============BOT STUFF===============
//Returns a weighting on the moves current usage
//Parameter for maximum level that can be used, max amount of energy
//Return 0 or less if useless
function float getBotWeight(int level, float energy)
{
local Pawn HitPawn;
local int numwecanheal;
//In order to not get too deep into AI, we will make it easy and only use the radius ones
if(level < 3)
return 0;
//Set our variable to 0
numwecanheal = 0;
foreach user.Pawn.RadiusActors(class'Pawn', HitPawn, levelUsed * 250)
{
//Make sure this person is on our team
if(HitPawn != None && HitPawn.PlayerReplicationInfo != None && HitPawn.PlayerReplicationInfo.Team != None
&& HitPawn.PlayerReplicationInfo.Team.TeamIndex == user.PlayerReplicationInfo.Team.TeamIndex)
{
//If they need healing then update counter
if(HitPawn.Health < 70)
{
numwecanheal++;
}
}
}
//Return a weight, as 1.0 is pretty much the top then any more than
// 4 people need healing will make this a priority
return numwecanheal / 0.25;
}
//Best level to use
function int getBestLevel(int maxlevel, BotPsychicLinkedReplicationInfo bplri)
{
//We want the best healing we can get
return maxlevel;
}
//Called during usage to tell the bot what to do
//Returns whether or not to turn the move off
function bool botUsing(BotPsychicLinkedReplicationInfo bplri)
{
local Pawn HitPawn;
local int numwecanheal;
//Set our variable to 0
numwecanheal = 0;
foreach user.Pawn.RadiusActors(class'Pawn', HitPawn, levelUsed * 250)
{
//Make sure this person is on our team
if(HitPawn != None && HitPawn.PlayerReplicationInfo != None && HitPawn.PlayerReplicationInfo.Team != None
&& HitPawn.PlayerReplicationInfo.Team.TeamIndex == user.PlayerReplicationInfo.Team.TeamIndex)
{
//If they need healing then update counter
if(HitPawn.Health < 70)
{
numwecanheal++;
}
}
}
//If less than 2 people need healing then we exit the move
if(numwecanheal < 2)
return true;
//Else stay in the move
return false;
}
//===========END BOT STUFF===============
defaultproperties
{
//Some information
thename="Heal"
description="Heal your buddies"
moveon=false
//Move specifics
targetPawn=None
bDidAHeal=false
healthdrawtype="honly"
bDrawOnHud=true
//For the Description tab panel, not all that important
levelDescription[0]="Heal only those who are very close to you who you are looking at"
levelDescription[1]="Heal at a further distance who you are looking at"
levelDescription[2]="Heal at an even further distance who you are looking at"
levelDescription[3]="Heal anyone within a radius of you up to 75% health"
levelDescription[4]="Heal anyone within a radius of you up to full health"
//What it costs to use, if we don't have this much energy the move won't start
// In this case we want it to be free
levelInitialCost[0]=0
levelInitialCost[1]=0
levelInitialCost[2]=0
levelInitialCost[3]=0
levelInitialCost[4]=0
//What it costs to use per second, in this case we only drain energy when it is in use
// so set to 0
levelUsingCost[0]=0
levelUsingCost[1]=0
levelUsingCost[2]=0
levelUsingCost[3]=0
levelUsingCost[4]=0
binstant = false //As this move works over a period of time it is not instant (Blast is instant)
botuseful = true //We will add some bot support to this so set this to true
Icon=Texture'HealIcon' //The icon, we'll set it later
}
Thnx very much for this, I will definitely put it to good use! :)
a video is a goood asistant for that type of stuff...