TUTORIAL: New Ammo Types and Weapons That Reload

It's been asked of me for ages..."Weasel, when are you going to write a tutorial about reloading weapons?" Well, now that ZDoom's SVNs have effectively made a "canon" reload key, I feel more comfortable about teaching people how to use it.

A note before you begin: ZDoom's Reload feature was only made official as of SVN r3530 (which you can get here). For users of ZDoom-derivative ports that are not updated to this SVN revision (i.e. Zandronum), I'll have an extra section below, where I describe the "old" method of reloading that requires CustomInventory items.

So for reference, here is a basic weapon, derived from Doom's pistol with a couple of changes to make it more obvious what we're doing.

actor NewPistol : Weapon
{
 Weapon.SelectionOrder 1900
 Weapon.AmmoUse 1
 Weapon.AmmoGive 20
 Weapon.AmmoType "Clip"
 Inventory.Pickupmessage "Picked up a pistol."
 States
 {
 Ready:
  PISG A 1 A_WeaponReady
  Loop
 Deselect:
  PISG A 1 A_Lower
  Loop
 Select:
  PISG A 1 A_Raise
  Loop
 Fire:
  PISG A 4
  PISG B 6 A_FireBullets(4,4,-1,6,"BulletPuff",1)
  PISG C 4
  PISG B 5 A_ReFire
  Goto Ready
 Flash:
  PISF A 3 Bright A_Light2
  PISF A 3 Bright A_Light1
  PISF A 0 Bright A_Light0
  Goto LightDone
 Spawn:
  PIST A -1
  Stop
 }
}

As it stands, this code does work. However, at the moment, it is firing ammo directly from the player's inventory of bullets (the "Clip" ammo actor). This is perhaps the simplest possible variant of this sort of weapon. But what we want to do is make it reload - more accurately, we want the weapon to keep track of how much ammo is loaded in it, and require the player to pause for a moment to put more into it. And since it occurs to me that I haven't written a tutorial on ammo types before, I'll cover that, too.

ACTOR 9mmAmmo : Ammo replaces Clip
{
 Inventory.Amount 10 // You'll get half this if a monster drops it.
 Inventory.MaxAmount 150
 Ammo.BackpackAmount 30 // How much you'll get out of backpacks or equivalent.
 Ammo.BackpackMaxAmount 300 // The new max amount if player has a backpack.
 Inventory.Icon "CLIPA0" // This displays on the fullscreen HUD.
 Inventory.PickupMessage "Picked up some 9mm ammo."
 States
 {
 Spawn:
  CLIP A -1
  Stop
 }
}

The comments in the code above should make the inner workings of an ammo type fairly obvious. There are a number of things I haven't mentioned above, though. If you decide you want to make a larger (or smaller) version of the ammo pickup, like Doom's Box of Bullets, you'd inherit from the previous ammo type instead of just from Ammo, like so:

ACTOR 9mmAmmoSmall : 9mmAmmo
{
 Inventory.Amount 6
 Inventory.PickupMessage "Picked up a bit of 9mm ammo."
 Inventory.PickupSound "Beretta/Drop"
 +DROPPED
 States
 {
 Spawn:
  9MMP A -1
  Stop
 }
}

Notice the +DROPPED flag? The ZDoom Wiki has this to say about it: "Actor always acts as if it was dropped. Dropped items have two properties: They never respawn, and they will be crushed by doors and moving sectors." In this case, it also has the effect that it will give you half of the specified Inventory.Amount, similarly to how dropped Chainguns only give you 10 bullets instead of the 20 you'd get if you found one that was deliberately placed in a map. Since 9mmAmmoSmall inherits from 9mmAmmo, instead of defining a whole new ammo type, ZDoom treats it as an alternate pickup for the base type, so you get 9mmAmmo from picking this new item up.

So you can go ahead and change the NewPistol to use this new ammo type:

Weapon.AmmoType "9mmAmmo"

But we're not finished. We're here to make a gun that reloads, right? Let's define a whole new ammo type then:

ACTOR NewPistolLoaded : Ammo
{
 Inventory.MaxAmount 12
 +IGNORESKILL
}

Since you aren't going to be picking this ammo type up, there is a lot less that we need to do with it, code-wise. However, note another flag I've added, +IGNORESKILL. There is a special context to this flag. When you play Doom on Nightmare! or I'm Too Young To Die skill level, all ammo pickups are worth double what they usually give. +IGNORESKILL prevents this from happening on a per-ammo basis. This is important for our gun, for reasons I'll get into once we write our reloading code.

