《土豆荣耀》重构笔记(十八)使用自定义Inspector窗口拓展Generator


前言

  到目前为止,我们已经实现了使用Generator来生成怪物不可交互物体可拾取道具的功能。但我们发现,现有的Generator只能做到在固定的时刻、在固定的位置随机生成某个Prefab,我们希望能拓展Generator的功能。Generator拓展后的功能需求如下:

Generator拓展后的功能需求

  1. 能动态地决定是使用固定的时间间隔还是使用随机的时间间隔来实例化预设对象,如果使用固定的时间间隔,那么要设置时间间隔的长度,如果使用随机的时间间隔,那么要设置最短的时间间隔最长的时间间隔
  2. 能动态地决定是在固定位置上还是在随机位置上实例化预设对象,如果选择在随机位置上实例化预设对象,那么需要能分别设置X、Y坐标的随机范围

拓展Generator.cs

  在知道了Generator拓展后的功能需求之后,我们先根据需求来改写Generator.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// 朝向
public enum Orientation {
    Left,        // 固定朝左
    Right,        // 固定朝右
    Random,        // 随机朝向
    None        // 不需要考虑朝向
}

public class Generator : MonoBehaviour {
    [Tooltip("多久之后开始实例化预设对象")]
    public float GenerateDelay = 2f;

    [Tooltip("是否使用随机时间间隔来实例化预设对象")]
    public bool RandomGenerateInterval = false;
    [Tooltip("实例化预设对象的最短时间间隔")]
    public float MinGenerateInterval;
    [Tooltip("实例化预设对象的最长时间间隔")]
    public float MaxGenerateInterval;
    [Tooltip("实例化预设对象的固定时间间隔")]
    public float GenerateInterval = 3f;

    [Tooltip("是否在随机的X坐标上实例化预设对象")]
    public bool RandomGeneratePositionX = false;
    [Tooltip("实例化预设对象时的最小X坐标")]
    public float MinGeneratePositionX;
    [Tooltip("实例化预设对象时的最大X坐标")]
    public float MaxGeneratePositionX;

    [Tooltip("是否在随机的Y坐标上实例化预设对象")]
    public bool RandomGeneratePositionY = false;
    [Tooltip("实例化预设对象时的最小Y坐标")]
    public float MinGeneratePositionY;
    [Tooltip("实例化预设对象时的最大Y坐标")]
    public float MaxGeneratePositionY;

    [Tooltip("预设对象的朝向")]
    public Orientation PrefabOrientation = Orientation.Right;
    [Tooltip("预设对象")]
    public GameObject[] Prefabs;

    private ParticleSystem m_Particle;

