2011 11 06

スパイダーを作るの6

昨日の続き。 カード移動のアニメーションについて。

カードの移動は、適当な時間間隔でカードの位置を更新することで表現する。 この時間間隔と位置の決め方を考える。

人が手でカードを動かす場合は、0から加速し、ある速度まで来たらほぼ同じ速さで移動し、また0まで減速する、という動きになるだろう。 これがもう大雑把なのだが、更なる単純化のために、最初と最後を無視してカードの移動を等速運動としよう。

タイマー呼び出しを単位時間、その間の移動距離を V とする。 この V が定数となる。

画面上の座標 (xs, ys) から (xe, ye) に移動する場合、タイマー周期毎のカードのx座標の変化 dx dx = ±( |xe - xs| / ( |xe - xs| + |ye - ys| ) )1/2 * V となる。 符号が正となるのは xs <= xe の場合。 xs > xe なら負。

同じくy座標の変化 dy dy = ±( |ye - ys| / ( |xe - xs| + |ye - ys| ) )1/2 * V となる。

x座標もy座標もやることは同じなのでx座標だけを考えると、速度 V での等速移動とは、漸化式 xn = xn-1 + dx xs から始めて xnxe とになるまで繰り返すことになる。

加速は、1回分の移動距離を例えば 0.1 → 0.3 → 0.6 などに細分化することで表現できそう。 減速ならこの逆。

と、ここまで考えたところで方針転換。 そもそもが近似なんだし、そんな律儀に人の動きを考えなくてもいいだろう。 移動元から移動先まで、一律適当な数で分割して表示するので十分ではないか。

と妥協して実装を考えた結果、こんな感じに。

そしてコード。 移動管理用オブジェクトを追加する。

// カード移動管理 function Mover() { var DISP = { COUNT : 4, // 移動途中の表示回数 INTERVAL : 30, // 移動途中の表示間隔(ms) ZINDEX : 105 // カードの全枚数(=13*4*2)よりも大きい値 }; var items = []; var timerId = null; var finished = false; this.use = function () { return document.getElementById( 'effect' ).checked; }; this.add = function ( card, left, top, zIndex ) { items.push( { 'card' : card, 'left' : left, 'top' : top, 'zIndex' : zIndex } ); }; this.fin = function () { finished = true; }; this.move = function () { if ( timerId ) { return; } _move0(); }; function _move0() { if ( items.length ) { var item = items.shift(); var pos = item.card.getPosition(); item.dLeft = ( item.left - pos.left ) / DISP.COUNT; item.dTop = ( item.top - pos.top ) / DISP.COUNT; _move1( items.shift(), DISP.COUNT ); } else { timerId = null; if ( finished ) { finished = false; alert( '完成' ); } } } function _move1( item, n ) { if ( n > 1 ) { var pos = item.card.getPosition(); item.card.setPosition( pos.left + item.dLeft, pos.top + item.dTop ).setZIndex( DISP.ZINDEX ); timerId = setTimeout( function () { _move1( item, n - 1 ); }, DISP.INTERVAL ); } else { item.card.setPosition( item.left, item.top ).setZIndex( item.zIndex ); timerId = setTimeout( _move0, DISP.INTERVAL ); } } }

items がキュー。 実装は配列で、push で登録して shift で取り出す。 名前が不釣り合いだけど、これはまあしょうがないか。

ゲームの完成は、全てのカードを完了分に移動して成り立つので、完成時の処理もこちらに持ってきた。

use

アニメーション効果が有効な場合、true を返す。 効果の有効/無効は、HTML に追加した checkbox で指定する。

add

移動情報をキューに登録する。 ここで、移動情報とは下記一式。

  • 移動対象のカード
  • 移動先の left と top
  • 移動先での zIndex
fin

ゲームの完了条件が成り立った時に呼ばれ、完了フラグをセットする。

move

タイマーが動作中でなければ、キューに登録されているカードの移動を行う。 実際の移動処理は下請けの _move0 に丸投げ。

_move0

キューにカードが有れば一枚取り出して、1回の移動分の ( dLeft, dTop ) を計算し、この値を移動情報に追加する。 この1枚分の移動情報を、一枚分の移動処理である下請けの _move1 に渡して実行する。

