- Home /
How do I make an empty GameObject with just my CustomClass and no renderers in it to be always pickable in the Editor, without using the ugly 3 axes position handle but having something nicer like a small dot to click on?
Hi,
I'm building a path system and don't like to always show the handles of each node (a list of Transforms).
Each node is actually also a gameobject with a PathNode info component on it, because in the future I may want to add some logic or code to each component, that's why I'm using a gameObject.
I already have the OnDrawGizmos defined, so I can see how each path node is linked, and the SimplePath classes handles the... showing of the path node handles when the path is selected.
And when selected, I have this ugly handles - I'd like to just have the node shown and drag it with the move tool
Anyway, I'd love to give the pathnodes a more stylish look and would like to be selectable when clicking on their Zoom dependant representation.
The quick and dirty way would be to attach a sprite renderer and hide it at runtime but... isn't there another option? Maybe using handles or some kind of code/class/method/helperclass/utility I don't know?
Thank you! :)
Answer by Bunny83 · Jan 06, 2017 at 12:55 AM
Well, the best option here would be to implement a custom inspector for your "path system" component ( the one that holds the list of Transforms). In there you can use OnSceneGUI to render Handles into the SceneView for each transform you have in your list.
You can use Handles.FreeMoveHandle
or implement your own handle. FreeMoveHandle allows you to specify any "cap" function (CubeCap, CircleCap, ConeCap, ArrowCap, CylinderCap, DotCap, RectangleCap, SphereCap) as visual representation.
The FreeMoveHandle is actually used inside the PositionHandle. It will show when you hold down "shift". In that case the 3 planar handles are hidden and a white rectangle will show. That's the FreeMoveHandle. It allows movement parallel to the current camera view. Since you use the 2d orthographic view it's always the x-y-plane.
edit
Here's a quick (untested) example of what i mentioned in the comments below. This is the basic framework:
[InitializeOnLoad]
public class PathHandlesEditorExtension
{
static PathHandles()
{
SceneView.onSceneGUIDelegate += OnSceneGUICallback;
}
static void OnSceneGUICallback(SceneView currentView)
{
// Your custom SceneView stuff goes here
}
}
The InitializeOnLoad attribute will make Unity "initialize" the class when the editor is loaded. Initialize doesn't mean that an instance is created, but only the static constructor will be called. There we subscribe to the scene view delegate. After that our "OnSceneGUICallback" method will be called whenever a SceneView receives an event.
To manage the nodes in the Scene you might want to use this:
// Inside your PathNode class:
public class PathNode : MonoBehaviour
{
#if UNITY_EDITOR
public PathNode()
{
PathHandlesEditorExtension.AddNode(this);
}
#endif
}
This will add the constructor only when compiled in the editor. Of course the PathHandlesEditorExtension class would need to that AddNode method:
// inside PathHandlesEditorExtension
private static List<PathNode> nodes = new List<PathNode>();
public static void AddNode(PathNode aNode)
{
lock(nodes)
{
nodes.Add(aNode);
}
}
static void OnSceneGUICallback(SceneView currentView)
{
lock(nodes)
{
// iterate backwards to remove null objects on the fly
for(int i = nodes.Count-1; i >=0; i--)
{
var node = nodes[i];
if (node == null)
{
nodes.RemoveAt(i);
continue;
}
float size = HandleUtility.GetHandleSize(node.transform.position);
node.transform.position = Handles.FreeMoveHandle(node.transform.position, Quaternion.identity, size, Vector3.zero, Handles.RectangleCap);
}
}
}
Note that this is untested. Since the nodes add themselfs to the static list, it's possible that you will get additional handles if you have a prefab of that node. Prefabs might be loaded as well but not actually in the scene. It's a bit tricky to distinguish between sceneobjects and prefabs but as far as i remember there was a solution if that really is a problem.
If the locking gives performance problems you could use a second temp / transfer list which is synchronised and only used to copy new nodes into the actual list.
If your "path" class is some sort of singleton (only possible if there's only one instance per scene) you could of course remove all that node list stuff and just use your singleton to get access to the node list. Keep in mind that your scene GUI callback will be called no matter which scene is loaded, so make sure you don't produce and errors (null ref).
I'm using AdvancedInspector, so writing a custom inspector would probably disable AdvancedInspector features :-/
I'm probably going to sort that out with the creator of the asset.
Thanks for answering!
Would your solution always work even when the object is not selected? I'd like the path handles (or equivalent solution) to always show and be pickable.
No, the actual path object has to be selected. When you click and drag the move handle you will not "pick" the actual object but just move it around. The selection stays at the path object. Always displaying the handles is a very bad idea. It adds quite a bit rendering overhead to the sceneview. What if you have a really large scene and thousands of handles and you would have no change to disable them?
If you really want this behaviour you can also create a seperate static editor class (not a custom inspector) that uses InitializeOnLoad. In the static constructor you can subscribe a static method to the static event SceneView.onSceneGUIDelegate
. This will make your method being called whenever a sceneview is redrawn, no matter what is selected and what scene is loaded. Of course you have to find a way to deter$$anonymous$$e / find your path nodes / path instances from inside that static class. The easiest way would be let the node class add themselfs to a thread-safe list inside the static class from their constructor. That way you automatically have a list of all nodes currently loaded. Of course this approach requires proper locking and you have to check the list items if they are null (destroyed / unloaded) and remove them from the list while you iterate it.
Thanks :)
At the current moment the handles of the nodes get shown when selecting the path object as well, I was looking for a way to always show them. Unfortunately I have the path nested deep in the scene hierarchy and to make this easy to edit for the level designer I'd love to make them always visible.
There are going to be 10 nodes at maximum in the scene so overcrowding is not an issue ;)
Could you please point me to this editor class in the docs or give me a quick example of the interface of the class I should be creating?
The onSceneGUIDelegate seems to be the only way to go. And I will still retain AdvancedInspector's features since I wouldn't have to write a custom inspector.
I may be adding buttons to the sceneview as well I suppose, to be able to toggle the paths on and off.
Woa, that was way more than I expected as an answer, thank you.
I wasn't even considering locks actually :D ...and the reverse counting to handle with null elements is another approach to a common problem, definitely a stylish solution, thanks for that!
I see you declared the constructor with a non related name, I suppose the InitializeOnLoad automatically picks the only constructor available, I'll do some more research on that.
Thanks, I'm marking this question solved :) First question on Answers that I can mark as Solved in years, hehehe :D
Working as a charm!
may I ask you to go a little OT with 3 extra questions?
1) How do I implement Undo with this approach?
2) How do I get the currently selected node (shown in yellow) for extra processing, from inside the callback function (or even externally if possible)?
3) Is there a way to show a contextual menu when right clicking on a handle, possibly to add/remove/insert before/after nodes? Even binding a shortcut that only works when a node is selected would do.
Thank you again! :)
Well, Undo is usually done the same way. You would call Undo.RecordObject before your change anything on the target object. In your case you would simply pass the transform or gameobject reference of the node that you're editing.
The second question is a bit tricky. It's actually not directly possible to deter$$anonymous$$e the current active control since you don't know it's controlID. However for controls with a single controlID you can use GUI.SetNextControlName before each Free$$anonymous$$ove handle to give them a unique name. Later you can use GUI.GetNameOfFocusedControl to get the name of the control that currently has the keyboard focus.
Free$$anonymous$$ove handle actually does this:
public static Vector3 Free$$anonymous$$oveHandle(Vector3 position, Quaternion rotation, float size, Vector3 snap, Handles.DrawCapFunction capFunc)
{
int controlID = GUIUtility.GetControlID(Handles.s_Free$$anonymous$$oveHandleHash, FocusType.$$anonymous$$eyboard);
return Free$$anonymous$$ove.Do(controlID, position, rotation, size, snap, capFunc);
}
You can find the code of the Free$$anonymous$$ove class here. GUIUtility.keyboardControl will hold the control ID of the focused control. However as i said you can't get the control ID of your Free$$anonymous$$ove handles easily. A common trick is to aquire an additional ID manually with the same "hints". This usually results in consecutive IDs so you can deter$$anonymous$$e which ID the Free$$anonymous$$ove handle has gotten
int controlID = GUIUtility.GetControlID("Free$$anonymous$$oveHandleHash".GetHashCode(), FocusType.$$anonymous$$eyboard);
If you do this before your Free$$anonymous$$ove handle, your handle will have the id controlID+1
if you do it after your handle it would be controlID-1
. That can be compared to GUIUtility.keyboardControl.
Point three follows in a second comment ...
To show a context menu you would have to check the right click yourself and display your context menu. For this it would be helpful if you have the controlID of your free move handle.
The cleanest solution would be to reimplement the whole free move handle functionality in your own code. That way it's very easy to implement the right click. Though some used methods inside the Free$$anonymous$$ove class might be internal and requires some workaround.
To implement it on top of the existing handle, just check for the mouse down event with button index "1" (right mouse button) and check the distance from the node center. For this you can use HandleUtility.WorldToGUIPoint to get the position of your node in 2d GUI space coordinates. just check if the distance of your event.mousePosition and the node is smaller than a threshold. In addition to avoid misdetection if nodes are close, checking the keyboardControl against the control id would help here as well.
Your answer
Follow this Question
Related Questions
Can I make a clickable object without an image? 1 Answer
editable 3d text? 2 Answers
Clickable sphere? 1 Answer
Clicking problem 0 Answers
Can I spread the destination of AI going towards a node. 1 Answer