The three virtues of a programmer: Laziness, Impatience, and Hubris. – Larry Wall

Legacy:Multiammo Weapons

From Unreal Wiki, The Unreal Engine Documentation Site
Jump to: navigation, search

The last two versions of the ChaosUT mods have made use of weapons that have multiple types of ammo. In the UT version we made a new weapon for each ammo type. Then when you picked up one of these weapons, we gave the user all of the other weapons based on the ammo types just with out any ammo. Once a user found a different ammo type in a map, that version of the weapon now had ammo which the user now could use. This help to create the illusion of one weapon that can fire different ammo types. However this method was not fool proof. For example if you dropped the weapon or die with these weapons then only the standard weapon ammo type could be picked up.

UT2003 provided us with a way to correct the issues and truly make one weapon that can support more than 2 ammo types. I will attempt to walk you through how we did it for a Grenade Launcher. Keep in mind that this code is what worked. Its not the best code and I am sure there are other ways that are more elegant in handling this task. This is my first tut so forgive a few mistakes as I try to learn how to write these better. I will to post at first the key functions then the complete source at the end. But with that out of the way lets start.

Requirements of Grenade Launcher[edit]

Here is a list of what we want our Grenade Launcher to do:

  • The Grenade Launcher must support at least four types of Grenades. Standard, Poison, Napalm and Flash.
  • The user can switch ammo any time they have another ammo type to use in the Grenade Launcher
  • The Grenade Launcher should have a timed Grenade as one fire type and an instant hit/explosion as the other fire type for each style of grenade
  • The Grenade Launcher should have an visible indicator (new skin on weapon mesh) on which ammo type you have loaded other than the Ammo Counter

Now that we have a design goal lets see how this can be done.

The basic plan of attack is like so:

  • We will add new fire mode(s) to handle the new each of the new ammo types
  • Each ammo will have two fire modes one for the timed grenade another for the instant grenade
  • We will reset the attachment and pickup classes to the players viewing this weapon (or if it was dropped) so anyone can see what ammo is currently loaded into the weapon
  • We will use an EXEC in the weapon class to do the ammo changing
  • We will need a separate weapon pickup and attachment class for each ammo type

Needed Functions[edit]

The main part we need is to do the actual swapping of the ammo types. We should use a variable to store which ammo is currently loaded (we will use var int Count for that). This should be a global and not a local function for reasons we will get to later. The basics are first we check to see if we have any ammo. Then we should not allow an ammo change if we are firing the weapon. We start at our current count and then look at the next available ammo type. If we have ammo of that type we will try to change to it. We do this by removing the current fire class. Spawning a new fire class for the new ammo type. We then change the ammo to that type. We repeat this process so we change both the current firing modes. Then we remove the current attachment and pickup classes and replace them with correct ones. We do this so that the 3rd player views and the dropped pickups will have the right skin and ammo types

