SaintsField
SaintsField copied to clipboard
A Unity Inspector extension tool focusing on script fields inspector enhancement
SaintsField
SaintsField is a Unity Inspector extension tool focusing on script fields like NaughtyAttributes but different.
Unity: 2019.1 or higher
(Yes, the project name comes from, of course, Saints Row 2)
Highlights
- Works on deep nested fields!
- Supports both IMGUI and UI Toolkit! And it can properly handle IMGUI drawer even with UI Toolkit enabled!
- Use and only use
PropertyDrawerandDecoratorDrawer(exceptSaintsEditor, which is disabled by default), thus it will be compatible with most Unity Inspector enhancements likeNaughtyAttributesand your custom drawer. - Allow stack on many cases. Only attributes that modified the label itself, and the field itself can not be stacked. All other attributes can mostly be stacked.
- Allow dynamic arguments in many cases
Installation
-
Using Unity Asset Store
-
Using OpenUPM
openupm add today.comes.saintsfield -
Using git upm:
add to
Packages/manifest.jsonin your project{ "dependencies": { "today.comes.saintsfield": "https://github.com/TylerTemp/SaintsField.git", // your other dependencies... } } -
Using a
unitypackage:Go to the Release Page to download a desired version of
unitypackageand import it to your project -
Using a git submodule:
git submodule add https://github.com/TylerTemp/SaintsField.git Assets/SaintsField
If you're using unitypackage or git submodule but you put this project under another folder rather than Assets/SaintsField, please also do the following:
- Create
Assets/Editor Default Resources/SaintsField. - Copy files from project's
Editor/Editor Default Resources/SaintsFieldinto your project'sAssets/Editor Default Resources/SaintsField. If you're using a file browser instead of Unity's project tab to copy files, you may want to exclude the.metafile to avoid GUID conflict.
Change Log
3.0.10
Fix in some Unity version (tested on 2022.3.20) a cached struct object can not correctly report the value inside it after changing #29
See the full change log.
Out-Of-Box Attributes
All attributes under this section can be used in your project without any extra setup.
namespace: SaintsField
Label & Text
RichLabel
-
string|null richTextXmlthe content of the label, supported tag:- All Unity rich label tag, like
<color=#ff0000>red</color> <label />for current field name<icon=path/to/image.png />for icon
nullmeans no labelfor
iconit will search the following path:"Assets/Editor Default Resources/SaintsField/"(You can override things here)"Assets/SaintsField/Editor/Editor Default Resources/SaintsField/"(this is most likely to be when installed usingunitypackage)"Packages/today.comes.saintsfield/Editor/Editor Default Resources/SaintsField/"(this is most likely to be when installed usingupm)Assets/Editor Default Resources/, then fallback to built-in editor resources by name (usingEditorGUIUtility.Load)
for
colorit supports:-
Standard Unity Rich Label colors:
aqua,black,blue,brown,cyan,darkblue,fuchsia,green,gray,grey,lightblue,lime,magenta,maroon,navy,olive,orange,purple,red,silver,teal,white,yellow -
Some extra colors from NaughtyAttributes:
clear,pink,indigo,violet -
Some extra colors from UI Toolkit:
charcoalGray,oceanicSlate -
html color which is supported by
ColorUtility.TryParseHtmlString, like#RRGGBB,#RRGGBBAA,#RGB,#RGBA
- All Unity rich label tag, like
-
bool isCallback=falseif true, the
richTextXmlwill be interpreted as a property/callback function, and the string value / the returned string value (tag supported) will be used as the label content -
AllowMultiple: No. A field can only have one
RichLabel
Special Note:
Use it on an array/list will apply it to all the direct child element instead of the field label itself. You can use this to modify elements of an array/list field, in this way:
- Ensure you make it a callback:
isCallback=true - It'll pass the element value and index to your function
- Return the desired label content from the function
using SaintsField;
[RichLabel("<color=indigo><icon=eye.png /></color><b><color=red>R</color><color=green>a</color><color=blue>i</color><color=yellow>i</color><color=cyan>n</color><color=magenta>b</color><color=pink>o</color><color=orange>w</color></b>: <color=violet><label /></color>")]
public string _rainbow;
[RichLabel(nameof(LabelCallback), true)]
public bool _callbackToggle;
private string LabelCallback() => _callbackToggle ? "<color=green><icon=eye.png /></color> <label/>" : "<icon=eye-slash.png /> <label/>";
[Space]
[RichLabel(nameof(_propertyLabel), true)]
public string _propertyLabel;
private string _rainbow;
[Serializable]
private struct MyStruct
{
[RichLabel("<color=green>HI!</color>")]
public float LabelFloat;
}
[SerializeField]
[RichLabel("<color=green>Fixed For Struct!</color>")]
private MyStruct _myStructWorkAround;
Here is an example of using on a array:
using SaintsField;
[RichLabel(nameof(ArrayLabels), true)]
public string[] arrayLabels;
// if you do not care about the actual value, use `object` as the first parameter
private string ArrayLabels(object _, int index) => $"<color=pink>[{(char)('A' + index)}]";
AboveRichLabel / BelowRichLabel
Like RichLabel, but it's rendered above/below the field in full width of view instead.
string|null richTextXmlSame asRichLabelbool isCallback=falseSame asRichLabelstring groupBy = ""SeeGroupBysection- AllowMultiple: Yes
using SaintsField;
[SerializeField]
[AboveRichLabel("┌<icon=eye.png/><label />┐")]
[RichLabel("├<icon=eye.png/><label />┤")]
[BelowRichLabel(nameof(BelowLabel), true)]
[BelowRichLabel("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~", groupBy: "example")]
[BelowRichLabel("==================================", groupBy: "example")]
private int _intValue;
private string BelowLabel() => "└<icon=eye.png/><label />┘";
OverlayRichLabel
Like RichLabel, but it's rendered on top of the field.
Only supports string/number type of field. Does not work with any kind of TextArea (multiple line) and Range.
Parameters:
string richTextXmlthe content of the label, or a property/callback. Supports tags likeRichLabelbool isCallback=falseif true, therichTextXmlwill be interpreted as a property/callback function, and the string value / the returned string value (tag supported) will be used as the label contentfloat padding=5fpadding between your input and the label. Not work whenend=truebool end=falsewhen false, the label will follow the end of your input. Otherwise, it will stay at the end of the field.string GroupBy=""this is only for the error message box.- AllowMultiple: No
using SaintsField;
[OverlayRichLabel("<color=grey>km/s")] public double speed = double.MinValue;
[OverlayRichLabel("<icon=eye.png/>")] public string text;
[OverlayRichLabel("<color=grey>/int", padding: 1)] public int count = int.MinValue;
[OverlayRichLabel("<color=grey>/long", padding: 1)] public long longInt = long.MinValue;
[OverlayRichLabel("<color=grey>suffix", end: true)] public string atEnd;
PostFieldRichLabel
Like RichLabel, but it's rendered at the end of the field.
Parameters:
string richTextXmlthe content of the label, or a property/callback. Supports tags likeRichLabelbool isCallback=falseif true, therichTextXmlwill be interpreted as a property/callback function, and the string value / the returned string value (tag supported) will be used as the label contentfloat padding=5fpadding between the field and the label.string GroupBy=""this is only for the error message box.- AllowMultiple: Yes
using SaintsField;
[PostFieldRichLabel("<color=grey>km/s")] public float speed;
[PostFieldRichLabel("<icon=eye.png/>", padding: 0)] public GameObject eye;
[PostFieldRichLabel(nameof(TakeAGuess), isCallback: true)] public int guess;
public string TakeAGuess()
{
if(guess > 20)
{
return "<color=red>too high";
}
if (guess < 10)
{
return "<color=blue>too low";
}
return "<color=green>acceptable!";
}
InfoBox
Draw an info box above/below the field.
-
string contentThe content of the info box
-
EMessageType messageType=EMessageType.InfoMessage icon. Options are
NoneInfoWarningError
-
string show=nulla callback name or property name for show or hide this info box.
-
bool isCallback=falseif true, the
contentwill be interpreted as a property/callback function.If the value (or returned value) is a string, then the content will be changed
If the value is
(EMessageType messageType, string content)then both content and message type will be changed -
bool above=falseDraw the info box above the field instead of below
-
string groupBy=""SeeGroupBysection -
AllowMultiple: Yes
using SaintsField;
[field: SerializeField] private bool _show;
[Space]
[InfoBox("Hi\nwrap long line content content content content content content content content content content content content content content content content content content content content content content content content content", EMessageType.None, above: true)]
[InfoBox(nameof(DynamicMessage), EMessageType.Warning, isCallback: true, above: true)]
[InfoBox(nameof(DynamicMessageWithIcon), isCallback: true)]
[InfoBox("Hi\n toggle content ", EMessageType.Info, nameof(_show))]
public bool _content;
private (EMessageType, string) DynamicMessageWithIcon => _content ? (EMessageType.Error, "False!") : (EMessageType.None, "True!");
private string DynamicMessage() => _content ? "False" : "True";
SepTitle
A separator with text
string title=nulltitle,nullfor no title at all. Does NOT support rich textEColor color, color for title and line separatorfloat gap = 2f, space between title and line separatorfloat height = 2f, height of this decorator
using SaintsField;
[SepTitle("Separate Here", EColor.Pink)]
public string content1;
[SepTitle(EColor.Green)]
public string content2;
General Buttons
There are 3 general buttons:
AboveButtonwill draw a button on aboveBelowButtonwill draw a button on belowPostFieldButtonwill draw a button at the end of the field
All of them have the same arguments:
-
string funcNamecalled when you click the button
-
string buttonLabel=nulllabel of the button, support tags like
RichLabel.nullmeans using function name as label -
bool isCallback = falsea callback or property name for button's label, same as
RichLabel -
string groupBy = ""See
GroupBysection. Does NOT work onPostFieldButton -
AllowMultiple: Yes
using SaintsField;
[SerializeField] private bool _errorOut;
[field: SerializeField] private string _labelByField;
[AboveButton(nameof(ClickErrorButton), nameof(_labelByField), true)]
[AboveButton(nameof(ClickErrorButton), "Click <color=green><icon='eye.png' /></color>!")]
[AboveButton(nameof(ClickButton), nameof(GetButtonLabel), true, "OK")]
[AboveButton(nameof(ClickButton), nameof(GetButtonLabel), true, "OK")]
[PostFieldButton(nameof(ToggleAndError), nameof(GetButtonLabelIcon), true)]
[BelowButton(nameof(ClickButton), nameof(GetButtonLabel), true, "OK")]
[BelowButton(nameof(ClickButton), nameof(GetButtonLabel), true, "OK")]
[BelowButton(nameof(ClickErrorButton), "Below <color=green><icon='eye.png' /></color>!")]
public int _someInt;
private void ClickErrorButton() => Debug.Log("CLICKED!");
private string GetButtonLabel() =>
_errorOut
? "Error <color=red>me</color>!"
: "No <color=green>Error</color>!";
private string GetButtonLabelIcon() => _errorOut
? "<color=red><icon='eye.png' /></color>"
: "<color=green><icon='eye.png' /></color>";
private void ClickButton()
{
Debug.Log("CLICKED 2!");
if(_errorOut)
{
throw new Exception("Expected exception!");
}
}
private void ToggleAndError()
{
Toggle();
ClickButton();
}
private void Toggle() => _errorOut = !_errorOut;
Field Modifier
GameObjectActive
A toggle button to toggle the GameObject.activeSelf of the field.
This does not require the field to be GameObject. It can be a component which already attached to a GameObject.
- AllowMultiple: No
using SaintsField;
[GameObjectActive] public GameObject _go;
[GameObjectActive] public GameObjectActiveExample _component;
SpriteToggle
A toggle button to toggle the Sprite of the target.
The field itself must be Sprite.
-
string imageOrSpriteRendererthe target, must be either
UI.ImageorSpriteRenderer -
AllowMultiple: Yes
using SaintsField;
[field: SerializeField] private Image _image;
[field: SerializeField] private SpriteRenderer _sprite;
[SerializeField
, SpriteToggle(nameof(_image))
, SpriteToggle(nameof(_sprite))
] private Sprite _sprite1;
[SerializeField
, SpriteToggle(nameof(_image))
, SpriteToggle(nameof(_sprite))
] private Sprite _sprite2;
MaterialToggle
A toggle button to toggle the Material of the target.
The field itself must be Material.
-
string rendererName=nullthe target, must be
Renderer(or its subClass likeMeshRenderer). When using null, it will try to get theRenderercomponent from the current component -
int index=0which slot index of
materialsonRendereryou want to swap -
AllowMultiple: Yes
using SaintsField;
public Renderer targetRenderer;
[MaterialToggle(nameof(targetRenderer))] public Material _mat1;
[MaterialToggle(nameof(targetRenderer))] public Material _mat2;
ColorToggle
A toggle button to toggle color for Image, Button, SpriteRenderer or Renderer
The field itself must be Color.
-
string compName=nullthe target, must be
Image,Button,SpriteRenderer, orRenderer(or its subClass likeMeshRenderer).When using
null, it will try to get the correct component from the target object of this field by order.When it's a
Renderer, it will change the material's.colorproperty.When it's a
Button, it will change the button'stargetGraphic.colorproperty. -
int index=0(only works for
Renderertype) which slot index ofmaterialsonRendereryou want to apply the color -
AllowMultiple: Yes
using SaintsField;
// auto find on the target object
[SerializeField, ColorToggle] private Color _onColor;
[SerializeField, ColorToggle] private Color _offColor;
[Space]
// by name
[SerializeField] private Image _image;
[SerializeField, ColorToggle(nameof(_image))] private Color _onColor2;
[SerializeField, ColorToggle(nameof(_image))] private Color _offColor2;
Expandable
Make serializable object expandable. (E.g. ScriptableObject, MonoBehavior)
Known issue:
-
IMGUI: if the target itself has a custom drawer, the drawer will not be used, because
PropertyDraweris not allowed to create anEditorclass, thus it'll just iterate and draw all fields in the object.For more information about why this is impossible under IMGUI, see Issue 25
-
IMGUI: the
Foldoutwill NOT be placed at the left space like a Unity's default foldout component, because Unity limited thePropertyDrawerto be drawn inside the rect Unity gives. Trying outside of the rect will make the target non-interactable. But in early Unity (like 2019.1), Unity will forceFoldoutto be out of rect on top leve, but not on array/list level... so you may see different outcomes on different Unity version. -
UI Toolkit:
ReadOnly(andDisableIf,EnableIf) can NOT disable the expanded fields. This is becauseInspectorElementdoes not work withSetEnable(false), neither withpickingMode=Ignore. This can not be fixed unless Unity fixes it.
- AllowMultiple: No
using SaintsField;
[Expandable] public ScriptableObject _scriptable;
ReferencePicker
A dropdown to pick a referenced value for Unity's SerializeReference.
You can use this to pick non UnityObject object like interface or polymorphism class.
Limitation:
- The target must have a public constructor with no required arguments.
- It'll try to copy field values when changing types but not guaranteed.
structwill not get copied value (it's too tricky to deal a struct)
- Allow Multiple: No
using SaintsField;
[Serializable]
public class Base1Fruit
{
public GameObject base1;
}
[Serializable]
public class Base2Fruit: Base1Fruit
{
public int base2;
}
[Serializable]
public class Apple : Base2Fruit
{
public string apple;
public GameObject applePrefab;
}
[Serializable]
public class Orange : Base2Fruit
{
public bool orange;
}
[SerializeReference, ReferencePicker]
public Base2Fruit item;
public interface IRefInterface
{
public int TheInt { get; }
}
// works for struct
[Serializable]
public struct StructImpl : IRefInterface
{
[field: SerializeField]
public int TheInt { get; set; }
public string myStruct;
}
[Serializable]
public class ClassDirect: IRefInterface
{
[field: SerializeField, Range(0, 10)]
public int TheInt { get; set; }
}
// abstruct type will be skipped
public abstract class ClassSubAbs : ClassDirect
{
public abstract string AbsValue { get; }
}
[Serializable]
public class ClassSub1 : ClassSubAbs
{
public string sub1;
public override string AbsValue => $"Sub1: {sub1}";
}
[Serializable]
public class ClassSub2 : ClassSubAbs
{
public string sub2;
public override string AbsValue => $"Sub2: {sub2}";
}
[SerializeReference, ReferencePicker]
public IRefInterface myInterface;
ParticlePlay
A button to play a particle system of the field value, or the one on the field value.
Unity allows play ParticleSystem in the editor, but only if you selected the target GameObject. It can only play one at a time.
This decorator allows you to play multiple ParticleSystem as long as you have the expected fields.
Parameters:
-
string groupBy = ""for error grouping. -
Allow Multiple: No
Note: because of the limitation from Unity, it can NOT detect if a ParticleSystem is finished playing
[ParticlePlay] public ParticleSystem particle;
// It also works if the field target has a particleSystem component
[ParticlePlay, FieldType(typeof(ParticleSystem), false)] public GameObject particle2;
Field Re-Draw
This will change the look & behavior of a field.
Rate
A rating stars tool for an int field.
Parameters:
-
int minminimum value of the rating. Must be equal to or greater than 0.When it's equal to 0, it'll draw a red slashed star to select
0.When it's greater than 0, it will draw
minnumber of fixed stars that you can not un-rate. -
int maxmaximum value of the rating. Must be greater thanmin. -
AllowMultiple: No
using SaintsField;
[Rate(0, 5)] public int rate0To5;
[Rate(1, 5)] public int rate1To5;
[Rate(3, 5)] public int rate3To5;
FieldType
Ask the inspector to display another type of field rather than the field's original type.
This is useful when you want to have a GameObject prefab, but you want this target prefab to have a specific component (e.g. your own MonoScript, or a ParticalSystem). By using this you force the inspector to sign the required object that has your expected component but still gives you the original typed value to field.
Overload:
FieldTypeAttribute(Type compType, EPick editorPick = EPick.Assets | EPick.Scene, bool customPicker = true)FieldTypeAttribute(Type compType, bool customPicker)
For each argument:
-
Type compTypethe type of the component you want to pick -
EPick editorPickwhere you want to pick the component. Options are:EPick.Assetsfor assetsEPick.Scenefor scene objects
For the default Unity picker: if no
EPick.Sceneis set, will not show the scene objects. However, omitAssetswill still show the assets. This limitation is from Unity's API.The custom picker does NOT have this limitation.
-
customPickershow an extra button to use a custom picker. Disable this if you have serious performance issue. -
AllowMultiple: No
using SaintsField;
[SerializeField, FieldType(typeof(SpriteRenderer))]
private GameObject _go;
[SerializeField, FieldType(typeof(FieldTypeExample))]
private ParticleSystem _ps;
Dropdown
A dropdown selector. Supports reference type, sub-menu, separator, and disabled select item.
If you want a searchable dropdown, see AdvancedDropdown.
-
string funcNamecallback function. Must return aDropdownList<T>. -
bool slashAsSub=truetreat/as a sub item.Note: In
IMGUI, this just replace/to unicode\u2215Division Slash ∕, and WILL have a little bit overlap with nearby characters. -
AllowMultiple: No
Example
using SaintsField;
[Dropdown(nameof(GetDropdownItems))] public float _float;
public GameObject _go1;
public GameObject _go2;
[Dropdown(nameof(GetDropdownRefs))] public GameObject _refs;
private DropdownList<float> GetDropdownItems()
{
return new DropdownList<float>
{
{ "1", 1.0f },
{ "2", 2.0f },
{ "3/1", 3.1f },
{ "3/2", 3.2f },
};
}
private DropdownList<GameObject> GetDropdownRefs => new DropdownList<GameObject>
{
{_go1.name, _go1},
{_go2.name, _go2},
{"NULL", null},
};
To control the separator and disabled item
using SaintsField;
[Dropdown(nameof(GetDropdownItems))]
public Color color;
private DropdownList<Color> GetDropdownItems()
{
return new DropdownList<Color>
{
{ "Black", Color.black },
{ "White", Color.white },
DropdownList<Color>.Separator(),
{ "Basic/Red", Color.red, true }, // the third arg means it's disabled
{ "Basic/Green", Color.green },
{ "Basic/Blue", Color.blue },
DropdownList<Color>.Separator("Basic/"),
{ "Basic/Magenta", Color.magenta },
{ "Basic/Cyan", Color.cyan },
};
}
And you can always manually add it:
DropdownList<Color> dropdownList = new DropdownList<Color>();
dropdownList.Add("Black", Color.black); // add an item
dropdownList.Add("White", Color.white, true); // and a disabled item
dropdownList.AddSeparator(); // add a separator
The look in the UI Toolkit with slashAsSub: false:
AdvancedDropdown
A dropdown selector. Supports reference type, sub-menu, separator, search, and disabled select item, plus icon.
Known Issue:
-
IMGUI: Using Unity's
AdvancedDropdown. Unity'sAdvancedDropdownallows to click the disabled item and close the popup, thus you can still click the disable item. This is a BUG from Unity. I managed to "hack" it around to show again the popup when you click the disabled item, but you will see the flick of the popup.This issue is not fixable unless Unity fixes it.
This bug only exists in IMGUI
-
UI Toolkit:
The group indicator uses
ToolbarBreadcrumbs. Sometimes you can see text get wrapped into lines. This is because Unity's UI Toolkit has some layout issue, that it can not has the same layout even with same elements+style+boundary size.This issue is not fixable unless Unity fixes it. This issue might be different on different Unity (UI Toolkit) version.
Arguments
string funcNamecallback function. Must return aAdvancedDropdownList<T>.- (IMGUI)
float itemHeight=-1fheight of each item.< 0means use Unity's default value. This will not change the actual height of item, but to decide the dropdown height. - (IMGUI)
float titleHeight=Defaultheight of the title. This will not change the actual height of title, but to decide the dropdown height. - (IMGUI)
float sepHeight=Defaultheight of separator. This will not change the actual height of title, but to decide the dropdown height. - (IMGUI)
bool useTotalItemCount=falseif true, the dropdown height will be decided using the number of all value item, thus the search result will always fit in the position without scroll. Otherwise, it'll be decided by the max height of every item page. - (IMGUI)
float minHeight=-1fminimum height of the dropdown.< 0means no limit. Otherwise, use this as the dropdown height and ignore all the other auto height config. - AllowMultiple: No
AdvancedDropdownList<T>
-
string displayNameitem name to display -
T valueorIEnumerable<AdvancedDropdownList<T>> children: value means it's a value item. Otherwise it's a group of items, which the values are specified bychildren -
bool disabled = falseif item is disabled -
string icon = nullthe icon for the item.Note: setting an icon for a parent group will result an weird issue on it's sub page's title and block the items. This is not fixable unless Unity decide to fix it.
-
bool isSeparator = falseif item is a separator. You should not use this, butAdvancedDropdownList<T>.Separator()instead
using SaintsField;
[AdvancedDropdown(nameof(AdvDropdown)), BelowRichLabel(nameof(drops), true)] public int drops;
public AdvancedDropdownList<int> AdvDropdown()
{
return new AdvancedDropdownList<int>("Days")
{
// a grouped value
new AdvancedDropdownList<int>("First Half")
{
// with icon
new AdvancedDropdownList<int>("Monday", 1, icon: "eye.png"),
// no icon
new AdvancedDropdownList<int>("Tuesday", 2),
},
new AdvancedDropdownList<int>("Second Half")
{
new AdvancedDropdownList<int>("Wednesday")
{
new AdvancedDropdownList<int>("Morning", 3, icon: "eye.png"),
new AdvancedDropdownList<int>("Afternoon", 8),
},
new AdvancedDropdownList<int>("Thursday", 4, true, icon: "eye.png"),
},
// direct value
new AdvancedDropdownList<int>("Friday", 5, true),
AdvancedDropdownList<int>.Separator(),
new AdvancedDropdownList<int>("Saturday", 6, icon: "eye.png"),
new AdvancedDropdownList<int>("Sunday", 7, icon: "eye.png"),
};
}
IMGUI
UI Toolkit
There is also a parser to automatically separate items as sub items using /:
using SaintsField;
[AdvancedDropdown(nameof(AdvDropdown))] public int selectIt;
public AdvancedDropdownList<int> AdvDropdown()
{
return new AdvancedDropdownList<int>("Days")
{
{"First Half/Monday", 1, false, "star.png"}, // enabled, with icon
{"First Half/Tuesday", 2},
{"Second Half/Wednesday/Morning", 3, false, "star.png"},
{"Second Half/Wednesday/Afternoon", 4},
{"Second Half/Thursday", 5, true, "star.png"}, // disabled, with icon
"", // root separator
{"Friday", 6, true}, // disabled
"",
{"Weekend/Saturday", 7, false, "star.png"},
"Weekend/", // separator under `Weekend` group
{"Weekend/Sunday", 8, false, "star.png"},
};
}
You can use this to make a searchable dropdown:
using SaintsField;
[AdvancedDropdown(nameof(AdvDropdownNoNest))] public int searchableDropdown;
public AdvancedDropdownList<int> AdvDropdownNoNest()
{
return new AdvancedDropdownList<int>("Days")
{
{"Monday", 1},
{"Tuesday", 2, true}, // disabled
{"Wednesday", 3, false, "star.png"}, // enabled with icon
{"Thursday", 4, true, "star.png"}, // disabled with icon
{"Friday", 5},
"", // separator
{"Saturday", 6},
{"Sunday", 7},
};
}
PropRange
Very like Unity's Range but allow you to dynamically change the range, plus allow to set range step.
For each argument:
string minCallbackorfloat min: the minimum value of the slider, or a property/callback name.string maxCallbackorfloat max: the maximum value of the slider, or a property/callback name.float step=-1f: the step for the range.<= 0means no limit.
using SaintsField;
public int min;
public int max;
[PropRange(nameof(min), nameof(max))] public float rangeFloat;
[PropRange(nameof(min), nameof(max))] public int rangeInt;
[PropRange(nameof(min), nameof(max), step: 0.5f)] public float rangeFloatStep;
[PropRange(nameof(min), nameof(max), step: 2)] public int rangeIntStep;
MinMaxSlider
A range slider for Vector2 or Vector2Int
For each argument:
-
int|float minorstring minCallback: the minimum value of the slider, or a property/callback name. -
int|float maxorstring maxCallback: the maximum value of the slider, or a property/callback name. -
int|float step=1|-1f: the step of the slider,<= 0means no limit. By default, int type use1and float type use-1f -
float minWidth=50f: (IMGUI Only) the minimum width of the value label.< 0for auto size (not recommended) -
float maxWidth=50f: (IMGUI Only) the maximum width of the value label.< 0for auto size (not recommended) -
AllowMultiple: No
a full-featured example:
using SaintsField;
[MinMaxSlider(-1f, 3f, 0.3f)]
public Vector2 vector2Step03;
[MinMaxSlider(0, 20, 3)]
public Vector2Int vector2IntStep3;
[MinMaxSlider(-1f, 3f)]
public Vector2 vector2Free;
[MinMaxSlider(0, 20)]
public Vector2Int vector2IntFree;
// not recommended
[SerializeField]
[MinMaxSlider(0, 100, minWidth:-1, maxWidth:-1)]
private Vector2Int _autoWidth;
[field: SerializeField, MinMaxSlider(-100f, 100f)]
public Vector2 OuterRange { get; private set; }
[SerializeField, MinMaxSlider(nameof(GetOuterMin), nameof(GetOuterMax), 1)] public Vector2Int _innerRange;
private float GetOuterMin() => OuterRange.x;
private float GetOuterMax() => OuterRange.y;
[field: SerializeField]
public float DynamicMin { get; private set; }
[field: SerializeField]
public float DynamicMax { get; private set; }
[SerializeField, MinMaxSlider(nameof(DynamicMin), nameof(DynamicMax))] private Vector2 _propRange;
[SerializeField, MinMaxSlider(nameof(DynamicMin), 100f)] private Vector2 _propLeftRange;
[SerializeField, MinMaxSlider(-100f, nameof(DynamicMax))] private Vector2 _propRightRange;
EnumFlags
A toggle buttons group for enum flags (bit mask). It provides a button to toggle all bits on/off.
This field has compact mode and expanded mode.
For each argument:
bool autoExpand=true: if the view is not enough to show all buttons in a row, automatically expand to a vertical group.bool defaultExpanded=false: if true, the buttons group will be expanded as a vertical group by default.- AllowMultiple: No
Known Issue:
- IMGUI: If you have a lot of flags and you turn OFF
autoExpand, The buttons WILL go off-view. - UI Toolkit: when
autoExpand=true,defaultExpandedwill be ignored
using SaintsField;
[Serializable, Flags]
public enum BitMask
{
None = 0, // this will be hide as we will have an all/none button
Mask1 = 1,
Mask2 = 1 << 1,
Mask3 = 1 << 2,
}
[EnumFlags] public BitMask myMask;
ResizableTextArea
This TextArea will always grow its height to fit the content. (minimal height is 3 rows).
Note: Unlike NaughtyAttributes, this does not have a text-wrap issue.
- AllowMultiple: No
using SaintsField;
[SerializeField, ResizableTextArea] private string _short;
[SerializeField, ResizableTextArea] private string _long;
[SerializeField, RichLabel(null), ResizableTextArea] private string _noLabel;
AnimatorParam
A dropdown selector for an animator parameter.
-
string animatorName=nullname of the animator. When omitted, it will try to get the animator from the current component
-
(Optional)
AnimatorControllerParameterType animatorParamTypetype of the parameter to filter
using SaintsField;
[field: SerializeField]
public Animator Animator { get; private set;}
[AnimatorParam(nameof(Animator))]
private string animParamName;
[AnimatorParam(nameof(Animator))]
private int animParamHash;
AnimatorState
A dropdown selector for animator state.
-
string animatorName=nullname of the animator. When omitted, it will try to get the animator from the current component
to get more useful info from the state, you can use AnimatorStateBase/AnimatorState type instead of string type.
AnimatorStateBase has the following properties:
int layerIndexindex of layerint stateNameHashhash value of statestring stateNameactual state namefloat stateSpeedtheSpeedparameter of the statestring stateTagtheTagof the statestring[] subStateMachineNameChainthe sub-state machine hierarchy name list of the state
AnimatorState added the following attribute(s):
AnimationClip animationClipis the actual animation clip of the state (can be null). It has alengthvalue for the length of the clip. For more detail see Unity Doc of AnimationClip
Special Note: using AniamtorState/AnimatorStateBase with OnValueChanged, you can get a AnimatorStateChanged on the callback (rather than the value of the field).
This is because AnimatorState expected any class/struct with satisfied fields.
using SaintsField;
[AnimatorState, OnValueChanged(nameof(OnChanged))]
public string stateName;
#if UNITY_EDITOR
[AnimatorState, OnValueChanged(nameof(OnChangedState))]
#endif
public AnimatorState state;
// This does not have a `animationClip`, thus it won't include a resource when serialized: only pure data.
[AnimatorState, OnValueChanged(nameof(OnChangedState))]
public AnimatorStateBase stateBase;
private void OnChanged(string changedValue) => Debug.Log(changedValue);
#if UNITY_EDITOR
private void OnChangedState(AnimatorStateChanged changedValue) => Debug.Log($"layerIndex={changedValue.layerIndex}, AnimatorControllerLayer={changedValue.layer}, AnimatorState={changedValue.state}, animationClip={changedValue.animationClip}, subStateMachineNameChain={string.Join("/", changedValue.subStateMachineNameChain)}");
#endif
Layer
A dropdown selector for layer.
- AllowMultiple: No
Note: want a bitmask layer selector? Unity already has it. Just use public LayerMask myLayerMask;
using SaintsField;
[Layer] public string layerString;
[Layer] public int layerInt;
// Unity supports multiple layer selector
public LayerMask myLayerMask;
Scene
A dropdown selector for a scene in the build list, plus a "Edit Scenes In Build..." option to directly open the "Build Settings" window where you can change building scenes.
- AllowMultiple: No
using SaintsField;
[Scene] public int _sceneInt;
[Scene] public string _sceneString;
SortingLayer
A dropdown selector for sorting layer, plus a "Edit Sorting Layers..." option to directly open "Sorting Layers" tab from "Tags & Layers" inspector where you can change sorting layers.
- AllowMultiple: No
using SaintsField;
[SortingLayer] public string _sortingLayerString;
[SortingLayer] public int _sortingLayerInt;
Tag
A dropdown selector for a tag.
- AllowMultiple: No
using SaintsField;
[Tag] public string tag;
InputAxis
A string dropdown selector for an input axis, plus a "Open Input Manager..." option to directly open "Input Manager" tab from "Project Settings" window where you can change input axes.
- AllowMultiple: No
using SaintsField;
[InputAxis] public string inputAxis;
LeftToggle
A toggle button on the left of the bool field. Only works on boolean field.
IMGUI: To use with RichLabel, you need to add 6 spaces ahead as a hack
using SaintsField;
[LeftToggle] public bool myToggle;
[LeftToggle, RichLabel(" <color=green><label />")] public bool richToggle;
CurveRange
A curve drawer for AnimationCurve which allow to set bounds and color
Override 1:
Vector2 min = Vector2.zerobottom left for boundsVector2 max = Vector2.onetop right for boundsEColor color = EColor.Greencurve line color
Override 2:
float minX = 0fbottom left x for boundsfloat minY = 0fbottom left y for boundsfloat maxX = 1ftop right x for boundsfloat maxY = 1ftop right y for boundsEColor color = EColor.Greencurve line color
using SaintsField;
[CurveRange(-1, -1, 1, 1)]
public AnimationCurve curve;
[CurveRange(EColor.Orange)]
public AnimationCurve curve1;
[CurveRange(0, 0, 5, 5, EColor.Red)]
public AnimationCurve curve2;
ProgressBar
A progress bar for float or int field. This behaves like a slider but more fancy.
Note: Unlike NaughtyAttributes (which is read-only), this is interactable.
Parameters:
-
(Optional)
float minValue=0|string minCallback=null: minimum value of the slider -
float maxValue=100|string maxCallback=null: maximum value of the slider -
float step=-1: the growth step of the slider,<= 0means no limit. -
EColor color=EColor.OceanicSlate: filler color -
EColor backgroundColor=EColor.CharcoalGray: background color -
string colorCallback=null: a callback or property name for the filler color. The function must return aEColor,Color, a name ofEColor/Color, or a hex color string (starts with#). This will overridecolorparameter. -
string backgroundColorCallback=null: a callback or property name for the background color. -
string titleCallback=null: a callback for displaying the title. The function signature is:string TitleCallback(float curValue, float min, float max, string label);rich text is not supported here
using SaintsField;
[ProgressBar(10)] public int myHp;
// control step for float rather than free value
[ProgressBar(0, 100f, step: 0.05f, color: EColor.Blue)] public float myMp;
[Space]
public int minValue;
public int maxValue;
[ProgressBar(nameof(minValue)
, nameof(maxValue) // dynamic min/max
, step: 0.05f
, backgroundColorCallback: nameof(BackgroundColor) // dynamic background color
, colorCallback: nameof(FillColor) // dynamic fill color
, titleCallback: nameof(Title) // dynamic title, does not support rich label
),
]
[RichLabel(null)] // make this full width
public float fValue;
private EColor BackgroundColor() => fValue <= 0? EColor.Brown: EColor.CharcoalGray;
private Color FillColor() => Color.Lerp(Color.yellow, EColor.Green.GetColor(), Mathf.Pow(Mathf.InverseLerp(minValue, maxValue, fValue), 2));
private string Title(float curValue, float min, float max, string label) => curValue < 0 ? $"[{label}] Game Over: {curValue}" : $"[{label}] {curValue / max:P}";
ResourcePath
A tool to pick an resource path (a string) with:
- required types or interfaces
- display a type instead of showing a string
- pick a suitable object using a custom picker
Parameters:
-
EStr eStr = EStr.Resource: which kind of string value you expected:Resource: a resource pathAssetDatabase: an asset path. You should NOT use this unless you know what you are doing.Guid: the GUID of the target object. You should NOT use this unless you know what you are doing.
-
bool freeSign=false:trueto allow to sign any object, and gives a message if the signed value does not match.falseto only allow to sign matched object, and trying to prevent the change if it's illegal. -
bool customPicker=true: use a custom object pick that only display objects which meet the requirements -
Type compType: the type of the component. It can be a component, or an object likeGameObject,Sprite. The field will be this type. It can NOT be an interface -
params Type[] requiredTypes: a list of required components or interfaces you want. Only objects with all of the types can be signed. -
AllowMultiple: No
Known Issue: IMGUI, manually sign a null object by using Unity's default pick will sign an empty string instead of null. Use custom pick to avoid this inconsistency.
using SaintsField;
// resource: display as a MonoScript, requires a BoxCollider
[ResourcePath(typeof(Dummy), typeof(BoxCollider))]
[InfoBox(nameof(myResource), true)]
public string myResource;
// AssetDatabase path
[Space]
[ResourcePath(EStr.AssetDatabase, typeof(Dummy), typeof(BoxCollider))]
[InfoBox(nameof(myAssetPath), true)]
public string myAssetPath;
// GUID
[Space]
[ResourcePath(EStr.Guid, typeof(Dummy), typeof(BoxCollider))]
[InfoBox(nameof(myGuid), true)]
public string myGuid;
// prefab resource
[ResourcePath(typeof(GameObject))]
[InfoBox(nameof(resourceNoRequire), true)]
public string resourceNoRequire;
// requires to have a Dummy script attached, and has interface IMyInterface
[ResourcePath(typeof(Dummy), typeof(IMyInterface))]
[InfoBox(nameof(myInterface), true)]
public string myInterface;
Field Utilities
AssetPreview
Show an image preview for prefabs, Sprite, Texture2D, etc. (Internally use AssetPreview.GetAssetPreview)
Note: Sometimes AssetPreview.GetAssetPreview simply does not return a correct preview image or returns an empty image. When no image returns, nothing is shown. If an empty image returns, an empty rect is shown.
This can not be fixed unless Unity decides to fix it.
Note: Recommended to use AboveImage/BelowImage for image/sprite/texture2D.
-
int width=-1preview width, -1 for original image size that returned by Unity. If it's greater than current view width, it'll be scaled down to fit the view. Use
int.MaxValueto always fit the view width. -
int height=-1preview height, -1 for auto resize (with the same aspect) using the width
-
EAlign align=EAlign.EndAlign of the preview image. Options are
Start,End,Center,FieldStart -
bool above=falseif true, render above the field instead of below
-
string groupBy=""See the
GroupBysection -
AllowMultiple: No
using SaintsField;
[AssetPreview(20, 100)] public Texture2D _texture2D;
[AssetPreview(50)] public GameObject _go;
[AssetPreview(above: true)] public Sprite _sprite;
AboveImage/BelowImage
Show an image above/below the field.
-
string image = nullAn image to display. This can be a property or a callback, which returns a
Sprite,Texture2D,SpriteRenderer,UI.Image,UI.RawImageorUI.Button.If it's null, it'll try to get the image from the field itself.
-
string maxWidth=-1preview max width, -1 for original image size. If it's greater than current view width, it'll be scaled down to fit the view. . Use
int.MaxValueto always fit the view width. -
int maxHeight=-1preview max height, -1 for auto resize (with the same aspect) using the width
-
EAlign align=EAlign.StartAlign of the preview image. Options are
Start,End,Center,FieldStart -
string groupBy=""See the
GroupBysection -
AllowMultiple: No
using SaintsField;
[AboveImage(nameof(spriteField))]
// size and group
[BelowImage(nameof(spriteField), maxWidth: 25, groupBy: "Below1")]
[BelowImage(nameof(spriteField), maxHeight: 20, align: EAlign.End, groupBy: "Below1")]
public Sprite spriteField;
// align
[BelowImage(nameof(spriteField), maxWidth: 20, align: EAlign.FieldStart)]
[BelowImage(nameof(spriteField), maxWidth: 20, align: EAlign.Start)]
[BelowImage(nameof(spriteField), maxWidth: 20, align: EAlign.Center)]
[BelowImage(nameof(spriteField), maxWidth: 20, align: EAlign.End)]
public string alignField;
OnValueChanged
Call a function every time the field value is changed
-
string callbackthe callback function nameIt'll try to pass the new value and the index (only if it's in an array/list). You can set the corresponding parameter in your callback if you want to receive them.
-
AllowMultiple: Yes
Special Note: AnimatorState will have a different OnValueChanged parameter passed in. See AnimatorState for more detail.
using SaintsField;
// no params
[OnValueChanged(nameof(Changed))]
public int value;
private void Changed()
{
Debug.Log($"changed={value}");
}
// with params to get the new value
[OnValueChanged(nameof(ChangedAnyType))]
public GameObject go;
// it will pass the index too if it's inside an array/list
[OnValueChanged(nameof(ChangedAnyType))]
public SpriteRenderer[] srs;
// it's ok to set it as the super class
private void ChangedAnyType(object anyObj, int index=-1)
{
Debug.Log($"changed={anyObj}@{index}");
}
ReadOnly/DisableIf/EnableIf
A tool to set field enable/disable status. Supports callbacks (function/field/property) and enum types. by using multiple arguments and decorators, you can make logic operation with it.
ReadOnly equals DisableIf, EnableIf is the opposite of DisableIf
Arguments:
For callback (functions, fields, properties):
-
(Optional)
EMode editorMode=EMode.Edit | EMode.PlayCondition: if it should be in edit mode or play mode for Editor. By default (omitting this parameter) it does not check the mode at all.
-
string by...callbacks or attributes for the condition.
-
AllowMultiple: Yes
For ReadOnly/DisableIf: The field will be disabled if ALL condition is true (and operation)
For EnableIf: The field will be enabled if ANY condition is true (or operation)
For multiple attributes: The field will be disabled if ANY condition is true (or operation)
Logic example:
EnableIf(A)==DisableIf(!A)EnableIf(A, B)==EnableIf(A || B)==DisableIf(!(A || B))==DisableIf(!A && !B)[EnableIf(A), EnableIf(B)]==[DisableIf(!A), DisableIf(!B)]==DisableIf(!A || !B)
A simple example:
using SaintsField;
[ReadOnly(nameof(ShouldBeDisabled))] public string disableMe;
private bool ShouldBeDisabled // change the logic here
{
return true;
}
It also support enum types. The syntax is like this:
using SaintsField;
[Serializable]
public enum EnumToggle
{
Off,
On,
}
public EnumToggle enum1;
[ReadOnly(nameof(enum1), EnumToggle.On)] public string enumReadOnly;
The rule is:
-
First optional parameter is
EMode editorMode=EMode.Edit | EMode.Play. Omit to ignore the mode check. -
Then, followed by the name of the field/property/function, and the expected
enumvalue, as a pair. You can at most write 4 pairs. -
It's true if the value of the field/property/function is equal to the expected value. If
enumhas[Flags]attribute, it will check if the value has the expected bit on. -
It's also OK to mix the normal callback with enum callback check, just ensure:
- the normal callbacks are always before the enum callbacks
- at most 4 callbacks (normal + enum pair) in total is allowed
using SaintsField;
[Serializable]
public enum EnumToggle
{
Off,
On,
}
public EnumToggle enum1;
public EnumToggle enum2;
public bool bool1;
public bool bool2 {
return true;
}
// example of checking two normal callbacks and two enum callbacks
[EnableIf(nameof(bool1), nameof(bool2), nameof(enum1), EnumToggle.On, nameof(enum2), EnumToggle.On)] public string bool12AndEnum12;
A more complex example:
using SaintsField;
[ReadOnly] public string directlyReadOnly;
[SerializeField] private bool _bool1;
[SerializeField] private bool _bool2;
[SerializeField] private bool _bool3;
[SerializeField] private bool _bool4;
[SerializeField]
[ReadOnly(nameof(_bool1))]
[ReadOnly(nameof(_bool2))]
[RichLabel("readonly=1||2")]
private string _ro1and2;
[SerializeField]
[ReadOnly(nameof(_bool1), nameof(_bool2))]
[RichLabel("readonly=1&&2")]
private string _ro1or2;
[SerializeField]
[ReadOnly(nameof(_bool1), nameof(_bool2))]
[ReadOnly(nameof(_bool3), nameof(_bool4))]
[RichLabel("readonly=(1&&2)||(3&&4)")]
private string _ro1234;
EMode example:
using SaintsField;
public bool boolVal;
[DisableIf(EMode.Edit)] public string disEditMode;
[DisableIf(EMode.Play)] public string disPlayMode;
[DisableIf(EMode.Edit, nameof(boolVal))] public string disEditAndBool;
[DisableIf(EMode.Edit), DisableIf(nameof(boolVal))] public string disEditOrBool;
[EnableIf(EMode.Edit)] public string enEditMode;
[EnableIf(EMode.Play)] public string enPlayMode;
[EnableIf(EMode.Edit, nameof(boolVal))] public string enEditOrBool;
// dis=!editor || dis=!bool => en=editor&&bool
[EnableIf(EMode.Edit), EnableIf(nameof(boolVal))] public string enEditAndBool;
ShowIf / HideIf
Show or hide the field based on a condition. . Supports callbacks (function/field/property) and enum types. by using multiple arguments and decorators, you can make logic operation with it.
Arguments:
-
(Optional)
EMode editorMode=EMode.Edit | EMode.PlayCondition: if it should be in edit mode or play mode for Editor. By default (omitting this parameter) it does not check the mode at all.
-
string by...callbacks or attributes for the condition.
-
AllowMultiple: Yes
You can use multiple ShowIf, HideIf, and even a mix of the two.
For ShowIf: The field will be shown if ALL condition is true (and operation)
For HideIf: The field will be hidden if ANY condition is true (or operation)
For multiple attributes: The field will be shown if ANY condition is true (or operation)
For example, [ShowIf(A...), ShowIf(B...)] will be shown if ShowIf(A...) || ShowIf(B...) is true.
HideIf is the opposite of ShowIf. Please note "the opposite" is like the logic operation, like !(A && B) is !A || !B, !(A || B) is !A && !B.
HideIf(A)==ShowIf(!A)HideIf(A, B)==HideIf(A || B)==ShowIf(!(A || B))==ShowIf(!A && !B)[Hideif(A), HideIf(B)]==[ShowIf(!A), ShowIf(!B)]==ShowIf(!A || !B)
A simple example:
using SaintsField;
[ShowIf(nameof(ShouldShow))]
public int showMe;
public bool ShouldShow() // change the logic here
{
return true;
}
It also support enum types. The syntax is like this:
using SaintsField;
[Serializable]
public enum EnumToggle
{
Off,
On,
}
public EnumToggle enum1;
[ShowIf(nameof(enum1), EnumToggle.On)] public string enum1Show;
The rule is:
-
First optional parameter is
EMode editorMode=EMode.Edit | EMode.Play. Omit to ignore the mode check. -
Then, followed by the name of the field/property/function, and the expected
enumvalue, as a pair. You can at most write 4 pairs. -
It's true if the value of the field/property/function is equal to the expected value. If
enumhas[Flags]attribute, it will check if the value has the expected bit on. -
It's also OK to mix the normal callback with enum callback check, just ensure:
- the normal callbacks are always before the enum callbacks
- at most 4 callbacks (normal + enum pair) in total is allowed
using SaintsField;
[Serializable]
public enum EnumToggle
{
Off,
On,
}
public EnumToggle enum1;
public EnumToggle enum2;
public bool bool1;
public bool bool2 {
return true;
}
// example of checking two normal callbacks and two enum callbacks
[ShowIf(nameof(bool1), nameof(bool2), nameof(enum1), EnumToggle.On, nameof(enum2), EnumToggle.On)] public string bool12AndEnum12;
A more complex example:
using SaintsField;
public bool _bool1;
public bool _bool2;
public bool _bool3;
public bool _bool4;
[ShowIf(nameof(_bool1))]
[ShowIf(nameof(_bool2))]
[RichLabel("<color=red>show=1||2")]
public string _showIf1Or2;
[ShowIf(nameof(_bool1), nameof(_bool2))]
[RichLabel("<color=green>show=1&&2")]
public string _showIf1And2;
[HideIf(nameof(_bool1))]
[HideIf(nameof(_bool2))]
[RichLabel("<color=blue>show=!1||!2")]
public string _hideIf1Or2;
[HideIf(nameof(_bool1), nameof(_bool2))]
[RichLabel("<color=yellow>show=!(1||2)=!1&&!2")]
public string _hideIf1And2;
[ShowIf(nameof(_bool1))]
[HideIf(nameof(_bool2))]
[RichLabel("<color=magenta>show=1||!2")]
public string _showIf1OrNot2;
[ShowIf(nameof(_bool1), nameof(_bool2))]
[ShowIf(nameof(_bool3), nameof(_bool4))]
[RichLabel("<color=orange>show=(1&&2)||(3&&4)")]
public string _showIf1234;
[HideIf(nameof(_bool1), nameof(_bool2))]
[HideIf(nameof(_bool3), nameof(_bool4))]
[RichLabel("<color=pink>show=!(1||2)||!(3||4)=(!1&&!2)||(!3&&!4)")]
public string _hideIf1234;
Example about EMode:
using SaintsField;
public bool boolValue;
[ShowIf(EMode.Edit)] public string showEdit;
[ShowIf(EMode.Play)] public string showPlay;
[ShowIf(EMode.Edit, nameof(boolValue))] public string showEditAndBool;
[ShowIf(EMode.Edit), ShowIf(nameof(boolValue))] public string showEditOrBool;
[HideIf(EMode.Edit)] public string hideEdit;
[HideIf(EMode.Play)] public string hidePlay;
[HideIf(EMode.Edit, nameof(boolValue))] public string hideEditOrBool;
[HideIf(EMode.Edit), HideIf(nameof(boolValue))] public string hideEditAndBool;
Required
Reminding a given reference type field to be required.
This will check if the field value is a truly value, which means:
ValuedTypelikestructwill always betrulybecausestructis not nullable and Unity will fill a default value for it no matter what- It works on reference type and will NOT skip Unity's life-circle null check
- You may not want to use it on
int,float(because only0is nottruly) orbool, but it's still allowed if you insist
Parameters:
string errorMessage = nullError message. Default is{label} is required- AllowMultiple: No
using SaintsField;
[Required("Add this please!")] public Sprite _spriteImage;
// works for the property field
[field: SerializeField, Required] public GameObject Go { get; private set; }
[Required] public UnityEngine.Object _object;
[SerializeField, Required] private float _wontWork;
[Serializable]
public struct MyStruct
{
public int theInt;
}
[Required]
public MyStruct myStruct;
ValidateInput
Validate the input of the field when the value changes.
-
string callbackis the callback function to validate the data.Parameters:
- If the function accepts no arguments, then no argument will be passed
- If the function accepts required arguments, the first required argument will receive the field's value. If there is another required argument and the field is inside a list/array, the index will be passed.
- If the function only has optional arguments, it will try to pass the field's value and index if possible. Otherwise the default value of the parameter will be passed.
Return:
- If return type is
string, thennullor empty string for valid, otherwise, the string will be used as the error message - If return type is
bool, thentruefor valid,falsefor invalid with message "`{label}` is invalid`"
-
AllowMultiple: Yes
using SaintsField;
// string callback
[ValidateInput(nameof(OnValidateInput))]
public int _value;
private string OnValidateInput() => _value < 0 ? $"Should be positive, but gets {_value}" : null;
// property validate
[ValidateInput(nameof(boolValidate))]
public bool boolValidate;
// bool callback
[ValidateInput(nameof(BoolCallbackValidate))]
public string boolCallbackValidate;
private bool BoolCallbackValidate() => boolValidate;
// with callback params
[ValidateInput(nameof(ValidateWithReqParams))]
public int withReqParams;
private string ValidateWithReqParams(int v) => $"ValidateWithReqParams: {v}";
// with optional callback params
[ValidateInput(nameof(ValidateWithOptParams))]
public int withOptionalParams;
private string ValidateWithOptParams(string sth="a", int v=0) => $"ValidateWithOptionalParams[{sth}]: {v}";
// with array index callback
[ValidateInput(nameof(ValidateValArr))]
public int[] valArr;
private string ValidateValArr(int v, int index) => $"ValidateValArr[{index}]: {v}";
MinValue / MaxValue
Limit for int/float field
They have the same overrides:
-
float value: directly limit to a number value -
string valueCallback: a callback or property for limit -
AllowMultiple: Yes
using SaintsField;
public int upLimit;
[MinValue(0), MaxValue(nameof(upLimit))] public int min0Max;
[MinValue(nameof(upLimit)), MaxValue(10)] public float fMinMax10;
GetComponent
Automatically sign a component to a field, if the field value is null and the component is already attached to current target. (First one found will be used)
-
Type compType = nullThe component type to sign. If null, it'll use the field type.
-
string groupBy = ""For error message grouping.
-
AllowMultiple: No
using SaintsField;
[GetComponent] public BoxCollider otherComponent;
[GetComponent] public GameObject selfGameObject; // get the GameObject itself
[GetComponent] public RectTransform selfRectTransform; // useful for UI
[GetComponent] public GetComponentExample selfScript; // yeah you can get your script itself
[GetComponent] public Dummy otherScript; // other script
GetComponentInChildren
Automatically sign a component to a field, if the field value is null and the component is already attached to its child GameObjects. (First one found will be used)
NOTE: Unlike GetComponentInChildren by Unity, this will NOT check the target object itself.
-
bool includeInactive = falseShould inactive children be included?
trueto include inactive children. -
Type compType = nullThe component type to sign. If null, it'll use the field type.
-
string groupBy = ""For error message grouping.
-
AllowMultiple: No
using SaintsField;
[GetComponentInChildren] public BoxCollider childBoxCollider;
// by setting compType, you can sign it as a different type
[GetComponentInChildren(compType: typeof(Dummy))] public BoxCollider childAnotherType;
// and GameObject field works too
[GetComponentInChildren(compType: typeof(BoxCollider))] public GameObject childBoxColliderGo;
FindComponent
Automatically find a component under the current target. This is very similar to Unity's transform.Find, except it accepts many paths, and it's returning value is not limited to transform
string patha path to searchparams string[] pathsmore paths to search- AllowMultiple: Yes but not necessary
using SaintsField;
[FindComponent("sub/dummy")] public Dummy subDummy;
[FindComponent("sub/dummy")] public GameObject subDummyGo;
[FindComponent("sub/noSuch", "sub/dummy")] public Transform subDummyTrans;
GetComponentInParent / GetComponentInParents
Automatically sign a component to a field, if the field value is null and the component is already attached to its parent GameObject(s). (First one found will be used)
-
Type compType = nullThe component type to sign. If null, it'll use the field type.
-
string groupBy = ""For error message grouping.
-
AllowMultiple: No
using SaintsField;
[GetComponentInParent] public SpriteRenderer directParent;
[GetComponentInParent(typeof(SpriteRenderer))] public GameObject directParentDifferentType;
[GetComponentInParent] public BoxCollider directNoSuch;
[GetComponentInParents] public SpriteRenderer searchParent;
[GetComponentInParents(typeof(SpriteRenderer))] public GameObject searchParentDifferentType;
[GetComponentInParents] public BoxCollider searchNoSuch;
GetComponentInScene
Automatically sign a component to a field, if the field value is null and the component is in the currently opened scene. (First one found will be used)
-
bool includeInactive = falseShould inactive GameObject be included?
trueto include inactive GameObject. -
Type compType = nullThe component type to sign. If null, it'll use the field type.
-
string groupBy = ""For error message grouping.
-
AllowMultiple: No
using SaintsField;
[GetComponentInScene] public Dummy dummy;
// by setting compType, you can sign it as a different type
[GetComponentInScene(compType: typeof(Dummy))] public RectTransform dummyTrans;
// and GameObject field works too
[GetComponentInScene(compType: typeof(Dummy))] public GameObject dummyGo;
GetComponentByPath
Automatically sign a component to a field by a given path.
-
(Optional)
EGetComp configOptions are:
EGetComp.ForceResign: when the target changed (e.g. you delete/create one), automatically resign the new correct component.EGetComp.NoResignButton: do not display a resign button when the target mismatches.
-
string paths...Paths to search.
-
AllowMultiple: Yes. But not necessary.
The path is a bit like html's XPath but with less functions:
| Path | Meaning |
|---|---|
/ |
Separator. Using at start means the root of the current scene. |
// |
Separator. Any descendant children |
. |
Node. Current node |
.. |
Node. Parent node |
* |
All nodes |
| name | Node. Any nodes with this name |
[last()] |
Index Filter. Last of results |
[index() > 1] |
Index Filter. Node index that is greater than 1 |
[0] |
Index Filter. First node in the results |
For example:
./sthorsth: direct child object of current object namedsth.//sth: any descendant child under current. (descendant::sth)..//sth: first go to parent, then find the direct child namedsth/sth: top level node in current scene namedsth//sth: first go to top level, then find the direct child namedsth///sth: first go to top level, then find any node namedsth./get/sth[1]: the child namedgetof current node, then the second node namedsthin the direct children list ofget
using SaintsField;
// starting from root, search any object with name "Dummy"
[GetComponentByPath("///Dummy")] public GameObject dummy;
// first child of current object
[GetComponentByPath("./*[1]")] public GameObject direct1;
// child of current object which has index greater than 1
[GetComponentByPath("./*[index() > 1]")] public GameObject directPosTg1;
// last child of current object
[GetComponentByPath("./*[last()]")] public GameObject directLast;
// re-sign the target if mis-match
[GetComponentByPath(EGetComp.NoResignButton | EGetComp.ForceResign, "./DirectSub")] public GameObject directSubWatched;
// without "ForceResign", it'll display a reload button if mis-match
// with multiple paths, it'll search from left to right
[GetComponentByPath("/no", "./DirectSub1")] public GameObject directSubMulti;
// if no match, it'll show an error message
[GetComponentByPath("/no", "///sth/else/../what/.//ever[last()]/goes/here")] public GameObject notExists;
GetPrefabWithComponent
Automatically sign a prefab to a field, if the field value is null and the prefab has the component. (First one found will be used)
Recommended to use it with FieldType!
-
Type compType = nullThe component type to sign. If null, it'll use the field type.
-
string groupBy = ""For error message grouping.
-
AllowMultiple: No
using SaintsField;
[GetPrefabWithComponent] public Dummy dummy;
// get the prefab itself
[GetPrefabWithComponent(compType: typeof(Dummy))] public GameObject dummyPrefab;
// works so good with `FieldType`
[GetPrefabWithComponent(compType: typeof(Dummy)), FieldType(typeof(Dummy))] public GameObject dummyPrefabFieldType;
GetScriptableObject
Automatically sign a ScriptableObject file to this field. (First one found will be used)
Recommended to use it with Expandable!
string pathSuffix=nullthe path suffix for thisScriptableObject.nullfor no limit. for example: if it's/Resources/mySo, it will only sign the file whose path is ends with/Resources/mySo.asset, likeAssets/proj/Resources/mySo.asset- AllowMultiple: No
using SaintsField;
[GetScriptableObject] public Scriptable mySo;
[GetScriptableObject("RawResources/ScriptableIns")] public Scriptable mySoSuffix;
AddComponent
Automatically add a component to the current target if the target does not have this component. (This will not sign the component added)
Recommended to use it with GetComponent!
-
Type compType = nullThe component type to add. If null, it'll use the field type.
-
string groupBy = ""For error message grouping.
-
AllowMultiple: Yes
using SaintsField;
[AddComponent, GetComponent] public Dummy dummy;
[AddComponent(typeof(BoxCollider)), GetComponent] public GameObject thisObj;
ButtonAddOnClick
Add a callback to a button's onClick event. Note this at this point does only supports callback with no arguments.
Note: SaintsEditor has a more powerful OnButtonClick. If you have SaintsEditor enabled, it's recommended to use OnButtonClick instead.
-
string funcNamethe callback function name -
string buttonComp=nullthe button component name.If null, it'll try to get the button component by this order:
- the field itself
- get the
Buttoncomponent from the field itself - get the
Buttoncomponent from the current target
If it's not null, the search order will be:
- get the field of this name from current target
- call a function of this name from current target
using SaintsField;
[GetComponent, ButtonAddOnClick(nameof(OnClick))] public Button button;
private void OnClick()
{
Debug.Log("Button clicked!");
}
RequireType
Allow you to specify the required component(s) or interface(s) for a field.
If the signed field does not meet the requirement, it'll:
- show an error message, if
freeSign=false - prevent the change, if
freeSign=true
customPicker will allow you to pick an object which are already meet the requirement(s).
Overload:
RequireTypeAttribute(bool freeSign = false, bool customPicker = true, params Type[] requiredTypes)RequireTypeAttribute(bool freeSign, params Type[] requiredTypes)RequireTypeAttribute(EPick editorPick, params Type[] requiredTypes)RequireTypeAttribute(params Type[] requiredTypes)
For each argument:
-
bool freeSign=falseIf true, it'll allow you to sign any object to this field, and display an error message if it does not meet the requirement(s).
Otherwise, it will try to prevent the change.
-
bool customPicker=trueShow a custom picker to pick an object. The showing objects are already meet the requirement(s).
-
EPick editorPick=EPick.Assets | EPick.SceneThe picker type for the custom picker.
EPick.Assetsfor assets,EPick.Scenefor scene objects. -
params Type[] requiredTypesThe required component(s) or interface(s) for this field.
-
AllowMultiple: No
using SaintsField;
public interface IMyInterface {}
public class MyInter1: MonoBehaviour, IMyInterface {}
public class MySubInter: MyInter1 {}
public class MyInter2: MonoBehaviour, IMyInterface {}
[RequireType(typeof(IMyInterface))] public SpriteRenderer interSr;
[RequireType(typeof(IMyInterface), typeof(SpriteRenderer))] public GameObject interfaceGo;
[RequireType(true, typeof(IMyInterface))] public SpriteRenderer srNoPickerFreeSign;
[RequireType(true, typeof(IMyInterface))] public GameObject goNoPickerFreeSign;
ArraySize
A decorator that limit the size of the array or list.
Note: Because of the limitation of PropertyDrawer:
- When the field is 0 length, it'll not be filled to target size.
- You can always change it to 0 size.
- Delete an element will first be deleted, then the array will duplicated the last element.
- UI Toolkit: you might see the UI flicked when you remove an element.
(If you enable SaintsEditor, there is a PlayaArraySize that does NOT have issue 1 & 2)
Parameters:
int sizethe size of the array or liststring groupBy = ""for error message grouping- AllowMultiple: No
using SaintsField;
[ArraySize(3)]
public string[] myArr;
SaintsRow
SaintsRow attribute allows you to draw Button, Layout, ShowInInspector, DOTweenPlay etc (all SaintsEditor attributes) in a Serializable object (usually a class or a struct).
This attribute does NOT need SaintsEditor enabled. It's an out-of-box tool.
Parameters:
-
bool inline=falseIf true, it'll draw the
Serializableinline like it's directly in theMonoBehavior -
AllowMultiple: No
Special Note:
- After applying this attribute, only pure
PropertyDrawer, and decorators fromSaintsEditorworks on this target. Which means, using third party'sPropertyDraweris fine, but decorator of Editor level (e.g. Odin'sButton, NaughtyAttributes'Button) will not work. - IMGUI:
ELayout.Horizontaldoes not work here - IMGUI:
DOTweenPlaymight be a bit buggy displaying the playing/pause/stop status for each function.
using SaintsField;
using SaintsField.Playa;
[Serializable]
public struct Nest
{
public string nest2Str; // normal field
[Button] // function button
private void Nest2Btn() => Debug.Log("Call Nest2Btn");
// static field (non serializable)
[ShowInInspector] public static Color StaticColor => Color.cyan;
// const field (non serializable)
[ShowInInspector] public const float Pi = 3.14f;
// normal attribute drawer works as expected
[BelowImage(maxWidth: 25)] public SpriteRenderer spriteRenderer;
[DOTweenPlay] // DOTween helper
private Sequence PlayColor()
{
return DOTween.Sequence()
.Append(spriteRenderer.DOColor(Color.red, 1f))
.Append(spriteRenderer.DOColor(Color.green, 1f))
.Append(spriteRenderer.DOColor(Color.blue, 1f))
.SetLoops(-1);
}
[DOTweenPlay("Position")]
private Sequence PlayTween2()
{
return DOTween.Sequence()
.Append(spriteRenderer.transform.DOMove(Vector3.up, 1f))
.Append(spriteRenderer.transform.DOMove(Vector3.right, 1f))
.Append(spriteRenderer.transform.DOMove(Vector3.down, 1f))
.Append(spriteRenderer.transform.DOMove(Vector3.left, 1f))
.Append(spriteRenderer.transform.DOMove(Vector3.zero, 1f))
;
}
}
[SaintsRow]
public Nest n1;
To show a Serializable inline like it's directly in the MonoBehavior:
using SaintsField;
[Serializable]
public struct MyStruct
{
public int structInt;
public bool structBool;
}
[SaintsRow(inline: true)]
public MyStruct myStructInline;
public string normalStringField;
Other Tools
Addressable
These tools are for Unity Addressable. It's there only if you have Addressable installed.
Namespace: SaintsField.Addressable
If you encounter issue because of version incompatible with your installation, you can add a macro SAINTSFIELD_ADDRESSABLE_DISABLE to disable this component (See "Add a Macro" section for more information)
AddressableLabel
A picker to select an addressable label.
- Allow Multiple: No
using SaintsField.Addressable;
[AddressableLabel]
public string addressableLabel;
AddressableAddress
A picker to select an addressable address (key).
-
string group = nullthe Addressable group name.nullfor all groups -
params string[] orLabelsthe addressable label names to filter. Onlyentrieswith this label will be shown.nullfor no filter.If it requires multiple labels, use
A && B, then only entries with both labels will be shown.If it requires any of the labels, just pass them separately, then entries with either label will be shown. For example, pass
"A && B", "C"will show entries with bothAandBlabel, or withClabel. -
Allow Multiple: No
using SaintsField.Addressable;
[AddressableAddress] // from all group
public string address;
[AddressableAddress("Packed Assets")] // from this group
public string addressInGroup;
[AddressableAddress(null, "Label1", "Label2")] // either has label `Label1` or `Label2`
public string addressLabel1Or2;
// must have both label `default` and `Label1`
// or have both label `default` and `Label2`
[AddressableAddress(null, "default && Label1", "default && Label2")]
public string addressLabelAnd;
AI Navigation
These tools are for Unity AI Navigation (NavMesh). It's there only if you have AI Navigation installed.
Namespace: SaintsField.AiNavigation
Adding marco SAINTSFIELD_AI_NAVIGATION_DISABLED to disable this component. (See "Add a Macro" section for more information)
NavMeshAreaMask
Select NavMesh area bit mask for an integer field. (So the integer value can be used in SamplePathPosition)
- Allow Multiple: No
using SaintsField.AiNavigation;
[NavMeshAreaMask]
public int areaMask;
NavMeshArea
Select a NavMesh area for a string or an interger field.
-
bool isMask=trueif true, it'll use the bit mask, otherwise, it'll use the area value. Has no effect if the field is a string. -
string groupBy = ""for error message grouping -
Allow Multiple: No
using SaintsField.AiNavigation;
[NavMeshArea] // then you can use `areaSingleMask1 | areaSingleMask2` to get multiple masks
public int areaSingleMask;
[NavMeshArea(false)] // then you can use `1 << areaValue` to get areaSingleMask
public int areaValue;
[NavMeshArea] // then you can use `NavMesh.GetAreaFromName(areaName)` to get areaValue
public int areaName;
SaintsArray/SaintsList
Unity does not allow to serialize two dimensional array or list. SaintsArray and SaintsList are there to help.
using SaintsField;
// two dimensional array
public SaintsArray<GameObject>[] gameObjects2;
public SaintsArray<SaintsArray<GameObject>> gameObjects2Nest;
// four dimensional array, if you like.
// it can be used with array, but ensure the `[]` is always at the end.
public SaintsArray<SaintsArray<SaintsArray<GameObject>>>[] gameObjects4;
SaintsArray implements IReadOnlyList, SaintsList implements IList:
using SaintsField;
// SaintsArray
GameObject firstGameObject = saintsArrayGo[0];
Debug.Log(saintsArrayGo.value); // the actual array value
// SaintsList
saintsListGo.Add(new GameObject());
saintsListGo.RemoveAt(0);
Debug.Log(saintsListGo.value); // the actual list value
These two can be easily converted to array/list:
using SaintsField;
// SaintsArray to Array
GameObject[] arrayGo = saintsArrayGo;
// Array to SaintsArray
SaintsArray<GameObject> expSaintsArrayGo = (SaintsArray<GameObject>)arrayGo;
// SaintsList to List
List<GameObject> ListGo = saintsListGo;
// List to SaintsList
SaintsList<GameObject> expSaintsListGo = (SaintsList<GameObject>)ListGo;
Because it's actually a struct, you can also implement your own Array/List, using [SaintsArray]. Here is an example of customize your own struct:
using SaintsField;
// example: using ISaintsArray so you don't need to specify the type name everytime
[Serializable]
public class MyList : ISaintsArray
{
[SerializeField] public List<string> myStrings;
#if UNITY_EDITOR
public string EditorArrayPropertyName => nameof(myStrings);
#endif
}
[SaintsArray]
public MyList[] myLis;
// example: any Serializable which hold a serialized array/list is fine
[Serializable]
public struct MyArr
{
[RichLabel(nameof(MyInnerRichLabel), true)]
public int[] myArray;
private string MyInnerRichLabel(object _, int index) => $"<color=pink> Inner [{(char)('A' + index)}]";
}
[RichLabel(nameof(MyOuterLabel), true), SaintsArray("myArray")]
public MyArr[] myArr;
private string MyOuterLabel(object _, int index) => $"<color=Lime> Outer {index}";
SaintsEditor
SaintsField is a UnityEditor.Editor level component.
Namespace: SaintsField.Playa
Compared with NaughtyAttributes and MarkupAttributes:
-
NaughtyAttributeshasButton, and has a way to show a non-field property(ShowNonSerializedField,ShowNativeProperty), but it does not retain the order of these fields, but only draw them at the end. It has layout functions (Foldout,BoxGroup) but it has notTablayout, and much less powerful compared toMarkupAttributes. It's IMGUI only. -
MarkupAttributesis super powerful in layout, but it does not have a way to show a non-field property. It's IMGUI only. It also supports shader editor. -
SaintsEditorLayoutlike markup attributes. Compared toMarkupAttributes, it allows a non-field property (e.g. a button or aShowInInspectorinside a group) (likeOdinInspector). However, it does not have aScopefor convenience coding.- It provides
Button(with less functions) and a way to show a non-field property (ShowInInspector). - It tries to retain the order, and allows you to use
[Ordered]when it can not get the order (c# does not allow to obtain all the orders). - Supports both
UI ToolkitandIMGUI.
Please note, any Editor level component can not work together with each other (it will not cause trouble, but only one will actually work). Which means, OdinInspector, NaughtyAttributes, MarkupAttributes, SaintsEditor can not work together.
If you are interested, here is how to use it.
Set Up SaintsEditor
Window - Saints - Apply SaintsEditor. After the project finish re-compile, go Window - Saints - SaintsEditor to tweak configs.
If you want to do it manually, check ApplySaintsEditor.cs for more information
DOTweenPlay
A method decorator to play a DOTween animation returned by the method.
The method should not have required parameters, and need to return a Tween or a Sequence (Sequence is actually also a tween).
Parameters:
[Optional] string label = nullthe label of the button. Use method name if null. Rich label not supported.ETweenStop stopAction = ETweenStop.Rewindthe action after the tween is finished or killed. Options are:None: do nothingComplete: complete the tween. This only works if the tween get killedRewind: rewind to the start state
using SaintsField.Playa;
using SaintsField;
[GetComponent]
public SpriteRenderer spriteRenderer;
[DOTweenPlay]
private Sequence PlayColor()
{
return DOTween.Sequence()
.Append(spriteRenderer.DOColor(Color.red, 1f))
.Append(spriteRenderer.DOColor(Color.green, 1f))
.Append(spriteRenderer.DOColor(Color.blue, 1f))
.SetLoops(-1); // Yes you can make it a loop
}
[DOTweenPlay("Position")]
private Sequence PlayTween2()
{
return DOTween.Sequence()
.Append(spriteRenderer.transform.DOMove(Vector3.up, 1f))
.Append(spriteRenderer.transform.DOMove(Vector3.right, 1f))
.Append(spriteRenderer.transform.DOMove(Vector3.down, 1f))
.Append(spriteRenderer.transform.DOMove(Vector3.left, 1f))
.Append(spriteRenderer.transform.DOMove(Vector3.zero, 1f))
;
}
The first row is global control. Stop it there will stop all preview.
The check of each row means auto play when you click the start in the global control.
Set Up
To use DOTweenPlay: Tools - Demigaint - DOTween Utility Panel, click Create ASMDEF
Button
Draw a button for a function.
Compared to NaughtyAttributes, this does not allow to specific for editing and playing mode. It also does not handle an IEnumerator function, it just Invoke the target function.
string buttonLabel = nullthe button label. If null, it'll use the function name.
using SaintsField.Playa;
[Button]
private void EditorButton()
{
Debug.Log("EditorButton");
}
[Button("Label")]
private void EditorLabeledButton()
{
Debug.Log("EditorLabeledButton");
}
ShowInInspector
Show a non-field property.
using SaintsField.Playa;
// const
[ShowInInspector, Ordered] public const float MyConstFloat = 3.14f;
// static
[ShowInInspector, Ordered] public static readonly Color MyColor = Color.green;
// auto-property
[ShowInInspector, Ordered]
public Color AutoColor
{
get => Color.green;
set {}
}
Ordered
SaintsEditor uses reflection to get each field. However, c# reflection does not give all the orders: PropertyInfo, MethodInfo and FieldInfo does not order with each other.
Thus, if the order is incorrect, you can use [Ordered] to specify the order. But also note: Ordered ones are always after the ones without an Ordered. So if you want to add it, add it to every field.
using SaintsField.Playa;
[Ordered] public string myStartField;
[ShowInInspector, Ordered] public const float MyConstFloat = 3.14f;
[ShowInInspector, Ordered] public static readonly Color MyColor = Color.green;
[ShowInInspector, Ordered]
public Color AutoColor
{
get => Color.green;
set {}
}
[Button, Ordered]
private void EditorButton()
{
Debug.Log("EditorButton");
}
[Ordered] public string myOtherFieldUnderneath;
Layout
A layout decorator to group fields.
string groupBythe grouping key. Use/to separate different groups and create sub groups.ELayout layout=ELayout.Verticalthe layout of the current group. Note this is aEnumFlag, means you can mix with options.
Options are:
VerticalHorizontalBackgrounddraw a background color for the whole groupTitleOutmaketitlemore visible if you haveTitleenabled. OnIMGUIit will draw an separator between title and the rest of the content. OnUI Toolkitit will draw a background color for the title.Foldoutallow to fold/unfold this group. If you have noTabon, then this will automatically addTitleTabmake this group a tab page separated rather than grouping itTitleshow the title
Known Issue
Horizental style is buggy, for the following reasons:
- On IMGUI,
HorizontalScopedoes NOT shrink when there are many items, and will go off-view without a scrollbar. BothOdinandMarkup-Attributeshave the same issue. However,Markup-AttributeuseslabelWidthto make the situation a bit better, whichSaintsEditordoes not provide (at this point at least). - On UI Toolkit we have the well-behaved layout system, but because Unity will try to align the first label, all the field except the first one will get the super-shrank label width which makes it unreadable.
Appearance
Example
using SaintsField;
using SaintsField.Playa;
[Layout("Titled", ELayout.Title | ELayout.TitleOut)]
public string titledItem1, titledItem2;
// title
[Layout("Titled Box", ELayout.Title | ELayout.Background | ELayout.TitleOut)]
public string titledBoxItem1;
[Layout("Titled Box")] // you can omit config when you already declared one somewhere (no need to be the first one)
public string titledBoxItem2;
// foldout
[Layout("Foldout", ELayout.Foldout)]
public string foldoutItem1, foldoutItem2;
// tabs
[Layout("Tabs", ELayout.Tab | ELayout.Foldout)]
[Layout("Tabs/Tab1")]
public string tab1Item1, tab1Item2;
[Layout("Tabs/Tab2")]
public string tab2Item1, tab2Item2;
[Layout("Tabs/Tab3")]
public string tab3Item1, tab3Item2;
// nested groups
[Layout("Nested", ELayout.Title | ELayout.Background | ELayout.TitleOut)]
public int nestedOne;
[Layout("Nested/Nested Group 1", ELayout.Title | ELayout.TitleOut)]
public int nestedTwo, nestedThree;
[Layout("Nested/Nested Group 2", ELayout.Title | ELayout.TitleOut)]
public int nestedFour, nestedFive;
// Unlabeled Box
[Layout("Unlabeled Box", ELayout.Background)]
public int unlabeledBoxItem1, unlabeledBoxItem2;
// Foldout In A Box
[Layout("Foldout In A Box", ELayout.Foldout | ELayout.Background | ELayout.TitleOut)]
public int foldoutInABoxItem1, foldoutInABoxItem2;
// Complex example. Button and ShowInInspector works too
[Ordered]
[Layout("Root", ELayout.Tab | ELayout.TitleOut | ELayout.Foldout | ELayout.Background)]
[Layout("Root/V1")]
[SepTitle("Basic", EColor.Pink)]
public string hv1Item1;
[Ordered]
[Layout("Root/V1/buttons", ELayout.Horizontal)]
[Button("Root/V1 Button1")]
public void RootV1Button()
{
Debug.Log("Root/V1 Button");
}
[Ordered]
[Layout("Root/V1/buttons", ELayout.Horizontal)]
[Button("Root/V1 Button2")]
public void RootV1Button2()
{
Debug.Log("Root/V1 Button");
}
[Ordered]
[Layout("Root/V1")]
[ShowInInspector]
public static Color color1 = Color.red;
[Ordered]
[DOTweenPlay("Tween1", "Root/V1")]
public Tween RootV1Tween1()
{
return DOTween.Sequence();
}
[Ordered]
[DOTweenPlay("Tween2", "Root/V1")]
public Tween RootV1Tween2()
{
return DOTween.Sequence();
}
[Ordered]
[Layout("Root/V1")]
public string hv1Item2;
// public string below;
[Ordered]
[Layout("Root/V2")]
public string hv2Item1;
[Ordered]
[Layout("Root/V2/H", ELayout.Horizontal), RichLabel(null)]
public string hv2Item2, hv2Item3;
[Ordered]
[Layout("Root/V2")]
public string hv2Item4;
[Ordered]
[Layout("Root/V3", ELayout.Horizontal)]
[ResizableTextArea, RichLabel(null)]
public string hv3Item1, hv3Item2;
[Ordered]
[Layout("Root/Buggy")]
[InfoBox("Sadly, Horizontal is buggy either in UI Toolkit or IMGUI", above: true)]
public string buggy = "See below:";
[Ordered]
[Layout("Root/Buggy/H", ELayout.Horizontal)]
public string buggy1;
[Ordered]
[Layout("Root/Buggy/H", ELayout.Horizontal)]
public string buggy2;
PlayaShowIf/PlayaHideIf
This is the same as ShowIf, HideIf, plus it's allowed to be applied to array, Button, ShowInInspector
Different from ShowIf/HideIf:
- apply on an array will directly show or hide the array itself, rather than each element.
- Callback function can not receive value and index
using SaintsField.Playa;
public bool boolValue;
[PlayaHideIf] public int[] justHide;
[PlayaShowIf] public int[] justShow;
[PlayaHideIf(nameof(boolValue))] public int[] hideIf;
[PlayaShowIf(nameof(boolValue))] public int[] showIf;
[PlayaHideIf(EMode.Edit)] public int[] hideEdit;
[PlayaHideIf(EMode.Play)] public int[] hidePlay;
[PlayaShowIf(EMode.Edit)] public int[] showEdit;
[PlayaShowIf(EMode.Play)] public int[] showPlay;
[ShowInInspector, PlayaHideIf(nameof(boolValue))] public const float HideIfConst = 3.14f;
[ShowInInspector, PlayaShowIf(nameof(boolValue))] public const float ShowIfConst = 3.14f;
[ShowInInspector, PlayaHideIf(EMode.Edit)] public const float HideEditConst = 3.14f;
[ShowInInspector, PlayaHideIf(EMode.Play)] public const float HidePlayConst = 3.14f;
[ShowInInspector, PlayaShowIf(EMode.Edit)] public const float ShowEditConst = 3.14f;
[ShowInInspector, PlayaShowIf(EMode.Play)] public const float ShowPlayConst = 3.14f;
[ShowInInspector, PlayaHideIf(nameof(boolValue))] public static readonly Color HideIfStatic = Color.green;
[ShowInInspector, PlayaShowIf(nameof(boolValue))] public static readonly Color ShowIfStatic = Color.green;
[ShowInInspector, PlayaHideIf(EMode.Edit)] public static readonly Color HideEditStatic = Color.green;
[ShowInInspector, PlayaHideIf(EMode.Play)] public static readonly Color HidePlayStatic = Color.green;
[ShowInInspector, PlayaShowIf(EMode.Edit)] public static readonly Color ShowEditStatic = Color.green;
[ShowInInspector, PlayaShowIf(EMode.Play)] public static readonly Color ShowPlayStatic = Color.green;
[Button, PlayaHideIf(nameof(boolValue))] private void HideIfBtn() => Debug.Log("HideIfBtn");
[Button, PlayaShowIf(nameof(boolValue))] private void ShowIfBtn() => Debug.Log("ShowIfBtn");
[Button, PlayaHideIf(EMode.Edit)] private void HideEditBtn() => Debug.Log("HideEditBtn");
[Button, PlayaHideIf(EMode.Play)] private void HidePlayBtn() => Debug.Log("HidePlayBtn");
[Button, PlayaShowIf(EMode.Edit)] private void ShowEditBtn() => Debug.Log("ShowEditBtn");
[Button, PlayaShowIf(EMode.Play)] private void ShowPlayBtn() => Debug.Log("ShowPlayBtn");
PlayaEnableIf/PlayaDisableIf
This is the same as EnableIf, DisableIf, plus it can be applied to array, Button
Different from EnableIf/DisableIf in the following:
- apply on an array will directly enable or disable the array itself, rather than each element.
- Callback function can not receive value and index
- this method can not detect foldout, which means using it on
Expandable,EnumFlags, the foldout button will also be disabled. For this case, useDisableIf/EnableIfinstead.
using SaintsField.Playa;
[PlayaDisableIf] public int[] justDisable;
[PlayaEnableIf] public int[] justEnable;
[PlayaDisableIf(nameof(boolValue))] public int[] disableIf;
[PlayaEnableIf(nameof(boolValue))] public int[] enableIf;
[PlayaDisableIf(EMode.Edit)] public int[] disableEdit;
[PlayaDisableIf(EMode.Play)] public int[] disablePlay;
[PlayaEnableIf(EMode.Edit)] public int[] enableEdit;
[PlayaEnableIf(EMode.Play)] public int[] enablePlay;
[Button, PlayaDisableIf(nameof(boolValue))] private void DisableIfBtn() => Debug.Log("DisableIfBtn");
[Button, PlayaEnableIf(nameof(boolValue))] private void EnableIfBtn() => Debug.Log("EnableIfBtn");
[Button, PlayaDisableIf(EMode.Edit)] private void DisableEditBtn() => Debug.Log("DisableEditBtn");
[Button, PlayaDisableIf(EMode.Play)] private void DisablePlayBtn() => Debug.Log("DisablePlayBtn");
[Button, PlayaEnableIf(EMode.Edit)] private void EnableEditBtn() => Debug.Log("EnableEditBtn");
[Button, PlayaEnableIf(EMode.Play)] private void EnablePlayBtn() => Debug.Log("EnablePlayBtn");
PlayaArraySize
Like ArraySize, but:
- it will set array size to expected size when the array is empty
- it does not have a
groupBy, and will not give any error if the target is not an array/list
Parameters:
int sizethe size of the array or list- AllowMultiple: No
using SaintsField.Playa;
[PlayaArraySize(3)] public int[] myArr3;
PlayaRichLabel
This is like RichLabel, but it can change label of an array/list
Please note: at the moment it only works for serialized property, and is only tested on array/list. It's suggested to use RichLabel for non-array/list
serialized fields.
Parameters:
string richTextXmlthe rich text xml for the labelbool isCallback=falseif it's a callback (a method/property/field)
using SaintsField.Playa;
[PlayaRichLabel("<color=lame>It's Labeled!")]
public List<string> myList;
[PlayaRichLabel(nameof(MethodLabel), true)]
public string[] myArray;
private string MethodLabel(string[] values)
{
return $"<color=green><label /> {string.Join("", values.Select(_ => "<icon=star.png />"))}";
}
OnButtonClick
This is a method decorator, which will bind this method to the target button's click event.
Parameters:
string buttonTarget=nullthe target button.nullto get it form the current target.object value=nullthe value passed to the method. Note unity only supportbool,int,float,stringandUnityEngine.Object. To pass aUnityEngine.Object, use a string name of the target, and set theisCallbackparameter totruebool isCallback=false: whenvalueis a string, set this totrueto obtain the actual value from a method/property/field
using SaintsField.Playa;
[OnButtonClick]
public void OnButtonClickVoid()
{
Debug.Log("OnButtonClick Void");
}
[OnButtonClick(value: 2)]
public void OnButtonClickInt(int value)
{
Debug.Log($"OnButtonClick ${value}");
}
[OnButtonClick(value: true)]
public void OnButtonClickBool(bool value)
{
Debug.Log($"OnButtonClick ${value}");
}
[OnButtonClick(value: 0.3f)]
public void OnButtonClickFloat(float value)
{
Debug.Log($"OnButtonClick ${value}");
}
private GameObject ThisGo => this.gameObject;
[OnButtonClick(value: nameof(ThisGo), isCallback: true)]
public void OnButtonClickComp(UnityEngine.Object value)
{
Debug.Log($"OnButtonClick ${value}");
}
Note:
- In UI Toolkit, it will only check once when you select the GameObject. In IMGUI, it'll constantly check as long as you're on this object.
- It'll only check the method name. Which means, if you change the value of the callback, it'll not update the callback value.
OnEvent
This is a method decorator, which will bind this method to the target UnityEvent (allows generic type) invoke event.
Parameters:
string eventTargetthe targetUnityEvent.object value=nullthe value passed to the method. Note unity only supportbool,int,float,stringandUnityEngine.Object. To pass aUnityEngine.Object, use a string name of the target, and set theisCallbackparameter totruebool isCallback=false: whenvalueis a string, set this totrueto obtain the actual value from a method/property/field
public UnityEvent<int, int> intIntEvent;
[OnEvent(nameof(intIntEvent))]
public void OnInt2(int int1, int int2) // dynamic parameter binding
{
}
[OnEvent(nameof(intIntEvent), value: 1)]
public void OnInt1(int int1) // static parameter binding
{
}
Note:
- In UI Toolkit, it will only check once when you select the GameObject. In IMGUI, it'll constantly check as long as you're on this object.
- It'll only check the method name. Which means, if you change the value of the callback, it'll not update the callback value.
About GroupBy
group with any decorator that has the same groupBy for this field. The same group will share even the width of the view width between them.
This only works for decorator draws above or below the field. The above drawer will not grouped with the below drawer, and vice versa.
"" means no group.
Add a Macro
Pick a way that is most convenient for you:
Using Saints Menu
Go to Window - Saints to enable/disable functions you want
Using csc.rsp
-
Create file
Assets/csc.rsp -
Write marcos like this:
#"Disable DOTween" -define:SAINTSFIELD_DOTWEEN_DISABLE #"Disable Addressable" -define:SAINTSFIELD_ADDRESSABLE_DISABLE #"Disable AI Navigation" -define:SAINTSFIELD_AI_NAVIGATION_DISABLED #"Disable UI Toolkit" -define:SAINTSFIELD_UI_TOOLKIT_DISABLE #"Apply SaintsEditor project wide" -define:SAINTSFIELD_SAINTS_EDITOR_APPLY #"Disable SaintsEditor IMGUI constant repaint" -define:SAINTSFIELD_SAINTS_EDITOR_IMGUI_CONSTANT_REPAINT_DISABLE
Note: csc.rsp can override settings by Saints Menu.
Using project settings
Edit - Project Settings - Player, find your platform, then go Other Settings - Script Compliation - Scripting Define Symbols to add your marcos. Don't forget to click Apply before closing the window.
Common Pitfalls & Compatibility
List/Array & Nesting
Directly using on list/array will apply to every direct element of the list, this is a limit from Unity.
Unlike NaughtyAttributes, SaintsField does not need a AllowNesting attribute to work on nested element.
public class ArrayLabelExample : MonoBehaviour
{
// works for list/array
[Scene] public int[] myScenes;
[System.Serializable]
public struct MyStruct
{
public bool enableFlag;
[AboveRichLabel("No need for `[AllowNesting]`, it just works")]
public int integer;
}
public MyStruct myStruct;
}
Order Matters
SaintsField only uses PropertyDrawer to draw the field, and will properly fall back to the rest drawers if there is one.
This works for both 3rd party drawer, your custom drawer, and Unity's default drawer.
However, Unity only allows decorators to be loaded from top to bottom, left to right. Any drawer that does not properly handle the fallback
will override PropertyDrawer follows by. Thus, ensure SaintsField is always the first decorator.
An example of working with NaughtyAttributes:
using SaintsField;
[RichLabel("<color=green>+NA</color>"),
NaughtyAttributes.CurveRange(0, 0, 1, 1, NaughtyAttributes.EColor.Green),
NaughtyAttributes.Label(" ") // a little hack for label issue
]
public AnimationCurve naCurve;
[RichLabel("<color=green>+Native</color>"), Range(0, 5)]
public float nativeRange;
// this wont work. Please put `SaintsField` before other drawers
[Range(0, 5), RichLabel("<color=green>+Native</color>")]
public float nativeRangeHandled;
// this wont work too. Please put `SaintsField` before other drawers
[NaughtyAttributes.CurveRange(0, 0, 1, 1, NaughtyAttributes.EColor.Green)]
[RichLabel("<color=green>+NA</color>")]
public AnimationCurve naCurveHandled;
Fallback To Other Drawers
SaintsField is designed to be compatible with other drawers if
-
the drawer itself respects the
GUIContentargument inOnGUIfor IMGUI, or addunity-labelclass to Label for UI ToolkitNOTE:
NaughtyAttributes(IMGUI) usesproperty.displayNameinstead ofGUIContent. You need to setLabel(" ")if you want to useRichLabel. Might not work very well withNaughtyAttributes's meta attributes because they are editor level components. -
if the drawer hijacks the
CustomEditor, it must fall to the rest drawersNOTE: In many cases
Odindoes not fallback to the rest drawers, but only toOdinand Unity's default drawers. So sometimes things will not work withOdin
Special Note:
NaughtyAttributes uses only IMGUI. If you're using Unity 2022.2+, NaughtyAttributes's editor will try fallback default drawers and Unity will decide to use UI Toolkit rendering SaintsField and cause troubles.
Please disable SaintsField's UI Toolkit ability by Window - Saints - Disable UI Toolkit Support (See "Add a Macro" section for more information)
My (not full) test about compatibility:
- Markup-Attributes: Works very well.
- NaughtyAttributes: Works well, need that
Labelhack. - OdinInspector: Works mostly well for MonoBehavior/ScriptableObject. Not so good when it's inside Odin's own serializer.