RxJava2でオペレータを自作してみる
Android アドベントカレンダー 17日目の記事です。
Rxは非常に便利なライブラリです。特にオペレータ群はとても強力で、filterやmapやflatmapなど使っておけば割となんとかなります。 が、それ故に雰囲気で書けるとこもあり、「オペレータって具体的にどんな感じで動いてるの?」となることもあります。僕も雰囲気で書いてます。
今回は、実際にオリジナルのオペレータを作ることで、中で何が起きているのかをざっくりと見てみましょう。
事前準備
RxJavaリポジトリをcloneしておきましょう。
今回はforkして手元に持ってきました。
作るオペレータ
なんでも良いですが、とりあえず2種類作ってみましょう。
本来はFlowable、Single、Maybe、CompletableなどそれぞれのPublisherに応じたオペレータを作る必要がありますが、数が多いので一旦Flowableに対応したものだけにします。
Imo ファクトリメソッド
justやtimerなど、ストリームの最上流にいるメソッドです。 彼らは実際にデータを生成してSubscriberに流すのがお仕事です。
今回は、imoオペレータを作ります。役割は以下です
- "imo" という文字列を生成し流す
需要は高そうですね。
Println オペレータ
こちらはfilterやmapなど、最上流ではなく上から流れてきたデータをごにょごにょするオペレータです。
役割は以下。
- 上流から流れてきたデータをSystem.out.printlnで出力する
これら二つを作成すると、最終的に以下のようなコードが書けるようになります。
Flowable.imo().println().subscribe();
// => imo とログが吐かれるだけ
Imoオペレータを作る
FlowableImo class
RxJava2において、全てのオペレータは独立したクラスになっています。 例えばFlowable.justの中身は以下です。
コンストラクタで値をもらって、購読時はSubscriptionにデータを包んでSubscriberに渡します。
そのままだと利用者側はメソッドチェーンで呼びづらいので、Flowableのメソッドで呼べるようにいい感じにラップしてるようです。
なので、新規にオペレータクラスを作るなら2つの実装が必要です。
- オペレータクラスの作成
- Flowableにstaticなメソッドを定義する
なのでまずはjustに習ってFlowableImoクラスを定義します。
public final class FlowableImo extends Flowable<String> { public FlowableImo() { } @Override protected void subscribeActual(Subscriber<? super String> s) { s.onSubscribe(new ScalarSubscription<String>(s, "imo")); } }
imo
の決め打ちになるので引数はありません。subscribeActualでは購読処理のためにSubscriptionをSubscriberに投げています。
継承元であるFlowableがSubscriberインタフェースを実装しているので、subscribeメソッドは実装済みですが、こちらは分岐させたりなどして実際にonSubscribeするところまで至ってないのでActualという意味で分けてるっぽいです。
subscribeActualが呼ばれるのは、Flowable内で適切なSubscriberを生成した後のようですね。
実際Subscribe呼ぶときはonNextだけのConsumerを渡したり、Subscriberを直渡ししたりだとバリエーションが多いので、それらを吸収するための措置っぽい。
定義し終わったら、Flowableから呼べるようにstaticなメソッドを追加します。
public abstract class Flowable<T> implements Publisher<T> { ~~~~~~~~~~~~~~~~~~~ @CheckReturnValue @BackpressureSupport(BackpressureKind.FULL) @SchedulerSupport(SchedulerSupport.NONE) public static Flowable<String> imo() { return RxJavaPlugins.onAssembly(new FlowableImo()); } }
back pressure対応やスケジューラなどの設定アノテーションが付いていますが、長くなりそうなので今回は気にしないことにします。
やっていることは、FlowableImoを生成して返すだけです。その際に、RxJavaPluginsでFlowable生成時のフック処理を書いていた場合、それも引っ付けて返すために RxJavaPlugins.onAssembly
を呼んでいます。
Printlnオペレータを作る
次は流れてきた値をprintlnするオペレータを定義しましょう。
public final class FlowablePrintln<T> extends AbstractFlowableWithUpstream<T, T> { public FlowablePrintln(Flowable<T> source) { super(source); } @Override protected void subscribeActual(Subscriber<? super T> s) { source.subscribe(new PrintlnSubscriber<T>(s)); } static final class PrintlnSubscriber<T> implements FlowableSubscriber<T>, Subscription { final Subscriber<? super T> actual; Subscription s; PrintlnSubscriber(Subscriber<? super T> actual) { this.actual = actual; } @Override public void onSubscribe(Subscription s) { if (SubscriptionHelper.validate(this.s, s)) { this.s = s; actual.onSubscribe(this); } } @Override public void onNext(T t) { System.out.println(t); // ここがplintlnオペレータのやりたいこと actual.onNext(t); } @Override public void onError(Throwable t) { actual.onError(t); } @Override public void onComplete() { actual.onComplete(); } @Override public void request(long n) { s.request(n); } @Override public void cancel() { s.cancel(); } } }
imoオペレータといくつか違う点があります。
AbstractFlowableWithUpstream
は上流のFlowableを扱うための抽象クラスです。
HasUpstreamPublisher
インタフェースを持つことで、自分より上流にPublisherがいることを保証しています。
この実装により、一個前のFlowableを保持させて、メソッドチェーンでオペレータを繋げられるようになっています。 FlowableImoはファクトリメソッドのため上流がありませんでしたが、printlnオペレータは必ず上流が存在するので継承が必要です。
また、インナークラスとして PrintlnSubscriber
が存在しています。
このSubscriberがオペレータのコアの機能で、onNextでデータを送るときにごにょごにょする部分です。
今回はonNextでprintlnして、データには変更を加えずそのまま流しています。
あとはファクトリメソッドと同じようにFlowableにメソッドを定義します。
public abstract class Flowable<T> implements Publisher<T> { ~~~~~~~~~~~~~~~~~~~ @CheckReturnValue @BackpressureSupport(BackpressureKind.FULL) @SchedulerSupport(SchedulerSupport.NONE) public final Flowable<T> println() { return RxJavaPlugins.onAssembly(new FlowablePrintln<T>(this)); } }
以上で、自作オペレータが実装できるようになりました。
Flowable.imo().println().subscribe();
// => imo とログが吐かれるだけ
できあがりはこちらです
見やすいようにPRにしてみました
所感
かなり端折りましたが、オペレータがどんな挙動をしているのかはざっくりわかりました。
ただ、SubscriberがSubscriberとSubscriptionの両方の役割を担っているので、そこのコードを追うのがめちゃめちゃ大変でした。 この辺りは別のタイミングで書こうかと思います。
昔作ったゲームを公開した
出稼ぎダンジョン進捗報告 2
割と時間開けてしまった。
今回は見た目の変更はほぼなし。その代わり、急ピッチで作った部分を色々とリファクタリングなどしていた。
Zenjectの導入
戦闘システム周りなど、敵と味方が相互に依存しあったりして割とつらいコードになっていた。そのあたりのリソースの参照を一か所に集中させるためにシーンごとにDataStore的なクラスを用意してそこから参照するようにしていたが、毎度用意するのがやや面倒だった。
また、Storeがシングルトンという役割を持つので、シングルトンにしたいPureClassなどもここに追いやられていた。
namespace Game.Store { /// <summary> /// Battleシーンのデータを全て保持しているやつ /// </summary> public class BattleStore { public BattlePartyView Party { get; private set; } public BattleEnemyView Enemy { get; private set; } public ActionResult InBattleResult { get; set; } public ActionDisplayView ActionDisplay { get; private set; } public View.MoneyView MoneyDisplay { get; private set; } public Model.Money Money { get; private set; } public void Initialize(){ // それぞれfindしたりなど // PureClassはここで生成したりなど Money = new Money(); } } public class Hoge { private BattleStore store; private Model.Money money; void Start(){ var party = store.Party; money = store.Money; // ほげほげ } } }
こういうのを用意して、全部Store経由で取ってたりした。 流石に将来的にしんどくなる気がしたので、ここにZenjectを導入して、データは極力直接injectする方針に変更した
namespace Game.DI { public class BattleInstaller : MonoInstaller<BattleInstaller> { public override void InstallBindings() { Container.Bind<Model.Money>().FromNew().AsSingle(); } } } public class Hoge { [Inject] private BattlePartyView party; [Inject] private Model.Money money; void Start(){ // ほげほげ } }
- Storeが不要になった
- PureClassの生成はInstallerに移行できたので置き場所がわかりやすくなった
コード量がだいぶ減って見やすくなったのだった。 まだ使いこなせたとは言い難いけど、現状でも十分便利。
BehaviorDesignerを導入
敵キャラの行動パターンは、完全なランダムで技を繰り出すだけだったので細かい思考を調整できるようにしたかった。
なので、昔に買ったけど放置していたBehaviorDesignerを導入することにした。
https://www.assetstore.unity3d.com/jp/#!/content/15277
BehaviorTreeを作ること自体はじめてだったけど、その辺はググりながらでサッと解決した。
Behavior Designer使って敵キャラの思考を細かく設定できるようにした。めっちゃ便利だけど、BehaviorTree慣れるまで少し時間かかりそう #出稼ぎダンジョン pic.twitter.com/dq7SshrKoK
— いも@EFB~相手は死ぬ~ (@adarapata) 2017年10月29日
上記のTreeは以下の思考パターンになってる
- HPが30%より大きかったら、「たいあたり」「れんぞくたいあたり」のどちらかを繰り出す
- HPが30%以下だったら、「すごいいちげき」「あばれる」のどちらかを繰り出す
いわゆる、ピンチになると発狂する挙動だけど、これくらいなら数分で書けるので非常に便利。そして楽しい
自前の戦闘システムに組み込むため、攻撃を行うActionとHPをチェックするConditionalは自分で実装した。 この辺りは仕組みが簡単なので割と量産しやすそう。
今のところ敵キャラだけに実装してるけど、味方もBehavior Treeで動けるような仕組みにはするので、HPが低かったら回復とかいい感じに思考してくれるやつをこれから作る予定。
裏側はだいぶよくなったので、そろそろ見えるとこも直していこう・・。
出稼ぎダンジョン進捗報告 1
定期的に書いていけってばっちゃが言ってた
お金払ったらテンションアゲアゲできるようにした。画面上が文字表示のウィンドウに占領されてるので所持金が自己主張強いところにしか置けなくてどうしようか迷ってる #出稼ぎダンジョン pic.twitter.com/VpZ3o1bp9x
— いも@EFB~相手は死ぬ~ (@adarapata) 2017年9月18日
前回のお産合宿の時からの変更点としては以下
- テンションシステム
- テンションアップコマンド
テンションシステム
パーティは戦闘時にテンションが変化します。テンションは攻撃したり、ダメージを受けたり様々な要素で上下します。
テンションが最高潮に達するとハイテンションモードになり、一定時間パーティが強化され戦闘を優位に進めることができます。逆にテンションが最低になればローテンションモードになりパーティは弱体化、ピンチになっていきます。うまいことコントロールしていきましょう。
テンションアップコマンド
先のテンションゲージを一回で最高潮にさせる大技です。具体的にはパーティにお金を払います。それなりの出費はかかるのでここぞというときに使うと良いでしょう。
あとダンジョンを一新したり。
ダンジョンは現状Nostalgia2を使ってます。
https://assetstore.unity.com/packages/tools/sprite-management/auto-tile-available-nostalgia-2-70610
次やりたいこと
まだコアである収益計算部分ができてないので、そっちをやりたいところ。
ただ、そろそろコードの依存関係がしんどくなってきたので、Zenjectを入れてDIしたい。Findしているアレコレをこの世から抹消したい。
https://www.assetstore.unity3d.com/jp/#!/content/17758
普段開発するとき1画面でも結構シーン分割して、動的にシーンをマージして全部結合したらFind云々をよくやってるんだけどその辺もZenjectで解決できるだろうか。
skipLast, takeLastオペレータはデータを流すタイミングをずらす
ちょっと面白かったのでメモ。
例えば複数のSubscriberを並行して処理したいなと思った時に、autoConnectで流すタイミングを合わせたとする。
Observable<Integer> foo = Observable.just(3, 4, 5).publish().autoConnect(2); foo.subscribe(data -> System.out.println("A:" + String.valueOf(data))); foo.subscribe(data -> System.out.println("B:" + String.valueOf(data)));
A:3 B:3 A:4 B:4 A:5 B:5
もちろん順番に値が流れることになる。
この時にskipLastで片方だけ流す量を調整してみる。
Observable<Integer> foo = Observable.just(3, 4, 5).publish().autoConnect(2); foo.subscribe(data -> System.out.println("A:" + String.valueOf(data))); // 5だけskipされる foo.skipLast(1).subscribe(data -> System.out.println("B:" + String.valueOf(data)));
するとこんな感じになる。
A:3 A:4 B:3 A:5 B:4
ABABAと最後のBが流れないのかと思いきや、AABABと一つ目のBがなくなってしまった。 なんでだろうと思ってソース読んだらかなりシンプルだった。
指定した数だけキューに溜めて、数を満たしたら順次OnNextに流していく実装になっている。 オペレータから見た時に、どのくらいの数のデータが流れてくるのかという情報はないので、先に指定分ずらしておくようだ。 なるほどという感じ。
ちなみにtakeLastもほぼ同じ感じ。
普通に使っててこの仕様にひっかかることはなさそう。
お産合宿11でゲーム作った
ペパボでは毎年お産合宿という創り出す系の合宿イベントをやっています。 今年はSUZURANというチーム名で参加してきました。僕はイーグルです。 osan.pepabo.com
僕は今までアクションゲームばかりでRPGを作ったことが無かったので、やってみたいなという気持ちからスマホ向けRPGを作ることにしました。
体調は良くなかったです。
過酷なお産合宿の現場です #ペパボお産11 pic.twitter.com/vgsXDctjRp
— としや (@hogemoge) 2017年8月29日
そしてできたプロトタイプがこちら。
出稼ぎダンジョンというRPGができました。 借金を背負った主人公が金で傭兵を雇ってダンジョンに潜ってお金を稼ぎ返済するRPGです。いかにコストを抑えて稼ぐかというのが肝になります。
バトルシステムはSFC時代のFFを踏襲したアクティブタイムゲージによるリアルタイムコマンド式です。一方で、味方の隊列は半熟英雄仕立てになっていて、先頭だけダメージをうける「ちょくれつ」か、全体でダメージを分散する「へいれつ」の二つの陣形を駆使して戦います。
割といい感じにできたのでデジゲー博に申し込みました。間に合うといいなあ。
おまけ
今回MVPアーキテクチャを採用しており、View = MonobehaviourとしてPureClassとしてPresenter、Modelを作成して開発してみた。 しかしながら、ゲームにおけるビューの責務は結構大きく、あれもこれもビューじゃね?みたいなことになってしまい結果的にモデルがないビューも存在したりしてしまった。あとビュー同士の関連(敵が攻撃を通知したら味方に影響を与えたりとか)もめっちゃあるので、その都度モデルにロジック書いて・・とするのが正しかったかもしれないがそれはそれで面倒さが増してたなあと思う。
下記のブログに書かれている Viewを積極的に拡大解釈していく というのは非常に大事だなと思った
あと、今回初めてRPG作ったけど今までと全然違ったのでどうやったらいいだろうみたいなのを苦労しまくった。 そのときに戦闘のフローをPlantUMLで書いたものが出てきたので、せっかくだし公開。
バトルシーン全体は開始 -> 戦闘ループ -> 終了 -> 結果という一方通行なので下記のような感じ
その中で戦闘ループを展開するとこんな感じ。アクションを受け付ける状態、アクションを実行している状態、アクションの結果を反映させる状態
Wait
受け付けるアクションをICommandableインタフェースで抽象化した様々なアクションをキューに入れる。基本一個でもキューがあるなら即時にActionに映る。
このキューはWait状態以外の時でも突っ込むことは可能で、その場合は積まれていき、Waitに戻った場合にまた先頭を取り出してActionに移る。
ちなみにこのキューはUniRxのReactiveCollectionを拡張して作った。
Action
再生するだけのアレ。ここは一方通行なので特に言うことはなく、Waitから送られてきたICommandableを実行するだけ。Animationだけ再生する予定が結局ICommandableががっつりロジック持ってたりするので失敗したっぽい。
Result
Actionの結果によってどうなったかというのを処理して、もし終了であるならInBattleのループから脱出する。
ゲームの良い設計、作ろうとしているものによって勝手が違い過ぎるので永遠の課題に思えてきた
大学のサークル合宿でUnityのハンズオンやってきた
8/19,20の二日間で、じょぎ(大学時代に所属していた技術系サークル)の合宿に参加してきた。
今回は講師として招かれたので、学生10人を対象にちょっとしたハンズオンを行った。
割と口頭ベースだったので資料は少ない
サンプルプロジェクト
こちら http://shicappa.chicappa.jp/jyogi/jyogi-bootcamp-2017.zip
公式の2Dゲームをベースにして、フィールド上のコインを集めるゲームにルールを変更しています。
この作品はユニティちゃんライセンス条項の元に提供されています
やったこと
サンプルプロジェクトをベースにUnityのハンズオンをやった。 流れは以下。
UnityのGUIだけでオブジェクトを追加したり動かしたりして改造してもらう
スクリプトを実際に書いてゲームに少し動きを付ける
残り時間フルに使って好きにゲーム作ってもらう
全員でお互いのゲームで遊んでみて、一番面白かった作品を投票する
1日目に1~3を行い、翌日の朝に4を行った。
なぜこのカリキュラムにしたか
参加者の情報がまったくわからなかったので、まずは事前アンケートでいくつかヒアリングをしてみた。
その中で、「作品を完成させたことがあるか?」という項目でほとんどがいいえと答えていたが、求める講座内容は「コードの書き方、技術の基礎部分を知りたい」という座学の要望が多いところが気になった。
個人的には、最初は技術レベルを上げることよりモノを作り上げるという経験を重ねたほうが良いと思っている。作って公開して遊んで感想をもらうというサイクルがモチベーション維持になるし、その中で苦労した部分を学べばいいのではと思う。知識が歯抜けになるのは否めない。
ということで、今回は 作る -> 見せる -> 改良する のサイクルを経験してほしかったのでハンズオン形式にした。
気を付けたこと
「遊んでもらう」という点をめちゃくちゃ重要視した。
4の試遊会はもちろんだけど、3の開発時間も定期的に隣の人に遊んでもらって感想をもらうようにしてもらった。 発表まで隠しておくというのをやるのは一つの楽しみ方だけど、他人に遊ばせずに作るゲームは往々にして高難易度ゲーで面白くなくなりがち。それに、定期的にフィードバックもらった方がモチベーションは維持できるし、他人に見せるハードルが下がっていくのでそのあたりを体験してほしかった。
どうだったか
全員無事に改造したゲームを作れていた。 10本全部遊んだけど、少なくともハンズオン終了時のままという作品はなかった。
というか、横スクロールシューティングになってたり、操作キャラが2体に増えたパズルアクションみたいになってたり割と別ゲーも散見された。
所感
全員熱意がすごかった。
講座自体は夜22時で終了していて、あとは自由時間なので、困ったら自分の部屋に来てくれたら教えますよというアナウンスをしていたら、割と代わる代わるで学生が部屋に相談に来たので、結局3時くらいまで相談に乗っていた。なので講座終了時と比べてだいぶブラッシュアップされてる作品が多かった。
元々熱意が凄かったのだと思うけど、今回の講座でそういう面白くなるサイクルにカッチリハマってくれたのであれば嬉しいなあと思うけどその辺はよくわからない。成功かどうかの判断は、11月の学園祭でどのくらい作品が出てくるかでいいと思う。
夜中にシャイアのjust do itを再生してたらやる気が出てきたけど、結局頭働かなくて寝てしまったし、睡眠は大事だなと思った。