Can I write my movement script shorthand? - c#

I am new to unity and C# coding, and I wanted to see if the code I have written for movement could be written more shorthand or even just improving how the movement flows. I think seeing the shortened solutions to my problem will also help me when I add more scripts onto this. Thanks!
//Variables
Rigidbody rb;
public GameObject player;
public float speed = 4f;
private float minLimit = 0.51f; //MinLimit is the height you need to be below so you can jump
private float airTime = 0.875f; //The amount of time spent in the air if you press a movement key
private float maxLimit = 3.66f; //Maxlimit is the maximum jump height
Below is the movement script. Imagine this copied and pasted for all four directions. The player travels in the opposite direction to normal because of the (I think) camera position.
if (Input.GetKey(KeyCode.LeftArrow) || Input.GetKey(KeyCode.A))
{
if (transform.position.y > minLimit && !Input.GetKey(KeyCode.Space) && transform.position.y < maxLimit)
{
if (Input.GetKeyDown(KeyCode.A) || Input.GetKeyDown(KeyCode.LeftArrow))
{
rb.velocity = new Vector3(1, -airTime, 0) * speed;
}
}
else
{
if (Input.GetKeyDown(KeyCode.Space) && player.transform.position.y < minLimit)
{
rb.velocity = new Vector3(1, 2, 0) * speed;
}
if (!Input.GetKey(KeyCode.S) && !Input.GetKey(KeyCode.W) && !Input.GetKey(KeyCode.UpArrow) && !Input.GetKey(KeyCode.DownArrow) && !Input.GetKey(KeyCode.Space))
{
rb.velocity = new Vector3(1, 0, 0) * speed;
}
if (Input.GetKey(KeyCode.S) || Input.GetKey(KeyCode.DownArrow))
{
rb.velocity = new Vector3 (1, 0, 1) * speed;
}
if (Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.UpArrow))
{
rb.velocity = new Vector3(1, 0, -1) * speed;
}
}
}

