Drawball - Unity 6 - C# - Solo Developer thumbnail

Drawball

Unity 6 C# Solo Developer

Drawball

Unity 6 C# Desarrollador en Solitario

About

Drawball was a solo project built in under 8 days for the Inbound Shovel Jam (Theme: 'Just Get Started'). It placed in the top 9% out of 1,144 entries. The core mechanic limits player control entirely to drawing physical lines on the screen to guide a ball through obstacles, naturally evolving into a fast-paced speedrunning platformer.

Project Info

  • Role: Solo Developer
  • Team Size: 1
  • Engine: Unity

Acerca de

Drawball fue un proyecto en solitario desarrollado en menos de 8 días para la Inbound Shovel Jam (Tema: 'Just Get Started'). Quedó en el 9% superior de las 1.144 entradas. La mecánica principal limita el control del jugador únicamente a dibujar líneas físicas en la pantalla para guiar una bola a través de obstáculos, evolucionando naturalmente hacia un juego de plataformas de velocidad rápida.

Información

  • Rol: Desarrollador en Solitario
  • Equipo: 1
  • Motor: Unity

Introduction

The core mechanic is simple: the only way to control the character is by drawing physical ink lines to guide, bounce, and protect them through obstacles. Once path optimization and getting the best time became the fun part, it naturally evolved into a time-based speedrunner.


Live Drawing Physics

Drawing and physics had to stay in sync without either one blocking the other, especially at high camera speeds.

I built DrawLine2D to decouple the visual line updates from the physics calculations. Visuals update in LateUpdate using Catmull-Rom spline smoothing to keep the ink looking fluid, while physics update in FixedUpdate by dynamically generating an EdgeCollider2D. The player can swap between "Standard" and "Bouncy" ink types, each with their own PhysicsMaterial2D and ink consumption costs.

Live Drawing Action

Live Drawing: Visuals dynamically smoothed using Catmull-Rom splines while physics generate EdgeCollider2Ds.

private List<Vector3> GenerateSmoothedPoints(List<Vector2> points)
{
    if (points.Count < 2) return new List<Vector3>();
    var res = new List<Vector3>();
    var pts = new Vector2[points.Count + 2];
    points.CopyTo(pts, 1);
    pts[0] = points[0];
    pts[^1] = points[^2];
    
    for (int i = 1; i < pts.Length - 2; i++)
        for (int j = 0; j < smoothSteps; j++)
            res.Add(CatmullRom(pts[i - 1], pts[i], pts[i + 1], pts[i + 2], j / (float)smoothSteps));
            
    res.Add(points[^1]);
    return res;
}

Spline Smoothing: Uses Catmull-Rom algorithms to smooth the raw input points into a fluid visual curve.


Procedural "Drawn-In" Level Geometry

Because the doodle aesthetic meant the level geometry itself needed to look hand-drawn, I made it animate in as if it were being sketched in real-time when the level starts.

I engineered LevelDrawer, which uses the Clipper2Lib library to perform boolean unions on multiple overlapping 2D colliders (DrawableLevelPiece). It merges the shapes, extracts the outer contour paths, and applies Chaikin smoothing. Finally, a coroutine animates a LineRenderer along these paths over time, creating the visual effect of the level boundaries being sketched in real-time.

Level Drawing Effect

Drawn-In Geometry: Overlapping 2D colliders are merged using Clipper2Lib and their contours are animated over time.

// Snippet from LevelDrawer: Merging colliders and applying Chaikin smoothing
Paths64 solution = new Paths64();
clipper.AddSubject(subject);
clipper.Execute(ClipType.Union, FillRule.NonZero, solution);

List<List<Vector2>> finalPaths = new List<List<Vector2>>();
foreach (var path in solution)
{
    List<Vector2> finalPath = new List<Vector2>();
    foreach (var p in path)
        finalPath.Add(new Vector2(p.X / ClipperScale, p.Y / ClipperScale));
        
    finalPaths.Add(finalPath);
}

// Applying Chaikin smoothing to the merged contours
for (int i = 0; i < smoothingIterations; i++) 
{ 
    animationPath = ChaikinSmooth(animationPath); 
}

Boolean Operations: Merges intersecting 2D shapes using Clipper2Lib and smooths the final contour via Chaikin iterations.


LootLocker Leaderboards

Since speedrunning became the core loop, I needed a robust backend to track completion times. I integrated the LootLocker SDK to handle guest sessions and score submissions. The DisplayLeaderboard system automatically queries leaderboards dynamically generated per-level (level_1_times, level_2_times), paginates the data, and formats the raw millisecond scores into clean mm:ss.fff readouts.

LootLocker Leaderboards

Leaderboards: Live server integration formatting millisecond precision times.

To prevent fake times from being submitted via network manipulation, I implemented a client-sided ScoreValidator. It generates a SHA256 hash by combining the raw score with a secret Salt phrase and submits it into the LootLocker metadata. When fetching the leaderboard, the client recalculates the hash and, if it doesn't match, hides the score from the UI, effectively voiding cheating attempts.

// ScoreValidator.cs: SHA256 Anti-Cheat Validation
private const string SecretKey = "[REDACTED_SECRET_KEY]";

public static string GenerateChecksum(string rawScoreString)
{
    string rawData = SecretKey + rawScoreString;
    using (SHA256 sha256 = SHA256.Create())
    {
        byte[] bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(rawData));
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < bytes.Length; i++)
            builder.Append(bytes[i].ToString("x2"));
        return builder.ToString();
    }
}

Metadata Checksum: Authenticates fetched scores by recalculating the hash on the client with a hidden Salt, silently ignoring scores submitted via illegitimate API requests.

What I Learned

This project was a masterclass in pivoting and finding the core "fun" of a game. Halfway through the jam, I scrapped my original idea—a massively over-scoped Unreal Engine 5 god-game—after realizing UE5 lacked web export, which would severely hurt the game's reach. Making the difficult call to pivot paid off immensely.

Once I transitioned to this physics-drawing concept, everything clicked. I focused on making the drawing mechanics satisfying, combining them with a frictionless character and speed pickups. The game naturally evolved into a speedrunner, which dictated the rest of the feature set: leaderboards, quick-reset buttons, and a zoomed-out camera to handle the speed. Finding the core loop also made level design incredibly intuitive; I learned to structure levels by starting slow, ramping up the velocity, and culminating in a lightning-fast reflex skill-check finale.

Introducción

La mecánica principal es sencilla: la única forma de controlar al personaje es dibujando líneas de tinta físicas para guiarlo, hacerlo rebotar y protegerlo a través de obstáculos. Una vez que la optimización de la ruta y conseguir el mejor tiempo se convirtió en la parte divertida, evolucionó naturalmente hacia un speedrunner basado en el tiempo.


Físicas de Dibujo en Tiempo Real

El dibujo y las físicas tenían que mantenerse sincronizados sin que ninguno de los dos bloqueara al otro, especialmente a altas velocidades de cámara.

Construí DrawLine2D para desacoplar las actualizaciones visuales de las líneas de los cálculos físicos. Los elementos visuales se actualizan en LateUpdate utilizando el suavizado de splines Catmull-Rom para que la tinta se vea fluida, mientras que las físicas se actualiza en FixedUpdate generando dinámicamente un EdgeCollider2D. El jugador puede alternar entre tipos de tinta "Standard" y "Bouncy", cada uno con sus propios PhysicsMaterial2D y costes de consumo de tinta.

Acción de Dibujo en Vivo

Dibujo en Vivo: Elementos visuales suavizados dinámicamente usando splines Catmull-Rom mientras la física genera EdgeCollider2Ds.

private List<Vector3> GenerateSmoothedPoints(List<Vector2> points)
{
    if (points.Count < 2) return new List<Vector3>();
    var res = new List<Vector3>();
    var pts = new Vector2[points.Count + 2];
    points.CopyTo(pts, 1);
    pts[0] = points[0];
    pts[^1] = points[^2];
    
    for (int i = 1; i < pts.Length - 2; i++)
        for (int j = 0; j < smoothSteps; j++)
            res.Add(CatmullRom(pts[i - 1], pts[i], pts[i + 1], pts[i + 2], j / (float)smoothSteps));
            
    res.Add(points[^1]);
    return res;
}

Suavizado de Splines: Utiliza algoritmos Catmull-Rom para suavizar los puntos de entrada brutos en una curva visual fluida.


Geometría de Nivel "Dibujada" Proceduralmente

La estética de "garabato" significaba que la propia geometría del nivel debía parecer dibujado a mano, así que hice que se animara como si estuviera siendo esbozado en tiempo real al comienzo del nivel.

