如何使编辑器句柄可选以显示属性检查器窗口 [英] How to make editor handles selectable to display properties inspector window

查看:145
本文介绍了如何使编辑器句柄可选以显示属性检查器窗口的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

这是来自解决方案

我聚会晚了吗?

以如下方式选择贝塞尔曲线手柄/控制点,即在选中时在检查器窗口中显示单个手柄的属性(例如,连续性).请注意,我希望在不为手柄/控制点创建游戏对象的情况下完成此操作

我通常喜欢@jour的解决方案,除了以下几点:Handles.Button您必须单击一个点以将其选中,然后然后单击并拖动以移动控制点.

我提出了另一种方法.使用相同的Handles.FreeMoveHandle,但具有一个变量来记住上次单击的句柄的ID,因此我可以识别它.

通常,内置的Handle不会为您提供比其设计目的更多的信息.例如,FreeMoveHandle返回其翻译的增量,仅此而已.问题是:您想捕获一个简单的单击,但是如果您只是单击但没有拖动,则返回值为Vector3.zero,它与您根本没有单击一样. >

好消息:在任何Handle的重载中,有一些调用名为controlID的参数-它是每个可交互GUI对象的标识符.如果您抑制它,引擎将选择一个,而您将永远不会知道.但是,如果您传递一个int,则该值将成为该句柄的ID. 但是如果我传递一个int值并且碰巧与我看不到的其他任何id冲突?那么,您可以致电GUIUtility.GetControlID以获得安全的ID.

然后,它很简单.如果Handle的ID与EditorGUIUtility.hotControl相同(即单击或具有键盘焦点的控件),那么我将此点的索引保存在selectedControlPointId中,并使用它在以下位置显示自定义属性检查员.

保留用于处理每个点的移动的单个方法,而不是为每个点的移动使用单独的方法.

嗯...在这里引起争议.如果我正确理解的话,那么您只需要一个代码即可绘制节点和切线.问题是:这两件事本质上是不同的.当然,如果保持简洁明了,它们是场景中机器人可移动的点.但是,当您引入约束(连续性或smooth)和选择之类的内容时,它们将成为具有不同逻辑的不同野兽.即使您想将ControlPoint设为struct(就像我现在所做的那样)并将其作为一个整体传递,您仍然需要指出要更新的组件,因此约束将适用于其他组件-您将始终需要一个主"字段来避免循环更新(您更改tangentBack,并使tangentFront进行更新,从而触发tangentBack进行再次更新,依此类推.)

这就是为什么,即使我以某种方式重组了ControlPoint方法并将其设置为struct,我也无法通过单一逻辑来绘制节点和切线.


这是一些代码.我从上一个问题中的我的答案中的代码重新开始.

ControlPoint.cs

using System;
using UnityEngine;

[Serializable]
public struct ControlPoint
{
  public Vector2 position;
  public Vector2 tangentBack;
  public Vector2 tangentFront;
  public bool smooth;

  static public ControlPoint MovePosition(ControlPoint pt, Vector2 newPos)
  {
    var newPt = pt;
    newPt.position = newPos;
    return newPt;
  }

  static public ControlPoint MoveTangentBack(ControlPoint pt, Vector2 newTanBack)
  {
    var newPt = pt;
    newPt.tangentBack = newTanBack;
    if (pt.smooth) newPt.tangentFront = pt.tangentFront.magnitude * -newTanBack.normalized;
    return newPt;
  }
  static public ControlPoint MoveTangentFront(ControlPoint pt, Vector2 newTanFront)
  {
    var newPt = pt;
    newPt.tangentFront = newTanFront;
    if (pt.smooth) newPt.tangentBack = pt.tangentBack.magnitude * -newTanFront.normalized;
    return newPt;
  }

  static public ControlPoint WithSmooth(ControlPoint pt, bool smooth)
  {
    var newPt = pt;
    if (smooth != pt.smooth) newPt.tangentBack = -pt.tangentFront;
    return newPt;

  }

