1.19 Learning Unity game development from 0 -- extended editor

In the previous chapters, we have been working in the editor, and after editing resources and code development, we can directly click the play button in the editor to actually run the game, but sometimes, We may want to add a button to the menu, so that we can directly perform some batch editing actions, or we want to draw a special edit for our custom data structure like the effect of array elements displayed on the Inspector panel interface, then we need to extend the Unity editor at this time.

Add a menu to the editor

We know that there are a series of menus at the top of the Unity editor window. We can add our custom menus by writing C# code. Now we create a new script resource called MenuTest. The code is as follows:

using UnityEditor;
using UnityEngine;

public static class MenuTest
{
    [MenuItem("MenuTest/Say Hello World")]
    public static void SayHelloWorld()
    {
        Debug.Log("Hello World!");
    }
}

It can be seen that we have created a new C# class, but it does not inherit from MonoBehaviour, but is a pure C# class directly. It doesn’t matter if it is written as static or not, but the method we use as a menu must be a static method, so we wrote A SayHelloWorld method, after execution, it will output a line of logs to the Console window. In order to make this function appear in the menu, we added an attribute MenuItem, which comes from the UnityEditor module. The content of the attribute is the location of our menu, separated by / , the first part is the menu name directly displayed on the interface, each of the following / is the option level inside the menu, let's take a look at the effect:

If we click on this menu, the static function we wrote will be executed, and a Hello World! will be output to our Console window.

If we wanted to have a deeper menu, we could write something like

[MenuItem("MenuTest/Say Hello World/Say1/Say2")]

Then we will see a menu like this:

OK, we now know how to make menus, so that we can do some batch operations we want. For example, when we packaged the game before, we always opened scenes one by one and added them to our Build Settings. It's hard, so can we do this directly through a menu? sure! Let's modify our code slightly:

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

public static class MenuTest
{
    [MenuItem("MenuTest/Add All Scenes to Build Settings")]
    public static void AddAllScenesToBuildSettings()
    {
        //获取所有场景
        string[] scenes = AssetDatabase.FindAssets("t:Scene");
        if (scenes == null || scenes.Length == 0)
        {
            Debug.Log("No scenes found.");
            return;
        }

        List<EditorBuildSettingsScene> editorBuildSettingsScenes = new List<EditorBuildSettingsScene>();

        //将所有场景添加到BuildSettings中
        foreach (string sceneGuid in scenes)
        {
            string scenePath = AssetDatabase.GUIDToAssetPath(sceneGuid);
            EditorBuildSettingsScene buildScene = new EditorBuildSettingsScene(scenePath, true);
            editorBuildSettingsScenes.Add(buildScene);
        }

        EditorBuildSettings.scenes = editorBuildSettingsScenes.ToArray();
        Debug.Log("All scenes have been added to Build Settings.");
    }
}

Here we call the AssetDatabase.FindAssets API, which is a method specially used to find resources in the editor. It is similar to Resources.Load, but the scope is all resources in the project. The parameter t:Scene indicates that the type is a scene resource. About this The magic string used for searching inside, Unity's official API documentation is actually very vague, but basically you can understand how to write it.

After finding all the resources of the scene type in the project, we get a bunch of guid strings, because we need to set the API of the scene included in the package and require us to provide the path of the scene resource, so we need to convert the resource path through the guid string , that is, through the AssetDatabase.GUIDToAssetPath API, and then create a new EditorBuildSettingsScene structure through this path.

Create this structure for each scene resource and pass it to the EditorBuildSettings.scenes array for assignment.

OK, let's click this menu to execute and then open Build Settings to see what happened:

Good guy, not only our Assets folder came in, but also the scene resources of other function packages in Packages. Of course, we don’t need these resources, so we can delete them manually.

From this we can understand that in addition to providing us with the APIs we need when the game is running, Unity also provides a series of APIs to help us call the editor's functions, so that we can better complete our work, and these editors The related APIs are all under the UnityEditor module.

UnityEngin and UnityEditor

Now that we talk about UnityEditor, we need to clarify the concept and scope of use of UnityEditor. If we package the game directly now, we will get a compilation error:

Assets\MenuTest.cs(7,6): error CS0246: The type or namespace name 'MenuItemAttribute' could not be found (are you missing a using directive or an assembly reference?)
Assets\MenuTest.cs(7,6): error CS0246: The type or namespace name 'MenuItem' could not be found (are you missing a using directive or an assembly reference?)

It seems strange at first, the error message prompts that the definition of MenuItemAttribute and MenuItem is not found in the MenuTest code we just wrote, but we just ran our code obviously!

