news about forums links tools tutorials contact

King Of The Hill - author: crack_addict
King of the Hill (KOTH for short) changes the goal of the game to being in a certain area for the longest amount of time, instead of the normal goal of getting the most frags. The way we accomplish this in UT is the creation of a new game type to make some basic gameplay changes and changes to the players (kothGame), a new ZoneInfo subclass (kothZone) to place in the level to designate the "Hill". We also have a new PlayerReplicationInfo subclass (kothPRI) to hold the time spent on the "Hill", a new HUD subclass to change the fragcount to display KOTH time instead, and a new scoreboard as well to do the same.
New game types are created under the GameInfo class, and they handle the basic rules-keeping of the game, creating/logging in players, creating/removing bots, and other miscellaneous stuff (like mutators and default inventory). Some noteworthy functions are InitGame() which initializes game data, Login() which handles when a new player (warm body) gets created in the game, RestartPlayer() which handles when a player (bot/human) is respawned, ScoreKill() which handles a death in the game, and finally SpawnBot() which handles the creation of new bots. If you are interested in making new game types, it is a good idea to run through GameInfo and DeathmatchPlus to get an idea of the setup.

    // ============================================================
    // koth.kothGame
    // ============================================================

    class kothGame expands DeathMatchPlus;

    // Change InitGame to zero out the fraglimit since
    // the point of the game isn't frags with koth.
    event InitGame( string Options, out string Error )
    {
      Super.InitGame(Options, Error);

      Fraglimit = 0;
    }

    // In order to keep track of the time the player has spent
    // in the kothZone, we need to use a new PlayerReplicationInfo.
    // In order to make sure our players use the new replication
    // info we change the Default replication info class before
    // they login, so that it is created instead of the normal
    // one.
    event playerpawn Login
    (
      string Portal,
      string Options,
      out string Error,
      class<playerpawn> SpawnClass
    )
    {
      // A quick change of the default class...
      SpawnClass.Default.PlayerReplicationInfoClass = class'koth.kothPlayerReplicationInfo';

      // and then login as normal.
      return Super.Login(Portal, Options, Error, SpawnClass);
    }

    // RestartPlayer() is called whenever a player respawns, and
    // we just use it to reset the ZoneEnterTime so as to not throw
    // off the calculations.
    function bool RestartPlayer( pawn aPlayer )

    {
      if (aPlayer.PlayerReplicationInfo.IsA('kothPlayerReplicationInfo'))
        kothPlayerReplicationInfo(aPlayer.PlayerReplicationInfo).ZoneEnterTime = 0;
      else if (aPlayer.PlayerReplicationInfo.IsA('kothBotReplicationInfo'))
        kothBotReplicationInfo(aPlayer.PlayerReplicationInfo).ZoneEnterTime = 0;

      return Super.RestartPlayer(aPlayer);
    }

    // ScoreKill() normally would determine who would get a frag
    // for a kill, or a negative frag if it was a suicide.  Instead
    // we use it to check if the player that died was in a kothZone,
    // and if so we calculate the time spent in the zone, just as
    // in the ActorLeaving() function in kothZone.  We need to do this
    // because ActorLeaving() isn't called until the player respawns,
    // so this way they can't just sit waiting to respawn and rack
    // up time.
    function ScoreKill(pawn Killer, pawn Other)
    {
      local kothPlayerReplicationInfo kPRI;
      local kothBotReplicationInfo kBRI;

      if (Other.IsA('PlayerPawn'))
      {
        if (PlayerPawn(Other).PlayerReplicationInfo.IsA('kothPlayerReplicationInfo'))
        {
          kPRI = kothPlayerReplicationInfo(PlayerPawn(Other).PlayerReplicationInfo);
          if (kPRI.ZoneEnterTime != 0)
            kPRI.ZoneExitTime = Level.TimeSeconds;
          kPRI.TotalTime += kPRI.ZoneExitTime - kPRI.ZoneEnterTime;
          kPRI.ZoneExitTime = 0;
          kPRI.ZoneEnterTime = 0;
          kPRI.bIsInZone = false;
          BroadcastMessage(kPRI.PlayerName$" left with "$kPRI.TotalTime$" total time.");
        }
      }
      else if (Other.IsA('Bot'))
      {
        if (Bot(Other).PlayerReplicationInfo.IsA('kothBotReplicationInfo'))
        {
          kBRI = kothBotReplicationInfo(Bot(Other).PlayerReplicationInfo);
          if (kBRI.ZoneEnterTime != 0)
            kBRI.ZoneExitTime = Level.TimeSeconds;
          kBRI.TotalTime += kBRI.ZoneExitTime - kBRI.ZoneEnterTime;
          kBRI.ZoneExitTime = 0;
          kBRI.ZoneEnterTime = 0;
          kBRI.bIsInZone = false;
          BroadcastMessage(kBRI.PlayerName$" left with "$kBRI.TotalTime$" total time.");
        }
      }

      BaseMutator.ScoreKill(Killer, Other);
    }

    // Bot's aren't as easy to change the default PlayerReplicationInfo,
    // we have to alter the SpawnBot() function to change the class, and
    // unfortunately we can't just call the Parent function after we
    // make our change, so this is just another cut & paste.
    function Bot SpawnBot(out NavigationPoint StartSpot)
    {
      local bot NewBot;
      local int BotN;
      local Pawn P;
      local class<bot> BotSpawnClass;

      if ( bRatedGame )
        return SpawnRatedBot(StartSpot);

      Difficulty = BotConfig.Difficulty;

      if ( Difficulty >= 4 )
      {
        bNoviceMode = false;
        Difficulty = Difficulty - 4;
      }
      else
      {
        if ( Difficulty > 3 )
        {
          Difficulty = 3;
          bThreePlus = true;
        }
        bNoviceMode = true;
      }
      BotN = BotConfig.ChooseBotInfo();

      // Find a start spot.
      StartSpot = FindPlayerStart(None, 255);
      if( StartSpot == None )
      {
      log("Could not find starting spot for Bot");
      return None;
      }

      // Get the spawn class and then change the replication
      // info class, then try to spawn it normally.
      BotSpawnClass = BotConfig.CHGetBotClass(BotN);
      BotSpawnClass.Default.PlayerReplicationInfoClass = class'koth.kothBotReplicationInfo';
      NewBot = Spawn(BotSpawnClass,,,StartSpot.Location,StartSpot.Rotation);

      if ( NewBot == None )
        log("Couldn't spawn player at "$StartSpot);

      if ( (bHumansOnly || Level.bHumansOnly) && !NewBot.bIsHuman )
      {
        log("can't add non-human bot to this game");
        NewBot.Destroy();
        NewBot = None;
      }

      if ( NewBot == None )
        {
            BotSpawnClass = BotConfig.CHGetBotClass(0);
            BotSpawnClass.Default.PlayerReplicationInfoClass = class'koth.kothBotReplicationInfo';
                    NewBot = Spawn(BotSpawnClass,,,StartSpot.Location,StartSpot.Rotation);
        }

      if ( NewBot != None )
      {
        // Set the player's ID.
        NewBot.PlayerReplicationInfo.PlayerID = CurrentID++;

        NewBot.PlayerReplicationInfo.Team = BotConfig.GetBotTeam(BotN);
        BotConfig.CHIndividualize(NewBot, BotN, NumBots);
        NewBot.ViewRotation = StartSpot.Rotation;
        // broadcast a welcome message.
        BroadcastMessage( NewBot.PlayerReplicationInfo.PlayerName$EnteredMessage,
     false );

        ModifyBehaviour(NewBot);
        AddDefaultInventory( NewBot );
        NumBots++;
        if ( bRequireReady && (CountDown > 0) )
          NewBot.GotoState('Dying', 'WaitingForStart');
        NewBot.AirControl = AirControl;

        if ( (Level.NetMode != NM_Standalone) && (bNetReady || bRequireReady) )
        {
          // replicate skins
          for ( P=Level.PawnList; P!=None; P=P.NextPawn )
            if ( P.bIsPlayer && (P.PlayerReplicationInfo != None) &&
     P.PlayerReplicationInfo.bWaitingPlayer && P.IsA('PlayerPawn') )
            {
              if ( NewBot.bIsMultiSkinned )
                PlayerPawn(P).ClientReplicateSkins(NewBot.MultiSkins[0],
     NewBot.MultiSkins[1], NewBot.MultiSkins[2], NewBot.MultiSkins[3]);
              else
                PlayerPawn(P).ClientReplicateSkins(NewBot.Skin);
            }
        }
      }

      return NewBot;
    }

    defaultproperties {
      HUDType=class'koth.kothChallengeHUD'
      ScoreBoardType=class'koth.kothScoreBoard'
    }

