How do I build a custom editor that iterates over a scene hierarchy and presents all the controls for Springs in one UI?
Here's the context:
I have a puppet with a set of multitouch controls that depend on a network of spring joints - visualised below with the colored handles and the lines. It's working nicely, but I'd like a custom inspector that collects all the relevant springjoint2D settings into one UI (in the inspector and eventually in the scene and game GUI.
So: the problems at the moment are:
(1) the spring float fields are readonly and not editable.
(2) I get an error at line 30 in SpringScriptEditor.cs: NullReferenceException: Object reference not set to an instance of an object. This I've read about elsewhere and don't know if it is related to (1).
The hierarchy of controls looks like this (a springJoint is on each controller):
Scene
|---PuppetRoot_Wayang
|------Controller 1
|------Controller 2
|------RodController
|------Controller 3
|------Controller 3 a
|------Controller 4
|------Controller 4a
etc...
Here's the result, followed by the code, which makes me feel I'm nearly there - though I don't think I get SerializedProperties / Objects properly yet - and I've tried several of the approaches on 'answers'.
(1) Here's the basic object class: GetSprings.cs
using System.Collections;
using System.Collections.Generic;
[System.Serializable]
public class GetSprings : MonoBehaviour {
public GameObject[] puppets;
public GameObject[] myObject {
get {return puppets;}
}
}
And
(2) The Custom Editor Script (SpringsScriptEditor.cs)
using UnityEngine;
using System.Collections;
using UnityEditor;
[CustomEditor(typeof(GetSprings)),CanEditMultipleObjects]
public class SpringsScriptEditor : Editor {
//SerializedProperty puppets;
void OnEnable()
{
//puppets = serializedObject.FindProperty("puppets");
}
public override void OnInspectorGUI() {
serializedObject.Update();
var t = target as GetSprings;
SerializedProperty tps = serializedObject.FindProperty ("puppets");
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(tps, true);
if(EditorGUI.EndChangeCheck())
serializedObject.ApplyModifiedProperties();
//NullReferenceException: Object reference not set to an instance of an object
// error here when array doesnt exist?
foreach (GameObject puppet in t.puppets) {
Traverse(puppet);
//Debug.Log(puppet.name);
}
}
void Traverse(GameObject obj) {
if (obj != null) {
foreach (Transform child in obj.transform) {
Component[] springJoints;
springJoints = child.gameObject.GetComponents( typeof(SpringJoint2D) );
//Debug.Log("Parent Object: "+obj.name);
foreach (SpringJoint2D spring in springJoints) {
//EditorGUILayout.LabelField(obj.name, );
if (spring) {
EditorGUI.BeginChangeCheck();
EditorGUILayout.LabelField(spring.gameObject.name,EditorStyles.boldLabel);
EditorGUILayout.FloatField(spring.gameObject.name+ " Frequency", spring.frequency);
//EditorGUILayout.Slider(spring.frequency,0.0f,200f);
EditorGUILayout.FloatField(spring.gameObject.name+ " Damping", spring.dampingRatio);
//EditorGUILayout.Slider(spring.dampingRatio,0.0f,200f);
EditorGUILayout.FloatField(spring.gameObject.name+ " Distance", spring.distance);
//EditorGUILayout.Slider(spring.distance,0.0f,10f);
if(EditorGUI.EndChangeCheck()) {
serializedObject.ApplyModifiedProperties();
}
//Debug.Log("Parent: "+spring.gameObject.name+"\r\n"+
// "Connected Body: "+spring.connectedBody+"\r\n"+
// "Distance: "+spring.distance+"\r\n"+
// "Damping: "+spring.dampingRatio+"\r\n"+
// "Frequency: "+spring.frequency+"\r\n");
}
}
Traverse(child.gameObject);
}
}
}
}
I hope I'm missing something simple...
Thanks in advance.
Ian
A class might be easier to access the data
[System.Serializable]
public class JointInfo
{
public string name;
public SpringJoint2D joint;
public float frequency;
public float dampingRatio;
public float distance;
public JointInfo(SpringJoint2D joint)
{
this.joint = joint;
this.name = joint.transform.gameObject.name;
this.frequency = joint.frequency;
this.dampingRatio = joint.dampingRatio;
this.distance = joint.distance;
}
//helper functions since you mentioned updating these on the fly
public void Frequency(float val)
{
this.frequency = val;
Refresh();
}
public void DampingRatio(float val)
{
this.dampingRatio = val;
Refresh();
}
public void Distance(float val)
{
this.distance = val;
Refresh();
}
//
void Refresh()
{
this.joint.frequency = this.frequency;
this.joint.dampingRatio = this.dampingRatio;
this.joint.distance = this.distance;
}
}
GetComponentsInChildren will get all of the components in all of the children of a game object. I would store these in a list
If you are trying to collect all the spring joints in a game object you can call
using System.Collections.Generic;
public List<JointInfo> jointInfoList;
void Start()
{
jointInfoList = new List<JointInfo>();
}
public void Traverse(GameObject obj)
{
SpringJoint2D[] joints = obj.GetComponentsInChildren<SpringJoint2D>();
foreach (SpringJoint2D sj in joints)
{
jointInfoList.Add(new JointInfo(sj));
}
}
now you can simply iterate through a list with a for loop to access any joint and change/modify parameters using the class built in functions. You can do a lot with the default inspector UI, so unless you are looking for something really fancy you could probably stick with that. Lets say you want to have a slider for the frequency that has a range between 0.0 and 1.0. just add [Range(0.0f, 1.0f)] above the variable. Want to add a tooltip that pops up when you over over the variable name? [Tooltip("Insert text")]
[Tooltip("The frequency at which the spring oscillates around the distance distance between the objects.")]
[Range(0.0f, 1.0f)]
public float frequency;
Turning this in to an editor script function is not any different, you just need to call the function from the editor with a button. Here is a really good tutorial on custom lists in the editor: http://catlikecoding.com/unity/tutorials/editor/custom-list/
For example. Your editor script could include something like...
public override void OnInspectorGUI()
{
YourScriptName jointController = (YourScriptName)target;
if (DrawDefaultInspector())
{
//this is where you would call a function in the "jointController" script to update the variables.
//This will be called when any value is changed.
jointController.UpdateValues();
}
//and a button to manually update
if (GUILayout.Button("Update"))
{
jointController.UpdateValues();
}
}
and inside the "jointController" script...
public void UpdateValues()
{
foreach (JointInfo ji in jointInfoList)
{
ji.Refresh();
}
}
Answer by ianjgrant · Oct 19, 2016 at 10:06 AM
That's fantastic. Thank you for such a detailed answer. I was on the right track - and used lists in a more 'manual' solution. I was looking at a custom class, getters and setters etc. but your walk-though has drawn that all together and the principle of moving it into a custom editor / custom window is working too. I read the catlikecoding tutorial and found that helpful. Thank you for your help.
I collect all the puppet controls (for live performance) into a GUI window. And exposing the spring settings helps speed up setting the objects. I've adjusted this to work in the editor.
I needed to make public the refresh method. And in the non-editor version had to call refresh from update in order for the slider GUI changes to effect the springs. Shame there isn't a method that updates on GUI change. In the editor script this works as expected.
void Refresh()
Changed:
public void Refresh()
Also, I added the [Range...] attributes. Thanks to you I get it a lot more! Cheers!
Ian
ps. If you promote the comment to an 'answer' - I'll up-vote it and mark it as the answer... cheers.