引子
最近在做一个有关序列帧的东西,如图,就是这种,我需要给他分解成 向前向后向左向右四种方向的动作组,(之后还会有攻击防御等–)。
功能上很简单,就是把图片的SpriteMode改成multiple把图片切分成小图,然后分组按照序列帧播放。
之后写一个脚本,把要按照序列播放的子sprite分别拖入对应的数组即可
但是产生了个问题,当我想预览某一组动画的时候,我只能启动游戏去用代码或者游戏功能来观看这个序列帧播放,我在想如果能不用启动游戏就能直接预览这些序列帧会方便很多,就像Spine那样,点击哪个动画下边就可以播放哪个
于是便开始写编辑器!
实现大体思路
经过所搜,下边的这个预览窗口,即Unity的Preview窗口是可以在Editor类里进行自定义拓展的,他是一个独立的空间,既不属于Scene空间,也不属于Prefab编辑器空间,而是一个独立的空间。想要在这里显示一个东西,需要在这个空间里新建物体,然后新建摄像机,对准物体渲染,然后把得到的图片取出。经过摸索,大体步骤如下
1.建立一个Editor空间下的类(要继承Editor类) ,和普通拓展编辑器一样使用[CustomEditor(typeof(XXXX))]关联到你的功能类。
2.在编辑器类里声明一个PreviewRenderUtility对象,以下简称Utlity。
3.新建你想要显示的物体,并且用Utility.AddSingleGO()方法把他加入空间,也要把Utility的camera对准你的物体。
4.复写OnInteractivePreviewGUI方法,使用Utility的BeginPreview()和EndPreview()包裹住中间你想做的显示操作EndPreview()会返回一张图片。
5.最后用GUI.DrawTexture()显示这张图片。
细节要点1:OnInteractivePreviewGUI() 和 OnPreviewGUI()
如名字所描述的一样,这两个函数都是绘制这个PreviewGUI的函数,OnInteractivePreviewGUI()就是可以允许用户进行交互的那种,比如滑动鼠标滚轮进行缩放,拖拽物体旋转,等等,一般3d模型展示常有这种效果,如果是有这样的需求的话,那么就必须使用OnInteractivePreviewGUI()来进行复写,如果是只是静态显示一些信息,那么用OnPreviewGUI就够了
细节要点2: 要开启HasPreviewGUI()
这个也是自己踩过的坑,一开始窗口上怎么都啥也没有,后来参考spine的源码发现了要把这个开启,这样才行
public override bool HasPreviewGUI()
{
return true;
}
细节要点3 :物体新建之后要AddSingleGO()
如代码所示,新建的物体,(我这里就是一个SpriteRender)要作为参数使用这个方法,我所理解的就是这样就把他加入到这个Preview空间里去了。
void Init()
{
if(mPreviewRenderUtility == null)
{
mPreviewRenderUtility = new PreviewRenderUtility(true);
Camera c = mPreviewRenderUtility.camera;
c.orthographic = true;
c.cullingMask = PreviewCameraCullingMask;
c.nearClipPlane = 0.01f;
c.farClipPlane = 1000f;
c.transform.LookAt(Vector3.zero);
c.transform.position = new Vector3(0, 0, -10f);
mPreviewCam = c;
DestroyPreviewGameObject();
}
if(mPreviewGo == null)
{
mPreviewGo = new GameObject("Pre", typeof(SpriteRenderer))
{
layer = PreviewLayer,
hideFlags = HideFlags.HideAndDontSave,
};
mPreviewRenderUtility.AddSingleGO(mPreviewGo);
}
}
同时,摄像机是要用PreviewUiltiy实例自己的摄像机哦,就像上边代码那样,位置设置到物体的后方一点,让他刚好能照到物体。
这里我是个spriteRenderer所以设置成了正交相机。 参考spine的代码我也把我的spriteRender物体设置了一个层级PreviewLayer并且我让这里的相机的cullingmask只是PreviewLayer,这样就保证这个相机只是渲染这个东西(但其实这个空间内我也只有这个东西,不这样设置layer我觉得应该也没啥问题)
要点4: 记得调用Camera 的Render()
直接上代码
override public void OnInteractivePreviewGUI(Rect r, GUIStyle background)
{
Init();
mPreviewRenderUtility.BeginPreview(r, background);
SpriteRenderer sr = mPreviewGo.GetComponent<SpriteRenderer>();
sr.sprite = sps[frameCount% sps.Length];
mPreviewCam.Render();
Texture previewTexture = mPreviewRenderUtility.EndPreview();
if (previewTexture != null)
GUI.DrawTexture(r, previewTexture, ScaleMode.StretchToFill, false);
HandleMouseScroll(r);//鼠标滚轮控制缩放
}
在Begin和End中间要调用预览相机的Render()函数 ,不然产生出来的图片previewTexture个无效的图哦!也是本人踩坑经验,一开始我以为我谁相机没对准还是什么尺寸设置错了,原来是这里。
成功静态显示
到这里已经可以静态显示图片了,当我们点击带有脚本的物体时,下边的预览就会有我们代码里想要的图
顺便说下
拓展预览窗口名称用的是这个函数
/// <summary>
/// 预览窗口名称
/// </summary>
/// <returns></returns>
public override GUIContent GetPreviewTitle()
{
return new GUIContent("角色动作预览");
}
/// <summary>
/// 自定义窗口名称右边的设置区
/// </summary>
public override void OnPreviewSettings()
{
timeScale= GUILayout.HorizontalSlider(timeScale, 0.1f, 1.0f,GUILayout.MaxWidth(150f));
}
而OnPreviewSettings是用来拓展窗口右上角那部分的,我这里是加了一个滑动条,我来控制动画播放速度的,也可加按钮,加文字等等。
接下来怎么让他动起来
那么现在要让他动起来,就需要每隔固定时间换一张图显示,那就得找到Editor模式下的类似update的函数,如图所示:
private void OnEnable()
{
EditorApplication.update -= OnEditorUpdate;
EditorApplication.update += OnEditorUpdate;
}
我选择监听了EditorApplication的update事件,这样在我的OnEditorUpdate根据规则定时更新我的图片就好啦,我是这样想的。
但是实验结果是它并不会一直update,只有我鼠标在里面划一划或者点一点的时候他才会执行一次update,属于是有够懒惰的update,看来编辑器模式下的update和运行时是不同的。
那么解决办法就是
float UpdateFrameCount => 60f / timeScale;
private void OnEditorUpdate()
{
if (currentFrame > UpdateFrameCount)
{
frameCount += 1;
currentFrame = 0;
Repaint();//使用这个 强制刷新
}
currentFrame += 1;
}
使用Repaint()在你想要他变化的时候强制刷新。
要点 记得销毁在预览空间下新建的东西哦
void DisposePreviewRenderUtility()
{
if (mPreviewRenderUtility != null)
{
mPreviewRenderUtility.Cleanup();
mPreviewRenderUtility = null;
}
}
void DestroyPreviewGameObject()
{
if (mPreviewGo != null)
{
DestroyImmediate(mPreviewGo);
mPreviewGo = null;
}
}
需要在ondestroy和OnDiseable里调用PreviewRenderUtility.Cleanup() (这个不调用编辑器都会报错提示) 还有destory掉你预览的对象。
鼠标滚轮控制缩放
这个就是有关于Event了,我自己理解的也不是很透彻,模仿spine写的,直接上代码
int SliderHash = "RoleSlider".GetHashCode();
void HandleMouseScroll(Rect position)
{
Event current = Event.current;
int controlID = GUIUtility.GetControlID(SliderHash, FocusType.Passive);
switch (current.GetTypeForControl(controlID))
{
case EventType.ScrollWheel:
if (position.Contains(current.mousePosition))
{
cameraOrthoGoal += current.delta.y * 0.06f;
cameraOrthoGoal = Mathf.Max(0.01f, cameraOrthoGoal);
mPreviewCam.orthographicSize= cameraOrthoGoal;
GUIUtility.hotControl = controlID;
current.Use();
}
break;
}
}
这个关于sliderHash,我虽理解的是可以自定义的,唯一即可。然后这个哈希值就和这个预览窗口绑定了,之后在代码里调用这个哈希值究竟是有关这个窗口UI的操作。 然后通过这个事件改变camera的orthographicSize即可完成缩放控制
题外话
关于我怎么在Editor脚本里获取我功能类的数据
我的功能类长这样:
public class RolePrefab : MonoBehaviour
{
SpriteRenderer mSpriteRender;
public Sprite[] Left;
public Sprite[] Right;
public Sprite[] Up;
public Sprite[] Down;
}
在Editor类里,我想获取Left这个数组,按照以前的经验,我是用serializedObject.FindProperty(“Left”)获取到SerializedProperty ,然后操作这个SerializedProperty 来获取的,但是以前都是Int类型 float或者string这样的类型,都是可以从SerializedProperty 直接获取到的,这个Sprite数组似乎没法从这种序列化方法中获取。(也许可以? 求懂哥解惑)
于是只要使用反射获取,(反射是真的强大)
FieldInfo left = typeof(RolePrefab).GetField("Left");
Sprite[] sps;
private void OnEnable()
{
sps = left.GetValue(target) as Sprite[];
EditorApplication.update -= OnEditorUpdate;
EditorApplication.update += OnEditorUpdate;
}
这样获取到的东西几乎就可以转化成任何类型了~
最后拓展一下编辑器GUI,尽量把编辑器做的友好一点,(稍微好看点,学学spine那种感觉)能让我直接在编辑器里点哪个就放哪个序列帧:
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
DrawAnimPreview();
}
GUIStyle mIdleStyle;
GUIStyle IdleStyle
{
get
{
if (mIdleStyle == null)
{
mIdleStyle = new GUIStyle(EditorStyles.miniButton);
mIdleStyle.normal.textColor = Color.white;
}
return mIdleStyle;
}
}
GUIStyle mActiveStyle;
GUIStyle ActiveStyle
{
get
{
if (mActiveStyle == null)
{
mActiveStyle = new GUIStyle(EditorStyles.miniButton);
mActiveStyle.normal.textColor = Color.red;
}
return mActiveStyle;
}
}
int curAnimIndex = 0;
public string[] AnimNameList = new string[4]
{
"Left","Right","Up","Down"
};
void DrawAnimPreview()
{
for (int i = 0; i < 4; i++)
{
using (new GUILayout.HorizontalScope())
{
if (GUILayout.Button("\u25BA", curAnimIndex == i ? ActiveStyle : IdleStyle, GUILayout.Width(24)))
{
curAnimIndex = i;
FieldInfo AnimField = typeof(RolePrefab).GetField(AnimNameList[i]);
sps = AnimField.GetValue(target) as Sprite[];
}
GUILayout.Label(AnimNameList[i]);
}
}
}
好!到这里功能就基本实现了。把坑都踩了一遍,以此记录~ 如果能帮到你一点,那实属荣幸!