GPD Pong - Trouble with Raycasting for Opponent's Look-Ahead AI

I want to program the opponent AI where it can predict where the ball would be when it’s heading towards its paddle so that it can be moved to strike the ball at that spot. After posting a similar question on how the AI should behave, I tried programming this myself, figuring out some logic. But I’m running into trouble, because it seems that most of the time, the AI is not predicting in two of three specific cases I made.

This is simply a look-ahead raycasting approach, where the paddle would move to a specific position if the ball is heading straight towards the player’s goal zone (with no collisions on the walls whatsoever) or if the ball hits either the north or south walls exactly once before it reaches the opponent’s strike zone. In all other cases (where the ball hits the walls exactly more than once), the paddle does not react quite yet.

The code for this kind of AI is right here: (In a function called ballsPredictedZPositionInOpponentZone; there are debug statements currently. transform.position.x is the X position of the opponent’s paddle, as the script this function contains is attached to, and pongBall is simply an object of class Ball, which is attached to a game object comprising of a sphere, a rigidbody, and a collider. Tags and layers are used to filter the collisions and which decision the AI makes here.)

float ballsPredictedZPositionInOpponentZone(out bool setPaddleIntoPosition)
{
    /* 
        Logic:
        - Use raycasting to find the normal vectors if there are any collisions with the walls.
        - If a collision with a wall is past the opponent's strike zone (using absolute coordinate 
        values, on look-ahead, then we return both the Y position and a signal for the paddle 
        to get ahead.
        
        Base Cases:
        - If the first raycast hit is the opponent's goal zone, we simply use a scalar multiple 
        of the movement vector to find the position of the ball at that zone. Then we 
        return both the position and the flag to let the opponent get set.
        - In the case where what we hit in our first raycast is a wall, we perform one of 
        the following:
            - If in the next ray cast (after getting the normal) the collision detected is the
            goal zone or a paddle,
            use the same approach as the first to get the Y value the paddle should 
            move to to strike the ball.
            Return that, and output a true as well.
            - Otherwise, return 0.0f and false.

        Longest Ray Distance: 12.5 units (11.8 by Pythagoras from width and height of board
        plus a margin for detecting goal areas)
    */

    // Case 1 : Goal Zone from Primary Raycast
    // We simply notice that the collision this ray cast hits is a trigger zone representing the player's goal.
    RaycastHit collisionObject;
    float scalar;

    Debug.Log("Pong ball's position: " + pongBall.getPosition() + "

Pong ball’s speed vector: " + pongBall.getSpeedVector());

    Debug.Log("Do we hit a collider? " + 
        Physics.Raycast(pongBall.getPosition(), pongBall.getSpeedVector(), out collisionObject, 12.5f, LayerMask.NameToLayer("Opponent AI")));
    Debug.Log("What's the collision object? " + collisionObject.collider);
    
    if (collisionObject.collider != null)
        Debug.Log("Is the collider object at the player's goal? " + collisionObject.collider.CompareTag("Player's Goal"));

    if (Physics.Raycast(pongBall.getPosition(), pongBall.getSpeedVector(), out collisionObject, 12.5f, LayerMask.NameToLayer("Opponent AI")) &&
        collisionObject.collider.CompareTag("Player's Goal"))
    {
        // Formula for scalar: absolute x position of paddle minus absolute x position of pong ball, 
        // divided by x value of speed vector

        Debug.Log("Case 1");

        scalar = (Mathf.Abs(transform.position.x) - Mathf.Abs(pongBall.getPosition().x)) / 
            Mathf.Abs(pongBall.getSpeedVector().x);
        setPaddleIntoPosition = true;
        return pongBall.getPosition().z + pongBall.getSpeedVector().z * scalar;
    }

    // Case 2 : Goal Zone / Paddle from Secondary Raycast
    // We extend the ray as two partial rays with one hitting the wall and another hitting a goal zone.

    Debug.Log("Case 2 Detect");

    RaycastHit collisionObject2;

    Debug.DrawRay(pongBall.getPosition(), pongBall.getSpeedVector() * 5, Color.red, 10.0f);
    if (Physics.Raycast(pongBall.getPosition(), pongBall.getSpeedVector(), out collisionObject, 12.5f, LayerMask.NameToLayer("Opponent AI")))
        Debug.Log(collisionObject.collider);

    if (Physics.Raycast(pongBall.getPosition(), pongBall.getSpeedVector(), out collisionObject, 12.5f, LayerMask.NameToLayer("Opponent AI")) &&
        (collisionObject.collider.CompareTag("North Wall") || collisionObject.collider.CompareTag("South Wall")))
    {

        Debug.DrawRay(pongBall.getPosition(), Vector3.Reflect(pongBall.getSpeedVector(), collisionObject.normal) * 5, Color.red, 10.0f);

        Debug.Log("Reflection Vector on Speed: " + Vector3.Reflect(pongBall.getSpeedVector(), collisionObject.normal) 
            + "

Pong ball’s speed vector: " + pongBall.getSpeedVector());
Debug.Log("Do we hit a secondary collider? " +
Physics.Raycast(collisionObject.normal, Vector3.Reflect(pongBall.getSpeedVector(), collisionObject.normal),
out collisionObject2, 12.5f, LayerMask.NameToLayer(“Opponent AI”)));
Debug.Log("What’s the other collider? " + collisionObject2);
Debug.Log("Is the collider object at the player’s goal? " + collisionObject2.collider.CompareTag(“Player’s Goal”));
}

    if (Physics.Raycast(pongBall.getPosition(), pongBall.getSpeedVector(), out collisionObject, 12.5f, LayerMask.NameToLayer("Opponent AI")) &&
        (collisionObject.collider.CompareTag("North Wall") || collisionObject.collider.CompareTag("South Wall")) &&
        
        Physics.Raycast(collisionObject.normal, Vector3.Reflect(pongBall.getSpeedVector(), collisionObject.normal), 
        out collisionObject2, 12.5f, LayerMask.NameToLayer("Opponent AI")) &&
        (collisionObject2.collider.CompareTag("Player's Goal")))
    {

        Debug.Log("Case 2");

        // Similar to above, but with the normal surface this time.
        scalar = (Mathf.Abs(transform.position.x) - Mathf.Abs(collisionObject.normal.x)) /
            Mathf.Abs(Vector3.Reflect(pongBall.getSpeedVector(), collisionObject.normal).x);
        setPaddleIntoPosition = true;
        return pongBall.getPosition().z + pongBall.getSpeedVector().z * scalar;
    }

    // Case 3 : None of the above
    Debug.Log("Case 3");

    setPaddleIntoPosition = false;
    return 0.0f;

}

What’s happening is, on my output, by the time the ball comes back and hits the opponent’s ball detector (which is a spherical trigger collider), even if the ball is definitely moving straight towards the player’s goal zone with no collisions along the way, Case 1 is omitted. Case 2 is also omitted as well when we see a collision has to be made with the north or south wall before the ball gets into the opponent’s strike zone.

For your convenience, here’s a photo of the different components I’m referring to:

Below is some sample output for the two specific cases I’m trying to deal with:

Please help me out here!

I decided to go on about and implemented a different solution, which somewhat works. I said “somewhat” because what I didn’t test is when the opponent is on the left side instead of the right side.

Basically, I use vector and scalar arithmetic to predict the Z position of the ball when its X matches the opponent’s paddle, by simply extending the speed vector’s Z component by some scalar that is how many times the x component of the speed vector needs to be so that by the next frame, the ball is exactly the same x position as the opponent’s paddle.

Debug.Log("Speed Vector: " + pongBall.getSpeedVector());

        // Distance to Player's Goal (X-component)
        float xDisplacementToPlayersGoal = Mathf.Abs(transform.position.x - pongBall.getPosition().x);

        // Scalar for Speed Vector
        float scalarForSpeedVector = xDisplacementToPlayersGoal / Mathf.Abs(pongBall.getSpeedVector().x);

        // Scaled Y-Value for speed vector
        float scaledZValueForSpeedVector = (scalarForSpeedVector * pongBall.getSpeedVector()).z;


        Debug.Log("X Displacement to Player's Goal: " + xDisplacementToPlayersGoal);
        Debug.Log("Scalar for Speed Vector: " + scalarForSpeedVector);
        Debug.Log("Scaled Z Value for Speed Vector: " + scaledZValueForSpeedVector);


        // Retrieve the Y value based on these float values
        if (scaledZValueForSpeedVector + pongBall.getPosition().z < 2.35f && scaledZValueForSpeedVector + pongBall.getPosition().z > -2.35f)
        {
            Debug.Log("Current predicted value [0]: " + (scaledZValueForSpeedVector + pongBall.getPosition().z));
            return scaledZValueForSpeedVector + pongBall.getPosition().z;
        }

        // The ball is moving downwards and has to traverse across the width of the board at least once.
        if (scaledZValueForSpeedVector < 0.0f && Mathf.Abs(scaledZValueForSpeedVector) >= 4.8f)
        {
            Debug.Log("Current predicted value [1]: " + (scaledZValueForSpeedVector + pongBall.getPosition().z));
            while (scaledZValueForSpeedVector + pongBall.getPosition().z <= -2.35f)
            {
                scaledZValueForSpeedVector += 4.8f;
                Debug.Log("Current predicted value [2]: " + (scaledZValueForSpeedVector + pongBall.getPosition().z));
            }

            return scaledZValueForSpeedVector + pongBall.getPosition().z;
        }

        // The ball is moving upwards and has to traverse across the width of the board at least once.
        if (scaledZValueForSpeedVector > 0.0f && Mathf.Abs(scaledZValueForSpeedVector) >= 4.8f)
        {
            Debug.Log("Current predicted value[3]: " + (scaledZValueForSpeedVector + pongBall.getPosition().z));
            while (scaledZValueForSpeedVector + pongBall.getPosition().z >= 2.35f)
            {
                scaledZValueForSpeedVector -= 4.8f;
                Debug.Log("Current predicted value[4]: " + (scaledZValueForSpeedVector + pongBall.getPosition().z));
            }

            return scaledZValueForSpeedVector + pongBall.getPosition().z;
        }



        displacementFromBallToWall = 2.35f - Mathf.Abs(pongBall.getPosition().z);

        if (scaledZValueForSpeedVector < 0.0f && scaledZValueForSpeedVector >= -4.8f)
        {
            scaledZValueForSpeedVector += displacementFromBallToWall;
            return -2.35f - scaledZValueForSpeedVector;
        }


        scaledZValueForSpeedVector -= displacementFromBallToWall;
        return 2.35f - scaledZValueForSpeedVector;

I do lose some accuracy because of floating-point decimal rounding, but it’s adequate enough for this cause.