本文へ移動
cccskills
無料GitHub で公開日本語紹介

melonjs-scenes-and-state

melonJSの画面やゲーム進行をStageとstateで組み立て、遷移・一時停止・更新処理・タイマー・Tweenを適切に扱う方法を確認できます。

原文原文の説明を見る

Use this skill for scene structure and game flow in melonJS — Stage subclasses, the state manager, transitions, pausing, the update loop, timers and tweens. Covers onResetEvent versus the constructor, state.set before state.change, the camera and world lifecycle per stage, and pause-aware timing. Triggers on: Tween, easing, fade out, flash, pulse, punch, shrink back, ease in, ease out, animate over time, countdown in update, Stage, state, state.set, state.change, state.transition, state.pause, onResetEvent, onDestroyEvent, onActivateEvent, onDeactivateEvent, PLAY, MENU, LOADING, update loop, GAME_UPDATE, timer, setTimeout, setInterval, Tween, freeze.

melonjs/melonJSスター 6,429(GitHub 6,429 + cccskills 0)

実行スコア 未検証

インストール方法を見る

こんなときに便利

  • メニューとプレイ画面を切り替えたいとき
  • 再入場時にシーンを組み直したいとき
  • 一時停止に合わせてタイマーを扱いたいとき
  • Tweenで値や見た目を変化させたいとき

日本語での紹介

できること

melonJSで画面をStageとして構成し、state managerでゲームの状態を切り替える方法を説明します。ステージの登録と変更、再入場時に呼ばれるonResetEvent、終了時の後片付け、画面遷移や一時停止の扱いを確認できます。毎フレームの更新でdtを使う考え方や、一時停止に対応するエンジンタイマー、一定時間で値を変化させるTweenの使い方も扱います。

こんなときに便利

メニューからプレイ画面へ切り替える構成を作りたいときや、ステージへ戻った際にシーンを正しく作り直したいときに役立ちます。一時停止中にタイマーが進む、Tweenが動かないなど、時間処理の不具合を調べる際にも使えます。

使い方の例

  • 「メニューからプレイ画面へ切り替えるStageを作って」
  • 「一時停止中にゲーム内タイマーが進まないようにしたい」
  • 「スプライトを一定時間で縮小させるTweenを設定して」

注意点

ブラウザー標準のwindow.setTimeoutはゲーム一時停止中も動作します。一時停止に合わせるタイマーにはmelonJSのtimerを使います。

この紹介文は、公開されている SKILL.md をもとに AI(OpenAI Codex)が作成しました。正確な仕様は下の原文を確認してください。

含まれるファイル(1)

  • SKILL.md17.5 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Scenes, game state and timing

Stages and the state manager

A Stage is a screen. Register it against a state id, then change to it:

import { Stage, state } from "melonjs";

class PlayScreen extends Stage {
    onResetEvent(app) { /* build the scene */ }
    onDestroyEvent(app) { /* tear it down */ }
}

state.set(state.PLAY, new PlayScreen());   // register first
state.change(state.PLAY);                   // then switch

Built-in ids: state.LOADING, state.MENU, state.READY, state.PLAY, state.GAMEOVER (no underscore), state.GAME_END, state.SCORE, state.CREDITS, state.SETTINGS, state.DEFAULT. state.USER (100) is the base for your own ids — const CUTSCENE = state.USER + 1.

Build scene content in onResetEvent, not the constructor. onResetEvent runs on every entry to the state; the constructor runs once. Scene content created in the constructor exists once and is not rebuilt when you return.

onResetEvent(app) receives the Application — use that rather than importing the global game.

Extra arguments to state.change are forwarded:

state.change(state.PLAY, false, { level: 2 });   // → onResetEvent(app, { level: 2 })

The second argument is forceChange: pass true to switch immediately instead of on a deferred tick, which is what you want when changing state right after a preload completes. It is ignored when a transition is configured — a transition always drives the switch from its own completion callback.

