Lerp brings object to unrelated position - c#

I've been trying to make a script that moves the selected object smoothly up and down, which seems simple enough, but I was pretty confused when my script kept putting the object in a weird Y position.
I finally got it to work, after trying half of the day, and then scrapping the code, but one question still remains unanswered in my head. Why does the following script not work?
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Spin : MonoBehaviour
{
string UpOrDown;
Vector3 originpos;
Vector3 goalUp;
Vector3 goalDown;
void start()
{
originpos = transform.position;
goalUp = originpos + new Vector3(0, 0.05f, 0);
goalDown = originpos - new Vector3(0, 0.05f, 0);
UpOrDown = "Up";
}
string TweenLerp(Vector3 destination, float time, string UpOrDownResult) {
while (transform.position != destination)
{
transform.position = Vector3.Lerp(transform.position, destination, time);
}
return UpOrDownResult;
}
void Update()
{
if (UpOrDown == "Up")
UpOrDown = TweenLerp(goalUp, 0.125f, "Down");
else if (UpOrDown == "Down")
UpOrDown = TweenLerp(goalDown, 0.125f, "Up");
}
}
Sorry if the script seems kind of stupid or with crappy logic, im still kind of new to c#, unity and vectors, so trying to figure out how it works.

so your code works fine, and everything is correct, the reason it's not working is that you called the Start function start(), lowercase, which makes unity not call it. To make this code work all you need is to substitute start() with Start().
Some extra notes to be aware of Lerp and the way you are using it: The way you're doing this animation does work, but not in the way that I think you intended, here's why:
Lerp stands for linear interpolation, and the way it works is that you give a start value (transform.position in your case), an end value (destination), and a value (t) between 0 and 1 that indicates if your value should be more to the start or to the end if t = 0, the result will be the start value, if t = 1, the result will be the end value.
The reason why this works is that everytime you call this lerp function with a value grater than 0, the result value is bit closer to the destination. and if you keep doing this many and many times, eventually it'll get to the destination, but not with linear speed, it will start very fast and end very slow
In your code, you are repeatedly calling this function until the transform.position is equal to the destination using a parameter t called time, the problem is that this "time" value does NOT affect the time of the animation. Since you're doing all of the lerps to get to the destination in one single while loop, they all happen in the same frame, which means that this function is doing the same as transform.position = destination but with a lot of unnecessary Lerp calculations. Btw, if you set the time to 0 in this case you get an infinite loop and you'll need to restart unity.
If you want to have the animations actually happen thru some time and not in one single frame, you should call this Lerp in the Update, so that in each frame it moves a little bit. Your Uptade could look like this:
void Update()
{
if (transform.position == goalUp)
UpOrDown = "Down";
else if (transform.position == goalDown)
UpOrDown = "Up";
transform.position = Vector3.Lerp(transform.position, UpOrDown == "Up" ? goalUp : goalDown, time);
}
One more thing: this time parameter actually makes more sense interpreted as speed, since the lower the value of time, the slower the animation will happen.
Sorry for the size of the answer, hope I have helped!

Related

How do I make a platform go down after a certain amount of time not colliding with the player object in Unity?

