If you ever find yourself copy-pasting code around your project, it’s most likely a sign you’re doing something wrong. Structuring your code in a way that removes any code duplication will reduce the amount of bugs you create and make your programming experience a lot better.


The goal#

Shooting

What I need is a way to add shooting for both the player and enemies. Both shoot in a similar way, and can have things like status effects affect how they shoot. We could do this in a few ways:

Bad example#

While I could make two separate components like PlayerShooting and EnemyShooting, but this would mean I would have to duplicate our logic for instantiating the bullets, and everytime I want to add new logic (eg. a status effect that halves all bullets’ damage), I would have to remember to implement this in both places.

Good example (universal interface component)#

The better way to implement this, would be to create a universal Shooting component. Here I implement some facade methods, for example:

Shoot(GameObject bullet, float direction, ...);
ShootRing(GameObject bullet, float direction, int count, ...);

By itself this is of course not too useful, so I also need to make a PlayerShooting component. Instead of implementing the logic there, I just call the methods on the Shooting component.

As for the enemies, I call these methods from our AI system.

Shot patterns#

Shot pattern

There is one more mechanic that these can share: Shot patterns.

I could have some parameters on the enemy shoot behaviour for the bullet, count, spread, etc. like this:

Inspector view of shoot behaviour

However, from the player’s side, I might want some weapons that alternate between 2 bullets, some that shoot in a ring or whatever.

Bad approach#

I could make a Shoot() method on the Bow class, and derive from it for each new pattern, e.g. Alternating2ShotBow and override Shoot().

You might already see a problem with this. For every different pattern we need to program in a new class, which is not a good idea.

imagine working with this...

Good approach (Abstract class)#

Let’s take a more data-oriented approach. Let’s define an abstract class called ShotPattern:

[System.Serializable]
public abstract class ShotPattern
{
    public abstract void Shoot(Shooting playerShooting, float dir, bool playerBullet);
}

and some example implementations:

public class RandomPattern : ShotPattern
{
    [SerializeReference, SubclassPicker] List<ShotPattern> children = new();

    public override void Shoot(Shooting shooting, float dir, bool playerBullet)
    {
        if (children.Count == 0) Debug.LogError("No children");
        children[Random.Range(0, children.Count)].Shoot(shooting, dir, playerBullet);
    }
}
public class RingPattern : ShotPattern
{
    public GameObject bullet;
    public int count = 2;

    public override void Shoot(Shooting shooting, float dir, bool playerBullet)
    {
        shooting.ShootRing(bullet, dir, count, playerBullet);
    }
}

(note: unity doesn’t serialize abstract classes, but I used my Subclass Picker Attribute)

now, we just have one field for a ShotPattern, and call the Shoot() method on it.

Because some patterns have child pattern slots (e.g. RandomPattern, which picks a random child each time), we can nest these to create complex cycling or random patterns.

Example of a nested pattern

This means we don’t have to program a new class and recompile each time, and we can also use this in our enemy AIs.

This enables some fun mechanics, like a staff that mostly does low damage, but has a low chance to shoot a powerful bullet, an inaccurate bow, etc…