But it is reasonable to think about it carefully, because we are calling the functions provided by the UnityEditor module instead of UnityEngine, which means that our functions are only available under the editor, but our game packaging will definitely not bring the functions of the editor. As a result, an error will obviously be reported that the relevant type definition cannot be found.

So in order to solve this problem, Unity provides two solutions:

  1. Put all the code related to the editor function under the Editor folder. This rule is similar to the Resources folder, as long as the parent directory is called Editor, no matter how many levels of parent directory it is, that is to say, under any folder called Editor All code will not be packaged when packaging.
  2. Using preprocessor macros in C# is very similar to C++, but Unity will pre-define some, UNITY_EDITOR is one of them, for example:
#if UNITY_EDITOR
// 这里面的代码打包不会带上
#endif

This way of writing is very suitable for calling some editor functions in the code that needs to be executed in the game, such as a certain component, so as to provide the debugging ability to run under the editor, but it will not be brought into the game package, of course it is written like this When using UnityEditor; this part must also be wrapped with #if UNITY_EDITOR, which is easy to forget.

Extended editor interface

Unity also supports developers to draw the editor interface by themselves. For example, the editing interface serialized from the array is the function of a previous tripartite component, which was later incorporated into the official engine as the default display method.

Before learning how to expand, we need to know what drawing scheme the editor uses: IMGUI. If you haven’t touched it, it may be confusing. Let’s take a look at the official instructions:

    void OnGUI() {
        if (GUILayout.Button("Press Me"))
            Debug.Log("Hello!");
    }

The above code can draw such a button in the interface, and the click state of this button will be returned directly through the drawing function, and then directly judge and execute the logic of outputting a Hello on the spot.

Of course, it is inaccurate to say this. Such a UI drawing method will actually run OnGUI multiple times. The first pass is for layout calculations, the second pass is for processing events such as user input, and the third pass is for rendering. Of course, what I said It may also vary with different IMGUI implementations, but the same.

If you still don't understand such a small code of Unity, you can directly look at the more famous GUI library in IMGUI:

https://github.com/ocornut/imgui​github.com/ocornut/imgui

OK, this kind of UI writing method, in fact, if you don’t understand it in depth, you can just use it, and you can get started quickly.

Unity provides a variety of ways for us to extend the editor:

  1. Draw a window directly by yourself, just like drawing an Inspector panel, all the content inside this window is drawn by ourselves
  2. Draw additional content in the existing window, which is actually similar to 1, except that we need to find this window first, and then continue to draw
  3. Customize the edit control display under the editor of the serialized member

Both 1 and 2 can be learned directly by referring to the official documentation ( Unity - Manual: Editor Windows ), because they are all GUI.xxx interface drawing, which is relatively easy to understand, so let everyone learn by themselves here.

We mainly explain 3 here.

For the member variables we write in the component, we can make it participate in serialization by writing it as public, so that the edit control will be displayed on the Inspector panel, for example, an input box will be displayed for us to enter data for Int.

Recall that we had a FireController before that needed to fill in the resource path of the Bullet, so that it could be loaded with Resources.Load, but this path is a pure string, and it is easy for us to write it wrong. If it is wrong, it will be triggered in the game It can only be discovered by corresponding logic. If the game process is relatively long, this will undoubtedly increase the cost of rework.

So we expect to assign this path like a drag-and-drop prefab assignment, but we hope to store the path in the serialized file, not a resource reference (it seems meaningless to do so, but it is for dynamic loading of resources. necessary).

OK, let's take a look at the methods provided by Unity: Property Drawers

We create a new script called PrefabPathDrawer, and we also need to create a new script called PrefabPathVariableAttribute

The code of PrefabPathVariableAttribute is relatively simple:

using UnityEngine;

public class PrefabPathVariableAttribute : PropertyAttribute
{
}

Inherited from the PropertyAttribute class, this PropertyAttribute is actually inherited from C#’s Attribute, as the name implies, it is an attribute for marking, because we have no additional requirements, so this attribute has no parameters, it is empty, and it is only used to mark the custom drawing that needs to be done like this member variables.

Then we need to write PrefabPathDrawer:

using UnityEditor;
using UnityEngine;

[CustomPropertyDrawer(typeof(PrefabPathVariableAttribute))]
public class PrefabPathDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        base.OnGUI(position, property, label);
    }
}

PrefabPathDrawer inherits from PropertyDrawer and has a property CustomPropertyDrawer. The parameter is the property type PrefabPathVariableAttribute we just used to mark. Obviously, Unity also uses the CustomPropertyDrawer property to collect all the PropertyDrawers that we need to customize drawing and the scope of this custom drawing.

