I have two direction vectors that I get by caching the initial targetDirection to the updated one.
_targetDirection = target - transform.position;
//Snap the direction to a plane with the specified normal.
_snapDir = Vector3.ProjectOnPlane(_targetDirection, axisToNormal[_snapPlaneNormal]).normalized;
//Calculate the angle from face to snapDir indicating axis to prevent rotation flips.
_angle = Vector3.SignedAngle(_snapCachedInitDir, _snapDir, axisToNormal[_snapPlaneNormal]);
But SignedAngle always return angle between -180 and 180. Is there a way to get an angle that will allow be to track the number or full rotations?
This is what is working right now, and I need to now keep track of the rotation count.
No. Angle and SignedAngle and anything else just looks at the current angle. What you can do is stop checking the crank object and instead create your own 0-360 rotation variable. You can make that go past 360 and 720 and so on.
The idea is, somehow you’re checking for the player turning the wheel, right? When that happens you probably do wheel.Rotate(0,0,amt);. Then you use SignAngle(theWheel) to read the result back somehow. Change all of that to wheelRotation+=amt; (your personal float variable) and use that to set the wheel: theWheel.rotation=Quaternion.Euler(0,0,amt);. When Unity sees amt=370 it will convert it to 10, but your copy is still 370 so it’s fine.
That idea is useful in general: have the master copy be your own variables, which you have full control over and can put in a form you prefer, and apply them to the gameObjects as they change.
I’m setting the rotation directly by getting an _angle like above and adding this _angle to the localEulerAngle.
void RotAngleToNewAngle(Vector3 _cachedLocalEuler)
{
Vector3 offsetAngle;
//Set the angle to be the current angle plus additional angle.
if (_snapPlaneNormal.Equals(CustomExtensions.CustomAxis.X) || _snapPlaneNormal.Equals(CustomExtensions.CustomAxis.X_Flip))
{
offsetAngle = new Vector3(_cachedLocalEuler.x + _angle, 0f);
}
else if (_snapPlaneNormal.Equals(CustomExtensions.CustomAxis.Y) || _snapPlaneNormal.Equals(CustomExtensions.CustomAxis.Y_Flip))
{
offsetAngle = new Vector3(0f, _cachedLocalEuler.y + _angle);
}
else
{
offsetAngle = new Vector3(0f, 0f, _cachedLocalEuler.z + _angle);
}
//Use the offset angle as localEulerAngle
transform.localEulerAngles = offsetAngle;
}
I understand the concept, I’m having trouble executing it with the set up I have. I know there are some hackish stuff there.
Full Setup for Rotating the Crank
IEnumerator SpinToFacePoint()
{
_isMoving = true;
Ray camRay = GameManager.Instance._mainCam.ScreenPointToRay(Mouse.current.position.ReadValue()); ;
RaycastHit hit;
//The length of this ray is the length of raycastcontroller length. hardcoding it now... may change later if needed.
Physics.Raycast(camRay, out hit, 3f, LayerMask.NameToLayer("Interactables"));
float distFromObj = Vector3.Distance(transform.position, GameManager.Instance._Character.GetPosition());
if (distFromObj > _InteractRange)
{
_isWithinRange = false;
}
else
{
_isWithinRange = true;
}
camRay = GameManager.Instance._mainCam.ScreenPointToRay(Mouse.current.position.ReadValue());
target = camRay.GetPoint(hit.distance);
Vector3 cachedInitDir = target - transform.position;
//Snap the direction to a plane with the specified normal.
_snapCachedInitDir = Vector3.ProjectOnPlane(cachedInitDir, axisToNormal[_snapPlaneNormal]).normalized;
Vector3 _cachedLocalEuler = transform.localEulerAngles;
//Reset the localeuler because it flips axis when over 90 degree
if (_snapPlaneNormal.Equals(CustomExtensions.CustomAxis.X) || _snapPlaneNormal.Equals(CustomExtensions.CustomAxis.X_Flip))
{
if(_cachedLocalEuler.y >= 180f || _cachedLocalEuler.z >= 180)
{
_cachedLocalEuler.x = -_cachedLocalEuler.x + 180f;
}
_cachedLocalEuler = new Vector3(_cachedLocalEuler.x, 0f);
}
else if (_snapPlaneNormal.Equals(CustomExtensions.CustomAxis.Y) || _snapPlaneNormal.Equals(CustomExtensions.CustomAxis.Y_Flip))
{
if (_cachedLocalEuler.x >= 180f || _cachedLocalEuler.z >= 180)
{
_cachedLocalEuler.y = -_cachedLocalEuler.y + 180f;
}
_cachedLocalEuler = new Vector3(0f, _cachedLocalEuler.y);
}
else
{
if (_cachedLocalEuler.y >= 180f || _cachedLocalEuler.x >= 180)
{
_cachedLocalEuler.y = -_cachedLocalEuler.z + 180f;
}
_cachedLocalEuler = new Vector3(0f, 0f, _cachedLocalEuler.z);
}
_IsRotating = true;
while (_isInteractHeld && _isWithinRange)
{
//Calculate distance from object
distFromObj = Vector3.Distance(rb.transform.position, GameManager.Instance._Character.cameraTransform.position);
if(distFromObj > _InteractRange)
{
_isWithinRange = false;
_isActive = false;
}
//Use camera to cast a ray with mouse poisiton and get a point from the ray.
//This allows point to be limited instead of ScreenToWorldPoint that takes account depth without needing to create new vector3
camRay = GameManager.Instance._mainCam.ScreenPointToRay(Mouse.current.position.ReadValue());
target = camRay.GetPoint(hit.distance);
_targetDirection = target - transform.position;
//Snap the direction to a plane with the specified normal.
_snapDir = Vector3.ProjectOnPlane(_targetDirection, axisToNormal[_snapPlaneNormal]).normalized;
//Calculate the angle from face to snapDir indicating axis to prevent rotation flips.
_angle = Vector3.SignedAngle(_snapCachedInitDir, _snapDir, axisToNormal[_snapPlaneNormal]);
RotAngleToNewAngle(_cachedLocalEuler);
// Draw a ray pointing at our target in
//Debug.DrawRay(transform.position, snapDir, Color.red);
yield return null;
}
_IsRotating = false;
_isMoving = false;
yield return null;
}
That’s a mess, but I’ve written worse. It looks like cachedLocalEuler and offsetAngle are just the old and new rotations. The problem is line 29 where you read cachedLocalEuler (I have that name – it’s not really a cached value, to me). If it was 370 in your code last frame, it’s saved as 10 in the transform, and that’s what you read back.
I’d think you could set CLE once at the start, never read it again from the transform, and copy CLE=offsetAngle; when done computing offsetAngle (which you can’t do know, since it’s passed-in, so maybe return it?)
Well it does cached the transform.localEulerAngles, but since the axis flip when reaching above 180 or below -180 line 32 to line 55, flips it back. CLE isn’t called again until the coroutine SpinToFacePoint(); happens again from player clicking on object.
The code below turns _angle to a 360 rotation. Now the problem is how to determine a full rotation. Since the player can rotate the crank in any direction at any time.
You can’t count the rotations with SignedAngle. It can only “observe” the current state (compared to some basis orientation), without knowing how it got there.
You yourself are supposed to count the rotations while they occur. You can easily do this by observing the angle deltas and accumulating them. This cumulative rotation is what you then apply to the visual part (mod(angle, 360f)*), but also you can easily deduce the amount of rotations from this number alone (floor(angle / 360f)).
mod is short for (true) modulo (somewhat distinct from the remainder operator %)
float mod(float n, float modulus) => (n %= modulus) < 0f? n + modulus : n;
//Calculate the angle from face to snapDir indicating axis to prevent rotation flips.
_angle = Vector3.SignedAngle(_snapCachedInitDir, _snapDir, axisToNormal[_snapPlaneNormal]);
Then using this to get a full 360. Using Mathf.floor will work except I wouldn’t know which direction the player might start rotation towards. So it could go from zero to 360f if they turn the opposite direction.
I basically had to remove a bunch of stuff and replaced it with a much simpler solution. I followed the answer here.
New Code
while (_isInteractHeld && _isWithinRange)
{
//Calculate distance from object
distFromObj = Vector3.Distance(rb.transform.position, GameManager.Instance._Character.cameraTransform.position);
if (distFromObj > _InteractRange)
{
_isWithinRange = false;
_isActive = false;
}
//Use camera to cast a ray with mouse poisiton and get a point from the ray.
//This allows point to be limited instead of ScreenToWorldPoint that takes account depth without needing to create new vector3
camRay = GameManager.Instance._mainCam.ScreenPointToRay(Mouse.current.position.ReadValue());
target = camRay.GetPoint(hit.distance);
_targetDirection = target - transform.position;
//Snap the direction to a plane with the specified normal.
_snapDir = Vector3.ProjectOnPlane(_targetDirection, axisToNormal[_snapPlaneNormal]).normalized;
float newAngle = Mathf.Atan2(_snapDir.y, _snapDir.x) * Mathf.Rad2Deg;
rotateAmount += Mathf.DeltaAngle(_angle, newAngle);
_angle = newAngle;
transform.localRotation = Quaternion.AngleAxis(-_angle - 90f, axisToNormal2[_snapPlaneNormal]);
yield return null;
}
I had to offset the angle by 90 to the handle is in the correct place. Is there a way to avoid the angle jumps? I think the jump happens because it uses a vector as forward to guide the rotation? How do I offset this jump?
I rewrote all the code. Took the concept from @Owen-Reynolds and the answer here to make this all work.
Hopefully final code
/// <summary>
/// Use Transform.Rotate to rotate the given angle taken from prev hit dir to current hit dir.
/// </summary>
/// <returns></returns>
IEnumerator StartRotation()
{
_isMoving = true;
//Get the eye position
Vector3 eyeForward = (GameManager.Instance._Character.cameraTransform.position + GameManager.Instance._Character.GetEyeForwardVector());
//Cache the hit direction and flatten on plane
Vector3 cachedHitDir = eyeForward - transform.position;
flatPrevHitDir = Vector3.ProjectOnPlane(cachedHitDir, axisToNormal[_planeNormal]).normalized;
//Get the current hit direction and flatten on plane
Vector3 currentHitDir = eyeForward - transform.position;
flatCurrentHitDir = Vector3.ProjectOnPlane(currentHitDir, axisToNormal[_planeNormal]).normalized;
//Create a new angle to store full angle
float newAngle = Mathf.Atan2(currentHitDir.y, currentHitDir.x) * Mathf.Rad2Deg;
float _angle = newAngle;
//Use signed angle to create a rotation for object
_signedAngle = Vector3.SignedAngle(flatPrevHitDir, flatCurrentHitDir, axisToNormal[_planeNormal]);
//Check if interact button is held.
while (_isInteractHeld)
{
//Update eye position
eyeForward = (GameManager.Instance._Character.cameraTransform.position + GameManager.Instance._Character.GetEyeForwardVector());
//Update current hit direction and flatten it
currentHitDir = eyeForward - transform.position;
flatCurrentHitDir = Vector3.ProjectOnPlane(currentHitDir, axisToNormal[_planeNormal]).normalized;
//Update sign angle by comparing prev hit and current hit
_signedAngle = Vector3.SignedAngle(flatCurrentHitDir, flatPrevHitDir, axisToNormal[_planeNormal]);
//Add to the full angle to keep track of number of turns
newAngle = Mathf.Atan2(currentHitDir.y, currentHitDir.x) * Mathf.Rad2Deg;
_fullAngle += Mathf.DeltaAngle(_angle, newAngle);
_angle = newAngle;
//Check if angle is past turn threshold
if(_signedAngle < -1f || _signedAngle > 1f)
{
//Create a rotation amount based on signed angle and reset the angle to prevent a constant rotation.
while (flatPrevHitDir != flatCurrentHitDir)
{
transform.Rotate(axisToRBNormal[_lookAtDir], _signedAngle * _speed, Space.World);
_signedAngle = 0f;
flatPrevHitDir = flatCurrentHitDir;
yield return new WaitForEndOfFrame();
}
}
yield return new WaitForFixedUpdate();
}
_isMoving = false;
}
To summarize for future self:
Create a Transform.Rotate rotation angle based on previous direction and current direction originated from transform.position.
Reset the angle when rotation gets executed so it doesn’t constantly rotate.
Store the full angle separately to keep turn counts.