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

# はじめに

phina.js では、grid があって、これを使って何か書けないかな～
と、思いついたのが倉庫番(*1)。
ルールは単純だし、phina.js でも実装できそうだったので作ってみました。

普段は、あまりタブレットやスマホを使わないので、タッチに対応したアプリとかなじみがなく…つい、ブラウザ上でキーボード操作する物になりがち。

結局マウス＆キーボードでの操作の実装を、タッチ操作より優先してしまいます。
phina.js も、もちろんキーボードにも対応してるので、入門編とかにもあるけれど、この記事はキーボード入力について書きます。

倉庫番は、ファルコン株式会社の登録商標で、ルールが単純で奥が深いけど実装は簡単にできてしまいます。
特になにもアレンジすることなく実装しているしオープンソースライセンスで github とかに公開するのも気が引けるので、なんとなく runstant で…

[サンプル](http://goo.gl/P3SMqI)
![image](https://qiita-image-store.s3.amazonaws.com/0/30417/c9b4b464-1aca-ebda-c3ea-f949fc8c99c4.png)

# 全体的な構成

`GameApp`を使って、デフォルトの`ManagerScene`にお任せで作成しています。
実装してるのは`MainScene`のみで以下のような処理をしています。

- ステージの表示
- プレイヤーの移動処理
- 箱の移動処理
- クリアの判定
- 各種ボタン操作

その他、ゲームで使用する各オブジェクトがあります。（ステージの表示に使われる）

- Wall
 - 壁、プレイヤーと荷物は移動できない
- Box
 - 荷物、プレイヤーが押せる。移動先に壁か別の荷物があったら押せない
- Player
 - プレイヤーを操作して、全ての荷物を決められた位置へ移動させればクリア
- Target
 - 荷物を置く場所、この上に荷物を置く

どれも RectangleShape や CircleShape などで、とりあえず見分けがつくように色や形を変更してるだけです。

# MainScene

## ステージの表示

ステージのデータは、文字列の配列をステージに見立てて、１文字１オブジェクトに対応させてます。
見たまま…

```coffeescript
    warehouse: [
      'WWWWW'
      'W00TW'
      'W0BPW'
      'W000W'
      'WWWWW'
    ]
```

これを、さらに配列にすることで、ステージを管理しています。

ステージ上のオブジェクトを作成する時に、後々処理しやすいように倉庫の位置を`Vector2`を使って保存しています。

```coffeescript
            o = @_createObject[c].call @
            o.addChildTo @
            o.pos = Vector2 x + 1, y + 1
```

`Vector2`は`{x,y}`の値を持っていて、上のステージで言うと、プレイヤーの`pos`は`{4,3}`となります。

*生成したオブジェクトに勝手にプロパティ`pos`を追加したりするのはなかなか微妙な感じもするけれど、そこは、まぁ～面倒なので…（本当なら各オブジェクトの`init`とかで`pos`を持たせておくべきかな～？でもそうすると基底クラス作ったりで、大変。JavaScriptのプロパティ万歳ってことで、って話がそれた）*


## プレイヤーの移動処理

プレイヤーの移動は、キーボードの十字キーで行います。
ゲームのフレームごとに呼ばれる 'enterframe' のイベントで、どの方向が押されているか取得してプレイヤーの移動処理をしています。

```coffeescript
    @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}`となって、斜めに移動されてしまうので上の様なちょっとややっこしい条件の判定をしています。*（両方０じゃなくて、どちらかが０の場合…？もすこし良い方法が無いものか…）*

移動方向が取得出来たら、プレイヤーを移動させます。

```coffeescript
  _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`には、２種類あります。

- スタティックメソッドの`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`を使ってます。

## 各ボタンの処理

この様なパズルだと打つ手がない状態になったら、リセットして最初からやりなおす必要があるので（もしくは１手戻しとか）リセットボタンと…
ステージをいくつか用意できるようにしたので、その移動のためのボタン「前・次」を配置します。

使っているのは、phina.js で用意されている`Button`クラスで、これは、マウス等でクリックされると'push'イベントが発生します。

なので、まずはこれを実装。

```coffeescript
  _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
````

```coffeescript
    prevBtn  = @_createButton '前へ(P)',-5,prevNum
    nextBtn  = @_createButton '次へ(N)',5,nextNum
    resetBtn = @_createButton 'リセット(R)',0,@stageNum
```

押されたときに、もう一度`MainScene`に移動させて、指定されたステージ`num`を表示してます。`num`が今のステージと同じならリセット。

このままでも一通りできるのですが、キーボードにも対応させました。
（キーボードで操作中にマウスさわりたくない派閥な人用）

```coffeescript
    @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/