I am creating bezier curves with the following code. The curves can be extended to join several bezier curves by shift clicking in the scene view. My code has functionality for making the whole curve continuous or non-continuous. I realised that I need to make individual points (specifically anchor points) have this functionality.
I believe the most ideal way to go about this is creating a new class for the points with this functionality (making points continuous or non-continuous) since this can be used to add other properties that might be specific to the points. How can do this?
Path
[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;
}
}
The basic question in your post is: 'Is it a good idea to a have a separate Class for the points of a bezier curve?'
Since the curve will be made up of such points and these are more than just two coordinates imo it most certainly is a good idea.
But, as usual when doing class design, let's collect a few use cases, i.e. things a point will be used for or things we expect to do to a point..:
Besides the mere location, a point, i.e. an 'anchor point' should have more properties and abilities/methods..:
It has control points; how these are related to the points is sometimes not exactly the same. Looking at the Unity docs we see that the
Handles.DrawLine
looks at two points and their 'inner' control poiints. Coming from GDI+GraphicsPath
I see a sequence of points, altenrating between 1 anchor and 2 control points. Imo, this makes an even stronger case for treating the two control points as properties of the anchor point. Since both must be movable they may have a common ancestor or be hooked up tomovecontroller
class; but I trust you know best how to do that in Unity..The property the question really started with was something like
bool IsContinuous
. Whentrue
we need to couplebool IsLocked
to prevent moving itbool IsProtected
to prevent removing it when reducing/simplifying the curve. (Which is hardly needed for constructed curves but very much so for curves from free-hand drawing or tracing with the mouse)Some use cases clearly mostly involve the curve but others don't; and some are useful for both.
So, clearly we have a lot of good reasons to create a clever ÀnchPoint` class..
((I'm a bit tied up but still plan to write my own editor for the GraphicsPath bezier curves. If and when this happens, I'll update the post with things I learned including the class design I come up with..))
I think your idea is fine: you can write two classes, named
ControlPoint
andHandlePoint
(make them serializable).ControlPoint
may representp0
andp3
of each curve - the points the path indeed pass through. For continuity, you must assert thatp3
of one segment equals top0
of the next segment.HandlePoint
may representp1
andp2
of each curve - the points that are tangents of the curve and provide direction and inclination. For smoothness, you must assert that(p3 - p2).normalized
of one segment equals to(p1 - p0).normalized
of the next segment. (if you want symetric smoothness,p3 - p2
of one must equalsp1 - p0
of the other.)Tip #1: Always consider matrix transformations when assigning or comparing points of each segment. I suggest you to convert any point to global space before performing the operations.
Tip #2: consider applying a constraint between points inside a segment, so when you move arround
p0
orp3
of a curve,p1
orp2
move accordingly by the same amount, respectively (just like any graphics editor software do on bezier curves).Edit -> Code provided
I did a sample implementation of the idea. Actually, after start coding I realized that just one class
ControlPoint
(instead of two) will do the job. AControlPoint
have 2 tangents. The desired behaviour is controled by the fieldsmooth
, that can be set for each point.ControlPoint.cs
I also coded a custom
PropertyDrawer
for theControlPoint
class, so it can be shown better on the inspector. It is just a naive implementation. You could improve it very much.ControlPointDrawer.cs
I followed the same architecture of your solution, but with the needed adjustments to fit the
ControlPoint
class, and other fixes/changes. For example, I stored all the point values in local coordinates, so the transformations on the component or parents reflect in the curve.Path.cs
PathEditor
is pretty much the same thing.PathCreator.cs
Finally, all the magic happens in the
PathCreatorEditor
. Two comments here:1) I moved the drawing of the lines to a custom
DrawGizmo
static function, so you can have the lines even when the object is notActive
(i.e. shown in the Inspector) You could even make it pickable if you want to. I don't know if you want this behaviour, but you could easily revert;2) Notice the
Handles.matrix = creator.transform.localToWorldMatrix
lines over the class. It automatically transforms the scale and rotation of the points to the world coordinates. There is a detail withPivotRotation
over there too.PathCreatorEditor.cs
Moreover, I added a
loop
field in case you want the curve to be closed, and I added a naive funcionality to remove points byCtrl+click
on the Scene. Summing up, this is just basic stuff, but you could do it as advanced as you want. Also, you can reuse your ControlPoint class with other Components, like a Catmull-Rom spline, geometric shapes, other parametric functions...