Fixed position for enemies health bar in Screen Space - Overlay

Hello!

I’m using Canvas with Screen Space Overlay mode in a top-down game to display a health bar above the enemies, using the following code:

public RectTransform canvasRect;
public RectTransform healthBar;
public Transform enemyToFollow;
public Vector3 offset;

void Update()
{
    Vector3 finalPos = enemyToFollow.position + offset;
    Vector2 screenPoint = RectTransformUtility.WorldToScreenPoint(Camera.main, finalPos);
    healthBar.anchoredPosition = screenPoint - canvasRect.sizeDelta / 2.0f;
}

The problem is the health bar is displaced up and down when enemies are moving (sometimes overlap their head), instead of keeping always in a fixed position when they moves along them. Something like Starcraft does with unit health bars, or any kind of RTS game.

I also tried with World Space mode but still I’m having the same problem.

Any ideas?

Thanks! :slight_smile:

A more visual description of my problem:

In this case, the floating text “Username” overlap the cube when moving down. I’m trying to keep the “Username” floating text always fixed along the cube position.

Any idea?

Well it seems to be properly following the cubes position, it’s just that since it’s 3d, the perspective of the camera makes the cubes position visually different in 2d as the cube moves around. So I think you’d have to do some kind of adjustment of the height offset based on the distance of the cube from the camera. Or its angle plus distance, or other trig stuff.

@DWilliams is right; I’m guessing this wouldn’t happen with an orthographic camera. Using the distance might work, though I’d probably try a couple of different ways just to see what feels best to you for a 3D perspective setting. One thing you might try is handle the offset in screenspace:

public Vector2 offset; //Vector2 for screenspace

void Update()
{
    Vector3 finalPos = enemyToFollow.position; //obsolete now, but kept for clarity
    Vector2 screenPoint = RectTransformUtility.WorldToScreenPoint(Camera.main, finalPos) + offset; //handle offset here; (needs to be negative now iirc)
    healthBar.anchoredPosition = screenPoint - canvasRect.sizeDelta / 2.0f;
}

This way the offset will always be constant in the screenspace Y direction, and the “3D height”/ almost parallex-scroll-like effect will be a little more gone.

There will still be some perceived scrolling though, as the distance (origin → farthest edge) will still change based on where the cube is due to perspective. If you want to totally remove this while still using a perspective camera, I think your best bet is to obtain the cube’s on-screen rect and place the text like so:

public Vector2 offset; //Vector2 for screenspace

void Update()
{
    Rect screenRect = CustomGetScreenRect(enemyToFollow); //AABB bounding box
    Vector2 screenPoint = new Vector2(screenRect.center.x, screenRect.yMin) + offset;
    healthBar.anchoredPosition = screenPoint - canvasRect.sizeDelta / 2.0f;
}

There is no built-in way to get an object’s screen rect, so you’ll have to implement CustomGetScreenRect yourself. However, there appear to be some existing ones here.

Hope this helped a bit; good luck!

Thank you guys, that clarified a lot of things! :slight_smile:

@Senshi : I tried both methods, the first one perceive this scrolling effect you commented, but on the second I also perceive the same scrolling effect. I’m trying to completely remove this effect using the bounds of an enemy CapsuleCollider:

public RectTransform canvasRect;
public RectTransform healthBar;
public Vector3 offset;
public CapsuleCollider enemyToFollow;

public Rect CustomGetScreenRect()
{
    Bounds mBounds = enemyToFollow.bounds;

    Vector3 cen = mBounds.center;
    Vector3 ext = mBounds.extents;
    Camera cam = Camera.main;

    Vector2[] extentPoints = new Vector2[8]
    {
        cam.WorldToScreenPoint(new Vector3(cen.x-ext.x, cen.y-ext.y, cen.z-ext.z)),
        cam.WorldToScreenPoint(new Vector3(cen.x+ext.x, cen.y-ext.y, cen.z-ext.z)),
        cam.WorldToScreenPoint(new Vector3(cen.x-ext.x, cen.y-ext.y, cen.z+ext.z)),
        cam.WorldToScreenPoint(new Vector3(cen.x+ext.x, cen.y-ext.y, cen.z+ext.z)),
          
        cam.WorldToScreenPoint(new Vector3(cen.x-ext.x, cen.y+ext.y, cen.z-ext.z)),
        cam.WorldToScreenPoint(new Vector3(cen.x+ext.x, cen.y+ext.y, cen.z-ext.z)),
        cam.WorldToScreenPoint(new Vector3(cen.x-ext.x, cen.y+ext.y, cen.z+ext.z)),
        cam.WorldToScreenPoint(new Vector3(cen.x+ext.x, cen.y+ext.y, cen.z+ext.z))
    };
      
    Vector2 min = extentPoints[0];
    Vector2 max = extentPoints[0];
      
    foreach(Vector2 v in extentPoints)
    {
        min = Vector2.Min(min, v);
        max = Vector2.Max(max, v);
    }
      
    return new Rect(min.x, min.y, max.x-min.x, max.y-min.y);
}

