Unity Tutorial: Making a 3D Compass
6 December 2022A video version of this tutorial is available here.
I recently wanted to create a handheld compass for a VR game that I am working on. This seemingly simple goal took me a day of fiddling around with rotations and quaternions, until I found a solution that was simple and satisfactory enough. Here, I share what I learned so that you can easily implement similar functionality in your games and apps. Let’s see how our compass works:
To start, we first create a simple compass object consisting of a compass body and a needle. The Compass Body object is a simple cylinder primitive that is squashed along the y-axis to create a flat disc.
Next, we create a simple needle which is an elongated cube that is aligned parallel to the compass body, outward from the center. This Needle will rotate along the y-axis of the compass body, to point towards our target direction.
To make rotating the needle easier, we first create an empty GameObject called Needle Parent as a child of the compass body, and reset its position and rotation so that it is perfectly aligned with the compass body. Needle is then added as a child under Needle Parent.
Tip: When creating your compass, pay attention to the rotation of each GameObject. It can be helpful to switch Unity’s Tool Handle Rotation setting to Local, and ensure that their rotations match the ones shown in this tutorial. This tutorial is written with the assumption that Needle is aligned towards the positive z-axis when creating the compass.
To make the needle point towards our target, we will write a script to rotate Needle Parent along the y-axis. This script will be attached to Needle Parent.
We first need to specify a Target that our compass will point towards. This Target can be a GameObject, a point, or a specific direction (e.g. Vector3.Forward
). In our case, we use a sphere GameObject in the scene that is referenced as targetObject
. We first obtain the position of targetObject
:
Vector3 target = targetObject.transform.position;
If we want to use a general direction instead of a GameObject, the target position can be obtained by adding the desired direction vector to the transform of Needle Parent:
Vector3 target = transform.position + Vector3.forward; // (to make needle point north)
The target position that we obtained is in the World space. To correctly determine the needle rotation even when the compass body itself is rotated, we need the obtain the position of our Target in the Local space of our compass. So, we convert the World space coordinates of Target to the Local space coordinates of our compass body (which is the parent of the Needle Parent GameObject that the script is attached to):
Vector3 relativeTarget = transform.parent.InverseTransformPoint(target);
Once we have the relativeTarget
coordinates, we can determine how much the needle should rotate along the y-axis so that it points towards Target. We use the Mathf.Atan2
function to calculate the needle rotation. Atan2(x, z)
calculates the angle between the ray from the origin to the point (x, z)
and the z-axis, as shown in the figure below. This is how much our needle should rotate.
Since relativeTarget
is in the Local space, the origin in the context of Atan2
is the origin of the compass body, not of the world itself. Also, we do not use the y coordinate of relativeTarget
since our needle is flat on the body of our compass, and it does not show any information about the y elevation of our target. Since Atan2
gives us an angle in radians, we convert it to degrees and obtain our needleRotation
as follows:
float needleRotation = Mathf.Atan2(relativeTarget.x, relativeTarget.z) * Mathf.Rad2Deg;
Now, all we need to do is to apply this needleRotation
to Needle Parent. We apply a localRotation
to our transform, passing needleRotation
as the y rotation and keeping the x and z rotations as zero:
transform.localRotation = Quaternion.Euler(0, needleRotation, 0);
Our final script looks like this:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PointCompass : MonoBehaviour
{
public GameObject targetObject;
void Update()
{
// get worldspace coordinates of target
Vector3 target = targetObject.transform.position;
// Vector3 target = transform.position + Vector3.forward; // (to make needle point north)
// convert to local coordinate space of compass body
Vector3 relativeTarget = transform.parent.InverseTransformPoint(target);
// determine needle rotation with atan2
float needleRotation = Mathf.Atan2(relativeTarget.x, relativeTarget.z) * Mathf.Rad2Deg;
// apply needle rotation
transform.localRotation = Quaternion.Euler(0, needleRotation, 0);
}
}
That’s it! The compass should now always point towards the Target. The code above is the minimum required to make the compass work, but you can modify it to make it even better. For example, you could make needleRotation
change smoothly with a specified speed by using Mathf.Lerp
. Hope it helped!