Post tutorial RSS Coding Tutorial - Part 3

Coding Tutorial - Part 3 Author: Charles "MrBlonde" Palmer Last Updated: April 26, 2001 at 02:43:49 PM

Posted by on - Intermediate Mapping/Technical

Originally posted Coding Tutorial - Part 3

Mirrored for archival purposes


Welcome back boy and girls. When I left you with the last tutorial there were a number of bugs (aside from those made unintentionally by me!).

Firstly if you died in the HillZone and stayed dead you could rack up your score. Also you could still score by killing people. Finally there was no indication of your current score! Well today we will fix all of that and have a fully featured DeathMatch Game Type complete.

A special thanks goes out to phaZe this week for helping find a few errors I made in the tutorial. Thanks!

Let's blitz the bugs...

Firstly the Player still gains time in the zone whilst he's dead, not good. To fix this we need to make sure that when the player is dead his time in the zone is stopped and reset. Thankfully Rune provides the ScoreKill function in which we can do this. So opening up COTM.uc we add the following function:

function ScoreKill(pawn Killer, pawn Other)
{
local COTMPlayerReplicationInfo PRI; //to shorten things a bit
local int ZoneTime; //for a nicer message

if(Other.IsA('PlayerPawn'))
{
if(PlayerPawn(Other).PlayerReplicationInfo.IsA('COTMPlayerReplicationInfo'))
{
PRI = COTMPlayerReplicationInfo(PlayerPawn(Other).PlayerReplicationInfo);
if(PRI.bIsInZone)
{
PRI.Z />PRI TotalTime += (PRI.ZoneExitTime - PRI.ZoneEnterTime);
Z - PRI.ZoneEnterTime);
BroadcastMessage(PRI.PlayerName$" was forced to leave after "$ZoneTime$" seconds.");
}
PRI.Z />PRI Z />PRI bIsInZ />}
}

}

Remember to place it between the class declaration and the defaultproperties. Taking a look at the function itself we see it takes 2 variables. Firstly the Pawn (in this case the Player) who made the kill and the victim. We want to access the COTMPlayerReplicationInfo to change a few values so we setup a variable to hold it.

function ScoreKill(pawn Killer, pawn Other)
{
local COTMPlayerReplicationInfo PRI; //to shorten things a bit
local int ZoneTime; //for a nicer message

We then come across something we have seen before in our HillZone, we check to make sure we have the right PlayerReplicationInfo then assign it to our previously created variable. This way we have now grabbed all the information for the victim and can do what we like with it!

if(PlayerPawn(Other).PlayerReplicationInfo.IsA('COTMPlayerReplicationInfo'))
{
PRI = COTMPlayerReplicationInfo(PlayerPawn(Other).PlayerReplicationInfo);

Then checking to make sure the Player is in the HillZone before calculating the time that he was in there for. With this done we add it to his total time and display a message to everyone.

if(PRI.bIsInZone)
{
PRI.Z />PRI TotalTime += (PRI.ZoneExitTime - PRI.ZoneEnterTime);
Z - PRI.ZoneEnterTime);
BroadcastMessage(PRI.PlayerName$" was forced to leave after "$ZoneTime$" seconds.");
}

With this done we then make sure that all of the victims stats are reset before he respawns, just in case!

PRI.Z />PRI Z />PRI bIsInZ />
Moving on, what happens if the Player is reset before he dies (you never know with those kooky server admins) well lets make sure that the stats are reset there using RestartPlayer. This function is now pretty much identical to the one above except we substitute Other for aPlayer and include one very important line at the bottom:

function bool RestartPlayer( pawn aPlayer )
{
local COTMPlayerReplicationInfo PRI; //to shorten things a bit
local int ZoneTime; //for a nicer message

if(aPlayer.IsA('PlayerPawn'))
{
if(PlayerPawn(aPlayer).PlayerReplicationInfo.IsA('COTMPlayerReplicationInfo'))
{
PRI = COTMPlayerReplicationInfo(PlayerPawn(aPlayer).PlayerReplicationInfo);
if(PRI.bIsInZone)
{
PRI.Z />PRI TotalTime += (PRI.ZoneExitTime - PRI.ZoneEnterTime);
Z - PRI.ZoneEnterTime);
BroadcastMessage(PRI.PlayerName$" was forced to leave after "$ZoneTime$" seconds.");
}
PRI.Z />PRI Z />PRI bIsInZ />}
}
return Super.RestartPlayer(aPlayer);
}

