I'm currently making 2d tour based platform game in style simillar to Worms games. There is one thing that i would like to recreate in my game, which is the way that team members are interacting with each other.
I'm looking for a solution to lock the velocity transfer between characters. Let's say that one character which is currently under controll of player is moving towards another character that is in Idle state. When those characters comes into collision, the one that is controllable should not be able to move further forward. But instead, when the first one is still moving, it starts to move second one.
So i'm looking for a solution to prevent the situation when velocity of currently controlled character is transfering to other characters.
Below is a PlayerController script that is responsible for moving the characters:
private float MoveSpeed = 5f;
private float JumpSpeed = 15f;
private float MoveInput;
private Rigidbody2D rb;
public bool FacingRight = true;
public bool Grounded;
private bool Jump;
public Transform GroundCheck;
public Transform HealthTag;
public LayerMask Ground;
public Animator animator;
private void Start()
{
rb = GetComponent<Rigidbody2D>();
}
private void Update()
{
if (Input.GetButtonDown("Jump") && Grounded == true)
{
Jump = true;
if (Jump == true)
{
rb.velocity = new Vector2(rb.velocity.x, JumpSpeed);
}
}
}
private void FixedUpdate()
{
Grounded = Physics2D.OverlapCircle(GroundCheck.position, 0.5f, Ground);
if(Grounded == false)
{
Jump = false;
}
MoveInput = Input.GetAxis("Horizontal");
animator.SetFloat("Speed", Mathf.Abs(MoveInput));
rb.velocity = new Vector2(MoveInput * MoveSpeed, rb.velocity.y);
if (MoveInput > 0 && FacingRight == false)
{
Flip();
}
else if (MoveInput < 0 && FacingRight == true)
{
Flip();
}
}
void Flip()
{
FacingRight = !FacingRight;
if (FacingRight == true)
{
transform.eulerAngles = new Vector3(0, 0, 0);
HealthTag.transform.eulerAngles = new Vector3(0, 0, 0);
}
if (FacingRight == false)
{
transform.eulerAngles = new Vector3(0, 180, 0);
HealthTag.transform.eulerAngles = new Vector3(0, 0, 0);
}
}
You have to set the rigidbody of the idle player to kinematic. This will prevent it from being pushed by the moving player.
Related
I'm trying to learn unity 2D and now iI need to do animations. I did walking animation, falling animation, jumping animation and the idle animation. However, when I jump, the falling animation doesn't play as intended. The falling animation isn't working properly, so it looks like this:
https://www.awesomescreenshot.com/video/8054610?key=208b095723f0bd4d8dfb936c88485e76
So when I'm falling, it doesn't play the animation right.
using UnityEngine;
public class PlayerMovement : MonoBehaviour
{
private Rigidbody2D rb;
private Animator anim;
private SpriteRenderer sprite;
private enum MovementState { idle, running, jumping, falling }
private float dirX = 0f;
[SerializeField] private float moveSpeed = 7f;
[SerializeField] private float jumpForce = 5f;
private void Start()
{
Debug.Log("program started...");
rb = GetComponent<Rigidbody2D>();
sprite = GetComponent<SpriteRenderer>();
anim = GetComponent<Animator>();
}
private void Update()
{
dirX = Input.GetAxisRaw("Horizontal");
rb.velocity = new Vector2(dirX * moveSpeed, rb.velocity.y);
if (Input.GetButtonDown("Jump"))
{
rb.velocity = new Vector2(rb.velocity.x, jumpForce);
}
Animation();
}
private void Animation()
{
MovementState state;
if (dirX > 0f)
{
state = MovementState.running;
sprite.flipX = false;
}
else if (dirX < 0f)
{
state = MovementState.running;
sprite.flipX = true;
}
else
{
state = MovementState.idle;
}
if (rb.velocity.y > .1f)
{
state = MovementState.jumping;
}
else if (rb.velocity.y < -.1f)
{
state = MovementState.falling;
}
anim.SetInteger("state", (int)state);
}
}
i don't know how to fix it, i've searched for about a hour how to fix it.
animations setting (my settings)
the settings that i want
There's a couple of things you can try to do to narrow down the issue. See if commenting out this else statement has an effect on the issue you are experiencing between jump and fall.
else
{
state = MovementState.idle;
}
I think this else could possibly to interrupt your Y velocity animation transition when the object reaches the max height of the jump.
Try to comment out different part of your if else statements until you find the culprit.
Also try to get rid of the overlap on the transition between jump and fall. I think you want to instantly move from jump animation to fall animation.
I suggest you to use two different floats for your animations, one for Movement and one for Jumping.
Then something like following to handle them.
private void Animation() {
sprite.flipX = dirX > 0f;
//Do some sort of ground check
if (isGrounded) {
anim.SetFloat("Move", dirX);
}
else {
anim.SetFloat("Jump", rb.velocity.y);
}
}
And of course you need to change your condition that you have setup in your Animator as well from your Animator window to make it work.
As it has been suggested, I would add what is called a ground check to your player, which is exactly what it sounds like. An easy way to do this would be to Raycast downward from your player's position offset by the sprite's extents and only detect casts against a specific ground LayerMask.
Once you have a ground check done, you can set the state of your animation based on 4 criteria.
Is the player grounded?
What is your relative velocity? (Positive / Negative)
With your four states, the logic would look as follows
•Idle - Grounded and X Velocity equals 0 (Or default)
•Running - Grounded and X Velocity not equal to 0
•Jumping - Not Grounded and Y Velocity greater than or equal to 0
•Falling - Not Grounded and Y Velocity less than 0
With this in mind, here is how I would edit your existing code to work as intended
using UnityEngine;
public class PlayerMovement : MonoBehaviour
{
private Rigidbody2D rb;
private Animator anim;
private SpriteRenderer sprite;
private enum MovementState { idle, running, jumping, falling }
private float dirX = 0f;
[SerializeField] private float moveSpeed = 7f;
[SerializeField] private float jumpForce = 5f;
[SerializeField] private LayerMask groundMask;
[SerializeField] private float groundLengthCheck = 0.03f;
private Vector3 spriteGroundOffset = Vector3.zero;
private bool isGrounded = false;
private void Start()
{
Debug.Log("program started...");
rb = GetComponent<Rigidbody2D>();
sprite = GetComponent<SpriteRenderer>();
anim = GetComponent<Animator>();
// offset the raycast check to be half our sprite bounds in the Y (the bottom of your sprite)
spriteGroundOffset = new Vector3(0f, sprite.bounds.extents.y, 0f);
}
private void Update()
{
dirX = Input.GetAxisRaw("Horizontal");
rb.velocity = new Vector2(dirX * moveSpeed, rb.velocity.y);
// determine if our player is grounded by casting down from their feet with our slight offset
isGrounded = Physics2D.Raycast(transform.position - spriteGroundOffset, Vector2.down, groundLengthCheck, groundMask);
if (Input.GetKeyDown(KeyCode.Space) && isGrounded)
{
rb.velocity = new Vector2(rb.velocity.x, jumpForce);
}
Animation();
}
private void Animation()
{
// default our state to idle - as if we are just standing
MovementState state = MovementState.idle;
// change
if (isGrounded)
{
if (dirX > 0f)
{
state = MovementState.running;
sprite.flipX = false;
}
else if (dirX < 0f)
{
state = MovementState.running;
sprite.flipX = true;
}
}
else
{
if (rb.velocity.y > 0)
{
state = MovementState.jumping;
}
else if (rb.velocity.y < 0f)
{
state = MovementState.falling;
}
}
anim.SetInteger("state", (int)state);
}
}
Keep in mind
Add a new layer, assign it to your ground objects, and set the serialized field groundMask to check for this new layer.
There are a few other issues that might come about with the current code. The current snippet I have provided only fixes the issue related to jumping/falling.
Here is a gif of the current code in action - I am using a public domain sprite as I do not currently have any art for a 2D player.
no matter what I do with my code, the Player doesn't seem to want to jump. Everything is referenced correctly in Unity and the code is hopefully error free. I've also checked to see if the Input System is corresponding to what I've got pressed. The only thing I can think of is the force isn't strong enough for the player to lift off of the ground, due to the high gravity (7f). But I lowered the gravity and nothing changed. Thanks for the help in advance.
using System.Collections;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
private float horizontal;
private float speed = 8f;
private float jumpingPower = 16f;
private bool isFacingRight = true;
private bool isJumping;
private float coyoteTime = 0.2f;
private float coyoteTimeCounter;
private float jumpBufferTime = 0.2f;
private float jumpBufferCounter;
[SerializeField] private Rigidbody2D rb;
[SerializeField] private Transform groundCheck;
[SerializeField] private LayerMask groundLayer;
private void Update()
{
horizontal = Input.GetAxisRaw("Horizontal");
if (IsGrounded())
{
coyoteTimeCounter = coyoteTime;
}
else
{
coyoteTimeCounter -= Time.deltaTime;
}
if (Input.GetButtonDown("Jump"))
{
jumpBufferCounter = jumpBufferTime;
}
else
{
jumpBufferCounter -= Time.deltaTime;
}
if (coyoteTimeCounter > 0f && jumpBufferCounter > 0f && !isJumping)
{
rb.velocity = new Vector2(rb.velocity.x, jumpingPower);
jumpBufferCounter = 0f;
StartCoroutine(JumpCooldown());
}
if (Input.GetButtonUp("Jump") && rb.velocity.y > 0f)
{
rb.velocity = new Vector2(rb.velocity.x, rb.velocity.y * 0.5f);
coyoteTimeCounter = 0f;
}
Flip();
}
private void FixedUpdate()
{
rb.velocity = new Vector2(horizontal * speed, rb.velocity.y);
}
private bool IsGrounded()
{
return Physics2D.OverlapBox(groundCheck.position, groundCheck.GetComponent<BoxCollider2D>().size, 0f, groundLayer);
//return Physics2D.OverlapCircle(groundCheck.position, 0.2f, groundLayer);
}
private void Flip()
{
if (isFacingRight && horizontal < 0f || !isFacingRight && horizontal > 0f)
{
Vector3 localScale = transform.localScale;
isFacingRight = !isFacingRight;
localScale.x *= -1f;
transform.localScale = localScale;
}
}
private IEnumerator JumpCooldown()
{
isJumping = true;
yield return new WaitForSeconds(0.4f);
isJumping = false;
}
}
The code works fine and the player jumps and moves without problems
Make sure to perform these steps inside Unity Engine
Inside the player you must add
Rigidbody2D
Box Collider2D
the code
Inside the code you must specify
Ground Layer : "Ignore Raycast"
Ground Check : "The ground the player is walking on"
And inside the ground must be added
Layer : "Ignore Raycast"
Component: "Box Collider2D"
As in these pictures
Player
enter image description here
Ground
enter image description here
The code worked. The ground check had a collider on it, when it wasn't necessary. The ground check in reality was inside the player.
In the example video, when the player walks off the ledge he immediately transitions into a slowed down fall state but when the player jumps, he's allowed to fall quickly unless there is no platform underneath him, in which case he transitions into the slower fall state. How can I achieve this in my game?
https://www.youtube.com/watch?v=PZfdQPAM9OE&feature=youtu.be
Thanks for the help!
There so many way, I'll give you one simple example:
You need set up a simple scene where you have a character (e.g capsule) with rigidbody (Use Gravity set to false) and collider, and many grounds (e.g cube) with collider, and set the tag of all grounds to ground, then change the view to 2D
using UnityEngine;
public class Test : MonoBehaviour
{
public float slowFallSpeed = 1f;
public float normalFallSpeed = 3f;
public float runSpeed = 5f;
bool aboveGround = false;
bool touchGround = false;
bool inFall = false;
bool inJump = false;
const string k_Ground = "ground"; // Tag of ground object.
float maxRaycastDist = 2;
float currentFallSpeed = 0f;
Rigidbody rg;
void Start()
{
rg = GetComponent<Rigidbody>();
}
void OnCollisionEnter(Collision col)
{
if (col.transform.CompareTag(k_Ground) && aboveGround)
touchGround = true;
}
void OnCollisionExit(Collision col)
{
if (col.transform.CompareTag(k_Ground))
touchGround = false;
}
void FixedUpdate()
{
// Check if character above the ground
RaycastHit[] hits = Physics.RaycastAll(transform.position, Vector3.down, maxRaycastDist);
aboveGround = false;
foreach (var hit in hits)
{
if (hit.transform.CompareTag(k_Ground))
{
aboveGround = true;
break;
}
}
// Move
Vector3 moveDir = Vector3.zero;
if (Input.GetKey("d"))
moveDir += new Vector3(1f, 0f);
if (Input.GetKey("a"))
moveDir += new Vector3(-1f, 0f);
inJump = Input.GetKey("w") && !inFall;
if (inJump)
moveDir += new Vector3(0f, 1f);
rg.position += moveDir.normalized * runSpeed * Time.fixedDeltaTime;
// Fall
if (!touchGround && !inFall && !inJump)
{
inFall = true;
currentFallSpeed = aboveGround ? normalFallSpeed : slowFallSpeed;
}
else if (touchGround && inFall)
{
inFall = false;
currentFallSpeed = 0f;
}
if (currentFallSpeed != 0f)
rg.position += Vector3.down * currentFallSpeed * Time.fixedDeltaTime;
}
}
You could using different settings and differen component or in the 2D space, but logic should be the same
I'm making a 2D platformer with Unity. I used this tutorial to write this code but changed it a little bit. I want to support both keyboard and gamepad, so I use the new Input System. I've defined three variables called GroundedRememeber, JumpPressedRemember and JumpPressedRememberTime and basically they work like timers and check if the player leaves the ground and then the player can jump when he is near the ground without need to touch it and I want to use it instead of famous "groundCheck". But the problem is that these timers are not working and the player can jump forever even in the air when I press jump button rapidly. Also, as you can see, I added a LayerMask named "groundLayers" for the player to jump only on this type of objects but when I choose "Ground" in the "groundLayers" slot in the Inspector, the player can't jump anymore.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerController : MonoBehaviour, PlayerInputActions.IPlayerActions
{
private PlayerInputActions controls;
[SerializeField] LayerMask groundLayers;
private Rigidbody2D rb;
private Animator anim;
private bool facingRight = true;
private Vector2 moveInput;
[SerializeField] private float jumpForce;
float JumpPressedRemember = 0;
[SerializeField] float JumpPressedRememberTime = 0.2f;
float GroundedRemember = 0;
[SerializeField] float GroundedRememberTime = 0.25f;
[SerializeField] float HorizontalAcceleration = 1;
[SerializeField] [Range(0, 1)] float HorizontalDampingBasic = 0.5f;
[SerializeField] [Range(0, 1)] float HorizontalDampingWhenStopping = 0.5f;
[SerializeField] [Range(0, 1)] float HorizontalDampingWhenTurning = 0.5f;
[SerializeField] [Range(0, 1)] float JumpHeight = 0.5f;
private void Awake()
{
controls = new PlayerInputActions();
controls.Player.SetCallbacks(this);
}
void Start()
{
rb = GetComponent<Rigidbody2D>();
anim = GetComponent<Animator>();
}
void PlayerInputActions.IPlayerActions.OnMove(InputAction.CallbackContext context)
{
moveInput = context.ReadValue<Vector2>();
}
void Jump() {
rb.AddForce(Vector2.up * jumpForce, ForceMode2D.Force);
GroundedRemember = 0;
JumpPressedRemember = 0;
}
bool TryJump() {
if (GroundedRemember > 0) {
Jump();
return true;
} else {
JumpPressedRemember = JumpPressedRememberTime;
return false;
}
}
void PlayerInputActions.IPlayerActions.OnJump(InputAction.CallbackContext context)
{
jumpForce = context.ReadValue<float>();
switch (context.phase) {
case InputActionPhase.Performed:
TryJump();
break;
}
}
void FixedUpdate()
{
if(facingRight == false && moveInput.x > 0){
Flip();
}else if (facingRight == true && moveInput.x < 0){
Flip();
}
}
void Flip(){
facingRight = !facingRight;
Vector3 Scaler = transform.localScale;
Scaler.x *= -1;
transform.localScale = Scaler;
}
void OnEnable()
{
controls.Enable();
}
void OnDisable()
{
controls.Disable();
}
void Update()
{
Vector2 GroundedBoxCheckPosition = (Vector2)transform.position + new Vector2(0, -0.01f);
Vector2 GroundedBoxCheckScale = (Vector2)transform.localScale + new Vector2(-0.02f, 0);
bool Grounded = Physics2D.OverlapBox(GroundedBoxCheckPosition, transform.localScale, 0, groundLayers);
GroundedRemember -= Time.deltaTime;
if (Grounded)
{
GroundedRemember = GroundedRememberTime;
}
JumpPressedRemember -= Time.deltaTime;
if ((JumpPressedRemember > 0)) {
TryJump();
}
rb.velocity = new Vector2(rb.velocity.x, jumpForce);
float HorizontalVelocity = rb.velocity.x;
HorizontalVelocity += moveInput.x;
if (Mathf.Abs(moveInput.x) < 0.01f)
HorizontalVelocity *= Mathf.Pow(1f - HorizontalDampingWhenStopping, Time.deltaTime * 10f);
else if (Mathf.Sign(moveInput.x) != Mathf.Sign(HorizontalVelocity))
HorizontalVelocity *= Mathf.Pow(1f - HorizontalDampingWhenTurning, Time.deltaTime * 10f);
else
HorizontalVelocity *= Mathf.Pow(1f - HorizontalDampingBasic, Time.deltaTime * 10f);
rb.velocity = new Vector2(HorizontalVelocity, rb.velocity.y);
}
}
If you know how much the player jumps for, then try maybe adding a delay to the next jump
Maybe using the Invoke() function ur just using Coroutines if you know how to use them.
But i would still recommend using a Ground Check since it's practical and just easier and i don't see a reason why you wouldn't use it.
For whatever reason, my player movement stutters every second or every other second, and I'm sure it's to do with my player movement code being in the wrong update method.
Here's my player movement code
[SerializeField] private LayerMask groundLayerMask;
public float speed;
public float Jump;
public sword swordScript;
public GameObject swordSprite;
private float move;
private Rigidbody2D rb;
private BoxCollider2D boxCollider2d;
private bool facingRight;
public SpriteRenderer spr;
public Animator PlayerAnims;
public bool movementAllowed;
public float knockback;
void Awake()
{
boxCollider2d = GetComponent<BoxCollider2D>();
rb = GetComponent<Rigidbody2D>();
facingRight = true;
spr = GetComponent<SpriteRenderer>();
}
// Start is called before the first frame update
void Start()
{
boxCollider2d = GetComponent<BoxCollider2D>();
rb = GetComponent<Rigidbody2D>();
facingRight = true;
spr = GetComponent<SpriteRenderer>();
}
// Update is called once per frame
void FixedUpdate()
{
if(movementAllowed == true)
{
rb.velocity = new Vector2(move * speed, rb.velocity.y);
move = Input.GetAxisRaw("Horizontal");
rb.velocity = new Vector2(move * speed, rb.velocity.y);
if (isGrounded() && Input.GetButtonDown("Jump"))
{
rb.AddForce(new Vector2(rb.velocity.x, Jump));
}
}
}
void Update()
{
if (movementAllowed == true)
{
Flip(move);
if (move == 0)
{
PlayerAnims.SetBool("isRunning", false);
}
else
{
PlayerAnims.SetBool("isRunning", true);
}
}
}
private bool isGrounded()
{
float extraHeightText = .1f;
RaycastHit2D raycasthit2d = Physics2D.BoxCast(boxCollider2d.bounds.center, boxCollider2d.bounds.size, 0f, Vector2.down, extraHeightText, groundLayerMask);
Color rayColour;
if (raycasthit2d.collider != null)
{
rayColour = Color.green;
PlayerAnims.SetBool("isJumping", false);
}
else
{
rayColour = Color.red;
PlayerAnims.SetBool("isJumping", true);
}
Debug.DrawRay(boxCollider2d.bounds.center + new Vector3(boxCollider2d.bounds.extents.x, 0), Vector2.down * (boxCollider2d.bounds.extents.y + extraHeightText), rayColour);
Debug.DrawRay(boxCollider2d.bounds.center - new Vector3(boxCollider2d.bounds.extents.x, 0), Vector2.down * (boxCollider2d.bounds.extents.y + extraHeightText), rayColour);
Debug.DrawRay(boxCollider2d.bounds.center - new Vector3(boxCollider2d.bounds.extents.x, boxCollider2d.bounds.extents.y + extraHeightText), Vector2.right * (boxCollider2d.bounds.extents.x), rayColour);
return raycasthit2d.collider != null;
}
private void Flip(float move)
{
if (move > 0 && !facingRight || move < 0 && facingRight)
{
facingRight = !facingRight;
Vector3 theScale = transform.localScale;
theScale.x *= -1;
transform.localScale = theScale;
if (swordScript.isFollowing == true)
{
Vector3 swordScale = swordSprite.transform.localScale;
swordScale.x *= -1;
swordSprite.transform.localScale = swordScale;
}
}
}
I tried out your script and I think I barely see that stutter, but one thing seems to fix it.
You have this particular line twice in the code for no reason
rb.velocity = new Vector2(move * speed, rb.velocity.y);
Just remove the first one.
As a rule of thumb:
Calling rb.AddForce: This interacts with Physics - place it in FixedUpdate
Setting rb.velocity: This only sets a value. You are not triggering physics interactions with this - place it in Update
So, at first you should move your code which sets the velocity to the Update function. Also you are setting the velocity twice. Just remove the first line.
As an example here ist the Update and FixedUpdate function:
void FixedUpdate()
{
if(movementAllowed == true)
{
if (isGrounded() && Input.GetButtonDown("Jump"))
{
rb.AddForce(new Vector2(rb.velocity.x, Jump));
}
}
}
void Update()
{
if (movementAllowed == true)
{
float move = Input.GetAxisRaw("Horizontal");
rb.velocity = new Vector2(move * speed, rb.velocity.y);
Flip(move);
if (move == 0)
{
PlayerAnims.SetBool("isRunning", false);
}
else
{
PlayerAnims.SetBool("isRunning", true);
}
}
}
Since you are not using move anywhere else than Update you can just declare it as a local variable and remove the private float move; at the top of the script.
Last thing which should be mentioned:
You are getting component references both in Start or Awake. Generally you should get your references in Start and only get them in Awake when needed.