Welcome to StackOverflow. Since you're new to coding at least in C# and Unity, do take in this advice first: Keep doing what you do - make it work first, then see whether you can improve on it. :)
To begin with, I'd suggest you to not use KeyCode based input, but rather investigate how the Input Manager works. For the time being, this is the standard way of declaring input keybindings without having to think about the actual keys in code. With the Unity versions following 2019.3, another method has emerged, but this might be a bit too complicated right now.
What the Input Manager allows you to do is to define a specific action, say Fire, and then bind keys, mouse movements, buttons and joystick input to it. In code, you'd refer to that action using e.g. GetAxis or GetButtonDown with the name of the action:
Input.GetButtonDown("Fire")
If you decide to change the key bindings later on, you don't need to touch your code.
For your code, I'd suggest to fist lay everything out into individual variables that have "speaking" names:
var moveLeft = Input.GetButton("Left"); // Input.GetKey(KeyCode.LeftArrow) || Input.GetKey(KeyCode.A);
var moveUp = Input.GetButton("Up"); // Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.UpArrow);
var moveDown = Input.GetButton("Down"); // Input.GetKey(KeyCode.S) || Input.GetKey(KeyCode.DownArrow);
var jump = Input.GetButtonDown("Jump"); // Input.GetKeyDown(KeyCode.Space);
if (moveLeft)
{
if (!jump && transform.position.y > minLimit && transform.position.y < maxLimit)
{
// No need to check for "Left Arrow || A" again, because you already did.
rb.velocity = new Vector3(1, -airTime, 0) * speed;
}
else
{
if (jump && player.transform.position.y < minLimit)
{
rb.velocity = new Vector3(1, 2, 0) * speed;
}
if (!moveUp && !moveDown)
{
rb.velocity = new Vector3(1, 0, 0) * speed;
}
if (moveDown)
{
rb.velocity = new Vector3 (1, 0, 1) * speed;
}
if (moveUp)
{
rb.velocity = new Vector3(1, 0, -1) * speed;
}
}
}
Next, try to remove the nesting by inverting conditions, or leaving the method at the earliest time possible:
private void Update()
{
var moveLeft = Input.GetButton("Left");
if (moveLeft) MoveLeft();
}
private void MoveLeft()
{
var moveUp = Input.GetButton("Up");
var moveDown = Input.GetButton("Down");
var jump = Input.GetButtonDown("Jump");
if (!jump && transform.position.y > minLimit && transform.position.y < maxLimit)
{
rb.velocity = new Vector3(1, -airTime, 0) * speed;
return;
}
// Note the use of "else if" here. You are overwriting "rb.velocity"
// in every block, so the last condition wins. In this change,
// the first condition to be true is applied, but it makes the priorities
// explicit.
if (jump && player.transform.position.y < minLimit)
{
rb.velocity = new Vector3(1, 2, 0) * speed;
}
else if (!moveUp && !moveDown)
{
rb.velocity = new Vector3(1, 0, 0) * speed;
}
else if (moveDown)
{
rb.velocity = new Vector3 (1, 0, 1) * speed;
}
else if (moveUp)
{
rb.velocity = new Vector3(1, 0, -1) * speed;
}
}
Going further, try to avoid lookups of properties such as transform.position and try to avoid instance creation wherever possible. It doesn't have a big influence on your current code, but let's try this out:
// Note that directions should always be normalized as they would otherwise
// introduce different "strength" factors.
private readonly Vector3 LeftJumpDirection = new Vector3(1, 2, 0).normalized;
private readonly Vector3 LeftDirection = new Vector3(1, 0, 0).normalized;
private readonly Vector3 DownDirection = new Vector3 (1, 0, 1).normalized;
private readonly Vector3 UpDirection = new Vector3(1, 0, -1).normalized;
private void float jumpSpeed = 1f;
private void Update()
{
var moveLeft = Input.GetButton("Left");
if (moveLeft) MoveLeft();
}
private void MoveLeft()
{
var moveUp = Input.GetButton("Up");
var moveDown = Input.GetButton("Down");
var jump = Input.GetButtonDown("Jump");
// This property is cached here since we only read from it.
var position = transform.position;
// Also, let's add clear names ...
var inAir = position.y > minLimit && position.y < maxLimit;
var canJump = position.y < maxLimit;
if (!jump && inAir)
{
rb.velocity = new Vector3(1, -airTime, 0) * jumpSpeed;
return;
}
// Note the use of "else if" here. You are overwriting "rb.velocity"
// in every block, so the last condition wins. In this change,
// the first condition to be true is applied, but it makes the priorities
// explicit.
if (jump && canJump)
{
rb.velocity = JumpDirection * jumpSpeed;
}
else if (!moveUp && !moveDown)
{
rb.velocity = LeftDirection * speed;
}
else if (moveDown)
{
rb.velocity = DownDirection * speed;
}
else if (moveUp)
{
rb.velocity = UpDirection * speed;
}
}
So far, your code only changed its readability (though, keep in mind that code is read by humans, not by machines).
For the next steps, here's some pointers:
Your object's transform (property) has a forward property that points in the forward direction of your object.
Always normalize your direction vectors, then scale them (e.g. by speed).
If you need to think about different directions at the same time, add your direction vectors, then normalize again.
To jump, maybe try to add an impulse force to your rigidbody by calling rb.AddForce(Direction * Intensity, ForceMode.Impulse).
With these information, try to keep the concerns separated:
private void Update()
{
// GetAxis already gives you a notion of positive or negative, e.g.
// left or right. Try using axisValue * transform.forward.
var moveIntensity = Input.GetAxis("Horizontal");
if (moveIntensity != 0) TryMove(moveIntensity);
var jump = Input.GetButtonDown("Jump");
if (jump) TryJump();
}
I personally like to separate these actions into DoSomething() and TryDoSomething(), where I'm only ever calling DoSomething() when something should be done for sure, and TryDoSomething() when that call could fail due to further logic. Here, TryJump() would indicate that the user expressed the wish to jump, but it might not result in any state change, e.g. because the user was mid air.
Maybe this already gives you some ideas.
Example code from somewhere else
Since you were asking about how to generalize your code for multiple directions and I just answered you to to have different code, but didn't give you a different solution - here's an extra. This is some code I've been using for a player controller some time ago that involves walking and jumping:
[SerializeField]
private float moveSpeed;
[SerializeField]
private float jumpForce;
[SerializeField]
private float groundDistance = 0.7f;
private Rigidbody _rig;
private void Awake()
{
_rig = GetComponent<Rigidbody>();
}
private void Update()
{
Move();
// In Unity, select "Edit > Project Settings > Input" for the input configuration.
if (Input.GetButtonDown("Jump"))
{
TryJump();
}
}
void Move()
{
var xInput = Input.GetAxis("Horizontal");
var zInput = Input.GetAxis("Vertical");
// Set a new speed velocity vector.
var dir = new Vector3(xInput, 0, zInput) * moveSpeed;
// Patch in the y velocity without affecting it by the move speed..
dir.y = _rig.velocity.y;
// Assign the updated velocity to our rigid body.
_rig.velocity = dir;
// Update the forward direction according to the movement vector.
var facingDir = new Vector3(xInput, 0, zInput);
if (facingDir.sqrMagnitude > 0)
{
transform.forward = facingDir;
}
}
void TryJump()
{
// This implements ray-cast jumping:
// We're shooting a ray downwards to see whether we're on the ground. With a player height of
// 1 unit and the reference point at the center of the object, a maximum distance
// of 0.7 would tell us whether there was an object up to 0.2 units below the player object.
// We treat this as an indicator that there was solid ground, so the player can jump.
var pos = transform.position;
var ray = new Ray(pos + Vector3.zero, Vector3.down);
var hit = Physics.Raycast(ray, groundDistance);
if (hit)
{
// We want the force to be applied instantaneously, not accelerating the player constantly.
// Because of this, we use "impulse" force mode.
_rig.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
}
}
Have fun! :)

