• Register
Post tutorial Report RSS Unreal Learning #7: Game Rules

This tutorial will show you how to write a mutator that uses Game Rules. The code was written by Erwan 'Xiongmao' Allain for Shee Labs Mutator Week.

Posted by on - Basic Server Side Coding

Today we're going to recreate a mutator Erwan 'Xiongmao' Allain wrote for Shee Labs Mutator Week! You can read about it here: Moddb.com

It's very short, very sweet and easy to modify to suit your needs. This time, we're going to use Game Rules. Game Rules are special, meaning you can deliberately effect game mode behavious without having to write a whole new game mode for a few little changes. If you were to make drastic changes, then perhaps it's time to write a new game mode instead.

Today, we're going to use the Game Rules to determine the best player and the worst player. We're also going to attach a light to the best and worst players, and we're going to give bonuses and maluses for killing them respectively.

First we need a mutator that implements a new game rules - this is easy. Don't forget to make sure your code is in a folder called 'BestWorst', or rename the code packages as you see fit.

class BestWorst extends UTMutator;

var class GRClass;

function InitMutator(string Options, out string ErrorMessage)
{
WorldInfo.Game.AddGameRules(GRClass);

Super.InitMutator(Options, ErrorMessage);
}

DefaultProperties
{
GRClass=class'BestWorst.BestWorst_Rules'
} 

Now the hard part. We want a new game rules class, so we derive from GameRules. We also want to get the controller of the best player, and the worst player. For our best and worst players, we need a PointLightComponent, both of which need a colour. Finally, we want a variable to tell us how many points to add or take away from a score.

class BestWorst_Rules extends GameRules;

var Controller m_bestPlayer;
var Controller m_worstPlayer;

var PointLightComponent m_bestPlayerLight;
var PointLightComponent m_worstPlayerLight;

var int m_bonus;
var int m_malus;

var color redColor;
var color greenColor;

defaultproperties
{
m_bonus = 2
m_malus = 2

redColor = (R=255,G=0,B=0,A=255);
greenColor = (R=0,G=255,B=0,A=255);
} 

Well, that's not too nasty. Next thing we're going to do, is to determine when a player is killed - and for that we have the ScoreKill function, which is called everytime a player is killed. We're also going to invent some new function to contain most of our logic (so our main function doesn't get huge!) as we write our code, so watch out for those in red.

function ScoreKill( Controller Killer, Controller Killed )
{
local int killerId, killedId, bestPlayerId, worstPlayerId;

killerId = Killer.playerReplicationInfo.PlayerId;
killedId = Killed.playerReplicationInfo.PlayerId;
bestPlayerId = getBestPlayerId( );
worstPlayerId = getWorstPlayerId( );

//check if the killed player is the worst player => malus
if(killedId == worstPlayerId)
{
setMalus(killer);
}

//check if the killed player is the best player = >bonus
if(killedId == bestPlayerId)
{
setBonus(killer);
}

//check if the killer was the worst player => bonus
if(killerId == bestPlayerId)
{
setBonus(killer);
}

//update status best and worst player
updateControllersStatus(killedId);

if ( NextGameRules != None )
{
NextGameRules.ScoreKill(Killer,Killed);
}
} 

So what happens? This is all quite easy. First things first, we want to get the Ids of both the player that is killed, and the player that kills him. We then also want to find the Ids of the 'best' and 'worst' players. We check to see if if the killed player is then the worst or the best, and set bonuses accordingly, and we also check if the killer is the best player and offer a bonus too. Finally we update the controllers based on the best and worst player, and check to see if there are any more Game Rules to be executed.

Next we need to write those sub functions. Let's get the best and worst player Ids. The best player can be the player with the highest score, but the least deaths, and the worst player can be the one with the lowest score and the most deaths.

