I-FRAME - Unity 6 - C# - Solo Developer - Roguelite Bullet Hell thumbnail

I-FRAME

Unity 6 C# Solo Developer Roguelite Bullet Hell

I-FRAME

Unity 6 C# Solo Developer Bullet Hell Roguelite

About

I-FRAME is a top down 2D roguelite bullet hell built solo in Unity for GameNameJam 2026, whose theme was Invincible. It is the result of a mid jam pivot: the original, far more ambitious 3D idea was abandoned halfway through the two week jam, and I-FRAME was built in the second half. The whole game hangs off one mechanic: the dodge roll is simultaneously your only attack, your only defensive tool and your invincibility window. Rolling generates heat and overheating stuns you, so the mechanic balances itself instead of relying on a flat cooldown. Around that sit a budget driven wave director, a three card upgrade draft between waves, a bitwise combo multiplier, and online leaderboards that had to survive scores far larger than a 32 bit integer.

Project Info

  • Role: Solo Developer
  • Team Size: 1
  • Time frame: April 2026, two week jam, built in the second half
  • Engine: Unity 6

Acerca de

I-FRAME es un bullet hell roguelite en 2D cenital hecho en solitario en Unity para la GameNameJam 2026, cuyo tema era Invincible. Es el resultado de un cambio de rumbo a mitad de jam: la idea original en 3D, mucho más ambiciosa, se abandonó a mitad de las dos semanas, y I-FRAME se construyó en la segunda mitad. Todo el juego gira en torno a una sola mecánica: la voltereta es a la vez tu único ataque, tu única herramienta defensiva y tu ventana de invulnerabilidad. Rodar genera calor y sobrecalentarse te aturde, así que la mecánica se equilibra sola en vez de depender de un cooldown plano. Alrededor de eso hay un director de oleadas por presupuesto, una elección de tres cartas de mejora entre oleadas, un multiplicador de combo por desplazamiento de bits, y rankings online que tuvieron que sobrevivir a puntuaciones mucho mayores que un entero de 32 bits.

Información

  • Rol: Solo Developer
  • Equipo: 1
  • Duración: Abril de 2026, jam de dos semanas, hecho en la segunda mitad
  • Motor: Unity 6

About the project

I-FRAME is a top down 2D roguelite bullet hell built solo for GameNameJam 2026, in the second half of a two week jam. The idea is a single one: the dodge roll is everything. It's your only attack, your only defence and your invincibility window, all in the same action.

That creates an immediate design problem: if rolling makes you invincible and kills things, what stops you rolling forever? The answer wasn't a longer cooldown, it was turning it into a resource.

Core combat

Wave 4 at a 128x multiplier. Bullets, corpses and score popups all on screen at once.


Two weeks, and a pivot halfway

The jam ran about two weeks and the theme was Invincible. I started straight away, on something completely different.

The original idea inverted the theme: you played the monster, and the RPG hero who turned up to kill you was the invincible one. He had plot armor, so every hit you landed was negated and threw up floating text reading "PLOT ARMOR", "BLOCKED" or "NOPE". At that stage a narrator served as the antagonist voice, escalating through moods (Calm, Confused, Annoyed, Panicked, Desperate) the longer you refused to die on schedule.

The real problem wasn't that idea, it was that it never sat still. Every pass made it bigger. The narrator eventually got dropped, and what grew in its place was cutscenes, multiple handcrafted levels with exploration and branching paths, dialogue, and a story to follow. Each shift was more ambitious than the last, and all of it leaned on 3D art and authored content that had to exist before any of it could land.

By the halfway mark the honest answer was that it wasn't feasible, and not because the programming was hard. The concept only works if the art and the content are there to carry it, and a two week jam was never going to produce that. So I cut it.

I-FRAME is what came out of the second half: the same theme read the other way round. Instead of your enemy being invincible, your invincibility is the entire game, compressed into the handful of frames inside a dodge roll. The art is almost entirely Kenney assets, which is a large part of why the pivot worked, because it took art off the critical path and left the week for mechanics.

