Reloading Weapons
Reloading Weapons.
I've put quite a bit of effort in to making robust reloading code for the Weapons in the LawDogs mod. Since reloading is something that is potentially useful to everyone, I have made the code available here (minus all the stuff that doesn't relate to reloading).
It is complete with features allowing for sounds, animations and other effects, it works on-line and as far as I can tell it is virtually free of bugs. All the main code is contained in a weapon class. It is an abstract class, the actual weapons are subclasses of it.
class ReloadingWeapon extends Weapon abstract; var() int ClipCount; //What's that you say? Clip is not a technically correct term? Do I care? var() float ReloadRate; //Time it takes to insert one bullet. var float ReloadTimer; var() sound ReloadBeginSound, ReloadSound, ReloadEndSound; //Sounds played when start to reload, on insertion of each bullet, and when reloading has ended. var() name ReloadAnim; //Animation to play when reloading is started. var() float ReloadAnimRate; var bool bIsReloading, bReloadEffectDone; replication { reliable if(Role == ROLE_Authority) ClipCount; //Functions called on the server from the client. reliable if(Role < ROLE_Authority) ReloadMeNow, FinishReloading; //Functions called on the client from the server. reliable if(Role == ROLE_Authority) ClientReload, ClientFinishReloading, ClientReloadEffects; } //So reloading can be bound to a key. Exec functions in weapons can only be called for the currently held weapon, which is perfect for this purpose. exec function ReloadMeNow() { if(!AllowReload()) return; bIsReloading = true; ReloadTimer = Level.TimeSeconds; PlaySound(ReloadBeginSound, SLOT_Misc, TransientSoundVolume,,,, false); ClientReload(); } //Called on the client when reloading starts. simulated function ClientReload() { bIsReloading = true; PlayAnim(ReloadAnim, ReloadAnimRate, 0.1); } //For effects during reloading, like smoke or shells ejected from the breech. simulated function ClientReloadEffects(){} //Reloading ends when the key is released. exec function FinishReloading() { if(!bIsReloading) return; PlaySound(ReloadEndSound, SLOT_Misc, TransientSoundVolume,,,, false); ClientFinishReloading(); bIsReloading = false; bReloadEffectDone = false; } //Called on the client when reloading ends. simulated function ClientFinishReloading() { bIsReloading = false; PlayIdle(); if(Instigator.PendingWeapon != None && Instigator.PendingWeapon != self) Instigator.Controller.ClientSwitchToBestWeapon(); } function bool AllowReload() { //Can't reload whilst firing. if(FireMode[0].IsFiring() || FireMode[1].IsFiring()) return false; //Can't reload if already reloading. else if(bIsReloading) return false; //Can't reload if already full. else if(ClipCount >= Default.ClipCount) return false; //Can't reload if not enough ammo. else if(Ammo[0] == None || Ammo[0].AmmoAmount <= ClipCount) return false; return true; } //Don't allow weapon switching whilst reloading. simulated function bool PutDown() { if(bIsReloading) return false; return Super.PutDown(); } simulated function BringUp(optional Weapon PrevWeapon) { Super.BringUp(PrevWeapon); bIsReloading = false; } //Reduce ClipCount every time a bullet is fired. function ConsumeAmmo(int Mode, float load) { if(Ammo[Mode] != None) { if(Ammo[Mode].UseAmmo(int(load)) && load > 0) ClipCount--; } } simulated function bool HasAmmo() { //Ignore FireMode[1] which doesn't use any ammo. return (Ammo[0] != None && FireMode[0] != None && Ammo[0].AmmoAmount >= FireMode[0].AmmoPerFire); } event WeaponTick(float dt) { if(!bIsReloading) { //Stupid bots like to run around with an empty weapon, so force them to reload when appropriate. if(!Instigator.IsHumanControlled()) { if(Level.TimeSeconds - Instigator.Controller.LastSeenTime > ClipCount) ReloadMeNow(); } } else { //Add one bullet at a time as long as reloading key is held down. if(Level.TimeSeconds - ReloadTimer >= ReloadRate) { if(ClipCount >= Default.ClipCount) //Full. { ClipCount = Default.ClipCount; FinishReloading(); } else if(Ammo[0].AmmoAmount <= ClipCount) //Out of ammo. { ClipCount = Ammo[0].AmmoAmount; FinishReloading(); } else //Add another bullet. { PlaySound(ReloadSound, SLOT_Misc, TransientSoundVolume,,,, false); InsertBullet(); if(ClipCount >= Default.ClipCount) { ReloadTimer = Level.TimeSeconds - (ReloadRate / 2); } else ReloadTimer = Level.TimeSeconds; } } else if(!bReloadEffectDone && Level.TimeSeconds - ReloadTimer >= ReloadRate / 2) { bReloadEffectDone = true; ClientReloadEffects(); } } } //Can play animations for inserting individual bullets here, or other effects. Server-side only, so should call a replicated function here and play animations in it, rather than in this function itself. function InsertBullet() { ClipCount++; } //Show how many bullets left until reload. simulated function float ChargeBar() { local float CurrentClip, MaxClip; //Store the int values as floats to avoid rounding errors. CurrentClip = ClipCount; MaxClip = Default.ClipCount; return FMin(1, CurrentClip/MaxClip); } function byte BestMode() { if(ClipCount > 0) return 0; return 1; } defaultproperties { ClipCount=6 ReloadRate=1.0 ReloadAnim=Reload ReloadAnimRate=1.0 bShowChargingBar=True }
Bullets are inserted one at a time. If you wanted to completely fill the weapon in one go (as would be the case with most modern-style weapons) then change the bit in the InsertBullet function to ClipCount = Default.ClipCount
To have reloading triggered from a key press, two functions need to be bound to one key. For example, in User.ini:
E=ReloadMeNow | OnRelease FinishReloading
To make it easier to bind for end-users, you can make a custom GUIUserKeyBinding, which will make the bind show up in the controls menu. Here is an example:
class ReloadBinding extends GUIUserKeyBinding; defaultproperties { KeyData(0)=(KeyLabel="YourMod",bIsSection=True) KeyData(1)=(Alias="ReloadMeNow | OnRelease FinishReloading",KeyLabel="Reload") }
As well as reloading from a key, it can also be done from alt-fire, like this:
class ReloadFire extends WeaponFire; event ModeDoFire() { ReloadingWeapon(Weapon).ReloadMeNow(); } function StopFiring() { ReloadingWeapon(Weapon).FinishReloading(); } function bool IsFiring() { return false; } defaultproperties { bModeExclusive=true bWaitForRelease=true FireRate=0.2 BotRefireRate=1.0 AmmoClass=class'BallAmmo' }
Last but not least, to ensure that the firemode(s) can only fire whilst there is ammo in the clip:
class ReloadFire extends ProjectileFire abstract; var float LastClickTime; var() Name EmptyAnim; var() float EmptyAnimRate; simulated function bool AllowFire() { if(ReloadingWeapon(Weapon).bIsReloading) return false; if(ReloadingWeapon(Weapon).ClipCount < 1) { if(Level.TimeSeconds - LastClickTime > FireRate) { Weapon.PlayOwnedSound(NoAmmoSound, SLOT_Interact, TransientSoundVolume,,,, false); LastClickTime = Level.TimeSeconds; if(Weapon.HasAnim(EmptyAnim)) weapon.PlayAnim(EmptyAnim, EmptyAnimRate, 0.0); } return false; } LastClickTime = Level.TimeSeconds; return Super.AllowFire(); } defaultproperties { }
In this case it is a ProjectileFire?, but it applies exactly the same for InstantFire classes.