Weapons

Overview

The weapon system in the FPS template is built around a handful of cooperating pieces:

BaseWeapon

Abstract class you inherit from to create individual weapons. Owns state (idle/firing/reloading/etc.), the weapon’s view model and animator, and one or more fire modes.

WeaponFireMode / BaseFireMode

Each input a weapon responds to (primary fire, secondary fire) is its own fire-mode object with its own ammo, fire rate, and refire timer.

AmmoContainer / AmmoType

Ammo is typed (9mm, shells, grenades, …) and pooled per type on the inventory. A fire mode’s container draws from that shared pool.

WeaponInventory

Manages a grid of weapon slots, ammo pools, selection/scrolling, and drives the Gum-based weapon HUD. The player controller owns one and calls into it every frame.

WeaponRegistry

Registers weapon types under a pickup name at startup, alongside which slot they occupy and how much ammo they grant on pickup.

Weapons are not tied to the player. Any entity can hold and fire a BaseWeapon, including NPCs; see Giving Weapons to NPCs below.

Creating a Weapon

Create a new file and inherit from BaseWeapon. At minimum you implement three abstract properties and SetupFireModes(), then register one BaseFireMode per input the weapon responds to. Here’s a minimal submachine gun:

public class WeaponSMG : BaseWeapon
{
    public override float ViewPunch => 0.025f * (Random.Shared.NextSingle() * 0.1f + 0.9f);
    public override float ViewPunchOffset => (Random.Shared.NextSingle() - 0.5f);
    public override string WeaponUIAssetName => "smg";

    protected override void SetupFireModes()
    {
        RegisterFireMode(FireMode.Primary, new SmgFireMode(this));
    }

    private class SmgFireMode : BaseFireMode
    {
        private readonly AmmoContainer container;
        protected override AmmoContainer Container => container;
        public override float FireRate => 0.1f;

        public SmgFireMode(BaseWeapon weapon) : base(weapon)
        {
            container = new AmmoContainer(AmmoType.NineMM);
        }

        public override bool OnFire()
        {
            Ballistics.ShootBullet(weapon.Owner, weapon.Position,
                weapon.Position + weapon.Right * 0.04f - weapon.Up * 0.04f + weapon.Forward,
            BaseWeapon.GetSpreadDirection(weapon.Forward, weapon.Right, weapon.Up, 0.024f),
            weapon.HeldByNPC ? 1 : 3);

            weapon.MuzzleFlash(weapon.Position);

            SoundScriptManager.PlaySound("Weapon.SMG.FireShot", weapon.Position, is3DOverride: weapon.Owner is not Player);

            return true;
        }
    }
}

The Abstract Properties

ViewPunch

How much the camera kicks when firing. Randomizing this slightly gives each shot natural variation.

ViewPunchOffset

The sine offset of the kick, which controls the kick direction. Randomizing this makes each shot feel slightly different.

WeaponUIAssetName

The internal name used to look up the HUD icon texture from Textures/ui/weapons/[WeaponUIAssetName].png.

Fire Modes

A fire mode implements:

FireRate

Seconds between shots. Assigned to the internal refire timer automatically after every successful fire

Container

The AmmoContainer this mode draws from, or null for unlimited/no ammo (see WeaponFists). Ammo is checked and consumed automatically before OnFire() runs.

AmmoPerShot (1)

Optional override for how much ammo one shot consumes.

OnFire()

Called once ammo has already been consumed and the refire timer allows it. Do the actual raycast/projectile spawn/sound/animation here and return whether the shot counts as a hit.

AfterFire()

Optional. Runs immediately after OnFire(). WeaponGrenade uses this to immediately reload its single-round container after every throw.

For weapons that need to fire on release instead (charge weapons) or manage input outside the normal held/pressed flow, call TryFireImmediate() directly from wherever your custom input handling lives rather than relying on OnInputPressed/Update.

Ammo

Ammo is typed via the AmmoType enum (None, NineMM, Shells, Grenade, and whatever else you add), with per-type magazine sizes registered in AmmoTypeManager:

{ AmmoType.NineMM, new AmmoProperties(17) },
{ AmmoType.Shells, new AmmoProperties(8) },
{ AmmoType.Grenade, new AmmoProperties(1) },

An AmmoContainer is a single fire mode’s magazine; it tracks CurrentAmmo up to MagazineSize and knows how to Reload() by pulling from the owning WeaponInventory’s shared ammo pool for that type. Construct one per fire mode that needs ammo:

container = new AmmoContainer(AmmoType.NineMM);

Reserve ammo itself lives on WeaponInventory, pooled by AmmoType rather than per-weapon. Two weapons that both use AmmoType.NineMM share the same reserve. Reloading calls ReloadFireMode() on the weapon, which delegates to the fire mode’s container, which pulls what it needs from that pool:

