I've been having trouble with Quaternion lerps. I'm simply looking to rotate my character 90 degrees based on their current rotation. The script below executes that almost perfectly, except for the fact that my character rotates a full 90 degrees long before rotationTime reaches the max value of 1. For some reason, the value of rotationTime is not properly synced with the progress of the lerp, and I can't seem to figure out why. What am I missing?
public class Movement : MonoBehaviour
{
bool Rotating = false;
Quaternion targetRotation;
public float rotationTime = 0f;
public float speed = 0.1F;
private Rigidbody rb;
private void Awake(){
rb = GetComponent<Rigidbody>();
}
public void Turn(InputAction.CallbackContext context){ //executes when 'E' is pressed
if (context.started && Rotating == false) {
targetRotation = Quaternion.Euler(0,transform.eulerAngles.y + 90f,0);
rotationTime = 0;
Rotating = true;
}
}
void Update() {
if (Rotating == true) {
rotationTime = rotationTime + Time.deltaTime * speed;
transform.rotation = Quaternion.Lerp(transform.rotation, targetRotation,rotationTime);
};
if (rotationTime > 1) {
Rotating = false;
}
}
}
I suspect that the main issue here is that you are using transform.rotation in your Quaternion.Lerp. You change it every frame, so every frame the start rotation will be closer to the target rotation. You should add some kind of _initRotation variable, and set it to transform.rotation in your Turn method. I mean something like this:
public void Turn(InputAction.CallbackContext context)
{
if (context.started && Rotating == false)
{
targetRotation = Quaternion.Euler(0,transform.eulerAngles.y + 90f,0);
_initRotation = transform.rotation;
rotationTime = 0;
Rotating = true;
}
}
...
void Update()
{
...
transform.rotation = Quaternion.Lerp(_initRotation, targetRotation,rotationTime);
...
}
Also, you have a logical issue with the lerp function. It does not affect the result in your particular case, but it can cause problems later.
You increment your rotation time by Time.deltaTime * speed every frame, it is not correct as it is not time passed from the start of the rotation.
According to the Quaternion.Lerp documentation, t value is always clamped to [0, 1]. So it is more convenient to use normalized time value instead of abstract speed value (right now it has no physical sense, it is just a multiplier).
It would be much clearer to use something like this:
void Update()
{
...
rotationTime += Time.deltaTime;
transform.rotation = Quaternion.Lerp(transform.rotation, targetRotation, rotationTime / fullRotationTime);
...
}
Generally, I almost always work with carriers, and I recommend that you do the same.
public class Movement : MonoBehaviour
{
bool Rotating = false;
Vector3 targetRotation;
public float rotationTime = 0f;
public float speed = 0.1F;
private Rigidbody rb;
private void Awake()
{
rb = GetComponent<Rigidbody>();
}
public void Turn(InputAction.CallbackContext context)
{ //executes when 'E' is pressed
if (context.started && Rotating == false)
{
targetRotation = new Vector3(0, transform.eulerAngles.y + 90f, 0);
rotationTime = 0;
Rotating = true;
}
}
void Update()
{
if (Rotating == true)
{
rotationTime = rotationTime + Time.deltaTime * speed;
transform.eulerAngles = Vector3.Lerp(transform.eulerAngles, targetRotation, rotationTime);
};
if (rotationTime > 1)
{
Rotating = false;
}
}
}
Also consider using coroutines for more readable code and to improve performance.
Good work!
As it was mentioned already by this answer one of your main issues is that every time you use a new transform.rotation as interpolation start point
=> Your rotation starts fast and then gets slower and slower the closer you reach the target value.
There other issues here though:
You are using the transform.rotation.eulerAngles! From the API:
When using the .eulerAngles property to set a rotation, it is important to understand that although you are providing X, Y, and Z rotation values to describe your rotation, those values are not stored in the rotation. Instead, the X, Y & Z values are converted to the Quaternion's internal format.
When you read the .eulerAngles property, Unity converts the Quaternion's internal representation of the rotation to Euler angles. Because, there is more than one way to represent any given rotation using Euler angles, the values you read back out may be quite different from the values you assigned. This can cause confusion if you are trying to gradually increment the values to produce animation.
To avoid these kinds of problems, the recommended way to work with rotations is to avoid relying on consistent results when reading .eulerAngles particularly when attempting to gradually increment a rotation to produce animation. For better ways to achieve this, see the Quaternion * operator.
And then in general, since I see there is a Rigibody involved you shouldn't set or read anything directly via the Transform component at all but rather only go through that rigidbody! Otherwise you might break/fight against the physics resulting in strange behavior and breaking collision detection.
In your case in my eyes it is way easier to control the entire thing in a Corouine which avoids the need for all the class fields and imho is way easier to understand, control and maintain:
public class Movement : MonoBehaviour
{
public float speed = 0.1F;
[SerializeField] private Rigidbody rb;
private bool isRotating = false;
private void Awake()
{
if(!rb) rb = GetComponent<Rigidbody>();
}
public void Turn(InputAction.CallbackContext context)
{
//executes when 'E' is pressed
if (context.started && !isRotating)
{
StartCoroutine(RotateRoutine());
}
}
private IEnumerator RotateRoutine()
{
// little safety check first
if(Mathf.Approximately(speed, 0f)
{
yield break;
}
// Just to be really sure you avoid any concurrent routines
if (isRotating)
{
yield break;
}
// Lock so no other routine ca be started
isRotating = true;
// wait until we are in the next physics frame
yield return new WaitForFixedUpdate();
// store the initial rotation -> go throug the rigibody not the transform
var start = rb.rotation;
var end = start * Quaternion.Euler(0, 90f, 0);
var duration = 1 / speed;
for (var rotationTime = 0f; rotationTime < duration; rotationTime += Time.deltaTime)
{
// this would be a linear growing factor from 0 to 1
var factor = rotationTime / duration;
// optionally you could add ease in and out at the ends
// basically you can add whatever curve function you like to grow from 0 to 1 within the given rotationTime
factor = Mathf.SmoothStep(0, 1, rotationTime);
// interpolate from start to end while the factor grows from 0 to 1
var rotation = Quaternion.Slerp(start,end, factor);
// again for rigibdoy rather do this instead of going through transform
rb.MoveRotation();
// again wait for the next physics update
yield return new WaitForFixedUpdate();
}
// Just to be sure to end on clean values
rb.MoveRotation(end);
// release the lock for the next routine to start
isRotating = false;
}
}
Related
Currently I'm using a normalized rb velocity to get the x and z velocity directions in order to compare it with eulerAngles of the camera in order to tell the camera which way to spin. But I'm doing this inside a bunch of nested if statements and it just feels.... wrong.
For actually following the ball I just have parent child relationships with Player -> RotatorObject -> Camera, so rotating the RotatorObject rotates around the center of the player.
Here is an example of what I'm currently doing.
// Velocity direction of the ball
Vector3 VelNormed = Vector3.Normalize(rb.velocity);
xVelNormed = VelNormed.x;
zVelNormed = VelNormed.z;
// Current CameraRotationObject angle
yEuler = transform.eulerAngles.y;
zEuler = transform.eulerAngles.z;
// If Moving forwards.
if (zVelNormed >= 0.5)
{
// If camera is within hoped for range. (270 is camera = perfectly forwards)
if (yEuler >= 260 && yEuler <= 280)
{
// Move camera to the right.
if (yEuler >= 260 && yEuler <= 270)
{
// Rotate camera right
transform.Rotate(0, (speed * Time.deltaTime) * 1, 0);
}
else if (yEuler <= 280 && yEuler >= 270)
{
// Rotate camera left
transform.Rotate(0, (speed * Time.deltaTime) * -1, 0);
}
else
{
}
}
}
// If Moving backwards.
else if (zVelNormed <= -0.5)
{
//etc.
}
Like I said before, using all of these if statements like this makes me feel like a monster! IE. I'm almost sure I'm doing this the old computer programming for dummies way
First of all the eulerAngles are very rarely what you want to use
When you read the .eulerAngles property, Unity converts the
Quaternion's internal representation of the rotation to Euler angles.
Because, there is more than one way to represent any given rotation
using Euler angles, the values you read back out may be quite
different from the values you assigned. This can cause confusion if
you are trying to gradually increment the values to produce animation.
Why do you actually need to make the camera spin at all? It sounds to me like your main issue actually origins in the camera being somewhere nested as child below the rolling ball.
You should rather extract the camera into scene root level and make it follow the ball via code like e.g.
public class CameraFollow : MonoBehaviour
{
// Interpolation factors for the position and rotation
[Range(0, 1)] public float moveInterpolationFactor = 5f;
[Range(0, 1)] public float rotateInterpolationFactor = 5f;
// The target this object shall follow
[SerializeField] Rigidbody targetObject;
// Positional offset in the movement direction of the target object
// e.g. (0,0, -1) will keep the camera 1 world space unit behind the move direction
public Vector3 positionOffset = new Vector3(0, 0.25f, -1);
private Vector3 targetPosition;
private Quaternion targetRotation;
private void Awake ()
{
if(!targetObject) return;
UpdateTargetValues();
}
private void UpdateTargetValues ()
{
targetPosition = targetObject.position + targetObject.rotation * positionOffset;
targetRotation = Quaternion.LookRotation(targetObject.velocity);
}
private void LateUpdate ()
{
// If there is no target stay where you are
if(!targetObject) return;
// Get the move velocity since we want to follow based
// on the move direction, not the objects orientation
var velocity = targetObject.velocity;
// If not moving simply continue moving to the last set position
// otherwise update the target values
if(velocity.sqrMagnitude > 0)
{
UpdateTargetValues ();
}
transform.position = Vector3.Slerp(transform.position, targetPosition, moveInterpolationFactor * Time.deltaTime);
transform.rotation = Quaternion.Lerp(transform.rotation, targetRotation, rotateInterpolationFactor * Time.deltaTime);
}
public void SetTarget(Rigidbody target)
{
targetObject = target;
if(!targetObject) return;
UpdateTargetValues ();
}
}
In Unity, I need to move a dynamic rigidbody from point A to point B. I know how to do so with a kinematic rigidbody, below the code:
[SerializeField] Transform destination;
[SerializeField] float moveTime = 0.1f;
private Rigidbody2D rb;
private float inverseMoveTime;
private void Start()
{
rb = GetComponent<Rigidbody2D>();
inverseMoveTime = 1f / moveTime;
Move(destination.position.x, destination.position.y);
}
public void Move(float xDir, float yDir)
{
Vector2 start = rb.velocity;
Vector2 end = start + new Vector2(xDir, yDir);
StartCoroutine(SmoothMovement(end));
}
public IEnumerator SmoothMovement(Vector3 end)
{
float sqrRemainingDistance = (transform.position - end).sqrMagnitude;
while (sqrRemainingDistance > float.Epsilon) // While that distance is greater than a very small amount (Epsilon, almost zero):
{
Vector3 newPosition = Vector3.MoveTowards(rb.position, end, inverseMoveTime * Time.deltaTime); // Find a new position proportionally closer to the end, based on the moveTime
rb.MovePosition(new Vector3(newPosition.x, newPosition.y));
sqrRemainingDistance = (transform.position - end).sqrMagnitude; // Recalculate the remaining distance after moving.
yield return null; // Return and loop until sqrRemainingDistance is close enough to zero to end the function
}
rb.MovePosition(end);
}
The problem is that using the function rigidbody.MovePosition() doesn't work well with dynamic rigidbodies, as Unity's docs and my experiments prove.
I've then tried with the following code, but it leads to weird results: the toon moves in the opposite direction of its destination! And the acceleration keeps increasing, the latter seeming the only point I think I can solve pretty easily.
[SerializeField] Transform destination;
[SerializeField] float moveTime;
private Rigidbody2D rb;
private float inverseMoveTime;
private void Start()
{
rb = GetComponent<Rigidbody2D>();
inverseMoveTime = 1f / moveTime;
Move(destination.position.x, destination.position.y);
}
public void Move(float xDir, float yDir)
{
Vector2 startVelocity = rb.velocity;
Vector2 end = startVelocity + new Vector2(xDir, yDir);
StartCoroutine(SmoothMovement(end));
}
public IEnumerator SmoothMovement(Vector3 end)
{
float sqrRemainingDistance = (transform.position - end).sqrMagnitude;
while (sqrRemainingDistance > float.Epsilon) // While that distance is greater than a very small amount (Epsilon, almost zero):
{
Vector2 newPosition = Vector2.MoveTowards(rb.position, end, inverseMoveTime * Time.deltaTime); // Find a new position proportionally closer to the end, based on the moveTime
rb.AddForce(new Vector3(newPosition.x, newPosition.y), ForceMode2D.Force);
sqrRemainingDistance = (transform.position - end).sqrMagnitude; // Recalculate the remaining distance after moving.
yield return null; // Return and loop until sqrRemainingDistance is close enough to zero to end the function
}
rb.MovePosition(end);
}
It's very similar to the first one, changing the way the character moves within the coroutine.
Basically my problem now is: how can I calculate the Vector2 to use withing the function AddForce(Vector2, ForceMode2D.Force) in the coroutine, so that it simply leads my character towards the destination and then the character stops when the destination is reached? Moreover, it should reach the destination at a constant speed, instead of keeping accelerating (but as I was saying above, I think I can solve this easily).
If you know any other way to move a 2D object with a dynamic rigidbody from point A to point B which is effective, of course, it's fine anyways: the coroutine seemed to me a good way but not mandatory. The only requirement is that it doesn't have to be a "tricky" way like transform.position or translate or any other function which ignores the phyisics.
Thanks again for the patience and for the next answer :slight_smile:
I think in your case you could simply make your Rigidbody a kinematic for the time the routine is running. But in general you should make sure that your code is executed in FixedUpdate not every frame
public IEnumerator SmoothMovement(Vector2 end)
{
yield return new WaitForFixedUpdate();
rb.isKinematic = true;
// If you really need a precision down to epsilon
//while (!Mathf.Approximately(Vector2.Distance(rb.position, end), 0f))
// otherwise for most use cases in physics the default precision of
// 0.00001f should actually be enough
while(rb.position != end)
{
rb.MovePosition(rb.position, end, inverseMoveTime * Time.deltaTime);
yield return new WaitForFixedUpdate();
}
rb.position = end;
rb.isKinematic = false;
}
What I don't understand though is why you pass in a position and treat it as it would be a direction added to the current velocity ...
If your goal is moving towards a certain position then rather simply do
StartCoroutine (SmoothMovement(destination.position));
so what I am trying to do is pull a spear back(like a slingshot) and send it flying, all the while making it rotate while it's flying in the air to mimic a real thrown spear. So far, I have only been able to make it rotate after a few seconds to a certain position but I can already tell that this isn't really a permanent fix and will definitely lead to problems down the line. I have tried looking for better ways of rotating the object but I can't really find anybody else who had a problem like this and it's hard to understand the unity documentation regarding rotation as it's for 3D objects.
Here is a gif showing how it looks like:
https://gfycat.com/chiefglasshorsemouse
This is the code that I have attached to my spear object that's responsible for letting me pull it back and launch it with the mouse:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.Scripting.APIUpdating;
public class spear : MonoBehaviour
{
Rigidbody2D rb;
Vector3 velocity;
private Vector3 _initialPosition;
[SerializeField] private float _launchPower = 500;
bool rotating = false;
public GameObject objectToRotate;
private float _timeSittingAround;
private bool _spearWasLaunched;
[SerializeField] private float _spearRotation = 360;
private void Awake()
{
_initialPosition = transform.position;
rb = GetComponent<Rigidbody2D>();
}
void Start()
{
rb = GetComponent<Rigidbody2D>();
}
//rotates the spear
IEnumerator rotateObject(GameObject gameObjectToMove, Quaternion newRot, float duration)
{
if(rotating)
{
yield break;
}
rotating = true;
Quaternion currentRot = gameObjectToMove.transform.rotation;
float counter = 0;
while(counter < duration)
{
counter += Time.deltaTime;
gameObjectToMove.transform.rotation = Quaternion.Lerp(currentRot, newRot, counter / duration);
yield return null;
}
rotating = false;
}
//reloads the scene if spear goes out of bounds or lays dormant for 2 seconds
void Update()
{
GetComponent<LineRenderer>().SetPosition(0, transform.position);
GetComponent<LineRenderer>().SetPosition(1, _initialPosition);
if (_spearWasLaunched &&
GetComponent<Rigidbody2D>().velocity.magnitude <= 0.1)
{
_timeSittingAround += Time.deltaTime;
}
if(transform.position.y > 30 ||
transform.position.y < -12.5 ||
transform.position.x > 40 ||
transform.position.x < -20 ||
_timeSittingAround > 2)
{
string currentSceneName = SceneManager.GetActiveScene().name;
SceneManager.LoadScene(currentSceneName);
}
}
//slingshot mechanic
private void OnMouseDrag()
{
Vector3 newPosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
transform.position = new Vector3(newPosition.x, newPosition.y);
}
private void OnMouseDown()
{
GetComponent<SpriteRenderer>().color = Color.red;
GetComponent<LineRenderer>().enabled = true;
}
//launches the spear when mouse is released as well as begins the rotating mechanic
private void OnMouseUp()
{
Vector2 directionToInitialPosition = _initialPosition - transform.position;
GetComponent<Rigidbody2D>().AddForce(directionToInitialPosition * _launchPower);
GetComponent<Rigidbody2D>().gravityScale = 1;
_spearWasLaunched = true;
GetComponent<LineRenderer>().enabled = false;
Quaternion rotation2 = Quaternion.Euler(new Vector3(0, 0, _spearRotation));
StartCoroutine(rotateObject(objectToRotate, rotation2, 3f));
}
}
There are a few ways to do it. You could try setting up rigidbodies and maybe joints and weighting different parts so that the tip of the spear naturally rotates the spear. More info on that here: https://answers.unity.com/questions/14899/realistic-rotation-of-flying-arrow.html
There's also another solution in that link for doing it with one line of code, making it rotate in the direction of movement.
In the top of your class, add this:
public Rigidbody2D mySpear;
Then in the inspector, drag the spear object into that slot to assign it. Then in your code, remove all your rotation code. In Update() add this:
mySpear.transform.right =
Vector2.Slerp(mySpear.transform.right, mySpear.rigidbody.velocity.normalized, Time.deltaTime)
A third way to do it is...
Figure out the rotation (on the z axis) of the object when it's pointing directly down. Put that down as a constant called downRotation or something.
Set a minimum velocity and maximum velocity that will be used by the rotation.
I recommend also adding an AnimationCurve variable.
Instead of having a special function for rotation, just put it in FixedUpdate. If the spear's velocity.x is not 0, then rotate.
The speed of the rotation should be based on the speed of the object. If the object's x velocity is very slow then it would probably fall (rotate) more quickly. If it's going very fast, the rotation would not change much.
Don't use this exact code, but it'd look something like this:
// This part figures out how fast rotation should be based on how fast the object is moving
var curve = speedCurve.Evaluate(1 - ((rigidBody.velocity.x - minVelocity) / (maxVelocity - minVelocity)));
// Rotates the spear
var rotate = rigidBody.rotation.eulerAngles;
var speed = curve * rotateSpeed * Time.fixedDeltaTime;
rotate.z = Mathf.MoveTowards(rotate.z, downRotation, speed);
transform.rotation = Quaternion.Euler(rotate);
Side note: You should use rigidbody rotation like I am here, and you should be assigning components to variables in the inspector instead of calling GetComponent every frame.
I want achieve moving object on x axis only with lerp to get smoothly movement.
this is picture what i need
I don't know how i can implement lerp to this code to get smooth movement between these values, it now works but it teleport the player and that is not smooth movement what i want to achieve
This is my working code that teleports player:
void Start()
{
}
void Update()
{
if (Input.GetMouseButtonDown(0))
{
Vector3 desiredPos = new Vector3(transform.position.x + 1.5f, transform.position.y, transform.position.z);
transform.position = desiredPos;
}
if (Input.GetMouseButtonDown(1))
{
Vector3 desiredPos = new Vector3(transform.position.x -1.5f, transform.position.y, transform.position.z);
transform.position = desiredPos;
}
}
I want to implement this but i don't understand how to do it .. When i put all code into update the player don't even move.. It only works for me when i copy paste all the code from docs, but how i can move the time from start method to update and always do the same to achieve to get smooth movement for player when going left and right i don't know really please help me guys..
This is the code that works but i don't know how to change it for my example..
https://docs.unity3d.com/ScriptReference/Vector3.Lerp.html
There are multiple ways. I would not use Translate as this gives you little control here but rather e.g. MoveTowards which makes sure you have no over shooting at the end. Use this for a linear movement with a given moveSpeed:
// set move speed in Units/seconds in the Inspector
public float moveSpeed = 1f;
private Vector3 desiredPos;
private bool isMoving;
private void Update()
{
if (!isMoving && Input.GetMouseButtonDown(0))
{
desiredPos = transform.position + Vector3.right * 1.5f;
isMoving = true;
}
if (!isMoving && Input.GetMouseButtonDown(1))
{
desiredPos = transform.position - Vector3.right * 1.5f;
isMoving = true;
}
if(isMoving)
{
transform.position = Vector3.MoveTowards(transform.position, desiredPos, moveSpeed * Time.deltaTime);
// this == is true if the difference between both
// vectors is smaller than 0.00001
if(transform.position == desiredPos)
{
isMoving = false;
// So in order to eliminate any remaining difference
// make sure to set it to the correct target position
transform.position = desiredPos;
}
}
}
Or as you asked use Vector3.Lerp like e.g.
// a factor between 0 and 1
[Range(0, 1)] public float lerpFactor;
...
transform.position = Vector3.Lerp(transform.position, desiredPos, lerpFactor);
lerpFactor has to be a value between 0 and 1 where in our case 0 would meen the object never moves and 1 it directly jumps to the target position. In other words the closer you set it to 0 the slower it will reach the target, the closer you set it to 1 the faster it will reach the target.
a lot of people do this to get "smooth" movements but what actually happens is e.g. if you set 0.5 for lerpFactor then every frame the object is placed in the middle between current position and target position.
That looks somehow smooth, moves very fast at the beginning and very very slow at the end ... but: It actually never really reaches the target position but just gets very slow.
For your case that is fine since anyway we compare the current and target position using == with a precision of 0.00001. One just has to have in mind how Lerp works.
But with this you won't have any control neither over the move speed nor the duration.
If you want overall more controll (as I do) I would recommend to use a Coroutine (it is not absolutely neccessary and you could do the same in Update as well but in my eyes Coroutines are better to maintain and keep track of).
Than you could also make a smooth eased-in and eased-out movement with an always fixed duration regardless how far the distance is
// set desired move duration in seconds
public float moveDuration = 1;
private bool isMoving;
privtae void Update()
{
if (!isMoving && Input.GetMouseButtonDown(0))
{
StartCoroutine(transform.position + Vector3.right * 1.5f, moveDuration);
}
if (!isMoving && Input.GetMouseButtonDown(1))
{
StartCoroutine(transform.position - Vector3.right * 1.5f, moveDuration);
}
}
private IEnumerator Move(Vector3 targetPosition, float duration)
{
if(isMoving) yield break;
isMoving = true;
var startPosition = transform.position;
var passedTime = 0f;
do
{
// This would move the object with a linear speed
var lerpfactor = passedTime / duration;
// This is a cool trick to simply ease-in and ease-out
// the move speed
var smoothLerpfactor = Mathf.SmoothStep(0, 1, lerpfactor);
transform.position = Vector3.Lerp(startPosition, targetPosition, smoothLerpfactor);
// increase the passedTime by the time
// that passed since the last frame
passedTime += Time.deltaTime;
// Return to the main thread, render this frame and go on
// from here in the next frame
yield return null;
} while (passedTime < duration);
// just like before set the target position just to avoid any
// under shooting
transform.position = targetPosition;
isMoving = false;
}
and you could still extend this example to also take the dtsnace to move into account like e.g.
var actualDuration = duration * Vector3.Distance(startPosition, targetPosition);
and then later everywhere use actualDuration.
Use transform.Translate instead:
public float moveSpeed = 3f;
void Update ()
{
//Moves Left and right along x Axis
transform.Translate(Vector3.right * Time.deltaTime * Input.GetAxis("Horizontal")* moveSpeed);
}
I have been trying to make this work for a while and I am failing.
I have a Rigidbody2D in a top down 2D level and I am trying to simply move it along the coordinates (the levels are gridlike) so one step / button press equals exactly one square walked. It should only be possible to walk in one of the four directions, and no matter when the user stops the walking motion, it should end on a square. A good game equivalent of what I want to achieve is the lufia / breath of fire / any similar RPG series. I've tried using coroutines to get the update function to wait one second after every step, but that does not seem to work. The closest I've come to is the code down below. Thanks guys!
public class PlayerMovement2D : MonoBehaviour
{
Rigidbody2D rbody;
Animator anim;
float speed = 1.25f;
Vector2 pos;
void Start()
{
rbody = GetComponent<Rigidbody2D>();
anim = GetComponent<Animator>();
pos = rbody.position;
}
void FixedUpdate()
{
Vector2 movement_vector = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical"));
if (movement_vector.x != 0 || movement_vector.y != 0)
{
if (movement_vector.x > 0)
{
pos += Vector2.right;
pos.y = 0;
}
else if (movement_vector.x < 0)
{
pos += Vector2.left;
pos.y = 0;
}
else if (movement_vector.y > 0)
{
pos += Vector2.up;
pos.x = 0;
}
else if (movement_vector.y < 0)
{
pos += Vector2.down;
pos.x = 0;
}
anim.SetBool("IsWalking", true);
anim.SetFloat("Input_x", movement_vector.x);
anim.SetFloat("Input_y", movement_vector.y);
}
else
{
anim.SetBool("IsWalking", false);
}
rbody.position = Vector2.MoveTowards(rbody.position, pos, speed * Time.deltaTime);
//transform.position = Vector3.MoveTowards(transform.position, pos, Time.deltaTime * speed);
pos = rbody.position;
}
}
I think what you try to program is similar to the RogueLike game that is part of the tutorial collection in Unity website. Check first the intro video to confirm that is what you are planning to achieve
https://unity3d.com/es/learn/tutorials/projects/2d-roguelike-tutorial/project-introduction?playlist=17150
If so, this is how they handle it:
using UnityEngine;
using System.Collections;
//The abstract keyword enables you to create classes and class members that are incomplete and must be implemented in a derived class.
public abstract class MovingObject : MonoBehaviour
{
public float moveTime = 0.1f; //Time it will take object to move, in seconds.
public LayerMask blockingLayer; //Layer on which collision will be checked.
private BoxCollider2D boxCollider; //The BoxCollider2D component attached to this object.
private Rigidbody2D rb2D; //The Rigidbody2D component attached to this object.
private float inverseMoveTime; //Used to make movement more efficient.
//Protected, virtual functions can be overridden by inheriting classes.
protected virtual void Start ()
{
//Get a component reference to this object's BoxCollider2D
boxCollider = GetComponent <BoxCollider2D> ();
//Get a component reference to this object's Rigidbody2D
rb2D = GetComponent <Rigidbody2D> ();
//By storing the reciprocal of the move time we can use it by multiplying instead of dividing, this is more efficient.
inverseMoveTime = 1f / moveTime;
}
//Move returns true if it is able to move and false if not.
//Move takes parameters for x direction, y direction and a RaycastHit2D to check collision.
protected bool Move (int xDir, int yDir, out RaycastHit2D hit)
{
//Store start position to move from, based on objects current transform position.
Vector2 start = transform.position;
// Calculate end position based on the direction parameters passed in when calling Move.
Vector2 end = start + new Vector2 (xDir, yDir);
//Disable the boxCollider so that linecast doesn't hit this object's own collider.
boxCollider.enabled = false;
//Cast a line from start point to end point checking collision on blockingLayer.
hit = Physics2D.Linecast (start, end, blockingLayer);
//Re-enable boxCollider after linecast
boxCollider.enabled = true;
//Check if anything was hit
if(hit.transform == null)
{
//If nothing was hit, start SmoothMovement co-routine passing in the Vector2 end as destination
StartCoroutine (SmoothMovement (end));
//Return true to say that Move was successful
return true;
}
//If something was hit, return false, Move was unsuccesful.
return false;
}
//Co-routine for moving units from one space to next, takes a parameter end to specify where to move to.
protected IEnumerator SmoothMovement (Vector3 end)
{
//Calculate the remaining distance to move based on the square magnitude of the difference between current position and end parameter.
//Square magnitude is used instead of magnitude because it's computationally cheaper.
float sqrRemainingDistance = (transform.position - end).sqrMagnitude;
//While that distance is greater than a very small amount (Epsilon, almost zero):
while(sqrRemainingDistance > float.Epsilon)
{
//Find a new position proportionally closer to the end, based on the moveTime
Vector3 newPostion = Vector3.MoveTowards(rb2D.position, end, inverseMoveTime * Time.deltaTime);
//Call MovePosition on attached Rigidbody2D and move it to the calculated position.
rb2D.MovePosition (newPostion);
//Recalculate the remaining distance after moving.
sqrRemainingDistance = (transform.position - end).sqrMagnitude;
//Return and loop until sqrRemainingDistance is close enough to zero to end the function
yield return null;
}
}
//The virtual keyword means AttemptMove can be overridden by inheriting classes using the override keyword.
//AttemptMove takes a generic parameter T to specify the type of component we expect our unit to interact with if blocked (Player for Enemies, Wall for Player).
protected virtual void AttemptMove <T> (int xDir, int yDir)
where T : Component
{
//Hit will store whatever our linecast hits when Move is called.
RaycastHit2D hit;
//Set canMove to true if Move was successful, false if failed.
bool canMove = Move (xDir, yDir, out hit);
//Check if nothing was hit by linecast
if(hit.transform == null)
//If nothing was hit, return and don't execute further code.
return;
//Get a component reference to the component of type T attached to the object that was hit
T hitComponent = hit.transform.GetComponent <T> ();
//If canMove is false and hitComponent is not equal to null, meaning MovingObject is blocked and has hit something it can interact with.
if(!canMove && hitComponent != null)
//Call the OnCantMove function and pass it hitComponent as a parameter.
OnCantMove (hitComponent);
}
//The abstract modifier indicates that the thing being modified has a missing or incomplete implementation.
//OnCantMove will be overriden by functions in the inheriting classes.
protected abstract void OnCantMove <T> (T component)
where T : Component;
}
Link to this part of the tutorial:
https://unity3d.com/es/learn/tutorials/projects/2d-roguelike-tutorial/moving-object-script?playlist=17150
You can use Vector2.Lerp method in combination of Unity's Coroutine system.
public class Movement
: MonoBehaviour
{
IEnumerator m_MoveCoroutine;
float m_SpeedFactor;
void Update()
{
// if character is already moving, just return
if ( m_MoveCoroutine != null )
return;
// placeholder for the direction
Vector2 direction; // between { -1, -1 } and { 1, 1 }
// calculate your direction based on the input
// and set that direction to the direction variable
// eg. direction = new Vector2(Input.GetAxisRaw("Horizontal") > 0 ? 1 : -1,...)
// then check if direction is not { 0, 0 }
if( direction != Vector2.zero )
{
// start moving your character
m_MoveCoroutine = Move(direction);
StartCoroutine(m_MoveCoroutine);
}
}
IEnumerator Move(Vector2 direction)
{
// Lerp(Vector2 a, Vector2 b, float t);
Vector2 orgPos = transform.Position; // original position
Vector2 newPos = orgPos + direction; // new position after move is done
float t = 0; // placeholder to check if we're on the right spot
while( t < 1.0f ) // loop while player is not in the right spot
{
// calculate and set new position based on the deltaTime's value
transform.position = Vector2.Lerp(orgPos, newPos, (t += Time.deltaTime * m_SpeedFactor));
// wait for new frame
yield return new WaitForEndFrame();
}
// stop coroutine
StopCoroutine(m_MoveCoroutine);
// get rid of the reference to enable further movements
m_MoveCoroutine = null;
}
}
This method assumes that you can move in the specified direction. But you still should check if your new position is "walkable" before starting the Move Coroutine.
I think you want to do the movement in a coroutine that, while it is active, disabled further movement until it's done.
bool isIdle = true;
void Update() {
if(isIdle) {
// Your movement code that gives pos
StartCoroutine(Move(pos));
}
}
IEnumerator Move(Vector2 destination) {
isIdle = false;
do {
transform.position = Vector2.MoveTowards(transform.position, destination, speed * Time.deltaTime);
yield return new WaitForEndOfFrame();
} while (transform.position != destination);
isIdle = true;
}
Let me know if you don't understand, need further clarification or if this approach doesn't work!