// manual key function to swich ammo out
exec function ChangeAmmo()
{
    local Inventory Inv;
 
    // Check to see if they even have ammo to switch
    if (!HasAmmo())
      return;
 
    // dont let them chang ammo if they are firing
    if (FireMode[0].bIsFiring || FireMode[1].bIsFiring)
       return;
 
    while (!bChangedAmmo)
    {
 
      if (Count == 0)
      {
         Count += 1;
 
         Inv = Pawn(Owner).FindInventoryType(class'GrenadeAmmoNapalm');
         if (Inv  != none  && Ammunition(inv).HasAmmo())
         {
             bChangedAmmo=true;
             // Remove old fire mode 0
             if (FireMode[0] != none)
                 FireMode[0].Destroy();
             // primary fire
             FireMode[0] = Spawn(class'GrenadeLauncher_Primary_napalm', self);
             FireMode[0].ThisModeNum = 0;
             FireMode[0].Weapon = self;
             FireMode[0].Instigator = Instigator;
             FireMode[0].AmmoClass =class'GrenadeAmmoNapalm';    // need these two to change the ammo
             Ammo[0] = Ammunition(Instigator.FindInventoryType(FireMode[0].AmmoClass));
             // Remove old fire mode 1
             if (FireMode[1] != none)
                 FireMode[1].Destroy();
             // secondary fire
             FireMode[1] = Spawn(class'GrenadeLauncher_Secondary_napalm', self);
             FireMode[1].ThisModeNum = 1;
             FireMode[1].Weapon = self;
             FireMode[1].Instigator = Instigator;
             FireMode[1].AmmoClass = class'GrenadeAmmonapalm';
             Ammo[1] = Ammunition(Instigator.FindInventoryType(FireMode[1].AmmoClass));
 
             // Remove old Attchment and spawn new one with correct skin
             if ( AttachmentClass != none)
               DetachFromPawn(instigator);
             AttachmentClass=class'GrenadeLauncher_NapalmAttachment';
             AttachToPawn(Instigator);
             PickupClass=class'GrenadeLauncher_NapalmPickup';
             Break;
         }
      }
      if (Count == 1)
      {
         Count += 1;
         Inv = Pawn(Owner).FindInventoryType(class'GrenadeAmmoFlash');
         if (Inv  != none  && Ammunition(inv).HasAmmo() )
         {
             bChangedAmmo=true;
             // Remove old fire mode 0
             if (FireMode[0] != none)
                 FireMode[0].Destroy();
             // primary fire
             FireMode[0] = Spawn(class'GrenadeLauncher_Primary_flash', self);
             FireMode[0].ThisModeNum = 0;
             FireMode[0].Weapon = self;
             FireMode[0].Instigator = Instigator;
             FireMode[0].AmmoClass=class'GrenadeAmmoFlash';    // need these two to change the ammo
             Ammo[0] = Ammunition(Instigator.FindInventoryType(FireMode[0].AmmoClass));
             // Remove old fire mode 1
             if (FireMode[1] != none)
                 FireMode[1].Destroy();
             // secondary fire
             FireMode[1] = Spawn(class'GrenadeLauncher_Secondary_flash', self);
             FireMode[1].ThisModeNum = 1;
             FireMode[1].Weapon = self;
             FireMode[1].Instigator = Instigator;
             FireMode[1].AmmoClass=class'GrenadeAmmoFlash';
             Ammo[1] = Ammunition(Instigator.FindInventoryType(FireMode[1].AmmoClass));
 
             // Remove old Attchment and spawn new one with correct skin
             if ( AttachmentClass != none)
               DetachFromPawn(instigator);
             AttachmentClass=class'GrenadeLauncher_FlashAttachment';
             AttachToPawn(Instigator);
             PickupClass=class'GrenadeLauncher_FlashPickup';
             Break;
         }
      }
      if (Count == 2)
      {
         Count += 1;
         Inv = Pawn(Owner).FindInventoryType(class'GrenadeAmmoPoison');
         if (Inv  != none  && Ammunition(inv).HasAmmo() )
         {
             bChangedAmmo=true;
             // Remove old fire mode 0
             if (FireMode[0] != none)
                 FireMode[0].Destroy();
             // primary fire
             FireMode[0] = Spawn(class'GrenadeLauncher_Primary_poison', self);
             FireMode[0].ThisModeNum = 0;
             FireMode[0].Weapon = self;
             FireMode[0].Instigator = Instigator;
             FireMode[0].AmmoClass=class'GrenadeAmmoPoison';    // need these two to change the ammo
             Ammo[0] = Ammunition(Instigator.FindInventoryType(FireMode[0].AmmoClass));
             // Remove old fire mode 1
             if (FireMode[1] != none)
                 FireMode[1].Destroy();
             // secondary fire
             FireMode[1] = Spawn(class'GrenadeLauncher_Secondary_poison', self);
             FireMode[1].ThisModeNum = 1;
             FireMode[1].Weapon = self;
             FireMode[1].Instigator = Instigator;
             FireMode[1].AmmoClass=class'GrenadeAmmopoison';
             Ammo[1] = Ammunition(Instigator.FindInventoryType(FireMode[1].AmmoClass));
 
             // Remove old Attchment and spawn new one with correct skin
             if ( AttachmentClass != none)
               DetachFromPawn(instigator);
             AttachmentClass=class'GrenadeLauncher_PoisonAttachment';
             AttachToPawn(Instigator);
             PickupClass=class'GrenadeLauncher_PoisonPickup';
             Break;
         }
      }
      if (Count == 3)
      {
         count = 0;  // reset counter on last type of ammo
         Inv = Pawn(Owner).FindInventoryType(class'GrenadeAmmoStandard');
         if (Inv  != none  && Ammunition(inv).HasAmmo())
         {
             bChangedAmmo=true;
             // Remove old fire mode 0
             if (FireMode[0] != none)
                 FireMode[0].Destroy();
             // primary fire
             FireMode[0] = Spawn(class'GrenadeLauncher_Primary', self);
             FireMode[0].ThisModeNum = 0;
             FireMode[0].Weapon = self;
             FireMode[0].Instigator = Instigator;
             FireMode[0].AmmoClass=class'GrenadeAmmoStandard';
             Ammo[0] = Ammunition(Instigator.FindInventoryType(FireMode[0].AmmoClass));
             // Remove old fire mode 1
             if (FireMode[1] != none)
                 FireMode[1].Destroy();
             // secondary fire
             FireMode[1] = Spawn(class'GrenadeLauncher_Secondary', self);
             FireMode[1].ThisModeNum = 1;
             FireMode[1].Weapon = self;
             FireMode[1].Instigator = Instigator;
             FireMode[1].AmmoClass=class'GrenadeAmmoStandard';
             Ammo[1] = Ammunition(Instigator.FindInventoryType(FireMode[1].AmmoClass));
 
             // Remove old Attchment and spawn new one with correct skin
             if ( AttachmentClass != none)
                 DetachFromPawn(instigator);
             AttachmentClass=class'GrenadeLauncher_Attachment';
             AttachToPawn(Instigator);
             PickupClass=class'GrenadeLauncher_Pickup';
             Break;
         }
      }
    }
    bChangedAmmo=false; // set to flase as we have changed ammo at this point and need to reset the flag
}

The next key function is the ammo check. The default HasAmmo only looks at the current fire types. With a weapon that has more than one, this is no good. We changed ours to look for the inventory types of each possible ammo type:

// needed to overide the weapons call as we have a multi-ammo weapon
simulated function bool HasAmmo()
{
       local Inventory Inv;
 
       inv = Pawn(Owner).FindInventoryType(class'GrenadeAmmoNapalm');
       if (inv != none  && Ammunition(inv).HasAmmo())
         return true;
       inv = Pawn(Owner).FindInventoryType(class'GrenadeAmmoFlash');
       if (inv != none  && Ammunition(inv).HasAmmo())
         return true;
       inv = Pawn(Owner).FindInventoryType(class'GrenadeAmmoPoison');
       if (inv != none  && Ammunition(inv).HasAmmo())
         return true;
       inv = Pawn(Owner).FindInventoryType(class'GrenadeAmmoStandard');
       if (inv != none  && Ammunition(inv).HasAmmo())
         return true;
       return false; // if we got this far no ammo for nade launcher
}

