news about forums links tools tutorials contact

CTF Scoring - author: DarkByte
This is the Tutorial for the CTFScoreCfg game MOD. I would like to start by saying that what I really wanted was to create a Mutator that would work only if the game type was CTF based. Step 1) Why it could not be a Mutator. Here is the definition of ScoreKill as found in the mutator class:
    function ScoreKill(Pawn Killer, Pawn Other)
That's a good start … now I know where I could have changed the scores but when is ScoreKill called ? I did a search in all .uc files … and it is found in many files including the CTFGame.uc file, the one that really interests me. Lets see:
    Botpack\Classes\CTFGame.uc(80):	BaseMutator.ScoreKill(Killer, Other);

    function ScoreKill(pawn Killer, pawn Other)
    {

      if( (killer == Other) || (killer == None) )
        Other.PlayerReplicationInfo.Score -= 1;
      else if ( killer != None )
      {
        killer.killCount++;
        if ( Killer.bIsPlayer && Other.bIsPlayer
          && (Killer.PlayerReplicationInfo.Team != Other.PlayerReplicationInfo.Team) )
          Killer.PlayerReplicationInfo.Score += 1;
      }
      if ( bAltScoring && (Killer != Other) && (killer != None) && Other.bIsPlayer )
        Other.PlayerReplicationInfo.Score -= 1;

      Other.DieCount++;

      BaseMutator.ScoreKill(Killer, Other);
    }
That's very fine and dandy … if killer is the same as killed it's a suicide and we lose 1 point. If someone else actually killed us, he gets 1 point and 1 kill. The score is retained in the PlayerReplicationInfo. If the game is working with alternate scoring, the killee loses 1 point. Then the killee gets his Die Count increased. Ok .. that's simple … in my Mutator, all I need is to check for the same states and revert what was done here for points (I don't want to change the Kill or Die count) … then I apply my personal scoring technique. Here is what it should look like:
    function ScoreKill(pawn Killer, pawn Other)
    {

       if ( killer != None && killer != Other )
      {
        //  Only if both are players and not in the same team.
        if ( Killer.bIsPlayer && Other.bIsPlayer
           && Killer.PlayerReplicationInfo.Team != Other.PlayerReplicationInfo.Team) )
        {
          // Remove the point that was awarded in CTFGame.ScoreKill()
          // and add (2 times subtract) the score that was chosen
          // by the server manager. Do the same for the points lost
          // or given for dying.

          Killer.PlayerReplicationInfo.Score -= 1 -MyKillScore;
          Other.PlayerReplicationInfo.Score += MyDeathScore;
        }
      }

      // Let the Mutator Class handle calling the next mutator.
      Super.ScoreKill(Killer, Other);
    }
Now I want to see if the killed person had the flag. Let find out how to tell, I would guess that the PlayerReplicationInfo has some information about that and I was right.
    var Decoration      HasFlag;
Here is my modified code:
    function ScoreKill(pawn Killer, pawn Other)
    {

       if ( killer != None && killer != Other )
      {
        //  Only if both are players and not in the same team.
        if ( Killer.bIsPlayer && Other.bIsPlayer
           && Killer.PlayerReplicationInfo.Team != Other.PlayerReplicationInfo.Team) )
        {
          // All we care is that the dead player had the flag.
          If ( Other.PlayerReplicationInfo.HasFlag )
          {
          // Remove the 5 points for killing the carrier and add
            // the score that was chose by the server manager.
            Killer.PlayerReplicationInfo.Score -= 5 -MyFlagKillScore;
            Other.PlayerReplicationInfo.Score += MyFlagDeathScore;
          }
          else
          {
          // Remove the point that was awarded in CTFGame.ScoreKill()
            // and add (2 times subtract) the score that was chosen
            // by the server manager. Do the same for the points lost
            // or given for dying.

            Killer.PlayerReplicationInfo.Score -= 1 -MyKillScore;
            Other.PlayerReplicationInfo.Score += MyDeathScore;
          }
        }
      }

      // Let the Mutator Class handle calling the next mutator.
      Super.ScoreKill(Killer, Other);
    }
