Menu rotation script question

Hey Everyone,

I’ve created a script to rotate a menu around a player and it works the way I want as long as the rotation is a positive value. The moment it goes negative, it spins over and over again. I think it has to do with the LerpUnclamped() but I’m not sure. Any help is appreciated.

Thanks!
-Kaze-

Here’s my code:

using UnityEngine;
using System.Collections.Generic;

public class MenuController : MonoBehaviour
{
    [SerializeField] private List<GameObject> menuObjList = new List<GameObject>();

    [SerializeField] private GameObject menuObjContainer;

    [SerializeField] private float rotationAmount;
    [SerializeField] private float rotationSpeed;
    [SerializeField] private float currentDegree;

    private void Awake()
    {
        CalcRotationAmount();
        CalcRotationSpeed();
    }

    private void Update()
    {
        CalculateDegree();
        RotateMenu();
    }

    private void CalcRotationAmount()
    {
        rotationAmount = 360 / menuObjList.Count;
    }

    private void CalcRotationSpeed()
    {
        rotationSpeed = rotationSpeed * Time.deltaTime;
    }

    private void CalculateDegree()
    {
        if(Input.GetKeyDown(KeyCode.RightArrow))
        {
            currentDegree -= rotationAmount;
            if(currentDegree <= -360)
            {
                currentDegree = 0;
            }
        }

        if(Input.GetKeyDown(KeyCode.LeftArrow))
        {
            currentDegree += rotationAmount;
            if(currentDegree >= 360)
            {
                currentDegree = 0;
            }
        }
    }

    private void RotateMenu()
    {
        Vector3 rotationDestination = new Vector3(0, 0, currentDegree);

        menuObjContainer.transform.eulerAngles = Vector3.LerpUnclamped(menuObjContainer.transform.rotation.eulerAngles, rotationDestination, rotationSpeed);
    }
}

The LerpUnclamped doesn’t appear to be doing anything useful in line 61. The t argument in a lerp is the percentage between the beginning and end values interpolated linearly, not a speed. I’m guessing that you want it to make it smoothly approach the destination point, in which case I’d recommend trying a SmoothDamp.

Also, I suspect that wrapping from 360 to 0 while lerping is causing unexpected behavior. Try lerping/damping the destination degree before applying wrapping between 0 and 360. Or you could just let the transform do the Euler wrapping and apply the non-wrapped value directly to the Euler angle, should work.

Hey,

Yeah I wasn’t too sure about the LerpUnclamped but the reason I used it is because of some tests that I ran and my basic understanding of it.

I thought that Lerp took a starting position and an ending position for movement and completed that distance in the amount of time outlined with some dampening at the end as it nears the destination. LerpUnclamped’s definition read that it allows for a time value outside 0-1. I took that to mean that you can speed up the transition time. So by doing 3.0f * Time.deltaTime, that would speed up the lerp movement by a factor of 3. Is that now how that’s working?

Also, when I took that same exact line and left it as Lerp instead of LerpUnclamped, it slowed down again.

Additionally, the behavior actually works just fine when moving in a positive direction up to 360. When it gets higher than 360, it loops backwards to the first option which is exactly what I want.

The problem occurs when I go into the negatives. If I go from 0 to -90, the menu just starts to spin continuously. The larger the negative number, the faster it spins. So when I hit -270, it’s spinning faster than when it was at -90.

I’ll try the SmoothDamp though. And I’m a little confused with what you mean by applying the non-wrapped value directly to the angle.

[quote=KazeEnji, po[/FONT]st: 2623670, member: 24019]
I thought that Lerp took a starting position and an ending position for movement and completed that distance in the amount of time outlined with some dampening at the end as it nears the destination.
[/quote]

This is what SmoothDamp does.

Lerp just interpolates at t% between two given values; if you gave 0 as start and 10 as end and used 0.5 as t, it would return 5. If you used 0.86 as t, it would return 8.6; if the start value was 4 and the end was -3, a t of 0.3 would return 1.9.

Essentially, it’s the formula

f(a, b, t) = (b - a)t + a

LerpUnclamped just allows you to use interpolants for t outside the constraint t = [0, 1], so it can also extrapolate; e.g. a = 0, b = 10, t = 1.1 would return 11.

My point is that the Lerp isn’t doing what you think it is, but it’s producing a similar effect because it’s iterated; I see that now. So as long as you like the effect, by all means use it.

Still, I believe the reason the rotation behaves unpredictably is that Lerp/SmoothDamp can’t handle wrapping values like the 0-360 wrapping in Euler angles.

Imagine: if the target value is -90 and the current value is 45, then the Lerp will put the new value somewhere between 45 and -90, most likely (using unclamped makes this uncertain, but multiplying by Time.deltaTime makes it probable that rotationSpeed is small). So let’s assume that we end up at 40 degrees. The next frame, the target value is still -90, and the current value is 40, and we end up at 36 degrees, and so on… eventually, we end up passing 0 degrees.

