In Unity, one of my MonoBehaviours has a field pointing to another object (a ScriptableObject). If I double-click that field, I can see the fields of that object. How do I render those fields into the top-level MonoBehaviour's property drawer?
In picture form
What I have
(double-click the element)
What I want
I have my own [CustomEditor] component, but I can't quite get it to work right; stuff like this:
SerializedProperty activityStack = serializedObject.FindProperty("activityStack");
EditorGUILayout.PropertyField(activityStack.GetArrayElementAtIndex(0));
just renders the "Element 0 (Idle Activity)" bit and not the actual contents of the reference.
Because the default PropertyField for a ScriptableObject is just the one you get: A UnityEngine.Object reference field like for GameObject and Components and other assets ;)
Of course you can implement what you want to achieve but that's a bit more complex and not really good for maintenance and I would not recommend it.
I don't know your ScriptableObject so here an example
public class ExampleSO : ScriptableObject
{
public int SomeInt;
[SerializeField] private string _someString;
}
and your MonoBehaviour e.g.
public class Example : MonoBehaviour
{
public List<ExampleSO> _SOList;
}
Then the editor could look like e.g.
using UnityEditor;
using UnityEngine;
// This is the namespace for the ReorderableList
using UnityEditorInternal;
[CustomEditor(typeof(Example))]
public class ExampleEditor : Editor
{
SerializedProperty _SOList;
Example _example;
MonoScript _script;
ReorderableList _list;
private void OnEnable()
{
// Link up the serializedProperty
_SOList = serializedObject.FindProperty("_SOList");
// get the casted target instance (only needed for drawing the script field)
_example = (Example) target;
// get the according script instance (only needed for drawing the script field)
_script = MonoScript.FromMonoBehaviour(_example);
// Set up the ReorderableList
_list = new ReorderableList(serializedObject, _SOList, true, true, true, true)
{
// What shall be displayed as header for the list?
drawHeaderCallback = (Rect rect) => EditorGUI.LabelField(rect, _SOList.displayName),
// How is each element displayed?
drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) =>
{
// Get the element in the list (SerializedProperty)
var element = _SOList.GetArrayElementAtIndex(index);
// and draw the default object reference field
EditorGUI.PropertyField(new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight), element, new GUIContent("Reference"));
// Check if an asset is referenced - if not we are done here
if (!element.objectReferenceValue) return;
// Otherwise get the SerializedObject for this asset
var elementSerializedObject = new SerializedObject(element.objectReferenceValue);
// and all the properties (SerializedProperty) of it you want to display
var someInt = elementSerializedObject.FindProperty("SomeInt");
var someString = elementSerializedObject.FindProperty("_someString");
// Similar to the OnInspectorGUI first load the current values into this serializedobject
elementSerializedObject.Update();
{
// Adding some indentation just to show that the following fields are actually belonging to the referenced asset
EditorGUI.indentLevel++;
{
rect = EditorGUI.IndentedRect(rect);
// shift down the rect by one line
rect.y += EditorGUIUtility.singleLineHeight;
// Draw the field for the Int
EditorGUI.PropertyField(new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight), someInt);
// Shift down the rect another line
rect.y += EditorGUIUtility.singleLineHeight;
// Draw the string field
EditorGUI.PropertyField(new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight), someString);
}
EditorGUI.indentLevel--;
}
// Write back the changed values and trigger the checks for logging dirty states and Undo/Redo
elementSerializedObject.ApplyModifiedProperties();
},
// How much vertical space should be reserved for each element?
elementHeightCallback = (int index) =>
{
// Get the elements serialized property
var element = _SOList.GetArrayElementAtIndex(index);
// by default we have only the asset reference -> single line
var lines = 1;
// if the asset is referenced adds space for the additional fields
if (element.objectReferenceValue) lines += 2; // or how many lines you'll need
return lines * EditorGUIUtility.singleLineHeight;
}
};
}
public override void OnInspectorGUI()
{
// draw th script field
DrawScriptField();
// Load the current values into the serializedObject
serializedObject.Update();
{
// let the ReorderableList do its magic
_list.DoLayoutList();
}
// Write back the changed values into the actual instance
serializedObject.ApplyModifiedProperties();
}
// Just draws the usual script field at the top of the Inspector
private void DrawScriptField()
{
EditorGUI.BeginDisabledGroup(true);
{
EditorGUILayout.ObjectField("Script", _script, typeof(Example), false);
}
EditorGUI.EndDisabledGroup();
EditorGUILayout.Space();
}
}
Which results in the following Inspector. As you can see I opened the Isnpectors of the MonoBehaviour and two instances of the ExampleSO to show how the values are taken over to the actual instances
Related
It appears that anything I add to a List<string[]> will get added, but when I save any scripts and Unity does compiles everything, the items in the list disappears.
Here is a simple class I wrote that just displays a window and adds labels according to how many items are in the list:
public class TestEditorWindow : EditorWindow
{
string windowLabel = "Test Window";
[SerializeField] List<string[]> myList = new List<string[]>();
[MenuItem("Tools/My Window")]
static void Init()
{
TestEditorWindow myWindow = (TestEditorWindow)GetWindow(typeof(TestEditorWindow));
myWindow.Show();
}
private void OnGUI()
{
GUILayout.Label(windowLabel, EditorStyles.boldLabel);
EditorGUILayout.Separator();
GUILayout.BeginVertical("box", GUILayout.ExpandWidth(true));
for(int i = 0; i < myList.Count; i++)
{
EditorGUILayout.LabelField("Stupid");
}
if(GUILayout.Button("+", GUILayout.MaxWidth(30)))
{
//myList.Add(new string[2]); //<-- Also tried it this way
myList.Add(new string[] { "" });
}
GUILayout.EndVertical();
}
}
The window shows and every time I hit the button a new label is added to the window, but as soon as Unity compiles anything, the values go away.
If I change the list to List<string> it behaves as intended
I've also tried setting up the list like so and got the same results:
[SerializeField] static List<string[]> myList;
[MenuItem("Tools/My Window")]
static void Init()
{
myList = new List<string[]>();
TestEditorWindow myWindow = (TestEditorWindow)GetWindow(typeof(TestEditorWindow));
myWindow.Show();
}
Am I doing something wrong with how I'm loading the list?
Unity cannot serialize multidimensional collections.
There is a work around though.
Create a new class that contains the string array, and create a list using that type.
[System.Serializable]
public class StringArray
{
public string[] array;
}
and in your window use:
public List<StringArray> myList = new List<StringArray>();
I try to achieve a tiny custom editor in Unity3D which offers buttons to modify a value in two components of the same game object. So in the "PlanetGameObject" there is the MonoBehaviour "Planet" where the member "phase" should change (to one of the matching enum values) and also a SpriteRenderer where the member "color" changes to the matching value of the phase.
Basically it works as intended. When I click the buttons in the editor both values get assigned correctly but when the game starts the phase switches back to the initial value while the color of the sprite renderer stays with the new value. When I close the running game the incorrect value stays.
The setup:
Planet script:
using UnityEngine;
public class Planet : MonoBehaviour
{
public enum Phase { NEUTRAL, RED, GREEN, BLUE }
public Phase phase = Phase.NEUTRAL;
public static Color PhaseColor(Phase phase)
{
switch (phase)
{
case Phase.RED: return Color.red;
case Phase.GREEN: return Color.green;
case Phase.BLUE: return Color.blue;
default: return Color.white;
}
}
}
The custom editor script:
[![using UnityEngine;
using UnityEditor;
\[CustomEditor(typeof(Planet))\]
\[CanEditMultipleObjects\]
public class PlanetEditor : Editor
{
public override void OnInspectorGUI()
{
GUILayout.BeginHorizontal();
if (GUILayout.Button("neutral"))
{
foreach(Object t in targets)
{
Planet planet = (Planet)t;
planet.phase = Planet.Phase.NEUTRAL;
planet.gameObject.GetComponent<SpriteRenderer>().color = Planet.PhaseColor(planet.phase);
}
};
if (GUILayout.Button("red"))
{
foreach (Object t in targets)
{
Planet planet = (Planet)t;
planet.phase = Planet.Phase.RED;
planet.gameObject.GetComponent<SpriteRenderer>().color = Planet.PhaseColor(planet.phase);
}
};
if (GUILayout.Button("green"))
{
foreach (Object t in targets)
{
Planet planet = (Planet)t;
planet.phase = Planet.Phase.GREEN;
planet.gameObject.GetComponent<SpriteRenderer>().color = Planet.PhaseColor(planet.phase);
}
};
if (GUILayout.Button("blue"))
{
foreach (Object t in targets)
{
Planet planet = (Planet)t;
planet.phase = Planet.Phase.BLUE;
planet.gameObject.GetComponent<SpriteRenderer>().color = Planet.PhaseColor(planet.phase);
}
};
GUILayout.EndHorizontal();
DrawDefaultInspector();
}
}][1]][1]
Never go through the actual component references in Editor scripts.
This won't work with saving and undo/redo etc. because it doesn't mark the changes as dirty automatically.
Rather go through the SerializedPropertys which automatically handles marking stuff as dirty => saving. And Undo/Redo etc.
Could look somewhat like
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(Planet))]
[CanEditMultipleObjects]
public class PlanetEditor : Editor
{
SerializedProperty _phase;
private void OnEnable()
{
// This links the _phase SerializedProperty to the according actual field
_phase = serializedObject.FindProperty("phase");
}
public override void OnInspectorGUI()
{
// This gets the current values from all serialized fields into the serialized "clone"
serilaizedObject.Update();
GUILayout.BeginHorizontal();
if (GUILayout.Button("neutral"))
{
// It is enough to do this without a loop as this will already affect all selected instances
phase.intValue = (int)Planet.Phase.NEUTRAL;
};
if (GUILayout.Button("red"))
{
phase.intValue = (int)Planet.Phase.RED;
};
if (GUILayout.Button("green"))
{
phase.intValue = (int)Planet.Phase.GREEN;
};
if (GUILayout.Button("blue"))
{
phase.intValue = (int)Planet.Phase.RED;
};
GUILayout.EndHorizontal();
// This writes the changed properties back into the actual instance(s)
serializedObject.ApplyModifiedProperties();
DrawDefaultInspector();
}
}
So far for saving the enum of this component itself.
Now the simpliest way for the renderer would be to let your class itself react to that change:
[RequireComponent(typeof(SpriteRenderer))]
public class Planet : MonoBehaviour
{
public enum Phase { NEUTRAL, RED, GREEN, BLUE }
public Phase phase = Phase.NEUTRAL;
// Will be called everytime a field in the Inspector is changed
private void OnValidate()
{
GetComponent<SpriteRenderer>().color = PhaseColor(phase);
}
public static Color PhaseColor(Phase phase)
{
switch (phase)
{
case Phase.RED: return Color.red;
case Phase.GREEN: return Color.green;
case Phase.BLUE: return Color.blue;
default: return Color.white;
}
}
}
As this also should handle the marking dirty and thereby saving correctly afaik.
Actually your editor becomes a bit redundant with this since this also already reacts if you change the enum field manually ;)
Note: Typed on smartphone but I hope the idea gets clear
I'm working on a small program that can modify the animation at run time(Such as when you run faster the animation not only play faster but also with larger movement). So i need to get the existing animation, change its value, then send it back.
I found it is interesting that i can set a new curve to the animation, but i can't get access to what i already have. So I either write a file to store my animation curve (as text file for example), or i find someway to read the animation on start up.
I tried to use
AnimationUtility.GetCurveBindings(AnimationCurve);
It worked in my testing, but in some page it says this is a "Editor code", that if i build the project into a standalone program it will not work anymore. Is that true? If so, is there any way to get the curve at run time?
Thanks to the clearify from Benjamin Zach and suggestion from TehMightyPotato
I'd like to keep the idea about modifying the animation at runtime. Because it could adapt to more situations imo.
My idea for now is to write a piece of editor code that can read from the curve in Editor and output all necesseary information about the curve (keyframes) into a text file. Then read that file at runtime and create new curve to overwrite the existing one. I will leave this question open for a few days and check it to see if anyone has a better idea about it.
As said already AnimationUtility belongs to the UnityEditor namespace. This entire namespace is completely stripped of in a build and nothing in it will be available in the final app but only within the Unity Editor.
Store AnimationCurves to file
In order to store all needed information to a file you could have a script for once serializing your specific animation curve(s) in the editor before building using e.g. BinaryFormatter.Serialize. Then later on runtime you can use BinaryFormatter.Deserialize for returning the info list again.
If you wanted it more editable you could as well use e.g. JSON or XML of course
UPDATE: In general Stop using BinaryFormatter!
In the newest Unity versions the Newtonsoft Json.NET package comes already preinstalled so simply rather use JSON
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Unity.Plastic.Newtonsoft.Json;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
public class AnimationCurveManager : MonoBehaviour
{
[Serializable]
public sealed class ClipInfo
{
public int ClipInstanceID;
public List<CurveInfo> CurveInfos = new List<CurveInfo>();
// default constructor is sometimes required for (de)serialization
public ClipInfo() { }
public ClipInfo(Object clip, List<CurveInfo> curveInfos)
{
ClipInstanceID = clip.GetInstanceID();
CurveInfos = curveInfos;
}
}
[Serializable]
public sealed class CurveInfo
{
public string PathKey;
public List<KeyFrameInfo> Keys = new List<KeyFrameInfo>();
public WrapMode PreWrapMode;
public WrapMode PostWrapMode;
// default constructor is sometimes required for (de)serialization
public CurveInfo() { }
public CurveInfo(string pathKey, AnimationCurve curve)
{
PathKey = pathKey;
foreach (var keyframe in curve.keys)
{
Keys.Add(new KeyFrameInfo(keyframe));
}
PreWrapMode = curve.preWrapMode;
PostWrapMode = curve.postWrapMode;
}
}
[Serializable]
public sealed class KeyFrameInfo
{
public float Value;
public float InTangent;
public float InWeight;
public float OutTangent;
public float OutWeight;
public float Time;
public WeightedMode WeightedMode;
// default constructor is sometimes required for (de)serialization
public KeyFrameInfo() { }
public KeyFrameInfo(Keyframe keyframe)
{
Value = keyframe.value;
InTangent = keyframe.inTangent;
InWeight = keyframe.inWeight;
OutTangent = keyframe.outTangent;
OutWeight = keyframe.outWeight;
Time = keyframe.time;
WeightedMode = keyframe.weightedMode;
}
}
// I know ... singleton .. but what choices do we have? ;)
private static AnimationCurveManager _instance;
public static AnimationCurveManager Instance
{
get
{
// lazy initialization/instantiation
if (_instance) return _instance;
_instance = FindObjectOfType<AnimationCurveManager>();
if (_instance) return _instance;
_instance = new GameObject("AnimationCurveManager").AddComponent<AnimationCurveManager>();
return _instance;
}
}
// Clips to manage e.g. reference these via the Inspector
public List<AnimationClip> clips = new List<AnimationClip>();
// every animation curve belongs to a specific clip and
// a specific property of a specific component on a specific object
// for making this easier lets simply use a combined string as key
private string CurveKey(string pathToObject, Type type, string propertyName)
{
return $"{pathToObject}:{type.FullName}:{propertyName}";
}
public List<ClipInfo> ClipCurves = new List<ClipInfo>();
private string filePath = Path.Combine(Application.streamingAssetsPath, "AnimationCurves.dat");
private void Awake()
{
if (_instance && _instance != this)
{
Debug.LogWarning("Multiple Instances of AnimationCurveManager! Will ignore this one!", this);
return;
}
_instance = this;
DontDestroyOnLoad(gameObject);
// load infos on runtime
LoadClipCurves();
}
#if UNITY_EDITOR
// Call this from the ContextMenu (or later via editor script)
[ContextMenu("Save Animation Curves")]
private void SaveAnimationCurves()
{
ClipCurves.Clear();
foreach (var clip in clips)
{
var curveInfos = new List<CurveInfo>();
ClipCurves.Add(new ClipInfo(clip, curveInfos));
foreach (var binding in AnimationUtility.GetCurveBindings(clip))
{
var key = CurveKey(binding.path, binding.type, binding.propertyName);
var curve = AnimationUtility.GetEditorCurve(clip, binding);
curveInfos.Add(new CurveInfo(key, curve));
}
}
// create the StreamingAssets folder if it does not exist
try
{
if (!Directory.Exists(Application.streamingAssetsPath))
{
Directory.CreateDirectory(Application.streamingAssetsPath);
}
}
catch (IOException ex)
{
Debug.LogError(ex.Message);
}
// create a new file e.g. AnimationCurves.dat in the StreamingAssets folder
var json = JsonConvert.SerializeObject(ClipCurves);
File.WriteAllText(filePath, json);
AssetDatabase.Refresh();
}
#endif
private void LoadClipCurves()
{
if (!File.Exists(filePath))
{
Debug.LogErrorFormat(this, "File \"{0}\" not found!", filePath);
return;
}
var fileStream = new FileStream(filePath, FileMode.Open);
var json = File.ReadAllText(filePath);
ClipCurves = JsonConvert.DeserializeObject<List<ClipInfo>>(json);
}
// now for getting a specific clip's curves
public AnimationCurve GetCurve(AnimationClip clip, string pathToObject, Type type, string propertyName)
{
// either not loaded yet or error -> try again
if (ClipCurves == null || ClipCurves.Count == 0) LoadClipCurves();
// still null? -> error
if (ClipCurves == null || ClipCurves.Count == 0)
{
Debug.LogError("Apparantly no clipCurves loaded!");
return null;
}
var clipInfo = ClipCurves.FirstOrDefault(ci => ci.ClipInstanceID == clip.GetInstanceID());
// does this clip exist in the dictionary?
if (clipInfo == null)
{
Debug.LogErrorFormat(this, "The clip \"{0}\" was not found in clipCurves!", clip.name);
return null;
}
var key = CurveKey(pathToObject, type, propertyName);
var curveInfo = clipInfo.CurveInfos.FirstOrDefault(c => string.Equals(c.PathKey, key));
// does the curve key exist for the clip?
if (curveInfo == null)
{
Debug.LogErrorFormat(this, "The key \"{0}\" was not found for clip \"{1}\"", key, clip.name);
return null;
}
var keyframes = new Keyframe[curveInfo.Keys.Count];
for (var i = 0; i < curveInfo.Keys.Count; i++)
{
var keyframe = curveInfo.Keys[i];
keyframes[i] = new Keyframe(keyframe.Time, keyframe.Value, keyframe.InTangent, keyframe.OutTangent, keyframe.InWeight, keyframe.OutWeight)
{
weightedMode = keyframe.WeightedMode
};
}
var curve = new AnimationCurve(keyframes)
{
postWrapMode = curveInfo.PostWrapMode,
preWrapMode = curveInfo.PreWrapMode
};
// otherwise finally return the AnimationCurve
return curve;
}
}
Then you can do something like e.e.
AnimationCurve originalCurve = AnimationCurvesManager.Instance.GetCurve(
clip,
"some/relative/GameObject",
typeof<SomeComponnet>,
"somePropertyName"
);
the second parameter pathToObject is an empty string if the property/component is attached to the root object itself. Otherwise it is given in the hierachy path as usual for Unity like e.g. "ChildName/FurtherChildName".
Now you can change the values and assign a new curve on runtime.
Assigning new curve on runtime
On runtime you can use animator.runtimeanimatorController in order to retrieve a RuntimeAnimatorController reference.
It has a property animationClips which returns all AnimationClips assigned to this controller.
You could then use e.g. Linq FirstOrDefault in order to find a specific AnimationClip by name and finally use AnimationClip.SetCurve to assign a new animation curve to a certain component and property.
E.g. something like
// you need those of course
string clipName;
AnimationCurve originalCurve = AnimationCurvesManager.Instance.GetCurve(
clip,
"some/relative/GameObject",
typeof<SomeComponnet>,
"somePropertyName"
);
// TODO
AnimationCurve newCurve = SomeMagic(originalCurve);
// get the animator reference
var animator = animatorObject.GetComponent<Animator>();
// get the runtime Animation controller
var controller = animator.runtimeAnimatorController;
// get all clips
var clips = controller.animationClips;
// find the specific clip by name
// alternatively you could also get this as before using a field and
// reference the according script via the Inspector
var someClip = clips.FirstOrDefault(clip => string.Equals(clipName, clip.name));
// was found?
if(!someClip)
{
Debug.LogWarningFormat(this, "There is no clip called {0}!", clipName);
return;
}
// assign a new curve
someClip.SetCurve("relative/path/to/some/GameObject", typeof(SomeComponnet), "somePropertyName", newCurve);
Note: Typed on smartphone so no warranty! But I hope the idea gets clear...
Also checkout the example in AnimationClip.SetCurve → You might want to use the Animation component instead of an Animator in your specific use case.
I am making a probject in Unity, and would like to have one play to access all my SceneNaming;
Right now in the UI, I have to set the scene name manually.
I would like to store all my scene name in an object, so that I can just use a drag drop to choose all my scenes names.
I tried to put a static class and have then like this
public static string SCENE_MENU = "Menu";
public static string SCENE_WORLD = "Demo";
or inside an enum
public enum SCENE_NAME{
Menu, Demo
}
and then use GetName on the enum to get the value
What is the best approach? 1: /storage/temp/135402-screenshot-1.png
With a customer editor script you could use a SceneAsset to store a Scene's path instead.
I will use a CustomEditor here since for starters it's easier to understand what happens there. Later you might want to switch it to a CustomPropertyDrawer wot a proper class or maybe even as Attribute.
Place this in anywhere in the Assets
public class SceneLoader : MonoBehaviour
{
public string ScenePath;
public void Load()
{
//e.g.
SceneManager.LoadSceneAsync(ScenePath);
}
}
Place this inside of a folder Editor (so it will not be included in a build where the UnityEditor namespace does not exist)
[CustomEditor(typeof(SceneLoader), true)]
public class ScenePickerEditor : Editor
{
private SerializedProperty _scenePath;
private void OnEnable()
{
_scenePath = serializezObject.FindProperty("ScenePath");
}
public override void OnInspectorGUI()
{
// Draw the usual script field
EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.ObjectField(.FromMonoBehaviour((SceneLoader)target), typeof(SceneLoader), false);
EditorGUI.EndDisabledGroup();
// Loads current Values into the serialized "copy"
serializedObject.Update();
// Get the current scene asset for the current path
var currentScene = !string.IsNullOrWhiteSpace(_scenePath.stringValue) ? AssetDatabase.LoadAssetAtPath<SceneAsset>(_scenePath.stringValue) : null;
EditorGUI.BeginChangeCheck();
var newScene = (SceneAsset)EditorGUILayout.ObjectField("Scene", currentScene, typeof(SceneAsset), false);
if (EditorGUI.EndChangeCheck())
{
_scenePath.stringValue = newScene != Null ? AssetDatabase.GetAssetPath(newScene) : "";
}
// Write back changes to the actual component
serializedObject.ApplyModifiedProperties();
}
}
And e.g. to your button attach that SceneLoader component.
Than you can simply reference the target scene in the Inspector via drag and drop. Internally it instead stores the according ScenePath.
Now in onClick instead use that SceneLoader.Load.
Note:
As mentioned here only storing the scene path might not be "save" and breaks if you later move the according scene or rename it. So maybe it would be a good extension to also store according object reference as a kind of fallback.
You could than also use this approach and extend it to be a central manager instead like
// It could as well be a ScriptableObject object
// this makes e.g. Awake run already in edit mode
[ExecuteInEditMode]
public class ScenePathManager : MonoBehaviour
{
// I would prefere references but for ease of this post
// use a Singleton for access
public static ScenePathManager Instance;
public List<string> AvailableScenePaths = new List<string>();
private void Awake ()
{
Instance = this;
}
}
and in the editor script use a list (again there are more beautiful ways like ReorderableList bit this would get to complex here
[CustomEditor(typeof(ScenePathManager))]
public class ScenePathManagerEditor : Editor
{
private SerializedProperty _availablePaths;
private void OnEnable ()
{
_availablePaths = serializedObject.FindProperty("AvailablScenePaths");
}
public override OnInpectorGUI ()
{
// Draw the usual script field
EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.ObjectField(.FromMonoBehaviour((SceneLoader)target), typeof(SceneLoader), false);
EditorGUI.EndDisabledGroup();
serializedObject.Update();
//Do the same thing as before but this time in a loop
for(var i=0; i<_availablePaths.arraySize; i++)
{
var _scenePath = _availablePaths.GetArrayElementAtIndex(i);
// Loads current Values into the serialized "copy"
serializedObject.Update();
// Get the current scene asset for the current path
var currentScene = !string.IsNullOrWhiteSpace(_scenePath.stringValue) ? AssetDatabase.LoadAssetAtPath<SceneAsset>(_scenePath.stringValue) : null;
EditorGUI.BeginChangeCheck();
var newScene = (SceneAsset)EditorGUILayout.ObjectField("Scene", currentScene, typeof(SceneAsset), false);
if (EditorGUI.EndChangeCheck())
{
_scenePath.stringValue = newScene != Null ? AssetDatabase.GetAssetPath(newScene) : "";
}
}
serializedObject.ApplyModifiedProperties();
}
}
Than you could reference all needed scenes in that manager and than on your SceneLoader instead have a Popup field (like for enums) in order to select the scene you want
[CustomEditor (typeof (SceneLoader))]
public class SceneLoaderEditor : Editor
{
private SerializedProperty _scenePath;
private void OnEnable ()
{
_scenePath = serializedObject.FindProperty("ScenePath");
}
public override void OnInpectorGUI ()
{
//Let me shorten it a bit this time ^^
serializedObject.Update();
var availablePaths = ScenePathManager.Instance ? ScenePathManager.Instance.AvailableScenePaths : new List<string>();
var currentIndex = availablePaths.FirstOrDefault(path => string.Equals(path, _scenePath.stringValue)));
var newIndex = EditorGUILayout.PopupField("Scene", currentIndex, availabePaths.ToArray());
_scenePath.stringValue = availablePaths[newIndex];
serializedObject.ApplyModifiedProperties();
}
}
This should than give you a selection dropdown for the scene.
Note this might, however, without the object reference as backing field break evem faster of any of those strings or indexes change...
But you could use this with your manager also without the whole SceneAsset approach but only for simple strings.
Typed on my smartphone so no warranty but I hope I make my point clear
I'm just asking if there is any possibility to hide the "Object Picker" (The little knob/menu next to an ObjectField) in a custom Inspector. I have some cases where changes are disabled (DisableGroup) and I would like to also hide the knob while the content can not be changed anyway.
Also to make things easier for users I think about making the field higher (EditorGUIUtility.SingleLineHeight * 2) -> the picker gets stretched as well what looks kind of shitty ^^
example:
using UnityEditor;
using UnityEngine;
public class Bla : MonoBehaviour {
[CustomEditor(typeof(Bla))]
public class BlaEditor : Editor
{
private AudioClip _clip;
public override void OnInspectorGUI()
{
EditorGUI.BeginDisabledGroup(true);
// do some magic to hide the object picker
_clip = (AudioClip) EditorGUILayout.ObjectField("some label", _clip, typeof(AudioClip), false);
EditorGUI.EndDisabledGroup();
}
}
}
I want to stick with an ObjectField rather than a simple Label for two reasons:
Even on a disabled `ObjectField| the "ping" functionality is still working. (If you click on it, the according asset gets highlighted in the Hierarchy.) This is not the case obviously with a label.
The user should not get confused with completely different looking controls but I rather only want to remove some unnecessary clutter.
You might find a solution to hide the object picker by usage of stylesheets.
If all you want is just to display some reference, you can use a simple button basically styled as text field, adding an image and ping the object from code yourself.
using UnityEngine;
namespace Test
{
public class TestBehaviour : MonoBehaviour
{
[SerializeField] private bool _audioEnabled;
[SerializeField] private AudioClip _audioClip;
}
}
editor:
using System.Reflection;
using UnityEditor;
using UnityEditor.Experimental.UIElements;
using UnityEngine;
namespace Test
{
[CustomEditor(typeof(TestBehaviour))]
public class TestBehaviourEditor : Editor
{
private SerializedProperty _clipProp;
private SerializedProperty _audioEnabledProp;
private ObjectField m_ObjectField;
private const BindingFlags FIELD_BINDING_FLAGS = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
private void OnEnable()
{
_clipProp = serializedObject.FindProperty("_audioClip");
_audioEnabledProp = serializedObject.FindProperty("_audioEnabled");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.PropertyField(_audioEnabledProp);
if(_audioEnabledProp.boolValue)
EditorGUILayout.PropertyField(_clipProp);
else
{
//TODO: calculate proper layout
var type = target.GetType().GetField(_clipProp.propertyPath, FIELD_BINDING_FLAGS).FieldType;
var clip = _clipProp.objectReferenceValue;
var guiContent = EditorGUIUtility.ObjectContent(clip, type);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Fake ObjectField Button");
var style = new GUIStyle("TextField");
style.fixedHeight = 16;
style.imagePosition = clip ? ImagePosition.ImageLeft : ImagePosition.TextOnly;
if (GUILayout.Button(guiContent, style ) && clip)
EditorGUIUtility.PingObject(clip);
EditorGUILayout.EndHorizontal();
}
serializedObject.ApplyModifiedProperties();
}
}
}
I've got another solution: Ignore the pick result of this object picker, and although the picker is still here and can show the picker window, pick up will not work.
(I still don't know how to hide this button and the pickerwindow, > <), and this answer was posted at unity answers as well.
Here is the code:
// Register another callback of this object field
myObjectField.RegisterValueChangedCallback(DefaultObjectFieldCallback);
// In this callback, is a trick
private void DefaultAssetFieldCallback(ChangeEvent<UnityEngine.Object> evt) {
// unregister the callback first
myObjectField.UnregisterValueChangedCallback(DefaultAssetFieldCallback);
// trick: set back to the old value
m_ConfigAssetField.value = evt.previousValue;
// register the callback again
myObjectField.RegisterValueChangedCallback(DefaultObjectFieldCallback);
}
I needed to do something similar and found a way to do this by stepping through the ObjectField in the UIToolkit Debugger. The type of the little object selector button is hidden, so we cant really work with the class itself.
This solution is using UIToolkit, so unfortunately it won't work with Unity Editor IMGUI, but hopefully it will be helpful to someone.
The Solution in easy steps:
Find out what the uss style class of the ObjectFieldSelector is.
Recursively search the children of the ObjectField for a VisualElement containing the uss style class.
Set visibility to false.
All done!
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
[CustomPropertyDrawer(typeof(MyClass))]
public class MyClassDrawer: PropertyDrawer
{
public override VisualElement CreatePropertyGUI(SerializedProperty property)
{
var field = new ObjectField(property.displayName);
field.objectType = typeof(MyClass);
var button = FindChild(field, "unity-object-field__selector");
button.visible = false;
return field;
}
private VisualElement FindChild(VisualElement parent, string ussClass)
{
foreach(var child in parent.Children())
{
if (child.ClassListContains(ussClass))
return child;
var subChild = FindChild(child, ussClass);
if (subChild != null)
return subChild;
}
return null;
}
}