Pretty simple heh … but guess what ? … It does not work. I could not understand so I digged deeper to understand what was going on. When Is the ScoreKill() function called in CTFGame.uc ? .. lets see … ah, there it is .. in GameInfo.Killed() .. it calls ScoreKill. You may wonder how I got to GameInfo ? .. lets see the structure of classes … CTFGame is derived from TeamGamePlus, which is derived from DeathMatchPlus, Which is derived from TournamentGameInfo which is derived from GameInfo. Also, lets take a look at Mutator.ScoreKill()
    function ScoreKill(Pawn Killer, Pawn Other)
    {
      // Called by GameInfo.ScoreKill()
      if ( NextMutator != None )
        NextMutator.ScoreKill(Killer, Other);
    }
Called by GameInfo.ScoreKill() … ok .. but where is GameInfo.ScoreKill called from ? … ok, GameInfo.Killed() …. So what does GameInfo.Killed() do ? Since I'm interested in CTFGame .. let's see how CTFGame.Killed() is handling it.
    function Killed( pawn Killer, pawn Other, name damageType )
    {
      local int NextTaunt, i;
      local bool bAutoTaunt;

      if ( Other.bIsPlayer && (Other.PlayerReplicationInfo.HasFlag != None) )
      {
        if ( (Killer != None) && Killer.bIsPlayer && Other.bIsPlayer &&
     (Killer.PlayerReplicationInfo.Team != Other.PlayerReplicationInfo.Team) )
        {
          killer.PlayerReplicationInfo.Score += 4;
          bAutoTaunt = ((TournamentPlayer(Killer) != None)
              && TournamentPlayer(Killer).bAutoTaunt);
          if ( (Bot(Killer) != None) || bAutoTaunt )
          {
            NextTaunt = Rand(class<ChallengeVoicePack>
                  (Killer.PlayerReplicationInfo.VoiceType).Default.NumTaunts);
            for ( i=0; i<4; i++ )
            {
              if ( NextTaunt == LastTaunt[i] )
                NextTaunt = Rand(class<ChallengeVoicePack>
                  (Killer.PlayerReplicationInfo.VoiceType).Default.NumTaunts);
              if ( i >> 0 )
                LastTaunt[i-1] = LastTaunt[i];
            }
            LastTaunt[3] = NextTaunt;
            killer.SendGlobalMessage(None, 'AUTOTAUNT', NextTaunt, 5);
          }
        }
        CTFFlag(Other.PlayerReplicationInfo.HasFlag).Drop(0.5 * Other.Velocity);
      }

      Super.Killed(Killer, Other, damageType);
    }
First, it gives 4 points to the flag carrier killer … killer.PlayerReplicationInfo.Score += 4;
But, wait a minute … Its telling the flag carrier to drop the flag … what is happening then ?

      Holder.PlayerReplicationInfo.HasFlag = None;

Its really resetting the HasFlag property to None .. which means that when ScoreKill() is called, the carrier does not have the flag anymore … darn. There is no way I can have the correct information in a Mutator .. so I have to make it a MOD.
Step 2) Making it a MOD
Okay, now I know I have to make it a MOD. It does not please me but I have no choice. I want it to be a CTF based MOD so I will derive a new class from CTFGame.

    //===============================================
    // CTFScoreCfgGame.
    //===============================================
    class CTFScoreCfgGame expands CTFGame
      config (CTFScoreCfg);

    var config int    RegularKillScore;
    var config int    FlagKillScore;
    var config int    RegularDeathScore;
    var config int    FlagDeathScore;
    var config int    FlagCaptureScore;
    var config int    FlagReturnScore;

