Day 5: Simple Collisions

In our latest class, we established a rudimentary collision and bouncing system by using a three-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 a reflected position inside should be
  3. Move the ball to that new position.

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.

    private void CheckBall()
    {
        // get the current ball position
        Vector3 currentPosition = myBall.transform.position;

        // check against the top & bottom
        if (currentPosition.z >= FIELD_TOP)
        {
            // bounce the ball
            BounceBall(currentPosition, "top");
        } else if (currentPosition.z <= FIELD_BOTTOM)
        {
            BounceBall(currentPosition, "bottom");
        }

        // check against the left & right
        if (currentPosition.x >= FIELD_RIGHT)
        {
            BounceBall(currentPosition, "right");
        } else if (currentPosition.x <= FIELD_LEFT)
        {
            BounceBall(currentPosition, "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 newPosition, string bounceWall) { ... }

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 currentPosition, string bounceWall)
    {
        // set up my new position vector
        Vector3 newPosition = currentPosition;

        // Compare the wall we should bounce from to execute the proper response
        switch (bounceWall)
        {
            case "top":
                // bounce off of the top
                newPosition.z = FIELD_TOP - (currentPosition.z - FIELD_TOP);
                direction.z = direction.z * -1f;
                break;
            case "bottom":
                // bounce off of the bottom
                newPosition.z = FIELD_BOTTOM - (currentPosition.z - FIELD_BOTTOM);
                direction.z = -direction.z;
                break;
            case "left":
                // bounce off of the left
                newPosition.x = 2 * FIELD_LEFT - currentPosition.x;
                direction.x = -direction.x;
                break;
            case "right":
                // bounce off of the right
                newPosition.x = 2 * FIELD_RIGHT - currentPosition.x;
                direction.x *= -1f;
                break;

            default:
                // we shouldn't be here - a bad string came in
                Debug.Log("Bounce Script received bad string: " + bounceWall);
                break;

        }

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

    }

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 of the assignment are available on Canvas, and a discussion about the assignment can be found towards the end of the class recording for Friday.


Ping.cs
using UnityEngine;

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

    public Vector3 direction;
    public Vector3 startPosition;
    public float speed;
    public float paddleSpeed;

    // field boundaries
    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 once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        myBall.transform.position = startPosition;

        // normalize my direction
        direction.Normalize();
    }

    // Update is called once per frame
    void Update()
    {
        // move the paddles
        MovePaddles();

        // Move The Ball
        myBall.transform.position = myBall.transform.position + (direction * speed * Time.deltaTime);

        // check the ball position
        CheckBall();

        
    }

    private void CheckBall()
    {
        // get the current ball position
        Vector3 currentPosition = myBall.transform.position;

        // check against top & bottom
        if (currentPosition.z >= FIELD_TOP)
        {
            // bounce the ball from the top
            BounceBall(currentPosition, "top");

        } else if (currentPosition.z <= FIELD_BOTTOM)
        {
            // bounce the ball from the bottom
            BounceBall(currentPosition, "bottom");
        }

        // check against right & left
        if (currentPosition.x >= FIELD_RIGHT)
        {
            BounceBall(currentPosition, "right");

        } else if (currentPosition.x <= FIELD_LEFT)
        {
            BounceBall(currentPosition, "left");

        }


    }

    private void BounceBall(Vector3 currentPosition, string bounceWall)
    {
        // debug test
        // Debug.Log("Bounce off of: " + bounceWall);

        // set up my new position vector
        Vector3 newPosition = currentPosition;

        // Compare the wall we should bounce from to execute the proper response
        switch (bounceWall)
        {
            case "top":
                // bounce off of the top
                newPosition.z = FIELD_TOP - (currentPosition.z - FIELD_TOP);
                direction.z = direction.z * -1f;
                break;
            case "bottom":
                // bounce off of the bottom
                newPosition.z = FIELD_BOTTOM - (currentPosition.z - FIELD_BOTTOM);
                direction.z = -direction.z;


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

                break;

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

            default:
                // we shouldn't be here - a bad string came in
                Debug.Log("Bounce Script received bad string: " + bounceWall);
                break;

        }

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

    }

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

        // adjust that position based on the keys that we press
        if (Input.GetKey("up"))
        {
            rightPaddlePosition.z = rightPaddlePosition.z + (paddleSpeed * Time.deltaTime);
        }
        if (Input.GetKey("down"))
        {
            rightPaddlePosition.z = rightPaddlePosition.z - (paddleSpeed * Time.deltaTime);
        }

        if (Input.GetKey(KeyCode.W))
        {
            leftPaddlePosition.z += (paddleSpeed * Time.deltaTime);
        }
        if (Input.GetKey(KeyCode.S))
        {
            leftPaddlePosition.z -= (paddleSpeed * Time.deltaTime);
        }
   
        // put the new position back into the paddles
        rightPaddle.transform.position = rightPaddlePosition;
        leftPaddle.transform.position = leftPaddlePosition; 

    }


}