Day 5: Hard Pong Collisions

In our latest class, we established our collision and bouncing system using a two-step process:

  1. Identify whether or not our ball was still inside of the field of play
  2. If we are outside of the box, figure out what our position inside should be and move the ball there.

To use this, we will run a number of checks and scenarios using the “if/elseif” and “switch” conditional statements.

Part 1: Determining the boundaries

Next week, we will use Unity to process our collisions, and it will do this quite efficiently. For now, however, we will create our own collision system to get a sense of how these work. In our ideal situation, walls have perfect reflection, and so if the ball were to move in a path that would move it beyond the edge of the playing field, it would perfectly bounce back along that axis to remain in play.

Since we are only reflecting based upon the outer walls, this process is easy – we simply need to make sure that with each move, the ball remains inside of the outer boundaries, and if it does not, we need to adjust the position to keep it inside. These checks will require us to know the exact numeric position of these border lines, and we will refer to them repeatedly. Rather than repeatedly hard code these values, it would make sense for us to define a set of private variables to hold these values.

    private float FIELD_LEFT = -48.5f;
    private float FIELD_RIGHT = 48.5f;
    private float FIELD_TOP = 23.5f;
    private float FIELD_BOTTOM = -23.5f;

In class, we originally set these as 50, -50, 25, and -25, to reflect the edges of the plane that we created.  But since our ball has a 3 unit radius, we trim 1.5 units off of each side to give it the appearance of bouncing off of the “edge”.  Also note the use here of SNAKE_CASE for the variable names.  While we are not setting these to be “static” variables (meaning their value will not change), we are going to treat them as such, and use the snake case format to remind us that these are holding constants, ones that we should not alter.

Now that we know our boundaries, we have to check each time the ball moves to see if it crossed one of these boundaries.  If it did, we will have to process that bounce.  Let’s start by creating the following code to check our ball position.

     void CheckBall () {
         Vector3 curPos = myBall.transform.position;
 
         if (curPos.z >= FIELD_TOP) {
             // bounce off of top
             BounceBall(curPos, "top");
         } else if (curPos.z <= FIELD_BOTTOM) {
             // bounce off of bottom
             BounceBall(curPos, "bottom");
         }
 
         if (curPos.x >= FIELD_RIGHT) {
             // bounce off of right
             BounceBall(curPos, "right");
         } else if (curPos.x <= FIELD_LEFT) {
             // bounce off of left
             BounceBall(curPos, "left");
         }
 
     }

In this function, we do the following:

  1. Get the current position of the ball (assuming the ball just moved).   Note that we don’t have to declare a “new” Vector 3, because the object we are passing into this variable, the current position of the ball, is already of that structure.
  2. We test to see if it has crossed the upper or lower boundaries.  Since both will never be true, we can use a “if-elseif” statement to check one condition, and then the other if the first is not met.
  3. If the condition is met (i.e., the ball crossed the left or right boundary), we run the BounceBall ( ) command that we will create next.  Notice that this time we actually put something inside of the parenthesis of the function.  These are called “parameters”, and they are values that are passed to the function to be processed.  In this case we are passing the position of the ball, and the direction it should be bouncing off of.
  4. Repeat the process for left/right walls.

Part 2: The “switch” statement

Now let’s make BounceBall ( ).  We start out with the declaration of the function like this:

 void BounceBall (Vector3 bouncePos, string bounceObj) { ... }

This function, like most, will be declared as a void.  This is because we do not expect anything to return from it.  If we were writing a function that would return the value of two decimals added together, we would declare it as a float.  If we wanted to write a function that would return TRUE or FALSE conditions, we would define it as a bool.  But for functions where something happens but nothing comes back, we use void.

During this declaration, we create two parameter variables to hold the values the function call is passing to this.  These variables must be appropriately typed, so here we declare a Vector3 called “bouncePos” that will receive the position of the ball, and a string called “bounceObj” that will hold the text of which wall we are bouncing off of so that we know how to reposition the ball and change the heading.

Since we have four possible bounces here (top, bottom, left, and right), we are going to use a switch statement to determine which code runs.   Switch statements look like this:

    switch (someVariable) {
 
         case [value 1]:
             [some code]
             break;
         case [value 2]:
             [some other code]
             break;
         case [value 3]:
             [yet more code]
             break;
             ...
         default:
             [default code]
             break;
         }

The “switch” tests a variable against conditions and if it finds a match in a “case”, it runs that code.  If it does not find a match and reaches the end, the “default” option will run.  Notice that each case has a “break” call.  This will stop any further evaluation and move back outside of the switch statement.   If you do not include a “break”, then your case will run and “default” will also run, so make sure you remember to escape the statement with a break.

To process the bounce, we covered some basic geometry in class that we will use as our “bounce math”.  To summarize, if a ball is traveling up and to the right and encounters the top wall, the bounced ball will still move as far to the right as it would have had the wall not been there.  So for a “top” bounce, we know our X value is still valid.  Our math tells us that our Z value needs to be changed to be the same distance below the top edge as it currently is above (CURRENT POSITION minus BOUNDARY).  So our new position = BOUNDARY – (CURRENT POSITION – BOUNDARY), or (2 * BOUNDARY) – CURRENT POSITION.

Finishing our Bounce

We completed our BounceBall( ) method to read like this. Notice that we have chosen multiple methods to get both the bounce position and the revised direction. All of these evaluate to the same result, they are different ways of expressing the same operation. Our new bounced position can be defined as BOUNDARY - (position - BOUNDARY), which is the same as BOUNDARY + (BOUNDARY - position) which is the same as (2 * BOUNDARY) - position.

