今回は国産のJavaScriptゲームエンジンとして定評のあるenchant.jsを触ってみました。
enchant.js
enchant.jsは、ユビキタスエンターテインメントが2011年4月にリリースした、HTML5+JavaScriptベースのゲームエンジンです。この記事の寄稿時ではバージョンはv0.8.0でした。公式サイトから特徴をピックアップすると、
- オブジェクト指向
- マルチプラットフォーム
- イベント駆動(イベントリスナによる非同期処理)
- アニメーションエンジン
- ハイブリッド描画(Canvas API、DOMによる描画に両対応)
- WebGL対応(プラグインによりWebGLを使った3Dゲームに対応)
といった事があげられますが、実際に触ってみない事にはと思いましたので、サンプルを作成しました。
ソースコード
※操作方法 : 移動「↑↓←→」キー、攻撃「space」キー
※使用している画像は公式サイトで配布されている画像素材を若干加工したものです。
enchant.jsで個人的に一番しっくりきたのは継承関係がしっかり設計されていて、ドキュメントを見ればどんなクラスが用意されていて、どんな機能が実装されているのかわかるというところです。良く使うであろうSpriteクラスの継承関係を見てみると以下のようになっています。
enchant.Sprite
(画像表示機能を持ったクラス)
↓
enchant.Entity
(DOM上で表示する実体を持ったクラス。直接使用することはない)
↓
enchant.Node
(Sceneをルートとした表示オブジェクトツリーに属するオブジェクトの基底クラス。直接使用することはない)
↓
enchant.EventTarget
(DOM Event風味の独自イベント実装を行ったクラス. ただしフェーズの概念はなし)
以下のようにSpriteクラスを継承してカスタムクラスを派生させる事も出来ます。
main.js(53行目~)
var Atack = Class.create(Sprite, {
initialize: function (x, y, r) {
Sprite.call(this, 8, 16);
this.x = x;
this.y = y;
this.rotation = r;
this.tl.delay(5).then(function () {
var e = new enchant.Event(REMOVE_ATACK_EVENT);
e.currentTarget = this;
game.rootScene.dispatchEvent(e);
})
}
});
enchant.jsで特に特徴的なのは、ゲームに特化したクラスがいくつも用意されているところです。今回はMapクラスを使用しています。マップに使用する画像と1コマ分のサイズをMapのコンストラクタに渡します。そしてどこにどのコマを表示させるかを指定した配列をloadDataメソッドに渡します。さらにはマップ上の障害物の判定をcollisionDataプロパティに配列で渡す事で、hitTestメソッドでの判定が出来るようになります。サンプルではMapクラスを拡張してFieldクラスを作っています。また編集しやすいようにloadDataに渡す配列には定数で指定したインデックスを渡しています。
main.js(42行目~)
var Field = Class.create(Map, {
initialize: function (image, loadData, collisionData) {
Map.call(this, 32, 32);
this.image = image;
this.loadData(loadData);
if (collisionData) this.collisionData = collisionData;
}
});
main.js(381行目~)
field = new Field(game.assets["map01.png"], [
[FCM, FCM, FCM, FCM, FCM, FCM, FCM, FCM, SLM, SCM],
[FCM, FCM, FCM, FCM, FCM, FCM, FCM, FCM, SLB, SCB],
[FCM, FCM, FCM, FCM, FCM, FCM, FCM, FCM, FCM, FCM],
[FCM, FCM, SLT, SCT, SCT, SRT, FCM, FCM, FCM, FCM],
[FCM, FCM, SLM, SCM, SCM, SRM, FCM, FCM, FCM, FCM],
[FCM, FCM, SLM, SCM, SCM, SRM, FCM, FCM, FCM, FCM],
[FCM, FCM, SLB, SCB, SCB, SRB, FCM, FCM, FCM, FCM],
[FCM, FCM, FCM, FCM, FCM, FCM, FCM, FCM, FCM, FCM],
[FCM, FCM, FCM, FCM, FCM, FCM, FCM, FCM, FCM, FCM],
[FCM, FCM, FCM, FCM, FCM, FCM, FCM, FCM, FCM, FCM],
[FCM, FCM, FCM, FCM, FCM, FCM, FCM, FCM, FCM, FCM],
[FCM, FCM, FCM, FCM, FCM, FCM, FCM, FCM, FCM, FCM]
]);
field2 = new Field(game.assets["map01.png"], [
[WCM, WRM, TRE, BLC, BLC, BLC, TRE, BLC, BLC, BLC],
[WCM, WRM, BLC, TRE, BLC, BLC, BLC, BLC, BLC, BLC],
[WCB, WRB, BLC, BLC, BLC, BLC, BLC, BLC, BLC, BLC],
[BLC, BLC, BLC, BLC, BLC, BLC, BLC, TRE, BLC, BLC],
[BLC, TRE, BLC, BLC, BLC, BLC, BLC, BLC, BLC, BLC],
[BLC, BLC, BLC, BLC, BLC, BLC, BLC, BLC, BLC, BLC],
[BLC, BLC, BLC, BLC, BLC, BLC, BLC, TRE, BLC, TRE],
[BLC, TRE, BLC, BLC, BLC, BLC, BLC, BLC, BLC, BLC],
[BLC, BLC, BLC, BLC, TRE, TRE, WLT, WCT, WCT, WCT],
[BLC, BLC, BLC, BLC, TRE, BLC, WLM, WCM, WCM, WCM],
[BLC, BLC, BLC, BLC, BLC, BLC, WLM, WCM, WCM, WCM],
[BLC, BLC, BLC, BLC, BLC, BLC, WLM, WCM, WCM, WCM]
], [
[1, 1, 1, 0, 0, 0, 1, 0, 0, 0],
[1, 1, 0, 1, 0, 0, 0, 0, 0, 0],
[1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 1, 0, 1],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 1, 1, 1, 1, 1],
[0, 0, 0, 0, 1, 0, 1, 1, 1, 1],
[0, 0, 0, 0, 0, 0, 1, 1, 1, 1],
[0, 0, 0, 0, 0, 0, 1, 1, 1, 1]
]);
イベントリスナによる非同期処理にも対応しています。
そのオブジェクトがenchant.EventTargetを継承していればイベントを受け取る事や発火する事ができます。FlasherになじみのあるENTER_FRAMEやADDED_TO_SCENE、タッチデバイスで使用するTOUCH_START等の様々なイベントを標準で実装しています。
main.js(340行目~)
game.rootScene.addEventListener(enchant.Event.ENTER_FRAME, function () {
if (1 <= enemies.length && 0 == this.age % 3) {
var arr = [];
// アタリ判定
var i = 0, max;
for (i = 0, max = enemies.length; i < max; i = i + 1) {
if (!enemies[i].isDie && player.within(enemies[i], 20)) {
player.die();
}
if (atackGroup.lastChild && enemies[i].intersect(atackGroup.lastChild)) {
enemies[i].die();
} else {
arr.push(enemies[i]);
}
}
enemies = arr;
if (enemies.length <= 0) game.rootScene.dispatchEvent(new enchant.Event(REVIVE_ENEMIES_EVENT));
}
});
カスタムイベントの使用も可能です。イベントオブジェクトはJavaScriptなのでダイナミックに情報を追加する事もできます。
main.js(359行目~)
game.rootScene.addEventListener(ADD_ATACK_EVENT, function (e) {
atackGroup.addChild(new Atack(e.tagX, e.tagY, e.tagR));
});
main.js(89行目~)
var e = new enchant.Event(ADD_ATACK_EVENT);
switch (this.rot) {
case 0:
e.tagR = 90;
e.tagX = this.x + 12;
e.tagY = this.y + 32;
break;
case 9:
e.tagR = 0;
e.tagX = this.x - 8;
e.tagY = this.y + 8;
break;
case 18:
e.tagR = 0;
e.tagX = this.x + 32;
e.tagY = this.y + 8;
break;
case 27:
e.tagR = 90;
e.tagX = this.x + 12;
e.tagY = this.y - 12;
break;
}
game.rootScene.dispatchEvent(e);
アニメーションエンジンも用意されています。以下の例ではキャラクターがやられるアニメーションを実行、その後でカスタムイベント(REVIVE_PLAYER_EVENT)を発火させています。
main.js(164行目~)
die: function () {
if (this.isDie) return;
this.isDie = true;
var scope = this;
this.frame = 0;
this.tl.tween({
rotation: 180,
time: 7
}).tween({
rotation: 360,
time: 7
}).then(function () {
scope.rotation = 0;
}).tween({
scaleX: 0,
scaleY: 0,
rotation: 180,
time: 7
}).then(function () {
scope.visible = false;
game.rootScene.removeChild(this);
game.rootScene.dispatchEvent(new enchant.Event(REVIVE_PLAYER_EVENT));
})
}
といった感じに、様々な機能が用意されています。ゲーム制作に特化されているという印象よりも、ゲーム用に機能が+αされていると認識した方がいいと思います。
まだ触ったばかりでもっと便利なクラスや間違った使い方をしているところもあるかもしれませんが、今後も積極的に使用していこうと思いました。
最後に雑感ですが、機能豊富なクラス群や継承やイベント処理やアニメーションエンジンなどホントに欲しいと思うものが一通り揃っていて、enchant.jsだけで完結できるのは非常にありがたいです。
CreateJSと比較すると、CreateJSはFlashを使用出来るためにアニメーションに関してはやはり強みがありますが、コードが複雑になったり、まだまだ不安定なところもあるかなと思います。enchant.jsに関しては今のところ、挙動が不安定な印象は受けませんでした。メソッドもCreateJS程ではありませんがActionScript3.0と似ていてFlasherにとっては学習コストが低いかと思います。プラグインも多く開発されていて、今後の展開がさらに期待出来そうです。物理演算系や3D系のプラグイン等、夢が広がります。
今度はThree.jsと併用して使うとどんな感じになるのか試してみたいと思います。