Skip to content
Arnel Robles edited this page Oct 2, 2026 · 2 revisions

Events

There are two event classes with different rules. Mixing them up is the most common mistake.

RewardSystem (an EventEmitter) kernel EventBus
Subscribe on(event, callback) on(event, callback, priority)
on returns a function that unsubscribes the bus, for chaining
Unsubscribe call the returned function, or off off(event, callback)
One-shot none once(event, callback)
Order insertion order higher priority first

Event names are lowercase strings. EventBus.Events.LEVEL_UP is a constant whose value is 'level_up'. Listening for 'LEVEL_UP' never fires.

A listener that throws is logged and does not stop the other listeners. This holds for both classes.

Reward events

const stop = rewards.on('level_up', ({ oldLevel, newLevel, totalXP }) => {});
stop();
Event Payload When
xp_gained { amount, reason, leveledUp, newLevel, total } after every addXP, including the XP from games and achievements
level_up { oldLevel, newLevel, totalXP } after xp_gained, when the level rose
achievement_unlocked the achievement definition plus id when an achievement unlocks
new_high_score { gameName, score } when recordGame beats the stored high score
streak_updated { current, longest } when the calendar day changes, in init() or recordGame()
save_failed { key } when a storage write fails

amount in xp_gained is what changed. A removal larger than the player's XP reports the XP that was there.

Kernel events

const onTick = ({ dt }) => {};
kernel.events.on('tick', onTick, 10);
kernel.events.off('tick', onTick);

Keep the callback in a variable. off needs the same function, and it removes every registration of it.

Event Payload When
tick { dt } start of each frame
fixed_update { dt } each fixed step, before systems run fixedUpdate
render { dt } end of each frame
kernel_started none kernel.start()
kernel_stopped none kernel.stop()
system_registered { name, system } systems.register
system_unregistered { name } systems.unregister
plugin_loaded { name, version } a plugin's init finished
plugin_unloaded { name } plugins.remove
collision_enter { a, b } two colliders start to overlap (2.4.0)
collision_exit { a, b } they stop overlapping, or one of them is removed (2.4.0)

dt is in seconds and capped at 0.1.

a and b are the two colliders, typed as CollisionEvent. Each event fires once per overlap. Components still get onCollisionEnter on every step while the pair overlaps. See Engine.

Other EventBus methods: emit(event, data), clear(event?), hasListeners(event).

Bridging rewards onto the bus

Plugins listen on the kernel bus. To let them see reward events, hand the bus to the reward system.

import { Game, RewardSystem, DataService } from 'dopaminejs';

const game = new Game();
const rewards = new RewardSystem(new DataService(), { events: game.kernel.events });
// or: { kernel: game.kernel }

Each reward event is then emitted on both. rewards.on(...) keeps working as before.

Plugin: Ecosystem needs this bridge. Without it BattlePassPlugin and LeaderboardPlugin hear nothing.

Clone this wiki locally