Zones are actors used to describe various areas in a level, such as water, lava, and in our case the "Hill" zone. Zones have all sorts of nifty variables, such as gravity, friction, etc etc...basically you can totally change the way a level plays by simply tweaking the zones. In kothZone we use the two functions ActorEntered() and ActorLeaving() to calculate the time spent in the zone, and other that its a normal zone.

    // ============================================================
    // koth.kothZone
    // ============================================================

    class kothZone expands ZoneInfo;

    // ActorEntered() is called whenever an actor enters the zone
    // and we use it here to log the time so that we can calculate
    // the total time spent in the zone when the actor leaves.
    event ActorEntered( actor Other )
    {
      // These are just some vars to save us from extra typing :)
      local kothPlayerReplicationInfo kPRI;
      local kothBotReplicationInfo kBRI;

      // Okay, if they are a player and have the right replication
      // info we set the time they entered and set bIsInZone to
      // true.  The code is the same for bots, except since they
      // have a different replication info class than players we
      // have to special handle it.
      if (Other.IsA('PlayerPawn'))
      {
        if (PlayerPawn(Other).PlayerReplicationInfo.IsA('kothPlayerReplicationInfo'))
        {
          kPRI = kothPlayerReplicationInfo(PlayerPawn(Other).PlayerReplicationInfo);
          kPRI.ZoneEnterTime = Level.TimeSeconds;
          BroadcastMessage(kPRI.PlayerName$" entered.");
          kPRI.bIsInZone = true;
        }
      } else if (Other.IsA('Bot'))
      {
        if (Bot(Other).PlayerReplicationInfo.IsA('kothBotReplicationInfo'))
        {
          kBRI = kothBotReplicationInfo(Bot(Other).PlayerReplicationInfo);
          kBRI.ZoneEnterTime = Level.TimeSeconds;

          BroadcastMessage(kBRI.PlayerName$" entered.");
          kBRI.bIsInZone = true;
        }
      }

      // Always be sure to call the parent's version of the function
      // unless we want to break things.
      Super.ActorEntered(Other);
    }

    // ActorLeaving() is called when an actor leaves the zone, and
    // here we calculate the time they spent in the zone and set their
    // TotalTime.  Note that when a player dies they don't leave the
    // zone until they actually respawn, so we have to check for
    // death in kothGame so that a player can't just sit and rack
    // up kothTime waiting to respawn.
    event ActorLeaving( actor Other )
    {
      local kothPlayerReplicationInfo kPRI;
      local kothBotReplicationInfo kBRI;

      // If they are a player and have the right replication info
      // just add the difference from when they entered and left
      // to the total time, and then reset everything.  Same code
      // for bots, just have to special handle it.
      if (Other.IsA('PlayerPawn'))
      {
        if (PlayerPawn(Other).PlayerReplicationInfo.IsA('kothPlayerReplicationInfo'))
        {
          kPRI = kothPlayerReplicationInfo(PlayerPawn(Other).PlayerReplicationInfo);
          if (kPRI.ZoneEnterTime != 0)
            kPRI.ZoneExitTime = Level.TimeSeconds;
          kPRI.TotalTime += kPRI.ZoneExitTime - kPRI.ZoneEnterTime;
          kPRI.ZoneExitTime = 0;
          kPRI.ZoneEnterTime = 0;
          kPRI.bIsInZone = false;
          BroadcastMessage(kPRI.PlayerName$" left with "$kPRI.TotalTime$" total time.");
        }
      }
      else if (Other.IsA('Bot'))
      {
        if (Bot(Other).PlayerReplicationInfo.IsA('kothBotReplicationInfo'))
        {
          kBRI = kothBotReplicationInfo(Bot(Other).PlayerReplicationInfo);
          if (kBRI.ZoneEnterTime != 0)
            kBRI.ZoneExitTime = Level.TimeSeconds;
          kBRI.TotalTime += kBRI.ZoneExitTime - kBRI.ZoneEnterTime;
          kBRI.ZoneExitTime = 0;
          kBRI.ZoneEnterTime = 0;
          kBRI.bIsInZone = false;
          BroadcastMessage(kBRI.PlayerName$" left with "$kBRI.TotalTime$" total time.");
        }
      }

      Super.ActorLeaving(Other);
    }

    defaultproperties {
    }

