Unclaimed Package Is this your package? Claim it to unlock full analytics and manage your listing.
Claim This Package

Install via UPM

Add to Unity Package Manager using this URL

https://www.pkglnk.dev/saintsfield.git

README Markdown

Copy this to your project's README.md

Style
Preview
pkglnk installs badge
## Installation

Add **SaintsField** to your Unity project via Package Manager:

1. Open **Window > Package Manager**
2. Click **+** > **Add package from git URL**
3. Enter:
```
https://www.pkglnk.dev/saintsfield.git
```

[![pkglnk](https://www.pkglnk.dev/badge/saintsfield.svg?style=pkglnk)](https://www.pkglnk.dev/pkg/saintsfield)

README

SaintsField

unity_version license_mit openupm Percentage of issues still open Average time to resolve an issue openupm repo-stars

SaintsField is a Unity extension tool for enhancing inspector and data serialization.

Developed by: TylerTemp, 墨瞳

Unity: 2022.2 or higher

[!TIP] A better document with TOC & Search: saintsfield.comes.today

(Yes, the project name comes from, of course, Saints Row 2)

Getting Started

Highlights

  1. Works on deep nested fields!
  2. When a target is drawn by the old IMGUI drawer, it will be rendered correctly inside UI Toolkit.
  3. 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.
  4. Allow dynamic arguments in many cases
  5. Directly serialize dictionary, interface, hashset and more
  6. Easily group different fields with box

Installation

  • Using Unity Asset Store

  • Using OpenUPM

    openupm add today.comes.saintsfield
    
  • Using git upm:

    add to Packages/manifest.json in your project

    {
        "dependencies": {
            "today.comes.saintsfield": "https://github.com/TylerTemp/SaintsField.git",
            // your other dependencies...
        }
    }
    
  • Using git upm (Unity UI):

    1. Window - Package Manager
    2. Click + button, Add package from git URL
    3. Enter the following URL:
    https://github.com/TylerTemp/SaintsField.git
    
  • Using a unitypackage:

    Go to the Release Page to download a desired version of unitypackage and import it to your project

  • Using a git submodule:

    git submodule add https://github.com/TylerTemp/SaintsField.git Packages/today.comes.saintsfield
    

If you have DOTween installed

  • Please also ensure you do: Tools - Demigaint - DOTween Utility Panel, click Create ASMDEF
  • Or disable related functions with Window - Saints - Disable DOTween Support
  • If you can not find this menu, please read the "Add a Macro" section about how to manually disable DOTween support in SaintsField.

[Optional] To use the full functions of this project, please also do: Window - Saints - Enable SaintsEditor. Note this will break your existing Editor plugin like OdinInspector, NaughtyAttributes, MyToolbox, Tri-Inspector.

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 the project's Editor/Editor Default Resources/SaintsField into your project's Assets/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 .meta file to avoid GUID conflict.

Troubleshoot

After installation, you can use Window - Saints - Troubleshoot to check if some attributes do not work.

namespace: SaintsField

Change Log

5.12.1

  1. Fix: context menu in old unity did not show correctly, context menu for SaintsArray/SaintsList did not show
  2. Fix: new gameobjects being spawned whenever a property is reset @peterdwdawe, PR#371
  3. Add: ResizableTextArea support ShowInInspector and Button
  4. Fix: reset context menu shows uppercase if a variable name starts with _, remove the k__BackingField information.

Note: all Handle attributes (draw stuff in the scene view) are in stage 1, which means the arguments might change in the future.

See the full change log.

General Attributes

Label & Text

LabelText

[!IMPORTANT] Enable SaintsEditor before using

Change the label text of a field. (To change element label of an array/list, use FieldLabelText instead.)

Parameters:

  • string richTextXml the rich text xml for the label. Supported tag:

    • All Unity rich label tag, like <color=#ff0000>red</color>
    • <icon=path/to/image.png /> for icon
    • <label /> for current field name
    • <field />, <field.subField/>, <field.subField=formatControl /> read the value from the field first, if tag has sub-field, continue to read, then use string.Format if there is a formatControl. See the example below.
    • <container.Type /> for the class/struct name of the container of the field
    • <container.Type.BaseType /> for the class/struct name of the field's container's parent
    • <index />, <index=formatControl /> for the index if the target is an array/list

    Note about format control:

    • If the format contains {}, it will be used like a string.Format. E.g. <field.subField=(--<color=red>{0}</color>--)/> will be interpreted like string.Format("(--<color=red>{0}</color>--)", this.subField).
    • Otherwise, it will be rewritten to {0:formatControl}. E.g., <index=D4/> will be interpreted like string.Format("{0:D4}", index).

    null means no label

    for icon, it 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 using unitypackage)
    • "Packages/today.comes.saintsfield/Editor/Editor Default Resources/SaintsField/" (this is most likely to be when installed using upm)
    • Assets/Editor Default Resources/, then fallback to built-in editor resources by name (using EditorGUIUtility.Load)

    You can also use Unity Editor's built-in icons. See UnityEditorIcons. e.g. <icon=d_AudioListener Icon/>

    for color, you can use Window - Saints - EColor Preview to view all the pre-set colors. It 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

    • Standard Unity Pre-Set Color Presets (Unity 6.2 as a reference), e.g. darkViolet, hotPink

    • Some extra colors from NaughtyAttributes & UI Toolkit:

      clear, pink, indigo, violet, charcoalGray, oceanicSlate

    • html color which is supported by ColorUtility.TryParseHtmlString, like #RRGGBB, #RRGGBBAA, #RGB, #RGBA

    color_list

    If it starts with $, the leading $ will be removed and isCallback will be set to true. Use \$ to escape the starting $.

  • bool isCallback=false (Depreacted, use $ with "richTextXml" instead)

    if it's a callback (a method/property/field)

// Please ensure you already have SaintsEditor enabled in your project before trying this example
using SaintsField.Playa;

[LabelText("<color=lime>It's Labeled!")]
public List<string> myList;

[LabelText("$" + nameof(MethodLabel))]
public string[] myArray;

private string MethodLabel(string[] values)
{
    return $"<color=green><label /> {string.Join("", values.Select(_ => "<icon=star.png />"))}";
}

PlayaRichLabel

Example of using <field /> to display field value/propery value:

using SaintsField;

public class SubField : MonoBehaviour
{
    [SerializeField] private string _subLabel;

    public double doubleVal;
}

[Separator("Field")]
// read field value
[LabelText("<color=lime><field/>")] public string fieldLabel;
// read the `_subLabel` field/function from the field
[LabelText("<field._subLabel/>"), GetComponentInChildren, Expandable] public SubField subField;
// again read the property
[LabelText("<color=lime><field.gameObject.name/>")] public SubField subFieldName;

[Separator("Field Null")]
// not found target will be rendered as empty string
[LabelText("<field._subLabel/>")] public SubField notFoundField;
[LabelText("<field._noSuch/>"), GetComponentInChildren] public SubField notFoundField2;

[Separator("Formatter")]
// format as percent
[LabelText("<field=P2/>"), PropRange(0f, 1f)] public float percent;
// format `doubleVal` field as exponential
[LabelText("<field.doubleVal=E/>")] public SubField subFieldCurrency;

video

Example of quoted fancy formatting:

[LabelText("<field=\">><color=yellow>{0}</color><<\"/> <index=\"[<color=blue>>></color>{0}<color=blue><<</color>]\"/>")]
public string[] sindices;

Image

FieldLabelText

Like LabelText, but it can be applied to an array/list to change the element label (instead of the label of array/list itself)

  1. Ensure you make it a callback: isCallback=true, or the richTextXml starts with $
  2. It'll pass the element value and index to your function
  3. Return the desired label content from the function

Here is an example of using on an array:

using SaintsField;

[FieldLabelText("$" + nameof(ArrayLabels))]
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)}]";

label_array

NoLabel

Hide the label for the field. When using on an array/list, hide label for every element.

[NoLabel] is a shortcut for [FieldLabelText(null)]

Note: NoLabel does not work with ShowInInspector. Use LabelText(null) instead.

[NoLabel] [ProgressBar(0, 100)] public int mp;
[NoLabel] [PairsValueButtons("<icon=lightMeter/greenLight/>", true, "<icon=lightMeter/redLight/>", false)] public bool allowed;

[Serializable, Flags]
public enum Direction
{
    [InspectorName("↑")]
    Up = 1,
    [InspectorName("→")]
    Right = 1 << 1,
    [InspectorName("↓")]
    Down = 1 << 2,
    [InspectorName("←")]
    Left = 1 << 3,
}

[NoLabel] [EnumToggleButtons] public Direction direction;

[NoLabel] [GetInChildren] public Transform[] transArray;

AboveText / BelowText

[!IMPORTANT] Enable SaintsEditor before using

Like LabelLabel, but it's rendered above/below the field in full width of view instead.

It can also be applied on a class/struct, which can act like a class/struct comment

Parameters:

  • string content the content to show. If it starts with $, then a callback/propery/field value is used. When a callback gives null or empty string, the label will be hidden. For AboveText the default value for this parameter is "<color=gray><label/>". For BelowText this parameter is required.
  • float paddingLeft=4, float paddingRight=0: add pading space for content
using SaintsField;
using SaintsField.Playa;

[AboveText("<color=gray>-- Above --")]
[AboveText("$" + nameof(dynamicContent))]
[AboveText("$" + nameof(dynamicContent))]
[BelowText("<color=gray>-- Below --")]
public string[] s;

[Space(20)]
public string dynamicContent;

Image

Example of using on a class/struct like a comment:

using SaintsField;
using SaintsField.Playa;

[AboveText("<color=gray>This is a class message")]
[AboveText("$" + nameof(dynamicContent))]
public class ClassPlayaAboveRichLabelExample : MonoBehaviour
{
    [ResizableTextArea]
    public string dynamicContent;

    [Serializable]
    [AboveText("<color=gray>--This is a struct message--")]
    public struct MyStruct
    {
        public string structString;
    }

    public MyStruct myStruct;
}

Image

FieldAboveText / FieldBelowText

Like AboveText / BelowText, but it can be applied to an array/list to draw text above/below each element (instead of the label of array/list itself)

Using on a single field, it can show text on field like AboveText / BelowText does. Useful if you can not enable SaintsEditor

  • string|null richTextXml Same as RichLabel
  • bool isCallback=false Same as RichLabel
  • string groupBy = "" See GroupBy section
  • Allow Multiple: Yes
using SaintsField;

[FieldAboveText("<color=DodgerBlue>┌<field/></color> at [<index/>]")]
[FieldBelowText("$" + nameof(GetAboveText))]
public string[] lis;

private string GetAboveText(string value, int index)
{
    bool isEven = index % 2 == 0;
    return isEven ? $"<color={EColor.SoftRed}>└Event" : $"<color={EColor.HotPink}>└Odd";
}

[FieldAboveText("Can also be used on single fields: <label/>(raw value)=<field/>")]
[FieldBelowText("$" + nameof(singleField))]  // callback, as parsed value
public string singleField;

full_width_label

OverlayText

Like TextLabel, 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.

Using on an array/list will apply to every element (instead of the array/list itself).

Parameters:

  • string richTextXml the content of the label, or a property/callback. Supports tags like LabelText

    If it starts with $, the leading $ will be removed and isCallback will be set to true. Use \$ to escape the starting $.

  • bool isCallback=false if true, the richTextXml will be interpreted as a property/callback function, and the string value / the returned string value (tag supported) will be used as the label content

  • float padding=5f padding between your input and the label. Not work when end=true

  • bool end=false when 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.

  • Allow Multiple: No

using SaintsField;

[OverlayText("<color=grey>km/s")] public double speed = double.MinValue;
[OverlayText("<icon=eye.png/>")] public string text;
[OverlayText("<color=grey>/int", padding: 1)] public int count = int.MinValue;
[OverlayText("<color=grey>/long", padding: 1)] public long longInt = long.MinValue;
[OverlayText("<color=grey>suffix", end: true)] public string atEnd;

overlay_rich_label

EndText

Like LabelText, but it's rendered at the end of the field.

Parameters:

  • string richTextXml the content of the label, or a property/callback. Supports tags like RichLabel

    If it starts with $, the leading $ will be removed and isCallback will be set to true. Use \$ to escape the starting $.

  • bool isCallback=false if true, the richTextXml will be interpreted as a property/callback function, and the string value / the returned string value (tag supported) will be used as the label content

  • float padding=5f padding between the field and the label.

  • string GroupBy="" this is only for the error message box.

  • AllowMultiple: Yes

using SaintsField;

[EndText("<color=grey>km/s")] public float speed;
[EndText("<icon=eye.png/>", padding: 0)] public GameObject eye;
[EndText("$" + nameof(TakeAGuess))] 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!";
}

post_field_rich_label

InfoBox/BelowInfoBox

[!IMPORTANT] Enable SaintsEditor before using

Draw an info box above/below the field.

It can also be directly applied on a class/struct definition, to act like a comment.

  • string content

    The content of the info box.

    If it starts with $, the leading $ will be removed and isCallback will be set to true. Use \$ to escape the starting $.

  • EMessageType messageType=EMessageType.Info

    Message icon. Options are

    • None
    • Info
    • Warning
    • Error
  • string show=null

    a callback name or property name for show or hide this info box.

  • bool isCallback=false

    if true, the content will 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 below=false

    Draw the info box below the field instead of above

  • string groupBy="" See GroupBy section

  • AllowMultiple: Yes

// Please ensure you already have SaintsEditor enabled in your project before trying this example
using SaintsField.Playa;

[InfoBox("Please Note: special label like <icon=star.png/> only works for <color=lime>UI Toolkit</color> <color=red>(not IMGUI)</color> in InfoBox.")]
[BelowInfoBox("$" + nameof(DynamicFromArray))]  // callback
public string[] strings = {};

public string dynamic;

private string DynamicFromArray(string[] value) => value.Length > 0? string.Join("\n", value): "null";

[InfoBox("MethodWithButton")]
[Button("Click Me!")]
[BelowInfoBox("GroupExample", groupBy: "group")]
[BelowInfoBox("$" + nameof(dynamic), groupBy: "group")]
public void MethodWithButton()
{
}

[InfoBox("Method")]
[BelowInfoBox("$" + nameof(dynamic))]
public void Method()
{
}

image

Example of using on a class/struct definition like a data comment:

using SaintsField;
using SaintsField.Playa;

[InfoBox("This is a class message", EMessageType.None)]
[InfoBox("$" + nameof(dynamicContent))]
public class ClassInfoBoxExample : MonoBehaviour  // The info box will show in inspector wherever you attach this component
{
    public string dynamicContent;

    [Serializable]
    [InfoBox("This is a struct message")]
    public struct MyStruct  // The info box will show at first row wherever you use this struct
    {
        public string structString;
    }

    public MyStruct myStruct;
}

Image

FieldInfoBox/FieldBelowInfoBox

Draw an info box above/below the field.

Unlike InfoBox, use this on an array/list, the box will be applied to every element (instead of the array/list itself).

  • string content

    The content of the info box.

    If it starts with $, the leading $ will be removed and isCallback will be set to true. Use \$ to escape the starting $.

  • EMessageType messageType=EMessageType.Info

    Message icon. Options are

    • None
    • Info
    • Warning
    • Error
  • string show=null

    a callback name or property name for show or hide this info box.

  • bool isCallback=false

    if true, the content will 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 below=false

    Draw the info box below the field instead of above

  • string groupBy="" See GroupBy section

  • AllowMultiple: Yes

BelowInfoBox is a shortcut for [InfoBox(..., below: true)]

using SaintsField;

[field: SerializeField] private bool _show;

