The three virtues of a programmer: Laziness, Impatience, and Hubris. – Larry Wall
Legacy:VampireShoppers
Contents
Vampire Shoppers Tutorial: (IN DEVELOPMENT)[edit]
Vampire Shoppers is a mutator we're making as a class project for our Gaming class at Bellevue Community College
The goal of the project is to combine two mutators that we've made. The two mutators are Super Vampire (A vampire that steals adrenaline too, and gets bonuses for headshots) with Buy Mode. (You can't just pick up weapons, you buy them with adrenaline. You've gotta work for it now! Adrenaline junkies be afraid.) This tutorial is meant to walk people through everything we did. Hopefully it can be helpful to others. If you have any comments for us on how we can do things better, please post them.
Our style for this tutorial is pretty much to show you what we did, and try and explain it as best we can. It's more of a reference manual than a "now press the left mouse button" walkthrough. We figure it's more useful that way.
Make sure to download the complete source code here. It's extensively commented, and will serve as a sort of tutorial all of it's own. That should be your main source of information.
Some terminology[edit]
Throughout this tutorial we're going to be using some terms that might be unfamiliar. Here they are:
- Bottom Feeder
- The currently lowest ranked player. Yes, we stole the term from one of our favorite game modes, Mutant! We also stole some code from them too. We give full credit where it's due. We never would've been able to learn what we've learned without their fine example.
Where we started, two lonely mutators[edit]
This project was actually about working in a group to combine two separate mutators that we (as individuals) had already made. When we started we had two mutators:
- Buy Mode
- You start out with a shield gun and an assault rifle. In order to get new weapons you have to spend adrenaline. To choose which weapon you want you bring up a menu by pressing the 'b' key.
- Super Vampire
- Like normal vampire, except that if you hit the max health, you can still continue to gain health. (It forces your maximum to go up.) After all, what's the point of being a vampire if you can't just keep getting better and better. Just to make the code more 'interesting' it also ended up adding vampiric adrenaline gain/loss too, to parallel the health.
We were assigned to bring these two mutators together into one, and write this tutorial, but in the process we decided we wanted the following features:
- Buy Mode:
- You can now buy powerups too. (For great Double-Damage!)
- You can sell weapons back when they're empty (or before) in exchange for a small amount of adrenaline
- Improved (and now hierarchical) menu system. (Break things down into sub-menus, rather than have everything on one big menu.)
- If you're the worst ranked player in the game, you get a special discount on weapons and powerups. (Hopefully you can use this to improve your standings, or just cause more chaos.)
- Super Vampire:
- You're a vampire. Headshots should mean something significant, right? So if you get a headshot, you get an additional little dose of vampirism.
- If you were the worst ranked player in the game, you get a few bonus health and adrenaline points, hopefully allowing you to stay alive a bit longer, buy a better weapon, and improve your standings a bit. (makes for more chaos!)
Now for some depth, a look at each file and it's methods:[edit]
MutVampShopper.uc:[edit]
Main Mutator file, Lots of different functions, that do lots of different things. Probably best to go through them 1 and a time.
- Tick()
- Loads our interaction(VampShopper_Inter) every tick. That's essentially all this does.
- CheckReplacement()
- Hides all pickup, and weapon bases. Moves all pickups to an unreachable location. Can't just destroy the pickups since they are required for Buying Powerups.
- PostBeginPlay()
- Loads out gamerules class(VampShopper_Rules) into the game.
- IsRelevant()
- Taken from the No Adrenaline MOD. Removes all Adrenaline pickups, not sure if that's all it does.
VampShopper_Rules.uc:[edit]
This file actually sets up the game rules. It controls what should happen when a player takes damaage, or is killed.
It also controls which player is the "bottom feeder".
This is where the meat of the vampire side of things comes in.
- ScoreKill()
- This method is called (I think) whenever someone is killed. Credit the code pretty much to the Mutant gametype. The one thing that had to be done is that we had to use ScoreKill() instead of what Mutant used, which was NotifyKilled(). Not really sure of the difference, other than that NotifyKilled is a part of the gametype, but ScoreKill is the game's ruleset. This means that we should be able to operate in any game type. I think.
All that happens in this method is that whenever someone dies, we trigger an update to see who the current "bottom feeder" is. (By calling UpdateBottomFeeder() strangely enough... :-D)
- NetDamage()
- Whenever someone takes damage, this function is called. This is where we compute how much health should be gained vampirically, and how much adrenaline should be both gained and lost. This includes adding the bonuses for either a headshot, or the bottom feeder.
I'm not too happy with the way we do all the checks for headshots and all that. It seems inelegant, but at least it's pretty straightforward.
The only thing this function does that might be hard to understand is the check to see if DamageType==Headshot. HeadShot is defined as a default property. It's just a DamTypeSniperHeadShot class. I'd have liked a way to check for head shots with any weapon, but it's not a built-in concept to the game, and I was too lazy to change everything that would've been required for any-weapon headshots. (I think it would've taken creating a new damage type for each weapon.)
Other than that, there's nothing at all complicated about what this function does, it just runs through a bunch of if statements. Looking at the (well commented) code is your best bet to see exactly what's going on.
- ComparePRI()
- This method takes two players (actually, their PlayerReplicationInfo) and compares them, to see which of the players is ranked lower. (This method also was stolen directly from the Mutant gametype.) It's also a pretty straightforward method. Just compare various player stats to see who is better.
The important thing to realize about this is that it gets called over and over again on all players...
- UpdateBottomFeeder()
- Decides which player is actually the worst by looping through them and calling ComparePRI() to compare them. Voodoo Magic!
Actually it's fairly simple, even though it's probably the single most complicated part of this file.
oldBF = CurrentBottomFeeder;
CurrentBottomFeeder = None;
We set the current bottom feeder into a temporary variable, and then clear the variable that keeps track of who the bottom feeder is.
Player = Level.getLocalPlayerController();
This is something that was hard to figure out. This code was taken from the Mutant gametype, but in mutant it was part of a class that extended deathmatch. Apparently GameReplicationInfo is inherited somehow in that class. Apparently though, it's not inherited by a class that extends gamerules. So we had to go and get the GameReplicationInfo from somewhere. (I think that the way we're doing it here is actually about the worst way you could do it, but I couldn't find any clue on how to do it another way. -Josh)
for(i=0; i<Player.GameReplicationInfo.PRIArray.Length; i++)
{ myPRI = Player.GameReplicationInfo.PRIArray[i]; // Spectators/mutant can't be BF if( myPRI.bOnlySpectator ) //|| (CurrentMutant != None && myPRI == CurrentMutant.PlayerReplicationInfo) ) continue; if(CurrentBottomFeeder == None) CurrentBottomFeeder = Controller(myPRI.Owner); else { // Compare current BF with this PRI. If its worse, make it the new BF. if( ComparePRI(CurrentBottomFeeder.PlayerReplicationInfo, myPRI) > 0 ) CurrentBottomFeeder = Controller(myPRI.Owner); } }
There's really nothing terribly complex about this either. It loops through all available players, makes sure that they're a valid potential Bottom Feeder, (IE, not a spectator. In Mutant, there were other ways of not being a valid candidate for Bottom Feeder, but all we care about is that they're not a spectator.) sets them to the bottom feeder automatically if there isn't currently a bottom feeder, or compares the current player represented by the iteration of the loop to the bottom feeder (if there is a bottom feeder.), and sets whichever is worse to be the new bottom feeder. (Although, if the player who WAS the bottom feeder, is still worse, the bottom feeder doesn't actually appear to change.)
VampShopper_Inter.uc:[edit]
This file gets and gives activity from the hud and key inputs. (When the player asks for a redeemer, give it to them!)
Spawns shopping hud, and 2ndary deathmatch hud.
"Bleh" is needed to override the gun that's spawned on the shopping hud. (Otherwise it ends up in the wrong place when the menu is open.)
If buymode is true, than each render cycle will know that it should be displaying a buy menu of some sort or the other.
Extends off console(Which in turn extends off interaction). Handles all key interactions from the player, and controlls the Shopping Menus. Consists of very few function.
- Initialized()
- This gets run when the file is first accessed. Instantiates the ShoppingHUD(this caused another gun to render over the weapons bar from the base HUD, I'm not sure why), and a 2nd DeathMatchHud(To override the Gun from the Shopping HUD).
- PostRender()
- This calls the render functions on each of the HUDs instantiated by the Initialized function. Also turns some elements that conflict off on the 2nd DM HUD.
- BuyWeapon()
- Custom Function, called whenver a player buys a weapon. Checks to see if the player has the weapon, and if they have enough adrenaline to buy it. If both check out, it awars the player the weapon, and deducts the cost of it from the player's adrenaline. If they do not, it throws an error out to the client.
function BuyWeapon(class<weapon> Weapon,int cost)
{
//Checks to see if the player already has the weapon if(ViewPortOwner.Actor.Pawn.findInventorytype(Weapon) != none) { ViewportOwner.Actor.ClientMessage("You Already have this weapon."); } //Checks to see if the player has enough adrenaline to buy the weapon else if(ViewportOwner.Actor.Adrenaline >= cost) { //Spawn the weapon, and give it to the player. W = ViewportOwner.Actor.Pawn.Spawn(Weapon); W.GiveTo(ViewPortOwner.Actor.Pawn); //Turn buyMode off, and turn all menus off BuyMode = false; BuyMenu = 0; BuyHUD.BuyHUDOn3 = false; BuyHUD.BuyHUDOn2 = false; //Deduct Adrenaline ViewportOwner.Actor.Adrenaline -= cost; } else { //toss an error if you don't have enough adrenaline ViewportOwner.Actor.ClientMessage("You do not have the Adrenaline Required"); }
}
- SellWeapon()
- Custom Function, called whenever a player wants to sell a weapon. Checks to see if the player actually has the weapon they are trying to sell. If they do, it destroys the weapon, and awards the appropriate amount of adrenaline.
function SellWeapon(class<weapon> Weapon,int worth)
{
//Checks to make sure the player actually has the weapon they are trying to sell if(ViewPortOwner.Actor.Pawn.findInventorytype(Weapon) != none) { //Assigns the weapon in the inventory to a variable, and destroy it W = Weapon(ViewportOwner.Actor.Pawn.findInventorytype(Weapon)); W.Destroy(); //Exit buy mode ViewportOwner.Actor.PrevWeapon(); BuyMode = false; BuyMenu = 0; BuyHUD.BuyHUDOn3 = false; BuyHUD.BuyHUDOn1 = false; //Award Adrenaline ViewportOwner.Actor.Adrenaline += worth; } else { //Error message incase they don't have the weapon they are trying to sell. ViewportOwner.Actor.ClientMessage("You do not have that weapon"); }
}
- BuyPowerup()
- Custom Function, called whenever a player tries to purchase a powerup, does basiclly the same thing as BuyWeapon.
function BuyPowerup(class<Pickup> PowerUp,int cost)
{ //Checking to see if the player has enough adrenaline to buy this.
if(ViewportOwner.Actor.Adrenaline >= cost) { //Same as weapon, however this spawns in the same location as all other pickups on the map(see the main mutator file) P = ViewportOwner.Actor.Pawn.Spawn(PowerUp); //So we have to set it's location to the player's location P.SetLocation(ViewportOwner.Actor.Pawn.Location); //And run the touch function on the PickUP P.Touch(ViewportOwner.Actor.Pawn); //Then Destroy the pickup to keep it from respawning P.Destroy(); //And Turn BuyMOde off BuyMode = false; BuyMenu = 0; BuyHUD.BuyHUDOn3 = false; BuyHUD.BuyHUDOn = false; //And Deduct the adrenaline cost. ViewportOwner.Actor.Adrenaline -= cost; } else { ViewportOwner.Actor.ClientMessage("You do not have enough Adrenaline"); }
}
- KeyEvent()
- Built in function of the Interaction class. Handles all inputs from the keyboard. Toggles switches on the instaniated version of ShoppingHUD. This causes ShoppingHUD to make certain menus visible/invisible each time PostRender() is called. Calles bIgnoreKeys to check and see if any of the keys pressed are relevant to the menu system(Escape, and 1-8 num keys). If true, it continues down the function. If false, it throws back to the inbedded key rendering system. The rest of the function is only conserned with handling changes in the menu system. Changing menus, buying/selling weapons and powerups. All this is commented in the file for more detail.
ShoppingHUD.uc:[edit]
This file controls the HUD used for buying things.
There's a bool for each menu that could potentially be displayed. If it's true, render the menu it's related to.
Hud is "always on" just not visible until VampShopper_Inter.uc tells it to display something.
Controlls all the displaying of the Shopping Menus. Each menu has a bool applied to it, that controls if that menu should be Rendered or not. Also has 4 draw passes, DrawHudPassA, DrawHudPassB, DrawHudPassC, and DrawHudPassD. These draw the hud to the canvas, however I'm not entirely sure if/why all 4 of them are needed. This file also contains the pricing information that can be set by the user. Those variables are listed in the section below the booleans.
- bool BuyHUD3
- The base HUD, Allows client to select the Buy Weapon, Sell Weapon, or Buy Powerup menu.
- bool BuyHUD3On
- Tells the DrawHUD passes to make the base HUD menu visible, or invisible.
- bool BuyHUD2
- The BuyWeapon HUD, Allows client buy weapons.
- bool BuyHUD2On
- Tells the DrawHUD passes to make the BuyWeapon HUD menu visible, or invisible.
- bool BuyHUD1
- The SellWeapon HUD, Allows client sell weapons.
- bool BuyHUD1On
- Tells the DrawHUD passes to make the SellWeapon HUD menu visible, or invisible.
- bool BuyHUD
- The BuyPowerup HUD, Allows client buy powerups.
- bool BuyHUDOn
- Tells the DrawHUD passes to make the BuyPowerup HUD menu visible, or invisible.
Weapon Price Variables[edit]
- int CostWeapon_Bio
- Holds BioRifle price.
- int CostWeapon_Link
- Holds Linkgun price.
- int CostWeapon_Shock
- Holds ShockRifle price.
- int CostWeapon_Mini
- Holds Minigun price.
- int CostWeapon_Flak
- Holds FlakCannon price.
- int CostWeapon_Sniper
- Holds SniperRifle price.
- int CostWeapon_Rocket
- Holds RocketLauncher price.
- int CostWeapon_Redeemer
- Holds Redeemer price.
PowerUp Price Variables[edit]
- int CostPowerUp_MiniHealth
- Holds MiniHealthPack price.
- int CostPowerUp_Health
- Holds HealthPack price.
- int CostPowerUp_SuperHealth
- Holds SuperHealthPack price.
- int CostPowerUp_Shield
- Holds ShieldPack price.
- int CostPowerUp_SuperShield
- Holds SuperShieldPack price.
- int CostPowerUp_DoubleDam
- Holds DoubleDamage price.
Concepts we learned, or that are worth noting.[edit]
Things we still are trying to do:[edit]
- Make a mutator options menu. (Josh will work on.. But probably not in time for the project due date. It'd be a great feature, but.. Ah, to be using CVS. -Josh)
- Ok all done,The last track with the bottom feeder, ended up working. Also I just had to drop a resolution calculation into my drawtext calls to adjust their position.
- final version is up for download. Will post full source of each file(all heavily commented), and make revisions to the page tommarow. -Antone
Discussion[edit]
SuperApe: I have no idea how old this page is or if it is still in development. Can we get an update (with a date)?