東方ゲームジャムに参加してゲーム作った
東方二次創作限定のゲームジャムが開催されてたので参加してみた。
木曜昼から日曜昼の72時間で開発・・というイベントだが、普通に仕事だったので木曜日に軽く触ってから土曜日から本格的に開発始めました。
作ったゲームの話
そんなわけで、「イブキン」というちび萃香で戦うアクションゲームができました。
ざっくり言うと、ピクミンに弾幕要素を足したゲームという感じです。最小の犠牲で倒してよりちび萃香を増やして強い敵を倒す。そんな感じです。デザインを整える時間がなかったので中々に厳しいUIをしていますが、ゲーム部分は歯ごたえあると思うので騙されたと思ってやってみて!
進捗の流れ
木曜日
今日の朝に気づいたので仕事終わってからチョット触ってた。
— いも (@adarapata) August 20, 2020
見切り発車過ぎてどうするか決めてないぞ!
#東方ゲームジャム pic.twitter.com/hGYPTY1idR
土曜日の夜
弾幕を避けながらちび萃香でボコボコにしてスコアを貯めたらさらにちび萃香を増やしてボコボコにするゲーム、仕組みはできたけどここから味付けか・・・ #東方ゲームジャム pic.twitter.com/FLGi5jbZgC
— いも (@adarapata) August 22, 2020
日曜日の朝
やべえよこれ pic.twitter.com/2hqcVM68qX
— いも (@adarapata) August 23, 2020
締切2分前
おもむろにプレイ動画を上げる
久しぶりに徹夜での開発をしたが、もう二度とやりたくないなと思った。
「2010年かな?」と思うほど東方のゲームが上がってきてハチャメチャにテンション上がりました。やはり未だに愛され続けてる幻想郷。
ここからはGodotEngineの話
普段はUnityなんだけど今回はGodotEngineで作ってみました。趣味でちょこちょこGodotを触ってはいたのだけどちゃんとリリースまでやったことはなかったので、いい機会とGodotチャレンジをねじ込んでみたのでした。
よかったところ
2D周りの機能が使いやすい
例えば今回はちび萃香や敵の移動として経路探索が必要なんだけど、Navigation2Dとタイルマップ機能が非常に使いやすくサクッと実装できた。タイルにナビゲーションの設定して設置していったらNavigation2Dのパス取得API叩いてよしなに帰ってくるようになってる。
この動画見たらすぐに使えたくらいの手軽さだった。
2D用のカメラ機能もあるんだけど、キャラクターを注視しつつ左右の移動幅の限界を付けるとか移動時のスムージングとかその辺はデフォルトで備わってるので凄く助かった。 UI部分も2Dと同じ座標空間で扱えるのでキャラの頭の上にライフのサークル置くとかも脳死でできたり。
GDScriptが楽しい
今回はGDScript(pythonベースの言語)で書いたけど、動的言語ではあるのでダックタイピングがやりやすく開発終盤にはめっちゃ助かった。衝突検知したオブジェクトにDamageメソッドあったら問答無用でぶん投げるとかそういうことを後半にやりまくってた。やりすぎるとどんなオブジェクトがやり取りされてるのかカオスになるけど、型を付けることはできるので基本的には型付けして一部の部分だけ自由奔放なコードができてた。 あとシグナル機能が標準で付いてるのは楽しい。完全に忘れてたので数年前の自分の記事を見ていた。
yieldとシグナルで非同期を待つのは手軽で便利だった。 Nodeに直接書く埋め込みスクリプトも、特定の部分だけ動かすためにさっと書くということができてうれしい。
イテレーションが早い
デバッグ実行までが異常に早く、コード書き直して1秒で再生できる。これのおかげでトライアンドエラーのモチベーションがかなり高く保ててる気がする。 GodotEngine内でコードを書けるというのも楽しい。当初はエディタは普通に多機能な外部IDEにした方が効率いいんじゃないかあと思ってたんだけど、ゲームエンジン <-> IDE画面を行ったり来たりすることがなくなると、集中が途切れにくくなるなと感じた。GodotEngine内蔵のエディタがちゃんと補完も利くしエラーとなりうる部分を教えてくれるのでそこまで不便ではないのも嬉しい。ドキュメント検索もしやすい。
大変だったところ
日本語記事の少なさ
本当に少ない、これはそう。素直に公式ドキュメントを見るかissueを見るかOSSなのでコードを読むか。今回は幸い近くにつよつよGodotエンジニアがいたので終盤の謎のクラッシュ問題の解消に協力してもらえたので何とか生き延びた。
逆に言えば日本語記事を考慮しなければyoutubeとかにも結構勉強になる動画が上がってるのでお勧めです。
おわりに
完成度は置いておいて、久々にちゃんと遊べるものとしてリリースまでもっていったので割と満足度高いです。やはり遊んでもらってナンボだなと思いました。 粗削りな部分が多いのでアップデートはかけていく予定です。
あと、30超えて徹夜で開発はするものではないなと思いました。
2月に読んだ本
今月は一冊のみ。読みかけのレガシーコード改善ガイドを読み終えた。
レガシーコードに対してどう立ち向かっていくのかを具体的に書いている本。とにかく実例がリアルだった。大量のif文やswitchが継ぎ足されたコードやほにゃららManagerなどなんだか見たことあるもののオンパレードで、且つ章のタイトルが「時間がないのに変更しなければなりません」など胸に刺さる。それらに対してどう切り込んでいくか。テストのないコードはレガシーコードであるとこの本では語っているので、兎にも角にもテストを書く。如何にテストハーネスに入れるかというところが多く書かれていて参考になった。 スプラウトクラス、スプライトメソッドなどの手法は元々行ってはいたが名前はこの本で初めて知ったので非常にためになった。レガシーコードに機能を入れるとき、当たり前ではあるが「これ以上複雑なコードにしたくない」という気持ちが根っこにある。しかし、どうしたら今よりシンプルかつ動くものになるのかを考えるのは難しくて、本当にこれでいいのかな?と試行錯誤しながら恐る恐る修正している。そんな時に書籍に載っていたこの手法!という風に確立されていればかなりの後押しになると思う。そういった点で、この本で名前を知れたことは凄く自分の中でありがたいことだった。もちろん手法が使えるかどうかはケースバイケースだけど。
目の前のレガシーが辛い!となっている人がこの先生きのこるためのサバイバル本って感じでした。
三月はせめて2冊くらいいきたいところ・・
1月に読んだ本
年末から読書欲が高まったので結構読んだ。
アジャイルな見積りと計画づくり ~価値あるソフトウェアを育てる概念と技法~
元々読みかけだったやつを読み終えた。 現場でプロダクトバックログがうまいこと消化できなかったりしたときにちょくちょく読み返している。本を読んだうえで今もよく感じるのは、見積りの精度を高めるために時間をかけすぎてしまうこと。精度を高めるためにPBIに書いてあることを深読みして実装方法を考え熟考してしまう。その実装方法かわからないのに考えて結局1時間とかかかってしまうこともままあるので、その都度本に書いてある労力と正確さの相関図を思い出すのだった。
カイゼン・ジャーニー たった1人からはじめて、「越境」するチームをつくるまで
これも元々読みかけだったやつ。計画づくりの事例よりももっと泥臭いストーリー展開が見れて面白かった。ほぼ小説なのでサクッと読めたのも良い。僕らは果たして江島さんになれるのだろうか。
アジャイルサムライ――達人開発者への道
アジャイル開発のマインドセットとCI/CDやテストなどがアジャイルの両翼であることが書かれててよいなーと思った。イテレーション・ゼロはこの本で知ったので新規でやる分は参考にできそうだなと思った。ちなみにゲームジャムとかハッカソンの時は「まずはビルドできるようにするぞ!」って意気込んで、なにより最初にビルドが通る環境を作るんだけど、これがイテレーション・ゼロなのではとふと思った。 あとマスター・センセイとか図とかが凄くいい日本語訳されててじわじわ来た。
ファンベース ──支持され、愛され、長く売れ続けるために
僕もガルパン限界オタクなので、ファンベースで定義されるファンの心理とか精神は非常に理解できた。「そう!それなんだよ!」と頷けるものがあるなら勝手に付いていくし勝手にガルパンはいいぞおじさんをする。新規を取得することは重要ではありつつもどうファンにしていくかは割と疎かになりがちなところなのでこの話は非常に面白かった。特にゲームはこのファン心理が強いものだと個人的に考えている。ファンは開発者を理解したがるし共にあろうとするのだ。
レガシーコードからの脱却 ―ソフトウェアの寿命を延ばし価値を高める9つのプラクティス
そもそもどうしてレガシーコードになるのか、という話の本。コードレベルだけではなくアジャイルの話とかテストの話とかも割と多めに割いている。どうやって綺麗であるべきかという点が書かれているので、ここを目標として現状との乖離を分析したりするのもよさそう。逆に現在のレガシーコードと立ち向かう方法は書かれていないので、そのあたりはレガシーコード改善ガイドを読むのがおススメ。
オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方
会社の人にお勧めされ読んだ本。オブジェクト指向がメッセージングのやり取りであること、というのを常に中心に添えているという印象だった。こんなやり取りを行うからこのクラスはこう名付けられるのだという考え方の順序が逆転するのはなるほどなと感心して、これがダックタイピングなのだなあと参考になった。最近どうレイヤー分けするのかというところを考えることが多く、そのレイヤーの中でどう実装するかという名前付けに苦しんでいた。これはどんなUseCaseなのかとかこれは何のEntityなのかとかそういうところ。クリーンアーキテクチャで言うところの「叫ぶアーキテクチャ」になっていないのではないかと悩んでいた。しかしこれこそ、どういうメッセージングを行うかを考えずに名前を付けて役割を考えようとしたことが原因なのかなあと立ち返るきっかけになった。
普段三ヵ月に一冊くらいしか読まないので、自分でも頑張った方やなって思った。 勢い着いたのでもっと読む癖付けよう。
2019年ふりかえり
毎年のやーつー
仕事
相も変わらずテスト書こうぜ!って言ってたりする。新機能を作ったり細々とした改善を行ってたりします。今年は「ゲームのクレジットに載る」という実績を達成したので最高にテンション上がりました。とはいえスマホゲームは運用が続くので永遠に踏ん張りどころだなというお気持ちです。
社内ではクリーンアーキテクチャ本読書会を完走しました
これいい本だからやりたいね!みたいな話から9人くらいで始まって、週一の半年くらいで読み終えました。クライアントとサーバサイドエンジニアの両方が参加していたことで、話し合うときのバックボーンがバラバラになっていたことは結構面白ポイントでした。サーバサイドはこういう設計が行われる、なぜならこうだからだ。ゲームクライアントはこの設計を良しとする、なぜならこうだからだ。みたいな話を聞けるのは新鮮だったと思います。
現在はTDD本読書会やってます。
これは前回よりは人数少ないけど細々とやっております。読んだり、テストフレームワークを写経したりなど。
社内では現行のプロジェクトにかかりっきりで中々自分が横展開できていないのが悩みどころなので来年だなあ。
登壇・LTとか
- どこから始めるUnity Test
- Humble Object Patternな話
- ふりかえりをいい感じにやるために
- Zenject Example ~さよならManager編~
- Zenject Example SubContainer
- Zenjectを導入する前に
Unityにおけるテスト&Zenjectみたいな感じで動いてました。 Zenjectに関しては、2019年はZenjectの情報をもっと増やして人口を増やすぞという計画を立てていたので意識的にZenjectのLT回数増やしてます。ブログもそんな感じで年初からZenjectで日本語で説明されているものがない部分を書いてたりしました。
前半頑張りすぎて後半止まってしまった感が否めない。その代わり本書いたので許して・・(後述)
別件で、個人のお仕事として他社のエンジニアを対象にUnityのテストとDIについてお話しするということを行いました。人生初の、「お金をもらって喋る」ということをやったのでめっちゃ緊張しました。これはコンサルティングになるのだろうか・・・。まだ相手の会社さんが記事出してないのでその辺出たら紹介します。
書いたもの
mixiの合同誌「mixi tech note Vol1」で「幸せな開発のために考えていること」という話を10ページくらい書きました。
コードの内側と外側の話を好き勝手書いてます。
9月の技術書典では、Zenjectの同人誌「ZenjectチョットワカルBook」を書きました。
個人誌は初めてなので死ぬかと思いました。こちらは物理本・電子本合わせて200くらい頒布できております。 割と好評いただいているようで、他社でこれ読んで作っておりますという話もいくつか聞いております。本当に感謝・・。 現在ぼちぼち後編も書き始めているので気長にお待ちいただけると幸いです。
ハッカソンとか
今年はPro Developers Game Jamの運営兼開発してました。
prodevelopers-gamejam.connpass.com
prodevelopers-gamejam.connpass.com
あと毎年恒例大八耐もやってました。
「本田君のリーゼントが!」という感じのパズルアクションを作ってました。塩漬けです。
今回の大八耐は、ぷよぷよみたいに降ってくるブロックに潰されないよう色変え弾を発射しつつ避けながら消していくアクションゲームを8時間くらいでガガっと作ってみました。やられ演出がお気に入りです #hachitai pic.twitter.com/fcPceDdqpJ
— いも@ZenjectチョットワカルBook (@adarapata) November 10, 2019
最近ハッカソン系が疎かだったのでシュッと作る能力が落ちてたけど、また勘が戻ってきた気がする
アニメ・映画
- PSYCHO PASS 1・2
- ジョジョ5部
- ダンベル何キロ持てる?
- ワンパンマン2期
- 上野さんは不器用
- 荒野のコトブキ飛行隊
- SHIROBAKO
- モブサイコ100 1・2
- 輪るピングドラム
- 妄想代理人
- かぐや様は告らせたい
- ゆるキャン△
- ケムリクサ
- どろろ(12話まで)
- 私に天使が舞い降りた!
- ハクメイとミコチ
- メイドインアビス
- プロメア
- 天気の子
- スパイダーマン・バース
- 名探偵ピカチュウ
- ゴジラ・キングオブモンスターズ
- アナと雪の女王2
- T-34 レジェンドオブウォー
- ガールズアンドパンツァー
- ガールズアンドパンツァー これが本当のアンツィオ戦です!
- ガールズアンドパンツァー 劇場版
- ガールズアンドパンツァー 最終章 1話
- ガールズアンドパンツァー 最終章 2話
去年より意識して観る数増やした。 スパイダーマン・バースは表現にひたすら衝撃を受けたのを覚えている。クライマックスの何が起こってるかよくわからないんだけどなんかカッコいいしなんか分かってしまうバトルシーンは凄かった。 プロメアはTRIGGER節が炸裂していて痛快娯楽だった。滅殺開墾ビームという単語が出てきて劇場で変な声出た。天気の子は昔Windows版遊んでた幻覚を観ました。
でも今期もガルパンが最高でしたね。
ちょっとマラソンしてくる pic.twitter.com/Zbii1D5lmY
— いも@ZenjectチョットワカルBook (@adarapata) June 15, 2019
ゲーム
- ポケモンシールド
- リングフィットアドベンチャー
- ガールズアンドパンツァー ドリームタンクマッチDX
- シャンティ ~海賊の呪い~
- シャンティ ~ハーフ・ジーニーヒーロー~
- MOON
- ペルソナ5
- スーパーマリオメーカー2
- スーパーロボット大戦T
- ライザのアトリエ
- くにおくん外伝 - River Side City
- 東方鬼形獣
- Project Winter
正直、RPGはポケモン以外クリアできてないのでつらい。PS4のゲームを腰を据えて遊べなくなってきた気がする。 それ以外としてはシャンティが想像してたより面白かった。結構難易度高めのアクションだったので歯ごたえは十分だった。あとキャラが可愛い。 スパロボTはいつもより絶望感少なくて笑える感じだった。ガンダムファイターとかスパイクとかヴァンとか肉弾戦が強すぎるメンバーが多すぎたのが原因かもしれない。ガンソードが軸にあるからか復讐上等みたいなキャラが多くて後押ししまくってたのもある。最終的にはフル改造したスコープドッグ一機で全滅させてた。 東方鬼形獣で久々にSTGをやったんだけど、どんどん腕が衰えて弾幕が避けられなくなってて悲しかった。ノーマルクリアはしてEXをまだクリアできてないのでリベンジしたい。 ガルパンドリームタンクマッチはオンラインの過疎化が辛い。みんなやろう。
大洗
7月と11月に行ってきた。最高だった
その他
バズった
友人の人生初バンジーを撮影したんだけど、閉園間際で蛍の光が流れ始めて非常に趣深いものが撮れてしまった pic.twitter.com/Kt2LUViZ2k
— いも@ZenjectチョットワカルBook (@adarapata) July 15, 2019
まとめ
ふりかえってみると去年より活発になっていた気がする。アニメ本数多いし。 意識的にやってたのもあり、この1年でUnity界隈において「Zenjectに詳しい人」という立ち位置にはなれたのかなと自負している。仕事のお話が出たのもそこからだったし。というか逆にそれ以外やってない。DOTS周りをもっと見たかったけどできてないし個人開発もあまり進んでないので全てはトレードオフなのだと実感する一年だった。 とは言え、個人で同人誌を出したり技術コンサル的活動をしたりと新しいことには取り組めたので割と良い2019年だった。来年は副業としてそういうお仕事をもっとやってみたい。お気軽にお声がけください。
仕事納めとスクラムの話
今日で仕事が納まった。それと同時にチームメンバーが一人チームを去ることになった。送別会もしたのでお別れはまあそれなりにしたんだけど、結構自分に影響を与えたのでなんとなくブログとして残しておくことにした。
うちの部署は複数のスクラムチームで構成されているスクラムオブスクラムという形を取っている。僕がこのチームに配属されたのは二ヶ月前くらいのことだ。部署内の別のチームからこのチームに異動したのがきっかけだ。チームメンバーの一人であり、今回お別れした人は認定スクラムマスターを持つエンジニアであり、このチームはちゃんとアジャイルなチームになろうとしているという話を事前に聞いていた。(以降、この人を彼と呼ぶ)
前職でもスクラム開発はやっていたつもりだったし、以前のチームでもスクラムを行なっていたのでなるほどなるほどという感じでチームのやり方に乗っかっていくことにした。見積もりをして、計画で洗い出し、ポイントを計測して振り返りでTRYを洗い出そう。そんな感じに思っていた。しかし、今まで自分が経験したやり方とはチームのやり方の違いに衝撃を受けた。特にこの2週間くらいの出来事を列挙する。
並行してタスクをこなすことがなくなった
スプリントでやることはPBI(プロダクトバックログアイテム)を基にして計画で洗い出す。一つのPBIが大きいならどんどん分割していく。完了条件が定義しやすくなるまで細かく刻んでいく。そこから具体的に何が必要かを職種問わず洗い出して付箋に貼り出していく。この時に誰がやるかとかは一切考えない。
この時に、分割で複数のPBIが現れることになる。分割しなくても関係ない別のタスクがPBIとして存在することもある。(新機能追加と既存の改修が同時に存在したり)でも優先度は計画時点ですでに決まっている。ここで、今までだったら複数人エンジニアがいるのだから並行してやるのでは?と思ってたがそうはならなかった。必ず優先度の高いPBIを全員でこなす。それが終わらない限りは次のPBIには触れなかった。そもそもスプリントに入らなそうなら今スプリントでやるPBIから外した。理由を聞いたら「並行して個人で行う時点でそれはチームの意味があまりない」とのことだった。最初はなるほどな〜くらいの意識だったが、朝会夕会もしくは業務時間でこれどう進めようかという話を全員で行えるのは気持ちが楽なことに気がついた。なによりやることがシンプルで、眼前にあるPBIをどう完了するかを考えるだけでよかったのは脳のスタックの消費が少なかった。
一人でコードを書かなくなった
このチームは自分を含めてエンジニアが3人いる。上記でPBIをこなす際に全員でという話をした通り、エンジニアも全員でこなす。つまりモブプログラミングの形式を取って実装を進めていた。一人が実際のコーディングを行い周りでやいのやいの話す形式だ。
全員でPBIを見ながら改めてこの実装が必要では?という話をする。逆にその実装はこのPBIを完了させるのに必要ではないのでは?なんてことを喋りながら付箋にやることを書き出す。それぞれに得意分野は違う。一人はプロダクトのキャリアが長くドメインに詳しいので、その観点から現状の仕様を話してどう実装すべきかを提案する。自分はチーム内ではプロダクトのドメインに乏しいが技術的な観点での話はできるので、こういう実装方法ならテスタビリティが高いのでは?ということを提案して実装を進めていく。それをやいのやいの話しながら動くものを作っていった。
この作業は効果的に働いたと思う。全員で考えて作ることで発生しうる問題をある程度予測できたからだ。三人寄れば文殊の知恵というが本当にその通りだなと感じた。
副次的な効果としてコードレビューの手間が省けた。そもそもみんなで作っているので改めてレビューする部分が少ないからだ。(とはいえPRで俯瞰して見た時に微妙な書き方のツッコミはしたくなったので無ではない)
もう一つの効果としてQAとの関わり方が少し変わった。
ちょっと前までモブプロでエンジニアが作る -> ビルドしてQAに動作確認してもらう -> 不具合を見つけたら報告してもらうというフローを採用していたが、これだとスプリントの前半はQAが暇になって後半が詰まってしまうよねという問題が発生した。これを解決したいねーというのがチームの課題になった時に、「モブプロに入って貰えばいいのでは?」という話になった。所轄モブワークである。エンジニアが作ろうとしている時点でチェック項目が生まれるのだから、ビルドを待たずとも最初から入ってもらえればその時点でチェック項目を作成できるし、Unityエディタ上で動作チェックもできるだろうという考えだ。
このやり方は成功した。エンジニアがこういう実装が必要という段階でQAさんが「このパターンとか大丈夫ですか?」みたいなツッコミが入り初期段階で懸念点を潰すことに成功したのだった。エンジニアが複数人集まってもうっかり気づけない項目が結構あることを思い知らされた。プロフェッショナルはすごい。
加えて隣の席に座っていたプランナーも会話が聞こえるので普通に入りやすく、こういう感じでやりたいとかこういうデータで試しにやってみるねなどのコミュニケーションが円滑に行われた。目の前で動かすし、即座にこうしたいが入るので、少なくとも出来上がったものに認識のずれはなかった。
なにより単純に楽しかった。どう作っていくかを話し合うのはそれ自体が楽しいのだ。
チームで作っていくという認識を改めた
「チームで作っている」というのは仕事なら当たり前なんだけど、本当にできていたのだろうかと考えるきっかけとなった。仕様が固まってないのでできませんとか、実装したけどQAの手が空いてないので進んでませんとか、僕が書いたコードではないのでわかりませんとか、仕様の詳細は企画に聞いてくださいとか。
彼は「チームの成果物はチームが責任を持つべきだ」と強く言った。スプリントレビューに出すものは、みなさんどうしたらいいですかね?と周りに意見を求めるものではなく、うちはコレを作った!バーン!と責任を持って出してGood!Bad!みたいなフィードバックを貰わなければならないという話だった。そうしないとチームとして成長できない。そのかわり責任はチームでシェアしたいよねということも付け加えた。だからこそ個人で分担するのではなくチーム全員で立ち向かわなくてはならない。隣の人が今日何をしていたかわからないというのはチームとして動けているのか?それはslackにログを流していれば解決することなのか?とか。
しかしその逆に、チーム外に関してはもっと閉鎖的で良いと考えていると彼は話した。この話のキッカケになったのはコードレビューだ。チーム内コードレビューは簡単だが、別のチームのコードレビューはコンテキストが把握しきれないことも多くそれなりに大変だった。コレに対して、レビューの必要性そのものを考えるべきということだった。それぞれチーム内レビューで完結させて、そこで不具合が出るのならばそれがそのチームの現状であるので、チームで改善していくのが良いのではという話。その改善の過程で他のチームにレビューを求めるならそれは応じてあげればいい。
自分はわりとプロダクト全部のコードを知っておくべきだと考えていたし、レビューそのものが技術的な成長のきっかけだと考えていたのでこの考え方は新鮮だった。意見の是非ではなく素直に参考にしたいと思った。
年末年始はUnityのDOTSについて調べようかと思ってたんだけど、一回アジャイル開発の本を読み直そうと思った。それくらいこの2週間は衝撃で、楽しかった。見積りをしてポイントを出したスクラム開発であることは今までと変わらないけど、今までとは明らかに違う部分に触れられた。
来年からは彼がいない状態のチームでやっていくことになる。今までのようなチームを維持できるかはわからないけど、彼がいないから上手くいかなくなりましたというのはそれこそ自己組織化ができていないのでそうならないように立ち回っていきたい。
おつかれさまでした。
Zenjectを使うときに気を付けていること
このエントリはUnity Advent Calendar 20194日目のやつです。
Zenjectは便利ですが、実際に扱っている人の知見やtipsがまだまだ世に出回ってない印象です。APIの使用方法も欲しいですが実際に使ってる人たちがどういうお気持ちで使っているのかというのはどんどん出回って欲しいので、今回は僕がZenjectを使う際にチョットだけ気を付けていることを列挙します。 そんなにテクニカルなことではなく心持ちみたいなのが多めです。
これが正しい!というのは中々決められないと思うので、1つの考え方として参考になれば幸いです。
- 基本的にコンストラクタインジェクションを行う
- intefaceをBindする
- primitiveな型をBindしない
- ListをBindしない
- 手動でもDIできるようにする
- Validateを使う
- DiContainerを持ち歩かない
- 積極的にSubContainerを使う
- Initializeで購読する
- デバッグ機能はSceneDecoratorContextで切り分ける
- おわりに
基本的にコンストラクタインジェクションを行う
Zenjectでインジェクションを行う場合、4つの選択肢があります
- フィールドインジェクション
- プロパティインジェクション
- メソッドインジェクション
- コンストラクタインジェクション
public class Foo { [Inject] private Bar bar; [Inject] public Hoge hoge { get; private set; } [Inject] public void Construct(Fuga fuga) { } public Foo(Piyo piyo) { } }
この中でコンストラクタは1度しか呼ばれないので、コンストラクタインジェクションするということは、あとから外部から変更される可能性を無くすことができます。 また、オブジェクト間で循環参照が発生したとき、コンストラクタインジェクションはエラーを吐きますがそれ以外の三つは循環参照を許容します。循環参照は複雑性が高くなり、明確な目的がない限りは避けていきたいところなのでバリデーションしてもらえるコンストラクタインジェクションはを使うように気を付けています。しかしMonoBehaviourはコンストラクタが呼べないので泣く泣くメソッドインジェクションを行っています。
public class FooBehaviour : MonoBehaviour { [Inject] public void Construct(Fuga fuga) { } }
名前はコンストラクタの代わりなのでConstruct
と付けるのが好きです。
intefaceをBindする
基本ですが、クラスはBindせずinterfaceだけを渡すようにします
public interface IFoo {} public class Foo : IFoo {} Container.BindInterfaces<Foo>().AsCached(); // IFooで登録するがFooは登録しない //Container.Bind<Foo>().AsCached(); // 直接クラスは登録しない
これを実現するために、Injectされる側もinterfaceでメンバ変数を持つような実装になります。あと、今回のIFooのような1つにしか実装されないようなものも毎回インターフェースで渡すことになります。これは基本的に依存関係逆転の原則(DIP)のルールに従うためです。もう一つとしてはDIを行われる側から詳細(実際のクラス)を減らしたいお気持ちがあるからです。
理想を言うのであれば、詳細の情報はinstallerから漏れ出ない状態が嬉しいです。Installerはクリーンアーキテクチャ本で言うところの「Mainコンポーネント」の役割を持っていると考えています。Mainコンポーネントは最も下位レイヤーの処理であり、初期状態を作成したり設定を構築したりと大変泥臭い部分です。ここでどんなデータがあるのか、どんな詳細を持っているのか、どう渡すのかという部分が詰め込まれることで、Mainコンポーネントを変えるだけでテスト、開発、本番環境などを切り替えれるようになります。この動きをInstallerで行うためには、実際のクラス情報はInstallerだけが知っている設計にする必要があり、そうなると各依存関係はintefaceでのやり取りにしたいよね・・という感じです。
とはいえ、Factoryとかはよく実体を返しがちなので徹底するならちゃんとカスタムファクトリ作らないとね~ってなる。あくまで理想としてです。
primitiveな型をBindしない
整数型や文字列などをそのままInstallerにBindするのは控えてます。
public class MainInstaller : MonoInstaller { public override void InstallBindings() { Container.Bind<string>().FromInstance("name").AsCached(); Container.Bind<int>().FromInstance(100).AsCached(); } } public class Player : MonoBehaviour { [Inject] public void Construct(int life, string name) {} }
Containerは型で全てを判断するのでBind<int>
Bind<string>
と言ったプリミティブ型はInstallerを見直したときに何を表すのか非常にわかりにくいです。また、同一コンテナ内で競合を起こしやすいです。上記の例では別のクラスが攻撃力としてintを求めていたとしてもぶつかってしまいます。WithId
オプションで差別化は可能ですが、意味のある値ならば素直にクラスを作ってあげる方が良いでしょう。
ListをBindしない
Container内部でIEnumerable<T>
が見つからなかったとき、Zenjectは空のリストをInjectする特性があります。
public class MainInstaller : MonoInstaller { public override void InstallBindings() { Container.Bind<Foo>().AsCached(); } } public class Foo { private List<Bar> _barList; public class Foo(List<Bar> barList){ _barList = barList; } }
上記の場合、List<Bar>
がないのでエラーが出そうに見えますが_barList
には空のリストが入り、バリデーションエラーも発生しません。これはうっかりBind忘れを起こしていた場合にも正常に動作してしまうので非常に問題に気づきにくいです。これもやはりちゃんとクラス作ってあげましょう。
手動でもDIできるようにする
実際に手動でやることはあんまりないですが、DiContainer
が無いと動かせない状況は避けています。具体的にはprivateなメソッドへのInjectは避けようという考えです。
public class FooBehaviour : MonoBehaviour { [Inject] private void Construct(Fuga fuga) { } }
上記のコードはZenjectの機能を用いないと初期化処理が難しい状況になっています。メソッドを不用意に公開しないというのは有効な手ではありますが、この初期化処理はZenjectに自動で行ってもらっているだけで外部公開してはいけないものではないと考えています。ここをPublicメソッドにしてもらえれば、軽い動作確認を行うときにInstallerを用意する必要はなくなるし、テストコードを書くときにContainerを用意せずに済むので少し楽になります。
手動で行うべきところをZenjectに自動でやってもらってるくらいの気持ちで[Inject]
を付けるといいかもしれません。
Validateを使う
ZenjectにはValidate機能が備わってて、シーン内の依存関係であるならPlayModeで実行せずにShift+Alt+v
で自動チェックしてくれます。
毎回PlayMode実行してAssert Hit!と怒られるよりは速いので癖をつけておくのはおススメです。 ただ、Factoryが生成してくれるかみたいな動的な部分は対応していないのでご注意ください。
DiContainerを持ち歩かない
割と基本ではありますがContainerの存在を極力隠します。目安としてはInstallerのコード以外でContainerを呼ばないように心がけています。Installer以外、つまりアプリケーションにContainerが入り込んでくるのはコンテナを用いたサービスロケータパターンのような振る舞いをしている恐れがあります。せっかくContainerが何も意識させることなくInjectしてくれてるのに自らConainerに依存するのは勿体ないです。動的な生成を行いたい場合はDiContainer.Instantiateではなく必ずFactoryを生成するようにしましょう。
例外として、ファクトリにContainerを持たせるのはアリかなと思う派です。理由としては依存関係のバリデーション機能が提供されているから使いたい・・というところです。
コードの部分を抜粋すると次の通り
public class CustomEnemyFactory : IFactory<IEnemy>, IValidatable { DiContainer _container; DifficultyManager _difficultyManager; public CustomEnemyFactory(DiContainer container, DifficultyManager difficultyManager) { _container = container; _difficultyManager = difficultyManager; } public IEnemy Create() { if (_difficultyManager.Difficulty == Difficulties.Hard) { return _container.Instantiate<Demon>(); } return _container.Instantiate<Dog>(); } public void Validate() { _container.Instantiate<Dog>(); _container.Instantiate<Demon>(); } } public class TestInstaller : MonoInstaller { public override void InstallBindings() { Container.BindFactory<IEnemy, EnemyFactory>().FromFactory<CustomEnemyFactory>(); } }
Validate
内部での処理は実際には生成されないいわゆるDry-runです。上記の場合、Dog
とDemon
の生成が正しく行えるか依存関係を遡ってチェックしてくれます。
積極的にSubContainerを使う
SceneContextのInstallerに全部書いていくと非常にFatなInstallerになってしまうので、隙あらば積極的にSubContainerに移し替えていきましょう。
個人的にはなんらかのスクリプトがアタッチされているPrefabには全部GameObjectContextを付けてSubContainer運用にしていいくらいまであります。Installerの数がかなり増えるので管理だけは気を付けて・・。
もし、様々な事情が絡み合いSubContainerが使えないという場合は、役割を明確に分離してInstallerを複数に切り分けるのも手です。後にContainerを分割する際に手がかかりにくいです。とはいえ、1つのContainerにめっちゃInstallされるので競合問題は起きうるので気を付けましょう。
Initializeで購読する
初期化をどのタイミングで行うか問題はやや面倒ですが、ZenjectだとIInitializable
を実装しておくとまとめて初期化処理を呼んでくれるので使うことが多いです。
public interface IFooUseCase { void DoFoo(); } public class FooUseCase : IFooUseCase, IInitializable { public void Initialize() { /* なんか初期化 */ } public void DoFoo() {} }
どうせ初期化処理の順番管理するクラスはいずれ必要になるので、Zenjectに任せてしまえるのは楽です。Container.BindInitializableExecutionOrder<FooUseCase>(1)
とかでInitializableの中でもクラスごとに順序は制御できます。
また、ゲームはイベント駆動で動きがちなので何らかのイベントや、UniRxを使っているならストリームを購読することが多いです。Initializeではそれら外部のイベントを購読する処理だけを書くことが多いです。
public class TimeOverUseCase : ITimeOverUseCase, IInitializable { private IGameTimer _timer; public TimeOverUseCase(IGameTimer timer) { _timer = timer; } public void Initialize() { _timer.TimeOverObservable.Subscribe(_ => DoGameEndSequence()); } public void DoGameEndSequence() { /* */ } }
ユニットテストを書くときには上記のように購読とビジネスロジックが明確に分かれていると楽です。このコードの例だと、プロダクションではイベント駆動で勝手に動きますがテストを書くときはInitializeを呼ばずに直接DoGameEndSequenceを呼べるのでユニットテストで手を抜けます。
デバッグ機能はSceneDecoratorContextで切り分ける
例えば、現在の状態を表示したいなあと思ったときに画面にデバッグ用のUIを仕込みたいことが度々あります。
画像で言うPlayerCharacterTurnStart
がそれ。
この辺りは該当のシーンのSceneDecoratorContextを作ると楽です。今回だとBattleシーンの細かいログとか取りたいのでBattleDebugシーンを作ってそこにSceneDecoratorContextを貼ってあげます。
そこに使い捨てでもいいので雑なデバッグクラスを差し込んであげる。
public class DebugInstaller : MonoInstaller { public override void InstallBindings() { Container.BindInterfacesTo<StateChangeNotifyUseCase>().AsSingle(); Container.BindInterfacesTo<StatePresenter>().AsCached(); } }
Zenjectをいい感じに使えているなら、デバッグ時に欲しい情報はDecorateされるシーン側のContainerにBindされているはずなので、サクサクっと画面表示などはできます。 もしインスタンスそのものに手を加えたいとかの場合(例えばデバッグ時だけは実行時間をロギングするとか)は、DecoratorBindingも組み合わせてみると綺麗にやれるのでおススメです。
直接コード書いていくより手間はかかりますが、プロダクションに入れたくないものが絶対に混入しないという安心感は心の平穏を保ってくれます。
おわりに
君だけのZenjectテクニックを教えてくれよな!
技術書典7でZenjectチョットワカルBookを頒布します
9/22の技術書典7にて、Zenjectについてつらつら書いた「ZenjectチョットワカルBook」というのを頒布します。ページは以下。
また、電子版もboothで同日から公開します。
会場では物理本を1000円で頒布します。かんたん後払いシステムをご利用の方は物理本+電子版をお渡しするのでお得です!ぜひかんたん後払いをご利用ください。
何の本なの?
ZenjectというDIフレームワークの基礎的な使い方をまとめた本です。前後篇を想定しており、今回はシーンのDIまで。
- DIとはなんなのか
- BindとInjectionとはなんなのか
- Installerとはなんなのか
- Contextとはなんなのか
- Bindはどんなオプションがあるのか
- Factoryとはなんなのか
- シーンを跨いだDIはどうやるのか
ざっくりと上の部分に対しての解説にはなっていると思います。
対象読者は誰なのか?
残念ながら全くZenjectを使ったことない人にはお勧めできません。そもそもアセットのダウンロードとかを省いちゃってるし・・・。 最低でもZenjectのライブラリがプロジェクトに入ってるところからのスタートになります。また、逆引き辞典にはなりません。ZenjectのAPIは数があまりにも多くそれを列挙するだけでも1冊の本になりそうだったので、使いそうなものだけ列挙しています。4章「様々なBind」では多くのメソッドの説明をしていますがそれも全部ではないのでご注意ください。
完全初心者向けでもなければ上級者向けでもないこの本は、「ネットやドキュメント見ながらなんとなくZenjectでDIしてるんだけどこれでいいのかな?と不安になりながら進んでいる人」に対してほんの少し後押しをするような内容になってます。完全に理解は難しいのでチョットワカルBookです。
最後に
Zenjectは採用事例がじわじわ増えてきているけど、世の中に記事が少ないのもあり「よくわからなくて難しい」という声をよく聞きます。これを機にZenject人口が増えていけば、みんな大手を振ってプロジェクトに導入しようぜ!って話ができるし、僕も仕事がしやすくなりますのでそこんところよろしくお願いしたします。そしてみんなZenject記事書いてください。