The next function we needed to change was the Outofammo(). The reason for this change is that the standard Outofammo function is not aware that you have more than one ammo type. So it should check to see if you have any of the other ammotypes in your current inventory. If it does not find any you are truly out of ammo. If it does, then you should change ammo types:

// needed to add this check for a multi-ammo weapon
simulated function OutOfAmmo()
{
 
   if (Level.NetMode == NM_Standalone && !Instigator.IsLocallyControlled())
      return;
 
    if ((Ammo[0] != None && FireMode[0] != None && Ammo[0].AmmoAmount >= FireMode[0].AmmoPerFire) || (Ammo[1] != none && FireMode[1] != none && Ammo[1].AmmoAmount >= FireMode[1].AmmoPerFire) )
    {
        return; // we have ammo return
    }
    else
    {
        if (HasAmmo())
        {
            ChangeAmmo(); // meaning we have some type of nade ammo change to it
        }
        else
        {
            //if we have gotten to this point we are really out of ammo...better swich weapons
            DoAutoSwitch();
        }
     }
}

That is the basics you will need. What if your new ammo type required a 1st person view mesh change? If you pickup up a dropped version what skin/mesh will be displayed on your screen? What about network replications? How does the animations play? What happens when a users tries to fire during an ammo change? What happens if you run out of ammo while your firing? This is where we have to get a bit ugly as the code above will have all of these are issues.

Complete Code[edit]

Since we have more than one weapon that supports multi-ammo types we decided to use a parent class and place in some specific code to help with network replication, changing of the mesh/skins and animations.

The parent class of the weapon is this:

//=============================================================================
// ChaosWeapon
//=============================================================================
class ChaosWeapon extends Weapon abstract;
 
var int count;                        // use this to keep track of the current selected ammo and replicate this to clients
var bool bChangedAmmo;      // use this flag to break out of loop  in change ammo functions and replicate this to clients
var Int OldCount;                   // used to tell us when to play the "changeammo" function and do not replicate this to clients
var bool bPlayChangeAmmo; // used by the Animnotify to tell use when to change mesh/skins and do not replicate this to clients
 
replication
{
    // Things the server should send to the client.
    reliable if( Role==ROLE_Authority )
        count, bChangedAmmo;
 
    // functions called by client on server
    reliable if( Role<ROLE_Authority )
        ChangeAmmo;
}
 
//---
// ChangeAmmo
//   *Switches ammo types.
//   *Implemented in subclasses.
// ---
exec function ChangeAmmo()
{
}
 
//---
// Timer
//   * This is needed to help us prevent playing animations client side during a relaod.
// ---
Simulated function Timer ()
{
   super.Timer();          // call to super
   if (bPlayChangeAmmo)    // dont play the animtions if we are reloading
     bPlayChangeAmmo=false;
 
}
 
 
//---
// PutDown()
//   * This is needed to help prevent ANs for multi-ammo weapons.
// ---
simulated function bool PutDown()
{
    local int Mode;
 
    if (ClientState == WS_BringUp || ClientState == WS_ReadyToFire)
    {
        // For our multi-ammo weapons Instigator.PendingWeapon maybe none so check for that
        if (Instigator.PendingWeapon != None && !Instigator.PendingWeapon.bForceSwitch)
        {
            for (Mode = 0; Mode < NUM_FIRE_MODES; Mode++)
            {
                if (FireMode[Mode].bFireOnRelease && FireMode[Mode].bIsFiring)
                    return false;
            }
        }
 
        if (Instigator.IsLocallyControlled())
        {
            for (Mode = 0; Mode < NUM_FIRE_MODES; Mode++)
            {
                if (FireMode[Mode].bIsFiring)
                    ClientStopFire(Mode);
            }
 
            if (ClientState != WS_BringUp && HasAnim(PutDownAnim))
                PlayAnim(PutDownAnim, PutDownAnimRate, 0.0);
        }
        ClientState = WS_PutDown;
 
        SetTimer(0.3, false);
    }
    for (Mode = 0; Mode < NUM_FIRE_MODES; Mode++)
    {
  FireMode[Mode].bServerDelayStartFire = false;
  FireMode[Mode].bServerDelayStopFire = false;
 }
    Instigator.AmbientSound = None;
    OldWeapon = None;
    return true; // return false if preventing weapon switch
}
 
 
//---
// State WaitforAmmoChange
// This state helps to pause the guns unitl so that
// We can switch ammos
// ---
Simulated State WaitforAmmoChange
{
   Ignores ServerStartFire, ServerStopFire, ClientWeaponSet, Fire, ClientStopFire,ClientStartFire;
 
 
 Begin:
       // put the fire modes to "sleep" while we run the ammo chaning animations
       if (FireMode[0] != none)
            FireMode[0].GotoState('ChangeAmmo');
       if (FireMode[1] != none)
            FireMode[1].GotoState('ChangeAmmo');
       if (Level.NetMode == NM_DedicatedServer)
         Sleep(1.75);
       // Ok back to normal
       GotoState('');
}

