The idea behind this page is this Infogrames Forum thread and this other Infogrames Forum thread further pushed it. :)
For those who doubt Epic's intentions as to what a mutator should do you need look no further than the header comments in Engine.Mutator.
Mutators allow modifications to gameplay while keeping the game rules intact.
There are several problems when coding a new mutator or gametype. This page deals with problems that are often neglected: Compatibility with other mods and mutators.
This page can probably be divided in subpages for Game Types and Mutators later, but for now two headings should be enough.
Contents
Mutators
Avoid replacing GameInfo-specific classes such as the HUD, the ScoreBoard, the PlayerReplicationInfos (or PRI for short), Pawn classes, Controller classes and the GameReplicationInfo (GRI).
Just imagine what happens when two mutators try to replace the PRI with their own versions. One of the mutators definately won't work. Maybe the gametype itself depends on its own PRI class, so both mutators can't be used with that gametype.
How to work round these problems:
HUD / ScoreBoard Additions
Use Interactions for this.
Storing and/or Replicating Per-Player Information
Sure, the most straightforward idea is replacing the PRI, but there's another way: Create your own ReplicationInfo subclass. This class might look like this:
class MyReplicationInfo extends ReplicationInfo; var PlayerReplicationInfo MyPRI; // Pawns or Controllers aren't generally available on clients var int MyImportantVariable; // the value you want to replicate per-player replication { unreliable if ( Role == ROLE_Authority ) MyPRI, MyImportantVariable; }
You only need to find and use the new MyReplicationInfo actors now. If you set the MyPRI
variable to reference the associate player's PlayerReplicationInfo, that's easily done both server- and client-side with the DynamicActors iterator:
function MyReplicationInfo FindInfoFor(PlayerReplicationInfo PRI) { local MyReplicationInfo MyReplicationInfo; foreach DynamicActors(Class'MyReplicationInfo', MyReplicationInfo) if (MyReplicationInfo.MyPRI == PRI) return MyReplicationInfo; return None; // no matching MyReplicationInfo actor found }
Movement and Physics: The Controller Classes
Regulating the amount of dodges per jump
You'd expect modifying the amount of dodging moves allowed per jump is deeply burried in one of the controller classes. Well, it is, but I was able to "hack into" the PlayerController class without actually subclassing it:
class DodgeHack extends Info; var PlayerController Controller; function PostBeginPlay() { Controller = PlayerController(Owner); Super.PostBeginPlay(); } function Tick(float DeltaTime) { if ( Controller == None ) { Destroy(); return; } if ( Controller.DoubleClickDir >= DCLICK_Active ) { Controller.ClearDoubleClick(); Controller.DoubleClickDir = DCLICK_None; } }
This simple piece of code re-enables the walldodge after a dodging move has been executed. It only needs to be spawned via:
Spawn(class'DodgeHack', thePlayerController);
Why does this work? Well, the PlayerController class has a DoubleClickDir property which stores the current state of the dodging move. (see Actor/Enums/EDoubleClickDir) You could very well limit this to 2 dodges per jump-session, using the fact that the count should be reset whenever the movement type is walking, a way to see whether people have landed.
Detecting which direction a player is pressing
This might be of use whenever you want to do a combo system of your own, without having to adhere to the strict rules Epic set for these combos, AND without having to replace the controller class. (The rules I was talking about: You can only do a combo when you're not currently using one, each combo must have 4 keypresses etc.)
The trick to use is that the original controller class stores the direction(s) a player is pressing in the OldKey variable, to compare it with the next tick. We could compose this variable ourselves by checking the aBaseY and aStrafe variables, but these are reset after the player's movement code. (meaning we only get second servings)
Now, if you want to change the xPawn's PlayerTick function without replacing it, just make sure there's a clientside object that has the exact same function, but replace the
if (aForward > 0) CurrentKey = CurrentKey | 1; // CK_Up else if (aForward < 0) CurrentKey = CurrentKey | 2; // CK_Down if (aStrafe > 0) CurrentKey = CurrentKey | 4; // CK_Left else if (aStrafe < 0) CurrentKey = CurrentKey | 8; // CK_Right
with something like this:
CurrentKey=CheckPlayer.OldKey;
And the rest of the function should work fine! (of course, any variables that belong to the player should have this CheckPlayer. in front of them, but that's only detail)
Now you can change whatever you want in the function. For an example of this code in action, check the Advanced Adrenaline mutator once it's out. (I will replace that with a link once it is :))
Explore the various functions and properties used in the controller classes to see if you can modify something without actually subclassing a Controller or Pawn class. Ofter there are ways to avoid subclassing, try to find them.
Game Types
With game types you don't have any problems replacing Controllers, Pawns, the HUD or any other things specific for game types. Still there are some things you should check. These include Mutator and GameRules function calls.
Some things to watch out for when subclassing certain classes:
GameInfo
The Login function should call BaseMutator.ModifyLogin before it parses any of the options.
RestartPlayer calls BaseMutator.PlayerChangedClass when the player changed his/her class since the last respawn.
Some functions ask the mutators to return a default weapon class via the mutators' GetDefaultWeapon function. You should do the same in every place where that class is used. (or cache the output in a class variable)
The SetPlayerDefaults function executes BaseMutator.ModifyPlayer. This function is used by mutators to initialize or update player-specific things and should always be called.
PreventDeath calls the GameRulesModifiers.PreventDeath function if there are any GameRules.
ReduceDamage should always call GameRulesModifiers.NetDamage. This is broken in UT2k3's TeamGame class, so please try to fix it in your GameInfo class. If you don't want GameRules classes to modify the actual damage you can still return a different value, but NetDamage often does other things based on the damage and not calling it might render those GameRules classes useless.
In PickupQuery the GameRules' OverridePickupQuery function is called and has the chance to make the gametype skip the HandlePickupQuery function of the Pawn's Inventory items.
GameRules also get a chance to override the RestartGame function with their own HandleRestartGame function, but they might as well only prepare for this event instead of completely handling it themselves.
GameRules can also are allowed to define additional end-of-game conditions via their CheckEndGame function which is called by the GameInfo's own CheckEndGame function.
This list is far from complete, but should give you an idea of what you have to look out for. Mutators, GameRules and also Controllers are allowed to handle a lot of things.
Related Topics
Discussion
Wormbo: This page has become neccessary since more and more mutator authors solve their problems (and unknowingly create a lot of new ones) by subclassing gametype-specific classes and replace them in their mutator. I hope it will minimize the number of incompatible mutators although I'm confident that there will still be enough people ignoring this fact. :(
Tarquin: How about we put a link to this page from all class pages that should NOT be subclassed in modding – there's a chance authors will look up those pages for info. :)
Wormbo: Good idea. /me goes and adds some linkage... :)
Trystan: At least here alternatives are proposed. A lot of the time you're working on something and don't really have time to wade through the various places to find an alternative - you just want to prototype and see if it works. With the alternatives to common pitfalls listed here you've saved the programmer that much time and made it that much more probable that he will do as the page suggests.
Boksha: Added a way to "replace" the PlayerTick function without replacing the controller.
Dingus: Not quite understanding the 'Replace PlayerTick' method described above. I need to do so in UT99 in order to check/modify mouseinput (aMouseX, aMouseY) on the client. Can you give a more specific example OR sample code. Thx.
Tarquin: When you make a custom PRI, how do you go about spawning them for each player? What about the LinkedReplicationInfo CustomReplicationInfo property in PlayerReplicationInfo? Basically, what's the "way in"?