VRChatのExpressionsMenuをC#スクリプトで作る話
特にプログラマでもなんでもない化学畑出身のpyocopelです.最近ChatGPTでエディタ拡張作れるやんということに気付いてから改変で楽をしようとC#に手を出してみています.
VRChat関係なくUnityの機能として動くものであればChatGPTに聞けばある程度のものを作ってくれるのですが,VRChat関連は「ドキュメント見てね」というコメントを返すだけなので自力で書くことを求められます.「じゃあドキュメント見るか」と思って検索していたのですが,一体どこに書いてるかわからない…….ということでこういうの書いたらそれっぽく動いてくれましたという備忘録的なものを書いていこうと思います(ドキュメントこれやでってのがあれば教えてください!).誰かに見せる/伝えるというより自分が勉強したことのまとめみたいなものです.
このnoteの目的
エディタ拡張を呼び出してボタン一つでExpressionsMenuを生成しよう
Controlを設定してみよう
という内容で,C#触ったことのない人が見てもわかるように書いていくつもりです.ただし,自分が「なんでこれが要るの?」というのを理解して進めていきたい人なので逐一必要な理由を書いていきます.そのため説明が長くなってしまう上にネットで調べたものがほとんどなので間違ってたらごめんなさい.
Expressionsを作る準備(C#の基本的な話)
Expressionの話じゃなくてC#の書き方的なところから始めます.そんなん説明いらんよって人は目次から読まずに飛びましょう.
準備
そもそも何で書くのという話ですが,お好きなものでいいと思います.自分はTeXを扱う時にお世話になってたのでVSCodeを使っています.
まず準備としてUnityを開いて「Editor」という名前のフォルダを作ります.このフォルダ内で右クリックして「Create > C# Script」と選択します.すると新しい「.cs」拡張子のついたファイルができるのでお好きな名前(今回ならExMenuGeneratorとか)を入れてこれを開きます.すると以下のようなコードが書かれていると思います:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class NewBehaviourScript : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
これを消します.Blenderの最初に豆腐を消す感じで消してあげてください.
これで下準備は終わりです.実際に書いていきましょう.
usingのお話
いきなり書いても何も動かないのでまずは「何を使うか」というのを宣言します.C#は「この文字列を書いたらこう機能する」というものがある程度用意されているのですが,UnityやVRChatに特有のものもあったりします.これを最初に書いていきます.
using UnityEditor; //UnityEditorのAPIを使うために必要
using UnityEngine; //Unityの基本的なクラスやAPIを使うために必要
using VRC.SDK3.Avatars.ScriptableObjects; // ExMenuとExParameterを扱うために必要
こういったものがないとUnityをいじってるときにたまに出てくるnamespaceがうんたらかんたらと書いてあるErrorが大量に出たりします(図1).
これは「"hoge"って文字列が書いてあるけど,どれのことを指してるかわかんないよ.どこでこれを定義しているのか教えてね.」って意味のようです.例えばEditorWindowであればUnityEditorという名前空間で定義されているのでusing UnityEditorと書くことで教えてあげる必要があります.一応""UnityEditor.EditorWindow""と書けば名前空間も指定しているのでエラーは消えるのですが...…,これを毎度書いていると冗長で読みづらいので,それを読みやすく整理するためにusingを使うことに利点があります.
(参考文献:https://qiita.com/4_mio_11/items/145c658078a7fe5f36a7)
classのお話
よく設計図という説明がされます.このclassというものの中に扱うデータであったりこのデータにどのような処理を行うかという操作をまとめたものと理解できます.と,説明はそんなところで以下のものを書きましょう.
public class ExMenuGenerator : EditorWindow
{
}
ここでアクセス修飾子について確認しましょう.アクセス修飾子はclassの前についてるpublicのことを指しているのですが,これのほかにprivateやinternalなど種類があります.
これはこのclassに対してどこからアクセスできるかを示しており,例えばprivateであればクラスの中からしかアクセスできないのでUnityエディタのツールメニューからエディタウィンドウを開くというような操作をすることはできなくなります.またpublicなど指定しない場合は自動的にinternalになるのですが,Unity上では同じEditorフォルダの中にあるコードからはアクセスできるのですがそれ以外からは不可能なので同様に今回は不適となります.publicはどこからでもアクセス可能なので今回はこれを選択します.
classの後に書かれてる""ExMenuGenerator""は今回作るエディタ拡張の名前です.自分はそのままの名前にしましたが,自由につけても問題ないところです.ただし後から見てもわかるように書きましょう.
次に"": EditorWindow""ですが,これはもともとUnityに備わっているEditorWindowというクラスを継承するということを表しています.これは既存のコードを再利用し,新たな機能を追加したり既存の機能を上書きするときに使うのですが,今回行うことはUnityが用意しているEditorWindowに機能をつけていくということなのでEditorWindowを継承します.
変数を設定する
今回は以下のように変数を設定しました.
string exMenuName = "ExMenu"; // ExMenuの名前
string exMenuControlsName = "Control"; // Controlの名前
string exParameterName = "Parameter"; // Parameterの名前
DefaultAsset exMenuFolder; // ExMenuの保存先のフォルダ
注釈で書いたようにそれぞれ名前や保存先のフォルダを自分でエディタ上で入力して決めたいので設定しました.""=""の後ろで書いてあるダブルクォーテーションで囲われたところはあらかじめエディタウィンドウ上に入力されているところです.完成はこんな感じになります(図2).
ウィンドウを表示する
今回はメニューバーの""Tools""からEditorWindowを呼び出してそこで作業するといったことを行いたいと考えています.そのため,Toolsにメニューを追加するために以下の行を追加します.
[MenuItem("Tools/ExMenuGenerator")]
ここのToolsを変えてあげれば別のところからも開くことができます.例えばWindowにするとメニューバーのWindowから開けますし,pyocopelとでも書いてあげれば新たにメニューバーに""pyocopel""という項目が追加されます(図2).
次にウィンドウを表示させます.
public static void ShowWindow()
{
GetWindow<ExMenuGenerator>("ExMenuGenerator");
}
これでメニューバーからExMenuGeneratorと書かれているところをクリックするとウィンドウが表示されるようになります.このvoid ~ から始まるまとまりをメソッドと言って機能の単位のようなものです.オブジェクト指向とかそういうワードを入れて検索すると解説が山のように出てくると思いますのでこれはそちらに任せます.
ではvoidの前についているstaticは必要なのか.一言で言うとMenuItemがstaticと書いているものしか受け付けないから必要であるということみたいです.自分自身概念がふわふわしている状態なので説明が間違っていたら申し訳ないのですが,まずstaticは「そのクラスから生成されたもの全体で一つの値を共有する」といった概念でかつ「クラスなどから生成されたもの(インスタンス)の存在が必要ない(インスタンスに依存しない)」らしいです.今回のウィンドウ表示はクラスで生成されたものが存在するしないに関わらず動いてほしいものなのでstaticをつける,とそのような理由のようです.
実際にExpressionsMenuを作ろう
ここまで作ったコードは以下のようなものです.
using UnityEditor;
using UnityEngine;
using VRC.SDK3.Avatars.ScriptableObjects;
public class ExMenuGenerator : EditorWindow
{
string exMenuName = "ExMenu"; // ExMenuの名前
string exMenuControlsName = "Control"; // Controlの名前
string exParameterName = "Parameter"; // Parameterの名前
DefaultAsset exMenuFolder; // ExMenuを保存する名前
[MenuItem("pyocopel/ExMenuGenerator")]
public static void ShowWindow() // ウィンドウを表示するためのメソッド
{
GetWindow<ExMenuGenerator>("ExMenuGenerator");
}
}
ここからShowWindowのメソッドの下にOnGUIメソッドを書いていきます.
エディタウィンドウの内容を作る
まずは以下のように書いていきましょう.
void OnGUI()
{
exMenuName = EditorGUILayout.TextField("ExMenu Name", exMenuName);
exMenuControlsName = EditorGUILayout.TextField("Controls Name", exMenuControlsName);
exParameterName = EditorGUILayout.TextField("Parameter Name", exParameterName);
exMenuFolder = (DefaultAsset)EditorGUILayout.ObjectField("生成するExMenuの保存先", exMenuFolder, typeof(DefaultAsset), false);
if (GUILayout.Button("Generate ExMenu"))
{
GenerateExMenu();
}
}
こちらでは上で宣言した各変数に対してエディタウィンドウ上で入力したものを代入できるようにしています.EditorGUILayout.TextFieldではエディタウィンドウ上でテキスト入力が可能になります.最初の引数「"ExMenu Name"」は入力ボックスの左横に書かれている文字(オプションのラベル)に該当し,2つ目の引数「exMenuName」が実際に入力する文字の部分になります.ここでは初期値として「exMenuName = "ExMenu"」となっているのでエディタウィンドウを開いた時にあらかじめ""ExMenu""と入力されていることになります.
次にEditorGUILayout.ObjectFieldはアセットをドラッグ&ドロップで持ってくることができるボックスを作ります.typeofはこのボックスに入るものがどのような種類のアセットなのかを指定する引数で今回はフォルダーなのでDefaultAssetを選択しています.また,最後のfalseはHierarchy内のオブジェクトを受け付けるかどうかで,こちらをtrueにするとシーン上にあるオブジェクトからドラッグ&ドロップできるようになります.フォルダには必要ないのでfalseにしています.
最後にボタンです.こちらでは「Generate ExMenu」と書かれたボタンが押された場合に行う処理について書いています.中身については下のところで書いていきます.
以上を入力すると以下のようなエディタウィンドウを表示することができます(図3).
ExpressionsMenuを生成する
結論,以下のように書けばとりあえず動きます.
void GenerateExMenu()
{
VRCExpressionsMenu currentExMenu = CreateInstance<VRCExpressionsMenu>();
currentExMenu.name = exMenuName;
ExpressionControl addExMenuControl = new ExpressionControl();
currentExMenu.controls.Add(addExMenuControl);
addExMenuControl.name = exMenuControlsName;
addExMenuControl.type = ExpressionControl.ControlType.Toggle;
var exParam = new ExpressionControl.Parameter
{
name = exParameterName
};
addExMenuControl.parameter = exParam;
addExMenuControl.value = 1;
string exMenuFolderPath = AssetDatabase.GetAssetPath(exMenuFolder);
AssetDatabase.CreateAsset(currentExMenu, exMenuFolderPath + "/" + exMenuName+ ".asset");
AssetDatabase.SaveAssets();
}
では何をしているかなのですが,まず最初の部分についてです.
VRCExpressionsMenu currentExMenu = CreateInstance<VRCExpressionsMenu>();
currentExMenu.name = exMenuName;
これは実際に生成するExpressionsMenuです.まずはcurrentExMenuと名付けたものがどのようなものなのかを宣言し,CreateInstanceにて生成したインスタンスをcurrentExMenuに代入するという操作になります.CreateInstanceの後ろについている「<>」はジェネリクスといい,こちらで指定した型のオブジェクトしか追加することができなくなります.こうすることで別の型のものを追加してエラーを吐くということがなくなります.そしてcurrentExMenu.nameによってこのcurrentExMenuの名前をエディタウィンドウで決めたexMenuNameであると指定しています.
VRC.SDK3.Avatars.ScriptableObjects.VRCExpressionsMenu.Control addExMenuControl = new VRC.SDK3.Avatars.ScriptableObjects.VRCExpressionsMenu.Control();
currentExMenu.controls.Add(addExMenuControl);
次にExpressionsMenuの中のControlを追加する処理を書きます.ところで""VRC.SDK3.Avatars.ScriptableObjects.VRCExpressionsMenu.Control""ってすごい長いですよね.見にくいので省略したものを作ってあげましょう.
using ExpressionControl = VRC.SDK3.Avatars.ScriptableObjects.VRCExpressionsMenu.Control;
これを""using VRC.SDK3.Avatars.ScriptableObjects;""の下にでも書いておけばよいと思います.すると上のコードは次のようになります.
ExpressionControl addExMenuControl = new ExpressionControl();
currentExMenu.controls.Add(addExMenuControl);
すっきりしましたね.これはusingエイリアスディレクティブという機能なのですが,名前空間に別名をつけてあげることができます.長い名前に別名をつけるとこのようにすっきりするのでわかる範囲で行うのが良いと思います.
実際にコードを見ていきます.まずは""addExMenuControl""という""VRCExpressionsMenu.Control""のインスタンス(クラスや構造体から作られたもの)を作ります.インスタンスを作るならCreateInstanceなのではと思わなくはないですが,CreateInstanceはScriptableObjectsのインスタンスを作るときに使うもので,今回はVRCExpressionsMenu.Controlのインスタンスなので「new ~」を使っています.
ここで作ったaddExMenuControlを""currentExMenu.controls.Add""によってcurrentExMenuのControlのリストに追加することができます.ここまででようやく何も設定されていないControlが一つ追加されたことになります.次にリストに追加したControlの具体的な中身を決めます.
addExMenuControl.name = exMenuControlsName;
addExMenuControl.type = ExpressionControl.ControlType.Toggle;
var exParam = new ExpressionControl.Parameter{name = exParameterName};
addExMenuControl.parameter = exParam;
addExMenuControl.value = 1;
ここで追加しているのは4つ:名前・タイプ・パラメータ・パラメータの値です.名前はそのままなのですが,タイプは以下の6つから選択することになります.
Button
FourAxisPuppet
RadialPuppet
SubMenu
Toggle
TwoAxisPuppet
今回はオブジェクトの出し入れとかができるようにするためにToggleを選ぶことにします.もちろん入れたい機能によってここは選択は変わります.ToggleとButton(おそらくvalueのところをintではなくfloatに変える必要はあるがRadialPuppetも?)は以下同じですが,SubMenuの場合は例えば以下のようにExpressionsMenuそれ自体を指定することで導入できます.
AddSubExMenuControl.subMenu = expressionMenus[i]; // for文で追加しているのでiがついています
パラメータについては一番最初に導入したのがstring型でパラメータの名前でした.ここでは""ExpressionControl.Parameter""を追加しないといけないので新たに設定する必要があります.それを""exParam""とし,そのexParamの名前を""name = exParameterName""で指定しています.そしてその次の ""addExMenuControl.parameter"" でControlにexParameterNameの名前がついたパラメータを追加,~.valueでそのパラメータの値を設定します.
※自分はあまりこだわらないので追加しませんでしたが,以下のように書くことでControlのアイコンも設定することができます:
addExMenuControl.icon = AssetDatabase.LoadAssetAtPath<Texture2D>(.pngや.jpgまで含んだアイコン画像のパス);
ここで一つControlのついたExMenuの生成が終わりました.最後に作ったExMenuを保存していきます.
string exMenuFolderPath = AssetDatabase.GetAssetPath(exMenuFolder);
AssetDatabase.CreateAsset(currentExMenu, exMenuFolderPath + "/" + exMenuName+ ".asset");
AssetDatabase.SaveAssets();
まずは保存先の指定です.今回は保存先のフォルダをあらかじめ指定してあります.このフォルダのパスを取得し(AssetDatabase.GetAssetPath(~)),このパスに作ったExMenuを実際に作っていきます(CreateAsset(~)).ここで作られたcurrentExMenuはメモリ上には保存されますが,一時的なものでエディタを閉じても保存されているようにするためには最後にSaveAssetsを呼び出す必要があります.
以上で生成と保存が終わりました.おつかれさまです.Unity上でフォルダとファイルを作った人はこれで終わり,作っていない人はこのスクリプトを「.cs」の拡張子で保存してUnity上の「Editor」という名前のフォルダに入れるだけです.Ctrl+Sでスクリプトを保存するのを忘れないようにしましょう.
コード全体
以下,参考用にコード全体を書いておきます.
using UnityEditor;
using UnityEngine;
using VRC.SDK3.Avatars.ScriptableObjects;
using ExpressionControl = VRC.SDK3.Avatars.ScriptableObjects.VRCExpressionsMenu.Control;
public class ExMenuGenerator : EditorWindow
{
string exMenuName = "ExMenu";
string exMenuControlsName = "Control";
string exParameterName = "Parameter";
DefaultAsset exMenuFolder;
[MenuItem("pyocopel/ExMenuGenerator")]
public static void ShowWindow() // ウィンドウを表示するためのメソッド
{
GetWindow<ExMenuGenerator>("ExMenuGenerator"); // ウィンドウを作成またはフォーカス
}
void OnGUI()
{
EditorGUILayout.LabelField("ExMenuGenerator");
GUILayout.Space(5);
exMenuName = EditorGUILayout.TextField("ExMenu Name", exMenuName);
exMenuControlsName = EditorGUILayout.TextField("Controls Name", exMenuControlsName);
exParameterName = EditorGUILayout.TextField("Parameter Name", exParameterName);
exMenuFolder = (DefaultAsset)EditorGUILayout.ObjectField("生成するExMenuの保存先", exMenuFolder, typeof(DefaultAsset), false);
if (GUILayout.Button("Generate ExMenu"))
{
GenerateExMenu();
}
}
void GenerateExMenu()
{
VRCExpressionsMenu currentExMenu = CreateInstance<VRCExpressionsMenu>();
currentExMenu.name = exMenuName;
ExpressionControl addExMenuControl = new ExpressionControl();
currentExMenu.controls.Add(addExMenuControl);
addExMenuControl.name = exMenuControlsName;
addExMenuControl.type = ExpressionControl.ControlType.RadialPuppet;
var exParam = new ExpressionControl.Parameter
{
name = exParameterName
};
addExMenuControl.parameter = exParam;
addExMenuControl.value = 1;
string exMenuFolderPath = AssetDatabase.GetAssetPath(exMenuFolder);
AssetDatabase.CreateAsset(currentExMenu, exMenuFolderPath + "/" + exMenuName+ ".asset");
AssetDatabase.SaveAssets();
}
}
あとは自分で実験したりしていろいろ作ってみてください.別のnoteでこれから作った排他的にオブジェクトをONOFFするアニメーションを作りつつパラメータとメニューも追加しちゃうエディタ拡張のコードでも置いておきます.
…….ほんとは素人じゃなくてちゃんと知識のある人に書いてもらいたいしなんならVRCのドキュメントが簡単に検索してすぐ見つかるところにあればそれの日本語訳だけでも充分だと思うんですけどね…….何か間違いがあれば教えてください.
― 了 ―
pyocopel
この記事が気に入ったらサポートをしてみませんか?