  public ControlPoint(Vector2 position, Vector2 tanBack, Vector2 tanFront, bool smooth = false)
  {
    this.position = position;
    this.tangentBack = tanBack;
    this.tangentFront = tanFront;
    this.smooth = smooth;
  }
}

我删除了ControlPointDrawer,因此您添加到其中的其他属性不会隐藏在检查器中.

Path.cs

using System;
using UnityEngine;
using System.Collections.Generic;

[Serializable]
public class Path
{
  [SerializeField] List<ControlPoint> _points;

  [SerializeField] bool _loop = false;

  public Path(Vector2 position)
  {
    _points = new List<ControlPoint>
    {
      new ControlPoint(position, -Vector2.one, Vector2.one),
      new ControlPoint(position + Vector2.right, -Vector2.one, Vector2.one)
    };
  }

  public bool loop { get { return _loop; } set { _loop = value; } }

  public ControlPoint this[int i]
  {
    get { return _points[(_loop && i == _points.Count) ? 0 : i]; }
    set { _points[(_loop && i == _points.Count) ? 0 : i] = value; }
  }

  public int NumPoints { get { return _points.Count; } }

  public int NumSegments { get { return _points.Count - (_loop ? 0 : 1); } }

  public ControlPoint InsertPoint(int i, Vector2 position)
  {
    _points.Insert(i, new ControlPoint(position, -Vector2.one, Vector2.one));
    return this[i];
  }
  public ControlPoint RemovePoint(int i)
  {
    var item = this[i];
    _points.RemoveAt(i);
    return item;
  }
  public Vector2[] GetBezierPointsInSegment(int i)
  {
    var pointBack = this[i];
    var pointFront = this[i + 1];
    return new Vector2[4]
    {
      pointBack.position,
      pointBack.position + pointBack.tangentFront,
      pointFront.position + pointFront.tangentBack,
      pointFront.position
    };
  }

  public ControlPoint MovePoint(int i, Vector2 position)
  {
    this[i] = ControlPoint.MovePosition(this[i], position);
    return this[i];
  }

  public ControlPoint MoveTangentBack(int i, Vector2 position)
  {
    this[i] = ControlPoint.MoveTangentBack(this[i], position);
    return this[i];
  }

  public ControlPoint MoveTangentFront(int i, Vector2 position)
  {
    this[i] = ControlPoint.MoveTangentFront(this[i], position);
    return this[i];
  }
}

PathCreator.cs 相同

PathCreatorEditor.cs

using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(PathCreator))]
public class PathCreatorEditor : Editor
{
  PathCreator creator;
  Path path;
  SerializedProperty property;
  static int selectedControlPointId = -1;

  public override void OnInspectorGUI()
  {
    serializedObject.Update();
    var loopProp = property.FindPropertyRelative("_loop");
    EditorGUI.BeginChangeCheck();
    EditorGUILayout.PropertyField(loopProp);
    var ptsProp = property.FindPropertyRelative("_points");
    var msg = "Total points in path: " + ptsProp.arraySize + "\n";
    if (selectedControlPointId >= 0 && ptsProp.arraySize > 0)
    {
      EditorGUILayout.HelpBox(msg + "Selected control point: " + selectedControlPointId, MessageType.Info);
      EditorGUILayout.Separator();
      EditorGUILayout.PropertyField(ptsProp.GetArrayElementAtIndex(selectedControlPointId), true);
    }
    else
    {
      EditorGUILayout.HelpBox(msg + "No control points selected", MessageType.Info);
    }
    if (EditorGUI.EndChangeCheck()) serializedObject.ApplyModifiedProperties();
  }

  void OnSceneGUI()
  {
    Input();
    Draw();
  }

  void Input()
  {
    Event guiEvent = Event.current;
    Vector2 mousePos = HandleUtility.GUIPointToWorldRay(guiEvent.mousePosition).origin;
    mousePos = creator.transform.InverseTransformPoint(mousePos);
    if (guiEvent.type == EventType.MouseDown && guiEvent.button == 0 && guiEvent.shift)
    {
      Undo.RecordObject(creator, "Insert point");
      path.InsertPoint(path.NumPoints, mousePos);
    }
    else if (guiEvent.type == EventType.MouseDown && guiEvent.button == 0 && guiEvent.control)
    {
      for (int i = 0; i < path.NumPoints; i++)
      {
        if (Vector2.Distance(mousePos, path[i].position) <= .25f)
        {
          Undo.RecordObject(creator, "Remove point");
          path.RemovePoint(i);
          break;
        }
      }
    }
  }