You should be able to follow the above pretty easily, but the bottom line is the most important (isn't it always). This makes sure that the previous version of RestartPlayer is called. Obviously needed since we haven't done anything to restart the Player. "But wait!" I hear you cry. Why didn't I do the same for ScoreKill, the simple answer is I didn't want any of the previous functionality! It awards points for killing people which is what we don't want! Fear not, people will still die and respawn okay. With all this setup we better make sure that there is no fraglimit, this is incredibly straight forward and handled in an event named InitGame:

event InitGame( string Options, out string Error )
{
Super.InitGame(Options, Error);
FragLimit = 0; //no frags thank you
}

There we are again calling the previous version, there's more things than just the frag limit to setup! Then we make sure the frag limit is set to 0. Calling it after we have run through the previous version ensures our changes are used, otherwise it might overwrite them.

Show me the, urrrm...

Time we let the Player see see how well he/she is doing within the game, first of all lets have a visual indicator that the Player can always refer back too whilst in the heat of battle (also known as a frag-indicator). To do this we need to sub-class the HUD and add our own changes to it, create a new files called COTMRuneHUD.uc and add the following:

//=============================================================================
// COTMRuneHUD
//=============================================================================
class COTMRuneHUD extends RuneHUD;

simulated function DrawFragCount(canvas Canvas, int x, int y)
{
local int CurrentTime;
local string Text;
local PlayerPawn PlayerOwner;
local COTMPlayerReplicationInfo PRI;

PlayerOwner = PlayerPawn(Owner);

// Draw Frag Icon
// Canvas.SetPos(X-100,Y);
// Canvas.DrawIcon(Texture'IconSkull', 1.0);
// Canvas.CurX -= 19;
// Canvas.CurY += 23;

if ( PlayerOwner.PlayerReplicati None )
return;
if(!PlayerOwner.PlayerReplicationInfo.IsA('COTMPlayerReplicationInfo'))
return;
PRI = COTMPlayerReplicationInfo(PlayerOwner.PlayerReplicationInfo);
CurrentTime = PRI.TotalTime;
if(PRI.bIsInZone)
CurrentTime += (Level.TimeSeconds - PRI.ZoneEnterTime);
Text = CurrentTime$" seconds";
Canvas.F />Canvas DrawTextRightJustify(text, X, Y);
Canvas.SetColor(255,255,255);
}

defaultproperties
{
}

The function itself is called by PostRender() which you can find in RuneHUD, it passes the Canvas to draw too (the HUD) and the coordinates to draw at. The simulated at the front of the function means that this function will be handled at the client, so the server will never process what is displayed on the HUD of the client (except for the Replicated values). We then declare all the needed variables.

//=============================================================================
// COTMRuneHUD
//=============================================================================
class COTMRuneHUD extends RuneHUD;

simulated function DrawFragCount(canvas Canvas, int x, int y)
{
local int currenttime;
local string text;
local PlayerPawn PlayerOwner;
local COTMPlayerReplicationInfo PRI;

We then check to see who the Owner of the HUD is (Owner being a global variable with the details of the local player stored) first checking to make sure that it is indeed a Player and secondly that it's a Player running our mod! Since our mod only calculates the scores when a Player dies or leaves the zone we need someway of making the scores more real-time (ie. count up with the passing seconds) since the HUD is redrawn with each frame this provides the perfect place (although there are other ways but we'll save them for another day).

if ( PlayerOwner.PlayerReplicati None )
return;
if(!PlayerOwner.PlayerReplicationInfo.IsA('COTMPlayerReplicationInfo'))
return;
PRI = COTMPlayerReplicationInfo(PlayerOwner.PlayerReplicationInfo);
CurrentTime = PRI.TotalTime;
if(PRI.bIsInZone)
CurrentTime += (Level.TimeSeconds - PRI.ZoneEnterTime);
Text = CurrentTime$" seconds";

Having done this we then need to draw the score to the screen, firstly choosing the font-size and drawing it justified to the right (it'll extend to the right when the line gets longer not left). Finally we reset the color back to white. You can if you like now compile and test out the new changes although the Score Board still isn't working properly! But you'll need to add this line to the defaultproperties of COTM.uc:

HUDType=Class'COTM.COTMRuneHUD'

Now I can see how well I'm doing what about the other scum?

Its all very well to be able to see your own score, but this is a game. No matter what people say we are here to WIN! But what goods a victory only you can see. Lets modify the existing Score Board and replace the frag count with Zone Time, create a new file called COTMRuneScoreBoard.uc and add the following:

//=============================================================================
// COTMRuneScoreBoard
//=============================================================================
class COTMRuneScoreBoard expands RuneScoreBoard;

var COTMPlayerReplicationInfo COTMOrdered[32];

function DrawTableHeadings( canvas Canvas)
{
local float XL, YL;
local float YOffset;
local string ZoneTimeText; //Text to hold the table heading name

Canvas.DrawColor = GoldColor;
Canvas.StrLen("00", XL, YL);
YOffset = Canvas.CurY;

// Name
Canvas.SetPos(Canvas.ClipX*0.1, YOffset);
Canvas.DrawText(NameText, false);


// TimeInZone - draws a heading Time instead of Frags
Z;
Canvas.SetPos(Canvas.ClipX*0.5, YOffset);
Canvas.DrawText(ZoneTimeText, false);


// Draw Awards
Canvas.SetPos(Canvas.ClipX*0.8, YOffset);
Canvas.DrawText(AwardsText, false);

if (Canvas.ClipX > 512)
{
// Ping
Canvas.SetPos(Canvas.ClipX*0.7, YOffset);
Canvas.DrawText(PingText, false);
}

// Draw seperator
YOffset += YL*1.25;
Canvas.DrawColor = WhiteColor;
Canvas.SetPos(Canvas.ClipX*0.1, YOffset);
Canvas.DrawTile(Seperator, Canvas.ClipX*0.8, YL*0.5, 0, 0, Seperator.USize, Seperator.VSize);
YOffset += YL*0.75;
Canvas.SetPos(Canvas.ClipX*0.1, YOffset);
}

function DrawPlayerInfo( canvas Canvas, PlayerReplicationInfo PRI, float XOffset, float YOffset)
{
local bool bLocalPlayer;
local PlayerPawn PlayerOwner;
local float XL,YL;
local int AwardPos, CurrentTime;
local COTMPlayerReplicationInfo COTM; //CurrentTime and this line added to access our variables

PlayerOwner = PlayerPawn(Owner);
bLocalPlayer = (PRI.PlayerName == PlayerOwner.PlayerReplicationInfo.PlayerName);
Canvas.F />
// Draw Ready
if (PRI.bReadyToPlay)
{
Canvas.StrLen("R ", XL, YL);
Canvas.SetPos(Canvas.ClipX*0.1-XL, YOffset);
Canvas.DrawText(ReadyText, false);
}

if (bLocalPlayer)
Canvas.DrawColor = VioletColor;
else
Canvas.DrawColor = WhiteColor;

// Draw Name
if (PRI.bAdmin)
Canvas.F />else
Canvas.F />Canvas SetPos(Canvas.ClipX*0.1, YOffset);
Canvas.DrawText(PRI.PlayerName, false);
Canvas.F />
// Draw Time - does what it says on the tin!
COTM = COTMPlayerReplicationInfo(PRI);
CurrentTime = COTM.TotalTime;
if(COTM.bIsInZone)
currenttime += (Level.TimeSeconds - COTM.ZoneEnterTime);

Canvas.SetPos(Canvas.ClipX*0.5, YOffset);
Canvas.DrawText(currenttime, false);

if (Canvas.ClipX > 512 && Level.Netmode != NM_Standalone)
{
// Draw Ping
Canvas.SetPos(Canvas.ClipX*0.7, YOffset);
Canvas.DrawText(PRI.Ping, false);

// Packetloss

Canvas.F />Canvas DrawColor = WhiteColor;
}

// Draw Awards
AwardPos = Canvas.ClipX*0.8;
Canvas.DrawColor = WhiteColor;
Canvas.F />Canvas StrLen("00", XL, YL);
if (PRI.bFirstBlood)
{ // First blood
Canvas.SetPos(AwardPos-YL+XL*0.25, YOffset-YL*0.5);
Canvas.DrawTile(FirstBloodIcon, YL*2, YL*2, 0, 0, FirstBloodIcon.USize, FirstBloodIcon.VSize);
AwardPos += XL*2;
}
if (PRI.MaxSpree > 2)
{ // Killing sprees
Canvas.SetPos(AwardPos-YL+XL*0.25, YOffset-YL*0.5);
Canvas.DrawTile(SpreeIcon, YL*2, YL*2, 0, 0, SpreeIcon.USize, SpreeIcon.VSize);
Canvas.SetPos(AwardPos, YOffset);
Canvas.DrawColor = CyanColor;
Canvas.DrawText(PRI.MaxSpree, false);
Canvas.DrawColor = WhiteColor;
AwardPos += XL*2;
}
if (PRI.HeadKills > 0)
{ // Head kills
Canvas.SetPos(AwardPos-YL+XL*0.25, YOffset-YL*0.5);
Canvas.DrawTile(HeadIcon, YL*2, YL*2, 0, 0, HeadIcon.USize, HeadIcon.VSize);
Canvas.SetPos(AwardPos, YOffset);
Canvas.DrawColor = CyanColor;
Canvas.DrawText(PRI.HeadKills, false);
Canvas.DrawColor = WhiteColor;
AwardPos += XL*2;
}
Canvas.F />}
function SortScores(int N)
{
local int i,j,Max;
local COTMPlayerReplicationInfo TempCOTM;

for (i=0; i<n-1; i++)br=""> {
Max = i;
for (j=i+1; j<n; j++)br=""> {
if (COTMOrdered[j].TotalTime > COTMOrdered[Max].TotalTime)
Max=j;
else if ((COTMOrdered[j].TotalTime == COTMOrdered[Max].TotalTime) && (COTMOrdered[j].Deaths < COTMOrdered[Max].Deaths))
Max=j;
else if ((COTMOrdered[j].TotalTime == COTMOrdered[Max].TotalTime) && (COTMOrdered[j].Deaths == COTMOrdered[Max].Deaths) &&
(COTMOrdered[j].PlayerID < COTMOrdered[Max].Score))
Max=j;
}

TempCOTM = COTMOrdered[Max];
COTMOrdered[Max] = COTMOrdered[i];
COTMOrdered[i]= TempCOTM;
}
}

function ShowScores( canvas Canvas )
{
local int PlayerCount, I;
local float XL, YL;
local float YOffset, YStart;
local COTMPlayerReplicationInfo PRI;

// Sort the PRIs
for (i=0; i<arraycount(cotmordered); i++)br=""> COTMOrdered[i]= None;
for (i=0; i<32; i++)
{
if (PlayerPawn(Owner).GameReplicationInfo.PRIArray[i]!= None)
{
PRI = COTMPlayerReplicationInfo(PlayerPawn(Owner).GameReplicationInfo.PRIArray[i]);
if ( !PRI.bIsSpectator || PRI.bWaitingPlayer )
{
COTMOrdered[PlayerCount] = PRI;
PlayerCount++;
if (PlayerCount == ArrayCount(Ordered))
break;
}
}
}
SortScores(PlayerCount);

Canvas.F />Canvas.DrawColor = WhiteColor;

// Header
DrawHeader(Canvas);
DrawTableHeadings(Canvas);

// Calculate vertical spacing
Canvas.StrLen("TEST", XL, YL);
YStart = Canvas.CurY;

//TODO: Calculate continuous spacing based on screensize available

if (PlayerCount < 15)
YL *= 2;
else if (PlayerCount < 20)
YL *= 1.5;
if (PlayerCount > 15)
PlayerCount = FMin(PlayerCount, (Canvas.ClipY - YStart)/YL - 1);

DrawBackground(Canvas, 0.1*Canvas.ClipX, YStart-YL*0.25+1, 0.8*Canvas.ClipX, PlayerCount*YL);

YOffset = YStart;
for ( I=0; I<playercount; i++="" )br=""> {
YOffset = YStart + I*YL;
DrawPlayerInfo(Canvas, Ordered[I], 0, YOffset);
}

// Draw bottom seperator
Canvas.StrLen("TEST", XL, YL);
YOffset += YL;
Canvas.SetPos(0, YOffset);

// Trailer
DrawTrailer(Canvas);

Canvas.DrawColor = WhiteColor;
}

defaultproperties
{
}

Looks horribly complicated doesn't it and to explain it all would take some considerable writing. I've placed all the bits I've added in bold so you can see them. The basic flow is that ShowScores() collects all the PlayerReplicationInfo for every player from an array which is held in the GameReplicationInfo (yes it really does exist). This is then sorted in SortScores with the largest time in the zone first followed by lowest deaths and finally PlayerID (the logic is fairly straight-forward if you write down what is happening). In the same way as the HUD for each player a continuous update of time spent in the zone is calculated and displayed. The reason for the large amounts of code is that you can't just declare the previous functions using Super.FunctionName() as we need to completely change and amend code that is part of the previous functions so we need to include the whole of previous functions regardless of whether we need to alter it or not. Now add the following line into the defaultproperties of COTM.uc and your ready to compile:

ScoreBoardType=Class'COTM.COTMRuneScoreBoard'

Whats next then?

That completes the FFA version of Chieftan of the Mound. In the next installment I'll introduce you to the teamplay version which will incorporates the ability to have more than one zone which are fought over ala Domination, up to 4 teams, the ability to add names to the zones to personalise maps and of course more HUD and Score Boards as well as everything needed to make it fit together!

Post a comment
Sign in or join with:

Only registered members can share their thoughts. So come on! Join the community today (totally free - or sign in with your social account on the right) and join in the conversation.

Tutorial
Browse
Tutorials
Views
60 (2 today)
Share
Related Games
Rune
Rune Adventure
Related Engines
Unreal Engine 1
Unreal Engine 1 Commercial
Related Groups
Gathering of Developers
Gathering of Developers Developer & Publisher
Human Head Studios
Human Head Studios Developer