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
大八耐6をやってきました
もっと書きたいのだけど疲労と日本語力が足りないのでサッと書く。
八耐とは?
福岡で月一で行われているなんでもありハッカソンイベントです。
http://daihachitai.npo-spice.com/about/
八時間でゲーム、CG、サウンド等好きなものを勝手に作ります。八時間ではありますが0からスタートである必要はなくやりかけのもの持ってきたりとかも全然オッケーのゆる~いハッカソンです。次の動画が空気感わかりやすいです。
本当になんでもありで、過去にいた例としてはデアゴスティーニの週刊3Dプリンタを全号持ってきてその場で開封して作ったりとか、8時間でモバゲーとかmixiとかメッセージ送りまくって彼女を作るとかいました。
元々は自分が学生の時に教授が研究室のメンバーに「8時間でクソゲー作ろうぜ!」と発起したのがきっかけで、研究室内での開催を経て外部公開して徐々に人数を増やしていき、福岡では7年くらいほぼ月一開催しています。
ハッカソンですがジャンルを問わないので様々な職種の人たちが集まるのが結構面白いです。
大八耐とは?
年に一回行われる、二日かけてやる八耐です。
http://daihachitai.npo-spice.com
やることは基本同じですが、二日かけてやるのと、審査員などを呼んだりオーディエンス賞を決めたりなどちょっと大きくなってるのが特徴です。でもゆるいのは変わりません。福岡では6回目、東京では2回目の開催となります。
今回は残念ながら福岡が台風で中止になったため東京のみの開催となりました。出来上がったものをいくつかピックアップします。
二郎の写真しか上げられない二郎スタをバックエンドfirebaseで作ったが、検証で通信しすぎて無料分を超過して本番デモできなくなったのじわじくる #hachitai pic.twitter.com/LqBsRrnzy1
— いも@efb~相手は死ぬ~ (@adarapata) 2018年10月7日
ラーメン二郎の写真だけを上げられるインスタ「二郎スタ」です。二郎以外上げるとアカウント止められるようにするらしい。 バックエンドがFirebaseなんだけど、検証で通信しすぎて本番時に無料分超過してデモできなかった悲しみを背負ったアプリです。
SNS連動型のアナログTCGの企画! #hachitai pic.twitter.com/sIKu6KB5Bp
— いも@efb~相手は死ぬ~ (@adarapata) 2018年10月7日
SNS連動型TCGの企画案を作った人もいました。絵を描いてる視点から欲しくなるカードゲームです実際の資料はもっとあります。
複数画面を同時に見れるライブチャットアプリ、パチンコ店の音がする #hachitai pic.twitter.com/wK2mXr8ngd
— いも@efb~相手は死ぬ~ (@adarapata) 2018年10月7日
「無限に浮気できるライブチャット」という危険な名前のアプリですが、要はtiktokのようなライブ配信は一度に一つしか見れないので同時にいっぱい見ていっぱいいいねしたいという欲求を満たすアプリです。 ただ、複数の動画が同時再生されたそれは完全にパチンコ店の音でした。
とあるキャラの髪と衣類作ってるようです #hachitai pic.twitter.com/uENcT6mucb
— いも@efb~相手は死ぬ~ (@adarapata) 2018年10月6日
こちらはアナログでコス衣装を作っていました。会場に自前のミシンを持ってくる徹底っぷりです。推しの服を作っているようで、感想を聞いたら「部屋に無造作に置いていると、だらしない推しと同居している感じがして非常に良い」とのことでした。
自分がつくったもの
僕はUnityのAudioMixerを触ったことがなかったので勉強していました。その過程で「聖飢魔Ⅱと平沢進ってどっちが強いんだろう?」と思い立ち、音楽を同時に流してバトルさせるゲームを作りました。
大八耐では平沢進と聖飢魔Ⅱのどっちが強いか知りたかったので音楽で戦わせるピンポンを作りました。玉が入るとピッチやテンポが変わります。公開しません #hachitai pic.twitter.com/SR2BWhz1Ep
— いも@efb~相手は死ぬ~ (@adarapata) 2018年10月7日
基本はピンポンで球が自陣に入ると減点ですが、点数が下がるのではなくピッチやテンポが変動します。原曲からずれまくると評価が下がり、ある程度差が付くと負けになります。 初めて音声の合成をしましたが、何の知識もなしに単純に混ぜると耳が不快になるということがわかりました。
しかし、この発表により会場の平沢進ファンを炙り出すことに成功しました。公開予定はありません。
感想
正直なところ、もともと勉強会やハッカソンがそこらじゅうで開催されている東京では八耐はいらないのではないかという気持ちをある程度持ってたんですが、こんなごった煮のハッカソンで且つ、ゆる~く自由にやれるイベントは東京でもそうあるものではないなあと再度認識しました。最新技術を使うでもなく、レギュレーションがあるわけでもない唯々好きなもの作るだけのイベントです。であれば別に家で独りでやってもいいんじゃない?というのはその通りだけど、八耐は交流にも重きを置いています。自分と違う職種のクリエイターと話すのは結構面白いです。独りでやってもいいけど、八耐でやった方が面白いなと思わせられるようにしていきたいですね。 東京はまだまだ参加者が少ないので、今後やってること報告して参加者増やせたら面白いなあと思ったのでした。 通常八耐も不定期でやります。いつやるかは未定ですが興味ある方は是非お声がけくださいな。
「大八耐 in 東京」無事終了いたしました!
— NPO-SPICE (@NPO_SPICE) 2018年10月7日
参加してくださった皆さま、会場を貸してくださったmixiさま本当にありがとうございました!#hachitai pic.twitter.com/RgwDRwqj8S
追記
XFLAG側でもブログ上がりました career.xflag.com
転職活動ログ
こちらの記事は転職ドラフト体験談投稿キャンペーンに参加しています
amazonギフト券が欲しいので書くことにした。
転職ドラフト以外も使って就職活動していたので、それらを話しつつ転職ドラフトの話をします。せっかくなので転職活動でこんなところで悩んでこんな解決したよというのも話します。
転職活動開始時
最初は知り合いから紹介された転職エージェントと話しながらいろんな会社に書類を送ったりした。それと並行してリクナビ、マイナビも登録していたが、これらで受けれる会社は大体エージェントが紹介できたので不要になり退会した。エージェントが紹介できない会社に限っては自分で書類送って受けに行くという感じで進めていた。
ちなみに転職活動の進捗状況はGitlabに「job-hop」というリポジトリ作って管理してた。
履歴書とか職務経歴書もここに管理。githubでもよかったんだけど、gitlabはissueにweightを付けられるので何かと便利。 面接で何話したかとかもissueにコメントしてたんだけど、会社によって明らかに文章量が違うので熱量がしっかり形に残ってるのは面白かった。
初期の課題
エージェント経由でも最初はだいぶ高速でお断りされていた。大体理由は二つかなあと思う。
書き慣れてない職務経歴書
転職活動は初めてなので正直なにやればいいの?感はあった。とりあえず履歴書と職務経歴書書いてね見たいな感じでやったけどそれも結構難易度が高く。 特に職務経歴書が難解で、僕の書いた経歴書の例文では「上流で設計を担当しました」「品質管理・テストを担当しました」とかプロダクトの担当範囲をしっかり書きましょうと説明書きがされていたけど別に上流もないしテスト担当だったわけでもないし全部やっとるわみたいなという気持ちになった。とにかく自分のやってきたことを文章にしようとしたときに職務経歴書フォーマットみたいなやつが全然当てはまらなくて完全オリジナル文書みたいなのができてた。
これが良いのか悪いのかみたいなのが見えないのが精神衛生上よくなかった気がする。
業界未経験
エージェントとも話してある程度理解はしていたが、やはり業界未経験が飛び込むのはちょっと大変らしい。
元々WEB業界だったところをゲームで探したので業界未経験という立ち位置になってしまい、結構いくつかの会社は経験が足りないとお断りされたのを覚えている。面接でも「この年齢で新たな職種に飛び込むのはだいぶ勇気が必要だと思われますが」と言われたりした。これは今でも「そんなに勇気いらんやろ・・」と思ってる。
最速だと書類送って20分で落ちた会社がある。バッチが走っているのかと思った。
ただこれは経験の有無だけではなくて、上記の職務経歴書書き慣れていない問題が重なって全く自分のことを伝えらえていなかったんじゃないかなあというのは反省している。
ポートフォリオ作る
この二点が結構問題だったので、業界の人とちょっとご飯食べながら相談した結果、やっぱポートフォリオあった方がいいよねーという話になった。
ポートフォリオとは言えないまでも、実は簡単な実績書いてるページはあった。
ただこれは殺風景だしなんかゲーム作ってる感がないのでやはりポートフォリオは作った方がいいということで、1日でさっと作った。
https://unityroom.com/games/portfolio
学生時代から遡って作ったゲームを紹介していった。実際に遊べるのは半分くらいだけどとりあえずこれで「ゲーム作れるんやな」感は出せた。
これも一緒に出してから選考通過率が上がったので最初からやればよかったほんと。 最終的には転職エージェント+自分で書類送った企業で合わせて2社内定いただきました。
転職ドラフトの話
転職活動初めて1ヵ月くらいのときに、ドラフト開催の情報がTLに流れてきたのでせっかくなのでちょっとやってみた。
レジュメは結構時間かかった。自分のコアに当たる部分なのでやはり経験だけかいても厳しいなと思い、何故やったか、何を解決したかったかというところに比重を置いた。業務の話になるので書けない内容も多くなるかなあと思ったけど、割と普段からブログで書いたり発表してスライド公開などしていたのでその辺をベースに書いていくと意外と分量は増えた。普段からのアウトプット大事。
個人的には現在の年収を問われないところが非常にありがたかった。前職を参考というのは本当によくわからない文化だと思う。
指名状況
最終的には7社から指名いただきました。 どれもちゃんとゲームでのお仕事というポジションだったのでありがたや・・と言ってた。そのうち3件辞退して、4件承諾してお話を聞きに行った。ちなみにミクシィもこの中の一つ。転職ドラフトは承諾後のフローは範囲外なのでここからは会社ごとでだいぶ動きが違っていて、通常の面接フローに入ることもあればカジュアル面談したら即内定もらっていたりなどかなり様々だった。指名されるいう形式なので志望理由とか特に聞かれないし、あちらもレジュメを吟味した後なのでしっかり話ができるというのはよかったなあと思う。
指名する理由がきちんと書かれているというのはこちらとしても納得感があり非常によかった。そこで自分がやりたいことじゃないなら最初から辞退できるし。
カジュアル面談で提示額が上がった
個人的にうれしかった話。
ドラフトで指名された企業とカジュアルな面談に行って、雑談してご飯食べて、その後公開してないソースコードを提出したらちゃんとした面接やる前に内定が出て、且つ当初提示された金額を更に引き上げてオファーいただいたことがあった。そこは人事の方もエンジニアで(兼任?)僕のソースコードをしっかりレビューしていただき、コードの良し悪しの話も含め、今後の成長性を評価して適切な額に変更しましたという連絡が来たのだった。ここまでしっかり理由込みでスキル面を評価してもらえたのは他社ではなかったのでかなり感動したのを覚えている。
最終的にはミクシィとその企業で最後まで悩みに悩んで、理由もすべてめっちゃ書いたお断りのメールを送ったらこれまた丁寧なお返事が返ってきたので、徹頭徹尾良い会社だなと思った。今もお会いしたら仲良くさせてもらっている。社名出していいなら出したい。
転職ドラフトの所感
僕自身はドラフトで見つかったのでいいサービスだとは思うけど、楽に転職できる!というサービスではないのでそこの理解は大事かなと思った。あくまでマッチングを目的としているので不相応な会社にいけるわけじゃなさそう。マッチングするためには私はこんな感じですという情報を出さないといけなくて、きちんと客観的に自分の能力とかスキルを判断できる何かが必要。そのためにはやはり定期的にアウトプットしないとなあという気持ちが強くなった。 なので僕は前職より更に会社で学んだことを公開するように意識している。
既に何かしらの武器を持ってる人には大変ありがたいサービスだと思うので、積極的に使ってよさそう。
最後に
転職活動中に色々相談させていただいた方々、本当にありがとうございました。おかげさまで僕は元気にUnityのバグ踏んでます。
社内LT会でキャリアキーノートをやってきた
弊社では隔週で社内LTが開かれている。
テーマはなんでもいいということなので、今回は前職でやっていたキャリアキーノートをやることにした。
キャリアキーノートとは?というところは次のブログに全て記されている。
とはいえLT時間は5分なのでかなり端折ることとなり駆け足の発表になってしまった。
5分で話せる内容は本当にごく僅かで、何を話そうかとかなり取捨選択した結果、自分が大事にしているものが見えた気がしないでもない。
周りに流されずに個人で黙々と続けられる人は本当に尊敬しているし自分もそうなりたいけどそれは本当に苦手。とにかく人の影響を受けまくる。後輩にも「周りの影響受けやすいですよね」と指摘されたことはあり、実際その通りだなと自覚しているので、じゃあいい人がいる場所を探した方がいいんじゃない?というのが僕の考え方になっている。転職活動時もその辺に気を使っていたので入ってから後悔も特にはないのだった。
ちなみにスライド内にある阿部さんのゲームは2008年製だけどWin10で動いて感動した。流石.NET Framework
開発方針についての文書を書いていた
色々決めごとをするときは社内にissueなりwikiなり残すのですが、これは外に公開してもいいんじゃない?と思ったので公開します。 今回は「開発方針について」です。
目的
- クライアントアウトゲーム全体の開発方法、指針を把握する
- あとから参加した人もスムーズに着手できるようにする
お品書き
- アーキテクチャのお話
- Rxのお話
- DIのお話
- テストのお話
アーキテクチャ
なぜソフトウェアアーキテクチャが必要か
- 大規模開発、運用にはスケールを見通した設計が必要
- 秩序なき開発は人間が犠牲になる
- 設計ルールをその都度作っていくのは大変
- すでに先人たちが積み上げてきた効率の良い設計が存在する
- それがソフトウェアアーキテクチャ
- 一般化されている設計ルールは独自設計より学習しやすい
- 後から入った人も理解しやすい、むしろそれを前提とした採用もできる
プロジェクトの設計(アウトゲーム)
- MV(R)Pアーキテクチャを採用している
- MVPアーキテクチャの派生
- ViewとPresenterをRxで繋ぐ
- 経緯 ここにissueのリンクが入る
MV(R)P アーキテクチャ
Model
- いわゆるロジックと呼ばれる部分
- PureClassであり、MonoBehaviorではない
- 自身を更新したり、処理結果を返すインタフェースを公開している
- 自身のパラメータをReactivePropertyで公開もする
- Modelと一口に言っても内部で分類が色々ある。
- Presenterを知らない
- Viewを知らない
- ユニットテストが書けるようにする
View
- ButtonやImageなど、画面に表示されるもの
- MonoBehavior
- 複数の要素をまとめてViewクラスとするのも可
- Viewはイベントストリームを持っている
- クリックされた、選択されたetc..
- ストリームを公開して、外部から購読できるようにする
- Viewは外部から情報を更新できるインタフェースを公開している
- 任意のキャラの情報を表示など
- Presenterを知らない
- 自身の情報更新のため、Modelは知っている
- メンバ変数で持つのは推奨しない
Presenter
- ViewとModelの間に位置するもの
- MonoBehavior
- ViewとModelを知っている
- Viewのストリームを購読し、Modelに更新をかける
- Modelの変更ストリームを購読し、Viewに更新をかける
- Presenterは複数のViewを購読してもよい
- 大きくなりすぎたら別のPresenterに切り出す
Modelの詳細
Modelは多種多様なので、役割ごとに適切に層を分けないと開発に支障が出る。
- どこにファイル置けばいいの?と考える時間は少ないほうがいい
逆に層を分けすぎてもそれは分割の手間や思考の時間を奪う
- スパゲティコードに対してラザニアコードと呼ばれる
MV(R)Pにおいて、モデル下は規約は特に定まっていない。 が、スタンダードな考え方はいくつかある
プロジェクトで考えてるモデルの分け方
- Model
- Model/Entity
- Model/UseCase
- Model/Repository
Model
- いわゆるロジックと呼ばれるもの
- 現状はここに大半いる
- 色々な役割のクラスが混ざっているので適切な分割は必要
Entity
- ロジックをほぼ持たないデータのみのクラス
- キャラクタのパラメータとか
- 通信のレスポンスオブジェクトとか
Repository
- リソースを処理する存在
- リソースのGET,PUT,CREATE,DELETEのイメージ
- どうやって処理するのかを書く
- 通信?ローカル?etc...
- 外部からはリソースがどこに存在するのかは見えない
- キャッシュ機構を持つのもあり
UseCase
- ビジネスロジックと呼ばれるもの
- 「やりたいこと」単位でクラスを作る
UserNameChangeUseCase
TitleUserLoginUseCase
など
- Presenterは基本UseCaseを使ってモデルを触る
名前変更処理の一例(現在こうなっているわけではない)
インゲームのアーキテクチャについて
- 現状定めていない
- パフォーマンスチューニングなどが必要になるので、レイヤーの分割が適切に行えないことがある
- ViewとModelに分割はできるかも?くらいの認識
Rx
ReactiveExtentionsとは
- こちらをご参照ください https://www.slideshare.net/torisoup/unity-unirx
MV(R)PにおけるRxの役割
- Viewのイベントを購読する、モデルの更新を購読する
- Presenterがイベントを検知するために使う
- https://www.slideshare.net/torisoup/unirxmvrp
ここの Subscribe
OnNext
の関係を実装するのがRx
async/await とObservableの使い分け
- 単純に非同期を待ち合わせたいだけならasync/awaitが簡単
- それだけじゃない複雑なことをするならObservable
- 特にイベント処理はObservableのが楽
- とりさんのスライドで大体かいてる https://niconare.nicovideo.jp/watch/kn3081
DI
Dependency Injection
DIはもはやもうこれ見てもらったほうが早い・・
https://qiita.com/toRisouP/items/b3d3c43db40857ca4ad4
プロジェクトにおける主なZenjectの使い方
- staticなオブジェクトをなくす
- 環境によるモジュールの差し替え
staticなオブジェクトをなくす
- 実体に強く依存するため、差し替えづらい
- isTestみたいなフラグは持ちたくない
- ゲームはシングルトンによるstaticが生まれやすいイメージ
- マスタデータ、サウンドマネージャetc...
これらをDIContainerに管理させることで、疎結合なシングルトンに変更する
環境によるモジュールの差し替え
- テスト時には通信したくない
- ローカルと本番でリソース取得先を差し替えるなど
通常だとこれらのフラグ管理などが必要 or 手でDIしなくてはならないが、これらをDIContainerに任せられる
何をBindすべきか
- あらゆるメンバをBindするとかえって面倒になる
- 普通にコンストラクタで渡せるならそれのが楽
- 基本的にはシングルトンだったものが対象
- Presenter、Viewの層まではやってしまってよい感覚
Model層にContainerを渡さない
- 上記の通り、コンストラクタで渡せるものは
- Containerそのものに依存してしまうのはそもそも設計としてよくない
- Presenterから適切なオブジェクトだけをモデルに渡すのが良い
- ここは努力目標で・・
public class BadUseCase { [Inject] DiContainer container; // UseCaseがcontainerまで気にするのはおかしい public void Foo() { var foo = new BadFoo(container.Resolve<Bar>()); } }
public class BadUseCase { private Bar bar; // 素直に受け取る public BadUseCase(Bar b) { bar = b; } public void Foo() { var foo = new BadFoo(bar); } }
テスト
テストいろいろ
プロジェクトで書けるテストは2種類 - ユニットテスト - UIテスト
なぜ書くのか?
- 大体issueにまとめている
- 以前発表したスライドも参照
ユニットテストはどのくらい書く?
- 基本的にモデルはすべてユニットテストが書けると考えている
- 作成したメソッドのテストは書いてほしい
- タスクの完了条件に「テストを書いている」を足したい
- 現場の状況で書かない判断はしましょう
テストでよくある質問
Q.仕様変更はしょっちゅう起こるし、その都度落ちたテストを書き換えるのは手間では?
A.確かに手間です。が、どう落ちたのかが可視化されるのは大きなメリットです。影響範囲がある程度わかれば修正もやりやすくなるので結果的には開発速度は上がると考えます。
テストでよくある質問
Q.テスト書く時間をかけすぎて辛い。
A.テストが書けない理由を掘り下げることが大事です。
- 何をテストしたらいいかわからない => そのクラスに何をしてほしいのか明確になっていない?
- テストのための準備が多くて書きづらい => 1クラスの依存関係が多すぎる?
- テストの構文がわかってない => ググるぞ!