  void Draw()
  {
    Handles.matrix = creator.transform.localToWorldMatrix;
    var rot = Quaternion.Inverse(creator.transform.rotation) * Tools.handleRotation;
    var snap = Vector2.zero;
    Handles.CapFunction cap = Handles.DotHandleCap;
    for (int i = 0; i < path.NumPoints; i++)
    {
      float size;
      var pos = path[i].position;
      size = HandleUtility.GetHandleSize(pos) * .05f;
      Handles.Label(pos, i.ToString());
      Handles.color = i == selectedControlPointId ? Handles.selectedColor : Color.red;
      int ctrlId = GUIUtility.GetControlID(FocusType.Passive);
      Vector2 newPos = Handles.FreeMoveHandle(ctrlId, pos, rot, size, snap, cap);
      if (ctrlId == EditorGUIUtility.hotControl) selectedControlPointId = i;
      if (pos != newPos)
      {
        Undo.RecordObject(creator, "Move point position");
        path.MovePoint(i, newPos);
      }
      pos = newPos;
      Handles.color = Color.black;
      if (path.loop || i != 0)
      {
        var tanBack = pos + path[i].tangentBack;
        Handles.DrawLine(pos, tanBack);
        size = HandleUtility.GetHandleSize(tanBack) * .03f;
        Vector2 newTanBack = Handles.FreeMoveHandle(tanBack, rot, size, snap, cap);
        if (tanBack != newTanBack)
        {
          Undo.RecordObject(creator, "Move point tangent");
          path.MoveTangentBack(i, newTanBack - pos);
        }
      }
      if (path.loop || i != path.NumPoints - 1)
      {
        var tanFront = pos + path[i].tangentFront;
        Handles.DrawLine(pos, tanFront);
        size = HandleUtility.GetHandleSize(tanFront) * .03f;
        Vector2 newTanFront = Handles.FreeMoveHandle(tanFront, rot, size, snap, cap);
        if (tanFront != newTanFront)
        {
          Undo.RecordObject(creator, "Move point tangent");
          path.MoveTangentFront(i, newTanFront - pos);
        }
      }
    }
    Repaint();
  }



  [DrawGizmo(GizmoType.Selected | GizmoType.NonSelected | GizmoType.Pickable)]
  static void DrawGizmo(PathCreator creator, GizmoType gizmoType)
  {
    Gizmos.matrix = creator.transform.localToWorldMatrix;
    var path = creator.path;
    for (int i = 0; i < path.NumSegments; i++)
    {
      Vector2[] points = path.GetBezierPointsInSegment(i);
      var pts = Handles.MakeBezierPoints(points[0], points[3], points[1], points[2], 30);
      Gizmos.color = Color.green;
      for (int j = 0; j < pts.Length - 1; j++)
      {
        Gizmos.DrawLine(pts[j], pts[j + 1]);
      }
    }
  }

  void OnEnable()
  {
    creator = (PathCreator)target;
    path = creator.path ?? creator.CreatePath();
    property = serializedObject.FindProperty("path");
  }
}

注意:我将Bezier线的图形从OnSceneGui移到了DrawGizmo,因此即使没有切开对象,绿线也将可见,并且可以在场景编辑器"中对其进行选择.选择.

最后,我建议对该脚本进行一些进一步的开发.使多点选择成为可能并不是很难.也许使默认手柄(例如位置和旋转)可以分别应用于点.或将创建和删除点的方法更改为直观的东西,例如双击或拖动路径线.甚至是用于智能点操作的自定义工具栏,例如对齐,分布,雕刻...不同的约束,例如平滑,对称,尖点或笔直...