onResetEvent runs when the switch is made. The first frame, STATE_CHANGE and a transition's reveal follow once the shaders the stage queued are ready, which is a few milliseconds once the browser has compiled them before, and at most 1.5s on a first visit. So build the stage in onResetEvent: an object a STATE_CHANGE listener adds is not waited for, and its first draw may still compile what it needs. That first frame steps the world once before drawing it, however short it is, so everything onResetEvent added is in it (the same goes for the frame after a level.load made while the game runs). Before 20.12 it was drawn unstepped, and showed the stage's background alone for one frame.

state.change throws Undefined Stage for state '<id>' if nothing was registered for that id, and state.set throws if the second argument is not a Stage instance. Neither fails quietly.

Always leave state.LOADING — it is transitional

loader.preload() switches to state.LOADING for you. Building your scene in the preload callback and then staying there looks like it works, and is one of the easiest mistakes to make:

// WRONG — the game now runs inside the loading stage, forever
loader.preload(assets, () => {
    app.world.addChild(new Player(...));
});

Nothing ever destroys that stage, so the built-in loading screen's logo and progress bar are never removed and sit on top of your game. (Eight of this repository's own examples did this before it was noticed, so it is not an obscure trap.)

Give the scene a stage of its own and switch to it:

class PlayScreen extends Stage {
    onResetEvent(app) {
        app.world.addChild(new Player(...));
    }
}

await loader.preload(assets);
state.set(state.PLAY, new PlayScreen());
state.change(state.PLAY);

The awaited form reads better here than the callback, because "load, then switch" is exactly what the code then says. If you genuinely want to keep using the loading stage as your scene, state.change(state.DEFAULT) at least gets you out of it — but a stage of your own is what you want.

Persistent objects across levels

if (typeof this.HUD === "undefined") {
    this.HUD = new Container();         // construct once (or UIBaseElement)
}
this.HUD.isPersistent = true;           // survives level changes
this.HUD.floating = true;               // screen coordinates, not world
app.world.addChild(this.HUD, 100);      // explicit z — see melonjs-renderables

Transitions and pausing

state.transition("fade", "#FFFFFF", 250);
state.transition("mask", "#000", 500, new Ellipse(0, 0, 1, 1));  // shape required
state.transition("pixelate", "#000", 300);  // blocks up to 32px; a 4th argument sets the size
state.pause();                               // freeze update, keep drawing
state.resume();
app.freeze(150);                             // brief hit-stop (proxies state.freeze)

state.transition is global and sticky — once set it applies to every later state.change. The "mask" form needs its fourth argument, an Ellipse or Polygon; without one it warns and falls back to a direct switch. The "pixelate" form breaks the screen up into blocks (its fourth argument is the largest block size in pixels). It is a shader effect, so on Canvas it does nothing and the state changes directly.

A state.change asked for while a transition covers the screen (Enter pressed twice on a menu, a timer firing as the player skips a screen) needs no guard: the transition already under way makes it, switching to the last state asked for with the last arguments, and a second cover never goes up. A change to a state with no transition of its own (state.setTransition(id, false)) is made at once, and the cover comes down with it. Before 20.12 the second change left the first cover on the camera for good, the screen black over a running game.

A Stage's own update() is NOT paused

This one is easy to get wrong and hard to see. Application.update calls both of these unconditionally:

this.isDirty = this.world.update(this.updateDelta);
this.isDirty = state.current().update(this.updateDelta) || this.isDirty;

Only Container.update consults the pause — it reads state.isPaused() and skips every child that is not updateWhenPaused. So world children (entities, sprites, meshes, a GLTFModel) freeze correctly, while the Stage subclass's own update() keeps running.

A game that keeps its logic in entities pauses for free and never notices. A game that drives the simulation from Stage.update — common for an endless runner, where one object moves and the world scrolls past it — keeps simulating through pauseOnBlur with its world frozen. The signature is bizarre and very misleading: the scenery scrolls on, the player character stands still in world space and slides off the screen, and it all snaps back on focus. It reads as a culling or animation bug, not a pause bug.

Guard it explicitly:

update(dt) {
    super.update(dt);
    if (state.isPaused()) {
        return true;        // world children are already frozen for you
    }
    // …simulation…
}

