Wrong timing on animation + sprite flip on Unity; - c#

I need to set an animation of flipping in a 2D game on Unity. How is the best way to do this?
I'm trying something like this pseudo code:
void FixedUpdate() {
if(lastSide!=currentSide)
flip();
}
void flip() {
if(lastSide == 1) // if is facing right
animator.SetTrigger("flipToLeft");
else if(lastSide == -1) // if is facing left
animator.SetTrigger("flipToRight");
this.player.transform.localScale = new Vector3(lastSide * -1, 1f, 1f);
}
This "works", but the sprite is fliping before the animation of fliping starts.

One option would be to hardcode a delay for the point at which you want the sprite to flip:
void FixedUpdate()
{
if(lastSide != currentSide)
StartCoroutine(Flip());
}
IEnumerator Flip()
{
if(lastSide == 1) // if is facing right
animator.SetTrigger("flipToLeft");
else if(lastSide == -1) //if is facing left
animator.SetTrigger("flipToRight");
yield return new WaitForSeconds(.5f); // delay before flipping sprite
transform.localScale = new Vector3(lastSide*-1,1f,1f);
}
If you don't want to hardcode the timing, you can look into Animation Events which you can use to call functions directly from the animation.

You can use DoTween for fliping in simple way.

Related

How to smoothly turn character controller in X Direction

Overview
I'm making an endless runner game. In this game, I have 5 lines, I want to player smoothly switch lines something like this Ref Link
In my case, I have everything the same but instead of a car, I have a player with PlayerController attached to it.
I'm changing the player line on Button click and also on IPointerDownHandler & IPointerUpHandler
Code
Full Code
[SerializeField] private List<Vector3> lines; // 5 lines in my case. Vector3 (0,0,0) and so on ...
private int flag;
Vector3 currLine;
private void ChangeLines ()
{
// Getting Inputs
if (Input.GetKey(KeyCode.LeftArrow)) { flag = -1; }
else if (Input.GetKey(KeyCode.RightArrow)) { flag = 1; }
else flag = 0;
if (flag > 0) MoveRight ();
else if (flag < 0) MoveLeft ();
}
//I used two approaches to moving but both are not working as indented
// 1 _ using DoTween
// 2 _ using Vector3.Lerp ()
private void MoveRight ()
{
// some input delay for Ipointers
if (inputDelay > 0) return;
if (currLine == lines [lines.Count - 1]) return; // can't move right anymore
transform.DoRotate (new Vector3(0, 45, 0) , 0.2f); // rotate player toward target
transform.DoMoveX (currLine.X, 0.3f) // 0.3f is coming from inspector
.SetEase (Ease.Linear) // i almost tried all Ease
.OnComplete ( ()=> DoTween.DoRotate (new Vector3(0, 0, 0) , 0.2f));
// using Lerp
LookAt (new Vector3 (currLine.x,Y,Z));
transform.position = Vector3.Lerp(transform.position, new Vector3(currLine.x, ..,..), lineChangeCurve
.Evaluate(Time.deltaTime * lineChangeSpeed));
}
private void MoveLeft ()
{
// same code as MoveRight
}
Problem
The code I wrote is prettier much working. the player is changing lines and also rotating towards the line but I'm unable to figure out what should i need to do to make this effect look like a reference.
Can you tell me how can I achieve the same smoother effect as the reference for my player?
Here is the link that I made so far
3D Assets link
Player lines distance :
new Vector3 (-8, 0,0)
new Vector3 (-4, 0,0)
new Vector3 (0, 0,0)
new Vector3 (4, 0,0)
new Vector3 (8, 0,0)
Thanks in Advance
You seem to be mixing two different animation techniques.
You do
transform.DoMoveX (currLine.X, 0.3f)
.SetEase (Ease.Linear)
...
which starts a tween animation
and then you also do Evaluate on the lerp which seems to be a bit redundant.
Your issue with the Lerp and Evaluate is
a) that you pass in
Time.deltaTime * lineChangeSpeed
which basically means
lineChangeSpeed / frame rate (e.g. 60)
=> This is a way to small value and will most probably basically mean you don't move at all (depending on your used curve)
You want to call this every frame and pass in a value increasing from 0 to 1 here (e.g. in a Coroutine)
b) you call it only exactly once. This will not result in any movement at all .. or at least only in a single step in the first frame => which probably causes a little "hiccup"
=> get rid of the lerp line all together. At best it interferes with the tween animation and is probably the cause of it looking somewhat off
To the question about keeping the button pressed.
You have nothing to prevent this in
private void ChangeLines ()
{
// Getting Inputs
if (Input.GetKey(KeyCode.LeftArrow)) { flag = -1; }
else if (Input.GetKey(KeyCode.RightArrow)) { flag = 1; }
else flag = 0;
if (flag > 0) MoveRight ();
else if (flag < 0) MoveLeft ();
}
First of all the flag seems quite redundant. And then you would
Either change to GetKeyDown so only the first frame where the button goes down is handled
Or add a flag that ignores all input while an animation is already running
Or even both (depending on your desired UX)
So e.g.
private bool alreadyAnimating;
private void ChangeLines ()
{
if(alreadyAnimating) return;
if (Input.GetKey(KeyCode.LeftArrow))
// Or as said maybe even better
if (Input.GetKeyDown(KeyCode.LeftArrow))
{
MoveLeft();
}
else if (Input.GetKey(KeyCode.RightArrow))
// same here
else if (Input.GetKeyDown(KeyCode.RightArrow))
{
MoveRight();
}
}
and then block and reset the flag when done with moving
alreadyAnimating = true;
transform.DoRotate (new Vector3(0, 45, 0) , 0.2f);
transform.DoMoveX (currLine.X, 0.3f)
.SetEase (Ease.Linear)
.OnComplete (()=>
{
DoTween.DoRotate (new Vector3(0, 0, 0) , 0.2f)
.OnComplete(() => { alreadyAnimating = false; })
});
I used Lerp() for these kind of conditions and it worked well all the time. As I saw your character correctly turns but it look like a snap turn. I couldn't spot an obvious error in your code. Play a little more with the rotation time and you will get your desired result. Sorry this must be a comment but dont have the reps.
The idea of the animation for switching lines is pretty simple. All you need is an animation of turning the main character to the side and back. Let's say the line change animation takes 1 second. Then you need that when moving to the left lane, the main character makes a turn to the left in 0.5 seconds and the next 0.5 seconds turns straight again. While the gameobject moves into the adjacent line, the main character will make a small smooth turn and the animation will look better. This is how it is done in the example you showed in the question. In your video, instead of this animation, the main character abruptly turns to a certain angle and returns to its original position abruptly back when it reaches the required line.

