本文介绍在Unity中多选Object时,Preview视图的绘制,欢迎访问😁

本篇文章,如果有不懂,欢迎留言~click here!

前言

先来介绍一下本篇文章的背景,在项目中通过使用一个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

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
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脚本,源码如下;

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
//源码来源于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类。

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
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布局算法。

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