自分用メモ。随時更新。
- Array / List型の入力フィールドを描画する(インスペクタ拡張の場合)
- Array / List型の入力フィールドを描画する(EditorWindowの場合)
- ウィンドウの中央にラベルを表示する
- エディタウィンドウで仕切り線(横線)を引く
- ファイル保存パネルを表示する(Asset Pathを取得)
- ファイル保存パネルを表示する(フルパスを取得)
- Texture2Dをpngとして保存する
- Inspectorのプロパティがクリックされたことを検知する
- GenericMenuを表示する
- Sceneビューがクリックされたことを検知する
- Sceneビュー上でクリックされた位置を取る
- Inspectorでコンポーネントを右クリックしたときのメニューを追加する
- MenuItemにショートカットキー(ホットキー)を設定する
- 特定の型のアセットのみを取得
- Tooltipのようなポップアップを表示する
- インスペクタ表示をカスタムする
- 文字のケースをUnityのプロパティ表示と揃える
- アイコン付きのプロパティを描画する
- フォルダが無かったら作る
- 空フォルダだったら削除する(親も含めて再帰的に判定)
- フォルダをコピーする
- アセットがなければ新規作成し、すでにあれば更新する
- Labelのサイズを取得する
- リッチテキストを使う
- ShaderGUIでプロパティの横にヘルプアイコンを付ける
- GameObjectのすべての子オブジェクトに同じ処理を行う
- オブジェクトがヒエラルキーにあるものかどうかを判定する
- オブジェクトがAssetかどうか調べる
- EditorWindowを開く際に他のウィンドウとドッキングする
- フルパスをAssetPathに変換する
- バイト配列を文字列に変換する
- DateTime型を文字列に変換
- 関連
Array / List型の入力フィールドを描画する(インスペクタ拡張の場合)
インスペクタ拡張の場合はEditorGUILayout.PropertyField()の第二引数をtrueにするだけで、
あとは他のプロパティ描画方法と同様。
using System.Collections.Generic; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif public class SomeBehaviour : MonoBehaviour { [SerializeField] private List<int> _someList; } #if UNITY_EDITOR [CustomEditor(typeof(SomeBehaviour))] public class SomeBehaviourEditor : Editor{ public override void OnInspectorGUI() { serializedObject.Update(); // 第二引数をtrueにする EditorGUILayout.PropertyField(serializedObject.FindProperty("_someList"), true); serializedObject.ApplyModifiedProperties(); } } #endif
Array / List型の入力フィールドを描画する(EditorWindowの場合)
EditorWindowはScriptableObjectの派生クラスなのでSerializeFieldな変数を定義できる。
あとは自身のScriptableObjectを取得してEditorGUILayout.PropertyField()で描画するだけ。
第二引数はtrue。
using System.Collections.Generic; using UnityEngine; using UnityEditor; public class SomeWindow : EditorWindow { // SerializeFieldを定義 [SerializeField] private List<int> _someList; [MenuItem("Window/Some Window")] private static void Open() { var window = GetWindow<SomeWindow>("Some Window"); } private void OnGUI() { // 自身のSerializedObjectを取得 var so = new SerializedObject(this); so.Update(); // 第二引数をtrueにしたPropertyFieldで描画 EditorGUILayout.PropertyField(so.FindProperty("_someList"), true); so.ApplyModifiedProperties(); } }
ウィンドウの中央にラベルを表示する
using (new EditorGUILayout.VerticalScope()) { GUILayout.FlexibleSpace(); using (new EditorGUILayout.HorizontalScope()) { GUILayout.FlexibleSpace(); var style = new GUIStyle(GUI.skin.label); style.wordWrap = true; EditorGUILayout.LabelField(text, style); GUILayout.FlexibleSpace(); } GUILayout.FlexibleSpace(); }
エディタウィンドウで仕切り線(横線)を引く
var splitterRect = EditorGUILayout.GetControlRect(false, GUILayout.Height(1)); splitterRect.x = 0; splitterRect.width = position.width; EditorGUI.DrawRect(splitterRect, Color.Lerp(Color.gray, Color.black, 0.7f));
ファイル保存パネルを表示する(Asset Pathを取得)
public static void SaveSample() { // 推奨するディレクトリがあればpathに入れておく var path = ""; string ext = "png"; if (string.IsNullOrEmpty(path) || System.IO.Path.GetExtension(path) != "." + ext) { // 推奨する保存パスがないときはシーンのディレクトリをとってきたりする(用途次第) if (string.IsNullOrEmpty(path)) { path = UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene().path; if (!string.IsNullOrEmpty(path)) { path = System.IO.Path.GetDirectoryName(path); } } if (string.IsNullOrEmpty(path)) { path = "Assets"; } } // ディレクトリがなければ作る else if (System.IO.Directory.Exists(path) == false) { System.IO.Directory.CreateDirectory(path); } // ファイル保存パネルを表示 var fileName = "name." + ext; fileName = System.IO.Path.GetFileNameWithoutExtension(AssetDatabase.GenerateUniqueAssetPath(System.IO.Path.Combine(path, fileName))); path = EditorUtility.SaveFilePanelInProject("Save Some Asset", fileName, ext, "", path); if (!string.IsNullOrEmpty(path)) { // 保存処理 Debug.Log(path); } }
ファイル保存パネルを表示する(フルパスを取得)
Texture2Dをpngとして保存する
Inspectorのプロパティがクリックされたことを検知する
using UnityEngine; using UnityEditor; public class Sample : MonoBehaviour { } [CustomEditor(typeof(Sample))] public class SampleEditor: Editor{ public override void OnInspectorGUI() { base.OnInspectorGUI(); DrawClickMenu(); } public void DrawClickMenu(){ var rect = GUILayoutUtility.GetLastRect(); if (rect.Contains(Event.current.mousePosition) && Event.current.type == EventType.MouseUp && Event.current.button == 0) { // 左クリック時の処理 } if (rect.Contains(Event.current.mousePosition) && Event.current.type == EventType.MouseUp && Event.current.button == 1) { // 右クリック時の処理 } } }
GenericMenuを表示する
GenericMenu menu = new GenericMenu(); menu.AddItem(new GUIContent ("メニュー名1"), enabled, () => { Debug.Log("処理1"); }); menu.AddItem(new GUIContent ("メニュー名2"), enabled, () => { Debug.Log("処理2"); }); menu.AddItem(new GUIContent ("メニュー名3"), enabled, () => { Debug.Log("処理3"); }); menu.ShowAsContext();
Sceneビューがクリックされたことを検知する
MonoBehaviourのOnDrawGizmosでやる場合。
public class Sample : MonoBehaviour { private void OnDrawGizmos() { if (Event.current.type == EventType.MouseUp && Event.current.button == 0) { Debug.Log("clicked"); } } }
Sceneビュー上でクリックされた位置を取る
private void OnDrawGizmos() { if(Event.current != null && Event.current.type == EventType.mouseUp && Event.current.button == 0){ // スクリーン座標が得られる var clickedPosition = Event.current.mousePosition; // 上下逆になるので補正 clickedPosition.y = SceneView.currentDrawingSceneView.camera.pixelHeight - clickedPosition.y; // ちなみにこの後座標変換にカメラを使いたい場合 // UnityEditor.SceneView.currentDrawingSceneView.cameraを使う } }
Inspectorでコンポーネントを右クリックしたときのメニューを追加する
MenuItemにショートカットキー(ホットキー)を設定する
特定の型のアセットのみを取得
エディタでのみ使う設定ファイルをScriptableObjectで作るときなどに使用します。
AssetDatabase.FindAssets()を利用するのがポイント。
var assets = AssetDatabase.FindAssets("t:" + typeof(SomeClass).Name) .Select(guid => { var path = AssetDatabase.GUIDToAssetPath(guid); return AssetDatabase.LoadAssetAtPath<SomeClass>(path); });
Tooltipのようなポップアップを表示する
インスペクタ表示をカスタムする
文字のケースをUnityのプロパティ表示と揃える
たとえば"_someValue"という文字列を"Some Value"のようにインスペクタに表示されるときのケースに変換する。
var propertyName = ObjectNames.NicifyVariableName("_someValue")
アイコン付きのプロパティを描画する
ひとまずEditorGUIのみ。
/// <summary> /// ボタンアイコン付きのPropertyFieldを描画する /// </summary> private bool IconedPropertyField(Rect position, SerializedProperty property, GUIContent iconContent, System.Action onClickIcon, GUIContent label = null, bool includeChildren = false) { if (iconContent.image == null) { Debug.LogWarning("invalid icon content."); return EditorGUI.PropertyField(position, property, label, includeChildren); } var iconWidth = iconContent.image.width; var iconHeight = iconContent.image.height; var iconRect = position; iconRect.xMin = iconRect.xMax - iconWidth; // 縦の位置を調整 // 縦幅が大きすぎるアイコンは想定しない if (iconRect.height > iconHeight) { // アイコンが小さい場合に上下センタリングする iconRect.y += (iconRect.height - iconHeight) * 0.5f; } position.xMax -= iconWidth; if (GUI.Button(iconRect, iconContent, GUIStyle.none) && onClickIcon != null) { onClickIcon(); } return EditorGUI.PropertyField(position, property, label, includeChildren); }
使い方はこんな感じ。
IconedPropertyField(fieldRect, _property.someProperty, EditorGUIUtility.IconContent("_Help"), () => Debug.Log("clicked"));
フォルダが無かったら作る
Unity内外どっちにも対応。
UnityのAPIで作ると親フォルダが無かったらエラーになったり色々と面倒なのでC#の機能を使う。
/// <summary> /// Create the folder. Also create the parent folders if not exists. /// </summary> /// <param name="folderPath"></param> public static void CreateFolder(string folderPath) { if (folderPath != null) { Directory.CreateDirectory(folderPath); } #if UNITY_EDITOR AssetDatabase.Refresh(); #endif }
空フォルダだったら削除する(親も含めて再帰的に判定)
指定したフォルダが空フォルダだったら削除する。
その結果として親フォルダも空フォルダになったら削除する。
これを再帰的に処理する。
private static void DeleteEmptyFolders(string folderPath) { DeleteEmptyFoldersRecursive(folderPath); #if UNITY_EDITOR if (folderPath.StartsWith("Assets")) { AssetDatabase.Refresh(); } #endif } private static void DeleteEmptyFoldersRecursive(string folderPath) { if (Directory.Exists(folderPath)) { // Delete DS_Store if exists. var dsStorePath = Path.Combine(folderPath, ".DS_Store"); if (Directory.GetFiles(folderPath).Length == 1 && File.Exists(dsStorePath)) { File.Delete(dsStorePath); } if (Directory.GetFiles(folderPath).Length == 0 && Directory.GetDirectories(folderPath).Length == 0) { Directory.Delete(folderPath, false); #if UNITY_EDITOR // Delete the meta file of the directory. var metaPath = $"{folderPath}.meta"; if (File.Exists(metaPath)) { File.Delete(metaPath); } #endif var nextFolderPath = Path.GetDirectoryName(folderPath); if (!string.IsNullOrEmpty(nextFolderPath)) { DeleteEmptyFolders(nextFolderPath); } } } }
フォルダをコピーする
public static void CopyFolder(string srcFolderPath, string destFolderPath, bool copySubFolders) { var folder = new DirectoryInfo(srcFolderPath); if (!folder.Exists) { throw new DirectoryNotFoundException($"{srcFolderPath} is not found."); } if (!Directory.Exists(destFolderPath)) { Directory.CreateDirectory(destFolderPath); } // フォルダ内の全てのファイルを新しいフォルダにコピーする var files = folder.GetFiles(); foreach (var file in files) { var newFilePath = Path.Combine(destFolderPath, file.Name); file.CopyTo(newFilePath, true); } // 子フォルダも再起的に処理する if (copySubFolders) { var folders = folder.GetDirectories(); foreach (var subFolder in folders) { var newFolderPath = Path.Combine(destFolderPath, subFolder.Name); CopyFolder(subFolder.FullName, newFolderPath, true); } } }
アセットがなければ新規作成し、すでにあれば更新する
/// <summary> /// アセットがなければ新規作成し、すでにあれば更新する /// </summary> private static void CreateOrUpdate(Object newAsset, string assetPath) { var oldAsset = AssetDatabase.LoadAssetAtPath<Object>(assetPath); if (oldAsset == null) { AssetDatabase.CreateAsset(newAsset, assetPath); } else { EditorUtility.CopySerializedIfDifferent(newAsset, oldAsset); AssetDatabase.SaveAssets(); } }
Labelのサイズを取得する
// EditorStyles.labelもしくはEditorStyles.boldLabelを複製してwordWrap等を設定 // wordWrapなどを設定しない場合は複製せずそのまま使ってもOK var style = new GUIStyle(EditorStyles.label); style.wordWrap = true; // CalcSizeでサイズを取得 style.CalcSize(new GUIContent("text"));
リッチテキストを使う
ShaderGUIでプロパティの横にヘルプアイコンを付ける
var prop = FindProperty("_MainTex", properties); var iconContent = EditorGUIUtility.IconContent("_Help"); var rect = EditorGUILayout.GetControlRect(false, MaterialEditor.GetDefaultPropertyHeight(prop)); materialEditor.DefaultShaderProperty(rect, prop, prop.displayName); rect.xMin += EditorStyles.label.CalcSize(new GUIContent(prop.displayName)).x; if(GUI.Button(rect, iconContent, GUIStyle.none)) { Debug.Log("clicked"); }
※Int型などの場合はクリックが効かなくなるので他の解決案が必要
GameObjectのすべての子オブジェクトに同じ処理を行う
オブジェクトがヒエラルキーにあるものかどうかを判定する
var selection = Selection.activeGameObject; var path = AssetDatabase.GetAssetOrScenePath(selection); if (!string.IsNullOrEmpty(path) && path.EndsWith(".unity")) { // .unityで終わっていたら(シーンのパスが取れていたら)Hierarchy上のオブジェクトであると判定する // Sceneアセットを選択中の場合はSelection.activeGameObjectがnullになるので問題ない Debug.Log("Hierarchy上にあります"); }
オブジェクトがAssetかどうか調べる
前節に関連して。
var selection = Selection.activeObject; var path = AssetDatabase.GetAssetPath(selection); if (!string.IsNullOrEmpty(path)) { Debug.Log("Assetです"); }
EditorWindowを開く際に他のウィンドウとドッキングする
下記のようにGetWindow()の引数にドッキングしたいWindowの型を渡します。
using UnityEditor; public class ChildWindow : EditorWindow { public static void Open() { // diredDockNextTo引数に渡した型のWindowにドッキングする GetWindow<ChildWindow>("Child Window", typeof(ParentWindow)); } }
さらにWindowを並べて表示したりサイズを変更したりしたいところですが現状難しそうです。
内部的にはDockAreaクラスを使ってやってそうです。
フルパスをAssetPathに変換する
var matchAssetPath = System.Text.RegularExpressions.Regex.Match(fullPath, "Assets/.*");
var assetPath = matchAssetPath.Value;
バイト配列を文字列に変換する
表示用に。
private string BytesToString(byte[] bytes) { var result = new StringBuilder(); for (var i = 0; i < bytes.Length; i++) { if (i >= 1) { result.Append(","); } var integer = (int)bytes[i]; result.Append(integer); } return result.ToString(); }
DateTime型を文字列に変換
DateTime型をシリアライズするときに使う。
const string DatetimeSerializeFormat = "yyyy-MM-dd HH:mm:ss"; public string DateTimeToString(DateTime dateTime) { return dateTime.ToString(DatetimeSerializeFormat); } private bool StringToDateTime(string dateTimeString, out DateTime result) { if (DateTime.TryParseExact(dateTimeString, DatetimeSerializeFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out var r)) { result = r; return true; } result = DateTime.MinValue; return false; }