"isGrounded" variable doesn't change to false after initial jump in Unity

Overview
Using Unity2D 2019.3.5, I am making a platformer game using C#. I implemented raycast to detect when my player is touching the ground and attempted to make it only so the player can jump only once.
Problem
Although I thought I programmed my character to jump once, after the first jump, the Unity engine still shows a checkmark to my "isGrounded" variable and only turns to false (unchecked) after a second jump before hitting the ground.
My Code
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player_Controller : MonoBehaviour
{
public int playerSpeed = 10;
public int playerJumpPower = 1250;
private float moveX;
public bool isGrounded;
public float distanceToBottomOfPlayer = .7f;
// Update is called once per frame
void Update()
{
PlayerMove();
PlayerRaycast();
}
void PlayerMove()
{
// CONTROLS
moveX = Input.GetAxis("Horizontal");
if (Input.GetButtonDown("Jump") && isGrounded == true)
{
Jump();
}
// ANIMATIONS
// PLAYER DIRECTION
if (moveX < 0.0f)
{
GetComponent<SpriteRenderer>().flipX = true;
}
else if (moveX > 0.0f)
{
GetComponent<SpriteRenderer>().flipX = false;
}
// PHYSICS
gameObject.GetComponent<Rigidbody2D>().velocity = new Vector2(moveX * playerSpeed,
gameObject.GetComponent<Rigidbody2D>().velocity.y);
}
void Jump()
{
GetComponent<Rigidbody2D>().AddForce(Vector2.up * playerJumpPower);
isGrounded = false;
}
void PlayerRaycast()
{
// Ray Down
RaycastHit2D rayDown = Physics2D.Raycast(transform.position, Vector2.down);
if (rayDown.collider != null && rayDown.distance < distanceToBottomOfPlayer &&
rayDown.collider.tag == "ground")
{
isGrounded = true;
}
}
}
Extra Info
I did have to change a Unity setting in Edit > Project Settings > Physics 2D > Queries Start In Colliders. I had to turn this setting off (uncheck) in order to get my player to jump using the code I wrote above. I know there are other ways of making my player jump, however, this seemed to be the most efficient while maintaining the readability of the code.
Solutions Tried
What I believe the problem is that I have a raycast issue that I don't know how to fix. I looked at other Stack Overflow posts including the ones recommended after writing this post, but none of them applied to my problem.
Final Notes
As I said before, I know there are other ways to make my player jump only once using different code, however, I would like to stick with this code for my own learning purposes and for future reference.
Because you can't be sure that isGrounded is false when you call Jump().
I think issue lies there.
Try not setting isGrounded flag when calling Jump(). Setting isGrounded is purely PlayerRaycast()'s job.
void Update()
{
// Raycast before moving
PlayerRaycast();
PlayerMove();
}
void PlayerRaycast()
{
// Ray Down
RaycastHit2D rayDown = Physics2D.Raycast(transform.position, Vector2.down);
if (rayDown.collider != null && rayDown.collider.tag == "ground")
{
if( rayDown.distance < distanceToBottomOfPlayer )
{
isGrounded = true;
}
else
{
isGrounded = false;
}
}
}
void Jump()
{
GetComponent<Rigidbody2D>().AddForce(Vector2.up * playerJumpPower);
//isGrounded = false;
}
The first problem is that you add the force and check for the ground at the same frame.
Applied Force is calculated in FixedUpdate or by explicitly calling
the Physics.Simulate method.
So after you press the "Jump" button, the object is still on the ground until the next frame comes.
To fix this, you can simply exchange the order of "move" and "raycast"
void Update()
{
PlayerRaycast();
PlayerMove();
}
The second problem is if the jump power is not large enough, the object can still be close to the ground in the next frame, you should avoid checking the landing when the jump is in ascending state.
void PlayerRaycast()
{
if(GetComponent<Rigidbody2D>().velocity.y > 0)
return;
...
}

