1
\$\begingroup\$

After the ball hits the paddle it continues for a frame or two in the physics, reflected direction, then my onCollisionEnter2D function runs and changes the velocity. At least, that's what it seems like. I'm a noob with Unity so I must be misunderstanding something.

https://streamable.com/ac2gg

if (other.gameObject.CompareTag("Paddle"))
    {
        float dist = ballRb.transform.position.x - paddleRb.transform.position.x;

        if (dist > 0)
        {
            print("right");
            ballRb.velocity = new Vector2(4, speedY);
        }
        else
        {
            print("left");
            ballRb.velocity = new Vector2(-4, speedY);
        }
    }

Thoughts on why there's a stutter after a redirection? I slowed it down so you can see better what's happening.

\$\endgroup\$
2
  • \$\begingroup\$ Have you tried it with kinematic rigid body? \$\endgroup\$
    – Swati
    Oct 29, 2019 at 8:09
  • \$\begingroup\$ For a bit. I didn't know how to calculate the balls new direction when it bounced off everything so I stopped that approach. I tried to negate the x or y vector, or both, when it hit something but couldn't figure out how to do that reliably in all situations, if that makes sense. \$\endgroup\$
    – tazboy
    Oct 29, 2019 at 21:51

3 Answers 3

2
+50
\$\begingroup\$

Great work on your code so far, it's not far off the solution.

One issue you are having is the bounce is applied twice. That is if it's positioned and timed right you'll bounce left and right. The other is physics is processing the bounce first which is off course from your fixed direction.

Note: For correctness I must mention setting velocity isn't the recommended method, though certainly makes it easier, and with fixes below it works as intended. This could be done by rotating a separate collider/rigidbody around a ray cast intersection to form the bounce surface invisible from the sprite, though that's another question. EDIT: I've added code for this at bottom of answer.

Fixes

  1. Add a boolean for rebounding while colliding

  2. Check/set rebounding

  3. Tick "is trigger" on the Paddle Collider (Remove physics)

While that may seem straight forward, there's more to it. I took the liberty of writing an example:


public class BallVelocity : MonoBehaviour
{
    // Ref to rigidbody
    private Rigidbody2D m_BallRigidbody2D;

    // Ball Collider
    private Collider2D m_BallCollider;

    // Fixed Velocity on the Y axis to rebound with
    public float VelocityY = 4f;

    // Store these for use each fixed update
    private Vector3 m_NewVelocityLeft, m_NewVelocityRight;

    // Store if we have rebounded this collision
    private bool m_Rebounding = false;

    // need empty start for enable checkbox in editor
    private void Start()
    {

    }

    void Awake()
    {
        // Set references to require components
        m_BallRigidbody2D = GetComponent<Rigidbody2D>();
        m_BallCollider = GetComponent<Collider2D>();

        // Allocate once to prevent GC, however if values are to be calculated, move to FixedUpdate
        m_NewVelocityRight = new Vector2(4, VelocityY);
        m_NewVelocityLeft = new Vector2(-4, VelocityY);

        GetComponent<Rigidbody2D>().velocity = new Vector2(4, -VelocityY); ;
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        // Manually check we have collided with paddle
        if (collision.gameObject.tag == "Paddle")
        {
            // Check we are not currently rebounding (still touching collider after last)
            if (!m_Rebounding)
            {
                m_Rebounding = true;

                float dist = transform.position.x - collision.transform.position.x;

                if (dist > 0)
                {
                    print("right");
                    // Set velocity once only in fixed update
                    // This is so other standard collisions will work, i.e. walls
                    m_BallRigidbody2D.velocity = m_NewVelocityRight;
                }
                else
                {
                    print("left");
                    m_BallRigidbody2D.velocity = m_NewVelocityLeft;
                }
            }
        }

    }

    private void OnTriggerExit2D(Collider2D collision)
    {
        if (collision.gameObject.tag == "Paddle")
        {
            // If not touching then we're clear of rebounding
            m_Rebounding = false;
        }
    }
}


Thrown together test: https://streamable.com/d3iyn

To complete my answer with a method of rotating the paddle collider for the physics to process the bound I have created a project and Tutorial.

Project on GitHub: https://github.com/Sibz/PaddleGame

