Align compass needle to compass surface

Updated on June 14, 2019 in [A] Unity Scripting
Share on Facebook0Tweet about this on TwitterShare on Google+0Share on Reddit0
19 on May 22, 2019

Hello everyone,

now I have a little prolem I couldn’t fix entirely.

I have a compass. This compass has a needle which will always show into the direction of the nearest exit. The problem here is, that the compass needle rotates in all 3 dimension. I want that the needle only rotates in the compass local plane, so parallel to it’s local surface. My compass needle should not stick out of the compass, that’s weird.

I have the following method for this:

private void RotateTowardsNearestExit()
    {
        Vector3 dir = GetNearestExitPosition() - pointer.transform.position;
        Vector3 pointerDir = Vector3.ProjectOnPlane(dir, transform.up);
        pointer.transform.forward = pointerDir;
    }

First I get the direction, where the pointer (compass needle) has to look at. Then I used the method “ProjectOnPlane” so that this direction will be set on the local surface of the compass. I used therefore the transform.up to get the normal vector of the compass, which is needed for “ProjectOnPlane”. At last I say that the pointers forward direction, in which it shows, is this pointerDir.

It sounds perfectly, but it does not work properly. If the compass is not parallel to the X-Z world plane, the needle still tries to rotate out of the compass. Here a picture:

 

  • Liked by
  • Mouledoux
Reply
3 on May 22, 2019

try setting the forward to the pointers position + the normalized direction.

IDK, whats going on inside the ProjectOnPlane function, but this is how you should assign the forward of an object if you are doing so.

private void RotateTowardsNearestExit()
 {
     Vector3 dir = GetNearestExitPosition() - pointer.transform.position;
     Vector3 pointerDir = Vector3.ProjectOnPlane(dir, transform.up);
     pointer.transform.forward = pointer.transform.position + pointerDir.normalized;
 }
Helpful
on May 22, 2019

Unfortunately, that does it even worse. The needle sticks now completly out, like if I had not done the “ProjectOnPlane” method.

 

But about your forward vector. I don’t think that it is neccessary to take the position and add the direction to it. The direction itself was always enough for me in all games I created so far. It’s because you set the blue arrow of this object in this direction, independent of it’s current position. So like the object is the origin (0,0,0)

 

I also tried it with something like this:

Vector3 dir = GetNearestExitPosition() - pointer.transform.position;
dir.y = 0;
pointer.transform.rotation = Quaternion.Slerp(pointer.transform.rotation, Quaternion.LookRotation(dir), Time.deltaTime * rotationSpeed);

 

But with it I also couldn’t stop the needle from rotating around it’s local X and Z axis. I only want that it rotates around it’s local Y axis.

Wise
on May 22, 2019

Could I see the ProjectOnPlane function please?

Helpful
on May 22, 2019

ProjectOnPlane is an Unity function in Vector3.

Show more replies
  • Liked by
Reply
Cancel
10 on May 22, 2019

Actually, try this first:

Vector3 pointerPos = pointer.transform.position;
Vector3 exitPos = GetNearestExitPosition();
pointerPos. y = exitPos.y = 0f;
Vector3 dir = exitPos - pointerPos;
pointer.transform.forward = pointer.transform.worldToLocalMatrix.MultiplyVector(dir);

 

see what that does for you.

Helpful
on May 22, 2019

It stays now in the X-Z world plane and unfortunately not in the X-Z local plane. It also glitches very fast between two orthogonal states. One of them in the exit direction.

Wise
on May 22, 2019

Hmmmmm…. I know I’ve done something like this before. Gimmie a bit, and I’ll post it here if you still need it.

Helpful
on May 23, 2019

Yes. I will take whatever you have for me.

 

Wise
on May 23, 2019

Ok, here’s what I got:

public class Compass : MonoBehaviour
{
     public Transform needle;
     public Transform target;
 
     void Update()
     {
          Vector3 relativePos = target.position - transform.position;
          relativePos.y = 0f;
          Quaternion rotation = Quaternion.LookRotation(relativePos, Vector3.up);
          needle.rotation = transform.rotation * rotation;
     }
}

 

The only thing with this, is that it doesn’t take the compass rotation into account for getting the direction. So what that means, is if the target is directly behind you, the needle will be at the bottom of the compass, even if the compass is upside-down.

 

Lemme know how this works for you. =)

Helpful
on May 23, 2019

Another nice trick, but this also does not work as intended. The pointer does not rotate at all now. It seems that the rotation of the main compass body, which is done by picking up and dragging around, will be negated with your code. This results into, that the pointer shows always in the east direction of the compass (not the world). But it should point always to the nearest exit, of course.

 

The compass started in north direction, so your code make it stick to the east. Perhaps this picture of the parent structure helps you.

 

Wise
on May 23, 2019

That is odd, because for me, the needle at least moves.

