How to Build an AI Paddle Game in Unity Using Neural Networks
12 mins read

How to Build an AI Paddle Game in Unity Using Neural Networks

Paddle Game
Paddle Game

Here is a step-by-step guide to building a classic Ping Pong-style game from scratch in Unity 6. Because you want to use 3D shapes (a sphere and 3D rectangles) but keep a 2D gameplay feel, we will use a 3D project but set the camera to view the world in 2D (Orthographic).

Step 1: Project and Camera Setup

  1. Open the Unity Hub and create a new 3D Core project in Unity 6.
  2. In the Hierarchy, select the Main Camera.
  3. In the Inspector, change the Projection from Perspective to Orthographic.
In the Inspector, change the Projection from Perspective to Orthographic
In the Inspector, change the Projection from Perspective to Orthographic
  1. Set the Size to 10 (this controls how much of the arena is visible).
  2. Set the Camera’s Transform Position to X: 0, Y: 0, Z: -10 and Rotation to 0, 0, 0. Change the Clear Flags to Solid Color and pick a black background.

Step 2: Building the Arena and Objects

  1. The Paddles:
    • Right-click in the Hierarchy > 3D Object > Cube. Name it Player1.
    • Set its Scale to X: 0.5, Y: 3, Z: 1 (making it a 3D rectangle).
    • Set its Position to X: -12, Y: 0, Z: 0.
    • Duplicate Player1 (Ctrl/Cmd + D), name it Player2, and set its position to X: 12, Y: 0, Z: 0.
  2. The Ball:
    • Right-click the Hierarchy > 3D Object > Sphere. Name it Ball.
    • Set its Position to X: 0, Y: 0, Z: 0.
  3. The Walls (Boundaries):
    • Create another Cube, name it TopWall.
    • Set its Scale to X: 30, Y: 1, Z: 1 and Position to X: 0, Y: 10, Z: 0.
    • Duplicate it, name it BottomWall, and set its Position to X: 0, Y: -10, Z: 0.

Step 3: Perfecting the Physics

Classic arcade games need perfectly elastic collisions where the ball never loses speed.

  1. Right-click in your Project window > Create > Physic Material. Name it Bouncy.
  2. Select Bouncy and set these exact values in the Inspector:
    • Dynamic Friction: 0
    • Static Friction: 0
    • Bounciness: 1
    • Friction Combine: Minimum
    • Bounce Combine: Maximum
  3. Apply this material: Select your Player1, Player2, TopWall, BottomWall, and Ball in the Hierarchy. Drag the Bouncy material into the Material slot of their Box Collider or Sphere Collider components.
  4. Configure the Ball’s Rigidbody:
    • Select the Ball and click Add Component > Rigidbody.
    • Uncheck Use Gravity.
    • Set Collision Detection to Continuous.
    • Under Constraints, freeze Position Z, and freeze Rotation X, Y, and Z.

Step 4: Writing the Scripts

1. The Paddle Movement Script

  • In the Project window, right-click > Create > C# Script. Name it PaddleController.
  • Attach this script to both Player1 and Player2.
  • Open it and paste the following code. This allows Player 1 to use W/S and Player 2 to use the Up/Down arrows.
using UnityEngine;

public class PaddleController : MonoBehaviour
{
    public float speed = 15f;
    public KeyCode moveUp;
    public KeyCode moveDown;
    
    // Set these in the Inspector based on where your walls are
    public float yMax = 8.5f; 
    public float yMin = -8.5f;

    void Update()
    {
        if (Input.GetKey(moveUp))
        {
            transform.Translate(Vector3.up * speed * Time.deltaTime);
        }
        else if (Input.GetKey(moveDown))
        {
            transform.Translate(Vector3.down * speed * Time.deltaTime);
        }

        // Clamp the Y position so the paddle doesn't go through the walls
        Vector3 clampedPosition = transform.position;
        clampedPosition.y = Mathf.Clamp(clampedPosition.y, yMin, yMax);
        transform.position = clampedPosition;
    }
}
  • Setup in Inspector: Select Player1, set Move Up to W and Move Down to S. Select Player2, set Move Up to Up Arrow and Move Down to Down Arrow.

Step 2: Create “Goal” Zones

We need invisible zones behind the paddles to detect when the ball goes out of bounds.

  1. Right-click the Hierarchy > 3D Object > Cube. Name it LeftGoal.
  2. Set its Scale to X: 1, Y: 25, Z: 1 and its Position to X: -15, Y: 0, Z: 0 (just behind Player 1).
  3. In the Inspector for LeftGoal, check the “Is Trigger” box on the Box Collider component.
  4. Uncheck the box next to Mesh Renderer so the cube becomes invisible.
  5. Duplicate LeftGoal (Ctrl/Cmd + D), name it RightGoal, and set its Position to X: 15, Y: 0, Z: 0.
  6. Assign Tags: At the very top of the Inspector, click the Tag dropdown, select Add Tag, and create two new tags: LeftGoal and RightGoal. Go back to your goal objects and assign these tags to them respectively.