The roll

The player is an explicit state machine (Free, Rolling, Overheated, Dead). The important part is that both the invincibility flag and the roll's damage collider are driven by the same Rolling state: the offensive and defensive halves of the mechanic are literally the same window of time.

public enum PlayerState { Free, Rolling, Overheated, Dead }

public bool IsInvincible => State == PlayerState.Rolling && !BerserkerMode;

For dodge based combat to feel responsive, there's an input buffer: press roll slightly before the cooldown expires and it's held, then fires the moment it's available instead of being swallowed.

Rolling through bullets

Turret spirals forcing constant movement. The roll passes straight through the bullets that would otherwise kill you.


Heat, the cost of the mechanic

Heat exists because of a playtesting problem. Before it, the roll only had a cooldown, and the optimal play was simply to roll the instant it came back up. Since rolling grants invincibility, rolling on cooldown made you effectively unkillable.

It was still fun, in fairness. Racking up points and watching the multiplier climb is enjoyable on its own. But it was mindless, and it was easy: there was no moment to moment decision to make, just a button to press whenever it lit up.

Heat is what fixed that. Every roll adds heat. Heat decays on its own while you aren't rolling, and hitting the cap overheats you: the roll is cut short, velocity is zeroed and you're stunned. Rolling is still always the right answer, but now you have to think about which rolls are worth spending, and rolling thoughtlessly is what kills you.

private void AddRollHeat()
{
    if (BerserkerMode) return;

    CurrentHeat += heatPerRoll;
    OnHeatChanged?.Invoke(HeatNormalized);

    if (CurrentHeat >= maxHeat)
    {
        EndRoll();
        State = PlayerState.Overheated;
        _overheatTimer = overheatStunDuration;
        _rb.linearVelocity = Vector2.zero;
        OnOverheat?.Invoke();
    }
}

The controller exposes heat already normalized, so the UI bar reads straight off the player and there's no duplicated state to fall out of sync.

Overheating

The heat bar (right) fills as rolls are chained until it caps out and the OVERHEATED stun fires, bottom left.


A combo built out of bit shifts

The multiplier isn't a counter, it's tiers: the combo is 1 shifted left by the current tier, giving 1x, 2x, 4x, 8x, 16x. On top of that sits a bar that drains continuously, and the drain rate scales with the tier, so holding a high multiplier gets progressively harder.

public int ComboTier { get; private set; } = 0;
public int Combo     => 1 << ComboTier;   // 1, 2, 4, 8, 16...
float currentDecay = baseDecayRate + (ComboTier * decayMultiplierScaling);
ComboFill -= currentDecay * Time.deltaTime;

if (ComboFill <= 0f)
{
    if (fullResetOnDecay)
    {
        ComboFill = 0f;
        SetComboTier(0);
    }
    else if (ComboTier > 0)
    {
        ComboFill += 1f;              // drop one tier with a full bar
        SetComboTier(ComboTier - 1);
    }
}

Losing the combo is configurable between a hard reset to 1x and dropping a single tier, so the punishment curve could be retuned mid jam without touching the scoring code.

Combo multiplier climbing

Wave 16 sitting at a 1024x multiplier, with the run total past 12 million.


The wave director

There are no hand authored waves. Each wave computes a credit budget that grows linearly with the wave number and switches to a quadratic ramp past a configurable wave, so difficulty accelerates in the back half of a run.

private int CalculateBudget(int wave)
{
    int budget = baseBudget + (wave * budgetPerWave);

    if (wave >= quadraticKickInWave)
    {
        int delta = wave - quadraticKickInWave;
        budget += Mathf.RoundToInt(quadraticCoefficient * delta * delta);
    }

    return budget;
}

