というわけで、ここまでいくつかのエントリを費やして、作ってみたもの・考えたことを、書き留めておきました。
このあと、予定しているエントリは、
- 穴掘り法での迷路自動生成アルゴリズム
- 最短での迷路脱出経路を求めるアルゴリズム
- 迷路の最深部にゴールを配置するアルゴリズム
- 敵移動:迷路内でランダムに動き回るアルゴリズム
- 敵移動:迷路内でプレイヤーを追跡するアルゴリズム
くらいかなー、と。
ここからは、OpenLeapへの投稿のための作業と並行して、少しづつ書いていきます。
というわけで、ここまでいくつかのエントリを費やして、作ってみたもの・考えたことを、書き留めておきました。
このあと、予定しているエントリは、
くらいかなー、と。
ここからは、OpenLeapへの投稿のための作業と並行して、少しづつ書いていきます。
「斜め上」から見下ろしたようなマップ上では、手前側にいるキャラクタのスプライトの上半身が、 奥側のキャラクタのスプライトの下半身を隠すような感じに、前後関係に応じて重ね合わせて表示をしてやると、 ちょっとリアルでイイ感じになります。
こうした描画の際の前後関係を判断する基準として、
という条件のもとならば、「スプライトの左上y座標+スプライトの高さ」の値の大小関係を用いることができます。
そういうわけで、スプライトの y座標の値を更新したときに、自動的に z-index の値のほうも更新されるようなSpriteのサブクラス、 ZSpriteクラス を作ってみることにしました。
ZSprite = new enchant.Class.create(enchant.Sprite,{ initialize: function(w, h){ enchant.util.ExSprite.call(this, w, h); }, moveTo: function(x, y){ this._element.style.zIndex = y + this._height; enchant.Sprite.prototype.moveTo.call(this, x, y); }, moveBy: function(dx, dy){ this._element.style.zIndex = this.y + this._height + dy ; enchant.Sprite.prototype.moveBy.call(this, dx, dy); }, zindex: { get: function(){ return this._element.style.zIndex; }, set: function(zindex){ this._element.style.zIndex = zindex; } }, y:{ get: function(){ return this._y; }, set: function(y){ this._element.style.zIndex = y + this._height; enchant.Sprite.prototype.y.set.call(this, y); } } });
なお、GitHub上でのIssue、「#3 z-indexのプロパティが欲しい」では、 開発者の方が「z-indexによる表示順序の管理は確かに柔軟でその場対応のようなことをやるのには便利なのですが、 様々なプラグイン等を利用した場合に整合性を保つのがやや難しく、enchant.jsでのサポートはしない考えです。」と 述べているので、今後、何か問題が生じる可能性があるのかもしれません(よくわかりません><)。
前のエントリで紹介したNodeのフェードイン・アウト機能を利用しつつ、 enchant.js に標準添付されているプラグイン nineleap.enchant.js を拡張して、次の機能を実現するプラグインを作成します。
これを、nineleap_fade.enchant.js というファイル名で作成するものとします。 なお、この開発にあたっては、次の点に注意しました。
というわけで、以下、これを実現するコードです。
/** * ローカル環境でのデバッグ時に、ゲームオーバー画面のクリックまたはキーダウンで * 「もう一度プレイする」を可能にするためには、この値をtrueにする。 * nineleapに投稿する際にはtrueにしておく。 */ var NINELEAP_RELEASE_MODE = false; var START_IMAGE = 'start.png'; var END_IMAGE = 'end.png'; enchant.nineleap_fade = {}; enchant.nineleap_fade.Game = enchant.Class.create(enchant.nineleap.Game, { initialize: function(width, height) { enchant.nineleap.Game.call(this, width, height); this.sceneTransition = false; this.overwriteLoadEventListener(); }, // enchant.nineleap.Gameにおけるloadイベントリスナを解除・上書き overwriteLoadEventListener: function(){ this._listeners['load'] = []; this.addEventListener('load', function() { this.startScene = new SplashScene(); this.startScene.id = 'startScene'; this.startScene.image = this.assets[START_IMAGE]; this.pushScene(this.startScene); fadeIn(this.startScene, 10); this.endScene = new SplashScene(); this.endScene.id = 'endScene'; this.endScene.image = this.assets[END_IMAGE]; this.addTransitionEventListeners(); }); }, // トランジションのためのイベントリスナを登録 addTransitionEventListeners: function(){ var game = this; this.startScene.addEventListener('touchend', function() { game.startScene.removeEventListener('touchend', arguments.callee); if (game.started == false && game.sceneTransition == false) { game.sceneTransition = true; if (game.onstart != null) game.onstart(); game.onGameStartTouched(); } }); this.endScene.addEventListener('touchend', function(){ if(game.sceneTransition == false){ game.sceneTransition = true; game.endScene.removeEventListener('touched', arguments.callee); game.onGameEndTouched();//fadeout endScnene and popScene } }); this.addEventListener('keydown', function() { if (game.currentScene == game.startScene && game.sceneTransition == false){ game.sceneTransition = true; game.removeEventListener('keydown', arguments.callee); if (game.started == false) { if(game.onstart != null) game.onstart(); game.onGameStartTouched();//fadeout startScnene and popScene } } }); this.addEventListener('keydown', function() { if (game.currentScene == game.endScene && game.sceneTransition == false){ game.sceneTransition = true; game.removeEventListener('keydown', arguments.callee); game.onGameEndTouched();//fadeout endScnene and popScene } }); }, //ユーザによるゲーム開始画面のタッチ後に実行される関数, onGameStartTouched: function(callback){ var game = this; game.started = true; gameStart = true; // deprecated fadeOut(game.startScene, 10, function(){ if(game.currentScene == game.startScene){ game.popScene(); } if(callback){ callback(); } game.sceneTransition = false; }); }, //ゲームオーバーのときの終了処理を実行する end: function(score, message){ this.started = false; enchant.nineleap.Game.prototype.end.call(this, score, message); fadeIn(this.endScene, 10); }, //ユーザによるゲーム終了画面のタッチ後に実行される関数 onGameEndTouched: function(callback){ var game = this; gameStart = false; // deprecated fadeOut(game.endScene, 10, function(){ if(game.currentScene == game.endScene){ game.popScene(); } }); if(NINELEAP_RELEASE_MODE){ return; } fadeOut(game.getGameNode(), 10, function(){ if(game.reset){ game.reset(); } fadeIn(game.getGameNode(), 10, function(){ game.pushScene(game.startScene); fadeIn(game.startScene, 10, function(){ game.addTransitionEventListeners(); if(callback){ callback(); } game.sceneTransition = false; }); }); }); }, /** * ゲーム終了・リセット再開時に、 * フェイドイン・フェイドアウトされるゲーム画面のnodeを返す関数。 * デフォルトではgame.rootSceneを返すので、開発者側で必要に応じて * game.getGameNode = function(){ return fooScene };のように定義して * おくことで内部的に呼び出される。 */ getGameNode: function(){ return this.rootScene; }, /** * NINELEAP_RELEASE_MODE==falseのときに、自前でゲームを再開するための、 * 各種ゲーム状態(スコア・自機位置・敵位置など)の初期化処理を記述するための関数。 * game.reset = function(){....}; のように定義しておくことで内部的に呼び出される。 */ reset: function(){ alert("reset関数を実装してください"); } });
以下、使い方です。
たとえば、enchant.jsのサンプルである、examples/action/game.jsにおいて、 この nineleap_fade.enchant.js を適用するならば、次のような修正をすることになります。
*** examples/action/game.js 2012-01-11 03:47:36.000000000 +0900 --- examples/action/game_fade.js 2012-01-20 21:47:17.000000000 +0900 *************** *** 62,68 **** --- 62,85 ---- bear.jumping = true; bear.jumpBoost = 0; bear.image = game.assets['chara1.gif']; + + game.reset = function(){ + bear.x = 8; + bear.y = -32; + bear.vx = 0; + bear.vy = 0; + bear.ax = 0; + bear.ay = 0; + bear.pose = 0; + bear.jumping = true; + bear.jumpBoost = 0; + stage.x = 0; + }; + bear.addEventListener('enterframe', function(e) { + if(game.started == false){ + return; + } var friction = 0; if (this.vx > 0.3) { friction = -0.3; *************** *** 162,167 **** --- 179,185 ---- this.y = dest.y-2; if (this.y > 320) { + game.started = false; game.assets['gameover.wav'].play(); var score = Math.round(bear.x); this.frame = 3; *************** *** 171,185 **** this.y += Math.min(Math.max(this.vy, -10), 10); if (this.y > 320) { game.end(score, score + 'mで死にました'); } }); - this.removeEventListener('enterframe', arguments.callee); } }); ! var stage = new Group(); stage.addChild(map); stage.addChild(bear); stage.addEventListener('enterframe', function(e) { if (this.x > 64 - bear.x) { this.x = 64 - bear.x; } --- 189,206 ---- this.y += Math.min(Math.max(this.vy, -10), 10); if (this.y > 320) { game.end(score, score + 'mで死にました'); + this.removeEventListener('enterframe', arguments.callee); } }); } }); ! stage = new Group(); stage.addChild(map); stage.addChild(bear); stage.addEventListener('enterframe', function(e) { + if(game.started == false){ + return; + } if (this.x > 64 - bear.x) { this.x = 64 - bear.x; }
「Nodeをフェードイン・フェードアウトさせるしくみ」を開発するにあたって、 まずは、「ある対象ノードに対してフレームごとに何らかの処理を実行させるしくみ」を作ってみます。
対象となるノードとして、Spriteではなく、GroupやSceneを与えても、ちゃんと動作させることが目的です。 現在のenchant.jsでのGroupやSceneのオブジェクトは、階層的なひとまとまりのHTML要素ではないので、 単なるjQueryによるアニメーション指定などでは、うまく動かないのです。fpsがうまく合わないと、 画面がちらつく原因にもなります。
なお、ひとつのNodeに対して、たとえばフェードインとフェードアウトが同時に実行されるのを許してしまうと、 画面がちらついたり、悪くすれば無限ループに陥ってしまうようなことがあるかもしれません。 そこで、たとえばフェードイン処理が最後まで完了してから、 次のフェードアウト処理が開始されるというように、 処理をキューに格納して順次実行するような仕組みを作ることにします。
具体的には、以下のようになります。
enchant.effect = { }; /** * ある対象ノードに対してフレームごとの処理をするしくみ */ enchant.effect.Action = enchant.Class.create({ /** * フレームごとの処理 * @param {Node} taregtNode 対象となるノード * @param {Function} tickFunc フレームごとに実行される関数 */ initialize: function(targetNode, tickFunc){ this.targetNode = targetNode; this.tickFunc = tickFunc; this.frame = 0; if(! targetNode.queue){ //対象となるノードにキューがなければ作る targetNode.queue = []; targetNode.addEventListener('enterframe', function(){ //フレームごとの処理 if(targetNode.queue &&0 < targetNode.queue.length){ //キューが空でなければ最初のアクションを実行 targetNode.queue[0].tick(); }else{ //キューが空ならフレームごとの処理を終了 targetNode.removeEventListener('enterframe', arguments.callee); delete targetNode.queue; } }); } // キューに自身を登録する targetNode.queue.push(this); }, /** * フレームごとの処理 */ tick: function(){ this.tickFunc(this); this.frame++; } });
次に、上記のコードを利用しつつ、指定したノードについて フェードイン・フェードアウトをするしくみを実現します。
/** * ノードとその子ノードに対して、透明度を設定する */ enchant.effect.setOpacity = function(targetNode, opacity){ if(targetNode instanceof Entity){ targetNode.opacity = opacity; } if(targetNode.childNodes){ for (var i = 0, len = targetNode.childNodes.length; i < len; i++) { var node = targetNode.childNodes[i]; setOpacity(node, opacity); } } }; /** * ノードとその子ノードに対して、徐々に透明度を変化させる * @param {Node} taregtNode 対象となるノード * @param {Number} from 透明度の初期値 * @param {Number} to 透明度の最終値 * @param {Number} time 透明度変化の所要フレーム数 * @param {Function} onEndCallback 終了時に実行される関数 */ enchant.effect.fade = function(targetNode, from, to, time, onEndCallback){ setOpacity(targetNode, from); new Action(targetNode, function(action){ if(action.frame < time){ var opacity = from + (action.frame / time) * (to - from) ; setOpacity(targetNode, opacity); }else{ setOpacity(targetNode, to); if(onEndCallback){ onEndCallback(); } targetNode.queue.shift(); } }); }; /** * フェードイン * @param {Node} taregtNode 対象となるノード * @param {Number} time 透明度変化の所要フレーム数 * @param {Function} onEndCallback 終了時に実行される関数 */ enchant.effect.fadeIn = function(targetNode, time, onEndCallback){ fade(targetNode, 0, 1, time, onEndCallback); }; /** * フェードアウト * @param {Node} taregtNode 対象となるノード * @param {Number} time 透明度変化の所要フレーム数 * @param {Function} onEndCallback 終了時に実行される関数 */ enchant.effect.fadeOut = function(targetNode, time, onEndCallback){ fade(targetNode, 1, 0, time, onEndCallback); };
jQueryによるアニメーションのプログラミングように、メソッドチェインを用いていろいろな効果を重ね合わせたり、 thenメソッドの呼び出し時の引数で、フェード処理が終了した時のコールバック関数を指定するといった書き方を できるようにするというのも、面白いかもしれませんが、ひとまずこのくらいの内容でとどめておくことにします。
ゲームで面クリアをしたとき、前の面の内容をフェードアウトし、いったん暗転してから、次の面の内容をフェードインさせる... というような画面のトランジション効果を付与するしくみを、なるべく簡単なプログラムで書けるようにしたいものです。
そこでまずは、最新のプラグイン animation.enchant.js が使えないか検討することにしました。animation.enchant.js は、GitHub から開発版のenchant.jsをダウンロードすると、plugins/animation.enchant.js というファイルとして入っています。このプラグインでは、Robert Penner 氏のEasing Equationsを内部的に利用しつつ、jQuery風のAPIでfadeIn,fadeOutなどのアニメーション効果を「スプライトに対して」指定できるようになっています。
しかし残念ながら、この animation.enchant.js は、今回の用途には使えませんでした。ぼくは画面のトランジション効果を実現したいので、単一の「スプライトに対して」効果を適用するのではなく、複数のスプライトによる「グループに対して」効果を適用したいのです。フェードイン・フェードアウトをさせるなら、enchant.Entityクラスには透明度をコントロールするためのopacityへのset関数が用意されているので、基本的にはこれを使えば良いというように一瞬思われますが、複数のスプライトを束ねて扱うための enchant.Group クラスとそのサブクラスであるenchant.Sceneが、 enchant.Entity のサブクラスではない!ので、enchant.Groupの子ノードに対して再帰的に透明度を適用させるといった使いかたをするには、いろいろと animation.enchant.js の書き直しが必要になることがわかります。
本来は、上記のような問題点を解消する、animation.enchant.js へのパッチを書くべきところですが、今回は時間が無いので、パス。すみません...。
というわけで、自前でお手軽に作ってみることにしました。次のエントリで具体的なコードを示します。
OpenLeapに投稿するためにいろいろと実験して、何から手をつければいいか考えました。
まず気になったのは、次の2点です。