Search code examples
c#unity-game-enginemenuvirtual-reality

Make UI element to follow camera


I would like to make the menu follow the camera so when the user gazes at the menu, the menu could be triggered.

I'm setting the position of the menu with reference to the FirstPersonCamera which is the main camera as follows in my Update function,

menuCanvas.transform.position = FirstPersonCamera.transform.position 
                               +(FirstPersonCamera.transform.forward * 4);

Unfortunately, the menu is sticking to the camera center always and hence it is always triggered. How do I position the contextual menu properly?


Solution

  • You can e.g. use a Vector3.Lerp in order to make the movement smoother (for far distance object moves faster, for shorter distance slower).

    Then you could limit the target position not to the center of the display but allow an offset in each direction. A very simply example might look like

    // how far to stay away fromt he center
    public float offsetRadius = 0.3f;
    public float distanceToHead = 4;
    
    public Camera FirstPersonCamera;
    
    // This is a value between 0 and 1 where
    // 0 object never moves
    // 1 object jumps to targetPosition immediately
    // 0.5 e.g. object is placed in the middle between current and targetPosition every frame
    // you can play around with this in the Inspector
    [Range(0, 1)]
    public float smoothFactor = 0.5f;
    
    private void Update()
    {
        // make the UI always face towards the camera
        transform.rotation = FirstPersonCamera.transform.rotation;
    
        var cameraCenter = FirstPersonCamera.transform.position + FirstPersonCamera.transform.forward * distanceToHead;
    
        var currentPos = transform.position;
        
        // in which direction from the center?
        var direction = currentPos - cameraCenter;
    
        // target is in the same direction but offsetRadius
        // from the center
        var targetPosition = cameraCenter + direction.normalized * offsetRadius;
        
        // finally interpolate towards this position
        transform.position = Vector3.Lerp(currentPos, targetPosition, smoothFactor);
    }
    

    Ofcourse this is far from perfect but I hope it is a good starting point.


    A moe complex/complete solution can e.g. found in the HoloToolkit: SphereBasedTagalong (any version before it was called MixedRealityToolkit 2.x.x)

    // Copyright (c) Microsoft Corporation. All rights reserved.
    // Licensed under the MIT License. See LICENSE in the project root for license information.
    
    using UnityEngine;
    
    namespace HoloToolkit.Unity
    {
        /// <summary>
        /// A Tagalong that stays at a fixed distance from the camera and always
        /// seeks to stay on the edge or inside a sphere that is straight in front of the camera.
        /// </summary>
        public class SphereBasedTagalong : MonoBehaviour
        {
            [Tooltip("Sphere radius.")]
            public float SphereRadius = 1.0f;
    
            [Tooltip("How fast the object will move to the target position.")]
            public float MoveSpeed = 2.0f;
    
            [Tooltip("When moving, use unscaled time. This is useful for games that have a pause mechanism or otherwise adjust the game timescale.")]
            public bool UseUnscaledTime = true;
    
            [Tooltip("Display the sphere in red wireframe for debugging purposes.")]
            public bool DebugDisplaySphere = false;
    
            [Tooltip("Display a small green cube where the target position is.")]
            public bool DebugDisplayTargetPosition = false;
    
            private Vector3 targetPosition;
            private Vector3 optimalPosition;
            private float initialDistanceToCamera;
    
            void Start()
            {
                initialDistanceToCamera = Vector3.Distance(this.transform.position, Camera.main.transform.position);
            }
    
            void Update()
            {
                optimalPosition = Camera.main.transform.position + Camera.main.transform.forward * initialDistanceToCamera;
    
                Vector3 offsetDir = this.transform.position - optimalPosition;
                if (offsetDir.magnitude > SphereRadius)
                {
                    targetPosition = optimalPosition + offsetDir.normalized * SphereRadius;
    
                    float deltaTime = UseUnscaledTime
                        ? Time.unscaledDeltaTime
                        : Time.deltaTime;
    
                    this.transform.position = Vector3.Lerp(this.transform.position, targetPosition, MoveSpeed * deltaTime);
                }
            }
    
            public void OnDrawGizmos()
            {
                if (Application.isPlaying == false) return;
    
                Color oldColor = Gizmos.color;
    
                if (DebugDisplaySphere)
                {
                    Gizmos.color = Color.red;
                    Gizmos.DrawWireSphere(optimalPosition, SphereRadius);
                }
    
                if (DebugDisplayTargetPosition)
                {
                    Gizmos.color = Color.green;
                    Gizmos.DrawCube(targetPosition, new Vector3(0.1f, 0.1f, 0.1f));
                }
    
                Gizmos.color = oldColor;
            }
        }
    }