Worth knowing that pauseOnBlur (default true) fires on a window blur, not only on tab visibilitychange — so clicking another window on the same screen triggers it while the canvas is still fully visible.

The update loop

There is no loop you own. Logic goes in update(dt) overrides, or a GAME_UPDATE subscription for scene-level animation:

event.on(event.GAME_UPDATE, () => { /* per frame */ });

update(dt) must return true when the object changed and needs redrawing. Returning nothing is falsy and the object can appear frozen while its state moves.

dt is milliseconds since the last frame and is the right basis for motion — the engine already paces it. Do not reach for performance.now().

Smooth a value toward a target with math.damp, not with lerp per frame. lerp(current, target, 0.1) once per frame moves further on a slow frame than on a fast one, so the feel drifts with the frame rate:

this.speed = math.damp(this.speed, targetSpeed, 5, dt / 1000);

damp(current, target, lambda, dt) takes SECONDS, so divide dt. Higher lambda converges faster. See melonjs-camera-and-drawing for the camera's own follow() damping, which is already frame-rate independent for you.

A common idiom for a per-frame game controller with nothing to draw is a Renderable(0, 0, 1, 1) with alwaysUpdate = true and an empty draw().

Timers: use the engine's, not the window's

import { timer } from "melonjs";

timer.setTimeout(fn, 1000);
const id = timer.setInterval(fn, 500);
timer.clearInterval(id);        // in the Stage's onDestroyEvent

Stage has only onResetEvent and onDestroyEvent — there is no onActivateEvent / onDeactivateEvent on a stage. Those two are Renderable hooks; put per-renderable teardown there instead.

The outgoing stage is destroyed before the incoming one is reset. That ordering is what makes "each stage owns what it started" work: a scene can release a resource in onDestroyEvent and the next scene can claim the same one in onResetEvent without the teardown arriving late and undoing it.

What a stage starts, it must end: input.registerPointerEvent regions, event.on subscriptions and running Tweens all outlive the stage that made them. Music is the exception rather than the rule, since audio.playTrack() stops the track it replaces, so stopTrack() in onDestroyEvent is only needed where the next screen has no music of its own.

Engine timers are pause-aware. window.setTimeout keeps firing while the game is paused, which produces enemies spawning behind a pause menu. Pass false as the third argument — timer.setInterval(fn, 500, false) — for one that keeps running through a pause, which is what a pause-menu animation or a countdown that should not freeze needs. (That argument was ignored by setInterval before 20.3.)

Tweens

You are hand-rolling a tween if you write this

The shape, in any of its spellings. If you are adding a field that counts milliseconds and dividing it by a constant to get a 0..1, stop:

// ✗ a tween, written out longhand
private flashMs = 0;
// ... in update(dt)
if (this.flashMs > 0) {
    this.flashMs -= dt;
    const k = this.flashMs / FLASH_MS;      // ← the 0..1
    effect.setUniform("uIntensity", k * k); // ← and a curve, by hand
}
// ✓ the same thing
const driver = { k: 1 };
new Tween(driver)
    .to({ k: 0 }, { duration: FLASH_MS })
    .easing(Tween.Easing.Quadratic.In)      // the curve, named
    .onUpdate(() => effect.setUniform("uIntensity", driver.k))
    .start();

The tells, any one of which is enough: a *Ms or *Age field that only update() touches; a division by a *_MS constant; k * k, (1 - k) ** 2 or ** 1.4 written inline, which is an easing curve spelled by hand; an if (x <= 0) arm that exists only to clean up at the end, which is onComplete; a second timer counting the same duration to chain what happens next, which is delay.

It matters beyond tidiness: a hand-rolled fade needs something to tick it, and an effect that is NOT a renderable (one living on the camera, say) has nothing that will. That forces an update(dt) method and a call site in the game loop whose only job is to drive the fade. A tween updates itself, so both disappear.

When NOT to reach for one

Four shapes that look similar and are not tweens:

shapewhat it isuse
approaching a value that keeps MOVINGa follow, not an animationmath.damp
a loop that never endsoscillationa sine of your own clock
a gate: cooldown, spawn interval, invulnerability windowa timer, nothing is interpolateda plain countdown
the same effect on dozens of live objectsper-instance churna hand-rolled countdown really is cheaper

The rule in one line: fixed duration, known endpoint, one-shot, few instances. All four, or it is not a tween.

import { getPool, Tween } from "melonjs";

const t = getPool("tween").get(sprite.pos) // typed pool, release when done
    .to({ x: 300 }, { duration: 500 })     // options object, not a number
    .easing(Tween.Easing.Quadratic.Out)
    .onComplete(() => { /* … */ })
    .start();                    // ← without this, nothing happens

Four traps:

  • A tween without .start() (or autoStart: true in the to() options) silently does nothing.
  • to()'s second argument is an options object ({ duration, easing, yoyo, repeat, delay, repeatDelay, interpolation, autoStart }). Passing a bare number is not a duration — it is read for a .duration property, finds none, and the tween silently runs the default 1000 ms.
  • Tweens freeze during state.pause() / freeze() unless tween.updateWhenPaused = true — which is how you get an effect that decays through a hit-stop.
  • Stop a tween before destroying its target, or onUpdate fires against a dead renderable.

To animate something that is not a property, tween a holder and push the value in onUpdate. A shader uniform and a light's intensity are both set through a method, so there is nothing for to() to interpolate directly. The callback is invoked with the tweened object as this and the eased progress 0..1 as its argument:

const fade = { level: 1 };
getPool("tween").get(fade)
    .to({ level: 0 }, { duration: 400 })
    .easing(Tween.Easing.Quadratic.Out)
    .onUpdate(function () {
        effect.setUniform("uIntensity", this.level);
        muzzleLight.intensity = this.level * 3;
    })
    .start();

Use a function, not an arrow, if you want this to be the tweened object; an arrow keeps the enclosing this and you read the holder by name instead. For a handful of pooled objects a hand-rolled countdown in update() is cheaper than a Tween each — this is the right tool for one-off effects, not for every particle.

Pick the easing, not just the duration

Eleven families, each with In, Out and InOut. Out is what most UI wants: the value arrives decisively instead of creeping in. The two worth knowing by name beyond the power curves:

familywhat it does
Backovershoots past the target and settles back, which is impact: a stamp, a button press, anything that should LAND rather than arrive
Bouncelands and bounces, for something dropping onto a surface
// a stamp: scale falls to 1 and springs through it
const driver = { scale: 5 };
new Tween(driver)
    .to({ scale: 1 }, { duration: 420 })
    .easing(Tween.Easing.Back.Out)
    .onUpdate(() => { label.currentTransform.identity(); label.scale(driver.scale); })
    .onComplete(() => camera.shake(12, 400))     // the thump, when it truly lands
    .start();

Back and Elastic leave the range between your start and end, so clamp anything that cannot take it, an opacity or a colour channel above all. The overshoot follows the direction of travel: a value tweened DOWN overshoots below its target.

Reach for onComplete for anything that must happen on arrival, rather than a second timer counting the same duration. delay in the to() options chains the next one without a timer either.

Guard teardown-adjacent callbacks

Callbacks can fire while a stage is being torn down, when the current state is already something else:

if (!state.isCurrent(state.PLAY)) return;

Without the guard, a "children emptied" handler can reset the wrong stage.

Persistence

import { save } from "melonjs";

save.add({ hiscore: 0 });      // once; idempotent, loads any stored value
save.hiscore = 1200;           // plain assignment writes to localStorage

In TypeScript, keep what add() returns. The namespace carries a [key: string]: unknown index for keys registered anywhere, so reading save.hiscore off it gives you unknown. add() hands back the same namespace typed with the keys you just registered, which is the handle to keep:

const store = save.add({ hiscore: 0, lives: 3 });
store.hiscore = 1200;          // number, no cast
if (score > store.hiscore) { /* ... */ }

Chained calls accumulate, so save.add({ a: 0 }).add({ b: "" }) is typed with both.

Symptom → cause

