Building a Systemic Gun

Games are somewhat obsessed with guns. Everything from nukes to squirt guns have been modelled and simulated in extraordinary detail. Since one reason that every widely copied design paradigm keeps getting copied is the volume of references, the fact that there are countless games to play for anyone who wants to research gun gameplay invariably leads to more games with gun gameplay. It’s an exponential curve, probably.

Another–better–reason is that shooting is fun. One of those mechanics that people will just “get” with very little explanation or tutorialization. Not least of all because you usually control shooting directly with no content go-between as is often the case with other types of combat. (A “content go-between,” such as a heavy attack animation that needs to play to completion.)

So let’s make some guns and use them to illustrate the difference between a feature and a system.

Peter van Uhm chose the gun.

A Gun Feature

We’ll first look at the kind of solution you’ll often reach for intuitively. The examples are from pseudofied code based on stuff I wrote for a freelance project in 2017.

This code did its job, by the way. It’s not great, but for a rapid prototype intended to demonstrate basic shooting mechanics it was good enough. So just because I bring this up as the opposite of what comes next doesn’t mean you should instantly rewrite your code if you have something that solves things in a similar way. (If you look at something like the FPS template in the Unreal Engine, it does things in a similar way, for example.)

In any case, the idea was that guns are defined by how they spawn projectiles. You had a fire selection choice per gun–Single, Semi, Auto–and that was represented as an enum that would switch how input was handled by the gun.

When the shotgun was added later, the concept of submunitions was also added. This allowed the gun to fire more than one projectile with a single call to the Fire method. Ammo was handled explicitly and fire rate was regulated directly in the code. Nested branching, nested loops, and some early returns. Beautiful!

This is a gun feature, because it’s really just taking the concepts that the player will consider defining for a “gun” and turning them into code procedurally. When a non-technical game designer asks a programmer to do specific things, this is often what they’ll get, because “oh, eh, can you also add ammo?” is the full extent of the thought process.

bool Gun::Fire(float Accuracy)
{
    if (CurrentAmmo == 0)
        return false;

    if(Time > Timer)
        Timer = Time + (1 / FireRate);

    CurrentAmmo--;

    for (int s = 0; s < SubMunitions; s++) 
    {
        Direction += Random.Vector * (Spread * (2f - Accuracy));
        Hits = Raycast();

        if (Hits > 0) 
        {
            RayHits = OrderByDistance(RayHits);

            for (int i = 0; i < Hits; i++) 
            {
                if (i < PenetrationCount) 
                {
                    SpawnProjectile(Muzzle, Direction);
                }
            }
        }
    }

    return true;
}

Sample Guns

Just a couple of examples to demonstrate how this pseudocode would handle different data. Each gun would have to be an instance of the same object, probably.

Pump-Action Shotgun

Classic pump-action shotgun, often used by action movie heroes. Goes ca-click and boom, but not necessarily in that order.

  • CurrentAmmo = 6. Holds six shells.
  • Spread = relatively high, to get the cone effect you expect from a video game shotgun.
  • FireRate = 1. You can shoot at most once per second.
  • SubMunitions = 8. Eight pellets per shot.
  • PenetrationCount = 0. Won’t penetrate anything at all–any hit stops the shot.

Assault Rifle

Your AK47, for when you absolutely positively… you know. Since it has to use the same variables, due to the infinite lack of flexibility on display, some of the numbers aren’t really used.

  • CurrentAmmo = 30. Has 30 rounds in a magazine.
  • Spread = low, since you want bullets to land roughly where you aim. Code doesn’t support any recoil or similar, so this spread will have to do.
  • FireRate = 0.1. Fires a bullet every 10th of a second. (An AK47 has a fire rate around 600 rounds per minute.)
  • SubMunitions = 1. No actual submunitions–just fire one bullet per bullet.
  • PenetrationCount = 2. Goes through two penetrable obstacles.

(Some) Problems