    private void Awake() {
        // 获取引用
        m_Particle = GetComponent<ParticleSystem>();

        if(Prefabs == null || Prefabs.Length == 0) &#123;
            Debug.LogError("请至少为Prefabs添加一个预设对象");
        &#125;
    &#125;

    private void Start () &#123;
        // 开始随机生成的协程
        StartCoroutine(RandomGenerate());
    &#125;

    private IEnumerator RandomGenerate() &#123;
        yield return new WaitForSeconds(GenerateDelay);

        while(true) &#123;
            // 确定下一次实例化预设对象的时间间隔
            float interval = GenerateInterval;
            if(RandomGenerateInterval) &#123;
                interval = Random.Range(MinGenerateInterval, MaxGenerateInterval);
            &#125;

            yield return new WaitForSeconds(interval);

            // 实例化预设对象
            Generate();
        &#125;
    &#125;

    // 实例化预设对象
    private void Generate() &#123;
        // 随机选择要实例化的预设的下标
        int index = Random.Range(0, Prefabs.Length);

        // 确定生成位置的X坐标
        float x = transform.position.x;
        if(RandomGeneratePositionX) &#123;
            x = Random.Range(MinGeneratePositionX, MaxGeneratePositionX);
        &#125;

        // 确定生成位置的Y坐标
        float y = transform.position.y;
        if(RandomGeneratePositionY) &#123;
            y = Random.Range(MinGeneratePositionY, MaxGeneratePositionY);
        &#125;

        // 更新位置
        transform.position = new Vector3(x, y, transform.position.z);

        // 实例化预设对象
        GameObject prefab = Instantiate(Prefabs[index], transform.position, Quaternion.identity);

        // 播放粒子特效
        if(m_Particle != null) &#123;
            m_Particle.Play();
        &#125;

        // 不需要考虑朝向
        if(PrefabOrientation == Orientation.None) &#123;
            return;
        &#125;

        if(PrefabOrientation == Orientation.Left) &#123;
            Wander wander = prefab.GetComponent<Wander>();
            if(wander.FacingRight) &#123;
                wander.Flip();
            &#125;
            return;
        &#125; 

        if(PrefabOrientation == Orientation.Right) &#123;
            Wander wander = prefab.GetComponent<Wander>();
            if(!wander.FacingRight) &#123;
                wander.Flip();
            &#125;
            return;
        &#125;

        if(PrefabOrientation == Orientation.Random) &#123;
            Wander wander = prefab.GetComponent<Wander>();
            // 有一半的概率进行翻转
            if(Random.value <= 0.5) &#123;
                wander.Flip();
            &#125;
            return;
        &#125;
    &#125;
&#125;

代码说明:
  这里,我们使用了RandomGenerateIntervalRandomGeneratePositionXRandomGeneratePositionY这三个bool变量来分别设置是否使用随机的时间间隔来实例化预设对象、是否在随机的X坐标上实例化预设对象和是否在随机的Y坐标上实例化随机对象。

  改写完Generator.cs,我们打开Generator物体下任意一个带有Generator.cs的子物体,可以看到在Inspector窗口多出了很多变量。

默认的Inspector窗口


使用自定义Inspector窗口隐藏无关变量

  我们希望在设置参数的时候,一些无关的变量能被隐藏不显示出来。因此,我们需要来自定义Generator.csInspector窗口的显示规则。首先,我们在Assets\Scripts下新建一个名为Editor的文件夹,然后在Assets\Scripts\Editor下新建一个名为GeneratorEditor的C#脚本:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(Generator))]
public class GeneratorEditor : Editor &#123;
    // 判断是否显示折叠框
    private bool m_ShowPrefabs;

    // Generator的ScriptAble实例对象
    private Generator m_Generator;
    // 用于存放Prefab
    private List<GameObject> m_PrefabList = null;

    public override void OnInspectorGUI() &#123;
        // 获取Generator脚本的ScriptAble实例对象
        m_Generator = (Generator)target;

        // 显示GenerateDelay参数
        m_Generator.GenerateDelay = EditorGUILayout.FloatField(new GUIContent("Generate Delay", "多久之后开始实例化预设对象"), m_Generator.GenerateDelay);

        // 显示RandomGenerateInterval参数
        m_Generator.RandomGenerateInterval = EditorGUILayout.Toggle(new GUIContent("Random Generate Interval", "多久之后开始实例化预设对象"), m_Generator.RandomGenerateInterval);
        if(m_Generator.RandomGenerateInterval) &#123;
            // 显示MinGenerateInterval和MaxGenerateInterval参数
            m_Generator.MinGenerateInterval = EditorGUILayout.FloatField(new GUIContent("Min Generate Interval", "实例化预设对象的最短时间间隔"), m_Generator.MinGenerateInterval);
            m_Generator.MaxGenerateInterval = EditorGUILayout.FloatField(new GUIContent("Max Generate Interval", "实例化预设对象的最长时间间隔"), m_Generator.MaxGenerateInterval);

            // 确保MaxGenerateInterval的数值比MinGenerateInterval大
            if(m_Generator.MaxGenerateInterval < m_Generator.MinGenerateInterval) &#123;
                m_Generator.MaxGenerateInterval = m_Generator.MinGenerateInterval;
            &#125;
        &#125; else &#123;
            // 显示GenerateInterval参数
            m_Generator.GenerateInterval = EditorGUILayout.FloatField(new GUIContent("Generate Interval", "实例化预设对象的固定时间间隔"), m_Generator.GenerateInterval);
        &#125;

        // 显示RandomGeneratePositionX参数
        m_Generator.RandomGeneratePositionX = EditorGUILayout.Toggle(new GUIContent("RandomG enerate PositionX", "是否在随机的X坐标上实例化预设对象"), m_Generator.RandomGeneratePositionX);
        if(m_Generator.RandomGeneratePositionX) &#123;
            // 显示MinGeneratePositionX和MaxGeneratePositionX参数
            m_Generator.MinGeneratePositionX = EditorGUILayout.FloatField(new GUIContent("Min Generate PositionX", "实例化预设对象的最小X坐标"), m_Generator.MinGeneratePositionX);
            m_Generator.MaxGeneratePositionX = EditorGUILayout.FloatField(new GUIContent("Max Generate PositionX", "实例化预设对象的最大X坐标"), m_Generator.MaxGeneratePositionX);

            // 确保MaxGeneratePositionX的数值比MinGeneratePositionX大
            if(m_Generator.MaxGeneratePositionX < m_Generator.MinGeneratePositionX) &#123;
                m_Generator.MaxGeneratePositionX = m_Generator.MinGeneratePositionX;
            &#125;
        &#125;

        // 显示RandomGeneratePositionY参数
        m_Generator.RandomGeneratePositionY = EditorGUILayout.Toggle(new GUIContent("RandomG enerate PositionY", "是否在随机的Y坐标上实例化预设对象"), m_Generator.RandomGeneratePositionY);
        if(m_Generator.RandomGeneratePositionY) &#123;
            // 显示MinGeneratePositionY和MaxGeneratePositionY参数
            m_Generator.MinGeneratePositionY = EditorGUILayout.FloatField(new GUIContent("Min Generate PositionY", "实例化预设对象的最小Y坐标"), m_Generator.MinGeneratePositionY);
            m_Generator.MaxGeneratePositionY = EditorGUILayout.FloatField(new GUIContent("Max Generate PositionY", "实例化预设对象的最大Y坐标"), m_Generator.MaxGeneratePositionY);

            // 确保MaxGeneratePositionY的数值比MaxGeneratePositionY大
            if(m_Generator.MaxGeneratePositionY < m_Generator.MinGeneratePositionY) &#123;
                m_Generator.MaxGeneratePositionY = m_Generator.MinGeneratePositionY;
            &#125;
        &#125;

        // 显示PrefabOrientation参数
        m_Generator.PrefabOrientation = (Orientation)EditorGUILayout.EnumPopup(new GUIContent("Prefab Orientation", "预设对象的朝向"), m_Generator.PrefabOrientation);

        // 获取当前有哪些已赋值的Prefab
        if(m_PrefabList == null) &#123;
            m_PrefabList = new List<GameObject>(m_Generator.Prefabs);
        &#125;
        // 绘制折叠框,设置Prefabs参数
        m_ShowPrefabs = EditorGUILayout.Foldout(m_ShowPrefabs, new GUIContent("Prefabs", "预设对象"));
        if(m_ShowPrefabs) &#123;
            // 缩进
            EditorGUI.indentLevel++;

            for (int i = 0; i < m_Generator.Prefabs.Length; i++) &#123;
                EditorGUILayout.BeginHorizontal();
                // 绘制元素赋值框
                m_Generator.Prefabs[i] = (GameObject)EditorGUILayout.ObjectField(new GUIContent("Prefab", "预设对象"), m_Generator.Prefabs[i], typeof(GameObject), false);

                // 删除指定元素
                if (GUILayout.Button("Remove")) &#123;
                    m_PrefabList.RemoveAt(i);
                    m_Generator.Prefabs = m_PrefabList.ToArray();
                &#125;
                EditorGUILayout.EndHorizontal();
            &#125;
            // 增加一个元素
            if (GUILayout.Button("Add")) &#123;
                m_PrefabList.Add(null);
                m_Generator.Prefabs = m_PrefabList.ToArray();
            &#125;

            // 取消缩进
            EditorGUI.indentLevel--;
        &#125;

        // 当值改变时,将target标记为已修改
        if (GUI.changed) &#123;
            EditorUtility.SetDirty(target);
        &#125;
    &#125;
&#125;

代码说明:

  1. 因为GeneratorEditor.cs是用于自定义Inspector窗口脚本,所以必须将GeneratorEditor.cs放在一个名为Editor的文件夹下(Editor文件夹可以位于任意位置)
  2. CustomEditor是Unity提供的Attribute,我们需要使用它来标识我们要自定义哪个脚本Inspector窗口
  3. 由于绘制Inspector窗口的代码是在OnInspectorGUI这一函数里执行的,我们需要让GeneratorEditor这个类继承UnityEditor命名空间下的Editor类,并使用关键字override覆写OnInspectorGUI函数
  4. targetEditor类提供的成员变量,我们可以通过它来获取当前脚本实例对象
  5. 由于我们覆写了OnInspectorGUI函数,所以Unity默认的脚本序列化代码不会被执行,我们需要自己使用EditorGUILayout提供的静态函数来序列化各个变量(同理,脚本里面的ToolTip也失效了,如果需要在Inspector窗口提示变量的含义,我们也需要自己编写)
  6. 由于EditorGUILayout没有提供数组的序列化方法,所以我们需要逐一对数组的每个元素进行序列化
  7. 最后,我们使用EditorUtility提供的静态方法SetDirty在我们修改参数之后将target标记为已修改,否则Prefab的实例对象上的参数会被Prefab上的值覆盖

  编写完GeneratorEditor.cs之后,再次打开Generator物体下任意一个带有Generator.cs的子物体,可以看到此时Inspector窗口少了很多变量。而且根据我们的选择,Inspector窗口显示的变量也会动态改变。

自定义的Inspector窗口


调整各个Generator的参数

  有了拓展后的Generator.cs,接下来,我们就要来调整各个Generator的参数了。需要注意的是,因为GeneratorPosition需要实例化的Prefab以及实例化Prefab时的朝向都没有改变,所以不再列出。

场景中各个Generator的设置

  • EnemyGenerator:
    • Generator Delay: 2
    • Random Generate Interval: true
    • Min Generate Interval: 2
    • Max Generate Interval: 8
    • RandomG enerate PositionX: true
    • Min Generate PositionX: -17.5
    • Max Generate PositionX: -6.5
    • RandomG enerate PositionY: false
  • EnemyGenerator (1):
    • Generator Delay: 2
    • Random Generate Interval: true
    • Min Generate Interval: 2
    • Max Generate Interval: 8
    • RandomG enerate PositionX: true
    • Min Generate PositionX: -6
    • Max Generate PositionX: 6
    • RandomG enerate PositionY: false
  • EnemyGenerator (2):
    • Generator Delay: 2
    • Random Generate Interval: true
    • Min Generate Interval: 2
    • Max Generate Interval: 8
    • RandomG enerate PositionX: true
    • Min Generate PositionX: 6.5
    • Max Generate PositionX: 17.5
    • RandomG enerate PositionY: false
  • CabGenerator:
    • Generator Delay: 1
    • Random Generate Interval: true
    • Min Generate Interval: 5
    • Max Generate Interval: 15
    • RandomG enerate PositionX: false
    • RandomG enerate PositionY: false
  • CabGenerator (1):
    • Generator Delay: 5
    • Random Generate Interval: true
    • Min Generate Interval: 5
    • Max Generate Interval: 15
    • RandomG enerate PositionX: false
    • RandomG enerate PositionY: false
  • BusGenerator:
    • Generator Delay: 4
    • Random Generate Interval: true
    • Min Generate Interval: 5
    • Max Generate Interval: 15
    • RandomG enerate PositionX: false
    • RandomG enerate PositionY: false
  • BusGenerator (1):
    • Generator Delay: 8
    • Random Generate Interval: true
    • Min Generate Interval: 5
    • Max Generate Interval: 15
    • RandomG enerate PositionX: false
    • RandomG enerate PositionY: falsealse
    • Prefab Orientation: Random
  • SwanGenerator:
    • Generator Delay: 2
    • Random Generate Interval: true
    • Min Generate Interval: 5
    • Max Generate Interval: 15
    • RandomG enerate PositionX: false
    • RandomG enerate PositionY: true
    • Min Generate PositionY: -7
    • Max Generate PositionY: 2
  • SwanGenerator (1):
    • Generator Delay: 2
    • Random Generate Interval: true
    • Min Generate Interval: 5
    • Max Generate Interval: 15
    • RandomG enerate PositionX: false
    • RandomG enerate PositionY: true
    • Min Generate PositionY: -7
    • Max Generate PositionY: 2
  • PickupGenerator:
    • Generator Delay: 5
    • Random Generate Interval: true
    • Min Generate Interval: 15
    • Max Generate Interval: 20
    • RandomG enerate PositionX: true
    • Min Generate PositionX: -15
    • Max Generate PositionX: -5
    • RandomG enerate PositionY: false
  • PickupGenerator (1):
    • Generator Delay: 5
    • Random Generate Interval: true
    • Min Generate Interval: 15
    • Max Generate Interval: 20
    • RandomG enerate PositionX: true
    • Min Generate PositionX: 5
    • Max Generate PositionX: 15
    • RandomG enerate PositionY: false

  运行游戏,可以发现此时各个Generator能按照我们的设置,在随机的时刻随机的位置随机生成某个Prefab。


后言

  至此,我们的拓展Generator的工作就全部完成了。需要说明的是,现有的Generator还不是最优的,因为我们没有利用资源池来回收在场景中生成的对象,而是不断地实例化和销毁对象。因为采用资源池的方法来改写Generator,我们需要改造Prefab,使得我们可以重置并回收已生成的资源,工作量较大,所以在这里我们并不提及,感兴趣的读者可以自己进一步拓展。

  最后,本篇文章所做的修改,可以在PotatoGloryTutorial这个仓库的essay16分支下看到,读者可以clone这个仓库到本地进行查看。


参考链接

  1. Building a Custom Inspector
  2. Editor类的说明文档
  3. EditorGUILayout的说明文档
  4. EditorUtility的说明文档

文章作者: RainbowCyan
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 RainbowCyan !
 上一篇
《土豆荣耀》重构笔记(十九)使用单例模式实现游戏主逻辑管理器 《土豆荣耀》重构笔记(十九)使用单例模式实现游戏主逻辑管理器
前言  到目前为止,我们已经能在场景中控制角色进行移动、攻击怪物并拾取道具,但我们还没有做出一个完整的游戏,我们还需要加入游戏的胜负条件。 游戏的胜负条件 胜利条件:每击杀一个怪物获得100分,当已获得的分数达到预
下一篇 
《土豆荣耀》重构笔记(十七)随机生成可拾取道具 《土豆荣耀》重构笔记(十七)随机生成可拾取道具
前言  我们在前面的文章中,已经实现了随机生成足够多的怪物的功能。为了能延长游戏时间,增加游戏的趣味性,我们需要随机生成一些可拾取的道具,来恢复角色的血量或者增加角色可释放的炸弹数。可拾取道具的需求如下: 可拾取道具的
  目录