Problem:
Dynamic animations based upon the speed of movement.
Assets used:
Futuristic, floating basketball hoop, hung in air by thrusters
First the hoop rotates a certain angle to point its thrusters in the desired move direction. Before this rotation finishes, it starts moving.
Towards the end of the movement, it reverses rotation direction to point the thrusters in the opposite direction to simulate stopping force. This rotation should end before the hoop arrives at the final location.
Finally, when the hoop finishes the movement, it rotates back to the idle position.
Flowchart for reference:

Naturally, the hoop’s speed should start slow, gain speed as it moves, then slow down towards the end. The rotations should do the same, to simulate real world movement.
Each movement cycle needs a certain angle to rotate based upon desired speed, which is based upon move distance. If the hoop moves a very small amount, it only needs to rotate a small angle to accomplish this.
My initial thought was to use Unity’s Animation.SetCurve, but that got real messy real quick. It seemed like I was playing with something I shouldn’t. Also, the transform of the object cannot be moved if an animation is applied to the transform.
The alternative: Coroutines!
First the rotation animation. Since the only difference between the 3 rotations is the angle, I just need one method which takes the angle as a parameter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
public class HoopRotationController : MonoBehaviour { const float maximumAngle = 30f; // Max angle to rotate const float rotateTimeToMaximumAngle = 0.4f; // Time to rotate to max angle Transform trans; // Hoop transform void Awake () { trans = this.transform; } IEnumerator Rotate (float angleToRotate) { // Get final rotation for lerp Quaternion finalRotation = Quaternion.AngleAxis (angleToRotate, Vector3.back); finalRotation *= trans.localRotation; // Get initial rotation for lerp Quaternion startRotation = trans.localRotation; // Get percent of max angle for rotate time float percentOfMaxAngle = Mathf.Abs (angleToRotate / maximumAngle); // Get rotate time float timeToRotate = rotateTimeToMaximumAngle * percentOfMaxAngle; // Initialize lerp parameter float t = 0f; while (t <= 1.0f) { // Amount to lerp float framePercentOfTime = Time.deltaTime / timeToRotate; // Add to current lerp amount t += framePercentOfTime; trans.localRotation = Quaternion.Lerp (startRotation, finalRotation, t); yield return null; } } } |
This method will rotate the object within a specific time. If you look at the flowchart, you’ll see the object movement will start during the initial rotation. Therefore we need to implement a flag to tell a movement script to begin movement. This can simply be a bool which a movement script constantly checks to see if it’s true. We’ll call this startMoving. Another variable we need is a float that represents the time the startMoving bool should be turned on. This can be called startMovementPosition. Finally, we implement it by placing a if statement in the while statement:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
public class HoopRotationController : MonoBehaviour { const float maximumAngle = 30f; // Max angle to rotate const float rotateTimeToMaximumAngle = 0.4f; // Time to rotate to max angle Transform trans; // Hoop transform public bool startMoving; float startMovementPosition = 0.5f; void Awake () { trans = this.transform; } IEnumerator Rotate (float angleToRotate) { // Get final rotation for lerp Quaternion finalRotation = Quaternion.AngleAxis (angleToRotate, Vector3.back); finalRotation *= trans.localRotation; // Get initial rotation for lerp Quaternion startRotation = trans.localRotation; // Get percent of max angle for rotate time float percentOfMaxAngle = Mathf.Abs (angleToRotate / maximumAngle); // Get rotate time float timeToRotate = rotateTimeToMaximumAngle * percentOfMaxAngle; // Initialize lerp parameter float t = 0f; while (t <= 1.0f) { // Amount to lerp float framePercentOfTime = Time.deltaTime / timeToRotate; // Add to current lerp amount t += framePercentOfTime; trans.localRotation = Quaternion.Lerp (startRotation, finalRotation, t); if (smoothInterpolate > startMovingPosition) { startMoving = true; } yield return null; } } } |
Since each of these steps in the movement process are all associated with the same, singular, movement, it makes sense to be in a Finite State Machine. Explanation of this is beyond the scope of this post, but there are plenty of resources out there on the internet.
A state machine contains individual steps, which are represented by each box in the flowchart. In each step, an enter method is initially run once, then an execute method is run continuously, and finally an exit method is run when the state is changed:
1 2 3 4 5 6 7 |
public void ChangeState (IFSMState<T> NewState) { PreviousState = CurrentState; PreviousState.Exit (Owner); CurrentState = NewState; CurrentState.Enter (Owner); } |
For the first initial rotation:
1 2 3 4 5 6 7 8 9 10 11 |
public void Enter (HoopLateralMovementController control) { control.InitialRotation (); } public void Execute (HoopLateralMovementController control) { control.CheckReadyToMove (); } public void Exit (HoopLateralMovementController control) { return; } |
Which is implemented like:
1 2 3 4 5 6 7 8 9 |
public void InitialRotation () { StartCoroutine (hoopRotation.StartRotation (currentMoveAmount)); } public void CheckReadyToMove () { if (hoopRotation.startMoving) { ChangeState (MovingState.Instance); } } |
Now the Moving State:
1 2 3 4 5 6 7 8 9 10 11 |
public void Enter (HoopLateralMovementController control) { control.StartMoving (); } public void Execute (HoopLateralMovementController control) { control.CheckReadyForReverseRotation (); } public void Exit (HoopLateralMovementController control) { return; } |
And again, the HoopLateralMovementController:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public void StartMoving () { currentSpeed = hoopRotation.MaximumAnglePercent * maxSpeed; StartCoroutine (Coroutine ()); } public void CheckReadyForReverseRotation () { float timeRemaining = Mathf.Abs ((finalPosition.x - trans.position.x)/currentSpeed); float rotationTimeNeeded = hoopRotation.ReverseAngleRotationTime (); timeRemaining *= 0.9f; if (timeRemaining <= rotationTimeNeeded) { ChangeState (ReverseRotateState.Instance); } else { currentSpeed = hoopRotation.MaximumAnglePercent * maxSpeed; } } } |
The CheckReadyForReverseRotation method that the MovingState continuously checks is key for the animation to behave, and thus look correct. The timeRemaining is the time left in the object movement. The rotationTimeNeeded is the amount of time needed to perform the reverse rotation. Since the reverse rotation needs to finish before the movement finishes, the timeRemaining is multiplied by 0.9f, which is 90% of the time. Then, if the new timeRemaining is less than or equal to the rotation time needed, the reverse rotation is started. The rotation will then finish right before the movement finishes.
The ReverseRotate state:
1 2 3 4 5 6 7 8 9 10 11 |
public void Enter (HoopLateralMovementController control) { control.StartReverseRotation (); } public void Execute (HoopLateralMovementController control) { control.CheckReadyForFinalRotation (); } public void Exit (HoopLateralMovementController control) { return; } |
And subsequent methods in HoopLateralMovementController:
1 2 3 4 5 6 7 8 9 |
public void StartReverseRotation () { StartCoroutine(hoopRotation.ReverseRotation ()); } public void CheckReadyForFinalRotation () { if (hoopRotation.currentlyRotating) { ChangeState (FinalRotateState.Instance); } } |
In the HoopRotation script:
1 2 3 4 5 6 |
public IEnumerator ReverseRotation () { yield return new WaitUntil (() => !currentlyRotating); float angleToRotate = Quaternion.Angle (initialRotation, trans.localRotation) * rotateBackDirection; angleToRotate *= 2f; yield return StartCoroutine (Rotate (angleToRotate)); } |
Like the startMoving flag set in the initial rotation, a similar flag called currentlyRotating in the movement coroutine is set to false, which signals the final rotation to trigger.
1 2 3 4 5 6 7 8 9 10 11 |
public void Enter (HoopLateralMovementController control) { control.StartFinalRotation (); } public void Execute (HoopLateralMovementController control) { return; } public void Exit (HoopLateralMovementController control) { return; } |
1 2 3 |
public void StartFinalRotation () { StartCoroutine (hoopRotation.RotateToInitial ()); } |
1 2 3 4 5 6 |
public IEnumerator RotateToInitial () { yield return new WaitUntil (() => !currentlyRotating); rotateBackDirection *= -1; float angleToRotate = Quaternion.Angle (initialRotation, trans.localRotation) * rotateBackDirection; StartCoroutine (Rotate (angleToRotate)); } |