void LateUpdate()
{
    Rect screenRect = CustomGetScreenRect();
    Vector2 screenPoint = new Vector2(screenRect.center.x, screenRect.yMin) + new Vector2(offset.x, offset.y);
    healthBar.anchoredPosition = screenPoint - canvasRect.sizeDelta / 2.0f;  
}

Unfortunately it seems still does not remove the “scroll-effect”. Is there something I’m missing?

Ok, this had me stumped for a bit. It seemed as it the bounds were somehow wrong, but the code seemed to be right. However, using 3D bounds to get the onscreen bounds is where things go wrong. I used OnGUI() to draw a box around the calculated bounds, which seemed very off. But then I added a 3D bounds cube using OnDrawGizmos(). A picture says a thousand words, so here we go:

To be honest I’m not quite sure how to best proceed. Raycasting is way too expensive for something like this, so that’s out of the question.

And you can’t just convert (center + height) to screen-space either, because the center-top part of the capsule isn’t nescesarily the “highest” point in the screen-view.

I wonder if there is some shader hack you could use (i.e.: by setting and subsequently reading a Material property), but that seems very hacky and potentially costly, though I’m not too sure on the latter.

If you only have a couple of vertices you might get away with just min-maxing for all of those, instead of just the bounding box’s inside CustomGetScreenRect() (which was only an illustrative name btw; something like just GetScreenRect() is probably better. ;)). You could then maybe even optimize by doing something like “only min-max the top 9 vertices (index #0-8)”. Of course this doesn’t work for more complex shapes and can quickly get out of hand.

I may give this another look later, but for now I’m pretty much out of ideas.

Tl;dr: I have no idea! Perhaps there’s some secret GetLastFillRect() method I’ve missed, but I’m sure it can be done. The question is just… how?

EDIT: Here’s the newly added debug code I used:

void OnDrawGizmos()
{
    Gizmos.color = new Color(1f, 0f, 0f, 0.4f);
    Gizmos.DrawCube(enemyToFollow.transform.position + enemyToFollow.center, enemyToFollow.bounds.size);
}

void OnGUI()
{
    //OnGUI() uses inverted y-axis; invert rect before drawing
    Rect r = CustomGetScreenRect();
    r.y = Camera.main.pixelHeight - r.y;
    r.height = -r.height;
    GUI.Box(r, "");
}

EDIT 2: Forgot to elaborate on why using 3D bounds is failing. While we get a close approximation, the bounding cube still suffers from perspective distortion, so the scrolling effect is just shifted to that upper “quad” (plane/ ceiling) in this case. The reason min-maxing vertices directly should work (forgot to test this, sorry!) is because you’re directly sampling the relevant points. Sorry for my in-hindsight-terrible idea of using bounding boxes!

EDIT 3: Using only the vertices indeed works, though the HP bar still shifts. The GUI.Box draws correctly though, so I’m guessing this is just an anchoredPosition quirk. I’ve never moved UI elements this way, so I’ll just leave it there for now. :wink:

1 Like

Uhm, weird. So, this task can’t be done with the new Unity UI ?

It doesn’t have anything to do with the UI implementation, but everything to do with how to get the proper position. Some “easy” “solutions” would be to use either an orthographic camera or 2D sprites instead of 3D models, though this obviously puts some restrictions on your game. There might also be some trig tricks I don’t know about, and of course there’s always the option of “just” dealing with the scrolling, though that too might not fit within your game’s visual style. And as I pointed out in EDIT 3, iterating over some verices directly works, so you might also use some kind of simplified mesh just for that, though again I’m not sure on the performance overhead.

I understand, I really appreciate your help @Senshi , your posts were lot of instructive for me and I learned a lot from them, so thank you very much. :slight_smile:

I can live with a bit of scrolling by setting the Transform to follow the head of enemies instead of the enemy transform per se, but would be great to find a correct and/or “more easy” solution for this issue if it’s possible, so if anyone have any idea do not hesitate to post here.

PD: sorry for my “bad english”, I’m not native english speaker! :smiley:

1 Like