Your SlideShare is downloading. ×
Orange Cube 自社フレームワーク 2015/3
Upcoming SlideShare
Loading in...5
×

Thanks for flagging this SlideShare!

Oops! An error has occurred.

×

Saving this for later?

Get the SlideShare app to save on your phone or tablet. Read anywhere, anytime - even offline.

Text the download link to your phone

Standard text messaging rates apply

Orange Cube 自社フレームワーク 2015/3

185
views

Published on

UnityのためのC#勉強会 2015/3/21 にて発表

UnityのためのC#勉強会 2015/3/21 にて発表

Published in: Technology

0 Comments
7 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total Views
185
On Slideshare
0
From Embeds
0
Number of Embeds
0
Actions
Shares
0
Downloads
3
Comments
0
Likes
7
Embeds 0
No embeds

Report content
Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
No notes for slide
  • 要するに、「データの更新に画面遷移(リロード)が必要とか、画面遷移するたびにロード長いとかそんなクソゲー作るな」という話
  • MonoBehaviour は1人で債務を持ちすぎ。債務分割まったくできてないど素人設計
  • ちなみに、C# 5.0のawaitはこれと似たようなコードに展開されてる。要は、C# 5.0と同じことを自前でやってる。
  • つまり、UnityとっととMonoのバージョン上げろよ
  • 命名規約的には、いまのところ await + イベント名でメソッド作ってる。いまいちかなぁと思いつつ定着。
    FirstAsync 無双。
  • Begin/End系の非同期処理と同様、行きと帰りが違う口なのが問題。ラウンドトリップをTaskで一本化してしまえば案外楽。
    逆に言うと、メッセンジャー/コマンドのペアに分解する補助関数かけば、既存MVVMフレームワークにもつなげる。
    この辺りは後でデモでライブコーディングします
  • ページ遷移をステート マシンで管理しようって言うのは割かしよくある発想。
    https://msdn.microsoft.com/ja-jp/magazine/dn818499.aspx
  • インベントリは紆余曲折あった
    ・ 昔、要るデータだけ取ってたら更新が保守しきれなくて心折れる
    ・全同期やり始める
    ・重たすぎてサーバー側に怒られる
    ・差分更新をフレームワーク化、コード生成(今ここ)
  • IteratorTasksでも言った通り、劣化コピーの実装は嫌
  • つまるところ、「C#でクロスプラットフォーム」ってこと以外にUnity使ってる利点もない。
    マップ表示だけかなぁ、ゲーム的な描画最適化頑張らないといけないの
  • このパターン守れないとだいたいスパゲッティ コード化して大変

  • Transcript

    • 1. Orange Cube 自社フレームワーク ++C++; Orange Cube 岩永信之
    • 2. 自己紹介 • C#でぐぐれ • C#でiOS/Androidゲームを作っています
    • 3. 本日の話 • 自社フレームワークの話 • 今、オープンなのは IteratorTasks だけ • できれば随時オープン化していきたい • 要求と解決策を中心に話す • チームが作っているゲームの性質・要求 • 要求に対する技術的な課題 • Unityという縛りの中での課題 • どうフレームワークを整備したか • 見せれる範囲で実際のコード • 実物デモ
    • 4. 背景 どういうものを作っていて、どういう要求があった
    • 5. チームが作っているもの 1. ストラテジー 2. RPG、リアルタイム バトルもの 3. ストラテジー
    • 6. 前提: 作っているゲームの性質 • サーバーとの通信だらけ • 非同期処理を楽にしたい • 差分更新、更新された部分の変更通知がほしい • アクション性は低い/結構なページ数 • ゲーム系フレームワークよりも、UI系フレームワーク使いたい
    • 7. こんなフレームワークができあがりました • IteratorTasks • TaskInteraction • TaskNavigation • TypeGen • Inventories/MasterRepository • Binding-CodeGen ライブコーディングでデモあり
    • 8. 非同期処理 IteratorTasks System.Threading.Tasks.Task もどき
    • 9. 非同期処理 • スマホゲームなんて非同期処理の塊 • いろんなロジックがサーバー上で動いてる • サーバーからもらったデータを表示 • そうでなくても、ネイティブUIには非同期処理が必要 • 参考: フリーズしないアプリケーションの作り方 エンド・ユーザーは、 0.5秒のフリーズでストレスを感じ、 3秒のフリーズはバグだと思う
    • 10. 例: バイト列読み込み • 同期 static byte[] ReadBytes(Stream s, int n) { var buffer = new byte[n]; s.Read(buffer, 0, n); return buffer; } ここでフリーズ の可能性あり
    • 11. 例: バイト列読み込み • begin/end、コールバック式 static void ReadBytes(Stream s, int n, Action<byte[]> callback) { var buffer = new byte[n]; s.BeginRead(buffer, 0, n, r => { var result = s.EndRead(r); callback(buffer); }, null); } 2個のメソッドをペアで 呼ぶ必要あり 後ろにさらに処理がつづいたり、 分岐・ループさせるとかなり面倒
    • 12. 例: バイト列読み込み • ContinueWith※、コールバック式 static Task<byte[]> ReadBytes(Stream s, int n) { var buffer = new byte[n]; return s.ReadAsync(buffer, 0, n).ContinueWith(t => buffer); } ※ 他のプログラミング言語だと Then という名前が多い いわゆる継続処理(continuation) 後ろにさらに処理がつづいたり、 分岐・ループさせるとかなり面倒
    • 13. 例: バイト列読み込み • await(C# 5.0※) • C#的には5.0(2012年に正式版)で解決した問題 • “Unityでなければ”、3年前に解消されているはずの問題 static async Task<byte[]> ReadBytes(Stream s, int n) { var buffer = new byte[n]; await s.ReadAsync(buffer, 0, n); return buffer; } 同期の場合とほぼ同じ 書き方で、フリーズしない ※ UnityはC# 3.0。つらい。本気でつらい。日々ソウルジェム濁る
    • 14. IteratorTasks • しょうがないんで「もどき」を使って運用してる • UnityがCoroutine(yield return)ベースの非同期処理なんで、同じ方針の、 Task互換ライブラリを作って使ってる • yield return+コールバック式のメソッドを、Task互換のクラスに変換 • Coroutineと比べた利点 • MonoBehaviourに依存しない、UnityEngine.dllに依存しない • 戻り値を返せる IteratorTasks https://github.com/OrangeCube/IteratorTasks
    • 15. 例: バイト列読み込み • iterator → Task • C#的には5.0(2012年に正式版)で解決した問題 • “Unityでなければ”、3年前に解消されているはずの問題 static Task<byte[]> ReadBytes(Stream s, int n) { return Task.Run<byte[]>(c => ReadBytesIterator(s, n, c)); } static IEnumerator ReadBytesIterator(Stream s, int n, Action<byte[]> callback) { var buffer = new byte[n]; var t = s.ReadAsync(buffer, 0, n); if (!t.IsCompleted) yield return t; callback(t.Result); } await の代わりに yield return 1段ラップ return result の代わりに callback(result) 作る側は相変わらず面倒だけども、 使う側は幾分かマシに
    • 16. 互換 • 標準TaskとIteratorTasksでコード共有 • 結構 #if 分岐でいける • 実際、後述のTaskInteraction, TaskNavigation, 通信コード生成 は #if で両対応してる #if UseIteratorTasks yield return Task.Delay(_pollingIntervalMilliseconds); #else await Task.Delay(_pollingIntervalMilliseconds).ConfigureAwait(false); #endif await のところを yield return に
    • 17. 反省: Rx使わないの? • 値1つ取るだけ(ラウンドトリップ1回)の非同期処理にRxはいまいち • (awaitと比べるとの話。begin/endとか同期よりはだいぶいい) • (Coroutineそのまま使うくらいならRx推奨) • 同期の時と同じフローで書けなきゃ嫌 • if → where、var → let になるのすら嫌 • if-else で困る • イベント ストリーム的な非同期処理には、うちでもRx的なもの使ってる • こっちはほんとにRxの本領 参考:Reactive Extensions(Rx)入門 UniRx
    • 18. 反省: バッド ノウハウすぎる • 標準ライブラリの互換ライブラリなんてものは超バッド ノウハウ • 要らなくなるべきもの • UnityがMono 3系になるだけで無用の長物 • 「すぐに要らなくなるはずだろう」が全然すぐじゃなかった… • おかげ様でものすごく安定したけども、それは恥だと思ってる • 所詮は劣化コピー • awaitと比べると不便 • スタック トレースとか追えない
    • 19. 反省: 両対応は大変 • IteraterTasks(IT)/System.Threading.Tasks(TT)両対応 #if 分岐 共通コード IT版 ライブラリA TT版 ライブラリA IteratorTasks 標準ライブラリライブラリA 共通コード ライブラリA Unity ゲーム 編集ツール(Desktop)や サーバー 4つ1セット
    • 20. 反省: 両対応は大変 • ライブラリAに依存するライブラリBがあったとして IteratorTasks A A.ForIterator A.ForThreading A.Shared B B.ForIterator B.ForThreading B.Shared ライブラリ1個増やすだけでも 参照設定がかなり面倒
    • 21. ビューとの相互アクション TaskInteraction チャネルを介したゲーム ロジックとビューとの非同期やり取り やり取りの記録、再現
    • 22. ビューとの通信 • よくあるシーケンス図 ビュー ゲーム ロジック サーバー ボタンAをタップ アニメーション表示 アニメーション表示 ボタンBをタップ よくあるミス: こいつが処理の起点 本来: こいつが処理の主体
    • 23. よくあるダメな実装 • ビューのイベントが起点で、そこに多くのコードが入る class View : MonoBehaviour, IPointerClickHandler { public void OnPointerClick(PointerEventData eventData) { // ここにゲーム ロジック書く } } ダメ!絶対!
    • 24. ビューとの通信 • ロジックが主体 ビュー ゲーム ロジック 結果のアニメーション表示 ボタンAをタップ コマンド選択して コマンドAが選ばれた 実行結果 アニメーション再生終わった この辺り結構複雑な処理 • コマンド再入力が必要なことも • アニメーションないときも ここが起点
    • 25. ベタなロジック実装 public void Start() { // 開始処理 var d = CommandSelecting; if (d != null) d(candidates); } public event Action<CommandCandidate[]> CommandSelecting; public void SelectCommand(CommandCandidate selected) { // 選ばれたコマンドを実行 var d = CommandExecuted; if (d != null) d(commandResult); } public event Action<CommandCandidate[]> CommandExecuted; public void EndCommand(CommandCandidate selected) { // ... } ビュー側に「コマンド選択して」 メッセージを投げる ビュー側から「コマンド選択結果」 を呼んでもらう Startの続きの処理 ここでいったん処理中断 ビュー側に「コマンド実行結果」 メッセージを投げる ビュー側から「結果アニメーション再生終わった」 を呼んでもらう SelectCommandの続きの処理
    • 26. ベタなロジック実装の問題 public void Start() { // 開始処理 var d = CommandSelecting; if (d != null) d(candidates); } public event Action<CommandCandidate[]> CommandSelecting; public void SelectCommand(CommandCandidate selected) { // 選ばれたコマンドを実行 var d = CommandExecuted; if (d != null) d(commandResult); } public event Action<CommandCandidate[]> CommandExecuted; public void EndCommand(CommandCandidate selected) { // ... } 処理がとびとび • フロー図と合わせて見ない と何してるのかわからない 呼んでほしいタイミングでだけ呼ばれる保証が ない • ダメなタイミングで呼ばれた時のエラー処理 が必要 • ビューを作る人がわかるドキュメントが必須
    • 27. フレームワークによっては • イベントの送り方、結果の戻し方が違ったりはする using GalaSoft.MvvmLight.Messaging; using System.Windows.Input; class BattleEngine { private Messenger _messenger; public void Start() { // 開始処理 _messenger.Send<CommandCandidate[]>(candidates); } public ICommand CommandSelecting { get; private set; } } ※ WPF, MVVM Lightの例 • ビューにメッセージを送る用のライブラリがあったり • ビューからの応答はメソッドじゃなくて、1段階クラスを挟んだり • フレームワークに適したクラスを挟んでるだけで、 やっぱりメッセージ送信と応答の受信がわかれてしんどい 「メッセンジャー パターン」 とか言ったりする
    • 28. 解決の手がかり: ビューはTask • ロジックから見て、ビュー上の動きは非同期処理(Task) • (コマンド選択など)ユーザーのタップを1つ待つ • (アニメーションなど)時間経過を待つ public static Task AwaitTap(this Button button) public static Task PlayAsync(this Animation anim) public static Task Delay(TimeSpan delay) Task使ったメッセンジャー パターンで解決 Rx使えば、 button.Tap.FirstAsync().ToTask() イベントを1つ待つ
    • 29. Taskを使ったメッセンジャー • Channelクラス Channel _channel; public async Task RunAsync() { // 開始処理 var selected = await _channel.Send<Command[], Command>(candidates); // 選ばれたコマンドを実行 await _channel.Send<CommandResult>(commandResult); // ... } CommandSelectingメッセージと SelectCommandメソッドをペアに CommandExecutedメッセージと EndCommandメソッドをペアに ビューの処理を 非同期にawait ※ IteratrTasks版だと、awaitのところがyield return
    • 30. Channelの追加の役割 Channelビュー ロジック コマンド選択して コマンドAが選ばれた 選ばれた 結果の記録 Channel ロジック コマンド選択して 記録した 結果の再現 Channel ロジック コマンド選択して サーバーと 通信 記録 再現 記録・再現ができることで • アプリ再起動時に、続きから再開 • サーバー上でのチート検証 • 対戦履歴の再生 • オンライン対戦・協力プレイ 同じ乱数シードと、 同じユーザー入力を与えれば 実行結果は一緒
    • 31. 反省 • 他のフレームワークとのつなぎこみをフレームワーク化したい • つなぎ先 • ビュー(データ バインディング)とのやり取り • サーバーとの通信 • 今は、結構手作業 • Channelにメッセージ ハンドラーを登録して、ビューを表示して、ユーザーの選択を入れ て返して… • サーバーAPIたたいて、タイムアウト管理して、通信エラー時の復帰処理して… • アプリのサスペンド時にChannelの途中記録を読みだして、ストレージに保存して、再起 動時に復元して…
    • 32. ここでいったんデモ Task, TaskInteractionの利用例
    • 33. デモ内容 • ゲームのログイン時の流れ ゲーム ロジック ゲーム ロジック ビュー • 確認ダイアログ表示 • ユーザー名入力 • 登録状況確認 • 新規登録 • ユーザー データ取得
    • 34. デモ内容 ゲーム開始 登録状況確認 サーバーと通信 登録済み 利用規約表示 同意します いいえ 名前入力 ビュー表示、ユーザー応答待ち 登録状況確認 有効な名前 NG はい データ取得 まだ 済み プレイ開始 OK
    • 35. デモ
    • 36. ページ遷移 TaskNavigation ページ遷移をステート マシンとして管理 遷移トリガーをTaskで表現
    • 37. • 例: 装備画面 ページ遷移 ユニット 装備 装備一覧 (空欄) 装備詳細 (装備中) 装備一覧 (変更) 装備詳細 (空欄) 比較 (変更) 装備一覧 (装備中) 比較 (装備中) 空欄選択 詳細 変更 戻る 戻る 戻る 選択 変更 選択 戻る 戻る 戻る 装備 選択 戻る 装備 装備 ユニット グループ 装備 グループ 装備強化 グループ
    • 38. ステート マシンとTask • ページ遷移はステート マシン • ステート • どのページにいる • トリガー • どのボタンをタップした • リストのどの要素をタップした • タイムアウトした ステート A ステート B トリガー1 トリガー2 Rx使えば、 button.Tap.FirstAsync().ToTask() Rx使えば、 list.Items.FirstAsync().ToTask() いずれにしろTaskが使える これら複数のうちの最初の1つを待つ Task.Any( button.AwaitTap(), Task.Delay(timeout)); Task.Delay(timeout)
    • 39. Taskナビゲーションをフレームワーク化 • ステート マシンの設定例 AddState(S.EquipmentInventory, new Transition { T.Item(ct => Cancel.AwaitTap, () => {}, TransitionKey.PageBack), T.Item(ct => Detail.AwaitTap(ct), x => SelectedItem = x, S.EquipmentDetail), }); AddState(S.EquipmentDetail, … どのステートのときに (一覧画面にいる) どういうトリガーで (戻るボタンを押した) どう遷移する (戻る) (詳細ボタンを押した) (詳細画面に遷移) 遷移前の処理 (選択したアイテムを記憶) CancellationTokenを受け取って Taskを返すメソッド (1つ終わったら残りはキャンセルする) ※ こいつも、WPFとUnityの両方で稼働
    • 40. フレームワーク化したことで • 「戻る」(AndroidのBackボタン)対応 • ビュー内のデータ(ViewModel※をスタックで保存、pop) • グループ • グループ内遷移: 1つのグループViewModelを共有 • グループ間遷移: 一気に数ページ戻れる • ページのビューはページ内のことに専念 • トリガーを起こすところまでがページの債務 • ページ遷移やViewModelの記録はナビゲーターの債務 ※ ビュー内でだけ必要なデータを記録しておくモデル
    • 41. 独自のUnityシーン管理 • UnityのApplication.LoadLevel使ってない • LoadLevelの問題 • 全オブジェクトの一斉破棄かけてるみたいでとにかく重たい • リソース リーク防止の一番手っ取り早い方法ではあるけど、いくらなんでも遅い • Application.LoadLevelAdditiveAsyncで読み込んで、自前でシーン管理 • 前のシーンを自前でDestroy • 読み込んだシーンをルート要素につなぎなおし というような処理を、ページ遷移管理のついでにフレームワーク化
    • 42. 反省: まだ結構定型コードが多い • ステート マシンの設定コードが結構煩雑 • コード生成で対応(Unityエディター拡張) • 結構、黒魔術的 グループ単位で設定 グループ内のページ一覧
    • 43. 反省: テキストで書くものじゃない • ステート マシン設定なんて、テキスト ベースのプログラミング言語で 書くものじゃない • ↓こういう絵で描けるVisualなDSL (と、編集用エディター拡張)が必要 • 実装も大変だし、カスタマイズ性と両立難しそう • (Visualなエディター拡張ありのナビゲーション フレームワーク自体はUnity用のものもあ るにはある) 装備一覧 (空欄) 装備詳細 (空欄)戻る 選択 戻る 装備
    • 44. 反省: ダイアログ • 今の実装はページのみ • ダイアログは別系統フレームワーク • 実際の要件的には… • UIデザイナーから上がってくるページ遷移フローはページとダイアログが同 列・混在 • ダイアログの遷移も同じナビゲーション フレームワークで動かしたいことが 多々ある
    • 45. 通信コード生成 TypeGen API定義・型定義をC#で(C#→C#コード生成)
    • 46. オンライン ゲーム • サーバーとの通信は定型文が多い • 手書きすると大量に似たようなコードを書く必要がある • シリアライズ、デシリアライズ • HTTP通信、エラー処理 • 多くの通信フレームワークはリフレクションで実現していて… • iOSで死ぬ • 性能的に、携帯端末であまり動的な処理をしたくない コード生成
    • 47. C# → C# コード生成 • 型定義、メソッド定義はstrongly-typedな言語使うのが楽 • (初代)XML、(2代目)RubyでDSL、(3代目)JSONとかで書いてた • だんだんやりたいことが複雑に • 配列に対応、nullableに対応、ジェネリック、型の派生に対応… • 要するに、型に厳しい言語で書けることと要件変わらなくなった • なら、最初からC#で書けばいい
    • 48. 型定義例 [Comment("装備品")] class Equipment { [Comment("アイテムID")] int Id; [MasterForeignKey(typeof(EquipmentMaster))] [Comment("アイテムマスターID")] int MasterId; [InventoryForeignKey("Enhancers")] [Comment("装備強化アイテムのID")] int?[] EnhancerIds; [Required(false, true)] [Comment("インスタンスごとの追加能力")] AbilityIndexedValue[] InstanceAbilities; } [Comment("装備する")] void AddEquipment( [Comment("誰の(ユニットのID)")] int unitId , [Comment("何番目の装備スロットに")] int slot , [Comment("何を(装備品のID)")] int equipmentId , [Required] [Comment("変更結果")] out SyncDifference<Unit>[] Units ); 型定義(C#でクラスを書く) API定義(インターフェイスのメソッドを書く) プレーンなクラス、フィールドを書く いくつか、属性を付けて生成結果を制御
    • 49. 定義C#の読み込み • ビルドしたDLLからリフレクションで読み込み • 普通に System.Type を読んでる • 他の選択肢(作り始めた当時はなかったもの) • System.Reflection.Metadata • 依存先の解決できなくてもDLL単体で読める • Roslyn C# Scripting API (Microsoft.CodeAnalysis.Scripting.CSharp) • 今まだ簡単に使える段階にない • ほんとはこれでやりたかったけども、Roslynのリリース自体が思った以上に遅く
    • 50. 生成物: 基本 • 型に厳しい言語で面倒なのは、シリアライズとUIバインディング • この辺りを生成 • JSONシリアライズ/デシリアライズ • データバインディング用(INotifyPropertyChanged)実装 • 通信処理 • メソッドと対応するURL作って • 送りたいデータをJSON化してPOST • 返ってきたデータをJSONデシリアライズ
    • 51. 生成物: 普通のクラス public partial class Equipment { /// <Summary> /// アイテムID /// </Summary> public int Id { get; set; } … public Equipment(int id, …) { … Id = id; … } public Equipment(Equipment x) { … } public static Equipment Clone(Equipment x) { … } プロパティ コンストラクター引数 コピー コンストラクター ディープ クローン
    • 52. 生成物: JSON化・JSON parse partial class Serializer { private string Key(int index, Equipment _) { switch (index) { case 0: { return "id"; } … default: return null; } } private void Serialize(int index, Equipment x) { switch (index) { case 0: { Serialize(x.Id); break; } … } } … partial class Deserializer { private Equipment Deserialize(string key, Equipment x) { x = x ?? new Equipment(); switch (key) { case "id": { x.Id = Deserialize(default(int)); break; } … } return x; } … コード生成で静的に(ビルド時に)作ってしまえば リフレクション要らない
    • 53. 生成物: データ バインディング用クラス [Serializable] [DataContract] [FileExtensionsAttribute("Equipment")] [Description("装備品")] public partial class Equipment : BindableBase, IIdentifiable, IChild, IValidatable { /// <Summary> /// アイテムID /// </Summary> [DataMember] [JsonProperty(PropertyName = "id")] public int Id { get { return _id; } set { SetProperty(ref _id, value); } } private int _id; … 主に編集ツール(Windowsデスクトップ、WPF)用 INotifyPropertyChanged実装
    • 54. 生成物: 通信コード namespace DataModels { public partial class Api : IUnitApi { public Task<AddEquipmentResponse> AddEquipmentAsync(AddEquipmentRequest arg, …) { OnRequest(arg); var t = _client.Post("AddEquipment", "/Hero/addEquipment", arg, … t.ContinueWith(_ => OnResponse(_.Result)); return t; } … HTTP Post JSON Serialize/Deserialize 呼び出し 全API共通のイベント発火 引数・戻り値をそれぞれ1つのクラスにラップ (モック作成でその方が都合がよかった※) ※ JSONでAPIの応答モック データを作れる 引数の追加・削除後のモック コード修正が楽だった
    • 55. 生成物: その他 • 型定義JSONも出力 • C#で型定義しだす前の旧型式 • (内部的には「contract JSON」と呼んでる) • サーバー側は外注、かつ、PHP • サーバー側のコード生成は発注先に任せてたので、C#を前提にできなかった • プロジェクトの途中でC#定義に切り替えた • 急に形式を変えるわけにもいかなかった • インベントリ/マスター リポジトリ • (次節で説明)
    • 56. 補足: サーバー側C# • 複雑なロジックだけサーバーもC# • 理由 • 2重開発がさすがに無理 • C#で書く方が楽 • 動かし方 • MonoでPHPと同一サーバー内稼働 • 合成・レベルアップ • ダンジョン、対人バトルのチート検証 • 一部(性能を求める)機能だけWindowsサーバー/IIS • リアルタイム バトル
    • 57. 反省: .NETの型システム引きずりすぎた • 非null参照型 • void null非許容 null許容 値型 int int? 参照型 string ない※ こいつがつらい • nullを認めたくない場合、[Requred]属性を付けてる • コード生成の分岐が増えて大変 • .NET経験のない人への説明が大変 ※ .NET最大の後悔(million dollar mistake って言われてる) 後からの修正でフレームワークに組み込むのはものすごく大変 Task A() Task B(T arg) Task<U> C() Task<U> D(T arg) Task<void> A(void arg) Task<void> B(T arg) Task<U> C(void arg) Task<U> D(T arg) 引数・戻り値の有無で4パターンの分岐 こう書けると楽だった † どちらも今、C#チームが新機能として検討中だけど、入るとしてC# 7.0(2年くらい先?)
    • 58. 反省: 高機能化しすぎた • 黒魔術度合いが半端ない • コード生成で、ジェネリックや派生クラスに対応するの結構大変 • リポジトリ(次節で説明)対応がやりすぎ感ある • 結構ぎりぎりのバランスで成り立ってて • 修正入れるのそこそこ大変に • ドキュメント整備できてないので公開してもきっと他人に使えない
    • 59. 反省: コード増えすぎる • JSON化、すべて静的コード生成 • ソースコード量のうちの結構な割合がJSONがらみ • アプリのバイナリ サイズ肥大 • 一方、利点もあって • ソースコードが目に見えるんで、問題を見つけやすい • JSON読み書きに問題あった時にブレイク ポイント仕掛けられる
    • 60. リポジトリ Inventories/MasterRepository サーバーとのデータ同期、差分更新 ローカル ストレージにデータをキャッシュ
    • 61. データは全部サーバー上にある • 必要な分だけ通信でもらってる • クライアント上でも正規化した状態で管理 • 差分更新 Unit Id: 1 MasterId: 1 EquipmentId: 120 Equipment Id: 120 MasterId: 39 EnhancerIds: [ 11, 15, 21 ] Enhancer Id: 11 MasterId: 93 Grade: 4 { { "action": "update", "item": { "id": 1, "master_id": 1, "equipment_id": 82 } }, { "action": "remove", "id": 2 } } 変化したところだけもらう
    • 62. 問題: インスタンスが変わる • サーバーとの同期でインスタンスが変わる • 漏れなく追従するの、手動では無理 UI インベントリ Unit Id: 1 MasterId: 1 EquipmentId: 120 参照 同期前 UI インベントリ Unit Id: 1 MasterId: 1 EquipmentId: 120 参照 同期後 Unit Id: 1 MasterId: 1 EquipmentId: 82 差分更新の粒度的に プロパティ1つだけの更新でも インスタンス丸ごと新しくなる 古い方参照しっぱなし UI側が更新されない ... インベントリ内でも同様 参照先が変わる 装備 変更
    • 63. 2系統のデータ • マスター • ユニットや装備の名前、パラメーターなど、ユーザーによらないデータ • ほとんど更新されない • かなりデータ量が多い • インベントリ • ユーザーの手持ちの品 • ことあるごとに更新がかかる デバイスのローカル ストレージに キャッシュを保存しておきたい
    • 64. マスター リポジトリ • ライブラリを整備 • バージョンとデータをローカルに保存 • バージョンが一致していたらローカルから読む • 不一致ならサーバーから取り直す • ID をキーにした Dictionary 化 • コード生成を整備 • [Master]属性がついている型を束ねて MasterRepository 型を生成 • LoadAsyncメソッドで、上記ライブラリを呼ぶ
    • 65. インベントリ リポジトリ • ライブラリを整備 • Inventoriesライブラリ • 現在のインスタンスをID検索できる • インスタンスの更新イベントを公開 • コード生成を整備 • 通信APIをフックして、リポジトリを自動更新 • 他のインベントリ、マスターが必要なクラスに それぞれのリポジトリを渡す • ユニット→装備 とかのID検索プロパティを生成
    • 66. インベントリ リポジトリ • ライブラリを整備 • Inventoriesライブラリ • 現在のインスタンスをID検索できる • インスタンスの更新イベントを公開 • コード生成を整備 • 通信APIをフックして、リポジトリを自動更新 • 他のインベントリ、マスターが必要なクラスに それぞれのリポジトリを渡す • ユニット→装備 とかのID検索プロパティを生成 class DictionaryInventory<T> { IEnumerable<T> Items { get; } IEvent<ChangedArg<T>> Changed { get; } } • IEventはIObservableと似たような機能 • つまり、IEnumerableかつIObservableな型 • 現在の値を取る → IEnumerable • 値の変化をもらう → IObservable • LINQ+Rx で、Where とか Select を定義可能
    • 67. インベントリ リポジトリ • ライブラリを整備 • Inventoriesライブラリ • 現在のインスタンスをID検索できる • インスタンスの更新イベントを公開 • コード生成を整備 • 通信APIをフックして、リポジトリを自動更新 • 他のインベントリ、マスターが必要なクラスに それぞれのリポジトリを渡す • ユニット→装備 とかのID検索プロパティを生成 internal IEnumerable<IDisposable> Change(SyncDifferenceItem diff) { switch(diff.PropertyName) { case "Unit": return Units.Change(diff.Difference); case "Equipments": return Equipments.Change(diff.Difference); case "Enhancers": return Enhancers.Change(diff.Difference); ...
    • 68. インベントリ リポジトリ • ライブラリを整備 • Inventoriesライブラリ • 現在のインスタンスをID検索できる • インスタンスの更新イベントを公開 • コード生成を整備 • 通信APIをフックして、リポジトリを自動更新 • 他のインベントリ、マスターが必要なクラスに それぞれのリポジトリを渡す • ユニット→装備 とかのID検索プロパティを生成 public partial class Equipment : IDependent<MasterRepository> { protected MasterRepository _masters; void SetRepository(MasterRepository repository) { _masters = repository; if (InstanceAbilities != null) InstanceAbilities.SetRepository(reposito } public EquipmentMaster EquipmentMaster { get { return _masters.GetEquipment( } 通信APIをフックして、このインターフェイス を持ったクラスにリポジトリを渡す ID検索して所望のインスタンスを得る
    • 69. 反省: IObservable • IObservableとほぼ同機能な型を作ってしまっている※ • IEvent<T> ≒ IObservable<EventPettern<T>> • Unityがシングル スレッド動作なので、同時実行制御だけさぼってる • IObservable<T>との差は: • senderを取れる • OnError/OnCompleteがない • でも結局、senderはほとんど使ってない • IObservableでよかった • Rxに移行しようか悩み中 • IEventに対して、Rxと同じような、Subject, Where, Select, Subscribe実装してる ※ 時期の問題もあった。今から作るならUniRx使うと思う
    • 70. データ バインディング Binding-CodeGen ビューには「UI上のどこにどのデータを出したい」だけを記述 データが更新されたらUIを自動更新
    • 71. UIが多いゲーム • 作ってるゲームの性質的にはUIフレームワーク中心 • 3Dとか物理エンジンとか要らない • むしろ、XAML※的機能がほしい • Data Binding (CommonView, DialogBase) • ConentControl, ItemsControl • UI仮想化 • ゲームだからって常にゲーム フレームワークが最適じゃない • UIが得意なのは一般OS • 一般OSのUIフレームワークの上にゲーム描写を重ねたい • 実際、Win8アプリはXAML UIの上にDirect Xサーフェスを 重ねれる ※ WPF(Windowsデスクトップ)、Silverlight(Webプラグイン)、WinRT(Windowsストア アプリ)の系譜
    • 72. データ バインディング • データ バインディング • UI系フレームワークの要件: • UI上のどこにどのデータを出したい • データが更新されたらそこだけ更新したい <StackPanel <TextBox Text="{Binding X}" /> <TextBox Text="{Binding Y}" /> </StackPanel> new Point { X = 10, Y = 20, }; オブザーバー パターンで実現 ※ この辺りはこれだけで1時間セッション コースになるので今回は割愛 検索すればWPF/WinRTとか、JavaScript系フレームワークの記事が出てくるはず
    • 73. データ バインディング コード生成 • 自社フレームワークでは、コード生成で実現 • リフレクションが使えないので • モデルのプロパティと、ビューのプロパティをつなぐだけの簡易なもの [DataContextType(typeof(EquipmentContentModel))] partial class EquipmentContent : MonoBehaviour { [BindingProperty("Equipment")] public Equipment Equipment { set { SetThumbnail(value); } } ビューのコード partial class EquipmentContent { public EquipmentContentModel ViewModel { get … void SourcePropertyChanged(object sender, … { base.SourcePropertyChanged(sender, e); var data = DataContext as EquipmentContentModel; if(data == null) return; if (e.PropertyName == "Equipment") Equipment = data.Equipment; … コード生成結果 コード生成 (Unityエディター拡張)
    • 74. 型に応じたプレハブの選択 • ContentControl, ItemsControlクラス • たいていのUIは、 • 「このデータ型に対して、このプレハブを作りたい」みたいな要件ばっかり • Unityインスペクターでプレハブを刺しとく • 1要素版がContentControl、リスト版がItemsControl
    • 75. UI仮想化 • VirtualizingListViewクラス • 仮想化 • リストのうちの、画面に見えてる範囲だけ、プレハブをInstantiate • 残りは作らない、隠れたら消す • これがないと • 「アイテムは100個までしか持てません」みたいなゲームになる • 数が多いと一覧画面に入った瞬間に数秒フリーズ • スクロールもきつい
    • 76. 反省: 同一プロジェクト内コード生成 • Unityエディター拡張は、コンパイル エラーがある状態で動かせない • コード生成結果でエラーになると、コード生成し直しがままならない • エラーがなくなるまでコードを元に戻してから生成しなおし
    • 77. まとめ
    • 78. まとめ (1/2) • IteratorTasks • System.Threading.Tasks.Task もどき • TaskInteraction • チャネルを介したゲーム ロジックとビューとの非同期やり取り • やり取りの記録、再現 • TaskNavigation • ページ遷移をステート マシンとして管理 • 遷移トリガーをTaskで表現
    • 79. まとめ (2/2) • TypeGen • API定義・型定義をC#で(C#→C#コード生成) • Inventories/MasterRepository • サーバーとのデータ同期、差分更新 • ローカル ストレージにデータをキャッシュ • Binding-CodeGen • ビューには「UI上のどこにどのデータを出したい」だけを記述 • データが更新されたらUIを自動更新