For our direction, we need to flip the value in either the x or z axis, which we can do by saying value =value * -1, or value = -value or value *= -1. All of these evaluate to the same result.

private void BounceBall(Vector3 bouncePos, string bounceObj)
{
    switch(bounceObj)
    {
        case "top":
            // bouncing off of the top
            bouncePos.z = FIELD_TOP - (bouncePos.z - FIELD_TOP);
            direction.z = -1.0f * direction.z;
            break;

        case "bottom":
            // bounce off of the bottom
            bouncePos.z = FIELD_BOTTOM - (bouncePos.z - FIELD_BOTTOM);
            direction.z = -direction.z;
            break;

        case "left":
            // bounce off of the left
            bouncePos.x = 2 * FIELD_LEFT - bouncePos.x;
            direction.x = -direction.x;
            break;

        case "right":
            // bounce off of the right
            bouncePos.x = 2 * FIELD_RIGHT - bouncePos.x;
            direction.x *= -1;
            break;

        default:
            Debug.Log("BounceBall Switch reached default");
            break;
    }

    //update the ball position
    myBall.transform.position = bouncePos;

}

Now our ball bounces against all four walls. But we have a new problem – the bounce is not clean. Our ball travels into the wall a bit before returning to us, instead of cleanly bouncing off of the sides. This is because our boundary position corresponds with the edge of the wall, but our ball position is at the center of our sphere object. Rather than building a complicated checking mechanism (after all, that’s what they physics system is for), we will simply reduce each boundary’s value by 1.5 units towards the origin, thus creating an invisible fence that will keep our ball the distance of the radius from the walls.

Assignment:

Your assignment for this weekend has two challenges:

  1. Make the ball bounce off of your paddle (front-face only) to keep it in play
  2. Limit the movement of the paddles to stay within the field of play (don’t go through the walls)

Details are available here: Assignment 1: Paddles


Ping.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Ping : MonoBehaviour
{
    public GameObject myBall;
    public GameObject leftPaddle;
    public GameObject rightPaddle;

    public Vector3 direction;
    public float speed;

    public float paddleSpeed;

    // Boundary Variables
    private float FIELD_LEFT = -48.5f;
    private float FIELD_RIGHT = 48.5f;
    private float FIELD_TOP = 23.5f;
    private float FIELD_BOTTOM = -23.5f;

    // Start is called before the first frame update
    void Start()
    {
        myBall.transform.position = new Vector3();  // Reset the ball to 0,0,0
        // normalize the direction vector
        direction.Normalize();
    }

    // Update is called once per frame
    void Update()
    {
        // move the ball
        myBall.transform.position = myBall.transform.position + (direction * speed * Time.deltaTime);

        // check the ball position
        CheckBall();

        // move the paddles
        MovePaddles();

    }

    private void CheckBall()
    {
        Vector3 curPos = myBall.transform.position;

        if (curPos.z >= FIELD_TOP)
        {
            // bounce off of the top
            BounceBall(curPos, "top");
        } else if (curPos.z <= FIELD_BOTTOM)
        {
            // bounce off of the bottom
            BounceBall(curPos, "bottom");
        }

        if (curPos.x <= FIELD_LEFT)
        {
            // bounce off of the left
            BounceBall(curPos, "left");
        } else if (curPos.x >= FIELD_RIGHT)
        {
            // bounce off of the right
            BounceBall(curPos, "right");
        }
    }

    private void BounceBall(Vector3 bouncePos, string bounceObj)
    {
        switch(bounceObj)
        {
            case "top":
                // bouncing off of the top
                bouncePos.z = FIELD_TOP - (bouncePos.z - FIELD_TOP);
                direction.z = -1.0f * direction.z;
                break;

            case "bottom":
                // bounce off of the bottom
                bouncePos.z = FIELD_BOTTOM - (bouncePos.z - FIELD_BOTTOM);
                direction.z = -direction.z;
                break;

            case "left":
                // bounce off of the left
                bouncePos.x = 2 * FIELD_LEFT - bouncePos.x;
                direction.x = -direction.x;
                break;

            case "right":
                // bounce off of the right
                bouncePos.x = 2 * FIELD_RIGHT - bouncePos.x;
                direction.x *= -1;
                break;

            default:
                Debug.Log("BounceBall Switch reached default");
                break;
        }

        //update the ball position
        myBall.transform.position = bouncePos;

    }

    private void MovePaddles()
    {
        // get the current position of the paddles
        Vector3 leftPadPos = leftPaddle.transform.position;
        Vector3 rightPadPos = rightPaddle.transform.position;

        // adjust that position based on the keys pressed
        if (Input.GetKey("up"))
        {
            rightPadPos.z = rightPadPos.z + (paddleSpeed * Time.deltaTime);
        }
        if (Input.GetKey("down"))
        {
            rightPadPos.z = rightPadPos.z - (paddleSpeed * Time.deltaTime);
        }
        if (Input.GetKey(KeyCode.W))
        {
            leftPadPos.z += (paddleSpeed * Time.deltaTime);
        }
        if (Input.GetKey(KeyCode.S))
        {
            leftPadPos.z -= (paddleSpeed * Time.deltaTime);
        }

        // put the new position back into the object's transform
        rightPaddle.transform.position = rightPadPos;
        leftPaddle.transform.position = leftPadPos;
    }
}