Game Development Community

dev|Pro Game Development Curriculum

How to add Ammo Clips and reload functionality

by Michael Schoonbrood · 02/20/2002 (9:48 am) · 10 comments

This was a pain to find out. And lots of people have helped out on the threads on the forum.

This is not a full featuring "add this, you're done". This tutorial is, for now, work-in-progress. I will be updating it, and making changes.
I've posted this tutorial so you can get where I am now. I hope that if you better the current version, you will let us know in return here!

============
= The Idea =
============
The way I went to work is as follows.
I added 2 new variables to the WeaponImage classes. "ammoInClip" stores the current ammo in the clip and "clipSize" contains the numbers of maximum bullets in a clip.

Next I changed the onFire function, to shoot from the ammoInClip counter. The ammo boxes you pick up are added to the ammo pile of that bullet type.

We need to set the ammo flag of the weapon image, so the state transitions keep working.
this is done by:
%obj.setImageAmmo( %slot, false );

Next up are the transition changes.
OnFire should no longer "Reload", it should go back to "Ready" on timeout.
OnFire should also switch to "NoAmmo" when out of Ammo.

"NoAmmo" now becomes the part that is run when the clip is empty. So it should have a script, that checks if there are bullets left on the ammo pile. If so, it should switch to the "Reload" state.

"Reload" now becomes an active script as well. It will play the reload sound, and it needs a script that does the actual reloading. It will also send a command to the Weapon HUD on screen, that hides the ammo counters and displays a reloading text.

To be able to hide the reloading text and show the ammo counters again, we need a new state. I called it "ReloadDone". I gave it a script to, only thing it does is hide the reloading text and show the counters again.

=========
= To DO =
=========
I can't get the DryFire state right. The problem is that when you hold fire untill a clip is empty, the game will stay in dryfire mode untill you release the firebutton.
It should of course reload, and when done, continue firing.

Test if all animations work correctly so far.

==========
= My Way =
==========
Here we go.
Step 1 is gonna be a quickie.
Make sure the bottom right corner of your screen is available, as I'm gonna give you the control to put on there, together with the required gui script changes.

Hold on tight... here we go!

File: fps\client\ui\playGui.gui
Place: At the end of the file, before the last };
new GuiBitmapCtrl(weaponHUD) {
      profile = "GuiDefaultProfile";
      horizSizing = "left";
      vertSizing = "top";
      position = "489 392";
      extent = "147 84";
      minExtent = "8 8";
      visible = "1";
      helpTag = "0";
      lockMouse = "0";
      bitmap = "./gfx/WeaponHUD_Empty.png";
      wrap = "0";

      new GuiTextCtrl(AmmoAmount) {
         profile = "WeaponHUDProfile";
         horizSizing = "left";
         vertSizing = "top";
         position = "56 64";
         extent = "57 20";
         minExtent = "8 8";
         visible = "1";
         helpTag = "0";
         lockMouse = "0";
         maxLength = "255";
      };
      new GuiTextCtrl(ClipAmount) {
         profile = "WeaponHUDProfile";
         horizSizing = "left";
         vertSizing = "top";
         position = "56 50";
         extent = "42 21";
         minExtent = "8 8";
         visible = "1";
         helpTag = "0";
         lockMouse = "0";
         maxLength = "255";
      };
      new GuiTextCtrl(ReloadText) {
         profile = "WeaponHUDProfile";
         horizSizing = "left";
         vertSizing = "top";
         position = "69 58";
         extent = "65 18";
         minExtent = "8 8";
         visible = "0";
         helpTag = "0";
         lockMouse = "0";
         text = "RELOADING!";
         maxLength = "255";
      };
   };

That part added the control to your screen, For more info on the weapon HUD, see my other tutorial!

You notice that I'm using a new profile, let's add it!
File: fps\client\ui\defaultGameProfiles.cs
Place: at bottom of file
//-----------------------------------------------------------------------------
// Weapon HUD profile

