Writing clean, reusable code
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#
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#
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:
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.
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.
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…