コールバックと戦う話
2017/09/03
@toRisouP
自己紹介
• 名前 とりすーぷ(@toRisouP)
• 同人ゲームとか作ってます
• 最近は割と忙しい
– しごと
– 同人ゲーム開発
– どんかつ
今回の内容
ネトゲを作ってたら通信ラグや非同期処理だらけに
なって死にそうな思いをしたので、
どうやってそこら辺の問題を解決したかを話す
今回の内容
ネトゲを作ってたら通信ラグや非同期処理だらけに
なって死にそうな思いをしたので、
どうやってそこら辺の問題を解決したかを話す
時間が足りないので話題をめっちゃしぼって
「コールバックとの戦い方」の話をします
ハクレイフリーマーケット
• 2016年1月からずっと作ってる東方二次創作ゲーム
– 2017年10月リリース予定
どんなゲーム?
• アイテムを持って運んで、陣地に集めるゲーム
• 試合終了時の合計点数が大きい方が勝ち
使ってるフレームワークとか
• Photon Cloud
– ネットワークゲーム用のクラウドサーバとAPI群を
提供してくれるサービス
• Photon Unity Networking(PUN)
– 上記 Photon CloundのUniyt向けSDK
• ニフティクラウド mobile backend (NCMB)
– ユーザやデータのDB管理やオブジェクトストレージを提供してくれ
るサービス(mBaaS)
非同期処理とコールバック
ネトゲ開発は非同期通信だらけ
• 通信があるせいでほとんど非同期処理だらけになる
– 「ログインしてユーザ情報を取得する」のも非同期処理
– 「オブジェクトをインスタンス化する」だけでも非同期処理
– 「アイテムが今拾える状態か確認する」のも非同期処理
– 「全員の準備が完了するまで待つ」のも非同期処理
非同期処理とコールバック
• 非同期処理の面倒くさい部分ライブラリがなんとかしてくれる
– スレッドの管理とか同期コンテキストとかは意識しなくてもいい
• 一方で、これらライブラリはコールバック処理を多用している
– 非同期処理の結果を受け取るにコールバック処理を使っている
コールバック?
• 非同期処理の実行後その結果を通知するために
関数を実行する手法
– 非同期処理の制御で一番シンプルで単純な実装
– 自分で任意の関数を登録するパターンや、決められた関数が実行される
パターンがある
• PUN、NCMBともにコールバックが大量に出て来る
– ライブラリの実装が悪いとかいうわけではなく、
シンプルに作ればそうなるよね
– ただしPUNテメーはだめだ
NCMBのコールバック
• NCMB
– コールバック関数を自分で登録する
コールバックの例:NCMBのログイン処理
PUNのコールバック
• PUN
– イベントが発生すると既定の名前の関数が実行される
– 例:
• 誰かがゲームに参加した:OnPhotonPlayerConnected()
• 部屋のリストが更新された:OnReceivedRoomListUpdate()
– ちなみにPUNはコールバックの実装にSendMessageを使ってる
• 旧Unity.Networkの実装に似せたのが原因
• このせいでめちゃくちゃ使い勝手が悪くなっている
vs コールバック
NCMBのコールバックを
使っていて発生する問題
コールバックがネストしまくる
• NCMBで ログイン → ステータス取得 → 更新して保存
コールバックがネストしまくる
• NCMBで ログイン → ステータス取得 → 更新して保存
ログインのコールバックブロック
詳細情報取得のコールバックブロック
データ保存のコールバックブロック
コールバックがネストすると…
• エラーハンドリングが困難になる
• 処理のスコープが曖昧になってしまう
• 単純にインデントしまくって読みにくい
• 人類の手に負えない
一方のPUNで起きる問題
PUNのコールバックの例
←関数がバラバラに書いてあるだけで、
つながりが全く読めないコードみたいだけ
どこれでちゃんと動きます
PUNのコールバックの例
PUNのコールバックの例
PUNを使って以下のフローを実装した例
ゲームサーバに繋ぐ
↓
ランダムな部屋に参加する
↓
部屋に参加できたらシーン遷移
失敗したら新しく部屋を作る
↓
部屋が作れたらシーン遷移
作るのに失敗したら終了
PUNのコールバックの例
サーバに接続する処理を実行
PUNのコールバックの例
成功 or 失敗でどっちかの処理に飛ぶ
成功時:OnJoinedLobby()
失敗時:OnFailedToConnetoToPhoton()
PUNのコールバックの例
「ランダムな部屋に参加」
成功時:OnJoinedRoom()
失敗時:OnPhotonRandomJoinFailed()
PUNのコールバックの例
参加できなかったので「新しく部屋を作る」
成功時:OnJoinedRoom()
失敗時:OnPhotonJoinRoomFailed()
PUNのコールバックの例
同じメソッドが別の文脈で実行されうる
・ランダムな部屋に参加して成功したパターン
・新しく部屋を作ってその部屋に参加したパターン
この形式のやばいところ
• コードを見ても処理のつながりが全く把握できない
– PUNのドキュメントを片手にコードを読まないとマジでわからない
• 処理の文脈が途切れてしまう
– 別の処理でも実行されるコールバックが同じだったりする場合、
このコールバックが何の処理に対するものなのかがわからない
– そのためにわざわざフィールドに状態変数作るのめっちゃ馬鹿らしい
コールバックのやばいところ
• 自分で関数登録するスタイルコールバック(NCMB)
– 処理を直列に連結したり、複数並列実行しようとすると
ネスト地獄になって管理できなくなる
• 既定の関数が呼び出されるコールバック(PUN)
– 何を実行したらどの処理が次に実行されるのか?が把握しにくい
– コード上から処理の文脈が失われて保守性が最悪になる
コールバックは総じてヤバイ
素手で立ち向かうともれなく死ぬ
コールバックと戦うための道具
コールバックと戦うための道具
• Task
– .NET Framework 4 から搭載された並列処理の機構の1つ
• 一連の処理を「Task」というかたまりにまとめて管理する
• async/await と組み合わせると強くなる
– JavaScriptでいうPromise / JavaでいうFutureに似てる
• Observable
– Reactive Extensionsで提案された概念
– Taskをめっちゃ高機能化したイメージ(ざっくりとした説明)
• 複数のイベントメッセージに対応
• Schedulerでスレッドやタイミング管理ができる
• Operatorでメッセージの連結、合成、加工ができる
Taskを使う? Observableを使う?
• 簡単な非同期処理であればTask (async/await) で十分
• そこからさらに複雑な処理が必要になるならObservable(Rx)
Taskを使う? Observableを使う?
• 単純な非同期処理であればTaskで十分
• そこからさらに複雑な処理が必要ならObservable(Rx)
と、言われているがこれはあくまで
Unityを使わないピュアなC#開発の話
Unityだとどうなの?
• Task
– Unity 2017.1以降であれば MonoRuntimeを.NET 4.6にすれば使える
• けど制約はある
• それ以前のバージョンでもゴニョゴニョすれば使える
– Unityでは導入に制約がある or 導入が面倒くさいのが難点
– 言語機能だけあって、async /await がめっちゃ強い
• Observable
– Unityなら「UniRx」を導入すれば使える
– 表現力が高すぎてTaskと比べると学習コストが高い
– Operatorとゲーム開発の親和性が高い
とりあえずは慣れてる方を
使えばいいんじゃない?
(自分はUniRxを多用するのでそのまま自然とObservable使うことになった)
コールバックを
Observableにしてしまおう
コールバックをObservableにする例
• Observable.Createで
コールバックごと処理を
ラップしちゃう
– 成功したらOnNextに結果を流す
– 失敗したらOnErrorに例外を流す
コールバックをObservableにする例
• Observable.Createで
コールバックごと処理を
ラップしちゃう
– 成功したらOnNextに結果を流す
– 失敗したらOnErrorに例外を流す
コールバック関数の内部で
Observerに値を渡す
Observableに変換すれば
• さっきのこのネストしまくってたやつが…
ログインのコールバックブロック
詳細情報取得のコールバックブロック
データ保存のコールバックブロック
こうなる
こうなる
ネストがなくて処理がフラットになってる!
データのフローもわかりやすい!(上から流れるように読める)
Observableにするのが
面倒くさい?
ライブラリ化しました
• NcmbAsObservable
– NCMBのUniRxラッパー
– https://github.com/TORISOUP/NcmbAsObservable
• PhotonRx
– PUNのUniRxラッパー
– https://github.com/TORISOUP/PhotonRx
• 両方ともgithubに公開しています
– プルリクエストお待ちしております
さらに応用として
Taskのawaitを再現する
コルーチンとObservable
• コルーチンを使えばawaitを再現できる
– Observableはコルーチン上でyield returnで待機できる
– 記述量はTask+awaitより増えちゃうけどね
– コルーチンのデメリットを引き継ぐデメリットはある
例:コルーチン上でObservableを待つ
• OnCollisionEnter()をコルーチン上で待ってみる
衝突検知ストリーム(3秒でタイムアウト)
結果が出るまでコルーチン上で待つ
結果をif文で判定して処理の継続
さっきのPUNのやつを書き換える
1.サーバ接続のコルーチンを作る
1.サーバ接続のコルーチンを作る
サーバへの接続結果を待つObservable
結果のBooleanを返すyield return
通信待機のyield return
1.サーバ接続のコルーチンを作る
←ここがawaitに相当する
2.部屋に参加するコルーチンを作る
2.部屋に参加するコルーチンを作る
部屋に繋ぐ方法は抽象化しておく
3.これらを順番に呼び出すコルーチンを作る
3.これらを順番に呼び出すコルーチンを作る
Observable.FromCoroutineValue<T>を使うと
コルーチンが yield return した値を取得できる
↓
コルーチンをObservableに変換し、
それをawaitすることで同期的に処理を書ける
3.これらを順番に呼び出すコルーチンを作る
サーバに繋ぐコルーチン
失敗したら終了
成功したら次へ
ランダムな部屋に参加するコルーチン
成功したらシーン遷移
失敗したら次へ
成功したらシーン遷移
失敗したら終了
部屋を作るコルーチン
←これと比べたら記述量は増えてるけど、
処理のフローは見やすくなった(はず)
まとめ
• コールバックはそのまま扱うともれなく死を迎える
– 仕組みが単純すぎて複雑なフローを表現できない
– TaskとかObservableとかのイケてる仕組みに載せるのが吉
• TaskとObservableのどっちを使うかは考えよう
– ピュアなC#開発とUnity開発は事情が違うので注意が必要
– 以前はUniRxが強かったが、
Unity 2017.1の登場で事情が変わってくるかも

コールバックと戦う話