588 lines
24 KiB
C#
588 lines
24 KiB
C#
/* This file is part of the "Simple Waypoint System" project by Rebound Games.
|
|
* You are only allowed to use these resources if you've bought them directly or indirectly
|
|
* from Rebound Games. You shall not license, sublicense, sell, resell, transfer, assign,
|
|
* distribute or otherwise make available to any third party the Service or the Content.
|
|
*/
|
|
|
|
using UnityEngine;
|
|
using UnityEditor;
|
|
using System.Collections.Generic;
|
|
|
|
namespace SWS
|
|
{
|
|
/// <summary>
|
|
/// Custom path inspector.
|
|
/// <summary>
|
|
[CustomEditor(typeof(PathManager))]
|
|
public class PathEditor : Editor
|
|
{
|
|
//define Serialized Objects we want to use/control
|
|
//this will be our serialized reference to the inspected script
|
|
private SerializedObject m_Object;
|
|
//serialized waypoint array
|
|
private SerializedProperty m_Waypoint;
|
|
//serialized waypoint array count
|
|
private SerializedProperty m_WaypointsCount;
|
|
//serialized path gizmo property booleans
|
|
private SerializedProperty m_Check1;
|
|
private SerializedProperty m_Check2;
|
|
//serialized scene view gizmo colors
|
|
private SerializedProperty m_Color1;
|
|
private SerializedProperty m_Color2;
|
|
//serialized custom waypoint renaming bool
|
|
private SerializedProperty m_SkipNames;
|
|
//serialized replace object gameobject
|
|
private SerializedProperty m_WaypointPref;
|
|
|
|
//waypoint array size, define path to know where to lookup for this variable
|
|
//(we expect an array, so it's "name_of_array.data_type.size")
|
|
private static string wpArraySize = "waypoints.Array.size";
|
|
//.data gives us the data of the array,
|
|
//we replace this {0} token with an index we want to get
|
|
private static string wpArrayData = "waypoints.Array.data[{0}]";
|
|
//currently selected waypoint node for position/rotation editing
|
|
private int activeNode = -1;
|
|
//modifier action that executes specific path manipulations
|
|
private PathModifierOption editOption = PathModifierOption.SelectModifier;
|
|
|
|
/*
|
|
private enum PathModifierOption
|
|
{
|
|
SelectModifier,
|
|
PlaceToGround,
|
|
InvertDirection,
|
|
RotateWaypointsToPath,
|
|
RenameWaypoints,
|
|
UpdateFromChildren,
|
|
ReplaceWaypointObject
|
|
}
|
|
*/
|
|
|
|
//called whenever this inspector window is loaded
|
|
public void OnEnable()
|
|
{
|
|
//we create a reference to our script object by passing in the target
|
|
m_Object = new SerializedObject(target);
|
|
|
|
//from this object, we pull out the properties we want to use
|
|
//these are just the names of our variables in the manager
|
|
m_Check1 = m_Object.FindProperty("drawCurved");
|
|
m_Check2 = m_Object.FindProperty("drawDirection");
|
|
m_Color1 = m_Object.FindProperty("color1");
|
|
m_Color2 = m_Object.FindProperty("color2");
|
|
m_SkipNames = m_Object.FindProperty("skipCustomNames");
|
|
m_WaypointPref = m_Object.FindProperty("replaceObject");
|
|
|
|
//set serialized waypoint array count by passing in the path to our array size
|
|
m_WaypointsCount = m_Object.FindProperty(wpArraySize);
|
|
//reset selected waypoint
|
|
activeNode = -1;
|
|
}
|
|
|
|
|
|
private Transform[] GetWaypointArray()
|
|
{
|
|
//get array count from serialized property and store its int value into var arrayCount
|
|
var arrayCount = m_Object.FindProperty(wpArraySize).intValue;
|
|
//create new waypoint transform array with size of arrayCount
|
|
var transformArray = new Transform[arrayCount];
|
|
//loop over waypoints
|
|
for (var i = 0; i < arrayCount; i++)
|
|
{
|
|
//for each one use "FindProperty" to get the associated object reference
|
|
//of waypoints array, string.Format replaces {0} token with index i
|
|
//and store the object reference value as type of transform in transformArray[i]
|
|
transformArray[i] = m_Object.FindProperty(string.Format(wpArrayData, i)).objectReferenceValue as Transform;
|
|
}
|
|
//finally return that array copy for modification purposes
|
|
return transformArray;
|
|
}
|
|
|
|
|
|
//similiar to GetWaypointArray(), find serialized property which belongs to index
|
|
//and set this value to parameter transform "waypoint" directly
|
|
private void SetWaypoint(int index, Transform waypoint)
|
|
{
|
|
//reset selected waypoint before manipulating array
|
|
activeNode = -1;
|
|
|
|
m_Object.FindProperty(string.Format(wpArrayData, index)).objectReferenceValue = waypoint;
|
|
}
|
|
|
|
|
|
//similiar to SetWaypoint(), this will find the waypoint from array at index position
|
|
//and returns it instead of modifying
|
|
private Transform GetWaypointAtIndex(int index)
|
|
{
|
|
return m_Object.FindProperty(string.Format(wpArrayData, index)).objectReferenceValue as Transform;
|
|
}
|
|
|
|
|
|
//get the corresponding waypoint and destroy the whole gameobject in editor
|
|
private void RemoveWaypointAtIndex(int index)
|
|
{
|
|
Undo.DestroyObjectImmediate(GetWaypointAtIndex(index).gameObject);
|
|
|
|
//iterate over the array, starting at index,
|
|
//and replace it with the next one
|
|
for (int i = index; i < m_WaypointsCount.intValue - 1; i++)
|
|
SetWaypoint(i, GetWaypointAtIndex(i + 1));
|
|
|
|
//decrement array count by 1
|
|
m_WaypointsCount.intValue--;
|
|
RenameWaypoints(GetWaypointArray(), true);
|
|
}
|
|
|
|
|
|
private void AddWaypointAtIndex(int index)
|
|
{
|
|
//increment array count so the waypoint array is one unit larger
|
|
m_WaypointsCount.intValue++;
|
|
|
|
//backwards loop through array:
|
|
//since we're adding a new waypoint for example in the middle of the array,
|
|
//we need to push all existing waypoints after that selected waypoint
|
|
//1 slot upwards to have one free slot in the middle. So:
|
|
//we're doing exactly that and start looping at the end downwards to the selected slot
|
|
for (int i = m_WaypointsCount.intValue - 1; i > index; i--)
|
|
SetWaypoint(i, GetWaypointAtIndex(i - 1));
|
|
|
|
//create new waypoint gameobject
|
|
GameObject wp = new GameObject("Waypoint " + (index + 1));
|
|
Undo.RegisterCreatedObjectUndo(wp, "Created WP");
|
|
|
|
//set its position to the selected one
|
|
wp.transform.position = GetWaypointAtIndex(index).position;
|
|
//parent it to the path gameobject
|
|
wp.transform.SetParent(GetWaypointAtIndex(index).parent);
|
|
wp.transform.SetSiblingIndex(index + 1);
|
|
//finally, set this new waypoint after the one clicked in waypoints array
|
|
SetWaypoint(index + 1, wp.transform);
|
|
RenameWaypoints(GetWaypointArray(), true);
|
|
activeNode = index + 1;
|
|
}
|
|
|
|
|
|
//called whenever the inspector gui gets rendered
|
|
public override void OnInspectorGUI()
|
|
{
|
|
//this pulls the relative variables from unity runtime and stores them in the object
|
|
m_Object.Update();
|
|
|
|
//get waypoint array
|
|
var waypoints = GetWaypointArray();
|
|
|
|
//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 (m_WaypointsCount.intValue < 2)
|
|
{
|
|
//button to create path manually
|
|
if (GUILayout.Button("Create Path from Children"))
|
|
{
|
|
Undo.RecordObjects(waypoints, "Create Path");
|
|
(m_Object.targetObject as PathManager).Create();
|
|
SceneView.RepaintAll();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
//create new checkboxes for path gizmo property
|
|
m_Check1.boolValue = EditorGUILayout.Toggle("Draw Smooth Lines", m_Check1.boolValue);
|
|
m_Check2.boolValue = EditorGUILayout.Toggle("Draw Direction", m_Check2.boolValue);
|
|
|
|
//create new property fields for editing waypoint gizmo colors
|
|
EditorGUILayout.PropertyField(m_Color1);
|
|
EditorGUILayout.PropertyField(m_Color2);
|
|
|
|
//calculate path length of all waypoints
|
|
Vector3[] wpPositions = new Vector3[waypoints.Length];
|
|
for (int i = 0; i < waypoints.Length; i++)
|
|
wpPositions[i] = waypoints[i].position;
|
|
|
|
float pathLength = WaypointManager.GetPathLength(wpPositions);
|
|
//path length label, show calculated path length
|
|
GUILayout.Label("Path Length: " + pathLength);
|
|
|
|
//button for switching over to the WaypointManager for further path editing
|
|
if (GUILayout.Button("Continue Editing"))
|
|
{
|
|
Selection.activeGameObject = (GameObject.FindObjectOfType(typeof(WaypointManager)) as WaypointManager).gameObject;
|
|
WaypointEditor.ContinuePath(m_Object.targetObject as PathManager);
|
|
}
|
|
|
|
//more path modifiers
|
|
DrawPathOptions();
|
|
EditorGUILayout.Space();
|
|
|
|
//waypoint index header
|
|
GUILayout.Label("Waypoints: ", EditorStyles.boldLabel);
|
|
|
|
//loop through the waypoint array
|
|
for (int i = 0; i < waypoints.Length; 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(waypoints[i], typeof(Transform), true);
|
|
|
|
//display an "Add Waypoint" button for every array row except the last one
|
|
if (i < waypoints.Length && GUILayout.Button("+", GUILayout.Width(30f)))
|
|
{
|
|
AddWaypointAtIndex(i);
|
|
break;
|
|
}
|
|
|
|
//display an "Remove Waypoint" button for every array row except the first and last one
|
|
if (i > 0 && i < waypoints.Length - 1 && GUILayout.Button("-", GUILayout.Width(30f)))
|
|
{
|
|
RemoveWaypointAtIndex(i);
|
|
break;
|
|
}
|
|
|
|
GUILayout.EndHorizontal();
|
|
}
|
|
|
|
//we push our modified variables back to our serialized object
|
|
m_Object.ApplyModifiedProperties();
|
|
}
|
|
|
|
|
|
//if this path is selected, display small info boxes above all waypoint positions
|
|
//also display handles for the waypoints
|
|
void OnSceneGUI()
|
|
{
|
|
//again, get waypoint array
|
|
var waypoints = GetWaypointArray();
|
|
//do not execute further code if we have no waypoints defined
|
|
//(just to make sure, practically this can not occur)
|
|
if (waypoints.Length == 0) return;
|
|
Vector3 wpPos = Vector3.zero;
|
|
float size = 1f;
|
|
|
|
//loop through waypoint array
|
|
for (int i = 0; i < waypoints.Length; i++)
|
|
{
|
|
if (!waypoints[i]) continue;
|
|
wpPos = waypoints[i].position;
|
|
|
|
size = HandleUtility.GetHandleSize(wpPos) * 0.4f;
|
|
|
|
//do not draw waypoint header if too far away
|
|
if (size < 3f)
|
|
{
|
|
//begin 2D 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, waypoints[i].name);
|
|
Handles.EndGUI(); //end GUI block
|
|
}
|
|
|
|
//draw handles per waypoint, clamp size
|
|
Handles.color = m_Color2.colorValue;
|
|
size = Mathf.Clamp(size, 0, 1.2f);
|
|
|
|
#if UNITY_5_6_OR_NEWER
|
|
var fmh_291_47_638770478105348637 = Quaternion.identity; Handles.FreeMoveHandle(wpPos, size, Vector3.zero, (controlID, position, rotation, hSize, eventType) =>
|
|
{
|
|
Handles.SphereHandleCap(controlID, position, rotation, hSize, eventType);
|
|
if(controlID == GUIUtility.hotControl && GUIUtility.hotControl != 0)
|
|
activeNode = i;
|
|
});
|
|
#else
|
|
Handles.FreeMoveHandle(wpPos, Quaternion.identity, size, Vector3.zero, (controlID, position, rotation, hSize) =>
|
|
{
|
|
Handles.SphereCap(controlID, position, rotation, hSize);
|
|
if(controlID == GUIUtility.hotControl && GUIUtility.hotControl != 0)
|
|
activeNode = i;
|
|
});
|
|
#endif
|
|
|
|
Handles.RadiusHandle(waypoints[i].rotation, wpPos, size / 2);
|
|
}
|
|
|
|
if(activeNode > -1)
|
|
{
|
|
wpPos = waypoints[activeNode].position;
|
|
Quaternion wpRot = waypoints[activeNode].rotation;
|
|
switch(Tools.current)
|
|
{
|
|
case Tool.Move:
|
|
if(Tools.pivotRotation == PivotRotation.Global)
|
|
wpRot = Quaternion.identity;
|
|
|
|
Vector3 newPos = Handles.PositionHandle(wpPos, wpRot);
|
|
if(wpPos != newPos)
|
|
{
|
|
Undo.RecordObject(waypoints[activeNode], "Move Handle");
|
|
waypoints[activeNode].position = newPos;
|
|
}
|
|
break;
|
|
|
|
case Tool.Rotate:
|
|
Quaternion newRot = Handles.RotationHandle(wpRot, wpPos);
|
|
|
|
if(wpRot != newRot)
|
|
{
|
|
Undo.RecordObject(waypoints[activeNode], "Rotate Handle");
|
|
waypoints[activeNode].rotation = newRot;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
//waypoint direction handles drawing
|
|
if(!m_Check2.boolValue) return;
|
|
Vector3[] pathPoints = new Vector3[waypoints.Length];
|
|
for(int i = 0; i < pathPoints.Length; i++)
|
|
pathPoints[i] = waypoints[i].position;
|
|
|
|
//create list of path segments (list of Vector3 list)
|
|
List<List<Vector3>> segments = new List<List<Vector3>>();
|
|
int curIndex = 0;
|
|
float lerpVal = 0f;
|
|
|
|
//differ between linear and curved display
|
|
switch(m_Check1.boolValue)
|
|
{
|
|
case true:
|
|
//convert waypoints to curved path points
|
|
pathPoints = WaypointManager.GetCurved(pathPoints);
|
|
//calculate approximate path point amount per segment
|
|
int detail = Mathf.FloorToInt((pathPoints.Length - 1f) / (waypoints.Length - 1f));
|
|
|
|
for(int i = 0; i < waypoints.Length - 1; i++)
|
|
{
|
|
float dist = Mathf.Infinity;
|
|
//loop over path points to find single segments
|
|
segments.Add(new List<Vector3>());
|
|
|
|
//we are not checking for absolute path points on standard paths, because
|
|
//path points could also be located before or after waypoint positions.
|
|
//instead a minimum distance is searched which marks the nearest path point
|
|
for(int j = curIndex; j < pathPoints.Length; j++)
|
|
{
|
|
//add path point to current segment
|
|
segments[i].Add(pathPoints[j]);
|
|
|
|
//start looking for distance after a certain amount of path points of this segment
|
|
if(j >= (i+1) * detail)
|
|
{
|
|
//calculate distance of current path point to waypoint
|
|
float pointDist = Vector3.Distance(waypoints[i].position, pathPoints[j]);
|
|
//we are getting closer to the waypoint
|
|
if(pointDist < dist)
|
|
dist = pointDist;
|
|
else
|
|
{
|
|
//current path point is more far away than the last one
|
|
//the segment ends here, continue with new segment
|
|
curIndex = j + 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case false:
|
|
//detail for arrows between waypoints
|
|
int lerpMax = 16;
|
|
//loop over waypoints to add intermediary points
|
|
for(int i = 0; i < waypoints.Length - 1; i++)
|
|
{
|
|
segments.Add(new List<Vector3>());
|
|
for(int j = 0; j < lerpMax; j++)
|
|
{
|
|
//linear lerp between waypoints to get additional points for drawing arrows at
|
|
segments[i].Add(Vector3.Lerp(pathPoints[i], pathPoints[i+1], j / (float)lerpMax));
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
//loop over segments
|
|
for(int i = 0; i < segments.Count; i++)
|
|
{
|
|
//loop over single positions on the segment
|
|
for(int j = 0; j < segments[i].Count; j++)
|
|
{
|
|
//get current lerp value for interpolating rotation
|
|
//draw arrow handle on current position with interpolated rotation
|
|
size = Mathf.Clamp(HandleUtility.GetHandleSize(segments[i][j]) * 0.4f, 0, 1.2f);
|
|
lerpVal = j / (float)segments[i].Count;
|
|
|
|
#if UNITY_5_6_OR_NEWER
|
|
Handles.ArrowHandleCap(0, segments[i][j], Quaternion.Lerp(waypoints[i].rotation, waypoints[i + 1].rotation, lerpVal), size, EventType.Repaint);
|
|
#else
|
|
Handles.ArrowCap(0, segments[i][j], Quaternion.Lerp(waypoints[i].rotation, waypoints[i + 1].rotation, lerpVal), size);
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private void DrawPathOptions()
|
|
{
|
|
Transform[] waypoints = GetWaypointArray();
|
|
editOption = (PathModifierOption)EditorGUILayout.EnumPopup(editOption);
|
|
|
|
switch (editOption)
|
|
{
|
|
case PathModifierOption.PlaceToGround:
|
|
foreach (Transform trans in waypoints)
|
|
{
|
|
//define ray to cast downwards waypoint position
|
|
Ray ray = new Ray (trans.position + new Vector3 (0, 2f, 0), -Vector3.up);
|
|
Undo.RecordObject (trans, "Place To Ground");
|
|
|
|
RaycastHit hit;
|
|
//cast ray against ground, if it hit:
|
|
if (Physics.Raycast (ray, out hit, 100)) {
|
|
//position waypoint to hit point
|
|
trans.position = hit.point;
|
|
}
|
|
|
|
//also try to raycast against 2D colliders
|
|
RaycastHit2D hit2D = Physics2D.Raycast (ray.origin, -Vector2.up, 100);
|
|
if (hit2D) {
|
|
trans.position = new Vector3 (hit2D.point.x, hit2D.point.y, trans.position.z);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case PathModifierOption.InvertDirection:
|
|
Undo.RecordObjects(waypoints, "Invert Direction");
|
|
|
|
//to reverse the whole path we need to know where the waypoints were before
|
|
//for this purpose a new copy must be created
|
|
Vector3[] waypointCopy = new Vector3[waypoints.Length];
|
|
for (int i = 0; i < waypoints.Length; i++)
|
|
waypointCopy[i] = waypoints[i].position;
|
|
|
|
//looping over the array in reversed order
|
|
for (int i = 0; i < waypoints.Length; i++)
|
|
waypoints[i].position = waypointCopy[waypointCopy.Length - 1 - i];
|
|
|
|
break;
|
|
|
|
case PathModifierOption.RotateWaypointsToPath:
|
|
Undo.RecordObjects(waypoints, "Rotate Waypoints");
|
|
|
|
//orient waypoints to the path in forward direction
|
|
for(int i = 0; i < waypoints.Length - 1; i++)
|
|
waypoints[i].LookAt(waypoints[i+1]);
|
|
|
|
waypoints[waypoints.Length - 1].rotation = waypoints[waypoints.Length - 2].rotation;
|
|
break;
|
|
|
|
case PathModifierOption.RenameWaypoints:
|
|
//disabled because of a Unity bug that crashes the editor
|
|
//this is taken directly from the docs, thank you Unity.
|
|
//http://docs.unity3d.com/ScriptReference/Undo.RegisterCompleteObjectUndo.html
|
|
//Undo.RegisterCompleteObjectUndo(waypoints[0].gameObject, "Rename Waypoints");
|
|
EditorGUILayout.BeginHorizontal();
|
|
EditorGUILayout.LabelField("Skip Custom Names?");
|
|
m_SkipNames.boolValue = EditorGUILayout.Toggle(m_SkipNames.boolValue, GUILayout.Width(20));
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
if(!GUILayout.Button("Rename Now"))
|
|
{
|
|
return;
|
|
}
|
|
|
|
RenameWaypoints(waypoints, m_SkipNames.boolValue);
|
|
break;
|
|
|
|
case PathModifierOption.UpdateFromChildren:
|
|
Undo.RecordObjects(waypoints, "Update Path From Children");
|
|
(m_Object.targetObject as PathManager).Create();
|
|
SceneView.RepaintAll();
|
|
break;
|
|
|
|
case PathModifierOption.ReplaceWaypointObject:
|
|
//draw object field for waypoint prefab
|
|
EditorGUILayout.PropertyField(m_WaypointPref);
|
|
|
|
//replace all waypoints with the prefab
|
|
if (!GUILayout.Button("Replace Now")) return;
|
|
else if (m_WaypointPref == null || m_WaypointPref.objectReferenceValue == null)
|
|
{
|
|
Debug.LogWarning("No replace object set. Cancelling.");
|
|
return;
|
|
}
|
|
|
|
//get prefab object and path transform
|
|
var waypointPrefab = m_WaypointPref.objectReferenceValue as GameObject;
|
|
var path = GetWaypointAtIndex(0).parent;
|
|
Undo.RegisterFullObjectHierarchyUndo(path, "Replace Object");
|
|
|
|
//loop through waypoint array of this path
|
|
for (int i = 0; i < m_WaypointsCount.intValue; i++)
|
|
{
|
|
//get current waypoint at index position
|
|
Transform curWP = GetWaypointAtIndex(i);
|
|
//instantiate new waypoint at old position
|
|
Transform newCur = ((GameObject)Instantiate(waypointPrefab, curWP.position, Quaternion.identity)).transform;
|
|
|
|
//parent new waypoint to this path
|
|
newCur.parent = path;
|
|
//replace old waypoint at index
|
|
SetWaypoint(i, newCur);
|
|
|
|
//destroy old waypoint object
|
|
Undo.DestroyObjectImmediate(curWP.gameObject);
|
|
}
|
|
break;
|
|
}
|
|
|
|
editOption = PathModifierOption.SelectModifier;
|
|
}
|
|
|
|
|
|
private void RenameWaypoints(Transform[] waypoints, bool skipCustom)
|
|
{
|
|
string wpName = string.Empty;
|
|
string[] nameSplit;
|
|
for (int i = 0; i < waypoints.Length; i++)
|
|
{
|
|
//cache name and split into strings
|
|
wpName = waypoints[i].name;
|
|
nameSplit = wpName.Split(' ');
|
|
|
|
//ignore custom names and just rename
|
|
if(!skipCustom)
|
|
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
|
|
waypoints[i].name = wpName;
|
|
}
|
|
}
|
|
}
|
|
|
|
enum PathModifierOption
|
|
{
|
|
SelectModifier,
|
|
PlaceToGround,
|
|
InvertDirection,
|
|
RotateWaypointsToPath,
|
|
RenameWaypoints,
|
|
UpdateFromChildren,
|
|
ReplaceWaypointObject
|
|
}
|
|
} |