That budget is then spent in full through a weighted random pass over ScriptableObject enemy spawn cards, and the resulting roster doesn't arrive all at once: it's drip fed into the arena through a concurrency cap acting as a funnel. The pressure comes from a sustained mix of enemies rather than from dumping an entire wave on the player.

A late wave

Wave 14. The director's budget has grown enough to keep a constant mixed crowd in the arena.


Three enemies, three answers

Each archetype forces a different use of the roll:

  • Charger: wanders, telegraphs with a pause and a colour shift, then dashes. You roll through or around the dash, and roll into it during its vulnerable recovery to kill it.
  • Shooter: orbits the player at a preferred radius and fires patterns. Here, rolling through bullets is the only way out.
  • Turret: stationary, rotates slowly and fires multi arm bullet spirals. Pure area denial, it exists to stop you standing still.

Every bullet comes from a pool, so bullet heavy waves don't generate garbage collection pressure from constant instantiation and destruction.

Mixed enemy crowd

Chargers, shooters and their bullets sharing the arena, which is what the concurrency funnel is pacing.


Corpses stay part of the game

When an enemy dies it doesn't vanish. It leaves a physics corpse that slides with friction and collides with walls. Rolling into it explodes it for score, combo fill and a chance to drop health. It turns a cleared area into something that still rewards movement rather than a pile of particles.


The upgrades

Clearing a wave offers three cards drawn from a ScriptableObject pool, with per upgrade stack limits. There are 17 in total, split between stat modifiers (roll speed, duration and cooldown, hitbox size, max HP, heat capacity and dissipation) and ones that change the mechanic outright:

  • Vampire: every kill heals you.
  • Aftershock: every kill deals area damage around it.
  • Ricochet Roll: the roll chains toward a nearby target.
  • Mouse Guide Roll: you can steer the roll toward the cursor mid dodge.
  • Orbital Shields: shields that orbit you, carry their own durability, and recoil inward on impact before springing back out.

And Berserker's Curse, which strips your invincibility frames entirely in exchange for power. In a game called I-FRAME.

The upgrade draft

The three card draft between waves: Momentum, Ricochet Roll and Guided Roll.


Scores too big for the leaderboard

This was the most interesting technical problem of the jam. With multipliers that double per tier, a good run's score runs past the range of a 32 bit integer, and the leaderboard backend (LootLocker) only accepts ints.

The fix was to separate the real score from the value it sorts by. The score is carried as a BigInteger, and an encoder maps it onto an order preserving int: below 10 million it uses the native value, and above that it switches to a magnitude format of digit count times 10,000,000 plus a five digit mantissa. That sorts correctly for numbers up to roughly 10^214.

public static int Encode(BigInteger score)
{
    if (score <= 0) return 0;

    string str = score.ToString();
    int length = str.Length;

    // Up to 9,999,999 the native value is used directly.
    if (length <= 7) return (int)score;

    // Above that: length * 10,000,000 + the first 5 digits.
    // At length 8 the smallest result is 80,000,000, just past the cutoff,
    // so the sequence keeps sorting perfectly.
    int mantissa = int.Parse(str.Substring(0, 5));
    return (length * 10000000) + mantissa;
}

The exact score isn't lost: it's packed as text alongside its checksum into the entry's metadata field, and reconstructed when the leaderboard is read back.

string trueScoreStr = score.ToString();
string checksum = ScoreValidator.GenerateChecksum(trueScoreStr);
string metadata = $"{trueScoreStr}|{checksum}";

int sortableMagnitude = MagnitudeEncoder.Encode(score);

LootLockerSDKManager.SubmitScore(playerID, sortableMagnitude, leaderboardKey,
    metadata, response => { /* ... */ });

The checksum is a SHA256 over the score combined with a secret salt, the same anti-cheat approach I'd built for Drawball. On fetch it's recalculated and compared, and entries that fail validation are flagged as unverified rather than dropped, so older scores without metadata still show up.

