Weapons
Overview
The weapon system is built around two classes: BaseWeapon and WeaponInventory.
BaseWeapon is an abstract class you inherit from to create individual weapons.
WeaponInventory manages a grid of weapon slots and handles selection, scrolling, and the HUD display.
The player controller owns a WeaponInventory and calls into it every frame.
Creating a Weapon
Create a new file in your project, for example Weapons/WeaponSMG.cs, and inherit from
BaseWeapon. You'll need to implement a set of abstract properties and two fire methods.
Here's what a minimal weapon looks like:
{
public override int MaxAmmo => 30;
public override int MaxReserveAmmo => 240;
public override int PrimaryFireAmmoUse => 1;
public override int SecondaryFireAmmoUse => 1;
public override float ViewPunch => 0.025f * (Random.Shared.NextSingle() * 0.1f + 0.9f);
public override float ViewPunchOffset => (Random.Shared.NextSingle() - 0.5f);
public override bool UseMuzzleFlash => true;
public override bool UseDefaultFireBehavior => true;
public override string WeaponName => "smg";
protected override void PrimaryFire(Vector3 forward, Vector3 right, Vector3 up, Vector3 position, Vector3? tracerVisualOrigin = null)
{
GameEngine.ShootBullet(Owner, position, tracerVisualOrigin ?? (position + right * 0.04f - up * 0.04f + forward * 1), GetSpreadDirection(forward, right, up, 0.01f), 4);
currentRefire = 0.1f;
SoundDevice.Device.PlaySound($"{GameEngine.Instance.Content.RootDirectory}/Audio/Weapons/BaseSMG/smgfire.ogg",
position,
pitch: (Random.Shared.NextSingle() * 0.1f) + 0.95f,
disable3D: Owner is Player);
}
protected override void SecondaryFire(Vector3 forward, Vector3 right, Vector3 up, Vector3 position, Vector3? tracerVisualOrigin = null)
{
// Secondary fire not used on this weapon
}
}
The Abstract Properties
These properties define the weapon's core behavior and are all required:
MaxAmmoHow many rounds fit in the clip.
MaxReserveAmmoTotal reserve ammo the player can carry.
PrimaryFireAmmoUseAmmo consumed per primary fire. Set to 0 for infinite ammo weapons.
SecondaryFireAmmoUseAmmo consumed per secondary fire. Set to 0 if unused.
ViewPunchHow much the camera kicks when firing. A small random multiplier gives it natural variation.
ViewPunchOffsetThe sine offset of the kick, which controls which direction it punches. Randomizing this gives each shot a slightly different feel.
UseMuzzleFlashWhether to spawn a brief point light at the muzzle when firing.
UseDefaultFireBehaviorControls how the player controller invokes fire. See below.
WeaponNameThe internal name of the weapon. Used to look up the HUD icon texture from Textures/ui/weapons/.
UseDefaultFireBehavior
This flag controls which code path the player controller uses to invoke firing. When set to
true, the player calls TryFirePrimary() and TryFireSecondary() every
frame while the fire buttons are held. The base class handles checking ammo, applying the refire timer, and
consuming ammo automatically before calling your PrimaryFire() or SecondaryFire()
method.
When set to false, the player instead calls the press and release handlers directly:
public virtual void HandleFireReleased(Vector3 forward, Vector3 right, Vector3 up, Vector3 position) { }
public virtual void HandleSecondaryFirePressed(Vector3 forward, Vector3 right, Vector3 up, Vector3 position) { }
public virtual void HandleSecondaryFireReleased(Vector3 forward, Vector3 right, Vector3 up, Vector3 position) { }
This is useful for weapons that need manual control over their firing logic, like a charge weapon that fires on release, or a weapon that behaves differently depending on how long the button is held. When using this mode you are responsible for managing ammo yourself.
The Refire Timer
The currentRefire field is a countdown timer that prevents the weapon from firing again until
it reaches zero. Setting it at the end of PrimaryFire() is how you control fire rate:
The base class decrements this automatically in Update() and TryFirePrimary() won't
call through to your fire method until it hits zero. You don't need to manage it yourself beyond setting it.
Firing Bullets
The FPS template provides a GameEngine.ShootBullet() helper that handles raycasting, entity
hit detection, decals, impact particles, and tracer effects all in one call:
Owner, // the entity firing the shot
position, // origin of the ray
position + right * 0.04f + forward, // visual origin of the tracer
GetSpreadDirection(forward, right, up, 0.01f), // direction with spread
4 // damage
);
The tracer visual origin is offset slightly from the actual ray origin so the tracer appears to come from
the weapon model rather than the center of the screen. The GetSpreadDirection() helper is
provided by BaseWeapon and randomly rotates the forward vector within a cone defined by
maxSpread in radians. Pass 0 for a perfectly accurate hitscan.
Optional Overrides
BaseWeapon also has a few virtual methods you can optionally override:
Reload()Called when the player presses reload. The default implementation moves ammo from reserve into the clip correctly, but you can override it for custom behavior.
UpdateSpecial()Called every frame regardless of firing state. Useful for things like charging mechanics or continuous effects.
OnEquipped()Called when this weapon becomes the active weapon.
OnHolstered()Called when the player switches away from this weapon.
Adding Weapons to the Player
Weapons are assigned to the inventory in your player controller's OnSpawn(). The inventory uses
a grid of slots and rows, where the slot corresponds to a number key and the row lets you stack multiple
weapons under the same key:
weaponInventory.AssignWeaponToSlot(new WeaponSMG(), 0, 0);
weaponInventory.AssignWeaponToSlot(new WeaponShotgun(), 1, 0);
weaponInventory.AssignWeaponToSlot(new WeaponRPG(), 2, 0);
The first argument is the weapon instance, the second is the slot (0 through 8, corresponding to keys 1 through 9), and the third is the row within that slot. If you assign multiple weapons to the same slot at different rows, pressing the slot key a second time while the weapon HUD is visible will cycle between them. Scrolling the mouse wheel also cycles through all weapons across all slots.
This behavior allows for all weapons to have an assigned slot, but not be required to be picked up in that exact order. Similar to Half-Life and Half-Life 2.
Each weapon needs a HUD icon texture at Textures/ui/weapons/[WeaponName].png in your content
directory, where WeaponName matches the string returned by your weapon's WeaponName
property. Without this the inventory will still work but the HUD slot will show no icon.
Giving Weapons to Non-Player Entities
BaseWeapon is not tied to the player at all. The Owner field is just a
WorldEntity, so any entity can hold and fire a weapon. You'd instantiate the weapon, set
Owner manually, and call TryFirePrimary() with whatever direction vectors make
sense for that entity:
gun.Owner = entity;
gun.Update(); // call this every frame to tick the refire timer
// when ready to shoot:
gun.TryFirePrimary(aimDirection, right, up, entity.position);
This is how you'd give the Grunt from the previous guide a weapon, for example.