Skip to content

Asteroids: fix the stray lines and stall when objects cross the screen edge (closes #103) - #242

Merged
mishamyte merged 1 commit into
devfrom
fix/asteroids-screen-wrap-lag
Jul 24, 2026
Merged

Asteroids: fix the stray lines and stall when objects cross the screen edge (closes #103)#242
mishamyte merged 1 commit into
devfrom
fix/asteroids-screen-wrap-lag

Conversation

@mishamyte

Copy link
Copy Markdown
Collaborator

Closes #103.

Two people reported the same thing: long straight lines appear when an asteroid leaves the screen, and the game then slows down until the Flipper has to be reset.

What was happening

canvas_draw_line() takes int32_t coordinates and passes them to u8g2_DrawLine(), whose parameters are u8g2_uint_t, that is uint16_t. A polygon vertex one pixel off the left edge therefore arrives as x = 65535, and u8g2 walks the whole Bresenham run to reach it: about 65000 pixel positions for a shape that should have cost thirty. The positions that land inside the frame buffer are the lines in the screenshots on the issue, and the walk itself is why the device stops responding, since the draw callback runs on the GUI service thread and that thread also delivers input.

A later change (update asteroids, Jul 2025) clamped every vertex into the frame. That stopped the stall but not the lines: the clamped part of a shape collapses onto the border, so an asteroid crossing an edge is still drawn as a straight line along it.

Rendering the same two asteroids with each version, on a canvas that reproduces u8g2's unsigned coordinates:

                              pixel positions walked
  original (pre-2025)                       131208    lines across the whole screen
  clamped (current dev)                        156    straight lines along the borders
  clipped + wrapped (this PR)                  173    no stray lines, shapes wrap

What this changes

Lines are clipped to the screen (Cohen-Sutherland) before they reach the canvas, so no coordinate outside the frame ever gets there. On top of that, a shape hanging over an edge is drawn again one screen over, so it now appears on the opposite side as it leaves, which is how the playfield already behaved for the game logic.

The rest of the change is the other reasons the game stopped responding:

  • render_callback() called notification_message() for the thruster buzz. That runs on the GUI service thread and blocks while the notification queue is full, taking input for the whole system down with it. The buzz is played from the game tick now.
  • The drone buddy's fire rate is documented in game ticks but was compared against furi_get_tick() in milliseconds, so the drone fired on every frame: a bullet and a vibration sequence ten times a second, on top of everything else. It now fires every two seconds, which is the one deliberate gameplay change here.
  • The crash feedback was replayed on every frame of the 15 frame hit animation. It is played once per collision now, and the sequence was lengthened so a single play still registers.
  • A shield ploughing into a cluster queued one sequence per asteroid it broke; it buzzes once per tick instead.
  • Ship velocity had no limit. Holding thrust for a few seconds moved the ship a good part of the screen between frames, through asteroids without touching them and much too fast to steer. It is capped at 6 px/tick, which is under the distance at which the ship collides with the smallest fragment.

And a few things found on the way through:

  • furi_record_open(RECORD_NOTIFICATION) was called for every sequence and never closed. The record is opened once now and closed on exit.
  • The asteroid outline recomputed sin()/cos() for the same eight angles for every asteroid on every frame; those are constants now, and the remaining float math uses the f suffixed calls.
  • add_powerUp() wrote one or two FURI_LOG_I lines on every game tick, and logging is a blocking UART write. Those are at debug level now.
  • should_add_powerUp() called srand(furi_get_tick()) on every tick, which kept reseeding the generator; it is seeded once at startup instead.
  • gui_add_view_port() ran before the game state was initialised, so the draw callback could read an uninitialised lives and loop on it.
  • The save file was a dump of the whole AsteroidsApp, transient arrays and live pointers included, validated by exact size. Any change to the structure therefore reset everyone's high score and left a random number in its place. It now holds just the high score, through saved_struct, so this is the last time it resets, and a failed save is reported instead of being dropped.

The defensive coordinate checks that came with the July 2025 patch are gone: object positions are wrapped into the playfield where they are set, and clipping makes any coordinate safe to draw, so those checks only had the effect of silently not drawing something. One of them also mutated the ship's position from the draw callback, on a different thread from the game tick that owns it.

Testing

Built with ufbt and run on a Flipper Zero on Unleashed (API 88.0).

  • The clipping was fuzzed against a brute-force reference over 60000 segments biased towards the edges and corners: no visible segment dropped or truncated, no coordinate emitted outside the frame.
  • The wrapping was checked against a reference that simply draws the shape at all nine surrounding positions, over 4000 placements of every asteroid size, the ship and the drone: identical output every time.
  • On the device: played through the reported situation, soaked with 140 randomised press and release events, exits cleanly on a long Back, returns its heap across launch cycles, and reloads its high score after a restart.
  • arm-none-eabi-objdump confirms no double-precision calls are left on the drawing path.

🤖 Generated with Claude Code

…n edge (closes #103)

canvas_draw_line() takes int32_t coordinates and passes them to
u8g2_DrawLine(), whose parameters are uint16_t. A polygon vertex one pixel
off the left edge therefore arrived as x=65535, and u8g2 walked the whole
Bresenham run to reach it: about 65000 pixel positions for a shape that
should have cost thirty. The positions that landed inside the frame buffer
are the lines in the screenshots on the issue, and the walk itself is why
the device stopped responding - the draw callback runs on the GUI service
thread, which also delivers input.

Rendering two asteroids that hang over an edge cost 131208 pixel positions
in a single frame. A later change clamped every vertex into the frame, which
stopped the stall but not the lines: the clamped part of a shape collapses
onto the border, so an asteroid crossing an edge is still drawn as a
straight line along it.

Lines are now clipped to the screen before they reach the canvas, and a
shape hanging over an edge is drawn again one screen over, so it appears on
the opposite side as it leaves. The same two asteroids now cost 173 pixel
positions.

The rest of the change covers the other reasons the game stopped responding:
the thruster buzz was played with notification_message() from inside the
draw callback, which blocks on a full notification queue and takes the GUI
service down with it; the drone buddy's fire rate was documented in game
ticks but compared against the millisecond clock, so it fired on every
frame; the crash feedback was replayed on every frame of the hit animation;
and ship velocity had no limit, so holding thrust made the ship jump across
the screen and pass through asteroids without touching them.

Also: the notification record is opened once instead of per sequence and
never closed; the asteroid outline no longer recomputes the same eight
sines and cosines for every asteroid on every frame; the per-tick FURI_LOG
lines are at debug level, since logging is a blocking UART write;
should_add_powerUp() no longer reseeds the PRNG ten times a second; the
viewport is published after the game state is set up rather than before;
and the save file holds just the high score instead of a dump of the whole
application structure, live pointers included, which meant any change to
the structure silently reset everyone's score.

Verified on a Flipper Zero running Unleashed (API 88.0): the clipping was
fuzzed against a brute-force reference over 60000 segments and the wrapping
against a full nine-offset torus reference over 4000 placements, both with
no mismatch, and the app was played, soaked with randomised input, exited
and relaunched with its high score intact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added category/games App category: games pack/catalog App in apps_source_code (catalog) labels Jul 23, 2026
@mishamyte mishamyte self-assigned this Jul 24, 2026
@mishamyte mishamyte added the type/bug Something isn't working label Jul 24, 2026
@mishamyte mishamyte added this to the unlshd-090 milestone Jul 24, 2026
@mishamyte mishamyte mentioned this pull request Jul 24, 2026
@mishamyte
mishamyte merged commit f016999 into dev Jul 24, 2026
2 checks passed
@mishamyte
mishamyte deleted the fix/asteroids-screen-wrap-lag branch July 24, 2026 13:16
Repository owner locked as resolved and limited conversation to collaborators Jul 25, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

category/games App category: games pack/catalog App in apps_source_code (catalog) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug on asteroids

1 participant