2
\$\begingroup\$

I am working on my first Tower Defense game and I got stuck with architecture. I found some past Q&A "Should the entity handle his own movement?", read about Entity Component approach, and decided to stick with it.

I want to have different scripts that are responsible for different behaviors. For example:

  • if a unit contains an IEnemy script then it will follow predefined path through the level.

  • If it has an IDamageable script it can receive damage.

  • If it has a Health script then the damage will affect health.

In order to create friendly unit all I have to do is throw away the IEnemy script and add the IFriendlyUnit script.

In order to do that, I decided to have some storage and a manager of that storage. The manager will have methods like Heal(healthPoints), RemoveArmorPoints(armorPoints), AddArmorPoints(armorPoints), DecreaseHealthBy(damage).

But the problem is that If I want to have a Damageable script which has only one method ReceiveDamage() then it needs to have a reference to the StateManager in order to inflict damage and it breaks the design because it forces me to add the StateManager script to the GameObject in order to remove some health points and I cannot easily add an independent Damageable script to the object.

I want to decouple my code as much as possible and stick to SOLID principles, but seems like I am either over engineering or choosing the wrong way.

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

One great tool for decoupling components in Unity are events.

The high-level idea of events is that one component says "Something happened with me" and other components can choose to subscribe to such events and then react on them. The component which posted the event does not need to know who listens to the event it invokes (or if anyone listens at all). And the components reacting to them do not need to know which component invoked the event. That means you have components which communicate with each other without even knowing about their existence.

Unity actually has several event architectures. I will present two here, the classic message broadcast events and the much newer UnityEvents.

Messages

One is the classic method SendMessage("OnDamage", damageAmount). It's part of the class Component, so you can call it from any script. This method looks at all components of the same gameObject for any methods called "OnDamage" and calls those methods passing arbitrary data as an argument. The advantage of this technique is that it is very easy to implement. But it has two major drawbacks:

  1. It is limited to components on the same gameObject, so you can't use it for communication between different objects. OK, there are also BroadcastMessage that calls the methods on the object or any children, and SendMessageUpward that calls it on its parents. But both require knowledge of your object hierarchy. That's not really loose coupling. And you can't use it to send events to objects on another branch of the scene graph at all.
  2. The method names are "stringly typed". When you misspell the method name (for example "onDamage" instead of "OnDamage"), then nothing is going to tell you. The code just runs and nothing happens. Which is a symptom that can be caused by all kinds of causes. Which can be really frustrating to debug.

UnityEvents

A far better event technique, though, are Unity Events. For example, when you have a component which should be allowed to post damage events, then you simply add a new field of type UnityEvent to the class. Here is an example of such a script. This object continuously loses HP and invokes events which allows others to listen to that:

public class Damageable : MonoBehaviour
{
    public float hp;
    public float maxHp = 10;

    public UnityEvent<float, float> damagedEvent;

    void Start()
    {
        hp = maxHp;
    }

    void Update()
    {
        hp -= Time.deltaTime;
        if (hp <= 0f) Destroy(gameObject);
        damagedEvent.Invoke(hp, maxHp);
    }
}

When you now look at the inspector of a gameObject with that component, then you will see that you have a new widget as you might know it from buttons in the input system:

Damageable Inspector

You can now add any methods from any components of any gameObject to it (as long as the signature matches). For example, here is a component that controls a health bar:

public class HealthBar : MonoBehaviour
{
    private Image myImage;
    void Start()
    {
        myImage = GetComponent<Image>();
    }

    public void UpdateBar(float current, float max) {
        myImage.fillAmount = current / max;
    }
}

Note that it does not reference the Damageable script. But it has a method "UpdateBar" which takes two floats, just like the UnityEvent. That means we can use the inspector to bind this method to the event of the damageable object. Just go to the inspector of the event, click the "+" icon, assign the health bar to the "Object" field and select the function "HealthBar->UpdateBar" in the function dropdown. It should now look like this:

Damageable Inspector 2

Now when the damageable invokes the damagedEvent, the health bar gets updated. Without the health bar knowing that the Damageable exists, or the Damageable knowing about the health bar. That's loose coupling!

You can also set up such event subscriptions at runtime. For example, when you have a different component on the same gameObject which should always react to the damageEvent of the Damageable component, you would do this:

void Start() {
    GetComponent<Damageable>().damagedEvent.AddListener(OnDamage)
} 

private void OnDamage(float current, float max) {
    Console.Log($"Ouch, I am down to {current} hp!");
}
\$\endgroup\$
6
  • \$\begingroup\$ Hi Philip, sorry for such a late response. I am using events in my projects, but correct me if I a wrong, you need to be aware of the event. Like there is next relation event -> some script that triggers the event -> script that subscribes for the event. Like Gun.script(calls OnGunFired) and GunFired.script(Listens for GunFired) So they both need access to OnGunFiredEvent and we need to add it as a component. \$\endgroup\$ Commented Oct 31, 2022 at 9:22
  • \$\begingroup\$ I guess I just not understand Component-based concept correctly, seems like it is not possible to add behavior to an entity simply by adding few new scripts and not modifying others \$\endgroup\$ Commented Oct 31, 2022 at 9:31
  • 1
    \$\begingroup\$ @TarasFityo UnityEvent variables are usually not components of their own but parts of the component which triggers the event (although it is of course possible to have them on separate components. Anyone can .Invoke an event, not just the owning object). Listener relationships are usually set up either in the inspector at design time or at runtime. The latter does of course require that you get a reference to the triggering object somehow. Which is why setting up event listeners in the inspector is preferable for true loose coupling. \$\endgroup\$
    – Philipp
    Commented Oct 31, 2022 at 11:14
  • \$\begingroup\$ @TarasFityo For example, let's say you have a Gun MonoBehaviour which has a UnityEvent fired. You want to play a sound effect when that event triggers. So you add an AudioSource (which can be on the same object but can also be on any other one), go to the inspector of the Gun component, click the "+" on the fired UnityEvent, drag the AudioSource object into the object field and select the "AudioSource->Play" method in the function dropdown. The gun doesn't know it triggers an audio source and the audio source does not know it gets triggered by a gun. That's loose coupling. \$\endgroup\$
    – Philipp
    Commented Oct 31, 2022 at 11:24
  • \$\begingroup\$ @TarasFityo I added a better description with complete sourcecode and some screenshots which should make it more clear how to set up event subscriptions via inspector. If you need anything else to make that answer acceptable, please let me know. \$\endgroup\$
    – Philipp
    Commented Oct 31, 2022 at 11:40

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .