Unity工具开发教程笔记(1/4)

源视频教程地址为Youtube

什么是Unity工具开发程序员

工具开发程序员目标客户是项目组里其他成员,它们创造出unity原本没有的功能,优化项目的流程。并且你只需要会C#就能搞定这些。

FieldAttributes

比如说我们有ExplosiveBarrel这样的类。比如我们可以首先在radius字段上添加 Range注解,这样在编辑器中,字段会呈现为slider,并且范围为(1f, 8f)。

public class ExplosiveBarrel : MonoBehaviour
{
    [Range(1f, 8f)]
    public float radius = 2;
    public float damage = 10;
    public Color Color = Color.red;
}

辅助图标 Gizmos

工具使用起来越顺手越好。我们希望调整radius的时候,能够在场景(scene view)窗口看到炸yao桶的作用半径。我们可以使用OnDrawGizmos。如果我们在场景中取消了Gizmos的显示,这个函数也将不会被调用。

OnDrawGizmos 函数会被UnityEditor调用,并将效果显示在Scene面板。
也可以使用OnDrawGizmosSelected如果你想在选中物件的时候才显示Gizmos。

具体可以再看看B站的教程

private void OnDrawGizmos()
{
    Gizmos.color = Color;
    Gizmos.DrawWireSphere(transform.position, radius);
    Gizmos.color = Color.yellow;
    Gizmos.DrawLine(transform.position, transform.position+Vector3.up*radius);

}

虽然上面的代码很少,但是对项目的作用是巨大的。

程序集Assembly和ExecuteInEditMode注解

Unity程序集定义中说到每次修改一处代码,Unity会重新编译所有其他的代码,对于代码迭代来说这是有害的,因为增加了编译时间。

  • Every time you change one script, Unity has to recompile all the other scripts, increasing overall compilation time for iterative code changes.

Unity之所以会立刻编译代码,是因为它希望代码的修改在编辑器中是立刻可见的。并且编译结束后,它会尝试重新加载编译生成的程序集。也就是我们编写的代码实时地更新到了Unity这个应用程序中。

如果我们对Barrel类添加构造函数,

public ExplosiveBarrel()
{
    Debug.Log("Create new Barrel");
}

那么当我们拖拽一个MonoBehaviorGameObejct上的时候,我们会发现这个构造函数被调用。也就是说,这个时候Unity在运行中创建了一个ExplosiveBarrel类。虽然是编辑器,但编辑器中也运行着游戏逻辑。只不过默认情况下,有些逻辑在Play Mode中被调用(Awake , Start), 有些逻辑在Edit Mode中会被调用(OnDrawGizmos)。

如果此时,我们添加一个OnEnable函数,这个函数不会被调用,但不意味着Unity此时不能调用它。

private void OnEnable()
{
    Debug.Log("OnEnable Explosive Barrel");
}

只要将这个脚本拖拽到对应的GameObject上,就会生成一个ExplosiveBarrel类。只是Edit模式下,Unity选择了不调用OnEnable等函数。我们可以通过对类添加ExecuteInEditMode注解来实现Unity调用这些函数的效果。

ExecuteInEditMode注解让组件能够在编辑模式执行LifeCycle中的回调函数

[ExecuteAlways]
public class ExplosiveBarrel : MonoBehaviour

By default, MonoBehaviours are only executed in Play Mode. By adding this attribute, any instance of the MonoBehaviour will have its callback functions executed while the Editor is in Edit Mode too.

管理类 ExplosiveBarrelManager

这里我们建立一个管理类,用于追踪所有的ExplosiveBarrel。为了避免Unity序列化以及初始化先后顺序带来的问题,这里原作者将字段设置为static。

public class ExplosiveBarrelManager : MonoBehaviour
{
    public static List<ExplosiveBarrel> ExplosiveBarrelList = new List<ExplosiveBarrel>();


    private void OnDrawGizmos()
    {
        foreach(var barrel in ExplosiveBarrelList)
        {
            Gizmos.DrawLine(transform.position, barrel.transform.position);
        }
    }
}

Handles类 & 预处理器

对于管理类中的Gizmos.DrawLine,我们可以用Handles.DrawAAPolyLine来替换。Handles类有着更为丰富的接口。需要注意的是,Handles类属于UnityEditor,而不属于UnityEngine,因此项目打包不加预处理器会找不到对应的类库

using System.Collections.Generic;
using UnityEngine;

#if UNITY_EDITOR
using UnityEditor;
#endif

public class ExplosiveBarrelManager : MonoBehaviour
{
    public static List<ExplosiveBarrel> ExplosiveBarrelList = new List<ExplosiveBarrel>();

    private void OnDrawGizmos()
    {
#if UNITY_EDITOR
        foreach(var barrel in ExplosiveBarrelList)
        {
            Handles.DrawAAPolyLine(transform.position, barrel.transform.position);
        }
#endif
    }
}

