How can I pitch and roll a circular platform without releasing vertical/horizontal input?

Greetings developers!

I have a simple goal in mind: control the pitch (x) and roll (z) of a circular platform (built using a sphere with scale 10, -0.1, 10) to balance a sphere on top of it.

However, I wish to limit the rotation both in the x and z axes (ideally the y axis is frozen), so that the platform cannot go beyond a certain tilt angle (50f) in any direction.

The code below works, but not exactly as desired: the moment the platform reaches the tilt angle, the ‘if’ statement no longer holds and the platform gets stuck, forcing me to release all keys to restore the original rotation of the platform (0, 0, 0).

My wish is that, once the platform has reached the tilt angle limit (or approached it), I may keep tilting the platform on the axis which has not reached the limit.

174202-asis.gif

I hope the GIFs can better express what my desired result is. In the first one (above), I am showing the as-is behavior, in the second one, I am showing the desired behavior (I used a simple work-around setting the max tilt angle to 80 giving the impression that the platform does not get stuck at the 50 limit and I can keep rotating within my desired limit).

I have been stuck on this for days, can anyone help?

Thanks in advance, and please let me know if I can provide any additional info to make the question clearer. Cheers!

174203-tobe.gif

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class KeyController : MonoBehaviour
{
    Quaternion originalRotation;
    Rigidbody m_Rigidbody;
    bool restoreRotation = false;
    [SerializeField] float tiltAngle = 50f;

    float h;
    float v;

    private void Start()
    {
        originalRotation = transform.rotation;
        m_Rigidbody = GetComponent<Rigidbody>();
    }

    void FixedUpdate()
    {
        m_Rigidbody.constraints = RigidbodyConstraints.FreezeRotation;
        m_Rigidbody.constraints = RigidbodyConstraints.FreezePosition;

        if (!Input.GetKey(KeyCode.LeftArrow) & !Input.GetKey(KeyCode.RightArrow) & !Input.GetKey(KeyCode.UpArrow) & !Input.GetKey(KeyCode.DownArrow) & !restoreRotation)
        {
            restoreRotation = true;
        }

// If I am not holding down any key, take me back to original rotation 0, 0, 0
        if (restoreRotation & !Input.anyKey)
        {
            transform.rotation = Quaternion.Lerp(transform.rotation, originalRotation, Time.deltaTime * 2);

            if (transform.rotation == originalRotation)
            {
                restoreRotation = false;
            }
        }
        else if (restoreRotation & Input.anyKey)
        {
            restoreRotation = false;
        }
// If I am holding down the left, right, up and/or down key
        else if (!restoreRotation)
        {
            h = Input.GetAxis("Horizontal") * tiltAngle;
            v = -Input.GetAxis("Vertical") * tiltAngle;

            if (Quaternion.Angle(originalRotation, transform.localRotation) <= tiltAngle)
            {
                transform.localRotation *= Quaternion.AngleAxis(h * Time.deltaTime, new Vector3(0, 0, 1));
                transform.localRotation *= Quaternion.AngleAxis(v * Time.deltaTime, new Vector3(1, 0, 0));
            }
        }
    }
}

Well, there are actually a few things that can be addressed here.

First, if you’re holding ANY key whatsoever, the platform won’t rotate back to its original orientation.

Second, you’re getting pretty lucky that this works by modifying transform.localRotation when using the physics system. While it should mean that it’s not necessarily applying meaningful/appropriate force against the ball it’s supporting while it’s actively rotating, it’s also relying on small rotations per physics frame to not simply pass through the ball.

Finally, there’s also a much simpler way that you can track the rotation in this scenario. You even have the variables available to support it, but didn’t really use them in that manner (h and v).


So, let's get to implementation, then.

First, as a matter of principle, because Input values are provided during Update() (specified because the new input system isn’t strictly limited to that), my example will apply input there.

Then, you can use Rigidbody.MoveRotation() to provide discrete rotations in FixedUpdate() while honoring physics interactions.

public float maxAngle; // The maximum angle of rotation allowed
public float tiltSpeed; // Rotation speed, up to maxAngle
public float returnSpeed; // Speed to return to original orientation
Vector2 currentInput; // The current frame's input to apply to the total
Vector2 totalInput; // The combined totals from rotation inputs

void Update()
{
	currentInput = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"));
}

void FixedUpdate()
{
	// If no input is being provided, ease total input(s) toward 0
	if(Mathf.Approximately(currentInput.sqrMagnitude, 0.0f))
	{
		// Time.deltaTime is implicitly converted to Time.fixedDeltaTime
		// when called inside FixedUpdate()
		totalInput = Vector2.MoveTowards(totalInput, Vector2.zero, returnSpeed * Time.deltaTime);
	}
	else
	{
		// If there's input, apply it to the running total here
		totalInput += currentInput * Time.deltaTime * tiltSpeed;
		// If the total vector length exceeds the maximum, adjust it
		if(totalInput.sqrMagnitude > maxAngle * maxAngle)
		{
			totalInput = totalInput.normalized * maxAngle;
		}
		// Note that this approach should move smoothly toward the current
		// input, while not aligning to cardinal directions easily. 
		// There are many ways to design this, so this is simply
		// one of many options available
	}
	// Now, regardless of how the totalInput has changed, the rotation can be
	// set at this point (add restrictions as desired)
	m_Rigidbody.MoveRotation(Quaternion.AngleAxis(totalInput.x, Vector3.forward) * Quaternion.AngleAxis(totalInput.y, Vector3.left));
}

I apologize in advance if there are any mistakes. I know that prototyping things when tired won’t necessarily end well (especially when untested), but wanted to get this typed up anyway.