2
\$\begingroup\$

I'm currently hard at work on a little platformer in the Unity3D engine, utilizing its 2D capabilities.

Now I have been having a problem where eventhough an object is clearly grounded it somehow doesn't register as such on every frame and thus the jump function, which only runs when the object is grounded, won't work. This so far only seems to happen whilst the player is on a platform that is currently moving downwards.

Now I'm not the best at 2D physics in Unity, so as such I decided to see what everyone here thinks might be the problem.

For illustration purposes here is what it looks like in the editor:

enter image description here

The white block is the player character, the blue block is the platform (currently moving down) and the red line is a raycast that checks wether or not the player character is grounded.

I hope you guys can figure out what's wrong. All I have so far is that it seems 50/50 wether or not the player is grounded during a frame on the platform, whilst said platform is moving down.

Hereby also my code for the player:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CharacterMovement : MonoBehaviour {

private bool left = false;
private bool right = false;

float velX = 0;
float velY = 0;

[SerializeField]
private float gravity;

[SerializeField]
private float speed;
[SerializeField]
private float maxSpeed;

[SerializeField]
private float jumpForce;
[SerializeField]
private float jumpSpeed;

bool isGrounded = false;
float groundedTime = 0;
float distanceToGround = 0;
float distanceSide = 0;

bool plug = false;
int plugCounter = 0;


void Start () {
    distanceToGround = this.GetComponent<Collider2D>().bounds.extents.y;
    distanceSide = this.GetComponent<Collider2D>().bounds.extents.x;
}

void Update () {

    Grounded();
    WalkingInput();
    Walking();
    Jumping();

    Debug.Log(isGrounded);

    this.GetComponent<Rigidbody2D>().velocity = new Vector2(velX, this.GetComponent<Rigidbody2D>().velocity.y);

    //Debug.Log(this.GetComponent<Rigidbody2D>().velocity.x + " " + this.GetComponent<Rigidbody2D>().velocity.y);

}

void Grounded()
{
    LayerMask mask = (1 << 8);
    mask = ~mask;
    if (Physics2D.Raycast(new Vector3(this.transform.position.x - distanceSide, this.transform.position.y - distanceToGround - 0.04f, this.transform.position.z),
                          Vector3.right,
                          distanceSide * 2,
                          mask))
    {
        isGrounded = true;
        groundedTime += Time.deltaTime;
    }
    else
    {
        isGrounded = false;
        groundedTime = 0;
    }

    //debugging ray
    Debug.DrawLine(new Vector3(this.transform.position.x - distanceSide, this.transform.position.y - distanceToGround - 0.04f, this.transform.position.z),
                   new Vector3(this.transform.position.x - distanceSide, this.transform.position.y - distanceToGround - 0.04f, this.transform.position.z) + new Vector3(distanceSide * 2, 0, 0),
                   Color.red);

}


void WalkingInput()
{
    if (Input.GetButtonDown("Left"))
    {
        left = true;
    }
    if (Input.GetButtonUp("Left"))
    {
        left = false;
    }

    if (Input.GetButtonDown("Right"))
    {
        right = true;
    }
    if (Input.GetButtonUp("Right"))
    {
        right = false;
    }
}


void Walking()
{

    if (left == true && velX >= -maxSpeed)
    {
        if(velX > 0)
        {
            velX = 0;
        }

        velX -= speed * Time.deltaTime;

        if (isGrounded)
        {
            if (velX < -maxSpeed)
            {
                velX = -maxSpeed;
            }
        } else
        {
            if (velX < -jumpSpeed)
            {
                velX = -jumpSpeed;
            }
        }
    }
    if (right == true && velX <= maxSpeed)
    {
        if (velX < 0)
        {
            velX = 0;
        }

        velX += speed * Time.deltaTime;

        if (isGrounded)
        {
            if (velX > maxSpeed)
            {
                velX = maxSpeed;
            }
        }
        else
        {
            if (velX > jumpSpeed)
            {
                velX = jumpSpeed;
            }
        }
    }

    if(left == false && right == false || left == true && right == true)
    {
        velX = 0;
    }

}


void Jumping()
{
    if (Input.GetButtonDown("Jump") && isGrounded)
    {
        Debug.Log("ActuallyJumped");
        this.GetComponent<Rigidbody2D>().AddForce(new Vector2(0, jumpForce));
    }

}

}