new GuiControlProfile ("WeaponHUDProfile")
{
   opaque = false;
   fillColor = "128 128 128";
   fontColor = "255 255 255";
   border = true;
   borderColor = "0 255 0";
};

Now let's add the gui script code:

File: fps\client\scripts\playGui.sc
Place: at bottom of file
//-----------------------------------------------------------------------------
// Weapon HUD functions
//-----------------------------------------------------------------------------

// Display the number of bullets left on the ammo pile on screen
function clientCmdSetAmmoAmountHud( %iAmount )
{
   AmmoAmount.setText( "Ammo: " @ %iAmount );
}



// Display the number of bullets left in the clip on screen
function clientCmdSetClipAmountHud(%iAmount)
{
   ClipAmount.setText( "Clip: " @ %iAmount );
}



// Show or Hide the WeaponReload HUD
function clientCmdShowWeaponReload( %bShow )
{
   // Check if we should show or hide the Weapon Reload
   if( %bShow == true )
   {
      // Reloading, show the RELOADING text...
      ClipAmount.setVisible( false );
      AmmoAmount.setVisible( false );
      // ...and hide the ammo counters
      ReloadText.setVisible( true );
   }
   else
   {
      // Not Reloading, hide the RELOADING text...
      ClipAmount.setVisible( true );
      AmmoAmount.setVisible( true );
      // ...and show the ammo counters
      ReloadText.setVisible( false );
   }
}



// Display the weapon HUD belonging to the weapon %weaponName
function clientCmddisplayWeaponHUD(%weaponName)
{
   // Check which weapon HUD to draw
   if(%weaponName $= "RifleImage")
   {
      weaponHUD.setBitmap("fps/client/ui/gfx/WeaponHUD_Rifle.png");
   }
   else if(%weaponName $= "CrossbowImage")
   {
      weaponHUD.setBitmap("fps/client/ui/gfx/WeaponHUD_Crossbow.png");
   }
   else
   {
      weaponHUD.setBitmap("fps/client/ui/gfx/WeaponHUD_Empty.png");
   }
}

I guess it all speaks for itself.
No lets head to the weapons!

First we will give the rifle a clip of 8 bullets!

File: fps\server\scripts\Rifle.cs
Place: At top of datablock ShapeBaseImageData(RifleImage)
// Ammo Clip
   clipSize   = 8;   // This is the size of the clip, it indicates the max. nr of bullets in it
   ammoInClip = 8;   // When you Pick up the weapon, it will have 1 full clip in it

Now go further down till you find the line that sais:
stateTransitionOnTimeout[3] = "Reload";
change it to:
stateTransitionOnTimeout[3] = "Ready";
and add the following line right beneath it:
stateTransitionOnNoAmmo[3] = "NoAmmo";

Go down again and find the line that sais:
stateTransitionOnTimeout[4] = "Ready";
Change it to:
stateTransitionOnTimeout[4] = "ReloadDone";
At the end of this [4] block add the following line:
stateScript[4] = "onReload";

Go to the state [5] block. and add at it's end:
stateScript[5] = "onNoAmmo";

Go to the state [6] block and add:
stateTransitionOnAmmo[6] = "Reload";

Finally, before the function closes, add:
// Done reloading
   // State is need to Hide the reload text and show the ammo counters
   stateName[7]                     = "ReloadDone";
   stateTransitionOnTimeout[7]      = "Ready";
   stateTimeoutValue[7]             = 0.01;
   stateScript[7]                   = "onReloadDone";

Now repeat all these steps for the crossbow.
The first step is different, because the crossbow will reload after each shot, so we give it a clip of 1 bullet!

File: fps\server\scripts\Crossbow.cs
Place: At top of datablock ShapeBaseImageData(CrossbowImage)
// Ammo Clip
   clipSize   = 1;   // This is the size of the clip, it indicates the max. nr of bullets in it
   ammoInClip = 1;   // When you Pick up the weapon, it will have 1 full clip in it
All other steps are the same.. just do it :)


No go to the onFire function definition, and call it onFireBAK, or delete the whole function all together. We will be readding this function in the weapon.cs file as part of the WeaponImage namespace.

