Notes on the Making Of Ping Pong Color

- Game Design

Above is the trailer I've made for my first (proper) game, Ping Pong Color. After spending a few years by learning Unity and C# with online tutorials I've decided to embark in this small project that took about 5-6 weeks of development.


As mentioned, this is my first game, plus, the purpose of this article is for me to document and write about my experience developing this game. You might witness some code gore and other atrocities, if you get something useful from this article that's awesome, if you want to share with me a better way to make something shown in this article (as that would be useful for future projects), feel free to send me an e-mail or DM, thanks!

On Gameplay

Ping Pong Color is a simple arcade game where your objective is to advance in an infinite level while you destroy everything in your path for a higher score. The colors are in sync with the music.

Ping Pong Color Gameplay

If the ball and the obstacle are the same color when they collide, you destroy the obstacle, if not, you lose.

Input Systems


On the PC version, you have a circle (called 'Paddle') that you move around with the mouse to hit the ball.

This paddle has a trigger attached to it to detect when it hits the ball, then, based on the velocity and direction of the paddle, physics are applied to the ball.

Example of PC Input A highly compressed GIF showing the input system on PC

Initially, the paddle was supposed to be a simple collider with a bounce material attached to it, this however became an issue as it meant you can literally 'push' the ball around the screen, which wasn't desired.


The PC input system would've made the game a hell to play on mobile devices for a multitude of reasons:

Example of Mobile Input

