【enchant.js】Hello Physical World vol.1【box2d】

最近、このブログに「box2d.enchant.js」で検索かけて辿り着く方が多いようです。*1
こんな記事も書いているわけですからさもありなん。しかし改めて読み返すと……結構つっけんどんですよね。もっと初歩からキチンと書いとけや、と。

というわけで、box2d.enchant.jsについて改めて記事を起こしてみることにしました。題して「Hello Physical World」! はじまり、はじまり~!

そも、Box2Dとは何なのでしょうか。
Wikipediaには「質量・速度・摩擦といった、古典力学的な法則をシミュレーションするゲーム用の2D物理演算エンジン」とあります。ゲーム用の2D物理演算エンジン、いいですね! 実際に使ってもらうと分かるのですが、プログラマーが特に処理を書かなくても、モノは落ちてくれるは床で跳ねるわジャンプの軌跡もなめらかと、一見いいこと尽くめですw もちろん、それ故の制約ってのもあるんですが、それはオイオイ……。

で、このBox2Dをenchant.jsでも手軽に使えるようにしたのがbox2d.enchant.jsなのです。
box2d.enchant.jsはbox2dwebをラップしたpluginで、@さんが開発されたPhySprite.enchant.jsがベースとなっています。*2

enchant.js+Box2Dというと、日経ソフトウエアの記事を思い出される方もいらっしゃるかと思います。後日ITproにも転載され、どうやらムックにも再掲されているようです。こちらで使われているのはBox2DJSというライブラリです。

さて。
物理演算、物理エンジンと聞くと何だかものすごそうで、尻込みしてしまうかと思います。でも気後れする必要はありません。確かにライブラリを作るとなると大変ですが、そのライブラリを利用する限りにおいてはそんなことは全くないのですから。
それではさっそく、ひとつコードを書いてみましょう。

enchant.jsでBox2Dを使うには、まずindex.htmlでBox2dWeb-2.1.a.3.jsとbox2d.enchant.jsを読み込むよう指示する必要があります。

<!doctype html>
<html>
<head>
    <script src='enchant.js'></script>
    <script src='plugins/libs/Box2dWeb-2.1.a.3.js'></script>
    <script src='plugins/box2d.enchant.js'></script>
    <script src='main.js'></script>
    <title>Box2D Sample</title>
</head>
<body>
</body>
</html>

次はコーディングですが、その前にまずmain.jsのテンプレートを用意します。

enchant();

window.onload = function() {
    var core = enchant.Core(320, 320);
    core.onload = function() {
        //物理世界の構築
        var world = new PhysicsWorld(0.0, 9.8);
        
        core.rootScene.onenterframe = function(e) {
            //物理世界の時間を進める
            world.step(core.fps);
        };
    };
    core.start();
};

構築した物理世界の時間を1ステップごと進め、その結果を画面上のオブジェクトに反映させるのが、Box2Dでのゲーム開発の基本です。「結果を画面上のオブジェクトに反映させる」などと書くと身構えられちゃいそうですが、box2d.enchant.jsではそのへんもキチンと考慮されており、通常のSprite配置と同じ感覚で扱えます。

それではこのテンプレートの解説に参りましょう。ポイントは2つです。
まずは7行目のPhysicsWorldです。このクラスをインスタンス化することで物理世界を構築します。引数は第一項目がx軸の重力、第二項目がy軸の重力です。x軸の重力は通常ありませんので0.0です。y軸は1G=9.8m/s^2ですので9.8を入れます。
もうひとつがonenterframe内で呼び出されているPhysicsWorld.stepメソッドです。引数は物理世界のフレームレートです。ゲームのフレームレートを入れておけばいいでしょう。

準備はここまでです。
次は実際にリンゴを床に落としてみましょう。

enchant.jsではゲーム画面にリンゴを登場させる際、Spriteでオブジェクトを生成し、そこにリンゴの画像を貼り付けSceneに加えます。

var apple = new Sprite(16, 16);
apple.image = core.assets['icon0.png'];
apple.frame = 15;
core.rootScene.addChild(apple);

box2d.enchant.jsでも同じです。リンゴを登場させるには以下のように記述します。

var apple = new PhyCircleSprite(8, enchant.box2d.DYNAMIC_SPRITE, 1.0, 0.5, 1.0, true);
apple.image = core.assets['icon0.png'];
apple.frame = 15;
core.rootScene.addChild(apple);

