VRChatの開発ツール導入ではなく、UdonSharpで開発・テストする上での
ノウハウをまとめた記事になります。
独学なのでこれが正しい・最適とは限らないことに注意です。
VRChatの説明ですが、ほかのプラットフォームでも使えるはずです。
方針
- Unityはクラッシュする前提で構築する
- クラッシュするとシーン・プレハブが壊れることがある
- クラッシュしても大丈夫なように、シーン・プレハブはEditorで自動生成する
- テスト用と製品用のスクリプトを分ける
- テスト用はブレイクポイントが使えるようにVRChatによならいコードにする
- テスト用から製品用のスクリプトを自動生成する
開発手順図
- テスト用スクリプトMockScriptを作成し、シーンMockSceneに配置する
- MockSceneのテストMockTestを行う
- MockScriptをビルドし製品用スクリプトProductScriptを作成、シーンProductSceneに配置する
- ProductSceneのテストをClientSimで行う
- ProductSceneをビルドしVRChatアプリを作成、ローカルテストを行う
- ProductSceneをアップロードしオンラインテストを行う
次の項から開発手順を詳しく説明していきます。
Assembly definitionの作成
プロジェクトの設定が終わったら、最初にAssembly definitionを作成します。
テスト用スクリプトのための「Mocksフォルダ」、
製品用スクリプトのための「Scriptsフォルダ」、
Editor拡張用のための「Editorフォルダ」以下に下記
Assembly definitionを作成します。
まず、Mocksフォルダ以下にAssembly definitionを作成します。
手順
- Create > Assembly definition
- MockAssemblyという名前で保存する
次に、Scriptsフォルダ以下にAssembly definitionと
U# Assembly Definitionを作成します。
手順
- Create > Assembly definition
- ProductAssemblyという名前で保存する
- Assembly Definition References
- VRC.SDK3
- VRC.SDKBase
- VRC.Udon
- UdonSharp.Runtime
- Apply
- Create > U# Assembly definition
- Source Assembly : ProductAssembly
- Apply
最後に、UdonSharpのEditorユーティリティを使えるようにするため、
Editorフォルダ以下にAssembly definition referenceを作成します。
手順
- Create > Assembly Definition Reference
- Assembly Definition : UdonSharp.Editor を選択
- Apply
テスト・製品用スクリプトの初期生成
Editor拡張でファイル名を入力すると、テスト・製品用スクリプトを
自動生成するスクリプトを作成しました。
テスト用スクリプトは「Mocks/Mockスクリプト名.cs」、製品用スクリプトは
「Scripts/スクリプト名.cs」という名前で配置するようにしました。
スクリプトは以下です。
using System.IO; using UdonSharp; using UnityEditor; using UnityEngine; namespace UdonSharpEditor { internal class CreateScript : EditorWindow { public string Text; string TemplatePath = @"C:\Program Files\Unity\Hub\Editor\2019.4.31f1\Editor\Data\Resources\ScriptTemplates\81-C# Script-NewBehaviourScript.cs.txt"; [MenuItem("My Window/My Editor")] public static void MyEditorWindow() { GetWindow(typeof(CreateScript)); } void OnGUI() { Text = EditorGUILayout.TextField("Script", Text); if (GUILayout.Button("Create Script")) { CreateUSharpScript($"Assets/Scripts/{Text}.cs"); ProjectWindowUtil.CreateScriptAssetFromTemplateFile(TemplatePath, $"Assets/Mocks/Mock{Text}.cs"); } } private static void CreateUSharpScript(string chosenFilePath) { // Packages\com.vrchat.udonsharp\Editor\Editors\UdonSharpBehaviourEditor.cs CreateUSharpScript() を参考に作成 } } }
テスト用スクリプトの生成はProjectWindowUtil.CreateScriptAssetFromTemplateFile
を使用しました。
UdonSharpの自動生成は「Packages\com.vrchat.udonsharp\Editor\Editors\UdonSharpBehaviourEditor.cs CreateUSharpScript()」
を参考にしました。
テスト用スクリプトの記述
ゲームのロジック部のスクリプトを記述します。
できるだけテスト用と製品用どちらでも動作するように、
基本的にUdonSharpに依存しないようにスクリプトを書いていきます。
もしテスト用と製品用でコードを分けたい場合は以下のように
PRODUCT defineを使用して分岐します。
PRODUCT defineは後述するように製品用スクリプトだけで定義されます。
#if !PRODUCT // テスト用コード #else // 製品用コード #endif
製品用スクリプトの生成
テスト用スクリプトから製品用スクリプトを生成します。
基本的にテスト用スクリプトをそのままコピーして製品用スクリプトにします。
足りない部分はUdonSharp専用の処理を追加します。
以下で詳しい手順を説明します。
Makeファイルの作成
変更したファイルのスクリプトだけを生成するため、
Makeファイルを作成するようにしました。
MakeファイルはPythonで作成しました。
import os from glob import glob import pathlib import textwrap PROJECT_NAME = 'MyProject' mock_files_abs_path = glob(f'{PROJECT_NAME}/Assets/Mocks/*.cs') mock_files = [ pathlib.Path(file).relative_to(f'{PROJECT_NAME}') for file in mock_files_abs_path ] script_files = [] for mock_file in mock_files: mock_file_base = os.path.basename(mock_file) script_file_base = mock_file_base.replace('Mock', '') script_files += [f'Assets/Scripts/{script_file_base}'] makefile = f""" .PHONY: all clean all: {' '.join(script_files)} """ for mock_file, script_file in zip(mock_files, script_files): makefile += textwrap.dedent(f""" {script_file}:{mock_file} make.py \tpython make.py $@ $< """) with open(f'{PROJECT_NAME}/Makefile', 'w') as f: f.write(makefile)
上記を実行すると、以下のようなMakeファイルが作成されます。
.PHONY: all clean
all: Assets/Scripts/MyScript1.cs Assets/Scripts/MyScript2.cs
Assets/Scripts/MyScript1.cs:Assets\Mocks\MockMyScript1.cs make.py
python make.py $@ $<
Assets/Scripts/MyScript2.cs:Assets\Mocks\MockMyScript2.cs make.py
python make.py $@ $<
上記は、MockMyScript1.csからMyScript1.csを、
MockMyScript2.csからMyScript2.csをMakeする例です。
Makeコマンドの実行
作成したMakeファイルに対してMakeコマンドを実行すると以下の
「make.py」が実行されます。
import os import re from glob import glob import sys script_file_path = sys.argv[1] mock_file_path = sys.argv[2] def get_mock_classes(): files = glob(f'{module_dir}/Assets/Mocks/*.cs') classes = {} for file in files: _class = os.path.splitext(os.path.basename(file))[0] target_class = _class.replace('Mock', '') classes[_class] = target_class return classes mock_classes = get_mock_classes() with open(mock_file_path, encoding='utf-8') as f: mock_strs = f.read() mock_strs = ''' #define PRODUCT using UdonSharp; using VRC.SDKBase; using VRC.Udon; using VRC.Udon.Common.Interfaces; using VRC.SDK3.Components; ''' + mock_strs replace_items = [ (rf'class {config["mock_prefix"]}(\w+)', r'class \g<1>'), ('MonoBehaviour', 'UdonSharpBehaviour'), (r'Debug.Assert\((.+)\)', r'if (!(\g<1>)) Debug.LogError($"Assert {name} (\g<1>) (at __FILE__)")'), ] for replace_item in replace_items: mock_strs = mock_strs.split("\n") replace_item1 = replace_item[1].replace("__FILE__", script_file_path) for i,mock_str in enumerate(mock_strs): mock_strs[i] = re.sub(replace_item[0], replace_item1, mock_str) mock_strs = "\n".join(mock_strs) for _class, target_class in mock_classes.items(): mock_strs = mock_strs.replace(_class, target_class) if not os.path.isfile(script_file_path): raise FileNotFoundError(script_file_path) with open(script_file_path, 'w', encoding='utf-8') as f: f.write(mock_strs)
「make.py」はテスト用スクリプトをコピーして
製品用スクリプトを作成した後、以下のようなUdonSharp用の
変更を行っています。
PRODUCT defineの追加
先頭行に「#define PRODUCT」を追加します。
usingの追加
UdonSharpのusingを追加します。
Assertの変更
VRChatではAssertが使えないため、
「Debug.Assert」を「Debug.LogError」に変更しています。
シーンの生成
テスト用シーン「MockScene.unity」、製品用シーン「ProductScene.unity」
をEditor拡張で自動生成するようにしました。
GameObjectはテスト・製品用シーンで共通の構造とし、Prefab化することで
共通化しました。
次からEditor拡張でシーンを生成する手順を説明します。
シーンを開く
まずプレハブを作るために、テスト用シーンを開きます。
string scenePath = "Assets/MockScene.unity"; var scene = EditorSceneManager.OpenScene(scenePath);
環境プレハブの作成
次に環境をまとめたプレハブ「Environment.prefab」を作成します。
static GameObject AddPrefab(string path, string name, out GameObject prefab, GameObject parent=null) { prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path); Debug.Assert(prefab != null, $"{path} {name}"); GameObject obj = PrefabUtility.InstantiatePrefab(prefab) as GameObject; //Undo.RegisterCreatedObjectUndo(obj, "Create GameObject"); if (parent != null) obj.transform.SetParent(parent.transform, false); obj.name = name; return obj; } var Environment = AddEmptyGameObject("Environment"); AddPrefab("Packages/com.vrchat.worlds/Samples/UdonExampleScene/Prefabs/VRCWorld.prefab", "VRCWorld", out _, Environment);
ルートGameObjectの作成
次にテスト・製品用ルートGameObject「Mocks」・「Products」を作成します。
static GameObject AddEmptyGameObject(string name, GameObject parent=null) { GameObject obj = new GameObject(name); //Undo.RegisterCreatedObjectUndo(obj, "Create GameObject"); if (parent != null) obj.transform.SetParent(parent.transform, false); return obj; } var Mocks = AddEmptyGameObject("Mocks"); var Products = AddEmptyGameObject("Products");
共通プレハブの作成
次にテスト・製品用共通プレハブ「SceneMgr.prefab」を作成します。
var SceneMgr = AddEmptyGameObject("SceneMgr"); // SceneMgr以下にGameObjectの作成、マテリアルの設定、オーディオの設定などを行う string SceneMgrPrefabPath = "Assets/Prefabs/SceneMgr.prefab"; PrefabUtility.SaveAsPrefabAssetAndConnect(SceneMgr, SceneMgrPrefabPath, InteractionMode.AutomatedAction); DestroyImmediate(SceneMgr);
スクリプトのAddComponent
次にテスト・製品用のSceneMgr.prefabにそれぞれのスクリプトをAddComponentします
static MonoBehaviour AddScriptComponent(object root, string objPath, string path, string objType, string scriptPathPrefix, bool enabled = false) { string _path = scriptPathPrefix + path; GameObject _root = null; if (root is GameObject) _root = root as GameObject; else _root = (root as MonoBehaviour).gameObject; Debug.Assert(root != null, $"{root.GetType().Name} {objPath} {_path}"); GameObject obj; if (objPath == "") { obj = _root; } else { Transform transform = _root.transform.Find(objPath); Debug.Assert(transform != null, $"Transform Not Found {root.GetType().Name} {objPath} {_path}"); obj = _root.transform.Find(objPath).gameObject; } Debug.Assert(obj != null, $"{objPath} {_path}"); var script = AssetDatabase.LoadAssetAtPath<MonoScript>(_path); Debug.Assert(script != null, $"Script Not Found {objPath} {_path}"); MonoBehaviour component; if (objType == "Mock") { //component = (MonoBehaviour)Undo.AddComponent(obj, script.GetClass()); component = (MonoBehaviour)obj.AddComponent(script.GetClass()); component.enabled = enabled; } else { //var udonSharpComponent = UdonSharpEditor.UdonSharpUndo.AddComponent(obj, script.GetClass()); var udonSharpComponent = obj.AddUdonSharpComponent(script.GetClass()); udonSharpComponent.enabled = enabled; component = udonSharpComponent; } return component; } foreach (var objType in new string[] { "Mock", "Product" }) { var root = (objType == "Mock") ? Mocks : Products; var scriptPefix = (objType == "Mock") ? "Mock" : ""; var prefabPefix = (objType == "Mock") ? "Mock" : "Product"; var dir = (objType == "Mock") ? "Mocks" : "Scripts"; var scriptPathPrefix = $"Assets/{dir}/{scriptPefix}"; var SceneMgr = AddPrefab("Assets/Prefabs/SceneMgr.prefab", "SceneMgr", out _, root); AddScriptComponent(SceneMgr, "", "SceneMgr.cs", objType, scriptPathPrefix, true); AddScriptComponent(SceneMgr, "MyObj", "MyObj.cs", objType, scriptPathPrefix); }
UIの設定
次に製品用のGameObjectにボタンなどのUIを設定します。
詳しくはUdonSharpのボタンクリックイベントをUnityEditor拡張で登録するをご覧ください。
さらに製品用のGameObjectにVRCUiShapeなどUdon独自のUIを設定します。
var MyUi = SceneMgr.transform.Find('MyUi').gameObject; MyUi..AddComponent<VRCUiShape>(); var MyAudio = SceneMgr.transform.Find('MyAudio').gameObject; MyAudio.AddComponent<VRCSpatialAudioSource>();
プレハブ・シーンの保存
最後に作成したGameObjectをプレハブ・シーンとして保存します。
PrefabUtility.SaveAsPrefabAssetAndConnect(Environment, "Assets/Prefabs/Environment.prefab", InteractionMode.AutomatedAction); PrefabUtility.SaveAsPrefabAssetAndConnect(Products, "Assets/Prefabs/Products.prefab", InteractionMode.AutomatedAction); EditorSceneManager.SaveOpenScenes();
テスト用シーンのテスト
UnityのPlayを使いテスト用シーンのテストを行います。
不具合が見つかった場合はブレイクポイントを使い調査します。
製品用シーンのテスト
ClientSimを使い製品用シーンのテストを行います。
不具合が見つかった場合はInspectorからpublic変数の値を見て調査します。
ローカルVRChatアプリのテスト
VRChat SDKのLocal Testingを使いテストを行います。
この時Number of Clientsに2以上を指定して同期のテストを行います。
不具合が見つかった場合はDebug.LogErrorを使い調査します。
オンラインVRChatアプリのテスト
プライベートパブリッシュを行いテストを行います。
Quest対応を行う場合はここでテストを行います。
バージョン
- Unity : 2019.4.31f1
- VRChat SDK - Worlds : 3.2.3
- UdonSharp : 1.1.9