Global leaderboard
Game over screen

The global leaderboard and the end of a run. The top entry reads 115,885,778,841, which is why the score cannot be submitted as a plain 32 bit integer.


Game feel

In a game about a single verb, that verb has to feel good. Hitstop is a singleton that freezes timescale on kills, using realtime waits so it unfreezes correctly even at timescale zero. Overlapping calls extend the freeze rather than stacking, so rapid kills stay punchy without locking the game.

On top of that sit camera shake, dodge VFX, roll trails, particle bursts, sprite fragmentation and floating score numbers.

The detail I'm happiest with is SpriteJuice, which brings static single frame sprites to life using scale only, never position, so it never fights the physics simulation. And since scaling the object would scale its collider too, it automatically counter scales any Collider2D on the same object to preserve its true world space size.

The camera doesn't follow the player directly either: Cinemachine follows a virtual point interpolated between the player and the cursor, which gives it that feeling of leading toward where you're aiming, with scrollwheel zoom control.

Aftershock chain

An Aftershock chain detonating: each kill damages everything nearby, cascading into stacked score popups.


Editor tooling

Even against a jam clock, I spent time on custom editor tooling because it paid for itself: a wizard that generates the upgrade data assets and their UI automatically, another for setting up the UI juice, and an in game debug menu for forcing specific upgrades while testing, instead of having to reach the right wave to find out whether one works.


Shipping it late anyway

I didn't make the official jam deadline. Things went wrong late in the week and the submission went in incomplete. What I did do was keep working and publish a finished, considerably more complete version afterwards, rather than leaving the prototype where it fell.

It's the project that most clearly taught me the constraint was the point: one mechanic, pushed until it holds up an entire game.

Sobre el proyecto

I-FRAME es un bullet hell roguelite en 2D cenital hecho en solitario para la GameNameJam 2026, en la segunda mitad de una jam de dos semanas. La idea es una sola: la voltereta lo es todo. Es tu único ataque, tu única defensa y tu ventana de invulnerabilidad, todo en la misma acción.

Eso plantea un problema de diseño inmediato: si rodar te vuelve invulnerable y además mata, ¿qué impide que ruedes sin parar? La respuesta no fue un cooldown más largo, sino convertirlo en un recurso.

Combate principal

Oleada 4 con multiplicador de 128x. Balas, cadáveres y números de puntuación a la vez en pantalla.


Dos semanas, y un cambio de rumbo a mitad

La jam duraba unas dos semanas y el tema era Invincible. Empecé de inmediato, con algo completamente distinto.

La idea original le daba la vuelta al tema: jugabas como el monstruo, y el héroe de RPG que venía a matarte era el invencible. Tenía plot armor, así que cada golpe que le dabas se anulaba y salía un texto flotante que decía "PLOT ARMOR", "BLOCKED" o "NOPE". En esa fase había un narrador que hacía de voz antagonista, subiendo de humor (Calmado, Confundido, Molesto, Aterrado, Desesperado) cuanto más te negabas a morir cuando tocaba.

El problema de verdad no era esa idea, sino que no paraba quieta. Cada pasada la hacía más grande. El narrador acabó cayéndose, y en su lugar creció todo lo demás: cinemáticas, varios niveles hechos a mano con exploración y caminos alternativos, diálogos y una historia que seguir. Cada giro era más ambicioso que el anterior, y todo dependía de arte 3D y de contenido autoral que tenía que existir antes de que nada de aquello funcionase.

A mitad de jam la respuesta honesta era que no era viable, y no porque programarlo fuera difícil. El concepto solo funciona si el arte y el contenido están ahí para sostenerlo, y una jam de dos semanas no iba a dar para eso. Así que lo corté.

I-FRAME es lo que salió de la segunda mitad: el mismo tema leído al revés. En vez de que tu enemigo sea invencible, tu invulnerabilidad es el juego entero, comprimida en los pocos fotogramas que dura una voltereta. El arte es casi todo de Kenney, que es buena parte de por qué el cambio funcionó, porque sacó el arte del camino crítico y dejó la semana para mecánicas.

