I'm fairly new to unity, and after scraping through threads about rotations through frames, I managed to find code to help me get the look that I wanted for my game, here's the code for the movement:
public class Movement : MonoBehaviour
{
IEnumerator RotateOctagon(Vector3 byAngles, float inTime)
{
var fromAngle = transform.rotation;
var toAngle = Quaternion.Euler(transform.eulerAngles + byAngles);
for(var t = 0f; t <= 1; t+= Time.deltaTime / inTime)
{
transform.rotation = Quaternion.Lerp(fromAngle, toAngle, t);
yield return null;
}
transform.rotation = toAngle;
}
// Update is called once per frame
void Update()
{
if (Input.GetKeyDown("left"))
{
StartCoroutine(RotateOctagon(Vector3.forward * 45, 0.1f));
}
if (Input.GetKeyDown("right"))
{
StartCoroutine(RotateOctagon(Vector3.forward * -45, 0.1f));
}
if (Input.GetKeyDown("up"))
{
StartCoroutine(RotateOctagon(Vector3.forward * 180, 0.1f));
}
}
}
And it generates desired results - putting the octagon to its next side, assuming you wait until the rotation is finished...
But The problem arises when you press left or right in the middle of the animation, in which the octagon incorrectly ends up on a point or weird angle (not a multiple of 45 degrees)...
My guess is that if the octagon is in the middle of the coroutine of rotating to the right say, at 30 degrees, any immediate key press will now rotate from that angle, ending up being 30+- 45 an unwanted angle.
I considered calculating the offset of the angle to the lower/higher 45 degree angle multiple, but I feel like there's a more correct approach and something I'm missing about coroutines that would help this.
Any help on how to get the octagon to rotate properly under the condition that multiple keys are pressed would be greatly appreciated.
Desired behavior: Say the current z axis is 0. After right is pressed and in the middle of rotating, left is pressed at 30 degrees. The octagon should rotate back to z=0
In order to simply not allow a new routine until the previous one has finished simply introduce a bool flag
private bool isRotating;
IEnumerator RotateOctagon(Vector3 byAngles, float inTime)
{
if(isRotating) yield break;
isRotating = true;
var fromAngle = transform.rotation;
var toAngle = Quaternion.Euler(transform.eulerAngles + byAngles);
for(var t = 0f; t <= 1; t+= Time.deltaTime / inTime)
{
transform.rotation = Quaternion.Lerp(fromAngle, toAngle, t);
yield return null;
}
transform.rotation = toAngle;
isRotating = false;
}
This way your input is basically ignored until the previous routine finished.
In order to be more efficient you can then also add
void Update()
{
if(isRotating) return;
...
}
Or the "fancy" alternative without additional flag: Make Start itself a Coroutine and do
IEnumerator Start ()
{
while(true)
{
if (Input.GetKeyDown("left"))
{
// This now rotates and waits at the same time until the rotation is finished
yield return RotateOctagon(Vector3.forward * 45, 0.1f));
}
else if (Input.GetKeyDown("right"))
{
yield return RotateOctagon(Vector3.forward * -45, 0.1f));
}
else if (Input.GetKeyDown("up"))
{
yield return RotateOctagon(Vector3.forward * 180, 0.1f));
}
else
{
// If none of the previous is true wait at least one frame
yield return null;
}
}
}
Alternatively since now we know you want to rather interrupt the current rotation and start a new one store it in a field like
private float rotation;
void Awake()
{
rotation = transform.localEulerAngles.z;
}
void Update()
{
if (Input.GetKeyDown("left"))
{
RotateAbout (45);
}
if (Input.GetKeyDown("right"))
{
RotateAbout (-45);
}
if (Input.GetKeyDown("up"))
{
RotateAbout (180);
}
}
private void RotateAbout(float angle)
{
StopAllCoroutines ();
rotation += angle;
StartCoroutine(RotateOctagon(rotation, 0.1f));
}
and then rather do
IEnumerator RotateOctagon(float toAngle, float inTime)
{
var fromRotation = transform.localRotation;
var toRotation = Quaternion.Euler(Vector3.forward * toAngle);
for(var t = 0f; t <= 1; t+= Time.deltaTime / inTime)
{
transform.localRotation = Quaternion.Lerp(fromRotation, toRotation, t);
yield return null;
}
transform.localRotation = toRotation;
}
Related
Basically I'm trying to create a rolling system for my top down RPG style game, but when I do the Vector3.Lerp it doesn't move the entire way
The code is:
public void CheckSpaceKey()
{
if (exhausted == false)
{
if (Input.GetKey(KeyCode.Space) && canRoll)
{
if (rollDir == RollDir.Up)
{
Vector3 rollStartPos = new Vector3(transform.position.x, transform.position.y, -1.0f);
Vector3 rollEndPos = new Vector3(transform.position.x, transform.position.y + 3, -1.0f);
transform.position = Vector3.Lerp(rollStartPos, rollEndPos, Time.deltaTime);
canRoll = false;
playerStats.stamina -= 10;
Invoke("RollCooldown", 1.5f);
}
}
}
}
public void RollCooldown()
{
canRoll = true;
}
This should be making my player move upwards 3 units, but it instead is moving a random number of around 0.35 to 0.45.
Also CheckSpaceKey is being called in update
Lerp doesn't do what you think it does. Assuming Time.deltaTime is about 1/60, it's going to move the unit 1/60th of the way to the destination, and that's it.
Consider using a coroutine that updates the t parameter for Lerp with each frame:
public void CheckSpaceKey()
{
// by the way, this could just be `if (!exhausted)`
// although some coding guidelines require this format below
if (exhausted == false)
{
if (Input.GetKey(KeyCode.Space) && canRoll)
{
if (rollDir == RollDir.Up)
{
StartCoroutine(DoRoll(Vector3.up * 3));
}
}
}
}
IEnumerator DoRoll(Vector3 offset)
{
float duration = 1f;
canRoll = false;
playerStats.stamina -= 10;
Invoke("RollCooldown", 1.5f);
Vector3 rollStartPos = new Vector3(transform.position.x, transform.position.y, -1.0f);
Vector3 rollEndPos = rollStartPos + offset;
float t = 0;
while (t < 1f)
{
t = Mathf.Min(1f, t + Time.deltaTime/duration);
transform.position = Vector3.Lerp(rollStartPos, rollEndPos, t);
yield return null;
}
}
public void RollCooldown()
{
canRoll = true;
}
This will not move 3 Units, you call
transform.position only inside the if case and access the if case only once every 1.5 seconds.
Lerp will not update your position every frame but only the frame it is called, if you the object to move constantly you have to update your position constantly you should crate rollStartPos and rollEndPos inside the first if and create add
else{
time += Time.deltaTime;
transform.position = Vector3.Lerp(rollStartPos, rollEndPos, time);
}
under the 2nd if. This way it will lerp til rollEndPos unless the ball(?) is exhausted, if it should roll even when it's exhausted you need to update the position outside the first if
I've got a setup right now where I have a 3D object with an empty object parented to it inside the object. When you press one button, an empty object that the camera is following rotates 90 degrees. If you press the other button, it rotates the other direction in 90 degrees. The result is that the camera can spin around the object 4 times before it makes a complete rotation.
Currently it works well, but I'm trying to figure out how I add some easing to the animation so it doesn't look so rough. I know a little about working with curves in animation but I'm not sure how I can apply that to code (or if it's even the best way to do things)
public InputMaster controls;
private bool isSpinning;
private float rotationSpeed = 0.3f;
IEnumerator RotateMe(Vector3 byangles, float intime)
{
var fromangle = transform.rotation;
var toangle = Quaternion.Euler(transform.eulerAngles + byangles);
for (var t = 0f; t < 1; t += Time.deltaTime / intime)
{
transform.rotation = Quaternion.Slerp(fromangle, toangle, t);
yield return null;
transform.rotation = toangle;
}
Debug.Log("finished rotation");
isSpinning = false;
Debug.Log("isSpinning now false");
}
code above is where I create a coroutine that says how it is going to transform. One thing that confuses me here a little is that if I don't have the line that says transform.rotation = toangle; , the rotation comes out at like 89.5 degrees or something, and if you do it multiple times it goes several degrees off. Not sure why that happens.
void Rotation(float amount)
{
Debug.Log("rotation number is " + amount);
float holdingRotate = controls.Player.Camera.ReadValue<float>();
if (holdingRotate == 1 && isSpinning == false)
{
isSpinning = true;
Debug.Log("isSpinning now true");
StartCoroutine(RotateMe(Vector3.up * 90, rotationSpeed));
}
else if (holdingRotate == -1 && isSpinning == false)
{
isSpinning = true;
Debug.Log("isSpinning now true");
StartCoroutine(RotateMe(Vector3.up * -90, rotationSpeed));
}
}
and this part is where the animation gets called up. Any help much appreciated.
Have a look at Animation Curves like #immersive said.
You can easily define your own curves in inspector
simply by adding this to your code:
public AnimationCurve curve;
You can then use AnimationCurve.Evaluate
to sample the curve.
The alternative is to use a premade library from the AssetStore/Package Manager like DOTween or LeanTween to name some.
Comments inline:
public class SmoothRotator : MonoBehaviour
{
// Animation curve holds the 'lerp factor' from starting angle to final angle over time
// (Y axis is blend factor, X axis is normalized 'time' factor)
public AnimationCurve Ease = AnimationCurve.EaseInOut(0, 0, 1, 1);
public float Duration = 1f;
private IEnumerator ActiveCoroutine = null;
public void RotateToward(Quaternion targetAngle) {
// If there's a Coroutine to stop, stop it.
if (ActiveCoroutine != null)
StopCoroutine(ActiveCoroutine);
// Start new Coroutine and cache the IEnumerator in case we need to interrupt it.
StartCoroutine(ActiveCoroutine = Rotator(targetAngle));
}
public IEnumerator Rotator(Quaternion targetAngle) {
// Store starting angle
var fromAngle = transform.rotation;
// Accumulator for Time.deltaTime
var age = 0f;
while (age < 1f) {
// normalize time (scale to "percentage complete") and clamp it
var normalisedTime = Mathf.Clamp01(age / Duration);
// Pull lerp factor from AnimationCurve
var lerpFactor = Ease.Evaluate(normalisedTime);
// Set rotation
transform.rotation = Quaternion.Slerp(fromAngle, targetAngle, lerpFactor);
// Wait for next frame
yield return null;
// Update age with new frame's deltaTime
age += Time.deltaTime;
}
// Animation complete, force true value
transform.rotation = targetAngle;
// Housekeeping, clear ActiveCoroutine
ActiveCoroutine = null;
}
}
for (var t = 0f; t < 1; t += Time.deltaTime / intime)
{
transform.rotation = Quaternion.Slerp(fromangle, toangle, t);
yield return null;
}
transform.rotation = toangle;
Because t won't equal to 1.
You just need to set the rotation to the target after the loop.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ScaleRotate : MonoBehaviour
{
public Vector3 minScale;
public Vector3 maxScale;
public float duration;
private bool scaling = true;
private void Start()
{
StartCoroutine(ScaleOverSeconds(maxScale, duration));
}
public IEnumerator ScaleOverSeconds(Vector3 scaleTo, float seconds)
{
float elapsedTime = 0;
Vector3 startingScale = transform.localScale;
while (elapsedTime < seconds)
{
transform.localScale = Vector3.Lerp(startingScale, scaleTo, (elapsedTime / seconds));
elapsedTime += Time.deltaTime;
yield return new WaitForEndOfFrame();
}
transform.localScale = scaleTo;
}
private void Update()
{
if(Input.GetKeyDown(KeyCode.S))
{
if(scaling == true)
{
StartCoroutine(ScaleOverSeconds(minScale, duration));
scaling = false;
}
}
}
}
Now it scales to max and when S is pressed to min.
But I want when I press on S it will scale to max/min each time pressing the S key.
And also I want when the object scales that it will rotate by 180 degrees according to the scaling duration.
I don't fully get the rotation part but as I understand you rather want to toggle the scaling so something like e.g.
// flag for the scale direction
private bool scaleToMax;
// flag for checking if already scaling to avoid concurrent routines
private bool alreadyScaling;
private void Start()
{
// initial scale to max
ToggleScale();
}
private void Update()
{
// first check if another routine is already running
// -> ignore/lock input
if(!alreadyScaling && Input.GetKeyDown(KeyCode.S))
{
ToggleScale();
}
}
// Invert the scale direction and start aroutine
private void ToggleScale()
{
if(alreadyScaling)
{
Debug.LogWarning("Already scaling -> Ignored!", this);
return;
}
// invert the scale direction
scaleToMax = !scaleToMax;
StartCoroutine(ScaleOverSeconds(scaleToMax ? maxScale : minScale, duration));
// This is basically a shortcut for doing something like
//Vector3 targetScale;
//if(scaleToMax)
//{
// targetScale = maxScale;
//}
//else
//{
// targetScale = minScale;
//}
//StartCoroutine(ScaleOverSeconds(targetScale, duration));
}
private IEnumerator ScaleOverSeconds(Vector3 scaleTo, float seconds)
{
// block concurrent routines
if(alreadyScaling) yield break;
alreadyScaling = true;
var startingScale = transform.localScale;
var initialRotation = transform.rotation;
// Here it is a bit unclear which axis you want to rotate on but you could do e.g.
var targetRotation = initialRotation * Quaternion.Euler(0, 180, 0);
var elapsedTime = 0f;
while (elapsedTime < seconds)
{
var factor = elapsedTime / seconds;
// Just fyi here you can also add some smoothing at the beginning and end e.g.
//factor = SmoothStep(0, 1, factor);
transform.localScale = Vector3.Lerp(startingScale, scaleTo, factor);
transform.rotation = Quaternion.Lerp(initialRotation, targetRotation, factor);
elapsedTime += Time.deltaTime;
// Unless you explicitly need the end of frame yielding null is just fine
yield return null;
}
transform.localScale = scaleTo;
transform.rotation = targetRotation;
// allow the next routine to start
alreadyScaling = false;
}
to make this you can just use else operator in your void Update()
And for rotation Unity uses Quaternions, which are math abstractions with 3 imaginary units and one real unit. If you want, you can read about it more
https://en.wikipedia.org/wiki/Quaternion
https://docs.unity3d.com/ScriptReference/Quaternion.html
but it's really not easy to understand so Unity provides localEulerAngles for easier rotation. It works just as Vector3 (you can see it in inspector in Transform.rotation field)
So, what will do our changed code:
if object has max scale (scaling == true), it will change to min scale and rotate to 180 degrees (clockwise) by Y-axis
And if object has min scale (scaling == false), it will change to max scale and rotate to 180 degrees (also clockwise) by Y-axis
If you want to change second rotation to counterclockwise, you can change second rotation from 180 degrees to -180 degrees.
Also it's good idea to not change size if size is changing right now. For example if object is becoming bigger right now and we press 'S' to make it smaller, it will be at the same time trying to become bigger and smaller and the result could be very strange!
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ScaleRotate : MonoBehaviour
{
//It's better to use private fields. If you want to see it in inspector, use SerializeField
[SerializeField]
private Vector3 minScale;
[SerializeField]
private Vector3 maxScale;
[SerializeField]
private float duration;
private bool scaling = true;
//Variable to set angle to rotate your object (in degrees)
[SerializeField]
private float rotationAngle = 180.0f;
//Bool which says: if object is changing it's size right now
private bool isInProcess = false;
private void Start()
{
StartCoroutine(ScaleOverSeconds(maxScale, duration));
}
public IEnumerator ScaleOverSeconds(Vector3 scaleTo, Vector3 rotateTo, float seconds)
{
isInProcess = true;
float elapsedTime = 0;
Vector3 startingScale = transform.localScale;
Vector3 startingRotation = transform.localEulerAngles;
//If you want, you can change axis of rotation or angle or everything you want. But what I do - I rotate by Y-axis for 180 degrees
while (elapsedTime < seconds)
{
transform.localScale = Vector3.Lerp(startingScale, scaleTo, (elapsedTime / seconds));
transform.localEulerAngles = Vector3.lerp(startingRotation, rotateTo, (elapsedTime / seconds));
elapsedTime += Time.deltaTime;
//You can use yield return null instead of yield return new WaitForEndOfFrame() - this will do the same, but it's easier to write
yield return null;
}
transform.localScale = scaleTo;
transform.localEulerAngles = rotateTo;
isInProcess = false;
}
private void Update()
{
if(Input.GetKeyDown(KeyCode.S))
{
//Check if object is not changing it's scale right now
if(!isInProcess){
//Use if(scaling) instead of if(scaling == true) - this means the same, but it's more readable
if(scaling)
{
Vector3 rotateTo = transform.localEulerAngles + new Vector3(0, rotationAngle, 0);
StartCoroutine(ScaleOverSeconds(minScale, rotateTo, duration));
//Remove scaling = false (we will paste it later)
}
//Add there else operator
else {
//If you want to change rotation to counterclockwise, change '+' to '-'
Vector3 rotateTo = transform.localEulerAngles + new Vector3(0, rotationAngle, 0);
StartCoroutine(ScaleOverSeconds(maxScale, rotateTo, duration));
}
//Change scaling value. If you want, you can move this line into ScaleOverSeconds coroutine
scaling = !scaling;
}
}
}
}
(Once it reaches point B, it goes to point A and back to point B in a smooth and orderly fashion). For some reason, the platform refuses to move and stays put. I have tried many things such as using vector3.movetowards and much more but nothing makes it move.
Here is the code. (Point A and Point B are empty game objects that are not parented to the platform)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MoveTwoTransforms : MonoBehaviour
{
public Transform pointA;
public Transform pointB;
bool HeadingtowardsB;
bool HeadingtowardsA;
public float speed = 10;
// Start is called before the first frame update
void Start()
{
transform.position = pointA.position;
HeadingtowardsB = true;
HeadingtowardsA = false;
GlideAround();
}
// Update is called once per frame
void Update()
{
}
public IEnumerator GlideAround()
{
while (true)
{
while ((Mathf.Abs((pointB.position.x - transform.position.x) + (pointB.position.y - transform.position.y)) > 0.05f) && HeadingtowardsB == true && HeadingtowardsA==false )
{
yield return new WaitForEndOfFrame();
transform.position = Vector2.Lerp(transform.position, pointB.position, speed * Time.deltaTime);
if(Mathf.Abs((pointB.position.x - transform.position.x) + (pointB.position.y - transform.position.y)) > 0.05f)
{
HeadingtowardsB = false;
HeadingtowardsA = true;
}
}
HeadingtowardsB = false;
HeadingtowardsA = true;
while (Mathf.Abs((pointA.position.x - transform.position.x) + (pointA.position.y - transform.position.y)) > 0.05f && HeadingtowardsA==true && HeadingtowardsB==false)
{
yield return new WaitForEndOfFrame();
transform.position=transform.position=Vector3.Lerp(transform.position, pointA.position, speed*Time.deltaTime);
}
}
}
}
There are no error messages, the platform won't move. The platform is still colliding and it seems to behave like a normal platform.
GlideAround() is an IEnumerator and can not be called like a method. You have to start it using StartCoroutine
StartCoroutine(GlideAround());
Also note that speed * Time.deltaTime makes little sense for usage in Lerp. You usually would want a constant value between 0-1 in your case (since you re-use the current position as first parameter).
E.g. a value of 0.5 means: Every frame set the new position to the center between the current and the target position.
Since you catch it using a threashold of 0.05f this should be fine but in general I wouldn't use Lerp like this ... with very small values you might never really reach the target position.
I would therefore prefer to either control the constant speed and use
bool isHeadingA = true;
while(true)
{
// if it was intended you can ofourse also again use
// Vector2.Distance(transform.position, isHeadingA ? pointA.position : pointB.position) <= 0.05f)
while (transform.position != (isHeadingA ? pointA.position : pointB.position))
{
yield return new WaitForEndOfFrame();
transform.position = Vector2.MoveTowards(transform.position, isHeadingA ? pointA.position : pointB.position, speed * Time.deltaTime);
}
// flip the direction
isHeadingA = !isHeadingA;
}
!= has a precision of 0.00001 and is fine here since MoveTowards avoids overshooting so at some point it will surely reach the position if speed != 0.
Or alternatively you can use Lerp if you rather want to control the duration of the movement with a smoothed in and out speed using e.g. Mathf.PingPong as factor and Mathf.SmoothStep for easing in and out like
while(true)
{
yield return new WaitForEndOfFrame();
// linear pingpong between 0 and 1
var factor = Mathf.PingPong(Time.time, 1);
// add easing at the ends
factor = Mathf.SmoothStep(0, 1, factor);
// optionally add even more easing ;)
//factor = Mathf.SmoothStep(0, 1, factor);
transform.position = Vector2.Lerp(pointA.position, pointB.position, factor);
}
Try this:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MoveTwoTransforms : MonoBehaviour
{
public Transform pointA;
public Transform pointB;
bool HeadingtowardsB;
bool HeadingtowardsA;
public float speed = 10;
// Start is called before the first frame update
void Start()
{
transform.position = pointA.position;
HeadingtowardsB = true;
HeadingtowardsA = false;
StartCoroutine(GlideAround());
}
// Update is called once per frame
void Update()
{
}
public IEnumerator GlideAround()
{
//Because we want a specific speed, the % between the two points
//that we should be at will be equal to (time * speed) / distance
//with an adjustment for going backwards.
float distance = Vector3.Distance(pointA, pointB) * 2;
float lapTime = distance / speed;
float startTime = Time.time;
Debug.Log("The platform speed is: " + speed.ToString());
Debug.Log("The distance for one full lap is: " + distance.ToString());
Debug.Log("One lap will take: " + lapTime.ToString() + " seconds");
while (true)
{
yield return new WaitForEndOfFrame();
float elapsedTime = (Time.time - startTime) % lapTime;
float progress = elapsedTime / (lapTime / 2);
if (progress > 1){
progress = 2 - progress;
}
Debug.Log("The platform speed is currently: " + progress.ToString() + "% between pointA and pointB");
transform.position = Vector2.Lerp(pointA.position, pointB.position, progress);
}
}
As I mentioned in the comments, you Lerp uses a percentage between two points as the return value. You need to give it a percent, not a speed.
Here is an implementation that uses speed, like you wanted, but #derHugo's answer with PingPong is much simpler!
The main issue is that you are not using coroutines properly, as pointed out in derHugo's answer.
However, I'll provide my own answer, seeing that you are making the rookie mistake of way over-engineering this problem.
I think teaching by example might be the most appropriate in this case, so here it is:
If the points dictating the platform's movement are static, you should do this with animation. I won't explain it here. Tutorials like this one can easily be found all over the unity tutorials, unity forums, other StackOverflow Q&As, and youtube.
If your points are dynamic, this is more than enough:
public class MoveTwoTransforms : MonoBehaviour {
public Transform pointA;
public Transform pointB;
public float speed = 10;
void Start() {
transform.position = pointA.position;
StartCoroutine(GlideAround());
}
private IEnumerator MoveTowards(Vector3 targetPosition) {
while (transform.position != targetPosition) {
transform.position = Vector3.MoveTowards(transform.position, targetPosition, speed * Time.deltaTime);
yield return null;
}
}
private IEnumerator GlideAround() {
while(true) {
yield return StartCoroutine(MoveTowards(pointA));
yield return StartCoroutine(MoveTowards(pointB));
}
}
}
Just a final note:
If the platform should have physics or a collider, it is preferable to add a Rigidbody, set it to be kinematic, and to the movement by setting the Rigidbody.position instead of the transform. This is because that is updated on the physics loop (FixedUpdate) rather than the frame loop (Update), and avoids some bugs related to asyncrony between the physics system and moving objects through transform's position.
rigidbody.position = Vector3.MoveTowards(rigidbody.position, //...
Here is the code that worked.
public IEnumerator GlideAround()
{
while (true)
{
while (HasReachedA == false)
{
yield return new WaitForEndOfFrame();
transform.position = Vector2.Lerp(transform.position, pointA.position, 0.01f);
if ((Mathf.Abs(Vector2.Distance(pointA.position, transform.position)) < 0.01f))
{
HasReacedB = false;
HasReachedA = true;
}
}
while (HasReacedB == false)
{
yield return new WaitForEndOfFrame();
transform.position = Vector2.Lerp(transform.position, pointB.position, 0.01f);
if ((Mathf.Abs(Vector2.Distance(pointB.position, transform.position)) < 0.01f))
{
HasReacedB = true;
HasReachedA = false;
}
}
}
}
If I set the rotation speed to 5 for example it will rotate facing the next target waypoint and then will move to it. But the camera rotation will be too fast.
Changing the speed to 0.01 make it rotating in a good slowly smooth speed. But then at 0.01 the camera rotate facing the next waypoint but never move to it. It stay on place.
This is the waypoints script:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Waypoints : MonoBehaviour
{
private GameObject[] waypoints;
private Transform currentWaypoint;
private enum CameraState
{
StartRotating,
Rotating,
Moving,
Waiting
}
private CameraState cameraState;
public GameObject player;
public float speed = 5;
public float WPradius = 1;
public LookAtCamera lookAtCam;
void Start()
{
cameraState = CameraState.StartRotating;
}
void Update()
{
switch (cameraState)
{
// This state is used as a trigger to set the camera target and start rotation
case CameraState.StartRotating:
{
// Sanity check in case the waypoint array was set to length == 0 between states
if (waypoints.Length == 0)
break;
// Tell the camera to start rotating
currentWaypoint = waypoints[UnityEngine.Random.Range(0, waypoints.Length)].transform;
lookAtCam.target = currentWaypoint;
lookAtCam.setTime(0.0f);
cameraState = CameraState.Rotating;
break;
}
// This state only needs to detect when the camera has completed rotation to start movement
case CameraState.Rotating:
{
if (lookAtCam.IsRotationFinished)
cameraState = CameraState.Moving;
break;
}
case CameraState.Moving:
{
// Move
transform.position = Vector3.MoveTowards(transform.position, currentWaypoint.position, Time.deltaTime * speed);
// Check for the Waiting state
if (Vector3.Distance(currentWaypoint.position, transform.position) < WPradius)
{
// Set to waiting state
cameraState = CameraState.Waiting;
// Call the coroutine to wait once and not in CameraState.Waiting
// Coroutine will set the next state
StartCoroutine(WaitForTimer(3));
}
break;
}
case CameraState.Waiting:
// Do nothing. Timer has already started
break;
}
}
IEnumerator WaitForTimer(float timer)
{
yield return new WaitForSeconds(timer);
cameraState = CameraState.StartRotating;
}
public void RefreshWaypoints()
{
waypoints = GameObject.FindGameObjectsWithTag("Target");
}
}
And the look at camera script:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LookAtCamera : MonoBehaviour
{
// Values that will be set in the Inspector
public Transform target;
public float RotationSpeed;
private float timer = 0.0f;
public bool IsRotationFinished
{
get { return timer > 0.99f; }
}
// Update is called once per frame
void Update()
{
if (target != null && timer < 0.99f)
{
// Rotate us over time according to speed until we are in the required rotation
transform.rotation = Quaternion.Slerp(transform.rotation,
Quaternion.LookRotation((target.position - transform.position).normalized),
timer);
timer += Time.deltaTime * RotationSpeed;
}
}
public void setTime(float time)
{
timer = time;
}
}
Problem
Your script basically works! The problem is in
private void Update()
{
if (target != null && timer < 0.99f)
{
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation((target.position - transform.position).normalized), timer);
timer += Time.deltaTime * RotationSpeed;
}
}
there are two issues with that:
You add Time.deltaTime * RotationSpeed so the time it takes to reach the 1 or in your case 0.99 simply takes 1/RotationSpeed = 100 times longer than usual. So your camera will stay in the Rotating state for about 100 seconds - after that it moves just fine!
(This one might be intentional but see below for a Better Solution) Quaternion.Slerp interpolates between the first and second rotation. But you always use the current rotation as startpoint so since the timer never reaches 1 you get a very fast rotation at the beginning but a very slow (in fact never ending) rotation in the end since the distance between the current rotation and the target rotation gets smaller over time.
Quick-Fixes
Those fixes repair your current solution but you should checkout the section Better Solution below ;)
In general for comparing both float values you should rather use Mathf.Approximately and than use the actual target value 1.
if (target != null && !Mathf.Approximately(timer, 1.0f))
{
//...
timer += Time.deltaTime * RotationSpeed;
// clamps the value between 0 and 1
timer = Mathf.Clamp01(timer);
}
and
public bool IsRotationFinished
{
get { return Mathf.Approximately(timer, 1.0f); }
}
You should either use Quaternion.Slerp storing the original rotation and use it as first parameter (than you will see that you need a way bigger RotationSpeed)
private Quaternion lastRotation;
private void Update()
{
if (target != null && !Mathf.Approximately(timer, 1.0f))
{
transform.rotation = Quaternion.Slerp(lastRotation, Quaternion.LookRotation((target.position - transform.position).normalized), timer);
timer += Time.deltaTime * RotationSpeed;
}
else
{
lastRotation = transform.rotation;
}
}
Or instead of Quaternion.Slerp use Quaternion.RotateTowards like
transform.rotation = Quaternion.RotateTowards(transform.rotation, Quaternion.LookRotation((target.position - transform.position).normalized), RotationSpeed * Time.deltaTime);
Better Solution
I would strongly suggest to use the Coroutines for everything instead of handling this kind of stuff in Update. They are way easier to control and makes your code very clean.
Look how your scripts would shrink and you wouldn't need all the properties, fields and comparing floats anymore. You could do most things you are currently getting and setting to wait for a certain thing to happen in only a few single lines.
In case you didn't know: You can actually simply yield return another IEnumerator on order to wait for it to finish:
Waypoints
public class Waypoints : MonoBehaviour
{
private GameObject[] waypoints;
public GameObject player;
public float speed = 5;
public float WPradius = 1;
public LookAtCamera lookAtCam;
private Transform currentWaypoint;
private void Start()
{
// maybe refresh here?
//RefreshWaypoints();
StartCoroutine(RunWaypoints());
}
private IEnumerator RunWaypoints()
{
// Sanity check in case the waypoint array has length == 0
if (waypoints.Length == 0)
{
Debug.Log("No Waypoints!", this);
yield break;
}
// this looks dnagerous but as long as you yield somewhere it's fine ;)
while (true)
{
// maybe refresh here?
//RefreshWaypoints();
// Sanity check in case the waypoint array was set to length == 0 between states
if (waypoints.Length == 0)
{
Debug.Log("No Waypoints!", this);
yield break;
}
// first select the next waypoint
// Note that you might get the exact same waypoint again you currently had
// this will throw two errors in Unity:
// - Look rotation viewing vector is zero
// - and transform.position assign attempt for 'Main Camera' is not valid. Input position is { NaN, NaN, NaN }.
//
// so to avoid that rather use this (not optimal) while loop
// ofcourse while is never good but the odds that you will
// always get the same value over a longer time are quite low
//
// in case of doubt you could still add a yield return null
// than your camera just waits some frames longer until it gets a new waypoint
Transform newWaypoint = waypoints[Random.Range(0, waypoints.Length)].transform;
while(newWaypoint == currentWaypoint)
{
newWaypoint = waypoints[Random.Range(0, waypoints.Length)].transform;
}
currentWaypoint = newWaypoint;
// tell camera to rotate and wait until it is finished in one line!
yield return lookAtCam.RotateToTarget(currentWaypoint);
// move and wait until in correct position in one line!
yield return MoveToTarget(currentWaypoint);
//once waypoint reached wait 3 seconds than start over
yield return new WaitForSeconds(3);
}
}
private IEnumerator MoveToTarget(Transform currentWaypoint)
{
var currentPosition = transform.position;
var duration = Vector3.Distance(currentWaypoint.position, transform.position) / speed;
var passedTime = 0.0f;
do
{
// for easing see last section below
var lerpFactor = passedTime / duration;
transform.position = Vector3.Lerp(currentPosition, currentWaypoint.position, lerpFactor);
passedTime += Time.deltaTime;
yield return null;
} while (passedTime <= duration);
// to be sure to have the exact position in the end set it fixed
transform.position = currentWaypoint.position;
}
public void RefreshWaypoints()
{
waypoints = GameObject.FindGameObjectsWithTag("Target");
}
}
LookAtCamera
public class LookAtCamera : MonoBehaviour
{
// Values that will be set in the Inspector
public float RotationSpeed;
public IEnumerator RotateToTarget(Transform target)
{
var timePassed = 0f;
var targetDirection = (target.position - transform.position).normalized;
var targetRotation = Quaternion.LookRotation(targetDirection);
var currentRotation = transform.rotation;
var duration = Vector3.Angle(targetDirection, transform.forward) / RotationSpeed;
do
{
// for easing see last section below
var lerpFactor = timePassed / duration;
transform.rotation = Quaternion.Slerp(currentRotation, targetRotation, lerpFactor);
timePassed += Time.deltaTime;
yield return null;
} while (timePassed <= duration);
// to be sure you have the corrcet rotation in the end set it fixed
transform.rotation = targetRotation;
}
}
Note
Again instead of Quaternion.Slerp and currentRotation you could also simply use Quaternion.RotateTowards like
transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRotation, RotationSpeed * Time.deltaTime);
And for the movement you can also still use Vector3.MoveTowards if you want
while (Vector3.Distance(currentWaypoint.position, transform.position) < WPradius)
{
transform.position = Vector3.MoveTowards(transform.position, currentWaypoint.position, Time.deltaTime * speed);
yield return null;
}
but I would prefer to use the Lerp solutions. Why I suggest to rather use Lerp?
You can very easy controll now whether you want to move/rotate by a certain speed or rather give it fixed duration in which the move/rotation shall be finished regardless how big the differenc is - or even have some additional checks in order to decide for one of those options!
You can ease-in and -out the movement/rotation! See below ;)
Hint for easing Lerp movements
For still maintaining an eased-in and/or eased-out movement and rotation I found this block How to Lerp like a pro very helpfull! (adopted to my examples)
For example, we could “ease out” with sinerp:
var lerpFactor = Mathf.Sin(passedTime / duration * Mathf.PI * 0.5f);
Or we could “ease in” with coserp:
var lerpFactor = 1f - Mathf.Cos(passedTime / duration * Mathf.PI * 0.5f);
We could even create exponential movement:
var lerpFactor = Mathf.Pow(passedTime / duration, 2);
The multiplication property mentioned above is the core concept behind some interpolation methods which ease in and ease out, such as the famous “smoothstep” formula:
var lerpFactor = Mathf.Pow(passedTime / duration, 2) * (3f - 2f * passedTime / duration);
Or my personal favorite, “smootherstep”:
var lerpFactor = Mathf.Pow(passedTime / duration, 3) * (6f * (passedTime / duration) - 15f) + 10f);