ZenjectからVContainerに移行する際に気をつけること
ニッチすぎる小ネタです。
DIライブラリのリプレースなので基本的にはそんなに手間ではないですが、微妙に思想の違いがあるのでちょっとだけ気をつけようねというお話。
基本的な移行の流れ
SceneContext
→LifetimeScope
に乗り換える- Installerのコードを見ながらLifetimeScopeに同様のオブジェクトを登録していく
Zenject.Inject
→VContainer.Inject
に乗り換える- シーン上のオブジェクトは
LifetimeScope
のAutoInjectGameObjects
に登録していく
- シーン上のオブジェクトは
DIコンテナとしての役割は同じなので、スクリプトがそのまま差し替わると理想的です。が、全部が全部とはいきません。
ZenjectにあってVContainerにないもの
この辺は、VContainerがそもそもMonoBehaviourへのInjection自体をあまり推奨していない設計思想だからです。ドキュメントに書かれてたりします。
https://vcontainer.hadashikick.jp/ja/resolving/gameobject-injection
ということで、コードに大きく修正を加えないならば上記2点はどうにかシーンから探し出し AutoInjectGameObjects
に登録していく必要があります。
ZenjectBindingが使われているか調べる
シーン内をひたすら ZenjectBinding
で検索します。Prefabに潜んでいる可能性もあるのでそちらも検索します。がんばる。
ちなみにRiderやReSharper使ってるとクラスからUnityプロジェクト内の参照検索ができるので、ZenjectBindingがアタッチされているPrefabやシーンのGameObjectが洗い出せます。
https://pleiades.io/help/rider/Features_Unity.html#find-usages
シーン内にアタッチされたInject属性のついたコンポーネントを探す
Zenjectはシーン内のInject属性を全検索してInjectionするので追加する分には何も考えなくていいので非常に楽。
その一方で「ヒエラルキー上のどのGameObjectがInjectionの対象なのか」を洗い出したいときに結構手間がかかります。そういう絞り込みができないので・・・。ということで今回は次の方法で調査しました。
- シーンにLifetimeScopeを置く
- おもむろにシーン内のSceneContextを削除する
- 実行したらものすごい数のエラーが出る
- だいたいInjectionされなくなったことによるnull参照エラー
- エラーが出たクラスの
Zenject.Inject
属性にVContainer.Inject
を重ねる
[VContainer.Inject] [Zenject.Inject] public void Construct(Foo foo) {}
- エラーが出たオブジェクトを全部
AutoInjectGameObjects
に登録する Zenject.Inject
を消す
これを1シーンごとにやっては修正する・・を繰り返しましょう。
所感
インジェクションに限らず、特定のルールに沿うだけで自動で設定してくれるという感じの機能は開発速度を向上させる頼もしい仕組みです。しかし、何が自動化の対象になっているのかが把握、またはロールバックする方法もセットで用意されていないと利用者側はただ把握できないだけになってしまうので、技術選定する際はこの辺気をつけたほうがいいな~と思ったのでした。
追記
SubContainerだったりOptionalInjectionだったりとSignalだったりZenjectにしかない機能はいっぱいあるので、その辺まで含めた場合の乗り換えはコードの見直しからが必要なのでもっと大変です。頑張って・・。
とはいえ、そこまでZenjectに乗っかっているならVContainerに移行する必要はないんじゃないとも思う。
UnityでTDDハンズオンRepositoryを作っている
最近更新したのでちょっと宣伝。
なにこれ
最近、副業だったりプライベートなどでUnityを使う人でテストに興味ある方を対象にTDDについてお話する活動などをしています。その際に座学としてお話することもあれば、ハンズオン形式で実際に手を動かして書いてもらうということもあります。その際に使うサンプルリポジトリです。 ぜひお手元で考えながら書いてみてください。勉強会の資料などで使っていただいても大丈夫です。
なぜやってるのか
ゲーム開発はソフトウェア開発の中でもかなり複雑性が高いと感じています。そして性質上変更頻度も高い。ゲームごとに作り方が変わる以上これは仕方ないことだと考えています。だからこそより変化に対応できるようにテスタビリティの高いコードになっていてほしいと考えていますがその難易度、そしてそもそものテストコード文化というものがないためにテストコードが書かれていることは少ないです。 まずはUnity界隈でテストコードを書く、という文化を作っていきたい。そんな気持ちで活動を行っています。
TDDという観点で話をすることもあれば、単純にテストコードの書き方、どこの品質を担保すべきかみたいなトリアージの話をすることもあります。ちなみにE2E側の知識は乏しいです。
課題02について
Unityでよくあるボタンを自作しようというテーマで資料を作ってみました。あ~あるある、と思えるようなストーリーにしています。というか実際僕が経験したことを抽象化したものです。
おもしろポイントはこの辺りです。
- 既存コードを壊さずにどうやって交換していくかの一例がわかる
- 単純な機能のはずなのにケースが増えて複雑化していく過程がわかる
- その際にテストがどのような効果を発揮するのかがわかる
一応、1例としてのコードをunitypackageとして含んでいるので参考までにご利用ください。
もし興味があればTwitterなどでもお気軽にお声がけください。
謝辞
ハンズオン資料を作ったら、いつもUnityゲーム開発者ギルドの方に練習台になってもらっています。毎回お付き合いいただきありがとうございます。
ディガップ!をふりーむで公開しました
東方ゲームジャムで作ったやつに多少の修正を加えてリリースしました。
スコアアタックという性質上、ゲームバランスには手を加えておりません。悪しからず。
10分くらいは遊べるゲームなのでぜひチャレンジしてください。
そろそろサークルのホームページをちゃんと作って作品整列させないとなあと思うんだけど、WEBサイト作るのって面倒だよね。はい。
ちなみに、どういうことを考えて作ったかみたいな話は前回のこちらをどうぞ
東方ゲームジャム2021でゲーム作った
去年に続き今年も参加しました。
割とガチめの全方位型STGです。全国ランキングもあるのでぜひ遊んでみてくださいな。
ゲームができるまで
せっかくなので、元々どういうゲームを考えていて、どういうふうに変わっていったのかをメモがてら書きなぐって行きます。 こういうゲーム開発プロセスもっとみんな公開してほしい。
1日目(8/7)
とりあえず、アイデア出し。正直ここはそんなに時間かからなかった。
テーマ「虹」
-> 虹龍洞
-> そういえば百々世が正々堂々盗掘に来いとか言ってたなあ
-> 魔理沙が虹龍洞で龍珠を掘るゲームでも作るか
-> レッツディガップ!
だいたいここまでテーマ出てから1時間。
ゲームルール初期案
- ショットボタンで弾が出る
- 岩を壊すと点数が入る
- 岩を壊すと撃ち返し弾が飛んでくる
- 弾の種類でバリエーション作れそう
- 無限に岩が生成される
- 岩の耐久値・得点でバリエーション作れそう
- 制限時間内に多く得点を稼ぐ!
このくらいの雑なところから始まって、面白くなりそうなところを詰めていく。
とりあえず弾出るところまで作成
とりあえず弾でも出しとくか!精神でやってる#東方ゲームジャム pic.twitter.com/i7YVfn2sDD
— いも (@adarapata) August 7, 2021
制限時間も付けて、ゲームの開始と終了がある最低限の状態まで持っていった。
最低限のサイクルは一応できた。#東方ゲームジャム pic.twitter.com/5pRnbrvs5L
— いも (@adarapata) August 7, 2021
とりあえずここまでやっとくと精神衛生上とても良い(最悪このままクソゲーとして出せる)
この段階で、「ショットボタンを無くして自動で撃ちっぱなし」という変更を行った。やってて面白かったからなんだけど、改めて噛み砕くと以下の通り。
- 撃ち返し弾しか存在しないので、プレイヤーが弾幕量のコントロールをしやすい
- 撃つ・休むのサイクルでかなり難易度を下げられるがSTG的にそれ面白いか?と遊んでて感じた
- 回避時は向きが変わるので、その方向にも弾を撃ってしまう仕組みにすることで想定してない方向の岩を壊してしまい回避を強いられる
- プレイスキルを上げることである程度コントロールできるから許容できる不自由さっぽかった(自分で触ってみて)
- テンパって動き回って不用意に周りを壊すさまが楽しい
2日目(8/8)
僕のTwitterでの投稿を見たFoRさんがUI周りをお手伝いしてくれることになった。ありがてえ・・・・
画像と背景とBGM当てるだけでちょっとディガップ感でるやつ(BGMは差し替えます)#東方ゲームジャム pic.twitter.com/nmKTW54SPO
— いも (@adarapata) August 8, 2021
ということで岩オブジェクトがめっちゃきれいになった。ここでテンションが上り作業スピードが2倍増しになった気がする。
また、ここで岩の種類が2種類に増えた。それぞれ「拡散弾」と「狙い撃ち弾」を撃ってくれる。
狙い撃ちとバラマキはハッキリと形状を変えてあげたほうが避けやすくて親切だなと思った pic.twitter.com/wsrAFhWQ3i
— いも (@adarapata) August 8, 2021
このあたりで弾幕を避けるに際してストレスになりそうなところをいくつか直していった。
- 狙い撃ちと拡散が同じ形状なのがわからなさすぎるので変更
- バラマキの速度をすべてランダムにしていたが、壊した岩の単位で固定するようにした
バラマキは丸にして、狙い撃ちはこちらを向いていることがわかるように鋭利な形状に変更。 黒色も見にくいので最終的には色ごと変えた。
速度ランダムは今回みたいな無限湧きゲームでやってしまうととてつもない理不尽を感じた。特に後ろから高速の小弾が1個だけ飛んできたら全く見えなかった。 岩から発生した弾はある程度「その岩から発生したグループ」みたいな認識にできると塊として大きく回避できるので、速度周りを調整した。
弾幕が避けられない!と思ったらボムボタンで百々世が弾消し&マインブラストで採掘の手伝いをしてくれるよ。頼りになるね!#東方ゲームジャム pic.twitter.com/eh9ezp02ms
— いも (@adarapata) August 8, 2021
やはりボムが欲しいよね!となったのでどういう実装にしようかと考えたら脳内の百々世が「マインブラストをさせろ」と囁いてきたので現場判断で実装した。作りたいから作ったんだけど、ちゃんとメリットはある
- すべての弾消しができる
- まとめて破壊するのでスコアを一気に稼ぐことができる
ボロボロと岩が壊れていくのはゲーム中で一番気持ちいいポイントになったと思う。ただ、デメリットとして「大量の弾幕が残される」というのが特徴。プレイヤーにお前何してくれてんねんという感情を与えていく。このゲームが被弾したら即終了ならクソゲーだけど、何度でも死んでいいので被弾したタイムロスよりもボムによるスコア回収のが遥かにメリットがあり、みんな笑って許してくれるやろ!くらいの気持ちで実装した。
逆に言えばここで被弾しなかったらドヤりポイントになる。シューターはここに楽しさを見出してくれるんじゃないかなと考えてた。
3日目(8/9)
連休最終日。夏季休暇を取らなかったことを後悔してた。
オンラインのランキング機能も付けたので、三連休で一応最低限遊ぶサイクルはできたぞー。明日から仕事なのでちまちまた肉付けしていこう・・。
— いも (@adarapata) August 9, 2021
あと龍王殺しのプリンセスアレンジがほしい#東方ゲームジャム pic.twitter.com/PA56HV2rsJ
ドヤりといえばランキングだ!ということでランキング機能を実装していた。
また、自機のショットを低速・高速で切り替えるようにした。
- 高速:広範囲の拡散ショット。分散されるので実質威力が低い
- 低速:正面のみの集中ショット。威力が高い
高速のが一度に複数の岩を攻撃できるのでスコア効率が高い。しかし撃ち返し弾も各方向から襲ってくる。低速だと撃ち返し弾のリスクは少ないが正面だけなのでスコア効率が低い。このへんをプレイヤーがコントロールできるようにしてみた。プレイヤーの考えるべきことが増えるが、状況が悪化する原因をユーザーに委ねることでおまえのせいどう改善していけばいいかを考えていけるようにしたかった。
あとこの辺で黄金岩を導入した。この時点では黄金はレア枠で、通常の岩より出現確率が低く得点が多いというポジションだった。なんとなく作ったよくあるレアアイテム。
4日目(8/10)
お仕事で一回休み
5日目(8/11)
お仕事で夜に作業。
遊んでて、これスコア狙いなら全方位壊す必要なくて、一方向だけ狙ってたら良くないか?となった。他の箇所を攻撃するメリットがない。
ということで泣きながらissueを立てた。
さらにこのタイミングでスコアラー的にどうなの?という問題と向き合うことになった。
スコアラーと乱数
自分の考えとしては、スコアアタックしてて一番つらいのは運要素が絡むところで結果が出ないことだと思う。多分繰り返す気になれない。ディガップだと次の2点とか。
この辺を解決するために、ルールが分かってくればある程度コントロールできるような仕組みに切り替えた
- レア岩はスポーン位置ごとに個数を定める
- 掘れば掘るほどレア岩が出る確率は下がる
- 同じ箇所を攻め続けたら最終的には枯渇する
- 百々世のボムは自機狙い弾3wayにする
- 意図的に金の多い位置を爆破できる
このあたりを実装して、スコアのばらつきを減らせるようにトライアンドエラーしてみた。
とはいいつつ、スポーン時の岩の大きさに関しては調整間に合わなかったごめんなさい(レア大岩が出るかどうかが結構重要になってしまっている)
6日目(8/12)
お仕事。
面白いのかわからん期に突入した
— いも (@adarapata) August 11, 2021
なんもわからん期に突入した。
レア岩の登場と枯渇の概念で一点集中プレイがなくなるかと思いきや、通常岩でも得点入るからレアを探すより無心で掘り続けたほうが概ねお得になってしまった。ここの得点と確率の調整をするのがめっっっっっちゃめちゃ大変そうな臭いがした。
なので、このタイミングで「レア岩のみ得点が入る」というルールに切り替えてみた。
- レア岩を見つける
- ゲットするためにその方向を掘る
- 採りきったら次の場所を掘る
動き回る動機としては十分だし、「金を掘れ!」だからやることがシンプルになりそうだった。最終的にこのルールがしっくりきたのでこれでいくことにした。
とはいえ、これでも1箇所ずつ丁寧に掘るプレイは健在なので、もう弾幕が多い事自体にインセンティブが入るようにしようかと考えた。ということで弾幕の量に応じて「危険度」が上下するように機能を追加した。
画面が弾幕だらけになるほど危険度が上がっていきます。しかし危険度が高いと龍珠の得点が上がるので、腕に覚えのある盗人は常に危険度を高めて採掘するそうです #東方ゲームジャム pic.twitter.com/hWh0ayY28l
— いも (@adarapata) August 12, 2021
危険度が高いほど取得したときのポイント自体に倍率がかかるようになっている。これにより意図的に壊して回ったほうがスコアとしてはお得になる。おまけに百々世のボムを使ったときは確実に危険度MAXなのでボーナスタイムとも言える。後のことはしらない。
また、これによりプレイヤーが被弾したときのデメリットに「危険度がリセットされる」が追加された。通常のプレイなら嬉しい限りだけどスコア狙う人には辛い。
とはいえ危険度上げっぱなしだと難易度が上がる一方なので、高速と低速で岩を破壊したときの撃ち返し弾の量を調整した。低速で破壊すると通常の半分以下の弾幕に抑えられるので、低速で動き続ければゆっくりと危険度は下がっていくようにしてみた。
前日にここまで変えたのはやっちゃった感あるけど、作業ゲーにはならなくなった気がする
最終日(8/13)
ひたすら微調整とバグ取りとテストプレイ。このタイミングでMac版は背景がバグることに気づき断念した。
あとは、操作はシンプルだが思ったよりテクニック的な部分が増えてきたのでそれをプレイヤーに伝える手段が必要そうになった。とはいえガッツリとしたチュートリアルを作る時間はもうない・・・。
ということで、リトライ画面で百々世にTipsを喋らせるようにしてみた。
意味のない会話も含めて8パターンくらい用意した。
余談ですが僕の幻想郷では百々世は飯綱丸のことを「龍」って呼びます。
ということで18:30くらいに無事投稿。ちゃんとリリースできました。
お疲れさまでした~
— いも (@adarapata) August 13, 2021
魔理沙が虹龍洞で龍珠をディガップする全方位型STG作りました。ゲームパッドも使えます。
21時に公開されるのでぜひ遊んでください~
#東方ゲームジャム pic.twitter.com/7AShB6kqvT
所感
今年は1週間あったからかどのゲームもレベルが高かった。そして個性が出てて面白い。好き放題やってるなーーー感があって最高だった。
個人的に好きだったやつを2つほど。
「天弓千亦服飾店」 touhou-gamejam.web.app
千亦に服着せるゲーム。意外とキャラ覚えてねーーってなったしヘカちゃんがいいです。
「モリヤリズム RAINBOW」 touhou-gamejam.web.app
リズムゲー&STGでやってて楽しかった。小傘に10回くらいやられました。
また来年もやれるのを楽しみにしてます。
おまけ
後戸エディッションです
摩多羅隠岐奈に後戸経由で弾をぶつけられる「ディガップ!後戸エディッション」も並行して作ってましたがあまりにも隠岐奈が無関係すぎるのでお蔵入りとなりました。 #東方ゲームジャム pic.twitter.com/RZ9NhihzWv
— いも (@adarapata) August 13, 2021
【GodotEngine】GutのDouble周りの使い方
Gut
GodotEngineではGutというユニットテスト用のプラグインがあります。 超絶便利です。
gutのアサーションなど基本的な使い方はとても良くまとめられた記事があるのでそちらを参照してください。
今回はGutでテストダブルの方法だけざっくりと紹介します。
テストダブル
テストダブルは、テストを行う際にテスト対象が依存するコンポーネントを利用せずに代わりのコンポーネントを利用してテストする手法です。例えばテスト対象が通信を行うコンポーネントに依存していた場合、そのままテストを実行すると毎回通信が走ることになり結構困ります。Twitterと通信するオブジェクトだったらテストのたびにツイートしたりするかもしれません。しかしテストは書きたい。そのような場合に依存コンポーネントを何らかの手段ででっち上げて、かつそれっぽい返り値を返してくれるようなダブル(代役)を立てるのがテストダブルです。
テストダブルは、その中でも役割によって名称が変わってきます。
- DummyObject
- Test Stub
- Test Spy
- Mock Object
- Fake Object
詳しく知りたい場合は、xUnit Test Patternsを読むとよいでしょう。
xUnit Test Patterns
GutでのDouble
じゃあGutでどうやってテストダブルするの?という話です。
基本形
var foo_double : Foo = double(Foo).new() # var foo_script = load("res://path/foo.gd") 型を使わない場合 # var foo_double : Foo = double(foo_script).new() # foo_double : Foo = partial_double(Foo).new() # 一部分だけのdouble
double(Foo).new()
でインスタンスが生成されます。この状態のインスタンスはメソッドをコールしても何もしてくれないし、何も返してくれません。ただただインスタンスがあるだけです。
逆にpartial_double(Foo).new()
は、この状態だと本来のインスタンスと全く同じ挙動のdoubleを生成します。こちらは後者のstubを行って特定のメソッドだけをでっち上げるときに使います。
ちなみに、ビルトインのクラスに対してもdoubleが可能です。
var double_node : Node = double(Node).new()
全部とは言わないがだいたいできるらしい。 https://github.com/bitwes/Gut/wiki/Doubles#what-do-i-do-with-my-double
Stub
都合のいい値を返してくれるようなダブルを「スタブ(stub)」と呼びます。
gutではstubメソッドで、特定のメソッドが呼ばれたときの返り値をでっち上げることができます。
var foo_double : Foo = double(Foo).new() stub(foo_double, "get_foo").to_return("foo") # get_foo() が呼ばれたら "foo" を返す gut.p(foo_double.get_foo()) # => "foo"
ちなみに引数ありのメソッドの場合は、特定の引数のときのみ返すという絞り込んだstubが行えます。
stub(foo_double, "get_foo_by_arg").to_return("foo").when_passed(10) # 引数が10で呼ばれたときのみfooを返す gut.p(foo_double.get_foo_by_arg(10)) # => "foo" gut.p(foo_double.get_foo_by_arg(50)) # => NULL
when_passed
メソッドでパラメータまで指定できます。
テスト対象にstubしたオブジェクトを渡すことで、対象が受け取るデータをこちらでコントロールできるようになり、テストケースの前提条件を用意しやすくなります。
Spy
テスト対象の処理を経由して、特定のインスタンスのメソッドがコールされたか?という観点で検証を行いたいときがあります。そのようなダブルを「スパイ(spy)」と呼びます。
var bar : Bar = double(Bar).new() var foo := Foo.new(bar) foo.do_foo() # この中でbar.do_bar()が呼ばれていてほしい assert_called(bar, "do_bar")
assert_called
は指定したオブジェクトのメソッドが呼ばれているかを検証します。例の場合はbar.do_bar()
が1度でも呼ばれていればOKです。
引数の検証まで行う場合は次のように記述します。
assert_called(bar, "do_bar_with_parameter", [10]) # do_bar_with_parameter(10) と呼ばれたか
ユースケース
簡単な例ですが、シューティングゲームっぽいケースを考えてみます。自機が存在して、ショットボタンを押したら弾が出る、といった感じです。
登場人物は次の3人です。
- MyGamePad: 入力を待つクラス
- BulletEmitter: 弾を生成するクラス
- Player: 上記を扱うクラス。今回のテスト対象
my_game_pad.gd
extends Reference class_name MyGamePad func is_shot() -> bool: return Input.is_action_pressed("ui_accept")
bullet_emitter.gd
extends Node2D class_name BulletEmitter func shot(direction_degree : int): # 弾が生成されるなにか pass
player.gd
extends KinematicBody2D class_name Player var _pad : MyGamePad onready var emitter : BulletEmitter = $Emitter func _init(pad : MyGamePad): _pad = pad func _process(delta): # ショットキー押してたら弾を出す的な if _pad.is_shot(): emitter.shot(rotation_degrees)
「ショットボタンが押された場合、弾を生成する」というテストケースを考えてみましょう。このテストを書くにあたって2つちょっと面倒ポイントが発生します。
- ショットが押されたという前提条件をどうやって用意するか
- 弾が生成されたことをどうやって検証するか
- godotだったらnodeの数数えてできないこともないが今回はやめておく
前者はstubを使ってでっち上げれそうです。後者はできないこともないですが、具体的な生成処理はBulletEmitter側の責務なので、Emitterにメッセージを送れたかという検証を行ってもよさそうです。ここはspyが使えそうです。
例えば次のように書けます。
test_player.gd
extends "res://addons/gut/test.gd" func test_player_shot_if_press_shot_button(): var game_pad : MyGamePad = double(MyGamePad).new() var emitter : BulletEmitter = double(BulletEmitter).new() var player := Player.new(game_pad) add_child_autofree(player) # nodeをtreeに追加 player.emitter = emitter stub(game_pad, "is_shot").to_return(true) # キーが押されたことにする gut.simulate(player, 1, .1) # 擬似的に1フレーム進行させる assert_called(emitter, "shot", [player.rotation_degrees]) # emitterが呼ばれたことを検証する
Playerの依存しているMyGamePadとBulletEmitterをdoubleにして渡しています。プレイヤーの入力はテスト中は取得できないので、game_pad.is_shot()
をスタブすることで擬似的に「キーが押された」という状況を作り出しています。
BulletEmitterはshotをコールされても何も行いませんが、assert_called
で呼ばれたのか、パラメータは適切なものが渡されたのかを検証しています。実際にどういう風に弾を出すのかはBulletEmitter側でテストを書くとよいでしょう。
また、gut.simulate
は擬似的にフレームの進行を再現するメソッドで、今回は1フレームとdelta_timeとして0.1秒を渡しています。これでplayerの_process(delta)
を呼び出しています。
ちなみにadd_child_autofree
はGut専用のノード追加メソッドです。このメソッド経由でaddするとGut側がテスト終了後によしなに開放してくれます。むしろ普通にadd_child
すると破棄まで自分で管理する必要があるので、特に理由がないならadd_child_autofree
を使うようにしましょう。
所感
動的言語だけあってdouble周りがかなりやりやすいという印象です。継承もいらないし。 とはいえdoubleしまくって本来のテストケースを正しく検証できなくては元も子もないので、ここぞというときに使うとよいでしょう。gut.simulateのおかげで割と挙動の再現しやすいし。
2020年ふりかえり
あっという間の1年だった。
今年やれたこと
リモートワーク
自分の意志でチャレンジしたというか、コロナ情勢下での新しい働き方が生まれたという感じ。 個人的にはリモートは凄くやりやすい。ただコミュニケーション問題をどう解決していくかは常に考えてかないといけないと思う。 個人的にはセカンドライフなりの空間でコミュニケーションとれると最高なんじゃないかと考えている
スクラムマスター
仕事では、4月からクライアントエンジニア兼スクラムマスターとしてあれやこれやとやっておりました。元々スクラムそのものには興味がありモチベーションとしては高かったものの、実際にロールとしてスクラムマスターを担ったときに自分が何をすべきかというのは結構悩みが多く、書籍を漁ったり人と会話するなどして模索しています。 幸いにも自分の部署はスクラムマスターが自分含めて3人ほどおり、スクラムマスター同士で情報を共有したり雑な相談などを定期的に行えるようにしていたおかげで精神的な負荷がかなり軽減されました。 この辺はなんか別で書きたいところ。
副業
副業で、某会社さんでUnityでテストを書いていくお手伝いをさせていただいてます。ハンズオンしたり、実際のコードを見ながらテストを書いて問題を見つけたりなどなど。テストを書くというか、基本はTDDをやって行けるチームにしましょうという目的でお手伝いしてます。 初めての副業で且つ、あんまり他でやっていることではないのでお互いに模索している段階ですが、今のところは継続させていただいてます。 メンバーの方々は自分より全然キャリアが長い方々なので僕自身も学ばせてもらっています。みんなモチベーションと技術力が凄い。
今年やれなかったこと
アウトプットの減少
コロナで勉強会系イベントが減ったということはあるけど、それを加味してもかなり少なくなったなあという感覚。
ゲーム開発時間の減少
全然ゲーム作ってねえ!ゲーム作りてぇ!
技術記事を書くのも大事だし、執筆して知の高速道路を作っていくのは超大事なんだけど、そもそも完成させるサイクルを増やしていかないと非常に危ないと思っている。自分の持っている知識がよいものとして使えるかどうかはやっぱり実際に使うしかない。それはAPIを触りましたではなく、作り切ってリリースすること。最近だとよく設計論を考えるのだけど、それこそ作るゲームのドメイン領域を突き詰めないと何がベターかを考えることは非常に難しいと思う。つまり、設計が綺麗だけど面白くないゲームというものじゃそもそも失敗なんじゃないかと考えるようになりはじめた。じゃあ面白い面白くないの評価ってなんぞとなるとそれは出すしかないのでリリースしていくべき。で、現状はリリースしてもないのに知識だけを出していっても仕方ないんじゃないか?と思い始めた年なのでした。
今年もお疲れ様でした
正直、とても自分の将来が不安になる一年でした。 まあなんとかやっていきましょう。
よいお年を
UnityでTDDハンズオンしたお話
Unityゲーム開発者ギルド2 Advent Calendar 2020 24日目のエントリです。2時間オーバーしちゃったごめんなさい
今月の頭くらいにUnityゲーム開発者ギルドの人たちを対象にTDDハンズオンをやってみました。
何故やったのか
TDDというのはなんとなくわかったのだけど、Unityでのゲーム開発を行うときにどう始めたらいいのかわからないということが最近あったのでどうしようかと悩んでいて。 FizzBuzzをTDDでやってみようなどは既にいっぱいあるけどやはりUnityというものと直接紐づいた事例がないので、もっと現実にありそうなテーマでTDDハンズオンをやってみようと考えたのでした。 何度も繰り返してブラッシュアップしていきたいなと思ったのでどこかで話を聞いてくれる人がいないかなあとギルドで募ったら意外とみなさん手を挙げてくださったので、感謝の正拳突きをしながらzoomでTDDハンズオンをしたのでした。
その時の画像。参加者の方にはモザイクをかけています。僕はアバターで参加しました。
二日連続でやったのですが二回参加された方もいて圧倒的感謝。 2時間超えの長丁場にお付き合いいただきありがとうございました。
UnityでのTDDは特殊なのか
特に何も変わらないと思っています。MonobehaviourがUnityレイヤにべったりで書きにくいというのはその通りですがそれはAndroidのActivityも大体そんな感じだしWebにおけるViewレイヤもそうであるのでUnityが特殊ということは無いでしょう。
どんなことをしたのか
名前変更機能を作ろう
ユーザーが自分の名前を変更できるようにしたい、というストーリー
- 開いたときに画面に現在の名前を表示させておきたい
- ユーザが入力フォームから入力できるようにしたい
- 名前はアルファベットのみで1~8文字以内にしたい
- 変更に成功したら変更成功したことを表示したい
- 変更に失敗したら変更成功したことを表示したい
- 現在の名前が変更後の名前に変わってほしい
- 名前の変更が保存されててほしい
画面には、自作コンポーネントの貼られていない、それっぽく作ったUIだけを添えています。
一切スクリプトがない状態か、これを実装していこう!みたいなストーリーです。
入力と加工と出力
要件は一杯あるので、まずはざっくりと入力と加工と出力に分類してみる。 基本的には出力はテストが難しい。出力はプラットフォーム固有の機能を利用することが多いのでテストに組み込みにくい。ログが吐かれていることをテストするのは若干骨が折れる。また、正解を定義しにくいのもある。「ゲームクリア!」と表示されていればいいのか?「GameClear!」になるとテストとして間違っているのか?みたいなところ。
加工部分は一番簡単である。何らかのインプットをすることで何らかのアウトプットが返ってくることを期待するのはプログラミングの基本だから定義しやすい。
入力
- ユーザが入力フォームから入力できるようにしたい
加工
- 名前はアルファベットのみで1~8文字以内にしたい
出力
- 開いたときに画面に現在の名前を表示させておきたい
- 変更に成功したら変更成功したことを表示したい
- 変更に失敗したら変更成功したことを表示したい
- 現在の名前が変更後の名前に変わってほしい
- 名前の変更が保存されててほしい
と思ったら出力ばかりでつらいですね。みたいなところから始まるTDDハンズオンです。
伝えたかったこと
大体この2点です
- 思考を書き出す
- 不安になったら書きましょう
大きな問題に大きいまま挑むとだいたい返り討ちに合います。メインのゲームを実行できるようにするぞと意気込むと、GameManagerでGodなClassが生まれて無限に連なるゲームを開始するメソッドが生えるでしょう。ゲームを実行するという言葉の解像度を少し上げてみると実はいろんな意味が含まれていたりします。
- 利用するアセットをロードする
- 敵キャラクターの生成
- プレイヤーの生成
- ゲームタイマーの設定 etc...
コードを書くその前に、対象を観察して問題を細かく分割していくことは大事です。これらは特別なスキルではなく、皆さん割と脳内で無意識に考えているはずです。その結果なんらかのクラスが生まれたりメソッドが生えたりするわけなので。その無意識に行っている分割と思考の整理をテストケースとして書き出すといいのではないかなと考えてます。テストを書くために特別な思考をというよりも、普段の脳内を垂れ流したらテストできてたみたいなイメージ。これを繰り返していくと手癖で書けるようになるのかなあとは思ってます。
不安になったら書きましょうはかなり抽象的な話ですが、自分自身こうしているので・・。すくなくとも全てのコードにテストを書く気はないしテストファーストじゃないといけないことはないです。なんかちょっと怖いな~と思ったら書きます。 怖いなー不安だなーをもっと具体的にするなら、コードの循環的複雑度が高そうなら書くといいのかなと思います。一切のif文のないまっすぐなコードを書くときに不安を感じる人はあまりいないでしょう。
また、不安の元をきちんと整理することも大事です。不安でテストを書きたいけどUnityが提供しているAPIが挟まってて書きにくいという状況もままあるでしょう。しかし一旦整理すると、その不安要素にUnity関係ある?みたいなこともあるかもしれません。僕はTransform.SetParentが正しく動くか不安だと感じたことはあまりありません。そこは想定したインプットをすれば想定したアウトプットをしてくれると期待しているからです。(もし期待通りに来なかったらUnity ForumにPostしましょう)
でも、Transform.SetParentの呼び出しに至るまでに自分で書いたロジックが大量に挟まっているなら不安になるかもしれません。その際は分離をしてみるか、みたいなアプローチを考えて書き直すのもアリかもしれません。
余談ですが、1メソッド単位でテスト書くのは僕は推奨しません。テストを書くためにカプセル化された機能が表に出るのは勿体ないし、メソッド名の変更だけでも修正の恐れがあるのはちょっと辛いからです。メソッドのテストを書くのではなく期待している振る舞いに対してテストを書く方が好きです。
反省
前半は簡単なテストから始めていったけど、後半は出力に近い領域のテストを書くためにinterfaceを挟んでモックを作るなどして一気に加速したのでそこからついていけないということが起きてました。この辺は今後改善していきたいところ・・・。
あとは、題材とした名前変更機能は昨今のスマホゲームなら確かにあり得るテーマだけど、リアルタイムなレスポンスがあったり、オブジェクト同士がぶつかり合ってメッセージングし合うとかいわゆる「ゲーム」みたいな場面とは少し離れているなぁと思った。なのでそのあたりをケアできるといいなあと考えている。
おまけ:欲しいのはテストかイテレーションか
ゲーム開発でテストコードを書くときに度々話題になることとして、Viewのテストが書きたいというモチベーションとの向き合い方があると思う。ここでいうViewは実際に画面に情報を出力する役割を持つクラスと定義する。 前述したが、出力に近い部分のテストは大抵書きにくい。
- 正常系の定義を定めにくい
- 変更頻度が多いのでメンテナンスコストが高い
- 出力部分の機能はテスト上で動かないことが多い
こういう理由があって割に合わないことが多いから自分はあまり出力部分をテストコードでカバーしたいと考えておらず、エディタとか実機で見ようぜ!みたいな考えがあるんだけどやっぱり声は聞いたりする。果たしてその気持ちはどこから溢れてくるのかというのを聞くと、こういうものがあった。
- 毎回実行して任意の画面まで遷移するコストが高い
コードを書いてPlayMode再生して、ログインしてホーム画面を通って目的の画面へえんやこら。これがアプリケーションの成長につれて時間がかかるようになる。なのでPlayせずに見たいし調整したい。テストコードでなんとかしたい!
凄く気持ちは伝わったけど、多分これはトライアンドエラーのイテレーション速度を上げたい欲求の話で、そこに対してテストコードで正常系を担保というのは目的と合ってなくて難易度が高そうだなと感じた。そもそもトライアンドエラーでいい感じに画面調整するために毎度毎度テストコード書き直すの辛いし。 これの実際の問題は直接画面を開くことができない設計になっていることで、何故直接開けないかというとその画面に必要な情報だけじゃなくてログインしたユーザデータとかその他諸々がシングルトンに鎮座していることが多いから。キャラクタのフレーバーテキストを見る画面でもユーザのインスタンスがないといけないとか稀に良くある。
そのあたりを改善するには、外から最低限のパラメータ流し込むだけで画面が単体で立ち上がるみたいなモジューラビリティの高い設計にしないといけなくて、それはコードを書き直していくしかなさそう。難易度が高いのはよくわかる・・。
なので、Viewのテストが書きたいとなったときは一旦立ち止まって見るのがいいなと思いました。
おまけのおまけ
Unityゲーム開発者ギルドでTDDハンズオンしたら欲しいものリストからどんどん送られてくるようになって人間の温かみを感じている
— いも (@adarapata) December 5, 2020
ありがたや… pic.twitter.com/vVAYV6vG9q
— いも (@adarapata) December 8, 2020
人間の温かさに包まれました