imog

主にUnityとかの事を書いています

UnityでTDDハンズオンRepositoryを作っている

最近更新したのでちょっと宣伝。

github.com

なにこれ

最近、副業だったりプライベートなどでUnityを使う人でテストに興味ある方を対象にTDDについてお話する活動などをしています。その際に座学としてお話することもあれば、ハンズオン形式で実際に手を動かして書いてもらうということもあります。その際に使うサンプルリポジトリです。 ぜひお手元で考えながら書いてみてください。勉強会の資料などで使っていただいても大丈夫です。

なぜやってるのか

ゲーム開発はソフトウェア開発の中でもかなり複雑性が高いと感じています。そして性質上変更頻度も高い。ゲームごとに作り方が変わる以上これは仕方ないことだと考えています。だからこそより変化に対応できるようにテスタビリティの高いコードになっていてほしいと考えていますがその難易度、そしてそもそものテストコード文化というものがないためにテストコードが書かれていることは少ないです。 まずはUnity界隈でテストコードを書く、という文化を作っていきたい。そんな気持ちで活動を行っています。

TDDという観点で話をすることもあれば、単純にテストコードの書き方、どこの品質を担保すべきかみたいなトリアージの話をすることもあります。ちなみにE2E側の知識は乏しいです。

課題02について

Unityでよくあるボタンを自作しようというテーマで資料を作ってみました。あ~あるある、と思えるようなストーリーにしています。というか実際僕が経験したことを抽象化したものです。

おもしろポイントはこの辺りです。

  • 既存コードを壊さずにどうやって交換していくかの一例がわかる
  • 単純な機能のはずなのにケースが増えて複雑化していく過程がわかる
  • その際にテストがどのような効果を発揮するのかがわかる

一応、1例としてのコードをunitypackageとして含んでいるので参考までにご利用ください。

もし興味があればTwitterなどでもお気軽にお声がけください。

twitter.com

謝辞

ハンズオン資料を作ったら、いつもUnityゲーム開発者ギルドの方に練習台になってもらっています。毎回お付き合いいただきありがとうございます。

unity-game-dev-guild.github.io

ディガップ!をふりーむで公開しました

東方ゲームジャムで作ったやつに多少の修正を加えてリリースしました。

www.freem.ne.jp

スコアアタックという性質上、ゲームバランスには手を加えておりません。悪しからず。

10分くらいは遊べるゲームなのでぜひチャレンジしてください。

そろそろサークルのホームページをちゃんと作って作品整列させないとなあと思うんだけど、WEBサイト作るのって面倒だよね。はい。

ちなみに、どういうことを考えて作ったかみたいな話は前回のこちらをどうぞ

adarapata.hatenablog.com

東方ゲームジャム2021でゲーム作った

去年に続き今年も参加しました。

touhou-gamejam.web.app

割とガチめの全方位型STGです。全国ランキングもあるのでぜひ遊んでみてくださいな。

ゲームができるまで

せっかくなので、元々どういうゲームを考えていて、どういうふうに変わっていったのかをメモがてら書きなぐって行きます。 こういうゲーム開発プロセスもっとみんな公開してほしい。

1日目(8/7)

とりあえず、アイデア出し。正直ここはそんなに時間かからなかった。

テーマ「虹」

-> 虹龍洞

-> そういえば百々世が正々堂々盗掘に来いとか言ってたなあ

-> 魔理沙が虹龍洞で龍珠を掘るゲームでも作るか

-> レッツディガップ!

だいたいここまでテーマ出てから1時間。

ゲームルール初期案

  • ショットボタンで弾が出る
  • 岩を壊すと点数が入る
  • 岩を壊すと撃ち返し弾が飛んでくる
    • 弾の種類でバリエーション作れそう
  • 無限に岩が生成される
    • 岩の耐久値・得点でバリエーション作れそう
  • 制限時間内に多く得点を稼ぐ!

このくらいの雑なところから始まって、面白くなりそうなところを詰めていく。

とりあえず弾出るところまで作成

制限時間も付けて、ゲームの開始と終了がある最低限の状態まで持っていった。

とりあえずここまでやっとくと精神衛生上とても良い(最悪このままクソゲーとして出せる)

この段階で、「ショットボタンを無くして自動で撃ちっぱなし」という変更を行った。やってて面白かったからなんだけど、改めて噛み砕くと以下の通り。

  • 撃ち返し弾しか存在しないので、プレイヤーが弾幕量のコントロールをしやすい
  • 撃つ・休むのサイクルでかなり難易度を下げられるがSTG的にそれ面白いか?と遊んでて感じた
  • 回避時は向きが変わるので、その方向にも弾を撃ってしまう仕組みにすることで想定してない方向の岩を壊してしまい回避を強いられる
    • プレイスキルを上げることである程度コントロールできるから許容できる不自由さっぽかった(自分で触ってみて)
    • テンパって動き回って不用意に周りを壊すさまが楽しい

2日目(8/8)

僕のTwitterでの投稿を見たFoRさんがUI周りをお手伝いしてくれることになった。ありがてえ・・・・

ということで岩オブジェクトがめっちゃきれいになった。ここでテンションが上り作業スピードが2倍増しになった気がする。

また、ここで岩の種類が2種類に増えた。それぞれ「拡散弾」と「狙い撃ち弾」を撃ってくれる。

このあたりで弾幕を避けるに際してストレスになりそうなところをいくつか直していった。

  • 狙い撃ちと拡散が同じ形状なのがわからなさすぎるので変更
  • バラマキの速度をすべてランダムにしていたが、壊した岩の単位で固定するようにした

バラマキは丸にして、狙い撃ちはこちらを向いていることがわかるように鋭利な形状に変更。 黒色も見にくいので最終的には色ごと変えた。

速度ランダムは今回みたいな無限湧きゲームでやってしまうととてつもない理不尽を感じた。特に後ろから高速の小弾が1個だけ飛んできたら全く見えなかった。 岩から発生した弾はある程度「その岩から発生したグループ」みたいな認識にできると塊として大きく回避できるので、速度周りを調整した。

やはりボムが欲しいよね!となったのでどういう実装にしようかと考えたら脳内の百々世が「マインブラストをさせろ」と囁いてきたので現場判断で実装した。作りたいから作ったんだけど、ちゃんとメリットはある

  • すべての弾消しができる
  • まとめて破壊するのでスコアを一気に稼ぐことができる

ボロボロと岩が壊れていくのはゲーム中で一番気持ちいいポイントになったと思う。ただ、デメリットとして「大量の弾幕が残される」というのが特徴。プレイヤーにお前何してくれてんねんという感情を与えていく。このゲームが被弾したら即終了ならクソゲーだけど、何度でも死んでいいので被弾したタイムロスよりもボムによるスコア回収のが遥かにメリットがあり、みんな笑って許してくれるやろ!くらいの気持ちで実装した。

逆に言えばここで被弾しなかったらドヤりポイントになる。シューターはここに楽しさを見出してくれるんじゃないかなと考えてた。

3日目(8/9)

連休最終日。夏季休暇を取らなかったことを後悔してた。

ドヤりといえばランキングだ!ということでランキング機能を実装していた。

また、自機のショットを低速・高速で切り替えるようにした。

  • 高速:広範囲の拡散ショット。分散されるので実質威力が低い
  • 低速:正面のみの集中ショット。威力が高い

高速のが一度に複数の岩を攻撃できるのでスコア効率が高い。しかし撃ち返し弾も各方向から襲ってくる。低速だと撃ち返し弾のリスクは少ないが正面だけなのでスコア効率が低い。このへんをプレイヤーがコントロールできるようにしてみた。プレイヤーの考えるべきことが増えるが、状況が悪化する原因をユーザーに委ねることでおまえのせいどう改善していけばいいかを考えていけるようにしたかった。

あとこの辺で黄金岩を導入した。この時点では黄金はレア枠で、通常の岩より出現確率が低く得点が多いというポジションだった。なんとなく作ったよくあるレアアイテム。

4日目(8/10)

お仕事で一回休み

5日目(8/11)

お仕事で夜に作業。

遊んでて、これスコア狙いなら全方位壊す必要なくて、一方向だけ狙ってたら良くないか?となった。他の箇所を攻撃するメリットがない。

ということで泣きながらissueを立てた。

f:id:adarapata328:20210815205650p:plain

さらにこのタイミングでスコアラー的にどうなの?という問題と向き合うことになった。

スコアラーと乱数

自分の考えとしては、スコアアタックしてて一番つらいのは運要素が絡むところで結果が出ないことだと思う。多分繰り返す気になれない。ディガップだと次の2点とか。

  • レア岩が出るかコントロールできない
  • ボムでうまいこと狙った場所を破壊できるかコントロールできない

この辺を解決するために、ルールが分かってくればある程度コントロールできるような仕組みに切り替えた

  • レア岩はスポーン位置ごとに個数を定める
    • 掘れば掘るほどレア岩が出る確率は下がる
    • 同じ箇所を攻め続けたら最終的には枯渇する
  • 百々世のボムは自機狙い弾3wayにする
    • 意図的に金の多い位置を爆破できる

このあたりを実装して、スコアのばらつきを減らせるようにトライアンドエラーしてみた。

とはいいつつ、スポーン時の岩の大きさに関しては調整間に合わなかったごめんなさい(レア大岩が出るかどうかが結構重要になってしまっている)

6日目(8/12)

お仕事。

なんもわからん期に突入した。

レア岩の登場と枯渇の概念で一点集中プレイがなくなるかと思いきや、通常岩でも得点入るからレアを探すより無心で掘り続けたほうが概ねお得になってしまった。ここの得点と確率の調整をするのがめっっっっっちゃめちゃ大変そうな臭いがした。

なので、このタイミングで「レア岩のみ得点が入る」というルールに切り替えてみた。

  • レア岩を見つける
  • ゲットするためにその方向を掘る
  • 採りきったら次の場所を掘る

動き回る動機としては十分だし、「金を掘れ!」だからやることがシンプルになりそうだった。最終的にこのルールがしっくりきたのでこれでいくことにした。

とはいえ、これでも1箇所ずつ丁寧に掘るプレイは健在なので、もう弾幕が多い事自体にインセンティブが入るようにしようかと考えた。ということで弾幕の量に応じて「危険度」が上下するように機能を追加した。

危険度が高いほど取得したときのポイント自体に倍率がかかるようになっている。これにより意図的に壊して回ったほうがスコアとしてはお得になる。おまけに百々世のボムを使ったときは確実に危険度MAXなのでボーナスタイムとも言える。後のことはしらない。

また、これによりプレイヤーが被弾したときのデメリットに「危険度がリセットされる」が追加された。通常のプレイなら嬉しい限りだけどスコア狙う人には辛い。

とはいえ危険度上げっぱなしだと難易度が上がる一方なので、高速と低速で岩を破壊したときの撃ち返し弾の量を調整した。低速で破壊すると通常の半分以下の弾幕に抑えられるので、低速で動き続ければゆっくりと危険度は下がっていくようにしてみた。

前日にここまで変えたのはやっちゃった感あるけど、作業ゲーにはならなくなった気がする

最終日(8/13)

ひたすら微調整とバグ取りとテストプレイ。このタイミングでMac版は背景がバグることに気づき断念した。

あとは、操作はシンプルだが思ったよりテクニック的な部分が増えてきたのでそれをプレイヤーに伝える手段が必要そうになった。とはいえガッツリとしたチュートリアルを作る時間はもうない・・・。

ということで、リトライ画面で百々世にTipsを喋らせるようにしてみた。 https://pbs.twimg.com/media/E8qatqZVIAg4Moa?format=jpg&name=large

意味のない会話も含めて8パターンくらい用意した。

余談ですが僕の幻想郷では百々世は飯綱丸のことを「龍」って呼びます。

ということで18:30くらいに無事投稿。ちゃんとリリースできました。

所感

今年は1週間あったからかどのゲームもレベルが高かった。そして個性が出てて面白い。好き放題やってるなーーー感があって最高だった。

個人的に好きだったやつを2つほど。

「天弓千亦服飾店」 touhou-gamejam.web.app

千亦に服着せるゲーム。意外とキャラ覚えてねーーってなったしヘカちゃんがいいです。

「モリヤリズム RAINBOW」 touhou-gamejam.web.app

リズムゲー&STGでやってて楽しかった。小傘に10回くらいやられました。

また来年もやれるのを楽しみにしてます。

おまけ

後戸エディッションです

【GodotEngine】GutのDouble周りの使い方

Gut

GodotEngineではGutというユニットテスト用のプラグインがあります。 超絶便利です。

github.com

gutのアサーションなど基本的な使い方はとても良くまとめられた記事があるのでそちらを参照してください。

dettalant.com

今回はGutでテストダブルの方法だけざっくりと紹介します。

テストダブル

テストダブルは、テストを行う際にテスト対象が依存するコンポーネントを利用せずに代わりのコンポーネントを利用してテストする手法です。例えばテスト対象が通信を行うコンポーネントに依存していた場合、そのままテストを実行すると毎回通信が走ることになり結構困ります。Twitterと通信するオブジェクトだったらテストのたびにツイートしたりするかもしれません。しかしテストは書きたい。そのような場合に依存コンポーネントを何らかの手段ででっち上げて、かつそれっぽい返り値を返してくれるようなダブル(代役)を立てるのがテストダブルです。

テストダブルは、その中でも役割によって名称が変わってきます。

  • DummyObject
  • Test Stub
  • Test Spy
  • Mock Object
  • Fake Object

詳しく知りたい場合は、xUnit Test Patternsを読むとよいでしょう。

xUnit Test Patterns

xunitpatterns.com

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時間オーバーしちゃったごめんなさい

adventar.org

今月の頭くらいにUnityゲーム開発者ギルドの人たちを対象にTDDハンズオンをやってみました。

何故やったのか

TDDというのはなんとなくわかったのだけど、Unityでのゲーム開発を行うときにどう始めたらいいのかわからないということが最近あったのでどうしようかと悩んでいて。 FizzBuzzをTDDでやってみようなどは既にいっぱいあるけどやはりUnityというものと直接紐づいた事例がないので、もっと現実にありそうなテーマでTDDハンズオンをやってみようと考えたのでした。 何度も繰り返してブラッシュアップしていきたいなと思ったのでどこかで話を聞いてくれる人がいないかなあとギルドで募ったら意外とみなさん手を挙げてくださったので、感謝の正拳突きをしながらzoomでTDDハンズオンをしたのでした。

abe

その時の画像。参加者の方にはモザイクをかけています。僕はアバターで参加しました。

二日連続でやったのですが二回参加された方もいて圧倒的感謝。 2時間超えの長丁場にお付き合いいただきありがとうございました。

UnityでのTDDは特殊なのか

特に何も変わらないと思っています。MonobehaviourがUnityレイヤにべったりで書きにくいというのはその通りですがそれはAndroidのActivityも大体そんな感じだしWebにおけるViewレイヤもそうであるのでUnityが特殊ということは無いでしょう。

どんなことをしたのか

名前変更機能を作ろう

ユーザーが自分の名前を変更できるようにしたい、というストーリー

  • 開いたときに画面に現在の名前を表示させておきたい
  • ユーザが入力フォームから入力できるようにしたい
  • 名前はアルファベットのみで1~8文字以内にしたい
  • 変更に成功したら変更成功したことを表示したい
  • 変更に失敗したら変更成功したことを表示したい
  • 現在の名前が変更後の名前に変わってほしい
  • 名前の変更が保存されててほしい

画面には、自作コンポーネントの貼られていない、それっぽく作ったUIだけを添えています。

image

一切スクリプトがない状態か、これを実装していこう!みたいなストーリーです。

入力と加工と出力

要件は一杯あるので、まずはざっくりと入力と加工と出力に分類してみる。 基本的には出力はテストが難しい。出力はプラットフォーム固有の機能を利用することが多いのでテストに組み込みにくい。ログが吐かれていることをテストするのは若干骨が折れる。また、正解を定義しにくいのもある。「ゲームクリア!」と表示されていればいいのか?「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のテストが書きたいとなったときは一旦立ち止まって見るのがいいなと思いました。

おまけのおまけ

人間の温かさに包まれました

東方ゲームジャムに参加してゲーム作った

東方二次創作限定のゲームジャムが開催されてたので参加してみた。

touhougamejam2020.web.app

木曜昼から日曜昼の72時間で開発・・というイベントだが、普通に仕事だったので木曜日に軽く触ってから土曜日から本格的に開発始めました。

作ったゲームの話

そんなわけで、「イブキン」というちび萃香で戦うアクションゲームができました。

touhougamejam2020.web.app

ざっくり言うと、ピクミン弾幕要素を足したゲームという感じです。最小の犠牲で倒してよりちび萃香を増やして強い敵を倒す。そんな感じです。デザインを整える時間がなかったので中々に厳しいUIをしていますが、ゲーム部分は歯ごたえあると思うので騙されたと思ってやってみて!

進捗の流れ

木曜日

土曜日の夜

日曜日の朝

締切2分前

おもむろにプレイ動画を上げる

www.youtube.com

久しぶりに徹夜での開発をしたが、もう二度とやりたくないなと思った。

「2010年かな?」と思うほど東方のゲームが上がってきてハチャメチャにテンション上がりました。やはり未だに愛され続けてる幻想郷。

ここからはGodotEngineの話

普段はUnityなんだけど今回はGodotEngineで作ってみました。趣味でちょこちょこGodotを触ってはいたのだけどちゃんとリリースまでやったことはなかったので、いい機会とGodotチャレンジをねじ込んでみたのでした。

よかったところ

2D周りの機能が使いやすい

例えば今回はちび萃香や敵の移動として経路探索が必要なんだけど、Navigation2Dとタイルマップ機能が非常に使いやすくサクッと実装できた。タイルにナビゲーションの設定して設置していったらNavigation2Dのパス取得API叩いてよしなに帰ってくるようになってる。

この動画見たらすぐに使えたくらいの手軽さだった。

www.youtube.com

2D用のカメラ機能もあるんだけど、キャラクターを注視しつつ左右の移動幅の限界を付けるとか移動時のスムージングとかその辺はデフォルトで備わってるので凄く助かった。 UI部分も2Dと同じ座標空間で扱えるのでキャラの頭の上にライフのサークル置くとかも脳死でできたり。

GDScriptが楽しい

今回はGDScript(pythonベースの言語)で書いたけど、動的言語ではあるのでダックタイピングがやりやすく開発終盤にはめっちゃ助かった。衝突検知したオブジェクトにDamageメソッドあったら問答無用でぶん投げるとかそういうことを後半にやりまくってた。やりすぎるとどんなオブジェクトがやり取りされてるのかカオスになるけど、型を付けることはできるので基本的には型付けして一部の部分だけ自由奔放なコードができてた。 あとシグナル機能が標準で付いてるのは楽しい。完全に忘れてたので数年前の自分の記事を見ていた。

adarapata.hatenablog.com

yieldとシグナルで非同期を待つのは手軽で便利だった。 Nodeに直接書く埋め込みスクリプトも、特定の部分だけ動かすためにさっと書くということができてうれしい。

イテレーションが早い

デバッグ実行までが異常に早く、コード書き直して1秒で再生できる。これのおかげでトライアンドエラーのモチベーションがかなり高く保ててる気がする。 GodotEngine内でコードを書けるというのも楽しい。当初はエディタは普通に多機能な外部IDEにした方が効率いいんじゃないかあと思ってたんだけど、ゲームエンジン <-> IDE画面を行ったり来たりすることがなくなると、集中が途切れにくくなるなと感じた。GodotEngine内蔵のエディタがちゃんと補完も利くしエラーとなりうる部分を教えてくれるのでそこまで不便ではないのも嬉しい。ドキュメント検索もしやすい。

大変だったところ

日本語記事の少なさ

本当に少ない、これはそう。素直に公式ドキュメントを見るかissueを見るかOSSなのでコードを読むか。今回は幸い近くにつよつよGodotエンジニアがいたので終盤の謎のクラッシュ問題の解消に協力してもらえたので何とか生き延びた。

逆に言えば日本語記事を考慮しなければyoutubeとかにも結構勉強になる動画が上がってるのでお勧めです。

www.youtube.com

おわりに

完成度は置いておいて、久々にちゃんと遊べるものとしてリリースまでもっていったので割と満足度高いです。やはり遊んでもらってナンボだなと思いました。 粗削りな部分が多いのでアップデートはかけていく予定です。

あと、30超えて徹夜で開発はするものではないなと思いました。