Using UnityEvents in a Game

In this tutorial you'll build a small dungeon room: a pressure plate that opens a door, a health system that drives a health bar and a game-over screen, and a key pickup. The scripts won't reference each other directly. They'll be connected with UnityEvents in the Inspector.

You'll need: a scene with a floor, a player object tagged "Player" with a Collider and Rigidbody (or CharacterController), and a UI Canvas. Basic C# knowledge (classes, methods, fields).

Why events?

The obvious way to make a plate open a door is to give the plate a Door field and call door.Open(). That works, until you want the plate to also turn on a light, play a sound, or spawn enemies. Then you're editing the plate script for every new idea, and it has to know about every other script.

A radio station. The station (the plate) broadcasts "I was pressed!" and doesn't know who's listening. Anyone who tuned in (door, light, speaker) reacts in their own way. Add a new listener and the station never changes. Remove one and nothing breaks.

Scripts that don't know about each other are called decoupled. They're easier to reuse, test and change.

Step 1: The pressure plate (event with no parameters)

Create a flat cube, tick Is Trigger on its Box Collider, and add this script:

using UnityEngine;
using UnityEngine.Events;

public class PressurePlate : MonoBehaviour
{
    [Tooltip("Called when the player steps on.")]
    [SerializeField] private UnityEvent onPressed;

    [Tooltip("Called when the player steps off.")]
    [SerializeField] private UnityEvent onReleased;

    [SerializeField] private bool oneTimeOnly = false;
    private bool used;

    void OnTriggerEnter(Collider other)
    {
        if (!other.CompareTag("Player") || used) return;
        onPressed.Invoke();
        if (oneTimeOnly) used = true;
    }

    void OnTriggerExit(Collider other)
    {
        if (!other.CompareTag("Player") || oneTimeOnly) return;
        onReleased.Invoke();
    }
}

The plate knows nothing about doors. It just announces what happened.

Step 2: The door (the listener)

This uses a coroutine to slide the door. (With DOTween you could do it in one line; see the DOTween tutorial.)

using System.Collections;
using UnityEngine;

public class Door : MonoBehaviour
{
    [SerializeField] private Vector3 openOffset = new Vector3(0f, 3f, 0f);
    [SerializeField] private float duration = 0.5f;

    private Vector3 closedPos;
    private Coroutine moving;

    void Awake() { closedPos = transform.position; }

    // Public + void + no parameters = shows up in the UnityEvent dropdown.
    public void Open()  { MoveTo(closedPos + openOffset); }
    public void Close() { MoveTo(closedPos); }

    private void MoveTo(Vector3 target)
    {
        if (moving != null) StopCoroutine(moving);
        moving = StartCoroutine(Slide(target));
    }

    private IEnumerator Slide(Vector3 target)
    {
        Vector3 start = transform.position;
        for (float t = 0f; t < 1f; t += Time.deltaTime / duration)
        {
            transform.position = Vector3.Lerp(start, target, t);
            yield return null;
        }
        transform.position = target;
    }
}

Step 3: Connect them in the Inspector

  1. Select the pressure plate. You'll see "On Pressed ()" and "On Released ()" boxes, like a Button's "On Click ()".
  2. Under On Pressed, click +. Drag the door GameObject into the empty object slot.
  3. In the function dropdown, choose Door > Open ().
  4. Under On Released, do the same with Door > Close ().
  5. Press Play and walk onto the plate.

Now add more reactions without writing code: click + again under On Pressed, drag in a Light object and pick GameObject > SetActive (bool) with the box ticked. Drag in an AudioSource and pick AudioSource > Play ().

PressurePlateonPressed.Invoke()onPressedUnityEvent1. Door.Open()2. Light: SetActive(true)3. AudioSource.Play()listeners set in the Inspector, called in orderhas no reference tothe door, light or audio
The plate only knows its own event. Which methods run is stored in the Inspector (dashed), so adding a fourth reaction never touches PressurePlate.cs.

What happens at runtime

  1. The player enters the trigger. Unity calls OnTriggerEnter on the plate.
  2. onPressed.Invoke() goes through its list: door.Open(), then light.SetActive(true), then audio.Play(), in the order shown in the Inspector.
  3. All of this happens during the same physics step, before the next Update.

Step 4: Health with a value (UnityEvent<T>)

