A Modular Weapons System

This tutorial focuses on creating a modular weapon system. We can create a large amount of variety and unexpected combinations without individually designing each weapon by stringing together different kinds of motion, replacing projectiles, changing the firing direction, and what happens when they hit the enemy.

Procedural and emergent gameplay

While procedural content uses algorithms, another facet of procedural games can be different combinations of game rules without strict design. We call this emergent gameplay. It’s the fun that happens in places where fun wasn’t explicitly designed. It’s not a content making algorithm, but it’s a vital part of a rules-based system. Modular weapons system is not a new concept. As far back as the days of shoot-em-ups, you had games like Raiden with its different weapon systems.

But in the days of the indie marvels, you have games like Binding of Isaac and Enter the Gungeon which embrace this idea more fully. Binding of Isaac changes how your weapons work based on what items you pick up, whereas Enter the Gungeon has set weapons with only a few modifiers. Either way, they embraced this idea of a modular weapons system and made cool and fun concepts out of it.

The final system’s design and usage

The system we are making will have its central part on a Node2D and expose some properties: a scene that contains the orientation and positions of the cannons, a scene that controls what kind of projectile it’s shooting, and two arrays of resources that combine different kinds of projectile movement and events that occur after impact, respectively.

You can see a plasma bolt that moves in a straight line and homes in on targets and explodes on impact out of a single cannon, and a laser that moves in a sine wave from three forward facing cannons configured below.

The project is setup so you can drag and drop scenes and resources in as needed when making combinations manually, and properties and helper functions control it through code. For example, this is a sample script for an upgrade that gives you homing missiles.

# Homing missile upgrade
extends Area2D


# Emitter to replace the current one with
var new_emitter = preload("res://ModularWeapon/Projectiles/MissileEmitter.tscn")

# Homing missiles _must_ have the homing motion
var new_motion = HomingMotion.new()

# Missiles _must_ explode when picked up
var new_event = ExplosionEvent.new()


func _ready() -> void:
    connect("body_entered", self, "_on_Area_body_entered")


func _on_Area_body_entered(body: PhysicsBody2D) -> void:
    var weapons = body.get("weapons")
    if weapons:
        weapons.projectile_emitter = new_emitter
        weapons.add_motion(new_motion)
        weapons.add_event(new_event)

        var hud = body.get("game_hud")
        if hud:
            hud.announce("Equipped homing missiles!")

        queue_free()

Keeping the code and resources separate

Once implemented, we want the source code to get out of the way. When opening the weapons folder, you should see the scenes and resources that go into the weapons system. Something like:

ModularWeapons/FiringConfigurations, ModularWeapons/Projectiles, ModularWeapons/Motions, and ModularWeapons/Events.

We mirror the folder structure inside of a src directory where we keep the more complex stuff needed only when implementing new data.

ModularWeapons/src/FiringConfigurations, ModularWeapons/src/Projectiles, ModularWeapons/src/ProjectileMotions, and ModularWeapons/src/ProjectileEvents. These will have more subfolders and extract scripts that are not relevant to the end product, but the goal is to separate the development from the usage.

For this tutorial, I note where I end up putting each main file I create and make a special note to say what is a resource and what is source code or an asset.