Let me touch on a few parts of the above code. The count var became useful to replicate to the clients. We can use that to display client message when your changing ammo as well as help figure out which mesh/skin to change to. The next three, bChangedAmmo, OldCount, bPlayChangeAmmo were all used to help avoid network animation issues. Basically they help to tell the client when then need to change skins/mesh for the new ammo types as well as help syncing of animations in network play. The modify PutDown() was needed to help avoid AN in the logs. The only weirdo thing their is the WaitforAmmoChange state. We needed to disallow people from playing firing animations. In on-line games you would hit the change ammo key and as soon as the animation started to play you could spam the fire key. This would cause the fire animations to play however no new projectile would be spawned. Thus it looked like a bug. Using this we help to lock out user fire commands during the ammo switch and this allows the animations to play with out being interrupted. The Sleep(1.75) is an ugly hack but this cause the weapon to sleep during the reloading. Again maybe a cleaner way to do this. But it accomplishes the effect and disallows any firing during the ammo change.

You also probably saw that FireMode[0].GotoState('ChangeAmmo') command. And you should realize that that is not a standard state in the weapon fire class. You are correct. With multi ammo we have some issues to work around in the fire class. Again these basically help to avoid the user from firing during an ammo change. As well as they help to address when you the ammo changes and weapon changes happen automatically. Here is the parent class we use for all changing ammo fire modes:

//==============================================================================
// CUT_ProjectileFire - by DJPaul
//   *$Id: CUT_ProjectileFire.uc,v 1.4 2003/07/01 12:32:54 Wormbo Exp $
//   *All variable ammo types should derive from this class.
//==============================================================================
class CUT_ProjectileFire extends ProjectileFire abstract;
 
// This function below will handle the automatic reload of the weapon
// if we have another ammo type when the current one runes out
function StopFiring()
{
     if ( Weapon.Ammo[ThisModeNum].AmmoAmount <= 0 )
       GotoState('ChangeAmmo');
}
 
 
// This function will disallow at the playing of the client side animation
// During a reload
simulated function bool AllowFire()
{
 return Super.AllowFire() && !ChaosWeapon(Weapon).bPlayChangeAmmo;
}
 
 
 
// We need this state to previent the client side fire animations
// to play while the "reload" animations are playing
State ChangeAmmo
{
 Ignores PlayFireEnd, PlayFiring, PlayStartHold, PlayPreFire;
 
 
  Begin:
     Sleep(0.5);         // Wait for current firing animation to finnish
     Weapon.OutOfAmmo();   // change the ammo
     Sleep(1.50);         // lets the animations play before we try to fire
     GotoState('');        // ok ready to rock...
}
 
function StartBerserk()
{
    FireRate = default.FireRate * 0.75;
    FireAnimRate = default.FireAnimRate / 0.75;
}

You can see now why we replicated the bPlayChangeAmmo to help avoid the issues of animations in network play.

Now for the complete GenadeLauncher code we used:

//=============================================================================
// GrenadeLauncher - by DJPaul
// $Id: GrenadeLauncher.uc,v 1.34 2003/07/13 01:03:23 jb Exp $
//=============================================================================
class GrenadeLauncher extends ChaosWeapon config(user);
 
var sound Nadelauncher_select1;
 
 
// AI Interface
function float GetAIRating()
{
 local Bot B;
 local float EnemyDist;
 local vector EnemyDir;
 
 B = Bot(Instigator.Controller);
 if ( (B == None) || (B.Enemy == None) )
  return AIRating;
 
 EnemyDir = B.Enemy.Location - Instigator.Location;
 EnemyDist = VSize(EnemyDir);
 if ( EnemyDist > 750 )
 {
  if ( EnemyDist > 2000 )
  {
   if ( EnemyDist > 3500 )
    return 0.2;
   return (AIRating - 0.3);
  }
  if ( EnemyDir.Z < -0.5 * EnemyDist )
   return (AIRating - 0.3);
 }
 else if ( (B.Enemy.Weapon != None) && B.Enemy.Weapon.bMeleeWeapon )
  return (AIRating + 0.35);
 else if ( EnemyDist < 400 )
  return (AIRating + 0.2);
 return FMax(AIRating + 0.2 - (EnemyDist - 400) * 0.0008, 0.2);
}
 
/* BestMode()
choose between regular or alt-fire
*/
function byte BestMode()
{
 local vector EnemyDir;
 local float EnemyDist;
 local bot B;
 
 B = Bot(Instigator.Controller);
 if ( (B == None) || (B.Enemy == None) )
  return 0;
 
 EnemyDir = B.Enemy.Location - Instigator.Location;
 EnemyDist = VSize(EnemyDir);
 if ( EnemyDist > 700 )
   return 1;
 else if ( (B.Enemy.Weapon != None) && B.Enemy.Weapon.bMeleeWeapon )
  return 0;
 else if ( (EnemyDist < 400) || (EnemyDir.Z > 30) )
  return 0;
 else if ( FRand() < 0.65 )
  return 1;
 return 0;
}
 
function float SuggestAttackStyle()
{
 if ( (AIController(Instigator.Controller) != None)
  && (AIController(Instigator.Controller).Skill < 3) )
  return 0.4;
    return 0.8;
}
 
