More Related Content Similar to Orange Cube 自社フレームワーク 2015/3
Similar to Orange Cube 自社フレームワーク 2015/3 (20) Orange Cube 自社フレームワーク 2015/33. 本日の話
• 自社フレームワークの話
• 今、オープンなのは IteratorTasks だけ
• できれば随時オープン化していきたい
• 要求と解決策を中心に話す
• チームが作っているゲームの性質・要求
• 要求に対する技術的な課題
• Unityという縛りの中での課題
• どうフレームワークを整備したか
• 見せれる範囲で実際のコード
• 実物デモ
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。つらい。本気でつらい。日々ソウルジェム濁る
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と比べると不便
• スタック トレースとか追えない
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
31. 反省
• 他のフレームワークとのつなぎこみをフレームワーク化したい
• つなぎ先
• ビュー(データ バインディング)とのやり取り
• サーバーとの通信
• 今は、結構手作業
• Channelにメッセージ ハンドラーを登録して、ビューを表示して、ユーザーの選択を入れ
て返して…
• サーバーAPIたたいて、タイムアウト管理して、通信エラー時の復帰処理して…
• アプリのサスペンド時にChannelの途中記録を読みだして、ストレージに保存して、再起
動時に復元して…
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の両方で稼働
43. 反省: テキストで書くものじゃない
• ステート マシン設定なんて、テキスト ベースのプログラミング言語で
書くものじゃない
• ↓こういう絵で描けるVisualなDSL (と、編集用エディター拡張)が必要
• 実装も大変だし、カスタマイズ性と両立難しそう
• (Visualなエディター拡張ありのナビゲーション フレームワーク自体はUnity用のものもあ
るにはある)
装備一覧
(空欄)
装備詳細
(空欄)戻る
選択
戻る
装備
44. 反省: ダイアログ
• 今の実装はページのみ
• ダイアログは別系統フレームワーク
• 実際の要件的には…
• UIデザイナーから上がってくるページ遷移フローはページとダイアログが同
列・混在
• ダイアログの遷移も同じナビゲーション フレームワークで動かしたいことが
多々ある
46. オンライン ゲーム
• サーバーとの通信は定型文が多い
• 手書きすると大量に似たようなコードを書く必要がある
• シリアライズ、デシリアライズ
• HTTP通信、エラー処理
• 多くの通信フレームワークはリフレクションで実現していて…
• iOSで死ぬ
• 性能的に、携帯端末であまり動的な処理をしたくない
コード生成
47. C# → C# コード生成
• 型定義、メソッド定義はstrongly-typedな言語使うのが楽
• (初代)XML、(2代目)RubyでDSL、(3代目)JSONとかで書いてた
• だんだんやりたいことが複雑に
• 配列に対応、nullableに対応、ジェネリック、型の派生に対応…
• 要するに、型に厳しい言語で書けることと要件変わらなくなった
• なら、最初からC#で書けばいい
49. 定義C#の読み込み
• ビルドしたDLLからリフレクションで読み込み
• 普通に System.Type を読んでる
• 他の選択肢(作り始めた当時はなかったもの)
• System.Reflection.Metadata
• 依存先の解決できなくてもDLL単体で読める
• Roslyn C# Scripting API (Microsoft.CodeAnalysis.Scripting.CSharp)
• 今まだ簡単に使える段階にない
• ほんとはこれでやりたかったけども、Roslynのリリース自体が思った以上に遅く
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;
}
…
コード生成で静的に(ビルド時に)作ってしまえば
リフレクション要らない
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. 反省: 高機能化しすぎた
• 黒魔術度合いが半端ない
• コード生成で、ジェネリックや派生クラスに対応するの結構大変
• リポジトリ(次節で説明)対応がやりすぎ感ある
• 結構ぎりぎりのバランスで成り立ってて
• 修正入れるのそこそこ大変に
• ドキュメント整備できてないので公開してもきっと他人に使えない
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側が更新されない
...
インベントリ内でも同様
参照先が変わる
装備
変更
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使うと思う
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エディター拡張)
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を自動更新
Editor's Notes 要するに、「データの更新に画面遷移(リロード)が必要とか、画面遷移するたびにロード長いとかそんなクソゲー作るな」という話 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使ってる利点もない。
マップ表示だけかなぁ、ゲーム的な描画最適化頑張らないといけないの このパターン守れないとだいたいスパゲッティ コード化して大変