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!