This is a follow-up question from How to make individual anchor points of bezier continuous or non-continuous. Please refer to it for the relevant code in the accepted answer (please note that I did this to keep this question clean since the related code is quite lengthy).

I am trying to achieve the following:

  1. Make the bezier curve handles/control points selectable in such a way that the properties (for example continuity) for an individual handle are displayed in the inspector window when selected. Please note I'd like this to be done without making creating game objects for the handles/ control points

  2. Retain a single method that handles the movement of each point instead of having separate methods for the movement of each point.

解决方案

Am I late to the party?

Make the bezier curve handles/control points selectable in such a way that the properties (for example continuity) for an individual handle are displayed in the inspector window when selected. Please note I'd like this to be done without making creating game objects for the handles/ control points

I like @jour's solution in general, except 1 thing: with Handles.Button you have to click a point to select it and then you click and drag to move the control point.

I propose a different approach. Using the same Handles.FreeMoveHandle, but having a variable to remember the id of the last clicked handle, so I can identify it.

Usually, a built in Handle won't give you more info than what it is designed to do. FreeMoveHandle, for example, returns the delta of its translation and that's all. The problem is: You want to capture a simple click, but if you just clicked and didn't drag, the return value is Vector3.zero and it is just the same as if you didn't do a click at all.

Good news: among the overloads of any Handle, there are some calls with an argument named controlID - it is an identifier for each interactable GUI object. If you supress it, the Engine will choose one and you will never know what. But if you pass an int, that value will be the id of the handle. But if I pass an int and it happen to conflict with any of the other ids i don't see? Well, you can call GUIUtility.GetControlID to get a safe id.

Then, it is straightforward. If the id of a Handle is the same as EditorGUIUtility.hotControl (that is, the control that got clicked or have the keyboard focus), then I save the index of this point in selectedControlPointId and use it to display a custom property in Inspector.

Retain a single method that handles the movement of each point instead of having separate methods for the movement of each point.

Hmm... Here become the controversy. If I understood it correctly, you want a single code to draw both nodes and tangents. The thing is: this two things are different in nature. Of course, if you keep it plain and simple, they are bot movable points in scene. But, when you introduces things like constraints (the continuity, or smooth) and selection, they become different beasts, with different logic. Even if you want to make ControlPoint a struct (like i did now) and pass it over as a whole, you'd still need to point what component you are aiming to update, so the constraints will apply to the others - you will always need a "master" field to avoid circular updates (you change tangentBack, and that makes tangentFront to update, that trigger tangentBack to update again and so on).

That's why, even though I reorganized somehow the ControlPoint methods and made it a struct, I can't make a single logic to draw both nodes and tangents.


Here are some codes. I started over from the codes on my answer in the previous question.

ControlPoint.cs

using System;
using UnityEngine;

[Serializable]
public struct ControlPoint
{
  public Vector2 position;
  public Vector2 tangentBack;
  public Vector2 tangentFront;
  public bool smooth;

  static public ControlPoint MovePosition(ControlPoint pt, Vector2 newPos)
  {
    var newPt = pt;
    newPt.position = newPos;
    return newPt;
  }

  static public ControlPoint MoveTangentBack(ControlPoint pt, Vector2 newTanBack)
  {
    var newPt = pt;
    newPt.tangentBack = newTanBack;
    if (pt.smooth) newPt.tangentFront = pt.tangentFront.magnitude * -newTanBack.normalized;
    return newPt;
  }
  static public ControlPoint MoveTangentFront(ControlPoint pt, Vector2 newTanFront)
  {
    var newPt = pt;
    newPt.tangentFront = newTanFront;
    if (pt.smooth) newPt.tangentBack = pt.tangentBack.magnitude * -newTanFront.normalized;
    return newPt;
  }

  static public ControlPoint WithSmooth(ControlPoint pt, bool smooth)
  {
    var newPt = pt;
    if (smooth != pt.smooth) newPt.tangentBack = -pt.tangentFront;
    return newPt;

  }

