9
\$\begingroup\$

I'm building a fairly simple marble racing game in Unity3D. The ball is a 3D physics object that moves only on the X and Y axes. It has the ability to roll left and right, and to jump. Pretty basic stuff, except that I've hit a game-breaking problem: When falling and striking the ground, the ball's bounce magnitude can be combined with its jumping force to create an extra-high jump. This means that, with well-timed button presses, the player can cause the ball to bounce exponentially higher, reaching unintended heights. I can't properly design levels until this glitch is fixed. I've illustrated this example:

Ball Bouncing vs Ball Bouncing + Jumping

The jumping, however, isn't as simple as just firing the ball straight upwards. In order to facilitate more complexity in the level design, I've programmed the jump angle to be relative to the surface that the ball is rolling on.

Ball Jump Angle Comparison

Figure 3, in that illustration, is how my game works thus far; not Figure 4. This makes solving for the bounce + jump problem much more challenging, because I can't simply measure and set an exact force or velocity on the Y-axis. Doing so results in weird behaviour, which becomes dramatically more noticeable as the ball travels on steeper slopes.

So far, I've been able to conceive a solution to all the other design problems in this game and then find out how to program them, but this one has me stuck. I've attempted a number of different approaches, but none of them have worked.

Here's the C# script that controls the ball's jumping:

using UnityEngine;
using System.Collections;

public class BallJumping : MonoBehaviour {

    public System.Action onJump;
    public Rigidbody objRigidbody; // Set this to the player
    public bool isGrounded; // Determines whether or not the ball is on the ground
    public Transform groundChecker; // A child object that's slightly larger than the ball
    public float groundRadius = 0.6f;
    public LayerMask whatIsGround; // Determines what layers qualify as ground
    public AudioClip jumpSFX;
    public AudioClip stickyJumpSFX;
    private float p_WillJumpTimeRemaining; // Grace periods before/after hitting the ground to trigger jump
    private float p_CanJumpTimeRemaining;
    public float earlyJumpToleranceDuration = 0.2f;
    public float lateJumpToleranceDuration = 0.2f;
    public float jump = 500f; // Jumping power
    private float halfJump = 250f; // Used for the sticky puddles
    public bool stuck = false; // Used for sticky materials
    private float contactX;
    private float contactY;


    // Input for jumping
    void Update () {
        if (Input.GetButtonDown ("Jump") && isGrounded == true) {
            ProcessJump();
        }
    }


    // Continuously checks whether or not the ball is on the ground
    void FixedUpdate () {
        if (Physics.CheckSphere (groundChecker.position, groundRadius, whatIsGround) == true) {
            isGrounded = true;
        } else {
            isGrounded = false;
        }
    }


    // Sets a grace period for before or after the ball contacts the ground for jumping input
    void ProcessJump () {
        bool boolGetJump = Input.GetButtonDown("Jump");

        if (boolGetJump && isGrounded == false) {
            p_WillJumpTimeRemaining = earlyJumpToleranceDuration;
        } else {
            if (p_WillJumpTimeRemaining > 0) {
                p_WillJumpTimeRemaining -= Time.fixedDeltaTime;
            }
        }

        if (isGrounded) {
            p_CanJumpTimeRemaining = lateJumpToleranceDuration;
        }

        if (isGrounded || p_WillJumpTimeRemaining > 0) {
            Jump();
        }

        if (p_CanJumpTimeRemaining > 0) {
            p_CanJumpTimeRemaining -= Time.fixedDeltaTime;
        }
    }


    // Sticky puddles script -- hinders jumping while in the puddle
    void OnTriggerEnter (Collider collision) {
        if (collision.gameObject.tag == "Sticky") {
            stuck = true;
        }
    }

    void OnTriggerExit (Collider collision) {
        if (collision.gameObject.tag == "Sticky") {
            stuck = false;
        }
    }


    // Calculates the normals for the jump angle
    void OnCollisionStay (Collision collision) {
        Debug.Log ("Collision.");
        foreach (ContactPoint contact in collision.contacts) {
            contactX = contact.normal.x;
            contactY = contact.normal.y;
        }
    }


    // Controls jumping
    void Jump() {
        Debug.Log ("Jump.");
        p_WillJumpTimeRemaining = 0.0f;
        p_CanJumpTimeRemaining = 0.0f;
        halfJump = jump * 0.5f; // Cuts jumping force in half while in a sticky puddle

        GetComponent<AudioSource>().volume = 1;
        GetComponent<AudioSource>().pitch = Random.Range (0.9f, 1.1f);

        if (stuck == false) {
            objRigidbody.AddForce (contactX * jump, contactY * jump, 0);
            GetComponent<AudioSource>().clip = jumpSFX;
            GetComponent<AudioSource>().Play ();
        }
        else if (stuck == true) {
            objRigidbody.AddForce (contactX * halfJump, contactY * halfJump, 0);
            GetComponent<AudioSource>().clip = stickyJumpSFX;
            GetComponent<AudioSource>().Play ();
        }


        if (onJump != null) {
            onJump();
        }
    }
}

My latest attempt was to try jump - rigidbody.velocity.magnitude * 50 , to reduce the jumping power by the speed that the ball is travelling. It almost solved the bounce + jump problem, by proportionally reducing the jump force down to zero as the ball's speed reached what seemed to be the equivalent in velocity.magnitude. It worked from a standstill, but the problem is, it also accounts for the magnitude while the ball is grounded, preventing the ball from rolling at full speed and jumping. I was close, but not quite there!

I'm a novice programmer, and I'm stumped here. Can anyone help me find a creative solution to this problem? As long as the player is able to continuously bounce and jump higher and higher, I can't design any levels, because they'll all just be able to be cheated through. I'd love to move onward -- this issue has been holding me back for a long time, so I'd greatly appreciate some advice!

\$\endgroup\$
  • \$\begingroup\$ Nice question:) did you try playing around with physic materials? You could set the bounciness of the ground to zero (or very low value). Maybe also the player, it depends. \$\endgroup\$ – M156 Apr 25 '16 at 7:29
0
\$\begingroup\$

First of all, i want to say that your question is very well written and it's a pleasure :), you would just need to remove what's not necessary in the code (audiosources, etc.) and it would be perfect. Cheers for that.

For the answer, you could clamp your velocity when jumping, which would prevent you from reaching too high speeds when pressing the jump button.

\$\endgroup\$
0
\$\begingroup\$

While I personally love bunny hopping... As a starting point we should know the intended "Jump Speed" as a delta velocity. This figure represents the velocity increase (in the line with the "Jump Normal") during the instant of jumping once.

Any velocity the player already has in line with the Jump Normal can be seen as a pre-existing "Jump Energy". This leads to a straightforward solution: The instantaneous delta velocity can be limited such that it never results in the player being accelerated beyond the target velocity.

In order to measure your pre-existing Jump Speed, we can take the dot product of your normalized your Jump Vector, and your player's velocity:

Vector2 JumpNormal = Vector2(contactX, contactY).normalized;
Vector2 PlayerVelocity = objRigidbody.velocity;
float ExistingSpeed = Vector2.Dot(PlayerVelocity, JumpNormal);
if (ExistingSpeed < 0) ExistingSpeed = 0;

The "Existing Speed" is also forced non-negative here; When the player is falling, a negative existing jump speed will compensate for their falling, allowing them to bounce on thin air if they trigger the jump while falling.

Now that we know how much to reduce the delta velocity precisely, we can compute the effective "Jump Vector" by scaling the Jump Normal to the target delta velocity.

float AdjustedSpeed = JumpSpeed - ExistingSpeed;
if (AdjustedSpeed < 0) AdjustedSpeed = 0;
Vector2 JumpVector = JumpNormal * AdjustedSpeed;
objRigidbody.velocity += JumpVector;

This time the Adjusted Jump Speed is being forced non-negative; If the player is already rising faster than they should be able to jump, they would attain a negative adjusted speed, which allows them to use the "jump" action as brakes. (to slow down to the intended jump speed instantly!)

Note: I believe your contact X and Y are already normalized as a pair. I included explicit detail for completeness sake though.

\$\endgroup\$
0
\$\begingroup\$

This answer is perhaps more of a design change than you're looking for, but how about this - the ball has a short period after the jump button is pressed where it stays firmly on the ground and cancels any upward vertical momentum (maybe squashing a bit to denote a spring-like compression), then leaps upward after that period has concluded. This would solve the problem of the bounce's momentum adding to the jump, although it would also allow players to control whether or not they bounced or just jumped. It also adds a delay to the jump function, which can be seen as good (feels more natural) or bad (doesn't allow players enough time to respond).

\$\endgroup\$

Your Answer

By clicking "Post Your Answer", you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.