When using Unity and c#, how do I make an objects movement less choppy and more fluid?

I'm trying to make a simple snake game on unity that can be played on a mobile device. I got to the point where the snakes head is able to move continuously in the direction that the user swiped in, but the movement of the snake's head is kind of choppy and I'd like it to be more fluid.
I have this in the public class:
Vector2 dir = Vector2.right;
My start function looks like this:
void Start() {
dragDistance = Screen.height * 5 / 100; //dragDistance is 5% height of the screen
InvokeRepeating("Move", 0.3f, 0.075f);
}
Inside of my Update function I have this:
if (Mathf.Abs(lp.x - fp.x) > dragDistance || Mathf.Abs(lp.y - fp.y) > dragDistance)
{//It's a drag
//check if the drag is vertical or horizontal
if (Mathf.Abs(lp.x - fp.x) > Mathf.Abs(lp.y - fp.y))
{ //If the horizontal movement is greater than the vertical movement...
if ((lp.x > fp.x)) //If the movement was to the right)
{ //Right swipe
dir = Vector2.right;
}
else
{ //Left swipe
dir = -Vector2.right;
}
}
else
{ //the vertical movement is greater than the horizontal movement
if (lp.y > fp.y) //If the movement was up
{ //Up swipe
dir = Vector2.up;
}
else
{ //Down swipe
dir = -Vector2.up;
}
}
}
And after the Update function I have:
void Move()
{
// Move head into new direction
transform.Translate(dir);
}
Thanks for your help.
This is simply due to how you are currently moving.
Using InvokeRepeating to move your character will be super choppy as you are just moving "forward" by a certain amount every time InvokeRepeating repeats.
You really shouldn't be using that to move your character. Typically you want to you some form of Rigidbody and physics to move. Perhaps try something like this :
public Rigidbody2D rb;
void FixedUpdate() {
rb.MovePosition((rb.position + dir) * Time.fixedDeltaTime);
}
That could be a very simple answer. Check out some more details on that method here.
Also just try looking up some tutorials on movement in Unity.
The problem is you aren't moving every frame. You could fix this by moving in the update() like so:
transform.Translate(dir * speed * Time.deltaTime);
Instead of doing InvokeRepeating and Move().
But what if you want to make the snake stop at each square like it does now, but move smoothly from square to square? You would need to save the snake's next position in your move() function and then smoothly move the snake there in Update(). It would look something like this:
void Move()
{
// Move head to next position
prevSquare = transform.position; // keep track of the previous square
// so we can use lerp() later on
nextSquare = transform.position + dir;
progress = 0; // progress is the percent distance between the two squares
isMoving = true; //we probably need isMoving so we can decide what to do if they
//swipe while snake is moving, I'll leave it to you to figure
//out
}
update()
{
...
if(isMoving)
{
progress = progress + time.deltaTime;
transform.position = Vector2.Lerp(prevSquare, nextSquare, progress);
if(progress > 1.0f)
{
isMoving = false;
}
}
}

Drag Controls and Jump with one touch game in Unity C#