When this happens, the angle is not read as a negative, but is instead wrapped to 360 degrees. So if it was set to -5 degrees in the previous frame, it will be read as 355 degrees next time, which means the script now tries to interpolate between -90 and 355, which would put you at 337. This is then repeated forever, and your menu spins freely.

To solve this, don’t manually wrap your angles (lines 41 - 44 and 50 - 53) and don’t use the Euler angle as the starting point for a Lerp/SmoothDamp (line 61). Instead, leave your values non-wrapped (i.e. values outside of -360 to +360 are fine), and perform a Lerp on some temporary value instead, then assign the temporary value directly to transform.eulerAngles.z:

...

float currentAngle
float targetAngle

...

private void CalculateDegree()
{
    if(Input.GetKeyDown(KeyCode.RightArrow))
    {
        targetAngle -= rotationAmount;  //Don't wrap values here
    }

   if(Input.GetKeyDown(KeyCode.LeftArrow))
    {
        targetAngle += rotationAmount;  //...Or here
    }
}

...

private void RotateMenu()
{
    currentAngle = Mathf.LerpUnclamped(currentAngle, targetAngle, rotationSpeed);  //Lerp the non-wrapped angles to prevent wrapping error
    Vector3 rotationDestination = new Vector3(0, 0, currentDegree);
    menuObjContainer.transform.eulerAngles = rotationDestination;  //Apply the lerped angle directly to the euler angle
}

The euler angles will wrap themselves automatically, so there should be no need to do it yourself.

That’s fantastic! Thanks! I’ll test this and the SmoothDamp this weekend.

Let me know how it works. I didn’t test it myself, so you never know.

Hey,

So I tried out what you said and after a little more research, this is what I ended up with:

using UnityEngine;
using System.Collections.Generic;

public class MenuController : MonoBehaviour
{
    [SerializeField] private List<GameObject> menuObjList = new List<GameObject>();

    [SerializeField] private GameObject menuObjContainer;

    [SerializeField] private float rotationAmount;
    [SerializeField] private float rotationSpeed;
    [SerializeField] private float rotationTarget = 0;
    [SerializeField] private float currentDegree;

    private void Awake()
    {
        CalcStartingRotation();
        CalcRotationAmount();
        CalcRotationSpeed();
    }

    private void Update()
    {
        CalculateDegreeAndRotate();
    }

    private void CalcRotationAmount()
    {
        rotationAmount = 360 / menuObjList.Count;
    }

    private void CalcRotationSpeed()
    {
        rotationSpeed = rotationSpeed * Time.deltaTime;
    }

    private void CalcStartingRotation()
    {
        menuObjContainer.transform.rotation = Quaternion.Euler(new Vector3(0, 0, currentDegree));
    }

    private void CalculateDegreeAndRotate()
    {
        currentDegree = Mathf.Lerp(currentDegree, rotationTarget, rotationSpeed);

        if(Input.GetKeyDown(KeyCode.RightArrow))
        {
            rotationTarget += rotationAmount;
        }

        if(Input.GetKeyDown(KeyCode.LeftArrow))
        {
            rotationTarget -= rotationAmount;
        }

        menuObjContainer.transform.rotation = Quaternion.Euler(new Vector3(0, 0, currentDegree));
    }
}

It’s not exactly what you said but your post definitely helped me get there. Now I need to figure out how to identify which one is on top of the circle given any rotation. I think I know how but I’d like to find a more efficient way of figuring it out.

Basically, you want to check for which menu item is brought into the selection range, right? I don’t know what your menu looks like, but if each item is spaced evenly, you could just divide the euler angle of the menu container by the separation angle between each item (if each menu item is 30 degrees apart, for example, divide the angle by 30) and use the resulting integer as an index to select a menu item. You can even create a list of references to each menu item GameObject and access the appropriate reference according to the above calculation.

You could also just define a point directly in front of the center of the menu, perhaps just outside the circle, and check to see which menu item is closest, choosing to activate that one.

Yeah I was considering your second option already but I don’t like the idea of needing another gameobject for detection, there’s got to be a better way, one more exact. Which actually leads back to your first recommendation…I like that.

So I could use the rotationAmount variable that I already have to assign the menu options…hmm…

Just so you know, you don’t need a GameObject to check positions against a point. You can just define it yourself as a Vector3 in local space. For example, if the radius of your menu is 3, you could define a variable checkPos as a Vector3 and set it to (0, 0, 3.25), and check each menu item’s Transform.localPosition against that value. This check can be done in the menu container’s script, so there’s never a need for an additional object.

It’s not considered sloppy to use tools like this to define a concept in the project’s world space instead of using hard math. A comparison like this would be no less exact than using a formula. If you decide that this would be easier, then use it; both methods should work, though.

Oh no, I wasn’t thinking it would be sloppier or anything like that. I was just thinking I already had the information I needed to get the result I wanted without needing to add something else to the mix and that’s where your previous suggestion comes in. I think I like the formula even more…there might even be a way that I don’t need to do that at all. If I modify how the menu objects populate, I could just assign a value then and add/subtract 1 as the user points left and right.