Let's do that:

File: fps\server\scripts\weapon.cs
Place: WeaponImage::onMount function

Replace the whole function by the following piece of code (yes, you do indeed replace 1 function by 5 new ones):
function WeaponImage::onMount(%this,%obj,%slot)
{
   // Retrieve the number of bullets of this ammo type left on the pile
   %ammoOnPile = %obj.getInvItem(%this.ammo.getName(),"amount");

   // Images assume a false ammo state on load.
   // We need to set the state according to the current inventory.
   if ((%this.ammoInClip > 0) || (%ammoOnPile > 0))
   {
      %obj.setImageAmmo(%slot,true);
   }

   // Display the correct weapon HUD
   %obj.client.displayWeaponHUD(%this.getName());

   // Display the amount of Ammo left in the clip
   %obj.client.setClipAmountHud(%this.ammoInClip);
   // Display the amount of Ammo left on the pile
   %obj.client.setAmmoAmountHud(%ammoOnPile);
}



function WeaponImage::onFire(%this, %obj, %slot)
{
   // Check if there is enough ammo in the clip
   if( %this.ammoInClip > 0 )
   {
      // Remove bullet from clip
      %this.ammoInClip = %this.ammoInClip - 1;

      // Show the new Ammo in Clip Amount on screen
      %obj.client.setClipAmountHud(%this.ammoInClip);

      // Adjust Ammo Amount on screen
      %ammoOnPile = %obj.getInvItem(%this.ammo.getName(),"amount");
      %obj.client.setAmmoAmountHud(%ammoOnPile);

      // Check if that was the last bullet in the clip
      if( %this.ammoInClip == 0 )
      {
         // No more ammo, make sure the state switches to Reload!
         %obj.setImageAmmo( %slot, false );
      }
   }
   else
   {
      // No ammo in clip!
      %obj.setImageAmmo(%slot,false);

      // Abort onFire, because there is no Ammo in the clip
      return 0;
   }

   %this.ammo.onInventory(%obj,%obj.getInvItem(%this.ammo.getName(),"amount"));

   // Create the projectile
   %projectile     = %this.projectile;

   // Determin initial projectile velocity based on the
   // gun's muzzle point and the object's current velocity
   %muzzleVector   = %obj.getMuzzleVector(%slot);
   %objectVelocity = %obj.getVelocity();
   %muzzleVelocity = VectorAdd( VectorScale(%muzzleVector, %projectile.muzzleVelocity),
                                VectorScale(%objectVelocity, %projectile.velInheritFactor));

   // Create the projectile object
   %p = new (%this.projectileType)() {
      dataBlock        = %projectile;
      initialVelocity  = %muzzleVelocity;
      initialPosition  = %obj.getMuzzlePoint(%slot);
      sourceObject     = %obj;
      sourceSlot       = %slot;
      client           = %obj.client;
   };
   MissionCleanup.add(%p);
   return %p;
}



function WeaponImage::onReload(%this, %obj, %slot)
{
   // Show the reloading text on screen.
   %obj.client.ShowWeaponReload( true );

   // Determine the nr of bullets left on the ammo pile
   %ammoOnPile = %obj.getInvItem( %this.ammo.getName(), "amount" );

   // Check if we can reload
   if( %ammoOnPile > 0 )
   {
      // Check if we need to reload (is the clip full?)
      if( %this.ammoInClip >= %this.clipSize )
      {
         // No need to reload, the clip is still full.
         return 0;
      }

      // Determine the number of bullets required to fill the clip
      %nrOfBulletsNeeded = %this.clipSize - %this.ammoInClip;

      // Check if there are enough bullets left on the ammo pile to fill the clip
      if( %ammoOnPile >= %nrOfBulletsNeeded )
      {
         // Refill clip
         %this.ammoInClip += %nrOfBulletsNeeded;
         // Remove bullets from ammo pile
         %obj.decInvItem( %this.ammo.getName(), %nrOfBulletsNeeded );
      }
      // Fill clip with remaining bullets
      else
      {
         // Put last bullets in clip
         %this.ammoInClip += %ammoOnPile;
         // Empty ammo pile
         %obj.decInvItem( %this.ammo.getName(), %ammoOnPile );
      }

      // Adjust Ammo Amount on screen
      %ammoOnPile = %obj.getInvItem( %this.ammo.getName(), "amount" );
      %obj.client.setAmmoAmountHud( %ammoOnPile );

      // Show the Ammo in Clip amount on screen
      %obj.client.setClipAmountHud( %this.ammoInClip );

      // We're ready to fire!
      %obj.setImageAmmo( %slot, true );
   }
}