La voltereta

El jugador es una máquina de estados explícita (Free, Rolling, Overheated, Dead). La clave está en que tanto la bandera de invulnerabilidad como el collider de daño de la voltereta se controlan desde el mismo estado Rolling: la mitad ofensiva y la mitad defensiva de la mecánica son literalmente la misma ventana de tiempo.

public enum PlayerState { Free, Rolling, Overheated, Dead }

public bool IsInvincible => State == PlayerState.Rolling && !BerserkerMode;

Para que un combate basado en esquivar se sienta responsivo, hay un buffer de input: si pulsas la voltereta un poco antes de que termine el cooldown, se guarda y sale sola en cuanto está disponible, en vez de perderse.

Rodando entre balas

Espirales de torreta que obligan a no parar de moverse. La voltereta atraviesa las balas que si no te matarían.


El calor, el coste de la mecánica

El calor existe por un problema que salió probando el juego. Antes de que existiera, la voltereta solo tenía cooldown, y lo óptimo era rodar en cuanto volvía a estar disponible. Como rodar te da invulnerabilidad, rodar siempre que podías te hacía prácticamente inmortal.

Y seguía siendo divertido, todo sea dicho: acumular puntos y ver subir el multiplicador engancha por sí solo. Pero era un juego sin cabeza y fácil: no había ninguna decisión que tomar momento a momento, solo un botón que pulsar cada vez que se encendía.

El calor es lo que arregló eso. Cada voltereta añade calor. El calor baja solo mientras no ruedas, y si llegas al máximo te sobrecalientas: la voltereta se corta, la velocidad se pone a cero y quedas aturdido. Rodar sigue siendo siempre la respuesta correcta, pero ahora tienes que pensar qué volteretas merece la pena gastar, y rodar sin pensar es justo lo que te mata.

private void AddRollHeat()
{
    if (BerserkerMode) return;

    CurrentHeat += heatPerRoll;
    OnHeatChanged?.Invoke(HeatNormalized);

    if (CurrentHeat >= maxHeat)
    {
        EndRoll();
        State = PlayerState.Overheated;
        _overheatTimer = overheatStunDuration;
        _rb.linearVelocity = Vector2.zero;
        OnOverheat?.Invoke();
    }
}

El controlador expone el calor ya normalizado, así que la barra de la UI lee directamente del jugador y no hay estado duplicado que se pueda desincronizar.

Sobrecalentamiento

La barra de calor (derecha) se llena al encadenar volteretas hasta que llega al tope y salta el aturdimiento por sobrecalentamiento, abajo a la izquierda.


Combo por bits

El multiplicador no es un contador, son niveles: el combo es 1 desplazado a la izquierda por el nivel actual, lo que da 1x, 2x, 4x, 8x, 16x. Encima hay una barra que se vacía constantemente, y la velocidad de vaciado escala con el nivel, así que mantener un multiplicador alto es cada vez más difícil.

public int ComboTier { get; private set; } = 0;
public int Combo     => 1 << ComboTier;   // 1, 2, 4, 8, 16...
float currentDecay = baseDecayRate + (ComboTier * decayMultiplierScaling);
ComboFill -= currentDecay * Time.deltaTime;

if (ComboFill <= 0f)
{
    if (fullResetOnDecay)
    {
        ComboFill = 0f;
        SetComboTier(0);
    }
    else if (ComboTier > 0)
    {
        ComboFill += 1f;              // baja un nivel con la barra llena
        SetComboTier(ComboTier - 1);
    }
}

Perder el combo es configurable entre un reinicio total a 1x o bajar un solo nivel, para poder retocar la curva de castigo durante la jam sin tocar el código de puntuación.

El multiplicador subiendo