Related

How can I stop my character Going Up Automatically?

Why my character going up automatically though I'm not pressing any button?
I want to control my player jump movement.
But I cant do it.
What is the solution?
Please explain.
Can you also suggest how can I customise my jump, like jump twice? etc
public class movement: MonoBehaviour {
public float speed = 10;
private Rigidbody2D rb;
public float jumpSpeed = 1;
void Start() {
rb = GetComponent < Rigidbody2D > ();
}
// Update is called once per frame
void Update() {
Jump();
var inputX = Input.GetAxis("Horizontal");
Vector3 movement = new Vector3(inputX, 0, 0) * speed;
movement *= Time.deltaTime;
transform.Translate(movement);
Vector3 characterScale = transform.localScale;
if (inputX < 0) {
transform.localScale = new Vector3(5, transform.localScale.y, transform.localScale.z);
}
if (inputX > 0) {
transform.localScale = new Vector3(-5, transform.localScale.y, transform.localScale.z);
}
}
void Jump() {
if (Input.GetKeyDown(KeyCode.Space));
rb.velocity = Vector2.up * jumpSpeed;
}
}
Your issue is with the if statement in Jump. If there are no curly braces after an if statement, only the first statement is enclosed within the if.
if (something)
do_stuff(); // This will execute if something evaluates to true
do_other_stuff(); // This will execute no matter what
In your case, the next statement looks like this:
if (Input.GetKeyDown(KeyCode.Space))
;
The semicolon is an empty statement. Then, the second line rb.velocity = Vector2.up * jumpSpeed; executes normally.
This is in java, but the same logic applies to C#
Proper code will look like:
if (Input.GetKeyDown(KeyCode.Space)) {
rb.velocity = Vector2.up * jumpSpeed;
}
Note that there is no semicolon on the line of the if statement.

Vector3.Lerp isn't moving the whole way

Basically I'm trying to create a rolling system for my top down RPG style game, but when I do the Vector3.Lerp it doesn't move the entire way
The code is:
public void CheckSpaceKey()
{
if (exhausted == false)
{
if (Input.GetKey(KeyCode.Space) && canRoll)
{
if (rollDir == RollDir.Up)
{
Vector3 rollStartPos = new Vector3(transform.position.x, transform.position.y, -1.0f);
Vector3 rollEndPos = new Vector3(transform.position.x, transform.position.y + 3, -1.0f);
transform.position = Vector3.Lerp(rollStartPos, rollEndPos, Time.deltaTime);
canRoll = false;
playerStats.stamina -= 10;
Invoke("RollCooldown", 1.5f);
}
}
}
}
public void RollCooldown()
{
canRoll = true;
}
This should be making my player move upwards 3 units, but it instead is moving a random number of around 0.35 to 0.45.
Also CheckSpaceKey is being called in update
Lerp doesn't do what you think it does. Assuming Time.deltaTime is about 1/60, it's going to move the unit 1/60th of the way to the destination, and that's it.
Consider using a coroutine that updates the t parameter for Lerp with each frame:
public void CheckSpaceKey()
{
// by the way, this could just be `if (!exhausted)`
// although some coding guidelines require this format below
if (exhausted == false)
{
if (Input.GetKey(KeyCode.Space) && canRoll)
{
if (rollDir == RollDir.Up)
{
StartCoroutine(DoRoll(Vector3.up * 3));
}
}
}
}
IEnumerator DoRoll(Vector3 offset)
{
float duration = 1f;
canRoll = false;
playerStats.stamina -= 10;
Invoke("RollCooldown", 1.5f);
Vector3 rollStartPos = new Vector3(transform.position.x, transform.position.y, -1.0f);
Vector3 rollEndPos = rollStartPos + offset;
float t = 0;
while (t < 1f)
{
t = Mathf.Min(1f, t + Time.deltaTime/duration);
transform.position = Vector3.Lerp(rollStartPos, rollEndPos, t);
yield return null;
}
}
public void RollCooldown()
{
canRoll = true;
}
This will not move 3 Units, you call
transform.position only inside the if case and access the if case only once every 1.5 seconds.
Lerp will not update your position every frame but only the frame it is called, if you the object to move constantly you have to update your position constantly you should crate rollStartPos and rollEndPos inside the first if and create add
else{
time += Time.deltaTime;
transform.position = Vector3.Lerp(rollStartPos, rollEndPos, time);
}
under the 2nd if. This way it will lerp til rollEndPos unless the ball(?) is exhausted, if it should roll even when it's exhausted you need to update the position outside the first if