function WeaponImage::onNoAmmo(%this, %obj, %slot)
{
   // Determine the nr of bullets left on the ammo pile
   %ammoOnPile = %obj.getInvItem( %this.ammo.getName(), "amount" );

   // No Ammo left in click, check if there is Ammo left on the ammo pile
   if( %ammoOnPile > 0 )
   {
      // Set the Ammo flag to true, this will make the state switch to Reload
      %obj.setImageAmmo( %slot, true );
   }
}



function WeaponImage::onReloadDone(%this, %obj, %slot)
{
   // Show the reloading text on screen.
   %obj.client.ShowWeaponReload( false );
}

at the end of the file add the following code:
//-----------------------------------------------------------------------------
//Ammo Display
//-----------------------------------------------------------------------------

// Display the number of bullets left in the clip on screen
function GameConnection::setClipAmountHud(%client, %iAmount)
{
   commandToClient(%client, 'SetClipAmountHud', %iAmount);
}

// Display the number of bullets left on the ammo pile on screen
function GameConnection::setAmmoAmountHud(%client, %iAmount)
{
   commandToClient(%client, 'SetAmmoAmountHud', %iAmount);
}

// Display the weapon HUD belonging to the weapon %weaponName
function GameConnection::displayWeaponHUD(%client, %strWeaponName)
{
   commandToClient(%client, 'displayWeaponHUD', %strWeaponName);
}

// Show or Hide the WeaponReload HUD
function GameConnection::ShowWeaponReload( %client, %bShow )
{
   commandToClient(%client, 'ShowWeaponReload', %bShow );
}

We're almost there now.
there are a couple a changes you need for the ammo counter.