I'm new to Unity and C# in general, so excuse my terminology(or lack thereof)...
I have succeeded -with the help of a friend, to credit where its due- in making a platform go up a certain amount after one frame of collision with the player! The only problem is that, now, I can't seem to get it moving down again... Said friend challenged me to make it go up, stay airborne for a certain amount of time, then go back down.
Here is a snippet of my working code that makes it go up.
bool HaveCollided = false; //Collision checker boolean
void Update()
{
Vector3 up = lifter * Time.deltaTime;
if (HaveCollided != true || transform.position.y >= 5)
{
return;
}
}
void OnCollisionEnter(Collision collision) //Makes HaveCollided true at collision event
{
if (collision.gameObject.GetComponent<Playertag>() != null) //don't use GetComponent in Update()
{
HaveCollided = true;
}
So if my logic is right, I'd need to nest another if statement inside the one where the condition is: HaveCollided != true || transform.position.y >= 5 which should be:
if (newTime == Time.deltaTime * CertainAmount && transform.position.y >= 1) //make platform go down until y position is equal to 1
{
//make platform go down
Debug.Log("reached");
transform.position = transform.position - up;
}
But it's not working. It doesn't even seem to reach the part that would make the platform descend. I literally do not know where to go from here...
Based on your comment I made a few revisions to make the platform movement a bit smoother. I also made it so the same function can be used for both the upward and downward motion.
using UnityEngine;
using System.Collections;
[SerializeField] private float timeToMovePlatform = 0.0f; // time it takes the platform to get from 1 point to another
[SerializeField] private float timeToRestAtPeak = 0.0f; // time that the platform rests at the top of its movement
[SerializeField] private float platformYOffset = 0.0f; // offset the platform moves from its origin point to float upward
private Coroutine movingPlatform = null; // determines if the platform is currently moving or not
private void OnCollisionEnter(Collision col)
{
// add a tag to your player object as checking a tag at runtime vs. a GetComponent is faster
if(col.gameObject.tag == "Player")
{
// only trigger when we are not currently moving
if(movingPlatform == null)
{
// start our coroutine so the platform can move
movingPlatform = StartCoroutine(MovePlatform(true));
}
}
}
/// <summary>
/// Moves this object up or down depending on the parameter passed in
/// </summary>
/// <param name="moveUpward">Determines if this object is currently moving upward or not</param>
private IEnumerator MovePlatform(bool moveUpward)
{
// store our start position
Vector3 startPos = transform.position;
// build our goal position based on which direction we are moving
Vector3 goalPos = new Vector3(startPos.x, startPos + (moveUpward ? platformYOffset : -platformYOffset), startPos.z);
// set our current time
float currentTime = 0.0f;
while(currentTime <= timeToMovePlatform)
{
// lerp the position over the current time compared to our total duration
transform.position = Vector3.Lerp(startPos, goalPos, currentTime / timeToMovePlatform);
// update our timer
currentTime += Time.deltaTime;
// need to yield out of our coroutine so it does not get stuck here
yield return null;
}
// just in case there are any floating point issues, set our position directly
transform.position = goalPosition;
// when we are moving upward, make sure to stop at the peak for the set duration
if(moveUpwards)
{
yield return WaitForSeconds(timeToRestAtPeak);
// once we are done waiting, we need to move back downward
StartCoroutine(MovePlatform(false));
}
else
{
// we finished moving downward, so set our coroutine reference to false
// so the player can set the platform to move again
movingPlatform = null;
}
}
Fair warning, I did not test this code it is more a direction I would take for your situation. Before doing anything, add a tag to your player. If you are unsure what tags are, check out the docs. Be sure to give the player the tag Player so that the code will work as expected.
You will notice the first three variables are Serialized but are private. By doing this, it allows for Unity to show them in the inspector but not allow other scripts to touch them.
To learn more about Coroutines, check out the docs and for Lerps, I enjoy this blog post. Let me know if this works for you and/or you have any questions.
Your best bet is to add colliders and use isTrigger. Seed a time and if it's not triggered within that time period raise an event.

Physics inconsistent even when using FixedUpdate() or Time.DeltaTime()

I want to move a rigidBody2D for a fixed amount of time with a fixed amount of velocity, when the player presses a button. I am calling the method in FixedUpdate() and the results are not consistent. Sometimes the rigidBody will travel more and sometimes less (Here is a relevant part of my code how i went about do this)
void Update()
{
GetPlayerInput();
}
private void FixedUpdate()
{
GroundCheck();
ApplayNormalPlayerMovement();
ApplyMove(); // This is the method of interest - I tried calling this in Update() with Time.DeltaTime - still inconsistent results.
MoveCooldownCounter(); // I tried calling this in Update() - inconsistent results also
}
IEnumerator MoveRB()
{
savedVelocityX = rb.velocity.x;
rb.gravityScale = 0;
savedXinput = xInput;
while (moveTimer >= 0)
{
if (xInput != 0)
xInput = 0;
cc.sharedMaterial = noFriction;
if (facingRight)
{
rb.velocity = new Vector2(moveSpeed, .0f); // I tried multiplying the moveSpeed by Time.DeltaTime and FixedDeltaTime - still inconsistent results.
} else
{
rb.velocity = new Vector2(-moveSpeed, .0f);
}
moveTimer -= Time.fixedDeltaTime;
yield return null;
}
moveTimer= moveDuration;
rb.gravityScale = gravity;
rb.velocity = new Vector2 (savedVelocityX, .0f);
xInput = savedXinput;
moveCooldownInternal -= Time.deltaTime; // I tried changing this to a specific float - still inconsistent results in physics..
}
private void MoveCooldownCounter()
{
if (moveCooldownInternal != moveCooldown)
{
moveCooldownInternal -= Time.deltaTime;
if (moveCooldownInternal <= 0)
{
moveCooldownInternal = moveCooldown;
}
if (moveCooldownInternal == moveCooldown && isGrounded)
canMove = true;
}
}
private void ApplyMove()
{
if (b_Fire3 && canMove)
{
StartCoroutine("MoveRB");
canMove= false;
}
}
Side note: right now i experience player input loss on occasions because i call this ApplyMove() in FixedUpdate() (will have to find a workaround for that as well - naturally if I can call the ApplyMove in Update() and get consistent results than this issue would be fixed).
Pleas excise my newbiness, i am trying to learn :)
Thank you in advance!
Add comment
Unity is a game engine. All game engines are based on the game loop - however Unity somewhat hides if from you, behind the 3 Update functions.
Not all parts are equally important. Drawing is generally skipped when the GPU is not ready or the FPS are capped. And Drawing and physics are generally calculated independantly of one another (unless you really messed up the development).
With Physics and Animations, you generally want to stuff to be consistent to the Game Tick Counter, not real time. All movement of game pieces and all animation should count in game ticks. Real Time is a nice figure and all, but ticks can have wildly different processing times. Especially since the CPU side drawing work also has to run in the same thread.
Just figure out your maximum ticks/second. And if you want to run something to run for "roughly" 10 ingame seconds, you multiply that value. It does not mater if the game on this machine needs 2 seconds for 200 ticks or 2 minutes for 200 ticks - with the number of ticks, the results will be consistent (unless the math itself is inprecise, like floating point math).
If it's not multiplayer game, it does not have to be hackproof and thus easiest fix is to capture the first frame of the button pressed and apply the force only on that frame, because of that we have to capture input on the Update function and also there immadietly apply the force even though it's suggested to do physics stuff in fixed update, but if you use delta time in update function for calculation it should be fine!
Now I think you know how to do that but I'll write it anyway we can either use apply force or set the velocity,
void Update()
{
if(Input.GetKeyDown("w"))
rb.velocity = new Vector2(rb.velocity.x, rb.velocity.y + 5f*Time.deltaTime);
}
Note: I didn't test it but it should be allright since GetKeyDown returns true only on the first frame when the button was pressed,
If it was just GetKey and in case of jump we would check the grounding of player, we could be during jump and still be grounded on the second frame even if we jumped because of not really precise ground checking.
PS. Use fixedDeltaTime in fixedUpdate method!