Events can carry data. Here, health sends out its current fraction (0-1) whenever it changes, so a health bar can listen.

using UnityEngine;
using UnityEngine.Events;

public class Health : MonoBehaviour
{
    [SerializeField] private float maxHealth = 100f;

    [Header("Events")]
    public UnityEvent<float> onHealthChanged; // sends health / maxHealth
    public UnityEvent<float> onDamaged;       // sends the damage amount
    public UnityEvent onDied;

    private float current;
    public bool IsDead => current <= 0f;

    void Start()
    {
        current = maxHealth;
        onHealthChanged.Invoke(1f);
    }

    public void TakeDamage(float amount)
    {
        if (IsDead || amount <= 0f) return;

        current = Mathf.Max(current - amount, 0f);
        onDamaged.Invoke(amount);
        onHealthChanged.Invoke(current / maxHealth);

        if (IsDead) onDied.Invoke();
    }

    public void Heal(float amount)
    {
        if (IsDead) return;
        current = Mathf.Min(current + amount, maxHealth);
        onHealthChanged.Invoke(current / maxHealth);
    }
}

Add this to the player. Then create a UI Slider (GameObject > UI > Slider), set its Min to 0 and Max to 1, and untick Interactable.

On the player's Health component, under On Health Changed (Single), click +, drag in the Slider, and pick Slider > Dynamic float > value.

Dynamic vs. Static"Dynamic float" means "use the value passed to Invoke()". The entries further down the list ("Static Parameters") ignore it and use a value you type in the Inspector. Pick Dynamic here so the bar follows the real health. ("Single" is the .NET name for float.)

Step 5: A game-over screen

using UnityEngine;
using UnityEngine.SceneManagement;

public class GameOverScreen : MonoBehaviour
{
    [SerializeField] private GameObject panel;

    void Awake() { panel.SetActive(false); }

    public void Show()
    {
        panel.SetActive(true);
        Time.timeScale = 0f;
    }

    public void Restart() // hook this to the panel's "Retry" Button OnClick
    {
        Time.timeScale = 1f;
        SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
    }
}

Connect Health > On Died to GameOverScreen > Show (). Also add PlayerMovement > enabled (a bool property, untick the box) so the player can't move after dying.

Step 6: Something that deals damage

using UnityEngine;

public class Spikes : MonoBehaviour
{
    [SerializeField] private float damage = 25f;

    void OnTriggerEnter(Collider other)
    {
        if (other.TryGetComponent(out Health health))
        {
            health.TakeDamage(damage);
        }
    }
}

Notice Spikes only knows about Health. It has no idea there's a health bar, a game-over screen or a hurt sound. You can add all of those later by adding listeners in the Inspector.

Step 7: Listening from code

Sometimes a script should always react, and you don't want to rely on someone wiring it in the Inspector. Use AddListener, and always pair it with RemoveListener.

using UnityEngine;

[RequireComponent(typeof(Health))]
public class HurtFlash : MonoBehaviour
{
    [SerializeField] private Renderer rend;
    private Health health;

    void Awake() { health = GetComponent<Health>(); }

    void OnEnable()  { health.onDamaged.AddListener(Flash); }
    void OnDisable() { health.onDamaged.RemoveListener(Flash); }

    private void Flash(float amount)
    {
        rend.material.color = Color.red;
        CancelInvoke(nameof(ResetColor));
        Invoke(nameof(ResetColor), 0.1f);
    }

    private void ResetColor() { rend.material.color = Color.white; }
}

Bonus: a key and a locked door

The same PressurePlate idea works for pickups. This one makes a reusable "interactable" that fires an event when the player presses E nearby:

using UnityEngine;
using UnityEngine.Events;

public class Interactable : MonoBehaviour
{
    [SerializeField] private UnityEvent onInteract;
    private bool playerInRange;

    void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) playerInRange = true; }
    void OnTriggerExit(Collider other)  { if (other.CompareTag("Player")) playerInRange = false; }

    void Update()
    {
        if (playerInRange && Input.GetKeyDown(KeyCode.E))
        {
            onInteract.Invoke();
        }
    }
}

Put it on a key and connect On Interact to: Door.Open() on the locked door, and GameObject.SetActive(false) on the key itself. On a lever: Door.Open() plus Animator.SetTrigger("Pull"). On a chest: spawn loot. One script, many uses.

When not to use UnityEvents

Summary

See also