Making A Class-Based Mod
Dissecting Riftwar: an early UT2003 example
Note: from a post made to the BuF forums, this is an early rough draft. Mych asked me to put it up here. Blame him Technically this would/should/will follow somewhere down the "UTute/Learning UScript" path ... RegX
A class in Riftwar is ultimately defined by a SpeciesType class. It holds what the class looks like, what it's base attributes are, and it's default weapon/powerup. This is somewhat based on the Species mute left in by Epic (and revived by Brizz). Riftwar creates custom Species classes which hold all the unique aspects that will make a player class a player class. So let's take a look at one of these classes, with some added comments:
class SBSpecies_AlienB extends SBSpecies; function getDescription() {} // deprecated defaultproperties{ // all of the properties for Pawn.Setup - this is what the player class will look like. FemaleMeshName = "HumanFemaleA.EgyptFemaleA"; FemaleBodySkinName = "PlayerSkins.EgyptFemaleABodyA"; FemaleFaceSkinName = "PlayerSkins.EgyptFemaleAHeadA"; MaleMeshName = "HumanMaleA.EgyptMaleA"; ... FemaleSkeleton = "HumanMaleA.SkeletonMale"; GibGroup="xEffects.xAlienGibGroup" RaceNum=1 } defaultproperties { ReceivedDamageScaling=+1.0 // Player class stats like speed and damage control DamageScaling=+1.2 ... DefaultItem="xWeapons.ShockRifle" // Default Weapon for this player Class PowerUpCombo="xGame.ComboSpeed" DefaultHealth=120 DefaultAdrenaline=0 DefaultShieldAmount=0 }
OK, so that's a "player class". How do you use it? Well, you have to get the Pawn and Controller to set it up, and you have to get it all replicated on the network. Here's a tip - Species from the Pawn isn't replicated (it's considered client info), so you'll need to have the Controller tell the Pawn what to do with some replicated vars. Here is the order in which Riftwar takes a normal player who hasn't chosen a class and allows them to choose one:
- In RestartPlayer, the Controller is checked to determine if the player class has been chosen.
- If it's determined that the player hasn't chosen a class, it stops restarting the player, tells the controller to hide it's pawn and present the user with an interface to choose one.
- The interface selects the player class and assigns variables to the Controller to remember what class to use.
- The Controller destroys it's pawn and goes back to RestartPlayer
- RestartPlayer detects the new variables, assigns the Controller a pawn and sets variables on the pawn to determine what class to use. This is vital becase they way pawns are(n't) replicated, they are the real authority online as to their appearance.
- The Pawn's setup uses these variables (in Riftwar, it's two ints) to know what mesh and skin to use and what species to assign.
- The ModifyPlayer function looks at the Species to determine modifications/weapons the player class has.
- RestartPlayer finishes, the player is in the game with it's new player class.
Let's look at this with some more detail and point and the pieces of code which comprise it:
The player hits RestartPlayer in the gameinfo when he gets to the level, there it checks to see if he has chosen a class:
function RestartPlayer( Controller aPlayer ) { local NavigationPoint startSpot; local int TeamNum; local class<Pawn> DefaultPlayerClass; ... if ( aPlayer.Pawn == None ) { aPlayer.GotoState('Dead'); return; } if(SBPlayer(aPlayer) != none && !aPlayer.PlayerReplicationInfo.bOutOfLives) { if(aPlayer.PlayerReplicationInfo.Team == none){ // No team means they don't know what player class they are SBPlayer(aPlayer).overview(); // This function tells the Controller to hide the pawn, open the GUI return; // stop here - we can't restart the player yet }else{ SBPawn(aPlayer.Pawn).superSpecies = SBPlayer(aPlayer).superSpecies; // we know how to set this pawn, continue SBPawn(aPlayer.Pawn).subSpecies = SBPlayer(aPlayer).subSpecies; } ... }
There's some pawn spawning code and bot class choosing code surrounding it, but the meat if the if that says if they don't have a team, do the overview function in the custom controller, which looks like this:
exec function overview() { if(PlayerReplicationInfo.Team != none) {return;} Pawn.setCollision(false,false,false); // we might need the pawn info, so don't destroy it Pawn.bHidden=True; // but hide it from the game world ClientOpenMenu("SpeciesBattle.SBTrader"); // open the GUIPage to decide a player class }
So that will open the SBTrader, a GUIPage which is essentially the class chooser. From there, the player selects their class which results in:
function bool InternalOnClick(GUIComponent Sender) { local PlayerController pc; local SBPlayer spc; pc = PlayerOwner(); spc = SBPlayer(pc); if(spc==none){return false;} if(Sender==Controls[4]) // close { if(SpecList.List.Get() == HumanATitle) { // User has selected "Human Soldier" spc.setSubSpecies(1); // Set the correct Team/Player Class to the Controller spc.setSuperSpecies(0); spc.SetPlayerTeam(0); } ...
So selecting the "Human Soldier" in the list has the controller set the following vars - superSpecies and subSpecies, using client replicated functions:
class SBPlayer extends xPlayer; var bool bIsFemale; var int superSpecies; var int subSpecies; var int hCount,aCount,uCount; replication { // Things the server should send to the client. reliable if ( bNetDirty && (Role == Role_Authority) ) getTeamCount,hCount,aCount,uCount,bIsFemale,superSpecies,subSpecies; reliable if( Role<ROLE_Authority ) gotype,goteam,clearSpecies,SetPlayerTeam,overview,ReturnToWorld,setSubSpecies; } ... function setSuperSpecies(int s) { superSpecies = s; } function setSubSpecies(int s) { subSpecies = s; } ...
When you close the SBTrader GUIPage it hits the ReturnToWorld function in the SBPlayer controller. All this does is destroy the current Pawn ( which didn't have the info to setup correctly ) and tell it to try again:
function ReturnToWorld() { Pawn.Destroy(); // We'll get a new one log("SBPlayer "$PlayerReplicationInfo.Team); // This should never be blank at this point GotoState('Returning'); // Head back to RestartPlayer() }
The team is now set - so when we hit RestartPlayer, the pawn remains and gets a chance to setup it's mesh and skin. We have integers in our sub and superspecies to pick out what mesh and skin the pawn should use:
simulated function Setup(xUtil.PlayerRecord rec, optional bool bLoadNow) { if(PlayerReplicationInfo.Team != none) { // this should be redundant if(PlayerReplicationInfo.Team.TeamName == "Humans"){SuperSpecies = 0;} if(PlayerReplicationInfo.Team.TeamName == "Aliens"){SuperSpecies = 1;} if(PlayerReplicationInfo.Team.TeamName == "Undead"){SuperSpecies = 2;} } //LOG("SBPAWN Super:"$superSpecies$" Sub:"$SubSpecies); if(SuperSpecies == 0){ // Is Human if ( subSpecies == 1 ) { SBSpecies = class'SpeciesBattle.SBSpecies_HumanB'; //Is Fatboy Class Species = class'SpeciesBattle.SBSpecies_HumanB'; }else if ( subSpecies == 0 ){ Species = class'SpeciesBattle.SBSpecies_HumanA'; //Is Soldier Class SBSpecies = class'SpeciesBattle.SBSpecies_HumanA'; }else{ Species = class'SpeciesBattle.SBSpecies_HumanC'; //Is Mecher Class SBSpecies = class'SpeciesBattle.SBSpecies_HumanC'; } } ... RagdollOverride = rec.Ragdoll; if ( Species.static.Setup(self,rec) ){ResetPhysicsBasedAnim();}
Which in turn hits the Species setup, which I won't paste here as it basically just setups up the mesh and voice and is ugly but fairly straightforward.
From there, the ModifyPlayer function in the SpeciesType and the GameRules that are added from the Gameinfo handle the rest of the customization.
Another method is to simply trick the Pawn's setup to look at a specific PlayerName to setup the mesh and determine the species from a upl file - which is exactly how the game works normally. I'm personally not a big fan of upls for anything but bots, but it's a matter of preference. It would simplify the process some, however.
So ... *whew* . Um. Any questions?
Mychaeel: Well. I think we have a Category To Do (formatting, subheaders, maybe some of the phrasing)...
RegularX: I need to add that I just put in a similar framework for Freehold that works on the same principle, but with about 1/4 of the code required for the Controller and the GameInfo classes. I have zero time to update this page correctly now (besides, a wise programmer will learn from my mistakes ) - but basically the species/class is held in a single var on the controller, the gameinfo checks for that to produce the class chooser, and otherwise goes to the super.