A game that is more than one scene needs a way from one to the next, so the graph can load one (#39)
game-maker / suite (push) Failing after 11m6s
game-maker / suite (push) Failing after 11m6s
Load scene sets a flag on the World like Stop and Restart do; the swap happens after the tick, and the last wish of a tick wins. Exactly one thing crosses over: the SharedState, so global variables and global machines keep what they know while everything local is built anew. Loading the scene that is already running rebuilds it, which is what "reset the level" means when the score must survive. A run starts either from the start scene (a new project field, set from the asset panel and marked with an arrow in the list) or from whatever scene is open. The toggle next to Play says which question is asked and remembers the answer in localStorage: it is a way of working, not part of the game. Sounds no longer end because a scene ends. A voice belongs to the behavior that started it and stops right after On destroy, so the scene swap is no longer a special case at all. For the enemy that plays its explosion and destroys itself in the same breath there is now a detached input on Play sound. The scene selector stays usable while a run plays, because a double click in the asset panel already did the same thing unguarded. It moves what the editor looks at, not what the run plays.
This commit is contained in:
1 parent
30090fc48e
commit
1e9d21cd5d
30 files changed
+976
-57
No files matched your search
@@ -2,9 +2,11 @@ import type { AudioSink } from '../runtime/systems/Audio';
|
||||
|
||||
const DEFAULT_BASE = '/game-maker/data/sounds';
|
||||
|
||||
/** One sound in flight: its source, its own gain, and the file it came from. */
|
||||
/** One sound in flight: its source, its own gain, the file it came from and who is making it. */
|
||||
interface Voice {
|
||||
file: string;
|
||||
/** The behavior this voice belongs to; `''` for one nothing owns (see `AudioSink.play`). */
|
||||
owner: string;
|
||||
source: AudioBufferSourceNode;
|
||||
gain: GainNode;
|
||||
}
|
||||
@@ -42,37 +44,46 @@ export class SoundLibrary implements AudioSink {
|
||||
*/
|
||||
private readonly stops = new Map<string, number>();
|
||||
private allStops = 0;
|
||||
/** The same counter per owner, for the same reason – a behavior destroyed while bytes were in
|
||||
* flight must not be given its voice afterwards. */
|
||||
private readonly ownerStops = new Map<string, number>();
|
||||
|
||||
/** Where the files are served from. Set once at start-up, before anything plays. */
|
||||
configure(base: string): void {
|
||||
this.base = base.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
play(file: string, volume: number, loop: boolean): void {
|
||||
play(file: string, volume: number, loop: boolean, owner = ''): void {
|
||||
if (file === '' || this.failed.has(file)) return;
|
||||
const buffer = this.buffers.get(file);
|
||||
if (buffer) {
|
||||
this.start(file, buffer, volume, loop);
|
||||
this.start(file, buffer, volume, loop, owner);
|
||||
return;
|
||||
}
|
||||
const askedAt = this.stopsFor(file);
|
||||
const askedAt = this.cancelsFor(file, owner);
|
||||
void this.load(file).then((loaded) => {
|
||||
// A `Stop sound` that happened while the bytes were in flight has to win, or a sound the
|
||||
// graph already silenced would start afterwards and play on its own.
|
||||
if (loaded && this.stopsFor(file) === askedAt) this.start(file, loaded, volume, loop);
|
||||
// A `Stop sound` – or the destruction of the behavior that asked – that happened while the
|
||||
// bytes were in flight has to win, or a sound the run already ended would start afterwards.
|
||||
if (loaded && this.cancelsFor(file, owner) === askedAt) this.start(file, loaded, volume, loop, owner);
|
||||
});
|
||||
}
|
||||
|
||||
stop(file: string): void {
|
||||
const going = file === '' ? this.voices : this.voices.filter((voice) => voice.file === file);
|
||||
for (const voice of going) voice.source.stop();
|
||||
// `onended` removes each one, but not before the next frame – the count has to be right now.
|
||||
this.voices = this.voices.filter((voice) => !going.includes(voice));
|
||||
this.stopped += going.length;
|
||||
this.silence(file === '' ? this.voices : this.voices.filter((voice) => voice.file === file));
|
||||
if (file === '') this.allStops += 1;
|
||||
else this.stops.set(file, (this.stops.get(file) ?? 0) + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Every voice of one behavior. An empty owner is deliberately no one: those are the voices that
|
||||
* were started to outlive their behavior, so "stop the owner" must not reach them.
|
||||
*/
|
||||
stopOwner(owner: string): void {
|
||||
if (owner === '') return;
|
||||
this.silence(this.voices.filter((voice) => voice.owner === owner));
|
||||
this.ownerStops.set(owner, (this.ownerStops.get(owner) ?? 0) + 1);
|
||||
}
|
||||
|
||||
/** Every voice at once: what ending a run does, so nothing outlives the world that started it. */
|
||||
stopAll(): void {
|
||||
this.stop('');
|
||||
@@ -137,7 +148,14 @@ export class SoundLibrary implements AudioSink {
|
||||
return work;
|
||||
}
|
||||
|
||||
private start(file: string, buffer: AudioBuffer, volume: number, loop: boolean): void {
|
||||
/** Ends these voices now: `onended` only fires next frame and the counts have to be right here. */
|
||||
private silence(going: Voice[]): void {
|
||||
for (const voice of going) voice.source.stop();
|
||||
this.voices = this.voices.filter((voice) => !going.includes(voice));
|
||||
this.stopped += going.length;
|
||||
}
|
||||
|
||||
private start(file: string, buffer: AudioBuffer, volume: number, loop: boolean, owner: string): void {
|
||||
const context = this.audio();
|
||||
// A context starts suspended where the page has not been interacted with; without this the
|
||||
// voice would run silently and end as if it had played.
|
||||
@@ -149,7 +167,7 @@ export class SoundLibrary implements AudioSink {
|
||||
gain.gain.value = Math.max(0, Math.min(1, volume));
|
||||
this.last = { file, volume: gain.gain.value, loop };
|
||||
source.connect(gain).connect(context.destination);
|
||||
const voice: Voice = { file, source, gain };
|
||||
const voice: Voice = { file, owner, source, gain };
|
||||
source.onended = () => {
|
||||
this.voices = this.voices.filter((other) => other !== voice);
|
||||
};
|
||||
@@ -162,9 +180,9 @@ export class SoundLibrary implements AudioSink {
|
||||
return (this.context ??= new AudioContext());
|
||||
}
|
||||
|
||||
/** Stops this file has seen, its share of the "everything" ones included. */
|
||||
private stopsFor(file: string): number {
|
||||
return (this.stops.get(file) ?? 0) + this.allStops;
|
||||
/** Everything that would have silenced this file for this owner, counted rather than flagged. */
|
||||
private cancelsFor(file: string, owner: string): number {
|
||||
return (this.stops.get(file) ?? 0) + this.allStops + (this.ownerStops.get(owner) ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user