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

melonjs-input

melonJSでキーボード、マウス、タッチ、ゲームパッドの入力を扱う方法を説明します。操作を共通化するアクション設定や、クリック判定、座標の選び方も確認できます。

原文原文の説明を見る

Use this skill for keyboard, pointer, mouse, touch and gamepad input in melonJS. Covers the bindKey action indirection, registerPointerEvent on regions, the isKinematic requirement that silently blocks pointer events, which of several overlapping objects receives an event and how to consume it, world vs screen pointer coordinates, and gamepad mapping onto the same actions. Triggers on: input, bindKey, isKeyPressed, KEY, registerPointerEvent, releasePointerEvent, pointerdown, pointerup, pointermove, POINTERMOVE, gamepad, bindGamepad, triggerKeyEvent, KEYDOWN, KEYUP, press any key, splash screen, click, touch, drag, hit test, event propagation, stop propagating, overlapping, which object gets the click.

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

実行スコア 未検証

インストール方法を見る

こんなときに便利

  • キーとゲームパッドの操作を共通化したいとき
  • タッチやポインター操作を追加したいとき
  • 重なったオブジェクトの入力先を制御したいとき
  • ドラッグ時の座標ずれを直したいとき

日本語での紹介

できること

キーボードやゲームパッドの入力を名前付きアクションに割り当て、移動やジャンプなどのゲーム処理からデバイス差を切り離す方法を扱います。マウスやタッチはポインターイベントとして登録し、重なったオブジェクトのどれが入力を受けるか、イベントをどこで止めるかを調整できます。画面座標・ワールド座標・コンテナ内座標の使い分け、ドラッグ中の追跡、仮想操作からキー入力を発生させる方法も確認できます。

こんなときに便利

クリックが反応しない、重なったボタンが同時に動く、カメラをドラッグすると位置が跳ねる、といった問題の調査に向いています。キーボードとゲームパッドで同じ操作を実現したい場合や、タッチ操作を加えたい場合にも役立ちます。

使い方の例

  • 「左右キーとゲームパッドで同じ移動操作ができるようにしてください」
  • 「重なったボタンのうち、手前だけがクリックを受けるようにしてください」
  • 「画面上のドラッグでカメラを動かしてください」

注意点

ポインター対象にする通常のRenderableやSpriteでは、isKinematic = falseの設定が必要です。ポインターイベント名にはpointerdownなどを使い、clickやmousedownは指定できません。

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

含まれるファイル(1)

  • SKILL.md14.4 KB

SKILL.md(原文)

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

Input: keyboard, pointer and gamepad

Keyboard goes through named actions

Do not poll key codes. Bind a key to an action string, then poll the action — this is what lets gamepad and virtual controls map onto the same handling.

import { input } from "melonjs";

input.bindKey(input.KEY.LEFT,  "left");
input.bindKey(input.KEY.RIGHT, "right");
input.bindKey(input.KEY.X,     "jump", true);   // true = locked: one press per keydown

// in update()
if (input.isKeyPressed("left"))  { /* … */ }
if (input.isKeyPressed("jump"))  { /* fires once per press, not per frame */ }

The third argument to bindKey is lock (default false). Without it, isKeyPressed stays true for as long as the key is held — correct for movement, wrong for jump or fire. A locked action stays consumed until the key is released or you call input.unlockKey("jump"). Use input.keyStatus(action) when you want the raw held/not-held state regardless of locking.

A poll can miss a press entirely

isKeyPressed is sampled once per frame, and keyup decrements the key's counter the moment it arrives — locked or not. A press that goes down and up between two frames therefore leaves the counter back at zero and is never seen. At 60fps that is anything under ~16ms: rare from a human finger, routine from a synthetic event, and more likely the slower the frame rate.

For a screen where a missed press is a dead end — a splash, a "press any key", a notice to acknowledge — listen for the event instead, which cannot be missed:

import { event } from "melonjs";

// action is the bound action name, or undefined for an unbound key
const onKey = (action) => { if (action === "menuSelect") continue(); };
event.on(event.KEYDOWN, onKey);      // in onResetEvent
event.off(event.KEYDOWN, onKey);     // in onDestroyEvent — it does not self-remove

KEYDOWN fires for every key, bound or not, so matching on action is what narrows it; it also repeats while the key is held, so guard anything that must happen once. Keep polling for movement and for a menu cursor, where one step per press and a key held across a state change are the things that matter.

The full signature is bindKey(keyCode, action, lock?, preventDefault?); preventDefault defaults to input.preventDefault (itself true). Pass the fourth argument per binding, or change the global default with input.setPreventDefault(false) — assigning input.preventDefault = false does not work, it is a read-only module binding.

Pointer events need isKinematic = false

This is the trap. Renderable.isKinematic defaults to true, and both the broadphase and the pointer dispatcher skip any candidate whose isKinematic is still true. The handler registers successfully and simply never fires.