Unreal/UT uses a seperate class called PlayerReplicationInfo to store some semi-persistent data for each player, seperate from the PlayerPawn class. By subclassing the PRI class you can easily add some variables to the players specific to your modification.

    // ============================================================
    // koth.kothPlayerReplicationInfo
    // ============================================================

    class kothPlayerReplicationInfo expands PlayerReplicationInfo;

    // These are the new variables we need for KOTH
    // TotalTime being the total time tallied after leaving
    // the kothZone, ZoneEnterTime is set when we entered
    // the zone, and ZoneExitTime is set when we left.
    // bIsInZone is just a boolean to determine if we need
    // to calculate the latest time in the Hud/Scoreboard,
    // or if we can just use TotalTime
    var float TotalTime;         // in seconds
    var float ZoneEnterTime;
    var float ZoneExitTime;
    var bool bIsInZone;

    defaultproperties {
    }

The HUD class is called every frame to draw all the nifty things like health, fragcount, etc etc for each player. Ultimately you'll probably want to go through every function in the HUD and customize the entire look for you modification, but since KOTH is a simple mod, we're going to make a simple change to the HUD. :) All we do is change the fragcount from displaying frags to displaying the KOTH Time.

    // ============================================================
    // koth.kothChallengeHUD
    // ============================================================

    class kothChallengeHUD expands ChallengeHUD;

    // For our HUD all we do is change DrawFragCount()
    // to display the kothTime instead of the frags, since
    // time is more important than frags.  Just a cut and
    // paste with a few alterations...
    simulated function DrawFragCount(Canvas Canvas)
    {
      local float Whiten;
      local int X,Y, CurrentTime;
      local kothPlayerReplicationInfo kPRI;

      if ( PawnOwner.PlayerReplicationInfo == None )
        return;

            // If our owner isn't a kothPlayer then something
            // is wrong, so abort! abort! abort! :)
        if ( !PawnOwner.PlayerReplicationInfo.IsA('kothPlayerReplicationInfo') )
          return;

        // CurrentTime is calculated by taking the TotalTime,
        // and then if the player is in a koth Zone, it determines
        // the latest time.
        kPRI = kothPlayerReplicationInfo(PawnOwner.PlayerReplicationInfo);
        CurrentTime = kPRI.TotalTime;
        if (kPRI.bIsInZone)
          CurrentTime += Level.TimeSeconds - kPRI.ZoneEnterTime;

      Canvas.Style = Style;
      if ( bHideAllWeapons || (HudScale * WeaponScale * Canvas.ClipX <=
     Canvas.clipx - 256 * scale) )
        y = Canvas.clipy - 63.5 * scale;
      else
        y = Canvas.clipy - 127.5 * scale;
      if ( bhideallweapons )
        x = 0.5 * Canvas.clipx - 256 * scale;
      Canvas.curx = x;
      Canvas.cury = y;
      Canvas.drawcolor = hudcolor;
      whiten = level.timeseconds - scoretime;
      if ( whiten < 3.0 )
      {
        if ( hudcolor == goldcolor )
          Canvas.drawcolor = whitecolor;
        else
          Canvas.drawcolor = goldcolor;
        if ( level.bhighdetailmode )
        {
          Canvas.curx = x - 64 * scale;
          Canvas.cury = y - 32 * scale;
          Canvas.style = erenderstyle.sty_translucent;
          Canvas.drawtile(texture'botpack.hudweapons', 256 * scale, 128 * scale, 0, 128, 256.0, 128.0);
        }
        Canvas.curx = x;
        Canvas.cury = y;
        whiten = 4 * whiten - int(4 * whiten);
        Canvas.drawcolor = Canvas.drawcolor + (hudcolor - Canvas.drawcolor) * whiten;
      }

      Canvas.drawtile(texture'botpack.hudelements1', 128*scale, 64*scale, 0, 128, 128.0, 64.0);
      Canvas.drawcolor = whitecolor;

            // normally it would draw the frag count right here, but
            // we just alter it to draw the time, and all is good.
      drawbignum(Canvas, currenttime, x + 40 * scale, y + 16 * scale);
    }

    defaultproperties {
    }

