melonJSを使ったゲーム制作で、目的に合う専門ガイドを選ぶための入口です。初期化から描画、入力、物理演算、音声、2D・3D機能まで案内します。
- melonJSで作る内容に合うガイドを探したいとき
- 新しいゲームの初期化手順を知りたいとき
- 3Dやモデル読み込みの参照先を探したいとき
melonJSのベクトル・行列計算を扱い、値が変更されるメソッドや補間、プールの使い方、3D成分が意図せず変わる引数の落とし穴を確認できます。
Use this skill for vector and matrix maths in melonJS — Vector2d, Vector3d, their observable variants, Matrix2d, Matrix3d, and the math utility functions. Covers which methods mutate and which allocate, the vector pool and when clone() is the wrong tool, interpolation with lerp, slerp, damp and moveTowards, and the argument defaults that silently drop a component. Triggers on: Vector2d, Vector3d, ObservableVector2d, ObservableVector3d, Matrix2d, Matrix3d, vector, vector maths, dot, cross, normalize, length, distance, angle, lerp, slerp, damp, moveTowards, project, clamp, degToRad, radToDeg, clone, vector pool, vector3dPool, setMuted, setCallback, my vector is wrong, z is zero, scale ignores z., moveTowards ignores z, projectN, currentTransform, setTransform, matrix, transform matrix, column-major, val, Float32Array, basis matrix, orientation matrix, Noise, getNoise1d, flicker, wobble.
実行スコア 未検証
インストール方法を見るmelonJSのVector2d・Vector3dやObservable系ベクトル、Matrix2d・Matrix3d、数学ユーティリティを使う際の判断材料をまとめています。メソッドが元の値を書き換えるのか、新しい値を作るのか、補間関数を用途に応じてどう選ぶかを確認できます。ベクトルプールやclone()の扱い、行列データの並び方、scaleやsetで引数を省いたときの挙動も説明します。
ベクトル計算の結果で元の値が変わって困ったときや、Z成分が変化しない・ゼロになる原因を探すときに役立ちます。毎フレームの処理で不要な生成を避けたい場合、方向の補間やフレームレートに左右されない追従を実装したい場合にも参照できます。描画対象の変換行列へ直接値を書き込む方法も扱います。
melonJSのAPIを前提とした内容です。
この紹介文は、公開されている SKILL.md をもとに AI(OpenAI Codex)が作成しました。正確な仕様は下の原文を確認してください。
インストールする前に、エージェントに与えられる指示の中身を確認できます。
Methods mutate this and return it. They do not return a new vector.
const a = new Vector3d(1, 0, 0);
const b = new Vector3d(0, 1, 0);
a.cross(b); // a IS NOW the cross product; the original a is gone
a.add(b).normalize().scale(3, 3, 3); // chains, all in place
That is what makes the API allocation-free, and it is the opposite of what a functional style expects. If you need to keep the original, copy into something you own first:
out.copy(a).cross(b); // a untouched, nothing allocated
The exceptions are named. A handful of methods return a NEW vector, and
each has a *Self sibling that does not:
| allocates a new vector | mutates in place |
|---|---|
negate() | negateSelf() |
floor() | floorSelf() |
ceil() | ceilSelf() |
clamp(low, high) | clampSelf(low, high) |
clone() | copy(v) |
So v.negate() in a per-frame loop allocates every frame and v.negateSelf()
does not. The names are the only clue; nothing in the call site shows it.
clone() comes from a pool, and only pays off if you give it backclone() is vector3dPool.get(...). A pool is fast when instances come back:
get() takes a free one and resets it. Nothing comes back on its own, so a
clone that is simply dropped makes get() miss, construct, and write two
registry entries.
Measured, 3M operations:
out.copy(a).cross(b) | 39ms |
new Vector3d(ax, ay, az).cross(b) | 37ms |
a.clone().cross(b) | 2919ms |
So:
new Vector3d(). Not clone().clone() and vector3dPool.release(v)
when finished. Releasing is what makes the pool a pool.out:const cross = (a, b, out = new Vector3d()) => out.copy(a).cross(b);
That is the engine's own convention, and it lets a hot caller supply scratch while a casual one ignores it.
A returned value can never be shared scratch. The next call overwrites something the caller is still holding, and nothing fails loudly:
const _t = new Vector3d();
const bad = (v) => _t.copy(v).normalize(); // WRONG: two calls alias
Three signatures quietly discard what you did not pass. All three are reachable by writing the obvious thing.
scale(x, y = x, z = 1) — z defaults to 1, not to x. A uniform
scale has to say so three times:
v.scale(2); // (1,2,3) -> (2,4,3) z UNTOUCHED
v.scale(2, 2, 2); // (1,2,3) -> (2,4,6) what you meant
set(x, y, z = 0) — setting two components zeroes the third:
v.set(10, 20); // z is now 0, whatever it was
Vector3d.pos on a renderable is typed Vector2d. Renderable.pos is an
ObservableVector3d at run time but is declared through Polygon as a
Vector2d, so .z, .setMuted and .setCallback need a cast from
TypeScript. See issue #1510.
cross is two different functionsv3.cross(other); // Vector3d: MUTATES v3, returns this
v2.cross(other); // Vector2d: returns a NUMBER, mutates nothing
The 2D one is the z component of the 3D cross, which is the useful scalar for "which side is this on". Same name, different shape, different return.
a.lerp(b, t); // straight line, t in 0..1
a.slerp(b, t); // along the ARC, constant angular rate
a.damp(b, lambda, dt); // frame-rate independent ease toward b
a.moveTowards(b, step); // fixed distance per call, never overshoots
moveTowards is X/Y ONLY, on Vector3d as well as Vector2d. It is the
one of the four that is not 3D: it measures the distance from dx and dy,
steps along atan2(dy, dx), and leaves z exactly where it was. The source
says so in a comment and the JSDoc says "on the x and y axis", but the name
gives no hint, and in a list like the one above it reads as the sibling of the
other three. Anything moving in three dimensions wants damp or an explicit
step; reach for moveTowards only when the motion really is planar.
lerp between two directions is uneven: it slows at the ends, runs fast
through the middle, and shortens the vector as it passes halfway. Anything
whose direction matters more than its components wants slerp — a camera
swinging to look somewhere, a turret tracking, a body moving over a sphere.
slerp carries length linearly, so two unit vectors give a unit vector and
nothing needs normalising afterwards.
damp is the one to use for "ease toward a target every frame", because
lerp(current, target, 0.1) per frame is frame-rate dependent: it converges
twice as fast at 120fps as at 60.
project scales THIS, and projectN is the one without the dividea.project(b) does not return the component of a along b. It scales a
by a.b / |b|^2 and returns a, so the result points along a, not along
b. Worth knowing before you reach for it expecting the textbook projection.
projectN(v) is the same thing for a v that is already unit, so it skips
the / v.length2():
a.project(b); // a scaled by (a.b / |b|^2)
a.projectN(b); // a scaled by (a.b) -- only correct when |b| = 1
Handing projectN a target that is NOT unit does not normalise anything, it
just scales by the raw dot product. That is the price of it being cheaper, and
it is the caller's job to meet the contract.
Until 20.9 both 3D classes got this wrong: Vector3d.projectN and
ObservableVector3d.projectN were copies of project, division and all,
under projectN's name and its "slightly more efficient" doc comment. Nothing
in the engine called either one and nothing tested them, which is how it
survived. Both are fixed and all four classes are now pinned by tests.
length() is sqrt(x*x + y*y + z*z)Fast and correct to 1 ULP for anything a game holds. It is NOT robust at
extremes: components above ~1e154 overflow to Infinity and below ~1e-162
underflow to 0, because the squares are formed before the root. Math.hypot
avoids that and costs 35 to 65% more. The engine takes the speed, which is the
right trade for coordinates between 1e-3 and 1e6; nothing in a game is 148
orders of magnitude from unit scale.
length2() skips the square root. Use it for comparisons:
if (a.distance(b) < r) { } // a square root you did not need
if (a.sub(b).length2() < r * r) { } // same answer, cheaper
ObservableVector2d / ObservableVector3d call a callback on every write,
which is how a renderable knows its bounds went stale.
v.setCallback(() => { ... });
v.setMuted(true); // write without firing it
setMuted is what you want when writing several components, so a
half-updated vector does not fire a callback that reads the other half.
set() already does this internally: it disables the callback, writes all
three, fires once, and re-enables.
Note the observable set(x = 0, y = 0, z = 0) defaults EVERY argument, so
set(10, 20) zeroes z exactly as the plain vector does.
Matrix2d is 3x3 for 2D affine transforms; Matrix3d is 4x4. Both mutate and
chain, same as vectors, and both come from pools.
m.identity().translate(x, y).rotate(angle).scale(sx, sy);
Order is the reverse of how it reads. These post-multiply, so the call made LAST reaches the vertex FIRST. To rotate a thing about its own centre, translate to the centre last.
Renderable.currentTransform is a Matrix3d. If you are orienting something
on screen, the matrix exists: write into it rather than building a parallel
structure and copying.
// the whole transform of a renderable, written in place
mesh.currentTransform.identity().translate(x, y, z).rotate(angle, axis);
val, column-majorComposing with translate/rotate/scale is one way to fill a matrix. The
other is to write the numbers, which is what you want when the orientation
comes from a basis you have already computed — three orthonormal vectors, a
surface frame, a camera basis.
val is a Float32Array(16) in column-major order, the OpenGL
convention: each basis vector occupies a stride-4 run, and the translation is
the last column.
| X column | Y column | Z column | translation | |
|---|---|---|---|---|
| indices | 0,1,2 | 4,5,6 | 8,9,10 | 12,13,14 |
// an orientation from three axes, straight into the renderable's own matrix
const m = mesh.currentTransform.val;
m[0] = right.x; m[1] = right.y; m[2] = right.z; m[3] = 0;
m[4] = up.x; m[5] = up.y; m[6] = up.z; m[7] = 0;
m[8] = fwd.x; m[9] = fwd.y; m[10] = fwd.z; m[11] = 0;
m[12] = 0; m[13] = 0; m[14] = 0; m[15] = 1;
A helper that builds one should take the destination, the same way the engine
takes an out, so it writes once into the matrix that will be used:
const orient = (frame, out) => { /* writes out.val */ return out; };
orient(frame, mesh.currentTransform);
setTransform(...) takes the sixteen as arguments, and copy(m) takes
another matrix:
m.setTransform(a, b, c, d, e, f, g, h, i, j, k, l, p, q, r, s);
m.copy(other); // Float32Array.set under the hood
Both are fine occasionally. In a per-frame loop, prefer writing val or
copy() over spreading sixteen arguments, and note that one matrix built
once and applied to many renderables is a copy() per renderable, not a
rebuild per renderable.
val is Float32Array, so it holds float32. Numbers you put in are
rounded to single precision. That is where they were always heading, since
this is what reaches the GPU, but it means a value read back out is not
bit-identical to the double you wrote.
import { math } from "melonjs";
math.clamp(v, lo, hi);
math.lerp(a, b, t); // scalars
math.damp(a, b, lambda, dt); // frame-rate independent
math.degToRad(deg); math.radToDeg(rad);
math.random(lo, hi); // integer
math.randomFloat(lo, hi);
math.weightedRandom(lo, hi); // integer, biased toward LO
math.round(n, decimals);
math.pow(n); // n squared
math.DEG_TO_RAD math.RAD_TO_DEG math.TAU math.ETA math.EPSILON
Noise, the generator behind NoiseTexture2d, also samples directly. For a
value that should drift without repeating (a torch's flicker, a sway, a camera
wander), sample it with time:
import { Noise } from "melonjs";
const noise = new Noise({ seed: 7, frequency: 1.5, octaves: 3 });
// around 1; give each object its own offset so they do not move together
const flicker = 1 + 0.2 * noise.getNoise1d(seconds + offset);
getNoise1d returns [-1, 1] (with fractal octaves most values fall within
about ±0.6), frequency is in changes per unit of the input, and the same seed
gives the same field every run. A sum of sines repeats where people notice it,
and Math.random() every frame jitters instead of drifting.
| symptom | cause |
|---|---|
| z unchanged after scaling | scale(s) leaves z alone; pass scale(s, s, s) |
| z became 0 for no reason | set(x, y) defaults z to 0 |
| the original vector changed | the method mutates; copy() into something first |
| memory climbs in a loop | clone() or negate()/floor()/ceil() per frame |
| two helper results are the same object | a shared scratch vector was returned |
.z is a type error | Renderable.pos is declared Vector2d (#1510) |
| rotation eases oddly between directions | lerp on directions; use slerp |
| you built your own 16-number array | Renderable.currentTransform is already a Matrix3d; write its val |
| a value read back from a matrix differs | val is Float32Array; it holds float32 |
| easing differs at 120fps | per-frame lerp; use damp |
| a flicker or sway looks mechanical, or visibly repeats | a sum of sines or a per-frame Math.random(): sample Noise#getNoise1d(time) |
melonjs-3d — Camera3d, meshes, and where these vectors are used in 3Dmelonjs-physics — bodies, velocities and collision responsemelonjs-performance — pooling in general, and what release() is forまだレビューはありません。使ってみた感想をお寄せください。
ログインなしで書き込めます。使ってみた感想、質問、動かなかった環境などを気軽にどうぞ。
まだコメントはありません。最初のコメントを書いてみませんか。
melonJSを使ったゲーム制作で、目的に合う専門ガイドを選ぶための入口です。初期化から描画、入力、物理演算、音声、2D・3D機能まで案内します。
melonJS 19.x以前の知識を20.x向けに更新し、初期化や描画効果、廃止・非推奨APIの変更点を確認して移行時の不具合を調べられます。
melonJSの3D・2.5Dゲーム向けに、カメラ、メッシュ、ライト、奥行き順、透明表現やフォグの設定と座標系の注意点を確認できます。
melonJSでglTF・GLBやOBJ・MTLの3Dモデルを読み込み、マテリアルやライト、アニメーション、影、インスタンスの扱い方を確認できます。
melonJSで効果音やBGMを読み込み、再生・停止・音量調整するほか、位置に応じた音や音声合成、ミックス全体へのエフェクト設定を扱うスキルです。
melonJSのカメラ追従や画面効果、座標変換、図形描画を実装する際の要点を確認できます。描画のクリッピングやCanvasへの焼き込み、ピクセルの読み出しも扱います。