class Button extends Renderable {
    constructor(x, y, w, h) {
        super(x, y, w, h);
        this.isKinematic = false;               // ← required
    }
    onActivateEvent() {
        input.registerPointerEvent("pointerdown", this, this.onDown.bind(this));
    }
    onDeactivateEvent() {
        input.releasePointerEvent("pointerdown", this);
    }
}

Container, Camera2d, Draggable, DropTarget and the UI* elements set it to false for you, and so does attaching a physics body (Body's constructor sets ancestor.isKinematic = false) — so plain Renderable and Sprite subclasses with no body are the ones that catch people.

Register in onActivateEvent and release in onDeactivateEvent, not in the constructor — pooled objects are reused and would otherwise accumulate handlers.

app.viewport works as a whole-screen region when you want global clicks. Note it is asked before everything else, not as a fallback after nothing matched: the dispatcher appends it to the candidate list and walks that list from the end. So a viewport handler must not return false unless it really means to swallow every pointer event in the game.

The accepted event names are exactly "pointerdown", "pointerup", "pointermove", "pointercancel", "pointerenter", "pointerover", "pointerleave" and "wheel". Anything else — "click", "mousedown", "touchstart" — makes both registerPointerEvent and releasePointerEvent throw invalid event type. melonJS maps the canonical name onto whichever mouse/touch events the device actually supports, so you always register the pointer name.

Which object gets the event

The dispatcher collects the regions near the pointer from the broadphase, bounds-checks each one, orders them the way they are drawn and asks the topmost first, walking down until something consumes.

A handler consumes by returning false. This is the DOM sense and it reads backwards: false means handled, and anything else, undefined included, lets the walk carry on to whatever is underneath.

input.registerPointerEvent("pointerdown", this, () => {
    this.fire();
    return false;        // nothing under this gets the click
});

Three things decide the order, and only the first is obvious:

  • pos.z, among siblings of one container. Higher is on top.
  • Across containers, the common ancestor decides. pos.z is container-local, since autoDepth numbers each container's children from 1, so a button at z = 8 inside one panel does not outrank a second panel at z = 2. The pair is resolved by walking both up to their shared parent and comparing the two siblings there, which is exactly how drawing resolves it. Raise a whole panel with ancestor.moveToTop(panel), not by inflating a child's z.
  • A child is asked before the container holding it, since it draws over it.

Equal z between siblings falls back to child order, lower index on top. Two callbacks registered on the same region for the same event run last-registered-first.

Consuming a move also takes the pointer away from the regions below: each one still holding it gets its pointerleave, because it is covered now and being inside its own bounds no longer decides. Consuming a press, a release or a wheel does not, since none of those says where the pointer is.

World coordinates versus screen coordinates

The pointer carries several, and picking the wrong one produces a feedback loop:

propertymeaninguse for
pointer.gameScreenX / gameScreenYcanvas-relative positionanything that moves the camera
pointer.gameWorldX / gameWorldYprojected into world spacepicking and spawning objects
pointer.gameLocalX / gameLocalYrelative to the region's ancestor containerhit-testing inside a container
pointer.gameX / gameYcontext-dependent — see belowregion handlers on non-floating regions

gameX/gameY is not a fixed coordinate space. The dispatcher rewrites it per candidate just before calling your handler: world coordinates for a normal region, screen coordinates for a isFloating === true region. On the global event.POINTERMOVE broadcast it is set to the screen coordinates. When you need one specific space, read gameWorldX/gameWorldY or gameScreenX/gameScreenY by name rather than relying on gameX.

Using world coordinates for a camera-orbit drag means the same pixel maps to a different world point every frame as the camera moves — the drag jumps wildly.

Pointer move and release outside a region

Region handlers only fire while the pointer is over the region. For drag tracking, listen globally:

import { event } from "melonjs";
event.on(event.POINTERMOVE, (pointer) => { /* … */ });

There is no global pointer-up system event — event.ts publishes only POINTERMOVE, POINTERLOCKCHANGE, DRAGSTART and DRAGEND. For "released anywhere" you need a raw listener:

globalThis.addEventListener("pointerup", onRelease);

For plain drag-and-drop, Draggable / DropTarget already wire all of this up (pointerdown / pointerup / pointercancel regions plus a global POINTERMOVE listener) and emit event.DRAGSTART / event.DRAGEND. DRAGEND still originates from a region handler, so it only fires when the release lands on the draggable itself.

Gamepad

Gamepad input maps onto the same action strings, so game code does not branch:

input.bindGamepad(0,
    { type: "buttons", code: input.GAMEPAD.BUTTONS.FACE_1 },
    input.KEY.X);        // → the "jump" action bound above

Order matters: bindGamepad throws no action defined for keycode N if the keycode has not already been passed to bindKey.

Axes use the same call with a threshold, and bind one half of the axis each — the sign selects which half:

input.bindGamepad(0, { type: "axes", code: input.GAMEPAD.AXES.LX, threshold: -0.15 }, input.KEY.LEFT);
input.bindGamepad(0, { type: "axes", code: input.GAMEPAD.AXES.LX, threshold:  0.15 }, input.KEY.RIGHT);

threshold is added to the deadzone, it does not replace it. The test is abs(value) >= deadzone + abs(threshold), so with the default deadzone of 0.1 a threshold of 0.5 does not fire until 0.6 of full travel, and 0.15 fires at 0.25. Pick the number you want the stick to break at and subtract the deadzone. Raising setGamepadDeadzone moves every axis binding with it.

unbindGamepad(index, button, type?), setGamepadDeadzone(value) and setGamepadMapping(id, mapping) complete the surface.

Which index to bind

Bindings are per pad index, and the index is plug order, not identity — nothing reports which pad is "the" controller. Slot 0 is a guess: a wheel, a spare pad or a keyboard-shaped device left plugged in can hold it for a whole session while the player presses buttons on slot 1.

So a one-player game binds every slot it will accept to the same actions, and lets activeGamepad() say which one is in use when something has to go back the other way:

for (let pad = 0; pad < 4; pad++) {
    input.bindGamepad(pad, { type: "buttons", code: input.GAMEPAD.BUTTONS.FACE_1 }, input.KEY.X);
}
input.activeGamepad();   // the last pad to produce a bound input, else the
                         // first connected one, else -1

Binding spare slots costs nothing: the poll skips a bound index with no pad in it. activeGamepad() follows bound inputs only, which is what makes it an answer about the player rather than about electrical noise — a worn stick at rest reports movement for ever and would otherwise claim to be the active pad.

Rumble

input.vibrateGamepad(input.activeGamepad(), {
    duration: 120, strongMagnitude: 0.6, weakMagnitude: 0.9,
});
input.stopGamepadVibration(index);   // cut a long effect short
input.hasGamepadVibration(index);    // for an options-screen toggle

Safe to call blind: no pad, no motors, or no Gamepad API at all resolves false and does nothing, so game code never branches on support. Magnitudes default to full — the underlying web API defaults every parameter to zero, which makes a bare playEffect a silent no-op. A second call preempts the first on the same pad, so a rapid series of hits reads as a rapid series of hits rather than as a queue. For a SUSTAINED rumble issue one effect with a long duration instead of calling every frame.

Hot-plugging needs no polling of your own — the engine listens for the browser's own events and republishes them:

import { event } from "melonjs";

event.on(event.GAMEPAD_CONNECTED,    (gamepad) => { /* gamepad.id, .index */ });
event.on(event.GAMEPAD_DISCONNECTED, (gamepad) => { /* pause, show a prompt */ });

Bindings survive a disconnect: they are attached to the index, not to the device, so a pad unplugged and plugged back into the same slot keeps working.

Virtual controls

On-screen buttons synthesise key events, again reaching the same actions:

input.triggerKeyEvent(input.KEY.LEFT, true);    // press
input.triggerKeyEvent(input.KEY.LEFT, false);   // release

Symptom → cause

symptomcause
pointer handler never firesisKinematic left at its true default
handlers fire twice, or on dead objectsregistered in the constructor instead of onActivateEvent
camera drag jumps aroundusing world coordinates instead of gameScreenX/gameScreenY
gameX holds screen coords where world coords were expectedread on event.POINTERMOVE, or on a floating region — use gameWorldX/gameWorldY
drag stops when leaving the objectregion events end at the region — use event.POINTERMOVE
release never detected outside the regionno global pointer-up event — add a raw listener
jump fires every frame while heldbindKey without its third lock argument
invalid event type thrown at registrationnon-pointer name ("click", "mousedown", …) passed to registerPointerEvent
no action defined for keycode NbindGamepad called before bindKey for that keycode
stick has to be pushed well past its threshold before it registersthreshold is ADDED to the deadzone, not used instead of it
one half of a stick does nothingeach half is a separate bindGamepad, chosen by the sign of threshold
a widget under an overlapping panel still reactsthe panel consumes nothing — return false from its onClick and onOver
a widget stays highlighted after the pointer moves onto a panel over itthe panel consumes no pointermove, so nothing takes the pointer off the widget
a child's big z does not put it in front of another containerpos.z is container-local — use ancestor.moveToTop() on the container
every click in the game stops workinga handler on app.viewport returning false, which is asked first

Related skills

  • melonjs-renderables — isKinematic also gates the physics broadphase
  • melonjs-ui-and-text — UISpriteElement and UITextButton handle this for you

レビュー

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

コメント

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

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

送信するとすぐに公開されます。メールアドレスや電話番号などの個人情報は書かないでください。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 のスキルをすべて見る

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