Lyiar
open main menu

How To Create a Pong Game - Part 4

/ 7 min read

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 as false before testing

cpu paddle is moving towards the ball

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:

cpu paddle is moving back to the center

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;
            }
            ...
        }
        ...
    }
}

cpu paddle is moving back to the center and decreases its speed

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();
    }
}

paddle trying to hit the ball and ball resetting its own state

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