- Home /
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.
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!
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));
}
}
}
}
Answer by Eno-Khaon · Jan 15, 2021 at 11:31 AM
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.
Hi Eno-Khaon,
Thank you so much for the explanation and for providing the implementation code as well. Really appreciated the patience, clarity and logic of it all. I simply had to add a reference to m_Rigidbody and declare m_Rigidbody = GetComponent() in Start() to make the code work - and it does so exactly as I asked.
If you don't $$anonymous$$d, can you indulge me with another answer and/or explanation? One challenge I intend to add to the game are heavy, falling objects meant to destabilize the platform while balancing the ball. So, I created a HeavyCube (100 mass) and let it drop from a certain height. The issue is that with Rigidbody.$$anonymous$$oveRotation(), the collision is not registered correctly, and the cube simply goes through the platform. Instead, the collision worked well when I used transform.localRotation. Any ideas as to why this happens and/or how to redress this issue? Thanks in advance :)
EDIT
I noticed that the moment I comment the last line of code you provided (i.e. m_Rigidbody.$$anonymous$$oveRotation(Quaternion.AngleAxis(totalInput.x, Vector3.forward) Quaternion.AngleAxis(totalInput.y, Vector3.left));*), the physics go back to acting as expected (like in the second GIF).