  public ControlPoint(Vector2 position, Vector2 tanBack, Vector2 tanFront, bool smooth = false)
  {
    this.position = position;
    this.tangentBack = tanBack;
    this.tangentFront = tanFront;
    this.smooth = smooth;
  }
}

I removed ControlPointDrawer, so the other propertis you added to it won't be hidden in inspector.

Path.cs

using System;
using UnityEngine;
using System.Collections.Generic;

[Serializable]
public class Path
{
  [SerializeField] List<ControlPoint> _points;

  [SerializeField] bool _loop = false;

  public Path(Vector2 position)
  {
    _points = new List<ControlPoint>
    {
      new ControlPoint(position, -Vector2.one, Vector2.one),
      new ControlPoint(position + Vector2.right, -Vector2.one, Vector2.one)
    };
  }

  public bool loop { get { return _loop; } set { _loop = value; } }

  public ControlPoint this[int i]
  {
    get { return _points[(_loop && i == _points.Count) ? 0 : i]; }
    set { _points[(_loop && i == _points.Count) ? 0 : i] = value; }
  }

  public int NumPoints { get { return _points.Count; } }

  public int NumSegments { get { return _points.Count - (_loop ? 0 : 1); } }

  public ControlPoint InsertPoint(int i, Vector2 position)
  {
    _points.Insert(i, new ControlPoint(position, -Vector2.one, Vector2.one));
    return this[i];
  }
  public ControlPoint RemovePoint(int i)
  {
    var item = this[i];
    _points.RemoveAt(i);
    return item;
  }
  public Vector2[] GetBezierPointsInSegment(int i)
  {
    var pointBack = this[i];
    var pointFront = this[i + 1];
    return new Vector2[4]
    {
      pointBack.position,
      pointBack.position + pointBack.tangentFront,
      pointFront.position + pointFront.tangentBack,
      pointFront.position
    };
  }

  public ControlPoint MovePoint(int i, Vector2 position)
  {
    this[i] = ControlPoint.MovePosition(this[i], position);
    return this[i];
  }

  public ControlPoint MoveTangentBack(int i, Vector2 position)
  {
    this[i] = ControlPoint.MoveTangentBack(this[i], position);
    return this[i];
  }

  public ControlPoint MoveTangentFront(int i, Vector2 position)
  {
    this[i] = ControlPoint.MoveTangentFront(this[i], position);
    return this[i];
  }
}

PathCreator.cs is the same

PathCreatorEditor.cs

using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(PathCreator))]
public class PathCreatorEditor : Editor
{
  PathCreator creator;
  Path path;
  SerializedProperty property;
  static int selectedControlPointId = -1;

  public override void OnInspectorGUI()
  {
    serializedObject.Update();
    var loopProp = property.FindPropertyRelative("_loop");
    EditorGUI.BeginChangeCheck();
    EditorGUILayout.PropertyField(loopProp);
    var ptsProp = property.FindPropertyRelative("_points");
    var msg = "Total points in path: " + ptsProp.arraySize + "\n";
    if (selectedControlPointId >= 0 && ptsProp.arraySize > 0)
    {
      EditorGUILayout.HelpBox(msg + "Selected control point: " + selectedControlPointId, MessageType.Info);
      EditorGUILayout.Separator();
      EditorGUILayout.PropertyField(ptsProp.GetArrayElementAtIndex(selectedControlPointId), true);
    }
    else
    {
      EditorGUILayout.HelpBox(msg + "No control points selected", MessageType.Info);
    }
    if (EditorGUI.EndChangeCheck()) serializedObject.ApplyModifiedProperties();
  }

  void OnSceneGUI()
  {
    Input();
    Draw();
  }

  void Input()
  {
    Event guiEvent = Event.current;
    Vector2 mousePos = HandleUtility.GUIPointToWorldRay(guiEvent.mousePosition).origin;
    mousePos = creator.transform.InverseTransformPoint(mousePos);
    if (guiEvent.type == EventType.MouseDown && guiEvent.button == 0 && guiEvent.shift)
    {
      Undo.RecordObject(creator, "Insert point");
      path.InsertPoint(path.NumPoints, mousePos);
    }
    else if (guiEvent.type == EventType.MouseDown && guiEvent.button == 0 && guiEvent.control)
    {
      for (int i = 0; i < path.NumPoints; i++)
      {
        if (Vector2.Distance(mousePos, path[i].position) <= .25f)
        {
          Undo.RecordObject(creator, "Remove point");
          path.RemovePoint(i);
          break;
        }
      }
    }
  }

