Over the past development cycle, I worked on two features for Grav Sync, these updates transformed the game from a static puzzle into a more dynamic and visually engaging experience.
Here’s a detailed look at what was implemented and why.
Branch 1: random-wave - Introducing Dynamic Challenge
The Problem
The original game featured a fixed target wave that players needed to match. While functional, this meant that once players learned the solution, the game became repetitive. Every playthrough was essentially the same puzzle.
The Solution: Randomized Target Frequencies
I implemented a system to randomize the target wave’s frequency at the start of each round, making every attempt unique and challenging.
Key Changes:
- Random Frequency Generation: Added
getRandomInt(10, 30)function to generate target frequencies within a balanced range - Dynamic Target Updates: The target frequency now regenerates each time the target wave resets (when it reaches the player’s orbital radius)
- UI Enhancement: Added on-screen display of the current target frequency to give players clear feedback
Implementation Details:
| |
Refining the Synchronization System
While implementing randomization, I also overhauled how synchronization is calculated. The original system treated frequency and amplitude equally, but through playtesting, it became clear that frequency matching is far more critical to achieving visual alignment.
The New Weighted System:
- 80% weight on frequency matching: Ensures players prioritize getting the wave count correct
- 20% weight on amplitude matching: Fine-tuning for perfect alignment
- Phase ignored: Simplified gameplay by removing phase as a factor, making the puzzle more approachable
Technical Implementation:
| |
This change made the game feel more intuitive, players could immediately see the impact of frequency adjustments while still needing to fine-tune amplitude for perfect scores.
Impact
The random-wave branch transformed Grav Sync from a single-solution puzzle into a replayable game with procedural variation. Each round now presents a fresh challenge, and the weighted sync system provides clearer feedback about what matters most.
Branch 2: sprites - Bringing Earth to Life
The Vision
The original Earth was rendered as a simple static circle with basic ellipse shapes for continents. While functional, it lacked the polish and visual interest the game deserved. I wanted Earth to feel like a living, rotating planet at the center of the cosmic waves.
Creating the Sprite System
Step 1: Sprite Sheet Preparation
I found a 94-frame sprite sheet showing Earth rotating (attribution has been made into the repo):
- Format: 480×480 PNG
- Layout: 10×10 grid (10 frames per row for 9 rows, 4 frames in the final row)
- Frame size: 48×48 pixels per frame
- Animation style: Flat earth texture scrolling across a circular mask
Step 2: Frame Data Mapping
Created animations/earth.json to map all 94 frames with precise coordinates:
| |
Step 3: Building the Sprite Class
Implemented a reusable Sprite class in sprite.js:
| |
Key Features:
- Flexible animation speed: Random speed on initialization creates subtle variation
- Scalable sprites: Can match any target size (scaled to 96×96 to match original Earth diameter)
- Center-based positioning: Uses
imageMode(CENTER)for intuitive placement - Error handling: Validates animation array to prevent crashes
Integration Challenges
Challenge 1: Variable Scope Issues
Initial implementation had bugs where the animation array wasn’t properly declared:
| |
Solution:
| |
Challenge 2: Sprite Positioning
Initially, sprites appeared in the top-left corner instead of centered. The issue was that p5.js’s image() function defaults to corner-mode positioning.
Solution: Wrapped rendering in push()/pop() and set imageMode(CENTER) to ensure sprites draw from their center point.
Challenge 3: Size Matching
The original hand-drawn Earth had a diameter of 96px (radius 48), but the sprite frames were 48×48.
Solution: Calculate scale factor dynamically:
| |
The Result
![]()
The sprite system brought immediate visual improvements:
- Living planet: Earth now rotates continuously, adding life to the scene
- Variable speed: Random animation speeds (0.1–0.4) create subtle uniqueness each game
- Smooth animation: 94 frames provide fluid rotation without obvious looping
- Maintained aesthetics: Scaled perfectly to match the original Earth size
Code Quality Improvements
Added validation to prevent edge-case crashes:
| |
Also fixed a stroke bleed issue where wave rendering affected subsequent draws:
| |
What’s Next?
With these two branches merged, Grav Sync now has:
- Dynamic, randomized gameplay
- Polished visual presentation
- Intuitive sync feedback
Future improvements could include:
- Audio integration: Frequency-based soundscapes using p5.sound
- Difficulty curves: Progressive challenge scaling
- Particle effects or shaders: Visual feedback when sync is achieved
Conclusion
These two updates made Grav Sync more engaging: random frequencies add replay value, and the animated Earth sprite improves the visual appeal. The game is now more fun to play than the original static version.
You can check out the full code on GitHub, and I’d love to hear feedback from anyone who tries it out!