We make pretty much the same change to the Scoreboard as we did to the HUD, just changing frags to KOTH time. Again you'll probably want to go through all the functions and customize the Scoreboard specific to your mod.

    // ============================================================
    // koth.kothscoreboard
    // ============================================================

    class kothscoreboard expands tournamentscoreboard;

    // We're going to change drawcategoryheaders() to only draw
    // koth time, and not frags/deaths/etc.
    function drawcategoryheaders(Canvas Canvas)
    {
      local float offset, xl, yl;

      offset = Canvas.cury;
      Canvas.drawcolor = whitecolor;

      Canvas.strlen(playerstring, xl, yl);
      Canvas.setpos((Canvas.clipx / 8)*2 - xl/2, offset);
      Canvas.drawtext(playerstring);

      Canvas.strlen("KOTH Time", xl, yl);
      Canvas.setpos((Canvas.clipx / 8)*5 - xl/2, offset);
      Canvas.drawtext("KOTH Time");

    }

    // Also need to adjust drawnameandping to draw koth time instead
    // of frags and deaths, etc.  Note this is mainly just a cut
    // and paste of the tournament scoreboard, I didn't really see
    // the need to make a brand new scoreboard, just change the
    // existing one slightly to fit our needs.
    function DrawNameandPing(Canvas Canvas, PlayerReplicationInfo pri, float xOffset, float yOffset)
    {
      local float xl, yl, xl2, yl2, xl3, yl3;
      local font Canvasfont;
      local bool blocalplayer;
      local playerpawn playerowner;
      local int time, kothtime;
      local kothplayerreplicationinfo kpri;
      local kothbotreplicationinfo kbri;

      playerowner = playerpawn(owner);

      bLocalPlayer = (pri.playername == playerowner.playerreplicationinfo.playername);
      Canvas.font = myfonts.getbigfont(Canvas.Clipx);

      // Draw name
      if ( pri.badmin )
        Canvas.Drawcolor = whitecolor;
      else if ( blocalplayer )
        Canvas.Drawcolor = goldcolor;
      else
        Canvas.Drawcolor = cyancolor;

      Canvas.setpos(Canvas.Clipx * 0.1875, yoffset);
      Canvas.Drawtext(pri.playername, false);

      Canvas.strlen( "0000", xl, yl );

      // Draw score
      if ( !blocalplayer )
        Canvas.Drawcolor = lightcyancolor;

            // here is where we determine the up-to-date time
            // to display, and just replace what used to Draw
            // the score with our up-to-date time. :)
      if (pri.isa('kothplayerreplicationinfo'))
      {
        kpri = kothplayerreplicationinfo(pri);
        kothtime = kpri.totaltime;
        if (kpri.bisinzone)
          kothtime += level.timeseconds - kpri.zoneentertime;
      }
      else if (pri.isa('kothbotreplicationinfo'))
      {
        kbri = kothbotreplicationinfo(pri);
        kothtime = kbri.totaltime;
        if (kbri.bisinzone)
          kothtime += level.timeseconds - kbri.zoneentertime;
      }

      Canvas.Strlen( kothtime, xl2, yl );
      Canvas.Setpos( Canvas.Clipx * 0.625 + xl * 0.5 - xl2, yoffset );
      Canvas.Drawtext( kothtime, false );

      if ( (Canvas.Clipx > 512) && (Level.NetMode != NM_Standalone) )
      {
        Canvas.DrawColor = WhiteColor;
        Canvas.Font = MyFonts.GetSmallestFont(Canvas.ClipX);

        // Draw Time
        Time = Max(1, (Level.TimeSeconds + PlayerOwner.PlayerReplicationInfo.StartTime - PRI.StartTime)/60);
        Canvas.TextSize( TimeString$": 999", XL3, YL3 );
        Canvas.SetPos( Canvas.ClipX * 0.75 + XL, YOffset );
        Canvas.DrawText( TimeString$":"@Time, false );

        // Draw FPH
        Canvas.TextSize( FPHString$": 999", XL2, YL2 );
        Canvas.SetPos( Canvas.ClipX * 0.75 + XL, YOffset + 0.5 * YL );
        Canvas.DrawText( FPHString$": "@int(60 * PRI.Score/Time), false );

        XL3 = FMax(XL3, XL2);
        // Draw Ping
        Canvas.SetPos( Canvas.ClipX * 0.75 + XL + XL3 + 16, YOffset );
        Canvas.DrawText( PingString$":"@PRI.Ping, false );

        // Draw Packetloss
        Canvas.SetPos( Canvas.ClipX * 0.75 + XL + XL3 + 16, YOffset + 0.5 * YL );
        Canvas.DrawText( LossString$":"@PRI.PacketLoss$"%", false );
      }
    }

    defaultproperties {
    }