Unity编辑器拓展之三十二:关于多选中Preview绘制探究

Unity编辑器拓展之三十二:关于多选中Preview绘制探究

个人博客-原文地址

前言

先来介绍一下本篇文章的背景,在项目中通过使用一个ScriptableObject,暂定名为SpriteSetting,SpriteSetting内通过一个字典序列化存储了一些Sprite(以SpriteName为key,以Sprite为Value),一般使用场景为一个图集对应一个SpriteSetting。那么在运行时,只需要Load这个SpriteSetting的so,就可以根据name这个key得到Sprite。

SpriteSetting ScriptableObject示意图

关于序列化字典,github有开源仓库,AssetStore上也有很多。

本示例使用的AsssetStore上的Serialized Dictionary Lite。地址【https://assetstore.unity.com/packages/tools/utilities/serialized-dictionary-lite-110992】
【注:示意图中的搜索框是我自己拓展的,不知道作者更新没有😁😁😁】

基于此,延伸出了一些编辑器拓展方面的需求:
1、选中Sprite时,支持Preview预览
2、支持多选,Preview预览显示多个

SpriteAtlasSetting

using System;
using RotaryHeart.Lib.SerializableDictionary;
using UnityEngine;

[CreateAssetMenu(menuName = "Game/Config/SpriteAtlasSetting")]
public class SpriteAtlasSetting : ScriptableObject
{
    //SerializableDictionaryBase是上述介绍的库,序列化字典的基类
    //SpriteAtlasDic以string为key,以Sprite为Value
    [Serializable]
    public class SpriteAtlasDic : SerializableDictionaryBase<string , Sprite> { }
    
    [SerializeField]
    private SpriteAtlasDic _spriteAtlasDic;

    //根据Key从字典中获取Sprite
    public Sprite TryGetSprite(string key)
    {
        _spriteAtlasDic.TryGetValue(key, out Sprite value);
        return value;
    }

    //判断是否包含某个Key
    public bool ContainsKey(string key)
    {
        return _spriteAtlasDic.ContainsKey(key);
    }
}

新建一个SpriteAtlasSetting,目前尚未自定义显示,示意图如下:

可以看到,Inspector面板底下的Preview空空如也,无法从这个so中得出Sprite长什么样子。

选中Sprite,绘制Preview

绘制Preview的前提是得知道当前选中了哪一个,或者哪几个。本文推荐的开源库里可以在RotaryHeart.Lib.SerializableDictionary.ReorderableList的Selected中拿到。SerializableDictionaryBase<TKey,TValue>的基类DrawableDictionary里包含了一个ReorderableList,这个ReorderableList要注意跟Unity自带的ReorderableList区分开,这里提到的ReorderableList是作者自己实现的一个。

绘制Preview主要用到了以下两个接口,AssetPreview.GetAssetPreview和EditorGUI.DrawTextureTransparent,前者是获取Preview,后者是绘制。

但是,绘制一个很简单,这里不做演示了,如何绘制多个呢。

unity 多选中Sprite Preview示意图

从图中可以看到,其中涉及到一个布局算法,当改变Preview宽和高时,其中的sprite示意图会跟着改变布局排列。

经过多番查看UnityEditor的开源部分代码(仓库地址,在Editor.cs中发现了以下函数:

下面的DrawPreview函数支持传入UnityObject数组,这里的UnityObject其实就是UnityEngine.Object,函数内根据传入的这个数组来绘制多个。我本意是想直接反射调用该函数,结果测试失败。在OnPreviewGUI函数里直接反射调用该函数会造成死循环。。。🤣🤣🤣

最后选择直接讲该布局算法拷贝出来。。。

新建一个DrawPreviewExtension脚本,源码如下;

//源码来源于https://github.com/Unity-Technologies/UnityCsReference/blob/2018.4/Editor/Mono/Inspector/Editor.cs
using UnityEditor;
using UnityEngine;
using UnityObject = UnityEngine.Object;

public static class DrawPreviewExtension
{
    const int kGridTargetCount = 25;
    const int kGridSpacing = 10;
    const int kPreviewMinSize = 55;
    const int kPreviewLabelHeight = 12;
    const int kPreviewLabelPadding = 5;
    static Styles s_Styles;
    class Styles
    {
        public GUIStyle preBackground = "PreBackground";
        public GUIStyle preBackgroundSolid = "PreBackgroundSolid";
        public GUIStyle previewMiniLabel = "PreMiniLabel";
        public GUIStyle dropShadowLabelStyle = "PreOverlayLabel";
    }

    public static void DrawPreview(Rect previewArea, UnityObject[] targets)
    {
        if (s_Styles == null)
            s_Styles = new Styles();

        //是否绘制多个目标的示意图
        if (targets.Length > 1)
        {
            Rect previewPositionInner = new RectOffset(16, 16, 20, 25).Remove(previewArea);
            int maxRows = Mathf.Max(1, Mathf.FloorToInt((previewPositionInner.height + kGridSpacing) / (kPreviewMinSize + kGridSpacing + kPreviewLabelHeight)));
            int maxCols = Mathf.Max(1, Mathf.FloorToInt((previewPositionInner.width + kGridSpacing) / (kPreviewMinSize + kGridSpacing)));
            int countWithMinimumSize = maxRows * maxCols;
            int neededCount = Mathf.Min(targets.Length, kGridTargetCount);
            bool fixedSize = true;
            int[] division = new int[2] {maxCols, maxRows};
            if (neededCount < countWithMinimumSize)
            {
                division = GetGridDivision(previewPositionInner, neededCount, kPreviewLabelHeight);
                fixedSize = false;
            }
            int count = Mathf.Min(division[0] * division[1], targets.Length);
            previewPositionInner.width += kGridSpacing;
            previewPositionInner.height += kGridSpacing;

            Vector2 cellSize = new Vector2(
                Mathf.FloorToInt(previewPositionInner.width / division[0] - kGridSpacing),
                Mathf.FloorToInt(previewPositionInner.height / division[1] - kGridSpacing)
            );
            float previewSize = Mathf.Min(cellSize.x, cellSize.y - kPreviewLabelHeight);
            if (fixedSize)
                previewSize = Mathf.Min(previewSize, kPreviewMinSize);
            for (int i = 0; i < count; i++)
            {
                Rect r = new Rect(
                    previewPositionInner.x + (i % division[0]) * previewPositionInner.width / division[0],
                    previewPositionInner.y + (i / division[0]) * previewPositionInner.height / division[1],
                    cellSize.x,
                    cellSize.y
                );
                r.height -= kPreviewLabelHeight;
                Rect rSquare = new Rect(r.x + (r.width - previewSize) * 0.5f, r.y + (r.height - previewSize) * 0.5f, previewSize, previewSize);
                GUI.BeginGroup(rSquare);
                //获取Preview示意图
                Texture previewTexture = AssetPreview.GetAssetPreview(targets[i]);
                if (previewTexture != null)
                {
                    EditorGUI.DrawTextureTransparent(new Rect(0, 0, previewSize, previewSize), previewTexture, ScaleMode.ScaleToFit);
                }
                GUI.EndGroup();
                r.y = rSquare.yMax;
                r.height = 16;
                GUI.Label(r, targets[i].name, s_Styles.previewMiniLabel);
            }
        }
        else
        {
            Texture previewTexture = AssetPreview.GetAssetPreview(targets[0]);
            if (previewTexture != null)
            {
                EditorGUI.DrawTextureTransparent(previewArea, previewTexture, ScaleMode.ScaleToFit);
            }
        }
    }

    private static float AbsRatioDiff(float x, float y)
    {
        return Mathf.Max(x / y, y / x);
    }
    
    private static int[] GetGridDivision(Rect rect, int minimumNr, int labelHeight)
    {
        // The edge size of a square calculated based on area
        float approxSize = Mathf.Sqrt(rect.width * rect.height / minimumNr);
        int xCount = Mathf.FloorToInt(rect.width / approxSize);
        int yCount = Mathf.FloorToInt(rect.height / (approxSize + labelHeight));
        // This heuristic is not entirely optimal and could probably be improved
        while (xCount * yCount < minimumNr)
        {
            float ratioIfXInc = AbsRatioDiff((xCount + 1) / rect.width, yCount / (rect.height - yCount * labelHeight));
            float ratioIfYInc = AbsRatioDiff(xCount / rect.width, (yCount + 1) / (rect.height - (yCount + 1) * labelHeight));
            if (ratioIfXInc < ratioIfYInc)
            {
                xCount++;
                if (xCount * yCount > minimumNr)
                    yCount = Mathf.CeilToInt((float)minimumNr / xCount);
            }
            else
            {
                yCount++;
                if (xCount * yCount > minimumNr)
                    xCount = Mathf.CeilToInt((float)minimumNr / yCount);
            }
        }
        return new int[] { xCount, yCount };
    }
}

新建一个SpriteAtlasSettingDrawer脚本用来绘制SpriteAtlasSetting这个so类。

using System.Reflection;
using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(SpriteAtlasSetting))]
public class SpriteAtlasSettingDrawer : UnityEditor.Editor
{
    //这里要返回true,表示有Preview需要绘制
    public override bool HasPreviewGUI()
    {
        return true;
    }

