LoginSignup
57
55

More than 5 years have passed since last update.

RxSwiftで昇竜拳

Last updated at Posted at 2016-04-22

元ネタ

目標

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

import RxSwift

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

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

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

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

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

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

上
右
右
下
右下
パンチ
昇竜拳!
上
下
...

仕様の簡略化

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

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

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

実装

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」のときのように、まず直近の4入力を配列にまとめます。

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

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

動作確認

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

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

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版ではこの点をちゃんと考えてあり、最後の入力を起点にtimeoutが実行されるようになっていました。昇竜拳以外の必殺技にも対応していたり、実際に遊べるようになっていたりして、色々すごい。

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

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

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 == [., ., .右下, .パンチ] }
    }
}

参考文献

57
55
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
57
55