UI event with non-instantiated Prefab as event handler. What happens?

I wonder if someone knows what happens internally when I select a Prefab from the Asset folder as an event handler for a UI event, e.g. Slider.OnValueChanged() → MyPrefab.DoIt(). I noticed that static methods on a script attached to that Prefab cannot be invoked (not considered valid handler methods which is a pity). However public instance methods seems to be considered event handler methods and are invoked, but I wonder how this is handled behind the scenes by Unity? Is an instance of the prefab created by Unity to handle the callback? Is this done once or on every callback (which would be very bad)? It is difficult to see since there is no new object created that is visible in the Hierarchy window when the callback happens.

I thought I ask here before digging through the UI source code to see if I can figure it out from there…

To handle any kind of UI event, handler method must be in loaded in scene. So registering event handler to a prefab object will not count. And it won’t instantiate new object of prefab automatically either. Similarly static methods are not allowed because they don’t require instance of class to be called. However you can call a static method from with in an instance method of your script. If it is difficult for you to understand, just take example of a script sitting in Assets folder, it won’t run its update() or start() methods unless it is attached to some object (present in scene). Hope it helps

Thanks for the reply @UmairEm. I do not think you are correct though based on some further investigation I decided to do by setting up a scene as follows.


Test setup

First I created a prefab “MyPrefab” that is not in the scene and attached MyScript.cs as a MonoBehavior as below to capture potential callbacks and object management in the log:

using UnityEngine;
using System;
using System.Threading;

public class MyScript : MonoBehaviour, IDisposable {
	private bool disposed = false;

	public MyScript() {
		Log(Type() + "()", false); // Not on main thread
	}

	~MyScript()
	{
		Log("~" + Type() + "()", false); // Not on main thread
		Dispose(false);
	}
		
	public void Dispose()
	{ 
		Log(Type() + ".Dispose()", false); // Not on main thread
		Dispose(true);
		GC.SuppressFinalize(this);           
	}

	public void Handle() {
		Log(GetType() + ".Handle()", true);
	}
		
	protected virtual void Dispose(bool disposing)
	{
		Log(Type() + ".Dispose(" + disposing + ")", false); // Not on main thread
		if (disposed) return; 
		disposed = true;
	}

	private string Type() {
		return GetType().ToString();
	}

	private void Log(string prefix, bool includeGameObject) {
		string msg = prefix + ", thread=" + 
			Thread.CurrentThread.ManagedThreadId;
		// Check flag, if not on main thread this would crash:
		// "get_gameObject can only be called on main thread"
		if (includeGameObject) {
			msg +=  ": gameObject:" + gameObject.name + 
				", instanceID=" + gameObject.GetInstanceID();
		}
		Debug.Log(msg);
	}
}

Hence, the only objects I have in the scene is the default “Main Camera”, a single “Canvas” with a single button as only child and a “EventSystem” instance. So I definitely do not have any object in the scene.


Test Procedure

  1. Restart Unity and open the project
  2. Click Run in the Editor Click the
  3. Click the button twice
  4. Register output written to the log window

Test 1 - No event handler attached

This results in no output during project/scene load.

This result in no output after play button have been pressed.

This results in no output when button is pressed two times.


Test 2 - OnClick of the button bound to MyPrefabs MyScript.Handle() via "Select Object - Assets instead of Select Object - Scene

This results in the following output during project/scene load:

MyScript(), thread=2

This result in the following output after play button have been pressed:

~MyScript(), thread=3
MyScript.Dispose(False), thread=3
MyScript(), thread=1

This results in the following output when button have been pressed two times:

MyScript.Handle(), thread=1: gameObject:MyPrefab, instanceID=7174
MyScript.Handle(), thread=1: gameObject:MyPrefab, instanceID=7174

Conclusion (so far)

Test 1 is pretty trivial, but I still wanted to investigate the object management when the Prefab wasn’t referenced from the Scene. In this case we get no output as one would expect.

In Test 2, we see that the prefab instance is instantiated during scene load. I guess Unity does this via reflection (default constructor) in order to be able to fetch default values of serialized properties or similar. Directly when the application is launched (enter play mode) we see that the object created at load time is destroyed by another thread. The object is then instantiated directly again (on the main thread) by Unity. I guess this is because Unity has determined that the prefab is referenced by the button UI callback (scene reference). However, this instance is not visible in the hierarchy but sure is allocated in memory. As we can see when the button is pressed, the callback works and uses the same instance to handle the event based on the object ID.

I guess this means that:

  • Unity automatically detects referenced prefabs (non-instantiated assets) and instantiates a hidden copy on scene load if the prefab is referenced.
  • Unity supports callbacks to these hidden object instances which might be beneficial if you have no interest in having the instance as part of the scene and rely purely on run-time interaction with the prefab instance.

If something above seems incorrect or someone can shed more light on these question marks. Please respond to this thread.