Modular Code Architecture

Why should I consider Modular Code?

After working on Stellar Winds (a university final project) for over 6 months, I have managed to build an intense amount of technical debt as a result of my own negligence, in the form of spaghetti code. If you’re reading this then maybe you can avoid the same mistakes that I have made by considering modular code.

Unity offers a very easy way to remove instance reference dependencies and facilitates a space for modular architecture to shine. In this blog, we will explore what Modular Code Architecture is within the context of Unity, how to design your systems in a scalable and reusable way, and define some limitations of the architecture.

If you want access to the source code, check out my Patreon where you can get it for just $1. It takes a lot of time to research and put these together so I’d appreciate the support, thanks!  


What is Modular Code?

Put simply, it is code that does one very specific thing really well. It is code that can be used by anything in any given context. Think of your classes as a specialisation, we wouldn’t want a plumber to install our roof as much as we wouldn’t want a level designer to program our AI.

Don’t worry, we’ll be going over some practical examples that you can pull from and use. By the end of this blog, you’ll have a great understanding of the concept and be able to do it yourself.

“Modular code architecture (enables) code to be split into many independent packages that are easily developed”

(Tayar, 2021).

First things first, make a plan

I get that you want to jump straight in and get it done. Programming and game dev is really fun, but that’s what I did with Stellar Winds and as a result, its codebase is brittle. If you jump straight in without a plan you’ll end up building insane technical debt and waste a lot of time on “unforeseen circumstances.” Just look at this health class that handles shields and health, ridiculous.

Depending on how you currently work or were previously taught, you may have been required to write a Technical Specifications Document. It’s pretty commonly disliked amongst my peers, including myself, however, in the context of modular code, it is the most important step.

Tech Spec 1

Let’s explore my process using an abstracted health system as an example. First, write out everything the SYSTEM should do. Refer to Tech Spec 1.

Next, critically analyse this system, what individual elements can you identify within it and what is its core function? Simply, its core function is to track the progress to the ‘fail’ state. It also needs audio, particle, camera shake, UI updates, and game event triggers.

One thing you may notice is that each element can be separated into two categories. Function and Feedback. With this outlook, we can then define every CLASS that we will need to create in order to develop this system. Remember while planning these out to specify a specialty for each class. Refer to Tech Spec 2.

Tech Spec 2

Finally, if you are more of a visual person like me – you can put together a  flow chart of sorts that clearly defines connections and each class’s specialty. I use Figma for this but a great alternative is draw.io– which can be connected to google drive.


Unity Events

We will be primarily using UnityEvents in order to make everything work and reduce the overall code we have to write, so let’s explore what we can do with them for clarity’s sake. For starters, we can call public methods from a class within the inspector. We can also parse strings and send floats if a method has a parameter. Note that only methods with a single parameter can be used like this.

We can also send dynamic variables by declaring the events a little differently. This is important to understand as we will be using it frequently. You write them as follows:

// Declaration
UnityEvent<float, int, bool> eventToCall;

// Invoking
eventToCall.Invoke(0.2f, 1, true);

Now when we hook up a class that has a method with one of the same parameters, a ‘dynamic’ option will appear in the response of the UnityEvent. Neat right? Note, you can still call normal methods when you’ve marked an event as dynamic, however, you still need to give the invoke method a value when invoking the event. That pretty much sums up UnityEvents, if there’s anything I may have missed let me know.


Execute your bad habits

Now that we have a plan written out and understand how UnityEvents work, we can very easily program all of our classes. Let’s begin with the Health Controller class, we’re simply going to have a float to represent health and three events.

public class Health : MonoBehaviour
    {
        [Header("Values")]
        [Range(0, 1)] public float health;

        [Header("Responses")]
        public UnityEvent<float> onDamaged;
        public UnityEvent<float> onHealed;
        public UnityEvent onDied;
    }
public void ProcessDamage(float dmg)
        {
            health -= dmg;
            if (health <= 0)
                OnDied();

            OnDamaged();
        }
        public void ProcessHeal(float dmg)
        {
            health += dmg; OnHealed();
        }

        private void OnDamaged() { onDamaged.Invoke(health); }
        private void OnHealed() { onHealed.Invoke(health); }
        private void OnDied() { onDied.Invoke(); }

Now add ProcessDamage() and ProcessHeal() methods that accept a float parameter. Then, add three methods that invoke each relative UnityEvent, so it’s very clear what we’re doing. For now, I suggest either hooking up two simple inputs or using EasyButtons so that we can test it in isolation.

Now that we have our health class set up, it’s time to move on to the individual classes: Audio Controller, Animation Controller, Slider Controller, Camera Shake Controller, and Game Event Controller. These are all really straightforward, with the exception of the animation controller – so I won’t go into detail for each class. Instead, this is a perfect opportunity for you to do it yourself – using your technical specifications as your guide. Try to ensure that a single public method can be called to invoke the desired feedback.