贝塞尔曲线 Drawing Bezier Curves

Unity自己的文档有个示例,可以看看。感觉就是3次贝塞尔曲线,startTangent参数就是3次贝塞尔曲线第二个点的位置。

原视频作者也有个视频讲贝塞尔曲线

private void OnDrawGizmos()
{
    foreach(var barrel in ExplosiveBarrelList)
    {

        var height = transform.position.y - barrel.transform.position.y;
        var offset = Vector3.up* height * 0.5f;
        
		Handles.zTest = UnityEngine.Rendering.CompareFunction.LessEqual;
        Handles.DrawBezier(transform.position, barrel.transform.position, 
                           transform.position - offset, barrel.transform.position + offset,
                           barrel.Color, EditorGUIUtility.whiteTexture, 1f);
    }
}

Curves and Splines 讲了如何自己画一个贝塞尔曲线。

Material & Mesh Modification Pitfalls

如果我们想修改材质的颜色,我们可以通过下面的代码实现。

//实例化一个material,额外的draw call
// will duplicate the material
GetComponent<MeshRenderer>().material.color = Color;

// will modify the asset
GetComponent<MeshRenderer>().sharedMaterial.color = Color;//修改材质的 asset

但它们都有各自的问题,一个会引入额外的draw call,一个会修改asset。

材质属性块 MaterialPropertyBlock

MaterialPropertyBlock is used by Graphics.DrawMesh and Renderer.SetPropertyBlock. Use it in situations where you want to draw multiple objects with the same material, but slightly different properties. For example, if you want to slightly change the color of each mesh drawn. Changing the render state is not supported.

Note that this is not compatible with SRP Batcher. Using this in the Universal Render Pipeline (URP), High Definition Render Pipeline (HDRP) or a custom render pipeline based on the Scriptable Render Pipeline (SRP) will likely result in a drop in performance.

MaterialPropertyBlock 可以帮我们使用同一个材质(material)渲染不同的物品,并且修改其中的一些属性。

static readonly int shaderPropColor = Shader.PropertyToID("_Color");
private MaterialPropertyBlock materialPropertyBlock;
public MaterialPropertyBlock MaterialPropertyBlock
{
    get
    {
        if (materialPropertyBlock == null) materialPropertyBlock = new MaterialPropertyBlock();
        return materialPropertyBlock;
    }
}

private void ApplyColor()
{
    MeshRenderer meshRenderer = GetComponent<MeshRenderer>();
    MaterialPropertyBlock.SetColor(shaderPropColor, Color);
    meshRenderer.SetPropertyBlock(materialPropertyBlock);
}

调用ApplyColor函数我们就可以通过MaterialPropertyBlock设置shader中属性。

接着我们需要调用ApplyColor函数。

private void OnEnable()
{
    ApplyColor();
    ExplosiveBarrelManager.ExplosiveBarrelList.Add(this);
}

private void OnValidate()
{
    ApplyColor();
}

OnValidate会在脚本的值发生改变,或者脚本被加载的时候调用。

ScriptableObjects

游戏设计中,炸弹桶的类型是有限的,通常我们不会一个个设置炸yao桶的属性,而是可以给予一个类型,比如烈性炸yao桶,小型炸yao桶,被浸湿的炸yao桶。这个可以通过Prefab实现,本次教程,作者使用了ScriptableObjects.

One of the main use cases for ScriptableObjects is to reduce your Project’s memory usage by avoiding copies of values.

You can use the CreateAssetMenu attribute to make it easy to create custom assets using your class

[CreateAssetMenu(menuName = "ScriptableObjects/BarrelType")]
public class BarrelType : ScriptableObject 
{
    [Range(1f, 8f)]
    public float radius = 1;
    public float damage = 5;
    public Color Color = Color.red;
}

序列化

The Inspector window shows the value of the serialized fields of the inspected objects.

字段序列化必须满足特定的条件,比如不能是静态字段(static),字段必须是public或者有SeriazlizeField注解,不能是HashSet等类型。

对于自定义类型的序列化,如果类型派生自UnityEngine.Object并且unity能够保存这个字段(比如MonoBehaviorScriptableObject),Unity会将这个字段序列化为到那个实例的一个引用(通过FileID?)。自己随随便便定义一个类派生自Object,Unity不知道怎么序列化它。

如果类型不派生自UnityEngine.Object,那么这个字段的数据会被内联到引用这个字段的类的序列化的数据里。详情见脚本序列化。

自定义属性面板 Make a Custom Inspector

官方现在强烈推荐使用UI Toolkit来自定义属性面板(新)

这里教程讲的是老方法。用IMGUI自定义属性面板(旧)的文档也可以看看。

Note: It’s strongly recommended to use the UI Toolkit to extend the Unity Editor, as it provides a more modern, flexible, and scalable solution than IMGUI.

