public override void OnInspectorGUI() { //don't draw inspector fields if the path contains less than 2 points //(a path with less than 2 points really isn't a path) if (script.bPoints.Count < 2) return; //force naming scheme RenameWaypoints(); //checkbox field to enable editable path properties EditorGUILayout.BeginHorizontal(); script.showHandles = EditorGUILayout.Toggle("Show Handles", script.showHandles); EditorGUILayout.EndHorizontal(); //checkbox field for drawing gizmo path lines script.drawCurved = EditorGUILayout.Toggle("Draw Smooth Lines", script.drawCurved); //create new color fields for editing path gizmo colors script.color1 = EditorGUILayout.ColorField("Color1", script.color1); script.color2 = EditorGUILayout.ColorField("Color2", script.color2); script.color3 = EditorGUILayout.ColorField("Color3", script.color3); //calculate path length of all waypoints float pathLength = WaypointManager.GetPathLength(script.pathPoints); //path length label, show calculated path length GUILayout.Label("Path Length: " + pathLength); float thisDetail = script.pathDetail; //slider to modify the smoothing factor of the final path script.pathDetail = EditorGUILayout.Slider("Path Detail", script.pathDetail, 0.5f, 10); //toggle custom detail when modifying the whole path if (thisDetail != script.pathDetail) script.customDetail = false; //draw custom detail settings EditorGUILayout.Space(); DetailSettings(); EditorGUILayout.Space(); //waypoint index header GUILayout.Label("Waypoints: ", EditorStyles.boldLabel); //loop through the waypoint array for (int i = 0; i < script.bPoints.Count; i++) { GUILayout.BeginHorizontal(); //indicate each array slot with index number in front of it GUILayout.Label(i + ".", GUILayout.Width(20)); //create an object field for every waypoint EditorGUILayout.ObjectField(script.bPoints[i].wp, typeof(Transform), true); //display an "Add Waypoint" button for every array row except the last one //on click we call AddWaypointAtIndex() to insert a new waypoint slot AFTER the selected slot if (i < script.bPoints.Count && GUILayout.Button("+", GUILayout.Width(30f))) { AddWaypointAtIndex(i); break; } //display an "Remove Waypoint" button for every array row except the first and last one //on click we call RemoveWaypointAtIndex() to delete the selected waypoint slot if (i > 0 && i < script.bPoints.Count - 1 && GUILayout.Button("-", GUILayout.Width(30f))) { RemoveWaypointAtIndex(i); break; } GUILayout.EndHorizontal(); } EditorGUILayout.Space(); //button to move all waypoints down to the ground if (GUILayout.Button("Place to Ground")) { //for each waypoint of this path foreach (BezierPoint bp in script.bPoints) { //define ray to cast downwards waypoint position Ray ray = new Ray(bp.wp.position + new Vector3(0, 2f, 0), -Vector3.up); Undo.RecordObject(bp.wp, "PlaceToGround"); RaycastHit hit; //cast ray against ground, if it hit: if (Physics.Raycast(ray, out hit, 100)) { //position waypoint to hit point bp.wp.position = hit.point; } //also try to raycast against 2D colliders RaycastHit2D hit2D = Physics2D.Raycast(ray.origin, -Vector2.up, 100); if (hit2D) { bp.wp.position = new Vector3(hit2D.point.x, hit2D.point.y, bp.wp.position.z); } } } EditorGUILayout.Space(); //invert direction of whole path if (GUILayout.Button("Invert Direction")) { Undo.RecordObject(script, "Invert"); //to reverse the whole path we need to know where the waypoints were before //for this purpose a new copy must be created BezierPoint[] waypointCache = new BezierPoint[script.bPoints.Count]; for (int i = 0; i < waypointCache.Length; i++) waypointCache[i] = script.bPoints[i]; //reverse order based on the old list for (int i = 0; i < waypointCache.Length; i++) { BezierPoint currentPoint = script.bPoints[waypointCache.Length - 1 - i]; script.bPoints[waypointCache.Length - 1 - i] = waypointCache[i]; Vector3 leftHandle = currentPoint.cp[0].position; Undo.RecordObject(currentPoint.cp[0], "Invert"); Undo.RecordObject(currentPoint.cp[1], "Invert"); currentPoint.cp[0].position = currentPoint.cp[1].position; currentPoint.cp[1].position = leftHandle; } } EditorGUILayout.Space(); //draw object field for new waypoint object script.replaceObject = (GameObject)EditorGUILayout.ObjectField("Replace Object", script.replaceObject, typeof(GameObject), true); //replace all waypoints with the prefab if (GUILayout.Button("Replace Waypoints with Object")) { ReplaceWaypoints(); } //recalculate on inspector changes if (GUI.changed) { script.CalculatePath(); EditorUtility.SetDirty(target); } }
//repositions the opposite control point if one changes private void PositionOpposite(BezierPoint point, bool isLeft, Vector3 newPos) { Vector3 pos = point.wp.position; Vector3 toParent = pos - newPos; toParent.Normalize(); if (isLeft) { //received the left handle, manipulating the right float magnitude = (pos - point.cp[1].position).magnitude; point.cp[0].position = newPos; point.cp[1].position = pos + toParent * magnitude; } else { //received the right handle, manipulating the left float magnitude = (pos - point.cp[0].position).magnitude; point.cp[1].position = newPos; point.cp[0].position = pos + toParent * magnitude; } }
//adds a waypoint when clicking on the "+" button in the inspector private void AddWaypointAtIndex(int index) { //create a new bezier point property class BezierPoint point = new BezierPoint(); //create new waypoint gameobject Transform wp = new GameObject("Waypoint").transform; //disabled because of a Unity bug that crashes the editor //Undo.RecordObject(script, "Add"); //Undo.RegisterCreatedObjectUndo(wp, "Add"); //set its position to the last one wp.position = script.bPoints[index].wp.position; //assign it to the class point.wp = wp; //assign new control points Transform left = new GameObject("Left").transform; Transform right = new GameObject("Right").transform; left.parent = right.parent = wp; left.position = wp.position + new Vector3(2, 0, 0); right.position = wp.position + new Vector3(-2, 0, 0); point.cp = new[] { left, right }; //parent bezier point to the path gameobject wp.parent = script.transform; //add new detail value for the new segment script.segmentDetail.Insert(index + 1, script.pathDetail); //finally, insert this new waypoint after the one clicked script.bPoints.Insert(index + 1, point); }
//if this path is selected, display small info boxes above all waypoint positions //also display handles for the waypoints and their bezier points void OnSceneGUI() { //do not execute further code if we have no waypoints defined //(just to make sure, practically this can not occur) if (script.bPoints.Count == 0) { return; } Vector3 wpPos = Vector3.zero; float size = 1f; //handles for (int i = 0; i < script.bPoints.Count; i++) { //get related bezier point class BezierPoint point = script.bPoints[i]; if (point == null || !point.wp) { continue; } wpPos = point.wp.position; size = HandleUtility.GetHandleSize(wpPos) * 0.4f; if (size < 3f) { //begin GUI block Handles.BeginGUI(); //translate waypoint vector3 position in world space into a position on the screen var guiPoint = HandleUtility.WorldToGUIPoint(wpPos); //create rectangle with that positions and do some offset var rect = new Rect(guiPoint.x - 50.0f, guiPoint.y - 40, 100, 20); //draw box at position with current waypoint name GUI.Box(rect, point.wp.name); Handles.EndGUI(); //end GUI block } Handles.color = script.color2; //draw bezier point handles, clamp size size = Mathf.Clamp(size, 0, 1.2f); //Vector3 newPos = Handles.PositionHandle(wpPos, Quaternion.identity); Vector3 newPos = Handles.FreeMoveHandle(wpPos, Quaternion.identity, size, Vector3.zero, Handles.SphereCap); //Handles.RadiusHandle(Quaternion.identity, wpPos, size / 2); if (wpPos != newPos) { Undo.RecordObject(point.wp, "Move Handles"); point.wp.position = newPos; } if (!script.showHandles) { continue; } Handles.color = script.color3; //draw control point handles //left handle, all control points except first one if (i > 0) { float controlSize = HandleUtility.GetHandleSize(point.cp[0].position) * 0.25f; controlSize = Mathf.Clamp(controlSize, 0, 0.5f); PositionOpposite(point, true, Handles.FreeMoveHandle( point.cp[0].position, Quaternion.identity, controlSize, Vector3.zero, Handles.SphereCap)); Undo.RecordObject(point.cp[0], "Move Control Left"); } //right handle, all waypoints except last one if (i < script.bPoints.Count - 1) { float controlSize = HandleUtility.GetHandleSize(point.cp[1].position) * 0.25f; controlSize = Mathf.Clamp(controlSize, 0, 0.5f); PositionOpposite(point, false, Handles.FreeMoveHandle( point.cp[1].position, Quaternion.identity, controlSize, Vector3.zero, Handles.SphereCap)); Undo.RecordObject(point.cp[1], "Move Control Right"); } //draw line between control points Handles.DrawLine(point.cp[0].position, point.cp[1].position); } if (GUI.changed) { EditorUtility.SetDirty(target); } //recalculate path points after handles script.CalculatePath(); if (!script.showHandles) { return; } //draw small dots for each path point (not waypoint) Handles.color = script.color2; Vector3[] pathPoints = script.pathPoints; for (int i = 0; i < pathPoints.Length; i++) { Handles.SphereCap(0, pathPoints[i], Quaternion.identity, Mathf.Clamp((HandleUtility.GetHandleSize(pathPoints[i]) * 0.12f), 0, 0.25f)); } }
public override void OnInspectorGUI() { //don't draw inspector fields if the path contains less than 2 points //(a path with less than 2 points really isn't a path) if (script.bPoints.Count < 2) { return; } //checkbox field to enable editable path properties script.showHandles = EditorGUILayout.Toggle("Show Handles", script.showHandles); //checkbox field for toggling control point connectedness script.connectHandles = EditorGUILayout.Toggle("Connect Handles", script.connectHandles); //checkbox field for drawing gizmo path lines script.drawCurved = EditorGUILayout.Toggle("Draw Smooth Lines", script.drawCurved); //create new color fields for editing path gizmo colors script.color1 = EditorGUILayout.ColorField("Color1", script.color1); script.color2 = EditorGUILayout.ColorField("Color2", script.color2); script.color3 = EditorGUILayout.ColorField("Color3", script.color3); //calculate path length of all waypoints float pathLength = WaypointManager.GetPathLength(script.pathPoints); //path length label, show calculated path length GUILayout.Label("Path Length: " + pathLength); float thisDetail = script.pathDetail; //slider to modify the smoothing factor of the final path script.pathDetail = EditorGUILayout.Slider("Path Detail", script.pathDetail, 0.5f, 10); //toggle custom detail when modifying the whole path if (thisDetail != script.pathDetail) { script.customDetail = false; } //draw custom detail settings EditorGUILayout.Space(); DetailSettings(); EditorGUILayout.Space(); //waypoint index header GUILayout.Label("Waypoints: ", EditorStyles.boldLabel); //loop through the waypoint array for (int i = 0; i < script.bPoints.Count; i++) { GUILayout.BeginHorizontal(); //indicate each array slot with index number in front of it GUILayout.Label(i + ".", GUILayout.Width(20)); //create an object field for every waypoint EditorGUILayout.ObjectField(script.bPoints[i].wp, typeof(Transform), true); //display an "Add Waypoint" button for every array row except the last one //on click we call AddWaypointAtIndex() to insert a new waypoint slot AFTER the selected slot if (i < script.bPoints.Count && GUILayout.Button("+", GUILayout.Width(30f))) { AddWaypointAtIndex(i); break; } //display an "Remove Waypoint" button for every array row except the first and last one //on click we call RemoveWaypointAtIndex() to delete the selected waypoint slot if (i > 0 && i < script.bPoints.Count - 1 && GUILayout.Button("-", GUILayout.Width(30f))) { RemoveWaypointAtIndex(i); break; } GUILayout.EndHorizontal(); } EditorGUILayout.Space(); EditorGUILayout.BeginHorizontal(); //button to rename waypoints to current index order if (GUILayout.Button("Rename Waypoints")) { string wpName = string.Empty; string[] nameSplit; for (int i = 0; i < script.bPoints.Count; i++) { //cache name and split into strings wpName = script.bPoints[i].wp.name; nameSplit = wpName.Split(' '); //ignore custom names and just rename if (!script.skipCustomNames) { wpName = "Waypoint " + i; } else if (nameSplit.Length == 2 && nameSplit[0] == "Waypoint") { //try parsing the current index and rename, //not ignoring custom names here int index; if (int.TryParse(nameSplit[1], out index)) { wpName = nameSplit[0] + " " + i; } } //set the desired index or leave it script.bPoints[i].wp.name = wpName; } } EditorGUILayout.LabelField("Skip Custom", GUILayout.Width(80)); script.skipCustomNames = EditorGUILayout.Toggle(script.skipCustomNames, GUILayout.Width(20)); EditorGUILayout.EndHorizontal(); //button to move all waypoints down to the ground if (GUILayout.Button("Place to Ground")) { //for each waypoint of this path foreach (BezierPoint bp in script.bPoints) { //define ray to cast downwards waypoint position Ray ray = new Ray(bp.wp.position + new Vector3(0, 2f, 0), -Vector3.up); Undo.RecordObject(bp.wp, "PlaceToGround"); RaycastHit hit; //cast ray against ground, if it hit: if (Physics.Raycast(ray, out hit, 100)) { //position waypoint to hit point bp.wp.position = hit.point; } //also try to raycast against 2D colliders RaycastHit2D hit2D = Physics2D.Raycast(ray.origin, -Vector2.up, 100); if (hit2D) { bp.wp.position = new Vector3(hit2D.point.x, hit2D.point.y, bp.wp.position.z); } } } //invert direction of whole path if (GUILayout.Button("Invert Direction")) { Undo.RecordObject(script, "Invert"); //to reverse the whole path we need to know where the waypoints were before //for this purpose a new copy must be created BezierPoint[] waypointCache = new BezierPoint[script.bPoints.Count]; for (int i = 0; i < waypointCache.Length; i++) { waypointCache[i] = script.bPoints[i]; } //reverse order based on the old list for (int i = 0; i < waypointCache.Length; i++) { BezierPoint currentPoint = script.bPoints[waypointCache.Length - 1 - i]; script.bPoints[waypointCache.Length - 1 - i] = waypointCache[i]; Vector3 leftHandle = currentPoint.cp[0].position; Undo.RecordObject(currentPoint.cp[0], "Invert"); Undo.RecordObject(currentPoint.cp[1], "Invert"); currentPoint.cp[0].position = currentPoint.cp[1].position; currentPoint.cp[1].position = leftHandle; } } EditorGUILayout.Space(); //draw object field for new waypoint object script.replaceObject = (GameObject)EditorGUILayout.ObjectField("Replace Object", script.replaceObject, typeof(GameObject), true); //replace all waypoints with the prefab if (GUILayout.Button("Replace Waypoints with Object")) { ReplaceWaypoints(); } //recalculate on inspector changes if (GUI.changed) { script.CalculatePath(); EditorUtility.SetDirty(target); } }
//bezier path placement void PlaceBezierPoint(Vector3 placePos) { //create new bezier point property class BezierPoint newPoint = new BezierPoint(); //instantiate waypoint gameobject Transform wayp = new GameObject("Waypoint").transform; //assign waypoint to the class newPoint.wp = wayp; //same as above if (wpList.Count == 0) { pathMan.transform.position = placePos; } //position current waypoint at clicked position in scene view if (mode2D) { placePos.z = 0f; } wayp.position = placePos; //parent it to the defined path wayp.parent = pathMan.transform; BezierPathManager thisPath = pathMan as BezierPathManager; //create new array with bezier point handle positions Transform left = new GameObject("Left").transform; Transform right = new GameObject("Right").transform; left.parent = right.parent = wayp; //initialize positions and last waypoint Vector3 handleOffset = new Vector3(2, 0, 0); Vector3 targetDir = Vector3.zero; int lastIndex = wpList.Count - 1; //position handles to the left/right of the waypoint respectively left.position = wayp.position + wayp.rotation * handleOffset; right.position = wayp.position + wayp.rotation * -handleOffset; newPoint.cp = new[] { left, right }; //position first handle in direction of the second waypoint if (wpList.Count == 1) { targetDir = (wayp.position - wpList[0].transform.position).normalized; thisPath.bPoints[0].cp[1].localPosition = targetDir * 2; } //always position last handle to look at the previous waypoint else if (wpList.Count >= 1) { targetDir = (wpList[lastIndex].transform.position - wayp.position); wayp.transform.rotation = Quaternion.LookRotation(targetDir) * Quaternion.Euler(0, -90, 0); } //position handle direction to the center of both last and next waypoints //takes into account 2D mode if (wpList.Count >= 2) { //get last point and center direction BezierPoint lastPoint = thisPath.bPoints[lastIndex]; targetDir = (wayp.position - wpList[lastIndex].transform.position) + (wpList[lastIndex - 1].transform.position - wpList[lastIndex].transform.position); //rotate to the center 2D/3D Quaternion lookRot = Quaternion.LookRotation(targetDir); if (mode2D) { float angle = Mathf.Atan2(targetDir.y, targetDir.x) * Mathf.Rad2Deg + 90; lookRot = Quaternion.AngleAxis(angle, Vector3.forward); } lastPoint.wp.rotation = lookRot; //cache handle and get previous of last waypoint Vector3 leftPos = lastPoint.cp[0].position; Vector3 preLastPos = wpList[lastIndex - 1].transform.position; //calculate whether right or left handle distance is greater to last waypoint //left handle should point to the last waypoint, so reposition if necessary if (Vector3.Distance(leftPos, preLastPos) > Vector3.Distance(lastPoint.cp[1].position, preLastPos)) { lastPoint.cp[0].position = lastPoint.cp[1].position; lastPoint.cp[1].position = leftPos; } } //add waypoint to the list of waypoints thisPath.bPoints.Add(newPoint); thisPath.segmentDetail.Add(thisPath.pathDetail); //add waypoint to temporary list wpList.Add(wayp.gameObject); //rename waypoint to match the list count wayp.name = "Waypoint " + (wpList.Count - 1); //recalculate bezier path thisPath.CalculatePath(); }