As you can clearly tell, there are many problems with this pseudocode. I’ll list some of the more obvious ones here:

  • If we come up with a new type of firearm or some kind of constraint, like overheating, we’ll have to add more ifs, elses, and early returns to the code. The risk of bloat and spaghetti through the course of a project is quite high and some things will become nested disasters faster than you can spell regression.
  • Coming up with a new gun requires that we write code for it–it’s not possible to add it by adding data. This is bad, because programmers become a dependency, and even minor tweaks to the central concept of “gun” will require programming support.
  • Every concept you want to introduce has to be represented by an explicit variable rather than logic. This is a big deal, and you’ll see why later. Variables on their own have no meaning and there’s always a risk that you use them in ways that are counter-intuitive because it makes sense in the code. (Like how SubMunitions will be used to fire only one projectile for everything that isn’t a shotgun.)
  • Max number of fired projectiles gets directly tied to the frame rate. This doesn’t have to be a problem, but it does mean you can’t handle realistic fire rates and that you’ll get serious issues if you have lag spikes for some reason. If we’re honest about it, simulating every projectile isn’t what you should do anyway, but what if we want to?!

Gameplay Systems

Before we make the other version of the gun, let’s talk very briefly about systems.

The simplest way to define a gameplay system is to look at it as a node that has inputs, outputs, and feedback.

  • Inputs include any and all data that the system needs to understand.
  • Outputs are what gets thrown out of the node after plugging in the inputs.
  • Feedback will typically be callbacks that communicate changes to the player.

With this line of thinking, let’s imagine we have two systems: a Health System, and a Level Up System.

Health System

The Health System takes damage received, healing received, and current character level as inputs. It outputs current health. It triggers callbacks on damage taken, on less than 10% health remaining, and on healed to full health. The kind of stuff that can make the screen flash red, or play a sound cue telling you you’re fully healed.

Level Up System

The Level System takes experience points gained as input and it outputs the current character level. It also has a callback triggering when you gain a level. The callback that can make a choir go nuts.

These two systems implement their own logic and care little about each other, but they still affect each other. When you gain levels, and Level System starts outputting a new current character level, the Health System will also display a new current health value. They are tied together without knowing about each other. Like a really sad love story.

This way of building gameplay can be extremely powerful, since the decoupling of logic and data allows for a huge degree of flexibility and modularity.

So now that you know what I mean when I say “system”–let’s get on with the systemic version of the gun!

A Systemic Gun

Guns can be divided into a number of abstract concepts. For this implementation, there are three: Triggers, Constraints, and Spawners. There can be more of them too, tailored to whatever futuristic or crazy game you want to make. Projectile could be its own thing. You may want a GunController that allows you to switch between multiple triggers. Maybe a GunDialogue component that lets you do High On Life-like things. This is just an example.

Do note that these concepts have nothing to do with the physical gun. Stocks, sights, muzzle breaks, and all the customization things that can be done are simply visual representatives of functionality–the stuff we’re doing here is the functionality. How you represent it is up to you. This is important, because it’s all too easy to get stuck “thinking like a user” when you implement things, when it’s the one time in development where you shouldn’t.

Trigger

A Trigger accepts player input and operates on that input based on the type of trigger used. In the nearby pseudocode, it simply triggers its callback when the correct input phase happens on whatever button it’s tied to.

  • Trigger_Charge: you must hold the button down for a set amount of time to “charge” the gun before it triggers. It can then fire either on reaching the charge timer or on release after reaching the charge timer.
  • Trigger_Continuous: while holding down the button, the gun continues to trigger–indefinitely, if allowed to do so.
  • Trigger_Once: when you press the button, the gun fires once. You must release the button and press again if you want to fire again.
  • etc.
class Trigger_Once : public Trigger
{
public:
    OnTriggerSignature OnTrigger;

    void ReceiveInput(EInputActionPhase Phase)
    {
        if (Phase == EInputActionPhase::Started)
            OnTrigger.Execute();
    };
}

Constraint

A Constraint is like a gatekeeper that stops a trigger from activating. Constraints can be added together, combined, and can also be required by a spawner or trigger if there is some kind of special conditions it demands (see Spawner_FullDischarge, in the next section). The pseudocode shows just how simple this can be–just have the constraint return whether it succeeds or not, and handle any relevant logic in a Process method that gets called if the gun is allowed to fire.

  • Constraint_Ammo: you must have ammo available to fire this gun. The constraint itself will say how much ammo you need and every time the constraint returns true, ammo is decreased by one.
  • Constraint_JamRisk: some percentage chance of jamming with every shot. If it jams, the trigger must be released before it can be pressed again. Whoever came up with this constraint is one annoying game designer (it was me).
  • Constraint_MaxHeat: builds up heat with every call and continuously decreases heat over time. If heat goes over a threshold, the gun is blocked from firing and must cool off before it can fire again. Cooling back down can be tied to an internal timer, to world heat levels, or to something else–the constraint doesn’t even have to care about the actual heat.
  • Constraint_WaitAction: sets an action in motion after firing that must be completed before the gun can be fired again. Think pumping or cocking or reloading, etc.
  • etc.