function float SuggestDefenseStyle()
{
    return -0.4;
}
// End AI Interface
 
 
// charge is used to both increase volecity and for the time delay if app,
simulated function float ChargeBar()
{
    if (FireMode[1].bIsFiring)
       return FMin(1,FireMode[1].HoldTime/GrenadeLauncher_SecondaryFireModeAbstract(FireMode[1]).MaxStrength);
    else
       return 0.0;
}
 
simulated function tick (float dt)
{
   // check for an ammo change and if so...change the 1st view skin..
   // and play a reloading animation
    if (OldCount != Count)
 {
        OldCount = Count;
        bPlayChangeAmmo=True;
        PlayChangeAmmo();
    }
}
 
Simulated function PlayChangeAmmo()
{
     // Because we have to swtich the skin we will have the animation call (via an amin notify)
     // the PlayAmmoMeshSwitch() function to ensure we are snyced as we dont want to swtich the
     // skin until we have finnshed playing the AmmoDown animation
 
     // use this timer to let animtions finnish playing 
     SetTimer(2.0, true);
     // Play the down animations here
     PlayAnim('AmmoDown', 1.25, 0.3);
}
 
simulated function PlayAmmoMeshSwitch()
{
   // The animnotify will fire off this function when the weapon is playing the Ammodown animation at the 9th frame
   // and the bPlayChangeAmmo flag is used to know that we are indeed in a changing of ammo sequnce
    if (bPlayChangeAmmo)
    {
        // Change 1st Skin here
        Skins[0]=Texture'ChaosTex_001.Nades.Chaos_Nadelauncher_id1';
        Switch (Count) {
        Case 1:
            Skins[1]= Texture'ChaosTex_001.Nades.Chaos_NadeFlame';
            Skins[2]= Texture'ChaosTex_001.Nades.Chaos_Nadeshell';
            Skins[3]= FinalBlend'XEffectMat.Shield.RedShell';
            if (PlayerController(Instigator.Controller) != none)
                  PlayerController(Instigator.Controller).ReceiveLocalizedMessage(class'NapalmAmmoChangeNotification', 0, none, none, FireMode[0].Class);
            PlayOwnedSound(Nadelauncher_select1, SLOT_Interact,,,,, false);
            break;
        Case 2:
            Skins[1]= Texture'ChaosTex_001.Nades.chaos_nadeflash';
            Skins[2]= Texture'ChaosTex_001.Nades.Chaos_NadeshellFlash';
            Skins[3]= Shader'XGameShaders.PlayerShaders.LightningHit';
            if (PlayerController(Instigator.Controller) != none)
                 PlayerController(Instigator.Controller).ReceiveLocalizedMessage(class'FlashAmmoChangeNotification', 0, none, none, FireMode[0].Class);
            PlayOwnedSound(Nadelauncher_select1, SLOT_Interact,,,,, false);
            break;
        Case 3:
             Skins[1]= Texture'ChaosTex_001.Nades.chaos_nadepoison';
             Skins[2]= Texture'ChaosTex_001.Nades.Chaos_NadeshellPoison';
             Skins[3]= FinalBlend'XEffectMat.goop.GoopFB';
             if (PlayerController(Instigator.Controller) != none)
                 PlayerController(Instigator.Controller).ReceiveLocalizedMessage(class'PoisonAmmoChangeNotification', 0, none, none, FireMode[0].Class);
             PlayOwnedSound(Nadelauncher_select1, SLOT_Interact,,,,, false);
             break;
        default:
            Skins[1]= Texture'ChaosTex_001.Nades.chaos_nadeexplo';
            Skins[2]= Texture'ChaosTex_001.Nades.Chaos_Nadeshell';
            Skins[3]= Texture'WeaponSkins.Skins.GrenadeTex';
            if (PlayerController(Instigator.Controller) != none)
                  PlayerController(Instigator.Controller).ReceiveLocalizedMessage(class'ExplosiveAmmoChangeNotification', 0, none, none, FireMode[0].Class);
            PlayOwnedSound(Nadelauncher_select1, SLOT_Interact,,,,, false);
            break;
        }
        // now we have changed the skins on the 1st views play the Ammoup animation
        Playanim('AmmoUp', 1.2, 0.3);
     }
}
 
 
// needed to overide the weapons call as we have a multi-ammo weapon
simulated function bool HasAmmo()
{
       local Inventory Inv;
 
       inv = Pawn(Owner).FindInventoryType(class'GrenadeAmmoNapalm');
       if (inv != none  && Ammunition(inv).HasAmmo())
         return true;
       inv = Pawn(Owner).FindInventoryType(class'GrenadeAmmoFlash');
       if (inv != none  && Ammunition(inv).HasAmmo())
         return true;
       inv = Pawn(Owner).FindInventoryType(class'GrenadeAmmoPoison');
       if (inv != none  && Ammunition(inv).HasAmmo())
         return true;
       inv = Pawn(Owner).FindInventoryType(class'GrenadeAmmoStandard');
       if (inv != none  && Ammunition(inv).HasAmmo())
         return true;
       return false; // if we got this far no ammo for nade launcher
}
 
