6
\$\begingroup\$

I'm a relative beginner trying to learn Unity, and I'm trying to do something that I thought would be relatively simple: Have a piece of text hover over an object, following it if it moves around the screen. I understand that a UI element can't be directly attached to an object because it has to exist on a canvas, so I tried writing a simple script that would move the UI text depending on where the object is on the screen.

Here's the code:

using UnityEngine;
using System.Collections;

public class NameFollow : MonoBehaviour {

    public GameObject robot;
    public Camera camera;
    private Transform thisTransform;
    private Vector3 roboPos;

    // Use this for initialization
    void Start () {
        roboPos = robot.transform.position;
        thisTransform = this.transform;
    }

    // Update is called once per frame
    void Update () {
        Vector3 roboScreenPos = camera.WorldToScreenPoint(roboPos);
        Vector2 local = new Vector2();
        RectTransformUtility.ScreenPointToLocalPointInRectangle(this.GetComponent<RectTransform>(), roboScreenPos, camera, out local);
        this.GetComponent<RectTransform>().anchoredPosition = local;
        //thisTransform.position.Set(roboScreenPos.x, roboScreenPos.y + 1f, roboScreenPos.z);
    }
}

However, the text just seems to jump from its original location to what seems to be the anchor point on the canvas and back every frame, so it just flickers between the two locations. It doesn't seem to respond to the target's movement at all.

Can someone point out what mistake I'm making in my code? I'm not familiar enough with Unity UI scripting to figure it out, but conceptually that makes sense to me.

UPDATE

I managed to kind of get there

void Update () {
    Vector3 roboScreenPos = camera.WorldToScreenPoint(robot.transform.TransformPoint(roboPos));
    Vector2 local = new Vector2();
    RectTransformUtility.ScreenPointToLocalPointInRectangle(this.GetComponent<RectTransform>(), roboScreenPos, camera, out local);
Debug.Log("Robopos " + roboScreenPos);
//Debug.Log(this.GetComponent<RectTransform>().anchoredPosition);
    this.GetComponent<RectTransform>().anchoredPosition = local;
    //thisTransform.position.Set(roboScreenPos.x, roboScreenPos.y + 1f, roboScreenPos.z);
}

It seems the jumping between two positions thing is being caused by RectTransformUtility.ScreenPointToLocalPointInRectangle, since roboScreenPos shows a consistent value as long as nothing moves, and changes reasonably when it does move.

Am I using that function the right way?

UPDATE 2

I was pointed to a not very intuitive solution, but it mostly works:

rt = GetComponent<RectTransform>();
canvasRT = GetComponentInParent<Canvas>().GetComponent<RectTransform>();
roboScreenPos = camera.WorldToViewportPoint(robot.transform.TransformPoint(roboPos));
rt.anchorMax = roboScreenPos;
rt.anchorMin = roboScreenPos;

I guess the idea is that if you only give the UI element one place to put itself, it has to put itself in that place. Maybe. Apparently I don't quite understand the way anchoredposition, anchorMax, and anchorMin work.

However it works, the UI text does now follow the character around as intended. The one issue I have now is that if the target object starts at any position that isn't the origin, then the UI text permanently offsets its position by that distance from the target, essentially placing it twice the distance away from the origin in the same direction. I does still move along with the target, but always with that offset.

Here's the current code:

public class NameFollow : MonoBehaviour
{

    public Vector3 pos;

    public GameObject robot;
    public Camera camera;
    private Vector3 roboPos;
    private RectTransform rt;
    private RectTransform canvasRT;
    private Vector3 roboScreenPos;

    // Use this for initialization
    void Start()
    {
        roboPos = robot.transform.position;

        rt = GetComponent<RectTransform>();
        canvasRT = GetComponentInParent<Canvas>().GetComponent<RectTransform>();
        roboScreenPos = camera.WorldToViewportPoint(robot.transform.TransformPoint(roboPos));
        rt.anchoredPosition = roboScreenPos + new Vector3(0,45,0);
        //rt.anchorMax = roboScreenPos;
        //rt.anchorMin = roboScreenPos;
    }

    // Update is called once per frame
    void Update()
    {
        roboScreenPos = camera.WorldToViewportPoint(robot.transform.TransformPoint(roboPos));
        rt.anchorMax = roboScreenPos;
        rt.anchorMin = roboScreenPos;
    }
}

As you can see, I tried setting the position of the element in Start, but that doesn't help. It's not a major issue since it can be easily worked around, but I want to understand what's going on.

