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
- Select the pressure plate. You'll see "On Pressed ()" and "On Released ()" boxes, like a Button's "On Click ()".
- Under On Pressed, click +. Drag the door GameObject into the empty object slot.
- In the function dropdown, choose Door > Open ().
- Under On Released, do the same with Door > Close ().
- 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 ().
PressurePlate.cs.What happens at runtime
- The player enters the trigger. Unity calls
OnTriggerEnteron the plate. onPressed.Invoke()goes through its list:door.Open(), thenlight.SetActive(true), thenaudio.Play(), in the order shown in the Inspector.- 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.
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
- Things that fire every frame (e.g. "position changed"). UnityEvents are slower than C# events or a direct call.
- Core systems that must always be connected. An Inspector wire can be forgotten or broken by renaming a method. Connect those in code, or use a C#
event. - Long chains where event A triggers B triggers C. It becomes hard to follow what causes what. Keep chains short.
Summary
- Declare
[SerializeField] private UnityEvent onSomething;and callonSomething.Invoke(). - Use
UnityEvent<T>to send a value; pick the "Dynamic" option in the Inspector to receive it. - Listener methods must be
publicwith 0 or 1 simple parameter. - From code:
AddListenerinOnEnable,RemoveListenerinOnDisable.
See also
- UnityEvent reference
- ScriptableObjects in a Game (event channels across scenes)
This page is a simplified, AI-assisted interpretation. For the complete and authoritative reference, see the official Unity documentation.