Skip to content

Pong in Unity ECS

In this series, I take games and rebuild them using Unity’s Entity Component System (ECS)

For full source code of this project, checkout my github

Table of Contents

Overview

The classic game Pong consists of a ball and 2 paddles. One of the paddles is controlled by the player and the other, either a player or an AI controlled opponent.  The goal of the game is to use your paddle to reflect the ball back to your opponent.  Whenever the ball gets past a paddle, the other player earns a point.  The goal is to score more points than your opponent.

Entity Data

We’ll want an idenitifier for the paddles and the ball. The paddles move at a given speed, so we’ll need data to represent that as well.  We’ll also be using components from Unity’s DOTS physics framework.

[GenerateAuthoringComponent]
public struct PongPaddle : IComponentData { }

[GenerateAuthoringComponent]
public struct Ball : IComponentData { }

[GenerateAuthoringComponent]    
public struct AI : IComponentData { }

[GenerateAuthoringComponent]
public struct Speed : IComponentData
{
    public float value;
} 

Entity Prefabs

Player Paddle Movement

In order to move the paddle up and down, we’ll need a system that accepts input and modifies the position component of the paddle entity.

public class PongPaddleMovementSystem : SystemBase
{
    protected override void OnUpdate()
    {
        // grab input from the vertical axis
        float movement = Input.GetAxis("Vertical");
        
        float dt = Time.DeltaTime;
        
        Entities.WithAll<Player, PongPaddle>().ForEach((ref Translation translation, in Speed speed) => {                
            translation.Value.y +=  dt * movement * speed.value;
            translation.Value.y = math.clamp(translation.Value.y, -4, 4);
        }).Schedule();
    }
} 

AI Paddle Movement

There’ are many different ways to design a competitive AI in pong.  In this article we’ll take a simple approach of having the paddle’s y position follow the y position of the ball

public class PongAIPaddleMovementSystem : SystemBase
{
    protected override void OnUpdate()
    {
        float deltaTime = Time.DeltaTime;

        // get the ball's position
        float3 ballPos = EntityManager.CreateEntityQuery(typeof(Translation), typeof(Ball)).GetSingleton<Translation>().Value;
        
        Entities
            .WithAll<AI>()
            .ForEach((ref Translation translation, in Speed speed) => {
                float dist = math.abs(ballPos.y - translation.Value.y);
                float dir = math.select(-1, 1, ballPos.y > translation.Value.y);
                dir = math.select(0, dir, dist > 0.3f);
                translation.Value.y +=  speed.value * deltaTime * dir;
                translation.Value.y = math.clamp(translation.Value.y, -4, 4);
            }).Schedule();
    }
} 

Scoring

We can use the x position of the ball and paddles to determine scoring.  If the ball’s x position is less than the player’s, that means that AI scored.  If it’s greater than the AI’s position, the player scored.

public class ScoreCalculationSystem : SystemBase
{
    GameHandler m_handler;
    protected override void OnCreate()
    {
        base.OnCreate();
        m_handler = UnityEngine.GameObject.FindObjectOfType<GameHandler>();
    }

    protected override void OnUpdate()
    {
        // get the ball's position
        EntityQuery query = EntityManager.CreateEntityQuery(typeof(Translation), typeof(Ball));
        if (query.CalculateEntityCount() != 1)
        {
            return;
        }
        float3 ballPos = EntityManager.CreateEntityQuery(typeof(Translation), typeof(Ball)).GetSingleton<Translation>().Value;

        query = EntityManager.CreateEntityQuery(typeof(Translation), typeof(Player));
        if (query.CalculateEntityCount() != 1)
        {
            return;
        }
        float3 playerPos = query.GetSingleton<Translation>().Value;

        query = EntityManager.CreateEntityQuery(typeof(Translation), typeof(AI));
        if (query.CalculateEntityCount() != 1)
        {
            return;
        }
        float3 aiPos = query.GetSingleton<Translation>().Value;

        if (ballPos.x <= playerPos.x)
        {
            // ai scored
            m_handler.PointEarned(1);
        }
        else if (ballPos.x >= aiPos.x)
        {
            // player scored
            m_handler.PointEarned(0);
        }
    }
} 

Conclusion and Things to Try

This example project uses very basic physics for reflecting the ball, but you could improve this by adding a force to the ball based on where the ball hit on the paddle and the movement direction of the paddle when the hit occurs.

Here are some other ideas:

  1. Have both the player and AI control 2 paddles
  2. Power ups that can be collected when hit by the ball (last player to hit the ball gets the power up)
  3.  Improve the AI by calculating the trajectory of the ball