    //Preview绘制函数
    public override void OnPreviewGUI(Rect r, GUIStyle background)
    {
        base.OnPreviewGUI(r, background);
        SpriteAtlasSetting spriteAtlasSetting = target as SpriteAtlasSetting;
        //_spriteAtlasDic前面是定义的私有的,这里反射拿
        FieldInfo spriteAtlasDicFieldInfo =
            spriteAtlasSetting.GetType().GetField("_spriteAtlasDic", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
        SpriteAtlasSetting.SpriteAtlasDic spriteAtlasDicFieldInfoValue =
            spriteAtlasDicFieldInfo.GetValue(spriteAtlasSetting) as SpriteAtlasSetting.SpriteAtlasDic;
        if (spriteAtlasDicFieldInfoValue != null && spriteAtlasDicFieldInfoValue.reorderableList != null)
        {
            if (spriteAtlasDicFieldInfoValue.reorderableList.Selected.Length > 0)
            {
                int[] selected = spriteAtlasDicFieldInfoValue.reorderableList.Selected;
                //字典的_values也是私有的,反射拿
                FieldInfo spriteAtlasDicValueFieldInfo = spriteAtlasDicFieldInfoValue.GetType().BaseType
                    .GetField("_values", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);

                //拿到字典中所有的sprite
                Sprite[] values = spriteAtlasDicValueFieldInfo.GetValue(spriteAtlasDicFieldInfoValue) as Sprite[];
                //根据selected拿到被选中的Sprite,这里就不考虑什么GC不GC的了,反正也只是演示。。
                Sprite[] drawSprites = new Sprite[selected.Length];
                for (int i = 0; i < selected.Length; i++)
                {
                    drawSprites[i] = values[selected[i]];
                }

                //调用前面封装的静态接口,绘制多选中preview
                DrawPreviewExtension.DrawPreview(r, drawSprites);
            }
        }
    }
}

多选中Gif示意图

结尾

表面上,我们只是实现了以Sprite为value的序列化字典的多选中绘制,实际上,别的类型资源的选中绘制也是一样的。例如材质,如下图:

这部分支持就不详细介绍了,主要是上述的多选中Preview布局算法。

以上知识分享,如有错误,欢迎指出,共同学习,共同进步。如果有不懂,欢迎留言评论!

转摘请注明:JingFengJi

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Unity编辑器拓展(Editor Extension)可以通过自定义的脚本来扩展Unity编辑器的功能和界面,以满足特定项目的需求。通过编辑器拓展,开发者可以创建自定义的编辑器窗口、工具栏按钮、菜单项、检视面板等,来增强Unity编辑器的功能和流程。 要创建一个Unity编辑器拓展,你可以使用C#编写一个继承自Editor类的脚本。这个脚本可以通过Unity的Inspector面板来设置相关的属性和行为。以下是一个简单的示例: ```csharp using UnityEngine; using UnityEditor; public class MyEditorExtension : EditorWindow { [MenuItem("Custom Tools/My Editor Window")] public static void OpenWindow() { // 创建并打开一个自定义的编辑器窗口 MyEditorExtension window = (MyEditorExtension)EditorWindow.GetWindow(typeof(MyEditorExtension)); window.Show(); } private void OnGUI() { // 在编辑器窗口中绘制UI元素 GUILayout.Label("Hello, I am a custom editor window!"); if (GUILayout.Button("Click Me")) { Debug.Log("Button clicked!"); } } } ``` 上述代码创建了一个自定义的编辑器窗口,并在窗口中绘制了一个标签和一个按钮。通过在Unity编辑器中点击"Custom Tools"菜单下的"My Editor Window",可以打开这个自定义的编辑器窗口。 除了编辑器窗口,你还可以通过继承Editor类来创建自定义的检视面板、菜单项等。Unity官方文档中有更详细的教程和示例,可以帮助你更深入地了解和使用Unity编辑器拓展
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值