  void Draw()
  {
    Handles.matrix = creator.transform.localToWorldMatrix;
    var rot = Quaternion.Inverse(creator.transform.rotation) * Tools.handleRotation;
    var snap = Vector2.zero;
    Handles.CapFunction cap = Handles.DotHandleCap;
    for (int i = 0; i < path.NumPoints; i++)
    {
      float size;
      var pos = path[i].position;
      size = HandleUtility.GetHandleSize(pos) * .05f;
      Handles.Label(pos, i.ToString());
      Handles.color = i == selectedControlPointId ? Handles.selectedColor : Color.red;
      int ctrlId = GUIUtility.GetControlID(FocusType.Passive);
      Vector2 newPos = Handles.FreeMoveHandle(ctrlId, pos, rot, size, snap, cap);
      if (ctrlId == EditorGUIUtility.hotControl) selectedControlPointId = i;
      if (pos != newPos)
      {
        Undo.RecordObject(creator, "Move point position");
        path.MovePoint(i, newPos);
      }
      pos = newPos;
      Handles.color = Color.black;
      if (path.loop || i != 0)
      {
        var tanBack = pos + path[i].tangentBack;
        Handles.DrawLine(pos, tanBack);
        size = HandleUtility.GetHandleSize(tanBack) * .03f;
        Vector2 newTanBack = Handles.FreeMoveHandle(tanBack, rot, size, snap, cap);
        if (tanBack != newTanBack)
        {
          Undo.RecordObject(creator, "Move point tangent");
          path.MoveTangentBack(i, newTanBack - pos);
        }
      }
      if (path.loop || i != path.NumPoints - 1)
      {
        var tanFront = pos + path[i].tangentFront;
        Handles.DrawLine(pos, tanFront);
        size = HandleUtility.GetHandleSize(tanFront) * .03f;
        Vector2 newTanFront = Handles.FreeMoveHandle(tanFront, rot, size, snap, cap);
        if (tanFront != newTanFront)
        {
          Undo.RecordObject(creator, "Move point tangent");
          path.MoveTangentFront(i, newTanFront - pos);
        }
      }
    }
    Repaint();
  }



  [DrawGizmo(GizmoType.Selected | GizmoType.NonSelected | GizmoType.Pickable)]
  static void DrawGizmo(PathCreator creator, GizmoType gizmoType)
  {
    Gizmos.matrix = creator.transform.localToWorldMatrix;
    var path = creator.path;
    for (int i = 0; i < path.NumSegments; i++)
    {
      Vector2[] points = path.GetBezierPointsInSegment(i);
      var pts = Handles.MakeBezierPoints(points[0], points[3], points[1], points[2], 30);
      Gizmos.color = Color.green;
      for (int j = 0; j < pts.Length - 1; j++)
      {
        Gizmos.DrawLine(pts[j], pts[j + 1]);
      }
    }
  }

  void OnEnable()
  {
    creator = (PathCreator)target;
    path = creator.path ?? creator.CreatePath();
    property = serializedObject.FindProperty("path");
  }
}

Note: I moved the drawing of the Bezier line from OnSceneGui to DrawGizmo, so the green line will be visible even when the object is not delected, and it will be pickable in the Scene Editor, for having it selected.

Lastly, I would suggest some further development of this scripts. It wouldn't be very hard to make multiple point selection posible. Maybe making the default handles (like position and rotation) to be appliable individually on points. Or changing the method for creating and deleting points to something move intuitive like double-clicking or dragging the path line. Or even a custom toolbar to smart point manipulation, like align, distribute, sculpt... Different constraints, like smooth, symetric, cusp or straight...

这篇关于如何使编辑器句柄可选以显示属性检查器窗口的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