Diseñé LevelDrawer, que utiliza la librería Clipper2Lib para realizar uniones booleanas en múltiples colliders 2D superpuestos (DrawableLevelPiece). Fusiona las formas, extrae las rutas de contorno exteriores y aplica el suavizado Chaikin. Finalmente, una corrutina anima un LineRenderer a lo largo de estas rutas a lo largo del tiempo, creando el efecto visual de que los límites del nivel se dibujan en tiempo real.

Efecto de Dibujo de Nivel

Geometría Dibujada: Los colliders 2D superpuestos se fusionan usando Clipper2Lib y sus contornos se animan con el tiempo.

// Snippet from LevelDrawer: Merging colliders and applying Chaikin smoothing
Paths64 solution = new Paths64();
clipper.AddSubject(subject);
clipper.Execute(ClipType.Union, FillRule.NonZero, solution);

List<List<Vector2>> finalPaths = new List<List<Vector2>>();
foreach (var path in solution)
{
    List<Vector2> finalPath = new List<Vector2>();
    foreach (var p in path)
        finalPath.Add(new Vector2(p.X / ClipperScale, p.Y / ClipperScale));
        
    finalPaths.Add(finalPath);
}

// Applying Chaikin smoothing to the merged contours
for (int i = 0; i < smoothingIterations; i++) 
{ 
    animationPath = ChaikinSmooth(animationPath); 
}

Operaciones Booleanas: Fusiona formas 2D que se intersectan utilizando Clipper2Lib y suaviza el contorno final mediante iteraciones Chaikin.


Clasificaciones de LootLocker

Dado que el speedrunning se convirtió en el bucle principal, necesitaba un sistema de tabla de clasificaciones. Integré el SDK de LootLocker para gestionar sesiones de invitados y envíos de puntuaciones. El sistema DisplayLeaderboard consulta automáticamente las tablas de clasificación generadas dinámicamente por nivel (level_1_times, level_2_times), pagina los datos y formatea las puntuaciones en milisegundos brutos en lecturas limpias mm:ss.fff.

Clasificaciones de LootLocker

Clasificaciones: Integración de servidor en vivo que formatea tiempos con precisión de milisegundos.

Para evitar que se envíen tiempos falsos mediante solicitudes de red manipuladas, implementé un ScoreValidator del lado del cliente. Este sistema genera un hash SHA256 combinando la puntuación bruta con una frase secreta (Salt) y lo envía en los metadatos de LootLocker. Al recuperar la clasificación, el cliente vuelve a calcular el hash y, si no coincide, oculta la puntuación de la interfaz de usuario, invalidando efectivamente los intentos de trampa.

// ScoreValidator.cs: SHA256 Anti-Cheat Validation
private const string SecretKey = "[REDACTED_SECRET_KEY]";

public static string GenerateChecksum(string rawScoreString)
{
    string rawData = SecretKey + rawScoreString;
    using (SHA256 sha256 = SHA256.Create())
    {
        byte[] bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(rawData));
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < bytes.Length; i++)
            builder.Append(bytes[i].ToString("x2"));
        return builder.ToString();
    }
}

Suma de Verificación de Metadatos: Autentica las puntuaciones recuperadas recalculando el hash en el cliente con una Salt oculta, ignorando así las puntuaciones enviadas a través de API ilegítimas.

Lo Que Aprendí

Este proyecto fue una clase magistral sobre pivotar y encontrar la "diversión" central de un juego. A mitad de la jam, descarté mi idea original (un god-game en Unreal Engine 5 con un alcance desproporcionado) tras darme cuenta de que UE5 carecía de exportación web, lo que limitaría drásticamente la audiencia del juego. Tomar la difícil decisión de pivotar valió completamente la pena.

Una vez que transicioné a este concepto de dibujo y físicas, todo encajó. Me centré en hacer que las mecánicas de dibujo fueran satisfactorias y las combiné con un personaje sin fricción y potenciadores de velocidad. El juego evolucionó naturalmente hacia un speedrunner, lo que dictó el resto de las características: tablas de clasificación, reinicios rápidos y una cámara más alejada para manejar la velocidad. Encontrar este núcleo también hizo que el diseño de niveles fuera increíblemente intuitivo; aprendí a estructurar los niveles comenzando lento, aumentando drásticamente la velocidad y culminando en un final rapidísimo que pone a prueba los reflejos.

Built with 11ty + Decap CMS