Dash against slopes using physics in Unity 2D

I'm working on a 2D project in Unity.
The character controller is physics based, so I use rigidbody to move the player. Everything is working fine except when I try to apply a high speed movement to the character, like a dash.
This is how the code looks like.
I just check if the player is dashing, so I increase the Vector2 movement in a certain amount.
private void DashMovement() {
if (isDashing) {
movement.x *= dashFactor;
}
}
I'm also calculating the ground angle, so I set the movement vector to follow the ground inclination.
private void OnSlopeMovement() {
if (isGrounded && !isJumping) {
float moveDistance = Mathf.Abs(movement.x);
float horizontalOnSlope = Mathf.Cos(groundAngle * Mathf.Deg2Rad) * moveDistance * Mathf.Sign(movement.x);
float verticalOnSlope = Mathf.Sin(groundAngle * Mathf.Deg2Rad) * moveDistance;
if (horizontalOnSlope != 0)
movement.x = horizontalOnSlope;
if (isGrounded && verticalOnSlope != 0)
movement.y = verticalOnSlope;
}
SetMaxFallVelocity();
}
So I set the rigidbody velocity for making it move.
private void Move() {
movement.x *= Time.fixedDeltaTime;
if(isGrounded && !isJumping) movement.y *= Time.fixedDeltaTime;
Vector3 targetVelocity = new Vector2(movement.x, movement.y);
PlayerController.rb2d.velocity = Vector3.SmoothDamp(PlayerController.rb2d.velocity, targetVelocity, ref velocity, movementSmoothing);
}
The problem appears when I apply a speed high enough. I understand this issue is because of physics.
I think the ray that checks the ground and is used to calculate the groundAngle doesn't work fast enough to keep track of that movement, so I can not keep the player fixed on the ground.
I would like to find a solution without making the player kinematic, or stopping the dash on slopes.
This is how it looks ingame.
And this is how the rigidbody movement remain right over the ground, following the slopes angle.
EDIT:
This is how I get the ground angle:
private void GroundAngle() {
Vector2 rayOrigin = feetCollider.bounds.center;
rayOrigin.y += 0.1f;
Vector2 rayDirection = (Input.GetAxisRaw("Horizontal") == 0) ? Vector2.right : new Vector2(Input.GetAxisRaw("Horizontal"), 0);
int groundCollisions = Physics2D.RaycastNonAlloc(rayOrigin, Vector2.down, groundResults, Mathf.Infinity, groundMask);
if (groundCollisions > 0) {
groundAngle = Vector2.Angle(groundResults[0].normal, rayDirection) - 90f;
//Debug.DrawRay(rayOrigin, Vector2.down, Color.green);
if (groundAngle > 0 && !isDashing) {
rayOrigin.x += Input.GetAxisRaw("Horizontal") * .125f;
Physics2D.RaycastNonAlloc(rayOrigin, Vector2.down, groundResults, Mathf.Infinity, groundMask);
groundAngle = Vector2.Angle(groundResults[0].normal, rayDirection) - 90f;
//Debug.DrawRay(rayOrigin, Vector2.down, Color.blue);
}
}
}
Thanks to #Ruzhim for the help. I just post a first "solution" for the problem.
According to Ruzhim advises, I've used him code this way.
private void SetPositionAfterTick() {
if (isDashMovement) {
Vector2 currentPosition = new Vector2(transform.position.x, transform.position.y);
currentPosition.y = feetCollider.bounds.min.y;
Vector2 feetPosAfterTick = currentPosition + PlayerController.rb2d.velocity * Time.deltaTime;
float maxFloorCheckDist = .1f;
RaycastHit2D groundCheckAfterTick = Physics2D.Raycast(feetPosAfterTick + Vector2.up * maxFloorCheckDist, Vector2.down, maxFloorCheckDist * 5f);
if (groundCheckAfterTick) {
Vector2 wantedFeetPosAfterTick = groundCheckAfterTick.point;
if (wantedFeetPosAfterTick != feetPosAfterTick) {
//PlayerController.rb2d.transform.position = (wantedFeetPosAfterTick + new Vector2(0f, feetCollider.bounds.min.y - PlayerController.rb2d.position.y));
PlayerController.rb2d.velocity = Vector2.zero;
}
}
}
}
This is how it looks like.
This is good enough to continue polishing that mechanic. I still need to set the position in some way. The rigidbody's position calculation is not working as it
is raised right now, as the condition (wantedFeetPosAfterTick != feetPosAfterTick) is always true, so the character goes throw the floor and fall.
As you can see, I also need to control the down slopes movement, as it uses the slopes movement sometimes, and dash straight forward others.
This is how asker Rubzero implemented the below code to work for them:
private void SetPositionAfterTick() {
if (isDashMovement) {
Vector2 currentPosition = new Vector2(transform.position.x, transform.position.y);
currentPosition.y = feetCollider.bounds.min.y;
Vector2 feetPosAfterTick = currentPosition + PlayerController.rb2d.velocity * Time.deltaTime;
float maxFloorCheckDist = .1f;
RaycastHit2D groundCheckAfterTick = Physics2D.Raycast(feetPosAfterTick + Vector2.up * maxFloorCheckDist,
Vector2.down, maxFloorCheckDist * 5f);
if (groundCheckAfterTick) {
Vector2 wantedFeetPosAfterTick = groundCheckAfterTick.point;
if (wantedFeetPosAfterTick != feetPosAfterTick) {
//PlayerController.rb2d.transform.position = (wantedFeetPosAfterTick + new Vector2(0f, feetCollider.bounds.min.y -
PlayerController.rb2d.position.y));
PlayerController.rb2d.velocity = Vector2.zero;
}
}
}
}
This is how it looks like.
This is good enough to continue polishing that mechanic. I still need
to set the position in some way. The rigidbody's position calculation
is not working as it is raised right now, as the condition
(wantedFeetPosAfterTick != feetPosAfterTick) is always true, so the
character goes throw the floor and fall.
As you can see, I need to control the down slopes movement, as it uses
the slopes movement sometimes, and dash straight forward others.
I agree with AresCaelum; using physics to do slope movement is pretty much the opposite of what you want to be doing if you don't want to preserve momentum when you're done going up/down the slope. Specifically, your problem is here:
float moveDistance = Mathf.Abs(movement.x);
float horizontalOnSlope = Mathf.Cos(groundAngle * Mathf.Deg2Rad) * moveDistance * Mathf.Sign(movement.x);
float verticalOnSlope = Mathf.Sin(groundAngle * Mathf.Deg2Rad) * moveDistance;
This is a problem because the more the player moves horizontally in a frame, the more they will move vertically based on the slope of the ramp they are on. However, this assumption doesn't hold if they should only be traveling up the ramp during only part of the movement during the frame. So, you need a way to handle that situation.
One solution is to use a raycast from where the player would be then if it's above the floor, alter the vertical velocity so that it would place them at that floor's position instead.
First, determine if slope movement has occurred in a physics frame...
private bool slopeMovementOccurred = false;
void FixedUpdate() {
slopeMovementOccurred = false;
// ...
}
private void OnSlopeMovement() {
if (isGrounded && !isJumping) {
slopeMovementOccurred = true;
// ...
}
SetMaxFallVelocity();
}
... and if it has, determine where the player is going to be after the physics update. Then do a physics2d raycast from above that position (by some amount) downward (double the previous amount) to find where the player's position should be, and then change the rb2d.velocity such that it will place the player exactly at the height they should be at.
Assuming you can calculate some kind of Vector2 feetOffset that has the local position of the player's feet:
void FixedUpdate() {
// ...
StickToSlopeLanding();
}
void StickToSlopeLanding() {
if (slopeMovementOccurred) {
Vector2 curVelocity = PlayerController.rb2d.velocity;
Vector2 feetPosAfterTick = PlayerController.transform.position
+ PlayerController.feetOffset
+ curVelocity * Time.deltaTime;
float maxFloorCheckDist = 1.0f;
// determine where the player should "land" after this frame
RaycastHit2D groundCheckAfterTick = Physics2D.Raycast(
feetPosAfterTick + Vector2.up * maxFloorCheckDist,
-Vector2.up, maxFloorCheckDist * 2f);
if (groundCheckAfterTick.collider != null) {
Vector2 wantedFeetPosAfterTick = groundCheckAfterTick.point;
// if basic physics won't take them to landing position
if (wantedFeetPosAfterTick != feetPosAfterTick) {
Vector2 wantedVelocity = curVelocity
+ Vector2.up
* ((wantedFeetPosAfterTick.y - feetPosAfterTick.y)
/ Time.deltaTime);
// adjust velocity so that physics will take them to landing position
PlayerController.rb2d.velocity = wantedVelocity;
// optionally, set a flag so that next frame
// it knows the player should be grounded
}
}
}
}
Hopefully this gets you towards a solution that will work.
Note: you may need to also move the rigidbody so that it doesn't try to clip through the corner at the top of the ramp, and you can determine where to put the rigidbody using another raycast, setting the velocity from that point to be horizontal:
void StickToSlopeLanding() {
if (slopeMovementOccurred) {
Vector2 curVelocity = PlayerController.rb2d.velocity;
Vector2 feetPosAfterTick = PlayerController.transform.position
+ PlayerController.feetOffset
+ curVelocity * Time.deltaTime;
float maxFloorCheckDist = 1.0f;
// determine where the player should "land" after this frame
RaycastHit2D groundCheckAfterTick = Physics2D.Raycast(
feetPosAfterTick + Vector2.up * maxFloorCheckDist,
-Vector2.up, maxFloorCheckDist * 2f);
if (groundCheckAfterTick.collider != null) {
Vector2 wantedFeetPosAfterTick = groundCheckAfterTick.point;
// if basic physics won't take them to landing position
if (wantedFeetPosAfterTick != feetPosAfterTick) {
// look for corner of ramp+landing.
// Offsets ensure we don't raycast from inside/above it
float floorCheckOffsetHeight = 0.01f;
float floorCheckOffsetWidth = 0.5f;
RaycastHit2D rampCornerCheck = Physics2D.Raycast(
wantedFeetPosAfterTick
- floorCheckOffsetHeight * Vector2.up
- floorCheckOffsetWidth * Mathf.Sign(movement.x) * Vector2.right,
Mathf.Sign(movement.x) * Vector2.right);
if (rampCornerCheck.collider != null) {
// put feet at x=corner position
Vector2 cornerPos = Vector2(rampCornerCheck.point.x,
wantedFeetPosAfterTick.y);
PlayerController.rb2d.position = cornerPos
- PlayerController.feetOffset;
// adjust velocity so that physics will take them from corner
// to landing position
Vector2 wantedVelocity = (wantedFeetPosAfterTick - cornerPos)
/ Time.deltaTime;
PlayerController.rb2d.velocity = wantedVelocity;
// optionally, set a flag so that next frame
// it knows the player should be grounded
}
}
}
}
}

