How can i store or read a animation clip data in runtime? - unity3d

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.

Related

ScriptableObject data modified from code in editor mode, but containing asset file not changed

I have a ScriptableObject reference in a MonoBehaviour class.
Modify its values with CustomEditor button.
Save It.
Values seems changed.
Even selected ScriptableObject Inspector displays data modified.
But asset file on disk don't have any data.
And there's no data available from code of any another MonoBehaviour instance.
Here is a code snippet used to save my ScriptableObject.
public TextAsset text;
[SerializeField]
//Thats My Scriptable Object
public PathFramesList pathFramesList;
[SerializeField]
public List<List<ICurveData>> frameDatasList = new List<List<ICurveData>>();
// Called By CustomEditor Button
public void ReadData()
{
#region BlahBlahBlah generating some data to save
var separator = new string[] { "%" };
var framesUnparsedData = text.text.Split(separator, StringSplitOptions.None);
foreach (var f in framesUnparsedData)
frameDatasList.Add(ParseFrame(f));
var result = new ICurveData[frameDatasList.Count][];
for (int i = 0; i < frameDatasList.Count; i++)
result[i] = frameDatasList[i].ToArray();
#endregion
#region Trying to save data
pathFramesList.frameDatasList = result; // Set data to object
EditorUtility.SetDirty(pathFramesList); // Even This didn't help
AssetDatabase.SaveAssets(); // Looks it doesn't work
AssetDatabase.Refresh(); // Hoped this will help, whatever it do
#endregion
}
And that's my ScriptableObjectClass
And Its CustomEditor
[CreateAssetMenu(fileName = "ParsedPathData", menuName = "Create Path Data Asset")]
[System.Serializable]
public class PathFramesList : ScriptableObject
{
[SerializeField]
public ICurveData[][] frameDatasList;
}
#if UNITY_EDITOR
[CustomEditor(typeof(PathFramesList))]
public class PathFramesListEditor : Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector();
PathFramesList pathFrameList = (PathFramesList)target;
if(pathFrameList.frameDatasList == null)
{
GUILayout.TextField("Frames Count is 0");
}
else
{
// It still has all data
GUILayout.TextField("Frames Count is " + pathFrameList.frameDatasList.Length, 200);
}
if(GUILayout.Button("SaveAsset"))
{
// Tried to force saving with this) Didn't help
EditorUtility.SetDirty(target);
EditorUtility.SetDirty(serializedObject.targetObject);
serializedObject.Update();
serializedObject.ApplyModifiedProperties();
AssetDatabase.SaveAssets();
}
}
}
#endif
Ok.
Looks like scriptable objects files just can't store interface implementations examples.
Any else data was written to file well,
Correct me, please, if i'm wrong.

Unity3D custom inspector not saving values correctly

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

How to use a ScriptableObject for a level's Prefab transforms?

I have 3 types of Prefab: A, B & C
Whilst designing the levels, in Play Mode, I add and position them within the level.
How do I create a ScriptableObject that holds references to all of the instances of these prefabs and their transforms?
Specifically, whilst in PlayMode, the Scriptable object should dynamically respond to position and rotation changes of the prefabs in PlayMode.
I can't conceive of how to do this, despite this seemingly being a good use of Scriptable Objects.
You could store the information you want in a dedicated class like e.g.
[Serializable]
public class InstanceInformation
{
public GameObject UsedPrefab;
public Vector3 Position;
public Quaternion Rotation;
public Vector3 Scale;
public InstanceInformation(GameObject usedPrefab, Transform transform)
{
UsedPrefab = usedPrefab;
Rotation = transform.rotation;
Position = transform.position;
Scale = transform.localScale;
}
public void UpdateValues(Transform transform)
{
Rotation = transform.rotation;
Position = transform.position;
Scale = transform.localScale;
}
}
and in your ScriptableObject have a
[CreateAssetMenu]
public class LevelData : ScriptableObject
{
public List<InstanceInformation> instances = new List<InstanceInformation>();
}
Then everytime you Instantiate a prefab you also create an according entry in that instances.
So later in your manager script where you Instantiate the stuff you do e.g.
// Reference this via the Inspector
public LevelData scriptable;
// For keeping a link for the currently Instantiated stuff
// so everytime you manipulate them later on you can update the according data entry
private Dictionary<GameObject, InstanceInformation> objectToInstanceInformation = new Dictionary<GameObject, InstanceInformation>();
...
var obj = Instantiate(aPrefab, aPosition, aRotation);
var instanceInfo = new InstanceInfo(aPrefab, obj.transform);
// Add the reference to the ScriptableObject list
scriptable.instances.Add(instanceInfo);
// And also keep track of the reference linked to the actual instance
objectToInstanceInformation.Add(obj, instanceInfo);
Now you could either repeatedly or at a certain moment call
public void SaveInstanceInformations()
{
foreach(var kvp in objectToInstanceInformation)
{
var obj = kvp.key;
var instanceInfo = kvp.value;
instanceInfo.UpdateValues(obj.transform);
}
}
Since InstanceInformation is a class and thereby a reference-type changing the values here automatically also changes the according entry in the instances in the ScriptableObject!
So later when you want to load the state you can simply do
foreach (var instance in scriptable.instances)
{
var obj = Instantiate(instance.UsedPrefab, instance.Position, instance.Rotation);
obj.transform.localScale = instance.Scale;
objectToInstanceInformation.Add(obj, instance);
}
Finally in order to be able to store this data persistent you could e.g. use a BinaryFormatter like
[CreateAssetMenu]
public class LevelData : ScriptableObject
{
// here you can set a filename the data shall be stored to
[SerializeField] private string fileName = "level.dat";
public List<InstanceInformation> instances = new List<InstanceInformation>();
// called when the object is loaded
private void OnEnable()
{
// try to load the data from drive
// in the editor use the Application.streamingAssetsPath
// in a build use Application.persistentDataPath
var folder = Application.isEditor ? Application.streamingAssetsPath : Application.persistentDataPath;
if(!File.Exists(Path.Combine(folder, fileName)))
{
// on the first run the folder and file will not exist
// in the editor do nothing (the file simply doesn't exist yet)
// in a build copy the content from the streaming assets folder to the persistent data path (if exists)
var fallbackFolder = Application.streamingAssetsPath;
if(!Driectoy.Exists(fallbackFolder)) return;
if(!File.Exists(Path.Combine(fallbackFolder, fileName))) return;
// copy fallback file to persistent file
File.Copy(Path.Combine(fallbackFolder, FileName), Path.Combine(folder, fileName));
}
// load the list
using(var file = File.Open(Path.Combine(folder, fileName), FileMode.Open, FileAccess.Read, FileShare.Read))
{
var binaryFormatter = new BinaryFormatter();
instances = (List<InstanceInformation>)binaryFormatter.Deserialize(file);
}
}
// called when the object is destroyed (you app ended)
private void OnDestroy()
{
// save the data to drive
// in the editor use the Application.streamingAssets
// in a build use Application.persistentDataPath
var folder = Application.isEditor ? Application.streamingAssets : Application.persistentDataPath;
if(!Directoy.Exists(folder)) Directory.CreateDirectory(folder);
using(var file = File.Open(Path.Combine(folder, fileName), FileMode.OpenOrCreate, FileAccess.Write, FileShare.Write))
{
var binaryFormatter = new BinaryFormatter();
binaryFormatter.Serialize(file, instances);
}
}
}

Where to Store all my scene name in one place

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

Is there any way to hide the "Object picker" of an EditorGUILayout.ObjectField in Unity Isnpector?

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;
}
}