The easiest reloading system we can do is something Duke Nukem 3D-styled - this style of reloading does not require a Reload key and only keeps track of how many shots have been fired from the current weapon. In order to implement this, we'll need a new Reload state in the gun, and two simple lines in the Fire state.

Your reload state only really needs one important line for housekeeping purposes:

Reload:
  PISG A 1 A_TakeInventory("NewPistolLoaded", 12)
  Goto Ready

Now, in our case, we don't really need to give the amount parameter here; if A_TakeInventory is called with no amount specified (i.e. A_TakeInventory("NewPistolLoaded")), ZDoom will interpret this as a command to take ALL of that item. So really, take it or leave it.

This state will make more sense once you see how we're altering the Fire state:

Fire:
  PISG A 0 A_JumpIfInventory("NewPistolLoaded", 12, "Reload")
  PISG A 4 A_GiveInventory("NewPistolLoaded", 1)
  PISG B 6 A_FireBullets(4,4,-1,6,"BulletPuff",1)
  PISG C 4
  PISG B 5 A_ReFire
  Goto Ready

It's important to know how ZDoom's A_JumpIfInventory works. It only checks if the player has at least as many as specified. Using A_JumpIfInventory, it is not directly possible to check if a player has less than the specified amount, nor is it possible to check for the player to have none of an amount. (It is possible, however, to run your check "backwards": if you want to proceed only if the player has none of an item, you can check for the item, and should the check fail, it would fall through to a state that would behave as if there were no item. I will be making use of this later.) Therefore, the ammo type that we've called "NewPistolLoaded" is actually keeping track of how many shots have been fired, not how many are in the gun. Attempting to check for 0 of an inventory item is actually interpreted as checking for the maximum of that item, so in this case you could actually change the 12 to a 0 and have the same functionality should you later decide to change the weapon's magazine size (i.e. the MaxAmount of NewPistolLoaded).

One other thing. Remember that +IGNORESKILL flag we added to NewPistolLoaded? That plays into the A_GiveInventory function - if the player is playing on a skill setting that affects ammo pickups, this also affects the giving of ammo items through weapon code and scripts. This means that players playing with this weapon on skill 1 or 5 will only be able to fire 6 shots from this pistol before needing to reload, since A_GiveInventory is doubled on those skill levels. Not the most desirable thing in the world, so +IGNORESKILL is added to that ammo type to prevent that from happening.

The code as it stands isn't really doing anything noticeable, so let's add a cheap-ass reloading animation to the Reload state.

Reload:
  PISG A 1 Offset(0,35) A_PlayWeaponSound("weapons/shotgr") // click-clack.
  PISG A 1 Offset(0,38)
  PISG A 1 Offset(0,44)
  PISG A 1 Offset(0,52)
  PISG A 1 Offset(0,62)
  PISG A 1 Offset(0,72)
  PISG A 1 Offset(0,82)
  TNT1 A 8 A_TakeInventory("NewPistolLoaded", 12)
  PISG A 1 Offset(0,82)
  PISG A 1 Offset(0,72)
  PISG A 1 Offset(0,62)
  PISG A 1 Offset(0,52)
  PISG A 1 Offset(0,44)
  PISG A 1 Offset(0,38)
  PISG A 1 Offset(0,35)
  PISG A 1 Offset(0,32)
  Goto Ready

I believe I already went on about Offsets in How to Make Your First Basic Gun.

So here's where our weapon is at so far:

actor NewPistol : Weapon
{
 Weapon.SelectionOrder 1900
 Weapon.AmmoUse 1
 Weapon.AmmoGive 20
 Weapon.AmmoType "9mmAmmo"
 Weapon.SlotNumber 2 // Forgot to put this in earlier...
 Inventory.Pickupmessage "Picked up a pistol."
 States
 {
 Ready:
  PISG A 1 A_WeaponReady
  Loop
 Deselect:
  PISG A 1 A_Lower
  Loop
 Select:
  PISG A 1 A_Raise
  Loop
 Fire:
  PISG A 0 A_JumpIfInventory("NewPistolLoaded", 12, "Reload")
  PISG A 4 A_GiveInventory("NewPistolLoaded", 1)
  PISG B 6 A_FireBullets(4,4,-1,6,"BulletPuff",1)
  PISG C 4
  PISG B 5 A_ReFire
  Goto Ready
 Reload:
  PISG A 1 Offset(0,35) A_PlaySound("weapons/shotgr", CHAN_WEAPON) // click-clack.
  PISG A 1 Offset(0,38)
  PISG A 1 Offset(0,44)
  PISG A 1 Offset(0,52)
  PISG A 1 Offset(0,62)
  PISG A 1 Offset(0,72)
  PISG A 1 Offset(0,82)
  TNT1 A 8 A_TakeInventory("NewPistolLoaded", 12)
  PISG A 1 Offset(0,82)
  PISG A 1 Offset(0,72)
  PISG A 1 Offset(0,62)
  PISG A 1 Offset(0,52)
  PISG A 1 Offset(0,44)
  PISG A 1 Offset(0,38)
  PISG A 1 Offset(0,35)
  PISG A 1 Offset(0,32)
  Goto Ready
 Flash:
  PISF A 3 Bright A_Light2
  PISF A 3 Bright A_Light1
  PISF A 0 Bright A_Light0
  Goto LightDone
 Spawn:
  PIST A -1
  Stop
 }
}

This pistol will fire 12 shots, and on the 12th, will dip off screen and play clicky noises. When it comes back up, it will be "loaded" again. Congratulations, you've made a weapon that reloads.

But wait, I hear you say, what about those modern games that let you reload in the middle of a clip by pressing a button? Well, there's another neat function you can use - and here's the great bit, it's part of a function you've already been using since the very first tutorial! (Zandronum users, you may ignore this and look at the special section later in the tutorial.)

Ready:
  PISG A 1 A_WeaponReady(WRF_ALLOWRELOAD)
  Loop

That's right, A_WeaponReady accepts flags! If you don't specify a flag for A_WeaponReady (you usually don't need to), the default behavior is to check for presses of the Fire or Altfire buttons and go to the appropriate state. What this WRF_ALLOWRELOAD flag does, though, is add an extra check for a third button: Weapon Reload. Go check out your Customize Controls menu in ZDoom. Right up there with your Fire and Alternate Fire buttons are two new keys: Weapon Reload and Weapon Zoom. Reload is the one we want for now. With that flag in place, ZDoom checks for presses of the Weapon Reload key and redirects the weapon to the Reload state. (Hey, didn't we actually call our state Reload? That means you can press Reload with your mod loaded and reload mid-clip!)

Now that we've figured out how to reload mid-clip, there are actually two other reload systems we can use. The first, and simpler to code, is a system that takes entire clips. The second counts individual bullets. We'll cover the first system first, of course.

First, let's modify the ammo types for the current weapon, so the correct ammo type is taken when the weapon is fired (and so the reserve ammo is still displayed on screen). For this system (and the next one), we're using the NewPistolLoaded as the primary ammo type, and the ammo pickup as the secondary.

Weapon.AmmoType1 "NewPistolLoaded"
 Weapon.AmmoUse1 1
 Weapon.AmmoGive1 0 // AmmoGive is actually 0 by default, so this isn't strictly necessary.
 Weapon.AmmoType2 "9mmAmmo"
 Weapon.AmmoUse2 0
 Weapon.AmmoGive2 20

You should pay particular care that the "in clip" ammo (NewPistolLoaded, in this case) is not given from weapon pickups, so that the weapon does not magically reload itself when you pick up another one. (If you want the weapon to start off fully-loaded instead of empty, give it to the player via Player.StartItem in a custom player class, or via an ACS script. If you didn't understand that sentence, don't worry about it.)

We'll obviously also need to alter our Reload state, otherwise the first shot from this weapon will completely empty it and you'll never be able to fire it.

Reload:
  PISG A 1 Offset(0,35) A_PlaySound("weapons/shotgr", CHAN_WEAPON) // click-clack.
  PISG A 1 Offset(0,38)
  PISG A 1 Offset(0,44)
  PISG A 1 Offset(0,52)
  PISG A 1 Offset(0,62)
  PISG A 1 Offset(0,72)
  PISG A 1 Offset(0,82)
  TNT1 A 0 A_GiveInventory("NewPistolLoaded", 12)
  TNT1 A 8 A_TakeInventory("9mmAmmo", 1) // We'll treat one 9mmAmmo as one full clip.
  PISG A 1 Offset(0,82)
  PISG A 1 Offset(0,72)
  PISG A 1 Offset(0,62)
  PISG A 1 Offset(0,52)
  PISG A 1 Offset(0,44)
  PISG A 1 Offset(0,38)
  PISG A 1 Offset(0,35)
  PISG A 1 Offset(0,32)
  Goto Ready

