How To Create a Pong Game - Part 4
Part 4 | Creating the Paddle AI and Increasing Ball Speed
Hi 👋
If you’ve just stumbled upon this and are unsure what’s going on, this chapter is a larger series on How To Create a Pong Game from scratch with MonoGame.
You can read the previous chapter by clicking here.
Creating a Switch
For now, we can only play our game with another player (or against yourself — exciting, right?). Let’s create a very simple AI to replace the abscence of a friend.
First, add a new attribute to the CPUPaddle
class called isControllable
. Initialize it as True
. Then inside the Update
method,
add the following conditional:
public class CPUPaddle : Paddle
{
public bool IsControllable;
public CPUPaddle(...) : base(...)
{
IsControllable = true;
FlipX();
}
public override void Update()
{
if (IsControllable)
{
Move(Keys.I, Keys.K);
}
else
{
// Our AI logic
}
base.Update();
}
}
This attribute determines whether we’ll play against another player (IsControllable
set to true
) or the CPU (IsControllable
set to false
).
Passing the Ball as Reference
Our AI should make the CPUPaddle
accelerate along the y-axis toward the ball when it’s moving to the right side of the screen. To achieve
this, we’ll need a reference to the ball.
// CPUPaddle.cs
public class CPUPaddle : Paddle
{
...
public Ball Ball;
...
}
// Game1.cs
public class Game1 : Game
{
...
protected override void LoadContent()
{
...
cpuPaddle.Ball = ball;
}
...
}
Why are we assigning the ball’s reference this way? If you pass it through the constructor, you’ll need to move the ball’s initialization above
the CPUPaddle
’s initialization to pass it as a reference.
However, remember that the ball also needs references for both paddles. If you move the ball’s initialization above
the CPUPaddle
’s initialization, the CPUPaddle
’s reference will be null
.
So, what we’re doing instead is keeping the initialization order as it is and assigning the ball’s reference to the CPUPaddle
outside
its initialization.
Moving the Paddle Toward the Ball
Inside the Update
method, we first need to calculate the distance between our paddle’s center and the ball’s center. Depending on the ball’s
position along the y-axis, we’ll either Accelerate
or Decelerate
our paddle to move toward the ball:
public class CPUPaddle : Paddle
{
...
public override void Update()
{
if (IsControllable)
{
...
}
else
{
float dist = MathHelper.Distance(ball.Bounds.Center.Y, Bounds.Center.Y);
if (dist > 5f && ball.Velocity.X > 0f)
{
// paddle is above the ball
if (Bounds.Center.Y < ball.Bounds.Center.Y)
{
if (Velocity.Y < MaxSpeed)
Velocity.Y += Acceleration;
}
// paddle is below the ball
if (Bounds.Center.Y > ball.Bounds.Center.Y)
{
if (Velocity.Y > -MaxSpeed)
Velocity.Y -= Acceleration;
}
}
Bounds.Position += Velocity;
}
}
}
If the distance between the ball’s center and the paddle’s center is greater than 5, then:
- If the ball is above the paddle, we accelerate downwards.
- If the ball is below the paddle, we accelerate upwards.
The conditions Velocity.Y > -MaxSpeed
and Velocity.Y < MaxSpeed
are used to limit the speed’s movement speed.
Notice the use of ball.Velocity.X
: here, we’re trying to access the current value of the ball’s velocity along the X-axis.
However, this attribute is currently protected
(accessible only to inherited members of the Paddle
class), so we need to make it public:
public class Ball
{
...
public Vector2 Velocity;
...
}
Right, if we test our game:
Make sure to set
IsControllable
asfalse
before testing
Really cool! The paddle is now moving on its own toward the ball.
Now, let’s make the paddle return to the screen center when the ball moves away.
Moving the Paddle Back to the Screen Center
To make the paddle move back to the center, we just need to apply the same logic as before. However, instead of moving the paddle toward the ball, we’ll move it toward the center of the screen.
But first, we need to calculate the center of the screen:
public class CPUPaddle : Paddle
{
...
private Vector2 screenCenter;
public CPUPaddle(..., Vector2 screenSize) : base(...)
{
...
screenCenter = screenSize / 2f;
}
...
}
and inside the Update
method:
public class CPUPaddle : Paddle
{
...
public override void Update()
{
if (IsControllable)
{
...
}
else
{
...
if (dist > 5f && Ball.Velocity.X > 0f)
{
...
}
else
{
if (Bounds.Center.Y < screenCenter.Y)
{
if (Velocity.Y < MaxSpeed)
Velocity.Y += Acceleration;
}
if (Bounds.Center.Y > screenCenter.Y)
{
if (Velocity.Y > -MaxSpeed)
Velocity.Y -= Acceleration;
}
}
Bounds.Position += Velocity;
}
...
}
}
This is what will happen:
- If the ball is moving toward the paddle, the paddle should try to hit the ball.
- If the ball is moving away from the paddle, the paddle should move back to the screen center.
If we test:
Really cool! Now the paddle moves to the center of the screen when the ball is moving away. However, there is an issue: The paddle never adjusts itself to the center and stops, instead, it keeps moving back and forth.
To fix this, it’s very simple: just multiply Velocity.Y
by a value close to 1 (but not exactly 1):
public class CPUPaddle : Paddle
{
...
public override void Update()
{
...
else
{
...
else
{
if (Bounds.Center.Y < screenCenter.Y)
{
...
}
if (Bounds.Center.Y > screenCenter.Y)
{
...
}
Velocity.Y *= 0.99f;
}
...
}
...
}
}
The smaller the multiplier, the faster the paddle will stop moving, and the harder it will be to make it move.
This is similar to the linear interpolation we used when creating the knockback effect on both paddles. Because of the multiplication, the value never becomes exactly 0; instead, it approaches 0.
As we did before, let’s add a simple check and set Velocity.Y
to 0:
public class CPUPaddle : Paddle
{
...
public override void Update()
{
...
else
{
...
else
{
...
if (MathHelper.Distance(Bounds.Center.Y, screenCenter.Y) < 0.1f)
Velocity.Y = 0f;
Velocity.Y *= 0.99f;
}
...
}
...
}
}
If you test now, the paddle should stop after a while.
Fixing the Acceleration
You may have noticed that when the paddle moves up or down and touches the edge of the screen, it can’t quickly move in the opposite direction. The paddle gets stuck at the edge for a moment before moving.
It’s your turn! Try to figure out why this is happening.
To fix this, go back to the ConstrainToScreenBounds
method in the Paddle
class and add the following:
protected void ConstrainToScreenBounds()
{
...
if (Bounds.Bottom >= ScreenSize.Y || Bounds.Top <= 0f)
Velocity = Vector2.Zero;
}
Amazing! Now you have a paddle that controls itself to play with you.
Increasing the Ball speed
Now, let’s do something: let’s increase the ball’s speed every time it hits a paddle, but only from the second hit onwards.
Inside the Ball
class, add a new attribute:
internal class Ball
{
private int hitCount;
...
}
Now create a new method:
private void IncreaseBallSpeed()
{
if (hitCount < 2)
hitCount++;
if (hitCount >= 2)
moveSpeed += 0.25f;
}
And inside the code that checks when the ball hits a paddle:
public void Update()
{
...
if (Bounds.Intersects(playerPaddle.Bounds) && Velocity.X < 0f)
{
...
IncreaseBallSpeed();
}
if (Bounds.Intersects(cpuPaddle.Bounds) && Velocity.X > 0f)
{
...
IncreaseBallSpeed();
}
...
}
Now, after the second hit on a paddle, every subsequent hit should increase the ball’s speed by 0.25.
Reseting the Ball Speed
When you score a point, the ball resets its position but keeps moving very fast.
To fix this, let’s create a public method specifically to reset the ball’s state:
public void Reset()
{
ResetPosition();
SetNewDirection();
hitCount = 0;
moveSpeed = 4f;
}
This method should be public
because we will use it soon when implementing the UI.
For now, just call it when the ball moves off the screen and the spacebar is pressed:
public void Update()
{
...
if (Bounds.Right < 0f || Bounds.Left > screenSize.X)
{
Reset();
}
if (KeyboardManager.IsKeyPress(Keys.Space))
{
Reset();
}
}
Perfect, now you can play with another person or even play alone with a very simple AI.
End
Perfect, this is all we need for now.
If you managed to get this far with everything working well, then… Congratulations 🎉
You are ready the next part:
Click to read Part 5 | Creating the UI and Managing a Menu
🔮 Any question or suggestion, send me a message at:
👾 You can find the source code on Github