Tutorial on YouTube: https://youtu.be/Vu0hSTDLuhk

And here's the code to go on the paddle. However you need to set up the scene correctly.

  • Set Preferences -> Physics2D -> Velocity Threshold to 0.0001
  • Set Collider of paddle to be trigger, really thin and along top of paddle.
  • Put this paddle on it's own layer and set PaddleLayer
  • Add a second 'invisible' Paddle with collider/rigidbody and set PaddleCollider
public class Paddle : MonoBehaviour
{
    public GameObject Ball;
    public GameObject PaddleCollider;

    public float RayDistance = 1;

    public LayerMask PaddleLayer;

    [Tooltip("Max angle of paddle collider change when hitting the very end of the paddle.")]
    [Range(1, 90)]
    public float AngularChange = 45;

    private Rigidbody2D m_BallRigidBody2D;
    private SpriteRenderer m_PaddleSpriteRenderer;
    private void Start()
    {
        m_BallRigidBody2D = Ball.GetComponent<Rigidbody2D>();
        m_PaddleSpriteRenderer = GetComponent<SpriteRenderer>();
    }

    private void FixedUpdate()
    {
        //Get the direction the ball is travelling
        //This is basically the velocity, however we normalize this
        Vector2 direction = m_BallRigidBody2D.velocity.normalized;

        //Ray cast towards the paddle
        RaycastHit2D hit = Physics2D.Raycast(Ball.transform.position, direction, RayDistance, PaddleLayer);

        // Draw a ray we can see in scene view
        Debug.DrawRay(Ball.transform.position, direction * RayDistance, Color.red);

        if (hit)
        {
            PaddleCollider.SetActive(true);

            // Reset our paddle collider so it is flat and at position of paddle
            PaddleCollider.transform.rotation = new Quaternion();
            PaddleCollider.transform.position = transform.position;

            // If we have hit we want to rotate the ball around that point
            Vector2 hitPoint = hit.point; // Worldspace

            // Transform above into local space for the paddle
            // so we can figure how far along paddle hit was
            Vector2 localHitPoint = hitPoint - (Vector2)transform.position;

            // Calculate the offseet on the paddle that we hit
            // this will be -1 to 1, with 0 being centre;
            float offset = /*point.x / length*/ localHitPoint.x / (m_PaddleSpriteRenderer.bounds.extents.x);

            // Get our incoming angle
            // -180' to 180'
            var incomingAngle = Vector2.SignedAngle(direction, Vector3.down);

            // Calculate our angularchange based on hit position
            float adjustedAngularChange = -AngularChange * offset;

            // We want to translate the incoming angle based on the offset
            // but if it's a really shallow angle, bounce it more upwards.
            // (This is to prevent overly sideways movment)
            // Setting the paddle to half incoming angle will reflect directly up
            // so we use this for our corrective figure
            float correctionForShallowHitsAngle = -incomingAngle / 2;

            // Now we can get a value between to two based on how
            // sideways the ball in coming in from
            // if its 90' then 100% use the correctionForShallowHitsAngle
            // if it's 0' then 100% use the adjustedAngularChange
            // we can use linear interpolation (lerp) for this.

            float computedAngularChange =
                Mathf.Lerp(adjustedAngularChange, correctionForShallowHitsAngle, Mathf.Abs(incomingAngle) / 90);

            // Rotate around hit point with angle adjusted by above percent
            PaddleCollider.transform.RotateAround(hitPoint, Vector3.forward, computedAngularChange);

        }
        else
        {
            PaddleCollider.SetActive(false);
        }
    }
}

