前面文章里的拓展都局限在Inspector窗口中,依附于挂载MonoBehaviour脚本的游戏物体存在。下面我们将学习如何创建一个和Inspector同级的窗口,作为独立的编辑器使用。
我们假设这样一个需求:敌人都挂载一个叫“Enemy”的脚本,并在这个脚本上有一个“EnemyData”的ScriptableObjct的引用。我们需要设计一个EnemyEditor,拥有独立的窗口来编辑EnemyData,并可以创建EnemyData或在场景创建拥有EnemyData的敌人物体。
EnemyData:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using UnityEngine;
[CreateAssetMenu(fileName = "EnemyData000", menuName = "EnemyData")]
public class EnemyData : ScriptableObject
{
public int id;
public string enemyName; //名称
public Sprite portrait; //头像
public float maxHealth; //血量

public bool canAttack; //是否可攻击
public float attack; //攻击力
public float attackCoolDown; //攻击冷却时间

public string description; //简介
public string dropItems; //掉落物名称
public float dropRate; //掉落概率
}

Enemy:
1
2
3
4
5
6
7
8
9
10
11
using UnityEngine;

public class Enemy : MonoBehaviour
{
[SerializeField] private EnemyData data;

public void SetData(EnemyData data)
{
this.data = data;
}
}

接下来我们编写具体的窗口。Unity中的自定义窗口需要继承EditorWindow类,可以且最好放进Editor文件夹。窗口类(采用IMGUI方案)有以下两个核心方法:

  • OpenWindow:自定义的公共静态方法,名称随意。用于唤出窗口
  • OnGUI:和Start、Update等相同的生命周期函数,用于绘制自定义的GUI元素和交互逻辑
    大致结构是这样的:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    using UnityEngine;
    using UnityEditor;
    public class EnemyEditor : EditorWindow
    {
    [MenuItem("Tools/EnemyEditor")] //将唤出按钮添加到顶部菜单栏
    public static void OpenWindow()
    {
    EnemyEditor wd = GetWindow<EnemyEditor>(); //唤出窗口
    wd.titleContent = new GUIContent("敌人编辑器"); //添加标题
    }

    private void OnGUI()
    {
    //绘制元素和交互逻辑
    }
    }
    上面的代码已经可以允许我们通过Unity编辑器上方菜单栏打开名为“敌人编辑器”的空窗口。而还未编辑的OnGUI是我们的老朋友,想必大家已经很清楚该如何自由发挥了。
    直接上代码和效果演示
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    using UnityEngine;
    using UnityEditor;
    using System.IO;
    public class EnemyEditor : EditorWindow
    {
    private EnemyData editingData; //正在编辑的数据
    public const string DATA_PATH = "Assets/Data/EnemyData"; //储存Asset的文件夹地址

    [MenuItem("Tools/EnemyEditor")] //将唤出按钮添加到顶部菜单栏
    public static void OpenWindow()
    {
    EnemyEditor wd = GetWindow<EnemyEditor>(); //唤出窗口
    wd.titleContent = new GUIContent("敌人编辑器"); //添加标题
    }

    private void OnEnable()
    {
    editingData = new EnemyData();
    }

    private void OnGUI()
    {
    GetInput();

    //两个按钮
    if (GUILayout.Button("创建EnemyData"))
    {
    CreateEnemyData();
    editingData = new EnemyData();
    }
    if (GUILayout.Button("创建敌人到场景"))
    {
    CreateEnemy();
    editingData = new EnemyData();
    }
    }
    /// <summary>
    /// 将当前编辑中的EnemyData作为Asset储存到指定路径
    /// 创建的资源根据id命名,同id资源将会被覆盖
    /// </summary>
    /// <returns>创建的资源</returns>
    private EnemyData CreateEnemyData()
    {
    if (!Directory.Exists(DATA_PATH)) Directory.CreateDirectory(DATA_PATH);
    string path = Path.Combine(DATA_PATH, "EnemyData" + string.Format("{0:D3}", editingData.id) + ".asset");
    EnemyData has = AssetDatabase.LoadAssetAtPath(path, typeof(EnemyData)) as EnemyData;
    if (has == editingData) return editingData;
    if (has != null) AssetDatabase.DeleteAsset(path);
    AssetDatabase.CreateAsset(editingData, path);
    return editingData;
    }
    /// <summary>
    /// 将当前编辑中的EnemyData作为Asset储存到指定路径,
    /// 并在场景中添加拥有该数据的敌人对象
    /// </summary>
    /// <returns>创建的敌人对象</returns>
    private GameObject CreateEnemy()
    {
    GameObject g = new GameObject(editingData.enemyName);
    Enemy e = (Enemy)g.AddComponent(typeof(Enemy));
    e.SetData(CreateEnemyData());
    return null;
    }
    /// <summary>
    /// 创建各参数的输入框,接受输入
    /// </summary>
    private void GetInput()
    {
    SerializedObject so = new SerializedObject(editingData);
    so.UpdateIfRequiredOrScript();

    editingData.id = EditorGUILayout.IntField(new GUIContent("ID"), editingData.id);
    editingData.enemyName = EditorGUILayout.TextField(new GUIContent("名称"), editingData.enemyName);
    editingData.portrait = EditorGUILayout.ObjectField(new GUIContent("头像"),
    editingData.portrait, typeof(Sprite), false) as Sprite;
    editingData.maxHealth = EditorGUILayout.FloatField(new GUIContent("生命值"), editingData.maxHealth);
    editingData.canAttack = EditorGUILayout.Toggle(new GUIContent("是否可攻击"), editingData.canAttack);
    if (editingData.canAttack)
    {
    editingData.attack = EditorGUILayout.FloatField(new GUIContent("攻击力"), editingData.attack);
    editingData.attackCoolDown = EditorGUILayout.FloatField(new GUIContent("攻击冷却时间"),
    editingData.attackCoolDown);
    }

    editingData.dropItem = EditorGUILayout.TextField(new GUIContent("掉落物"), editingData.dropItem);
    editingData.dropRate = EditorGUILayout.Slider(new GUIContent("掉落率"), editingData.dropRate, 0, 1);

    EditorGUILayout.LabelField(new GUIContent("简介"));
    editingData.description = EditorGUILayout.TextArea(editingData.description,
    GUILayout.Height(EditorGUIUtility.singleLineHeight * 4));

    so.ApplyModifiedProperties();
    }
    }
    效果展示
    QQ截图20230815175614.png

延伸

EditorWindow生命周期