File: fps\server\scripts\game.cs
Place: At the beginning of function GameConnection::onDeath(
//reset AmmoAmountHud
   %this.setAmmoAmountHud("0");
   %this.setClipAmountHud("0");

and back the the weapon script
File: fps\server\scripts\weapon.cs
Place: The body of function Ammo::onInventory

let's replace this really ugly piece of code:
for (%i = 0; %i < 8; %i++) {
      if ((%image = %obj.getMountedImage(%i)) > 0)
         if (isObject(%image.ammo) && %image.ammo.getId() == %this.getId())
            %obj.setImageAmmo(%i,%amount != 0);
   }
seriously folks... this is in the top 10 list of things you NEVER do!
That piece of code should have looked like this:
for (%i = 0; %i < 8; %i++) {
      if ((%image = %obj.getMountedImage(%i)) > 0) {
         if (isObject(%image.ammo) && %image.ammo.getId() == %this.getId()) {
            %obj.setImageAmmo(%i,%amount != 0);
         }
      }
   }
Never ever do an If or for statement without the { }
Someone might add a line of code later, and you will be debugging like mad, to why that line always runs!

never mind the coding tips, here is what the whole body should look like:
{
   // The ammo inventory state has changed, we need to update any
   // mounted images using this ammo to reflect the new state.
   // Edited for Ammo Hud
   for( %i = 0; %i < 8; %i++ )
   {
      if( ( %image = %obj.getMountedImage( %i ) ) > 0 )
      {
         if( isObject( %image.ammo ) && %image.ammo.getId() == %this.getId() )
         {
            // Show new Ammo amount on screen
            %currentAmmo = %obj.getInvItem(%this.getName(),"amount");
            %obj.client.setAmmoAmountHud(%currentAmmo);
         }
      }
   }
}


** REMARK **
If you haven't added the in-game inventory screen, all:
%obj.getInvItem(%this.getName(),"amount")
should be replaced by:
%obj.getInventory(%this.ammo)

Also, to have Weapon HUD's without drawing them yourself, you need this zip:
http://www.gameplayheaven.com/downloads/Torque/WeaponHUD_gfx.zip.
Unzip it to your "/fps/client/ui" dir. It will create a subdir called "gfx"


That's it for now.
It's still not finished, but now you can all help me finish it :)

#1
02/20/2002 (1:43 pm)
Dryfire sounds like it works fine. I doubt you'd be reloading as you pull the trigger... might have some nasty accidents.

Or are you wanting automatic reloading? I personally find manual reload to be a bit more "strategic" People have to consider "Do I want to try to gun it with 4 rounds left in clip, or spend time reloading?".

Good tut! I'll mess with it later today.
#2
02/25/2002 (5:19 pm)
did you realize this is completely wrong?!
The datablock is loaded only once per mission... i mean the Image Datablock... that means that when you die it isnt deleted and created again when u pickup a new one... so if you have ammoInClip = 10; and you die... you will still have ammoInClip = 10; ... it wont go back to the default... that's why this things are stored in the player class, because the player class is deleted when you die and recreated when you respawn... so you should change all this to store the ammoInClip on the player class... maybe even into the inventory...
#3
03/02/2002 (12:41 am)
I could build a reset function for the ammo in the clip... What you're doing is fine to of course.

And your way of solving things might actually be easier for displaying the ammo in the weapon clip.

I'm stuck in my inventory... I wanna show weapon information, but I have no clue how to access the weaponImage from there :(
#4
03/06/2002 (1:09 pm)
I agree with Matt about reloading. At least stop pulling the trigger when reloading:)

I added a "onReload" action to help with this. Before I had to "dryfire" once to force a reload.

I noticed the reloading text gets "stuck" if you switched weapons during reloading. The shell eject action needed some fixing as well.

I had a bunch of other small glitches that came up too, but might have been caused by other changes I've already made. Thanks for the useful information you put out. I'm still picking up on how all the scripting works in Torque.
#5
11/02/2002 (10:19 am)
Mickael,(or any1 cause it's march dated)
Really cool, Thanks

What do you mean by :

"If you haven't added the in-game inventory screen"

Is it another ressource ? Maybe this could solve my problem here :) (got HEAD version)

When I put
%obj.getInvItem(%this.getName(),"amount")
I get only 1 shot from the crossbow and when picking other ammos item, it don't include it

and when
%obj.getInventory(%this.ammo)
I get unlimited ammo, the counter don't countdown
#6
01/17/2004 (11:54 am)
Edited
#7
06/29/2006 (6:45 pm)
Well my question is a bit different. Has anyone come up with a modification to this so the clips actually have their own bullet inventory? For example if you drop a weapon with a partially fired clip, someone should be able to pick up that weapon with the partially fired clip still in its less than full state.
#8
12/07/2006 (9:17 am)
@Ron

You would just create a variable on the item that stores the amount of variables inside the clip and then transfer that variables date to and from the ShapeBaseImageData
#9
06/03/2008 (1:37 am)
Another, much easier (if long-winded) would be to create 8 'Fire' states, and switch between them...
annoyingly large amounts of code for machine-guns and such - 80+ 'Fire' states...
#10
09/13/2011 (3:53 pm)
No can do - ShapeBaseImageData has a maximum of 31 states. It's also a bit of a ridiculous way to solve the problem, when using an ammo counter would be much easier and more flexible.

A note for anyone coming here in the future:

This resource's onFire function checks %this.ammoInClip, which is a datablock value on the weapon image datablock. ALL weapon images using this datablock (i.e., all Crossbows or all Rifles) will share that variable. So whenever you fire, the ammo count of all of the same weapon is decreased.