The callback function we want to rewrite is OnGUI. This function gives us several parameters. Position indicates where the current interface is drawn. We should continue drawing from position. Property is the serialization of data affected by our redrawing logic. In this embodiment, the result of data serialization can be directly manipulated by operating the property, and the label is the label field from the upper layer, and we generally do not need to change it.

OK, imagine what we need to do now to achieve our goal:

  1. Get the current path string through SerializedProperty
  2. Get the resource itself through this path
  3. Draw an input box that drags and assigns Prefab instead of a text input box. This input box takes the resources found in 2 as the current input
  4. Get the current value of the input box of 3, compare whether it has changed, and if it has changed, get the path of this resource
  5. Update the data of SerializedProperty through the path of 4

So let's modify the code:

using UnityEditor;
using UnityEngine;

[CustomPropertyDrawer(typeof(PrefabPathVariableAttribute))]
public class PrefabPathDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        // 1. 获取当前填的字符串是啥
        string prefabPath = property.stringValue;
        // 2. 找资源
        GameObject res = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
        // 3. 绘制Object的输入框,而非文本的,同时开启检查变更
        EditorGUI.BeginChangeCheck();
        Object newRes = EditorGUI.ObjectField(position, label, res, typeof(GameObject), false);

        if (EditorGUI.EndChangeCheck())
        {
            // 4. 如果发生变更了,获取这个新资源的路径
            string newPath = AssetDatabase.GetAssetPath(newRes);
            // 5. 更新数据
            property.stringValue = newPath;
        }
    }
}

AssetDatabase is used here for resource loading and path finding, and EditorGUI.ObjectField can help us draw an input box for dragging and dropping GameObject objects. The last false of this API is more important, which means that objects in the scene are not allowed to be assigned , since what we need to assign is a resource, it is not allowed to fill in. If it is allowed, then the object of the component with this member can only exist in the scene. After all, assigning a resource that does not exist in the scene to an object in the scene itself There is a problem with this citation.

EditorGUI.BeginChangeCheck() and EditorGUI.EndChangeCheck() can help us check if there is any GUI content change in the middle, and if so, we try to update the serialized value.

Next, we need to add this attribute to the member variable we want to customize the drawing:

public class FireController : MonoBehaviour
{
    private bool isMouseDown = false;
    private float lastFireTime = 0f;
    private Vector3 fireDirection;
    private AddVelocity bullet;
    [PrefabPathVariable]
    public string bulletResourcesPath;
    public float fireInterval = 0.1f;
    public Transform fireBeginPosition;

Here we add the PrefabPathVariable attribute to bulletResourcesPath. Note that the Attribute is gone. In fact, this is a rule of C#. The type declaration of the attribute is PrefabPathVariableAttribute, but when it is actually used for marking, the Attribute on the tail needs to be omitted.

Then we don't need to run the game, just select FireController in the scene to see the effect:

Originally, the first input box here is the input box for inputting strings, but now it has become an input box that can be dragged and dropped into the Prefab. We can drag and drop GameObject to it at will, save it, and then view the serialized result of this scene:

It can be seen that although we are in the form of assignment objects on the interface, in fact, what we serialize and store is indeed the resource path.

But there is a problem here. The resource path we obtained using the AssetDatabase series API is actually in the Assets directory, but our resource loading is using Resrouces.Load, so the path will not meet the requirements, so we also load the resource. Need to do some adaptation:

  1. Cut off the previous Resources folder path
  2. remove the file suffix

Let's slightly adjust the code for loading resources in FireController:

    private void Start()
    {
        if (bulletResourcesPath != null)
        {
            int lastIndex = bulletResourcesPath.LastIndexOf("Resources/", StringComparison.OrdinalIgnoreCase);
            if (lastIndex != -1)
            {
                bulletResourcesPath = bulletResourcesPath.Substring(lastIndex + "Resources/".Length);
                string fileExtension = Path.GetExtension(bulletResourcesPath);
                bulletResourcesPath = bulletResourcesPath.Substring(0, bulletResourcesPath.Length - fileExtension.Length);
                bullet = Resources.Load<AddVelocity>(bulletResourcesPath);
            }
        }
    }

In the same Fire function, remember to judge the bullet resource as empty.

OK, now run the game again, and you should be able to fire bullets normally.

One more thing:

PropertyDrawer also comes from UnityEditor, we need to move this code to the Editor folder as well:

next chapter

In fact, a lot of basic engine usage methods have been taught. Through the study of these usage methods, some learning methodologies can actually be sorted out. In the next chapter, we will sort out all the content learned in this chapter and piece together some basic ones. Ideas and methods, so that our engine entry can basically be touched in a certain direction, and it can also lay a better foundation for more in-depth learning later.

Guess you like

Origin blog.csdn.net/z175269158/article/details/130294911
Recommended