melonJSを使ったゲーム制作で、目的に合う専門ガイドを選ぶための入口です。初期化から描画、入力、物理演算、音声、2D・3D機能まで案内します。
- melonJSで作る内容に合うガイドを探したいとき
- 新しいゲームの初期化手順を知りたいとき
- 3Dやモデル読み込みの参照先を探したいとき
melonJSで効果音やBGMを読み込み、再生・停止・音量調整するほか、位置に応じた音や音声合成、ミックス全体へのエフェクト設定を扱うスキルです。
Use this skill for sound and music in melonJS — loading audio, playing effects, background tracks, audio sprites, volume and muting, spatial audio, procedural tone/noise generation, and effects such as reverb or a compressor on the whole mix. Covers the mandatory audio.init format list, the directory-not-file src convention, playTrack versus play, the fixed listener and pixel-scale distances of positional sound, and browser autoplay unlocking. Triggers on: audio, sound, audio.init, audio.play, playTrack, stopTrack, audio.load, sfx, music, volume, mute, fade, rate, pitch, sprite audio, stereo, panner, position, listener, refDistance, reverb, echo, delay, compressor, distortion, filter, audio effect, audio.tone, audio.noise, getAudioContext, getMasterGain, hasAudio, hasFormat, autoplay, no sound, sound stopped working, context suspended, silent after idle, clips silent but tones play.
実行スコア 未検証
インストール方法を見るmelonJSの音声機能を使い、効果音やBGMの読み込み、再生、停止、音量調整を行います。効果音と、現在のトラックとして管理するBGMの使い分け、音声スプライト、再生位置や速度の変更、フェードにも触れます。ゲーム内の位置に応じて音を配置したり、リスナーを動かして距離や左右の聞こえ方を調整したりする方法も扱います。音声ファイルを使わないトーンやノイズの生成、Web Audioを使ったミックス全体へのリバーブなどの適用も対象です。
melonJSでゲームの音を実装するときや、BGMが切り替わらない、音が鳴らないといった問題を調べるときに役立ちます。音声形式の指定、読み込みパス、ブラウザーの自動再生制限、ストリーミング音声で使えない空間音響など、設定時につまずきやすい点も確認できます。
音声ファイルの再生形式は audio.init() で読み込み前に指定します。空間音響やトーン・ノイズ生成、ミックスへのエフェクト適用にはWeb Audioが必要です。ブラウザーでは最初のユーザー操作まで音声再生が保留される場合があります。
この紹介文は、公開されている SKILL.md をもとに AI(OpenAI Codex)が作成しました。正確な仕様は下の原文を確認してください。
インストールする前に、エージェントに与えられる指示の中身を確認できます。
import { audio, loader } from "melonjs";
audio.init("mp3,ogg"); // 1. BEFORE preloading any audio
await loader.preload([
{ name: "cling", type: "audio", src: "data/sfx/" }, // 2. a PREFIX, not a file
]);
audio.init(format) must run before any audio asset is preloaded, and
lists the formats you actually ship. Preloading first throws
target audio extension(s) should be set through me.audio.init() before calling the preloader. It returns false when no audio backend is
available, so it is worth checking. The format list defaults to "mp3".src is a prefix, not a file. The loader appends the asset name plus
each configured extension and uses the first that loads — so
data/sfx/ + cling + mp3 → data/sfx/cling.mp3. Passing a full filename
gives you a 404. A data:audio/... URL is the one exception: it is used
verbatim, prefix and extension skipped.audio.hasAudio() says whether the browser can play audio at all, and
audio.hasFormat("ogg") whether it can play one codec — useful for choosing
the init list or hiding a sound toggle on a device with no audio.
Order matters — melonJS tries the listed formats left to right, so put the
preferred one first. Two formats ("webm,mp3" or "mp3,ogg") is the usual
belt-and-braces for codec coverage. Accepted tokens: mp3, mpeg, opus,
ogg, oga, wav, aac, caf, m4a, m4b, mp4, weba, webm, dolby,
flac.
audio.play("cling"); // fire and forget
audio.play("cling", false, null, 0.5); // loop, onend, volume
const id = audio.play("engine", true); // looping; keep the id to stop it
audio.playTrack("theme"); // background music
audio.stopTrack();
Use playTrack for music, not play. playTrack(name, volume?) always
loops, and it records the name as the current track — which is what
app.pauseOnBlur / resumeOnFocus / stopOnBlur act on, via
state.pause(true) → pauseTrack(). stopTrack(), pauseTrack(),
resumeTrack() and getCurrentTrack() all read that same single slot, so there
is only ever one current track name.
playTrack stops the track it replaces (20.9.0 and later; before that it
only assigned the slot, and the previous bed kept looping underneath where
stopTrack() could no longer reach it). So a scene just states its own music
and nothing cancels anything:
class PlayScreen extends Stage {
onResetEvent() { audio.playTrack("battle", 0.6); }
}
Calling it with the track already playing restarts it, the same as play()
on any other clip. A scene that wants the bed to carry on across a screen
change should not call it again, rather than rely on it doing nothing.
stopTrack() is for where nothing follows: a scene whose successor has no
music of its own, or a game going quiet. state.change destroys the outgoing
stage before it resets the incoming one, so a stopTrack() in
onDestroyEvent and a playTrack() in the next onResetEvent are in that
order and never cancel each other.
To end a track on an event rather than on a screen change, say a run ending
while the player stays on the same screen, audio.fade(name, from, 0, ms)
takes it down gently. A fade to zero is silence, not a stop, so the voice is
still live until something stops it or replaces it.
Every per-clip function throws audio clip <name> does not exist if the clip was
never loaded, and play additionally throws when a named sprite region is
missing.
One file carrying many effects, with named regions in milliseconds:
await loader.preload([{
name: "sfx", type: "audio", src: "data/sfx/",
sprite: { jump: [0, 450], hit: [2000, 250], music: [4000, 12000, true] },
}]);
audio.play("sfx", { sprite: "jump" });
An MP3 encoder pads the end of its file. Chrome and Firefox trim the padding when they decode it, Safari keeps it, so a looping MP3 has a short silence at every loop there (27 ms on a 12 s clip at 22 kHz, measured). Declare a region of the loop's exact length and loop on that: the voice never reaches the padding.
await loader.preload([{
name: "ambience", type: "audio", src: "data/sfx/",
sprite: { loop: [0, 12000] },
}]);
audio.playTrack("ambience", { sprite: "loop", volume: 0.6 });
// or, as a plain clip: audio.play("ambience", { sprite: "loop", loop: true })
Ogg Vorbis carries no such padding.
The optional third element marks a region as looping: it loops unless play() is
told loop: false (before 20.11 it only looped when loop: true was passed as
well). play()'s second argument
takes either the original loop boolean or a PlayOptions object
(sprite, loop, onend, volume, plus the spatial follow, at and
stopWithTarget) — both forms work. Object fields win over the positional
onend / volume arguments when both are supplied.
audio.duration("theme"); // seconds — 0 while still loading
audio.duration("sfx", id); // duration of the region THAT instance plays
audio.playing("theme"); // is anything playing?
audio.playing("theme", id); // is that instance playing?
audio.state("theme"); // "unloaded" | "loading" | "loaded"
stop(), pause() and resume() take an optional instance id too; omit it
and the whole group is affected. audio.stop() with no arguments at all stops
every sound. audio.seek(name) reads the position, audio.seek(name, s, id?)
writes it; audio.rate(name, r, id?) changes speed and pitch together
(0.5..4.0, 1 is normal) and audio.fade(name, from, to, ms, id?) ramps
the volume (0..1, over milliseconds).
audio.setVolume(0.5); // global, 0..1
audio.getVolume(); // read it back
audio.mute("cling"); // one clip — mute(name, id?, shouldMute = true)
audio.unmute("cling");
audio.muteAll(); // everything (audio.disable() is an alias)
audio.unmuteAll(); // (audio.enable() is an alias)
audio.muted(); // global mute state
audio.unload(name) / audio.unloadAll() free the decoded buffers; unload
also clears the current-track pointer if it named that clip.
When the window loses focus the engine mutes all audio, and unmutes it on
focus. That covers sound effects, procedural tone / noise and the mixer
effects, not just the music track that state.pause() pauses. Set
pauseOnBlur: false in the application settings if a clip has to keep playing
in the background.
audio.position("engine", x, y, z);
audio.orientation("engine", x, y, z);
audio.stereo("engine", -1); // -1 left … 1 right
audio.panner("engine", { coneInnerAngle: 90, /* … */ });
position(name, x, y = 0, z = -0.5, id?) and
orientation(name, x, y, z, id?) both default id to "the whole group".
Called with just the clip name, each of the four returns the current value —
stereo gives 0 and position gives [0, 0, 0] before they have ever been
set.
follow and atThe shortest path to positional audio is a play option. Both take world
pixels, the same numbers as pos.x / pos.y, and the engine converts to
audio space for you:
// tracks the renderable every frame, and stops when it is destroyed
audio.play("ufo", { follow: this, stopWithTarget: true });
// pinned to a fixed world point
audio.play("portal", { at: { x: 1200, y: 480 } });
follow and at are mutually exclusive: passing both throws, rather than
quietly picking one. follow re-reads the target's absolute position each
frame, so a renderable nested in a moving container is placed correctly. stopWithTarget is what you
want for a sound that belongs to an entity: without it a looping clip on a
destroyed renderable keeps playing from wherever it last was. audio.unfollow(id)
detaches a sound and leaves it playing where it is.
audio.setListener(player); // tracked every frame
audio.listener(x, y, z); // or place it by hand
audio.listener(); // → [x, y, z] in world pixels
audio.listenerOrientation(fx, fy, fz, ux, uy, uz);
audio.setListener(null); // stop tracking
With a listener in the world, source positions are absolute world
coordinates, not offsets. The older idiom — passing source.pos.x - player.pos.x every frame with the ear stuck at the origin — still works, but
only if you leave the listener at (0, 0, 0); mixing the two double-counts the
player's position.
setListener places the ear at the target's absolute position after the world
updates. A plain renderable contributes position only, so a side-scroller keeps
the default "facing into the screen" and gets left-right panning plus distance
falloff from the one call.
A Camera3d target also contributes its orientation, taken from its own
basis, so turning the camera swings the stereo image with it. That is the whole
of what a posed 3D scene needs:
audio.setListener(app.viewport); // in a 3D scene this is a Camera3d
audio.play("drone", { follow: saucer }); // scene coordinates, directly
Y is down. Every one of these calls takes and returns melonJS world
coordinates, where y grows downward; the Y-up flip Web Audio wants happens
inside the engine. Pass pos.y as it is.
audio.setSpatialDefaults() / getSpatialDefaults() set the panner attributes
a sound placed through follow or at starts from, and the shipped values
assume a 2D game measured in pixels:
| attribute | default |
|---|---|
refDistance | 240 |
maxDistance | 10000 |
panningModel | "equalpower" |
distanceModel | "inverse" |
With those, a placed sound is at full volume out to 240 px and then halves
each time the distance doubles: about half at 480, a quarter at 960, roughly
an eighth at 2000. maxDistance is where it stops getting quieter.
They apply to placed sounds only. A plain audio.play() followed by a
bare audio.position() keeps WebAudio's own defaults, where refDistance is
1 METRE and a source 100 px out is already near silent. That path is
unchanged, which is what keeps old code working.
Widen or tighten the field once, globally:
audio.setSpatialDefaults({ refDistance: 400, maxDistance: 20000 });
audio.panner(name, attrs, id?) still overrides per clip or per voice, and it
merges over the current values rather than replacing them, so writing one field
no longer resets the rest. Called without an id it writes the group
defaults, which is what a clip-wide tuning call looks like.
For a 3D scene where front-to-back matters, panningModel: "HRTF" is worth the
cost; for a 2D one it mostly muddies the stereo image.
stereo and position: last writer winsBoth drive the same per-instance panner node, and the engine swaps the node to
match whichever you called last: stereo() after position() gives you
plain left-right panning, and position() after stereo() gives you 3D
falloff. Neither silently does nothing any more, and the order you call them in
no longer matters.
Still pick one per voice and stay with it. A 2D game usually wants stereo()
alone; reach for position() (or follow / at) when distance falloff
matters.
Spatial audio is WebAudio-only. On a clip loaded with stream: true (or
html5: true, or preload: false) stereo / position / orientation return early and do
nothing — which is exactly the case for the long music tracks people reach for
first. follow and at are equally inert on a streamed clip.
For beeps, blips and noise without shipping files. Both take one options
object, and duration (in seconds) is required:
audio.tone({ freq: 440, duration: 0.2, gain: 0.3, pan: -0.5 });
audio.tone({ freq: [440, 660, 880], duration: 0.4 }); // layered partials
audio.noise({ duration: 0.5, filter: { type: "lowpass", frequency: 800 } });
tone also takes wave (default "sine"), attack (0.005) and pitchSlide
(1). noise takes type ("white" | "pink" | "brown", default
"white"), attack, pan, filter and filterSweep. gain defaults to 0.1
on both.
delay, not setTimeoutBoth take delay — seconds before the sound starts, scheduled on the audio
clock. That is what builds a multi-part sound: a stinger's second note, an
explosion's double-tap.
audio.tone({ freq: 392, duration: 0.12 });
audio.tone({ freq: 587, duration: 0.18, delay: 0.11 });
audio.tone({ freq: [784, 1176], duration: 0.75, delay: 0.23 });
Do not reach for setTimeout here. A timer fires on the main thread, so a busy
frame slips the note late and the sequence audibly loses time — the fault is
easy to blame on the sound design rather than the scheduling. delay is
sample-accurate because the whole envelope is scheduled up front.
Both are silent no-ops when there is no WebAudio context — they bail on
getAudioContext() === null rather than throwing. Neither needs the clip to be
loaded; neither goes through the file-playback path at all.
audio.getAudioContext() and audio.getMasterGain() are the escape hatches for
custom Web Audio work; both return null when audio is unavailable, so guard.
Connect a custom graph to the master gain rather than ctx.destination if you
want it to respect setVolume / muteAll.
There is no built-in reverb, delay, distortion or compressor, and no per-clip
filter on file playback. There does not need to be one for the common case:
every buffered clip and every tone / noise passes through the master gain
on its way to the speakers, so an effect inserted after it applies to the
whole mix.
const ctx = audio.getAudioContext(); // creates the context if needed
const master = audio.getMasterGain();
if (ctx && master) { // null when audio is unavailable
const comp = ctx.createDynamicsCompressor();
master.disconnect(); // master -> speakers becomes
master.connect(comp); // master -> compressor -> speakers
comp.connect(ctx.destination);
}
Any Web Audio node or chain works the same way: a ConvolverNode with an
impulse response for reverb, a DelayNode with a feedback gain for echo, a
BiquadFilterNode for an underwater low-pass, a WaveShaperNode for
distortion. Do it once, at startup; the master gain lives as long as the
context.
setVolume / muteAll still work, because they act on the master gain,
which is now upstream of the effect.stream: true (html5: true) clip
plays through an <audio> element and never reaches the master gain — which
is usually the music. Preload the music buffered if it must go through the
effect.noise has its own filter for generated sounds.master.disconnect(); master.connect(ctx.destination);.Browsers create the audio context in the suspended state and keep it there
until a user interaction. Nothing errors — audio simply does not play, which
reads as a bug. melonJS installs one-shot touchstart / touchend / click /
keydown unlock listeners on document, so the first real gesture anywhere
unlocks it and fires the clip's on.unlock callback. Design for it: start music
on the first click or key press, not on load. The usual way to guarantee one is
a gate screen — a splash, a "press any key", or a health/photosensitivity notice
— whose dismissal is the gesture, so the menu behind it can start its music
unconditionally. Note the unlock listens for keydown too, so a keyboard-only
player is covered.
Unlocking is only the first half. A context that has been running can be stopped later: melonJS suspends it itself after thirty idle seconds to save power, and browsers stop it for their own reasons, a backgrounded tab among them. Recovery is automatic and you do not have to write it: a user gesture or the tab coming back to the front revives it, and a deliberate idle suspension is left alone so the power saving still works.
The symptom worth memorising, because it points straight at the cause:
if tone and noise are audible while loaded clips are silent, the context
is healthy and the engine's playback state is wrong. The generators only need
a live context, where a clip goes through the engine's state machine and can
be parked waiting for a resume. It is never "the context died".
Both routes ask the same owner to resume, so hearing a tone tells you the hardware is back; it tells you nothing about whether the clips waiting on the engine's state have been let go.
That asymmetry was a real Safari-only bug: thirty idle seconds before the first click left the engine waiting on a suspend the browser never confirmed, so every clip queued for the life of the page while generated sound played normally. Fixed in 20.9, and the lesson generalises: a clip that will not play is a state question, not a hardware one.
Playback also starts asynchronously: play() returns an id while the
instance is still starting, so playing() can legitimately be false on the
very next line.
{ name: "sfx", type: "audio", src: "data/sfx/",
sprite: { jump: [0, 450] },
pool: 5, // finished instances kept for reuse — NOT a concurrency cap
rate: 1.0, // initial playback rate, 0.5..4.0
mute: false,
loop: false,
autoplay: false,
preload: true, // false: fetched on first play, then streamed; or "metadata"
format: "mp3", // explicit hint when the URL has no usable extension
stream: true, // long music: stream rather than decode into memory
on: { play() {}, end() {}, stop() {}, unlock() {} },
}
pool (default 5) is a reuse cap on finished voices, not a limit on
simultaneous playback: when the number of ended voices exceeds it, the surplus
are disconnected and dropped. Raise it for a clip triggered in rapid bursts, not
to allow more overlap.
Use stream: true for long music tracks — it plays through an HTML5 <audio>
element instead of decoding the whole file into memory. (html5: true is the
same switch under its backend name.) on accepts the full lifecycle set:
play, pause, stop, end, fade, seek, rate, volume, mute,
unlock.
preload: false keeps a clip out of the preload. Nothing is downloaded for
it until it is first played (audio.play, audio.playTrack, or autoplay).
That play fetches it and streams it, as stream: true does, so the sound starts
as soon as enough of the file has arrived, not once all of it has; a clip never
played is never downloaded. Use it for music a player may never reach (a boss
theme, the ending) to shorten the wait before the game starts.
audio.state(name) is
"unloaded" until its first play, then "loading", then "loaded".stream: true: positional sound does
not apply to it, and it ignores withCredentials (see below).stream: true alone still fetches the clip during the preload; combine it with
preload: false to defer that too. preload: "metadata" only matters for a
streamed clip: the preload fetches just enough to know its duration.
Outside a preload, audio.load(asset, onload?, onerror?) takes the same
descriptor and loads one clip at runtime — for a level's sounds fetched when
the level starts. The loader calls the same function, so the same rules apply.
Re-preloading a manifest that already contains a loaded clip is a no-op —
audio.unload(name) first if you genuinely want to reload it.
Credentials use the same global switch as every other asset type —
loader.setOptions({ withCredentials: true }), not an audio-specific option.
Buffered audio fetches through the shared loader path, so it honours it exactly
as an image or a JSON file does. A streamed clip (stream: true, or
preload: false) does not: it plays
through an <audio> element, which needs a crossorigin attribute rather than
fetch credentials. Preload a clip buffered if it sits behind a login.
Failing clips and setStopOnAudioError(false). The default is to fail the
preload when a clip cannot be loaded after its retries: the error reaches the
loader, so await loader.preload(...) rejects and your catch runs, and the
reason is logged. Turn the flag off and the engine mutes audio, warns
(failed loading <name>, disabling audio) and lets the load continue — the
preload finishes and the game reaches the next scene without sound.
Before 20.4 both halves were broken: with the flag off it disabled audio and failed the preload anyway, and a clip whose bytes arrived but would not decode (a missing file served as HTML by an SPA rewrite, say) reported nothing at all either way — blank screen, empty console.
| symptom | cause |
|---|---|
| audio assets 404 | src given as a full filename instead of a directory prefix |
target audio extension(s) should be set... thrown at preload | audio.init() not called before preloading |
audio clip X does not exist | the clip was never preloaded, or the name is mis-cased — every per-clip call throws this |
| nothing plays until the user clicks | browser autoplay policy — expected, design around it |
playing() is false right after play() | playback starts asynchronously |
| music keeps playing after leaving a scene | used play instead of playTrack, or no stopTrack in teardown |
two music tracks playing at once, and stopTrack() only silences one | melonJS before 20.9.0: playTrack did not stop the previous track, so the scene that started it had to stopTrack() on the way out |
| the music restarts when returning to a screen | playTrack with the track already playing restarts it; don't call it again if the bed should carry on |
stereo / position do nothing | the clip was loaded with stream: true or preload: false — spatial is WebAudio-only |
a preload: false clip never plays, and audio.state() stays "unloaded" | melonJS before 20.12.0 never fetched such a clip; preload it, or update |
| one format works, another does not | that codec is not in the audio.init() list |
| audio behind a login fails, or the preload hangs | loader.setOptions({ withCredentials: true }) — the same switch every asset type uses |
a streamed clip ignores withCredentials | stream: true plays through an <audio> element; preload it buffered instead |
audio.tone(440, {...}) does nothing / errors | tone takes a single options object with a required duration |
tone / noise are silent with no error | no WebAudio context — getAudioContext() returned null |
| a positioned sound is silent, or only audible right next to the player | placed by hand with audio.position(), which keeps WebAudio's metre defaults (refDistance: 1) — use follow / at, or set refDistance in pixels with audio.panner() |
| a positioned sound does not follow the player | nothing is placing it — audio.play(name, { follow: entity }), and audio.setListener(player) once |
| a followed sound pans to the wrong side as the player moves | the listener was moved and positions are still relative to the player — with a listener in the world, pass absolute world coordinates |
| a looping sound outlives the entity that owns it | follow without stopWithTarget: true |
a voice panned with stereo() loses its 3D placement, or the reverse | one panner node per voice and last writer wins — pick one per voice and stay with it |
| a reverb / compressor on the master gain does not affect the music | the music is stream: true, which bypasses the master gain — preload it buffered |
| a looping MP3 drops out for a moment at every loop, in Safari only | Safari keeps the encoder's padding at the end of the file: loop a sprite region of the clip's exact length (see "A loop with no gap") |
| sound effects keep playing after the window loses focus | pauseOnBlur and stopOnBlur are both off, so the engine is deliberately leaving the game running in the background |
melonjs-getting-started — preloading and asset orderingまだレビューはありません。使ってみた感想をお寄せください。
ログインなしで書き込めます。使ってみた感想、質問、動かなかった環境などを気軽にどうぞ。
まだコメントはありません。最初のコメントを書いてみませんか。
melonJSを使ったゲーム制作で、目的に合う専門ガイドを選ぶための入口です。初期化から描画、入力、物理演算、音声、2D・3D機能まで案内します。
melonJS 19.x以前の知識を20.x向けに更新し、初期化や描画効果、廃止・非推奨APIの変更点を確認して移行時の不具合を調べられます。
melonJSの3D・2.5Dゲーム向けに、カメラ、メッシュ、ライト、奥行き順、透明表現やフォグの設定と座標系の注意点を確認できます。
melonJSでglTF・GLBやOBJ・MTLの3Dモデルを読み込み、マテリアルやライト、アニメーション、影、インスタンスの扱い方を確認できます。
melonJSのカメラ追従や画面効果、座標変換、図形描画を実装する際の要点を確認できます。描画のクリッピングやCanvasへの焼き込み、ピクセルの読み出しも扱います。
melonJSゲームのWeb向けビルドやネイティブアプリ化、ミニゲーム配信に向けた準備を進め、ブラウザー対応や容量の注意点も確認できます。