The config extension is simply for keeping the values selected by the server manager into the CTFScoreCfg.ini file instead of UnrealTournament.ini .. this way, if one wants to get rid of CTFScoreCfg, he can delete CTFScoreCfg.* and all will be gone.
I also need some default initial values that I will use:

    defaultproperties
    {
      GameName="CTF Configurable Scoring"
      RegularKillScore=1
      RegularDeathScore=0
      FlagKillScore=5
      FlagDeathScore=0
      FlagCaptureScore=8
      FlagReturnScore=0
    }

I give it a new game name and put in the values that the game normally gives to players for the events. Now, instead of modifying the Mutator.ScoreKill() function, I will create my own .Killed() function where I will catch the player's flag state before the flag is dropped. Don't forget that the higher derived class gets the call first. Here is the code now.

    function Killed( pawn Killer, pawn Other, name damageType )
    {
      // Both must be players, and not from the same team …
      // check for Killer != None before checking anything else
      // from killer.
      if ( Other.bIsPlayer && Killer != None && Killer.bIsPlayer &&
    (Killer.PlayerReplicationInfo.Team != Other.PlayerReplicationInfo.Team) )
      {
        if ( Other.PlayerReplicationInfo.HasFlag != None )
        {
          killer.PlayerReplicationInfo.Score -= 5 - FlagKillScore;
          Other.PlayerReplicationInfo.Score += FlagDeathScore;
        }
        else
        {
          killer.PlayerReplicationInfo.Score -= 1 - RegularKillScore;
          Other.PlayerReplicationInfo.Score += RegularDeathScore;
        }

      }

      // Don't forget to call parent Killed Code.
      Super.Killed(Killer, Other, damageType);
    }

I also wanted to allow the score attributed to capturing the flag and returning the flag to change. After doing some long research, I stepped on the CTFGame.ScoreFlag() function. And as a bonus, this function is called whether the flag is Scored or Returned. Exactly what I was looking for. Here is the code I used:

    function ScoreFlag(Pawn Scorer, CTFFlag theFlag)
    {
      if (Scorer.PlayerReplicationInfo.Team != theFlag.Team)
        Scorer.PlayerReplicationInfo.Score -= 7 - FlagCaptureScore;
      else
        Scorer.PlayerReplicationInfo.Score += FlagReturnScore;

      // Again, be graceful and call parent code
      Super.ScoreFlag(Scorer, theFlag);
    }

Basically, if the team of the scorer is not same as the team of the flag, this means we captured the flag. Otherwise, we just returned our own flag.
Step 3) A user interface for choosing points for each event.
A while ago, I had downloaded the EavyHunter Mod and had seen that it added a menu entry in the Mod Menu. It also features a Dialog window with which one could select some options for it. The same applied for the ReProtect mutator. I wanted to have the same kind of dialog window for my MOD. I started by extracting the code for the ReProtect mutator … and looked how they did it. Its not that hard, at least once you have an example to follow. First, lets add the Menu.

    ///////////////////////////////////////////////////////
    // CTFScoreCfgMenuItem
    ///////////////////////////////////////////////////////
    class CTFScoreCfgMenuItem expands UMenuModMenuItem;

    function Execute()
    {
      MenuItem.Owner.Root.CreateWindow(class'CTFScoreCfgWindow',
          10, 10, 150, 100);
    }

    defaultproperties
    {
         MenuCaption="CTF Score Configuration"
         MenuHelp="Configure the points given when killing or being killed in CTF."
    }

As you can see … its not very hard. But you can see that its using the CreateWindow() function with the CTFScoreCfgWindow class when the menu entry is selected. What is in this file ?

    ///////////////////////////////////////////////////////
    // CTFScoreCfgWindow
    ///////////////////////////////////////////////////////
    class CTFScoreCfgWindow extends UMenuFramedWindow;

    function Created()
    {
      bSizable=False;

      SetSize(350, 190);

      WinLeft=(Root.WinWidth-WinWidth)/2;
      WinTop=(Root.WinHeight-WinHeight)/2;

      Super.Created();
    }

    function ResolutionChanged(float W, float H)
    {
      WinLeft=(Root.WinWidth-WinWidth)/2;
      WinTop=(Root.WinHeight-WinHeight)/2;

      Super.ResolutionChanged(W, H);
    }

    defaultproperties
    {
         ClientClass=Class'CTFScoreCfg.CTFScoreCfgClientWindow'
         WindowTitle="CTF Score Configuration"
    }