Why Coroutine Method Work Only Once Unity3D

I have an object that I want to move by swipe, for example when the swipe is up the object should move forward smoothly from point A to point B, swipe right the object move smoothly to the right etc...
To do that, I've tried Lerp, MoveTowards and SmoothDamp but every time the object just disappear from point A and appear on point B instantly.
So I used coroutine to give a time to the movement, and as you can see in the code bellow, there's 4 coroutine methods, each one is for a direction. the problem I have is that when playing, the first movement work properly, but in the second swipe the object didn't reach the destination point, and the third one also and the object have some weird movements.
Can you tell me what's wrong in my code?
Here's the Coroutine methods for movements:
public IEnumerator MoveForward()
{
Vector3 DestinationF = new Vector3(transform.position.x, transform.position.y, transform.position.z + DistanceF);
while (Vector3.Distance(transform.localPosition, DestinationF) > 0)
{
float totalMovementTimeF = 0.3f;
float currentMovementTimeF = 0f;
currentMovementTimeF += Time.deltaTime;
transform.localPosition = Vector3.Lerp(transform.position, DestinationF, currentMovementTimeF / totalMovementTimeF);
yield return null;
}
}
public IEnumerator MoveBackward()
{
Vector3 DestinationB = new Vector3(transform.position.x, transform.position.y, transform.position.z - DistanceB);
while (Vector3.Distance(transform.localPosition, DestinationB) > 0)
{
float totalMovementTimeB = 0.3f;
float currentMovementTimeB = 0f;
currentMovementTimeB += Time.deltaTime;
transform.localPosition = Vector3.Lerp(transform.position, DestinationB, currentMovementTimeB / totalMovementTimeB);
yield return null;
}
}
and there is still 2 coroutine methods MoveRight() and MoveLeft().
And here's the code for the swipe directions:
if (Input.GetMouseButtonDown(0))
{
//save began touch 2d point
firstPressPos = new Vector3(Input.mousePosition.x, Input.mousePosition.y);
}
if (Input.GetMouseButtonUp(0))
{
//save ended touch 2d point
secondPressPos = new Vector3(Input.mousePosition.x, Input.mousePosition.y);
//create vector from the two points
currentSwipe = new Vector3(secondPressPos.x - firstPressPos.x, secondPressPos.y - firstPressPos.y);
//normalize the 2d vector
currentSwipe.Normalize();
// swipe up
if (currentSwipe.y > 0 && currentSwipe.x > -0.5f && currentSwipe.x < 0.5f)
{
StartCoroutine(MoveForward());
}
// swipe down
if (currentSwipe.y < 0 && currentSwipe.x > -0.5f && currentSwipe.x < 0.5f)
{
StartCoroutine(MoveBackward());
}
//swipe left
if (currentSwipe.x < 0 && currentSwipe.y > -0.5f && currentSwipe.y < 0.5f)
{
StartCoroutine(MoveLeft());
}
//swipe right
if (currentSwipe.x > 0 && currentSwipe.y > -0.5f && currentSwipe.y < 0.5f)
{
StartCoroutine(MoveRight());
}
}
Your first Coroutine works because:
Vector3 DestinationF = new Vector3(transform.position.x, transform.position.y, transform.position.z + DistanceF);
will result in a positive position, so, the distance will be greater than 0:
while (Vector3.Distance(transform.localPosition, DestinationF) > 0)
On the other hand, while subtracting the distanceB from the z value:
Vector3 DestinationB = new Vector3(transform.position.x, transform.position.y, transform.position.z - DistanceB);
may result in a negative value, therefore:
while (Vector3.Distance(transform.localPosition, DestinationB) > 0)
will start as < 0, so the condition is never met. Check your condition. Do you want absolute values, or not equal to 0?
The issue is that you never reach the target.
Your lerping with factor
currentMovementTimeF / totalMovementTimeF
makes not much sense since you reset it every frame to
var currentMovementTimeF = Time.deltaTime;
which in the most cases will be < 0.3f (this would mean you have only about 3 frames per second) so it will always be
currentMovementTimeF < totalMovementTimeF
and therefore
currentMovementTimeF / totalMovementTimeF < 1
So you always start a new interpolation between the current position and the target. So the distance gets smaller and smaller but literally never reaches the final position actually (though it seems so).
Additionally you mixed position and localPosition there so if the GameObject is not on root level it gets even worse!
What you would want instead is probebly either using MoveTowards with a certain speed. (position based)
// adjust these in the Inspector
public float speed;
public float MoveDistance;
public IEnumerator Move(Vector3 direction)
{
var destinaton = transform.position + direction * MoveDistance;
while (Vector3.Distance(transform.position, destinaton) > 0)
{
transform.position = Vector3.MoveTowards(transform.position, MoveDistance, Time.deltaTime* speed);
yield return null;
}
}
MoveTowards makes sure there is no overshooting.
or using Lerp (time based) like
// adjust these in the Inspector
public float totalMovementTime = 0.3f;
public float MoveDistance;
public IEnumerator Move(Vector3 direction)
{
var originalPosition = transform.position;
var destination = transform.position + direction * MoveDistance;
// here you could make it even more accurate
// by moving always with the same speed
// regardless how far the object is from the target
//var moveDuration = totalMovementTime * Vector3.Distance(transform.position, destinaton);
// and than replacing totalMovementTime with moveDuration
var currentDuration = 0.0f;
while (currentDuration < totalMovementTime)
{
transform.position = Vector3.Lerp(originalPosition, destination, currentDuration / totalMovementTime);
currentDuration += Time.deltaTime;
yield return null;
}
// to be really sure set a fixed position in the end
transform.position = destinaton;
}
Another issue is that you currently still could start two concurrent coroutines which leeds to strange behaviours. You rather should either interrupt the coroutines everytime you start a new one like
if (currentSwipe.y > 0 && currentSwipe.x > -0.5f && currentSwipe.x < 0.5f)
{
// stop all current routines
stopAllCoroutines();
StartCoroutine(MoveForward());
}
or add a flag to have only one routine running and ignore input in the meantime:
private bool isSwiping;
public IEnumerator MoveForward()
{
if(isSwiping)
{
Debug.LogWarning("Already swiping -> ignored", this);
yield break;
}
isSwiping = true;
//...
isSwiping = false;
}

