Bullet Shower Walkthrough
This walkthrough covers the Bullet Shower demo. It exists for one reason: prove the SDK doesn’t fall over under load and ship the performance telemetry to back that up.
If your game has bullet hell, particles, simulation, or anything else that pressures FPS, copy these patterns.
What you’ll see in action
Section titled “What you’ll see in action”| SDK feature | Where in game.gd |
|---|---|
| Device info on session start (auto) | SDK ships OS, CPU, GPU, memory on first event |
perf_sample events every 1s | _update_perf_sampler() |
track_error on FPS drop under load | _update_perf_sampler() |
| Wave-milestone achievements | _spawn_wave() |
| Survival leaderboard with metadata | _end_run() |
| Remote-config tunable difficulty | _apply_remote_config() |
Run it
Section titled “Run it”docker compose up -ddocker exec -i quest-data-db psql -U quest -d quest_data < backend/sql/seed.sql# Open examples/bullet-shower/project.godot, F5WASD/Arrows to move. B to boost difficulty (multiplier compounds). R to restart. Survive as long as you can.
Code tour
Section titled “Code tour”Performance sampling — the core pattern
Section titled “Performance sampling — the core pattern”Every second the demo grabs fps, memory_mb, bullet_count and ships a perf_sample:
# game.gd:227-245func _update_perf_sampler(delta: float) -> void: _perf_timer += delta if _perf_timer < _perf_interval: return _perf_timer = 0.0
var fps := Engine.get_frames_per_second() var mem_mb := OS.get_static_memory_usage() / (1024.0 * 1024.0) var survived_sec := float(Time.get_ticks_msec() - _run_start_ms) / 1000.0
QuestData.track("perf_sample", { "fps": fps, "memory_mb": mem_mb, "bullet_count": _bullets.size(), "wave": _wave, "survived_sec": survived_sec, })
# Escalate to an error if FPS tanks under load if fps < _fps_warning and not _fps_warning_sent and _bullets.size() > 100: _fps_warning_sent = true QuestData.track_error("fps_drop_under_load", "FPS=%d bullets=%d wave=%d" % [fps, _bullets.size(), _wave])Three deliberate decisions:
- 1-second interval is a sweet spot. Faster than 1Hz makes the dashboard charts noisy without revealing more. Slower than 1Hz misses brief stutters.
bullet_countandwaveride along. Without context, an FPS dip is meaningless. With context, you can plot FPS vs bullet count and find the device-class breakpoint.- Only escalate to
track_errorunder load. A 30 FPS reading at the title screen isn’t a bug. The_bullets.size() > 100guard makes sure the error fires only when load matters. The_fps_warning_sentflag deduplicates — one error per run, not per frame.
Device info — automatic, not your job
Section titled “Device info — automatic, not your job”You don’t see device-info code in this demo. The SDK captures it automatically on the first event of the session — OS, CPU, GPU, memory. Open the dashboard’s Performance page after a run and you’ll see the FPS distribution sliced by GPU model.
This is why track("app_start") should be the first call you make: it makes sure the SDK has triggered its session bootstrap before anything else fires.
Wave milestones → achievements
Section titled “Wave milestones → achievements”# game.gd:165-179QuestData.track("wave_spawned", { "wave": _wave, "bullets_added": spawned, "total_bullets": _bullets.size(), "boost": _boost_multiplier,})if _wave == 1: QuestData.unlock_achievement("first_dodge")elif _wave == 10: QuestData.unlock_achievement("wave_10")elif _wave == 25: QuestData.unlock_achievement("wave_25")if _bullets.size() >= 2000: QuestData.unlock_achievement("apocalypse") # hidden achievementapocalypse is a hidden achievement — players don’t see it in the achievement list until they unlock it. Mark it as hidden in the dashboard’s Achievements page; the SDK call is the same.
Survival leaderboard with metadata
Section titled “Survival leaderboard with metadata”# game.gd:265-282func _end_run() -> void: _run_alive = false var survived := float(Time.get_ticks_msec() - _run_start_ms) / 1000.0
QuestData.track("run_end", { "survived_sec": survived, "wave_reached": _wave, "peak_bullets": _peak_bullets, }) QuestData.submit_score("bullet_shower_survival", survived, { "wave": _wave, "peak_bullets": _peak_bullets, }) if survived >= 300.0: QuestData.unlock_achievement("survivor_5m")peak_bullets in the score metadata lets you filter the leaderboard for “longest survival under heavy load” — a useful boast for the dashboard’s hardware breakdown.
Boost as engagement signal
Section titled “Boost as engagement signal”# game.gd:259-262elif event.is_action_pressed("boost_difficulty") and _run_alive: _boost_multiplier = min(_boost_multiplier + 0.5, 5.0) QuestData.track_ui("gameplay", "boost_difficulty", "x%.1f" % _boost_multiplier)How often do players self-impose harder difficulty? track_ui answers that. If players never press B, your default difficulty is tuned correctly. If 30% of players boost to 5x within 30 seconds, the default is too easy.
Remote-config tunable everything
Section titled “Remote-config tunable everything”Almost every magic number is read from Remote Config:
# game.gd:88-97_wave_interval = float(QuestData.get_config("wave_interval_sec", DEFAULT_WAVE_INTERVAL))_bullets_per_wave = int(QuestData.get_config("bullets_per_wave", DEFAULT_BULLETS_PER_WAVE))_bullet_speed = float(QuestData.get_config("bullet_speed", DEFAULT_BULLET_SPEED))_max_bullets = int(QuestData.get_config("max_bullets", DEFAULT_MAX_BULLETS))_player_speed = float(QuestData.get_config("player_speed", DEFAULT_PLAYER_SPEED))_perf_interval = float(QuestData.get_config("perf_sample_interval_sec", DEFAULT_PERF_INTERVAL))_fps_warning = int(QuestData.get_config("fps_warning_threshold", DEFAULT_FPS_WARNING))In a stress test this is exactly right — everything that affects load should be remote-tunable so you can dial it up or down without rebuilding.
Dashboard walkthrough
Section titled “Dashboard walkthrough”| Page | What to look at |
|---|---|
| Performance | FPS distribution per GPU. Memory growth over survived_sec. |
| Custom Events → perf_sample | Plot FPS against bullet_count to find your breakpoint |
| Errors → fps_drop_under_load | Flight Recorder showing the run leading up to the dip |
| Leaderboards → bullet_shower_survival | Filter by peak_bullets for hardware-class boasts |
| Achievements → apocalypse | Hidden until unlocked |
| Configuration → Remote Config | Tune bullets_per_wave, max_bullets, fps_warning_threshold |
Try it yourself
Section titled “Try it yourself”- Per-frame timing budget. Add
frame_time_ms = 1000.0 / max(fps, 1)toperf_sample. Plot the 99th percentile per GPU model — if any device sits above 16ms p99, you have a stutter problem on that hardware. - Quality-tier auto-downgrade. If
fps_drop_under_loadfires for a player, setset_user_property("quality_tier", "low")and target a Remote Config segment that lowersmax_bullets. - Memory leak detector.
memory_mbis already tracked. Plot it oversurvived_secand look for a positive slope in long runs — that’s a leak.
Next Steps
Section titled “Next Steps”- Performance Telemetry — the dashboard view that makes
perf_sampleactionable - Error Tracking — Flight Recorder semantics
- Remote Config — segment-targeted tuning