Designer friendly component scripts with Unity events

Unity recently introduced a serializable UnityEvent class which allows scripts to expose events in the inspector allowing designers to wire up handlers without coding. This is a really powerful feature because it allows developers to implement components that can now be assembled much more freely by designers… almost like a basic form of visual scripting.

Events that have no parameters can be exposed using the ready made UnityEvent class. Here is an example of a MonoBehaviour that exposes an event that is triggered after the specified delay has elapsed:

public sealed class DelayedEvent : MonoBehaviour
{
    [SerializeField]
    private float delayInSeconds = 1f;
    [SerializeField]
    private bool repeat = false;
    [SerializeField]
    private int maximumRepeatCount = 0;

    [SerializeField]
    private UnityEvent triggeredEvent = null;


    public float DelayInSeconds {
        get { return this.delayInSeconds; }
        set { this.delayInSeconds = value; }
    }

    public bool Repeat {
        get { return this.repeat; }
        set { this.repeat = value; }
    }

    public int MaximumRepeatCount {
        get { return this.maximumRepeatCount; }
        set { this.maximumRepeatCount = value; }
    }


    public UnityEvent TriggeredEvent {
        get { return this.triggeredEvent; }
    }


    private void OnEnable()
    {
        this.StartCoroutine(this.StartDelay());
    }

    private void OnTriggered()
    {
        this.TriggeredEvent.Invoke();
    }


    private IEnumerator StartDelay()
    {
        int count = 0;

        do {
            yield return new WaitForSeconds(this.DelayInSeconds);
            this.OnTriggered();

            if (this.MaximumRepeatCount > 0) {
               count += 1;
            }
        }
        while (this.Repeat && count <= this.MaximumRepeatCount);

        this.enabled = false;
    }
}

Alone this component doesn’t seem particularly useful; however, if you implement action components, like the following, then it becomes possible to wire them up using the inspector.

public sealed class SpawnPrefabAction : MonoBehaviour
{
    [SerializeField]
    private GameObject prefab = null;

    [SerializeField]
    private Transform parent = null;
    [SerializeField]
    private bool inheritPosition = true;
    [SerializeField]
    private bool inheritRotation = true;
    [SerializeField]
    private bool inheritScale = true;


    public GameObject Prefab {
        get { return this.prefab; }
        set { this.prefab = value; }
    }


    public Transform Parent {
        get { return this.parent; }
        set { this.parent = value; }
    }

    public bool InheritPosition {
        get { return this.inheritPosition; }
        set { this.inheritPosition = value; }
    }

    public bool InheritRotation {
        get { return this.inheritRotation; }
        set { this.inheritRotation = value; }
    }

    public bool InheritScale {
        get { return this.inheritScale; }
        set { this.inheritScale = value; }
    }


    public void ExecuteAction()
    {
        // In a real-world implementation I would suggest using
        // a pooling solution rather than making new ones each time.
        var instanceTransform = Instantiate(this.prefab, this.Parent).transform;

        if (this.InheritPosition) {
            instanceTransform.position = this.transform.position;
        }
        if (this.InheritRotation) {
            instanceTransform.rotation = this.transform.rotation;
        }
        if (this.InheritScale) {
            instanceTransform.localScale = this.transform.localScale;
        }
    }
}
public sealed class PlaySoundEffectAction : MonoBehaviour
{
    [SerializeField]
    private AudioClip clip = null;
    [SerializeField]
    private float volume = 1f;


    public AudioClip Clip {
        get { return this.clip; }
        set { this.clip = value; }
    }

    public float Volume {
        get { return this.volume; }
        set { this.volume = Mathf.Clamp01(value); }
    }


    public void ExecuteAction()
    {
        AudioSource.PlayClipAtPoint(this.Clip, this.transform.position, this.Volume);
    }
}

Which can then be wired up like demonstrated below using the inspector:

Spawning evil spiders at delayed intervals