```
\$\endgroup\$
4
  • \$\begingroup\$ I'm a paddle ball addict. I wrote another version to use FixedUpdate to see if running before physics made a difference. I think it's the same. Though this version speeds up over time! \$\endgroup\$ Nov 2, 2019 at 4:01
  • \$\begingroup\$ I wish I understood how to do the whole raycast intersection thing you mentioned. Why do you prefer to use that and not the physics engine? \$\endgroup\$
    – tazboy
    Nov 3, 2019 at 14:04
  • \$\begingroup\$ @tazboy The raycast thing is only to find to point of rotation for the paddle collider to orientate it so the bounce is completely done by physics. \$\endgroup\$ Nov 4, 2019 at 23:46
  • \$\begingroup\$ Nicely done. Thank you for the detail information and the great video! \$\endgroup\$
    – tazboy
    Nov 6, 2019 at 0:58
2
\$\begingroup\$

This looks like the result of the discrete way game physics get simulated.

Here's a rough breakdown of what the steps might look like under the hood.

In the first three panels, you can see the ball approaching the paddle. Each physics step, it moves to a new position given by its velocity multiplied by your fixed time step (shown here in purple)

In the third panel, we can see that the ball's new position overlaps the paddle.

Diagram showing steps of ball's approach & reflection.

To fix this, the physics engine computes the minimum separation vector - the smallest amount it would need to move one (or both) objects in order to resolve the penetration. Here, that offset takes the ball straight up - out of its original line of travel. The physics engine also computes a separating impulse and uses that to modify the ball's velocity. You can see these changes in the 4th panel.

All this happens during the internal physics update step. Any collision pairs detected in this way are queued-up, for their collision/trigger event functions to be called after the physics update is complete.

That's what happens in the 5th panel: your script runs and modifies the velocity to point in the new direction, but it doesn't undo the offset applied by the physics engine to resolve the overlapping colliders.

The change in velocity you've applied won't be visible until the next physics step, so if a frame is rendered at this moment it might look like the ball slid sideways or hesitated on contact with the paddle, before bouncing away.

Lastly, in the sixth panel, we see the effect of the next physics update. Your ball starts moving along the new velocity vector you've set in your script. But because of the corrective offset applied earlier, it's now travelling along a path slightly shifted from the one it came in on.


So, what can we do about it?

If you don't want the physics engine to resolve penetrations for you, you can set one or both colliders' IsTrigger property to True. This will still generate events for your scripts to act on - you'll just have to listen for OnTriggerEnter2D instead of OnCollisionEnter2D - but it won't apply any physics impulses or restitution offsets, leaving you in full control.

You can use raycasts or shapecasts to determine the exact moment the ball would have struck the paddle, and scale the offset according to how much "spare change" time was left in the frame after that moment, so you see the new direction of bounce immediately.

You can also enable interpolation or extrapolation on the ball's rigidbody, to generate intermediate display positions based on its movement history or velocity respectively, to smooth its displayed motion and avoid choppiness or apparent hesitation due to the mismatch between the physics timestep and rendering framerate.

\$\endgroup\$
2
  • \$\begingroup\$ The top portion has great detail. You're good at explaining things, for sure. I tried onTrigger for a bit but I wasn't able to recreate the physics of the ball. Plus, it seems like a lot of work for something that should be taken care of by the physics system for any projectile, especially a simple ball bouncing off of something. \$\endgroup\$
    – tazboy
    Oct 29, 2019 at 21:54
  • \$\begingroup\$ Can you show us what you've tried with OnTriggerEnter2D, and how the resulting behaviour differs from what you want? With this detail, we should be able to guide you in what specifically you need to change to get the desired behaviour. \$\endgroup\$
    – DMGregory
    Oct 29, 2019 at 22:50
1
\$\begingroup\$

It seems that you are running your physics scene in a lower frame rate.

In project setting->physics setting, try a larger physics frame rate and it should work fine.

\$\endgroup\$
3
  • \$\begingroup\$ I don't see anything stated as "frame rate" but I did increase velocity iterations to 500 and position iterations to 200 and there's no difference. Any other ideas would be greatly appreciated. \$\endgroup\$
    – tazboy
    Oct 28, 2019 at 22:30
  • \$\begingroup\$ My apology, it should be fixed update rate. check this out: docs.unity3d.com/Manual/class-TimeManager.html \$\endgroup\$
    – Noah Zuo
    Oct 29, 2019 at 12:17
  • \$\begingroup\$ That seems to have helped quite a bit. There's still a micro stutter but it's pretty unnoticeable. After changing the value a bit the game seems to be more "snappy", which, in a way, makes it quicker. I'll probably have to decrease some speed values because of it. \$\endgroup\$
    – tazboy
    Oct 29, 2019 at 21:57

You must log in to answer this question.

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