How to reference the Canvas
Been going crazy here after one hour of googling and reading all I could find on this forum about the subject. I'm instantiating a Text object from a script on a prefab, and in order to be visible the Text has to be parented to the Canvas object in the scene. But in my script, I can find no way of getting a reference to the Canvas (short of using GameObject.Find which I don't want to use). No public variable allows dragging the Canvas from the scene onto the public field (be it Canvas, GameObject, Transform, or RectTransform). What am I doing wrong?
using UnityEngine;
using UnityEngine.UI;
public class DisplayPointsAtScreenPosition : MonoBehaviour {
public Canvas canvas;
public Text UITextObject;
public void DisplayPoints(string text)
{
Text t = Instantiate(UITextObject);
t.transform.SetParent(canvas.transform, false);
t.transform.position = Camera.main.WorldToScreenPoint(gameObject.transform.position);
t.text = text;
Destroy(t, 1.0f);
}
}
Ah..."the prefab." There's your problem. Those can't store references to anything in a Scene. Search for Qs on this for more.
I have, like I wrote above. Does this mean that if I want to reference the Canvas then I have to instantiate it (and my whole UI) from code? It's possible of course, but it seems like it beats the point of having a visual editor in the first place.
Answer by ElijahShadbolt · Feb 04, 2018 at 08:41 PM
Prefabs cannot store references to scene objects, only other prefabs or assets. When you instantiate the Text object, you must assign scene-specific references then.
Option 1: Tags
Give your canvas GameObject the tag "MainCanvas".
When initializing your Text object
canvas = GameObject.FindGameObjectWithTag("MainCanvas").GetComponent<Canvas>();
Option 2: Singleton Class
Keep all of your scene references in one class instance in the scene.
public class ReferenceHub : MonoBehaviour {
// singleton static reference
private static ReferenceHub _Instance;
public static ReferenceHub Instance {
get {
if (_Instance == null) {
_Instance = FindObjectOfType<ReferenceHub>();
if (_Instance == null)
Debug.LogError("There is no ReferenceHub in the scene!");
}
return _Instance;
}
}
// assign this in the inspector
[SerializeField]
private Canvas _mainCanvas;
public Canvas MainCanvas { get { return _mainCanvas; } }
// _mainCanvas field is private with a public getter property to ensure it is read-only.
}
When initializing your Text object
canvas = ReferenceHub.Instance.MainCanvas;
Thank you. Like I wrote, I was hoping to avoid using finding by strings (like FindGameObjectWithTag), because I am instantiating Text objects at runtime (whenever an enemy is hit, a score value is displayed at its transform position), and from what I've read this is an expensive call to make. But I guess that if I cache it in the class instantiating the Text object then I should be O$$anonymous$$. The singleton approach seems like a lot for a rather simple purpose, but thanks a lot for both ideas.
EDIT: Actually, I remember now why I didn't want to use "Find" methods. The gameObject I'm intending to put this component on is itself instantiated and destroyed regularly (not every second, but still). And I'd really like to avoid slow method calls if I can. I've already manage to work around the problem with a completely different approach (UI$$anonymous$$anager with event subscription and an interface). I was just hoping to convert this into a component so I could just plug it where I need it without implementing the interface every time. Anyway, thanks again.
I'm glad I could assist you in finding a solution that best meets your needs!
@Cresspresso 's answer is really good, tagging and singleton are the best approaches to this. $$anonymous$$y question is though: why do you destroy and create the Canvas all the time? Why not just create a Canvas once, name it something meaningful ("DamageUI"?), and then leave it in the scene? $$anonymous$$eaning, is there a reason to destroy it? Or if there is, could you circumvent that reason in a way that the whole system works cleaner?
I suggest that the Canvas element stays in the scene and is not destroyed at any point. Text elements instantiated as children of the canvas would work fine, and is a good technique for stuff like enemy markers and health bars. You may never know how many enemies will be in the scene, so you will need to make more markers at runtime. I never said to destroy or replace the canvas, just that when the Text elements are instantiated, you should fix their null reference to the canvas in the scene.
Answer by Hamburgert · Feb 04, 2018 at 11:58 AM
It may be easier to work with the canvas transform. Rather use:
public RectTransform canvas;
Now you can drag-n-drop the canvas in the editor, and it will automatically now reference the transform. Remember to make a change in your DisplayPoints from canvas.transform to just canvas.
public void DisplayPoints(string text)
{
Text t = Instantiate(UITextObject);
t.transform.SetParent(canvas, false); // Also change this
t.transform.position = Camera.main.WorldToScreenPoint(gameObject.transform.position);
t.text = text;
Destroy(t, 1.0f);
}
An alternate way of doing it may be to add the Text at the correct position in the editor, then disable it. In your script you can then simply to t.gameObject.SetActive(true) instead of instantiating.
Thanks. I'm sorry but I've tried that a zillion times, and one more time after reading your post. The prefab clearly shows a field called "Canvas" with "None (Rect Transform)" in it. I can drag the Canvas object from the hierarchy as much as I want, it won't register. It's like dragging a a sound clip into a GameObject field.
Prefabs cannot have references to scene objects, and this prefab is instantiated during the game, so you cannot "drag and drop in the Editor".
Prefabs are, however, fully capable of referencing parts of themselves. Of course, if the Canvas is not part of the prefab, then that is where I misunderstood the question.
You are right, prefabs can reference themselves, so if the prefab has a Canvas on it, then it can be referenced (dragged onto the Text component). However, if I understood it correctly, even the Canvas is instantiated run-time, so I think it is not part of the prefab.
Your answer
Follow this Question
Related Questions
Possible for 10 canvas in 1 scene? 1 Answer
Dark UI canvas isssue 0 Answers
Need help with shield bar enable and disable. 0 Answers
Hard crash when enabling/disabling UI elements 4 Answers
How to find an UI button by name? 2 Answers