and the code for the platform:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MovingPlatform : MonoBehaviour {

[SerializeField]
Transform pointA;

[SerializeField]
Transform pointB;

[SerializeField]
float platformSpeed;

private float wayThere = 0;

bool going = true; //true = going to b, false = going to a


void FixedUpdate()
{
    if (going == true)
    {
        Vector2 offset = DetermineOffset(pointA.transform.position, pointB.transform.position);
        this.GetComponent<Rigidbody2D>().velocity = new Vector2(offset.x * platformSpeed * Time.deltaTime, offset.y * platformSpeed * Time.fixedDeltaTime);
        if (Vector2.Distance(this.transform.position, pointB.transform.position) < 0.2f)
        {
            going = false;
        }

    }
    else
    {
        Vector2 offset = DetermineOffset(pointB.transform.position, pointA.transform.position);
        this.GetComponent<Rigidbody2D>().velocity = new Vector2(offset.x * platformSpeed * Time.deltaTime, offset.y * platformSpeed * Time.fixedDeltaTime);
        if (Vector2.Distance(this.transform.position, pointA.transform.position) < 0.2f)
        {
            going = true;
        }
    }
}

private Vector2 DetermineOffset(Vector2 PointA, Vector2 PointB)
{

    int x = 0;
    int y = 0;

    if(PointB.x > PointA.x)
    {
        x = 1;
    }
    else if (PointB.x < PointA.x)
    {
        x = -1;
    }
    else
    {
        x = 0;
    }

    if(PointB.y > PointA.y)
    {
        y = 1;
    }
    else if (PointB.y < PointA.y)
    {
        y = -1;
    }
    else
    {
        y = 0;
    }

    return (new Vector2(x, y));

}

}

Also the editor setups for both the character and the platform in that order:

enter image description here

All in all I hope one of you good people can help me out on this one and thanks already!

EDIT: Changed the code for the platform to use velocity instead of rigidbody.moveposition. This works better. The bug is now non-existant on low speeds, it is still visible at high speeds. I think it might have to do with how Unity calculates rigidbody gravity because it becomes 0 when a rigidbody is grounded which means that currently the rigidbody is grounded, then it falls, then is grounded, then it falls which would explain the 50/50 ratio as well. I do however find it strange that this would work fine at a lower speed though.

If someone somehow can still manage to find out how to do this at faster speeds I'd be more than happy!

EDIT2: Answered my own question in the end. Check the answers to see how I fixed it.

\$\endgroup\$
11
  • \$\begingroup\$ Are you trying to model the physics of your player-character with a Rigidbody? That's very rarely a good idea in a typical platformer. \$\endgroup\$
    – Philipp
    Commented Aug 1, 2017 at 15:23
  • \$\begingroup\$ There are a number of platformers that do just fine with a Rigidbody for the character, so I don't think that's the root cause of your issue. The way you're raycasting looks unusual though. Typically we'd fire a ray downward, into the collider, intercepting it over a range of distances. Firing the ray sideways means you only see a collision along a very narrow slice - easily missed if your ground platform moves downward under that line. To get a check with some width, maybe you'd want a BoxCast instead? \$\endgroup\$
    – DMGregory
    Commented Aug 1, 2017 at 16:56
  • \$\begingroup\$ @Phillip Loads of people use rigidbody's to model player characters both in 2D and 3D. In Unity this just happens to be the easiest way to get a (mostly) good collisions model for a moving object without writing loads of custom stuff. \$\endgroup\$
    – LAKster
    Commented Aug 2, 2017 at 6:19
  • \$\begingroup\$ @DMGregory Tried that before. It actually used to be a box, but that had problems back then and I switched to vertical raycasting. That worked for a while until I wanted the character to move up/down a slope or stand on a curved object, I had two raycasts, one each side of the character, pointing perfectly down, but like I said, circular objects it does not like them. Then I started using the horizontal ray you see here both for performance and because it'd actually register along the entire with of the body. I might use a box again cause what it was interfering with has since been scrapped. \$\endgroup\$
    – LAKster
    Commented Aug 2, 2017 at 6:22
  • \$\begingroup\$ @DMGregory okay so I didn't know that a boxcast was different from a boxcollider, but I decided to google the term just to make sure I wasn't being dense, turned out I was being dense :') currently looking in to what a boxcast could mean for me! \$\endgroup\$
    – LAKster
    Commented Aug 2, 2017 at 6:29