I am trying to get touch controls to work for iOS, the result i want is drag left to move left, drag right to move right and tap to jump. At the moment the functions do work but what happens is if i don't want to jump and just want to drag the character will jump first as soon as touch the screen. Is there a way i can get the result i want i have added my code below. I want the first touch to just register for the drag and when the player lifts and then taps or maybe double taps the character will jump. Thanks in advance.
//Mobile Touch Drag Controls
{
if (Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Moved)
{
// Get movement of the finger since last frame
Vector2 touchDeltaPosition = Input.GetTouch(0).deltaPosition;
// Move object across X plane
transform.Translate(touchDeltaPosition.x * speed, 0, 0);
}
}
////Mobile Touch To Jump
{
if (Input.GetMouseButtonDown(0))
{
transform.Translate(Vector3.up * jumpForce * Time.deltaTime, Space.World);
}
When the button goes down, there is no way to know what the player wants to do. You should jump when the finger goes up instead.
An easy solution is to use a boolean to know if the player has moved before the finger was released. If yes, you don't want to jump
public class PlayerController // Or whatever name your class has
{
private bool _moved;
private void Update()
{
//Mobile Touch Drag Controls
if (Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Moved)
{
// Get movement of the finger since last frame
Vector2 touchDeltaPosition = Input.GetTouch(0).deltaPosition;
// Move object across X plane
transform.Translate(touchDeltaPosition.x * speed, 0, 0);
_moved = true; // Remember that the player moved
}
////Mobile Touch To Jump
if (Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Ended)
{
if(!_moved) // Only jump if we didnt move
{
transform.Translate(Vector3.up * jumpForce * Time.deltaTime, Space.World);
}
_moved = false;
}
}
}
I assume this code goes in your update function. This will work fine but here is a more elegant solution in case you want to try it:
Your class can implement IDragHandler and IEndDragHandler (among others, look for all possibilities that extend IEventSystemHandler)
When you implement these, you can override functions that get called when the user touches the screen, such as public void OnDrag(PointerEventData eventData)
With this you don't have to use the Update function anymore

Unity - Destroy the DoorController after finishing the task

I have scripted a controller for opening doors in a TopDown-Shooter.
I rotate pivot points around their local Y-Axis to open the door objects. The doors should stay open, so I do not need the Controller and the Controller object anymore. I want to destroy it after finishing its job.
My script looks this:
public class DoorController : MonoBehaviour
{
public Transform pivotLeftTransform; // the left pivot point
public Transform pivotRightTransform; // the right pivot point
int openAngle = 90; // how far should the door open up?
bool startOpen = false; // start opening?
float smooth = 2; // smooth rotation
private void OnTriggerEnter(Collider other)
{
if (other.gameObject.tag == "Player")
{
startOpen = true; // when the Player triggers, start opening
}
}
void Update()
{
if (startOpen)
{
OpenDoor(pivotLeftTransform, openAngle);
OpenDoor(pivotRightTransform, -openAngle);
if (pivotLeftTransform.localRotation.y == openAngle && pivotRightTransform.localRotation.y == -openAngle) // when the doors are rotated, destroy this object
{
Destroy(gameObject);
}
}
}
private void OpenDoor(Transform pivotTransform, int rotationAngle)
{
Quaternion doorRotationOpen = Quaternion.Euler(0, rotationAngle, 0); // desired door rotation
pivotTransform.localRotation = Quaternion.Slerp(pivotTransform.localRotation, doorRotationOpen, smooth * Time.deltaTime); // rotate the door to the desired rotation
}
}
My Question is, how can I destroy the object. My code
if (pivotLeftTransform.localRotation.y == openAngle && pivotRightTransform.localRotation.y == -openAngle)
does not seem to work. After having the pivot point rotated to 90 or -90 degrees the statement still stays false. I also tried
pivotTransform.rotation.y
but it does not work neither. Which rotation do I need to pass in?
Thanks.
Two things wrong in your code:
1.Comparing floats
2.Using transform.localRotation or transform.rotation to check for angle.
You can solve this by using eulerAngles or localEulerAngles. Also for comparing floats, use >= instead of = as that may never be true.
Replace
if (pivotLeftTransform.localRotation.y == openAngle && pivotRightTransform.localRotation.y == -openAngle)
{
Destroy(gameObject);
}
with
if (pivotLeftTransform.localEulerAngles.y >= openAngle && pivotRightTransform.localRotation.y >= -openAngle)
{
Destroy(gameObject);
}
If you have problem with the answer above you have to troubleshoot it one by one. Separate the && and see which one is failing like this:
if (pivotLeftTransform.localEulerAngles.y >= openAngle)
{
Debug.Log("pivotLeftTransform");
}
if (pivotRightTransform.localRotation.y >= -openAngle)
{
Debug.Log("pivotRightTransform");
}
The right way to archieve this is adding a small tolerance value:
if (pivotLeftTransform.localEulerAngles.y >= openAngle - 0.1f && pivotRightTransform.localEulerAngles.y <= 360 - openAngle + 0.1f)
This may not be code clean but it works for the moment :)

Categories

Resources