Skip to content

Event Ideas for Action Games

Once the basics are in (app_start, game_over, submit_score — see the Dodge the Creeps Walkthrough), the question is what to track next. This recipe is a curated list of eight events that earn their bandwidth for a small action game, grouped by what question they answer.

Every event below is wired live in examples/dodge-the-creeps/. Open the demo, play a few rounds, and check the dashboard pages noted at the bottom of each section to see real data flow.

These answer how much your game gets played.

Already covered in the basic walkthrough. Worth re-stating: every round opens with one.

# main.gd:new_game()
QuestData.track("game_start")

Same — the leaderboard event from the walkthrough. The point of repeating it: score and duration_seconds belong on the same event, not split across two. If you only ship score, you can’t tell whether high scores come from skill or from grinding.

# main.gd:game_over()
var duration = Time.get_ticks_msec() / 1000.0 - _game_start_time
QuestData.track("game_over", {
"score": score,
"duration_seconds": snappedf(duration, 0.1),
})

Fired only on rounds after the first. If a player’s first round ends and they immediately start another, that’s the strongest engagement signal you’ll find — they’re hooked.

# main.gd:new_game()
if _total_games > 0:
QuestData.track("player_respawn", {
"previous_best_score": _best_score,
"games_played": _total_games,
})

What this answers: “Of all players who reached a Game Over, what fraction started another round?” Build it in the dashboard as a funnel: game_overplayer_respawn. Anything below ~40% means your game-over screen has friction (or your loop isn’t satisfying).

These answer whether your game is too easy, too hard, or wrong-shaped.

Don’t fire one per spawn — that’s noise. Fire once at game over with the count of enemies on the field at that moment.

# main.gd: counter in _on_MobTimer_timeout
_mobs_spawned_this_round += 1
var alive_now = get_tree().get_nodes_in_group("mobs").size()
if alive_now > _peak_mobs_alive:
_peak_mobs_alive = alive_now
# main.gd:game_over()
var mobs_at_death = get_tree().get_nodes_in_group("mobs").size()
QuestData.track("mob_spawned", {
"count_at_death": mobs_at_death,
"peak_alive_this_round": _peak_mobs_alive,
"total_spawned": _mobs_spawned_this_round,
"score_at_death": score,
})

What this answers: “Is the difficulty curve reasonable?” If 80% of deaths happen at count_at_death > 20, your spawn rate is too aggressive at that score. Dial mob_spawn_interval (Remote Config) until the death-density distribution looks healthy.

Fire at fixed score thresholds (10, 50, 100, …). Aggregating these in the dashboard gives you a drop-off chart: how many runs cleared each milestone.

# main.gd:_on_ScoreTimer_timeout
if score % 10 == 0 and score > 0:
QuestData.track("score_milestone", {"score": score})

What this answers: “Where do players give up?” If 90% reach score 10 but only 5% reach score 50, you have a difficulty cliff between them. Tune.

Send the player’s position every 5 seconds, not every frame. 5s is sparse enough to be cheap, dense enough to show camping vs roaming.

# player.gd:_process(delta)
_movement_timer += delta
if _movement_timer >= 5.0:
_movement_timer = 0.0
QuestData.track("movement_pattern", {
"_x": position.x,
"_y": position.y,
"level": "dodge_the_creeps",
})

The _x and _y underscore prefix tells the backend to project these into spatial columns — they show up on the Heatmap page automatically.

What this answers: “Do all players hide in the corner?” If yes, your map design is failing — central positions need to be more attractive. Or the corner is too safe; nerf it.

These answer whether your game runs well on real hardware.

Fire once per round when FPS falls below 30. The dedup flag is important — without it, a sustained dip would fire dozens of events per second.

# player.gd:_process(delta)
if not _fps_warning_sent:
var fps := Engine.get_frames_per_second()
if fps < 30:
_fps_warning_sent = true
QuestData.track("fps_drop", {
"fps": fps,
"resolution": "%dx%d" % [int(screen_size.x), int(screen_size.y)],
})

Reset _fps_warning_sent = false at the start of each round (in Player.start()).

What this answers: “Which devices struggle?” The SDK auto-attaches OS, CPU, and GPU info to every event — slice the fps_drop count by GPU model in the Performance dashboard. If a specific GPU model dominates, that’s your support-ticket source.

You don’t need a track("screen_resolution") call. The SDK captures screen_size (and OS, CPU, GPU, memory) automatically on the first event of each session — visible in the Performance dashboard under the device-info breakdown.

If you want resolution as a property of a specific event (e.g. paired with fps_drop like above), include it in that event’s payload instead of as its own event.

Once these are flowing, you have answers for the four questions every action game has to answer in its first month:

QuestionAnswer comes from
Are players coming back?player_respawn count vs game_over count
Is the difficulty right?mob_spawned.count_at_death distribution + score_milestone drop-off
Where do players spend their time?movement_pattern heatmap
Does it run on real hardware?fps_drop sliced by GPU + auto device info

That’s a useful base for any small action game. Add more only when a specific question can’t be answered with what’s already flowing.