[Space]
[FieldInfoBox("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)]
[FieldBelowInfoBox("$" + nameof(DynamicMessage), EMessageType.Warning)]
[FieldBelowInfoBox("$" + nameof(DynamicMessageWithIcon))]
[FieldBelowInfoBox("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";

video

Separator/BelowSeparator

[!IMPORTANT] Enable SaintsEditor before using

Draw text, separator, spaces for field/property/button/layout/class/struct on above / below with rich text & dynamic text support.

Parameters:

  • string title=null display a title. null for no title, only separator.

    If it starts with $, the leading $ will be removed and isCallback will be set to true. Use \$ to escape the starting $.

  • EColor color=EColor.Gray color for the title and the separator

  • EAlign eAlign=EAlign.Start how the title is positioned, options are:

    • EAlign.Start
    • EAlign.Center
    • EAlign.End
  • bool isCallback=false when true, use title as a callback to get a dynamic title

  • int space=0 leave some space above or below the separator, like what Space does.

  • bool below=false when true, draw the separator below the field.

[Separator("Separator", EAlign.Center)]
public string separator;

[Separator("Left", EAlign.Start)] public string left;

[Separator("$" + nameof(right), EAlign.End)]
public string right;

[Separator(20)]
[Separator("Space 20")]
public string[] arr;

[Separator("End", below: true)] public string end;

image

Using it with Layout, you can create some fancy appearance:

[LayoutStart("Equipment", ELayout.TitleBox)]

[LayoutStart("./Head")]
[Separator("Head", EAlign.Center)]
public string st;
[LayoutCloseHere]
public MyStruct inOneStruct;

[LayoutStart("./Upper Body")]

[InfoBox("Note:left hand can be empty, but not right hand", EMessageType.Warning)]

[LayoutStart("./Horizontal", ELayout.Horizontal)]

[LayoutStart("./Left Hand")]
[Separator("Left Hand", EAlign.Center)]
public string g11;
public string g12;
public MyStruct myStruct;
public string g13;

[LayoutStart("../Right Hand")]
[Separator("Right Hand", EAlign.Center)]
public string g21;
[LabelText("<color=lime><label/>")]
public string g22;
[LabelText("$" + nameof(g23))]
public string g23;

public bool toggle;

image

Use it on a class to get a class default seperator. This is useful for inherent.

// AbsSepMono.cs
[BelowSeparator("Inherent Fields of <color=brown><container.Type/>", EColor.Brown, EAlign.Center)]
[BelowSeparator(10)]
public abstract class AbsSepMono : SaintsMonoBehaviour
{
    public string absField1;
    public string absField2;
}

// ChildSepMono.cs
[Separator("Begin of <container.Type/>", EAlign.Center)]
public class ChildSepMono : AbsSepMono
{
    public string childField;
}

FieldSeparator / FieldBelowSeparator

[!TIP] Only use this if you can not enable SaintsEditor

Draw text, separator, spaces for field on above / below with rich text & dynamic text support. Using on an array will apply to every element (instead of the array/list itself).

Parameters:

  • string title=null display a title. null for no title, only separator.

    If it starts with $, the leading $ will be removed and isCallback will be set to true. Use \$ to escape the starting $.

  • EColor color=EColor.Gray color for the title and the separator

  • EAlign eAlign=EAlign.Start how the title is positioned, options are:

    • EAlign.Start
    • EAlign.Center
    • EAlign.End
  • bool isCallback=false when true, use title as a callback to get a dynamic title

  • int space=0 leave some space above or below the separator, like what Space does.

  • bool below=false when true, draw the separator below the field.

using SaintsField;

[Space(50)]

[FieldSeparator("Start")]
[FieldSeparator("Center", EAlign.Center)]
[FieldSeparator("End", EAlign.End)]
[FieldBelowSeparator("$" + nameof(Callback))]
public string s3;
public string Callback() => s3;

[Space(50)]

[FieldSeparator]
public string s1;

[FieldSeparator(10)]  // this behaves like a space
[FieldSeparator("[ Hi <color=LightBlue>Above</color> ]", EColor.Aqua, EAlign.Center)]
[FieldBelowSeparator("[ Hi <color=Silver>Below</color> ]", EColor.Brown, EAlign.Center)]
[FieldBelowSeparator(10)]
public string hi;

[FieldBelowSeparator]
public string s2;

image

This is very useful when you what to separate parent fields from the inherent:

using SaintsField;

public class SeparatorParent : MonoBehaviour
{
    [BelowSeparator("End Of <b><color=Aqua><container.Type/></color></b>", EAlign.Center, space: 10)]
    public string parent;
}

public class SeparatorInherent : SeparatorParent
{
    public string inherent;
}

image

SepTitle

A separator with text. This is a decorator type attribute, which means used on list/array will draw itself above the list/array (not above each element of list/array).

  • string title=null display a title. null for no title, only separator.

    UI Toolkit: support rich labels except <field/> & <container/> tag.

    IMGUI: only support unity standard rich labels

  • EColor color=EColor.Gray color for the title and the separator

  • EAlign eAlign=EAlign.Start how the title is positioned, options are:

    • EAlign.Start
    • EAlign.Center
    • EAlign.End
  • int space=0 leave some space above or below the separator, like what Space does.

using SaintsField;

[SepTitle("Separate Here", EColor.Pink)]
public string content1;

[SepTitle(EColor.Green)]
public string content2;

sep_title

GUIColor

Change the color of the field.

Override:

  • GUIColor(EColor eColor, float alpha = 1f)

    Use EColor with custom alpha value

  • GUIColor(string hexColorOrCallback)

    Use hex color which starts with #, or use a callback, to get the color

  • GUIColor(float r, float g, float b, float a = 1f)

    Use rgb/rgba color (0-1 range)

// EColor + alpha
[GUIColor(EColor.Cyan, 0.9f)] public int intField;
// Hex color
[GUIColor("#FFC0CB")] public string[] stringArray;
// rgb/rgba
[GUIColor(112 / 255f, 181 / 255f, 131 / 255f)]
public GameObject lightGreen;
[GUIColor(0, 136 / 255f, 247 / 255f, 0.3f)]
public Transform transparentBlue;

// Dynamic color of field
[GUIColor("$" + nameof(dynamicColor)), Range(0, 10)] public int rangeField;
public Color dynamicColor;

// Dynamic color of callback. `$` can be omitted
[GUIColor(nameof(DynamicColorFunc)), TextArea] public string textArea;
private Color DynamicColorFunc() => dynamicColor;

// validation
[GUIColor("$" + nameof(ValidateColor))]
public int validate;

private Color ValidateColor()
{
    const float c = 207 / 255f;
    return validate < 5 ? Color.red : new Color(c, c, c);
}

video

It works with ShowInInspector, Button, InfoBox, etc. too:

[GUIColor(EColor.Gold)]
[InfoBox("This is colored gold using <b><u><color=white>GUIColor</color></u></b> attribute.")]
[Button]
private void ButtonGold(){}

[ShowInInspector]
[GUIColor("#00FF00")]
private int _greenInt = 42;

[ShowInInspector]
[GUIColor(EColor.Burlywood)]
private int Calc([PropRange(0,  10)] int v) => v + Random.Range(1, 9) * 100;

[ShowInInspector]
[GUIColor("$" + nameof(GetColor))]
[InfoBox("Dynamic callback")]
private DateTime _dt;

private byte _color255;

private Color GetColor()
{
    _color255 = (byte)((_color255 + 10) % 256);
    byte r = (byte)(_color255 * 1 % 256);
    byte g = (byte)((_color255 * 2 + 100) % 256);
    byte b = (byte)((_color255 * 3 + 100) % 256);
    return new Color32(r, g, b, 255);
}

video

(UI Toolkit implementation code is partly from EditorAttributes, go give a star to them!)

Button

Button

[!IMPORTANT] Enable SaintsEditor before using

Draw a button for a function. If the method have arguments (required or optional), it'll draw inputs for these arguments. UI Toolkit: if the method have a return value, the result will also be shown.

  • string buttonLabel = null the button label. If null, it'll use the function name. If it starts with $, use a callback or field value as the label. Rich text is supported.
  • bool hideReturnValue = false do not display the returned value.

Known Issue: Using dynamic label in SaintsRow, the label will not update in real time. This is because a Serializable class/struc field value will be cached by Unity, and reflection can not get an updated value. This issue can not be solved unless there is a way to reflect the actual value from a cached container.

// Please ensure you already have SaintsEditor enabled in your project before trying this example
using SaintsField.Playa;

public string dynamicLabel;

[Button("$" + nameof(dynamicLabel))]
private void ButtonWithDynamicLabel()
{
}

[Button("Normal <icon=star.png/>Button Label")]
private void ButtonWithNormalLabel()
{
}

[Button]
private void ButtonWithoutLabel()
{
}

image

Example with arguments:

// Please ensure you already have SaintsEditor enabled in your project before trying this example
using SaintsField.Playa;

[Button]
private void OnButtonParams(UnityEngine.Object myObj, int myInt, string myStr = "hi")
{
    Debug.Log($"{myObj}, {myInt}, {myStr}");
}

image

// Please ensure you already have SaintsEditor enabled in your project before trying this example
using SaintsField.Playa;

[Button]  // Display the returned value when clicked
private int AddCalculator(int a, int b) => a + b;

[GetComponentInChildren] public GameObject[] goLis;

private class ResultClass
{
    public GameObject Go;
}

[Button]  // A struct, class, UnityObject return type is supported too
private ResultClass ReturnClass(int v) => new ResultClass
{
    Go = goLis[v % goLis.Length]
};

[Button(hideReturnValue: true)]  // Hide the returned value
private int ReturnIgnored() => Random.Range(0, 100);

video

AboveButton/BelowButton/PostFieldButton

There are 3 general buttons:

  • AboveButton will draw a button on above
  • BelowButton will draw a button on below
  • PostFieldButton will draw a button at the end of the field

All of them have the same arguments:

  • string funcName

    called when you click the button

  • string buttonLabel=null

    label of the button, support tags like RichLabel. null means using function name as label.

    If it starts with $, the leading $ will be removed and isCallback will be set to true. Use \$ to escape the starting $.

  • bool isCallback = false

    a callback or property name for button's label, same as RichLabel

  • string groupBy = ""

    See GroupBy section. Does NOT work on PostFieldButton

  • AllowMultiple: Yes

Note: Compared to Button in SaintsEditor, these buttons can receive the value of the decorated field, and will not get parameter drawers.

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), groupBy: "OK")]
[AboveButton(nameof(ClickButton), "$" + nameof(GetButtonLabel), groupBy:  "OK")]

[PostFieldButton(nameof(ToggleAndError), nameof(GetButtonLabelIcon), true)]

[BelowButton(nameof(ClickButton), "$" + nameof(GetButtonLabel), groupBy: "OK")]
[BelowButton(nameof(ClickButton), "$" + nameof(GetButtonLabel), groupBy: "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(int intValue)
{
    Debug.Log($"get value: {intValue}");
    if(_errorOut)
    {
        throw new Exception("Expected exception!");
    }
}

private void ToggleAndError()
{
    Toggle();
    if(_errorOut)
    {
        throw new Exception("Expected exception!");
    }
}

private void Toggle() => _errorOut = !_errorOut;

video

Game Related

Layer

A dropdown selector for layer. Allowed type:

  • string: to pick a layer name

  • int: to pick a layer number. (use 1 << int to get the mask value)

  • LayerMask: to pick a single layer into the LayerMask

  • Allow Multiple: 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;

[Layer]  // But you can enforce a single layer picker instead
public LayerMask singleLayerMask;

layer

It can work with ShowInInspector

[ShowInInspector, Layer] private int layerIntRaw
{
    get => layerInt;
    set => layerInt = value;
}

It can work with ShowInInspector/Button parameters and return value

[ShowInInspector]
private (int i, string s, LayerMask mask) Layer([Layer] int layerI, [Layer] string layerS, [Layer] LayerMask layerMask)
{
    return (layerI, layerS, layerMask);
}

[Button]
private (int i, string s, LayerMask mask) Layer([Layer] int layerI, [Layer] string layerS, [Layer] LayerMask layerMask)
{
    return (layerI, layerS, layerMask);
}

Scene

A dropdown selector for a scene in the build list, plus "Edit Scenes In Build..." option to directly open the "Build Settings" window where you can change building scenes.

Parameters:

  • bool fullPath = false: true to use the full-path name, false to use the scene name only. Useful if you have the same scene name under different path. Only works for string field type.

  • AllowMultiple: No

using SaintsField;

[Scene] public int _sceneInt;
[Scene] public string _sceneString;
[Scene(true)] public string _sceneFullPath;

image

It can work with ShowInInspector

[ShowInInspector, Scene]
private string sceneSRaw
{
    get => sceneS;
    set => sceneS = value;
}

It can work with ShowInInspector/Button parameters and return value

[ShowInInspector]
private (int i, string s) ButtonParamScene([Scene] int sceneI, [Scene] string sceneS)
{
    return (sceneI, sceneS);
}

[Button]
private (int i, string s) ButtonParamScene([Scene] int sceneI, [Scene] string sceneS)
{
    return (sceneI, sceneS);
}

SortingLayer

A dropdown selector for sorting layer, plus an "Edit Sorting Layers..." option to directly open "Sorting Layers" tab from "Tags & Layers" inspector where you can change sorting layers.

  • Allow Multiple: No
using SaintsField;

[SortingLayer] public string _sortingLayerString;
[SortingLayer] public int _sortingLayerInt;

image

It can work with ShowInInspector

[ShowInInspector, SortingLayer] private string SortingLayerString
{
    get => _sortingLayerString;
    set => _sortingLayerString = value;
}

It can work with ShowInInspector/Button parameters and return value

[ShowInInspector]
private (int i, string s) ButtonParamScene([Scene] int sceneI, [Scene] string sceneS)
{
    return (sceneI, sceneS);
}

[Button]
private (int i, string s) ButtonParamScene([Scene] int sceneI, [Scene] string sceneS)
{
    return (sceneI, sceneS);
}

Tag

A dropdown selector for a tag.

  • Allow Multiple: No
using SaintsField;

[Tag] public string tag;

tag

It can work with ShowInInspector

[ShowInInspector, Tag]
private string ShowTag
{
    get => _tag;
    set => _tag = value;
}

It can work with ShowInInspector/Button parameters and return value

[ShowInInspector]
private string Tag([Tag] string myTag) => myTag;

[Button]
private string Tag([Tag] string myTag) => myTag;

InputAxis

A string dropdown selector for an input axis, plus an "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;

image

It can work with ShowInInspector

[ShowInInspector, InputAxis]
private string ShowInputAxis
{
    get => inputAxis;
    set => inputAxis = value;
}

It can work with ShowInInspector/Button parameters and return value

[ShowInInspector]
private string ShowInputAxis([InputAxis] string myInput) => myInput;

[Button]
private string ShowInputAxis([InputAxis] string myInput) => myInput;

ShaderParam

Select a shader parameter from a shader, material or renderer. (Requires Unity 2021.2+)

For string, it will save the name. For int, it will save the hash.

Parameters:

  • [Optional] string name: the target. Be a property or a callback that returns a shader, material or renderer. When omitted, it will try to get the Renderer component from the current component.
  • [Optional] ShaderPropertyType propertyType: filter the shader parameters by type. Omitted to show all types.
  • [Optional] int index=0: which material index to use when the target is a Renderer.
[ShaderParam] public string shaderParamString;
[ShaderParam(0)] public int shaderParamInt;
[ShaderParam(ShaderPropertyType.Texture)] public int shaderParamFilter;

[Separator("By Target")]
[GetComponent] public Renderer targetRenderer;

[ShaderParam(nameof(targetRenderer))] public int shaderParamRenderer;

private Material GetMat() => targetRenderer.sharedMaterial;
[ShaderParam(nameof(GetMat))] public int shaderParamMat;

private Shader GetShader() => targetRenderer.sharedMaterial.shader;
[ShaderParam(nameof(GetShader))] public int shaderParamShader;

image

It works with ShowInInspector

[ShowInInspector, ShaderParam]
public int ShowShaderParamInt
{
    get => shaderParamInt;
    set => shaderParamInt = value;
}

It works with ShowInInspector/Button parameters and return value

[ShowInInspector]
private string ShowShaderParam([ShaderParam] string shaderS) => shaderS;

[Button]
private string ShowShaderParam([ShaderParam] string shaderS) => shaderS;

ShaderKeyword

Select a shader keyword from a shader, material or renderer. (Requires Unity 2021.2+)

Parameters:

  • [Optional] string name: the target. Be a property or a callback that returns a shader, material or renderer. When omitted, it will try to get the Renderer component from the current component.
  • [Optional] int index=0: which material index to use when the target is a Renderer.
[ShaderKeyword] public string shaderKeywordString;
[ShaderKeyword(0)] public string shaderKeywordIndex;

[Separator("By Target")]
[GetComponent] public Renderer targetRenderer;

[ShaderKeyword(nameof(targetRenderer))] public string shaderKeywordRenderer;

private Material GetMat() => targetRenderer.sharedMaterial;
[ShaderKeyword(nameof(GetMat))] public string shaderKeywordMat;

private Shader GetShader() => targetRenderer.sharedMaterial.shader;
[ShaderKeyword(nameof(GetShader))] public string shaderKeywordShader;

image

It works with ShowInInspector

[ShowInInspector, ShaderKeyword]
private string ShowShaderKeywordString
{
    get => shaderKeywordString;
    set => shaderKeywordString = value;
}

It works with ShowInInspector/Button parameters & return value

[ShowInInspector]
[ShaderParam]
private string ShowShaderParam([ShaderParam] string shaderS) => shaderS;

[Button]
[ShaderParam]
private string ShowShaderParam([ShaderParam] string shaderS) => shaderS;

Toggle & Switch

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;

video

SpriteToggle

A toggle button to toggle the Sprite of the target.

The field itself must be Sprite.

  • string imageOrSpriteRenderer

    the target, must be either UI.Image or SpriteRenderer

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

video

MaterialToggle

A toggle button to toggle the Material of the target.

The field itself must be Material.

  • string rendererName=null

    the target, must be Renderer (or its subClass like MeshRenderer). When using null, it will try to get the Renderer component from the current component

  • int index=0

    which slot index of materials on Renderer you want to swap

  • AllowMultiple: Yes

using SaintsField;

public Renderer targetRenderer;
[MaterialToggle(nameof(targetRenderer))] public Material _mat1;
[MaterialToggle(nameof(targetRenderer))] public Material _mat2;

video

ColorToggle

A toggle button to toggle color for Image, Button, SpriteRenderer or Renderer

The field itself must be Color.

  • string compName=null

    the target, must be Image, Button, SpriteRenderer, or Renderer (or its subClass like MeshRenderer).

    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 .color property.

    When it's a Button, it will change the button's targetGraphic.color property.

  • int index=0

    (only works for Renderer type) which slot index of materials on Renderer you 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;

video

Data Editor

Expandable

Make serializable object expandable. (E.g. ScriptableObject, MonoBehavior)

Known issues:

  1. the Foldout will NOT be placed at the left space like a Unity's default foldout component, because Unity limited the PropertyDrawer to be drawn inside the rect Unity gives. Trying outside the rect will make the target non-interactable. But in early Unity (like 2019.1), Unity will force Foldout to be out of rect on top leve, but not on array/list level... so you may see different outcomes on different Unity version.

    If you see unexpected space or overlap between foldout and label, use Window - Saints - Create or Edit SaintsField Config to change the config.

  2. ReadOnly (and DisableIf, EnableIf) can NOT disable the expanded fields. This is because InspectorElement does not work with SetEnable(false), neither with pickingMode=Ignore. This can not be fixed unless Unity fixes it.

  • Allow Multiple: No
using SaintsField;

[Expandable] public ScriptableObject _scriptable;

expandable

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:

  1. The target must have a public constructor with no required arguments.
  2. It'll try to copy field values when changing types but not guaranteed. struct will not get copied value (it's too tricky to deal a struct)

Parameters:

  • bool hideLabel=false: true to hide the label of picked type

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

// abstract 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;

reference_picker

SaintsRow

SaintsRow attribute allows you to draw Button, Layout, ShowInInspector, DOTweenPlay etc. (all SaintsEditor specific attributes) in a Serializable object (usually a class or a struct).

[!TIP] If you've enabled SaintsEditor, you do not need this attribute at all. It'll kickin by default.

Parameters:

  • bool inline=false

    If true, it'll draw the Serializable inline like it's directly in the MonoBehavior

  • Allow Multiple: No

Special Note:

After applying this attribute, only pure PropertyDrawer, and decorators from SaintsEditor works on this target. Which means, using third party's PropertyDrawer is fine, but decorator of Editor level (e.g. Odin's Button, NaughtyAttributes' Button) will not work.

using SaintsField;
using SaintsField.Playa;  // SaintsEditor is not required here

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

saints_row

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;

saints_row_inline

SerializeReference

SaintsRow can work on SerializeReference. If using it together with ReferencePicker, ensure ReferencePicker is before SaintsRow!

using SaintsField;

public interface IRefInterface
{
    public int TheInt { get; }
}

[Serializable]
public struct StructImpl : IRefInterface
{
    [field: SerializeField]
    public int TheInt { get; set; }
    [LayoutStart("Hi", ELayout.FoldoutBox)]
    public string myStruct;

    public ClassDirect nestedClass;
}

[SerializeReference, ReferencePicker, SaintsRow]
public IRefInterface saints;

[SerializeReference, ReferencePicker(hideLabel: true), SaintsRow(inline: true)]
public IRefInterface inline;

image

Drawer

alternatively, you can make a drawer for your data type to omit [SaintsRow] everywhere:

using SaintsField.Editor.Playa;

[CustomPropertyDrawer(typeof(Nest))]
public class MySaintsRowAttributeDrawer: SaintsRowAttributeDrawer {}

ListDrawerSettings

[!IMPORTANT] Enable SaintsEditor before using

Allow you to search and paging a large list/array.

Parameters:

  • bool searchable = false: allow search in the list/array
  • int numberOfItemsPerPage = 0: how many items per page by default. <=0 means no paging
  • string extraSearch = null: set a callback function to use your custom search. If not match, use the default search.
  • string overrideSearch = null: set a callback function as a custom search. When present, ignore extraSearch and default search.

Note about input:

  • When input anything, it'll wait for 0.6 seconds for next input, then perform the actual searching
  • You can always use Enter to search immediately
// Please ensure you already have SaintsEditor enabled in your project before trying this example
using SaintsField.Playa;

[Serializable]
public struct MyData
{
    public int myInt;
    public string myString;
    public GameObject myGameObject;
    public string[] myStrings;
}

[ListDrawerSettings(searchable: true, numberOfItemsPerPage: 3)]
public MyData[] myDataArr;

image

The first input is where you can search. The next input can adjust how many items per page. The last part is the paging.

Async Search

In UI Toolkit you can also see the async searching which does not block the editor when searching in a BIG list:

video

Custom Search

extraSearch & overrideSearch uses the following signiture:

  • bool CustomSearch(T item, int index, IReadOnlyList<SaintsField.Playa.ListSearchToken> searchToken)
  • bool CustomSearch(T item, IReadOnlyList<SaintsField.Playa.ListSearchToken> searchToken)
  • bool CustomSearch(int index, IReadOnlyList<SaintsField.Playa.ListSearchToken> searchToken)

ListSearchToken is a struct of:

public readonly struct ListSearchToken
{
    public readonly ListSearchType Type;  // filter type: Include, Exclude
    public readonly string Token;  // search string
}

example:

[Serializable]
public enum WeaponType
{
    Sword,
    Arch,
    Hammer,
}

[Serializable]
public struct Weapon
{
    public WeaponType weaponType;
    public string description;
}

private bool ExtraSearch(Weapon weapon, int _, IReadOnlyList<ListSearchToken> tokens)
{
    string searchName = new Dictionary<WeaponType, string>
    {
        { WeaponType.Arch , "弓箭 双手" },
        { WeaponType.Sword , "刀剑 单手" },
        { WeaponType.Hammer, "大锤 双手" },
    }[weapon.weaponType];
    return RuntimeUtil.SimpleSearch(searchName, tokens);
}

[ListDrawerSettings(extraSearch: nameof(ExtraSearch))]
public Weapon[] weapons;

You can now search as you want, both your custom search & serialized property search:

video

ShowInInspector is supported with this attribute.

[ShowInInspector, ListDrawerSettings(numberOfItemsPerPage: 5)]
private List<MyStruct> FullFeatures = new List<MyStruct>{ /*...*/ };

Table

[!IMPORTANT] Enable SaintsEditor before using

Show a list/array of class/struct/ScriptableObject(or MonoBehavior if you like) as a table.

It allows to resize the rows, hide rows.

UI Toolkit: Button, ShowInInspector & Playa* will work as expected, and Layout will be ignored.

Parameters:

  • bool defaultExpanded=false: Should the foldout be expanded by default?
  • bool hideAddButton=false: Should the add button be hidden?
  • bool hideRemoveButton=false: Shoule the remove button be hidden?
using SaintsField;

[Table]
public Scriptable[] scriptableArray;

[Serializable]
public struct MyStruct
{
    public int myInt;
    public string myString;
    public GameObject myObject;
}

[Table]
public MyStruct[] myStructs;

video

TableColumn

TableColumn allows you to merge multiple fields into one column.

[Serializable]
public struct MyStruct
{
    public int myInt;

    [TableColumn("Value"), AboveRichLabel]
    public string myString;
    [TableColumn("Value"), AboveRichLabel]
    public GameObject myObject;
}

[Table]
public List<MyStruct> myStructs;

image

For UI Toolkit, You can also use Button, ShowInInspector etc.:

using SaintsField;
using SaintsField.Playa;

[Serializable]
public struct MyValueStruct
{
    // ...

    [TableColumn("Buttons")]
    [Button("Ok")]
    public void BtnOk() {}

    [TableColumn("Buttons")]
    [Button("Cancel")]
    public void BtnCancel() {}

    [ShowInInspector] private int _showI;
}

[Table, DefaultExpand]
public MyValueStruct[] myStructs;

image

TableHide

[!NOTE] This feature is UI Toolkit only

You can use TableHide attribute to exclude some column from the table. It'll hide the column by default, and you can still toggle it in header - right click menu

[Serializable]
public struct MyStruct
{
    // Hide a single row
    [TableHide] public int hideMeInTable;

    // Hide a grouped column
    [TableColumn("HideGroup"), TableHide]
    public int hideMeGroup1;

    [TableColumn("HideGroup")]
    [ShowInInspector] private const int HideMeGroup2 = 2;

}

[Table]
public List<MyStruct> myStructs;

video

TableHeaders/TableHeadersHide

[!NOTE] This feature is UI Toolkit only

You can use TableHeaders to default show some columns for the table, or TableHeadersHide to hide them.

Note: this does not remove the header, but hide it by default. You can still toggle it in header - right click menu.

Thus, it'll only affect the appearance when the table is rendered, and will NOT dynamicly update it, unless you select away and back, as it will trigger the re-paint process.

Parameters:

  • string headers...: the headers to show/hide.

    If it starts with $, a callback/property/field value is used. The target must return a string, or IEnumerable<string>

using SaintsField;

[Serializable]
public struct TableHeaderStruct
{
    public int i1;

    [TableColumn("Custom Header")] public int i2;
    [TableColumn("Custom Header")] [Button] private void D() {}

    public string str1;
    public string str2;

    [TableColumn("String")] public string str3;
    [TableColumn("String")] public string str4;

    public string str5;
    public string str6;
}

[Table]
[TableHeaders(  // what should be shown by default
    nameof(TableHeaderStruct.i1),  // directly name
    "Custom Header",  // directly custom name
    "$" + nameof(showTableHeader),  // callback of a single name
    "$" + nameof(ShowTableHeaders))  // callback of mutiple names
]
public TableHeaderStruct[] tableStruct;

[Table]
[TableHeadersHide(  // what should be hidden by default
        nameof(TableHeaderStruct.i1),  // directly name
        "Custom Header",  // directly custom name
        "$" + nameof(showTableHeader),  // callback of a single name
        "$" + nameof(ShowTableHeaders))  // callback of mutiple names
]
public TableHeaderStruct[] tableHideStruct;

[Space]
public string showTableHeader = nameof(TableHeaderStruct.str2);

protected virtual IEnumerable<string> ShowTableHeaders() => new[]
{
    nameof(TableHeaderStruct.str5),
    nameof(TableHeaderStruct.str6),
};

Then you can inherent or change field to make the table display differently

public class TableHeadersExampleInh : TableHeadersExample
{
    protected override IEnumerable<string> ShowTableHeaders() => new[]
    {
        "String",
    };
}

Results:

image

ShowInInspector

[!IMPORTANT] Enable SaintsEditor before using

Show a non-serialized target, be a field, property or a method.

This attribute allow you to edit the corresponding field like odin do. It does not use custom drawer even the type has one (Same as Odin)

// Please ensure you already have SaintsEditor enabled in your project before trying this example
using SaintsField.Playa;

// const
[ShowInInspector] public const float MyConstFloat = 3.14f;
// static
[ShowInInspector] public static readonly Color MyColor = Color.green;

// auto-property
[ShowInInspector]
public Color AutoColor
{
    get => Color.green;
    set {}
}

show_in_inspector

You can use it on a function to show a computed value. If parameters / return value is provided, they'll be shown too.

using SaintsField.Playa;
// A function is also supported
[ShowInInspector]
private string Function() => $"Function is supported ({Random.Range(0, 10)})";

// Make a function like a real time calculator
[ShowInInspector]
private int AddCalculator(int a, int b)
{
    return a + b;
}

// class, struct and unity object are supported too
private class ClassType
{
    public Transform Value;
}
[ShowInInspector]
private ClassType GetClassType(int index) => new ClassType { Value = childrenTrans[index % childrenTrans.Length] };

video

A null-class can be created, edited and set to null:

// Please ensure you already have SaintsEditor enabled in your project before trying this example
using SaintsField.Playa;

private class MyClass
{
    public string MyString;
}

[ShowInInspector] private MyClass _myClass;
[ShowInInspector] private MyClass _myClassD = new MyClass
{
    MyString = "Hi",
};

video

An array/list can be created, edited and set to null:

using SaintsField;

private class MyClass
{
    public string MyString;
    public GameObject MyObj;
    private MyEnum _myEnum;
}

[ShowInInspector] private Color[] _colors = {Color.red, Color.green, Color.blue};
[ShowInInspector, Ordered] private Color[] _colorEmptyArray;

[Button, Ordered]
private void ArrayChange0ToRed()
{
    _colorEmptyArray[0] = Color.red;
}

[ShowInInspector, Ordered] private MyClass[] _myClasses;

video

It can also create/edit an interface. Depending on the actual type is Unity Object or general class/struct, it'll show object picker or field editor accordingly.

public class GeneralDummyClass: IDummy
{
    public string GetComment()
    {
        return "DummyClass";
    }

    public int MyInt { get; set; }
    public int GenDumInt;
    public string GenDumString;
}

[ShowInInspector] private static IDummy _dummy;

[Button]
private void DebugDummy() => Debug.Log(_dummy);

video

dictionary/IReadOnlyDictionary is now supported

[ShowInInspector] private Dictionary<string, int> _myDictionaryNull;

[Button]
private void DictExternalAdd()
{
    _myDictionaryNull["External"] = 1;
}

video

Supported Attributes: ShowInInspector can work together with many attributes, please see each attribute section to know if it's been supported.

Numerical

Rate

A rating stars tool for an int field.

Parameters:

  • int min minimum 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 min number of fixed stars that you can not un-rate.

  • int max maximum value of the rating. Must be greater than min.

  • Allow Multiple: No

using SaintsField;

[Rate(0, 5)] public int rate0To5;
[Rate(1, 5)] public int rate1To5;
[Rate(3, 5)] public int rate3To5;

video

It works with ShowInInspector:

[ShowInInspector, Rate(0, 5)]
public int ShowRate0To5
{
    get => rate0To5;
    set => rate0To5 = value;
}

It works with ShowInInspector/Button parameters and return value:

[ShowInInspector]
[Rate(1, 5)]
private int ShowRate([Rate(0, 5)] int rate) => rate;

[Button]
[Rate(1, 5)]
private int ShowRate([Rate(0, 5)] int rate) => rate;

PropRange

Very like Unity's Range but allow you to dynamically change the range, plus allow to set range step.

Supports int, uint, short, ushort, byte, sbyte, long, ulong, float, double

For each argument:

  • string minCallback or float min: the minimum value of the slider, or a property/callback name.
  • string maxCallback or float max: the maximum value of the slider, or a property/callback name.
  • float step=-1f: the step for the range. <= 0 means 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;

range

Adapt

PropRange can work with [Adapt(EUnit.Percent)] to show a percent value, but still get the actual float value:

[
    PropRange(0f, 1f, step: 0.05f),
    Adapt(EUnit.Percent),
    OverlayText("<color=gray>%", end: true),
    BelowText("$" + nameof(DisplayActualValue)),
] public float stepRange;

private string DisplayActualValue(float av) => $"<color=gray>Actual Value: {av}";

video

PropRange can work with ShowInInspector

[ShowInInspector, PropRange(nameof(min), nameof(max))]
public int RawPropRange
{
    get => propRange;
    set => propRange = value;
}

PropRange can work with ShowInInspector/Button parameters and return value

[ShowInInspector]
private int ShowPropR([PropRange(0, 100)] int p) => p;

[ShowInInspector]
private int ShowPropR([PropRange(0, 100)] int p) => p;

MinMaxSlider

A range slider for Vector2 or Vector2Int

For each argument:

  • int|float min or string minCallback: the minimum value of the slider, or a property/callback name.

  • int|float max or string maxCallback: the maximum value of the slider, or a property/callback name.

  • int|float step=1|-1f: the step of the slider, <= 0 means no limit. By default, int type use 1 and float type use -1f

  • float minWidth=50f: (IMGUI Only) the minimum width of the value label. < 0 for auto size (not recommended)

  • float maxWidth=50f: (IMGUI Only) the maximum width of the value label. < 0 for 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;

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

video

MinMaxSlider can work with ShowInInspector

[ShowInInspector, MinMaxSlider(-1f, 3f, 0.3f)]
private Vector2 ShowVector2Step03
{
    get => vector2Step03;
    set => vector2Step03 = value;
}

MinMaxSlider can work with ShowInInspector/Button parameters and return value

[ShowInInspector]
private Vector2Int MinMaxV2([MinMaxSlider(-10, 10)] Vector2Int minMax) => minMax;

[ShowInInspector]
private Vector2Int MinMaxV2([MinMaxSlider(-10, 10)] Vector2Int minMax) => minMax;

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, <= 0 means 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 a EColor, Color, a name of EColor/Color, or a hex color string (starts with #). This will override color parameter.

  • 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
    ),
]
[NoLabel]  // 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}";

video

ProgressBar can work with ShowInInspector

[ShowInInspector, ProgressBar(0, 10)]
public int ShowMyHp
{
    get => myHp;
    set => myHp = value;
}

ProgressBar can work with ShowInInspector/Button parameters and return value

[ShowInInspector]
private int ProgressBar([ProgressBar(0, 10)] int hp) => hp;

[Button]
private int ProgressBar([ProgressBar(0, 10)] int hp) => hp;

Animation

AnimatorParam

A dropdown selector for an animator parameter.

  • string animatorName=null

    name of the animator. When omitted, it will try to get the animator from the current component

  • (Optional) AnimatorControllerParameterType animatorParamType

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

animator_params

It works with ShowInInspector:

[ShowInInspector, AnimatorParam(nameof(MyAnimator))]
public int ShowAnimParamHash
{
    get => animParamHash;
    set => animParamHash = value;
}

[ShowInInspector]
[AnimatorParam]
private int ShowAnimatorParam([AnimatorParam] string animName) => Animator.StringToHash(animName);

[Button]
[AnimatorParam]
private int ShowAnimatorParam([AnimatorParam] string animName) => Animator.StringToHash(animName);

AnimatorState

A dropdown selector for animator state.

  • string animatorName=null

    name 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 layerIndex index of layer
  • int stateNameHash hash value of state
  • string stateName actual state name
  • float stateSpeed the Speed parameter of the state
  • string stateTag the Tag of the state
  • string[] subStateMachineNameChain the substate machine hierarchy name list of the state

AnimatorState added the following attribute(s):

  • AnimationClip animationClip is the actual animation clip of the state (can be null). It has a length value for the length of the clip. For more detail see Unity Doc of AnimationClip

Special Note: using AniamtorState/AnimatorStateBase with OnValueChanged, you will get a AnimatorState on the callback.

using SaintsField;

[AnimatorState]
public string stateName;

[AnimatorState(nameof(MyAnimator))]  // you can specific an animator
public AnimatorState state;

public AnimatorStateBase stateBase;

animator_state

It works with ShowInInspector:

[ShowInInspector]
private AnimatorState ShowAnimatorState
{
    get => animatorState;
    set => animatorState = value;
}

It works with ShowInInspector/Button parameters and return value

[ShowInInspector]
[AnimatorState]
private string ShowAnimatorState([AnimatorState] string animName) => animName;

[Button]
[AnimatorState]
private string ShowAnimatorState([AnimatorState] string animName) => animName;

CurveRange

A curve drawer for AnimationCurve which allow to set bounds and color

Override 1:

  • Vector2 min = Vector2.zero bottom left for bounds
  • Vector2 max = Vector2.one top right for bounds
  • EColor color = EColor.Green curve line color

Override 2:

  • float minX = 0f bottom left x for bounds
  • float minY = 0f bottom left y for bounds
  • float maxX = 1f top right x for bounds
  • float maxY = 1f top right y for bounds
  • EColor color = EColor.Green curve 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;

curverange

It works with ShowInInspector:

[ShowInInspector, CurveRange(EColor.Orange)]
public AnimationCurve ShowCurve1
{
    get => curve1;
    set => curve1 = value;
}

It works with ShowInInspector/Button parameters and return value

[ShowInInspector]
[CurveRange(EColor.Aquamarine)]
private AnimationCurve ShowCurveRange([CurveRange(EColor.YellowNice)] AnimationCurve animCurve) => animCurve;

[Button]
[CurveRange(EColor.Aquamarine)]
private AnimationCurve ShowCurveRange([CurveRange(EColor.YellowNice)] AnimationCurve animCurve) => animCurve;

Auto Getter

Note: You can change the default behavior of these attributes using Window/Saints/Create or Edit SaintsField Config

GetComponent

Automatically assign 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)

  • (Optional) EXP config: config. See Saints XPath-like Syntax section for more information.

    Note: You can change the default behavior of these attributes using Window/Saints/Create or Edit SaintsField Config

  • Type compType = null

    The component type to assign. 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