Let’s look into the Animation Controller. As you may know, the Animator uses parameters for its state machine to function. With this in mind, you can create simple methods that take a string as a parameter to control the animator. We can invoke a Trigger, Set a bool, and Set a float value for blend trees.

Animator.SetTrigger(paramater)
Animator.SetBool(paramater, condition)
Animator.SetFloat(paramater, float)
public void SetAnimationTrigger(string animationTrigger)
        {
            animator.SetTrigger(GetParamName(animationTrigger));
        }
        public void SwitchAnimationBool(string animationTrigger)
        {
            string param = GetParamName(animationTrigger);
            animator.SetBool(param, !animator.GetBool(param));
        }

        public void SetBlendTarget(string blendParam)
        {
            targetBlendParam = blendParam;
        }
        public void SetBlendValue(float value)
        {
            animator.SetFloat(targetBlendParam, value);
        }

There could be any number of ways to do this, however, how I did it was to create a list of strings that act as an identifier for each of the parameters in the Animator. I then have four methods to interface with the Animator and a handler for checking the parameter name.

Pretty easy right? I just took inventory of what the animator can do and put together methods that facilitate UnityEvent interaction.

Now of course you could declare a UnityEvent that takes in both a string and float to skip the need to store a reference to the blend parameter – if that’s how you want to do it. However, I wanted to ensure that I didn’t have anything other than health variables in the health class. Again, it all depends on your system and the plan that you created.

If blend trees are a core function of your system then maybe this would be the way to go, however, I don’t mind the extra step of storing a reference to the target parameter. 

With the hardest class explained, you’ll be able to easily put together every other controller class without instruction. Give it a go before moving on.

// Decleration
string blendTreeParam = "objectScale";
float health;

UnityEvent<string, float> onChangeBlendEvent

// invoking
public void ProcessDamage (float amount)
{
health -= amount;
onChangeBlendEvent.Invoke(blendTreeParam, health);
}

Putting it together

Right so now that we have all of our classes, let’s set up our player object. I have a very specific way I like to name and structure my objects (which I’ll make another post on in the future), so I’ll quickly put it together and show you where I have put each class.

Now, by referring to the first instance of our tech spec, we can clearly see what methods we need to call through our UnityEvents. So let’s quickly do that. Note: objects highlighted in red are objects that the UnityEvents reference.

On Damaged

  • Slider Controller – UpdateSliderValue (dynamic float)
  • Animation Controller – SetAnimationTrigger(Damage Anim param)
  • Animation Controller – SetBlendTarget(Blend param)
  • Animation Controller – SetBlendValue (dynamic float)
  • Audio Controller – Play
  • Particle System – Play
  • Camera Shake – LightShake

On Healed

  • Slider Controller – UpdateSliderValue (dynamic float)
  • Audio Controller – Play
  • Animation Controller – SetBlendTarget(Blend param)
  • Animation Controller – SetBlendValue (dynamic float)

On Died

  • Game Event Controller – Trigger

Notice how quick that was? Now if we don’t have an audio source for some reason, don’t worry, the core of your system will still work. This is because we separated everything into modular parts that don’t need a lot of messy instance references. You can also save this as a prefab calling it “Object With Health,” or something like that, knowing full well that everything will work straight out of the box. You can then adapt it to an enemy, follower, destructible box – literally anything that has health. The power shines here with the destructible box example. We don’t want UI to show health on a box so we just remove the UI controller, everything still works, and you didn’t have to refactor your hardcoded system to remove UI, great!


Increasing Scope

So now we’ve managed to create a nicely organised modular system, in definite contrast to the spaghetti code you may have been writing up until now. It’s refreshing, isn’t it? However, overall, this class is much simpler than the health class in Stellar Winds (about 250 lines simpler). One thing to consider here is Stellar Winds features shields and this example system does not. “Is that why it’s 250 lines shorter?” aha, no… The next step is to scale the systems up further, so let’s start by adding shields. 

Now you could just add another float, a set of UnityEvents, and their relative methods to the Health class and call it a day. However, that’s not very modular, is it? What if we wanted to add an ability that gave the player another temporary layer of protection in the form of an over shield? That means we need to add, again, another float, a set of unity events, and their relative methods. Just as this paragraph is getting longer, so will your code.

Tech Spec 3

“Okay I get it, what do we do then?” I hear you ask. Well this is where I remind you that it’s important to plan this stuff out in the technical specifications (as I have done – refer to Tech Spec 3), but I won’t get you to do that, instead just follow these really simple steps.

