Okay so I'm trying to create a small dash coroutine for my 2d character. When the coroutine calls, gravity switches off, he lerps between 2 speeds over a time. The issue is within my Dash coroutine, the while loop checks when time.time(current time) > start time + dash duration.
While debugging this with Mono, I'm finding that my variable currentTime is not changing after being set, even though I can clearly see the while loop running more than once. This puts me in an infinite loop.
Any suggestions?
void Update () {
MoveAndJump ();
CheckDash ();
}
void MoveAndJump(){
if(dashing){
return;
}
Vector2 moveDir = new Vector2 (Input.GetAxisRaw ("Horizontal") * moveSpeed, rb.velocity.y);
rb.velocity = moveDir;
// Consider Switching to an overlap circle based on the actual character graphic. This currently uses a rectangle
// This can also use 4 points to create a more complex box shape. This only works well when using at least about 1 unit size. any smaller and it is innacurate
isGrounded = Physics2D.OverlapArea (groundPoint_right.position, groundPoint_left.position, groundMask);
Debug.Log (isGrounded);
if (Input.GetAxisRaw ("Horizontal") == 1) {
transform.localScale = new Vector3 (1 , 1 * transform.localScale.y, 1* transform.localScale.z);
} else if (Input.GetAxisRaw ("Horizontal") == -1) {
transform.localScale = new Vector3 (-1, 1* transform.localScale.y, 1* transform.localScale.z);
}
if(Input.GetKeyDown(KeyCode.Space) && isGrounded){
rb.AddForce (new Vector2 (0, jumpHeight));
}
}
void CheckDash(){
if(Input.GetKeyDown(KeyCode.W) && !dashing && Time.time > nextdashtime){
dashing = true;
StartCoroutine ("Dash");
}
}
IEnumerator Dash(){
//Before dash loop
rb.gravityScale = 0f;
float startDashTime = Time.time;
float endDashTime = startDashTime + dashDuration;
float currentDashTime;
//Dash Loop
while(Time.time < (startDashTime + dashDuration)){
currentDashTime = Time.time;
// The value to lerp by should be the % that time.time is between start time and end time
rb.velocity = new Vector2 (Mathf.Lerp (startDashSpeed, endDashSpeed, ((currentDashTime - startDashTime) / (endDashTime - startDashTime))),0f);
}
//When dash loop is complete
nextdashtime = Time.time + dashcooldown;
dashing = false;
rb.gravityScale = 1;
yield return null;
}
// FOR CHECKING GROUND CHECK LIMITS
/*void OnDrawGizmos(){
Gizmos.color = Color.red;
Gizmos.DrawLine (groundPoint_left.position, groundPoint_right.position);
}*/
}
You don't need all that for dashing !
If you use rigid bodies then the following code will do:
using UnityEngine;
public class CubeController : MonoBehaviour
{
private Rigidbody _rigidbody;
public float Force = 10;
private void Start()
{
_rigidbody = GetComponent<Rigidbody>();
}
private void FixedUpdate()
{
ForceMode? forceMode = null;
if (Input.GetKeyDown(KeyCode.W)) // normal
forceMode = ForceMode.Force;
else if (Input.GetKeyDown(KeyCode.S)) // dash
forceMode = ForceMode.Impulse;
if (forceMode.HasValue)
_rigidbody.AddRelativeForce(Vector3.forward*Force, forceMode.Value);
}
}
This is an example using 3D but you have the same for 2D : https://docs.unity3d.com/ScriptReference/Rigidbody2D.html
Steps:
add rigidbody component to your ground, set it as kinematic
add rigidbody component to your player
sketch the above component for your player using Rigidbody2D class
adjust the parameters as you'd like
you should have a simple and robust dashing by now :)
I forgot a yield return null within my while loop that stuck me in an infinite loop. Works fine now.
while(Time.time < (startDashTime + dashDuration)){
currentDashTime = Time.time;
// The value to lerp by should be the % that time.time is between start time and end time
rb.velocity = new Vector2 (Mathf.Lerp (startDashSpeed, endDashSpeed, ((currentDashTime - startDashTime) / (endDashTime - startDashTime))),0f);
yield return null;
}
Related
(I'm a beginner so please have patience with me).
How it needs to happend:
I have a player that can jump and hit a tile, on collision the tile with move a fixed distance up and come back down with a smooth transition.
What I have so far:
I am detecting the collision now there's only the matter of moving the tile up and down.
The catch
The tiles are suspended in air so basically they either don't have a RigidBody2D or they have one with gravity scale 0
What I've tried
Basically I've tried 2 solutions:
(I'm not limited to these 2, I want to implement the solution that is correct so I am open to other ideas)
I was thinking to simply take the current position, calculate another vector with another position, and lerp to the new position and then lerp back to the initial one.
Vector2 initialPosition = transform.position;
Vector2 targetPosition = new Vector2(initialPosition.x + 4f, initialPosition.y + 4f);
print("Initial position: " + initialPosition);
print("Target position: " + targetPosition);
Vector2.Lerp(initialPosition, targetPosition, 1f);
I tried adding a rigidbody with scale 0
private void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.tag == "Ground")
{
Jumping = false;
anim.SetInteger("State", 0);
}
// print("Colision layer: " + collision.collider.gameObject.layer);
if (collision.collider.gameObject.layer == Mathf.Log(layerMask.value, 2))
{
GameObject Tile = collision.gameObject;
Rigidbody2D rigidBody = Tile.GetComponent<Rigidbody2D>();
rigidBody.gravityScale = 1;
rigidBody.AddForce(new Vector2(0, 10f), ForceMode2D.Force);
StartCoroutine(MoveTileWithForce(rigidBody));
}
}
IEnumerator MoveTileWithForce(Rigidbody2D TileRigidBody)
{
yield return new WaitForSeconds(1);
TileRigidBody.AddForce(new Vector2(0, -5f), ForceMode2D.Force);
TileRigidBody.gravityScale = 0;
print("END MY COROUTINE, gravityScale: " + TileRigidBody.gravityScale);
}
I think the best way you can achieve this is in this simple way:
1
Make sure you have a box collider for your character and a Rigidbody and the same for the block you wanna move. For both of the colliders set on trigger. If you want to have another collider for the player in order to make him touch the ground or hit enemies you can add another box collider, but make sure you have one on trigger on his head
2
Add a script to the block you wanna move and also a tag to the player, for example "player"
3
Inside of this new script check if the block is triggering the player:
void OnTriggerEnter2D(Collision other)
{
if(other.compareTag("player"))
{ StartCoroutine(MovingBlock(0.5f, transform.position, upperPosition));}
|
It will enter inside the if when the player touches the block, it will start a coroutine that I will now write so you will understand the 0.5f and the other variables
IEnumerator MovingBlock(float time, Vector2 startpos, Vector2 endpos)
{
float elapsedTime = 0;
while (elapsedTime < time)
{
transform.position= Vector2.Lerp(startpos, endpos, (elapsedTime / time));
elapsedTime += Time.deltaTime;
yield return null;
}
elapsedTime = 0f;
while (elapsedTime < time)
{
transform.position= Vector2.Lerp(endpos, startpos, (elapsedTime / time));
elapsedTime += Time.deltaTime;
yield return null;
}
}
Basically, the Coroutine will move the object from startpos to endpos and then back again in the amount of time that you decide (in this case 0.5 seconds). I have used the Vector2 variable upperposition and it's just the height you want to reach with the platform.
If you want, you can also add inside the coroutine some yield return new WaitForSeconds(timeToWait) and make the platform wait in a certain position the amount of seconds you want (timeToWait)
Declare Vector2 initialPosition at the beginning of your class.
On Start() of the tile object you should get the initialPosition = transform.position.
And in the OnCollisionEnter2D(Collision2D collision) you could start a coroutine to bring the tile back down.
So for example:
Player Script:
public class Player : MonoBehaviour
{
public Rigidbody2D rb;
//Initialize the rigidbody on the editor.
// Update is called once per frame
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
rb.AddForce(new Vector2(0f, 1000f));
}
}
}
And the tile script:
public class MyTile : MonoBehaviour
{
Vector2 initialPosition;
float speed = 2f; //the speed the tile will go down.
private void Start()
{
initialPosition = transform.position;
}
private void OnCollisionEnter2D(Collision2D collision)
{
//The amount you want to go up on the Y component, for this example I used 2.
transform.position = new Vector2(transform.position.x, transform.position.y + 2f);
StartCoroutine(MoveTileDown());
}
IEnumerator MoveTileDown()
{
while (transform.position.y > initialPosition.y)
{
transform.position = Vector2.Lerp(transform.position, initialPosition, Time.deltaTime * speed);
yield return null; //Make it run every frame, just like Update()
}
StopCoroutine(MoveTileDown());
}
}
There are a lot of ways you can achieve the same result, this is just one of them.
I'm trying to achieve the simplest thing. I have code for moving an object smoothly, and I want it to stop at a specific distance in relation to where the object was when the button was clicked. So I can't store the position inside any function that updates every frame. Currently the object simply starts moving and doesn't stop.
void Update()
{
Vector3 targetPosition = new Vector3(transform.position.x - 1, transform.position.y, transform.position.z);
if (movement == true)
{
float step = speed * Time.deltaTime;
transform.position = Vector3.MoveTowards(transform.position, targetPosition, step);
}
}
public void coroutineStarter()
{
float targetPosition = transform.position.x - 1;
StartCoroutine(OnClick(targetPosition));
}
IEnumerator OnClick(float targetPosition)
{
if (transform.position.x != targetPosition)
{
movement = true;
}
else
{
movement = false;
yield return null;
}
}
}
I should specify that pressing the button starts the coroutineStarter. I tried to use a while loop instead of the if statement inside OnClick but apparently using while loops anywhere freezes unity if it's infinite, and if it isn't it freezes unity for the duration of the loop.
I wouldn't mix up a Coroutine with code running in Update. A Coroutine already is like a temporary Update method so you could simply do
public void coroutineStarter()
{
var targetPosition = transform.position + Vector3.left;
StartCoroutine(OnClick(targetPosition));
}
IEnumerator OnClick(Vector3 targetPosition)
{
while (transform.position.x != targetPosition)
{
float step = speed * Time.deltaTime;
transform.position = Vector3.MoveTowards(transform.position, targetPosition, step);
// Important! Tells Unity to pause here, render this frame
// and continue from here in the next frame
yield return null;
}
// When done set the position fixed once
// since == has a precision of 0.00001 so it never reaches an exact position
transform.position = targetPosition;
}
This moves the object to the target position with a constant speed.
A slightly different approach is moving the object within a certain time but allows to add ease-in and ease-out at start and end of the movement using Vector3.Lerp and SmoothStep
IEnumerator OnClick(Vector3 targetPosition)
{
var startPosition = transform.position;
var distance = Vector3.Distance(startPosition, targetPosition);
if(distance <= 0) yield break;
var duration = distance / speed;
var timePassed = 0f;
while (timePassed < duration)
{
var factor = timePassed / duration;
// Optionally add easing
factor = Mathf.SmoothStep(0,1,factor);
transform.position = Vector3.Lerp(startPosition, targetPosition, factor);
// increase by time since last frame avoiding overshooting
timePassed += Mathf.Min(Time.deltaTime, duration - timePassed);
// Important! Tells Unity to pause here, render this frame
// and continue from here in the next frame
yield return null;
}
// When done set the position fixed once
// this time just in case you never know ;)
transform.position = targetPosition;
}
(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);
Hi there fellow Overflowers!
I have recently dived into C# in Unity, because I believe that it has more functionality than UnityScript, and I come from a C++ background. Referencing some code from a 2D script in the Sample Assets, I have tried to create my own. The code is below:
using UnityEngine;
public class PlayerControl : MonoBehaviour {
//Variables
[HideInInspector]
public bool facingForward = true; //for Flip() Function
bool isGround = true; //for Grounded() Function
public float maxSpeed = 5.0f; //Terminal sideways velocity
public float HorizonAxis; //Checks for a/d movement
public float jumpFloat = 1000.0f; //Modular, use Unity Editor
public float moveFloat = 400.0f; // " "
void Start() {
//transform.position(0,0,0);
}
void Flip() {
facingForward = !facingForward; //switches boolean
Vector3 theScale = transform.localScale; //assigns vector to localscale of Player
theScale.x *= -1; //if x = 1, position becomes -1 and thus flips
transform.localScale = theScale; //reassigns the localscale to update theScale
}
bool Grounded() {
if (transform.position.y > 1) { //if position of gameObject is greater that 1, not grounded
isGround = false;
} else {
isGround = true;
}
return isGround; //function returns true or false for isGround
}
void Update() {
HorizonAxis = /*UnityEngine.*/Input.GetAxis ("Horizontal"); //assigns HorizonAxis to a/d movement from UnityEngine.Input
if (HorizonAxis * rigidbody2D.velocity.x > maxSpeed) { // if Input a/d by current x velocity of gameObject is greater than maxSpeed
rigidbody2D.velocity = new Vector2 (Mathf.Sign (rigidbody2D.velocity.x) * maxSpeed, rigidbody2D.velocity.y); //1 or -1 times the max speed, depending on direction
}
else if (HorizonAxis * rigidbody2D.velocity.x < maxSpeed) { //if Input a/d is less than terminal velocity
rigidbody2D.AddForce(Vector2.right * HorizonAxis * moveFloat); //add force to the right equivilant to Input by scalar moveFloat
}
if (Input.GetButtonDown ("Jump")) { //If Space
if(isGround) { //and isGround returns true
rigidbody2D.AddForce(new Vector2(0.0f, jumpFloat)); //add upwards force to bottom of rigidbody2D
isGround = false; //Resets isGround value
}
}
if (HorizonAxis > 0 && !facingForward) {//if UnityEngine.Input is to the right and facing left
Flip (); //execute
}
else if (HorizonAxis < 0 && facingForward) { //else
Flip (); //execute
}
}
}
Unfortunately, the code just doesn't work. I get no compile errors, but any Input does not effect the current position of the character. Should I be using transform.Translate to change the position, or stick with AddForce to a Vector2 until the character hits a maxSpeed?
Thanks heaps :)
I (kinda) fixed the jerky jumping issue, basically you look for the input in the Update() function which switches a Bool in FixedUpdate()
void Update () {
if (Input.GetKeyDown (KeyCode.Space) && playerGrounded) {
playerJumped = true;
}
}
Then in FixedUpdate() I looked for this Bool and did my AddForce
void FixedUpdate () {
if (playerJumped) {
playerJumped = false;
rigidbody2D.AddForce (new Vector2 (0, jumpForce),ForceMode2D.Impulse);
playerGrounded = false;
}
}
Setting playerJumped to false makes sure it doesn't run several times and the player can't jump again because I also set the grounded to false. grounded gets set back to true when the player collides with things tagged "ground".
I'm still new to Unity and C# overall so I can't guarantee this is the best (or even a good) way. Hope I could help somehow though ;)