get_component

GetComponentInChildren/GetInChildren

Automatically assign a component to a field, if the field value is null and the component is already attached to itself or its child GameObjects. (First one found will be used)

NOTE: Like GetComponentInChildren by Unity, this will check the target object itself.

  • (Optional) EXP config: config. See Saints XPath-like Syntax section for more information.

    Note: You can change the default behavior of these attributes using Window/Saints/Create or Edit SaintsField Config

  • bool includeInactive = false

    Should inactive children be included? true to include inactive children.

  • Type compType = null

    The component type to sign. If null, it'll use the field type.

  • bool excludeSelf = false

    When true, skip checking the target itself.

  • string groupBy = ""

    For error message grouping.

  • Allow Multiple: No

GetInChildren by default does NOT check the target object itself, and does INCLUDE inactive objects.

using SaintsField;

[GetComponentInChildren] public BoxCollider childBoxCollider;
// by setting compType, you can assign it as a different type
[GetComponentInChildren(compType: typeof(Dummy))] public BoxCollider childAnotherType;
// and GameObject field works too
[GetComponentInChildren(compType: typeof(BoxCollider))] public GameObject childBoxColliderGo;

get_component_in_children

GetComponentInParent / GetComponentInParents

Automatically assign 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)

Note:

  1. Like Unity's GetComponentInParent, this will check the target object itself.
  2. GetComponentInParent will only check the target & its direct parent. GetComponentInParents will search all the way up to the root.

Parameters:

  • (Optional) EXP config: config. See Saints XPath-like Syntax section for more information.

    Note: You can change the default behavior of these attributes using Window/Saints/Create or Edit SaintsField Config

  • (For GetComponentInParents only) bool includeInactive = false

    Should inactive GameObject be included? true to include inactive GameObject.

    Note: only GetComponentInParents has this parameter!

  • Type compType = null

    The 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;  // equals [GetByXPath("//parent::")]
[GetComponentInParent(typeof(SpriteRenderer))] public GameObject directParentDifferentType;  // equals [GetByXPath("//parent::/[@GetComponent(SpriteRenderer)]")]
[GetComponentInParent] public BoxCollider directNoSuch;

[GetComponentInParents] public SpriteRenderer searchParent;  // equals [GetByXPath("//ancestor::")]
[GetComponentInParents(compType: typeof(SpriteRenderer))] public GameObject searchParentDifferentType;
[GetComponentInParents] public BoxCollider searchNoSuch;

get_component_in_parents

FindObjectsByType/GetInScene

Automatically assign 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)

(Old Name: GetComponentInScene)

  • (Optional) EXP config: config. See Saints XPath-like Syntax section for more information.

    Note: You can change the default behavior of these attributes using Window/Saints/Create or Edit SaintsField Config

  • Type type = null

    The component type to assign. If null, it'll use the field type.

  • bool findObjectsInactive = false

    Should inactive GameObject be included? true to include inactive GameObject.

  • string groupBy = ""

    For error message grouping.

  • Allow Multiple: Yes

[GetInScene(bool includeInactive = true, Type compType = null, string groupBy = "")] is an alias of FindObjectsByType(null, true)

using SaintsField;

[GetComponentInScene] public Dummy dummy;
// by setting compType, you can assign it as a different type
[GetComponentInScene(compType: typeof(Dummy))] public RectTransform dummyTrans;
// and GameObject field works too
[GetComponentInScene(compType: typeof(Dummy))] public GameObject dummyGo;

get_component_in_scene

GetPrefabWithComponent

Automatically assign 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!

  • (Optional) EXP config: config. See Saints XPath-like Syntax section for more information.

    Note: You can change the default behavior of these attributes using Window/Saints/Create or Edit SaintsField Config

  • Type compType = null

    The component type to sign. If null, it'll use the field type.

  • string groupBy = ""

    For error message grouping.

  • Allow Multiple: Yes

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;

get_prefab_with_component

GetScriptableObject

Automatically assign a ScriptableObject file to this field. (First one found will be used)

Recommended to use it with Expandable!

  • (Optional) EXP config: config. See Saints XPath-like Syntax section for more information.

    Note: You can change the default behavior of these attributes using Window/Saints/Create or Edit SaintsField Config

  • string pathSuffix=null the path suffix for this ScriptableObject. null for no limit. for example: if it's /Resources/mySo, it will only assign the file whose path is ends with /Resources/mySo.asset, like Assets/proj/Resources/mySo.asset

  • Allow Multiple: Yes

using SaintsField;

[GetScriptableObject] public Scriptable mySo;
[GetScriptableObject("RawResources/ScriptableIns")] public Scriptable mySoSuffix;

GetScriptableObject

GetInSiblings

Automatically assign a sibling target.

  • (Optional) EXP config: config. See Saints XPath-like Syntax section for more information.

  • bool includeInactive = true

    Should inactive GameObject be included? true to include inactive GameObject.

  • Type compType = null

    The component type to assign. If null, it'll use the field type.

  • Allow Multiple: Yes

using SaintsField;

[GetInSiblings] public SpriteRenderer sr;
[GetInSiblings] public SpriteRenderer[] srArray;

GetByXPath

Note: You can change the default behavior of these attributes using Edit/Project Settings/SaintsField/Config

Please read Saints XPath-like Syntax section for more information.

Parameters

  • (Optional) EXP config: config tweak

  • string path...: resource searching paths.

    Using $ as a start to get a path from a callback/property/field.

  • Allow multiple: Yes. With multiple decorators, all results from each decorator will be used.

Showcase:

// get the main camera from scene
[GetByXPath("scene:://[@Tag = MainCamera]")] public Camera mainCamera;

// only allow the user to pick from the target folder, which the `Hero` script returns `isAvaliable` as true
[GetByXPath(EXP.JustPicker, "assets::/Art/Heros/*.prefab[@{GetComponent(Hero).isAvaliable}]")]
public GameObject[] heroPrefabs;

// get all prefabs under `Art/Heros` AND `Art/Monsters`
[GetByXPath("assets::/Art/Heros/*.prefab")]
[GetByXPath("assets::/Art/Monsters/*.prefab")]
public GameObject[] entityPrefabs;

// callback: auto find a resource depending on another resource
public Sprite normalIcon;
[GetByXPath("$" + nameof(EditorGetFallbackXPath))]
public Sprite alternativeIcon;

public string EditorGetFallbackXPath() => normalIcon == null
    ? ""
    : $"assets::/Alternative/{AssetDatabase.GetAssetPath(normalIcon)["Assets/".Length..]}";

GetMainCamera

Get main camera (or a gameObject/Component with main camera) from the current scene.

This looks for scene object with Camera component and MainCamera tag. (This is an alias of [GetByXPath("scene:://@{GetComponent(Camera)}[@{tag} = 'MainCamera']")])

using SaintsField;

// Get Main Camera
[GetMainCamera] public Camera mainCameraComp;
// Get the transform that has the main camera
[GetMainCamera] public Transform mainCameraTrans;

AddComponent

Automatically add a component to the current target if the target does not have this component. (This will not assign the component added)

Recommended to use it with GetComponent!

  • Type compType = null

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

add_component

FindComponent

Deprecated: use GetByXPath instead.

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

  • (Optional) EXP config: config. See Saints XPath-like Syntax section for more information.
  • string path a path to search
  • params string[] paths more 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;

find_component

GetComponentByPath

Deprecated: use GetByXPath instead.

Automatically assign a component to a field by a given path.

  • (Optional)EGetComp config

    Options 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 re-sign 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 function:

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:

  • ./sth or sth: direct child object of current object named sth
  • .//sth: any descendant child under current. (descendant::sth)
  • ..//sth: first go to parent, then find the direct child named sth
  • /sth: top level node in current scene named sth
  • //sth: first go to top level, then find the direct child named sth
  • ///sth: first go to top level, then find any node named sth
  • ./get/sth[1]: the child named get of current node, then the second node named sth in the direct children list of get
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;

get_component_by_path

Validate & Restrict

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 assign the required object that has your expected component but still gives you the original typed value to field.

This can also be used when you just want a type reference to a prefab, but Unity does not allow you to pick a prefab because "performance consideration".

Overload:

  • FieldTypeAttribute(Type compType, EPick editorPick = EPick.Assets | EPick.Scene, bool customPicker = true)
  • FieldTypeAttribute(Type compType, bool customPicker)
  • FieldTypeAttribute(EPick editorPick = EPick.Assets | EPick.Scene, bool customPicker = true)

For each argument:

  • Type compType the type of the component you want to pick. null for using current type

  • EPick editorPick where you want to pick the component. Options are:

    • EPick.Assets for assets
    • EPick.Scene for scene objects

    For the default Unity picker: if no EPick.Scene is set, will not show the scene objects. However, omit Assets will still show the assets. This limitation is from Unity's API.

    The custom picker does NOT have this limitation.

  • customPicker show 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;

// this allows you to pick a perfab with field component on, which Unity will only give an empty picker.
[FieldType(EPick.Assets)] public Dummy dummyPrefab;

field_type

OnValueChanged

Call a function every time the field value is changed

  • string callback the callback function name

    It'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.

  • Allow Multiple: Yes

Special Note: AnimatorState will have a different OnValueChanged parameter passed in. See AnimatorState for more detail.

Known Issue:

Unity changed how the TrackPropertyValue and RegisterValueChangeCallback works. Using on a SerializeReference, you can still get the correct callback, but the callback will happen multiple times for one change.

Using OnValueChanged on an array/list of SerializeReference can cause some problem when you add/remove an element: the Console will give error, and the inspector view will display incorrect data. Selecting out then selecting back will fix this issue. However, you can just switch back to the old way if you do not care about the field change in the reference field. (Because Unity, still, does not fix related issues about property tracking...)

These two issues can not be fixed unless Unity fixes it.

See: 1, 2

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

You can use static method too (see the syntax in the end of the document)

// Call `Debug.Log(oj)` on changed
[OnValueChanged(":Debug.Log")] public Object oj;

List/Array Change

[!IMPORTANT] Enable SaintsEditor before using this feature

If you have SaintsEditor enabled, OnValueChanged can response to element add/remove.

For add element, it uses the same signature.

For remove element, it uses

MyCallback(MyType arrayOrListType, int negativeCount);
  • MyType arrayOrListType: the array/list field. Need to be the same as you announced.
  • negativeCount: a negative number to present how many elements has been removed.
// Enable `SaintsEditor` before trying this example
using SaintsField;

[OnValueChanged(nameof(OnArrayChanged))] public string[] arrayChanged;

private void OnArrayChanged(string content, int index)
{
    Debug.Log($"array[{index}]={content}");
}

private void OnArrayChanged(string[] arrayItself, int removedCount)
{
    Debug.Log($"array.length removed {-removedCount}, current length={arrayItself.Length}");
}

Output:

// Click the `+` button
array[1]=Something
// Click the `+` button
array[2]=Something
// Input `1` in the size input in array/list drawer
array.length removed 2, current length=1
// Input `5` in the size input in array/list drawer
array[1]=Something
array[2]=Something
array[3]=Something
array[4]=Something
// Click the `-` button
array.length removed 1, current length=4

OnArraySizeChanged

[!CAUTION] Deprecated. Use OnValueChanged instead.

[!IMPORTANT] Enable SaintsEditor before using

OnValueChanged can not detect if an array/list is changed in size. OnArraySizeChanged attribute will call a callback for that.

Using it together with OnValueChanged to get all changing notification for an array/list.

Parameters:

  • string callback: the callback function when the size changed.
  • Allow Multiple: No
using SaintsField;  // namespace for OnValueChanged
using SaintsField.Playa;  // namespace for OnArraySizeChanged

[Serializable]
public class MyClass  // generic class change is also detectable
{
    public string unique;
}

[OnValueChanged(nameof(ValueChanged))]  // optional
[OnArraySizeChanged(nameof(SizeChanged))]
public MyClass[] myClasses;

public void ValueChanged(MyClass myClass, int index) => Debug.Log($"OnValueChanged: {myClass.unique} at {index}");

// if you do not care about values, just omit the parameters
public void SizeChanged(IReadOnlyList<MyClass> myClassNewValues) => Debug.Log($"OnArraySizeChanged {myClassNewValues.Count}: {string.Join("; ", myClassNewValues.Select(each => each?.unique))}");

ReadOnly/DisableIf/EnableIf

[!IMPORTANT] Enable SaintsEditor before using

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

    Condition: if it should be in edit mode, play mode for Editor or in some prefab stage. By default, (omitting this parameter) it does not check the mode at all.

    See Misc - EMode for more information.

  • object by...

    callbacks or attributes for the condition. For more information, see Callback section

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

// This also works on static/const callbacks using `$:`
[DisableIf("$:" + nameof(Util) + "." + nameof(Util._shouldDisable))] public int disableThis;
// you can put this under another file like `Util.cs`
public static class Util
{
    public static bool _shouldDisable;
}

It also supports 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;

A more complex example:

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 about logic operation:

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))]
[LabelText("readonly=1||2")]
private string _ro1and2;


[SerializeField]
[ReadOnly(nameof(_bool1), nameof(_bool2))]
[LabelText("readonly=1&&2")]
private string _ro1or2;


[SerializeField]
[ReadOnly(nameof(_bool1), nameof(_bool2))]
[ReadOnly(nameof(_bool3), nameof(_bool4))]
[LabelText("readonly=(1&&2)||(3&&4)")]
private string _ro1234;

video

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;

It also supports sub-field, and value comparison like ==, >, <=. Read more in the "Syntax for Show/Hide/Enable/Disable/Required-If" section.

It works with ShowInInspector & Button

// Please ensure you already have SaintsEditor enabled in your project before trying this example
using SaintsField.Playa;

[DisableIf] public int[] justDisable;
[EnableIf] public int[] justEnable;

[DisableIf(nameof(boolValue))] public int[] disableIf;
[EnableIf(nameof(boolValue))] public int[] enableIf;

[DisableIf(EMode.Edit)] public int[] disableEdit;
[DisableIf(EMode.Play)] public int[] disablePlay;
[EnableIf(EMode.Edit)] public int[] enableEdit;
[EnableIf(EMode.Play)] public int[] enablePlay;

[Button, DisableIf(nameof(boolValue))] private void DisableIfBtn() => Debug.Log("DisableIfBtn");
[Button, EnableIf(nameof(boolValue))] private void EnableIfBtn() => Debug.Log("EnableIfBtn");
[Button, DisableIf(EMode.Edit)] private void DisableEditBtn() => Debug.Log("DisableEditBtn");
[Button, DisableIf(EMode.Play)] private void DisablePlayBtn() => Debug.Log("DisablePlayBtn");
[Button, EnableIf(EMode.Edit)] private void EnableEditBtn() => Debug.Log("EnableEditBtn");
[Button, EnableIf(EMode.Play)] private void EnablePlayBtn() => Debug.Log("EnablePlayBtn");

image

FieldEnableIf/FieldDisableIf/FieldReadOnly

Like EnableIf/DisableIf, but:

  1. When using on array/list, it works on every element of array/list rather than array/list itself
  2. Only works on serialized field, not work with ShowInInspector, Button
  3. Using on a single serialized field, it works just like EnableIf/DisableIf

This can be helpful if you can not enable SaintsEditor.

It also supports sub-field, and value comparison like ==, >, <=. Read more in the "Syntax for Show/Hide/Enable/Disable/Required-If" section.

// Control by field/property
public bool enable;
[FieldEnableIf(nameof(enable))] public string byField;

// Control by callback
[FieldEnableIf(nameof(ShouldEnable))] public string byCallback;
// You can omit the parameter if you do not need it
public bool ShouldEnable(string input) => input.Length < 6;

[FieldEnableIf(nameof(ShouldEnableElement))] public string[] forArray;
public bool ShouldEnableElement(string element, int index) => index % 2 == 0;

ShowIf/HideIf

[!IMPORTANT] Enable SaintsEditor before using

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

    Condition: if it should be in edit mode, play mode for Editor or in some prefab stage. By default, (omitting this parameter) it does not check the mode at all.

    See Misc - EMode for more information.

  • object by...

    callbacks or attributes for the condition. For more information, see Callback section.

  • Allow Multiple: 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.Playa;

[ShowIf(nameof(ShouldShow))]
public int showMe;

public bool ShouldShow()  // change the logic here
{
    return true;
}

// This also works on static/const callbacks using `$:`
[HideIf("$:" + nameof(Util) + "." + nameof(_shouldHide))] public int hideMe;
// you can put this under another file like `Util.cs`
public static class Util
{
    [ShowInIspector] private static bool _shouldHide;
}

It also supports enum types. The syntax is like this:

using SaintsField.Playa;

[Serializable]
public enum EnumToggle
{
    Off,
    On,
}
public EnumToggle enum1;
[ShowIf(nameof(enum1), EnumToggle.On)] public string enum1Show;

A more complex example:

using SaintsField.Playa;

[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 about logic operation:

using SaintsField.Playa;

public bool _bool1;
public bool _bool2;
public bool _bool3;
public bool _bool4;

[ShowIf(nameof(_bool1))]
[ShowIf(nameof(_bool2))]
[LabelText("<color=red>show=1||2")]
public string _showIf1Or2;


[ShowIf(nameof(_bool1), nameof(_bool2))]
[LabelText("<color=green>show=1&&2")]
public string _showIf1And2;

[HideIf(nameof(_bool1))]
[HideIf(nameof(_bool2))]
[LabelText("<color=blue>show=!1||!2")]
public string _hideIf1Or2;


[HideIf(nameof(_bool1), nameof(_bool2))]
[LabelText("<color=yellow>show=!(1||2)=!1&&!2")]
public string _hideIf1And2;

[ShowIf(nameof(_bool1))]
[HideIf(nameof(_bool2))]
[LabelText("<color=magenta>show=1||!2")]
public string _showIf1OrNot2;

[ShowIf(nameof(_bool1), nameof(_bool2))]
[ShowIf(nameof(_bool3), nameof(_bool4))]
[LabelText("<color=orange>show=(1&&2)||(3&&4)")]
public string _showIf1234;

[HideIf(nameof(_bool1), nameof(_bool2))]
[HideIf(nameof(_bool3), nameof(_bool4))]
[LabelText("<color=pink>show=!(1||2)||!(3||4)=(!1&&!2)||(!3&&!4)")]
public string _hideIf1234;

video

Example about EMode:

using SaintsField.Playa;

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;

It also supports sub-field, and value comparison like ==, >, <=. Read more in the "Syntax for Show/Hide/Enable/Disable/Required-If" section.

// Please ensure you already have SaintsEditor enabled in your project before trying this example
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");

image

It also supports sub-field, and value comparison like ==, >, <=. Read more in the "Syntax for Show/Hide/Enable/Disable/Required-If" section.

FieldShowIf / FieldHideIf

[!WARNING] Deprecated. Use ShowIf/HideIf instead.

Only use this if you can NOT have SaintsEditor enabled. If you can not use SaintsEditor, you should be able to use an alternative ShowIf/HideIf provided by the editor plugin you're globally using.

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

    Condition: if it should be in edit mode, play mode for Editor or in some prefab stage. By default, (omitting this parameter) it does not check the mode at all.

    See Misc - EMode for more information.

  • object by...

    callbacks or attributes for the condition. For more information, see Callback section.

  • AllowMultiple: Yes

You can use multiple FieldShowIf, FieldShowIf, and even a mix of the two.

For FieldShowIf: The field will be shown if ALL condition is true (and operation)

For FieldShowIf: 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, [FieldShowIf(A...), FieldShowIf(B...)] will be shown if FieldShowIf(A...) || FieldShowIf(B...) is true.

FieldHideIf is the opposite of FieldShowIf. Please note "the opposite" is like the logic operation, like !(A && B) is !A || !B, !(A || B) is !A && !B.

  • FieldHideIf(A) == FieldShowIf(!A)
  • FieldHideIf(A, B) == FieldHideIf(A || B) == FieldShowIf(!(A || B)) == FieldShowIf(!A && !B)
  • [FieldHideIf(A), FieldHideIf(B)] == [FieldShowIf(!A), FieldShowIf(!B)] == FieldShowIf(!A || !B)

A simple example:

using SaintsField;

[FieldShowIf(nameof(ShouldShow))]
public int showMe;

public bool ShouldShow()  // change the logic here
{
    return true;
}

// This also works on static/const callbacks using `$:`
[FieldHideIf("$:" + nameof(Util) + "." + nameof(_shouldHide))] public int hideMe;
// you can put this under another file like `Util.cs`
public static class Util
{
    [ShowInIspector] private static bool _shouldHide;
}

It also supports enum types. The syntax is like this:

using SaintsField;

[Serializable]
public enum EnumToggle
{
    Off,
    On,
}
public EnumToggle enum1;
[FieldShowIf(nameof(enum1), EnumToggle.On)] public string enum1Show;

A more complex example:

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
[FieldShowIf(nameof(bool1), nameof(bool2), nameof(enum1), EnumToggle.On, nameof(enum2), EnumToggle.On)] public string bool12AndEnum12;

A more complex example about logic operation:

using SaintsField;

public bool _bool1;
public bool _bool2;
public bool _bool3;
public bool _bool4;

[FieldShowIf(nameof(_bool1))]
[FieldShowIf(nameof(_bool2))]
[LabelText("<color=red>show=1||2")]
public string _showIf1Or2;


[FieldShowIf(nameof(_bool1), nameof(_bool2))]
[LabelText("<color=green>show=1&&2")]
public string _showIf1And2;

[FieldHideIf(nameof(_bool1))]
[FieldHideIf(nameof(_bool2))]
[LabelText("<color=blue>show=!1||!2")]
public string _hideIf1Or2;


[FieldHideIf(nameof(_bool1), nameof(_bool2))]
[LabelText("<color=yellow>show=!(1||2)=!1&&!2")]
public string _hideIf1And2;

[FieldShowIf(nameof(_bool1))]
[FieldHideIf(nameof(_bool2))]
[LabelText("<color=magenta>show=1||!2")]
public string _showIf1OrNot2;

[FieldShowIf(nameof(_bool1), nameof(_bool2))]
[FieldShowIf(nameof(_bool3), nameof(_bool4))]
[LabelText("<color=orange>show=(1&&2)||(3&&4)")]
public string _showIf1234;

[FieldHideIf(nameof(_bool1), nameof(_bool2))]
[FieldHideIf(nameof(_bool3), nameof(_bool4))]
[LabelText("<color=pink>show=!(1||2)||!(3||4)=(!1&&!2)||(!3&&!4)")]
public string _hideIf1234;

video

Example about EMode:

using SaintsField;

public bool boolValue;

[FieldShowIf(EMode.Edit)] public string showEdit;
[FieldShowIf(EMode.Play)] public string showPlay;

[FieldShowIf(EMode.Edit, nameof(boolValue))] public string showEditAndBool;
[FieldShowIf(EMode.Edit), FieldShowIf(nameof(boolValue))] public string showEditOrBool;

[FieldHideIf(EMode.Edit)] public string hideEdit;
[FieldHideIf(EMode.Play)] public string hidePlay;

[FieldHideIf(EMode.Edit, nameof(boolValue))] public string hideEditOrBool;
[FieldHideIf(EMode.Edit), HideIf(nameof(boolValue))] public string hideEditAndBool;

It also supports sub-field, and value comparison like ==, >, <=. Read more in the "Syntax for Show/Hide/Enable/Disable/Required-If" section.

Required

Reminding a given reference type field to be required.

This will check if the field value is a truly value, which means:

  1. ValuedType like struct will always be truly because struct is not nullable and Unity will fill a default value for it no matter what
  2. It works on reference type and will NOT skip Unity's life-cycle null check
  3. You may not want to use it on int, float (because only 0 is not truly) or bool, but it's still allowed if you insist

If you have addressable installed, using Required on addressable's AssetReference will check if the target asset is valid

If you have RequiredIf, Required will work as a config privider instead. See RequiredIf section for more information.

Parameters:

  • string errorMessage = null Error message. Default is {label} is required
  • EMessageType messageType = EMessageType.Error Custom message type.
  • Allow Multiple: 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;

image

[Required(messageType: EMessageType.Info)]
public GameObject empty2;

Image

RequiredIf

Like Required, but only required if the condition is a truly result.

Parameters:

Arguments:

  • (Optional) EMode editorMode

    Condition: if it should be in edit mode, play mode for Editor or in some prefab stage. By default, (omitting this parameter) it does not check the mode at all.

    See Misc - EMode for more information.

  • object by...

    callbacks or attributes for the condition.

  • Allow Multiple: Yes

You can use multiple RequiredIf. The field will be required if ALL condition is true (and operation)

For multiple RequiredIf: The field will be required if ANY condition is true (or operation)

It also supports sub-field, and value comparison like ==, >, <=. Read more in the "Syntax for Show/Hide/Enable/Disable/Required-If" section.

You can use Required to change the notice message & icon. See the example below

using SaintsField;

[Separator("Depende on other field or callback")]
public GameObject go;
[RequiredIf(nameof(go))]  // if a field is a dependence of another field
public GameObject requiredIfGo;

public int intValue;
[RequiredIf(nameof(intValue) + ">=", 0)]
public GameObject requiredIfPositive;  // if meet some condition; callback is also supported.

[Separator("EMode condition")]

[RequiredIf(EMode.InstanceInScene)]
public GameObject sceneObj;  // if it's a prefab in a scene

[Separator("Suggestion")]

// use as a notice
public Transform hand;
[RequiredIf(nameof(hand))]
[Required("It's suggested to set this field if 'hand' is set", EMessageType.Info)]  // this is now a config provider
public GameObject suggestedIfHand;

[Separator("And")]

// You can also chain multiple conditions as "and" operation
public GameObject andCondition;
[RequiredIf(EMode.InstanceInScene, nameof(andCondition))]
public GameObject instanceInSceneAndCondition;  // if it's a prefab in a scene and 'andCondition' is set

[Separator("Or")]

// You can also chain multiple RequiredIf as "or" operation
public GameObject orCondition;
public int orValue;
[RequiredIf(nameof(orCondition))]
[RequiredIf(nameof(orValue) + ">=", 0)]
public GameObject requiredOr;  // if it's a prefab in a scene and 'andCondition' is set

video

ValidateInput

Validate the input of the field when the value changes.

  • string callback is the callback function to validate the data.

    Parameters:

    1. If the function accepts no arguments, then no argument will be passed
    2. 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.
    3. 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:

    1. If return type is string, then null or empty string for valid, otherwise, the string will be used as the error message
    2. If return type is bool, then true for valid, false for 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}";