異なるのはリンゴを生成するクラスがSpriteではなくPhyCircleSpriteなところですね。
まずは通しでコードを書いて、実際にリンゴが自由落下するところを見てみましょう。

たったこれだけのコードでリンゴが落ちていってくれました。
次はこのコードに床を足して、リンゴを跳ねさせてみましょう。

お気付きでしょうか。
リンゴと床を生成するクラスが異なっています。名前から分かる通り、PhyCircleSpriteは円を生成するクラス、PhyBoxSpriteは矩形を生成するクラスです。ともにPhySpriteをベースとしています。PhySpriteとはSpriteの拡張クラスで、物理世界の動きをSpriteに伝える役目を担います。PhySpriteを直接扱うことはありませんが、メンバは当たり前のように利用しますので注意して下さい。

では、PhyCircleSpriteについて解説してゆきましょう。
第一項目は円の半径です。実際に作成されるSpriteの縦横は、指定した値の倍の長さになります。
第二項目は生成したオブジェクトの、物理世界でのタイプを指定します。タイプには以下のふたつがあります。*3

  • enchant.box2d.DYNAMIC_SPRITE
    • 動的なオブジェクトです。一言で言うと動きます、動かせます。
  • enchant.box2d.STATIC_SPRITE
    • 静的なオブジェクトです。動きません。

第三項目は密度です。重さですね。同じ容積のオブジェクトでも密度が違えば動きが変わります。
第四項目は摩擦係数で、値は0.0~1.0の範囲を取ります。
第五項目は反発係数で、一般には0.0~1.0の範囲で指定します。1.0以上も指定可能です。
第六項目は物理演算を最初から適用するかどうかを指定します。trueであればゲームが始まった途端オブジェクトが重力に従って落ちてきますが、falseの場合は何らかの契機があるまで静止状態になります。
次はPhyBoxSpriteですが、第一項目と第二項目はそれぞれSpriteの幅と高さを指定するもので、続く項目はPhyCircleSpriteの第二項目以降と同じです。

何だか初回から盛りだくさんになっていますが、もうすこしお付き合い下さい。

imageやframeといったプロパティはいいですよね。これはSpriteのプロパティと同じものです。
ですがpositionというプロパティはSpriteにもEntityにもNodeにもありません。このプロパティはPhySpriteのもので、中心座標ベクトルを指定します。いいですか、中心座標です。Spriteの左上座標ではありません。

さて。
今回のソースはこれでひと通りの説明が終わりました。
でも、まだまだ謎なことばかりですよね。反発係数1.0と0.5の物体がぶつかったらどうなるのか、摩擦係数が違うもの同士はどのように影響を与えるのか……。
ゆっくりとですが、いろいろと実験し公開してゆきたいと思います。

*1:ちょっと前までは「くうドール」だったのにねw

*2:PhySprite.enchant.jsの方が開発が進んでおり多角形とジョイントを利用することができますが、まだenchant.js v0.6には未対応のようです。

*3:Box2D自身には、b2_dynamicBody(DYNAMIC_SPRITE)とb2_staticBody(STATIC_SPRITE)の他にb2_kinematicBodyというタイプが存在します。これはDYNAMICとSTATICの間の子のようなタイプみたいなのですが、詳細が分かっておりません。また、box2d.enchant.jsでは除かれています。

【enchant.js】TShape.js作りました【プラグイン】


というtweetを見たので、マルや多角形を描画するライブラリを作ってみました。
ソース

サンプルはこちら

【tmlib.js】プログラミングぞめ【box2d】

あけましておめでとうございます。
本年もよろしくおねがいします。

はてさて。わたくし、休暇中はほとんどプログラミングをやらない人間で、先日仕事の折(4日から仕事ですよ!)にちびーっといぢって見ました。成果は以下の通り。
http://jsdo.it/blogparts/8Nc4

tmlib.js、ほとんど使っていませんw

【enchant.js】Nodeの表示順 v0.6対応版【Advent Calendar】

はてなのアクセス解析のぞくと、Nodeの表示順で検索されて当方のブログに辿り着く方がいらっしゃるようで。enchant.jsもバージョンが上がったことですし、ここいらで再度まとめてみたいと思います。

まずは基本のおさらい。
enchant.jsでは、addChildをすると手前に、手前にという具合にNodeを重ねていきます。