In Created(), we handle the initial properties of the window, like its size, and we center the window on the desktop. With ResolutionChanged() we recenter the window if the resolution is changed. The ClientClass default property is where we tell the framed window what its content will be. So again, we have to define a new window class. But a window client class this time.

    ///////////////////////////////////////////////////////
    // CTFScoreCfgClientWindow
    ///////////////////////////////////////////////////////
    class CTFScoreCfgClientWindow extends UMenuDialogClientWindow;

We need to define some variables. Our dialog will contain 1 push button marked as OK (I just realized I forgot to put a cancel button). We also will need 6 sliders. So lets define those needed local variables.

    var UWindowSmallButton    OKButton;
    var UWindowHSliderControl   RKSSlider;
    var UWindowHSliderControl   RDSSlider;
    var UWindowHSliderControl   FKSSlider;
    var UWindowHSliderControl   FDSSlider;
    var UWindowHSliderControl   FCSSlider;
    var UWindowHSliderControl   FRSSlider;

Ok .. we have our sliders and an OK button ready to put in the dialog. We also need to have some value retainers.

    var int    RegularKillScore;
    var int    RegularDeathScore;
    var int    FlagKillScore;
    var int    FlagDeathScore;
    var int    FlagCaptureScore;
    var int    FlagReturnScore;

And of course, we want our default text properties to be used. I am not sure exactly how default properties are set but it seems that the following variables automatically take the value of their default properties. My guess is that the name is very important.

    var string  OKHelp, OKText;
    var string  RKSHelp, RKSText;
    var string  RDSHelp, RDSText;
    var string  FKSHelp, FKSText;
    var string  FDSHelp, FDSText;
    var string  FCSHelp, FCSText;
    var string  FRSHelp, FRSText;

Now … again in the Created() function, we will initialize all our values and prepare the sliders to be useable.
For each of the sliders / values, I do the following:

      RegularKillScore=class'CTFScoreCfg.CTFScoreCfgGame'.default.RegularKillScore;
      RKSSlider=CreateSlider(RegularKillScore, RKSText, RKSHelp, nRow);
      nRow += 20;

Get the saved value that is stored in the CTFScoreCfgGame source file. Its actually saving all this to a CTFScoreCfg.ini file (makes it easier to clean if the Mod becomes unwanted).
Then I initialize each slider and increment the placement for the next slider. For more information about initializing, check the CreateSlider() and ScoreChanged() function, they should be self explanatory.
Handling the slider changes is also easy. Just handle all in the Notify() function. The 2 parameters contain the control and the type of notification. Again, should be self explanatory as you can see:

    function Notify(UWindowDialogControl C, byte E)
    {
      Super.Notify(C, E);

      switch(E)
      {
      case DE_Click:
        switch (C)
        {
        case OKButton:
          Close();
          break;
        }
      case DE_Change:
        switch(C)
        {
        case RKSSlider:
          ScoreChanged(RegularKillScore, RKSSlider, RKSText);
          break;

        case RDSSlider:
          ScoreChanged(RegularDeathScore, RDSSlider, RDSText);
          break;

        case FKSSlider:
          ScoreChanged(FlagKillScore, FKSSlider, FKSText);
          break;

        case FDSSlider:
          ScoreChanged(FlagDeathScore, FDSSlider, FDSText);
          break;

        case FCSSlider:
          ScoreChanged(FlagCaptureScore, FCSSlider, FCSText);
          break;

        case FRSSlider:
          ScoreChanged(FlagReturnScore, FRSSlider, FRSText);
          break;
        }
      }
    }

My final word on this tutorial is that you should not be afraid to look and search in the core classes and also to seek in others code and find what they did to achieve their results. Half of the work is to study, the other half is creating.