Sometimes we need to be able to provide additional information about an event. Unity provides a selection of generic UnityEvent implementations that take varying quantities of custom parameters. Due to limitations with the Unity serialization mechanism it is necessary to create a little boilerplate for each specialization.

The following demonstrates one way to create a component to maintain the health of an actor (any object that can take damage):

[Serializable]
public sealed class HealthDamagedEvent : UnityEvent<HealthComponent, float, object> { }
[Serializable]
public sealed class HealthDestroyedEvent : UnityEvent<HealthComponent, object> { }


public sealed class HealthComponent : MonoBehaviour
{
    [SerializeField]
    private float initialHealth = 100f;

    [SerializeField]
    private HealthDamagedEvent damageTakenEvent = null;
    [SerializeField]
    private HealthDestroyedEvent destroyedEvent = null;


    private float health;


    public float InitialHealth {
        get { return this.initialHealth; }
        set { this.initialHealth = value; }
    }

    public float Health {
        get { return this.health; }
        set { this.health = value; }
    }

    public bool IsAlive {
        get { return this.Health > 0f; }
    }

    public bool IsDead {
        get { return this.Health <= 0f; }
    }

    
    public HealthDamagedEvent DamageTakenEvent {
        get { return this.damageTakenEvent; }
    }
    
    public HealthDestroyedEvent DestroyedEvent {
        get { return this.destroyedEvent; }
    }


    private void Start()
    {
        this.RestoreInitialHealth();
    }


    public void RestoreInitialHealth()
    {
        this.Health = this.InitialHealth;
    }

    public void TakeDamage(float damage, object actuator = null)
    {
        if (this.IsDead) {
            // Already dead! please no more!
            return;
        }

        this.Health = Mathf.Max(0f, this.Health - damage);

        this.DamageTakenEvent.Invoke(this, damage, actuator);
        if (this.IsDead) {
            this.DestroyedEvent.Invoke(this, actuator);
        }
    }
}

For more complex objects it is useful to create empty objects to group related actions together and to workaround the limitation whereby UnityEvent cannot determine which component to invoke when the game object contains multiple components of the same type:

Screenshot of health component with sub-objects to group actions

Finally I’ll demonstrate how you might create a component that inflicts damage upon whatever it collides with. Collision can be controlled using the physics layer matrix feature of Unity although you could obviously add various constraints to your components.

[Serializable]
public sealed class DamagerDamagedTargetEvent : UnityEvent<DamagerComponent, float, HealthComponent> { }
[Serializable]
public sealed class DamagerDestroyedTargetEvent : UnityEvent<DamagerComponent, HealthComponent> { }


public sealed class DamagerComponent : MonoBehaviour
{
    [SerializeField]
    private float damageAmount = 10f;

    [SerializeField]
    private DamagerDamagedTargetEvent damagedTargetEvent = null;
    [SerializeField]
    private DamagerDestroyedTargetEvent destroyedTargetEvent = null;


    public float DamageAmount {
        get { return this.damageAmount; }
        set { this.damageAmount = value; }
    }

    
    public DamagerDamagedTargetEvent DamagedTargetEvent {
        get { return this.damagedTargetEvent; }
    }
    
    public DamagerDestroyedTargetEvent DestroyedTargetEvent {
        get { return this.destroyedTargetEvent; }
    }


    private void OnCollisionEnter2D(Collision2D other) {
        var health = other.gameObject.GetComponent<HealthComponent>();
        if (health != null && health.IsAlive) {
            health.TakeDamage(this.DamageAmount, this.gameObject);

            this.DamagedTargetEvent.Invoke(this, this.DamageAmount, health);
            if (health.IsDead) {
                this.DestroyedTargetEvent.Invoke(this, health);
            }
        }
    }
}

I hope that this post is useful for people that are looking for a lightweight approach for wiring up their components visually. I used this approach in my most recent client project and they said that they found the game’s components flexible and easy to use.