In this short tutorial, we are going to implement a visual tracker around objects that are pointed to with the mouse cursor. The tracker is an UI element, ie. it’s in the 2D UI canvas space. It draws a scaled rectangle around the objects and adds icons and texts beside that can hold status or symbols.
In terms of game object hierarchy, the entire construct is built like this:
The “UITrackerElement” remains always active in order to receive update events. On this object is attached the script that does the magic. The sub-elements are adapted by the script. The “RectangleImage” is a 9-slice sprite (“Assets/UI Target Selection Box Sprite.psd“) that gets resized to surround the 3D object. The other elements are placed on the right-hand side of the rectangle.
The entire problem resides in finding the rectangle that does visually surround the 3D object when mapping it onto the 2D canvas. The idea behind the logic implemented below is to simply project all points of the 3D bounds to the 2D surface and then take the most outer points in each direction. In the Unity context, “Bounds” are approximations made by Unity about the objects location and its extents, so all word is done for you by the engine. Next, adding a bit of padding distance et voilà…
Note that the way the coordinates are set in the below script only works correctly if anchor points are set to 0 – that’s why it’s enforced in the “Start()” method. Relative position of texts and icons can be modified using the Rect Transform X,Y coordinates, which are conserved.
Lastest version of source code: https://github.com/imifos/unity3d-workspace/UITargetTrackerOnCursor.
Assets/UITargetTracker.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class UITargetTracker : MonoBehaviour { // Defines how much % the tracker rectangle is enlarged compared to the 3D screen object bounds public float rectanglePaddingMultiplier=0.1f; // Here, we get the current cursor selection from private ScreenPointerToObjectSelectionMapper selectionManager; // // void Start() { selectionManager = FindObjectOfType<ScreenPointerToObjectSelectionMapper>(); // Set anchor and pivot points of main and child objects to overwrite bad configuration the user made in the inspector. // The Y size of sub-elements other than the rectangle is taken from the inspector and not modified. RectTransform rt = this.GetComponent<RectTransform>(); rt.position = Vector2.zero; // left bottom corner rt.anchorMax = Vector2.zero; rt.anchorMin = Vector2.zero; rt.pivot = Vector2.zero; foreach (Transform child in transform) { child.gameObject.SetActive(false); rt = child.GetComponent<RectTransform>(); rt.anchorMax = Vector2.zero; // left bottom rt.anchorMin = Vector2.zero; rt.pivot = Vector2.zero; } } // // void Update() { if (selectionManager.selectedObject != null) { // New tracker rectangle size Rect visualRect = TargetObjectBoundsToScreenSpace(selectionManager.selectedObject.GetComponentInChildren<Renderer>()); // Parent UI component position on the target object visual 2D UI position (anchor is 0,0) RectTransform rt = GetComponent<RectTransform>(); rt.position = new Vector2(visualRect.xMax, visualRect.yMin); // Adjust all child elements. The tracker rectangle image to the size of the visual 2D bounds and // the other object statically on the side, attacked to the rectangle foreach (Transform child in transform) { // For testing, to see if it's working if (child.name == "Text1") child.GetComponent<Text>().text = selectionManager.selectedObject.name; // Make all child elements visible // The parent UI components itself is always active, otherwise it does not receive update events child.gameObject.SetActive(true); float xPadding = visualRect.width * rectanglePaddingMultiplier; float yPadding = visualRect.height * rectanglePaddingMultiplier; if (child.name == "RectangleImage") { // Resize the tracker sprite rectangle rt = child.GetComponent<RectTransform>(); rt.position = new Vector2(visualRect.xMin-xPadding, visualRect.yMin-yPadding); rt.sizeDelta = new Vector2(visualRect.width+xPadding*2, visualRect.height+yPadding*2); } else { // Reposition the other objects (texts and sprites) beside the tracker rectangle rt = child.GetComponent<RectTransform>(); rt.position = new Vector2(visualRect.xMin+ visualRect.width + xPadding , rt.position.y); } } } else { // No selection, no tracker display foreach (Transform child in transform) child.gameObject.SetActive(false); } } /* * Project the 3D scene object towards the 2D UI canvas, then take the most left, the most right, the most up and the most * down 2D points of all to determine the outer visual bounds on the UI overlay of the object. This is the size of the * rectangle "visually" surrounding the object when drawn on the UI plane. */ private Rect TargetObjectBoundsToScreenSpace(Renderer r) { // Object visual rectangle in world space Bounds b = r.bounds; // Calculate the screen space rectangle surrounding the 3D object Camera c = Camera.main; Rect rect = Rect.zero; Vector3 screenSpacePoint; screenSpacePoint = c.WorldToScreenPoint(new Vector3(b.center.x + b.extents.x, b.center.y + b.extents.y, b.center.z + b.extents.z)); AdjustRect(ref rect, screenSpacePoint, true); screenSpacePoint = c.WorldToScreenPoint(new Vector3(b.center.x + b.extents.x, b.center.y + b.extents.y, b.center.z - b.extents.z)); AdjustRect(ref rect, screenSpacePoint); screenSpacePoint = c.WorldToScreenPoint(new Vector3(b.center.x + b.extents.x, b.center.y - b.extents.y, b.center.z + b.extents.z)); AdjustRect(ref rect, screenSpacePoint); screenSpacePoint = c.WorldToScreenPoint(new Vector3(b.center.x + b.extents.x, b.center.y - b.extents.y, b.center.z - b.extents.z)); AdjustRect(ref rect, screenSpacePoint); screenSpacePoint = c.WorldToScreenPoint(new Vector3(b.center.x - b.extents.x, b.center.y + b.extents.y, b.center.z + b.extents.z)); AdjustRect(ref rect, screenSpacePoint); screenSpacePoint = c.WorldToScreenPoint(new Vector3(b.center.x - b.extents.x, b.center.y + b.extents.y, b.center.z - b.extents.z)); AdjustRect(ref rect, screenSpacePoint); screenSpacePoint = c.WorldToScreenPoint(new Vector3(b.center.x - b.extents.x, b.center.y - b.extents.y, b.center.z + b.extents.z)); AdjustRect(ref rect, screenSpacePoint); screenSpacePoint = c.WorldToScreenPoint(new Vector3(b.center.x - b.extents.x, b.center.y - b.extents.y, b.center.z - b.extents.z)); AdjustRect(ref rect, screenSpacePoint); return rect; } private void AdjustRect(ref Rect rect,Vector3 pnt,bool firstCall=false) { if (firstCall) { rect.xMin=pnt.x; rect.yMin = pnt.y; rect.xMax = pnt.x; rect.yMax = pnt.y; } else { rect.xMin = Mathf.Min(rect.xMin, pnt.x); rect.yMin = Mathf.Min(rect.yMin, pnt.y); rect.xMax = Mathf.Max(rect.xMax, pnt.x); rect.yMax = Mathf.Max(rect.yMax, pnt.y); } } }