Why Coroutine Method Work Only Once Unity3D - c#

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;
}

Related

How can i take only one finger swerve input

My project is a Runner game where character constantly moves forward and if player slides left or right, character moves that position too. But in mobile, if i slide with one finger and touch with my other finger in the mean time, character starts to take 2 inputs and move wrong directions.
This is my code below:
private void Update(){
float newz = transform.position.z + movementSpeed * Time.deltaTime;
float newx = 0, swipeDelta = 0;
if(Input.touchCount == 1 && Input.GetTouch(0).phase == TouchPhase.Moved)
{
swipeDelta = Input.GetTouch(0).deltaPosition.x / Screen.width;
}
newx = transform.position.x + swipeDelta * 5f *Time.deltaTime;
transform.position = new Vector3(newx, transform.position.y, newz);
}
I have set Input.touchCount to 1 because i want it to get only 1 finger input but it did not work. What should i do to make it work with one finger and make it accurate?
From your code it seems that as soon as you touch with second finger it will just not read location of your first finger.
Input.touchCount == 1 in your if statement looks like a problem for me, it means that if statement will only execute if you have a single finger on your screen. if you change it to Input.touchCount > 0 it will execute even if there are more fingers on the screens and should work correctly since you are already only taking one input with Input.GetTouch(0).
private void Update(){
float newz = transform.position.z + movementSpeed * Time.deltaTime;
float newx = 0, swipeDelta = 0;
if(Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Moved)
{
swipeDelta = Input.GetTouch(0).deltaPosition.x / Screen.width;
}
newx = transform.position.x + swipeDelta * 5f *Time.deltaTime;
transform.position = new Vector3(newx, transform.position.y, newz);
}
Changing Input.touchCount == 1 to Input.touchCount > 0 did not solve my problem.
I solved it by adding this line of code in the PlayerController script's Start function:
Input.multiTouchEnabled = false;

How can I stop my camera clipping through terrain?

Having a bit of a problem in Unity3D. I have a fly camera with both a box collider and rigidbody, and it still moves through my terrain, which has a terrain collider and a rigidbody. The thing is, it interacts with my capsules just fine, bounces them away and everything. Obvious this is the exact opposite of what I want ha ha...
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FlyCamera : MonoBehaviour
{.
/*
Writen by Windexglow 11-13-10. Use it, edit it, steal it I don't care.
Converted to C# 27-02-13 - no credit wanted.
Simple flycam I made, since I couldn't find any others made public.
Made simple to use (drag and drop, done) for regular keyboard layout
wasd : basic movement
shift : Makes camera accelerate
space : Moves camera on X and Z axis only. So camera doesn't gain any height*/
float mainSpeed = 25.0f; //regular speed
float shiftAdd = 250.0f; //multiplied by how long shift is held. Basically running
float maxShift = 1000.0f; //Maximum speed when holdin gshift
float camSens = 0.25f; //How sensitive it with mouse
private Vector3 lastMouse = new Vector3(255, 255, 255); //kind of in the middle of the screen, rather than at the top (play)
private float totalRun = 1.0f;
void Update()
{
if (Input.GetKey(KeyCode.R))
{
transform.position = new Vector3(26f, 4f, 14f);
}
lastMouse = Input.mousePosition - lastMouse;
lastMouse = new Vector3(-lastMouse.y * camSens, lastMouse.x * camSens, 0);
lastMouse = new Vector3(transform.eulerAngles.x + lastMouse.x, transform.eulerAngles.y + lastMouse.y, 0);
transform.eulerAngles = lastMouse;
lastMouse = Input.mousePosition;
//Mouse camera angle done.
//Keyboard commands
float f = 0.0f;
Vector3 p = GetBaseInput();
if (Input.GetKey(KeyCode.LeftShift))
{
totalRun += Time.deltaTime;
p = p * totalRun * shiftAdd;
p.x = Mathf.Clamp(p.x, -maxShift, maxShift);
p.y = Mathf.Clamp(p.y, -maxShift, maxShift);
p.z = Mathf.Clamp(p.z, -maxShift, maxShift);
}
else
{
totalRun = Mathf.Clamp(totalRun * 0.5f, 1f, 1000f);
p = p * mainSpeed;
}
p = p * Time.deltaTime;
Vector3 newPosition = transform.position;
if (Input.GetKey(KeyCode.Space))
{ //If player wants to move on X and Z axis only
transform.Translate(p);
newPosition.x = transform.position.x;
newPosition.z = transform.position.z;
transform.position = newPosition;
}
else
{
transform.Translate(p);
}
}
private Vector3 GetBaseInput()
{ //returns the basic values, if it's 0 than it's not active.
Vector3 p_Velocity = new Vector3();
if (Input.GetKey(KeyCode.W))
{
if (transform.position.x > 2 && transform.position.x < 53)
{
if (transform.position.y > 0 && transform.position.y < 40)
{
if (transform.position.z > 5 && transform.position.z < 52)
{
p_Velocity += new Vector3(0, 0, 1);
}
}
}
}
if (Input.GetKey(KeyCode.S))
{
if (transform.position.x > 2 && transform.position.x < 53)
{
if (transform.position.y > 0 && transform.position.y < 40)
{
if (transform.position.z > 5 && transform.position.z < 52)
{
p_Velocity += new Vector3(0, 0, -1);
}
}
}
}
if (Input.GetKey(KeyCode.A))
{
if (transform.position.x > 2 && transform.position.x < 53)
{
if (transform.position.y > 0 && transform.position.y < 40)
{
if (transform.position.z > 5 && transform.position.z < 52)
{
p_Velocity += new Vector3(-1, 0, 0);
}
}
}
}
if (Input.GetKey(KeyCode.D))
{
if (transform.position.x > 2 && transform.position.x < 53)
{
if (transform.position.y > 0 && transform.position.y < 40)
{
if (transform.position.z > 5 && transform.position.z < 52)
{
p_Velocity += new Vector3(1, 0, 0);
}
}
}
}
return p_Velocity;
}
}
This flycam isn't mine so if it's not optimal for the use I'm looking for, please notify me about that too.
Basically you are. Transforming it not adding velocity thats why it is crossing the collider
You need to get reference of the rigidbody then add velocity to it
Then it won't cross the collider
You need to modify your code for that
Where you are transforming
Instead of transforming
You need to
Rigidbody.velocity= vector3(.....)
Never mind, I fixed the problem.
I ended up having Kinematic ticked on the box collider of the camera, so that must have messed it up.

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
}
}
}
}
}