Unity Mathf.Lerp only executes ones

void Fire(float firingRate)
{
TimePerFrame += Time.deltaTime;
if(TimePerFrame >= firingRate)
{
Vector3 ProjectileDistance = new Vector3(0, 30, 0); //distance between center of the campion and it's head
GameObject beam = Instantiate(projectile, transform.position + ProjectileDistance, Quaternion.identity) as GameObject;
beam.GetComponent<Rigidbody2D>().velocity = new Vector3(0, projectileSpeed, 0);
// AudioSource.PlayClipAtPoint(fireSound, transform.position);
TimePerFrame = 0;
}
}
void Update ()
{
if (freezePosition == false)
{
Fire(firingRate);
PositionChaning();
firingRate = Mathf.Lerp(minFiringRate, maxFiringRate, 0.1f);
Debug.Log(firingRate);
}
}
I want my firerate to be flexible, i want it to start by shooting fast and let it automatically lower it's fire rate. (the bigger the firingRate float is the slower the speed is)
The problem is that firingRate = Mathf.Lerp(minFiringRate, maxFiringRate, 0.1f);
triggers once and only once. It doesn't seem to change it's value every frame.
Debug.Log(firingRate); tells the value every frame but it seems to remain a constant.
Why does this happen?
The update triggers every Frame, and so does your Mathf.Lerp However you are not changing the interpolation, which in your case is defined as 0.1f.
By changing this interpolation, you will be able to achieve the 'shifting' of fire rate.
In your case you could define a variable t outside the scope of your update, and update it inside the Update() through t += 0.5f * Time.deltaTime;
The Mathf.Lerp documentation has a pretty good sample of how todo so as well
void Update()
{
// animate the position of the game object...
transform.position = new Vector3(Mathf.Lerp(minimum, maximum, t), 0, 0);
// .. and increate the t interpolater
t += 0.5f * Time.deltaTime;
// now check if the interpolator has reached 1.0
// and swap maximum and minimum so game object moves
// in the opposite direction.
if (t > 1.0f)
{
float temp = maximum;
maximum = minimum;
minimum = temp;
t = 0.0f;
}
}
Your problem is here:
firingRate = Mathf.Lerp(minFiringRate, maxFiringRate, 0.1f);
As you can see here your t has to be calculated every frame.
public static float Lerp(float a, float b, float t);
You can change it like this:
private float fireTimer = 1.0f;
public float fireLimiter = 0.05f;
void Fire(float firingRate)
{
TimePerFrame += Time.deltaTime;
if(TimePerFrame >= firingRate)
{
Vector3 ProjectileDistance = new Vector3(0, 30, 0); //distance between center of the campion and it's head
GameObject beam = Instantiate(projectile, transform.position + ProjectileDistance, Quaternion.identity) as GameObject;
beam.GetComponent<Rigidbody2D>().velocity = new Vector3(0, projectileSpeed, 0);
// AudioSource.PlayClipAtPoint(fireSound, transform.position);
TimePerFrame = 0;
}
}
void Update ()
{
if (freezePosition == false)
{
if(fireTimer > 0.0f){
fireTimer -= Time.deltaTime * fireLimiter;
}
Fire(firingRate);
PositionChaning();
firingRate = Mathf.Lerp(minFiringRate, maxFiringRate, fireTimer);
Debug.Log(firingRate);
}
}
Do not forget to reset fireTimer to 1.0f after shooting!
The delay of the firerate can be controlled by fireLimiter

Categories