class Constraint_Ammo : public Constraint
{
public:
    int CurrentAmmo;

    int MaxAmmo;

    Constraint_Ammo()
    {
        CurrentAmmo = MaxAmmo;
    };

    bool Evaluate()
    {
        if (CurrentAmmo == 0)
            return false;

        return true;
    };

    void Process()
    {
        CurrentAmmo--;
    };
}

Spawner

Finally, after a trigger has triggered and the constraints have agreed that it’s okay, the Spawner comes in and make gun-things happen. In the nearby pseudocode, you see that this specific spawner requires a Constraint_Ammo constraint. If this doesn’t exist, you can throw exceptions, have your game crash hard to desktop, or have the spawner add the constraint with some default settings. Whatever pleases your particular programming fancies. Point is that it shows some spawners will only work under certain conditions–in this case, because it’s firing every round in the magazine in a single discharge.

  • Spawner_FullDischarge demands that there is a Constraint_Ammo applied and will fire every projectile in that constraint in a single go. Think rocket rack, volley gun, or Super Shotgun.
  • Spawner_SingleDischarge is the standard way to shoot–every time the trigger says “shoot,” this spawner spawns one projectile.
  • Spawner_MultipleDischarge is that submunition shotgun thing from the old definition–it spawns a set number of projectiles every time the trigger says “shoot.” But now it’s completely decoupled and only used by guns that actually need it.
  • etc.
class Spawner_FullDischarge : public Spawner
{
private:
    Constraint_Ammo AmmoConstraint;

public:
    void Spawn()
    {
        while (AmmoConstraint.CurrentAmmo > 0)
        {
            base.Trigger();
            AmmoConstraint.CurrentAmmo--;
        }
    };
}

The Systems in our System

There are now a number of different things to choose from. Let’s say that a gun must have one Trigger and one Spawner and may have anything between zero and all constraints. That already gives us a large variation of guns. Technically, a gun could have more than one of the same constraint. Say, one Constraint_Ammo(100) that represents the battery, and another Constraint_Ammo(30) that represents the pellets it fires. The way things are combined doesn’t really care how you combine them.

Pump-Action Shotgun

Fires a buckshot shell (eight pellets) when you pull the trigger, but must then be pumped and only has six shells in the magazine.

  • Trigger_Once
  • Spawner_MultipleDischarge(8)
  • Constraint_Ammo(6)
  • Constraint_WaitAction(PumpAction)

Assault Rifle

Classic bullet-hosing rifle, good for modern soldiering. Hold down the trigger and it keeps spitting lead, but each bullet fired has a 1% risk to jam the gun and the magazine clicks after 30 shots.

  • Trigger_Continuous
  • Spawner_SingleDischarge
  • Constraint_Ammo(30)
  • Constraint_JamRisk(1%)

Battle Rifle

A more reliable burst-firing rifle that is good for slightly longer range modern soldiering. Fires a three-round burst on each pull of the trigger and will run dry after 20 shots.

  • Trigger_Burst(3)
  • Spawner_SingleDischarge
  • Constraint_Ammo(20)

40-Watt Phased Plasma Rifle

The mythic rifle that a certain killer cyborg asks for in a gun store before bemurdering the store proprietor with a shotgun. You need to hold the trigger to charge it for two seconds, then release to fire, and if it gets too hot (200 degrees) it needs to cool down for a spell. No one knows exactly how it cools down–that’s a problem for other systems in the game.

  • Trigger_Charge(2, FireOnRelease)
  • Spawner_SingleDischarge
  • Constraint_MaxHeat(200)

Conclusions

If a designer comes up with a new cool type of gun, they can now describe them in terms of Trigger, Constraints, and Spawner. If we figure down the line that we want one gun to have multiple spawners, or we want to add more types of logic in addition to these three, it’s very simple to do and can be done as isolated additions that can then be added through some kind of custom gunsmithing tool.

We’ve successfully made a more systemic gun, and made it much easier to please Mr. Schwarzenegger’s iconic death machine in the process!

Published by mannander

Professional game developer since 2006. Opinionated rambler since 1982.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s