video

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;

video

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=false

    If true, it'll allow you to assign 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=true

    Show a custom picker to pick an object. The showing objects are already meet the requirement(s).

  • EPick editorPick=EPick.Assets | EPick.Scene

    The picker type for the custom picker. EPick.Assets for assets, EPick.Scene for scene objects.

  • params Type[] requiredTypes

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

RequireType

ArraySize

A decorator that limit the size of the array or list.

Note: Because of the limitation of PropertyDrawer:

  1. Delete an element will first be deleted, then the array will duplicate the last element.
  2. UI Toolkit: you might see the UI flicked when you remove an element.

Enable SaintsEditor if possible, otherwise:

  1. When the field is 0 length, it'll not be filled to target size.
  2. You can always change it to 0 size.

If you have SaintsEditor enabled, recommend to use it together with ListDrawerSettings, the + & - will be enabled/disabled accordingly.

Parameters:

  • int size the size of the array or list
  • int min min value of the size
  • int max max value of the size
  • string groupBy = "" for error message grouping

Parameters overload:

  • string callback: a callback or property for the size.

    If the value is an integer, the size is fixed to this value.

    If the value is a (int, int) tuple, a Vector2/Vector2Int, the size will be limited to the range. If any value in the range is < 0, then the side is not limited. For example, (1, -1) means the size is at least 1. (-1, 20) means the max size is 20.

    If the value is a Vector3/Vector3Int, then the x, y value will be used as the limit

    If the min >= 0 and the max < min, the max value will be ignored

  • string groupBy = "" for error message grouping

  • Allow Multiple: Yes

For example:

  • [ArraySize(3)] will make the array size fixed to 3
  • [ArraySize(1, 5)] will make the array size range to 1-5 (both included)
  • [ArraySize(min: 1)] will make the array size at least 1 (no max value specific). Useful to require a non-empty array.
using SaintsField;

[ArraySize(3)]
public string[] myArr;

image

using SaintsField;
using SaintsField.Playa;

[MinValue(1), Range(1, 10)] public int intValue;
[ArraySize(nameof(intValue)), ListDrawerSettings] public string[] dynamic1;

[Space]
public Vector2Int v2Value;
[ArraySize(nameof(v2Value)), ListDrawerSettings] public string[] dynamic2;

video

Miscellaneous

Dropdown

A tree dropdown selector. Supports reference type, sub-menu, separator, search, and disabled select item, plus icon.

This is the same as AdvancedDropdown, except it uses a tree view to pick.

This is the recommended way to make a searchable dropdown.

Arguments

  • string funcName=null callback function. Must return either a AdvancedDropdownList<T> or a IEnumerable<object> (list/array etc.). When using on an enum, you can omit this parameter, and the dropdown will use the enum values as the dropdown items. When omitted, it will try to find all the static values from the field type. You can use ../ to get upward callback/property for a callback
  • EUnique unique=EUnique.None: When using on a list/array, a duplicated option can be removed if Enique.Remove, or disabled if EUnique.Disable. No use for non-list/array.
  • Allow Multiple: No

First, it can make a quick searchable dropdown:

[Dropdown(nameof(BookDrop))] public string bookName;

private IEnumerable<string> BookDrop()
{
    return new[]
    {
        "Hackers & Painters, Vol 1",
        "Hackers & Painters, Vol 2",
        "The Art of Unix Programming, Vol 1",
        "The Art of Unix Programming, Vol 2",
        "The Mythical Man-Month, Vol 1",
        "The Mythical Man-Month, Vol 2",
    };
}

Second, it can set labels for these items with rich text support

[Dropdown(nameof(QuickDrop))] public float percent;

private AdvancedDropdownList<float> QuickDrop()
{
    AdvancedDropdownList<float> result = new AdvancedDropdownList<float>
    {
        { "20%", 0.2f },
        { "40%", 0.4f },
        { "60%", 0.6f },
    };
    // `Add` is supported
    result.Add("80%", 0.8f);
    // rich tag is supported
    result.Add($"<color={EColor.GoldenRod}>100%<icon=lightMeter/redLight/>", 1f);
    // disable is supported
    result.Add("120%", 1.2f, true);
    return result;
}

Finally, it support nested items

using SaintsField;

[Dropdown(nameof(AdvDropdown))] public int drops;

public AdvancedDropdownList<int> AdvDropdown()
{
    return new AdvancedDropdownList<int>
    {
        // 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: "star.png"),
                new AdvancedDropdownList<int>("Afternoon", 8),
            },
            new AdvancedDropdownList<int>("Thursday", 4, true, icon: "star.png"),
        },
        // direct value
        new AdvancedDropdownList<int>("Friday", 5, true),
        AdvancedDropdownList<int>.Separator(),
        new AdvancedDropdownList<int>("Saturday", 6, icon: "star.png"),
        new AdvancedDropdownList<int>("Sunday", 7, icon: "star.png"),
    };
}

Example of up-walk

[Serializable]
public struct Down
{
    [Dropdown("../../" + nameof(options))]  // Up walk 2 levels
    public string stringV;
}

[Serializable]
public struct MyStruct
{
    [Dropdown("../" + nameof(options))]  // Up walk 1 level
    public string stringV;
    public Down down;
}

public List<string> options;

public MyStruct myStruct;

It can work with ShowInInspector

using SainsField;

[ShowInInspector, Dropdown(nameof(GetItems))]
public string SelectedItem
{
    get => _selectItem;
    set => _selectItem = value;
}

It can work with Button/ShowInInspcetor function parameters

using SainsField;
using SainsField.Playa;

[ShowInInspector]
[Dropdown(nameof(GetItems))]  // change the returned value display
private string SelectItemWithButton([Dropdown(nameof(GetItems))] string item)
{
    return item;
}

[Button]
[Dropdown(nameof(GetItems))]  // change the returned value display
private string SelectItemWithButton([Dropdown(nameof(GetItems))] string item)
{
    return item;
}

OptionsDropdown / PairsDropdown

Like Dropdown, but allows you to quickly set some const expression value

[!WARNING] UI Toolkit only.

use SaintsField;

[OptionsDropdown(EUnique.Disable, "Hor/Left", "Hor/Right", "Vert/Top", "Vert/Bottom", "Center")]
public string[] treeOpt;

use SaintsField;

public enum Direction
{
    None,
    Left,
    Right,
    Up,
    Down,
    Center,
}

[PairsDropdown("negative/1", -1, "negative/2", 2, "negative/3", -3, "zero", 0, "positive/1", 1, "positive/2", 2, "positive/3", 3)]
public int treeIntOpt;

// useful if you don't want the entire enum
[PairsDropdown(EUnique.Disable, "Hor/<-", Direction.Left, "Hor/->", Direction.Right, "Vert/↑", Direction.Up, "Vert/↓", Direction.Down)]
public Direction[] treeDireOpt;

FlagsDropdown

A searchable dropdown for enum flags (bit mask). Useful when you have a big enum flags type.

using SaintsField;

[Serializable, Flags]
public enum F
{
    [InspectorName("[Null]")]  // InspectorName is optional
    Zero,
    [InspectorName("Options/Value1")]
    One = 1,
    [InspectorName("Options/Value2")]
    Two = 1 << 1,
    [InspectorName("Options/Value3")]
    Three = 1 << 2,
    Four = 1 << 3,
}

[FlagsDropdown]
public F flags;

image

AdvancedDropdown

A dropdown selector. Supports reference type, sub-menu, separator, search, and disabled select item, plus icon.

Known Issue:

  1. IMGUI: Using Unity's AdvancedDropdown. Unity's AdvancedDropdown allows 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

  2. 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 have 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 funcName=null callback function. Must return either a AdvancedDropdownList<T> or a IEnumerable<object> (list/array etc.). When using on an enum, you can omit this parameter, and the dropdown will use the enum values as the dropdown items. When omitted, it will try to find all the static values from the field type. You can use ../ to get upward callback/property for a callback
  • EUnique unique=EUnique.None: When using on a list/array, a duplicated option can be removed if Enique.Remove, or disabled if EUnique.Disable. No use for non-list/array.
  • AllowMultiple: No

AdvancedDropdownList<T>

  • string displayName item name to display

  • T value or IEnumerable<AdvancedDropdownList<T>> children: value means it's a value item. Otherwise, it's a group of items, which the values are specified by children

  • bool disabled = false if item is disabled

  • string icon = null the icon for the item.

    Note: setting an icon for a parent group will result a weird issue on its subpage's title and block the items. This is not fixable unless Unity decide to fix it.

  • bool isSeparator = false if item is a separator. You should not use this, but AdvancedDropdownList<T>.Separator() instead

using SaintsField;

