Flukz — open-source shmup community resource open-source GPL devlog  ·  about
// indie shmup & game-dev resource

Game Design · June 30, 2026

Bullet Pattern Design in Shmups: Parametric Scripting and Spatial Choreography

A bullet pattern is a spatial puzzle the player solves in real time. The best ones feel inevitable in retrospect — once you understand the geometry, the safe lane is obvious. Getting there requires thinking in polar coordinates, writing patterns as parameterized functions rather than hardcoded lists, and respecting the telegraph window that gives the player any chance of reading the attack before it lands.

Most developers new to shmup design start by placing bullets with specific x/y velocities. The approach works for one-off projectiles but breaks down entirely when you want rings, spirals, aimed fans, and rotating arcs — the vocabulary of every boss fight worth remembering. The underlying geometry of all these patterns is polar, and treating it as such from the start makes every subsequent pattern trivial to write and easy to modify.

Thinking in polar coordinates

Every bullet in a dense pattern is fully described by an angle and a speed rather than separate dx/dy velocity components. A ring burst of n bullets pointing outward is a loop from 0 to 2π in steps of 2π/n. A spiral is the same loop with an accumulated angle offset added each spawning tick. A rotating ring is a ring with an angular velocity applied to the whole emitter each frame. Trying to express any of these in Cartesian coordinates is not impossible, but the relationship between parameters and visual output becomes opaque. When a designer wants to tighten a spread or add a clockwise rotation, the change should be a single number, not a trigonometric refactor.

The conversion back to Cartesian for actual bullet movement is a single pair of lines and happens once at spawn time:

func spawn_bullet(angle_rad, speed): bullet.vel_x = cos(angle_rad) * speed bullet.vel_y = sin(angle_rad) * speed

Scripting patterns as parameterized functions

The useful abstraction is a function that accepts numeric parameters and produces a sequence of bullet spawn events. Speed, bullet count, spread angle, rotation rate, burst delay, and emitter offset are the core knobs. Hardcoding a pattern as a fixed list of spawn commands is the single most common mistake in early shmup prototypes: when difficulty scaling needs the pattern 15% faster, or a designer wants fewer bullets on easy mode, every value in the list must be updated individually.

func ring_burst(count, speed, spread=TWO_PI, base_angle=0.0, rotation=0.0): var step = spread / count for i in range(count): var angle = base_angle + (step * i) + rotation spawn_bullet(angle, speed)

With this structure, an entire difficulty curve becomes a single multiplier table passed into the function. The pattern geometry stays identical; only the parameters change. This also makes it straightforward to animate patterns — having the rotation value advance each frame produces a spinning ring without any additional code.

The telegraph window

A bullet pattern is a design problem before it is a technical one. The player must be able to read what is about to happen before the first bullet fires. Telegraph windows — the period between when an emitter activates (signaled by a charge animation, color shift, or audio cue) and when the first bullet leaves — determine whether a pattern feels fair or arbitrary.

Four hundred milliseconds is the practical minimum for a trained player reacting to a known pattern. Six hundred to seven hundred milliseconds is comfortable for a first-time encounter. Patterns with high bullet density or unusual geometry need longer telegraphs because the safe corridor is harder to identify. Boss transitions in particular should err toward longer telegraphs: the player has just finished a prior phase and is repositioning, not waiting in a ready state.

Density and coverage constraints by difficulty

Difficulty Max bullets per burst Min bullet spacing Telegraph window
Easy1228 px700 ms
Normal2018 px550 ms
Hard3212 px450 ms
Lunatic488 px380 ms

Spacing values assume a 480 × 640 portrait play field. Bullet visual radius matters too: large sprites with identical gaps between their centers still create smaller navigable corridors than small sprites with the same gap. Measure available corridor width, not inter-center distance.

Layering complexity without obscuring the hitbox

Multi-layer patterns — two or more emitters firing simultaneously with different speeds and angles — are the source of the genre's most memorable moments. They are also how many developers accidentally produce unfair patterns. A slow ring and a fast spiral can together eliminate every navigable lane in 480 pixels of horizontal space. The check is practical: run the pattern in slow motion, trace the player hitbox through it, and verify at least one navigable corridor exists at all times. If there is none, reduce bullet count or increase speed differential between layers so the fast and slow bullets separate cleanly before converging.

Stacking multiple scaling axes simultaneously — faster speed, higher count, and a rotating emitter all at once — compresses difficulty too rapidly. Players can adapt to one change at a time; three simultaneous changes read as arbitrary rather than challenging. Introduce complexity in stages across the boss phase timeline: start with the base pattern, add speed after 30 seconds, add rotation in the final health segment.

The discipline of parametric pattern design pays off most during iteration. A pattern that feels too easy at normal difficulty is one parameter adjustment from a satisfying hard-mode variant, not a rewrite. That flexibility, more than any individual pattern, is what keeps a shmup's pacing tunable through the full playtesting cycle.