新建一个Editor文件夹,这里的代码会归为新的程序集 Assembly-CSharp-Editor ,该程序集不会被打包到构建的游戏中,只在开发阶段使用。不新建文件夹也能运行,但是生成项目时会有问题。

The CustomEditor attribute informs Unity which component it should act as an editor for. The CanEditMultipleObjects attribute tells Unity that you can select multiple objects with this editor and change them all at the same time. Unity executes the code in OnInspectorGUI it displays the editor in the Inspector.

using UnityEditor;

//CustomEditor表示这个Inspector应该作用在哪里类上
[CustomEditor(typeof(BarrelType))]
[CanEditMultipleObjects]
public class BarrelTypeEditor : Editor
{
    public override void OnInspectorGUI()
    {
        GUILayout.Label("Draw your inspector here");
    }
}

IMGUI布局模型分为固定布局(Fixed Layout)和自动布局(Automatic Layout)。如果想使用自动布局,使用GUILayout代替GUI。另外GUIGUILayout在编辑模式和Play Mode都可以使用。EditorGUIEditorGUILayout只能在编辑模式使用。

原作者此时罗列了一些她觉得有用的函数EditorGUILayout.EnumPopupGUILayout.HorizontalSliderGUILayout.BeginHorizontal(HorizontalScope),GUILayout.WidthEditorGUILayout.ObjectField

另外还可以使用GUI.skin,EditorStyles.helpBox等修改样式。

[CustomEditor(typeof(BarrelType))]
[CanEditMultipleObjects]
public class BarrelTypeEditor : Editor
{

    public override void OnInspectorGUI()
    {
        BarrelType barrelType= (BarrelType)target; //The object being inspected.

        barrelType.radius = EditorGUILayout.FloatField("radius", barrelType.radius);
        barrelType.damage = EditorGUILayout.FloatField("damage", barrelType.damage);
        barrelType.Color = EditorGUILayout.ColorField("color", barrelType.Color);
    }
}

SerializedProperty and SerializedObject are classes for editing properties on objects in a completely generic way that automatically handles undo, multi-object editing and Prefab overrides.

上面这样的代码有个坏处是无法Undo。我们可以使用SerializedProperty来实现undo。

Note that flushing data to a Unity object via SerializedObject.ApplyModifiedProperties will not respect any data validation logic you may have in property setters associated with the serialized fields.

[CustomEditor(typeof(BarrelType))]
[CanEditMultipleObjects]
public class BarrelTypeEditor : Editor
{
    SerializedProperty propRadius;
    SerializedProperty propDamage;
    SerializedProperty propColor;

    private void OnEnable()
    {
        propRadius = serializedObject.FindProperty("radius");
        propDamage = serializedObject.FindProperty("damage");
        propColor = serializedObject.FindProperty("Color");
    }

    public override void OnInspectorGUI()
    {
        serializedObject.Update();
        EditorGUILayout.PropertyField(propRadius);
        EditorGUILayout.PropertyField(propDamage);
        EditorGUILayout.PropertyField(propColor);
        if(serializedObject.ApplyModifiedProperties()){
            // update color
        }
    }
}

ContextMenu 和 MenuItem

ContextMenu

Custom context menu

MenuItem

Menu item example

Editor Window

public class EditModeWindow : EditorWindow
{
    [MenuItem("Window/ToolWindow &e")]
    static void InitWindow()
    {
        var window = EditorWindow.GetWindow(typeof(EditModeWindow));
        window.Show();

    }

    private void OnGUI()
    {
        GUIStyle tempFontStyle = new GUIStyle();
        tempFontStyle.normal.textColor = Color.red;
        if(GUILayout.Button("Add Edit Mode Enhance"))
        {
            AddEditModeScript2Barrel();
        }
        if(GUILayout.Button("Remove Edit Mode Enhance"))
        {
            RemoveEditModeScript4Barrel();
        }
    }

    private void AddEditModeScript2Barrel()
    {
        var list = GameObject.FindObjectsOfType<GameObject>(true);

        foreach( GameObject go in list)
        {
            if (go.GetComponent<ExplosiveBarrel>() == null)
                continue;

            if (go.GetComponent<ExplosiveBarrel_Edit>() != null)
                continue;

            go.AddComponent<ExplosiveBarrel_Edit>();
        }
    }

    private void RemoveEditModeScript4Barrel()
    {
        var list = GameObject.FindObjectsOfType<GameObject>(true);

        foreach (GameObject go in list)
        {
            var editScript = go.GetComponent<ExplosiveBarrel_Edit>();
            if (editScript == null)
                continue;

            DestroyImmediate(editScript);
        }
    }
}
posted @ 2023-05-24 19:41  dewxin  阅读(89)  评论(0编辑  收藏  举报