First, refactor our current health controller class into a normal C# Serializable class called Stat. Change the float, all relative events, and methods from “health” to “stat.” We’ll also want to change “OnDied” to “OnStatZero” and “ProcessDamage/ ProcessHealth” to “DecreaseStat/ IncreaseStat” for clarity. 

Let’s also add logic for recharging the stat over time with a bool (or enum if you like) controlling whether it can recharge or not. Finally, put the logic into a Tick() method that we can call from another update method.

Next, let’s make a new Health Controller class that has two “Stat” variables. These will replace our health float and acts as our new shield float. Just as before we’ll need to add ProcessDamage() and ProcessHeal() methods. Let’s also add an onDamaged and onDied UnityEvents so we have a higher level of control and remember to call each stats Tick() method in the Update() method. 

Okay great, so now we’re pretty much back to where we were except this time we have two stats to work with. The best thing is, we can add as many as it makes sense. Setting this system up is exactly the same as the previous, however, now we have control over individual stat changes nested within the new Health class.

Now you may also be thinking, “But what if as you said, we want to add a temporary overshield? Would we have to declare a new stat for each temporary item we want to add? If so, would that not just be spaghetti code creeping back into our development patterns?” You can probably guess what I’ll say at this point. Did you plan ahead and allow for temporary stats? If the answer is yes then you probably didn’t ask that question.

Luckily, because of this modular architecture, it is really easy to add a list of stats for temporary buffs. These could dynamically change through play or you could replace your health and shield stats with a list of stats and use ints to identify each one. 

Now I couldn’t possibly answer every question you may have in a blog written before you read it, but hopefully, you can adopt a modular mindset and put together a system that fits your needs.


Advanced Simulated Health System

Now, as part of increasing the scale of our modular system, let’s explore a simulated health system. I will be using Fallout’s limb damage as an example here and focus purely on the legs.

I will again remind you that this is something that needs to be planned in advance, however, due to the holistic approach of this blog, we can explore how to do this without a solid plan.

Here’s a quick and dirty movement class that I wouldn’t recommend using in an actual project, but will work well for this example.

public class MovementExample : MonoBehaviour
    {
        public Vector2 speed = new Vector2(0.5f, 5f);
        float currentSpeed;

        private void Start()
        {
            SetSpeed(1);
        }

        private void Update()
        {
            Vector3 input = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
            transform.Translate(input * currentSpeed * Time.deltaTime);
        }

        public void SetSpeed(float percent)
        {
            currentSpeed = Mathf.Lerp(speed.x, speed.y, percent);
        }
    }

Now let’s create a new health class similar to the other, allowing for individual limb damage. Create a Unity Event called onLimbsDamaged where we use a float as a parameter. This is going to send the per cent of total leg health to our movement class. This looks like:

Float percent = (leftLeg.value + rightLeg.value) / 2

Next, let’s hook up the movement class’s ChangeSpeed() method to the onLimbsDamaged responses in the inspector. Let’s also set up some UI to represent limb damage.

Easy, now when each limb takes damage, the overall speed of the player is reduced linearly. You can make this as complex or as simple as you want, but notice how straightforward it is to create more advanced systems with this modular method.

You can easily isolate function and all feedback works as intended while you make changes to your existing systems. No more spaghetti code and no more wasted time.

I said I was going to discuss some of the limitations of this system and I will, however, this should be enough for you to go ahead and start planning out and executing your own systems. If you want to go ahead and do your thing then go for it, but there are some limitations to be aware of. 

Before I get into that though, if you want access to the source code, check out my Patreon where you can get it for just $1. It takes a lot of time to produce videos and write out blogs so if you can support me then I’ll be able to make more resources for you to use and I would be forever grateful. Thanks for your time, it means a lot.


Limitations, Considerations, and Possible Solutions

Limitation 1: As designers gain more control over game logic, programmers lose control. By moving all logic into Unity Events, you effectively make instance references useless in some cases.

Potential Solution: Clearly define what systems REQUIRE programming to be the main form of control while allowing designers to interface with it in a similar and easy fashion.

Limitation 2: If you are experiencing a logic error, it could potentially take more time to dig through your nested prefabs to find where the error is located.

Potential Solution: Take advantage of the modularity and test everything in isolation before marking the asset as ready. Create prefabs that have preset feedback and create variations to preserve the original working copy.

Limitation 3: You cannot interface with static classes or manager classes that are instantiated at run-time as you would with feedback classes. This is due to the fact that you need an instance reference.

Potential Solution: Use a Game Event system similar to or exactly the same as defined in this video. I have created a controller class for this in the package on Patreon or you could make your own.

There may be more that I haven’t come across yet, but if you keep these in mind, I am positive that you’ll have a great system that is easy to debug, expand, and entertain. Thanks again!


Leave a comment