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...
Answer by UmairEm · Jul 12, 2016 at 04:10 PM
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
Answer by ekcoh · Jul 13, 2016 at 10:56 AM
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
Restart Unity and open the project
Click Run in the Editor Click the
Click the button twice
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.
This might be included in newer version of Unity, I never tested it with later version. But I am pretty sure that this didn't work for older versions. Can you explain why object is created by thread 2 at first (and what time is it being called. I couldn't get it) and then is destroyed by a different thread 3 and they created again by thread 1? Why 3 different threads? However implementing IDisposable
interface for such scenarios is a great idea.
I can only guess, the initial call by thread 2 have to do with the inner workings of Unity (which I do not know), but probably Unity wants to inspect the default values of the class or check that it can be instantiated at load time (This is when open/load the project in the editor - Not when running)
$$anonymous$$y guess for the destruction by thread 3 is also some kind of editor thread that has been spawned to destroy all objects that live in the editor before spawning the "runnable" version of the application on thread 1 (main thread).
Based on other investigations it seems like you cannot use the IDisposable interface for much "real work" in Unity since the C# lifetime management and disposal do not seem to be used on the component level when running (during play mode, component create/destroy, gameObject container create/destroy etc). Not from what I have seen at least.... but it was a quick and not so thorough investigation. $$anonymous$$ight come back to it though :)
Regarding your (@UmairEm) comment about version I do not know if this is something new or not, but maybe it is. For your reference I tested this in 5.3.5.
Your answer
Follow this Question
Related Questions
Working with UI as prefab, will change values outside the prefab 0 Answers
What is the best way to implement a more responsive custom drag event with Scroll Rect? 0 Answers
Unable to modify game object in scene 0 Answers
[Solved] Scroll not working when elements inside have click events 3 Answers
Undesirable call of Toggle's "On Value Changed" when "isOn" is changed through script 2 Answers