function int getBestPlayerId()
{
local int i;
local PlayerReplicationInfo bestPlayer, currentPlayer;
for(i=0;i
{
currentPlayer = worldInfo.GRI.PRIArray[i];
if(i==0)
bestPlayer = currentPlayer;

if(bestPlayer!=none &amp;&amp; bestPlayer.score < currentPlayer.score)
bestPlayer = currentPlayer;

else if(bestPlayer!=none &amp;&amp; bestPlayer.score == currentPlayer.score)
{
if(bestPlayer.deaths > currentPlayer.deaths)
bestPlayer = currentPlayer;
}

}
return bestPlayer.playerId;
}

function int getWorstPlayerId()
{
local int i;
local PlayerReplicationInfo worstPlayer, currentPlayer;
for(i=0;i
{
currentPlayer = worldInfo.GRI.PRIArray[i];
if(i==0)
worstPlayer = currentPlayer;

if(worstPlayer!=none &amp;&amp; worstPlayer.score > currentPlayer.score )
worstPlayer = currentPlayer;

else if(worstPlayer!=none &amp;&amp; worstPlayer.score == currentPlayer.score)
{
if(worstPlayer.deaths < currentPlayer.deaths)
worstPlayer = currentPlayer;
}
}
return worstPlayer.playerId;
} 

Both functions essentially work the same way, so let's dissect getBestPlayerId, and I'll let you work out the rest. We get the GameReplicationInfo (GRI) from the worldinfo, and from that we can get an array of PlayerReplicationInfos (PRI) for every player in the game.

We cycle through this array, and on the first pass, we check to see if the beat player is the current player we're looking at. On successive passes, we check to see if there is a best player, and if their score is less than the current player we're looking at - if so, the best player is actually the current player. Otherwise, we check to see if there is a best player, and if the best player's score is the same as the current player we're looking at, then we compare their deaths. When we're finished checking through every player, then we return the playerId of the best player.

Read it through a few times and wrap your head around it, it's not as horrible as it sounds.

We also need to be able to set the player score relative to the bonus and the malus. This is really easy.We'll set the bonus points for the killing the best player to 2, and we'll set the malus points for killing the worst points to 2 as well - get 2 bonus points or lose 2 extra points.

function setBonus(Controller c)
{
c.PlayerReplicationInfo.score += m_bonus;
c.PlayerReplicationInfo.bForceNetUpdate = true;
c.PlayerReplicationInfo.Kills+= m_bonus;
}

function setMalus(Controller c)
{
c.PlayerReplicationInfo.score -= m_malus;
c.PlayerReplicationInfo.bForceNetUpdate = true;
c.PlayerReplicationInfo.Kills -= m_malus;
}

In these functions, we take the controller and find the score in the PRI. From there, we change their scores, force these to be updated on all clients and then update their kills.

Next we'll write the function that updates the controller status.

function updateControllersStatus(int killedId)
{
local Controller controller;
local int bestPlayerId, worstPlayerId, currentPlayerId;

bestPlayerId = getBestPlayerId();
worstPlayerId = getWorstPlayerId();

if(bestPlayerId == worstPlayerId)
return;

//detach previous light component
if(m_bestPlayer!=None &amp;&amp; m_bestPlayer.pawn != None)
m_bestPlayer.pawn.detachComponent(m_bestPlayerLight);

if(m_worstPlayer!=None &amp;&amp; m_worstPlayer.Pawn != None)
m_worstPlayer.pawn.detachComponent(m_worstPlayerLight);

m_bestPlayer = None;
m_worstPlayer = None;

//create the point light
if(m_bestPlayerLight == None)
m_bestPlayerLight = createPointLight(false);

if(m_worstPlayerLight == None)
m_worstPlayerLight = createPointLight(true);

//retrieve best and worst controller
ForEach DynamicActors(class'Controller', controller)
{
currentPlayerId = controller.PlayerReplicationInfo.PlayerId;

if(m_bestPlayer != None &amp;&amp; m_worstPlayer != None) //if best and worst found break
break;

if(currentPlayerId == bestPlayerId &amp;&amp; currentPlayerId != killedId)
m_bestPlayer = controller;

if(currentPlayerId == worstPlayerId &amp;&amp; currentPlayerId != killedId)
m_worstPlayer = controller;
}

//attach the light ccmponent
if(m_bestPlayer!=none &amp;&amp; m_bestPlayer.Pawn != None)
m_bestPlayer.pawn.attachComponent(m_bestPlayerLight);

if(m_worstPlayer!=none &amp;&amp; m_worstPlayer.pawn!=None)
m_worstPlayer.pawn.attachComponent(m_worstPlayerLight);
} 

This is quite a long one, but it's relatively incomplex. First things first, we get the best and worst playerIds. If the best player is the worst player, then there's no point doing anything, so we just return and do nothing.

We then remove any lights that we might have already attached from the previous best and worst players (we don't want players marked incorrectly!) and create a new point light for the current best and worst players. We cycle through the controllers, find the best and worst controller based on playerId (and if the same, just break and end the function), and finally actually attach the lights.

What's this in red? We haven't written a point light function yet!

function PointLightComponent createPointLight(bool red)
{
  local PointLightComponent plc;

  local class plcClass;

  plcClass = class'Engine.PointLightComponent';
  plc = new (self)plcClass;

plc.Radius=250;
plc.SetEnabled(true);
if(red)
plc.SetLightProperties(1000, redColor);
else
plc.SetLightProperties(1000, greenColor);
return plc;
} 

Nice an simple. We just create a new PointLightComponent, set it's radius, brightness and colour, enable it and return the light. That's it! We're done. Compile and try and stay in the green!

Post comment Comments
mikep
mikep

Thanks for the tutrial first off.

I think your for loops got clipped in your editing in getBestPlayerId() and getWorstPlayerId()

Shouldn't it be something like for(i=0;i < WorldInfo.Game.NumPlayers; i ++)

Also there are some missing semi-colons in the default propertis after assigning 2 to m_bonus and m_malus.

I had to typecast var class<GameRules> myClass; to get the WorldInfo.Game.AddGameRules(GRClass); to take the argument as well.

Can you put your files up?

Reply Good karma Bad karma+1 vote
fuNGoo
fuNGoo

First of all, I'd like to say how appreciative I am of the time you put into these great tutorials for beginners. However, it would a lot more helpful if the code was explained more thoroughly for our feeble minds.

Personally I'm having trouble remembering which functions and classes are the ones we're creating, and the ones already there for us to use. Perhaps you could create a primer tutorial like your third tutorial "Delving Deeper into UnrealScript\" except giving us an overview of common functions and classes we use often and explaining how they work or their usefulness, etc.

Or maybe a primer exists already that I'm not aware of? I know of the UnrealWiki but that database of information without actually applying to an exercise makes it hard for me to grasp the concepts.

Anyway just some (hopefully constructive) criticism to help others like me understand and learn better.

Reply Good karma Bad karma+1 vote
fuNGoo
fuNGoo

EDIT: Oops didn't mean to reply to you with my first post, mikep.

But it seems ambershee didn't address your last problem with the typecasting GameRules class. I'm also having that issue.

Reply Good karma Bad karma+1 vote
ambershee Author
ambershee

Yeah, looked into it. You should use
var class<GameRules> GRClass;

I'm not sure why it compiles for me, but it shouldn't. Updated tutorial.

Reply Good karma+1 vote
ambershee Author
ambershee

Well spotted, the loops have indeed been truncated. I'll fix that right away.

Also, you do not put parsing braces (semi colons) in default properties. I won't put the files up, because all the code should be right here :)

Reply Good karma+1 vote
ambershee Author
ambershee

Fixed. Loops are:

for(i=0;i<worldInfo.GRI.PRIArray.length;i++)
{
//code
}

Reply Good karma+1 vote
fuNGoo
fuNGoo

Hmm... seems I have another issue. In the PointLightComponent function, at the line:

plc = new (self)plcClass;

The compile window is telling me: Error, Type mismatch in '='

Reply Good karma Bad karma+1 vote
ambershee Author
ambershee

I've spotted precisely why that is - the text editor has treated something like an HTML tag when clearly it isn't (I've had no end of trouble with it).

the line that appears to read 'local class plcClass' should be 'local class<PointLightComponent> plcClass'.

Reply Good karma+1 vote
ambershee Author
ambershee

Ack. This is so frustrating. It cannot and will not accept the correct line, despite formatting, or even writing it into HTML.

Reply Good karma+1 vote
fuNGoo
fuNGoo

Cool, works now. Sucks though that the text formatting is causing so many typos and frustration. All that's left is for me to read through it all and try to understand what's going on.

Reply Good karma Bad karma+1 vote
ChainsawFilms
ChainsawFilms

I've altered the code with the new line 'local class<PointLightComponent> plcClass' but now I'm getting plc errors when I compile. Any chance the source code be downloaded?

I wsa looking at learning some uscript and was thinking of doing a mutator with more points for killing the best player - and then I come across this tutorial, so it's perfect LOL :)

Thanks!

Reply Good karma Bad karma+1 vote
p3gamer
p3gamer

//check if the killer was the worst player => bonus
if(killerId == bestPlayerId)
{
setBonus(killer);
}

Should it be:

"(killerId == worstPlayerId)" since we are rewarding the worst player?

In the GameInfo.uc class, you are awarded 1 point for killing someone no matter what, since this GameRule is applied/called later in the ScoreKill() via the "GameRulesModifiers.ScoreKill(Killer, Other);". So if your malus/penalty is 2, you will actually lose only 1 point. (at least in my case)

Has anyone tried testing this mutator when you have only 2 players? I have been getting strange logical issues. Ex. both players score and deaths = 0, so after the I kill the other player(first kill of the match), I tend to lose points, also it would make sense that one player will be the best player and the other will be the worst most of the time, but at times one player is green or red, and the other player is not.

I am working on modifying the if statements/logic


Reply Good karma Bad karma+1 vote
p3gamer
p3gamer

The if statemensts/logic look fine to me. I found that this mutator works better when there are more than 2 players. Also, I figured out that when the score is tied (ex. 0 to 0), you get 1 point for killing your opponent, but then the new game rules sees that the score is now (1 to 0), so you get penalized for killing first which then makes you the worst player. I believe that creating a class that extends the GameInfo.uc and modifying/overriding the scoreKill() function in that class, will fix the minor issues.

Thanks, I learned a lot from digging into this mutator

Reply Good karma Bad karma+1 vote
ambershee Author
ambershee

Thanks for the input - I'll look into it all =]

Reply Good karma+1 vote
axlefoley
axlefoley

Hello I have done the tutorial and it compiles fine but it doesn't seem to do much, the players don't change colour the points seem to only go up by 1 rather than what ever

Reply Good karma Bad karma+1 vote
axlefoley
axlefoley

accidently double posted

Reply Good karma Bad karma+1 vote
Post a comment

Your comment will be anonymous unless you join the community. Or sign in with your social account: