Skip to content

Instantly share code, notes, and snippets.

@N-Carter
Created May 27, 2011 22:35
Show Gist options
  • Save N-Carter/996330 to your computer and use it in GitHub Desktop.
Save N-Carter/996330 to your computer and use it in GitHub Desktop.
Path editor script
using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
[CustomEditor(typeof(Path))]
public class PathEditor : Editor
{
protected Path m_Path;
protected Vector2 m_ScrollPosition;
protected HashSet<Path.Waypoint> m_SelectedWaypoints;
protected Path.Waypoint m_WaypointProperties; // FIXME: for multiple selection editing, if I get there
protected GUISkin m_InspectorSkin;
protected GUISkin m_SceneSkin;
protected const int kWaypointHandleSize = 16;
protected Color selectionColour = new Color(0.5f, 0.7f, 1.0f, 1.0f);
protected Texture m_EventTexture;
[MenuItem("Toolkit/Path/Create Path")]
protected static void CreatePath()
{
new GameObject("Path").AddComponent<Path>();
}
protected void OnEnable()
{
m_Path = (Path)target;
m_InspectorSkin = EditorGUIUtility.GetBuiltinSkin(EditorSkin.Inspector);
m_SceneSkin = EditorGUIUtility.GetBuiltinSkin(EditorSkin.Scene);
m_EventTexture = (Texture2D)EditorGUIUtility.Load("Path Editor/Textures/Path Event.tif");
// m_EventTexture = (Texture2D)EditorGUIUtility.FindTexture("Path Event.tif");
m_SelectedWaypoints = new HashSet<Path.Waypoint>();
Repaint();
}
#region Inspector GUI
public override void OnInspectorGUI()
{
if(m_Path.m_Waypoints.Count == 0)
m_Path.m_Waypoints.Add(new Path.Waypoint("", m_Path.transform.position));
// base.OnInspectorGUI();
EditorGUIUtility.LookLikeControls();
m_Path.m_Loop = EditorGUILayout.Toggle("Loop", m_Path.m_Loop);
m_Path.m_Subdivisions = EditorGUILayout.IntField("Subdivisions", m_Path.m_Subdivisions);
using(new EditorGUIGroupHorizontal())
{
EditorGUILayout.PrefixLabel("Next path");
m_Path.m_NextPath = (Path)EditorGUILayout.ObjectField(m_Path.m_NextPath, typeof(Path), GUILayout.Height(22));
}
m_Path.m_Colour = EditorGUILayout.ColorField("Colour", m_Path.m_Colour);
m_Path.m_ShowVertices = EditorGUILayout.Toggle("Show vertices", m_Path.m_ShowVertices);
m_Path.m_ProjectedDownwards = EditorGUILayout.Toggle("Project down", m_Path.m_ProjectedDownwards);
// Waypoint list:
EditorGUILayout.Separator();
GUILayout.Label("Waypoints", EditorStyles.boldLabel);
SelectionButtons();
int numSelectedWaypoints = m_SelectedWaypoints.Count;
if(numSelectedWaypoints == 0)
{
GUILayout.Label("Nothing selected.", "Box", GUILayout.ExpandWidth(true));
AlignButtons("Align all waypoints");
}
else if(numSelectedWaypoints == 1)
WaypointGUI(m_SelectedWaypoints.First());
else
{
// FIXME: add support for setting properties of multiple waypoints.
MultipleWaypointsGUI();
AlignButtons("Align selected waypoints");
}
// Event list:
EditorGUILayout.Separator();
GUILayout.Label("Events", EditorStyles.boldLabel);
EventScrollViewGUI();
// Debugging log:
// if(GUILayout.Button("Dump Data to Log"))
// {
// System.Console.WriteLine("Path {0}", path.name);
//
// foreach(Path.Segment segment in m_Path)
// {
// System.Console.WriteLine("{4}: {0} -> {1} ({2}) - diff {3}", segment.vertex, segment.nextVertex,
// (segment.isCurve ? "Curve" : "Line"), segment.nextVertex - segment.vertex, segment.t);
// }
// }
if(GUI.changed)
EditorUtility.SetDirty(m_Path);
}
protected void SelectionButtons()
{
GUILayout.Label("Select");
using(new EditorGUIGroupHorizontal())
{
// Annoyingly, you have to call SetDirty on the path, otherwise the scene view doesn't get redrawn. That means
// it keeps setting the "scene needs saving" flag.
if(GUILayout.Button("All"))
{
SelectAllWaypoints();
SceneView.RepaintAll();
}
if(GUILayout.Button("None"))
{
DeselectAllWaypoints();
SceneView.RepaintAll();
}
if(GUILayout.Button("First"))
{
DeselectAllWaypoints();
m_SelectedWaypoints.Add(m_Path.m_Waypoints.First());
SceneView.RepaintAll();
}
if(GUILayout.Button("Last"))
{
DeselectAllWaypoints();
m_SelectedWaypoints.Add(m_Path.m_Waypoints.Last());
SceneView.RepaintAll();
}
GUI.enabled = (m_SelectedWaypoints.Count == 1);
if(GUILayout.Button("<-"))
{
SelectAdjacentWaypoint(Direction.Previous);
SceneView.RepaintAll();
}
if(GUILayout.Button("->"))
{
SelectAdjacentWaypoint(Direction.Next);
SceneView.RepaintAll();
}
GUI.enabled = true;
}
}
protected void SelectAllWaypoints()
{
m_SelectedWaypoints.Clear();
m_Path.m_Waypoints.ForEach(waypoint => m_SelectedWaypoints.Add(waypoint));
}
protected void DeselectAllWaypoints()
{
m_SelectedWaypoints.Clear();
m_WaypointProperties = null;
}
protected void SelectWaypoint(Path.Waypoint waypoint)
{
m_SelectedWaypoints.Add(waypoint);
}
protected void DeselectWaypoint(Path.Waypoint waypoint)
{
m_SelectedWaypoints.Remove(waypoint);
}
protected enum Direction {Previous = -1, Next = 1}
protected void SelectAdjacentWaypoint(Direction direction)
{
int index = FindWaypointIndex(m_SelectedWaypoints.First());
m_SelectedWaypoints.Clear();
int numWaypoints = m_Path.m_Waypoints.Count;
m_SelectedWaypoints.Add(m_Path.m_Waypoints[(index + (int)direction + numWaypoints) % numWaypoints]);
}
protected void AlignButtons(string label)
{
GUILayout.Label(label);
using(new EditorGUIGroupHorizontal())
{
if(GUILayout.Button("X"))
AlignWaypoints(0);
if(GUILayout.Button("Y"))
AlignWaypoints(1);
if(GUILayout.Button("Z"))
AlignWaypoints(2);
}
}
protected void WaypointGUI(Path.Waypoint waypoint)
{
m_WaypointProperties = null;
using(new EditorGUIGroupVertical("Box"))
{
waypoint.name = EditorGUILayout.TextField("Name", waypoint.name);
using(new EditorGUIGroupHorizontal())
{
EditorGUILayout.PrefixLabel("Curve factors");
waypoint.factorBefore = GUILayout.HorizontalSlider(waypoint.factorBefore, 0.0f, 0.5f);
waypoint.factorBefore = Mathf.Clamp(EditorGUILayout.FloatField(waypoint.factorBefore, GUILayout.Width(37)), 0.0f, 0.5f);
waypoint.factorAfter = GUILayout.HorizontalSlider(waypoint.factorAfter, 0.0f, 0.5f);
waypoint.factorAfter = Mathf.Clamp(EditorGUILayout.FloatField(waypoint.factorAfter, GUILayout.Width(37)), 0.0f, 0.5f);
}
EditorGUILayout.Separator();
waypoint.position = EditorGUILayout.Vector3Field("Position", waypoint.position);
EditorGUILayout.Separator();
// Insert and remove:
using(new EditorGUIGroupHorizontal())
{
if(GUILayout.Button("Insert Waypoint After"))
InsertWaypointAfter(FindWaypointIndex(waypoint));
GUI.enabled = (m_Path.m_Waypoints.Count > 1);
if(GUILayout.Button("Remove Waypoint"))
RemoveWaypoint(FindWaypointIndex(waypoint));
GUI.enabled = true;
}
}
}
protected void MultipleWaypointsGUI()
{
if(m_WaypointProperties == null)
m_WaypointProperties = new Path.Waypoint();
using(new EditorGUIGroupVertical("Box"))
{
GUILayout.Label(string.Format("{0} waypoints selected.", m_SelectedWaypoints.Count), GUILayout.ExpandWidth(true));
EditorGUILayout.Separator();
using(new EditorGUIGroupHorizontal())
{
m_WaypointProperties.name = EditorGUILayout.TextField("Name", m_WaypointProperties.name);
if(GUILayout.Button("Apply", GUILayout.Width(50)))
{
foreach(var waypoint in m_SelectedWaypoints)
waypoint.name = m_WaypointProperties.name;
SceneView.RepaintAll();
}
}
using(new EditorGUIGroupHorizontal())
{
EditorGUILayout.PrefixLabel("Factor before");
m_WaypointProperties.factorBefore = GUILayout.HorizontalSlider(m_WaypointProperties.factorBefore, 0.0f, 0.5f);
m_WaypointProperties.factorBefore = Mathf.Clamp(EditorGUILayout.FloatField(m_WaypointProperties.factorBefore, GUILayout.Width(37)), 0.0f, 0.5f);
if(GUILayout.Button("Apply", GUILayout.Width(50)))
{
foreach(var waypoint in m_SelectedWaypoints)
waypoint.factorBefore = m_WaypointProperties.factorBefore;
SceneView.RepaintAll();
}
}
using(new EditorGUIGroupHorizontal())
{
EditorGUILayout.PrefixLabel("Factor after");
m_WaypointProperties.factorAfter = GUILayout.HorizontalSlider(m_WaypointProperties.factorAfter, 0.0f, 0.5f);
m_WaypointProperties.factorAfter = Mathf.Clamp(EditorGUILayout.FloatField(m_WaypointProperties.factorAfter, GUILayout.Width(37)), 0.0f, 0.5f);
if(GUILayout.Button("Apply", GUILayout.Width(50)))
{
foreach(var waypoint in m_SelectedWaypoints)
waypoint.factorAfter = m_WaypointProperties.factorAfter;
SceneView.RepaintAll();
}
}
if(GUILayout.Button("Remove Waypoints"))
RemoveSelectedWaypoints();
}
}
protected void EventScrollViewGUI()
{
Path.Event selectedPathEvent = null;
int numSelectedPathEvents = 0;
float rowHeight = GUI.skin.textArea.CalcHeight(new GUIContent(""), 100) + GUI.skin.textField.margin.vertical;
var minHeight = GUILayout.MinHeight(Mathf.Min(m_Path.m_Events.Count, 4) * rowHeight);
m_ScrollPosition = EditorGUILayout.BeginScrollView(m_ScrollPosition, minHeight, GUILayout.ExpandHeight(false));
{
foreach(var pathEvent in m_Path.m_Events)
{
GUI.backgroundColor = (pathEvent.selected ? Color.grey : Color.white);
bool selected = GUILayout.Toggle(pathEvent.selected,
string.Format("t: {0}, {1}({2}) -> {3}{4}", pathEvent.t,
(pathEvent.name != "" ? pathEvent.name : "Unnamed"),
pathEvent.parameter,
(pathEvent.eventResponder != null ? pathEvent.eventResponder.name : "None"),
(pathEvent.notifyPathFollowers ? ", Notify" : "")), "TextArea");
if(selected != pathEvent.selected)
{
if((Event.current.modifiers & (EventModifiers.Shift | EventModifiers.Command)) == 0)
{
DeselectAllPathEvents();
pathEvent.selected = true;
}
else
pathEvent.selected = selected;
}
if(pathEvent.selected)
{
if(++numSelectedPathEvents == 1)
selectedPathEvent = pathEvent;
}
}
GUI.backgroundColor = Color.white;
}
EditorGUILayout.EndScrollView();
using(new EditorGUIGroupHorizontal())
{
if(GUILayout.Button("Add Event"))
{
m_Path.m_Events.Add(new Path.Event(0.0f, "PathEvent", 0.0f, null, false));
SortEvents();
}
}
EditorGUILayout.Separator();
// Event editor:
if(numSelectedPathEvents == 0)
{
using(new EditorGUIGroupVertical("Box"))
GUILayout.Label("No events selected.", GUILayout.ExpandWidth(true));
}
else if(numSelectedPathEvents == 1)
EventGUI(selectedPathEvent);
else
{
using(new EditorGUIGroupVertical("Box"))
{
GUILayout.Label(string.Format("{0} events selected.", numSelectedPathEvents), GUILayout.ExpandWidth(true));
if(GUILayout.Button("Remove Selected Events"))
RemoveSelectedEvents();
}
}
}
protected void EventGUI(Path.Event pathEvent)
{
using(new EditorGUIGroupVertical("Box"))
{
float t = EditorGUILayout.FloatField("t", pathEvent.t);
if(t != pathEvent.t)
{
pathEvent.t = t;
SortEvents();
}
pathEvent.name = EditorGUILayout.TextField("Name", pathEvent.name);
pathEvent.parameter = EditorGUILayout.FloatField("Parameter", pathEvent.parameter);
// The GetComponent call ensures that scripts that don't implement the interface are rejected:
var responder = EditorGUILayout.ObjectField("Responder", pathEvent.eventResponder, typeof(MonoBehaviour)) as MonoBehaviour;
if(responder != null)
pathEvent.eventResponder = (MonoBehaviour)responder.GetComponent(typeof(Path.IEventResponder));
pathEvent.notifyPathFollowers = EditorGUILayout.Toggle("Notify followers", pathEvent.notifyPathFollowers);
using(new EditorGUIGroupHorizontal())
{
if(GUILayout.Button("Duplicate"))
{
m_Path.m_Events.Add(pathEvent.Clone());
SortEvents();
}
if(GUILayout.Button("Remove"))
m_Path.m_Events.Remove(pathEvent);
}
}
}
#endregion
#region Scene GUI
protected void OnSceneGUI()
{
Event currentEvent = Event.current;
Camera sceneCamera = Camera.current;
Undo.SetSnapshotTarget(m_Path, "Edit Path");
// Selection dragging:
if(m_SelectedWaypoints.Count > 0)
{
Vector3 averagePosition = m_SelectedWaypoints.First().position;
foreach(var waypoint in m_SelectedWaypoints.Skip(1))
averagePosition += waypoint.position;
averagePosition /= m_SelectedWaypoints.Count;
Vector3 newPosition = m_Path.transform.InverseTransformPoint(
Handles.PositionHandle(m_Path.transform.TransformPoint(averagePosition), Tools.handleRotation));
var offset = newPosition - averagePosition;
if(offset.sqrMagnitude > 0.00001f) // FIXME: is this fuzzy enough?
{
foreach(var waypoint in m_SelectedWaypoints)
waypoint.position += offset;
EditorUtility.SetDirty(m_Path);
}
}
// Draw waypoint selection buttons:
for(var i = 0; i < m_Path.m_Waypoints.Count; ++i)
{
var waypoint = m_Path.m_Waypoints[i];
Vector3 position = m_Path.transform.TransformPoint(waypoint.position);
if(m_Path.m_ProjectedDownwards)
{
object hit = HandleUtility.RaySnap(new Ray(position, -Vector3.up));
if(hit != null)
{
Handles.color = Color.blue;
Handles.DrawLine(position, ((RaycastHit)hit).point);
}
}
Handles.color = (i > 0 ? m_Path.m_Colour * 0.5f : m_Path.m_Colour);
Vector3 screenPoint = sceneCamera.WorldToScreenPoint(position);
screenPoint.y = sceneCamera.pixelHeight - screenPoint.y;
if(screenPoint.z > 0.0f)
{
Handles.BeginGUI();
GUI.skin = m_InspectorSkin;
bool selected = m_SelectedWaypoints.Contains(waypoint);
if(selected)
GUI.color = selectionColour;
if(GUI.Button(new Rect(screenPoint.x - kWaypointHandleSize / 2, screenPoint.y - kWaypointHandleSize / 2,
kWaypointHandleSize, kWaypointHandleSize), ""))
{
// Handle multiple selection:
if((currentEvent.modifiers & (EventModifiers.Shift | EventModifiers.Command)) == 0)
{
DeselectAllWaypoints();
selected = false;
}
if(selected)
DeselectWaypoint(waypoint);
else
SelectWaypoint(waypoint);
Repaint();
}
GUI.color = Color.white;
// Using this instead of Handles.Label because that can still be seen when it's behind you!
string name = (waypoint.name != null && waypoint.name != "" ? waypoint.name : i.ToString());
Rect labelRect = new Rect(screenPoint.x + 4, screenPoint.y + 4, 200, 20);
string labelText = string.Format(" {0}", name);
GUI.Label(labelRect, labelText, EditorStyles.whiteBoldLabel);
GUI.skin = m_SceneSkin;
Handles.EndGUI();
}
}
// Draw events and vertex dots:
Handles.BeginGUI();
int currentEventIndex = 0;
foreach(var segment in m_Path)
{
Vector3 screenPoint = sceneCamera.WorldToScreenPoint(segment.vertex);
screenPoint.y = sceneCamera.pixelHeight - screenPoint.y;
if(screenPoint.z > 0.0f)
{
if(m_Path.m_ShowVertices)
{
GUI.DrawTexture(new Rect(screenPoint.x, screenPoint.y, 2.0f, 2.0f), EditorGUIUtility.whiteTexture);
// GUI.Label(new Rect(screenPoint.x, screenPoint.y, 200, 20), segment.totalT.ToString("0.00"), EditorStyles.miniLabel);
}
while(currentEventIndex < m_Path.m_Events.Count)
{
var pathEvent = m_Path.m_Events[currentEventIndex];
float endT = segment.totalT + 1.0f / m_Path.m_Subdivisions;
if(pathEvent.t >= segment.totalT && pathEvent.t < endT)
{
GUI.DrawTexture(new Rect(screenPoint.x - m_EventTexture.width * 0.5f, screenPoint.y - m_EventTexture.height * 0.5f,
m_EventTexture.width, m_EventTexture.height), m_EventTexture);
GUI.Label(new Rect(screenPoint.x, screenPoint.y, 200, 20), pathEvent.name, EditorStyles.miniLabel);
}
else if(pathEvent.t >= endT)
break;
++currentEventIndex;
}
}
}
Handles.EndGUI();
}
#endregion
#region Waypoint tools
protected int FindWaypointIndex(Path.Waypoint waypoint)
{
return m_Path.m_Waypoints.IndexOf(waypoint); // Returns -1 if not found
}
protected void InsertWaypointAfter(int index)
{
m_SelectedWaypoints.Clear();
Undo.RegisterUndo(m_Path, "Insert Waypoint");
var waypointToMove = m_Path.m_Waypoints[index];
m_Path.m_Waypoints.Insert(index, waypointToMove); // Insert the existing waypoint, then insert the new one after it
var newWaypoint = waypointToMove.Clone();
newWaypoint.position += Vector3.one;
m_Path.m_Waypoints[index + 1] = newWaypoint;
EditorUtility.SetDirty(m_Path);
m_SelectedWaypoints.Add(newWaypoint);
}
protected void RemoveWaypoint(int index)
{
Undo.RegisterUndo(m_Path, "Delete Waypoint");
m_SelectedWaypoints.Remove(m_Path.m_Waypoints[index]);
m_Path.m_Waypoints.RemoveAt(index);
EditorUtility.SetDirty(m_Path);
}
protected void RemoveSelectedWaypoints()
{
var waypoints = m_Path.m_Waypoints.Where(waypoint => !m_SelectedWaypoints.Contains(waypoint)).ToList();
if(waypoints.Count == 0)
waypoints.Add(new Path.Waypoint());
m_SelectedWaypoints.Clear();
m_Path.m_Waypoints = waypoints;
EditorUtility.SetDirty(m_Path);
}
protected void AlignWaypoints(int axis)
{
// TODO: this could take a float for when you want to set the value explicitly instead of averaging it.
ICollection<Path.Waypoint> collection = null;
int numSelectedWaypoints = m_SelectedWaypoints.Count;
if(numSelectedWaypoints == 0)
{
collection = m_Path.m_Waypoints;
numSelectedWaypoints = collection.Count;
}
else if(numSelectedWaypoints > 1)
collection = m_SelectedWaypoints;
// TODO: try copying this stuff into a new array, then transforming the array into world space if
// Tools.pivotRotation is set to Global, doing the operation, then setting it back again.
float average = collection.First().position[axis];
foreach(var waypoint in collection.Skip(1))
average += waypoint.position[axis];
average /= numSelectedWaypoints;
foreach(var waypoint in collection)
waypoint.position[axis] = average;
EditorUtility.SetDirty(m_Path);
}
#endregion
#region Event tools
protected void DeselectAllPathEvents()
{
foreach(var pathEvent in m_Path.m_Events)
pathEvent.selected = false;
}
protected void RemoveSelectedEvents()
{
m_Path.m_Events.RemoveAll(pathEvent => pathEvent.selected);
}
protected void SortEvents()
{
m_Path.m_Events.Sort((x, y) => x.t.CompareTo(y.t));
}
#endregion
#region Other events
protected Bounds OnGetFrameBounds()
{
Bounds bounds;
if(m_SelectedWaypoints.Count > 0)
{
// Focus the selected waypoints:
bounds = new Bounds(m_Path.transform.TransformPoint(m_SelectedWaypoints.First().position), Vector3.zero);
foreach(var waypoint in m_SelectedWaypoints.Skip(1))
bounds.Encapsulate(m_Path.transform.TransformPoint(waypoint.position));
}
else
{
// Focus all waypoints and the path's transform.position:
bounds = new Bounds(m_Path.transform.position, Vector3.zero);
foreach(var waypoint in m_Path.m_Waypoints)
bounds.Encapsulate(m_Path.transform.TransformPoint(waypoint.position));
}
return bounds;
}
#endregion
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment