How can i prevent my character rotating around nonY axes?

Discussion in 'Scripting' started by Nanako, Nov 11, 2015.

1. Nanako

Joined:
Sep 24, 2014
Posts:
1,047
So i'm working on a game with third person shooter type controls. It has mouselook, wasd keys move based on camera pointing. But since the character is on the ground, they can only move on the XZ plane, and only rotate around the Y axis. In theory anyway

I wrote some code for slow turning, it gradually rotates the target towards the direction they're moving. (not necessarily the direction the camera is facing, but often)
It mostly works to limiit the character's rotation, but sometimes, for very large rotations (like if i try to move in the opposite direction the character is facing) it screws up, and in order to point at the target, my character gets flipped through several other axes to find a shorter path than spinning around Y.

I wrote most of my code, but the solution for finding the direction of rotation, i may have found on a random unity answers post. And therefore don't fully understand it.

It's worth noting, the character is using a characterController. Though i'm not sure it has any special functions for this. Most notably though, there is no rigidbody, and so i can't just freeze rotation on the undesired axes

The relevant part of my code:
Code (CSharp):
1. //Code for rotating towards direction
2.         Quaternion newTargetRot = (Quaternion.FromToRotation(transform.forward, VectorTools.Flatten(controller.velocity)) * body.transform.rotation);
3.         if (newTargetRot != targetTurningRotation)
4.         {
5.             Shortcuts.ClearConsole();
6.             targetTurningRotation = newTargetRot;
7.
8.             Vector3 fwdCurrent = body.transform.rotation * Vector3.forward;
9.             Vector3 fwdTarget = targetTurningRotation * Vector3.forward;
10.
11.             float angleCurrent = Mathf.Atan2(fwdCurrent.x, fwdCurrent.z) * Mathf.Rad2Deg;
12.             float angleTarget = Mathf.Atan2(fwdTarget.x, fwdTarget.z) * Mathf.Rad2Deg;
13.
14.             float diff = Mathf.DeltaAngle(angleCurrent, angleTarget);
15.
16.             if (diff < 0f)
17.             {
18.                 targetTurningDirection = -1;
19.             }
20.             else
21.             {
22.                 targetTurningDirection = 1;
23.             }
24.             Debug.Log("Calculating! "+targetTurningDirection);
25.             turningAngleToTarget = Mathf.Abs(diff);
26.         }
27.         else
28.         {
29.             turningAngleToTarget = Quaternion.Angle(targetTurningRotation, body.transform.rotation);
30.         }
31.
32.         if (turningAngleToTarget < angularSpeedPerFixedUpdate)
33.         {
34.             body.transform.rotation = targetTurningRotation;
35.             anim.SetFloat("Direction", 0f);
36.         }
37.         else
38.         {
39.             float fraction = angularSpeedPerFixedUpdate / turningAngleToTarget;
40.             Debug.Log("Turning: " + fraction);
41.             anim.SetFloat("Direction", (turningAngleToTarget / 180f));
42.             body.transform.rotation = Quaternion.Lerp(body.transform.rotation, targetTurningRotation, fraction);
43.         }
I think the main problem here is that Quaternion.Lerp is too good at finding the shortest path, and that path is often not only around my desired axes. So i probably need to replace the lerp with some custom lerping. but how?

What can i do to fix this?

2. Nanako

Joined:
Sep 24, 2014
Posts:
1,047
poke

alternative lerping solution, anyone?

3. jmjd

Joined:
Nov 14, 2012
Posts:
45
You are probably right that Quaternion.Lerp is giving values off your desired axis. And, it's a little hard to dig through the code you provided, unless you can provide the whole script and it's dependencies or a simplified version showing the problem, which I know isn't always possible.

So what I will do is provide a small script that I frequently use when I want an object to face the direction of it's movement:
Code (CSharp):
1. using UnityEngine;
2. using System.Collections;
3.
4. public class FaceMovement : MonoBehaviour
5. {
6.     public float turnSpeed = 360f; // degrees/second
7.
8.     private Vector3 _lastPos;
9.
10.     void Start()
11.     {
12.         _lastPos = transform.position;
13.     }
14.
15.     void Update()
16.     {
17.         Vector3 lookAtDir = transform.position - _lastPos;
18.         transform.forward = Vector3.RotateTowards(transform.forward, lookAtDir, (turnSpeed * Mathf.Deg2Rad * Time.deltaTime), 0f);
19.         _lastPos = transform.position;
20.     }
21. }
22.