for (var i = 0; i < 3; i++) {
    var spr = new Sprite(32, 32);
    spr.image = game.assets['chara1.png'];
    spr.frame = i;
    spr.x = i * 8;
    game.rootScene.addChild(spr);
}

上述のコードの場合、最後にaddChildされたframe == 2のSpriteがもっとも手前に表示され、その後ろにframe == 1、frame == 0と続きます。

enchant.jsには、もうひとつNodeをGroupに追加する関数があります。それがinsertBeforeです。上述のコードのfor文の後に、以下のコードを加えて下さい。

var ins = new Sprite(32, 32);
ins.image = game.assets['chara1.png'];
ins.frame = 5;
ins.x = 14;
game.rootScene.insertBefore(ins,game.rootScene.lastChild);

表示されるSpriteの順が、手前からframeの値が2、5、1、0となっているのが分ります。

では、今度はGroupが介在したらどうなるのでしょう。

var grp1 = new Group();
game.rootScene.addChild(grp1);
var grp2 = new Group();
game.rootScene.addChild(grp2);

var spr1 = new Sprite(32, 32);
spr1.image = game.assets['chara1.png'];
spr1.frame = 0;
spr1.x = 0;
grp2.addChild(spr1);
var spr2 = new Sprite(32, 32);
spr2.image = game.assets['chara1.png'];
spr2.frame = 5;
spr2.x = 8;
grp1.addChild(spr2);

Spriteだけに注目するとspr1、spr2の順でaddChildされているわけですから、spr2が手前に来るはずです。しかしGroupはgrp1、grp2の順で追加されており、grp2の方が手前にあります。spr1はgrp2に、spr2はgrp1にaddChildされているわけですから、表示も親NodeであるGroupの表示順に左右されます。

ちなみにGroupにも座標があります。

var grp = new Group();
grp.x = grp.y = 160;
game.rootScene.addChild(grp);

var spr = new Sprite(32, 32);
spr.image = game.assets['chara1.png'];
grp.addChild(spr);

子Nodeは親Nodeの座標に左右されることが分ります。
気になるのは当たり判定ですね。上述のコードに以下を足して下さい。

var obj = new Sprite(32, 32);
obj.image = game.assets['chara1.png'];
obj.y = 160;
game.rootScene.addChild(obj);

grp.onenterframe = function() {
    this.x -= 4;
    if (obj.intersect(spr)) console.log("ok");
}

相対的な座標ではなく、画面上の絶対座標で当たり判定が行われていることが分ります。(前は違ったんだよね。苦労した......)

「同じGroupに所属させつつも表示位置を細かく設定したい」という場合はどうでしょうか。

var grp = new Group();
game.rootScene.addChild(grp);

var node = new Node();
grp.addChild(node);

var spr1 = new Sprite(32, 32);
spr1.image = game.assets['chara1.png'];
spr1.frame = 0;
spr1.x = 0;
grp.addChild(spr1);
var spr2 = new Sprite(32, 32);
spr2.image = game.assets['chara1.png'];
spr2.frame = 5;
spr2.x = 8;
grp.insertBefore(spr2,node);

至極簡単な話でして、マーカーとなる非表示のNodeをまず設置します。そしてそれを軸に表示するNodeを親Nodeに追加してゆきます。上述のコードは2レーンですが、3レーンにする場合はさらに非表示Nodeを追加します。「走れ! ハチ!」の輪っかくぐりはこれの応用なんです(Group分けはしてないですけどね)。

最後にいくつか。
こちらでも指摘したaddChildの問題、未だ健在です。一度addChildされたNodeを再度addChildしなおすと、表示順は変更されるのですが親NodeのchildNodesにカラの領域を残してしまうことになり、childNodes.lengthの値がおかしくなってしまいます。一度addChildされたNodeは、きちんとremoveChildしてから再度addChildしなおしましょう。
私もzindexを操作する裏技を紹介したことがありますが、v0.6から操作できなくなりました。v0.6以降を使う人は注意しましょう。

【enchant.js】「セント・メリーの銃弾」を投稿しました【9leap】

そうそう。
こちらで告知するのを忘れていました。
9leapに新作ゲーム「セント・メリーの銃弾」を投稿しました。
プレイヤーはメリーとなって、サンタに紛れて盗みを働こうとする泥棒たちを狙撃するゲームです。ぜひ遊んでみてください。

【enchant.js】TCard.jsはv0.6対応の夢をみるか【Advent Calendar】

