BeeEngine JAVASCRIPT 2D ENGINE
Documentazione – BeeEngine
v2.5.0

BeeTimer — Documentazione Tecnica

Cooldown e loop di evento sul clock di BeeTime: one-shot, catch-up, scaled vs unscaled

BeeEngine Mascot

📁 Panoramica

BeeTimer è l’evento a tempo. Vive in src/core/BeeTimer.js, il barrel lo re-esporta insieme a BeeTimerClock e BEE_TIMER_DEFAULTS. BeeTime è l’orologio; questa classe è il cooldown, lo spawn, il blink HUD, la durata di un bonus. Non è un secondo rAF, non è un tween, non è un cronometro di rete.

BeeEngine crea un clock: this.timers = new BeeTimerClock(). Ogni frame, anche in pausa, il loop fa time.tick(timestamp) e subito this.timers.tick(this.time). I timer sul clock avanzano da soli. Quelli costruiti a mano (BeePlayer._boost, BeeEnemyShooter.fire) no: li ticki tu con update.

import { BeeEngine, BeeTimer } from ‘beeengine’; const gioco = new BeeEngine(‘testCanvas’, 800, 600); const ricarica = gioco.after(1.5, () => arma.ready = true); const hud = gioco.every(1, () => blink = !blink, { unscaled: true }); ricarica.pause(); ricarica.resume(); hud.cancel();

📁 Due vie: clock del motore vs update manuale

gioco.after(duration, fn, options) e gioco.every(duration, fn, options) sono zucchero su timers.create: iniettano clock, autoStart: true. every forza anche loop: true. after non forza loop: false: se passi { loop: true } ottieni un loop.

Un new BeeTimer({...}) senza clock non entra in gioco.timers. Devi chiamare timer.update(gioco.time) (o un numero). Se lo metti in scene.update / entity.update, in pausa del motore quell’hook non parte: l’opzione unscaled da sola non basta. Il callback engine.update gira anche in pausa; oppure registralo sul clock.

Non fare entrambe. Un timer già sul clock che riceve anche update a mano avanza due volte nello stesso frame.

📁 Due assi: scaled e unscaled

Il timer non sceglie il delta da solo se gli passi un numero. Sceglie solo se gli passi BeeTime (ha delta) o un oggetto { dt, unscaledDt }.

Argomento di update Timer scalato Timer unscaled
BeeTime (ha delta) time.delta(false)dt (0 in pausa, segue timeScale) time.delta(true)unscaledDt
oggetto { dt, unscaledDt } dt se è un number, altrimenti 0 unscaledDt se è un number, altrimenti 0
numero quello step, punto. Il flag unscaled viene ignorato

unscaled e l’alias useUnscaledTime si allineano nel costruttore. resolveStep legge timer.unscaled. Se muti solo useUnscaledTime a caldo, lo step non cambia.

gioco.after(2, apriPorta); // si ferma con F4 / pause() gioco.every(1, toggleBlink, { unscaled: true }); // sbagliato: il numero è già lo step, unscaled non si applica timer.update(gioco.time.dt);

📁 One-shot vs loop

Default loop: false. I due rami in update non si mescolano.

One-shot. Quando elapsed >= duration: prima finished = true, running = false, paused = false, poi la callback. Nello stesso tick del clock il timer esce dalla lista live. duration <= 0 è legale: al primo update con step ≥ 0 scatta subito (elapsed parte da 0).

Loop. Sottrae duration, spara, ripete finché elapsed sta sotto o raggiungi maxCatchUp (default 8, minimo 1). Dopo il tetto, il resto fa elapsed %= duration: non perdi il residuo, perdi solo i fire oltre il cap. Se in callback fai pause / cancel / spegni running, il catch-up di quel frame si interrompe.

Loop con duration <= 0 lancia Error('BeeTimer: loop richiede duration > 0') nel costruttore e in start. In update, se muti duration a 0 a caldo, il ramo loop fa return: niente while infinito.

📁 start, pause, resume, reset, cancel

Non sono sinonimi. Fluent: tutti ritornano this.

Metodo elapsed running / paused finished / cancelled Clock
start() azzera true / false entrambi false re-add (idempotente)
pause() / stop() invariato false / true (no-op se non era running) invariati resta in lista, update esce subito
resume() invariato true / false no-op se finished o cancelled re-add
reset() azzera non tocca running né paused entrambi false non add / non remove
cancel() azzera false / false finished false, cancelled true clock.remove(this)

start due volte è un restart, non un no-op: elapsed torna a 0. Dopo un one-shot finito, resume non riparte: serve start() (oppure reset() + resume()). Dopo cancel, stesso discorso: solo start (che riazzera cancelled e re-add).

Non esiste BeeTimer.destroy. Teardown = cancel(). engine.destroy() chiama timers.clear(), che cancella tutti i live e stacca il puntatore clock.

📁 update — chi avanza, e quando no

Guardia in testa: se non è running, o è finished, o è cancelled, return. Poi elapsed += resolveStep(...).

Uno step non finito (NaN, undefined, oggetto senza i campi) diventa 0: il timer non avanza. Uno step numerico negativo sottrae da elapsed (nessun clamp a 0 sul decremento). In pausa del motore, timers.tick(this.time) parte lo stesso: i scalati sommano dt = 0, gli unscaled sommano unscaledDt.

La callback riceve il timer: fn(this). Alias: onComplete vince su callback se entrambi sono valorizzati. Nessun try/catch: un throw ferma il resto del clock.tick di quel frame.