2 Answers 2

0
\$\begingroup\$

Alright! So I finally figured it out! DmGregory was right and it was a problem with my raycast. However because I couldn't for the life of me figure out how a boxcast is supposed to be done the right way and it didn't quite seem like the type of thing I need, and it's way more expensive on performance than a raycast or a linecast, I decided to switch to the following piece of code in my Grounding(); function

LayerMask mask = (1 << 8);
    mask = ~mask;
    if (Physics2D.Linecast(new Vector2(this.transform.position.x - distanceSide, this.transform.position.y - distanceToGround),
                           new Vector2(this.transform.position.x + distanceSide, this.transform.position.y - distanceToGround - 0.1f),
                           mask))
    {
        isGrounded = true;
        groundedTime += Time.deltaTime;
    }
    else
    {
        isGrounded = false;
        groundedTime = 0;
    }

    //debugging ray
    Debug.DrawLine(new Vector3(this.transform.position.x -distanceSide, this.transform.position.y - distanceToGround, this.transform.position.z),
                   new Vector3(this.transform.position.x + distanceSide, this.transform.position.y - distanceToGround - 0.1f, this.transform.position.z),
                   Color.red);

Basically all it is is the raycast is now diagonal. I am gonna add another one that goes at a diagonal the other way (just make sure we covered most corners). Overall thanks for the answers everyone and I hope this'll help someone in the future.

\$\endgroup\$
-1
\$\begingroup\$

You can try calling the Grounded() and Jumping() methods from FixedUpdate(), because it uses the Physics Engine, with a fixed delta time. Perhaps it happens during the difference between delta time and fixed delta time.

Read more on Unitys execution order.

\$\endgroup\$
5
  • \$\begingroup\$ The reason those are in the update is because fixedUpdate doesn't neceserilly run every frame. This makes both the player input in the Jumping(); function inaccurate and the jumping function needs an as accurate as can be grounded function (thus ran as many frames as possible) to give responsive feedback. I might be wrong on the latter part though so I'll try it real quick and report back. \$\endgroup\$
    – LAKster
    Commented Aug 2, 2017 at 6:24
  • \$\begingroup\$ Yeah no, just tried switching the grounded function to fixed and it does the exact same thing it already did, now it just does it less times per second (as in it calls the function less times per second, not that the problem has decreased) \$\endgroup\$
    – LAKster
    Commented Aug 2, 2017 at 6:27
  • \$\begingroup\$ FixedUpdate runs with every move of the underlying physics objects, so putting the grounded check there won't miss any movements due to velocity. Checking for jump input in Fixed Update can actually reduce input latency, because you can act on the jump in this frame's physics steps (if there are any) instead of queuing them for the next frame's. You can use a pattern like this to ensure you don't drop inputs when running faster than your physics update rate. \$\endgroup\$
    – DMGregory
    Commented Aug 2, 2017 at 6:33
  • \$\begingroup\$ My bad, you all right, I must first cleary remember this word from unity docs:If the rigidbody has isKinematic set false then it works differently. It works like transform.position=newPosition and teleports the object (rather than a smooth transition). \$\endgroup\$
    – DevilNeko
    Commented Aug 2, 2017 at 7:32
  • \$\begingroup\$ That pattern looks more like a work around than a solution for a genuine problem. In the end you are still calling it whenever you press the button to call said function. \$\endgroup\$
    – LAKster
    Commented Aug 2, 2017 at 7:45

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .