DEV Community

Weird Codes
Weird Codes

Posted on • Originally published at weirdcodes.itch.io

Optimized for mobile | Moksha

📱 Devlog #6 — Mobile Performance Pass + Critical Bug Fixes

"योगः कर्मसु कौशलम्"
Yoga is skill in action — and smoother action means better karma.


What Changed

This update focused on making Moksha feel responsive and smooth on mobile browsers, while fixing several bugs that crept in alongside the touch controls and tutorial system introduced last week.


Performance Fixes

The Hidden GC Bomb — Double getState() Call

Every single frame, the game's draw() function was calling engine.getState() twice. Once to read audio state, and once to pass to the renderer. Each call created a new JavaScript object — a snapshot of the entire engine state. At 60fps, that's 120 object allocations per second going straight to the garbage collector.

The fix was one line: reuse the snapshot already captured at the top of the function.

// before: two full state snapshots per frame
const st = engine.getState();
Renderer.drawScene({ ...engine.getState(), ... }); // second copy!

// after: one snapshot, reused
const st = engine.getState();
Renderer.drawScene({ ...st, ... }); // same object
Enter fullscreen mode Exit fullscreen mode

Small change, meaningful GC pressure reduction — especially noticeable on low-end Android where garbage collection causes visible frame stutters.

Visibility API — Tab Switch Pause

When you switch tabs on mobile (which happens constantly — notifications, messages, etc.), the browser would keep the requestAnimationFrame loop running in the background. The game would accumulate a massive dt spike, and when you returned, everything would lurch forward in time.

Now when the tab goes hidden, the rAF loop is cancelled entirely. When you return, lastTime is reset before the loop restarts — no spike, no lurch.

document.addEventListener('visibilitychange', () => {
    if (document.hidden) {
        cancelAnimationFrame(_rafId); // stop burning CPU/battery
    } else {
        lastTime = performance.now(); // reset dt baseline
        requestAnimationFrame(gameLoop);
    }
});
Enter fullscreen mode Exit fullscreen mode

Audio Context Mobile Unlock

Mobile browsers suspend the Web Audio API context until a user gesture occurs. On some devices, the ambient audio layers (running horses, dream breath) would stay silent even after the game started. A single touchstart listener now explicitly resumes the audio context on first touch — no more silent gameplay on iOS Safari.

GPU Layer Hints for Touch Controls

Added will-change: transform, background to touch buttons and will-change: opacity to the touch controls container. This signals to the browser compositor to promote these elements to their own GPU layer — reducing paint work during button press animations.


Bug Fixes

Shastra Close → Controls Stuck (Critical)

After opening and closing the 📜 Shastra overlay, all controls would stop responding. Two root causes:

1. isPaused left as true after Shastra close.
The engine's toggleShastra() has a wasAlreadyPaused flag to restore pause state after closing Shastra. If the tab was switched or the game was paused before Shastra opened, this flag would be set incorrectly — leaving isPaused = true even though no pause overlay was visible. Fix: if the pause overlay isn't visible when Shastra closes, force isPaused = false.

2. keys = {} breaking TouchControls reference.
Three places in main.js were using keys = {} to clear the input state — the blur handler, the visibility change handler, and toggleShastra(). Each reassignment created a new object and broke the reference held by TouchControls, which still pointed to the old one. After the reassignment, touch buttons would inject into the old (discarded) keys object while the game loop read from the new one.

Fix: replace all keys = {} with in-place clearing:

Object.keys(keys).forEach(k => { keys[k] = false; });
touch.clearAll(); // sync touch button visual state too
Enter fullscreen mode Exit fullscreen mode

What's Still Planned (Issue #43)

The following items from the performance checklist are tracked but not yet complete:

  • Tutorial card offscreen cachedrawTutorialCard() still redraws every frame while the card is static; an offscreen canvas cache will fix this
  • Particle/Maya draw call batching — grouping draw calls by type reduces GPU state switches
  • ctx.roundRect() polyfill overhead — inline fallback for older mobile browsers
  • CSS contain: strict on #gameContainer — layout isolation

These will land in a future update before v0.1.0.


Commit Summary

fix: mobile performance pass + controls bug fixes

- getState() double-call removed (GC pressure fix)
- Visibility API tab-switch rAF cancel + dt reset
- AudioContext mobile unlock on first touchstart
- will-change GPU hints on touch controls
- toggleShastra isPaused stuck bug fixed
- keys = {} reassignment replaced with in-place clear
- TouchControls reference preserved across all state resets

Closes #43 (partial)

Enter fullscreen mode Exit fullscreen mode

Commit:

https://github.com/weirdcodesofficial/MOKSHA/issues/43


Moksha is a solo-developed browser game rooted in Sanatan Shastra.
Play it at weirdcodes.itch.io/moksha
Support the project at ko-fi.com/weirdcodes

Top comments (0)