【感想】メイドインアビス 闇を目指した連星
Steamで購入
とりあえず月笛になったので第三階層を今から潜ろうというくらいの状態。 Steam評価が賛否両論になってるのは、だいたい同意。
楽しいところ
探窟家ライフを満喫できる
DeepAbyssは完全オリジナルストーリーで、主人公が見習いから白笛を目指して探窟家として成長していく物語となっている。ちゃんとアビスの世界の探窟家として、遺物を集め、依頼をこなしてどんどん探索範囲を広げていくのは冒険している感がある。アビスがかなり広いので、あちらには何があるんだろうと探索するだけでも結構楽しい。第三階層まで行ったけど、実は第一階層もあんまり探索できていないフロアが未だに残ってたりする。
シビアなローグライク
原作だとリコたちはもうラストダイブ前提なので戻ってくる気はさらさらない感じだったけど、主人公は普通の探窟家なのでアビスで遺物を集めてちゃんと街に帰ってこないといけない。なので常に退路を確保しつつ、食料は十分か、武器は足りているかなどを気にしながら潜っていくのはシビアながらも楽しい。特に持ち運べるアイテムの重さには上限があるので、遺物をひたすら回収していたらすぐに動けなくなるということもままある。このあたりもレベルが上っていけばスキルツリーが開放されて自由度が上がり、戦略の幅が広がって気持ちが良い。
リソース管理が大事な直球のローグライク+アクションという感じなので、ダンジョンに潜って素材集めてビルドしていくみたいなのを繰り返すのが好きな人は楽しめそうな気がする。
気になるところ
QTE
特定の大型生物は倒したあとにQTEイベントが発生して、そこで正しく入力できたら撃破。失敗したら敵のHPが多少回復され、再度HPを0にする→QTEの流れを繰り返す。
これが特定のイベント戦だけならいいんだけど、普通に野生で何度も出てくる大型生物を倒すたびに発生するのでめっちゃテンポ悪い。というかなんでQTEなの・・。
上昇負荷
アビスの呪いもきっちり再現されており、ゲーム中では高いところに移動し続けると上昇負荷が発生し、満腹度を減少させたり幻覚や視界を奪ったりと階層に応じたペナルティが与えられる。
リコたちは常に下っていくからよかったけど、プレイヤーは帰るまでが冒険なので確実に上昇負荷を受けることになる。カートリッジはなさそう。
ただ、上昇負荷は発生の予兆が見えるようになっているので、上昇負荷が起きそうになったら一旦止まるか下れば一定時間で正常な状態に戻る。なので、帰りは登る→休憩→登る→休憩を繰り返せば基本的には上昇負荷は起きない。
なので、実際はそこまで上昇負荷を受けることはなく、結果的にテンポが少し悪いかな・・?くらいの立ち位置になっている気がする。これはもしかしたら第三階層以降でもっとエグいのかもしれない。
無限湧き
いくつかの雑魚敵は倒しても結構な速度で敵がリスポーンする。だいたい3秒くらい。なのでその場で留まって食事したりしようとしたらリスポーンした敵に小突かれてキャンセルされがちでだいぶストレスが溜まる。
倒したら素材を落とすので稼げると前向きに捉えられないこともないけど、このゲームは武器に耐久値があるのでずっと相手してたら武器が壊れる方が早くジリ貧になる。その上、ロープや壁を登っている状況でも平然と虫と鳥が無限に湧いて小突いてくる。崖を掴むあたりで小突かれようものならそのまま勢いで落下死。ハンターハンターで試験会場の壁面を下りようとしたら怪鳥にやられたロッククライマー(試験番号86番)の気分になれる。
ストーリーモードというチュートリアル
最初からオリジナルモード(DEEP IN ABYSS)を遊ぶことはできなくて、先に原作ストーリーを追体験するHELLO ABYSSをクリアする必要がある。こちらはゲームのチュートリアルという側面もあるから色々違うところがある。
- NPCとしてレグがいる(つよい)
- スキルツリーがない
- 武器の耐久値がなく壊れない
- 途中から街に戻れない
レグはポンコツなので「この敵は強敵なのでしゃがんでやりすごそう!」というチュートリアルメッセージが出たあとに躊躇なく敵に殴りかかってた。しかもそのまま倒してしまうという度し難い脳筋っぷりを発揮してくれる。
レグもいるし武器が壊れないのでそれだけでリソースの心配がないし、楽といえば楽なんだけど、スキルツリーもないし育成的な要素がなく単調になりがち。更に、街に戻れないけど遺物はガッツリ出てくるのと、取得時点だと合成できないなど不要となるアイテムが多い。しかも説明がないのでプレイヤーはそれが不要かどうかも判断できない。なので知らないアイテムが出てきても「?」を浮かべながら遊び続けることになる。
そして、普通に遊んで4時間くらいかかった・・
これがチュートリアルという説明すらないので、この時点で離脱した人多いんじゃないのかという気になった。
所感
いわゆるトルネコやシレンといった「裸一貫でサバイバル」という遊び方ではなく、きちんと事前に街で整えていきましょう、最悪道中でサバイバルしましょう、くらいの温度感のゲームだと思う。良くも悪くもアビスの恐ろしさが反映されたものになっていて非常に度し難い。
とはいえ文句言いながらずっとアビスに潜っているので割りと中毒性はありそう。憧れは止められねえんだ。
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を触りましたではなく、作り切ってリリースすること。最近だとよく設計論を考えるのだけど、それこそ作るゲームのドメイン領域を突き詰めないと何がベターかを考えることは非常に難しいと思う。つまり、設計が綺麗だけど面白くないゲームというものじゃそもそも失敗なんじゃないかと考えるようになりはじめた。じゃあ面白い面白くないの評価ってなんぞとなるとそれは出すしかないのでリリースしていくべき。で、現状はリリースしてもないのに知識だけを出していっても仕方ないんじゃないか?と思い始めた年なのでした。
今年もお疲れ様でした
正直、とても自分の将来が不安になる一年でした。 まあなんとかやっていきましょう。
よいお年を