Unity自定义Preview窗口的方法

引子

最近在做一个有关序列帧的东西,如图,就是这种,我需要给他分解成 向前向后向左向右四种方向的动作组,(之后还会有攻击防御等–)。在这里插入图片描述

功能上很简单,就是把图片的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]);
          }
      }
  }

好!到这里功能就基本实现了。把坑都踩了一遍,以此记录~ 如果能帮到你一点,那实属荣幸!

猜你喜欢

转载自blog.csdn.net/qq_27275225/article/details/141404287