【Unity】【エディタ拡張】エディタ拡張チートシート

自分用メモ。随時更新。

Array / List型の入力フィールドを描画する(インスペクタ拡張の場合)

f:id:halya_11:20180708133714p:plain

インスペクタ拡張の場合は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の場合)

f:id:halya_11:20180708133855p:plain

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();
    }
}

ウィンドウの中央にラベルを表示する

f:id:halya_11:20180708134140p:plain

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を取得)

f:id:halya_11:20180712222155p:plain

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);
    }
}

ファイル保存パネルを表示する(フルパスを取得)

light11.hatenadiary.com

Texture2Dをpngとして保存する

light11.hatenadiary.com

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を表示する

f:id:halya_11:20180713194015p:plain

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でコンポーネントを右クリックしたときのメニューを追加する

f:id:halya_11:20180718121159p:plain

light11.hatenadiary.com

light11.hatenadiary.com

特定の型のアセットのみを取得

エディタでのみ使う設定ファイルをScriptableObjectで作るときなどに使用します。
AssetDatabase.FindAssets()を利用するのがポイント。

var assets = AssetDatabase.FindAssets("t:" + typeof(SomeClass).Name)
    .Select(guid => {
        var path = AssetDatabase.GUIDToAssetPath(guid);
        return AssetDatabase.LoadAssetAtPath<SomeClass>(path);
    });

Tooltipのようなポップアップを表示する

light11.hatenadiary.com

インスペクタ表示をカスタムする

light11.hatenadiary.com

文字のケースをUnityのプロパティ表示と揃える

たとえば"_someValue"という文字列を"Some Value"のようにインスペクタに表示されるときのケースに変換する。

var propertyName = ObjectNames.NicifyVariableName("_someValue")

アイコン付きのプロパティを描画する

f:id:halya_11:20180817113554p:plain

ひとまず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"));

リッチテキストを使う

light11.hatenadiary.com

ShaderGUIでプロパティの横にヘルプアイコンを付ける

f:id:halya_11:20181114164259p:plain

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のすべての子オブジェクトに同じ処理を行う

light11.hatenadiary.com

オブジェクトがヒエラルキーにあるものかどうかを判定する

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を開く際に他のウィンドウとドッキングする

f:id:halya_11:20190325152903p:plain

下記のようにGetWindow()の引数にドッキングしたいWindowの型を渡します。

using UnityEditor;

public class ChildWindow : EditorWindow
{
    public static void Open()
    {
        // diredDockNextTo引数に渡した型のWindowにドッキングする
        GetWindow<ChildWindow>("Child Window", typeof(ParentWindow));
    }
}

さらにWindowを並べて表示したりサイズを変更したりしたいところですが現状難しそうです。
内部的にはDockAreaクラスを使ってやってそうです。

github.com

フルパスを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;
}

関連

light11.hatenadiary.com

light11.hatenadiary.com