You will obviously also want to remove the A_GiveInventory and A_JumpIfInventory from the Fire state. Go ahead and test it. You should notice two major issues with the code as it stands. Firstly, you can continuously reload your gun, even if it's full, and waste 9mmAmmo in the process. Second, if the weapon is empty, it auto-switches to a different weapon. The latter can be fixed with one flag:

+Weapon.Ammo_Optional

This flag actually has a number of effects, of which we're really only after one: the weapon will not switch away if AmmoType1 is empty. It also allows you to keep selecting the weapon even if both AmmoType1 and AmmoType2 are empty, which depending on what you're doing with it, may not be desirable, in which case you'd also add the flag +Weapon.Ammo_CheckBoth (see this ZDoom Wiki page for more information), which will only allow you to select the weapon if at least one of its ammo pools has ammo in it. Granted, that doesn't completely fix it - it makes the weapon instead able to keep firing even when empty. So where's our middle ground? Easy: A_JumpIfNoAmmo.

Fire:
  PISG A 0 A_JumpIfNoAmmo("Reload")
  PISG A 4
  PISG B 6 A_FireBullets(4,4,-1,6,"BulletPuff",1)
  PISG C 4
  PISG B 5 A_ReFire
  Goto Ready

There's still a huge hole here, in that even if you have no extra clips, you can still reload the gun. That's because we're not doing anything to check if the player has any left. So we'll fix both that and the problem where you can reload with a full clip by doing some essential checking in the Reload state before letting anything happen to the gun at all.

Reload:
  PISG A 0 A_JumpIfInventory("NewPistolLoaded", 12, 2) // If the gun's full, jump 2 states.
  PISG A 0 A_JumpIfInventory("9mmAmmo", 1, "ReloadWork") // If there's extra ammo, reload.
  PISG A 1 // Here's where that first line ends up - the gun does nothing and returns to Ready.
  Goto Ready
 ReloadWork:
  PISG A 1 Offset(0,35) A_PlayWeaponSound("weapons/shotgr") // click-clack.
  PISG A 1 Offset(0,38)
  PISG A 1 Offset(0,44)
  PISG A 1 Offset(0,52)
  PISG A 1 Offset(0,62)
  PISG A 1 Offset(0,72)
  PISG A 1 Offset(0,82)
  TNT1 A 0 A_GiveInventory("NewPistolLoaded", 12)
  TNT1 A 8 A_TakeInventory("9mmAmmo", 1) // We'll treat one 9mmAmmo as one full clip.
  PISG A 1 Offset(0,82)
  PISG A 1 Offset(0,72)
  PISG A 1 Offset(0,62)
  PISG A 1 Offset(0,52)
  PISG A 1 Offset(0,44)
  PISG A 1 Offset(0,38)
  PISG A 1 Offset(0,35)
  PISG A 1 Offset(0,32)
  Goto Ready

So here we go; this weapon will not fire if it's empty, can be reloaded mid-clip, and will not reload if there is no reserve ammo. But some players may complain about you being stingy with ammo (never mind the fact that, since we set 9mmAmmo to have a max of 150, you could carry 150 clips!) if you let them waste half-finished clips, so let's figure out how to do things bullet by bullet. This is a bit more complicated, but it only requires a little magic in the Reload state.

Reload:
  PISG A 0 A_JumpIfInventory("NewPistolLoaded", 12, 2)
  PISG A 0 A_JumpIfInventory("9mmAmmo", 1, "ReloadWork")
  PISG A 1 // Remember, this is where we end up if both of the previous checks fail.
  Goto Ready
 ReloadWork:
  PISG A 1 Offset(0,35) A_PlayWeaponSound("weapons/shotgr")
  PISG A 1 Offset(0,38)
  PISG A 1 Offset(0,44)
  PISG A 1 Offset(0,52)
  PISG A 1 Offset(0,62)
  PISG A 1 Offset(0,72)
  PISG A 1 Offset(0,82)
 ReloadLoop: // Here's where the magic happens!
  TNT1 A 0 A_TakeInventory("9mmAmmo", 1)
  TNT1 A 0 A_GiveInventory("NewPistolLoaded", 1) // Only give ONE bullet at a time)
  TNT1 A 0 A_JumpIfInventory("NewPistolLoaded", 12, "ReloadFinish") // If it's full, finish up.
  TNT1 A 0 A_JumpIfInventory("9mmAmmo", 1, "ReloadLoop") // If it's NOT full, keep it rolling.
  Goto ReloadFinish // And if it's not full but there's no reserve ammo, finish up anyway.
 ReloadFinish:
  TNT1 A 8
  PISG A 1 Offset(0,82)
  PISG A 1 Offset(0,72)
  PISG A 1 Offset(0,62)
  PISG A 1 Offset(0,52)
  PISG A 1 Offset(0,44)
  PISG A 1 Offset(0,38)
  PISG A 1 Offset(0,35)
  PISG A 1 Offset(0,32)
  Goto Ready

