- 投稿日:2019-08-20T22:14:05+09:00
Unity勉強メモ2 ~Collider続き~
はじめに
この記事は5日前にUnityを触り始めた人の勉強メモです。過度な期待はしないでください。
誤った内容が含まれる可能性が多々あります。誤りに気付いた方は、コメントにて脇腹をつついてください。kanatatara、MeshColliderやめるってよ
前回の記事で「ええやん、MeshCollider」と話していたkanatataraさん。
冒頭でMeshCollider使うのやめる宣言。乱心!乱心じゃー!
・・・というのも、Unityのマニュアルで、以下の記述を見つけちゃったのです。「MeshColliderは処理重くなるから極力使わんほうがええよー!」
「複合プリミティブコライダー推奨やよー!」ってことらしい。なるほど。で、意味は?
「複合プリミティブコライダー 意味」でGoogle先生に聞いてみるものの、
「複合プリミティブコライダー」という単語自体がUnityのマニュアルでしか出てきていない・・・。
「ふくごーぷりみてぃぶこらいだー?????」とあほ面してたのですが、どうやら以下の意味みたいです。■プリミティブコライダー
以下のような、単純でプロセッサ負荷の小さいコライダーのこと
Box Collider、Sphere Collider、Capsule Collider■複合プリミティブコライダー
プリミティブコライダーを複数組み合わせて作るコライダーのことMeshColliderを使おうとして、やめた理由
VRで物を掴みたい → 正確な衝突判定が必要 → ならMeshCollider必要やん?
という思考プロセスの元、MeshColliderに行き着いたのですが、
別に全身の衝突判定を綺麗に行う必要性がなかったので、必要性を感じなくなったのです。
更にVRの場合、アセットが用意されており、そちらを使うのが得策なんだなと気づきました。
(私の場合、Oculus Rift Sを使用しているので、「OVR Grabber」「OVR Grabbable」が該当します。)微速前進!ヨーソロー!
・・・とまあ、「とりあえず触ってみては失敗して」を繰り返す感じですが、
少しずつでも前進できているのは良いことなのかなと思っています。
とりあえずUnityでMMDモデルを使って、バ美肉して、手をグッパーするところまでは進みました。
次はUnity内に何かしらの空間を作って、バ美肉度を高めていきながら勉強したいな、と思います。Unityは成果が目に見えて現れるので、インフラおじさんとしては、とても楽しいです・・・!
- 投稿日:2019-08-20T13:06:48+09:00
Unityでテストコードを書いて会得した最強のTips(苦肉の策ともいう)
最強のTipsが生まれるきっかけ
自分が現在開発に携わっているゲームでは、作中に多くのシナリオが含まれています。
その数はデータにして200を超えていますが、このデータが増えるにつれて、ある問題が発生しました。それは、シナリオデータの数が多すぎて、不具合のあるデータがあることに気付けないということです。
(特定の選択肢を選ぶと、進行不能になるデータがいくつか見られました)これに対処するため、「全シナリオの動作チェックを行うUIのテストコードを書こう!」と思ったのが、事の始まりです。
まさか、あんな苦肉の策を取ることになるとは、この時は考えてもいませんでした…
目次(Tips一覧)
自分がテストコードを書いて会得したTipsを紹介します。
ちなみに、最強のTipsは「6. テストの失敗が後続のテストにまで影響を与えないようにする」です。
それ以外は、まとも(のはず…)なTipsになります。1. Play Modeテストで使うAttributeとその注意点
2. テスト時間を短縮する
3. 手軽にUIテストをできるようにする
4. テストの失敗をハンドリングする
5. コード側からTest Runnerを制御する
6. テストの失敗が後続のテストにまで影響を与えないようにする
7. CommandLineからTest Runnerを実行するTipsを紹介する前に
テストコードを書くにあたって、Unity上でテストできるというメリットから、Unityに標準で付属しているTest RunnerのPlay Modeテストを使用しています。
Play Modeテストはコルーチンを使って記述され、このコルーチンが最後までエラーを吐かずに終了すればテストが成功扱いになるという、とてもシンプルなものです。
public class ScenarioTest { [UnityTest] public IEnumerator Run() { Debug.Log("テスト開始"); // シナリオの再生処理 yield return RunScenario(); Debug.Log("最後までいったのでテスト成功!!"); } }これはUnityエディタから実行できるようになっており、実行すると自動的にUnityがPlay状態になり、直後にコルーチンを実行する仕組みとなっています。
そしてもちろん、CommandLineからの実行にも対応しています。完璧ですね。
Unity.exe -projectPath <projectPath> -batchmode -runTests -testPlatform playmode -testResults <resultPath>
これから紹介するTipsは、このTest RunnerのPlay Modeテストを行う際のTipsとなります。Tips
1. Play Modeテストで使うAttributeとその注意点
Test Runnerを使う際に覚えておきたいAttributeがあります。
また、Play Modeテストにおいて注意すべき点もあるので、それも交えて紹介します。テストメソッド用
[UnityTest]
- テストしたいコルーチンに必ず付ける必要がある
[TestCase]
- テストケースを定義する
[TestCaseSource]
- テストケースをリストで定義する(動的に定義することができる)
[Timeout]
- テストのタイムアウト時間を指定する
public class ScenarioTest { // テストしたいコルーチンに必ず付ける必要がある [UnityTest] public IEnumerator Run() { yield return RunScenario(); } // コルーチンに引数として値を渡すことができる(複数指定可) // この例だと、RunWithTestCase("file_0001")、RunWithTestCase("file_0002")として扱われる // 【注意】 コルーチンに[TestCase]を使う場合、ExpectedResult = null を記述しないとエラーになる [TestCase("file_0001", ExpectedResult = null)] [TestCase("file_0002", ExpectedResult = null)] [UnityTest] public IEnumerator RunWithTestCase(string file) { yield return RunScenarioWithFile(file); } // [TestCaseSource]に渡すリスト public static IEnumerable FILES { get { foreach (var path in Directory.GetFiles("<path>")) { // 【注意】 [TestCase]同様、Returns(null) を記述しないとエラーになる yield return new TestCaseData(path).Returns(null); } } } // [TestCase]をリストで管理できるようにしたもの(動的に定義したい場合などで有用) // 各要素ごとを渡したテストとして扱われる // Directory.GetFilesから取得できる文字列がfile_0001、file_0002とすると、 // RunWithTestCaseSource("file_0001")、RunWithTestCaseSource("file_0002")となる [TestCaseSource("FILES")] [UnityTest] public IEnumerator RunWithTestCaseSource(string file) { yield return RunScenarioWithFile(file); } // テストのタイムアウト時間を指定する // デフォルトだと30秒 // ms単位で指定(この例だと5分になる) [Timeout(300000)] [UnityTest] public IEnumerator RunWithTimeout() { yield return RunScenario(); } }ちなみにUnityエディタ上ではこう見えています。
コールバック
[OneTimeSetUp]
- クラス内で最初のテストが実行される前に一度だけ呼ばれる
[SetUp]
- 各テストの最初に呼ばれる
[TearDown]
- 各テストの最後に呼ばれる(テストが失敗しても呼ばれる)
[OneTimeTearDown]
- クラス内で最後のテストが実行された後に一度だけ呼ばれる
public class ScenarioTest { // クラス内で最初のテストが実行される前に一度だけ呼ばれる [OneTimeSetUp] public void OneTimeSetUp() { Debug.Log("テスト開始"); } // 各テストの最初に呼ばれる [SetUp] public void SetUp() { Debug.Log("file_0001 のテスト開始"); } // 各テストの最後に呼ばれる(テストが失敗しても呼ばれる) [TearDown] public void TearDown() { Debug.Log("file_0001 のテスト終了"); } // クラス内で最後のテストが実行された後に一度だけ呼ばれる [OneTimeTearDown] public void OneTimeTearDown() { Debug.Log("テスト終了"); } }なお、Test RunnerにはNUnitと呼ばれるテストフレームワークが使われているため、もっと知りたい方はNUnitのドキュメントを参照ください。
2. テスト時間を短縮する
Play Modeテストでは、UnityをPlay状態にしてテストを実行するため、最速でも実プレイと同じ速度しか出ません。
そこで、ゲーム内速度やFPSを上げることでスピードUPを図ります。
public class ScenarioTest { // クラス内で最初のテストが実行される前に一度だけ呼ばれる [OneTimeSetUp] public void OneTimeSetUp() { // テスト時間短縮のために高速化する Application.targetFrameRate = 60; Time.timeScale = 2.0f; } }これだけで、テスト時間を大幅に削減することができます。
Play Modeテストならではの手法ですね。3. 手軽にUIテストをできるようにする
シナリオの動作チェックのようなUIテストを行う場合、画面タップなどのUIの操作が必要となります。
ただ、これを自前で実装するのは手間になるため、すでにあるUnity UI Test Automation Frameworkというフレームワークを使用することをオススメします。
これを使えば、
public class ScenarioTest : UITest { [UnityTest] public IEnumerator Run() { // targetという名前のボタンを押す yield return Press("target"); // Scenarioシーンをロードする yield return LoadScene("Scenario"); // targetというオブジェクトが登場するまで待機する yield return WaitFor(new ObjectAppeared("target")); } }といったことが簡単にできるようになります。
4. テストの失敗をハンドリングする
テストコードを書いていて、テストが失敗した場合のみ特定の処理をしたい、ということがあると思います。
もし、
[TearDown]
で指定したメソッドに引数として情報が渡ってきたり、テスト失敗時に呼ばれるコールバックなどがあればよかったのですが、NUnitのドキュメントを見た限りなさそうでした。ではどうするか。
Play Modeテストはエラーログが出れば失敗となるので、Unityのログメッセージを取得することでハンドリングできるようになります。public class ScenarioTest { private List<string> _errorLogs = new List<string>(); // クラス内で最初のテストが実行される前に一度だけ呼ばれる [OneTimeSetUp] public void OneTimeSetUp() { // ログを残す Application.logMessageReceived += Log; } public void Log(string logString, string stackTrace, LogType type) { // エラーログを保持しておく if (type == LogType.Error || type == LogType.Exception) { _errorLogs.Add($"{logString}\n{stackTrace}\n"); } } [UnityTest] public IEnumerator Run() { // 各テストの頭でクリアする _errorLogs.Clear(); yield return RunScenario(); } // 各テストの最後に呼ばれる(テストが失敗しても呼ばれる) [TearDown] public void TearDown() { // エラーログがあるならテスト失敗 if (_errorLogs.Count > 0) { Debug.Log("テスト失敗"); Debug.Log(string.Join("\n", _errorLogs.ToArray())); } } }こうすれば、失敗時のみ処理をすることができます。
5. コード側からTest Runnerを制御する
Test RunnerがUnityエディタやCommandLineから実行できる術を用意しているとしても、やはり自前で拡張したい時はあります。
そういった場合、コード側からTest Runnerを制御することになると思いますが、実はこれがかなり面倒です。
理由は単純。Test RunnerのAPIが公開されていないからです。そのため、リフレクションを使って無理やり実行することになります。
public class ScenarioTestCommand { public static void Execute() { // テスト情報を追加する var engineAssembly = Assembly.Load("UnityEngine.TestRunner"); var testFilterType = engineAssembly.GetType("UnityEngine.TestTools.TestRunner.GUI.TestRunnerFilter"); var testFilter = Activator.CreateInstance(testFilterType); // テストの名前を使って実行する // 実行するテスト名(メソッド名)は名前空間とクラス名も含めること var testNamesField = testFilterType.GetField("testNames"); testNamesField.SetValue(testFilter, new string[] { "ScenarioTest.Run(\"file_0001\")", "ScenarioTest.Run(\"file_0002\")" }); // Test Runnerを実行できるクラスを参照する var editorAssembly = Assembly.Load("UnityEditor.TestRunner"); var runnerWindowType = editorAssembly.GetType("UnityEditor.TestTools.TestRunner.TestRunnerWindow"); var runnerWindow = runnerWindowType.GetField("s_Instance", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null); var listGUIField = runnerWindowType.GetField("m_PlayModeTestListGUI", BindingFlags.Instance | BindingFlags.NonPublic); var listGUI = runnerWindow != null ? listGUIField.GetValue(runnerWindow) : Activator.CreateInstance(listGUIField.FieldType); // Test Runnerを実行する var runMethod = listGUIField.FieldType.GetMethod("RunTests", BindingFlags.Instance | BindingFlags.NonPublic); runMethod.Invoke(listGUI, new object[] { testFilter }); } }正直「何やっているんだ、このコードは」状態になると思うので、dnSpyなどのデコンパイラを使ってTestRunner.dllの中身を見ることをオススメします。
6. テストの失敗が後続のテストにまで影響を与えないようにする
Play Modeテストでは、複数のテストを実行した場合も、同じPlay状態でテストを実行します。
そのため、変数などで状態を持っていれば、それは後続のテストにも引き継がれることになります。この特性は、途中のテストが失敗した時に厄介で、
- テストAが失敗する
- テストAが途中で失敗したので、途中の状態が保持されたままになる
- 次のテストBに移る
- 想定しない状態が保持されていることで、問題ないはずのテストBが落ちてしまう
ということが起きてしまいます。
これに対処するには、各テストが正しく初期化された状態で実行される必要があります。
ただ、もしstaticやDontDestroyOnLoadをよく使っているプロジェクトであれば、これは困難を極めます。シーンの再ロードなどでは初期化できず、しらみつぶしに初期化されていないところを探していくしかないからです。
自分が現在開発に携わっているゲームも同じ状況で、初めはしらみつぶしに探していましたが、途方もなく途中で断念しました。打開策はないか…そう思案して、考えついた最強のTips、それが
一旦UnityのPlay状態を止めて再度Play状態にすれば、事実上初期化されたことと同義になる!!(スマホのタスクキルと同じ)
というものでした。
実際のコードを見てみましょう。
public class ScenarioTest { private static object _testFilter = null; private List<string> _errorLogs = new List<string>(); private List<string> _runTestNames = new List<string>(); // クラス内で最初のテストが実行される前に一度だけ呼ばれる [OneTimeSetUp] public void OneTimeSetUp() { // 【注意】 この例では、Logメソッドを省略 Application.logMessageReceived += Log; // Restartするため、テスト情報を保持しておく var assembly = Assembly.Load("UnityEngine.TestRunner"); var controllerType = assembly.GetType("UnityEngine.TestTools.TestRunner.PlaymodeTestsController"); var controllerObject = GameObject.Find("Code-based tests runner"); var controller = controllerObject.GetComponent(controllerType); var settingsField = controllerType.GetField("settings"); var settings = settingsField.GetValue(controller); var settingsType = assembly.GetType("UnityEngine.TestTools.TestRunner.PlaymodeTestsControllerSettings"); var filterField = settingsType.GetField("filter"); _testFilter = filterField.GetValue(settings); } // 【注意】 この例では、FILES変数の宣言は省略 [TestCaseSource("FILES")] [UnityTest] public IEnumerator Run(string file) { // 前回のテストが失敗していたら、UnityのPlay状態を止める if (_errorLogs.Count > 0 && _testFilter != null) { // 【注意】 テスト情報の更新処理(今回は名前で更新しているが、カテゴリーなら別の処理が必要) var assembly = Assembly.Load("UnityEngine.TestRunner"); var testFilterType = assembly.GetType("UnityEngine.TestTools.TestRunner.GUI.TestRunnerFilter"); var testNamesField = testFilterType.GetField("testNames"); var testNames = (string[])testNamesField.GetValue(_testFilter); // すでに実行しているテストは削除する testNames = testNames.Where(testName => !_runTestNames.Any(runTestName => testName.Contains(runTestName))).ToArray(); testNamesField.SetValue(_testFilter, testNames); // Unityを止めて、Test Runnerを再び実行する EditorApplication.isPlaying = false; EditorApplication.update += OnRestart; } _errorLogs.Clear(); _runTestNames.Add(file); yield return RunScenarioWithFile(file); } private static void OnRestart() { Restart(_testFilter); } // コード側からTest Runnerを実行する private static void Restart(object testFilter) { var assembly = Assembly.Load("UnityEditor.TestRunner"); var runnerWindowType = assembly.GetType("UnityEditor.TestTools.TestRunner.TestRunnerWindow"); var runnerWindow = runnerWindowType.GetField("s_Instance", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null); var listGUIField = runnerWindowType.GetField("m_PlayModeTestListGUI", BindingFlags.Instance | BindingFlags.NonPublic); var listGUI = runnerWindow != null ? listGUIField.GetValue(runnerWindow) : Activator.CreateInstance(listGUIField.FieldType); var runMethod = listGUIField.FieldType.GetMethod("RunTests", BindingFlags.Instance | BindingFlags.NonPublic); runMethod.Invoke(listGUI, new object[] { testFilter }); EditorApplication.update -= OnRestart; } }やっていることは、以下の通りです。
- テスト開始時にTest Runnerからテスト情報を抜き出す
- 各テスト開始時、前回のテストが失敗したか確認する
- テストに失敗していれば、すでにテスト済みのものを、開始時の保持しておいたテスト情報から削除する
- UnityのPlay状態を止め、更新したテスト情報をもとにTest Runnerを再実行する
Test Runnerを実行すれば、UnityはPlay状態になるため、事実上の初期化が完成です。
注意として、テスト情報が名前ではなくカテゴリーで管理されていることもあるため、その際は別の更新処理が必要です。
苦肉の策ではありますが、これで全シナリオのテストが可能となりました。ただ一点、この方法で問題になることがありました。
次のTipsに移ります。7. CommandLineからTest Runnerを実行する
通常のケース
冒頭でも記述した、Test Runnerで用意されているコマンドで実行可能です。
Unity.exe -projectPath <projectPath> -batchmode -runTests -testPlatform playmode -testResults <resultPath>これに
-quit
が指定されていないことにお気づきでしょうか?実は、このコマンドを実行すると、テスト終了時にUnityが自動的に終了するようになっています。
逆に、-quit
を付けると失敗してしまうためご注意ください。特殊なケース
「6. テストの失敗が後続のテストにまで影響を与えないようにする」で紹介した方法だと、通常のケースと同じコマンドを実行しても上手くいきません。
問題は2つあります。
解決策とともに、一つずつ紹介していきます。Unityが終了しない問題
通常のケースにて紹介した、
-quit
を指定せずとも、テスト終了時にUnityが自動的に終了する機能。
本来であれば問題がないこの挙動ですが、先ほど紹介した一旦Play状態を止める方法だと厄介です。というのも、一旦Play状態を止めるという動作は、Test Runnerが不正終了したとみなされ、その機能が働かなくなってしまうからです。
このままでは、CommandLineからの実行時、途中でテストに失敗すると、Unityが終了されません。
そこで、batchmodeでの実行であれば、全テスト終了時にUnityを終了するようにしました。
public class ScenarioTest { // クラス内で最後のテストが実行された後に一度だけ呼ばれる [OneTimeTearDown] public void OneTimeTearDown() { // batchmodeなら終了させる if (Environment.CommandLine.Contains("-batchmode")) { // エラー扱いにするため、1以上を返しても良い EditorApplication.Exit(0); } } }途中でテストが失敗しPlay状態が止められても、
[OneTimeTearDown]
は呼ばれないことが肝ですね。
(複数のクラスが存在する場合は、他のクラスのテストが終了しているかのチェックが必要になります)無限に同じテストを実行され続ける問題
もしこの記事のコードをそのまま使っている場合、無限に同じテストが実行され続けてしまいます。
これは、
Unity.exe -projectPath <projectPath> -batchmode -runTests -testPlatform playmode -testResults <resultPath>このコマンドで作成されるテスト情報は名前で管理されていないため、記事のコードのように、すでに実行したテストの名前を削除しても意味がないためです。
もし、この記事のコードを使い回したい場合は、名前で管理されているテスト情報を作成しTest Runnerで実行するメソッドを、CommandLineから呼ぶことをオススメします。
Unity.exe -projectPath <projectPath> -batchmode -executeMethod ScenarioTestCommand.Execute実行している
ScenarioTestCommand.Execute
メソッドは、「5. コード側からTest Runnerを制御する」で紹介しているコードを参照してください。
こちらのサンプルコードは、名前で管理されているテスト情報を作成し、実行しています。まとめ
自分の開発しているゲームでは、この全シナリオ動作テストを毎朝走らせ、失敗すればslackに通知するようにしています。
今回テストコードを入れたことで、シナリオデータの不具合が消えただけでなく、シナリオ再生用のコードを変更するハードルが下がったことも嬉しいポイントです。
ゲーム開発だとテストコードを書く機会も少なかったですが、この機会に今後も積極的に取り組んでいきたいと思います。
Twitter: @yukiarrr
その他参考文献
- 投稿日:2019-08-20T00:08:59+09:00
VRChatで他のプレイヤーにダメージを与える銃を作る
前回の続きです。
前回作った銃を使って、弾を当てた別のプレイヤーに、VRC_CombatSystemのダメージを与えられるシステムを作ります。PvPです。(※VRC SDK 2019.2.5の仕様に従っており、遠回りな方法かもしれません。またこのSDKでしか使えない方法かもしれません。)
自身の当たり判定の作成
以下のようなHitという名前のCubeを作成します。Box ColliderはTriggerにしておきます。
分かりやすく色を付けていますが何色でもかまいません。
(VRCMirrorで見ない限り、相手からも自分からも見えないので)
次にこのCubeを選択したまま、AnimationタブからAnimatorを作成します。
(タブの右上からAdd Tabで出ます)
Add PropertyからTransform->PositionとScaleを追加して、以下のように設定します。
サンプル数1だけのアニメーションです。
このように作成するとHitにAnimatorが追加されており、このアニメーションがずっと実行されます。
このHitはPrefabにします。
VRC Combat Systemの設定
空のGameObjectにVRC_Combat Systemを追加します。
設定は以下の通りです。
Visual Damege Prefabという項目に先程作成したHitを設定しています。
本来こちらには、VRCSDKの中にあるVRC_PlayerVisualDamageのPrefabを設定します。
そうするとダメージを受けたときの張り付きエフェクトが表示されますが、今回このシステムを流用して自分に追従するオブジェクトを実現しています。HitでAnimationを作成した理由ですが、Visual Damege Prefabに設定したものはScaleが変わってしまうので、Animationで上書きして固定しています。
ToyBoxのテクニックを参考にしました。
https://vrcworld.wiki.fc2.com/wiki/Toyboxレイヤー設定
以前作成した銃弾のSphereのPrefabで、以下の赤丸をクリックしてAddLayerします。
ProjectileとHitというLayerを追加しました。
銃弾のSphereにProjectileのレイヤーを設定します。
(今回Hitのレイヤーは使いませんが、ProjectileとHitだけ判定するようにした方が軽くなるはず)
ダメージ
HitのPrefabを選択し以下のようにVRC_TriggerでAdd Damageを設定します。
Projectileのレイヤーのオブジェクトに当たったら自分がダメージを受けるようになります。
Broadcast TypesをLocalに設定することで、自分にのみ影響するようにします。
(現在の仕様では、Local以外では他のプレイヤーにもダメージが入ってしまうと思われます)
Broadcast Typesについてはこちら
https://docs.vrchat.com/docs/trigger-broadcast-typesローカルテストではうまく動作しないため、実際にPublishして確認してみてください。
私はPCとOculus Questでアカウントを2つ作って確認していました。あとHitの方はローカルでしか作用しないため、被弾エフェクト等は銃弾の方のトリガーで出すようにした方がいいと思います。
告知
これらのシステムを使って
ゲームのワールドを作成し、VTuberでe-sports開催を目指しています。
詳細はこちらhttps://twitter.com/void_vtuber/status/1160160487176204288環境
Unity 2017 4.2.8f1
VRC SDK 2019.2.5 (31 July 2019)