RxSwiftで昇竜拳

# 元ネタ

<blockquote class="twitter-tweet" data-lang="en"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/attefes?src=hash">#attefes</a> で <a href="https://twitter.com/akio0911">@akio0911</a> さんが「Rxってゲームの入力に使えそう」と言っていてなるほどと思ったので、試しに昇竜拳判定をやってみようかな。直近が➡️⬇️↘️👊か判定。ズンドコキヨシとの違いは各入力間の時間制限か <a href="https://t.co/ynFFFo42RL">https://t.co/ynFFFo42RL</a></p>&mdash; Shinichiro Oba (@ooba) <a href="https://twitter.com/ooba/status/722583581474721793">April 20, 2016</a></blockquote>

# 目標

制限時間内に`右`、`下`、`右下`、`パンチ`が入力されたら値を流す`昇竜拳`オペレータを作ります。時間を扱うのでスケジューラを引数で渡せるようにしてます。

```swift
import RxSwift

enum 入力: Int {
    case 左下 = 1, 下, 右下, 左, 右 = 6, 左上, 上, 右上
    case パンチ, キック
}

let 入力川 = PublishSubject<入力>()

_ = 入力川
    .subscribeNext { print($0) }

_ = 入力川
    .昇竜拳(MainScheduler.instance)
    .subscribeNext { _ in print("昇竜拳！") }
```

本当は➡️や👊をcaseに使いたかったんですが、無理でした…。

実行イメージはこんな感じ。

```
上
右
右
下
右下
パンチ
昇竜拳！
上
下
...
```

# 仕様の簡略化

実装を楽にするために仕様を簡略化しています。

- 入力猶予フレームは全入力間で7フレームに固定する（小昇竜の仕様）
- 同時押しはサポートしない
- 押しっぱなしやニュートラルは考慮しない

格ゲーのコマンド判定ロジックって、ちゃんと考えると結構複雑ですね…。

# 実装

```swift
extension ObservableType where E == 入力 {
    func 昇竜拳(scheduler: SchedulerType) -> Observable<[E]> {
        return self
            .scan([]) { Array(($0 + [$1]).suffix(4)) }
            .timeout(8 / 60, scheduler: scheduler)
            .retry()
            .filter { $0 == [.右, .下, .右下, .パンチ] }
    }
}
```

「[ズンドコキヨシ with RxSwift](http://qiita.com/bricklife/items/4bf8c0e17043498f4452)」のときのように、まず直近の4入力を配列にまとめます。

そのあと、`timeout`で8フレーム以上値が流れていない場合はエラーを起こし、それを`retry`で捕まえて最初から`subscribe`し直すようにします。これによって、連続で7フレーム以内に入力されたものだけが配列にまとまるようになります。

あとは、その配列が昇竜拳コマンドである`[右, 下, 右下, パンチ]`かどうかで`filter`するだけです。

# 動作確認

ジョイスティックをつなげてその入力を`入力川`に流すようにしようかと思ったのですが、かなり壮大になりそうなので断念。そもそもフレーム単位の判定になるため、手入力でのきちんとした動作確認は厳しそうです。

そこで、時間を自由に操れる`HistoricalScheduler`を使ってみます。`advanceTo()`というメソッドで任意の時点に同期的に時間を進めることができます。便利！

```swift
let scheduler = HistoricalScheduler()

_ = 入力川
    .subscribeNext { print($0) }

_ = 入力川
    .昇竜拳(scheduler)
    .subscribeNext { _ in print("昇竜拳！") }

入力川.onNext(.右)
scheduler.advanceTo(scheduler.now.dateByAddingTimeInterval(7 / 60))
入力川.onNext(.下)
scheduler.advanceTo(scheduler.now.dateByAddingTimeInterval(7 / 60))
入力川.onNext(.右下)
scheduler.advanceTo(scheduler.now.dateByAddingTimeInterval(7 / 60))
入力川.onNext(.パンチ)
scheduler.advanceTo(scheduler.now.dateByAddingTimeInterval(5 / 60))
入力川.onNext(.右)
scheduler.advanceTo(scheduler.now.dateByAddingTimeInterval(7 / 60))
入力川.onNext(.下)
scheduler.advanceTo(scheduler.now.dateByAddingTimeInterval(8 / 60))
入力川.onNext(.右下)
scheduler.advanceTo(scheduler.now.dateByAddingTimeInterval(7 / 60))
入力川.onNext(.パンチ)
scheduler.advanceTo(scheduler.now.dateByAddingTimeInterval(7 / 60))
入力川.onNext(.右)
```

実行結果

```
右
下
右下
パンチ
昇竜拳！
右
下
右下
パンチ
右
```

ちゃんと7フレーム以内に昇竜拳コマンドが入力できているときだけ値が流れているようです。

RxTestsに含まれる`TestScheduler`を使うと、同じように時間を操作してオペレータの単体テストもできるのですが、それはまた次の機会に。

# 改良

この実装だと、何も入力をしないと8フレームごとに`入力川`に対して`subscribe`が走ってしまい、一応意図通り動いているとはいえちょっと気持ち悪いです。

@inamiy さんが実装された[ReactiveCocoa版](https://github.com/inamiy/ReactiveCocoaCatalog/pull/10)ではこの点をちゃんと考えてあり、最後の入力を起点に`timeout`が実行されるようになっていました。昇竜拳以外の必殺技にも対応していたり、実際に遊べるようになっていたりして、色々すごい。

該当箇所は以下になります。`flatMapLatest`で`timeout`につなげているのがポイントです。
https://github.com/inamiy/ReactiveCocoaCatalog/blob/ab7291561b4ec015a68b915c2ed56b6b75a626b7/ReactiveCocoaCatalog/Samples/GameCommandViewController.swift#L45-L49

これを真似したのが以下の実装です。この方式であれば、最後の入力によって入力猶予フレームを可変にしたりできますね。

```swift
extension ObservableType where E == 入力 {
    func 昇竜拳(scheduler: SchedulerType) -> Observable<[E]> {
        return self
            .scan([]) { Array(($0 + [$1]).suffix(4)) }
            .flatMapLatest { input -> Observable<[E]> in
                Observable.just(input)
                    .concat(Observable.never())
                    .timeout(8 / 60, scheduler: scheduler)
            }
            .retry()
            .filter { $0 == [.右, .下, .右下, .パンチ] }
    }
}
```

# 参考文献

http://www8.plala.or.jp/ichirou/hypersf2/input.htm