先日書いたAdvent Calendar記事では@さんのtrakpad.enchant.jsを取り上げましたが、思わぬ方向からブーメランが飛んできました。

そっかー。TCard.js、v0.6だと動かないのかー。
そっかー……。

で、ここで終わったら何とも悲しい。せっかくなのでTCard.jsをv0.6に対応させつつ、それをAdvent Calendarのネタにしてしまいましょー! というのが今回のお話。

まずはこちらからTCard.jsのソースをダウンロードしてきてください。次にzipを解凍したら、中のenchant.jsを最新版に置き換え、index.htmlを開いて見て下さい。

Error: superclass is undefined
    throw new Error("superclass is undefined");     enchant.js (202 行目)

ほむほむ。@さんの指摘通り、enchant.jsの201行目あたりでエラーが出ていますね。それではこのあたりを調べてみましょう。

まずこちら。v0.6のエラー該当箇所前後の抜粋です。

enchant.Class.create = function(superclass, definition) {
    if (superclass == null){
        throw new Error("superclass is undefined");
    }
    if (arguments.length === 0) {
        return enchant.Class.create(Object, definition);
    } else if (arguments.length === 1 && typeof arguments[0] !== 'function') {
        return enchant.Class.create(Object, arguments[0]);
    }

次いでこちら。TCard.js開発当時の最新版(v0.5)の同じところです。

enchant.Class.create = function(superclass, definition) {
    if (arguments.length === 0) {
        return enchant.Class.create(Object, definition);
    } else if (arguments.length === 1 && typeof arguments[0] !== 'function') {
        return enchant.Class.create(Object, arguments[0]);
    }

エラーも鑑みるに、v0.6から加えられた以下の部分が問題なのは一目瞭然ですね。

    if (superclass == null){
        throw new Error("superclass is undefined");
    }

「よっし。それじゃあ、ここをコメントアウトだ!」
ちょっと待ってください。
このようなエラーチェックが加えられたということは、何らかの意図があるはずです。それに、enchant.jsの新バージョンが出るたびに該当部分をコメントアウトするのは非効率です。ここはひとつ、TCard.jsの方を改造しましょう!(ぢゃないとここで話が終わっちゃうもんね)

はてさて。
対応するにはまずエラーの意味を理解しないといけません。ちょっと公式ドキュメントをのぞいて見ましょうか。

Parameters:
{Function} superclass Optional
    継承するクラス.
{*} definition Optional
    クラス定義. 

……Optional。ウソを吐け、ウソを! 必須じゃないか!
ぬわーん! 公式のバグじゃんかよ~ん。

……
………
………………

いや、本当にバグだろうか。
もしかしたらドキュメントの修正漏れかもしれない。
こんな時は便利なtwitter。とりあえずtweetしてみる。


すると……

開発者の@さんからご回答頂きました!
superclassはOptional扱い、必須なのはdefinitionの方なんですね*1

というわけで、TCard.jsの1行目を以下のように書き換えてみて下さい。

TCard = enchant.Class.create({});

そしてindex.htmlを再度ブラウザで読み込むと……はい! エラーが表示されず、ちゃんと画面が出てきましたね!
OK! これで終わりです。
あとは@さんも言及されている「enchant.Game」の「enchant.Core」への書き換えですね。これは必須の書き換えではないのですが、将来的にGameが廃止される可能性もあるので、今のうちに書き換えておいた方が無難でしょう。

……はい、何か?
これで終わりかって? 終わりなんですよ*2
TCard.jsがenchant.jsに依存している部分って、クラス定義と画像周りだけですからね。enchant.jsが如何に変わろうとも、ほとんど影響を受けないんですよ。また、多言語への移植も容易です。そもそも今のバージョンも、ActionScriptで書かれたものをほとんど変更なく流用しています(TCard.setImageとTCard.getCardだけがenchant.js verから加わったものです)*3

TCard.jsは見ての通りとても単純なライブラリです。これを元に、UNO用カードのライブラリ等、他のゲームのカードセットを作ってみるのも面白いんじゃないかと思います。
お次は誰だー!

*1:現時点では公式ドキュメントも修正されています。

*2:自分でもビックリしました。

*3:さらに言っておくと、TCard.js及びその前身のソースは、友人がC++用に書いたモノを私が他言語に移植したものです。@Buravo46さんが「読む人が読めばわかりやすい」とほめて下さっていますが、それは友人に向けられるべき称賛だったりします。