// needed to add this check for a multi-ammo weapon
simulated function BringUp(optional Weapon PrevWeapon)
{
    OutOfAmmo();  // check to make sure we have some type of ammo
    // Make sure we load the right skin... but don't wast server time with this...
    If(Level.NetMode != NM_DedicatedServer)
    {
        Skins[0]=Texture'ChaosTex_001.Nades.Chaos_Nadelauncher_id1';
        if (Count == 1)
        {
            Skins[1]= Texture'ChaosTex_001.Nades.Chaos_NadeFlame';
            Skins[2]= Texture'ChaosTex_001.Nades.Chaos_Nadeshell';
            Skins[3]= FinalBlend'XEffectMat.Shield.RedShell';
        }
        else if (Count == 2)
        {
            Skins[1]= Texture'ChaosTex_001.Nades.chaos_nadeflash';
            Skins[2]= Texture'ChaosTex_001.Nades.Chaos_NadeshellFlash';
            Skins[3]= Shader'XGameShaders.PlayerShaders.LightningHit';
        }
        else if (Count == 3)
        {
             Skins[1]= Texture'ChaosTex_001.Nades.chaos_nadepoison';
             Skins[2]= Texture'ChaosTex_001.Nades.Chaos_NadeshellPoison';
             Skins[3]= FinalBlend'XEffectMat.goop.GoopFB';
        }
        else
        {
            Skins[1]= Texture'ChaosTex_001.Nades.chaos_nadeexplo';
            Skins[2]= Texture'ChaosTex_001.Nades.Chaos_Nadeshell';
            Skins[3]= Texture'WeaponSkins.Skins.GrenadeTex';
        }
     }
     Super.BringUp();
}
 