📁 BeeTimerClock — lista live, compact, create

add rifiuta i non-BeeTimer (li ritorna com’è). Imposta timer.clock = this e fa push solo se indexOf è negativo: niente duplicati sullo stesso clock. Non toglie il timer da un clock precedente: non spostare un’istanza tra due clock.

tick(time) scorre la lista, chiama update, poi compatta in-place: butta i cancelled e i one-shot finished. I loop e i timer in pausa restano. size è #live.length dopo il compact.

Se una callback, nello stesso tick, fa gioco.after / create, il nuovo timer viene pushato e for (i < list.length) lo vede: può ricevere lo stesso dt nello stesso frame. Un one-shot a duration 0 creato così spara subito.

const clock = gioco.timers; clock.create({ duration: 0.25, autoStart: true, onComplete(t) { /* t === il timer */ } }); clock.size; // live: running, paused, loop — non i one-shot già finiti

📁 Integrazione: loop, scene, entity

Ordine reale nel loop V2.4:

this.time.tick(timestamp); this.timers.tick(this.time); // anche in pausa if (!this.time.paused) { this.scenes.update(dt, this.input); this.updateEntities(dt, this.input); this.time.consumeFixedSteps((fixedDt) => this.physics.step(fixedDt)); }

I timer sul clock sparano prima di scene e entity. Un after che fa scenes.change vede ancora le entity vecchie in quello stesso frame (il change da scene.update è un altro percorso).

BeeEnemyShooter.fire e il boost temporaneo del player sono istanze manuali: update(engine.time) nell’entity, cancel() in destroy. Non occupano gioco.timers. Il fire dello shooter segue la simulazione: in freeze non viene chiamato.

📁 Esempio: simulazione vs HUD

import { BeeEngine } from ‘beeengine’; const gioco = new BeeEngine(‘testCanvas’, 800, 600); const porta = gioco.after(3, () => { // one-shot: finished è già true qui }); const hudTick = gioco.every(0.5, () => { overlay.visible = !overlay.visible; }, { unscaled: true }); gioco.start((dt, input, time) => { if (input.wasPressed(‘Escape’)) time.togglePause(); // non chiamare porta.update(time): è già sul clock });

📁 Note per chi integra

Clock per i timer di gioco/HUD. after / every se deve vivere la pausa o non hai un hook ogni frame. Update manuale se il timer muore con l’entity.

Un numero è uno step. Per rispettare pausa e slow-mo passa gioco.time, non gioco.time.dt.

start = restart. Utile per un cooldown che si rinfresca. Non è “parte solo se era fermo”.

Callback che lancia. Non è wrappata. Gli altri timer dello stesso tick possono non girare; il compact del clock può restare a metà. Tieni le callback corte e senza throw.

Niente destroy sull’istanza. cancel in entity.destroy se l’hai creato a mano. Il motore pulisce solo gioco.timers.

📁 Riepilogo API

Fluent: start, pause, resume, stop, reset, cancel, update e i metodi del clock ritornano this (tranne create / add, che tornano il timer). Default in BEE_TIMER_DEFAULTS (frozen).

Membro Ritorna Scopo
BEE_TIMER_DEFAULTS oggetto frozen duration 1 · loop false · unscaled false · autoStart false · maxCatchUp 8.
new BeeTimer(durationOrOptions, onComplete?, loop?, options?) BeeTimer Numero + args, oppure un oggetto spec. callback è alias di onComplete. Throw se loop e duration <= 0.
duration / elapsed number Secondi. duration è mutabile a caldo (lo shooter lo riallinea a shootInterval).
remaining / progress number Rimanente clamp-ato a 0. Progress in [0, 1]; se duration <= 0 vale 1.
time number Alias deprecato di elapsed (getter/setter).
loop / unscaled / useUnscaledTime boolean Loop vs one-shot. I due flag unscaled partono uguali; lo step legge unscaled.
running / paused / finished / cancelled boolean paused è getter su campo privato: true solo dopo pause su un timer che stava running.
onComplete / callback function | null #fire chiama onComplete || callback con (timer).
maxCatchUp number Tetto fire per hitch in loop. Intero ≥ 1.
clock BeeTimerClock | null Clock owner, o null se è un timer manuale / dopo remove.
start() this Azzera e parte. Restart se già running. Throw se loop e duration ≤ 0.
pause() / stop() this Ferma, tiene elapsed. stop è alias di pause.
resume() this Riparte senza azzerare. No-op se finished o cancelled.
reset() this Azzera elapsed / finished / cancelled. Non cambia running.
cancel() this Spegne, marca cancelled, toglie dal clock.
update(dtOrTime) this Avanza. Accetta BeeTime, { dt, unscaledDt }, o un number (step esplicito).
new BeeTimerClock() BeeTimerClock Lista live vuota. Il motore ne tiene una in gioco.timers.
clock.size number Quanti timer sono ancora in lista (paused e loop inclusi).
clock.add(timer) timer Registra se è un BeeTimer. Dedup per identità.
clock.create(options) BeeTimer new BeeTimer({ ...options, clock: this }).
clock.remove(timer) this Toglie dalla lista. Se timer.clock === this, lo azzera.
clock.tick(time) this Update + compact. Chiamato dal loop ogni frame.
clock.clear() this Svuota, stacca, cancel su ciascuno. engine.destroy lo chiama.
gioco.after(duration, fn, options?) BeeTimer Create + autoStart sul clock del motore. Non forza loop: false.
gioco.every(duration, fn, options?) BeeTimer Come after, con loop: true.