[AdvancedDropdown(nameof(AdvDropdown)), BelowText(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

advanced_dropdown

UI Toolkit

video

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

image

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

image

Example of returning an array/list:

// field:
[AdvancedDropdown(nameof(childTrans))] public Transform selected;
[GetComponentInChildren] public Transform[] childTrans;

// or a callback of IEnumerable:
[AdvancedDropdown(nameof(ChildTrans))] public Transform selectedCallback;
private IEnumerable<Transform> ChildTrans() => transform.Cast<Transform>();

image

Finally, using it on an enum to select one enum without needing to specify the callback function.

If you add RichLabel to the enum, the item name will be changed to the RichLabel content.

[Serializable]
public enum MyEnum
{
    [InspectorName("1")]  // RichLabel is optional. Just for you to have more fancy control
    First,
    [InspectorName("2")]
    Second,
    [InspectorName("3")]
    Third,
    [InspectorName("4/0")]
    ForthZero,
    [InspectorName("4/1")]
    ForthOne,
}

[AdvancedDropdown] public MyEnum myEnumAdvancedDropdown;

image

Also, using on a type like Color to pick a pre-defined static value:

[AdvancedDropdown] public Color builtInColor;
[AdvancedDropdown] public Vector2 builtInV2;
[AdvancedDropdown] public Vector3Int builtInV3Int;

image

Example of up-walk

[Serializable]
public struct Down
{
    [AdvancedDropdown("../../" + nameof(options))]  // Up walk 2 levels
    public string stringV;
}

[Serializable]
public struct MyStruct
{
    [AdvancedDropdown("../" + nameof(options))]  // Up walk 1 level
    public string stringV;
    public Down down;
}

public List<string> options;

public MyStruct myStruct;

AdvancedOptionsDropdown / AdvancedPairsDropdown

Like AdvancedDropdown, but allows you to quickly set some const expression value

Useful when you don't want the entire enum

use SaintsField;

[AdvancedOptionsDropdown(0.5f, 1f, 1.5f, 2f, 2.5f, 3f)]
public float floatOpt;

[AdvancedOptionsDropdown(EUnique.Disable, "Left", "Right", "Top", "Bottom", "Center")]
public string[] stringOpt;

use SaintsField;

[AdvancedPairsDropdown("negative/1", -1, "negative/2", 2, "negative/3", -3, "zero", 0, "positive/1", 1, "positive/2", 2, "positive/3", 3)]
public int intOpt;

public enum Direction
{
    None,
    Left,
    Right,
    Up,
    Down,
    Center,
}

// useful if you don't want the entire enum
[AdvancedPairsDropdown(EUnique.Disable, "<-", Direction.Left, "->", Direction.Right, "↑", Direction.Up, "↓", Direction.Down)]
public Direction[] direOpt;

MenuDropdown

A dropdown selector. Supports reference type, sub-menu, separator, and disabled select item.

If you want a searchable dropdown, see AdvancedDropdown.

  • string funcName=null callback function. Must return a DropdownList<T>. When using on an enum, you can omit this parameter, and the dropdown will use the enum values as the dropdown items.

  • bool slashAsSub=true treat / as a sub item.

    Note: In IMGUI, this just replace / to Unicode \u2215 Division Slash ∕, and WILL have a little bit of overlap with nearby characters.

  • EUnique unique=EUnique.None: When using on a list/array, a duplicated option can be removed if Enique.Remove, or disabled if EUnique.Disable. No use for non-list/array.

  • AllowMultiple: No

If you're using UI Toolkit, the search box can also search the path too (rather than just the value).

Example

using SaintsField;

[MenuDropdown(nameof(GetDropdownItems))] public float _float;

public GameObject _go1;
public GameObject _go2;
[MenuDropdown(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},
};

dropdown

To control the separator and disabled item

using SaintsField;

[MenuDropdown(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

color

The look in the UI Toolkit with slashAsSub: false:

dropdown_ui_toolkit

Finally, using it on an enum to select one enum without needing to specify the callback function.

If you add RichLabel to the enum, the item name will be changed to the RichLabel content.

[Serializable]
public enum MyEnum
{
    [InspectorName("1")]  // InspectorName is optional. Just for you to have more fancy control
    First,
    [InspectorName("2")]
    Second,
    [InspectorName("3")]
    Third,
    [InspectorName("4/0")]
    ForthZero,
    [InspectorName("4/1")]
    ForthOne,
}

[MenuDropdown] public MyEnum myEnumDropdown;

image

CustomContextMenu

[!IMPORTANT] Enable SaintsEditor before using

Add a context menu (right click) item for a target

Parameters:

  • string funcName null: which function should it call when selected. If null, add an seperator instead.

  • string menuName = null: menu item name.

    If null, use "funcName" as name.

    If starts with $, use a callback instead. Callback can must be one of:

    string MyMenuName(MyFieldType fieldValue);
    string MyMenuName();
    (string menuName, EContextMenuStatus menuStatus)  MyMenuName(MyFieldType fieldValue);
    (string menuName, EContextMenuStatus menuStatus)  MyMenuName();
    

EContextMenuStatus can be:

  • Normal
  • Checked
  • Disabled

Allow Multiple: Yes

Simple Example:

using SaintsField;

[CustomContextMenu(nameof(MyCallback))]  // use default name
[CustomContextMenu(nameof(Func1), "Custom/Debug")]  // use sub item
[CustomContextMenu(nameof(Func2), "Custom/Set")]  // use sub item
public string content;

private void MyCallback()
{
    Debug.Log("clicked on MyCallback");
}
private void Func1(string c)  // you can accept the current field's value
{
    Debug.Log(c);
}
private void Func2()
{
    content = "Hi There";
}

Dynamic item control:

using SaintsField;

[CustomContextMenu(":Debug.Log", "$" + nameof(DynamicMenuCallback))]  // use `:` for static method calling
[CustomContextMenu(nameof(DynamicMenuInfoClick), "$" + nameof(DynamicMenuInfo))]
public string content;

public string DynamicMenuCallback()  // dynamic control the item name;
{
    return $"Random {Random.Range(0, 9)}";
}

public bool hasContextMenu;
public bool isChecked;
public bool isDisabled;

private void DynamicMenuInfoClick()
{
    isChecked = !isChecked;
}

public (string menuName, EContextMenuStatus menuStatus) DynamicMenuInfo()  // control it's label & status
{
    if (!hasContextMenu)
    {
        return (null, default);
    }
    EContextMenuStatus status = EContextMenuStatus.Normal;
    if (isChecked)
    {
        status = EContextMenuStatus.Checked;
    }
    else if (isDisabled)
    {
        status = EContextMenuStatus.Disabled;
    }
    return ($"My Menu {status}", status);
}

video

IEnumerator callback is supported:

using SaintsField;

[CustomContextMenu(nameof(MyGenerator))] // use default name
public int myInt;

private IEnumerator MyGenerator()
{
    for (int i = 0; i < 100; i++)
    {
        Debug.Log(i);
        myInt = i;
        yield return null;
    }
}

video

This attribute works with ShowInInspector

using SaintsField;

[CustomContextMenu("$" + nameof(ResetIntV))]
[ShowInInspector] private int _intV;

private void ResetIntV()
{
    _intV = 0;
}

Using on a function, it'll add context to the whole component target inspector

using SaintsField;

[CustomContextMenu]  // The whole component will have this context menu
private void Wizard()
{
}

Using on a function of a general struct/class, it'll add context to the drawer of this struct/class

using SaintsField;

[Serializable]
public struct MyStruct
{
    public string myString;

    [CustomContextMenu("Set My String")]  // The whole `MyStruct` type will have this context menu
    public void SetString()
    {
        myString = "My Struct Value";
    }
}

public MyStruct myStructWithContextMenu;

FieldCustomContextMenu

Add a context menu (right click) item for a target. Same as CustomContextMenu except:

  1. When used on an array/list, it will work on every element of the array/list, instead of the array/list itself
  2. When used on an array/list, the callback can optionaly addionally receive a index parameter
  3. Does not require SaintsEditor
  4. Only works on serializable field
using SaintsField;

[FieldCustomContextMenu(nameof(Func1), "Custom/Debug")]  // use like a CustomContextMenu
public int myInt;

[FieldCustomContextMenu(nameof(ClickItemRemover), "$" + nameof(ClickItemRemoverLabel))]
public List<string> lis;

private void ClickItemRemover(object _, int index)
{
    lis.RemoveAt(index);
}

private string ClickItemRemoverLabel(string value, int index) => $"Delete {index}({value})";

ValueButtons

Like tree dropdown, but this will list all options as buttons

Parameters:

  • string funcName=null callback function. Must return either a OptionDropdownList<T> or a IEnumerable (list/array etc.).

    When using on an enum, you can omit this parameter, and the dropdown will use the enum values as the dropdown items.

    When using on a bool, you can omit thiss parameter, and a True, a False button will show.

    When omitted, it will try to find all the static values from the field type.

  • EUnique unique=EUnique.None: When using on a list/array, a duplicated option can be removed if Enique.Remove, or disabled if EUnique.Disable. No use for non-list/array.

  • Allow Multiple: No

Use property/field/function as options

using SaintsField;

public List<string> stringItems;
[ValueButtons(nameof(stringItems))] public string clickAButton;

[GetComponentInChildren] public Transform[] transOpts;
[ValueButtons(nameof(transOpts))] public Transform transformSelect;  // This will use the object's `.ToString()` as button label

// Use a function (list, array, etc.)
private IEnumerable<Transform> GetTransOpts() => transOpts; // list, array, anything that is IEnumerable
[ValueButtons(nameof(GetTransOpts))] public Transform transformCallback;

You can also control the enable/disable using callback

using SaintsField;

// Use OptionList for a bit more control
private OptionList<Transform> GetTransAdvanced()
{
    OptionList<Transform> result = new OptionList<Transform>
    {
        {transOpts[0].name, transOpts[0]},  // inline add
    };

    // direct add
    result.Add(transOpts[1].name, transOpts[1], true);  // true means disabled
    // rich tags are supported
    result.Add($"<color={EColor.Aquamarine}><icon=star.png/> {transOpts[1].name}", transOpts[2]);
    result.Add(transOpts[3].name, transOpts[3]);
    return result;
}
[ValueButtons(nameof(GetTransAdvanced))] public Transform transformAdvanced;

Using on an enum to pick one value

Note: this does not allow bitwise/flags select. It only select one value. For Flags, please check EnumToggleButtons

using SaintsField;

// Use on enum
[Serializable]
public enum EnumOpt
{
    First,
    Second,
    Third,
    [InspectorName("<color=lime><label/>")]  // change name is supported
    Forth,
}
[ValueButtons] public EnumOpt myEnum;

Using on a bool to toggle True/False

using SaintsField;

// Use on bool
[ValueButtons] public bool myBool;

Using on a type without callback to get all the static/const values

using SaintsField;

// Use to get const/static from type
[ValueButtons] public Color unityColors;

This can works with ShowInInspector

[ShowInInspector, ValueButtons(nameof(stringItems))]
public string ShowClickAButton
{
    get => clickAButton;
    set => clickAButton = value;
}

It works with ShowInInspector/Button parameters & return value

[ShowInInspector]
[ValueButtons(nameof(ValueButtonsResultProvider))]
private int ShowValueButton([ValueButtons(nameof(ValueButtonsOptionProvider))] int opt) => opt;

[Button]
[ValueButtons(nameof(ValueButtonsResultProvider))]
private int ShowValueButton([ValueButtons(nameof(ValueButtonsOptionProvider))] int opt) => opt;

private AdvancedDropdownList<int> ValueButtonsOptionProvider() => new AdvancedDropdownList<int>
{
    { "<icon=lightMeter/greenLight/>", 2 },
    { "<icon=lightMeter/redLight/>", 4 },
};

private AdvancedDropdownList<int> ValueButtonsResultProvider() => new AdvancedDropdownList<int>
{
    { "<icon=toggle on focus@2x/>", 2 },
    { "<icon=toggle focus@2x/>", 4 },
};

OptionsValueButtons / PairsValueButtons

Select an option directly in the attribute.

[OptionsValueButtons(0.5f, 1f, 1.5f, 2f, 2.5f, 3f)]
public float floatOpt;

Or add labels for these values

[PairsValueButtons(
        "<icon=d_scrollleft/>", Direction.Left,
        "<icon=d_scrollup/>", Direction.Up,
        "<icon=d_scrollright/>", Direction.Right,
        "<icon=d_scrolldown/>", Direction.Down
    )]
public Direction direct;

[PairsValueButtons(
    "<color=brown>Broken", 0,
    "<color=green>Normal", 1,
    "<color=blue>Rare", 2,
    "<color=yellow>Legend", 3
)]
public int quality;

EnumToggleButtons

A toggle buttons group for enum flags (bit mask) or a normal enum. It provides a button to toggle all bits on/off for flags, or a quick selector for normal enum.

This field has compact mode and expanded mode.

Note: Use DefaultExpand if you want it to be expanded by default.

(Old Name: EnumFlags)

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

[EnumToggleButtons] public BitMask myMask;

video

For a normal enum it allows you to do a quick select

[Serializable]
public enum EnumNormal  // normal enum, not flags
{
    First,
    Second,
    [InspectorName("<color=lime><label /></color>")]
    Third,
}

[EnumToggleButtons] public EnumNormal myEnumNormal;

[Serializable]
public enum EnumExpand
{
    Value1,
    Value2,
    Value3,
    Value4,
    Value5,
    Value6,
    Value7,
    Value8,
    Value9,
    Value10,
}
// expand it by default
[EnumToggleButtons, DefaultExpand] public EnumExpand enumExpand;

image

You can use RichLabel to change the name of the buttons. Note: only standard Unity RichText tag is supported at this point.

[Serializable, Flags]
public enum BitMask
{
    None = 0,  // this will be replaced for all/none button
    [InspectorName("M<color=red>1</color>")]
    Mask1 = 1,
    [InspectorName("M<color=green>2</color>")]
    Mask2 = 1 << 1,
    [InspectorName("M<color=blue>3</color>")]
    Mask3 = 1 << 2,
    [InspectorName("M4")]
    Mask4 = 1 << 3,
    Mask5 = 1 << 4,
}

[EnumToggleButtons]
public BitMask myMask;

image

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.

  • Allow Multiple: No
using SaintsField;

[SerializeField, ResizableTextArea] private string _short;
[SerializeField, ResizableTextArea] private string _long;
[SerializeField, LabelText(null), ResizableTextArea] private string _noLabel;

video

It works with ShowInInspector

[ShowInInspector, ResizableTextArea]
public string RawTa
{
    get => ta;
    set => ta = value;
}

It can work with ShowInInspector

[ShowInInspector, ResizableTextArea]
private string TextArea
{
    get => _textArea;
    set => _textArea = value;
}

It can work with ShowInInspector/Button parameters and return value

[Button, ResizableTextArea]
private string TestTextAreaBtn([ResizableTextArea] string text)
{
    return "Button: " + text;
}

[ShowInInspector, ResizableTextArea]
private string TestTextAreaShowInInspector([ResizableTextArea] string text)
{
    return "ShowInInspector: " + text;
}

LeftToggle

A toggle button on the left of the bool field. Only works on boolean field.

using SaintsField;

[LeftToggle] public bool myToggle;
[LeftToggle, LabelText("<color=green><label />")] public bool richToggle;

left_toggle

ResourcePath

A tool to pick a resource path (a string) with:

  1. required types or interfaces
  2. display a type instead of showing a string
  3. pick a suitable object using a custom picker

Parameters:

  • EStr eStr = EStr.Resource: which kind of string value you expected:

    • Resource: a resource path
    • AssetDatabase: 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:

    true to allow to assign any object, and gives a message if the signed value does not match.

    false to only allow to assign 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 like GameObject, 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 the types can be signed.

  • AllowMultiple: No

Known Issue: IMGUI, manually assign a null object by using Unity's default pick will assign 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;

resource_path

ResourceFolder

A folder picker to pick a resource folder under any Resources. It'll give error if the selected folder is not a resource.

Parameters:

  • string folder="" default folder to open. If it's an empty string, it'll first try the current value of the field, then the first Resources folder found.
  • string title="Choose a folder inside resources" title of the picker
  • string groupBy = "" See the GroupBy section
using SaintsField;

[ResourceFolder] public string resourcesFolder;
[ResourceFolder] public string[] resourcesFolders;

video

FieldDefaultExpand

Expand every element inside an array/list. Works on normal field too.

using SaintsField;

[Serializable]
public struct SaintsRowStruct
{
    [LayoutStart("Hi", ELayout.TitleBox)]
    public string s1;
    public string s2;

}

[FieldDefaultExpand]
public SaintsRowStruct[] expandEveryElement;

[FieldDefaultExpand]
public SaintsRowStruct defaultStruct;

[FieldDefaultExpand, SaintsRow] public SaintsRowStruct row;

[FieldDefaultExpand, GetScriptableObject, Expandable] public Scriptable so;

[Serializable, Flags]
public enum BitMask
{
    None = 0,  // this will be replaced for all/none button
    [InspectorName("M<color=red>1</color>")]
    Mask1 = 1,
    [InspectorName("M<color=green>2</color>")]
    Mask2 = 1 << 1,
    [InspectorName("M<color=blue>3</color>")]
    Mask3 = 1 << 2,
    [InspectorName("M4")]
    Mask4 = 1 << 3,
    Mask5 = 1 << 4,
}

[FieldDefaultExpand, EnumFlags] public BitMask mask;

DefaultExpand

[!IMPORTANT] Enable SaintsEditor before using

Expand the field by default.

using SaintsField;

[DefaultExpand]
public string[] arrayDefault;

[DefaultExpand]
public List<string> listDefault;

[DefaultExpand, ListDrawerSettings]
public string[] arrayDrawer;

[Serializable]
public struct TableStruct
{
    public string name;
    public int value;
}

[DefaultExpand] public TableStruct structField;

[DefaultExpand, Table] public TableStruct[] table;

AssetFolder

A folder picker to pick a folder under Assets. It'll give error if the selected folder is outside of Assets.

Parameters:

  • string folder="Assets" default folder to open.
  • string title="Choose a folder inside assets" title of the picker
  • string groupBy = "" See the GroupBy section
using SaintsField;

[AssetFolder] public string assetsFolder;
[AssetFolder] public string[] assetsFolders;

video

AssetPreview

Show an image preview for prefabs, Sprite, Texture2D, Addressable AssetReference, etc. (Internally use AssetPreview.GetAssetPreview), or on type.

Note: Recommended to use AboveImage/BelowImage for image/sprite/texture2D.

  • int width=-1

    preview 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.MaxValue to always fit the view width.

  • int height=-1

    preview height, -1 for auto resize (with the same aspect) using the width

  • EAlign align=EAlign.FieldStart

    Align of the preview image. Options are Start, End, Center, FieldStart

  • bool above=false

    if true, render above the field instead of below

  • string groupBy=""

    See the GroupBy section

  • AllowMultiple: No

using SaintsField;

[AssetPreview(20, 100)] public Texture2D _texture2D;
[AssetPreview(50)] public GameObject _go;
[AssetPreview(above: true)] public Sprite _sprite;

asset_preview

AboveImage/BelowImage

Show an image above/below the field.

  • string image = null

    An image to display. This can be a property or a callback, which returns a Sprite, Texture2D, SpriteRenderer, UI.Image, UI.RawImage, UI.Button, or Addressable AssetReference type.

    If it's null, it'll try to get the image from the field itself.

    You can use path to specify a hierarchy target. See the example below.

  • string maxWidth=-1

    preview 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.MaxValue to always fit the view width.

  • int maxHeight=-1

    preview max height, -1 for auto resize (with the same aspect) using the width

  • EAlign align=EAlign.Start

    Align of the preview image. Options are Start, End, Center, FieldStart

  • string groupBy=""

    See the GroupBy section

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

show_image

You can use ./Path/Of/Current/Field (starts with ./) to find a target from current field's hierarchy. This is useful when you want to show an image inside a prefab.

Using /Path/Of/Current/Target (starts with /) to find a target from current target's hierarchy

// field object, then find the target in hierarchy
[InfoBox("Show Image under subPrefab/SR")]
[BelowImage("./SR", maxWidth: 40)] public GameObject subPrefab;

// find the target in current object's hierarchy
[InfoBox("Show Image under current GameObject/SubWithSpriteRenderer/SR")]
[BelowImage("/SubWithSpriteRenderer/SR", maxWidth: 40)] public string thisSub;

show image in hierarchy

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;

video

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 funcName the callback function name

  • string buttonComp=null the button component name.

    If null, it'll try to get the button component by this order:

    1. the field itself
    2. get the Button component from the field itself
    3. get the Button component from the current target

    If it's not null, the search order will be:

    1. get the field of this name from current target
    2. 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!");
}

buttonaddonclick

OnButtonClick

[!IMPORTANT] Enable SaintsEditor before using

This is a method decorator, which will bind this method to the target button's click event.

Parameters:

  • string buttonTarget=null the target button. null to get it form the current target.
  • object value=null the value passed to the method. Note unity only support bool, int, float, string and UnityEngine.Object. To pass a UnityEngine.Object, use a string name of the target, and set the isCallback parameter to true
  • bool isCallback=false: when value is a string, set this to true to obtain the actual value from a method/property/field
// Please ensure you already have SaintsEditor enabled in your project before trying this example
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}");
}

image

Note:

  1. 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.
  2. 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

[!IMPORTANT] Enable SaintsEditor before using

This is a method decorator, which will bind this method to the target UnityEvent (allows generic type) invoke event.

Parameters:

  • string eventTarget the target UnityEvent. If you have dot in it, it will first find the field (or property/function), then find the target event on the found field using the name after the dot(s) recursively.
  • object value=null the value passed to the method. Note unity only support bool, int, float, string and UnityEngine.Object. To pass a UnityEngine.Object, use a string name of the target, and set the isCallback parameter to true
  • bool isCallback=false: when value is a string, set this to true to obtain the actual value from a method/property/field

Note:

  1. 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.
  2. It'll only check the method name. Which means, if you change the value of the callback, it'll not update the callback value.

Example:

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

image

Example of using dot(s):

// CustomEventChild.cs
public class CustomEventChild : MonoBehaviour
{
    [field: SerializeField] private UnityEvent<int> _intEvent;
}

// CustomEventExample.cs
public class CustomEventExample : MonoBehaviour
{
    public CustomEventChild _child;

    // it will find the `_intEvent` on the `_child` field
    [OnEvent(nameof(_child) + "._intEvent")]
    public void OnChildInt(int int1)
    {
    }
}

ColorPalette

A simple color palette tool to select a color from a list of colors.

Use Window - Saints - Color Palette to manage the color palette.

Parameters:

  • string[] names: the tags of the palette. If null, it'll use all the palette in the project. If it starts with $, then a property/callback will be invoked, which should returns a string (or a collection of string) for the tags.
  • Allow Multiple: No
[ColorPalette] public Color allPalette;

video

Window - Saints - Color Palette:

video

video

Searchable

[!IMPORTANT] Enable SaintsEditor before using

[!NOTE] This is UI Toolkit only

This allows you to search for a field in a MonoBehavior (Component) or ScriptableObject. This is useful is you have a big list of fields.

It will draw a search icon in the header. Once clicked, you can input the field name you want to search.

You can also use Ctrl + F (Command + F on macOS) to open the search bar.

Note: this only search the field name. It does not search the nested fields, and it does not check with the RichLabel, PlayaRichLabel.

using SaintsField.Playa;

[Searchable]
public class SearchableMono : MonoBehaviour
{
    public string myString;
    public int myInt;
    [ShowInInspector] private string MyInspectorString => "Non Ser Prop";
    [ShowInInspector] private string otherInspectorString = "Non Ser Field";
    public string otherString;
    public int otherInt;

    [ListDrawerSettings(searchable: true)]
    public string[] myArray;

    [Serializable]
    public struct MyStruct
    {
        public string MyStructString;
    }

    [Table] public MyStruct[] myTable;
}

video

DateTime

Allows you to pick a datetime using long type.

[!TIP] This will requires you to manually convert long to DateTime. You may want to see Extended Serialization to directly serialize a DateTime type

using SaintsField;

[DateTime]  // Save value in this
public long dt;
// Use this in script
public DateTime MyDateTime => new DateTime(dt);

[ShowInInspector] private long v => dt;

video

It works with ShowInInspector

[ShowInInspector, DateTime]
private long ShowDt
{
    get => dt;
    set => dt = value;
}

It works with ShowInInspector/Button parameters & return value

[ShowInInspector]
[DateTime]
private long ShowDatetime([DateTime] long dt) => dt;

[Button]
[DateTime]
private long ShowDatetime([DateTime] long dt) => dt;

TimeSpan

Allows you to set a timespan using long type.

[!TIP] This will requires you to manually convert long to TimeSpan. You may want to see Extended Serialization to directly serialize a TimeSpan type

using SaintsField;

[DateTime]  // Save value in this
public long ts;
// Use this in script
public TimeSpan MyTimeSpan => new TimeSpan(dt);

[ShowInInspector] private long v => dt;

[ShowInInspector, TimeSpan]  // Show This as TimeSpan
private long _showTsLong;

It works with ShowInInspector

[ShowInInspector, TimeSpan]
private long ShowTsLong
{
    get => dt;
    set => dt = value;
}

It works with ShowInInspector/Button parameters & return value

[ShowInInspector]
[TimeSpan]
private long ShowTimeSpan([TimeSpan] long ts) => ts;

[Button]
[TimeSpan]
private long ShowTimeSpan([TimeSpan] long ts) => ts;

Guid

Allows you to set a Guid using string type.

You can pick a guid from new, empty, current prefab(or prefabs if it's nested), current scriptableObject, or current mono script

[!TIP] This will requires you to manually convert string to Guid. You may want to see Extended Serialization to directly serialize a Guid type

using SaintsField;

[Guid] public string guidString;

Invalid input will get a notice

It works with ShowInInspector

[ShowInInspector, Guid]
public string ShowGuidString
{
    get => guidString;
    set => guidString = value;
}

It works with ShowInInspector/Button parameters & return value

[ShowInInspector]
[Guid]
private string ShowGuid([Guid] string guidString) => guidString;

[Button]
[Guid]
private string ShowGuid([Guid] string guidString) => guidString;

Layout System

Overview

[!IMPORTANT] Enable SaintsEditor before using

Layout system allows you to group severral target (field, property, button etc) together. It also allows you to box it with/without a title box or foldout box.

This will generate:

  1. Temp files under Library/SaintsFieldTemp
  2. Assets/Editor Default Resources/SaintsField/Temp.SaintsFieldSourceParser.additionalfile (You should ignore this in your version control)

You can change configs under Assets/Editor Default Resources/SaintsField/Config.SaintsFieldSourceParser.additionalfile

The field can be groupd as:

using SaintsField.Playa;

[LayoutStart("Group", ELayout.FoldoutBox)]
public int i1;
public int i2;

[LayoutStart("./Sub", ELayout.FoldoutBox)]
public string s1;
public string s2;

[LayoutEnd(".")]
public int i3;
public int i4;

[LayoutEnd]
public GameObjct out1;
public GameObjct out2;

You can also mix it with Button & ShowInInspector

[LayoutStart("Left Hand", ELayout.FoldoutBox)]
public GameObject leftEquipment;
public int leftAttack;
[Button]
public void SetLeftHand() {}

[LayoutStart("Right Hand", ELayout.FoldoutBox)]
public GameObject rightEquipment;
public int rightAttack;
[Button]
public void SetRightHand() {}

[LayoutEnd]
public int hp;
public int mp;

All together showcase:

Layout

[!IMPORTANT] Enable SaintsEditor before using

A layout decorator to group fields.

  • string groupBy the grouping key. Use / to separate different groups and create subgroups.
  • ELayout layout=ELayout.Vertical the layout of the current group. Note this is a EnumFlag, means you can mix with options.
  • bool keepGrouping=false: See LayoutStart below
  • float marginTop = -1f add some space before the layout. -1 for using default spacing.
  • float marginBottom = -1f add some space after the layout. -1 for using default spacing.

For more information, see LayoutStart below

LayoutStart / LayoutEnd

[!IMPORTANT] Enable SaintsEditor before using

LayoutStart allows you to continuously grouping fields with layout, until a new group appears. LayoutEnd will stop the grouping.

LayoutStart(name) is the same as Layout(name, keepGrouping: true)

For LayoutStart:

  • string groupBy same as Layout
  • ELayout layout=0 same as Layout
  • float marginTop = -1f same as Layout
  • float marginBottom = -1f same as Layout

For LayoutEnd:

  • string groupBy=null same as Layout. When null, close all existing groups.

It supports ./SubGroup to create a nested subgroup:

ELayout Options are:

  • Vertical
  • Horizontal
  • Background draw a background color for the whole group
  • Title show the title
  • TitleOut make title more visible. Add this will by default add Title. On IMGUI it will draw a separator between title and the rest of the content. On UI Toolkit it will draw a background color for the title.
  • Foldout allow to fold/unfold this group. If you have no Tab on, then this will automatically add Title
  • Collapse Same as Foldout but is collapsed by default.
  • Tab make this group a tab page separated rather than grouping it
  • TitleBox = Background | Title | TitleOut
  • FoldoutBox = Background | Title | TitleOut | Foldout
  • CollapseBox = Background | Title | TitleOut | Collapse

Example of title:

[LayoutStart("Titled", ELayout.Title)]
public string t1;
public string t2;
public string t3;

[LayoutStart("Titled <color=Chartreuse>Box", ELayout.TitleBox)]
public string b1;
public string b2;
public string b3;

[LayoutStart("Titled<icon=d_orangeLight/>", ELayout.TitleOut)]
public string o1;
public string o2;
public string o3;

Example of foldout:

[LayoutStart("Foldout", ELayout.Foldout)]
public string t1;
public string t2;
public string t3;

[LayoutStart("Foldout <color=DeepSkyBlue>Box", ELayout.Foldout |  ELayout.TitleBox)]
public string b1;
public string b2;
public string b3;

[LayoutStart("<icon=LensFlare Gizmo/>Foldout", ELayout.Foldout | ELayout.TitleOut)]
public string o1;
public string o2;
public string o3;

Example of tabs:

[LayoutStart("Tabs", ELayout.Tab)]
[LayoutStart("./Tab1")]
public string t1;
public string t2;
public string t3;

[LayoutStart("../Tab2")]
public string b1;
public string b2;
public string b3;

[LayoutStart("../Tab3")]
public string o1;
public string o2;
public string o3;

// Mix with title

[LayoutStart("MixedTabs", ELayout.Tab | ELayout.TitleBox | ELayout.Foldout)]
[LayoutStart("./Tab1")]
public string mt1;
public string mt2;
public string mt3;

[LayoutStart("../Tab2")]
public string mb1;
public string mb2;
public string mb3;

[LayoutStart("../Tab3")]
public string mo1;
public string mo2;
public string mo3;

// Colors + Icons

[LayoutStart("Color Tab", ELayout.Tab)]

[LayoutStart("./<color=#FCBF07><icon=d_AudioClip Icon/>Music")]
public string m1;
public string m2;
public string m3;

[LayoutStart("../<color=#34F42B><icon=greenLight/>Light")]
public string l1;
public string l2;
public string l3;

[LayoutStart("../<color=#B0FC58><icon=d_Cloth Icon/>Skin")]
public string skin1;
public string skin2;
public string skin3;

[LayoutStart("../<color=Aquamarine><icon=d_Settings Icon/>Settings")]
public string s1;
public string s2;
public string s3;

[LayoutStart("../<color=Bisque><icon=d_UnityEditor.GameView/>Controller")]
public string g1;
public string g2;
public string g3;

[LayoutStart("../<color=CadetBlue><icon=star.png/>Favorite")]
public string f1;
public string f2;
public string f3;

[LayoutStart("../<color=Chartreuse><icon=AudioSource Gizmo/>Audio")]
public string a1;
public string a2;
public string a3;

Example of horizental

[LayoutStart("Horizontal", ELayout.Horizontal)]
public string t1;
public string t2;
public string t3;

[LayoutStart("HorizontalBox", ELayout.Horizontal | ELayout.TitleBox)]
public string b1;
public string b2;
public string b3;

[LayoutStart("HorizontalFoldout", ELayout.Horizontal | ELayout.FoldoutBox)]
public string o1;
public string o2;
public string o3;

By combining Layout with Seperator/InfoBox, you can create some complex layout struct:

[LayoutStart("Equipment", ELayout.TitleBox | ELayout.Vertical)]
[LayoutStart("./Head", ELayout.TitleBox)]
public string st;
public MyStruct inOneStruct;
[LayoutEnd(".")]

[LayoutStart("./Upper Body", ELayout.TitleBox)]

[InfoBox("Note:left hand can be empty, but not right hand", EMessageType.Warning)]

[LayoutStart("./Horizontal", ELayout.Horizontal)]

[LayoutStart("./Left Hand", ELayout.TitleBox)]
public string g11;
public string g12;
public MyStruct myStruct;
public string g13;

[LayoutStart("../Right Hand", ELayout.TitleBox)]
public string g21;
[LabelText("<color=lime><label/>")]
public string g22;
[LabelText("$" + nameof(g23))]
public string g23;

public bool toggle;

image

If titled box is too heavy, you can use Separator instead. See Separator section for more information

// Please ensure you already have SaintsEditor enabled in your project before trying this example
using SaintsField.Playa;

[LayoutStart("Root", ELayout.FoldoutBox)]
public string root1;
public string root2;

[LayoutStart("./Sub", ELayout.FoldoutBox)]  // equals "Root/Sub"
public string sub1;
public string sub2;
[LayoutEnd(".")]

[LayoutStart("./Another", ELayout.FoldoutBox)]  // equals "Root/Another"
public string another1;
public string another2;

[LayoutEnd(".")]  // equals "Root"
public string root3;  // this should still belong to "Root"
public string root4;

[LayoutEnd]  // this should close any existing group
public string outOfAll;

[LayoutStart("Tabs", ELayout.Tab | ELayout.Collapse)]
[LayoutStart("./Tab1")]
public string tab1Item1;
public int tab1Item2;
[LayoutEnd(".")]

[LayoutStart("./Tab2")]
public string tab2Item1;
public int tab2Item2;

image

example of using LayoutStart with LayoutEnd:

// Please ensure you already have SaintsEditor enabled in your project before trying this example
using SaintsField.Playa;

public string beforeGroup;

[LayoutStart("Group", ELayout.Background | ELayout.TitleOut)]
public string group1;
public string group2;  // starts from this will be automatically grouped into "Group"
public string group3;

[LayoutEnd("Group")]  // this will end the "Group"
public string afterGroup;

image

example of using new group name to stop grouping:

// Please ensure you already have SaintsEditor enabled in your project before trying this example
using SaintsField.Playa;

public string breakBefore;

[LayoutStart("break", ELayout.Background | ELayout.TitleOut)]
public string breakGroup1;
public string breakGroup2;

// this group will stop the grouping of "break"
[LayoutStart("breakIn", ELayout.Background | ELayout.TitleOut)]
public string breakIn1;
public string breakIn2;

[LayoutStart("break")]  // this will be grouped into "break", and also end the "breakIn" group
public string breakGroup3;
public string breakGroup4;

[LayoutEnd("break")]  // end, it will not be grouped
public string breakAfter;

image

example of using keepGrouping: false to stop grouping, but keep the last one in group:

// Please ensure you already have SaintsEditor enabled in your project before trying this example
using SaintsField.Playa;

public string beforeGroupLast;

[LayoutStart("GroupLast")]
public string groupLast1;
public string groupLast2;
public string groupLast3;
[Layout("GroupLast", ELayout.Background | ELayout.TitleOut)]  // close this group, but be included
public string groupLast4;

public string afterGroupLast;

image

LayoutCloseHere / LayoutTerminateHere

[!WARNING] You don't need this for most of the time. The new layout system can handle this quite well.

[!IMPORTANT] Enable SaintsEditor before using

Include the current field into the coresponding group, then:

  • LayoutCloseHere will close the most recent group, like a LayoutEnd(".")
  • LayoutTerminateHere will close all groups, like a LayoutEnd

LayoutCloseHere is useful when you're done with your subgroup, but you might add some field later, but at the point you don't have a field to put a LayoutEnd

[LayoutStart("Tab", ELayout.TitleBox)] public string tab;

[LayoutStart("./1", ELayout.TitleBox)]
public string tab1Sub1;
public string tab1Sub2;
[LayoutCloseHere]
// same as: [Layout(".", keepGrouping: false), LayoutEnd(".")]
public string tab1Sub3;

// some feature day you might add some field below, `LayoutCloseHere` ensures you don't accidently include them into the subgroup
// ... you field added in the feature

[Button]
public void AFunction() {}
[Button]
public void BFunction() {}

image

LayoutTerminateHere is useful when you're done with your group, and your script is also done here (so nowhere to put EndLayout). Oneday you come back and add some new fields, this attribute can avoid them to be included in the group accidently.

[LayoutStart("Tab", ELayout.TitleBox)] public string tab;

[LayoutStart("./1", ELayout.TitleBox)]
public string tab1Sub1;
public string tab1Sub2;
[LayoutTerminateHere]
// same as: [Layout("."), LayoutEnd]
public string tab1Sub3;

[Button]
public void AFunction() {}
[Button]
public void BFunction() {}

image

LayoutDisableIf / LayoutEnableIf

[!IMPORTANT] Enable SaintsEditor before using

Disable or enable an entire layout group. These attributes will work on the first layout underneath it.

Arguments:

  • (Optional) EMode editorMode

    Condition: if it should be in edit mode, play mode for Editor or in some prefab stage. By default, (omitting this parameter) it does not check the mode at all.

    See Misc - EMode for more information.

  • object by...

    callbacks or attributes for the condition.

  • AllowMultiple: Yes

You can use multiple LayoutDisableIf, LayoutEnableIf, and even a mix of the two.

For LayoutDisableIf: The layout group will be disabled if ALL condition is true (and operation)

For LayoutEnableIf: The layout group will be enabled if ANY condition is true (or operation)

For multiple attributes: The layout group will be disabled if ANY condition is true (or operation)

It also supports sub-field, and value comparison like ==, >, <=. Read more in the "Syntax for Show/Hide/Enable/Disable/Required-If" section.

using SaintsField.Playa;

public bool editableMain;

[LayoutEnableIf(nameof(editableMain))]
[LayoutStart("Main", ELayout.FoldoutBox)]
public bool editable1;

[LayoutEnableIf(nameof(editable1))]
[LayoutStart("./1", ELayout.FoldoutBox, marginBottom: 10)]
public int int1;
public string string1;

[LayoutStart("..")]
public bool editable2;

[LayoutEnableIf(nameof(editable2))]
[LayoutStart("./2", ELayout.FoldoutBox)]
public int int2;
public string string2;

[LayoutEnd]
[Space]
public string layoutEnd;

video

LayoutShowIf / LayoutHideIf

[!IMPORTANT] Enable SaintsEditor before using

Show or hide an entire layout group. These attributes will work on the first layout underneath it.

Arguments:

  • (Optional) EMode editorMode

    Condition: if it should be in edit mode, play mode for Editor or in some prefab stage. By default, (omitting this parameter) it does not check the mode at all.

    See Misc - EMode for more information.

  • object by...

    callbacks or attributes for the condition.

  • Allow Multiple: Yes

You can use multiple LayoutShowIf, LayoutHideIf, and even a mix of the two.

For LayoutShowIf: The layout group will be shown if ALL condition is true (and operation)

For LayoutHideIf: The layout group will be hidden if ANY condition is true (or operation)

For multiple attributes: The layout group will be shown if ANY condition is true (or operation)

using SaintsField.Playa;

public bool visibleMain;

[LayoutShowIf(nameof(visibleMain))]
[LayoutStart("Main", ELayout.FoldoutBox)]
public bool visible1;

[LayoutShowIf(nameof(visible1))]
[LayoutStart("./1", ELayout.FoldoutBox, marginBottom: 10)]
public int int1;
public string string1;

[LayoutStart("..")]
public bool visible2;

[LayoutShowIf(nameof(visible2))]
[LayoutStart("./2", ELayout.FoldoutBox)]
public int int2;
public string string2;

[LayoutEnd]
[Space]
public string layoutEnd;

video

Handles

Handles is drawn in the scene view instead of inspector.

When using handles (except SceneViewPicker and DrawLabel), you can use right click to show/hide some handles.

image

SceneViewPicker

Allow you to pick a target from a scene view, then assign it into your field.

Once clicked the picking icon, use left mouse to choose a target. Once a popup is displayed, choose the target you want.

If you just want the closest one, just click, then click again (because the closest one is always at the position of your cursor)

Usage:

  • Left Mouse: pick; when popup is displayed, click away to close the popup
  • Middle Mouse: cancel
using SaintsField;

[SceneViewPicker] public Collider myCollider;
// works with SaintsInterface
[SceneViewPicker] public SaintsObjInterface<IInterface1> interf;

// a notice will diplay if no target is found
[SceneViewPicker] public NoThisInScene noSuch;
// works for list elements too
[SceneViewPicker] public Object[] anything;

video

This feature is heavily inspired by Scene-View-Picker! If you like this feature, please consider go give them a star!

Because Scene-View-Picker does not provide API for script calling, I have to completely re-write the logic for in SaintsField instead of depended on it.

DrawLabel

Draw a text in the view scene where the field object is. The decorated field need to be either a GameObject/Component or a Vector3/Vector2.

This is useful if you want to track an object's state (e.g. a character's basic states) in the scene.

Parameters:

  • [Optional] EColor eColor: color of the label. Default is white.
  • string content = null: the label text to show. Starting with $ to make it an attribute/callback. null means using the field's name.
  • string space = "this": when using on a Vector3 or Vector2, "this" means using current object as the space container, null means world space, otherwise use the space from this callback/field value.
  • string color = null: use a html color if this starts with #, otherwise use a callback/field value as the color.
using SaintsField;

[DrawLabel("Test"), GetComponent]
public GameObject thisObj;

[Serializable]
public enum MonsterState
{
    Idle,
    Attacking,
    Dead,
}

public MonsterState monsterState;

[DrawLabel(EColor.Yellow ,"$" + nameof(monsterState))] public GameObject child;

draw-label

PositionHandle

Draw and use a position handle in the scene. If The decorated field is a GameObject/Component, the handle will just it's position. If the field is a Vector3/Vector2, the handle will write the world/local position to the field.

Parameters:

  • string space="this":the containing space. "this" means using the current target, null means using the world space, otherwise means using a callback or a field value. Only works for Vector3/Vector2 type.

You can use right click to show/hide handles.

Example of using it with vector types + DrawLabel:

using SaintsField;

[PositionHandle(space: null), DrawLabel(nameof(worldPos3), space: null)] public Vector3 worldPos3;
[PositionHandle(space: null), DrawLabel(nameof(worldPos2), space: null)] public Vector2 worldPos2;

[PositionHandle, DrawLabel(nameof(localPos3))] public Vector3 localPos3;
[PositionHandle, DrawLabel(nameof(localPos2))] public Vector2 localPos2;

video

Example of using with objects:

using SaintsField;

[PositionHandle, DrawLabel("$" + nameof(LabelName)), GetComponentInChildren(excludeSelf: true)]
public MeshRenderer[] meshChildren;

private string LabelName(MeshRenderer target, int index) => $"{target.name}[{index}]";

video

DrawLine

Draw a line between different objects. The decorated field need to be a GameObject/Component or a Vector3/Vector2, or a list/array of them.

You can use right click to show/hide handles.

Parameters:

  • string start = null: where does the line start. null for the current field.
  • int startIndex = 0: when start is not null, and the start is a list/array, specify the index of the start.
  • string startSpace = "this": the containing space. "this" means using the current target, null means using the world space, otherwise means using a callback or a field value
  • string end = null: where does the line end. null for the current field.
  • int endIndex = 0: when end is not null, and the end is a list/array, specify the index of the end.
  • string endSpace = "this": the containing space. "this" means using the current target, null means using the world space, otherwise means using a callback or a field value
  • EColor eColor = EColor.White: color
  • float alpha = 1f: the alpha of the color. Not works with color.
  • string color = null: the color of the line. If it starts with #, use html hex color, otherwise use as a callback. This overrides the eColor.
  • float dotted = -1f: when >=0, draw dotted line instead.

And also DrawLineFrom, DrawLineTo as a shortcut to connect current field with another:

  • string target = null: target point of the line from current field
  • int targetIndex = 0: if the target is a list/array, specify the index of the target.
  • string targetSpace = "this": the containing space. "this" means using the current target, null means using the world space, otherwise means using a callback or a field value
  • string space = "this": the containing space. "this" means using the current target, null means using the world space, otherwise means using a callback or a field value
  • EColor eColor = EColor.White: color
  • float alpha = 1f: the alpha of the color. Not works with color.
  • string color = null: the color of the line. If it starts with #, use html hex color, otherwise use as a callback. This overrides the eColor.
  • float dotted = -1f: when >=0, draw dotted line instead.
using SaintsField;

[SerializeField, GetComponent, DrawLabel("Entrance"),
 // connect this to worldPos[0]
 DrawLineTo(target: nameof(localPos), targetIndex: 0, targetSpace: Space.Self),
] private GameObject entrance;

[
    // connect every element in the list
    DrawLine(color: EColor.Green, endSpace: Space.Self),
    // connect every element to the `centerPoint`
    DrawLineTo(target: nameof(centerPoint), color: EColor.Red, colorAlpha: 0.4f),

    DrawLabel("$" + nameof(PosIndexLabel)),
]
public Vector3[] localPos;

[DrawLabel("Center")] public Vector3 centerPoint;

[DrawLabel("Exit"), GetComponentInChildren(excludeSelf: true),
 // connect worldPos[0] to this
 DrawLineFrom(target: nameof(localPos), targetIndex: -1, targetSpace: Space.Self),
] public Transform exit;

private string PosIndexLabel(Vector3 pos, int index) => $"[{index}]\n{pos}";

image

SaintsArrow

Note: this feature requires SaintsDraw 4.0.5 installed

Draw an arrow between different objects. The decorated field need to be a GameObject/Component or a Vector3/Vector2, or a list/array of them.

Parameters:

  • string start = null: where does the arrow start. null for the current field.
  • int startIndex = 0: when start is not null, and the start is a list/array, specify the index of the start.
  • string startSpace = "this": the containing space. "this" means using the current target, null means using the world space, otherwise means using a callback or a field value
  • string end = null: where does the arrow end. null for the current field.
  • int endIndex = 0: when end is not null, and the end is a list/array, specify the index of the end.
  • string endSpace = "this": the containing space. "this" means using the current target, null means using the world space, otherwise means using a callback or a field value
  • EColor eColor = EColor.White: color
  • float alpha = 1f: the alpha of the color. Not works with color.
  • string color = null: the color of the line. If it starts with #, use html hex color, otherwise use as a callback. This overrides the eColor.
  • float dotted = -1f: when >=0, draw dotted line instead.
  • float headLength = 0.5f: the length of the arrow head.
  • float headAngle = 20.0f: the angle of the arrow head.

Specially

  1. using on an array/list without specifying start and end will arrow-connect the element from first to last.
  2. startIndex & endIndex can be negative, which means to count from the end. -1 means the last element.

A complex showcase:

using SaintsField;

[SerializeField, GetComponent, DrawLabel("Entrance"),
 // connect this to worldPos[0]
 SaintsArrow(end: nameof(worldPos), endIndex: 0, endSpace: Space.Self),
] private GameObject entrance;

[
    // connect every element in the list
    SaintsArrow(color: EColor.Green, startSpace: Space.Self, headLength: 0.1f),
    // connect every element to the `centerPoint`
    SaintsArrow(start: nameof(centerPoint), color: EColor.Red, startSpace: Space.Self, endSpace: Space.Self, headLength: 0.1f, colorAlpha: 0.4f),

    PositionHandle,
    DrawLabel("$" + nameof(PosIndexLabel)),
]
public Vector3[] worldPos;

[DrawLabel("Center"), PositionHandle] public Vector3 centerPoint;

[DrawLabel("Exit"), GetComponentInChildren(excludeSelf: true), PositionHandle,
 // connect worldPos[0] to this
 SaintsArrow(start: nameof(worldPos), startIndex: -1, startSpace: Space.Self),
] public Transform exit;

private string PosIndexLabel(Vector3 pos, int index) => $"[{index}]\n{pos}";

video

ArrowHandleCap

Like SaintsArrow but using Unity's default ArrowHandleCap to draw. (No dependency required)

Draw an arrow between different objects. The decorated field need to be a GameObject/Component or a Vector3/Vector2, or a list/array of them.

Parameters:

  • string start = null: where does the arrow start. null for the current field.
  • int startIndex = 0: when start is not null, and the start is a list/array, specify the index of the start.
  • Space startSpace = Space.World: if the start is a Vector3/Vector2, should it be in world space or local space.
  • string end = null: where does the arrow end. null for the current field.
  • int endIndex = 0: when end is not null, and the end is a list/array, specify the index of the end.
  • Space endSpace = Space.World: if the end is a Vector3/Vector2, should it be in world space or local space.
  • EColor eColor = EColor.White: color
  • float alpha = 1f: the alpha of the color. Not works with color.
  • string color = null: the color of the line. If it starts with #, use html hex color, otherwise use as a callback. This overrides the eColor.
  • float dotted = -1f: when >=0, draw dotted line instead.

Specially

  1. using on an array/list without specifying start and end will arrow-connect the element from first to last.
  2. startIndex & endIndex can be negative, which means to count from the end. -1 means the last element.

Example:

using SaintsField;

[SerializeField, GetComponent, DrawLabel("Entrance"),
 // connect this to worldPos[0]
 ArrowHandleCap(end: nameof(worldPos), endIndex: 0),
] private GameObject entrance;

[
    // connect every element in the list
    ArrowHandleCap(eColor: EColor.Green),
    // connect every element to the `centerPoint`
    ArrowHandleCap(end: nameof(centerPoint), eColor: EColor.Red),

    PositionHandle,
    DrawLabel("$" + nameof(PosIndexLabel)),
]
public Vector3[] worldPos;

[DrawLabel("Center"),
 PositionHandle
] public Vector3 centerPoint;

[DrawLabel("Exit"), GetComponentInChildren(excludeSelf: true),
 PositionHandle,
 // connect worldPos[0] to this
 ArrowHandleCap(start: nameof(worldPos), startIndex: -1),
] public Transform exit;

private string PosIndexLabel(Vector3 pos, int index) => $"[{index}]\n{pos}";

image

DrawWireDisc

Like Unity's DrawWireDisc, this attributes allows you to draw a disc in the scene.

The field can be a GameObject, a Component, a Vector2(2D game), a Vector3 (3D game), or a number.

You can specify which parent you want the circle be, the rotate/offset/facing of the circle, and color of course.

Parameters:

  • float radius = 1f: radis of the disk. If the target field is a number, use the field's value instead
  • string radiusCallback = null: use a callback or a field value as the radius. If the target field is a number, use the field's value instead
  • string space = "this": the containing space of the disc. this means using the current target, null means using the world space, otherwise means using a callback or a field value
  • float norX = 0f, float norY = 0f, float norZ = 1f: Vector3 direction for the normal (facing) of the disc. It's facing forward by default
  • string norCallback = null: use a callback or a field value as the normal direction, the value must be a Vector3
  • float posXOffset = 0f, float posYOffset = 0f, float posZOffset = 0f: Vector3 position offset for the disc related to the space
  • string posOffsetCallback = null: use a callback or a field value as the position offset. The value must be a Vector3
  • float rotX = 0f, float rotY = 0f, float rotZ = 0f: rotation of the disc related to space
  • string rotCallback = null: use a callback or a field value as the rotation. The value must be a Quaternion
  • EColor eColor = EColor.White: color
  • float alpha = 1f: the alpha of the color. Not works with color.
  • string color = null: the color of the line. If it starts with #, use html hex color, otherwise use as a callback. This overrides the eColor.
using SaintsField;

// Draw a circle for my character
[DrawWireDisc(radis: 0.2f, EColor.Yellow)] public MyCharacter character2D;

// Draw a circle on the ground for my character (disc facing upward)
// the hight from center to the ground is 0.5f
[DrawWireDisc(norY: 1, norZ: 0, posYOffset: -0.5f)] public MyCharacter character3D;

// Make a struct to let it follow
[Serializable]
public struct PlayerWeapon
{
    [PositionHandle(Space.Self)]
    [DrawWireDisc]
    public Vector3 firePointOffset;

    // your other fields
}

video

A simple example to show debugging a player's alert/idle range:

[GetComponent]
[DrawWireDisc(norY: 1, norZ: 0, posYOffset: -1f, color: nameof(curColor), radiusCallback: nameof(curRadius))]
[DrawLabel(EColor.Brown, "$" + nameof(curStatus))]
public Transform player;

[Range(1f, 1.5f)] public float initRadius;
[Range(1f, 1.5f)] public float alertRadius;

[AdvancedDropdown] public Color initColor;
[AdvancedDropdown] public Color alertColor;

public Transform enemy;

[InputAxis] public string horizontalAxis;
[InputAxis] public string verticalAxis;

[ShowInInspector]
private Color curColor;

[ShowInInspector] private float curRadius = 0.5f;
[ShowInInspector] private string curStatus = "Idle";

private void Awake()
{
    curColor = initColor;
    curRadius = initRadius;
}

public void Update()
{
    Vector3 playerPos = player.position;
    Vector3 enemyPos = enemy.position;

    float distance = Vector3.Distance(playerPos, enemyPos);

    float nowRadius = distance < alertRadius ? alertRadius : initRadius;
    Color nowColor = distance < alertRadius ? alertColor : initColor;
    curStatus = distance < alertRadius ? "Alert" : "Idle";

    curRadius = Mathf.Lerp(curRadius, nowRadius, Time.deltaTime * 10);
    curColor = Color.Lerp(curColor, nowColor, Time.deltaTime * 10);

    float horizontal = Input.GetAxis(horizontalAxis);
    float vertical = Input.GetAxis(verticalAxis);

    Vector3 move = new Vector3(horizontal, 0, vertical);
    player.Translate(move * Time.deltaTime * 3);
}

video

SphereHandleCap

Draw a sphere in the scene like Unity's SphereHandleCap.

Parameters:

  • float radius = 1f: radius of the sphere. If the target field is a number, use the field's value instead
  • string radiusCallback = null: use a callback or a field value as the radius
  • string space = "this": the containing space of the sphere. "this" means using the current target, null means using the world space, otherwise means using a callback or a field value
  • float posXOffset = 0f, float posYOffset = 0f, float posZOffset = 0f: Vector3 position offset for the sphere related to the space
  • string posOffsetCallback = null: use a callback or a field value as the position offset. The value must be a Vector3
  • EColor eColor = EColor.White: color
  • float alpha = 1f: the alpha of the color. Not works with color.
  • string color = null: the color of the line. If it starts with #, use html hex color, otherwise use as a callback. This overrides the eColor.
[DrawLine]  // also draw the lines
[SphereHandleCap(color: "#FF000099", radius: 0.1f)]
public Vector3[] localPos;

[SphereHandleCap(radius: 0.1f)]
public GameObject[] objPos;  // use obj's position

image

Component Header

Component Header allows you to draw extra stuffs on a component like this:

image

If you have SaintsEditor enabled, this works by default.

(If you can not enable SaintsEditor, it can work as stand-alone, go window - Saints - Enable Stand-Alone Header GUI Support)

HeaderButton / HeaderLeftButton

Draw a button in the component header. Method which returns an IEnumerator will start a coroutine

Arguments:

  • string label = null: label of the button. null means using function name. If starts with $, then a dynamic label will be created to use a field/property/callback as label. Dynamic label when returning null or empty string will hide the button. Rich Label supported.
  • string toolTip = null: tool tip for the button.
using SaintsField;
using SaintsField.ComponentHeader;

[HeaderLeftButton]
public void L1()
{
}

[HeaderLeftButton("<color=brown><icon=star.png/>")]
public void OnClickL2()
{
}

[HeaderButton]
public void R1()
{
    Debug.Log("R1");
}

[HeaderButton("<color=lime><icon=star.png/></color>+1", "Add a star")]
public void StartAdd()
{
    rate = (rate + 1) % 6;
}

[HeaderButton("<color=gray><icon=star.png/></color>-1", "Remove a star")]
public void StartRemove()
{
    rate = (rate - 1 + 6) % 6;
    // Debug.Log("OnClickR2");
}

[Rate(1, 5)] public int rate;

video

HeaderGhostButton / HeaderGhostLeftButton

Draw a button in the component header, without frame and background color.

This is useful for icon buttons.

Method which returns an IEnumerator will start a coroutine

Arguments:

  • string label = null: label of the button. null means using function name. If starts with $, then a dynamic label will be created to use a field/property/callback as label. Dynamic label when returning null or empty string will hide the button. Rich Label supported.
  • string toolTip = null: tool tip for the button.
using SaintsField;
using SaintsField.ComponentHeader;

[HeaderGhostLeftButton("<icon=pencil.png/>")]
public void Edit()
{
}

[HeaderGhostButton("<icon=refresh.png/>", "Play")]
public void Play()
{
}

[HeaderGhostButton("<color=gray><icon=save.png/>", "Pause")]
public void Pause()
{
}

[HeaderGhostButton("<color=gray><icon=trash.png/>", "Resume")]
public void Resume()
{
}

image

A more complex example of dynamic buttons:

using SaintsField;
using SaintsField.ComponentHeader;

private string _editButtonIcon = "<icon=pencil.png/>";
private bool _editing;

[HeaderGhostButton("$" + nameof(_editButtonIcon), "Edit")]
private void StartEdit()
{
    _editing = true;
    _editButtonIcon = "";

    _saveLabel = "<color=brown><icon=save.png/>";
}

[HeaderGhostButton("$" + nameof(_saveLabel), "Save")]
private IEnumerator Click()
{
    _editing = false;
    _saveLabel = "<color=gray><icon=save.png/>";
    foreach (int i in Enumerable.Range(0, 200))
    {
        // Debug.Log($"saving {i}");
        yield return null;
    }
    _saveLabel = "<color=lime><icon=check.png/>";
    foreach (int i in Enumerable.Range(0, 200))
    {
        // Debug.Log($"checked {i}");
        yield return null;
    }
    _saveLabel = "";

    _editButtonIcon = "<icon=pencil.png/>";
}

private string _saveLabel = "";

[EnableIf(nameof(_editing)), OnValueChanged(nameof(OnChanged))] public string nickName;
[EnableIf(nameof(_editing)), OnValueChanged(nameof(OnChanged))] public string password;
[EnableIf(nameof(_editing)), OnValueChanged(nameof(OnChanged))] public int age;

private void OnChanged() => _saveLabel = "<color=lime><icon=save.png/>";

video

It works with ShowInInspector

public interface IMyInterface
{
}

// ... implements...

[OnValueChanged(nameof(ChangeCallback)), ShowInInspector]
public IMyInterface myInterface;

private void ChangeCallback(IMyInterface value)
{
    Debug.Log(value);
}

HeaderLabel / HeaderLeftLabel

Draw a label in the component header. This can be used on a method, property, field, or a Component class.

using SaintsField;
using SaintsField.ComponentHeader;

[HeaderLeftLabel("Fixed Text")]
[HeaderLabel]  // dynamic text
public string label;  // also works if it's a private (non-serialized) type

Image

Can be used on a component class:

using SaintsField;
using SaintsField.ComponentHeader;

[HeaderLabel("$" + nameof(value))]
[HeaderLeftLabel("dynamic:")]
public class HeaderLabelClassSaExample : MonoBehaviour
{
    public string value;
}

Image

HeaderDraw / HeaderLeftDraw

Allow you to manually draw items on the component headers

Parameters:

  • string groupBy = null: group the header items virtically by this name. If null, it will not share space with anyone.

Signature:

The method must have this signaure:

HeaderUsed FuncName(HeaderArea headerArea)

SaintsField.ComponentHeader.HeaderArea has the following fields:

/// <summary>
/// Rect.y for drwaing
/// </summary>
public readonly float Y;
/// <summary>
/// Rect.height for drawing
/// </summary>
public readonly float Height;
/// <summary>
/// the x value where the title (component name) started
/// </summary>
public readonly float TitleStartX;
/// <summary>
/// the x value where the title (component name) ended
/// </summary>
public readonly float TitleEndX;
/// <summary>
/// the x value where the empty space start. You may want to start draw here
/// </summary>
public readonly float SpaceStartX;
/// <summary>
/// the x value where the empty space ends. When drawing from right, you need to backward drawing starts here
/// </summary>
public readonly float SpaceEndX;

/// <summary>
/// The x drawing position. It's recommend to use this as your start drawing point, as SaintsField will
/// change this value accordingly everytime an item is drawn.
/// </summary>
public readonly float GroupStartX;
/// <summary>
/// When using `GroupBy`, you can see the vertical rect which already used by others in this group
/// </summary>
public readonly IReadOnlyList<Rect> GroupUsedRect;

public float TitleWidth => TitleEndX - TitleStartX;
public float SpaceWidth => SpaceEndX - SpaceStartX;

/// <summary>
/// A quick way to make a rect
/// </summary>
/// <param name="x">where to start</param>
/// <param name="width">width of the rect</param>
/// <returns>rect space you want to draw</returns>
public Rect MakeXWidthRect(float x, float width) => new Rect(x, Y, width, Height);

After you draw your item, use return new HeaderUsed(useRect); to tell the space you've used.

A simple example of progress bar

using SaintsField;
using SaintsField.ComponentHeader;

#if UNITY_EDITOR
[HeaderDraw]
private HeaderUsed HeaderDrawRight1G1(HeaderArea headerArea)
{
    // this is drawing from right to left, so we need to backward the rect space
    Rect useRect = new Rect(headerArea.MakeXWidthRect(headerArea.GroupStartX - 100, 100))
    {
        y = headerArea.Y + 2,
        height = headerArea.Height - 4,
    };
    Rect progressRect = new Rect(useRect)
    {
        width = range1 * useRect.width,
    };

    EditorGUI.DrawRect(useRect, Color.gray);
    EditorGUI.DrawRect(progressRect, Color.red);

    return new HeaderUsed(useRect);
}
#endif

[Range(0f, 1f)] public float range1;

video

A more complex example:

using SaintsField;
using SaintsField.ComponentHeader;

#if UNITY_EDITOR
private bool _started;

[HeaderGhostButton("<icon=play.png/>")]
private IEnumerator BeforeBotton()
{
    _started = true;
    while (_started)
    {
        range1 = (range1 + 0.01f) % 1;
        range2 = (range2 + 0.03f) % 1;
        range3 = (range3 + 0.02f) % 1;
        yield return null;
    }
}

[HeaderDraw("group1")]
private HeaderUsed HeaderDrawRight1G1(HeaderArea headerArea)
{
    Rect useRect = new Rect(headerArea.MakeXWidthRect(headerArea.GroupStartX - 40, 40))
    {
        height = headerArea.Height / 3,
    };
    Rect progressRect = new Rect(useRect)
    {
        width = range1 * useRect.width,
    };

    EditorGUI.DrawRect(useRect, Color.gray);
    EditorGUI.DrawRect(progressRect, Color.red);

    return new HeaderUsed(useRect);
}

[HeaderDraw("group1")]
private HeaderUsed HeaderDrawRight1G2(HeaderArea headerArea)
{
    Rect useRect = new Rect(headerArea.MakeXWidthRect(headerArea.GroupStartX - 40, 40))
    {
        y = headerArea.Y + headerArea.Height / 3,
        height = headerArea.Height / 3,
    };
    Rect progressRect = new Rect(useRect)
    {
        width = range2 * useRect.width,
    };

    EditorGUI.DrawRect(useRect, Color.gray);
    EditorGUI.DrawRect(progressRect, Color.yellow);

    return new HeaderUsed(useRect);
}

[HeaderDraw("group1")]
private HeaderUsed HeaderDrawRight1G3(HeaderArea headerArea)
{
    Rect useRect = new Rect(headerArea.MakeXWidthRect(headerArea.GroupStartX - 40, 40))
    {
        y = headerArea.Y + headerArea.Height / 3 * 2,
        height = headerArea.Height / 3,
    };
    Rect progressRect = new Rect(useRect)
    {
        width = range3 * useRect.width,
    };

    EditorGUI.DrawRect(useRect, Color.gray);
    EditorGUI.DrawRect(progressRect, Color.cyan);

    return new HeaderUsed(useRect);
}

[HeaderGhostButton("<icon=pause.png/>")]
private void AfterBotton()
{
    _started = false;
}
#endif

[Range(0f, 1f)] public float range1;
[Range(0f, 1f)] public float range2;
[Range(0f, 1f)] public float range3;

video

Data Types

SaintsArray/SaintsList

Unity does not allow to serialize two-dimensional array or list. SaintsArray and SaintsList are there to help.

The target can also be interface / abstract type.

using SaintsField;

// two dimensional array
public SaintsArray<GameObject>[] gameObjects2;
public SaintsArray<SaintsArray<GameObject>> gameObjects2Nest;
// four dimensional array, if you like.
public SaintsArray<SaintsArray<SaintsArray<GameObject>>>[] gameObjects4;

image

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;

SaintsArray Attribute

This allows you to set search and paging.

  • bool searchable = true: make it searchable
  • int numberOfItemsPerPage = 0: items per page
[SaintsArray(numberOfItemsPerPage: 5)] public SaintsArray<int[]> pagging;

Reference Type

Example of using interface

public SaintsArray<IInterface1[]> inters;

SaintsDictionary<,>

A simple dictionary serialization tool. It allows:

  1. Allow any type of kay/value type as long as Dictionary<,> allows
  2. Give a warning for duplicated keys
  3. Allow search for keys & values
  4. Allow paging for large dictionary
  5. Allow inherence to add some custom attributes, especually with auto getters to gain the auto-fulfill ability.

Basic usage:

public SaintsDictionary<string, GameObject> genDict;

image

Interface is also supported. You can pick a Unity Object or a serializable class/struct for it, just like a normal interface.

public SaintsDictionary<string, IInterface1> interfaceDictValue;
// You can also use it as key
public SaintsDictionary<IInterface1, IInterface1> interfaceDict;

// list/array is also supported
public SaintsDictionary<IInterface1, List<IInterface1>> interfaceDictLis;

If the type is abstract, an SerializeReference will be automatically used:

[Serializable]
public abstract class BaseC
{
    public string absC;
}

// Derived classes...

public SaintsDictionary<BaseC, BaseC> absDict;

If you want to explicitly use SerializeReference, use KeyAttribute(typeof(SerializeReference)) and ValueAttribute(typeof(SerializeReference)) to inject the attribute to key/value:

[Serializable]
public class Sub1
{
    public string sub1;
}

[Serializable]
public class Sub2 : Sub1
{
    public string sub2;
}

[KeyAttribute(typeof(SerializeReference))]  // Key will be polymorphism
// [ValueAttribute(typeof(SerializeReference))]  // committed out so value will only be `Sub1` type
public SaintsDictionary<Sub1, Sub1> dymDict;

You can use KeyAttribute(type, arguments...) and ValueAttribute(type, arguments...) to add extra attributes to key/value fields.

[KeyAttribute(typeof(PropRangeAttribute), 0f, 10f, -1f)]
[ValueAttribute(typeof(ExpandableAttribute))]
[ValueAttribute(typeof(RequiredAttribute))]
public SaintsDictionary<int, SpriteRenderer> valueInject;

Add SaintsDictionary attribute to control:

  1. Change keys' label & values' label
  2. Disable the search, if you want
  3. Enable the paging

Parameters:

  • string keyLabel = "Keys"
  • string valueLabel = "Values"
  • bool searchable = true: false to disable the search ability
  • int numberOfItemsPerPage = 0: items per page. 0 for no paging
  • string keyWidth = null: key column width. Can be percent like "20%", or pixel like "50" (or "50px"). null for auto width.
  • string valueWidth = null: value column width. Can be percent like "20%", or pixel like "50" (or "50px"). null for auto width.
using SaintsField;

[SaintsDictionary("Slot", "Enemy", numberOfItemsPerPage: 5)]
public SaintsDictionary<int, GameObject> slotToEnemyPrefab;

video

Using on a general struct/class is supported:

using SaintsField;

[Serializable]
public struct MyStruct
{
    public string myStringField;
    public int myIntField;
}


public SaintsDictionary<int, MyStruct> basicType;

image

Set init width

using SaintsField;

[SaintsDictionary(keyWidth: "30%")] public SaintsDictionary<int, string> keyWidthControl;
[SaintsDictionary(valueWidth: "120px")] public SaintsDictionary<int, string> valueWidthControl;

[SaintsDictionary] can work with [ShowInInspector]

[ShowInInspector, SaintsDictionary(numberOfItemsPerPage: 5)]
private Dictionary<int, string> FullFeature
{
    get => _plainDict;
    set => _plainDict = value;
}

SaintsInterface<>

SaintsInterface is a simple tool to serialize a UnityEngine.Object (usually your script component) or a serializable struct/class with a required interface.

You can access the interface with the .I field, actual unity object with .V field, and actual serializable class/struct with .VRef field.

[!TIP] This will requires you to use .I to access the interface. You may want to see Extended Serialization to directly serialize a interface type.

It provides a drawer to let you only select the object that implements the interface.

You can toggle the button at left to toggle either you want an unity instance (object, prefab, scriptableObject etc) or a serializable class/struct.

using SaintsField;

[Serializable]
private struct NorStructInterface1 : IInterface1
{
    public int common;
    public string structString;
}

[Serializable]
private struct NorClassInterface1 : IInterface1
{
    public int common;
    public string classString;
}

public SaintsInterface<IInterface1> myInter1;

private void Awake()
{
    Debug.Log(myInter1.I);  // the actual interface
}

Result of unity object:

image

Result of serializable class/struct:

image

This component is inspired by Serialize Interfaces! and Unity3D-SerializableInterface, please go give them a star!

Compared to Serialize Interfaces!, this version has several differences:

  • It supports UI Toolkit too.
  • Many SaintsField attributes can work together with this one, especially these auto getters, validators etc.

SaintsHashSet<> / ReferenceHashSet<>

A serializable HashSet<> for serializable type, SerializedReference type & interface type. Duplicated element will have a warning color.

If the type is an interface or an abstract class, the polymorphism picker will be used.

You can use SaintsHashSet attribute to control paging & searching

Parameters:

  • bool searchable = true: false to disable the search function
  • int numberOfItemsPerPage = 0: how many items per page. 0 for no paging
public SaintsHashSet<string> stringHashSet;  // default

[SaintsHashSet(numberOfItemsPerPage: 5)]  // paging control
public SaintsHashSet<int> integerHashSet = new SaintsHashSet<int>
{
    1, 2, 3, 4, 5, 6, 7, 8, 9, 0,
};

public interface IReference
{
    string Name { get; }
}

// ... implement of IReference omited here

public SaintsHashSet<IReference> refHashSet;

video

Example of using on interface where you can pick either Unity Object or serializable class/struct:

ReferenceHashSet is used if you want to pick a polymorphism type (while the type itself is not abstract)

[Serializable]
public class Sub1 : BaseC
{
    public string sub1;
}

[Serializable]
public class Sub2 : Sub1
{
    public string sub2;
}

// SaintsHashSet will treat it as a serializable type
public SaintsHashSet<Sub1> noPolymorphism;
// ReferenceHashSet will treat it as a reference type, allows you to pick polymorphism types
public ReferenceHashSet<Sub1> polymorphism;

TypeReference

Serialize a System.Type

[!WARNING] This function saves the target's domain and type name. Which means if you change the name, the data will be lost. Carefully think about it before using it.

By default, it searchs the current assembly and referenced assembly and shows all visible (public) types.

You can add an extra [TypeReference] to control the behavior.

Parameters:

  • EType eType = EType.Current: Options. See below
  • Type[] superTypes = null: A list of type/interface, the option list types are inhirent from these types. The type in the list is also included in list.
  • string[] onlyAssemblies = null: only use these assembly names
  • string[] extraAssemblies = null: extrally add these assemblies to the result lists
  • string defultSearch = "": input a default search string when opening up the popup

EType:

[Flags]
public enum EType
{
    /// <summary>
    /// Current assembly
    /// </summary>
    CurrentOnly = 1,
    /// <summary>
    ///  Current referenced assemblies.
    /// </summary>
    CurrentReferenced = 1 << 1,

    /// <summary>
    /// Current & referenced assemblies.
    /// </summary>
    Current = CurrentOnly | CurrentReferenced,

    /// <summary>
    /// Include "mscorlib" assembly.
    /// </summary>
    MsCorLib = 1 << 2,
    /// <summary>
    /// Include "System" assembly.
    /// </summary>
    System = 1 << 3,
    /// <summary>
    /// Include "System.Core" assembly.
    /// </summary>
    SystemCore = 1 << 4,
    /// <summary>
    /// Anything except "mscorlib", "System", "System.Core" assemblies.
    /// </summary>
    NonEssential = 1 << 5,
    /// <summary>
    /// All assemblies.
    /// </summary>
    AllAssembly = MsCorLib | System | SystemCore | NonEssential,

    /// <summary>
    /// Allow non-public types.
    /// </summary>
    AllowInternal = 1 << 6,
}

Please note: if you have many type options, the big dropdown list will be SLOW.

Example:

using SaintsField;

// default using current & referenced assembly
public TypeReference typeReference;

// current assembly, and group it
[TypeReference(EType.CurrentOnly | EType.GroupAssmbly)]
[BelowButton(nameof(TestCreate))]
public TypeReference typeReference2;

private void TestCreate(TypeReference tr)
{
    // you can also use `tr.Type`
    object t = Activator.CreateInstance(tr);
    Debug.Log(t);
}

// all assembly with non-public types, and group it
[TypeReference(EType.AllAssembly | EType.AllowInternal)]
public TypeReference typeReference3;

public interface IMyTypeRef {}
private struct MyTypeStruct: IMyTypeRef{}
private class MyTypeClass : IMyTypeRef{}

// Only types that implement IMyTypeRef
[TypeReference(EType.AllAssembly | EType.AllowInternal, superTypes: new[]{typeof(IMyTypeRef)})]
public TypeReference typeReferenceOf;

This feature is heavily inspired by ClassTypeReference-for-Unity, please go give them a star!

SaintsEvent

SaintsEvent is an alternative to Unity's UnityEvent. It's inspired by UltEvents & ExtEvents

You need to install Unity Serialization to use this type.

Features:

  • Support any serializable parameter type (Unity Object or Non Unity Object)
  • Support up to 4 serialized parameters (UnityEvent: 0-1)
  • Support static method (UnityEvent: Only Unity Object's instance method)
  • Support non-public methods (UnityEvent: Only public methods)

Here are some features that is supported by other plugins:

  • IMGUI is not supported, while UltEvents & ExtEvents does
  • Chained call (use one callback's result as another one's input) is not supported and will not be added, while UltEvents does
  • Renamed type is partly supported. If a renamed type is a MonoBehavior, then rename works as expected. However, ExtEvent will try to find a general type's rename
  • Implicit conversions for arguments is not supported, while ExtEvents does
  • Performance optimization is limited to first-time cache, while ExtEvents using code generator to make the runtime much more fast. So in general, speed comparison is (fast to slow) UnityEvent - ExtEvent - SaintsEvent - UltEvent

[!WARNING] This component is quite heavy and might not be stable for using (compare to others), and I understand a callback can be very wildly used in project. To avoid breaking changes, you may consider using it after some iteration of this component so it'll be more stable to use.

Basics:

using SaintsField.Events;

public SaintsEvent sEvent;

image

Here, +s to add a static callback, +i for an instance's callback, - to remove a callback.

R is a switch to change to Off, Editor & Runtime or Runtime Only, which is the same as UnityEvent.

image

S is a switch to change mode between "static callback" and "instance callback".

Then, the object picker is to decide which type you want to limit:

  • For static callback, it'll use the object's type you pick here. use None if your target is not any Unity Object (e.g. just a helper class)
  • For instance callback, it'll use the object as the instance

The next dropdown is depending on the mode:

  • For static mode with no target, you need to select a type

  • For static mode with target, or instance mode, you need to select a component on that target

The dropdown below is where you pick your actual callback:

Finally, if your function has parameters, you need to check how each parameter is processed. Lets using another example:

using SaintsField.Events;

public class MyClass
{
    public int N;
    public override string ToString()
    {
        return $"<MyClass N={N}/>";
    }
}

public SaintsEvent<MyClass, int, string> withName;

public void Callback(MyClass d, string s, int i = -1)
{
    Debug.Log($"Callback: i={i}, s={s}, d={d}");
}

In the picture

  • S is serialized, which allows you to pick a subclass (if any) and fill public fields.
  • D is dynamic, which allows you to bind its value to the event's value. In this example, it's binded to T2 (string) in SaintsEvent<MyClass, int, string>
  • X is default, which uses function parameter's default value. In this example, int i = -1, so use -1

Runtime

In runtime, you can use SaintsEvent.Invoke(), SaintsEvent.AddListener(callback) and SaintsEvent.RemoveListener(callback), SaintsEvent.RemoveAllListeners() just like UnityEvent.

Config

For a better naming, use SaintsEventArgs to rename the event generic parameters.

using SaintsField.Events;

[SaintsEventArgs("Custom", "Number", "Text")]
public SaintsEvent<MyClass, int, string> withName;

For static mode, you can also use TypeReference to filter the types you want.

using SaintsField;
using SaintsField.Events;

[TypeReference(onlyAssemblies: new []{"mscorlib"})]  // we only want types from mscorlib
public SaintsEvent sEvent;

[TypeReference(EType.CurrentOnly)]  // we only want types from current assembly
public SaintsEvent sEvent2;

SceneReference

Serialize scene asset GUID, and then visit the path & index of the scene

It'll give error & fixing button if the selected scene is not in build list or is disabled.

using SaintsField;

public SceneReference sceneRef;

private void Load()
{
    SceneManager.LoadScene(sceneRef);  // load by scene path (name)
    SceneManager.LoadScene(sceneRef.index);  // load by scene index
}

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;

addressable_label

AddressableAddress

A picker to select an addressable address (key).

  • string group = null the Addressable group name. null for all groups

  • params string[] orLabels the addressable label names to filter. Only entries with this label will be shown. null for 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 both A and B label, or with C label.

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

addressable_address

AddressableResource

A simple inline editor for AssetReference field.

This tool allows you to add/edit/delete an addressable asset's address, label, and group, without opening the Addressable window.

  • Allow Multiple: No
[AddressableResource]
public AssetReferenceSprite spriteRef;

video

AddressableScene

A picker to select an addressable scene into a string field.

Parameters:

  • string group = null: the Addressable group name. null for all groups

  • params string[] orLabels: the addressable label names to filter. Only entries with this label will be shown. null for 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 both A and B label, or with C label.

[AddressableScene] public string sceneKey;
// only use scenes from `Scenes` group
// with label `Battle`, or `Profile`
[AddressableScene("Scenes", "Battle", "Profile")] public string sceneKeySep;

AddressableSubAssetRequired

Validate if a sub-asset is signed in type like Addressable.AssetReferenceSprite

[AddressableSubAssetRequired] public AssetReferenceSprite sprite2;
[AddressableSubAssetRequired("Please pick a sub asset", EMessageType.Warning)] public AssetReferenceSprite sprite3;

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;

nav_mesh_area_mask

NavMeshArea

Select a NavMesh area for a string or an interger field.

  • bool isMask=true if 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;

[NavMeshArea] public string areaNameString;  // sting name

nav_mesh_area

Spine

Spine has Unity Attributes like SpineAnimation, but it has some limit, e.g. it can not be used on string, it can not report an error if the target is changed, mismatch with skeleton or missing etc.

SainsField's spine attributes allow more robust references:

  1. Check reference when possible
  2. Supported by Auto Validator tool, with searching supported
  3. Unity's default right click context menu works as expected

These tools are there only if you have Spine installed.

Namespace: SaintsField.Spine

SpineAnimationPicker

Pick a spine animation from a spine skeleton renderer, to a string field or a AnimationReferenceAsset field.

Parameters

  • string skeletonTarget = null: the target, either be a SkeletonData, SkeletonRenderer, or component/gameObject with SkeletonRenderer attached. Use GetComponent<SkeletonRenderer>() to the current object if null.
using SaintsField.Spine;

// get on current target
[SpineAnimationPicker] private string animationName;

// get from other field or callback
public SkeletonAnimation _spine;
[SpineAnimationPicker(nameof(_spine))] private AnimationReferenceAsset animationRef;

video

SpineSkinPicker

Pick a spine skin from a spine skeleton renderer, into a string field.

Parameters

  • string skeletonTarget = null: the target, either be a SkeletonData, SkeletonRenderer, or component/gameObject with SkeletonRenderer attached. Use GetComponent<SkeletonRenderer>() to the current object if null.
using SaintsField.Spine;

// get on current target
[SpineSkinPicker] private string skinName;

// get from other field or callback
public SkeletonAnimation _spine;
[SpineSkinPicker(nameof(_spine))] private string skinNameFromTarget;

spine_skin

SpineSlotPicker

Pick a spine slot from a spine skeleton renderer, into a string field.

Parameters

  • bool containsBoundingBoxes = false: Disables popup results that don't contain bounding box attachments when true.
  • string skeletonTarget = null: the target, either be a SkeletonData, SkeletonRenderer, or component/gameObject with SkeletonRenderer attached. Use GetComponent<SkeletonRenderer>() to the current object if null.
using SaintsField.Spine;

// get on current target
[SpineSlotPicker] private string slotName;

// get from other field or callback
public SkeletonAnimation _spine;
[SpineSlotPicker(nameof(_spine))] private string slotNameFromTarget;

SpineAttachmentPicker

Pick a spine attachment from a spine skeleton - skin - slot, into a string field.

Parameters

  • string skeletonTarget = null: the target, either be a SkeletonData, SkeletonRenderer, or component/gameObject with SkeletonRenderer attached. Use GetComponent<SkeletonRenderer>() to the current object if null.
  • string skinTarget = null: If specified, a locally scoped field with the name supplied by in skinTarget will be used to limit the popup results to entries of the named skin
  • string slotTarget = null: If specified, a locally scoped field with the name supplied by in slotTarget will be used to limit the popup results to children of a named slot
  • bool currentSkinOnly = true: Filters results to only include the current Skin. Only valid when a SkeletonRenderer is the data source.
  • bool returnAttachmentPath = false: Returns a fully qualified path for an Attachment in the format "Skin/Slot/AttachmentName". This path format is only used by the SpineAttachment helper methods like SpineAttachment.GetAttachment and .GetHierarchy. Do not use full path anywhere else in Spine's system
  • bool placeholdersOnly = false: Filters results to exclude attachments that are not children of Skin Placeholders
  • bool sepAsSub = true: do not seperate as sub items in the picker.
using SaintsField.Spine;

// get on current target
[SpineAttachmentPicker] private string spineAttachmentCurrent;

// get from other field or callback
public SkeletonAnimation _spine;
[SpineAttachmentPicker(nameof(_spine))] private string spineAttachment;

image

SpineBonePicker

Alternative to [SpineBone], allows searching and supports auto validator.

Parameters

  • string skeletonTarget = null: the target, either be a SkeletonData, SkeletonRenderer, or component/gameObject with SkeletonRenderer attached. Use GetComponent<SkeletonRenderer>() to the current object if null.
using SaintsField.Spine;
[SpineBonePicker] public string spineBonePicker;
[SpineBonePicker(nameof(_spine))] public string spineBonePicker;

SpineEventPicker

Alternative to [SpineEvent], allows searching and supports auto validator.

Parameters

  • string skeletonTarget = null: the target, either be a SkeletonData, SkeletonRenderer, or component/gameObject with SkeletonRenderer attached. Use GetComponent<SkeletonRenderer>() to the current object if null.
using SaintsField.Spine;

[SpineEventPicker] public string spineEventPicker;
[SpineEventPicker(nameof(_spine))] public string spineEventPicker;

SpineIkConstraintPicker

Alternative to [SpineIkConstraint], allows searching and supports auto validator.

Parameters

  • string skeletonTarget = null: the target, either be a SkeletonData, SkeletonRenderer, or component/gameObject with SkeletonRenderer attached. Use GetComponent<SkeletonRenderer>() to the current object if null.
using SaintsField.Spine;

[SpineIkConstraintPicker] public string spineIKConstraintPicker;
[SpineIkConstraintPicker(nameof(_spine))] public string spineIKConstraintPicker;

SpinePathConstraintPicker

Alternative to [SpinePathConstraint], allows searching and supports auto validator.

Parameters

  • string skeletonTarget = null: the target, either be a SkeletonData, SkeletonRenderer, or component/gameObject with SkeletonRenderer attached. Use GetComponent<SkeletonRenderer>() to the current object if null.
using SaintsField.Spine;

[SpinePathConstraintPicker] public string spinePackConstraintPicker;
[SpinePathConstraintPicker(nameof(_spine))] public string spinePackConstraintPicker;

SpineTransformConstraintPicker

Alternative to [SpineTransformConstraint], allows searching and supports auto validator.

Parameters

  • string skeletonTarget = null: the target, either be a SkeletonData, SkeletonRenderer, or component/gameObject with SkeletonRenderer attached. Use GetComponent<SkeletonRenderer>() to the current object if null.
using SaintsField.Spine;

[SpineTransformConstraintPicker] public string spineTransformConstraintPicker;
[SpineTransformConstraintPicker(nameof(_spine))] public string spineTransformConstraintPicker;

DOTween

DOTweenPlay

[!IMPORTANT] Enable SaintsEditor before using

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 = null the label of the button. Use method name if null. Rich label not supported.
  • ETweenStop stopAction = ETweenStop.Rewind the action after the tween is finished or killed. Options are:
    • None: do nothing
    • Complete: complete the tween. This only works if the tween get killed
    • Rewind: rewind to the start state
// Please ensure you already have SaintsEditor enabled in your project before trying this example
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 autoplay when you click the start in the global control.

video

Setup

To use DOTweenPlay: Tools - Demigaint - DOTween Utility Panel, click Create ASMDEF

Wwise

Wwise itself already has very nice drawer. SaintsField only provide some utility to make it easier to use.

If you can't see the wwise related attributes, please add marco SAINTSFIELD_WWISE to enable it.

If you face compatibility issue because of API changes in Wwise, please add marco SAINTSFIELD_WWISE_DISABLE to disable it.

GetWwise

[!NOTE] This feature is UI Toolkit only

Like the auto getters, this can auto-sign a wwise object (a state, a switch, a soundBank etc.) to a field.

using SaintsField.Wwise;

[GetWwise("BGM*")]  // Get a soundBank starts with `BGM`
public Bank bank;

[GetWwise("*BGM*")]  // Get all events contains `BGM`
public Event[] events;

[GetWwise]  // Get the first rtpc found
public RTPC rtpc;

[GetWwise("*/BGM/Stop*")]  // Get events that's under any work unit, under BGM folder, and starts with `Stop`
public Event stopEvents;

image

I2 Localization

Tools for I2 Localization. Enable it in Window - Saints - Enable I2 Localization Support

NameSpace: SaintsField.I2Loc

LocalizedStringPicker

Pick a term from I2 Localization to a string field or a LocalizedString field. Support search so you don't need to deal with I2's default painful picker.

using SaintsField.I2Loc;

[LocalizedStringPicker] public LocalizedString displayNameTerm;
[LocalizedStringPicker] public string descriptionTerm;

SaintsEditor

SaintsEditor is a UnityEditor.Editor level component, which gives:

  1. All attributes that requires SaintsEditor to be enabled
  2. Auto kick-in sub-editor. A serializable class/struct now automatically use SaintsRow (SaintsEditor) to allow many attributes works
  3. Auto kick-in drawer. Unity by default loading attribute from top to buttom, left to right. If you write [Range(0, 100), OnValueChanged] by default won't work because Range (Unity attribute) won't fallback. SaintsEdtior will handle the order to make OnValueChanged (SaintsField attributes) to work with Range (other attributes)

Namespace: SaintsField.Playa

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, EditorAttributes, SaintsEditor can not work together.

Setup

Edit - Project Settings - SaintsField

If you want to do it manually, check ApplySaintsEditor.cs for more information

Inherent

You need to inherent SaintsField.Editor.SaintsEditor:

[CustomEditor(typeof(MyMonoBehavior))]
public class LabelTestEditor : SaintsField.Editor.SaintsEditor  // <-- Use this
{
    public override VisualElement CreateInspectorGUI()
    {
        VisualElement root = new VisualElement();

        root.Add(base.CreateInspectorGUI());  // fill the default fields

        // If you want to use IMGUI, put it inside IMGUIContainer
        root.Add(new IMGUIContainer(() =>
        {
            using (new EditorGUILayout.HorizontalScope())
            {
                GUILayout.TextField("IMGUI Text Field");
                GUILayout.Button("IMGUI Button");
            }
        }));

        return root;
    }
}

Extend

[!WARNING] Some APIs might get changed in the future

You can use AbsRenderer to control how each field is rendered.

Example type:

public class MyMonoWithCustom : MonoBehaviour
{
    public string myString;

    public bool toggle;
    public string input;

    public int myInt;
}

Now let's make a toggleable input

using SaintsField.Editor.Playa;
using SaintsField.Editor.Playa.Renderer.BaseRenderer;
using UnityEditor;
using UnityEngine.UIElements;

[CustomEditor(typeof(MyMonoWithCustom), true)]
public class MyMonoWithCustomEditor : SaintsField.Editor.SaintsEditor
{
    public override IEnumerable<AbsRenderer> MakeRenderer(SerializedObject so, SaintsFieldWithInfo fieldWithInfo)
    {
        if (fieldWithInfo.FieldInfo != null && fieldWithInfo.FieldInfo.Name == "toggle")
        {
            yield break;  // returns nothing to show nothing
        }

        if (fieldWithInfo.FieldInfo != null && fieldWithInfo.FieldInfo.Name == "input")
        {
            yield return new ToggleInputRenderer(so, fieldWithInfo);  // custom rendering
            yield break;
        }

        // default rendering
        foreach (AbsRenderer defaultRenderer in base.MakeRenderer(so, fieldWithInfo))
        {
            yield return defaultRenderer;
        }
    }
}

public class ToggleInputRenderer: AbsRenderer
{
    private readonly SerializedObject _serializedObject;
    public ToggleInputRenderer(SerializedObject serializedObject, SaintsFieldWithInfo fieldWithInfo) : base(serializedObject, fieldWithInfo)
    {
        _serializedObject = serializedObject;
    }

    public override void OnDestroy()
    {
    }

    public override void OnSearchField(string searchString)
    {
    }

    protected override void RenderTargetIMGUI(float width, PreCheckResult preCheckResult)
    {
    }

    protected override float GetFieldHeightIMGUI(float width, PreCheckResult preCheckResult)
    {
        return 0;
    }

    protected override void RenderPositionTargetIMGUI(Rect position, PreCheckResult preCheckResult)
    {
    }

    protected override (VisualElement target, bool needUpdate) CreateTargetUIToolkit(VisualElement container)
    {
        VisualElement root = new VisualElement
        {
            style =
            {
                flexDirection = FlexDirection.Row,
            },
        };

        Toggle toggle = new Toggle("Input")
        {
            bindingPath = _serializedObject.FindProperty("toggle").propertyPath,
        };
        toggle.AddToClassList(Toggle.alignedFieldUssClassName);
        root.Add(toggle);

        TextField input = new TextField
        {
            bindingPath = _serializedObject.FindProperty("input").propertyPath,
            style =
            {
                flexGrow = 1,
                flexShrink = 1,
            },
        };
        root.Add(input);

        return (root, true);
    }

    protected override PreCheckResult OnUpdateUIToolKit(VisualElement root)
    {
        // If needUpdate=true, this function is called every 0.1s. You can do some ticking update here.
        TextField textField = root.Q<TextField>();
        textField.SetEnabled(_serializedObject.FindProperty("toggle").boolValue);

        // return the required value
        return base.OnUpdateUIToolKit(root);
    }
}

video

Integerate

You can integrate SaintsEditor with other editors.

public class MyEditorCore: SaintsEditorCore
{
    public MyEditorCore(UnityEditor.Editor editor, bool editorShowMonoScript) : base(editor, editorShowMonoScript)
    {
    }

    public override IEnumerable<IReadOnlyList<AbsRenderer>> MakeRenderer(SerializedObject so, SaintsFieldWithInfo fieldWithInfo)
    {
        if(fieldWithInfo.FieldInfo?.Name == "...")  // skip a field
        {
            return Array.Empty<IReadOnlyList<AbsRenderer>>();
        }
        if(fieldWithInfo.PlayaAttributes?.Any(each => each is ShowInInspectorAttribute || each is ButtonAttribute))  // render ShowInInspector/Button
        {
            return base.MakeRenderer(so, fieldWithInfo);
        }
        // Otherwise, do not render it
        return Array.Empty<IReadOnlyList<AbsRenderer>>();
    }
}

Then, in your editor script:

[CustomEditor(typeof(MyTargetType), true)]
public class IntegerateSaintsEditor: MyEditor
{
    public override VisualElement CreateInspectorGUI()
    {
        VisualElement root = new VisualElement();
        root.Bind(serializedObject);

        root.Add(base.CreateInspectorGUI());  // Fill with default behavior
        root.Add(new MyEditorCore(this).CreateInspectorGUI());  // Fill with SaintsEditor
        return root;
    }
}

Netcode for Game Objects

Unity's Netcode for Game Objects uses a custom editor that SaintsEditor can not be applied to.

To use ability from SaintsEditor, the most simple way is to inherent from SaintsField.Playa.SaintsNetworkBehaviour

using SaintsField.Playa;
using Unity.Netcode;
using UnityEngine;

public class RpcTestSaints : SaintsNetworkBehaviour  // inherent this one
{
    [PlayaInfoBox("Saints Info Box for Array")]  // SaintsEditor specific decorator
    public int[] normalIntArrays;

    [LayoutStart("SaintsLayout", ELayout.FoldoutBox)]  // SaintsEditor specific decorator
    public string normalString;

    [ResizableTextArea]
    public string content;

    public NetworkVariable<int> testVar = new NetworkVariable<int>(0);
    public NetworkList<bool> TestList = new NetworkList<bool>();

    [Button]  // SaintsEditor specific decorator
    private void TestRpc()
    {
        Debug.Log("Button Invoked");
    }
}

Result using SaintsNetworkBehaviour:

image

Result using default one:

image

The drawer is called SaintsField.Editor.Playa.NetCode.SaintsNetworkBehaviourEditor, in case if you want to apply it manually.

Please note: NetworkVariable and NetworkList will always be rendered at the top, just like Unity's default behavior. Putting it under Layout will not change this order and will have no effect.

Scriptable Renderer Data

ScriptableRendererData uses ScriptableRendererDataEditor with IMGUI. Which makes SaintsEditor unable to kick-in.

To use the functions in SaintsEditor, inherent from SaintsScriptableRendererData

using SaintsField.ScriptableRenderer;

public class MyScriptableRendererData: SaintsScriptableRendererData
{
    // ...
}

Result:

To use URP, inherent from SaintsUniversalRendererData:

using SaintsField.ScriptableRenderer;

public class MyUniversalRendererData : SaintsUniversalRendererData
{
    // ...
}

Note: this requires you to enable SaintsEditor in project too. If you can not, you also need to inherent from SaintsScriptableRendererFeature

using SaintsField.ScriptableRenderer;

public class MyRendererFeature: SaintsScriptableRendererFeature
{
    // ...
}

Extended Serialization

SaintsEditor supports some types that usually can not be serialized. To use this function:

  1. Ensure the type is supported by this functionality
  2. Add [SaintsSerialized] for these fields or properties. Does not matter if the field is public/private etc.
  3. ensure the containing class/struct (and its containing parent classes/structs) of these fields/properties is marked with keyword partial

If a field is not supported, the default serialization provided by Unity will be used.

Dictionary<,>

You can mark a dictionary directly for serialization. SaintsField will internally use SaintsDictionary to serialize it.

// Note the `partial`!
public partial class SerDictionaryExample : MonoBehaviour
{
    [Serializable]
    public struct Sub
    {
        public string subString;
        public int subInt;
    }

    // normal serializable
    [SaintsSerialized] public Dictionary<int, Sub> dictIntToStruct;
    // interface is also supported, just like SaintsDictionary
    [SaintsSerialized] private Dictionary<int, IInterface1> _dictInterface;

    // can be nested inside array/list, just like a normal serializable type
    [SaintsSerialized] private Dictionary<IInterface1, int>[] _dictInterfaceArr;
    [SaintsSerialized] private List<Dictionary<IInterface1, IInterface1>> _dictInterfaceLis;
}

Because it uses SaintsDictionary internally, if you want to receive the field value for OnValueChanged, you need to use SaintsDictionary<,> instead.


HashSet<>

You can mark a HashSet directly for serialization. SaintsField will internally use SaintsHashSet to serialize it.

It support serializable types, abstract class/struct types, and interface types as element type.

// Note the `partial`!
public partial class SerDictionaryExample : MonoBehaviour
{
    [SaintsSerialized]
    public HashSet<string> stringHashSet;
    [SaintsSerialized]
    public HashSet<IInterface1> refHashSet;
}

interface

[!WARNING] Not work with OnValueChanged yet

Serialize any interface type, either of a Unity Object, or a serializable class/struct.

This is the same as SaintsInterface<>, but now you can simply use the interface type directly.

IMPORTANT: Set your MonoBehaviour/ScriptableObject to partial if the field is declaration inside. If it's inside a normal class/struct, you need to set all parent class/struct to partial

using SaintsField;

// Note the `partial`!
public partial class SerInterfaceExample : SaintsMonoBehaviour
{
    [SaintsSerialized] private IInterface1 _interface1;
}

// use in a normal class/struct, set parents partial recursively
public partial class SerInterfaceExample : SaintsMonoBehaviour
{
    // Use inside class/struct, you need to set as `partial`, together with all it's container
    [Serializable]
    public partial struct SerInterfaceStruct
    {
        [SaintsSerialized] private IInterface1 _interface1InStruct;
    }

    public SerInterfaceStruct structWithInterface;
}

long/ulong Enum

You can serialize a long/ulong base typed enum with SaintsSerialized, which is not supported by Unity.

  1. IMPORTANT: Set your MonoBehaviour/ScriptableObject to partial
  2. Add [SaintsSerialized] to your enum field
// IMPORTANT: partial class

using SaintsField.Playa;

public partial class MyBehavior: MonoBehaviour
{
    [Flags]  // no need to use Serializable, because Unity just can not serialize it.
    public enum TestULongEnum: ulong
    {
        None = 0,
        First = 1,
        Second = 1 << 1,
        Third = 1 << 2,
        All = First | Second | Third,
    }

    public enum TestULongEnumNormal: ulong
    {
        None,
        First,
        Second,
        Third,
    }

    [FormerlySerializedAs("MyOldName")]  // FormerlySerializedAs works too
    [SaintsSerialized] public TestULongEnum ULongEnumPub;
    [SaintsSerialized] public TestULongEnumNormal ULongEnumNormalPub;
    // EnumToggleButtons is supported too
    [SaintsSerialized, EnumToggleButtons] public TestULongEnum ULongEnumPubBtns;

    [Serializable]  // use inside class/struct requires `partial` keyword too
    public partial class MyClass
    {
        [SaintsSerialized]  // This work inside a normal class/struct. Note the `partial`!
        public MyULongEmun myEnum;
    }

    // We don't need `SaintsSerialized` for this field
    public MyClass myClass;
}

This can work with EnumToggleButtons.

DateTime

Serialize a DateTime type.

IMPORTANT: Set your MonoBehaviour/ScriptableObject to partial if the field is declaration inside. If it's inside a normal class/struct, you need to set class/struct to partial, and all the class/struct's parent class/struct.

using SaintsField;

// set as partial
public partial class SerDateTimeExample : MonoBehaviour
{
    [SaintsSerialized]
    private DateTime _dt;
}


// partial here too
public partial class SerDateTimeExample : MonoBehaviour
{
    [Serializable]   // if you use inside a normal class/struct, set parents partial recursively
    public partial class MyClass
    {
        [SaintsSerialized]
        private DateTime[] _dtArray;
    }

    // No SaintsSerialized here, it's just a class/struct
    public MyClass myClass;
}

video

TimeSpan

Serialize a TimeSpan type.

IMPORTANT: Set your MonoBehaviour/ScriptableObject to partial if the field is declaration inside. If it's inside a normal class/struct, you need to set all parent class/struct to partial

using SaintsField;

// note the partial
public partial class SerTimeSpanExample : MonoBehaviour
{
    [SaintsSerialized]
    private TimeSpan _dt;

    [SaintsSerialized]
    private List<TimeSpan> _dtList;
}

// use in a normal class/struct, set parents partial recursively
public partial class SerTimeSpanExample : MonoBehaviour
{
    [Serializable]
    public partial class MyClass
    {
        [SaintsSerialized]
        private TimeSpan[] _dtArray;
    }

    // No SaintsSerialized here
    public MyClass myClass;
}

Guid

Serialize a Guid type.

IMPORTANT: Set your MonoBehaviour/ScriptableObject to partial if the field is declaration inside. If it's inside a normal class/struct, you need to set all parent class/struct to partial

using SaintsField;

// note the partial
public partial class SerGuidExample : MonoBehaviour
{
    [SaintsSerialized]
    private Guid _guid;

    [SaintsSerialized]
    private List<Guid> _guidList;
}

// use in a normal class/struct, set parents partial recursively
public partial class SerGuidExample : MonoBehaviour
{
    [Serializable]
    public partial class MyClass
    {
        [SaintsSerialized]
        private Guid[] _guidArray;
    }

    // No SaintsSerialized here
    public MyClass myClass;
}

SaintsEditorWindow

An EditorWindow class to easily create an editor (a bit like Odin's OdinEditorWindow), with support of StartEditorCoroutine and StopEditorCoroutine

Usage & Example

Basic usage: inherent from SaintsField.Editor.SaintsEditorWindow

#if UNITY_EDITOR
using SaintsField.Editor;

public class ExamplePanel: SaintsEditorWindow
{

    [MenuItem("Window/Saints/Example/SaintsEditor")]
    public static void OpenWindow()
    {
        EditorWindow window = GetWindow<ExamplePanel>(false, "My Panel");
        window.Show();
    }

    // fields
    [ResizableTextArea]
    public string myString;

    [ProgressBar(100f)] public float myProgress;

    // life-cycle: OnUpdate function
    public override void OnEditorUpdate()
    {
        myProgress = (myProgress + 1f) % 100;
    }

    [ProgressBar(100f)] public float myCoroutine;

    // Layout supported
    [LayoutStart("Coroutine", ELayout.Horizontal)]

    private IEnumerator _startProcessing;

    // Button etc supported
    // EditorCoroutine supported
    [Button]
    public void StartIt()
    {
        StartEditorCoroutine(_startProcessing = StartProcessing());
    }

    [Button]
    public void StopIt()
    {
        if (_startProcessing != null)
        {
            StopEditorCoroutine(_startProcessing);
        }

        _startProcessing = null;
    }

    private IEnumerator StartProcessing()
    {
        myCoroutine = 0;
        while (myCoroutine < 100f)
        {
            myCoroutine += 1f;
            yield return null;
        }
    }

    // Other life-cycle
    public override void OnEditorEnable()
    {
        Debug.Log("Enable");
    }

    public override void OnEditorDisable()
    {
        Debug.Log("Disable");
    }

    public override void OnEditorDestroy()
    {
        Debug.Log("Destroy");
    }
}

#endif

video

An example of using as a ScriptableObject editor (or any serializable object)

#if UNITY_EDITOR
using SaintsField.Editor;
using SaintsField.Playa;

public class ExampleSo: SaintsEditorWindow
{
    [MenuItem("Window/Saints/Example/ScriptableEditor")]
    public static void OpenWindow()
    {
        EditorWindow window = GetWindow<ExampleSo>(false, "Scriptable Editor");
        window.Show();
    }

    [
        AdvancedDropdown(nameof(ShowDropdown)),
        OnValueChanged(nameof(TargetChanged))
    ]
    public ScriptableObject inspectTarget;

    [WindowInlineEditor]  // this will inline the serialized object editor
    public Object editorInlineInspect;

    private IReadOnlyList<ScriptableObject> GetAllSo() => Resources.LoadAll<ScriptableObject>("");

    private AdvancedDropdownList<ScriptableObject> ShowDropdown()
    {
        AdvancedDropdownList<ScriptableObject> down = new AdvancedDropdownList<ScriptableObject>();
        down.Add("[Null]", null);
        foreach (ScriptableObject scriptableObject in GetAllSo())
        {
            down.Add(scriptableObject.name, scriptableObject);
        }

        return down;
    }

    private void TargetChanged(ScriptableObject so)
    {
        Debug.Log($"changed to {so}");
        editorInlineInspect = so;
        titleContent = new GUIContent(so == null? "Pick One": $"Edit {so.name}");
    }

    [LayoutStart("Buttons", ELayout.Horizontal)]

    [Button]
    private void Save() {}

    [Button]
    private void Discard() {}
}
#endif

video

Life Cycle & Functions

The following are the life cycle functions, some are wrapped around Unity EditorWindow's default life cycle callback:

  • public virtual void OnEditorDestroy() -> OnDestroy
  • public virtual void OnEditorEnable() -> OnEnable
  • public virtual void OnEditorDisable() -> OnDisable
  • public virtual void OnEditorUpdate(): like OnUpdate, gets called every 1 millisecond in UI Toolkit, and every frame when user have interactive in IMGUI.
  • public void StartEditorCoroutine(IEnumerator routine): like StartCoroutine, but for editor coroutine. No return value.
  • public void StopEditorCoroutine(IEnumerator routine): like StopCoroutine, but for editor coroutine.

WindowInlineEditor

This decorator only works in SaintsEditorWindow. It'll render the target object's editor in the window. Not work for array/list.

See the example above.

Misc

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 groupd with the below drawer, and vice versa.

"" means no group.

EMode

  • EMode.Edit: the Unity Editor is not playing
  • EMode.Play: the Unity Editor is playing
  • EMode.InstanceInScene: target is a prefab placed in a scene
  • EMode.InstanceInPrefab: target is inside a prefab (but is not the top root of that prefab)
  • EMode.Regular: target is at the top root of the prefab
  • EMode.Variant: target is at the top root of the prefab, and is also a variant prefab
  • EMode.NonPrefabInstance: target is not a prefab (but can be inside a prefab)
  • EMode.PrefabInstance = InstanceInPrefab | InstanceInScene
  • EMode.PrefabAsset = Variant | Regular

Callback

For decorators that accept a callback, you can usually use $ to indicate that you want a callback. The callback can be a method, a property, or a field.

Use \\$ if you do not want it to be a callback. This is useful for decorators like RichLabel, InfoBox that the displaying string itself starts with $.

Using $: if the callback is a static/const field. We support the following style:

namespace SaintsField.Samples.Scripts
{
    public class StaticCallback : SaintsMonoBehaviour
    {
        private static readonly string StaticString = "This is a static string";
        private const string ConstString = "This is a constant string";

        // using full type name
        [AboveText("$:SaintsField.Samples.Scripts." + nameof(StaticCallback) + "." + nameof(StaticString))]
        // using only type name. This is slow and might find the incorrect target.
        // We'll first search the assembly of this object. If not found, then search all assemblies.
        [InfoBox("$:" + nameof(StaticCallback) + "." + nameof(ConstString))]
        public int field;

#if UNITY_EDITOR
        private static Texture2D ImageCallback(string name) =>
            AssetDatabase.LoadAssetAtPath<Texture2D>(
                $"Assets/SaintsField/Editor/Editor Default Resources/SaintsField/{name}.png");
#endif

#if UNITY_EDITOR
        // use only field/method name. This will only try to search on the current target.
        [BelowImage("$:" + nameof(ImageCallback), maxWidth: 20)]
#endif
        public string imgName;

        [ShowInInspector] private static bool _disableMe = true;

#if UNITY_EDITOR
        [DisableIf("$:" + nameof(_disableMe))]
        [RequiredIf("$:" + nameof(_disableMe), false)]
#endif
        public string disableIf;
    }
}

You can skip the namespace part. And if you also skip the type part, we'll try to find the callback from the current type first, then search all types in the current assembly, and finally search all types in all assemblies.

Note: decorators like OnEvent, OnButtonClick does not support this $: yet. I'm still working on making all APIs consistent.

Syntax for Show/Hide/Enable/Disable/Required-If

This applies to ShowIf, HideIf, EnableIf, DisableIf, RequiredIf, PlayaShowIf, PlayaHideIf, PlayaEnableIf, PlayaDisableIf.

These decorators accept many objects.

By Default

Passing many strings, each string is represent either a field, property or a method. SaintsField will check the value accordingly. If it's a method, it'll also receive the field's value and index if it's inside an array/list.

Sub Field

You can use dot (.) to obtain a sub-field from a field/property/method. This is useful if your condition is relayed on a sub-field of a field.

using SaintsField;
[GetComponentInChildren, Expandable] public ToggleSub toggle;

// Show if toggle.requireADescription is a truly value
[ShowIf(nameof(toggle) + ".requireADescription")]
public string description;

// ToggleSub.cs
public class ToggleSub : MonoBehaviour
{
    [LeftToggle] public bool requireADescription;
}

video

Value Equality

You can also pass a string, then followed by a value you want to compare with. For example:

using SaintsField;

public int myInt;

[EnableIf(nameof(myInt), 2] public int enableIfEquals2;

This field will only be enabled if the myInt is equal to 2.

This can be mixed with many pairs:

using SaintsField;

public int myInt1;
public int myInt2;

[EnableIf(nameof(myInt1), 2, nameof(myInt2), 3] public int enableIfMultipleCondition;

multiple conditions are logically chained accordingly. See each section of these decorators for more information.

Value Comparison

The string can have ! prefix to negate the comparison.

And ==, !=, > etc. suffix for more comparison you want. The suffix supports:

Comparison Suffixes:

  • ==: equal to the next parameter
  • ==$: equal, but the value is a callback by the next parameter
  • !=: not equal to the next parameter
  • >: greater than the next parameter
  • >$: greater than, but the value is a callback by the next parameter
  • <: less than the next parameter
  • <$: less than, but the value is a callback by the next parameter
  • >=: greater or equal to the next parameter
  • >=$: greater or equal, but the value is a callback by the next parameter
  • <=: less or equal to the next parameter
  • <=$: less or equal, but the value is a callback by the next parameter

Bitwise Suffixes:

  • &: bitwise and with the next parameter
  • &$: bitwise and, but the value is a callback by the next parameter
  • ^: bitwise xor with the next parameter
  • ^$: bitwise xor, but the value is a callback by the next parameter
  • &==: bitwise has flag of the next parameter
  • &==$: bitwise has flag, but the value is a callback by the next parameter

Some examples:

using SaintsField;

public bool boolValue;
[EnableIf("!" + nameof(boolValue)), LabelText("Reverse!")] public string boolValueEnableN;

[Range(0, 2)] public int int01;

[EnableIf(nameof(int01), 1), LabelText("default")] public string int01Enable1;
[EnableIf(nameof(int01) + ">=", 1), LabelText(">=1")] public string int01EnableGe1;
[EnableIf("!" + nameof(int01) + "<=", 1), LabelText("! <=1")] public string int01EnableNLe1;

[Range(0, 2)] public int int02;
// we need the "==$" to tell the next string is a value callback, not a condition checker
[EnableIf(nameof(int01) + "==$", nameof(int02)), LabelText("==$")] public string int01Enable1Callback;
[EnableIf(nameof(int01) + "<$", nameof(int02)), LabelText("<$")] public string int01EnableLt1Callback;
[EnableIf("!" + nameof(int01) + ">$", nameof(int02)), LabelText("! >$")] public string int01EnableNGt1Callback;

// example of bitwise
[Serializable]
public enum EnumOnOff
{
    A = 1,
    B = 1 << 1,
}

[Space]
[Range(0, 3)] public int enumOnOffBits;

[EnableIf(nameof(enumOnOffBits) + "&", EnumOnOff.A), LabelText("&01")] public string bitA;
[EnableIf(nameof(enumOnOffBits) + "^", EnumOnOff.B), LabelText("^10")] public string bitB;
[EnableIf(nameof(enumOnOffBits) + "&==", EnumOnOff.B), LabelText("hasFlag(B)")] public string hasFlagB;

Default Value Comparison

When passing the parameters, any parameter that is not a string, means it's a value comparison to the previous one.

(Which also means, to compare with a literal string value, you need to suffix the previous string with ==)

[ShowIf(nameof(MyFunc), 3)] means it will check if the result of MyFunc equals to 3.

However, if the later parameter is a bitMask (an enum with [Flags]), it'll check if the target has the required bit on:

using SaintsField;

[Flags, Serializable]
public enum EnumF
{
    A = 1,
    B = 1 << 1,
}

[EnumFlags]
public EnumF enumF;

[EnableIf(nameof(enumF), EnumF.A), LabelText("hasFlag(A)")] public string enumFEnableA;
[EnableIf(nameof(enumF), EnumF.B), LabelText("hasFlag(B)")] public string enumFEnableB;
[EnableIf(nameof(enumF), EnumF.A | EnumF.B), LabelText("hasFlag(A | B)")] public string enumFEnableAB;

Saints XPath-like Syntax

XPath

This part is how a target is found, a simplified XML Path Language.

basic syntax: step/step/step...

for each step: axis::nodetest[predicate][predicate OR predicate]/othersteps...

Please note: powerful as the seems, there are many edge cases I have not covered yet. Report an issue if you face any.

nodetest

nodetest is like a path, use / to separate, // means any descendant

. means current object, .. means parent, * means any node.

nodetest always starts from the current object.

// `DirectChild` object under this object
[GetByXPath("/DirectChild")] public GameObject directChild;
// Search all children that starts with `StartsWith`,
// under which, search all children ends with `Child`
// and get all the direct children of that.
[GetByXPath("//StartsWith*//*Child/*")] public Transform[] searchChildren;

axis

axis redirect the target point

  • ancestor::: all parents
  • ancestor-or-self::: the object itself, and all it's parents.
  • ancestor-inside-prefab::: all parents inside this prefab
  • ancestor-or-self-inside-prefab::: the object itself, and all it's parents inside this prefab
  • parent::: parent of the object
  • parent-or-self::: the object itself, and it's parent
  • parent-inside-prefab::: parent inside this prefab
  • parent-or-self-inside-prefab::: this object itself, and it's parent inside this prefab
  • scene::: root of the active scene
  • prefab::: root of the current prefab
  • resources::: Resources
  • assets::: root folder Assets
// search all parents that starts with `Sub`
[GetByXPath("ancestor:://Sub*")] public Transform ancestorStartsWithSub;

// search object itself, and all it's parents, which contains `This`
[GetByXPath("ancestor-or-self::*This*")] public Transform[] parentsSelfWithThis;

// search current scene that ends with `Camera`
[GetByXPath("scene:://*Camera")] public Camera[] sceneCameras;

// get the first folder starts with `Issue`, and get all the prefabs
[GetByXPath("assets:://Issue*/*.prefab")] public GameObject[] prefabs;

attribute

attribute allows you to extract an attribute from a target, starting with a @ letter. Normally, {} means it can be directly executed on the target.

  • @layer: Get the layer string name

  • @{layer}: Get the layer mask (int)

  • @{tag}: Get the tag value

  • @{gameObject}: Get the gameObject (this is the default behavior)

  • @{transform}: Get the transform

  • @{rectTransform}: Get the RectTransform. This is just a shortcut ofGetComponent(RectTransform)

  • @{activeSelf}/@{gameObject.activeSelf}

  • @{GetComponent(MyScript)}/@{GetComponents(MyScript)[2]} Get a component from the target. You can continuously chain the calling like: @{GetComponents(MyComponent)[-1].MyFunction().someField['key']}.

    Please note: this is not an actual code executing, and with these limits:

    1. Parameters are not supported
    2. indexing for array/list is allowed
    3. indexing for dictionary only supports string key type, and single quote / double quote are the same

    GetComponent & GetComponents are a special function. You can pass type name. If you have multiple type with the same name, prefix it with some namespace is allowed: GetComponent(MyNameSpace.MyScript).

    Base class is allow allowed, but generic class is not supported.

  • @{GetComponents()}: Get all components of the target

  • @resource-path()

  • @asset-path()

// 1.  search all the children which has `FunctionProvider` script, grab the first result
// 2.  call `GetTransforms()` as the results
// 3.  from the results, get first direct children named "ok"
[GetByXPath("//*@{GetComponent(FunctionProvider).GetTransforms()}/ok")] public GameObject[] getObjs;

// FunctionProvider.cs
public class FunctionProvider : MonoBehaviour
{
    // Example of returning some target
    public Transform[] GetTransforms() => transform.Cast<Transform>().ToArray();
}

filter

filter check if the results match the expected condition. There are two types of filter:

  • index filter:

    either just use the index: child*[1] (second one), child*[last()]/child*[-1] (last one)

    or compared value: child*[index() > 3]

  • value filter: use any attribute mentioned above, with >, != etc. for comparison. e.g. [@{gameObject.activeSelf}][@layer = "UI"]

using multiple filters means all conditions must be met. Otherwise, use the keyword OR: [@{GetComponent(Collider)} OR @{GetComponent(MyScript)}]

// find the first main camera in the scene
[GetByXPath("scene:://[@{tag} = MainCamera]")] public Camera mainCamera;

// find the prefabs with component "Hero"
[GetByXPath("assets:://Heros/*.prefab[@GetComponent(Hero)]")] public Camera mainCamera;

EXP

EXP is how all the auto getters works, behaviors in the inspector. The values are:

  • NoInitSign: do not assign the value if the value is null on firsts rendering.
  • NoAutoResignToValue: do not assign the value to the correct value on the following renderings.
  • NoAutoResignToNull: do not assign the value to null value if the target disappears on the following renderings.
  • NoResignButton: when NoAutoResign is on, by default there will be a reload button when value is mismatched. Turn this on to hide the reload button.
  • NoMessage: when NoAutoResign and NOResignButton is on, by default there will be an error box when value is mismatched. Turn this on to hide the error message.
  • NoPicker: this will remove the custom picker. This is on by default (if you do not pass EXP as first argument) to keep the consistency.
  • KeepOriginalPicker: UI Toolkit only. By default, when a custom picker is shown, Unity's default picker will hide. This will keep Unity's picker together.
  • ForceReOrder: Force the auto-getters to changes the order the way how the resources are found for list/array. This is useful when you want to get children of way points, as the order is important.

And some shortcut:

  • NoAutoResign = NoAutoResignToValue | NoAutoResignToNull
  • Silent = NoAutoResign | NoMessage. Useful if you want to allow you to manually assign a different value with no buttons and error box.
  • JustPicker = NoInitSign | NoAutoResignToValue | NoAutoResignToNull | NoResignButton | NoMessage. Do nothing but just give you a picker with matched targets.
  • Message = NoAutoResignToValue | NoAutoResignToNull | NoResignButton. Just give an error message if target is mismatched.

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

Saints Menu

Using csc.rsp

  1. Create file Assets/csc.rsp

  2. Write marcos like this:

    #"Disable DOTween"
    -define:SAINTSFIELD_DOTWEEN_DISABLE
    
    #"Disable Addressable"
    -define:SAINTSFIELD_ADDRESSABLE_DISABLE
    
    #"Disable AI Navigation"
    -define:SAINTSFIELD_AI_NAVIGATION_DISABLED
    
    #"Enable SaintsEditor project wide"
    -define:SAINTSFIELD_SAINTS_EDITOR_APPLY
    
    #"Enable the code analysis"
    -define:SAINTSFIELD_CODE_ANALYSIS
    

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.

Auto Validator

UI Toolkit: A simple validation tool under Window - Saints - Auto Validator, related to #115

This tool allows you to check if some target has Required but not filled, or an auto getter (e.g. GetComponentInChildren) but not filled or mismatched. Auto getters error will give you a button to fix it there. Note the fix function might be broken if the target is inside a prefab.

You can specify the targets as you want. Currently, it supports scenes, and folder searching.

It can also specify if you want to skip the hidden fields (hidden by ShowIf, HideIf. Not work for LayoutShowIf, LayoutHideIf)

This tool is very simple, and will get more update in the future.

See Auto Validator example code to learn how to make a quick auto validator for a specific group of assets.

video

Use With Other Drawers

SaintsField is designed to be compatible with other drawers if

  1. The attribute in SaintsField does not says "Enable SaintsEditor before using"
  2. the drawer itself respects unity-label class to Label for UI Toolkit
  3. if the drawer hijacks the CustomEditor, it must fall to the rest drawers

Note about OdinInspector: Odin is IMGUI based with UI Toolkit partly supported. It has label align issue with UI Toolkit. I asked them in discord but never get response. Using SaintsField with Odin, you might see incorrect label width.

Donation

Donation Link

PayPal: Image

Donation List

Thanks for the following generous donors:

Comments

No comments yet. Be the first!