weapon.ReloadFireMode(FireMode.Primary);

A fire mode with Container => null, like WeaponFists, never checks or consumes ammo at all.

Weapon State & Animation

BaseWeapon.State is a small state machine (Idle, Drawing, Holstering, Firing, Reloading, Custom) that a fire mode checks before allowing a shot (CanFire requires State == WeaponState.Idle). You don’t set State directly, instead it’s driven by WeaponAnimator, which requests a state transition whenever a registered animation starts or finishes.

If your weapon has a view model, wire up its animator in RegisterAnimations():

public override void RegisterAnimations()
{
    WeaponModel = new CModelDisplay("Models/Weapons/v_smg.ccmdl");
    WeaponModel.DrawShadow = false;
    WeaponModel.Player = new CLinearAnimator(WeaponModel.Model, new AnimationLayer(WeaponModel.Model, new AnimNode(WeaponModel.Model.Sequences[0], 1, true)));
    Model.SetSlot("weapon", WeaponModel);
    WeaponModel.AttachTo(Model, "weapon_grip");

    Animator.RegisterSlot("weapon");

    Animator.RegisterAnimation(AnimDraw, loop: false);
    Animator.RegisterAnimation(AnimReady, loop: true);
    Animator.RegisterAnimation(AnimFire, loop: false);
    Animator.RegisterAnimation(AnimReload, loop: false);

    Animator.RegisterStateOnStart(AnimDraw, WeaponState.Drawing);
    Animator.RegisterStateOnStart(AnimReady, WeaponState.Idle);
    Animator.RegisterStateOnStart(AnimReload, WeaponState.Reloading);
}

RegisterStateOnStart/RegisterStateOnFinish map an animation name to a state transition; Animator.Play()/Animator.Enqueue() then drive both the visuals and the weapon’s state together. The common pattern in OnReload() and OnEquipped() is to play the transition animation and enqueue the idle loop right after:

public override void OnReload()
{
    Animator.Play(AnimReload);
    Animator.Enqueue(AnimReady);
}
public override void OnEquipped()
{
    Animator.Play(AnimDraw);
    Animator.Enqueue(AnimReady);
}
public override void OnHolstered()
{
    Animator.UnregisterSlot("weapon");
    Model.ClearSlot("weapon");
}

Muzzle Flash & Spread

weapon.MuzzleFlash(position) spawns a brief point light at the given world position. BaseWeapon.GetSpreadDirection(forward, right, up, maxSpread) is a static helper that randomly rotates the forward vector within a cone defined by maxSpread radians.

Adding Weapons to the Player

Register weapons with WeaponRegistry at startup, now including how much ammo a pickup grants:

WeaponRegistry.RegisterWeapon("w_fists",   typeof(WeaponFists),   0, 0);
WeaponRegistry.RegisterWeapon("w_smg",     typeof(WeaponSMG),     1, 0, (AmmoType.NineMM, 170));
WeaponRegistry.RegisterWeapon("w_shotgun", typeof(WeaponShotgun), 2, 0, (AmmoType.Shells, 80));
WeaponRegistry.RegisterWeapon("w_rpg",     typeof(WeaponRPG),     3, 0, (AmmoType.Grenade, 8));

The arguments are: internal pickup name, weapon type, slot number (1–9 maps to number keys, 0 for an unlisted/always-available weapon like fists), row within that slot, then any number of (AmmoType, amount) pairs to add to the inventory’s shared pools the moment the weapon is first picked up. Multiple weapons can share a slot at different rows; pressing the slot key while the HUD is open cycles between them.

Players pick up weapons from your item entities. The pickup name from the registry matches the item entity to the weapon class, so an item entity named w_smg gives the player a WeaponSMG and adds 170 rounds of NineMM to their pool — or, if they already have the weapon, just tops up the pool.

Note

Each weapon needs a HUD icon texture at Textures/ui/weapons/[WeaponUIAssetName].png. Without this, the slot will show no icon but still function.

Giving Weapons to NPCs

BaseWeapon is not tied to the player. The Owner field is just a WorldEntity, so any entity can hold and fire one. Set HeldByNPC so fire modes that scale damage or behavior by who’s holding the weapon (like the SMG’s fire mode above) can tell the difference:

BaseWeapon gun = new WeaponSMG();
gun.Owner = entity;
gun.HeldByNPC = true;

// In OnUpdate, call this every frame to tick fire mode refire timers:
gun.Update();

// When ready to shoot:
gun.FireModePressed(FireMode.Primary, aimDirection, right, up, entity.Position);

The NpcController in the FPS template handles weapon management for NPCs automatically if you assign one. See NPC System.