How do I create an object to move between two positions in unity, using C#, and why is my code not working?

I have this piece of code right here that should make the block object move between the startPos and endPos objects, but something is wrong about it and I don't know what.
void FixedUpdate()
{
if (block.transform.position == startPos.transform.position)
{
check = false;
}
if(block.transform.position == endPos.transform.position)
{
check = true;
}
if (check == false)
{
block.transform.position = Vector3.Lerp(block.transform.position, endPos.transform.position, .03f);
}
if (check == true)
{
block.transform.position = Vector3.Lerp(block.transform.position, startPos.transform.position, .03f);
}
}
At some point the block will reach endPos, and then on its way back to startPos it will stop, because the functions will be executed simultaneously. But how is this possible because my if's right there should not allow this to happen?
In general you should always be using
Update &rightarrow; called every frame
instead of
FixedUpdate &rightarrow; called in certain realtime intervals
except you are dealing with Physics somehow (which doesn't seem to be the case here). Also see the Update and FixedUpdate Tutorial
The issue with Vector3.Lerp is that it doesn't behave as you expect.
I guess you like that it starts fast and then becomes "smooth" ... but actually this might be your problem.
It never reaches the target position really. It just gets closer and closer and slower and slower ...
... until at some moment the == with a precision of 0.00001f eventually becomes true.
So it might seem that it stopped but actually it might still be moving just really really slow.
For the following two alternatives you have to decide a bit what you want to control:
Option: The speed
If you want to have a linear velocity for the object you should rather use
// adjust via the Inspector
[SerializeField] private float moveSpeedInUnityUnitPerSecond = 1f;
// you should use Update here in general
void Update()
{
if (block.transform.position == startPos.transform.position)
{
check = false;
}
// always use else in cases where only on condition can be
// true at the same time anyway
else if(block.transform.position == endPos.transform.position)
{
check = true;
}
block.transform.position = Vector3.MoveTowards(block.transform.position, check ? startPos.transform.position : endPos.transform.position, Time.deltaTime * moveSpeed);
}
Option: The duration
If you rather want a smooth movement but control the duration it takes to reach the target you should use a Lerp but with a factor depending on the time like
// adjust via the Inspector
[SerializeField] private float moveDurationInSeconds = 1f;
private float passedTime;
// you should use Update here in general
void Update()
{
// prevent overshooting
passedTime += Mathf.Min(moveDurationInSeconds - passedTime, Time.deltaTime);
if(passedTime >= moveDurationInSeconds)
{
check = !check;
passedTime = 0;
}
var lerpFactor = passedTime / moveDurationInSeconds;
// and now add ease-in and ease-out
var smoothedLerpFactor = Mathf.SmoothStep(0, 1, lerpFactor);
var fromPosition = check ? endPos.transform.position : startPos.transform.position;
var toPosition = check ? startPos.transform.position : endPos.transform.position;
block.transform.position = Vector3.Lerp(fromPosition, toPosition, smoothedLerpFactor);
}
For this you could also use a Coroutine which usually is a bit easier to interpret and maintain:
// adjust via the Inspector
[SerializeField] private float moveDurationInSeconds = 1f;
// yes you see correctly one can directly use the Start
// as a Coroutine
private IEnumerator Start()
{
var fromPosition = startPos.transform.position;
var toPosition = endPos.transform.position;
// looks strange but as long as you yield somewhere inside
// the loop it simply means repeat the sequence forever
// just like the Update method
while(true)
{
var passedTime = 0f;
while(passedTime < moveDurationInSeconds)
{
var lerpFactor = passedTime / moveDurationInSeconds;
// and now add ease-in and ease-out
var smoothedLerpFactor = Mathf.SmoothStep(0, 1, lerpFactor);
block.transform.position = Vector3.Lerp(fromPosition, toPosition, smoothedLerpFactor);
passedTime += Mathf.Min(moveDurationInSeconds - passedTime, Time.deltaTime);
// reads like: "pause" here, render this frame and continue
// from here in the next frame
yield return null;
}
// once reached flip the positions
var temp = fromPosition;
fromPosition = toPosition;
toPosition = temp;
}
}
in both cases you could still add more flexibility and instead of simply using the moveDurationInSeconds use
var fixedDuration = moveDurationInSeconds * Vector3.Distance(fromPosition, toPosition);
this way the movement takes shorter if the positions are closer together and longer if they are further apart. This comes pretty close to the Lerp you used before regarding to the smoothness of motion but you can control very good how long the movement will take.
The third argument of Vector3.Lerp is a percentage of the distance between the first two arguments. When you pass in 0.03f, you're getting 3% closer to your end position but never actually getting exactly there (You can prove this by logging the block's position and the target's position and you'll see they're never perfectly equal).
Instead of checking if your block's position with the == operator (which works to an accuracy of 0.00001f) to the target position, you could simply check if it's close enough with Vector3.Distance:
if(Vector3.Distance(block.transform.position, endPos.transform.position) < 0.1f) {
You can make your "close enough threshold" (0.1f in the example) a named variable for easy adjustment until it feels right.
For starters, Update() loops every frame. FixedUpdate() depends on the number of frames per second set in the time setting. So, put your code into Void Update() instead.
If you refer to Vector3.MoveTowards documentation, you may find a much better approach to your problem.
The script below should do the trick. The target variable should be set to the position of your end point. Speed is how fast your object should move.
Inside Update(), the step variable is used to determine how far the object should move based off its speed and the time elapsed since it last moved. Finally, the last line changes the position of the object and records the new position.
public Transform target;
public float speed;
void Update() {
float step = speed * Time.deltaTime;
transform.position = Vector3.MoveTowards(transform.position, target.position, step);
}

How do I use a Coroutine (or any other method) to slide a sprite across the screen on click?

I'm developing a 2D game in Unity, and I am stuck with a problem. I am trying to move a sprite from point A to point B when the user clicks. I have a Vector3 for both the starting point and the target.
I was able to get the desired change in position but only in one frame. I want the sprite to actually slide across, but I have been unable to. I tried using Coroutines but have not been successful.
IEnumerator CubeSlider(Vector3 start, Vector3 target)
{
while(start.x != target.x)
{
start.x += 0.1f;
yield return null;
}
}
I'm only taking x into account because there is no movement on the y axis. I also tried using:
start = Vector3.MoveTowards(parameters here)
but was not successfull.
I started and called this Coroutine on my "onMouseDown()" function of the script like so:
StartCoroutine(CubeSlider(square1.transform.position, p3));
I would expect that when the user makes a click, the sprite would smoothly move across from start to target. I'm not getting any compiler errors, it's simply not moving at all.
Your first approach is incorrect. You are just adding to that float value another float value. Instead, you need to use Vector2 or Vector3. You can use your approach and that is not going to have any errors, but that won't affect your gameObject. If you wanted to move an object like that, you needed to use Vector2 or Vector3 in your loop. Also, you will need a helper method because values between those 2 Vectors will never be exactly equal. Below is one example of your first approach.
start.position += new Vector3(2f, 0f, 0f) * Time.deltaTime;
Since I see that you are passing arguments to your Coroutine, if you want to move a gameObject that doesn't have this script attached, pass its Transform Component. Of course, because this is an example I have placed everything in 1 Script. Below is an example of Unity Method.
public class Example : MonoBehaviour
{
public Vector3 myTarget;
public Transform myStart;
void Start()
{
StartCoroutine(CubeSlider(myStart, myTarget));
}
IEnumerator CubeSlider(Transform start, Vector3 target)
{
while (start.position.x != target.x)
{
start.position = Vector3.MoveTowards(start.position, target, 2f * Time.deltaTime);
yield return null;
}
Debug.Log("I reached my target. Done!");
}
}
If you are interested you may also want to see my other answer on this topic, here.

Character cannot move after touching a wall

I'm making a pacman clone using an online tutorial. I've just gotten to pacman's movement and when Pacman touches a wall he can no longer move. Here is the movement script
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class pacman : MonoBehaviour
{
public int speed;
Vector2 dest;
// Start is called before the first frame update
void Start()
{
dest = transform.position;
speed = 5;
}
// Update is called once per frame
void FixedUpdate()
{
Vector2 p = Vector2.MoveTowards(transform.position, dest, speed);
GetComponent<Rigidbody2D>().MovePosition(p);
// Check for Input if not moving
if ((Vector2)transform.position == dest)
{
if (Input.GetKey("w") )
dest = (Vector2)transform.position + Vector2.up;
if (Input.GetKey("d"))
dest = (Vector2)transform.position + Vector2.right;
if (Input.GetKey("s"))
dest = (Vector2)transform.position - Vector2.up;
if (Input.GetKey("a"))
dest = (Vector2)transform.position - Vector2.right;
}
}
}
You have a couple of things you should watch out for.
1) You're never resetting "dest" anywhere in your code. As you're moving based on the calculation being set to "p". I imagine you're character is hitting a wall, which is as close as it can get to "dest", therefore it cannot move any closer.
I can't predict what you're trying to get your gameplay to be, but I would imagine you'd want to reset "dest" in your OnCollision() to keep the object moving instead of staring at a wall.
As a general piece of advice, I wouldn't set PacMac (the player controlled unit) to be going to a destination. You'd want to calculate an offset based on the input, and then attempt to add that to the transform.position (probably safer through the RigidBody system) and then let the simulation take over from there.
2) You're moving without any reference to gametime. You should really change your offset to be calculated with taking Time.deltaTime into account. This is important for when you're running on a fast computer or a slow computer.
With your current code, you'll move a lot faster on a strong computer and slower on a slow computer.
3) Based on your pacman experience, you might want to change these to else if statements. Even better, but is harder, only accept the last input. This will keep you from moving diagonally (which your current code is susceptible to. If you do the second method, you'll want to keep a stack of all the buttons pressed in case someone tries to hold down multiple buttons at the same time.

Categories

Resources