はじめに
phina.js では、grid があって、これを使って何か書けないかな~
と、思いついたのが倉庫番(*1)。
ルールは単純だし、phina.js でも実装できそうだったので作ってみました。
普段は、あまりタブレットやスマホを使わないので、タッチに対応したアプリとかなじみがなく…つい、ブラウザ上でキーボード操作する物になりがち。
結局マウス&キーボードでの操作の実装を、タッチ操作より優先してしまいます。
phina.js も、もちろんキーボードにも対応してるので、入門編とかにもあるけれど、この記事はキーボード入力について書きます。
倉庫番は、ファルコン株式会社の登録商標で、ルールが単純で奥が深いけど実装は簡単にできてしまいます。
特になにもアレンジすることなく実装しているしオープンソースライセンスで github とかに公開するのも気が引けるので、なんとなく runstant で…
全体的な構成
GameApp
を使って、デフォルトのManagerScene
にお任せで作成しています。
実装してるのはMainScene
のみで以下のような処理をしています。
- ステージの表示
- プレイヤーの移動処理
- 箱の移動処理
- クリアの判定
- 各種ボタン操作
その他、ゲームで使用する各オブジェクトがあります。(ステージの表示に使われる)
- Wall
- 壁、プレイヤーと荷物は移動できない
- Box
- 荷物、プレイヤーが押せる。移動先に壁か別の荷物があったら押せない
- Player
- プレイヤーを操作して、全ての荷物を決められた位置へ移動させればクリア
- Target
- 荷物を置く場所、この上に荷物を置く
どれも RectangleShape や CircleShape などで、とりあえず見分けがつくように色や形を変更してるだけです。
MainScene
ステージの表示
ステージのデータは、文字列の配列をステージに見立てて、1文字1オブジェクトに対応させてます。
見たまま…
warehouse: [
'WWWWW'
'W00TW'
'W0BPW'
'W000W'
'WWWWW'
]
これを、さらに配列にすることで、ステージを管理しています。
ステージ上のオブジェクトを作成する時に、後々処理しやすいように倉庫の位置をVector2
を使って保存しています。
o = @_createObject[c].call @
o.addChildTo @
o.pos = Vector2 x + 1, y + 1
Vector2
は{x,y}
の値を持っていて、上のステージで言うと、プレイヤーのpos
は{4,3}
となります。
生成したオブジェクトに勝手にプロパティpos
を追加したりするのはなかなか微妙な感じもするけれど、そこは、まぁ~面倒なので…(本当なら各オブジェクトのinit
とかでpos
を持たせておくべきかな~?でもそうすると基底クラス作ったりで、大変。JavaScriptのプロパティ万歳ってことで、って話がそれた)
プレイヤーの移動処理
プレイヤーの移動は、キーボードの十字キーで行います。
ゲームのフレームごとに呼ばれる 'enterframe' のイベントで、どの方向が押されているか取得してプレイヤーの移動処理をしています。
@on 'enterframe', @updateInputEvent
:
updateInputEvent: ->
dir = @app.keyboard.getKeyDirection()
unless dir.x is 0 and dir.y is 0
if dir.x is 0 or dir.y is 0
unless @player.tweener.playing
@_movePlayer dir
シーンクラスにはアプリケーションのインスタンス@app
があって、@app
には、キーボードの情報を持っているkeyboard
があります。
-
@app.keyboard.getKey('A')
Aを押している -
@app.keyboard.getKeyDown('A')
Aを押した -
@app.keyboard.getKeyUp('A')
Aを離した
などなど、ここでは、十字キーのどの方向を押しているか欲しいのでgetKeyDirection()
を使用しています。
返されるのはVector2
で、十字キーを押した方向によって{x,y}
に、それぞれ -1,0,1 の値が設定されます。
例えば、上を押した場合には{x,y}={0,-1}
です。
ちなみに、上と右を押した場合には{x,y}={1,-1}
となって、斜めに移動されてしまうので上の様なちょっとややっこしい条件の判定をしています。(両方0じゃなくて、どちらかが0の場合…?もすこし良い方法が無いものか…)
移動方向が取得出来たら、プレイヤーを移動させます。
_movePlayer: (dir) ->
pos = Vector2.add @player.pos,dir
return if @_findObj @walls,pos
box = @_findObj @boxs,pos
if box?
pos = Vector2.add box.pos,dir
return if @_findObj @walls,pos
return if @_findObj @boxs,pos
@_moveObj box,dir
box.tweener.call @_checkGameClear,@,[]
@_moveObj @player,dir
ゲームのトリガーのほとんどが、このプレイヤーの移動なので、ここで箱の移動処理やクリアの判定、壁の当たり判定まで、まとめて処理しています。(プレイヤー以外のオブジェクトが能動的に動かないから楽)
Vector2
にしていると、ここがなかなか便利。
@player.pos
のVector2
と移動方向のdir
のVector2
を足すだけで、移動先の座標Vector2
になります。(どの方向へ移動したか意識することなく判定ができる)
移動先の座標がわかったら、その位置が「壁じゃないか?」その位置に「箱があるか?」箱があった場合は、さらにその箱の先に「壁か箱が無いか?」などなどを判定して、移動できる場合に、それぞれのオブジェクトを移動させてます。
プレイヤーや箱の移動にtweener
を使用しているので、クリア判定は、箱が移動し終わったtweener
処理の最後にcall
タスクを追加して、クリア判定用のメソッドを呼んでいます。
Vector2
のadd
Vector2
にもいろいろメソッドがありますがadd
について少し。
add
がVector2
には、2種類あります。
- スタティックメソッドの
add
- インスタンスメソッドとしての
add
オブジェクト指向的な話そのものですが…
スタティックメソッドの場合
pos = Vector2.add(@player.pos,dir)
だと@player.pos
とdir
を足したものが返されます。@player.pos
は変更されない。(+
ってこと)
インスタンスメソッドの場合
@player.pos.add(dir)
だと@player.pos
にdir
が足されます。(+=
ってこと)
プレイヤーの移動処理では、箱とか壁とかいろいろ判定するので、本当に移動が可能かわかるまで@player.pos
を変更せずにstatic
のVector2.add
を使ってます。
各ボタンの処理
この様なパズルだと打つ手がない状態になったら、リセットして最初からやりなおす必要があるので(もしくは1手戻しとか)リセットボタンと…
ステージをいくつか用意できるようにしたので、その移動のためのボタン「前・次」を配置します。
使っているのは、phina.js で用意されているButton
クラスで、これは、マウス等でクリックされると'push'イベントが発生します。
なので、まずはこれを実装。
_createButton: (text,offset,num) ->
b = Button(
text: text
width: @objectSize * 4
height: @objectSize
fontSize: 16
).addChildTo @
b.x = @gridX.center(offset)
b.y = @gridY.span 14
b.on 'push', @exit.bind @,'main',num:num
prevBtn = @_createButton '前へ(P)',-5,prevNum
nextBtn = @_createButton '次へ(N)',5,nextNum
resetBtn = @_createButton 'リセット(R)',0,@stageNum
押されたときに、もう一度MainScene
に移動させて、指定されたステージnum
を表示してます。num
が今のステージと同じならリセット。
このままでも一通りできるのですが、キーボードにも対応させました。
(キーボードで操作中にマウスさわりたくない派閥な人用)
@on 'keydown', (e) ->
kb = @app.keyboard
if kb.getKey('P')
prevBtn.flare 'push' if prevBtn?
if kb.getKey('N')
nextBtn.flare 'push' if nextBtn?
if kb.getKey('R')
resetBtn.flare 'push'
十字キーの様にenterframe
でチェックしても良いけど、フレーム毎にチェックするよりイベントkeydown
で判定しています。
keydown
は、何かキーが押されたときに発生するイベントなので、その中で実際に押されたキーが何か判定しつつ、対応するボタンへ'push'のイベントを発生させています。
おわりに
以上、倉庫番を元にキーボード操作のサンプルの説明を書いてみました。
getKeyDirection()
でVector2
が取得できるのは、使い方によってどの方向が押されたか意識することなく実装できて、なかなか便利だと思います。プレイヤーの移動とかにはぴったり。
もし次があるなら…これをタッチ操作に対応させるにはどうするのか?検討してみたいかも
- (*1) 「倉庫番」および「sokoban」は、ファルコン株式会社の登録商標または商標です。http://sokoban.jp/