5.2 — The Door: Opening It with Events
You have an axe, and the cave door does not care — yet. Today you close the last gap: press E beside the lever, and a door the lever has never met swings open. The lever knows nothing about doors. It flips a sprite and it fires an event, and who is listening is somebody else's business. That gap between cause and effect is called abstraction, and it is the most exam-relevant idea in this whole project.
Press E beside a lever, and a cave door you have never touched disappears - the wall you could feel, the wall you could see, and a panel that has nothing to do with any of it, all switched off at the same instant by one object that only ever flips a sprite.
First, before any code: cut that into parts small enough that you could look each one up on its own.
- Notice one press of a keyInput is a state, not a moment:
Input.GetKey(...)is true on every frame the key is held, andInput.GetKeyDown(...)is true on exactly one frame. An interaction wants the one frame, or a single press becomes sixty.Read: theInput.GetKeyDown(...)row of the quick reference, and theInput.GetMouseButtonDown(0)you wrote for the attack in 1.6. - Decide the player is close enoughA straight-line distance between two positions, compared against a number you chose and put in the Inspector so a designer can change it.Read: the
Vector3.Distancerow of the quick reference, and the range check you wrote on the turret in 3.1. - Find the lever without knowing its nameA scene-wide search for a
Levercomponent —Object.FindFirstObjectByType<Lever>()in Unity 6, which replaces the deprecatedFindObjectOfType<T>(). It hands back the component, not the GameObject, and it hands backnullwhen the scene has no lever in it at all.Read: theGameObject.FindWithTag("Coin")row of the quick reference, and the same search on the turret in 3.1. - Ask the lever to act, without doing its job for itOne call:
lever.Interact(). The lever owns its own state, its own sprite swap and its ownInvoke(); the player contributes a moment in time and nothing else. This is whyInteract()ispublicand almost everything else in that file is not.Read:Assets/Scripts/Lever.csitself —public void Interact(), and the comment above it that already says who calls it. - Let the lever announce that it happened, without naming anyone
onLeverOn.Invoke()runs every method that has subscribed, and the lever holds no reference to a single one of them. That gap is what your exam calls abstraction: a procedure causing other procedures to run without knowing their names.Read: the twoUnityEventfields at the top ofLever.cs, and theOn Lever Onlist onCaveSwitch— three subscribers the lever has never met. - Subscribe to the announcement, and open the door
AddListenerputs your method on the event andRemoveListenertakes it off again; one without the other is how a listener outlives the object that added it. The field isprivate, so the Inspector can subscribe to it and C# cannot.Read:Lever.csagain — the access level ononLeverOn— and the three listeners already sitting onCaveSwitch, which is exactly what that field was leftprivatefor.
Four of those six parts are work you have already done: a key that fires on one frame is 1.6, a distance measured against a number is 3.1, and a search plus a method call is what you have been writing since 2.1. The last two have no precedent anywhere in this course, and they are the reason this lesson is a full walkthrough rather than a guided one. Every part before today had a name you could look up. An event does not: Invoke() is the first line in this project that causes code to run without naming the code, and the only way to learn a mechanism you cannot search for is to be shown the whole of it once.
The test: could you look this part up on its own, right now, in one search? "Open a door with a key" cannot be looked up — there is no such API. "Make a method run when an event fires" can: that is AddListener(...), one search away.
Your turn
The lever is driving three subscribers today and has never met any of them. Say what it would have had to know if it had been written the obvious way — holding a reference to the door and calling a method on it — and say what the event removed from the file.
The event that connects the lever to the door is private. Which of the two roads this lesson builds — the Inspector list, or AddListener(...) in a script — works without editing somebody else's file, and what does the other road cost you?
-
Project window →
Assets/Scripts/Lever.cs— the one script in that folder which is not an empty shell. Read the whole thing; it is short. Two fields at the top areUnityEvents, and the word in front of both isprivate. Then notice what is missing: the word "door" appears nowhere in that file. The lever has no idea what it is switching on. -
Hierarchy → Overworld → CaveSwitch — the object standing in the
scene, not the prefab in the Project window — then
Inspector → On Lever On. Somebody has already wired it up: three
listeners, each one calling
SetActivewith a true/false argument. Those listeners are prefab-instance overrides, so the prefab asset's own lists are still empty — inspecting the prefab and seeing nothing wired is not a bug. -
Inspector → expand the first two listeners: they target
Collision Door RemovedandNo Collision Door Removed. Find both in the Hierarchy and look at them in the Scene view. Together they are the door — one object for the wall you can feel, one for the wall you can see, switched off at the same moment. The door mechanism already works.Inside Lever (Script), the two event lists. A listener is two decisions and an argument: which object, which method, and what that method is handed — and every entry below callsSetActivewithfalse, because the lever's job is to remove things. -
Inspector → expand the third listener: it points at the scene's
Canvas, which nothing in this scene uses yet, with no note explaining why the lever turns it off. That is your first piece of genuine legacy wiring. Then expand On Lever Off: it is empty. (If the list shows five rows rather than three, two are leftover blank entries from when it was made shorter — they target nothing and never fire.) - Game view → press Play, walk to the lever, and press E. Nothing happens. The lever is fully wired to the door, and no line in your controller has ever asked about the E key — that is the half you are about to write.
- Once both walkthroughs are done — Game view → Play, walk to the lever, press E. The wall vanishes: a door somebody else built, opened by the interaction you wrote, with nothing about the door in your code and nothing about your code in the door. That is the whole lesson in one key press.
- Inspector → On Lever On → add a fourth listener of your own choosing — reveal a message, swap a sprite, disable a hazard. Everything that list can call is a public method or property on some component, which is the same constraint your own scripts live under: zero lines of code, and the game has a new rule.
Every row below is a call you will type in the next two walkthroughs. Four are calls you have met in another lesson; two are the new mechanism.
| API | What it gives you | What it costs |
|---|---|---|
Input.GetKeyDown(KeyCode.E) |
True on the single frame the key goes down — the moment rather than
the state, and the keyboard twin of the GetMouseButtonDown behind your
attack in 1.6. |
Choosing the wrong twin is invisible in the code and obvious in the game:
Input.GetKey(KeyCode.E) is true on every frame the key is held, so one
press becomes sixty. |
Object.FindFirstObjectByType<Lever>() |
A scene-wide search that hands back the first Lever component, without
you ever holding a reference to it. The Unity 6 spelling — the old
FindObjectOfType<T>() is deprecated. |
It searches the whole scene: fine once per key press, wasteful once per frame. And it hands back a component, never a GameObject. |
Vector3.Distance(a, b) |
The straight-line distance between two points — the call that decided whether the turret could see you in 3.1, now deciding whether you can reach the lever. | It measures between two positions, so both have to exist. Ask it about a
null reference and the script stops, every frame, pointing at that
line. |
lever.transform |
A component's shortcut to the Transform of the object it is attached to — how a search that returns a component still gives you a position. | Nothing. A component has no position of its own; it always reaches through its own object. |
lever.Interact() — and the word public |
The lever's one offered method. The player contributes a moment in time; the lever does everything else. | It has to be public. A private method is visible only
inside its own class, so the compiler refuses the call outright — error CS0122.
That is the difference between a method that is used and one that is
offered. |
onLeverOn.Invoke()onLeverOn.AddListener(m)onLeverOn.RemoveListener(m) |
The cause, and the two ways to attach an effect. Invoke() runs every
method that has subscribed and names not one of them; AddListener(m)
hands your method over as a value, with no parentheses on it. |
The event accepts only methods that take nothing and return nothing, so the shape of
your handler is not yours to choose. And a listener you never remove outlives its
object — the next Invoke() calls into something that has been
destroyed. |
- The moment. Which input call fires on a single frame, and what does the lever do if you use the one that fires on every frame instead?
- The search. What does the search hand back — a component or a GameObject — and what does it hand back in a scene with no lever in it? What happens on the next line if you do not check for that?
- The reach. What does "close enough" mean numerically, where does that number live so a designer can change it, and which comparison operator did you pick at the boundary?
- The call. Why must
Interact()bepublicfor your line to compile — and what does your own script know about the lever's state after it calls it?
Socratic: Where does the game's knowledge live?
- The lever opens a door it has never met. Which window do you open to teach it about the door — and what does your answer say about where the game's rules actually live?
- If the lever had been written the obvious way — holding a reference to the door and calling
door.Open()directly — what would you have to change to make a second lever open a second door? What does the event version cost instead? - The exam calls a procedure that calls other procedures without knowing their names abstraction. What would the lever have needed to know without UnityEvent, and what complexity does the event remove?
The question this answers“how do I press E beside a lever and make the lever do something?”
You are editing Assets/Scripts/PlayerController.cs. The lever is already wired to the door. What is missing is any way for the player to reach it: press E today and nothing happens, because no line in your controller has ever asked about the E key. The whole interaction is about fifteen lines, and every one is given and explained below.
Step 1Declare how far your reach is
One field, in the class body, beside moveSpeed and the rest: the [SerializeField] private pattern from 1.3 — private in code, a knob in the Inspector.
[SerializeField] private float interactRange = 1.5f;- 1How close counts as near enough, in world units.
1.5fis roughly one player width — a sane first guess for a lever standing on the ground.It has to be a field, not a number typed into the condition. A number written into theifis invisible: "I have to stand exactly on top of the lever" then means editing code and waiting for a recompile. As a serialized field it is a slider, and every playtest becomes a drag instead of a build. The same argument asmoveSpeedin 1.4, and the same as the threshold you tuned on the turret in 3.1.
Step 2Notice the press - once, not sixty times a second
These four lines go at the bottom of the Update() you have been adding to since 1.4, after your movement and animation lines. The method they call is written in step 3, so the file will not compile until step 5 — type all five steps, then build.
if (Input.GetKeyDown(KeyCode.E))
{
TryInteract();
}- 1The new key question.
Inputis the class from 1.4,KeyCode.Enames the key, andGetKeyDownis true on the single frame the key goes down.Input.GetKey(KeyCode.E)is the wrong one here, and it fails in the loudest possible way. Without theDownit is true on every frame the key is held, so half a second of holding E toggles the lever about thirty times and the door opens and shuts until you let go. The lever looks broken; it is not, the condition is. The exact pair you met on the mouse in 1.6. - 3Calls the method you write in step 3. The parentheses are empty because it needs nothing handed to it:
interactRangeis a field, and the lever is found inside the method.A method you write yourself does nothing until something calls it — the rule you met in 1.6 withUpdateAnimation()andCheckAttack(). Leaving this line out is the quietest bug in the lesson: no error, no warning, nothing.
Step 3Find the lever, and survive finding nothing
A method of your own, and its first job is to answer "which lever?". You have never held a reference to it and you never will — it is one object standing somewhere in a scene, and your script has no slot for it.
private void TryInteract()
{
Lever lever = Object.FindFirstObjectByType<Lever>();
if (lever == null)
{
return;
}- 1
private, because nothing outside this script calls it — compareInteract()on the lever, which ispublicfor exactly the opposite reason.The interaction is its own method soUpdate()stays readable as three ideas plus one new one: the same separation you made forUpdateAnimation()in 1.6. - 3Searches the whole scene for the first
Levercomponent and stores it. Two things to read carefully: the type isLever, the component, notGameObject— andFindFirstObjectByTypeis the Unity 6 spelling.Object.FindObjectOfType<Lever>()is the old spelling and Unity 6 marks it deprecated. It still runs and still finds things, which is why the mistake survives: what you get is a Console carrying an obsolete-call warning on every compile, and a line the docs have retired. - 4The guard. The search hands back
null— "nothing at all" — when the scene contains no lever, and this line catches that before anything tries to use it.The Overworld has one lever; the other scenes have none.Start (index 0),Platformer, and any empty test scene you press Play in contain noLeverat all, so the search returnsnullthere every single time. Without this guard the next line throws aNullReferenceExceptioninto the Console every frame E is pressed — in a scene that looks like it has nothing to do with the lever you are working on, which is what makes it take an hour to find. - 6Leaves
TryInteract()immediately. Nothing below runs, andUpdate()carries on as normal — the player still walks, and E simply does nothing.
Step 4Measure the distance, and refuse if you are too far
The same measurement the turret made in 3.1, with two differences: it is between the player and the lever, and the number it is compared against is the field from step 1.
float distance = Vector3.Distance(transform.position, lever.transform.position);
if (distance > interactRange)
{
return;
}- 1The straight-line distance from the player to the lever, as a
float. On the left,transform.positionis yours — this script sits on the Player. On the right it is reached through the component you just found.lever.transformanswers the component-versus-GameObject question. A component has no position of its own; every MonoBehaviour carries atransformproperty pointing at the Transform of the object it is attached to. So the search handing back a component costs you nothing. - 2True when the lever is further away than your reach — the refusal rather than the permission, because that is how the guard below is written.
>decides what happens at the boundary.distance > interactRangerefuses anything past your reach and allows standing exactly at it;>=would refuse the edge itself. Neither is wrong — pick one and know which. The same decision you made about the arrival threshold in 3.1. - 4Leaves
TryInteract()again: you found a lever and decided it is not one you can reach, so there is nothing left to do this frame.
Step 5Tell the lever
One line, and it is the only thing your script ever says to the lever. Everything else about the lever — its state, its sprite, what else should happen — is not your business and never appears in your file.
lever.Interact();
}- 1Calls the lever's own public method. The parentheses are empty because the lever needs nothing from you: it already knows whether it is on or off, and it holds its own sprites.
Interact()has to bepublicor this line will not compile.privatemeans "visible only inside this class", so aprivate Interact()would be refused the moment another script named it — error CS0122. The lever marks it public because the lever expects to be called from outside: that is the difference between a method that is merely used and a method that is offered.
Check your work: the finished filebuild it as you read — then open this to check every line
Compare against your own file rather than copying it. What has to match is the shape of the interaction, not its text: a single-frame key check, a scene search whose result is allowed to be nothing, a distance measured against a serialized number, and one call to Interact(). This version folds both refusals into one && condition, where the walkthrough used two separate early returns — on every frame the two files do the same thing.
// ------------------------------------------------------------------
// REFERENCE FILE — Assets/Scripts/PlayerController.cs
// Written to be read, not pasted. Build it with the walkthrough
// above, one step at a time, then open this and compare.
// If it is sitting in your project unchanged, that is the thing
// to go and do now — the walkthrough is still above you.
// ------------------------------------------------------------------
using UnityEngine;
// PlayerController.cs - the interaction half, complete. Everything you built
// from 1.4 to 2.2 (input, movement, animation, coins, health) stays exactly as
// you wrote it; the two members below are the whole of what is new.
public class PlayerController : MonoBehaviour
{
[SerializeField] private float moveSpeed = 5f; // your number from 2.2
[SerializeField] private float interactRange = 1.5f; // how close is "near enough"
private Rigidbody2D body;
void Start()
{
body = GetComponent<Rigidbody2D>(); // looked up once, per 2.2
}
void Update()
{
// ... the input, movement and animation lines from 1.4, 1.6 and 2.2 ...
if (Input.GetKeyDown(KeyCode.E))
{
TryInteract();
}
}
// One search, one measurement, one call. The search is allowed to come back
// empty, so the two refusals are folded into the same condition - which
// leaves exactly one line in this file that can do anything at all.
private void TryInteract()
{
Lever lever = Object.FindFirstObjectByType<Lever>();
if (lever != null &&
Vector3.Distance(transform.position, lever.transform.position) <= interactRange)
{
lever.Interact();
}
}
}The question this answers“how do I make my own object open the door when the lever is pulled?”
You are editing Assets/Scripts/DoorOpener.cs (plus one line of Lever.cs). The Inspector list on CaveSwitch already opens the cave door — that is the road you just used, and it needs no code and no edits to anybody else's file. This walkthrough builds the same connection in a script of your own, which is the road you take when the thing you want to happen is not already a listener. Both roads end at the same door.
Step 1Open the event up - the one edit to somebody else's file
Read the two lines at the top of Lever.cs again. The events are [SerializeField] private — and that one word, private, is the whole reason the Inspector was the only road available.
// BEFORE: [SerializeField] private UnityEvent onLeverOn;
public UnityEvent onLeverOn;- 1A comment, so you can see what you are changing. Nothing else in
Lever.csmoves today: notInteract(), not the sprite swap, not theInvoke(). - 2The same field with one word changed: delete
privateand the[SerializeField]in front of it, and writepublic.privatemeans only code insideLevermay touch this field — andAddListener(...)IS touching it. YourDoorOpeneris a different class, so the compiler refuses with error CS0122, and there is no way around it from your side.[SerializeField]is what made the field visible in the Inspector while staying private to C#: Unity's serialization can reach a private field and your code cannot, and the Inspector is not code. So the field was reachable by the three listeners somebody wired and by nobody else — and that was almost certainly deliberate. Exposing it says "any script may now subscribe to this", and that sentence is the actual cost. If you also want to subscribe to the switch-off, make the identical change toonLeverOff.
Step 2Declare what this script is listening to
A brand new file in Assets/Scripts/. It has two slots to fill in the Inspector and no other state at all — a listener owns nothing, it only reacts.
[SerializeField] private Lever lever;
[SerializeField] private GameObject door;- 1A slot holding the lever component this door listens to.
Leveris the type name fromLever.cs, so the field can hold nothing else — and an empty slot is a bug you can see before you press Play.A wired reference rather than a scene search. You could callFindFirstObjectByType<Lever>()inStart()here and it would work. The slot is better for this job: a door that listens to a specific lever should say so where a designer can see it. Same argument as the waypoint slots in 4.1 — a reference to a thing in the scene belongs to the scene. - 2The object to switch off when the lever is pulled — the same
Collision Door Removedobject the Inspector listeners target, or one you made yourself.GameObject, notTransform. The call the listeners make lives on the GameObject, so the slot has to be able to hold one. ATransformslot would give youdoor.gameObject.SetActive(false)and one extra hop for nothing.
Step 3Subscribe in Start()
This is the line that makes the whole mechanism work: your method is handed to the lever's event, and from now on the lever calls it whether it knows your name or not.
void Start()
{
lever.onLeverOn.AddListener(OpenDoor);
}- 1The once-only setup method from 1.3 — the same one that finds your body and seeds your memory in every other script you own. Here it does the subscribing.Once, and only once. Put the subscription in
Update()instead and you add a new listener every frame: after one second the lever has sixty subscribers, and one pull calls your method sixty times. It still looks like it works, because switching a door off twice is the same as switching it off once — which is what makes that bug so easy to ship. - 3Adds
OpenDoorto the lever's event. Read it left to right: the lever, its event field, the method to add. Notice there is no dot betweenAddListenerandOpenDoor—OpenDooris not being called here, it is being handed over as a value.This is the whole point of the lesson in one line. The lever stores a reference to your method and calls it later. It does not check what the method does, where it lives, or what the object is for:OpenDoorcould equally start music or count a coin, andLever.cswould not change by a character. That is what your exam means by abstraction, and it is what the three listeners onCaveSwitchare doing already.
Step 4Write the method that actually opens the door
A plain method with an empty parameter list — and the emptiness is not an accident. It has to fit the shape the event is expecting: a method that takes nothing and returns nothing.
private void OpenDoor()
{
door.SetActive(false);
}- 1
private, because nothing outside this script calls it — the lever calls it through the subscription, and the subscription was set up in here. No parameter and no return value, which is the only shapeonLeverOncan accept.An event fixes the shape of the methods it will call.AddListener(...)wants a method that takes no arguments and hands nothing back, soOpenDoor(int n)or anint OpenDoor()would be refused at compile time. That constraint is the price of the lever being able to call you without knowing anything about you. - 3The one thing this script does: switch the door object off, the same call the Inspector listeners make.
falsemeans inactive — the object still exists, it is simply not in the world any more.This is the line the Inspector list has been running all along. Open one of the three listeners onCaveSwitchand you will see the same call, with the samefalse, wired to a different object. You have not built anything new here; you have built the same thing somewhere you can read it, which is the point of the exercise.
Step 5Unsubscribe in OnDestroy()
The line everybody forgets. Start() put your method on the lever's event, and the lever will keep calling it for as long as the event exists — whether or not the object that subscribed is still there.
private void OnDestroy()
{
lever.onLeverOn.RemoveListener(OpenDoor);
}- 1Another lifecycle method, like
Start()andUpdate()— but Unity calls this one when the object is about to be destroyed. Scene changes,Destroy(gameObject), and the end of Play mode all land here.The pair toStart(). Every subscription has a matching unsubscription, and Unity runs the two at the two ends of an object's life. Once you know to look for the pattern you will see it everywhere: whoever adds a listener is responsible for removing it. - 3Takes
OpenDoorback off the event. Same three pieces as theAddListener(...)line, with the same method, in the same order.A listener that outlives its object is a call into something that is no longer there. The event is holding a reference to your object, and if you are destroyed while still subscribed, the next pull of the lever calls a method on a destroyed object and Unity throws aMissingReferenceException. You will meet this properly in 5.3, when objects start outliving scenes. This two-line pair is the smallest version of being careful.
Check your work: the finished filebuild it as you read — then open this to check every line
Compare against your own file rather than copying it. Three things carry the lesson, and all three have to match: the access level you changed on onLeverOn, the subscription in Start(), and the unsubscription in OnDestroy(). This version keeps the method in a UnityAction field and hands the same value to both calls, where the walkthrough passed the method group straight into each one — the walkthrough's form is correct and shorter, and this is the form you reach for once there are several methods to remove.
// ------------------------------------------------------------------
// REFERENCE FILE — Assets/Scripts/DoorOpener.cs (plus one line of Lever.cs)
// Written to be read, not pasted. Build it with the walkthrough
// above, one step at a time, then open this and compare.
// If it is sitting in your project unchanged, that is the thing
// to go and do now — the walkthrough is still above you.
// ------------------------------------------------------------------
using UnityEngine;
using UnityEngine.Events;
// DoorOpener.cs - this script's whole job is to be listening when the lever is
// pulled. It never touches Lever.cs at runtime: the lever calls OpenDoor without
// knowing this file exists, and it would call anything else the same way.
public class DoorOpener : MonoBehaviour
{
[SerializeField] private Lever lever; // the lever this door listens to
[SerializeField] private GameObject door; // the thing that disappears
// The same method, kept as a value. AddListener and RemoveListener have to
// be handed the SAME delegate, and holding it in a field is what makes the
// removal exact rather than hopeful - and it is the only way once there is
// more than one method to take off.
private UnityAction onLeverPulled;
private void Start()
{
onLeverPulled = OpenDoor;
lever.onLeverOn.AddListener(onLeverPulled);
}
private void OnDestroy()
{
// The pair to Start(). A listener that outlives its object is a call
// into something that is no longer there.
lever.onLeverOn.RemoveListener(onLeverPulled);
}
private void OpenDoor()
{
door.SetActive(false);
}
}Checkpoint — before moving on, you must be able to:
- Explain a UnityEvent as cause-and-effect with a deliberate gap: the lever never names the door
- Wire a listener in the Inspector, and change the game's rules with no code at all
- Explain why
Interact()must be public — and whyonLeverOnbeing private is the one line of somebody else's file this lesson changes, and what changing it costs - Say what a listener that outlives the object that added it does, and where you take it off
- Commit and push
Reference Docs
Need a deeper explanation? These pages cover the same concepts:
- Unit 5: The Axe & The Cave — levers, events and scene switching
- Unity + C# Quick Reference — input, distance, search APIs and object lifetime
- 5.1 — The Axe: Item Pickups & Inventory Bools — the capability the door is about to start caring about
Next: 5.3 — Into the Cave: Scenes, Persistence & The Refactor, where you leave the overworld and everything you have stored breaks — including the listener you wrote today.
Saved on this device only — the Dashboard tracks your progress.