- Home /
Quaternion Shortest Route?
I am trying to make a little abstract arcadey-esque gamey-thing.
I have a cube, around the centre point of which my camera rotates.
The camera always faces one side of the cube.
Each rotation is 90°. look at one face, button press, look at the next face. The rotation can be in any direction, "up", "down", "left" or "right".
My problem is that after a few rotations, rather than simply rotating the human-obvious 90° to the next face, the camera decides to rotate around the cube in an arc back the wrong way. It even ignores the 90° planes and whips round via corners and all sorts.
Chatting to an acquaintance, he suspects that my script doesn't like rotating from (hypothetical example) 270° to 90° via the 0° point. Rather than jumping the 0° point it will rotate alllllll the way back round the long way.
I am pretty much a scripting virgin, just doing tutorials and trying to apply them to my game when the light bulb goes on above me head.
PLEASE HELP ME
public float turnSpeed = 200f;
private int rotation = 0;
private Quaternion qTo = Quaternion.identity;
void Update ()
{
if ((Input.GetKeyDown ("w")) & (Input.GetKey (KeyCode.RightShift)))
{
rotation += 90;
qTo = Quaternion.Euler(rotation, 0, 0);
Debug.Log ("Camera Rotated Screen-North");
}
if ((Input.GetKeyDown ("s")) & (Input.GetKey (KeyCode.RightShift)))
{
rotation -= 90;
qTo = Quaternion.Euler(rotation, 0, 0);
Debug.Log ("Camera Rotated Screen-South");
}
if ((Input.GetKeyDown ("d")) & (Input.GetKey (KeyCode.RightShift)))
{
rotation -= 90;
qTo = Quaternion.Euler(0, 0, rotation);
Debug.Log ("Camera Rotated Screen-East");
}
if ((Input.GetKeyDown ("a")) & (Input.GetKey (KeyCode.RightShift)))
{
rotation += 90;
qTo = Quaternion.Euler(0, 0, rotation);
Debug.Log ("Camera Rotated Screen-West");
}
transform.rotation = Quaternion.RotateTowards(transform.rotation, qTo, turnSpeed * Time.deltaTime);
}
Answer by Bunny83 · Dec 09, 2015 at 01:02 PM
Your code doesn't make much sense. You only use one rotation variable which you increment / decrement in each of your cases but one time you use it as x-rotation and one time as z-rotation.
Also your rotations are absolute worldspace rotation an not relative to the screen. You should just dump your rotation variable and only use quaternions. Something like that:
void Update ()
{
if (Input.GetKey (KeyCode.RightShift))
{
if ( Input.GetKeyDown ("w") )
{
qTo = Quaternion.AngleAxis(90, transform.right) * qTo;
}
else if ( Input.GetKeyDown ("s"))
{
qTo = Quaternion.AngleAxis(-90, transform.right) * qTo;
}
else if (Input.GetKeyDown ("d"))
{
qTo = Quaternion.AngleAxis(90, transform.up) * qTo;
}
else if (Input.GetKeyDown ("a"))
{
qTo = Quaternion.AngleAxis(-90, transform.up) * qTo;
}
transform.rotation = Quaternion.RotateTowards(transform.rotation, qTo, turnSpeed * Time.deltaTime);
}
Note: this is untested but should work that way. You might need to flip the sign of the "90" if it rotates into a wrong direction.
Keep in mind that you can implicitly rotate the cube around the z axis as well. If you do an up, right down rotation you're back on the starting side but it's rotated by 90° counterclockwise. It's 3d, you can't prevent that unless you force a certain orientation on a certain side. However that will yield some strange transitions where you will experience a double rotation when going from one side to another.
Games which allow such a game mechanic might want to provide a way to rotate around z as well (usually with "q" and "e")
Also note that using tranform.up /.right /.forward
might cause problems if you quickly press two different keys in succession. You might want to replace them with aTo*Vector3.up
and aTo*Vector3.right
. That way the axis is calculated from the current target orientation and not the current intermediate rotation.
AAAAAAAAAAA!
That's working JUST how I want it to with the qto*Vector3 rather than transform. !
I now just have to add something to stop it from allowing a rotation command before the current action is completed.
Also, regarding the point about rotating all the way around the cube and being inverted once you return to the starting face: yup. I know. But it's important that screen-up is true to itself rather than the cube. I think it will be more intuitively understandable for gameplay. The cube around which I am rotating is the play-space. It's a very abstract game, so word-orientation doesn't really apply.
In case you're interested, this is the script as it stands now :)
Thanks,
Jo
public float turnSpeed = 200f;
private Quaternion qTo = Quaternion.identity;
// Update is called once per frame
void Update ()
{
if (Input.Get$$anonymous$$ey ($$anonymous$$eyCode.RightShift)) {
if ((Input.Get$$anonymous$$eyDown ("w")) & (Input.Get$$anonymous$$ey ($$anonymous$$eyCode.RightShift)))
{
qTo = Quaternion.AngleAxis (90, qTo*Vector3.right) * qTo;
}
else if ((Input.Get$$anonymous$$eyDown ("s")) & (Input.Get$$anonymous$$ey ($$anonymous$$eyCode.RightShift)))
{
qTo = Quaternion.AngleAxis (-90, qTo*Vector3.right) * qTo;
}
else if ((Input.Get$$anonymous$$eyDown ("d")) & (Input.Get$$anonymous$$ey ($$anonymous$$eyCode.RightShift)))
{
qTo = Quaternion.AngleAxis (-90, qTo*Vector3.forward) * qTo;
}
else if ((Input.Get$$anonymous$$eyDown ("a")) & (Input.Get$$anonymous$$ey ($$anonymous$$eyCode.RightShift)))
{
qTo = Quaternion.AngleAxis (90, qTo*Vector3.forward) * qTo;
}
transform.rotation = Quaternion.RotateTowards (transform.rotation, qTo, turnSpeed * Time.deltaTime);
}
}
Well, in that case i suggest to use a coroutine. It could be implemented like this:
public float turnSpeed;
Coroutine RotateCo = null;
IEnumerator RotateTo(Quaternion aTarget)
{
Quaternion startRotation = transform.rotation;
for (float t = 0; t < 1.0f; t += Time.deltaTime * turnSpeed / 90)
{
transform.rotation = Quaternion.Slerp(startRotation, aTarget, t);
yield return null;
}
RotateCo = null;
}
void StartRotateTo(Quaternion aTarget)
{
RotateCo = StartCoroutine( RotateTo( aTarget ) );
}
void Update()
{
if (RotateCo == null && Input.Get$$anonymous$$ey($$anonymous$$eyCode.RightShift))
{
if (Input.Get$$anonymous$$eyDown($$anonymous$$eyCode.W))
{
StartRotateTo(Quaternion.AngleAxis(90, transform.right));
}
else if (Input.Get$$anonymous$$eyDown($$anonymous$$eyCode.S))
{
StartRotateTo(Quaternion.AngleAxis(-90, transform.right));
}
else if (Input.Get$$anonymous$$eyDown($$anonymous$$eyCode.D))
{
StartRotateTo(Quaternion.AngleAxis(90, transform.up));
}
else if (Input.Get$$anonymous$$eyDown($$anonymous$$eyCode.A))
{
StartRotateTo(Quaternion.AngleAxis(-90, transform.up));
}
}
}
I've replaced the "string version" of Get$$anonymous$$eyDown with the $$anonymous$$eyCode version. Also you don't need to check "RightShift" in each case since it's checked once for all. The RotateCo == null
check will prevent you from starting a new coroutine while another one is still executing. The coroutine will set RotateCo back to "null" when it's finished rotating.
As a side note: In most cases you don't want to use &
to combine several conditions. &
will always evaluate both sides, even then the first one is already false. &&
which is the logical "and" operator will only evaluate the second operand if the first is true.
So this:
if (A() & B())
DoSomething();
is the same as
bool a = A();
bool b = B();
if (a && b)
DoSomething();
while this:
if (A() && B())
DoSomething();
is the same as
if (A())
if (B())
DoSomething();
So "B" is only executed when "A" returns true. Since both (A and B )have to return true in order to execute DoSomething it's enough to know that A returns false. So we don't have to call "B" since it's return value can't change the outcome.
Hey Bunny83,
You're code is working well, $$anonymous$$y rotations no longer go wild and rotation the longest way round in some free-form jazzy manner!
Unfortunatly i think the coroutine is only disallowing execution for the duration of the registry of the button-press, rather than the execution of the full rotation. If you catch my drift...
I can press for a new rotation and it will start before the current rotation is completed.
I worry this will lead to confusion in gameplay.
Is there a way around it? Can I simply return the RotateCo to null at a different point in the code?
you are clearly a marvellous human being, and we should set up temples in your honour.
I will test this whole shebang later once I have got my dayjob stuff done :D
Answer by wibble82 · Dec 08, 2015 at 02:58 PM
Hey
I thought Quaternion.RotateTowards handled this automatically, but perhaps it doesn't.
Assuming it's not handled internally, the answer lies in the fact that a quaternion represents 720 degrees of rotation! Your rotation 'qTo' does represent the euler angle required. However if you imagine it as an instruction 'rotate to 90 degrees around the x axis', you could also say 'rotate to -270 degrees around the x axis' - the end result is the same.
If you want to make sure you always go in the correct direction, you need to do a bit of extra work, You need to make sure your quaternion and the other quaternion represent rotations in the same direction. This is done with a dot product:
if (Quaternion.Dot(transform.rotation,qTo) < 0)
qTo = -qTo;
Here we've said 'if transform.rotation is in the opposite direction to qTo, flip the direction of qTo'.
That should solve the problem, though I've not tested it. Let me know if you're still struggling and I'll take a look at home once I have unity in front of me.
-Chris
Hey $$anonymous$$r Chris
Thanks for your rapid response!
Sadly, on adding your code to the script, Unity tells me - cannot be applied to Quaternions :/
Assets/Scripts/CamShift2QuatEul.cs(42,32): error CS0023: The -' operator cannot be applied to operand of type
UnityEngine.Quaternion'
Is there a way around this? Or am i going to have to add something terrifying?
Thanks
Jo
public float turnSpeed = 200f;
private int rotation = 0;
private Quaternion qTo = Quaternion.identity;
void Update ()
{
if ((Input.Get$$anonymous$$eyDown ("w")) & (Input.Get$$anonymous$$ey ($$anonymous$$eyCode.RightShift)))
{
rotation += 90;
qTo = Quaternion.Euler(rotation, 0, 0);
Debug.Log ("Camera Rotated Screen-North");
}
if ((Input.Get$$anonymous$$eyDown ("s")) & (Input.Get$$anonymous$$ey ($$anonymous$$eyCode.RightShift)))
{
rotation -= 90;
qTo = Quaternion.Euler(rotation, 0, 0);
Debug.Log ("Camera Rotated Screen-South");
}
if ((Input.Get$$anonymous$$eyDown ("d")) & (Input.Get$$anonymous$$ey ($$anonymous$$eyCode.RightShift)))
{
rotation -= 90;
qTo = Quaternion.Euler(0, 0, rotation);
Debug.Log ("Camera Rotated Screen-East");
}
if ((Input.Get$$anonymous$$eyDown ("a")) & (Input.Get$$anonymous$$ey ($$anonymous$$eyCode.RightShift)))
{
rotation += 90;
qTo = Quaternion.Euler(0, 0, rotation);
Debug.Log ("Camera Rotated Screen-West");
}
if (Quaternion.Dot (transform.rotation, qTo) < 0)
{
qTo = -qTo;
}
transform.rotation = Quaternion.RotateTowards(transform.rotation, qTo, turnSpeed * Time.deltaTime);
}
Hey $$anonymous$$r Chris,
I added the line of code you suggested to the bottom of the script, before the execution of the rotation.
Sadly on entering Unity, it tells me - cannot be applied to Quaternions :/
" Assets/Scripts/CamShift2QuatEul.cs(42,32): error CS0023: The -' operator cannot be applied to operand of type
UnityEngine.Quaternion' "
Is there a way around this?
public float turnSpeed = 200f;
private int rotation = 0;
private Quaternion qTo = Quaternion.identity;
void Update ()
{
if ((Input.Get$$anonymous$$eyDown ("w")) & (Input.Get$$anonymous$$ey ($$anonymous$$eyCode.RightShift)))
{
rotation += 90;
qTo = Quaternion.Euler(rotation, 0, 0);
Debug.Log ("Camera Rotated Screen-North");
}
if ((Input.Get$$anonymous$$eyDown ("s")) & (Input.Get$$anonymous$$ey ($$anonymous$$eyCode.RightShift)))
{
rotation -= 90;
qTo = Quaternion.Euler(rotation, 0, 0);
Debug.Log ("Camera Rotated Screen-South");
}
if ((Input.Get$$anonymous$$eyDown ("d")) & (Input.Get$$anonymous$$ey ($$anonymous$$eyCode.RightShift)))
{
rotation -= 90;
qTo = Quaternion.Euler(0, 0, rotation);
Debug.Log ("Camera Rotated Screen-East");
}
if ((Input.Get$$anonymous$$eyDown ("a")) & (Input.Get$$anonymous$$ey ($$anonymous$$eyCode.RightShift)))
{
rotation += 90;
qTo = Quaternion.Euler(0, 0, rotation);
Debug.Log ("Camera Rotated Screen-West");
}
if (Quaternion.Dot (transform.rotation, qTo) < 0)
{
qTo = -qTo;
}
transform.rotation = Quaternion.RotateTowards(transform.rotation, qTo, turnSpeed * Time.deltaTime);
}
Answer by ATLGAN · Aug 18, 2020 at 11:41 AM
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RotateQuaNoneShortWay : MonoBehaviour
{
public float rotateAngle;
public float speed;
Quaternion targetRot;
float targetAngle;
float currentAngle;
Quaternion firstRotation;
public bool canRotate;
Transform c_Transform;
private void Start()
{
c_Transform = GetComponent<Transform>();
firstRotation = c_Transform.rotation;
}
void Update()
{
if (Input.GetKeyDown(KeyCode.R))
{
targetAngle += rotateAngle;
if (!canRotate)
{
firstRotation = c_Transform.rotation;
canRotate = true;
}
}
if (canRotate)
{
currentAngle = Mathf.Lerp(currentAngle, targetAngle, Time.deltaTime * speed);
targetRot = Quaternion.AngleAxis(currentAngle, c_Transform.up) * firstRotation;
c_Transform.rotation = Quaternion.Slerp(c_Transform.rotation, targetRot, Time.deltaTime * speed);
if (Quaternion.Angle(c_Transform.rotation,targetRot) < 0.2f)
{
c_Transform.rotation = targetRot;
currentAngle = 0;
targetAngle = 0;
canRotate = false;
}
}
}
}
Your answer
Follow this Question
Related Questions
Rotate Towards doesnt work as expected 2 Answers
How to rotate towards the mouse on one axis while snapping 0 Answers
Doing 2D only rotation using FromToRotation + RotateTowards 0 Answers
How to store direction so that player faces last direction when joystick is idle 2 Answers
Measuring the Angle Between Two Transforms for t Param of Quaternion.Slerp 1 Answer