\$\endgroup\$
  • \$\begingroup\$ If u want to make the UI text to follow the gameobject then u will have to attach the text inside the camera then where your camera will go to follow the gameobject the UI text will automatically follow that object and keep in mind it should be on canvas. \$\endgroup\$ – Shraddha Patel Jan 6 '16 at 5:56
  • \$\begingroup\$ But that would only work in the case where the camera follows a single object. What if I wanted to have multiple objects on the screen with text following each of them, like having multiple enemies on the screen with their names above each of them? \$\endgroup\$ – ViggyNash Jan 6 '16 at 14:47
  • \$\begingroup\$ I've done stuff like this before, but never using a built-in UI element for my text. I've always done it with 3D MeshText or some other thing attached to a GameObject. The trick was orientating it properly (some trig) and making sure it stayed at a fixed distance from the camera (some vector math). \$\endgroup\$ – Draco18s Jan 6 '16 at 16:32
  • \$\begingroup\$ Does the text need to respond to objects near the camera, or are you trying to make something like a HUD? \$\endgroup\$ – lase Jan 6 '16 at 18:53
  • \$\begingroup\$ @Draco18s UI seems like the way you should do it, but I didn't realize it would be this awkward to do. That's definitely a viable, and maybe more intuitive, solution. \$\endgroup\$ – ViggyNash Jan 6 '16 at 20:40
4
\$\begingroup\$

Based on your description, you might have better luck using a Text Mesh, which will be treated more in line with the other Components that the rest of Unity uses (rather than delving into the Canvas system).

You could create GameObjects with Text Mesh components, and make them children of the enemies, with the y-component of the transform offest so they'd be above them.

You could then attach a simple script to your child GameObject that has something along the lines of:

void Update() {
    this.transform.LookAt(player.transform);
}

... in order to make sure your Text Mesh is always oriented towards your player. While this is different than what your above code, it may be cleaner & easier to understand going forward.

My $0.02 would be that the canvas is more geared towards more static applications & is great at rescaling for aspect ratios, but is less apt for what you're trying to do.

\$\endgroup\$
  • \$\begingroup\$ I think that's ultimately the better way to handle it yeah. But like I said, I still trying to learn Unity and doing these sometimes impractical projects help me learn. \$\endgroup\$ – ViggyNash Jan 6 '16 at 23:30
1
\$\begingroup\$

Upgrading my comment to an answer.

You need to add the canvas to the object and set its render mode to "WorldSpace".

Read more here:

https://docs.unity3d.com/560/Documentation/Manual/HOWTO-UIWorldSpace.html

\$\endgroup\$
  • \$\begingroup\$ Question is old enough that I'm not sure that uGUI was available (I'd have to double check and don't really care). World Space Canvas objects would definitely work though! \$\endgroup\$ – Draco18s Dec 21 '17 at 16:45
  • \$\begingroup\$ I didn't even consider how old the question was. I just saw the concept and thought "...I think I learned how to do that like a week ago, and it definitely wasn't this complex!" \$\endgroup\$ – Ethan The Brave Dec 21 '17 at 18:46
  • \$\begingroup\$ I figured. I just think it might've been that old because I commented on it and must not have been aware of uGui at the time or I would have posted as such! \$\endgroup\$ – Draco18s Dec 21 '17 at 19:07
0
\$\begingroup\$

I agree with lase. Text Mesh is probably what you would want for your solution. If you do need to use a canvas specific item then I have a simple function that will get you where you need to be. You simply pass your target object, canvas, and camera to the function and it will return a position you can use on your text object in the canvas.

Here is that function (C#)

        //Get the screen position of a physical object in world space (i.e. The Robot)
public static Vector3 GetScreenPosition(Transform transform,Canvas canvas,Camera cam)
{
    Vector3 pos;
    float width = canvas.GetComponent<RectTransform> ().sizeDelta.x;
    float height = canvas.GetComponent<RectTransform > ().sizeDelta.y;
    float x = Camera.main.WorldToScreenPoint (transform.position).x / Screen.width;
    float y = Camera.main.WorldToScreenPoint (transform.position).y / Screen.height;
    pos = new Vector3 (width * x - width / 2, y * height - height / 2); 
    return pos;    
}

Here is how to use it...

   text.transform.position = GetScreenPosition(object.transform, canvas, mainCamera);

or if you use RectTransform just modify the left side of the statement to match to position.

\$\endgroup\$
  • \$\begingroup\$ This does not work at all. \$\endgroup\$ – creator Jun 11 '17 at 11:49
0
\$\begingroup\$

I use a Text mesh and this script for player names in my game.

 public class BlendFollower : MonoBehaviour
        {
            public Transform leader;
            public float followSharpness = 1f;
            private Vector3 OffSetVector;

            void LateUpdate()
            {
                OffSetVector = new Vector3(leader.position.x, leader.position.y, leader.position.z);
                transform.position += (OffSetVector - transform.position) * followSharpness;
            }
        }
\$\endgroup\$
0
\$\begingroup\$

Use a TextMesh, as said in the answers above.

And a Parent Constraint Component on the TextMesh, that's 'connected' to the object you desire to mimic the movement of. Offsets are available, and can be animated, within the Parent Constraint Component, too.

It's also got individualised control over which axis and rotations are parented. Making it a quite powerful and useful component.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.