With that little change, your weapon will now reload individual bullets instead of entire clips at once - that loop is where all the magic happens. In short, it takes 1 bullet from reserve, gives 1 bullet to the gun, checks if the gun is full, and if it's not full, checks if there's ammo still in reserve to continue the cycle. The cycle ends if the gun is completely filled, or if there is not enough reserve ammo to finish filling the gun. If you decide you want the gun to have a different ammo capacity, you will need to change not only the Inventory.MaxAmount in the ammo type actor, but also the checks in Reload and ReloadLoop.

Save and test - your weapon ought to work just fine. Unless you're using Zandronum, in which case let's keep going!

As I stated at the beginning of this tutorial, Zandronum is (at the time of this writing) not nearly up to date with ZDoom, so WRF_ALLOWRELOAD (the magic flag that makes the Reload key work) currently produces an error on startup if this weapon is loaded in Zandronum. How'd we get by before ZDoom r3530? Read on, and I'll show you.

The reload "key" is created through the use of two CustomInventory items (one for the key's "on" state and one for the "off" state) and one regular Inventory item that keeps track of the state. Here's the code for them:

ACTOR Action_Reload : CustomInventory
{
    Inventory.Amount 1
    Inventory.MaxAmount 1
    -INVBAR
    States
    {
    Use:
        TNT1 A 0 A_GiveInventory("IsReloading",1)
        Fail
    }
}

ACTOR Action_ReloadCancel : CustomInventory
{
    Inventory.Amount 1
    Inventory.MaxAmount 1
    -INVBAR
    States
    {
    Use:
        TNT1 A 0 A_TakeInventory("IsReloading",1)
        Fail
    }
}

ACTOR IsReloading : Inventory
{
    Inventory.Amount 1
    Inventory.MaxAmount 1
    -INVBAR
}

A CustomInventory item is an item that stays in the player's inventory and will execute the code in the Use state when it's used from the inventory. We are deliberately hiding this item from ZDoom's built-in inventory bar, because we're going to use it directly with a keyboard key. Add this to a KEYCONF lump in your file:

addkeysection "My Tutorial Pistol" tutorpistol
addmenukey "Reload" +tutor_reload
alias +tutor_reload "use Action_Reload"
alias -tutor_reload "use Action_ReloadCancel"
defaultbind r +tutor_reload

The text in quotes for Addkeysection actually creates a named section in Customize Controls labeled "My Tutorial Pistol". Make sure that the text afterwards - "tutorpistol" in this case - is unique for every mod you create, because your INI file will store a section in it with that name. Name them uniquely every time unless you want configuration conflicts, like keys not being bound correctly. You can also rename the console action ("+tutor_reload"), but make sure to keep that name consistent when changing the defaultbind and alias lines.

In order to use this reload "key" to reload your weapon, you will need to add the appropriate line to your weapon's Ready state:

Ready:
  PISG A 0 A_JumpIfInventory("IsReloading", 1, "Reload")
  PISG A 1 A_WeaponReady
  Loop

Pay particular attention that the A_JumpIfInventory line has a duration of 0, or else the weapon may be unresponsive at times when the trigger is pulled. Also be careful with the initial check in the Reload state itself; if there is not at least 1 tic before the weapon returns to the Ready state, the game may freeze if you try to reload with a full clip.

You can actually use the Action_Reload trick to create more weapon functions if needed, for example you might want a Call of Duty-style melee button, or a button to throw grenades, or a button to change the current weapon's rate of fire. In that case, just create new CustomInventory items and a new Inventory to keep track of their state.

I do hope this tutorial has taught you a lot about how weapon reloading works in a game. If there are further questions, I'll be happy to clarify the tutorial a bit.