Step 3: Build the Score UI

  1. Right-click the Hierarchy > UI > Text – TextMeshPro. (If a window pops up asking to Import TMP Essentials, click import).
  2. Name the new text object ScoreText.
  3. In the Canvas object that was automatically created, change the Canvas Scaler component’s UI Scale Mode to Scale With Screen Size.
  4. Select ScoreText, set its text to 0 – 0, center the alignment, change the color to white, and increase the font size. Anchor it to the Top Center of the screen and adjust its position.

Step 4: The GameManager Script

We need a central manager to handle the score math and update the UI.

  1. Right-click in the Hierarchy > Create Empty. Name it GameManager.
  2. Create a new C# script called GameManager and attach it to this empty object.
  3. Paste the following code:
using UnityEngine;
using TMPro; // Needed for UI text

public class GameManager : MonoBehaviour
{
    public int player1Score = 0;
    public int player2Score = 0;
    public TextMeshProUGUI scoreText;
    public BallController ball;

    public void AddScore(int playerID)
    {
        if (playerID == 1)
        {
            player1Score++;
        }
        else if (playerID == 2)
        {
            player2Score++;
        }

        // Update the UI
        scoreText.text = player1Score + " - " + player2Score;
        
        // Reset the ball after a point is scored
        ball.ResetBall();
    }
}

Step 5: Update the Ball to Detect Goals

Finally, we need to tell the ball what happens when it touches the invisible triggers and how to reset itself. Open your Ball, then add a component Script BallController and paste it:

using UnityEngine;

public class BallController : MonoBehaviour
{
    public float initialSpeed = 10f;
    private Rigidbody rb;
    public GameManager gameManager; // We will link this in the Inspector

    void Start()
    {
        rb = GetComponent<Rigidbody>();
        LaunchBall();
    }

    void LaunchBall()
    {
        float x = Random.Range(0, 2) == 0 ? -1 : 1;
        float y = Random.Range(0, 2) == 0 ? -1 : 1;
        rb.linearVelocity = new Vector3(x * initialSpeed, y * initialSpeed, 0f);
    }

    public void ResetBall()
    {
        // Stop the ball and move it back to the center
        rb.linearVelocity = Vector3.zero;
        transform.position = Vector3.zero;
        
        // Launch again after 1 second
        Invoke(nameof(LaunchBall), 1f); 
    }

    // This detects when the ball hits our invisible goal zones
    void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("LeftGoal"))
        {
            gameManager.AddScore(2); // Player 2 gets a point
        }
        else if (other.CompareTag("RightGoal"))
        {
            gameManager.AddScore(1); // Player 1 gets a point
        }
    }
}

Final Link-up: Go back to Unity. Select your GameManager object in the Hierarchy. In the Inspector, drag your ScoreText into the Score Text slot, and drag your Ball into the Ball slot. Then, select your Ball object and drag it into the ball’s Game Manager slot.

Here is the complete step-by-step guide to transforming your Pong-style paddle into an intelligent, self-playing AI using Unity ML-Agents

Phase 1: Python and Miniconda Environment Setup

ML-Agents requires a Python environment to run the machine learning backend (PyTorch) that communicates with Unity.

  1. Install Miniconda: Download and install Miniconda for your operating system.
  2. Create the Virtual Environment: Open your Anaconda Prompt (or terminal) and create a dedicated environment by running:
conda create -n mlagents python=3.10.12

Activate the Environment:

conda activate mlagents

Install PyTorch and ML-Agents: Install the required Python packages inside this environment:

mlagents 1.1.0 (version)

pip install torch==2.1.1 torchvision==0.16.1 torchaudio==2.1.1
pip install mlagents
pip install onnx==1.15.0 protobuf==3.20.3

Phase 2: Unity Package Setup

  1. Open your Unity project.
  2. Go to Window > Package Manager.
  3. Click the “+” button in the top left and select “Add package from Unity Registry”.
  4. Search for ML Agents and click Install.
Unity Registry -ML Agents
Unity Registry -ML Agents

Phase 3: Converting the C# Script to an ML-Agent

We need to rewrite your PaddleController code so the neural network can perceive the game, make decisions, and receive rewards.

Create a new script named PaddleAgent.cs and attach it to Player1. Remove the old PaddleController.

using UnityEngine;
using Unity.MLAgents;
using Unity.MLAgents.Actuators;
using Unity.MLAgents.Sensors;

public class PaddleAgent : Agent
{
    public float speed = 15f;
    public float yMax = 8.5f; 
    public float yMin = -8.5f;

    [Header("Game References")]
    public Transform ball;
    private Rigidbody ballRb;
    public BallController ballController; // Reference to your existing ball script

    public override void Initialize()
    {
        ballRb = ball.GetComponent<Rigidbody>();
    }