Oleada 16 con un multiplicador de 1024x y el total de la partida por encima de los 12 millones.


El director de oleadas

No hay oleadas escritas a mano. Cada oleada calcula un presupuesto de créditos que crece de forma lineal con el número de oleada y pasa a una rampa cuadrática a partir de una oleada configurable, para que la dificultad se dispare al final de la partida.

private int CalculateBudget(int wave)
{
    int budget = baseBudget + (wave * budgetPerWave);

    if (wave >= quadraticKickInWave)
    {
        int delta = wave - quadraticKickInWave;
        budget += Mathf.RoundToInt(quadraticCoefficient * delta * delta);
    }

    return budget;
}

Ese presupuesto se gasta entero en una pasada aleatoria ponderada sobre unas cartas de enemigo en ScriptableObject, y el resultado no entra de golpe: se va soltando en la arena a través de un límite de enemigos simultáneos que funciona como embudo. La presión viene de la mezcla sostenida de enemigos, no de que te tiren la oleada entera encima.

Una oleada avanzada

Oleada 14. El presupuesto del director ya da para mantener una mezcla constante de enemigos en la arena.


Tres enemigos, tres respuestas

Cada arquetipo obliga a usar la voltereta de una forma distinta:

  • Charger: deambula, se telegrafía con una pausa y un cambio de color, y embiste. Hay que rodar a través o alrededor de la embestida, y meterle la voltereta durante su fase de recuperación vulnerable.
  • Shooter: orbita al jugador a un radio preferido y dispara patrones. Aquí rodar a través de las balas es la única salida.
  • Turret: estático, gira despacio y lanza espirales de balas de varios brazos. Es negación de área pura, existe para que no te quedes quieto.

Todas las balas salen de un pool para que las oleadas con mucha bala no generen presión de recolección de basura por instanciar y destruir constantemente.

Mezcla de enemigos

Chargers, shooters y sus balas compartiendo arena, que es justo lo que va dosificando el embudo de concurrencia.


Los cadáveres siguen siendo jugables

Cuando un enemigo muere no desaparece: deja un cadáver físico que se desliza con fricción y choca con las paredes. Rodar contra él lo hace explotar, dando puntos, relleno de combo y una probabilidad de soltar vida. Convierte limpiar una zona en algo que sigue premiando el movimiento en vez de un montón de partículas.


Las mejoras

Al limpiar una oleada se ofrecen tres cartas sacadas de un pool en ScriptableObject, con límites de acumulación por mejora. Hay 17 en total, entre modificadores de estadísticas (velocidad, duración y cooldown de la voltereta, tamaño del collider, vida máxima, capacidad y disipación de calor) y otras que cambian la mecánica:

  • Vampire: cada muerte te cura.
  • Aftershock: cada muerte hace daño en área alrededor.
  • Ricochet Roll: la voltereta encadena hacia un objetivo cercano.
  • Mouse Guide Roll: puedes corregir la dirección de la voltereta hacia el cursor a mitad de esquiva.
  • Orbital Shields: escudos que orbitan, tienen su propia durabilidad y retroceden hacia dentro al recibir un impacto antes de volver a salir.

Y Berserker's Curse, que te quita las ventanas de invulnerabilidad por completo a cambio de potencia. En un juego que se llama I-FRAME.

La elección de mejoras

La elección de tres cartas entre oleadas: Momentum, Ricochet Roll y Guided Roll.


Puntuaciones demasiado grandes para el ranking

Este fue el problema técnico más interesante de la jam. Con multiplicadores que se duplican por nivel, las puntuaciones de una buena partida se salen del rango de un entero de 32 bits, y el backend de rankings (LootLocker) solo acepta ints.

La solución fue separar la puntuación real de el valor por el que se ordena. La puntuación se lleva en BigInteger, y un codificador la mapea a un int que conserva el orden: por debajo de 10 millones usa el valor nativo, y por encima cambia a un formato de magnitud, número de dígitos por 10.000.000 más una mantisa de 5 dígitos. Eso ordena correctamente números de hasta unos 10^214.

public static int Encode(BigInteger score)
{
    if (score <= 0) return 0;

    string str = score.ToString();
    int length = str.Length;

    // Hasta 9.999.999 se usa el valor nativo tal cual.
    if (length <= 7) return (int)score;

    // Por encima: longitud * 10.000.000 + los 5 primeros dígitos.
    // Con longitud 8 el mínimo es 80.000.000, justo por encima del corte,
    // así que la secuencia sigue ordenando de forma perfecta.
    int mantissa = int.Parse(str.Substring(0, 5));
    return (length * 10000000) + mantissa;
}

La puntuación exacta no se pierde: se empaqueta como texto junto a su checksum en el campo de metadatos de la entrada, y se reconstruye al leer el ranking.

string trueScoreStr = score.ToString();
string checksum = ScoreValidator.GenerateChecksum(trueScoreStr);
string metadata = $"{trueScoreStr}|{checksum}";

int sortableMagnitude = MagnitudeEncoder.Encode(score);

LootLockerSDKManager.SubmitScore(playerID, sortableMagnitude, leaderboardKey,
    metadata, response => { /* ... */ });

El checksum es un SHA256 sobre la puntuación combinada con una sal secreta, el mismo enfoque anti-trampas que ya había construido para Drawball. Al descargar el ranking se recalcula y se compara, y las entradas que no validan se marcan como no verificadas en vez de descartarse, para que las puntuaciones antiguas sin metadatos sigan apareciendo.

Ranking global
Pantalla de fin de partida

El ranking global y el final de una partida. La primera entrada marca 115.885.778.841, que es exactamente por lo que la puntuación no se puede enviar como un entero de 32 bits.


Game feel

En un juego que va de un solo verbo, ese verbo tiene que sentirse bien. El hitstop es un singleton que congela el timescale en las muertes, usando esperas en tiempo real para que se descongele correctamente incluso con el timescale a cero. Las llamadas solapadas extienden el congelado en vez de acumularse, así que las muertes rápidas se sienten contundentes sin bloquear el juego.

Encima hay temblor de cámara, VFX de esquiva, estelas en la voltereta, ráfagas de partículas, fragmentación de sprites y números de puntuación flotantes.

El detalle del que más contento estoy es SpriteJuice, que da vida a sprites estáticos de un solo fotograma solo con escala, nunca con posición, para no pelearse con la simulación física. Y como escalar el objeto también escalaría su collider, contra-escala automáticamente cualquier Collider2D del mismo objeto para mantener su tamaño real en el mundo.

La cámara tampoco sigue al jugador directamente: Cinemachine sigue a un punto virtual interpolado entre el jugador y el cursor, lo que le da esa sensación de adelantarse hacia donde apuntas, con zoom controlado por la rueda del ratón.

Cadena de Aftershock

Una cadena de Aftershock detonando: cada muerte daña lo que tiene cerca y encadena números de puntuación apilados.


Herramientas de editor

Aun con el reloj de la jam en contra, dediqué tiempo a herramientas propias de editor porque salía rentable: un asistente que genera automáticamente los assets de datos de las mejoras y su UI, otro para configurar el juice de la interfaz, y un menú de depuración en el propio juego para forzar mejoras concretas mientras se prueba, en vez de tener que llegar a la oleada correcta para ver si una funciona.


Llegar tarde y terminarlo igual

No llegué al plazo oficial de la jam. Hubo problemas al final de la semana y la entrega se quedó incompleta. Lo que sí hice fue seguir trabajando y publicar después una versión terminada y bastante más completa, en vez de dejar el prototipo tirado.

Es el proyecto donde más claro tengo que la restricción ayudó: una sola mecánica, empujada hasta que sostiene un juego entero.

Built with 11ty + Decap CMS