キューにカードが無い場合は、タイマー実行中フラグをクリアする。 完了フラグがセットされている場合は、完了フラグをクリアし、完了メッセージを表示する。

_move1

引数で渡すカウントを1ずつ減らし、1になるまで、タイマーで繰り返し呼び出す。

カウントが1より大きい場合は、カードの位置を1回の移動分だけ移動する。 このとき、移動中のカードが他のカードに隠れないように、カードの全数よりも大きな値を zIndex として設定する。 その後、1減らしたカウントを引数として、タイマーに自分自身の呼び出しを設定する。

カウントが1になったら、カードの位置を目標位置に設定し、また zIndex も本来の値に設定する。 その後、タイマーに _move0 の呼び出しを設定し、次のカードの処理移動に移行する。

コードの見た目が何となくすっきりしないんだけど、まあいいか。

これを使うコントローラーの変更部分。

function Controller() { var mover = new Mover(); this.fin = function( lineNo ) { fin.add( lines[ lineNo ].get( 13 ) ); if ( self.isFinished() ) { mover.fin(); score.win(); win = true; } } this.moveTo = function ( card, left, top, zIndex ) { if ( mover.use() ) { mover.add( card, left, top, zIndex ); mover.move(); } else { card.setPosition( left, top ).setZIndex( zIndex ); } }; }

変更は3カ所。

コントローラーの変更はこれだけ。

次に、コントローラーに追加した移動処理を呼ぶ側。

function Line( lineNo, dummyHeadCard, controller ) { this.add = function ( cards ) { cards.each( function ( card ) { controller.moveTo( card, POS.left, POS.nextTop(), myCards.length ); myCards.push( card.open().addEventListener( 'click', _select, false ) ); } ); if ( self.canGet(1).length === 13 ) { controller.fin( lineNo ); } }; } function Fin( controller ) { this.add = function ( cards ) { cards.reverse().each( function ( card ) { controller.moveTo( card, POS.nextLeft(), POS.top, myCards.length ); myCards.push( card ); } ); } }

列オブジェクト Line と完了用オブジェクト Fin の、カード追加用メソッド add が変更対象。 以前は、このメソッドで直ちにカードを移動していたのを、コントローラーの moveTo を呼ぶように変更した。 その他は変更無し。

移動処理の本体以外に、既存のコードをもっといろいろ変更することになるかと思ったけど、意外に簡単だったな。

折角簡単だったので、ついでにリロード対応もやってしまう。

ページをロードしたタイミングでゲームを開始するので、ゲームの状態は、ゲーム中か完了した状態かのどちらか。 なので、リロードのとき、もっと汎用的にページを離れるときには、ゲームの途中で離れるか完了して離れるかのどちらか。 前者であれば敗走として負け数を増やし、後者であれば何もしない。

という方針で変更する。 まずはコントローラー。

function Controller() { this.flushScore = function () { if ( win ) { win = false; } else { score.lose(); } }; this.clear = function () { lines.each( function ( line ) { line.clear(); } ); fin.clear(); yet.clear(); self.flushScore(); return self; }; }

元は clear の中でやっていた負け判定を、flushScore としてメソッド化し、外から呼べるようにする。 コントローラーの変更はこれだけ。

次に、ロードイベント処理。

window.onload = function () { var controller = new Controller(); controller.init().deal(); document.getElementById( 'score' ).addEventListener( 'click', controller.clearScore, false ); document.getElementById( 'back' ).addEventListener( 'click', controller.back, false ); document.getElementById( 'init' ).addEventListener( 'click', function () { if ( controller.isFinished() || confirm( 'このゲームを捨てて新しく始める' ) ) { controller.clear().init().deal(); } }, false ); window.onunload = controller.flushScore; };

コントローラーに追加した flushScore を unload 時に実行するように、イベント処理を設定。

これで 一応完成 とする。 頑張ればもっとコンパクトにできるんだろうけど、それはまたいつか考えよう。 どうせ半年ぐらい経って見直したら、丸ごと作り直したくなるのだから。