Zenject Decorator Binding
ZenjectにはDecoratorパターンをサポートするDecorator Bindingなる機能があったりします。あんまりメインで使うような機能ではありませんが覚えておくと便利かもしれません。
Decorator パターン
DecoratorパターンはGoFで定義されてるデザインパターンの一つです。既存のオブジェクトに対して動的な機能追加を行う場合などに効果を発揮します。 例えば超絶シンプルなRPGを作るときに、攻撃力とHPを持つクラスがあるとします。ステータス部分は共通のインタフェースとして、戦士と魔法使いの二種類を作ります。
public interface IStatus { int Life { get; } int Attack { get; } } public class Knight : IStatus { public int Life => 20; public int Attack => 10; } public class Wizard : IStatus { public int Life => 15; public int Attack => 2; }
これらはIStatusで抽象化できるので、攻撃の数値部分などは同じように扱えます。ところでRPGにはやはり攻撃力アップなどバフ効果も欲しいですね。その場合はAttackの補正処理をかけることになるのですがどこに実装しましょう。例えばIStatusを持った抽象クラスのBaseCharacterなどが生まれて、そこにバフクラスを渡すような処理にするのもありだと思います。
public abstruct class BaseCharacter : IStatus { public virtual int Life { get; } public virtual int Attack { get; } public void SetBuffStatus(IStatusBuffer buff) { // 内部パラメータをあれこれする処理 } } var knight = new Knight(); knight.SetBuffStatus(new AttackUpBuffer()); // バフをかける
この場合、バフの種類が増えていくとちょっと大変そうですね。メソッド内に変更の実装を書くならSetBuffStatusが肥大化します。じゃあBuff側に実装を書いてそのメソッドを呼ぶだけ、という形にすると今度はBuff側から更新できるようにBaseCharacterがパラメータ更新メソッドを外部に公開することになります。どちらにせよ、既存への影響を与えがちです。気合でなんとかしましょう。
Decoratorパターンは、上からインスタンスを被せることで変更を少なくします。
// 攻撃力を高めるバフ public class AttackUpDecorator : IStatus { private readonly IStatus _decorator; public int Life => _decorator.Life; public int Attack => _decorator.Attack * 2; // 強い! public AttackUpDecorator(IStatus decorator) { _decorator = decorator; } } var knight = new Knight(); var buffKnight = new AttackUpDecorator(knight); // バフをかける
同じinterfaceを持ったDecoratorクラスを用意することで、同じ振る舞いを持たせたまま拡張ができるようになります。
この実装方法のメリットは以下です。
- 既存のオブジェクトへの変更が少ない
- 呼び出し側が変化を気にしなくてよい
すでに動いているプロジェクトに対して効果的だったりします。
Zenjectでの実装方法
Zenjectでバインドしているインスタンスに同様のDecorateを行いたい場合は以下で実現できます。
public class StatusInstaller : MonoInstaller { public override void InstallBindings() { // いつものバインド Container.Bind<IStatus>().To<Knight>().AsCached(); // IStatusをAttackUpDecoratorでDecorateする Container.Decorate<IStatus>().With<AttackUpDecorator>().AsCached(); } }
Decorate<T>().With<InheritedT>
といった感じで一行で実装できます。便利。この場合ResolveAttackUpDecorator
です。
どう使うの?
上記のようなRPGはかなり極端な例なので実際使えるかという推敲すべきですが、デバッグ、プロファイリングの処理を書くには有効ではないかなと思います。あとゲームで言うとローディング中の表示をDecorateで切り出すのももしかしたらありかもしれないですね。
public interface IAsyncLoadable<T> { Task<T> Load(); } public class FooLoader : IAsyncLoadable<Foo> { public async Task<Foo> Load() { // なんか読み込みロジック } } public class LoadingIconDecorator : IAsyncLoadable<T> { private readonly IAsyncLoadable<T> _decorator; public LoadingIconDecorator(IAsyncLoadable<T> decorator) { _decorator = decorator; } public async Task<T> Load() { ShowLoadIcon(); // ローディングアイコン出すなどの処理 await _decorator.Load(); CloseLoadIcon(); // 閉じるなど } } Container.BindInterfacesAndSelfTo<FooLoader>().AsCached(); Container.Decorate<IAsyncLoadable<Foo>>() .With<LoadingIconDecorator>() // FooLoader実行時にローディングアイコンを出すようにDecorate .AsCached();
実際便利かはわからないので誰かやってみて!
性質上、DecoratorContextとの相性はよいのでこちらも併せて使っていくといい感じに分離できておススメです。
https://github.com/svermeulen/Zenject#scene-decorators
ただ、Bind終わった後に動的なDecorateしたいなあという気持ちが個人的には強かったりする。シーン開始時は何もないけど、ゲーム中に動的にFactoryから生成されたものにDecorateされたりとか。その辺うまくやれないか考えてみよ。
InjectとInjectLocal attribute
Zenjectのソース読んでたらInjectLocal
というAttributeが内部にあったのでなんだろうと調べたメモ。実用的なやつではない。
どのContainerからInjectされるのか
そもそも、Bindされたオブジェクトはどこから依存しているオブジェクトをもらえるのでしょう。これは基本的には自身がBindされているContainerになります。次のコードを見てみましょう。Fooに依存しているBarを生成するコードです。
public class Foo { public ContainerName { get; private set; } public Foo(string containerName) { ContainerName = containerName; } } public class Bar { [Inject] Foo _foo; public void DoBar() => Debug.Log(_foo.ContainerName); }
public class FooInstaller : MonoInstaller { public override void InstallBindings() { // 現在のContainerとProjectContextの両方でFooを用意しておく ProjectContext.Instance.Container.Bind<Foo>().AsCached().WithArguments("ProjectContext"); Container.Bind<Foo>().AsCached().WithArguments("Scene"); var subContainer = Container.CreateSubContainer(); subContainer.Bind<Foo>().AsCached().WithArguments("Sub"); // 現在のContainerにBarをBindする Container.Bind<Bar>(); Container.Resolve<Bar>().DoBar(); } }
=> Scene
この場合、Barに渡されるFooインスタンスは同じContainerに所属しているインスタンスを渡されます。 同じContainerにFooが存在しない場合、親のContainerを探しに行きます。親も持っていなかったらさらにその親・・と最終的にProjectContextまで辿っていくことになります。逆に自身のSubContainerを見に行くなど下っていくことはありません。
上を辿っていくのが基本的なルールとして、どのくらいまで辿っていくのかという部分はAttributeやOptionで制御できたりします。
InjectLocal
InjectLocal
をつけると親を辿りません。
public class Bar { // 同じContainerからしか探さない [InjectLocal] Foo _foo; public void DoBar() => Debug.Log(_foo.ContainerName); } public class FooInstaller : MonoInstaller { public override void InstallBindings() { // 現在のContainerとProjectContextの両方でFooを用意しておく ProjectContext.Instance.Container.Bind<Foo>().AsCached().WithArguments("ProjectContext"); // BarがいるContainerにはFooをBindしない //Container.Bind<Foo>().AsCached().WithArguments("Scene"); var subContainer = Container.CreateSubContainer(); subContainer.Bind<Foo>().AsCached().WithArguments("Sub"); // 現在のContainerにBarをBindする Container.Bind<Bar>(); Container.Resolve<Bar>().DoBar(); } }
=> ZenjectException: Unable to resolve 'Foo' while building object with type 'Bar'. Object graph: Bar
自身の所属するContainerにはないのでそこでエラーが出てしまいます。
こんな感じで実は細かい設定ができたりします。Attributeの書き換えでもいいですし、Inject(Source = InjectSources.Local)
といったInjectSources
列挙体で渡すやりかたでも同じことができます。列挙体は以下の四つです。
- Any
- Local
- Parent
- AnyParent
InjectSources.Any
自身から探して、見つからなければ親を延々と辿っていきます。
通常のInject
がAnyに設定されているのでいつものやつです。
InjectSources.Local
上記の通り、自身のContainerだけを探して見つからなければエラーを吐きます。
InjectLocal
の設定と同じです。
InjectSources.Parent
自身の直接の親のContainerを探しに行きます。自身と同じContainerに存在していても参照されません。親が持っていない場合エラーを吐きます。
InjectSources.AnyParent
自身の親を延々と辿っていきます。自身のContainerは参照しません。
参考
InjectSourcesのテストコード見るとなるほど感あります。
ちなみにZenject内部ではKernel内部の各種Managerが混ざらないようにInjectLocalで分けてるっぽい。
サブコンテナ使ったりProjectContextでBindしてるインスタンスと同一のものが必要になったりとかしたら明示的に分けていいかもしれませんね。スコープが狭まることはいいこと。
2018年ふりかえり
とはいえ、先月ふりかえったばっかりなのであまり書くことはないかもしれない。
若干被ることも多いけど年末の行事なので書くことにした。なのでアニメとかゲームの話書こう。
仕事
一番はやはり転職が大きかった気がする
WEB&ネイティブアプリからゲーム作るエンジニアになりました。果たして生きていけるのかと思ったけど、割といい感じにやってます。趣味での開発は案外有用だったので、RxとかZenjectとか導入したり、逆に業務やコンシューマでのアレコレを教えてもらったりしてます。まだ頭は追いついてないです。
また、初めてチームビルディング周りを勉強したりし始めました。前職でやってた人たちを思い出して見よう見まねから始まり、本を読んだり勉強会で話を聞いたりするなど。
あと、社内のLT大会のにぎやかしとして小ネタを喋ったりしていた。
イベント
社外でのLT、登壇は三つだけだった
- Zenjectとテスト - Gotanda.unity #5
- Unityプロダクトにテストを導入していくまで - Unityテスト完全に理解した
- Zenject Optionalアレコレ -【年末だよ!】UnityおとなのLT大会
あと、夏コミと技術書店でUnity関連の本を少し書いたりしていた。
上のイベントふりかえると全部Zenjectの話してる気がする。
イベントに関しては自分が登壇したのはそんなにないけど、会場の誘致をしたりなどしていた。
- Unity テスト完全に理解した
- Gotanda.unity #7
- Unity Zenject完全に理解した
- 大八耐2018 in 東京
- Unity ECS完全に理解した
- Unity Network 完全に理解した
会場提供おじさん業を繰り返し行った結果、社内の人も気軽に勉強会見に来るようになったのでよい。
ゲーム
クリアしたものだけ列挙
スパロボXは久々の異世界系だったけど普通に楽しめた。ヴィルキスとビルバインの二大切り込み隊長にはお世話になりました。個人的にはストーリーは暗めのVが好きだけど、ワタルのおかげ基本明るいのでそれはそれでよかった。Tは予約します。 世界樹はとにかくボリュームが多くて三か月くらいは時間かかったけど満足。基本脳筋戦術が好きなのでプリンスなどがバフかけまくってインペリアルの全力ドライブが最高に気持ちよかった。Vからだけど裏ボスも割と好きなパーティで戦えるようになったので楽しい。次回作はどうなるか。
あと、友人から勧められたアルトネリコ2が面白かった。システムが斬新で覚えるまでが苦労したけどリズムとRPGがいい感じに混ざっていてよかった。アドベンチャーパートは濃厚というかこれがやりたかったからRPG要素入れたんだなという勢いだった。クローシェルートクリアしたので二週目やろうと思ったけど引継ぎがないのが残念。
メタルマックスゼノは5週クリアしました。難易度ゴッドの最強の賞金首を倒したあたりで一旦ストップ。
アニメ・映画
一か月の無職期間にNetflix契約したのもあってだいぶ観ることが増えた。覚えてる奴だけ抜粋。
- キングコング髑髏島の巨人
- インターステラー
- キャビン
- マッキー
- バーフバリ~王の凱旋~
- 殿、利息でござる
- ポプテピピック
- キルラキル
- 王立宇宙軍 オネアミスの翼
- トップをねらえ!1&2合体版
- マジンカイザーSKL
- バキ
- 逆シャア
- アニゴジ三部作
- 未確認で進行形
- Devilman Crybaby
- NEW GAME!2期
- クレヨンしんちゃん逆襲のロボとーちゃん
- クレヨンしんちゃんモーレツオトナ帝国
- クレヨンしんちゃんアッパレ戦国大合戦
- クレヨンしんちゃん嵐を呼ぶジャングル
- ゾンビランド・サガ
- SSSS.GRIDMAN
劇場で見て一番良かったのはバーフバリでした。2時間越えの長丁場だけど最後までバリバリバリバーフバリできるやつだった。アニゴジは賛否両論凄いけど個人的には全編ふりかえると悪くないなあと思った。怪獣プロレスはないけどそれはそれで。実はリアルタイムで見たのはゾンビランドサガとGRIDMANの二つだけ。これらは途中で評判聞いて観始めたら面白くて、気づいたら毎週楽しみにしていた。改めて見直したら、設定が奇抜でも結局王道な展開が好きなのかもしれない。GRIDMANもゾンビランドサガも最後は読めてるけど楽しめるやつだったし。
でも今期もやはりガルパンが最高でしたね。
大洗
5月と11月に二回行ってきた。最高だった。
まとめ
今年の頭に絵とか音楽とかやってみるぞ!と意気込んでみたが、蓋を開けてみればDIだの設計だのテストだの去年よりエンジニアよりのことに頭を使う一年でした。ただ楽しかったので来年はさらに特化していいかもしれない。ブログとかLTとかは引き続きやって行くけど、縁と機会があるなら他所にそういうZenjectアドバイスするおじさんとしてお邪魔しに行くのもありかもしれませんね。楽しそう。
今年もお疲れさまでした。
【年末だよ】Unity お・と・なのLT大会 2018」でLTしてきた
ポロリはしませんでした。
毎年年末に酒を飲みながらLTするイベントが行われているということでプレモル3杯目を飲みながらLTしてきた。
発表内容
発表資料はこちらでございます。
ZenjectにはOptionalExtrasと呼ばれる便利機能がいっぱいあるので触ってみようぜ!という内容。
本文中でも書いてるけど、テストの件は以前にも発表しているのでこちらをご査収ください。
Signalsはこっち
個人的にはSignalは可能性が広がるので積極的に使ってほしい。MemoryPoolはまだ必要な場面に遭遇したことが無いのであまり言及できないのだった。所有アイテム一覧みたいな相当数のViewが必要な場合に活躍するかも?くらいのお気持ち。ReflectionBakingに関してはみなさんの検証をお待ちしております。
ZenjectのOptionは本当に悲しいくらい日本語記事無いので増やしていきたい所存。自分で書いていくでもいいけど寂しいのでこうやって普及させていく。 ちなみに僕の発表は後半で、すでに酒が入ってることもあったからか頭回ってるか怪しいところがいくつかありましたがご了承ください。Unity Material Learningに載るらしいので動画として酔っぱらった光景が保存されています。ご了承ください。
おまけ
LT会後の居酒屋での二次会でやっていきが高まった結果の所信表明です。
来年はMicrosoft MVP目指すかーという気持ちになった
— いも@efb~相手は死ぬ~ (@adarapata) December 15, 2018
Zenject Signals
本記事は Unity Advent Calendar 2018 の 13日目 の記事です。
普段Zenjectを趣味で使っているのですが、Zenjectは基本的なDIContainer機能の他に、ちょっとDIから離れた便利機能をいくつか提供しています。MemoryPool
TestFrameWork
Reflection Baking
Signal
などです。今回はこれらの中で Signal
の使い方について解説していきます。
Pub/Subの話
Signalsの話をする前に、Signalsの考え方であるPub/Subについて軽く解説していきます。
メッセージング
ゲームはイベント駆動が多いので、状態の変更を通知したくなるときが結構あります。ゲーム開始したらキャラクタを動かし始めたいとか、時間切れを知らせてプレイヤーの操作を受け付けなくしたりとか。そんなとき、通知元と通知先のクラスが必ずしも強い関係性を持っているとは限らなかったりします。
一時停止を実装しよう
例えば、マリオのようなアクションゲームにおいて一時停止を実装したくなったときのことを考えてみましょう。一時停止の概要は次の通りです。
- 任意のボタンを押すと発動する
- 一時停止状態はプレイヤーの動きが止まる。操作も受け付けない
- 敵キャラも止まる
- 残り時間のカウントも止まる
- 一時停止が解除されると上記三つは再度動き始める
- 画面描画はそのまま。キャラが見えなくなったりはしない
アクションゲームを作るうえで割と必須な機能です。ヒエラルキーのオブジェクトたちを一気にSetActiveして解決したくなりますが、それだと描画も消えてしまうのでここはちゃんとコードで実装することにしましょう。
一時停止を実装するにあたって、なにはともあれ「今が停止しているのか?」という情報を監視して取ってくる必要があります。今回は雑にゲーム中の入力を管理する GameInputManager
と停止対象の Player
を作ってみましょう。
public class GameInputManager { public event Action Pause = delegate { }; public event Action Resume = delegate { }; private bool _isPause = false; void Update() { if (Input.GetKeyDown(KeyCode.A)) { if (_isPause) { Resume(); } else { Pause(); } _isPause = !_isPause; } } } public class Player : MonoBehaviour { [Inject] private GameInputManager _inputManager; private bool _isPause; void Start() { _inputManager.Pause += () => _isPause = true; _inputManager.Resume += () => _isPause = false; } void Update() { if (!_isPause) { // いつもの処理 } } }
GameInputManager
は内部で任意の条件(今回はAが押されたかどうか)で停止と再開を切り替えます。外部のオブジェクトはイベントにコールバックを登録することでそれらを検知し、自身の状態を更新します。ひとまずはこれで一時停止達成です。実装は割愛しますが敵キャラEnemy
や時間管理クラスGameTimer
も同様にGameInputManager
のイベントを購読すれば解決するでしょう。この状態を図で示すと以下の通りです。
GameInputManager
のモテ期ですが、まだいける。
増えていくイベント
ところがこの一時停止するという概念、ボタンを押して停止したいという場合以外にも使いたくなってきたのでした。
- ゲーム開始時なども一定時間止めたい
- デバッグとして無条件にいつでも止めれるようにしたい
現在の一時停止は「ユーザが任意のタイミングで実行できる機能」ですが、今回追加される二つはゲーム側が自動で実行する機能だったり、開発者のみ使える機能だったりとタイミングもコンテキストも大きく異なります。これらを実装するとしたらどうすべきでしょうか。
一番手っ取り早いのは上記二つの実装をGameInputManager
に突っ込むことなのですが、これはそもそもユーザ入力でもないしクラスの責務が多くなってしまい最強のManagerクラスが爆誕してしまうでしょう。それはつらい。
素直に各クラスを作るのが一つの手でしょう。試しにゲーム開始時の一時停止を入れてみましょう。何秒止めるとかは割と演出の都合にもなりそうなのでGameDirector
とかそのあたりに書かれるかもしれません。
public class Player : MonoBehaviour { [Inject] private GameInputManager _inputManager; // ゲームの演出管理してるやつ [Inject] private GameDirector _director; private bool _isPause; void Start() { _inputManager.Pause += () => _isPause = true; _inputManager.Resume += () => _isPause = false; // 演出の一時停止も購読だ! _director.Pause += () => _isPause = true; _director.Resume += () => _isPause = false; } void Update() { if (!_isPause) { // いつもの処理 } } }
3行追加で済んだのでまだ傷は浅そうですね。EnemyもGameTimerも同様に3行追加すれば実装できるでしょう。この場合の依存の状態を図示すると次のような感じです。
ちょっと雲行きが怪しくなってきました。ここからデバッグ用のクラスも作るとなるともう一つ依存が増えるので組み合わせ爆発がえらいことになりそうですね。一時停止可能なクラスが増えるたびに手を加えることになってしまいます。
Pub/Subメッセージングモデル
今回の要件において、Player
はGameInputManager
に依存すべきなのでしょうか。(プレイヤーなら他のキー入力もあるから必要だろ!というのは一旦置いといてください・・)
実際欲しいのは一時停止が起きたというイベントだけで、ユーザが一時停止ボタンを押したから止まったというGameInputManager
のコンテキストは特に求めていません。デバッグ時の停止も演出での停止も同様で、それらの何故発生したかという文脈は今回必要としていません。今回のような不特定多数が発行するイベントを不特定多数が購読するPub/Subの場合、直接依存関係を持つと拡張しづらいという問題が発生します。
なので、このようなPub/Subを実現するときは中央に仲介人(Broker)を構えることになります。
イベント発行側はBrokerにメッセージを送る(Publish)して、購読側はBrokerからイベントを受け取る(Subscribe)仕組みです。この構成は購読側が発行側をまったく意識しなくてよくなるのでスケーラブルな点がメリットです。今後どれだけイベントの発行側が増えても購読側に影響はありません。
Unityにおいては、UniRxのMessageBrokerが有名です。
サッと導入できて非常に便利なのでおススメです。
Zenject Signal
前置きが長くなりましたが、ZenjectではSignal
という機能を提供しています。
Zenject/Signals.md at master · svermeulen/Zenject · GitHub
これはZenjectのDIを利用している場合に使えるPub/Sub機構です。
とりあえず書き換えてみる
最短ルートでサッと書いてみましょう。 Zenjectを利用するので、登場人物は全てZenject BindingなどでBindされているものとします。
購読側
// イベントの種類をクラスにしてラベリング public class PauseSignal { } public class ResumeSignal { } public class Player : MonoBehaviour { private bool _isPause; // Signalを受け取るメソッド public void OnPause(PauseSignal signal) => _isPause = true; public void OnResume(ResumeSignal signal) => _isPause = false; void Update() { if (!_isPause) { // いつもの処理 } } }
まず、今回からPlayerはイベントがどこから来るのかわからなくなります。なのでイベント自体をクラスにして明示的にしてあげます。今回は停止イベントをPauseSignal
、再開イベントをResumeSignal
としました。
PlayerはManagerなどの依存がなくなる代わりに、イベントの受け皿としてOnPause
OnResume
メソッドを定義してあげます。
発行側
public class GameInputManager { // Zenjectが用意したBroker [Inject] private SignalBus _signalBus; private bool _isPause = false; void Update() { if (Input.GetKeyDown(KeyCode.A)) { if (_isPause) { // PauseEventを発行する _signalBus.Fire<PauseSignal>(); } else { _signalBus.Fire<ResumeSignal>(); } _isPause = !_isPause; } } }
変更点としては、eventがなくなり SignalBus
というものが現れます。これはZenjectが提供するBrokerで、このインスタンスを通してイベントを発行したり購読ができるようになります。コールバックを呼んでいた部分はFire<T>
で任意のイベントをSignalBusに発行する処理に変わります。
Installer
public class SignalInstaller : MonoInstaller { public override void InstallBindings() { // SignalBusを利用する SignalBusInstaller.Install(Container); // Signalを定義する Container.DeclareSignal<PauseSignal>(); Container.DeclareSignal<ResumeSignal>(); // Signalを受け取った際の処理 Container.BindSignal<PauseSignal>().ToMethod<Player>(p => p.OnPause).FromResolve(); Container.BindSignal<ResumeSignal>().ToMethod<Player>(p => p.OnResume).FromResolve(); } }
基本的には以下の流れです。
- Signalを利用するために
SignalBusInstaller
をインストールする - 利用したいSignalを
DeclareSignal<T>
で定義する BindSignal<T>().ToMethod<T2>
でSignalを受け取った場合のT2
の処理を定義
1,2は基本的に毎回同じです。3は場合によって異なりますが、だいたいはメソッドを呼んでそのままSignalを渡すことになるでしょう。また、今回はPlayerがBind済みなので FromResolve()
でBind済みのPlayerに流すようにしています。これにより、発行側がSignalBusにFireしてくれればPlayerは全て受け取ることができるようになりました。
このままEnemyもGameTimerもBindSignal.ToMethodすればいいのですが、それだと結局何回も定義することになり面倒です。せっかくZenjectを使っているのでインタフェースでまとめてあげるとだいぶ楽ができます。
public interface IPauseable { void OnPause(PauseSignal signal); void OnResume(ResumeSignal signal); }
BindSignalは以下に書き換えます。
Container.BindSignal<PauseSignal>().ToMethod<IPauseable>(p => p.OnPause).FromResolveAll(); Container.BindSignal<ResumeSignal>().ToMethod<IPauseable>(p => p.OnResume).FromResolveAll();
FromResolveAll
は任意の型のバインドオブジェクト全てを取ってくるので、PlayerやEnemyにIPauseable
を実装してあげれば自動的にSignalを受け取れるようになります。
動的な購読
先ほどの例は最初からバインドされている状態なので、動的に生成されるインスタンスには適用できません。その場合はSignalBusを受け取って手動で購読する必要があります。
public class DynamicSpawnEnemy : MonoBehaviour { private SignalBus _signalBus; [Inject] void Initialize(SignalBus signalBus) { _signalBus = signalBus; _signalBus.Subscribe<PauseSignal>(OnPause); } void OnDestroy() { // 明示的に購読解除しないと残りっぱなしになる _signalBus.UnSubscribe<PauseSignal>(OnPause); } private void OnPause(PauseSignal signal) => print("Pause!!"); }
SignalBus.Subscribe
、SignalBus.UnSubscribe
で購読及び解除ができます。手動で破棄が必要なのでうっかり忘れないようにしましょう。
また、ZenjectはUniRxとの連携ができます。その場合、IObservable
への変換が可能です。
_signalBus.GetStream<PauseSignal>() // IObservable<PauseSignal>に変換
.Subscribe(OnPause)
.AddTo(gameObject);
UniRx使ってる場合はこっちの方が管理しやすいかもしれません。
Zenject Signalsの特徴
SignalsはDIしているという条件はありますが、割と細かく面白い機能がついてます。
Subscribeを必須にできる
シグナルを発行したときに購読者の有無で挙動を変えられます。
// イベント発行時に誰もSubscribeしていないなら例外を吐く Container.DeclareSignal<PauseSignal>().RequireSubscriber() // 誰もSubscribeしてなくても動作する(デフォルト設定) Container.DeclareSignal<PauseSignal>().OptionalSubscriber() // 誰もSubscribeしてない場合Warningを出す Container.DeclareSignal<PauseSignal>().OptionalSubscriberWithWarning()
必ずSubscriberがいるなら積極的にRequireSubscriber
にしておくと早期にエラー発見できて便利
定義してないSignalを発行したときの挙動
原則として、DeclareSignal
で定義してないシグナルを発行することはできず、Fire
を呼んだときに例外を吐きます。もし状況に応じて定義されるかわからないシグナルが存在する場合、例外を無視するTryFire
を代わりに使うと良いでしょう。
// 例外を吐く _signalBus.Fire<NoDeclareSignal>(); // 定義されていない場合Warningを吐いて無視する _signalBus.TryFire<NoDeclareSignal>();
Signalにpriorityを設定できる
通常、同一フレーム内に複数のSignalが発行された場合、それらは順番に処理されていきます。
Container.DeclareSignal<FooSignal>(); Container.DeclareSignal<BarSignal>(); // Signalが来たらLogに吐く Container.BindSignal<FooSignal>().ToMethod(_ => Debug.Log("foo")); Container.BindSignal<BarSignal>().ToMethod(_ => Debug.Log("bar")); var bus = Container.Resolve<SignalBus>(); // 雑に交互に発行する bus.Fire<FooSignal>(); bus.Fire<BarSignal>(); bus.Fire<FooSignal>(); bus.Fire<BarSignal>();
実行結果は以下です。
foo bar foo bar
これらは同期的に処理されるため、Fireされた順番にログが吐かれています。Zenject Signalsはこの処理を非同期に、且つSignalに優先順位を付けてさばくことができます。
// FooSignalの優先度は0 Container.DeclareSignal<FooSignal>().RunAsync().WithTickPriority(0); // BarSignalの優先度は1(FooSignalより優先度が低い) Container.DeclareSignal<BarSignal>().RunAsync().WithTickPriority(1); // Signalが来たらLogに吐く Container.BindSignal<FooSignal>().ToMethod(_ => Debug.Log("foo")); Container.BindSignal<BarSignal>().ToMethod(_ => Debug.Log("bar")); var bus = Container.Resolve<SignalBus>(); // 雑に交互に発行する bus.Fire<FooSignal>(); bus.Fire<BarSignal>(); bus.Fire<FooSignal>(); bus.Fire<BarSignal>();
結果は以下の通りです。
foo foo bar bar
WithTickPriorityで設定したとおり先にFooSignalが処理されています。これは個人的には面白い機能だと思っていて、必ず実行順が固定されなければならない場合に効果を発揮します。例えばキャラが死んだイベントと、それとは別でキャラにアクセスするようなイベントが同時に発生したとき、実行順番によってはすでにDestroyしたインスタンスにアクセスしてエラーでちゃうなんてことがたまーーーにあったりして、0.1msずらしDestroyしたりとかyield return nullしたりRxでNextFrame.Subscribeしたりしてお茶を濁すようなことをしなくて済みます。
Signalsのメリット
個人的にはスコープが狭まることが大きいかなと考えてます。
Pub/Subのメリットは先に述べたとおりですが、一方で疎結合故に「どこからイベントが発行されるのか把握しにくい」という問題もあります。最初は問題ないけど、プロダクトの規模が大きくなるにつれあらゆる個所からイベントが発生する可能性を考慮することになります。とある1画面を改修して問題ないと思ったら、全く想定していないバックエンドで動いていた何かから突然イベントが飛んできて謎・・・みたいなことになりがちです。且つ、便利故に「データの渡し方に困ったらとりあえずBrokerに送ってSubscribeでいいか」みたいな判断を取ってしまう危険性もあります。実質グローバルな何かに依存しているみたいな状況です。 Zenject Signalsの良いところは影響範囲が同一のDIContainerだけだという点です。Containerに登録されているSignalしか発行できず、Containerはシーン単位、またはサブコンテナなど細かい粒度で構築できるため、遠いところからSignalを発行するのは難易度が高いです。この制限のおかげで安心して使っていけるのかなと思います。 もちろん、ProjectContextにSignalを定義してしまえば超便利になる代わりに完全にグローバルになって問題が再燃してしまうので使いどころはよく見極めましょう。
最後に
Zenject Signalは色々条件はありますが割と面白い機能です。Zenjectを使っていて通知周りに困っていたらぜひ使ってみてください。 それ以外にもZenjectのオプションたちは便利なものが多いので興味があれば調べてみてください。そしてブログを書いてください。僕が喜びます。
WEB漫画「魔法使い」が完結するまで死ねない
このエントリは2018年オススメのマンガ Advent Calendar 2018の参加エントリです。前職の同僚である@june29さんのエントリがTLに流れてきたので速・登録しました。
好きなアニメは今期もガルパンが完全勝利しているんですが漫画は割と色々読んでいるので甲乙つけがたいのが実際のところです。ということで、今回は僕が長年読み続けているWEB漫画「魔法使い~Wizard~」を紹介します。結構なネタバレにはなりますが、問題ない方だけ先に進んでください。
今すぐ読むぞ!という方は下記のリンクからどうぞよろしくお願いします。
知ったきっかけ
僕がこの漫画と出会ったのは実は8年前で、学生時代WEB漫画漁りがマイブームだったときに偶然目についたのが始まりでした。その時に全部読み終わって感動していたものの、ブックマークもしておらずタイトルも覚えておらず、「何か面白いWEB漫画あった」というおぼろげな記憶しか残っていない状態でした。
しかし今年三月に何の拍子か再開してしまったのです。
8年前にハマってたWEB漫画と再会できて感動している / 他2コメント https://t.co/1qZkeeoal2 “魔法使い | WEB漫画 週刊マナバナイ” https://t.co/CeNcr3ZO0d
— いも@efb~相手は死ぬ~ (@adarapata) March 17, 2018
どういう経緯で見つけたのかは覚えてないのですが、僕は8年分の更新を読み終えて再びファンとなったのでした。
概要
「魔法使い~Wizard~」はマナバナイ先生が2006年からWEBで連載しているバトル漫画です。2018年12月時点で全38話、現在も連載中です。
常人より遥かに高い身体能力を持つ人間「魔法使い(ウィザード)」が現れ始めた荒廃した近未来、村同士の資源の取り合いは魔法使いによる代理戦争で行われていた。名無しの村の魔法使い黒子は自分の村を守るために戦う・・というのが基本的なストーリーです。現在三章まで続いており、村を襲う様々な魔法使いと戦う第一章、正式な魔法使いとなるため都会へ向かう第二章、日本にクーデターを起こしたシャングリラ公国と戦う第三章(現在進行中)となっています。
好きなところ
魔法使いという設定
名前こそ魔法使いですが、火とか水とか好きに出せるわけとかではなく単純に超人のような動きをする人たちなので、バトルは基本的に殴り合いです。その中でもスピード特化、パワー特化などキャラクタごとに個性を活かした戦い方をしてきます。魔法使い同士の戦いは生死を賭けたデッドオアアライブなものではなく、魔法使い法によってルールが定められた異種格闘技に近いものなので技の応酬が激しく、空手、プロレス、八極拳など王道のバトルものとして読んでいて熱いです。それぞれキャラが立ってます。
個人的にはそれに加えて「魔法使いという超人の人権を守るための法律」である魔法使い法の設定が好きです。畏怖の対象とされたり、軍事利用されたりと人間を超えた故に人間に扱われないという問題への解決策としてなるほどと納得感が高かったです。これはこれで問題があるのですが、設定としてスッと入ってきました。
シャングリラ公国の魔法使いたち
三章で現れるシャングリラ公国の魔法使いたちは全体的にキャラが立ってて好きです。めっちゃ好きです。この手の敵陣営って基本主人公側より強いようなやつらが多くて、そんな敵にどう工夫して立ち向かうかみたいなところが物語の肝だと思うんですけど、魔法使いたちは各能力が数値化されていて、数値上で見るとシャングリラ勢は主人公側より弱いということも結構あります。実際クーデターなのでそんな大きな戦力を持つわけでもなく、本編でサラが言っているように魔法使いの大半は日本国が管理しているため質や量で圧倒的な差なんですよね。だけど強い。主人公勢はかなりの苦戦を強いられるバトルが多いです。敢えて国のトップ(普通の人間)を狙うことで前線の戦力を削いだり、自分の土俵に誘い込んで能力差を埋めたりなど敵の方がかなり賢く動き回ってます。なんなら愛の力で覚醒したりとどっちが主人公かわからないレベルです。シャングリラ公国のキャラクタは何かしらの信念を持っていたりと悪役ではなかったりするのもポイントかもしれません。
好きなシーン
全編好きだけど二大好きなバトル書いて終わりにします。
オール アウト アタック (20~22話)
連合軍VSシャングリラ公国の軍隊の正面からのバトルです。シャングリラ公国の軍隊はノーマルだけですが火力と練度で幾度も魔法使いを退けた最強のノーマル部隊。連合軍の魔法使い達は敵味方、たった一人の死傷者も出さずに制圧していきます。
魔法使いたちの圧倒的な強さを伝える回です。ここで魔法使いとノーマルの差をこれでもかと見せつけてくれます。テラフォのM.O回しかり村田版ワンパンマンの怪人協会アジト突入回しかり、登場人物の多いバトルものに必ず差し込まれる王道ですが、最高に気持ちいいから王道なんです。特に最後のアインは本編で一番かっこいいんじゃないでしょうか。
ファイヤーウォール (30~34話)
シャングリラ公国の本能寺真理&ユーリ・カーヴェ VS マリー&バッドのプロレスタッグマッチ回です。真理&ユーリは元々タッグ「ファイヤーウォール」で村を守っていた夫婦で数十年ぶりの再結成。基礎能力ではマリー&バッドに少し劣るがそれを補う圧倒的な技で二人を苦しめる・・といった流れ。
このバトルは本当に熱いから何回も見てほしい。この回の主役は間違いなくファイヤーウォールの二人だと思う。
最後に
WEB漫画は気長に自由にやれるのがいいところですが、それもモチベーションが続く限りの話です。僕らファンのできることは楽しみにしていることをこうやってアピールするのみです。
長生きするので完結まで描き切ってくれ!!!
上期ふりかえり
なんでこの時期に上期ふりかえりかというと、会社の上期が9月までなので。10月は面談とか自己評価とか諸々を行ったりそれの結果が出たり、人の出入りがあったりと転換としてちょうどよく、せっかくなのでやったことをふりかえることにした。
開発環境の整備
整備って言うと広いけど大きく書くと次の三つ。
長く生きられるように基盤を整えて行きましょうということでアーキテクチャを導入しテスト書ける状態にした。その際にZenjectを導入してstaticなオブジェクトを減らしてテストを書きやすくしたりなど。まだ半年くらいしか経ってないので効果絶大だったかはわからないけど、少なくともUnityやライブラリのバージョンアップ時とか新機能追加時にとりあえずテスト通ってるから大丈夫やろくらいの安心感は与えてくれたのでプラスなんじゃないかとは思う。
その辺りのやったことは書ける範囲でブログにしたり登壇したり本に書いたりなどしたので興味ある方はご一読ください。
前職でこういう活動していたわけではなく、そんなに設計に精通しているというほどでもないので大変だったけど、チームの人もすぐに馴染んでいったので凄くやり易かった。というかチームの人たちテストとかDIとかアーキテクチャもほとんどやったことないと言っていたのに、あまりサポートすることなく全員普通に書いてたので「ヤバい人たちだ・・」ってなった。 特に設計周りは割と実装しながらこのレイヤがいるかという議論もできたので納得しながら進められた気がする。例を挙げるとTranslater層を設けるかというのは結構議論した。結局Zenject Factoryが担ってるよねということでやめた。
ちなみに設計周りは最近だとこれを読んでいる。とあるアーキテクチャに言及というよりはもっと根幹の五大原則とかに触れていて、且つ読みやすいので便利。
開発チームのサポート
ここで言うサポートは、開発をしやすくするための支援とかそっちに近い。止まっていたふりかえりを再開したりとか、カンバン復活したりとかプランニングポーカーやったりとか結果的にスクラムのサポートだった気がする。一応チームリーダーだったのだけど、個人的な裏目標として「人のタスクを管理しない」を立てていた。エンジニア全体の進捗どうですか的なのをリーダーに振られるとそれを把握するための作業が必要だし僕がボトルネックになるしなによりしんどさが高まるので自分に聞かなくてもわかる状態にしたかった。なのでカンバン立ててやってること一覧を見れるようにした。あと、タスクも極力これお願いという渡し方を避けて自分で選んでもらうなどしてた。結果的に決まらずこちらで決めるはあった気がしないでもない。
よかったこととしては、TRYの実行率が上がっていったというのがある。回数を重ねるごとに抽象的なTRYから具体的に実行できるレベルまでの落とし込みができるようになっていたと思う。
これまた今までやったことない活動だったけど、前職でやってた人たちの活動を思い出しながらやってるといい感じになった気がする。あと、エンジニアリング組織論への招待はやはり名著なのでチームで仕事をする全ての人に読んでほしい。
課題としては、これらの活動をエンジニア以外にあまり伝搬させられなかったので下期に頑張りたいなというお気持ち。
登壇とかアウトプットなど
今期は結構文章を書くことが多かった気がする。 上記のXFLAG Tech Noteや、UNIBOOK10など。
上期前半は登壇用のスライドを書いたり本を書いたりなどをしていて、上期後半は久々にハッカソンでクソゲーを作るなどしていた。
平沢進と聖飢魔Ⅱが対戦したり、「卍」をバトルさせたりと、ハッカソンは作ったものに責任を持たなくてよいので非常に楽しい。
あと、自分のアウトプットだけでなく会社の人たちのアウトプットを増やすという活動にもいくらかお手伝いしたりしていた。勉強会を開いたり誘致したりしてイベントと会社の人たちの距離を縮めたり、何人か外部の勉強会で発表してもらったりなど。自分自身も社内LTなどは頻繁に参加していたりする。
Unityからジョジョまで質も内容もバラバラだけど、実は半分近く喋ってることに気づいた。
課題
弊社のエンジニアは基本的にマッシブな人々が多く、それこそコンシューマ時代からガシガシやってた人もいたりして非常に強い。低レイヤーからUnityの最新までイケる感じの人が多く、完全に理解したとは口が裂けても言えなかったりする。そんなわけで開発の基礎体力がだいぶ違うなと感じた。こういう風に実装したら行けそうという発想が早く、実装も早い。経験もあるけどそれより多分知識量だなと思う。
ということで開発リーダーから簡単な数学本を借りて今読んでる。
設計する力もチームを進める力も実装する力も別軸なんだけど、今の自分は実装する力で度々苦戦しているので下期はこの辺重点的にやろうと思った。
ゲームも作り始めているんだけど、割と長いスパンになりそうなのでエターナらないように気を付けたいところ。
最後に
スマブラが楽しみです。
全力を出した pic.twitter.com/5C0VqMx8Sh
— いも@efb~相手は死ぬ~ (@adarapata) November 3, 2018