I am making a space game where I have a box which follows a ship around the screen.
The box is following the ship fine, however the box is a set size and when the ship is far away the box looks huge compared to the ship, when it is close the box looks tiny. Here is an example.
What i’d like to be able to do is have the bracket resize and reshape as it is tracking the ship, so the box could turn into a rectangle shape etc.
I thought about having a canvas and anchoring a bracket piece to each corner of the canvas, so when the canvas resizes the brackets stay in the corner, and that effect works, but I just don’t know how to get the canvas to resize to match the ship size.
Here is a video of the same effect in the game Freespace2
From what I understand the problem comes from the depth of the image (the further it is, the smaller it is). I specified it because this won’t work if you change the scale of the objet.
You can use a canvas set into World Space rendering mode in front of the ship. The canvas has to be a child object of the ship, so it will follow it all around the world. Then, the local position of the canvas will need to be set in a way that the normal of the canvas plane is colinear with the line between the camera position and the ship.
That way, the box will always be centered around the ship, it will follow the ship around and as it is in world space, will also be subject to the depth factor that makes the ship look small.
I did something similar recently. Here’s what I did:
First create a Quad as a child of the ship. Scale it so it’s the appropriate amount larger than the ship. Then make a script for it that billboards it towards the camera in LateUpdate so that it always appears as a full rectangle in the eye of the camera. Get that working and test it out with different angles etc.
Next, in your UI, under your root canvas create an empty GameObject that is size 0, 0 and is sitting in the bottom left corner of the screen:
Then, create a Panel as a child of the empty object and set up its rectTransform like this:
Then attach a script to it. The gist is we will get the screen space coordinates of the corners of the Quad attached to the ship (as a Rect). Then we’ll apply that rect as the size and position of our panel:
var points = quad.GetComponent<MeshFilter>().mesh.vertices;
// Convert the point to world space
for (int i = 0; i < points.Length; i++) {
points[i] = quad.transform.TransformPoint(points[i]);
}
Vector2[] screenPoints = new Vector2[points.Length];
for (int i = 0; i < points.Length; i++) {
screenPoints[i] = cam.WorldToScreenPoint(points[i]);
}
var min = screenPoints[0];
var max = screenPoints[0];
// Find the bottom left and top right coordinates
foreach (var v in screenPoints) {
min = Vector2.Min(min, v);
max = Vector2.Max(max, v);
}
return new Rect(min, max - min);
Then finally you can reposition and resize your panel:
Rect rect = <get the rect from the other code>
RectTransform rt = GetComponent<RectTransform>();
Rt.anchoredPosition = rect.position;
Rt.sizeDelta = rect.size;
Thank you for your reply. Can I just be clear as to what is happening?
We have a quad on the ship in 3D space and we are using that to set the size of the UI panel?
I’ve got this part working and the billboarding is working and the panel is resizing based on the depth of the ship, so I just need to position it correctly and add in the box graphic.
However there is still one thing that I cannot do, it’s not essential but I think it looks cool so would love to get it working.
See in this first screenshot where I’ve set the quad size to fit the ship? Is it possible to get the quad to stretch across the ship when the ship rotates? So in the second pic the ship has rotated and is now much wider on the screen, however the quad is still a box.
Is there a way to stretch the quad to encompass the ship when this happens?
I’m looking into using the bounds of the ship mesh to do it. Maybe get the bounds then set the size of the bounds to the size of the quad?
Thanks again everyone for your help, it’s much appreciated.
Yeah that’s correct. Basically you’re using the points on the quad with “Camera.WorldToScreenPoint” to determine where the corners of the quad are in screen space, so you get a nice way to position your UI panel. Naturally when you get it working you will turn off the MeshRenderer on the Quad so only your UI element is actually visible
Ok I see your ships can be very oblong. In this case it might make sense to do something a little different than using a quad since your ship’s shape changes a lot on screen when it rotates. The quad makes more sense if the ship is more of a cube or sphere shape.
In your case we can try using your ship’s renderer bounds instead! To do this we can get rid of the quad you created before. Instead, we will get screen coordinates based on the renderer bounds:
public static Rect GUIRectWithObject(Camera cam, GameObject ship)
{
var renderer = ship.GetComponent<Renderer>();
Vector3 cen = renderer.bounds.center;
Vector3 ext = renderer.bounds.extents;
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);
}
Then you can use that Rect to align your UI graphic. If it doesn’t look quite right you could also sort of go back to the Quad method, but use a 3D Cube mesh instead (scaled to your preference), which will have 8 vertices as well like the renderer, but you’ll have more control over its shape.
Brilliant, thank you.
As soon as I replied to you I looked into other methods and came across the ship renderer bounds.
I then tested the code I found on this Unity page https://docs.unity3d.com/ScriptReference/Renderer-bounds.html which shows me how to draw a gizmo according to the bounds of the mesh.
The problem I ran into is that, unfortunately, my ships are pieced together like Lego, with many different smaller meshes placed together to make the ship, so it is not one large mesh.
The only way I can think of to overcome this is to create a simple cube, scale it to the size of the ship, then turn the renderer off but use the bounds of that cube mesh with your solution.
I will report back once I’ve implemented it. Thanks again Much appreciated.
Sounds like a good plan! I’d love to see it when you get it working!
Also I was thinking about this a little bit. If you go with the 3d scaled cube, you may need to modify the algorithm a little bit. Rather than just finding the smallest and largest Vector2 screen points, we may actually want to get the smallest and largest x and y coordinates we run into period. For example if we have a cube like this:
We’ll end up with a rectangle like this on the screen:
And what we want is this:
So maybe the code for building the rect changes from this:
Vector2 min = extentPoints[0];
Vector2 max = extentPoints[0];
foreach(Vector2 v in extentPoints)
{
min = Vector2.Min(min, v);
max = Vector2.Max(max, v);
}
to this:
Vector2 min = extentPoints[0];
Vector2 max = extentPoints[0];
foreach(Vector2 v in extentPoints)
{
min = new Vector2(Mathf.min(min.x, v.x), Mathf.min(min.y, v.y);
max = new Vector2(Mathf.max(max.x, v.x), Mathf.max(max.y, v.y);
}
Yes the invisible cube concept is working exactly how I wanted. The blue AABB are encompassing the ship perfectly.
The small blue cube was what I was referring to above where each piece is a separate mesh.
The green cubes can be ignored as they are part of the game Almost there I believe.
It does work, but the AABB doesn’t look like it’s the solution, take these 2 screenshots.
Notice how the box is smaller even though the cub ship is turned and taking up more space?
Seems that because it’s Axis Aligned it’s not the best solution.
I think what I should be doing is using the vertices of the cube, using the verticies at the bottom left and top right to resize the plane?
I used your original quad code on the cube and it seems to be resizing to the vertices perfectly.
I just need to change the graphics so I’ve got individual bracket images anchored to the 4 corners, instead of this ugly stretching image, and then position it over the ship.
Here is a screenshot of how it currently is.
I changed a line in your code, I changed the rt.anchoredPosition to rt.position. After doing this the box then followed the ship around the screen, however, as can be seen in the attached screenshot, the box is off centre from the cube.
The purple is the original image and the blue box are 4 individual images I’ve added to the same rect and anchored to the corners.
So it’s all looking great and the blue box is doing what I want it to do, it’s just being off centre that’s the problem now.
Rect rect = TestCube2();
RectTransform rt = GetComponent<RectTransform>();
//rt.anchoredPosition = rect.position;
rt.position = rect.position;
rt.sizeDelta = rect.size;
I’ve deleted my last 2 pics because I had the wrong pics on there.
This one is very interesting.
it shows the bottom left of the cube lines up exactly with the bottom left of the panel, as it should.
But the top right of the panel is far outside of the top right of the cube.
Interesting. What does your code look like for grabbing the world space coordinates for the cube vertices ( if you drag gizmos for them do they line up with the cube?)
public Rect TestCube2()
{
var points = cube.GetComponent<MeshFilter>().mesh.vertices;
// Convert the point to world space
for (int i = 0; i < points.Length; i++)
{
points[i] = cube.transform.TransformPoint(points[i]);
}
Vector2[] screenPoints = new Vector2[points.Length];
for (int i = 0; i < points.Length; i++)
{
screenPoints[i] = Camera.main.WorldToScreenPoint(points[i]);
}
var min = screenPoints[0];
var max = screenPoints[0];
// Find the bottom left and top right coordinates
foreach (var v in screenPoints)
{
min = Vector2.Min(min, v);
max = Vector2.Max(max, v);
}
//return new Rect(min, max - min);
return new Rect(min, max - min);
}
Interesting however that the code to get the verticies from the cube finds 24 points, that can’t be right? The cube should have 8 verticies?
Sorry, drag which gizmos?
If I resize the cube the plane will resize but still show a gap at the top right, the gap scales with the cube size.
That may actually be correct. Each vertex has a normal vector for rendering purposes. In order to render nice clean edges, the cube probably has repeated vertices at each corner so that they can have different normal vectors at those vertices for each face. Each face is essentially its own quad, so 4 * 6 = 24. If this becomes a performance issue later you can create your own 1x1 cube mesh that doesn’t have repeated corner vertices, since we won’t be rendering our cube it won’t matter if the normals aren’t pretty.
As for the points not lining up, do either of these apply?
Do you have any scaling in your UI hierachy?
Are you using multiple cameras or a camera with a viewport that doesn’t fill the whole screen?