unity script gui custom c# unity3d unity3d-editor

c# - gui - unity editor script



Cómo hacer puntos de anclaje individuales de bezier continuo o no continuo (2)

Estoy creando curvas de bezier con el siguiente código. Las curvas se pueden ampliar para unir varias curvas de bezier haciendo clic con la tecla Mayús en la vista de escena. Mi código tiene funcionalidad para hacer que toda la curva sea continua o no continua. Me di cuenta de que necesito que los puntos individuales (específicamente los puntos de anclaje) tengan esta funcionalidad.

Creo que la forma más ideal de hacerlo es crear una nueva clase para los puntos con esta funcionalidad (hacer puntos continuos o no continuos) ya que puede usarse para agregar otras propiedades que podrían ser específicas de los puntos. ¿Cómo se puede hacer esto?

Camino

[System.Serializable] public class Path { [SerializeField, HideInInspector] List<Vector2> points; [SerializeField, HideInInspector] public bool isContinuous; public Path(Vector2 centre) { points = new List<Vector2> { centre+Vector2.left, centre+(Vector2.left+Vector2.up)*.5f, centre + (Vector2.right+Vector2.down)*.5f, centre + Vector2.right }; } public Vector2 this[int i] { get { return points[i]; } } public int NumPoints { get { return points.Count; } } public int NumSegments { get { return (points.Count - 4) / 3 + 1; } } public void AddSegment(Vector2 anchorPos) { points.Add(points[points.Count - 1] * 2 - points[points.Count - 2]); points.Add((points[points.Count - 1] + anchorPos) * .5f); points.Add(anchorPos); } public Vector2[] GetPointsInSegment(int i) { return new Vector2[] { points[i * 3], points[i * 3 + 1], points[i * 3 + 2], points[i * 3 + 3] }; } public void MovePoint(int i, Vector2 pos) { if (isContinuous) { Vector2 deltaMove = pos - points[i]; points[i] = pos; if (i % 3 == 0) { if (i + 1 < points.Count) { points[i + 1] += deltaMove; } if (i - 1 >= 0) { points[i - 1] += deltaMove; } } else { bool nextPointIsAnchor = (i + 1) % 3 == 0; int correspondingControlIndex = (nextPointIsAnchor) ? i + 2 : i - 2; int anchorIndex = (nextPointIsAnchor) ? i + 1 : i - 1; if (correspondingControlIndex >= 0 && correspondingControlIndex < points.Count) { float dst = (points[anchorIndex] - points[correspondingControlIndex]).magnitude; Vector2 dir = (points[anchorIndex] - pos).normalized; points[correspondingControlIndex] = points[anchorIndex] + dir * dst; } } } } else { points[i] = pos; } }

PathCreator

public class PathCreator : MonoBehaviour { [HideInInspector] public Path path; public void CreatePath() { path = new Path(transform.position); } }

PathEditor

[CustomEditor(typeof(PathCreator))] public class PathEditor : Editor { PathCreator creator; Path path; public override void OnInspectorGUI() { base.OnInspectorGUI(); EditorGUI.BeginChangeCheck(); bool continuousControlPoints = GUILayout.Toggle(path.isContinuous, "Set Continuous Control Points"); if (continuousControlPoints != path.isContinuous) { Undo.RecordObject(creator, "Toggle set continuous controls"); path.isContinuous = continuousControlPoints; } if (EditorGUI.EndChangeCheck()) { SceneView.RepaintAll(); } } void OnSceneGUI() { Input(); Draw(); } void Input() { Event guiEvent = Event.current; Vector2 mousePos = HandleUtility.GUIPointToWorldRay(guiEvent.mousePosition).origin; if (guiEvent.type == EventType.MouseDown && guiEvent.button == 0 && guiEvent.shift) { Undo.RecordObject(creator, "Add segment"); path.AddSegment(mousePos); } } void Draw() { for (int i = 0; i < path.NumSegments; i++) { Vector2[] points = path.GetPointsInSegment(i); Handles.color = Color.black; Handles.DrawLine(points[1], points[0]); Handles.DrawLine(points[2], points[3]); Handles.DrawBezier(points[0], points[3], points[1], points[2], Color.green, null, 2); } Handles.color = Color.red; for (int i = 0; i < path.NumPoints; i++) { Vector2 newPos = Handles.FreeMoveHandle(path[i], Quaternion.identity, .1f, Vector2.zero, Handles.CylinderHandleCap); if (path[i] != newPos) { Undo.RecordObject(creator, "Move point"); path.MovePoint(i, newPos); } } } void OnEnable() { creator = (PathCreator)target; if (creator.path == null) { creator.CreatePath(); } path = creator.path; } }


Creo que su idea está bien: puede escribir dos clases, denominadas ControlPoint y HandlePoint (hacer que sean serializables).

ControlPoint puede representar p0 y p3 de cada curva: los puntos por los que pasa el camino. Para la continuidad , debe afirmar que p3 de un segmento es igual a p0 del siguiente segmento.

HandlePoint puede representar p1 y p2 de cada curva, los puntos que son tangentes de la curva y proporcionan dirección e inclinación. Para la suavidad , debe afirmar que (p3 - p2).normalized de un segmento es igual a (p1 - p0).normalized del siguiente segmento. (Si desea suavidad simétrica , p3 - p2 de uno debe ser igual a p1 - p0 del otro.)

Consejo # 1 : siempre considere las transformaciones matriciales cuando asigne o compare puntos de cada segmento. Le sugiero que convierta cualquier punto al espacio global antes de realizar las operaciones.

Consejo # 2 : considere aplicar una restricción entre los puntos dentro de un segmento, de modo que cuando se mueva alrededor de p0 o p3 de una curva, p1 o p2 muevan en la misma cantidad, respectivamente (al igual que cualquier software de editor de gráficos en curvas de bezier).

Editar -> Código proporcionado

Hice una implementación de muestra de la idea. En realidad, después de iniciar la codificación, me di cuenta de que solo una clase ControlPoint (en lugar de dos) hará el trabajo. Un ControlPoint tiene 2 tangentes. El comportamiento deseado está controlado por el campo smooth , que se puede establecer para cada punto.

ControlPoint.cs

using System; using UnityEngine; [Serializable] public class ControlPoint { [SerializeField] Vector2 _position; [SerializeField] bool _smooth; [SerializeField] Vector2 _tangentBack; [SerializeField] Vector2 _tangentFront; public Vector2 position { get { return _position; } set { _position = value; } } public bool smooth { get { return _smooth; } set { if (_smooth = value) _tangentBack = -_tangentFront; } } public Vector2 tangentBack { get { return _tangentBack; } set { _tangentBack = value; if (_smooth) _tangentFront = _tangentFront.magnitude * -value.normalized; } } public Vector2 tangentFront { get { return _tangentFront; } set { _tangentFront = value; if (_smooth) _tangentBack = _tangentBack.magnitude * -value.normalized; } } public ControlPoint(Vector2 position, bool smooth = true) { this._position = position; this._smooth = smooth; this._tangentBack = -Vector2.one; this._tangentFront = Vector2.one; } }

También codifiqué un PropertyDrawer personalizado para la clase ControlPoint , para que se pueda ver mejor en el inspector. Es solo una implementación ingenua. Se podría mejorar mucho.

ControlPointDrawer.cs

using UnityEngine; using UnityEditor; [CustomPropertyDrawer(typeof(ControlPoint))] public class ControlPointDrawer : PropertyDrawer { public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { EditorGUI.BeginProperty(position, label, property); int indent = EditorGUI.indentLevel; EditorGUI.indentLevel = 0; //-= 1; var propPos = new Rect(position.x, position.y, position.x + 18, position.height); var prop = property.FindPropertyRelative("_smooth"); EditorGUI.PropertyField(propPos, prop, GUIContent.none); propPos = new Rect(position.x + 20, position.y, position.width - 20, position.height); prop = property.FindPropertyRelative("_position"); EditorGUI.PropertyField(propPos, prop, GUIContent.none); EditorGUI.indentLevel = indent; EditorGUI.EndProperty(); } public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { return EditorGUIUtility.singleLineHeight; } }

Seguí la misma arquitectura de su solución, pero con los ajustes necesarios para adaptarse a la clase ControlPoint y otras correcciones / cambios. Por ejemplo, almacené todos los valores de puntos en coordenadas locales, por lo que las transformaciones en el componente o los padres se reflejan en la curva.

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), new ControlPoint(position + Vector2.right) }; } public bool loop { get { return _loop; } set { _loop = value; } } public ControlPoint this[int i] { get { return _points[(_loop && i == _points.Count) ? 0 : i]; } } public int NumPoints { get { return _points.Count; } } public int NumSegments { get { return _points.Count - (_loop ? 0 : 1); } } public ControlPoint InsertPoint(int i, Vector2 position, bool smooth) { _points.Insert(i, new ControlPoint(position, smooth)); 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].position = position; return this[i]; } public ControlPoint MoveTangentBack(int i, Vector2 position) { this[i].tangentBack = position; return this[i]; } public ControlPoint MoveTangentFront(int i, Vector2 position) { this[i].tangentFront = position; return this[i]; } }

PathEditor es más o menos lo mismo.

PathCreator.cs

using UnityEngine; public class PathCreator : MonoBehaviour { public Path path; public Path CreatePath() { return path = new Path(Vector2.zero); } void Reset() { CreatePath(); } }

Finalmente, toda la magia sucede en el PathCreatorEditor . Dos comentarios aquí:

1) DrawGizmo el dibujo de las líneas a una función estática DrawGizmo personalizada, para que pueda tener las líneas incluso cuando el objeto no esté Active (es decir, se muestra en el Inspector) Incluso podría hacer que sea seleccionable si lo desea. No sé si desea este comportamiento, pero podría revertirlo fácilmente;

2) Observe las líneas Handles.matrix = creator.transform.localToWorldMatrix sobre la clase. Transforma automáticamente la escala y la rotación de los puntos en las coordenadas mundiales. Hay un detalle con PivotRotation allí también.

PathCreatorEditor.cs

using UnityEngine; using UnityEditor; [CustomEditor(typeof(PathCreator))] public class PathCreatorEditor : Editor { PathCreator creator; Path path; SerializedProperty property; public override void OnInspectorGUI() { serializedObject.Update(); EditorGUI.BeginChangeCheck(); EditorGUILayout.PropertyField(property, true); 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, false); } 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 = Tools.pivotRotation == PivotRotation.Local ? creator.transform.rotation : Quaternion.identity; var snap = Vector2.zero; Handles.CapFunction cap = Handles.CylinderHandleCap; for (int i = 0; i < path.NumPoints; i++) { var pos = path[i].position; var size = .1f; Handles.color = Color.red; Vector2 newPos = Handles.FreeMoveHandle(pos, rot, size, snap, cap); if (pos != newPos) { Undo.RecordObject(creator, "Move point position"); path.MovePoint(i, newPos); } pos = newPos; if (path.loop || i != 0) { var tanBack = pos + path[i].tangentBack; Handles.color = Color.black; Handles.DrawLine(pos, tanBack); Handles.color = Color.red; 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.color = Color.black; Handles.DrawLine(pos, tanFront); Handles.color = Color.red; Vector2 newTanFront = Handles.FreeMoveHandle(tanFront, rot, size, snap, cap); if (tanFront != newTanFront) { Undo.RecordObject(creator, "Move point tangent"); path.MoveTangentFront(i, newTanFront - pos); } } } } [DrawGizmo(GizmoType.Selected | GizmoType.NonSelected)] static void DrawGizmo(PathCreator creator, GizmoType gizmoType) { Handles.matrix = creator.transform.localToWorldMatrix; var path = creator.path; for (int i = 0; i < path.NumSegments; i++) { Vector2[] points = path.GetBezierPointsInSegment(i); Handles.DrawBezier(points[0], points[3], points[1], points[2], Color.green, null, 2); } } void OnEnable() { creator = (PathCreator)target; path = creator.path ?? creator.CreatePath(); property = serializedObject.FindProperty("path"); } }

Además, agregué un campo de loop en caso de que quieras que la curva se cierre, y agregué una funcionalidad ingenua para eliminar puntos con Ctrl+click en la escena. Resumiendo, esto es algo básico, pero puedes hacerlo tan avanzado como quieras. Además, puede reutilizar su clase ControlPoint con otros componentes, como una spline Catmull-Rom, formas geométricas, otras funciones paramétricas ...


La pregunta básica en su publicación es: ''¿Es una buena idea tener una Clase separada para los puntos de una curva de bezier?''

Dado que la curva se compondrá de tales puntos y estos son más que dos coordenadas , ciertamente es una buena idea .

Pero, como es habitual cuando se realiza el diseño de clase, recopilemos algunos casos de uso , es decir, para qué se utilizará un punto o cosas que esperamos hacer en un punto ...:

  • Un punto se puede agregar o quitar de una curva
  • Se puede mover un punto
  • Su punto (s) de control puede ser movido

Además de la mera ubicación, un punto, es decir, un ''punto de anclaje'' debería tener más propiedades y habilidades / métodos ...:

  • Tiene puntos de control; cómo estos están relacionados con los puntos a veces no es exactamente lo mismo. Mirando los documentos de Unity, vemos que Handles.DrawLine analiza dos puntos y sus puntos de control "internos". Desde GDI + GraphicsPath veo una secuencia de puntos, que se alternan entre 1 ancla y 2 puntos de control. Imo, esto constituye un caso aún más sólido para tratar los dos puntos de control como propiedades del punto de anclaje. Como ambos deben ser móviles, pueden tener un ancestro común o estar conectados a la clase de movecontroller de movecontroller ; pero confío en que sepas mejor cómo hacerlo en Unidad ...

  • La propiedad con la que realmente comenzó la pregunta fue algo así como bool IsContinuous . Cuando sea true necesitamos pareja

    • moviendo un punto de control para mover el otro en la dirección ''opuesta''.
    • mover el ancla para mover ambos puntos de control en paralelo
  • Tal vez un bool IsLocked propiedad está bool IsLocked para evitar que se mueva
  • Tal vez una propiedad bool IsProtected para evitar eliminarlo al reducir / simplificar la curva. (Lo que no es necesario para las curvas construidas, pero sí lo es para las curvas desde el dibujo a mano alzada o el trazado con el mouse)
  • Tal vez una propiedad para saber que el punto en un grupo de puntos que se pueden editar juntos.
  • Tal vez un marcador general.
  • Tal vez una anotación de texto
  • Tal vez un indicador de tipo que denota una ruptura / división en la curva.
  • Tal vez métodos para aumentar o disminuir la suavidad frente a la puntualidad.

Algunos casos de uso claramente implican principalmente la curva, pero otros no; y algunos son útiles para ambos.

Así que, claramente tenemos muchas buenas razones para crear una clase inteligente de ÀnchPoint`.

((Estoy un poco atado, pero aún planeo escribir mi propio editor para las curvas más pequeñas de GraphicsPath. Cuando esto suceda, actualizaré la publicación con las cosas que aprendí, incluido el diseño de la clase que se me ocurrió ...) )