Can I write my movement script shorthand?

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! :)

Unity: Infinite While Loop in Coroutine

Okay so I'm trying to create a small dash coroutine for my 2d character. When the coroutine calls, gravity switches off, he lerps between 2 speeds over a time. The issue is within my Dash coroutine, the while loop checks when time.time(current time) > start time + dash duration.
While debugging this with Mono, I'm finding that my variable currentTime is not changing after being set, even though I can clearly see the while loop running more than once. This puts me in an infinite loop.
Any suggestions?
void Update () {
MoveAndJump ();
CheckDash ();
}
void MoveAndJump(){
if(dashing){
return;
}
Vector2 moveDir = new Vector2 (Input.GetAxisRaw ("Horizontal") * moveSpeed, rb.velocity.y);
rb.velocity = moveDir;
// Consider Switching to an overlap circle based on the actual character graphic. This currently uses a rectangle
// This can also use 4 points to create a more complex box shape. This only works well when using at least about 1 unit size. any smaller and it is innacurate
isGrounded = Physics2D.OverlapArea (groundPoint_right.position, groundPoint_left.position, groundMask);
Debug.Log (isGrounded);
if (Input.GetAxisRaw ("Horizontal") == 1) {
transform.localScale = new Vector3 (1 , 1 * transform.localScale.y, 1* transform.localScale.z);
} else if (Input.GetAxisRaw ("Horizontal") == -1) {
transform.localScale = new Vector3 (-1, 1* transform.localScale.y, 1* transform.localScale.z);
}
if(Input.GetKeyDown(KeyCode.Space) && isGrounded){
rb.AddForce (new Vector2 (0, jumpHeight));
}
}
void CheckDash(){
if(Input.GetKeyDown(KeyCode.W) && !dashing && Time.time > nextdashtime){
dashing = true;
StartCoroutine ("Dash");
}
}
IEnumerator Dash(){
//Before dash loop
rb.gravityScale = 0f;
float startDashTime = Time.time;
float endDashTime = startDashTime + dashDuration;
float currentDashTime;
//Dash Loop
while(Time.time < (startDashTime + dashDuration)){
currentDashTime = Time.time;
// The value to lerp by should be the % that time.time is between start time and end time
rb.velocity = new Vector2 (Mathf.Lerp (startDashSpeed, endDashSpeed, ((currentDashTime - startDashTime) / (endDashTime - startDashTime))),0f);
}
//When dash loop is complete
nextdashtime = Time.time + dashcooldown;
dashing = false;
rb.gravityScale = 1;
yield return null;
}
// FOR CHECKING GROUND CHECK LIMITS
/*void OnDrawGizmos(){
Gizmos.color = Color.red;
Gizmos.DrawLine (groundPoint_left.position, groundPoint_right.position);
}*/
}
You don't need all that for dashing !
If you use rigid bodies then the following code will do:
using UnityEngine;
public class CubeController : MonoBehaviour
{
private Rigidbody _rigidbody;
public float Force = 10;
private void Start()
{
_rigidbody = GetComponent<Rigidbody>();
}
private void FixedUpdate()
{
ForceMode? forceMode = null;
if (Input.GetKeyDown(KeyCode.W)) // normal
forceMode = ForceMode.Force;
else if (Input.GetKeyDown(KeyCode.S)) // dash
forceMode = ForceMode.Impulse;
if (forceMode.HasValue)
_rigidbody.AddRelativeForce(Vector3.forward*Force, forceMode.Value);
}
}
This is an example using 3D but you have the same for 2D : https://docs.unity3d.com/ScriptReference/Rigidbody2D.html
Steps:
add rigidbody component to your ground, set it as kinematic
add rigidbody component to your player
sketch the above component for your player using Rigidbody2D class
adjust the parameters as you'd like
you should have a simple and robust dashing by now :)
I forgot a yield return null within my while loop that stuck me in an infinite loop. Works fine now.
while(Time.time < (startDashTime + dashDuration)){
currentDashTime = Time.time;
// The value to lerp by should be the % that time.time is between start time and end time
rb.velocity = new Vector2 (Mathf.Lerp (startDashSpeed, endDashSpeed, ((currentDashTime - startDashTime) / (endDashTime - startDashTime))),0f);
yield return null;
}

Categories