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:
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
Retain a single method that handles the movement of each point instead of having separate methods for the movement of each point.
To open an Inspector window, do one of the following: From the menu, select Windows > General > Inspector to open a floating Inspector window. From any window's More Items menu (⋮), select Add Tab > Inspector to open an Inspector in a new tab.
When you select a GameObject in the Hierarchy or Scene View, the Inspector will show the Properties of all Components and Materials on that object and allow you to edit them. The image above shows the inspector with the default 3D camera GameObject selected.
Enable script debugging in a Unity player In Unity, open the Build Settings by selecting File > Build Settings. In the Build Settings window, mark the Development Build and Script Debugging checkboxes.
The Inspector displays detailed information about your currently selected GameObject, including all attached Components and their properties. Here, you modify the functionality of GameObjects in your scene.
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...
Not entirely sure if I understood the question, but
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
You need to use OnSceneGUI option to be able to select the handles, and everytime you select a new point just store its value.
private void OnSceneGUI ()
{
spline = target as Path;
handleTransform = spline.transform;
handleRotation = Tools.pivotRotation == PivotRotation.Local
? handleTransform.rotation : Quaternion.identity;
Vector3 p0 = ShowPoint (0);
Color gg = Color.gray;
gg.a = 0.25f;
for (int i = 1; i < spline.ControlPointCount; i += 3)
{
Vector3 p1 = ShowPoint (i);
Vector3 p2 = ShowPoint (i + 1);
Vector3 p3 = ShowPoint (i + 2);
Handles.color = gg;
Handles.DrawLine (p0, p1);
Handles.DrawLine (p2, p3);
Handles.DrawBezier (p0, p3, p1, p2, Color.white, null, 2f);
p0 = p3;
}
ShowDirections ();
}
private Vector3 ShowPoint (int index)
{
Vector3 point = handleTransform.TransformPoint (spline.Points[index]);
float size = HandleUtility.GetHandleSize (point);
if (index == 0)
{
size *= 2f;
}
Handles.color = modeColors[(int) spline.GetControlPointMode (index)];
if (Handles.Button (point, handleRotation, size * handleSize, size * pickSize, Handles.DotCap))
{
selectedIndex = index;
Repaint ();
}
if (selectedIndex == index)
{
EditorGUI.BeginChangeCheck ();
point = Handles.DoPositionHandle (point, handleRotation);
if (EditorGUI.EndChangeCheck ())
{
Undo.RecordObject (spline, "Move Point");
EditorUtility.SetDirty (spline);
spline.SetControlPoint (handleTransform.InverseTransformPoint (point), index);
}
}
return point;
}
and for showing the point on inspector GUI
private void DrawSelectedPointInspector ()
{
GUILayout.Label ("Selected Point");
EditorGUI.BeginChangeCheck ();
Vector3 point = EditorGUILayout.Vector3Field ("Position", spline.Points[selectedIndex]);
if (EditorGUI.EndChangeCheck ())
{
Undo.RecordObject (spline, "Move Point");
EditorUtility.SetDirty (spline);
spline.SetControlPoint (point, selectedIndex);
}
EditorGUI.BeginChangeCheck ();
BezierControlPointModes mode = (BezierControlPointModes) EditorGUILayout.EnumPopup ("Mode", spline.GetControlPointMode (selectedIndex));
if (EditorGUI.EndChangeCheck ())
{
Undo.RecordObject (spline, "Change Point Mode");
spline.SetControlPointMode (selectedIndex, mode);
EditorUtility.SetDirty (spline);
}
}
and call this on the InspectorGUI
if (selectedIndex >= 0 && selectedIndex < spline.points.Count)
{
DrawSelectedPointInspector ();
}
Retain a single method that handles the movement of each point instead of having separate methods for the movement of each point. For this you just need to retain the
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With