    // Called at the start of every training episode
    public override void OnEpisodeBegin()
    {
        // Reset the paddle position
        transform.localPosition = new Vector3(-12f, 0, 0);
        
        // Reset the ball using your existing logic
        ballController.ResetBall();
    }

    // This is how the AI "sees" the game. We give it 5 numbers.
    public override void CollectObservations(VectorSensor sensor)
    {
        sensor.AddObservation(transform.localPosition.y); // Paddle's Y position (1)
        sensor.AddObservation(ball.localPosition.x);      // Ball's X position (1)
        sensor.AddObservation(ball.localPosition.y);      // Ball's Y position (1)
        sensor.AddObservation(ballRb.linearVelocity.x);   // Ball's X velocity (1)
        sensor.AddObservation(ballRb.linearVelocity.y);   // Ball's Y velocity (1)
    }

    // This is where the AI takes action based on its neural network
    public override void OnActionReceived(ActionBuffers actions)
    {
        int moveAction = actions.DiscreteActions[0];
        Vector3 move = Vector3.zero;

        // 0 = Stay, 1 = Up, 2 = Down
        if (moveAction == 1) move = Vector3.up;
        if (moveAction == 2) move = Vector3.down;

        transform.Translate(move * speed * Time.deltaTime);

        // Clamp the Y position
        Vector3 clampedPosition = transform.position;
        clampedPosition.y = Mathf.Clamp(clampedPosition.y, yMin, yMax);
        transform.position = clampedPosition;
    }

    // Allows you to manually test the agent using keyboard input before training
    public override void Heuristic(in ActionBuffers actionsOut)
    {
        var discreteActions = actionsOut.DiscreteActions;
        discreteActions[0] = 0; // Default stay

        if (Input.GetKey(KeyCode.W)) discreteActions[0] = 1;
        if (Input.GetKey(KeyCode.S)) discreteActions[0] = 2;
    }

    // Reward the AI for hitting the ball, punish it for missing
    private void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.CompareTag("Ball"))
        {
            AddReward(1.0f); // Positive reinforcement!
        }
    }

    private void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("LeftGoal")) // Assuming the AI is Player 1 on the left
        {
            SetReward(-1.0f); // Negative reinforcement!
            EndEpisode();     // Restart the round
        }
    }
}

Phase 4: Configuring the Agent in the Inspector

  1. Select Player1 in the Hierarchy.
  2. In the PaddleAgent component, assign the Ball object and the BallController component to their respective slots.
  3. Add a Behavior Parameters component to Player1:
    • Behavior Name: PaddleAI
    • Vector Observation > Space Size: 5 (matches the 5 observations we collected).
    • Actions > Discrete Branches: 1
    • Branch 0 Size: 3 (Stay, Up, Down).
  4. Add a Decision Requester component to Player1. Leave the Decision Period at 5 (this means the AI makes a choice every 5 frames, which prevents jittering).

Phase 5: Training the Neural Network

  1. In your Unity project folder (where Assets is located), create a new file named paddle_config.yaml.
  2. Paste the following configuration, which tells the Python backend how to optimize the network:
behaviors:
  PaddleAI:
    trainer_type: ppo
    hyperparameters:
      batch_size: 128
      buffer_size: 2048
      learning_rate: 0.0003
      beta: 0.005
      epsilon: 0.2
      lambd: 0.95
      num_epoch: 3
      learning_rate_schedule: linear
    network_settings:
      normalize: false
      hidden_units: 128
      num_layers: 2
    reward_signals:
      extrinsic:
        gamma: 0.99
        strength: 1.0
    max_steps: 1000000
    time_horizon: 64
    summary_freq: 10000

Go back to your Anaconda Prompt, ensure your mlagents environment is active, and navigate to the directory containing your .yaml file.

Run the training command:

mlagents-learn paddle_config.yaml --run-id=Paddle_v1
  1. The terminal will ask you to press Play in the Unity Editor. Go to Unity and press Play.
  2. You will see the paddle start acting erratically. Over thousands of episodes, the reinforcement learning algorithm will update the neural weights, and the paddle will learn to flawlessly track and hit the ball.

Phase 6: Injecting the “Brain”

Once training finishes (or if you stop it manually in the terminal by pressing Ctrl + C after the agent looks smart enough), ML-Agents will generate a neural network file.

  1. In your project folder, navigate to results/Paddle_v1/PaddleAI.onnx.
  2. Drag and drop this .onnx file into your Unity Assets folder.
  3. Select Player1 in the Unity Hierarchy.
  4. In the Behavior Parameters component, drag your newly imported .onnx file into the Model slot.
  5. Press Play in Unity. Python is no longer needed; your AI paddle is now running inference locally directly inside your game engine.

to resume your training run:

mlagents-learn paddle_config.yaml --run-id=Paddle_v1 --resume
Resume Training Press the Play Button on Unity
Resume Training Press the Play Button on Unity