Skip to content

Instantly share code, notes, and snippets.

@darbotron
Last active June 6, 2022 09:11
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save darbotron/9622723bfbbe983bf0ea5096cd94a427 to your computer and use it in GitHub Desktop.
Save darbotron/9622723bfbbe983bf0ea5096cd94a427 to your computer and use it in GitHub Desktop.
Super easily (and extensibly) add a project global settings asset to the Unity Editor's project settings panels and/or edit in a floating window
//
// GenericUnityEditorSettings by Alex 'darbotron' Darby
//
// License: https://opensource.org/licenses/unlicense
// TL;DR:
// 1) you may do what you like with it...
// 2) ...except blame me for any consequence of acting on rule 1)
//
using UnityEngine;
using UnityEditor;
[CreateAssetMenu( fileName = "ExampleSettings.asset", menuName = "Example/Example Settings" )]
public class ExampleSettings : GenericEditorSettings< ExampleSettings >
{
public bool m_someFlag = true;
[Space][Header( "yep, editor attributes work!" )][SerializeField] private bool m_someOtherFlag = true;
public TestAsset m_objectReference;
[ObjectInspectorDrawer.ShowInlineGUI] public TestAsset m_objectReferenceWithInlineGUI;
protected override void DefaultInitialise()
{
m_someFlag = true;
m_someOtherFlag = true;
}
}
class ExampleSettingsProvider : GenericEditorSettingsProvider< ExampleSettingsProvider, ExampleSettings >
{
// IMPORTANT: this registers the settings provider with the editor
[SettingsProvider] public static SettingsProvider CreateProvider() => InitialiseSettingsProvider( new ExampleSettingsProvider() );
public ExampleSettingsProvider() : base( $"{ObjectNames.NicifyVariableName( nameof( ExampleSettings ) )}", SettingsScope.Project )
{}
}
class ExampleSettingsWindow : GenericEditorSettingsEditorWindow< ExampleSettingsWindow, ExampleSettings >
{
[MenuItem( "Example/Show Example Settings Editor" )]
static void MenuShowWindow() => FindOrCreateWindow();
protected override float VGetRequiredLabelWidth() => 400f;
}
//
// GenericUnityEditorSettings by Alex 'darbotron' Darby
//
// License: https://opensource.org/licenses/unlicense
// TL;DR:
// 1) you may do what you like with it...
// 2) ...except blame me for any consequence of acting on rule 1)
//
using System;
using System.Linq;
using System.Reflection;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
//////////////////////////////////////////////////////////////////////////////
/// base class for generically handled editor settings asset used by
/// GenericEditorSettingsProvider
///
/// Inherit from this and implement DefaultInitialise() to create a class
/// which can be generically handled as a settings item in the Project
/// settings
///
/// For editor access, treat GenericEditorSettings< T >.GetOrCreateSettings()
/// like a singleton instance function
///
/// The asset is created in the root of "Assets/" with the namespace qualified
/// name of type T
//////////////////////////////////////////////////////////////////////////////
public abstract class GenericEditorSettings< T > : ScriptableObject where T : GenericEditorSettings< T >
{
//------------------------------------------------------------------------
/// loads an asset of derived type T at AssetPath()
/// if not present it's created
/// treat this like a singleton instance function
//------------------------------------------------------------------------
public static T GetOrCreateSettings()
{
var settings = AssetDatabase.LoadAssetAtPath< T >( AssetPath );
if( settings == null )
{
settings = ScriptableObject.CreateInstance< T >() as T;
settings.DefaultInitialise();
AssetDatabase.CreateAsset( settings, AssetPath );
AssetDatabase.SaveAssets();
}
return settings;
}
//------------------------------------------------------------------------
/// change this in derived classes to change the location of the singleton
/// asset in the project
///
/// NOTE: for this to work you will need to use [InitializeOnLoadMethod]
/// <example>
/// [InitializeOnLoadMethod]
/// private static void OnLoad_SetPathForAssetSingleton() => AssetPath = $"Assets/NewDirectory/{nameof( DerivingType )}.asset";
/// </example>
//------------------------------------------------------------------------
public static string AssetPath { get; protected set; } = $"Assets/{typeof( T ).FullName}.asset";
//------------------------------------------------------------------------
internal static SerializedObject GetSettingsAssetAsSerialisedObject() => new SerializedObject( GetOrCreateSettings() );
//------------------------------------------------------------------------
/// implement this in derived types to default initialise the settings
/// asset on creation
//------------------------------------------------------------------------
protected abstract void DefaultInitialise();
}
//////////////////////////////////////////////////////////////////////////////
/// base class for generically handling editor project settings
///
/// You can use this to create settings under either
/// * [Edit -> Project Settings...] or
/// * [Edit -> Preferences...]
/// depending on the params passed to the base constructor in a derived class
///
/// Inherit from GenericEditorSettings create an asset class which contains
/// the editor settings properties you want.
///
/// This generic class uses ObjectInspectorDrawer to draw all fields of the
/// serialised settings asset in a customiseable manner
///
/// NOTES:
/// * the settings won't appear until after the settings asset exists EITHER:
/// ** after the 1st call to GenericEditorSettings< T >.GetOrCreateSettings()
/// ** if you use [CreateAssetMenu( ... )] on the asset type to create one from the editor menu manually
///
/// * You MUST register the settings provider from the deriving type (see
/// example below )
///
/// <example>
/// class ExampleSettingsProvider : GenericEditorSettingsProvider< ExampleSettingsProvider, ExampleSettings >
/// {
/// // IMPORTANT: this registers the settings provider with the editor
/// [SettingsProvider] public static SettingsProvider CreateProvider() => InitialiseSettingsProvider( new ExampleSettingsProvider() );
/// public ExampleSettingsProvider() : base( $"{ObjectNames.NicifyVariableName( nameof( ExampleSettings ) )}", SettingsScope.Project )
/// {}
/// }
/// </example>
//////////////////////////////////////////////////////////////////////////////
public class GenericEditorSettingsProvider< TProvider, TSettings > : SettingsProvider
where TProvider : GenericEditorSettingsProvider< TProvider, TSettings >
where TSettings : GenericEditorSettings< TSettings >
{
//------------------------------------------------------------------------
public static bool SettingsAssetExists() => System.IO.File.Exists( GenericEditorSettings< TSettings >.AssetPath );
//------------------------------------------------------------------------
public static SettingsProvider InitialiseSettingsProvider( GenericEditorSettingsProvider< TProvider, TSettings > provider )
{
if( SettingsAssetExists() )
{
// Automatically extract all keywords from the Styles.
var finfoArrayAllSerialised = ReflectionUtils.GetSerialisedFieldsforType( typeof( TSettings ) );
var keywordList = new List< string >( finfoArrayAllSerialised.Length );
foreach( var finfo in finfoArrayAllSerialised )
{
keywordList.Add( ObjectNames.NicifyVariableName( finfo.Name ) );
}
provider.keywords = keywordList;
return provider;
}
// Settings Asset doesn't exist yet; no need to display anything in the Settings window.
return null;
}
//------------------------------------------------------------------------
public override void OnActivate( string searchContext, UnityEngine.UIElements.VisualElement rootElement )
{
// This function is called when the user clicks on the MyCustom element in the Settings window.
m_GenericEditorSettings = GenericEditorSettings< TSettings >.GetSettingsAssetAsSerialisedObject();
}
//------------------------------------------------------------------------
public override void OnGUI( string searchContext )
{
if( m_objectInspectorDrawer is null )
{
m_objectInspectorDrawer = VGetObjectInspectorDrawer();
}
using( var checkChanges = new EditorGUI.ChangeCheckScope() )
{
var originalLabelWidth = EditorGUIUtility.labelWidth;
EditorGUIUtility.labelWidth = VGetRequiredLabelWidth();
m_objectInspectorDrawer.DrawObjectGUI( m_GenericEditorSettings );
EditorGUIUtility.labelWidth = originalLabelWidth;
if( checkChanges.changed )
{
m_GenericEditorSettings.ApplyModifiedProperties();
}
}
}
//------------------------------------------------------------------------
/// constructor must be called from deriving type
/// for params see https://docs.unity3d.com/ScriptReference/SettingsProvider-ctor.html
/// note: the keywords param is optional and defaults to null
//------------------------------------------------------------------------
protected GenericEditorSettingsProvider( string path, SettingsScope scope ) : base( path, scope )
{}
//------------------------------------------------------------------------
protected virtual float VGetRequiredLabelWidth() => 200f;
//------------------------------------------------------------------------
protected virtual ObjectInspectorDrawer VGetObjectInspectorDrawer() => new ObjectInspectorDrawer();
private SerializedObject m_GenericEditorSettings;
private ObjectInspectorDrawer m_objectInspectorDrawer;
}
//////////////////////////////////////////////////////////////////////////////
/// base class for generically handling a "singleton" settings asset with its
/// own editor window
///
/// Inherit from GenericEditorSettings to create an asset class which
/// contains the properties you want (TSettings)
///
/// This generic class uses ObjectInspectorDrawer to display all serialised
/// fields of TSettings in the appropriate editor window panel.
///
/// NOTES:
/// * the settings won't appear until after the settings asset exists EITHER:
/// ** after the 1st call to GenericEditorSettings< T >.GetOrCreateSettings()
/// ** if you use [CreateAssetMenu( ... )] on the asset type to create one from the editor menu manually
///
///<example>
/// class ExampleSettingsWindow : GenericEditorSettingsEditorWindow< ExampleSettingsWindow, ExampleSettings >
/// {
/// [MenuItem( "Example/Show Example Settings Editor" )]
/// static void MenuShowWindow() => FindOrCreateWindow();
///
/// protected override float VGetRequiredLabelWidth() => 400f;
/// }
///</example>
//////////////////////////////////////////////////////////////////////////////
public class GenericEditorSettingsEditorWindow< TEditorWindow, TSettings > : EditorWindow
where TEditorWindow : GenericEditorSettingsEditorWindow< TEditorWindow, TSettings >
where TSettings : GenericEditorSettings< TSettings >
{
//------------------------------------------------------------------------
protected static void FindOrCreateWindow()
{
var thisWindow = EditorWindow.GetWindow< TEditorWindow >( ObjectNames.NicifyVariableName( typeof( TSettings ).Name ) ) as GenericEditorSettingsEditorWindow< TEditorWindow, TSettings >;
thisWindow.Show();
}
//------------------------------------------------------------------------
public virtual void OnGUI()
{
if( m_objectInspectorDrawer is null )
{
m_objectInspectorDrawer = VGetObjectInspectorDrawer();
}
using( var scrollScope = new EditorGUILayout.ScrollViewScope( m_v2ScrollPosition, false, true, GUILayout.ExpandWidth( true ) ) )
{
float maxWidthAccountingForTheVerticalScrollbar = position.width - GUI.skin.verticalScrollbar.fixedWidth;
using( var vertical = new EditorGUILayout.VerticalScope( GUILayout.MaxWidth( maxWidthAccountingForTheVerticalScrollbar ) ) )
{
using( var checkChanges = new EditorGUI.ChangeCheckScope() )
{
var originalLabelWidth = EditorGUIUtility.labelWidth;
EditorGUIUtility.labelWidth = VGetRequiredLabelWidth();
var serialisedObject = GenericEditorSettings< TSettings >.GetSettingsAssetAsSerialisedObject();
m_objectInspectorDrawer.DrawObjectGUI( serialisedObject );
EditorGUIUtility.labelWidth = originalLabelWidth;
if( checkChanges.changed )
{
serialisedObject.ApplyModifiedProperties();
}
serialisedObject.Dispose();
m_v2ScrollPosition = scrollScope.scrollPosition;
}
}
}
}
//------------------------------------------------------------------------
protected virtual float VGetRequiredLabelWidth() => 200f;
//------------------------------------------------------------------------
protected virtual ObjectInspectorDrawer VGetObjectInspectorDrawer() => new ObjectInspectorDrawer();
[SerializeField] private Vector2 m_v2ScrollPosition = Vector2.zero;
private ObjectInspectorDrawer m_objectInspectorDrawer;
}
/// <summary>
/// Hand rolled version of https://docs.unity3d.com/ScriptReference/Editor.DrawDefaultInspector.html but with extensions!
/// 1) when tagged with the [DrawObjectInspectorInline] attribute the GUI for referenced objects will be rendered inline after the object reference field
/// 2) can override its behaviour to easily customise drawing to extend the default
///
/// Also respects the standard Unity property attributes e.g. [Header()], [Space()] etc.
///
/// Uses SerialisedObjectReferenceInspectorDrawer to draw the inline GUI for object references, which can also be customised be deriving.
/// Use a custom SerialisedObjectReferenceInspectorDrawer derived class by calling the constructor which takes one as a param.
/// </summary>
public class ObjectInspectorDrawer
{
/// <summary>
/// add this to a serialized monobehaviour or scriptable object reference to add its GUI inline in the inspector
/// </summary>
public class ShowInlineGUIAttribute : Attribute
{
public bool DrawInBox = true;
public bool Indent = true;
}
public ObjectInspectorDrawer()
{
m_objectReferenceGUIDrawer = new SerialisedObjectReferenceInspectorDrawer();
}
public ObjectInspectorDrawer( SerialisedObjectReferenceInspectorDrawer objectReferenceDrawer )
{
m_objectReferenceGUIDrawer = objectReferenceDrawer;
}
public void DrawObjectGUI( SerializedObject serialisedObject )
{
var allSerialisedFields = ReflectionUtils.GetSerialisedFieldsforType( serialisedObject.targetObject.GetType() );
VOnDrawAllProperties( allSerialisedFields, serialisedObject );
if( serialisedObject.hasModifiedProperties )
{
serialisedObject.ApplyModifiedProperties();
}
}
/// <summary>
/// override this in a derived type to easily add additional GUI before / after the default elements
/// </summary>
/// <param name="allSerialisedFields"></param>
/// <param name="serialisedObject"></param>
/// <example>
/// public class Example : ObjectInspectorDrawer
/// {
/// protected override void VOnDrawAllProperties( ... )
/// {
/// // add additional GUI before default
/// base.VOnDrawAllProperties( ... )
/// // add additional GUI after default
/// }
/// }
/// </example>
protected virtual void VOnDrawAllProperties( FieldInfo[] allSerialisedFields, SerializedObject serialisedObject )
{
foreach( var finfo in allSerialisedFields )
{
var property = serialisedObject.FindProperty( finfo.Name );
if( property != null )
{
switch( property.propertyType )
{
case SerializedPropertyType.ObjectReference:
VOnDrawObjectReferenceProperty( finfo, property );
break;
default:
VOnDrawProperty( finfo, property );
break;
}
}
}
}
/// <summary>
/// override this in a derived type to change drawing for individual properties
/// you can test info.name to do custom drawing for (a) specific propert(y/ies)
/// </summary>
/// <param name="finfo"></param>
/// <param name="property"></param>
/// <example>
/// public class Example : ObjectInspectorDrawer
/// {
/// protected override void VOnDrawProperty( ... )
/// {
/// // add additional GUI before property
/// base.VOnDrawProperty( ... )
/// // add additional GUI after property
/// }
/// }
/// </example>
protected virtual void VOnDrawProperty( FieldInfo finfo, SerializedProperty property )
{
EditorGUILayout.PropertyField( property );
}
/// <summary>
/// override this in a derived type to change drawing for individual object rederence properties
/// you can test info.name to do custom drawing for (a) specific propert(y/ies)
/// NOTE: if you want to affect the rendering of an object reference's inline GUI
/// you will need to create a custom SerialisedObjectReferenceInspectorDrawer
/// </summary>
/// <param name="finfo"></param>
/// <param name="property"></param>
/// <example>
/// public class Example : ObjectInspectorDrawer
/// {
/// protected override void VOnDrawObjectReferenceProperty( ... )
/// {
/// // add additional GUI before property
/// base.VOnDrawObjectReferenceProperty( ... )
/// // add additional GUI after property
/// }
/// }
/// </example>
protected virtual void VOnDrawObjectReferenceProperty( FieldInfo finfo, SerializedProperty property )
{
EditorGUILayout.PropertyField( property );
if( finfo.IsDefined( typeof( ShowInlineGUIAttribute ), false ) )
{
var attribute = finfo.GetCustomAttribute< ShowInlineGUIAttribute >();
m_objectReferenceGUIDrawer.DrawInlineGUIForSerialisedObjectReference( property, attribute.DrawInBox, attribute.Indent );
}
}
SerialisedObjectReferenceInspectorDrawer m_objectReferenceGUIDrawer;
}
/// <summary>
/// Hand rolled version of https://docs.unity3d.com/ScriptReference/Editor.DrawDefaultInspector.html
/// but for drawing inline GUI for an Object Reference property (which just show up as
/// the reference box in standard editor GUI)
///
/// Used by ObjectInspectorDrawer to render inline UI for properties which are object references
/// </summary>
public class SerialisedObjectReferenceInspectorDrawer
{
public void DrawInlineGUIForSerialisedObjectReference( SerializedProperty propertyToDraw, bool inBox = true, bool indented = true )
{
Debug.Assert( propertyToDraw.propertyType == SerializedPropertyType.ObjectReference );
var propertyAsSerialisedObj = new SerializedObject( propertyToDraw.objectReferenceValue );
var allFieldInfo = ReflectionUtils.GetSerialisedFieldsforType( propertyToDraw.objectReferenceValue.GetType() );
var indentAmount = ( indented ? 1f : 0f ) * 20f;
var externalScope = ( inBox ? new EditorGUILayout.VerticalScope( EditorStyles.helpBox ) : new EditorGUILayout.VerticalScope() );
using( externalScope )
{
var isToggled = EditorGUILayout.Foldout( IsFoldOutToggledForProperty( propertyToDraw ), ObjectNames.NicifyVariableName( propertyToDraw.name ) );
SetFoldOutToggledForProperty( propertyToDraw, isToggled );
if( isToggled )
{
using( new EditorGUILayout.HorizontalScope() )
{
EditorGUILayout.Space( indentAmount, false );
using( new EditorGUILayout.VerticalScope() )
{
foreach( var finfo in allFieldInfo )
{
var property = propertyAsSerialisedObj.FindProperty( finfo.Name );
if( property != null )
{
VOnDrawPropertyOfObjectReference( property );
}
}
if( propertyAsSerialisedObj.hasModifiedProperties )
{
propertyAsSerialisedObj.ApplyModifiedProperties();
}
propertyAsSerialisedObj.Dispose();
}
}
}
}
}
protected virtual void VOnDrawPropertyOfObjectReference( SerializedProperty property )
{
EditorGUILayout.PropertyField( property );
}
private bool IsFoldOutToggledForProperty( SerializedProperty property ) => ( m_dictFoldoutForPropertyIsToggled.ContainsKey( property.propertyPath ) ? m_dictFoldoutForPropertyIsToggled[ property.propertyPath ] : false );
private bool SetFoldOutToggledForProperty( SerializedProperty property, bool toggled ) => m_dictFoldoutForPropertyIsToggled[ property.propertyPath ] = toggled;
private Dictionary< string, bool > m_dictFoldoutForPropertyIsToggled = new Dictionary< string, bool >();
}
public static class ReflectionUtils
{
public static FieldInfo[] GetAllFieldsIncludingBaseTypes( this Type thisType ) => GetAllFieldsIncludingBaseTypes( thisType, null );
public static FieldInfo[] GetAllFieldsIncludingBaseTypes( this Type thisType, Type baseClassToStopAt )
{
var listSelfAndAllBaseTypes = new List< Type >();
for
(
var currentType = thisType;
( ( currentType != null )
&& ( currentType != baseClassToStopAt ) );
currentType = currentType.BaseType )
{
listSelfAndAllBaseTypes.Add( currentType );
}
listSelfAndAllBaseTypes.Reverse();
var allFieldsList = new List< FieldInfo >();
foreach( var type in listSelfAndAllBaseTypes )
{
allFieldsList.AddRange( type.GetFields( BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly ) );
}
return allFieldsList.ToArray();
}
// all fields which are public OR have the [SerializeField] attribute
public static FieldInfo[] GetFieldsIncludingBaseSerialisedOnly( this Type thisType ) => GetAllFieldsIncludingBaseTypes( thisType ).Where( field => ( field.IsPublic || field.IsDefined( typeof( SerializeField ), false ) ) ).ToArray();
// this cache will naturally be reset on every domain reload (i.e. script compile) in the editor
private static Dictionary< Type, FieldInfo[] > sm_dictTypeToSerialisedFieldsCache = new Dictionary< Type, FieldInfo[] >();
public static FieldInfo[] GetSerialisedFieldsforType( Type queriedType )
{
if( sm_dictTypeToSerialisedFieldsCache.TryGetValue( queriedType, out var serialisedFieldArray ) )
{
return serialisedFieldArray;
}
serialisedFieldArray = queriedType.GetFieldsIncludingBaseSerialisedOnly();
sm_dictTypeToSerialisedFieldsCache[ queriedType ] = serialisedFieldArray;
return serialisedFieldArray;
}
}
//
// GenericUnityEditorSettings by Alex 'darbotron' Darby
//
// License: https://opensource.org/licenses/unlicense
// TL;DR:
// 1) you may do what you like with it...
// 2) ...except blame me for any consequence of acting on rule 1)
//
using UnityEngine;
using System.Collections.Generic;
//
// used only to demo the inline object reference GUI in ExampleSettings.cs
//
[CreateAssetMenu( fileName = "TestAsset.asset", menuName = "Example/TestAsset" )]
public class TestAsset : ScriptableObject
{
public bool m_someFlag = true;
[SerializeField] private bool m_someOtherFlag = true;
[SerializeField] private List< int > m_listOfInt;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment