パスワードを忘れた? アカウント作成
15244020 journal
日記

yumeの日記: 正しいコード 2 3

日記 by yume

さて、Jamの参加や仕事も少し落ち着いてきたので、ぼちぼち『メデューサ・ゲーム』の制作を再開したい……ところだが、改めて見るとびっくりするほどひどいコードだ。と言っても、当時はC#もUnityもほぼ初めてみたいな状態だったので無理もないか。
ステージを追加したり、修正したり修飾するにしても、Tileも使ってないので、なかなかに大変そう……。これを直すくらいなら作り直した方が早いんじゃないか。Spineで作ったアニメーションやイラストはそのまま持ってこられるし、いくつかのScriptは単にコピペで引っ越せるだろう。
それと、前回はマウスで滑らかに動く仕組みだったが、マス目移動の仕組みに直したい。パズルの組み立てとしても都合がいいし、情報力を抑えられてよりフェアなパズルになりそうだから。いくつかギミックも考えている。

さて、改めて土台を直すに当たって、どうせならいくつかの事柄に挑戦していこう。
これまでになんとか色々小さなゲームは作ってこられたが、改めて考えるとC#に関してもぼんやりしている部分がすごく多い。とくに単語としては覚えたけど、全然活用できていない領域について、改めて腰を据えてやっていこう。具体的には、次のようなやつ。
・テストコード(Unity Test Runner)
・Try Catch
・Task(UniTask)
・Addressable(Unityのリソースファイルの管理のなんかすごいやつらしい)
・TileMap
・パーティクル
・あとUniRxにもっと慣れたい

何はともあれ土台を作ろう。というわけで、早速ある程度動くものを作った。
ここまでで実装したものは
・タイルマップの基盤(スプライトはダミー)
・WASDで移動・向きを変えるプレイヤー
・プレイヤーの視界(EvilSight)
・プレイヤーの視界に入ると反応して赤色になるSeeReactor
まだ壁の概念は完璧にはできていないが、差し当たり視界の邪魔になるようにした。壁に埋め込まれたり、EvilSightは壁越しに反応してしまう。

視界関係はUniversal RP + 2D RendererのLight2Dを使った。これはExperimentalな機能らしいが、自分で書いたあの複雑怪奇な視野角の仕組みよりは信用できる。
実際のEvilSightの判定は、前回に近い考え方で組む。

ではまずテストである。
Unity Test Runnerを使うにあたって、まずAssembly Definition Filesという概念について知らなければならないようだ。
テラシュールブログ曰く、
・Assembly Definition Filesはアセンブリを分割する機能
だそうだ。アセンブリとは確かコードを機械語に翻訳することとかその辺のアレだったとおもう。
アセンブリを分割することで、それぞれの単位でコンパイルすることが可能になり、結果的にビルドが早くなったりするそうだ。その代わり、この機能を使う設計者はこれの依存関係とかを管理しなきゃならなくなる。
イメージ的には、一個のでっかいゲームを複数のアプリに分割する感じだろうか。

Unity Test Runnerは指定したアセンブリをテスト対象にしてくれる。全体を対象にもできるが、それだと色々と都合が悪いので、なるべく特定の範囲に限定すべき、らしい。そのためにAssembly Definition Filesを使え。という理屈らしい。

今回は、自分で書くゲームのためのScript全てをまとめて「Yumehiko」とした。
さらに、UniRx、UniTask、DOTweenを導入しているので、これらのそれぞれのAssembly Definition Filesへの参照をYumehikoに追加。

これで一旦Assembly Definition FilesのくだりはOK。実際にテストを書いていく……のだが、こいつがなかなかに厄介だ。

まずは最も基本的な「単体テスト」からやっていこう。Unity Test Runnerでは、これはEditor Modeテストで行えるらしい。
単体テストとは、個別のクラス、ないし関数のテストのことだそうだ。例えば
public int AddInt(int a, int b)
という引数を足して返すだけの関数があったとしたら、テストの方では
Assert.That(AddInt(1, 2), Is.EqualTo(3));
という具合に書くらしい。

これはわかる。引数と返り値しかないから、このテストは超明快だ。
しかし、少なくともここまでに書いたコードには、そんな単純な場合はほとんどない。
例えばFaceDirection.csは、プレイヤーの顔の向きを入力に基づいて変更するクラスだ。
プレイヤーは、シーン開始時にこのクラスを実体化し、入力と向きの状態に応じて、視界のTransformを回転させる。
このクラスの本質的な部分は、TurnToDirection関数だろう。この関数が呼ばれたとき、渡された方向に応じて、DOTweenで指定時間かけて回転させる。

ここで問題になるのは、DOTweenを使うとか、Updateを使うとか、Unityのライフサイクルに絡む処理は単体テストができないっぽい、ということだ。
(この場合は、指定時間分待ってからテストを実行する……みたいなことをすればいけるのかもしれないけど、どうなんだろう)

それでもとにかく一度単体テストをやってみたかったので、ひねりだしたテストコードがこれだ。
元の関数内でメンバ変数Directionに、引数direction代入しているので、この結果はもはや自明だとさえ思える。っていうかこの関数の本質的な部分はそこじゃないから、これは意味のないテストだ。

しかしとにかく、動いたのでこれで一旦よしとして、今度はUnityのライフサイクルも使えるPlay Modeテストをやってみよう。

これは、とにかくUnityの実際のシーンをテスト用に作って、そこに必要なものをコードで生成して、そいつらを使ってテストをするという具合の仕組みだそうだ。何しろ実際のUnity世界と、実体を持ったGameObjectが出てくるわけだから、かなり実際に近いテストになる。
とにかく試しに動くものを書いてみよう。
実際のプレイヤーと全く同じGameObjectを生成して、そいつが生成できてたらテストOKというやつだ。今気づいたがこれ何にもテストしてないな。
まずこのテストのSetUpに大苦戦。
というのもこれまでプレハブはResourcesというシンプルな仕組みで生成してたけど、これはもう古くなってて非推奨らしく、なら推奨されてるAddressableを使ってみよう、ということにしていた。
そうするとAddressableは何を生成するにしても非同期的に処理するらしく、でもSetUp関数は非同期処理を待ったりはしてくれないので、まずそれを待つコードを書かなければならない。非同期処理全然わかってないってのに。

Console Adventureのころに一度Taskを使ったとおもうが、結局のところこれの使い方が全然身についていない。
ここでやりたいことは、SetUp関数でゲームオブジェクトをInstantiateAsyncで作りだすが、この関数が非同期なのでこれが完了するまでは他の処理に行って欲しくないのだ。
(あるいは、他の処理にいってもいいけど、PlayerInstanceTest()はInstantiateAsync()の処理の完了を待ってほしい)
UniTaskはUnityが用意した大抵の非同期処理をUniTask化できるらしく(なにそれすごい)、ここでのInstantiateAsyncもUniTask化して、それをAwaitすることはできる……。
できるが、Awaitをするならそのメソッド全体がasyncになっていないといけないわけで、そうするとそのasyncなメソッドを呼び出した側は、そのメソッドの完了は待ってくれないことになる。ということは、そのメソッドの完了を待つために元のメソッドをasyncにして、awaitをして、でもそうするとそれをawaitするために……ウワーッ!!!

このテストがどういう仕組みか詳しくはわからんけど、多分、まずSetUpアトリビュートがついてる関数をはじめにぶん回して、それが終わったら(同期処理は待たずに)即座にTestアトリビュートの関数をぶん回していくんだろう。何が困るって、どのテスト関数をどのタイミングで回すかをこっちで制御できないっぽいことだ。

なんとなく一番確かで正しそうなのは、やっぱりPlayerInstanceTest()関数が走るとき、まだオブジェクトの生成が終わってないなら一旦待つ、ということだろうか。しかしどうすればそれができるんだろう。うーむ……。
全然賢くも正しくもないけど、とにかくPlayerInstanceTest()側で一旦1秒待ってから処理をすることにした。するとまぁテストは成功した。

これ自体はすごくシンプルな事例な気がするが、こういう状態すらパパッと組み立てられないということは、非同期処理の基本的なことがまずわかってないんだろうな。

でも、これでとにかくPlay Modeテストはできた。これを応用すればFaceDirectionが実際に回転したかも確認できるはずだ。非同期処理はともかく、テスト自体はなんとかできそうだ。といったところで今日はここまで。

この議論は賞味期限が切れたので、アーカイブ化されています。 新たにコメントを付けることはできません。
  • by Anonymous Coward on 2021年03月29日 9時29分 (#4002686)

    マイクロソフトの悪いところで、「コードを機械語に翻訳することとかその辺のアレ」とは関係ないんですよ。
    .NET Frameworkで作成されたライブラリファイルや実行ファイルぐらいに思ってください。

    • by nekopon (1483) on 2021年03月29日 9時33分 (#4002690) 日記
      原義の "集まり" のほうなんですよ
      // "集合"とかくとsetのほうが浮かんでしまう数学屋なのであえて和語
      親コメント
      • by Anonymous Coward

        ちゅーかCIL。なんでわざわざ別の名前をつけたのかは謎のまま。なんでわざわざアセンブリなんてまぎらわしい名前にしたのかはもっと深い謎。

typodupeerror

※ただしPHPを除く -- あるAdmin

読み込み中...