// needed to add this check for a multi-ammo weapon
simulated function OutOfAmmo()
{
 
   if (Level.NetMode == NM_Standalone && !Instigator.IsLocallyControlled())
      return;
 
    if ((Ammo[0] != None && FireMode[0] != None && Ammo[0].AmmoAmount >= FireMode[0].AmmoPerFire) || (Ammo[1] != none && FireMode[1] != none && Ammo[1].AmmoAmount >= FireMode[1].AmmoPerFire) )
    {
        return; // we have ammo return
    }
    else
    {
        if (HasAmmo())
        {
            ChangeAmmo(); // meaning we have some type of nade ammo change to it
        }
        else
        {
            //if we have gotten to this point we are really out of ammo...better swich weapons
            DoAutoSwitch();
        }
     }
}
 
 
// manual key function to swich ammo out
exec function ChangeAmmo()
{
   local Inventory Inv;
 
   // Check to see if they even have ammo to switch
   if (!HasAmmo())
     return;
 
   // dont let them chang ammo if they are firing
   if (FireMode[0].bIsFiring || FireMode[1].bIsFiring)
       return;
 
   //DJP: Stops stuff firing when the type switches (when out of ammo).
   if (Pawn(Owner) != none && Pawn(Owner).Controller != none)
   {
        Pawn(Owner).Controller.bFire = 0;
        Pawn(Owner).Controller.bAltFire = 0;
   }
 
 
    while (!bChangedAmmo)
    {
 
      if (Count == 0)
      {
         Count += 1;
 
         Inv = Pawn(Owner).FindInventoryType(class'GrenadeAmmoNapalm');
         if (Inv  != none  && Ammunition(inv).HasAmmo())
         {
             bChangedAmmo=true;
             // Remove old fire mode 0
             if (FireMode[0] != none)
                 FireMode[0].Destroy();
             // primary fire
             FireMode[0] = Spawn(class'GrenadeLauncher_Primary_napalm', self);
             FireMode[0].ThisModeNum = 0;
             FireMode[0].Weapon = self;
             FireMode[0].Instigator = Instigator;
             FireMode[0].AmmoClass =class'GrenadeAmmoNapalm';    // need these two to change the ammo
             Ammo[0] = Ammunition(Instigator.FindInventoryType(FireMode[0].AmmoClass));
             // Remove old fire mode 1
             if (FireMode[1] != none)
                 FireMode[1].Destroy();
             // secondary fire
             FireMode[1] = Spawn(class'GrenadeLauncher_Secondary_napalm', self);
             FireMode[1].ThisModeNum = 1;
             FireMode[1].Weapon = self;
             FireMode[1].Instigator = Instigator;
             FireMode[1].AmmoClass = class'GrenadeAmmonapalm';
             Ammo[1] = Ammunition(Instigator.FindInventoryType(FireMode[1].AmmoClass));
 
             // Remove old Attchment and spawn new one with correct skin
             if ( AttachmentClass != none)
               DetachFromPawn(instigator);
             AttachmentClass=class'GrenadeLauncher_NapalmAttachment';
             AttachToPawn(Instigator);
             PickupClass=class'GrenadeLauncher_NapalmPickup';
             Break;
         }
      }
      if (Count == 1)
      {
         Count += 1;
         Inv = Pawn(Owner).FindInventoryType(class'GrenadeAmmoFlash');
         if (Inv  != none  && Ammunition(inv).HasAmmo() )
         {
             bChangedAmmo=true;
             // Remove old fire mode 0
             if (FireMode[0] != none)
                 FireMode[0].Destroy();
             // primary fire
             FireMode[0] = Spawn(class'GrenadeLauncher_Primary_flash', self);
             FireMode[0].ThisModeNum = 0;
             FireMode[0].Weapon = self;
             FireMode[0].Instigator = Instigator;
             FireMode[0].AmmoClass=class'GrenadeAmmoFlash';    // need these two to change the ammo
             Ammo[0] = Ammunition(Instigator.FindInventoryType(FireMode[0].AmmoClass));
             // Remove old fire mode 1
             if (FireMode[1] != none)
                 FireMode[1].Destroy();
             // secondary fire
             FireMode[1] = Spawn(class'GrenadeLauncher_Secondary_flash', self);
             FireMode[1].ThisModeNum = 1;
             FireMode[1].Weapon = self;
             FireMode[1].Instigator = Instigator;
             FireMode[1].AmmoClass=class'GrenadeAmmoFlash';
             Ammo[1] = Ammunition(Instigator.FindInventoryType(FireMode[1].AmmoClass));
 
             // Remove old Attchment and spawn new one with correct skin
             if ( AttachmentClass != none)
               DetachFromPawn(instigator);
             AttachmentClass=class'GrenadeLauncher_FlashAttachment';
             AttachToPawn(Instigator);
             PickupClass=class'GrenadeLauncher_FlashPickup';
             Break;
         }
      }
      if (Count == 2)
      {
         Count += 1;
         Inv = Pawn(Owner).FindInventoryType(class'GrenadeAmmoPoison');
         if (Inv  != none  && Ammunition(inv).HasAmmo() )
         {
             bChangedAmmo=true;
             // Remove old fire mode 0
             if (FireMode[0] != none)
                 FireMode[0].Destroy();
             // primary fire
             FireMode[0] = Spawn(class'GrenadeLauncher_Primary_poison', self);
             FireMode[0].ThisModeNum = 0;
             FireMode[0].Weapon = self;
             FireMode[0].Instigator = Instigator;
             FireMode[0].AmmoClass=class'GrenadeAmmoPoison';    // need these two to change the ammo
             Ammo[0] = Ammunition(Instigator.FindInventoryType(FireMode[0].AmmoClass));
             // Remove old fire mode 1
             if (FireMode[1] != none)
                 FireMode[1].Destroy();
             // secondary fire
             FireMode[1] = Spawn(class'GrenadeLauncher_Secondary_poison', self);
             FireMode[1].ThisModeNum = 1;
             FireMode[1].Weapon = self;
             FireMode[1].Instigator = Instigator;
             FireMode[1].AmmoClass=class'GrenadeAmmopoison';
             Ammo[1] = Ammunition(Instigator.FindInventoryType(FireMode[1].AmmoClass));
 
             // Remove old Attchment and spawn new one with correct skin
             if ( AttachmentClass != none)
               DetachFromPawn(instigator);
             AttachmentClass=class'GrenadeLauncher_PoisonAttachment';
             AttachToPawn(Instigator);
             PickupClass=class'GrenadeLauncher_PoisonPickup';
             Break;
         }
      }
      if (Count == 3)
      {
         count = 0;  // reset counter on last type of ammo
         Inv = Pawn(Owner).FindInventoryType(class'GrenadeAmmoStandard');
         if (Inv  != none  && Ammunition(inv).HasAmmo())
         {
             bChangedAmmo=true;
             // Remove old fire mode 0
             if (FireMode[0] != none)
                 FireMode[0].Destroy();
             // primary fire
             FireMode[0] = Spawn(class'GrenadeLauncher_Primary', self);
             FireMode[0].ThisModeNum = 0;
             FireMode[0].Weapon = self;
             FireMode[0].Instigator = Instigator;
             FireMode[0].AmmoClass=class'GrenadeAmmoStandard';
             Ammo[0] = Ammunition(Instigator.FindInventoryType(FireMode[0].AmmoClass));
             // Remove old fire mode 1
             if (FireMode[1] != none)
                 FireMode[1].Destroy();
             // secondary fire
             FireMode[1] = Spawn(class'GrenadeLauncher_Secondary', self);
             FireMode[1].ThisModeNum = 1;
             FireMode[1].Weapon = self;
             FireMode[1].Instigator = Instigator;
             FireMode[1].AmmoClass=class'GrenadeAmmoStandard';
             Ammo[1] = Ammunition(Instigator.FindInventoryType(FireMode[1].AmmoClass));
 
             // Remove old Attchment and spawn new one with correct skin
             if ( AttachmentClass != none)
                 DetachFromPawn(instigator);
             AttachmentClass=class'GrenadeLauncher_Attachment';
             AttachToPawn(Instigator);
             PickupClass=class'GrenadeLauncher_Pickup';
             Break;
         }
      }
    }
    bChangedAmmo=false; // set to flase as we have changed ammo at this point and need to reset the flag
    GoToState('WaitforAmmoChange'); // "Pauses" the gun so that the player can load the new ammo
}
 
 
function GiveTo(Pawn Other, optional Pickup Pickup)
{
 local Inventory Inv;
 
 Super.GiveTo(Other, Pickup);
 
 // add empty ammo for player (for Mutant, Last Man Standing and AllAmmo)
 if ( !bDeleteMe ) {
  Inv = Pawn(Owner).FindInventoryType(class'GrenadeAmmoStandard');
  if ( Inv == None ) {
   Inv = Spawn(class'GrenadeAmmoStandard');
   Ammunition(Inv).AmmoAmount = 0;
   Inv.GiveTo(Other);
  }
  Inv = Pawn(Owner).FindInventoryType(class'GrenadeAmmoNapalm');
  if ( Inv == None ) {
   Inv = Spawn(class'GrenadeAmmoNapalm');
   Ammunition(Inv).AmmoAmount = 0;
   Inv.GiveTo(Other);
  }
  Inv = Pawn(Owner).FindInventoryType(class'GrenadeAmmoPoison');
  if ( Inv == None ) {
   Inv = Spawn(class'GrenadeAmmoPoison');
   Ammunition(Inv).AmmoAmount = 0;
   Inv.GiveTo(Other);
  }
  Inv = Pawn(Owner).FindInventoryType(class'GrenadeAmmoFlash');
  if ( Inv == None ) {
   Inv = Spawn(class'GrenadeAmmoFlash');
   Ammunition(Inv).AmmoAmount = 0;
   Inv.GiveTo(Other);
  }
 }
}

