phina.js のキーボード操作(倉庫番を作ってみた話)


はじめに

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.posVector2と移動方向のdirVector2を足すだけで、移動先の座標Vector2になります。(どの方向へ移動したか意識することなく判定ができる)

移動先の座標がわかったら、その位置が「壁じゃないか?」その位置に「箱があるか?」箱があった場合は、さらにその箱の先に「壁か箱が無いか?」などなどを判定して、移動できる場合に、それぞれのオブジェクトを移動させてます。

プレイヤーや箱の移動にtweenerを使用しているので、クリア判定は、箱が移動し終わったtweener処理の最後にcallタスクを追加して、クリア判定用のメソッドを呼んでいます。

Vector2add

Vector2にもいろいろメソッドがありますがaddについて少し。
addVector2には、2種類あります。

  • スタティックメソッドのadd
  • インスタンスメソッドとしてのadd

オブジェクト指向的な話そのものですが…

スタティックメソッドの場合
pos = Vector2.add(@player.pos,dir)だと@player.posdirを足したものが返されます。@player.posは変更されない。(+ってこと)

インスタンスメソッドの場合
@player.pos.add(dir) だと@player.posdirが足されます。(+=ってこと)

プレイヤーの移動処理では、箱とか壁とかいろいろ判定するので、本当に移動が可能かわかるまで@player.posを変更せずにstaticVector2.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/