The code controlling the movement of the object can be in an entirely different script, this script just rotates the object to face the direction it is moving. So as long as the code controlling the object keeps it on a level plane (xz), then this should keep the object rotating around the y axis. If you want to integrate it into your current script, it looks like the values you want to put into Vector3.RotateTowards() is your forward, and velocity vectors (the ones in the first line of the code you provided).

Last edited: Nov 12, 2015
4. Nanako

Joined:
Sep 24, 2014
Posts:
1,047

I may be wrong here, but i don't think my whole code is relevant, since i'm 99% sure the problem is just Quaternion.Lerp interpolating smoothly over the surface of a 4D sphere, as quaternions do. They lerp well.

The problem is, when i start moving in the exact opposite direction, then all possible rotations are equidistant to the target point.

i'm not quite clear on how your solution will prevent this though. I don't see anything in Vector3.RotateTowards that will restrict it to rotating around a single axis. Can you elaborate on it?

5. jmjd

Joined:
Nov 14, 2012
Posts:
45
So for me, the quickest way to solve a coding problem is to be able to reproduce it. You're probably right, and the rest of your code has nothing to do with causing your problem... but it is relevant to solving it. I understand that it's not always possible to provide more code, but if I can't actually run your code and figure out what it's doing, then I can only read your code and give suggestions to what might help, instead of actual solutions. So if you can provide more... enough so that someone can reproduce the problem on their end, then yes it will be very helpful.

Anyways, there's nothing explicit in my code that is restricting it to only rotate around the y-axis. But did you try it? Place that script on an object and move it around on a flat plane. Is it doing what you wanted it to do, face the direction of movement? (You can just throw it on a cube in an empty scene, and move it in the editor in play mode. Move it straight forward, then straight back.)

Now, the code I gave you is a general facing direction of movement script, and I said that as long as the objects movement stayed on a flat plane, then it would only rotate around the y-axis. We obviously don't have the code for Vector3.RotateTowards(), but I've used it countless times for the behavior that you are describing.

My guess to how it works though is that it finds an orthogonal vector to the two that you provide, and then rotates the 'from' vector towards the 'to' vector using the orthogonal one as the rotation axis. So if you provide two vectors on the x-z plane, then it will only rotate around the y axis.

If your character's movement does include some y movement due to slopes or jumping, then we can just make that explicit by zeroing out the y values to the vectors we pass in.

Here's an updated script that hopefully makes it clearer:
Code (CSharp):
1. using UnityEngine;
2.
3. public class FaceMovement : MonoBehaviour
4. {
5.     public float turnSpeed = 360f; // degrees/second
6.
7.     private Vector3 _lastPos;
8.
9.     void Start()
10.     {
11.         _lastPos = transform.position;
12.     }
13.
14.     void Update()
15.     {
16.         Vector3 from = transform.forward;
17.         from.y = 0f;
18.         Vector3 to = transform.position - _lastPos;
19.         to.y = 0f;
20.
21.         transform.forward = Vector3.RotateTowards(from, to, (turnSpeed * Mathf.Deg2Rad * Time.deltaTime), 0f);
22.         _lastPos = transform.position;
23.     }
24. }
25.

Hope this helps, and let me know if you have anymore questions about how it's working.

Last edited: Nov 13, 2015
Nanako likes this.
6. Nanako

Joined:
Sep 24, 2014
Posts:
1,047
i finally got around to testing this. it works like a dream, amazing, and it's massively reduced the size of my turning code down to 3 lines.

I had no idea you could assign a value to transform.forward, i thought that was a read only property. that's crazy

7. jmjd

Joined:
Nov 14, 2012
Posts:
45
Great! I'm glad that worked out for you!

Nanako likes this.
unityunity