Whew. Big and ugly. Again I should touch on a few key points. In the tick we needed to modify it to tell the client that we have changed ammo and we need to play the changeammo animations as well as change the skin. That's where the vars from the ChaosWeapon were used. The PlayChangeAmmo() was used to kick off the animation of the changing of the ammo. However we uses different skins for the different ammo type loaded into the 1st view of the weapon. So we needed to change the skins. We want to wait until the 1st model is out of view before we change the skin. We used an AnimNotify in the animation file to do this. We broke the animation up into two parts, an Ammodown then and Ammoup. Ammodown plays the "unloading" part. At the 2nd to the last frame of that AmmoDown animation we set up the AnimNotify to call the PlayAmmoMeshSwitch() function. This function will then change to the new skin (or mesh) and then play the up animation. Thus the skin/mesh changing all happens out of view of the player and when see the AmmoUp animation play its playing with the correct skin (or mesh). Please note that for this example we just changed the skin. In the other weapons we had to change the weapon mesh and did that in the PlayAmmoMeshSwitch() as well. We also play to the client a change ammo message. This is not needed. The BringUp() function needed to be modify to handle the dropped weapons. With out a check for the skins (or mesh) you would get the default skin/mesh of the weapon and not the current loaded ammo skin/mesh. The rest is more or less standard "stuff'.

A couple of things I should mention.

  • Using this method you will may need a new attachment class for each of the other ammo types. This attachment class can be a subclass of the default attachment class you use in your weapon. Nothing special is need here just change any skins/mesh in this attachment class if it applies.
  • You will also need to have a new weapon pickup for each of the other ammo type that you have. This handles the case of you having a different ammo type loaded when you toss your weapon or die. If you did not add a new pickup, then the person that picks up your dropped weapon is left with the original ammo and not the ammo you have it loaded with. Here is an example of one:
//=============================================================================
// GrenadeLauncher_Napalmpickup
// $Id: GrenadeLauncher_Napalmpickup.uc,v 1.9 2003/07/12 20:48:44 jb Exp $
//=============================================================================
class GrenadeLauncher_Napalmpickup extends GrenadeLauncher_pickup;
 
//over ride this to give out proper ammo to Nade (Napalm ammo)
function inventory SpawnCopy( pawn Other )
{
 local inventory Copy;
        local Weapon W;
    local GrenadeLauncher GL;
 
 if ( Inventory != None )
 {
  Copy = Inventory;
  Inventory = None;
 }
 else
  Copy = spawn(InventoryType,Other,,,rot(0,0,0));
 
    // add in the Napalm nade stuff here
    W = Weapon(copy);
    // Clean up "old" fire mode(s)
    // primary fire
    if (W.FireMode[0] != none)
      W.FireMode[0].Destroy();
    // Set Primary Fire to Napalm Stuff
    W.FireMode[0] = Spawn(class'GrenadeLauncher_Primary_napalm', self);
    W.FireMode[0].ThisModeNum = 0;
    W.FireMode[0].Weapon = W;
    W.FireMode[0].Instigator = Other;
    W.FireMode[0].AmmoClass =class'GrenadeAmmoNapalm';    // need these two to change the ammo
    W.Ammo[0] = Ammunition(Other.FindInventoryType(W.FireMode[0].AmmoClass));
 
    // secondary fire
    if (W.FireMode[1] != none)
       W.FireMode[1].Destroy();
    // Set secondary Fire to Napalm Stuff
    W.FireMode[1] = Spawn(class'GrenadeLauncher_Secondary_napalm', self);
    W.FireMode[1].ThisModeNum = 1;
    W.FireMode[1].Weapon = W;
    W.FireMode[1].Instigator = Other;
    W.FireMode[1].AmmoClass = class'GrenadeAmmonapalm';
    W.Ammo[1] = Ammunition(Other.FindInventoryType(w.FireMode[1].AmmoClass));
    W.PickupClass=class'GrenadeLauncher_NapalmPickup';
    W.AttachmentClass=class'GrenadeLauncher_NapalmAttachment';
 
    // Finally set the Count of the nade launcher to Napalm and give it to em :)
    GL = GrenadeLauncher(copy);
    if (GL != none)
       GL.Count=1;
 
 Copy.GiveTo( Other, self );
 
 return Copy;
}

This ammo pickup looks a lot like our change ammo function and its very simular to that other than a few tweaks. So did you make it through this tut? I hope that this gives you the basics on how to handle a weapon that can fire more than one ammo types. It also should avoid all of the issues with animations, network play and handle out of ammo, change ammo and droppen weapon issues. Thanks for your time.

Jb: Fixed an error that produced AN on the weapon pickup class. Sorry saw it only a few times and never had a chance to go back and fix it till now.

Zer0: This needs updating for UT2004, there are some weird issues with FireModes in 2k4. I can't get firemodes to change at all in 04, various type mismatch/destroy is not a member of Firemode/bad or missing etc errors.

zugy: Hmmm...Is there a way to get this working in 2k4? It'd help out a LOT for the grenade code I'm working on for Project Torlan...