symptomcause
scene builds once and is empty on returncontent created in the constructor, not onResetEvent
Undefined Stage for state 'N' thrownno state.set for that id first
a tween finishes in 1000 ms whatever you passto() takes { duration }, not a number
object frozen while its state updatesupdate() not returning true
timers fire behind a pause menuwindow.setTimeout instead of timer.setTimeout
the world freezes on blur but the game keeps advancingsimulation lives in Stage.update, which the pause does not gate — only Container.update checks state.isPaused()
the player character is left behind and scrolls off after a lost windowsame cause: the stage advanced the camera while its world children were paused
a tween does nothing.start() never called
effect stops during a hit-stoptween needs updateWhenPaused = true
crash after a stage switchcallback ran during teardown — guard with state.isCurrent
HUD scrolls away with the cameramissing floating = true

Related skills

  • melonjs-getting-started — the Application and the first scene
  • melonjs-renderables — floating, draw order, update/draw contracts

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

コメント

ログインなしで書き込めます。使ってみた感想、質問、動かなかった環境などを気軽にどうぞ。

まだコメントはありません。最初のコメントを書いてみませんか。

送信するとすぐに公開されます。メールアドレスや電話番号などの個人情報は書かないでください。URL は 2 つまでです。

0 / 1000 文字

このフォームは reCAPTCHA で保護されており、Google のプライバシーポリシーと利用規約が適用されます。

同じリポジトリのスキル

melonjs

無料概要あり

melonJSを使ったゲーム制作で、目的に合う専門ガイドを選ぶための入口です。初期化から描画、入力、物理演算、音声、2D・3D機能まで案内します。

  • melonJSで作る内容に合うガイドを探したいとき
  • 新しいゲームの初期化手順を知りたいとき
  • 3Dやモデル読み込みの参照先を探したいとき
melonjs/melonJS6,4292026年10月29日 更新

melonjs-20-migration

無料概要あり

melonJS 19.x以前の知識を20.x向けに更新し、初期化や描画効果、廃止・非推奨APIの変更点を確認して移行時の不具合を調べられます。

  • 19.x以前のゲームを20.xへ移行したいとき
  • 古いAPIが見つからない原因を調べたいとき
  • 非推奨の描画効果を置き換えたいとき
melonjs/melonJS6,4292026年10月29日 更新

melonjs-3d

無料概要あり

melonJSの3D・2.5Dゲーム向けに、カメラ、メッシュ、ライト、奥行き順、透明表現やフォグの設定と座標系の注意点を確認できます。

  • 3Dカメラをステージに設定したいとき
  • メッシュやビルボードを配置したいとき
  • 奥行き順や透明表現を調整したいとき
melonjs/melonJS6,4292026年10月29日 更新

melonjs-3d-assets

無料概要あり

melonJSでglTF・GLBやOBJ・MTLの3Dモデルを読み込み、マテリアルやライト、アニメーション、影、インスタンスの扱い方を確認できます。

  • glTF・GLBのシーンを読み込みたいとき
  • OBJモデルとMTL素材を使いたいとき
  • モデルの向きや大きさを調整したいとき
melonjs/melonJS6,4292026年10月29日 更新

melonjs-audio

無料概要あり

melonJSで効果音やBGMを読み込み、再生・停止・音量調整するほか、位置に応じた音や音声合成、ミックス全体へのエフェクト設定を扱うスキルです。

  • 効果音とBGMを読み込みたいとき
  • ゲーム内の位置に応じて音を鳴らしたいとき
  • 音声ファイルなしで短い音を作りたいとき
melonjs/melonJS6,4292026年10月29日 更新

melonjs-camera-and-drawing

無料概要あり

melonJSのカメラ追従や画面効果、座標変換、図形描画を実装する際の要点を確認できます。描画のクリッピングやCanvasへの焼き込み、ピクセルの読み出しも扱います。

  • プレイヤーを追うカメラの範囲を設定したいとき
  • ワールド座標と画面座標を変換したいとき
  • 独自描画に図形やグラデーションを使いたいとき
melonjs/melonJS6,4292026年10月29日 更新

melonjs のスキルをすべて見る

このスキルの問題を報告する