昨日の続き。 カード移動のアニメーションについて。
カードの移動は、適当な時間間隔でカードの位置を更新することで表現する。 この時間間隔と位置の決め方を考える。
人が手でカードを動かす場合は、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
から始めて
xn
が
xe
とになるまで繰り返すことになる。
加速は、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 で取り出す。 名前が不釣り合いだけど、これはまあしょうがないか。
ゲームの完成は、全てのカードを完了分に移動して成り立つので、完成時の処理もこちらに持ってきた。
アニメーション効果が有効な場合、true を返す。 効果の有効/無効は、HTML に追加した checkbox で指定する。
移動情報をキューに登録する。 ここで、移動情報とは下記一式。
ゲームの完了条件が成り立った時に呼ばれ、完了フラグをセットする。
タイマーが動作中でなければ、キューに登録されているカードの移動を行う。 実際の移動処理は下請けの _move0 に丸投げ。
キューにカードが有れば一枚取り出して、1回の移動分の ( dLeft, dTop ) を計算し、この値を移動情報に追加する。 この1枚分の移動情報を、一枚分の移動処理である下請けの _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 時に実行するように、イベント処理を設定。
これで 一応完成 とする。 頑張ればもっとコンパクトにできるんだろうけど、それはまたいつか考えよう。 どうせ半年ぐらい経って見直したら、丸ごと作り直したくなるのだから。