This is what it looks like on my end.

Helpful
on May 24, 2019

That looks great. I try a few variations of your code and see what I can do.

 

Nevertheless, I saw that you only have a sphere as a pointer for the compass, which is displaced from the center. Try adding a cube which is connected from the center to the sphere by stretching it and look if this cube rotates awkwardly.

 

This is basically what my first code did. The direction is fine, but it rotates weirdly around the blue axis.

 

Helpful
on May 24, 2019

Hey I found something. I came up with another approach based on your last code. It looks like this:

 

Vector3 dir = GetNearestExitPosition() - pointer.transform.position;
Vector3 pointerForward = pointer.transform.forward;
dir.y = pointerForward.y = 0f;
float angle = -Vector3.SignedAngle(dir, pointerForward, transform.up);
Quaternion endRotation = pointer.transform.localRotation * Quaternion.Euler(0, angle, 0);
pointer.transform.localRotation = Quaternion.Slerp(pointer.transform.localRotation, endRotation, Time.deltaTime * rotationSpeed);

 

So what it actually do is:

  • get the direction where it has to look and where it looks at the moment
  • set the y value to 0, so that we are working on the X-Z plane
  • calculate now the angle between both directions
  • calculate the end rotation by adding the angle only to the y component to the local rotation of the pointer
  • do the transition smoothly with the slerp method

 

It is nearly perfect, but if the compass stands, so is orthogonal to the X-Z world plane, the arrow is about +/-45° off. I can live with this compromise, except you find a better solution.

Wise
on May 24, 2019

I’ll see what I can do, and for mine in the video, the sphere isn’t the needle. There’s an empty game object at the center, and the sphere is just moved forward a little bit. stretching a cube (or any model really) might distort with the rotation.

Helpful
on May 24, 2019

Yeah I know. So basically this hierachy:

  1. Compass
  2. Key Point/Center (empty GO, where we rotate with script)
  3. 1. Needle Top (your sphere) + 2. Needle Body (the cube I tell you, stretched from center to needle top)

It’s because, with only a sphere as a pointer, you don’t see if the pointer does a weird rotation around it’s forward axis.

Show more replies
  • Liked by
Reply
Cancel
2 on May 24, 2019

Another idea came into my mind.

Imagine that the compass script creates at start e.g. 100 points in 360° around the compass surface on the x-z plane.

Then calculate for each point the distance to the nearest exit and hold only the point with the lowest distance.

After that you have a point on the compass surface which is the closest to the exit. So the only thing left is, that the needle is pointing to that point. Basically set it’s forward vector to that point. Done.

But I think there could be two “problems”:

  • we only have a discretized and not continuous number of possible directions (should be no problem if the number of points is high enough)
  • the time to compute this every frame is higher the more points and objects with that script we have (does it really have a noticeable performance impact though? let’s test)

What do you think?

Wise
on May 24, 2019

I was trying to avoid doing it like this, but try this, and lemme know:

 needle.LookAt(target);
 Vector3 eulers = needle.localEulerAngles;
 eulers.x = eulers.z = 0f;
 needle.localEulerAngles = eulers;

 

Edit: this seems to have trouble with elevations, but if you just adjust it so they have the same y it should work better.

 

Edit 2: I also have an off screen target indicator I made on here a while back. I bet you or I could adjust it to work in 3d space instead of just UI.

heres a link to it if you wanna try something:

https://github.com/Mouledoux/ConnectedHome/blob/master/Assets/Scripts/TargetIndicator.cs

Helpful
on May 24, 2019

Haha so simple and it works perfectly. Nicely done! A slerp is not possible this way, because with LookAt you fix the pointer every frame in target direction, but I take it. It looks “smooth” enough if I carry the compass around.

 

Your TargetIndicator script looks good too. But I don’t think that we need that much code for my problem. Your other small code is good enough. It works pretty well.

 

 

Show more replies
  • Liked by
Reply
Cancel
0 on June 14, 2019

I want to add a line here, because I made the pointer now smoothly.

I just added an empty GameObject to the same parent as the pointer. This is my dummyPointer. This dummyPointer has the same transform values as the real pointer. Then I only did the following in code:

        //dummy pointer rotates instantly to target
        dummyPointer.transform.LookAt(GetNearestTargetPosition());
        Vector3 eulers = dummyPointer.transform.localEulerAngles;
        eulers.x = eulers.z = 0f;
        dummyPointer.transform.localEulerAngles = eulers;
        //real pointer rotates slowly in dummy pointer direction
   pointer.transform.localEulerAngles = Vector3.Slerp(pointer.transform.localEulerAngles, dummyPointer.transform.localEulerAngles, Time.deltaTime * rotationSpeed);

Now it works perfectly.

And for those who a curious: I working at this game.

  • Liked by
Reply
Cancel