For these main reasons, another input system was designed exclusively for mobile devices (and it's more fun than the PC input)

On mobile you just use a finger (quite possible your thumb, if you're grabbing the phone with two hands) anywhere on the screen to swipe on the direction you want to hit the ball. This also considers the speed at which you swipe to apply the proper physics to the ball.

On Level Generation

Ping Pong Color generates an infinite level when you start playing in a simple manner:


As of time of writing this, there are 100 hand-made blocks, lots of them 'small' variations of others. These blocks are by themselves built by objects such as 'Moving Spikes', 'Black Holes', 'Pick-ups'

Examples of pre-built blocks Examples of some blocks used in the level generation

Bounding Box Generation

Bounding Boxes of the first batch Above is an example of the bounding boxes generated by the script below

// added to every block
public class BoundingBox : MonoBehaviour {
    public Bounds bounds;
    [HideInInspector] public Vector2 offsetToMin;

    List<Collider2D> allColliders = new List<Collider2D>();
    List<Transform> allTransforms = new List<Transform>();

    float boxSizeX;

    void Awake() => GetBoundingBox();

    public void GetBoundingBox() {

        // An offset from the origin of the block to the min point of the bounds
        // Used in the level generator to avoid overlapping when placing a new block
        offsetToMin = transform.position - bounds.min;

    // We add all the colliders of the block recursively to allColliders
    void GetAllColliders(Transform trans) {
        foreach (Transform t in trans) {
            Collider2D collider = t.GetComponent<Collider2D>();
            if (collider != null)


    // We add all the transforms of the block recursively to allTransforms
    void GetAllTransforms(Transform trans) {
        foreach(Transform t in trans) {

    void CalculateBoundingBox() {
        // we make use of the provided Bounds() class by Unity
        bounds = new Bounds(transform.position,;

        // We add all the transforms to the bounds
        foreach (Transform t in allTransforms)

        // We add the min and max positions of the colliders to the bounds
        foreach (Collider2D col in allColliders) {
            bounds.Encapsulate(col.bounds.extents +;
            bounds.Encapsulate(-col.bounds.extents +;

        // we only care about the x-size of the bounding box (as the level is horizontal)
        boxSizeX = bounds.size.x;

Sometimes, empty game objects are used to add a manual margin to specific blocks

A big lesson was learned when trying to get the level generation to work: If your code is getting messier and messier and still doesn't work, you're probably missing something essential (in my case, some basic vector addition and subtraction)

On Visuals

The art style is just basic shapes. Cubes, triangles and circles (I just really like Suprematism.)

Most of the 'interesting' visual stuff on the game comes from:


Making the UI in Unity was, naturally, painful. Hopefully the new UI Builder (that looks a lot nicer) will relieve that pain a bit in future projects.

Main Menu of Ping Pong Color The most complex shape in all of Ping Pong Color might be the cog wheel in the main menu (unless we count text?).

The main menu includes:

Settings Menu of Ping Pong Color This is the settings menu for PC

It doesn't include much out of the ordinary, some stuff:

TMP_TextInfo textInfo = colorText.textInfo;
Color32[] vertexColors = textInfo.meshInfo[0].colors32;
Color prevColor = new Color(0, 0, 0, 0);

for(int i = 0; i < textInfo.characterCount; i++) {
    int vertexIndex = textInfo.characterInfo[i].vertexIndex;

    Color randomColor;
    do randomColor = ColorManager.Instance.GetRandomColor().color;
    while (randomColor == prevColor);
    prevColor = randomColor;

    vertexColors[vertexIndex + 0] = randomColor;
    vertexColors[vertexIndex + 1] = randomColor;
    vertexColors[vertexIndex + 2] = randomColor;
    vertexColors[vertexIndex + 3] = randomColor;


Leaderboard of Ping Pong Color

The Leaderboard was implemented with a Scroll Rect component and was heavily modified so it looks like I wanted it to.

The Leaderboard code itself is based mostly on this tutorial, modified to fit the game.

Palette System

Palettes are implemented with ScriptableObjects, here's an example of how they look like in the Inspector:

Palette Scriptable Object

The Colors array is an array of a custom struct that includes a Color and a string to identify the color with a name. This string is used for comparisons in other places of the game.

Color Blind Palette

I've made a color-blind friendly palette (don't trust me on this, haven't actually tested it with color-blind people) out of curiosity.

I've done a tiny bit of research and found this excellent GDC talk and this amazing resource that lets you upload an image and see how someone with certain type of color blindness would see it.

As far as I understand, deuteranomaly is the most common type, so I mainly focused on the palette to work for that type (it works better and worse in other types, feel free to upload a screenshot of the game to the color-blind simulator and see!)

(If you're not color-blind) this is what you would see normally: No Deuteranomaly

And this is what someone with deuteranomaly would see instead: Deuteranomaly

As a rule of thumb: If you can distinguish the colors in black and white, you're probably good to go!

The Black Hole

I took a shader code somewhere from the internet and I fiddle with it a bit.

This is the shader code used in my game:

sampler2D _MainTex; // texture from the camera
uniform float2 _Position; // position of the black hole in screen space
uniform float _Radius; // radius of the black hole

fixed4 frag (v2f i) : SV_Target{
    float ratio = _ScreenParams.x / _ScreenParams.y; // gets screen ratio
    fixed2 pos = _Position / _ScreenParams; // idk
    float2 offset = pos - i.uv; // don't remember

    float rad = length (offset);
    float intensity = sin (_Time.y * 1.5);
    float deformation = 1 / pow (rad, abs(intensity)) * _Radius * intensity; // certainly idk

    offset.x *= ratio;
    offset = i.uv + normalize (offset) * 0.001; // absolutely no  idea
    offset = offset * (1 - deformation);

    return tex2D (_MainTex, offset);

This is the code in charge of sending the data to the shader:

public Gravitor gravitor;
public Material mat;
float radius;

void Start() { radius = gravitor.range; }

void OnRenderImage(RenderTexture source, RenderTexture destination) {
    if (gravitor != null) {
        mat.SetVector("_Position", gravitor.screenPos);
        mat.SetFloat("_Radius", radius * gravitor.effectIntensity);

        Graphics.Blit(source, destination, mat);
    } else

In the Gravitor (A.K.A Black Hole) game object, I add the code above when a Gravitor is created.

void Awake() {
    blackHole = Camera.main.gameObject.AddComponent<BlackHoleEffect>();
    blackHole.gravitor = this;
    blackHole.mat = blackHoleMat;

void Update() {
    // This code disables the black hole if it's outside the camera's view
    screenPos = mainCamera.WorldToScreenPoint(transform.position);
    blackHole.enabled = !(Mathf.Abs(screenPos.x) > Screen.width || Mathf.Abs(screenPos.y) > Screen.height);

Because the black hole shader acts as a post-processing effect, the UI gets distorted by it!

UI being affected by Black Hole

On Sound

What I'm certainly not is a sound expert. My knowledge on making music is not much, and I've tried to quickly learn FMOD and Reaper in a few days for this project.

I've chosen FMOD over Wwise because it looks better

Reaper Song Project

Above is what the music from Ping Pong Color looks like in Reaper, it's quite cluttery, but I'm happy with the result. It's divided into different sections that correspond to a Game State.

I could've reduced the amount of sections in the Reaper project if I knew FMOD, but I "learned" FMOD after the song was done :(

Inside the Unity project I have an Enum that keeps track of the current state of the game, this is linked to the GameState parameter in the FMOD project. When the state changes in-game, FMOD takes care of transitioning to the proper section of the song.

On Mobile Optimization

I haven't studied a lot about optimization and profiling in Unity, however I've found some ways to optimize the game to work properly on mobile devices, specially low-end devices.

Fixed DPI

Fixed DPI

Under the Resolution and Presentation settings of the Project Settings you can find the option to force the rendering to a specific DPI, meaning that you will (probably) not render at full resolution unnecessarily. In this case I went with 280 Dots per Inch. This improved performance on mobile by a lot.

Performance Mode

Another thing I've did was to add an extra option to the settings menu.

This toggle is linked to the script below, which disables all post-processing effects except for the Lens Distortion.

public void SetPerformance(bool performance) {
    isPerformance = performance; // why TF did I do that???

    PostProcessProfile profile = postFxVolume.sharedProfile;
    foreach (PostProcessEffectSettings setting in profile.settings)
        if (setting.GetType().Equals(typeof(LensDistortion)))
   = !isPerformance;

On Publishing

I haven't put much effort on publishing or in marketing this game. Probably this section will be updated if I decide to publish on Steam, iOS, or Android. Right now the game is available for free on

Ping Pong Color Gameplay

The summary is that, even without this game being a 'success' (as in, making money from it), what I've got from making it was extremely valuable.

  1. It made me feel accomplished, after years of learning game development without actually making a game of my own.
  2. I learned a lot of stuff from it, stuff that generally tutorials don't or can't teach you, mainly problem solving (lots of problem solving...)
  3. It makes for a great portfolio piece that might potentially get you a job in the industry.
  4. It was fun doing it :)

Ping Pong Color Gameplay

My suggestion, if you already know the basics (be it, Unity, Unreal, Godot, some basic vector/trigonometry math) just make a little project on your own. It might take a week, or a month, it might never get finished. But you will learn a lot from it.

Hope you enjoyed the article! For any comments, here's my email:

~ I'm currently working on my second game, wish me luck :)