2024-07-18 Unity插件 Odin Inspector8 —— Type Specific Attributes

1 说明

​ 本文介绍 Odin Inspector 插件中有关特定类型特性的使用方法。

2 特定类型特性

2.1 AssetList

用于 List、Array 和单个对象,将默认导航选择窗口替换为可筛选的 Asset 列表,以筛选、包含或排除 List、Array 中的 Asset。

  • string Path

    筛选 Asset 列表,仅包括位于指定路径的 Asset。

  • bool AutoPopulate

    如果为 true ,则将 Asset 列表中找到并显示的所有资产自动添加到列表中。

  • string LayerNames

    筛选 Asset 列表,使其仅包含具有指定 Layer 的资源。

  • string AssetNamePrefix

    筛选 Asset 列表,使其仅包含名称以开头的 Asset。

  • string Tags

    标签列表(以逗号分隔),用于筛选 Asset 列表。

  • string CustomFilterMethod

    筛选 Asset 列表,使其仅包含给定筛选方法返回 true 的 Asset。

image-20240718015148658
// AssetListExamplesComponent.cs

using Sirenix.OdinInspector;
using System.Collections.Generic;
using UnityEngine;

public class AssetListExamplesComponent : MonoBehaviour
{
    [AssetList]
    [PreviewField(70, ObjectFieldAlignment.Center)]
    public Texture2D SingleObject;

    [AssetList(Path = "/Plugins/Sirenix/")]
    public List<ScriptableObject> AssetList;

    [FoldoutGroup("Filtered Odin ScriptableObjects", expanded: false)]
    [AssetList(Path = "Plugins/Sirenix/")]
    public ScriptableObject Object;

    [AssetList(AutoPopulate = true, Path = "Plugins/Sirenix/")]
    [FoldoutGroup("Filtered Odin ScriptableObjects", expanded: false)]
    public List<ScriptableObject> AutoPopulatedWhenInspected;

    [AssetList(LayerNames = "MyLayerName")]
    [FoldoutGroup("Filtered AssetLists examples")]
    public GameObject[] AllPrefabsWithLayerName;

    [AssetList(AssetNamePrefix = "Rock")]
    [FoldoutGroup("Filtered AssetLists examples")]
    public List<GameObject> PrefabsStartingWithRock;

    [FoldoutGroup("Filtered AssetLists examples")]
    [AssetList(Tags = "MyTagA, MyTabB", Path = "/Plugins/Sirenix/")]
    public List<GameObject> GameObjectsWithTag;

    [FoldoutGroup("Filtered AssetLists examples")]
    [AssetList(CustomFilterMethod = "HasRigidbodyComponent")]
    public List<GameObject> MyRigidbodyPrefabs;

    private bool HasRigidbodyComponent(GameObject obj) {
        return obj.GetComponent<Rigidbody>() != null;
    }
}

2.2 AssetSelector

可用于所有 Unity 类型,在对象字段旁边添加小按钮。单击该按钮将显示资产下拉列表以供选择,可从该属性中进行自定义。

  • bool FlattenTreeView

    下拉列表是否树状显示。

  • string Paths

    指定搜索的文件夹。不指定任何文件夹则在整个项目中搜索(使用“|”作为分隔符表示多条路径)。

  • string Filter

    调用 AssetDatabase.FindAssets 时使用的过滤器。

  • bool DisableListAddButtonBehaviour = false

    禁用列表添加按钮行为。

  • bool DrawDropdownForListElements = true

    当 ValueDropdown 特性应用于该对象时,则禁用 DrawDropdownForListElements 将正常呈现所有子元素。

  • bool ExcludeExistingValuesInList

    当 ValueDropdown 特性应用于该对象时,并且 IsUniqueList 为 true,则启用 ExcludeExistingValuesInList 时,排除现有值,而不是显示一个复选框,指示该项是否已包含在内。

  • bool IsUniqueList = true

    子元素是否唯一。

  • bool ExpandAllMenuItems = true

    如果下拉菜单呈现树视图,则将其设置为 true 时,默认情况下所有内容都会被展开显示。

  • string DropdownTitle

    自定义下拉列表标题。

image-20240718020158603
// AssetSelectorExamplesComponent.cs

using Sirenix.OdinInspector;
using System.Collections.Generic;
using UnityEngine;

public class AssetSelectorExamplesComponent : MonoBehaviour
{
    [AssetSelector]
    public Material AnyAllMaterials;

    [AssetSelector]
    public Material[] ListOfAllMaterials;

    [AssetSelector(FlattenTreeView = true)]
    public Material NoTreeView;

    [AssetSelector(Paths = "Assets/MyScriptableObjects")]
    public ScriptableObject ScriptableObjectsFromFolder;

    [AssetSelector(Paths = "Assets/MyScriptableObjects|Assets/Other/MyScriptableObjects")]
    public Material ScriptableObjectsFromMultipleFolders;

    [AssetSelector(Filter = "name t:type l:label")]
    public UnityEngine.Object AssetDatabaseSearchFilters;

    [Title("Other Minor Features")]
    [AssetSelector(DisableListAddButtonBehaviour = true)]
    public List<GameObject> DisableListAddButtonBehaviour;

    [AssetSelector(DrawDropdownForListElements = false)]
    public List<GameObject> DisableListElementBehaviour;

    [AssetSelector(ExcludeExistingValuesInList = false)]
    public List<GameObject> ExcludeExistingValuesInList;

    [AssetSelector(IsUniqueList = false)]
    public List<GameObject> DisableUniqueListBehaviour;

    [AssetSelector(ExpandAllMenuItems = true)]
    public List<GameObject> ExpandAllMenuItems;

    [AssetSelector(DropdownTitle = "Custom Dropdown Title")]
    public List<GameObject> CustomDropdownTitle;
}

2.3 ChildGameObjectsOnly

可用于 Components 和 GameObject,并在对象旁添加小按钮,点击按钮将显示其子物体中所有满足条件的对象。

  • IncludeSelf = true

    是否包含自身。

  • bool IncludeInactive

    是否包含未激活的子物体。

image-20240718021903335
// ChildGameObjectsOnlyAttributeExamplesComponent.cs

using Sirenix.OdinInspector;
using UnityEngine;

public class ChildGameObjectsOnlyAttributeExamplesComponent : MonoBehaviour
{
    [ChildGameObjectsOnly]
    public Transform ChildOrSelfTransform;

    [ChildGameObjectsOnly]
    public GameObject ChildGameObject;

    [ChildGameObjectsOnly(IncludeSelf = false)]
    public Light[] Lights;
}

2.4 ColorPalette

用于任何 Color,允许从一组预定义的颜色选项中进行选择。

  • string paletteName

    调色板名称。

  • bool ShowAlpha = true

    是否显示透明度。

image-20240718022418293
// ColorPaletteExamplesComponent.cs

using Sirenix.OdinInspector;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class ColorPaletteExamplesComponent : MonoBehaviour
{
    [ColorPalette]
    public Color ColorOptions;

    [ColorPalette("Underwater")]
    public Color UnderwaterColor;

    [ColorPalette("My Palette")]
    public Color MyColor;

    public string DynamicPaletteName = "Clovers";

    // The ColorPalette attribute supports both 
    // member references and attribute expressions.
    [ColorPalette("$DynamicPaletteName")]
    public Color DynamicPaletteColor;

    [ColorPalette("Fall"), HideLabel]
    public Color WideColorPalette;

    [ColorPalette("Clovers")]
    public Color[] ColorArray;

    // ------------------------------------
    // Color palettes can be accessed and modified from code.
    // Note that the color palettes will NOT automatically be included in your builds.
    // But you can easily fetch all color palettes via the ColorPaletteManager 
    // and include them in your game like so:
    // ------------------------------------

    [FoldoutGroup("Color Palettes", expanded: false)]
    [ListDrawerSettings(IsReadOnly = true)]
    [PropertyOrder(9)]
    public List<ColorPalette> ColorPalettes;

    [Serializable]
    public class ColorPalette
    {
        [HideInInspector]
        public string Name;

        [LabelText("$Name")]
        [ListDrawerSettings(IsReadOnly = true, ShowFoldout = false)]
        public Color[] Colors;
    }

#if UNITY_EDITOR
    [FoldoutGroup("Color Palettes"), Button(ButtonSizes.Large), GUIColor(0, 1, 0), PropertyOrder(8)]
    private void FetchColorPalettes() {
        this.ColorPalettes = Sirenix.OdinInspector.Editor.ColorPaletteManager.Instance.ColorPalettes
                                    .Select(x => new ColorPalette() {
                                         Name   = x.Name,
                                         Colors = x.Colors.ToArray()
                                     })
                                    .ToList();
    }
#endif
}

2.5 DisplayAsString

用于任何对象,在 Inpector 窗口中将对象显示为字符串文本。想在 Inpector 窗口中显示字符串,但不允许进行任何编辑时,请使用此选项。

  • bool overflow

    true:字符串将溢出绘制的空间,并在没有足够的空间容纳文本时被剪切。

    false:如果绘制时没有足够的空间,字符串将扩展为多行。

  • int fontSize

    字体大小。

  • TextAlignment alignment

    对齐方式。

  • bool enableRichText

    是否开启富文本。

  • string Format

    用于格式化值的字符串。类型必须实现 IFormattable 接口。

image-20240718022535984
// DisplayAsStringExamplesComponent.cs

using Sirenix.OdinInspector;
using UnityEngine;

public class DisplayAsStringExamplesComponent : MonoBehaviour
{
    [InfoBox(
                "Instead of disabling values in the inspector in order to show some information or debug a value. " +
                "You can use DisplayAsString to show the value as text, instead of showing it in a disabled drawer")]
    [DisplayAsString]
    public Color SomeColor;

    [BoxGroup("SomeBox")]
    [HideLabel]
    [DisplayAsString]
    public string SomeText = "Lorem Ipsum";

    [InfoBox("The DisplayAsString attribute can also be configured to enable or disable overflowing to multiple lines.")]
    [HideLabel]
    [DisplayAsString]
    public string Overflow = "A very very very very very very very very very long string that has been configured to overflow.";

    [HideLabel]
    [DisplayAsString(false)]
    public string DisplayAllOfIt = "A very very very very very very very very long string that has been configured to not overflow.";

    [InfoBox("Additionally, you can also configure the string's alignment, font size, and whether it should support rich text or not.")]
    [DisplayAsString(false, 20, TextAlignment.Center, true)]
    public string CustomFontSizeAlignmentAndRichText = "This string is <b><color=#FF5555><i>super</i> <size=24>big</size></color></b> and centered.";
}

2.6 EnumPaging

为枚举添加“下一步”和“上一步”按钮选择器,循环查看枚举属性的可用值。

image-20240715003928053
// EnumPagingExamplesComponent.cs

using Sirenix.OdinInspector;
using UnityEngine;

public class EnumPagingExamplesComponent : MonoBehaviour
{
    [EnumPaging]
    public SomeEnum SomeEnumField;

    public enum SomeEnum
    {
        A, B, C
    }

#if UNITY_EDITOR // UnityEditor.Tool is an editor-only type, so this example will not work in a build
    [EnumPaging, OnValueChanged("SetCurrentTool")]
    [InfoBox("Changing this property will change the current selected tool in the Unity editor.")]
    public UnityEditor.Tool sceneTool;

    private void SetCurrentTool() {
        UnityEditor.Tools.current = this.sceneTool;
    }
#endif
}

2.7 EnumToggleButtons

在水平按钮组中绘制枚举,而不是下拉列表。

image-20240715004031983
// EnumToggleButtonsExamplesComponent.cs

using Sirenix.OdinInspector;
using UnityEngine;

public class EnumToggleButtonsExamplesComponent : MonoBehaviour
{
    [Title("Default")]
    public SomeBitmaskEnum DefaultEnumBitmask;

    [Title("Standard Enum")]
    [EnumToggleButtons]
    public SomeEnum SomeEnumField; // 单选枚举

    [EnumToggleButtons, HideLabel]
    public SomeEnum WideEnumField; // 单选枚举

    [Title("Bitmask Enum")]
    [EnumToggleButtons]
    public SomeBitmaskEnum BitmaskEnumField; // 多选枚举

    [EnumToggleButtons, HideLabel]
    public SomeBitmaskEnum EnumFieldWide; // 多选枚举

    [Title("Icon Enum")]
    [EnumToggleButtons, HideLabel]
    public SomeEnumWithIcons EnumWithIcons;

    [EnumToggleButtons, HideLabel]
    public SomeEnumWithIconsAndNames EnumWithIconsAndNames;

    public enum SomeEnum
    {
        First, Second, Third, Fourth, AndSoOn
    }

    public enum SomeEnumWithIcons
    {
        [LabelText(SdfIconType.TextLeft)]   TextLeft,
        [LabelText(SdfIconType.TextCenter)] TextCenter,
        [LabelText(SdfIconType.TextRight)]  TextRight,
    }

    public enum SomeEnumWithIconsAndNames
    {
        [LabelText("Align Left", SdfIconType.TextLeft)]
        TextLeft,

        [LabelText("Align Center", SdfIconType.TextCenter)]
        TextCenter,

        [LabelText("Align Right", SdfIconType.TextRight)]
        TextRight,
    }

    [System.Flags]
    public enum SomeBitmaskEnum
    {
        A   = 1 << 1,
        B   = 1 << 2,
        C   = 1 << 3,
        All = A | B | C
    }
}

2.8 FilePath

用于字符串,为文件路径提供接口。支持下拉选择文件路径和拖拽文件路径。

  • string ParentFolder

    父路径。可以相对于 Unity 项目,也可以是绝对路径。

  • string Extensions

    文件扩展名列表(以逗号分隔)。扩展名中的 “.” 可不写。

  • bool AbsolutePath

    是否为绝对路径。

  • bool RequireExistingPath

    true:若路径不存在,则显示警告提示。

  • bool UseBackslashes

    是否使用反斜杠(默认使用斜杠)。

image-20240718024034855
// FilePathExamplesComponent.cs
using Sirenix.OdinInspector;
using UnityEngine;

public class FilePathExamplesComponent : MonoBehaviour
{
    // By default, FolderPath provides a path relative to the Unity project.
    [FilePath]
    public string UnityProjectPath;
    
    // It is possible to provide custom parent path. Parent paths can be relative to the Unity project, or absolute.
    [FilePath(ParentFolder = "Assets/Plugins/Sirenix")]
    public string RelativeToParentPath;
    
    // Using parent path, FilePath can also provide a path relative to a resources folder.
    [FilePath(ParentFolder = "Assets/Resources")]
    public string ResourcePath;
    
    // Provide a comma seperated list of allowed extensions. Dots are optional.
    [FilePath(Extensions = "cs")]
    [BoxGroup("Conditions")]
    public string ScriptFiles;
    
    // By setting AbsolutePath to true, the FilePath will provide an absolute path instead.
    [FilePath(AbsolutePath = true)]
    [BoxGroup("Conditions")]
    public string AbsolutePath;
    
    // FilePath can also be configured to show an error, if the provided path is invalid.
    [FilePath(RequireExistingPath = true)]
    [BoxGroup("Conditions")]
    public string ExistingPath;
    
    // By default, FilePath will enforce the use of forward slashes. It can also be configured to use backslashes instead.
    [FilePath(UseBackslashes = true)]
    [BoxGroup("Conditions")]
    public string Backslashes;
    
    // FilePath also supports member references with the $ symbol.
    [FilePath(ParentFolder = "$DynamicParent", Extensions = "$DynamicExtensions")]
    [BoxGroup("Member referencing")]
    public string DynamicFilePath;
    
    [BoxGroup("Member referencing")]
    public string DynamicParent = "Assets/Plugins/Sirenix";
    
    [BoxGroup("Member referencing")]
    public string DynamicExtensions = "cs, unity, jpg";
    
    // FilePath also supports lists and arrays.
    [FilePath(ParentFolder = "Assets/Plugins/Sirenix/Demos/Odin Inspector")]
    [BoxGroup("Lists")]
    public string[] ListOfFiles;
}

2.9 FolderPath

用于字符串,为目录路径提供接口。支持下拉选择文件夹目录和拖拽文件夹目录。

  • string ParentFolder

    父路径。可以相对于 Unity 项目,也可以是绝对路径。

  • bool AbsolutePath

    是否为绝对路径。

  • bool RequireExistingPath

    true:若路径不存在,则显示警告提示。

  • bool UseBackslashes

    是否使用反斜杠(默认使用斜杠)。

image-20240718024439650
// FolderPathExamplesComponent.cs

using Sirenix.OdinInspector;
using UnityEngine;

public class FolderPathExamplesComponent : MonoBehaviour
{
    // By default, FolderPath provides a path relative to the Unity project.
    [FolderPath]
    public string UnityProjectPath;

    // It is possible to provide custom parent path. Parent paths can be relative to the Unity project, or absolute.
    [FolderPath(ParentFolder = "Assets/Plugins/Sirenix")]
    public string RelativeToParentPath;

    // Using parent path, FolderPath can also provide a path relative to a resources folder.
    [FolderPath(ParentFolder = "Assets/Resources")]
    public string ResourcePath;

    // By setting AbsolutePath to true, the FolderPath will provide an absolute path instead.
    [FolderPath(AbsolutePath = true)]
    [BoxGroup("Conditions")]
    public string AbsolutePath;

    // FolderPath can also be configured to show an error, if the provided path is invalid.
    [FolderPath(RequireExistingPath = true)]
    [BoxGroup("Conditions")]
    public string ExistingPath;

    // By default, FolderPath will enforce the use of forward slashes. It can also be configured to use backslashes instead.
    [FolderPath(UseBackslashes = true)]
    [BoxGroup("Conditions")]
    public string Backslashes;

    // FolderPath also supports member references and attribute expressions with the $ symbol.
    [FolderPath(ParentFolder = "$DynamicParent")]
    [BoxGroup("Member referencing")]
    public string DynamicFolderPath;

    [BoxGroup("Member referencing")]
    public string DynamicParent = "Assets/Plugins/Sirenix";

    // FolderPath also supports lists and arrays.
    [FolderPath(ParentFolder = "Assets/Plugins/Sirenix")]
    [BoxGroup("Lists")]
    public string[] ListOfFolders;
}

2.10 HideInInlineEditors

如果对象被 InlineEditor 特性绘制,则该对象在 Inspector 窗口中隐藏。

补充 InlineEditor 特性:将继承 UnityEngine.Object 的类(如 ScriptableObject)的详细信息显示在 Inspector 窗口。

image-20240718025035866

​ 数据结构类:

using UnityEngine;

#nullable disable
namespace Sirenix.OdinInspector.Editor.Examples
{
  public class MyInlineScriptableObject : ScriptableObject
  {
    [ShowInInlineEditors]
    public string ShownInInlineEditor;
    [HideInInlineEditors]
    public string HiddenInInlineEditor;
  }
}

​ 挂载的脚本:

// ShowAndHideInInlineEditorExampleComponent.cs
using Sirenix.OdinInspector;
using UnityEngine;

#if UNITY_EDITOR // Editor namespaces can only be used in the editor.
using Sirenix.OdinInspector.Editor.Examples;
#endif

public class ShowAndHideInInlineEditorExampleComponent : MonoBehaviour
{
#if UNITY_EDITOR // MyInlineScriptableObject is an example type and only exists in the editor
    [InfoBox("Click the pen icon to open a new inspector window for the InlineObject too see the differences these attributes make.")]
    [InlineEditor(Expanded = true)]
    public MyInlineScriptableObject InlineObject;
#endif
    
#if UNITY_EDITOR // Editor-related code must be excluded from builds
    [OnInspectorInit]
    private void CreateData()
    {
        InlineObject = ExampleHelper.GetScriptableObject<MyInlineScriptableObject>("Inline Object");
    }
    
    [OnInspectorDispose]
    private void CleanupData()
    {
        if (InlineObject != null) Object.DestroyImmediate(InlineObject);
    }
#endif
}

2.11 HideInTables

用于防止对象在使用 TableListAttribute 绘制的表中显示为列。

image-20240718025150278
// HideInTablesExampleComponent.cs

using Sirenix.OdinInspector;
using System;
using System.Collections.Generic;
using UnityEngine;

public class HideInTablesExampleComponent : MonoBehaviour
{
    public MyItem Item = new MyItem();

    [TableList]
    public List<MyItem> Table = new List<MyItem>() {
        new MyItem(),
        new MyItem(),
        new MyItem(),
    };

    [Serializable]
    public class MyItem
    {
        public string A;

        public int B;

        [HideInTables]
        public int Hidden;
    }
}

2.12 HideMonoScript

阻止某个类型的所有对象在 Inspector窗口中显示其成员。

image-20240718030213540

​ 数据结构类:

using UnityEngine;

#nullable disable
namespace Sirenix.OdinInspector.Editor.Examples
{
  public class ShowMonoScriptScriptableObject : ScriptableObject
  {
    public string Value;
  }
    
  [HideMonoScript]
  public class HideMonoScriptScriptableObject : ScriptableObject
  {
    public string Value;
  }
}

​ 挂载的脚本:

// HideMonoScriptExampleComponent.cs

using Sirenix.OdinInspector;
using UnityEngine;

#if UNITY_EDITOR // Editor namespaces can only be used in the editor.
using Sirenix.OdinInspector.Editor.Examples;
#endif

public class HideMonoScriptExampleComponent : MonoBehaviour
{
#if UNITY_EDITOR // HideMonoScriptScriptableObject and ShowMonoScriptScriptableObject are example types and only exist in the editor
    [InfoBox("Click the pencil icon to open new inspector for these fields.")]
    public HideMonoScriptScriptableObject Hidden;

    // The script will also be hidden for the ShowMonoScript object if MonoScripts are hidden globally.
    public ShowMonoScriptScriptableObject Shown;
#endif

#if UNITY_EDITOR // Editor-related code must be excluded from builds
    [OnInspectorInit]
    private void CreateData() {
        Hidden = ExampleHelper.GetScriptableObject<HideMonoScriptScriptableObject>("Hidden");
        Shown  = ExampleHelper.GetScriptableObject<ShowMonoScriptScriptableObject>("Shown");
    }

    [OnInspectorDispose]
    private void CleanupData() {
        if (Hidden != null) Object.DestroyImmediate(Hidden);
        if (Shown != null) Object.DestroyImmediate(Shown);
    }
#endif
}

2.13 HideReferenceObjectPicker

将多态对象选择器隐藏在非 Unity 序列化引用类型的属性中。
当对象选择器隐藏时,可以右键单击并将实例设置为 null,以设置新值。

也可以使用 DisableContextMenu 特性来确保无法更改该值。

image-20240718030548801
// HideReferenceObjectPickerExamplesComponent.cs

using Sirenix.OdinInspector;
using UnityEngine;

public class HideReferenceObjectPickerExamplesComponent : SerializedMonoBehaviour
{
    [Title("Hidden Object Pickers")]
    [HideReferenceObjectPicker]
    public MyCustomReferenceType OdinSerializedProperty1 = new MyCustomReferenceType();

    [HideReferenceObjectPicker]
    public MyCustomReferenceType OdinSerializedProperty2 = new MyCustomReferenceType();

    [Title("Shown Object Pickers")]
    public MyCustomReferenceType OdinSerializedProperty3 = new MyCustomReferenceType();

    public MyCustomReferenceType OdinSerializedProperty4 = new MyCustomReferenceType();

    // Protip: You can also put the HideInInspector attribute on the class definition itself to hide it globally for all members.
    // [HideReferenceObjectPicker]
    public class MyCustomReferenceType
    {
        public int A;
        public int B;
        public int C;
    }
}

2.14 InlineEditor

将继承 UnityEngine.Object 的类(如 ScriptableObject)的详细信息显示在 Inspector 窗口。

  • InlineEditorModes inlineEditorMode = InlineEditorModes.GUIOnly

    绘制模式。

  • InlineEditorObjectFieldModes objectFieldMode = InlineEditorObjectFieldModes.Boxed

    绘制方式。

image-20240718031212511

2.15 Multiline

使用多行文本字段编辑字符串。

  • int lines = 3

    行数。

image-20240718031531320
// MultiLinePropertyExamplesComponent.cs

using Sirenix.OdinInspector;
using UnityEngine;

public class MultiLinePropertyExamplesComponent : MonoBehaviour
{
    // Unity's TextArea and Multiline attributes and Odin's MultiLineProperty attribute
    // are all very similar.
    // 
    // TextArea specifies a minimum and maximum number of lines. It will display at least
    // the minimum number of lines, but will expand with its content up to the maximum
    // number of lines, and display a scrollbar past that.
    // 
    // Multiline and MultiLineProperty are given a precise number of lines to occupy and
    // will never contract or expand based on contents; instead they display a scrollbar
    // if the content does not fit into the given number of lines.
    // 
    // Finally, unlike Multiline, Odin's MultiLineProperty can be applied to any member
    // type including fields, properties, method arguments, types, and so on.

    [TextArea(4, 10)]
    public string UnityTextAreaField = "";

    [Multiline(10)]
    public string UnityMultilineField = "";

    [Title("Wide Multiline Text Field", bold: false)]
    [HideLabel]
    [MultiLineProperty(10)]
    public string WideMultilineTextField = "";

    [InfoBox("Odin supports properties, but Unity's own Multiline attribute only works on fields.")]
    [ShowInInspector]
    [MultiLineProperty(10)]
    public string OdinMultilineProperty { get; set; }
}

2.16 PreviewField

绘制方形 ObjectField,预览对象类型。

  1. 拖拽对象到另一个对象,将交换值。
  2. 按住 ctrl 同时松开,将替换值。
  3. 按住 ctrl 并单击对象,将快速删除值。

可以从 Odin 首选项窗口选择性启用和全局自定义操作。

  • string previewGetter

    预览纹理的解析值。

  • float height

    预览窗口高度。

  • ObjectFieldAlignment alignment

    预览窗口对齐方式。

  • FilterMode filterMode = FilterMode.Bilinear

    预览纹理的过滤模式。

image-20240718032140205
// PreviewFieldExamplesComponent.cs

using Sirenix.OdinInspector;
using UnityEngine;

#if UNITY_EDITOR // Editor namespaces can only be used in the editor.
using Sirenix.OdinInspector.Editor.Examples;
#endif

public class PreviewFieldExamplesComponent : MonoBehaviour
{
    [PreviewField]
    public Object RegularPreviewField;

    [VerticalGroup("row1/left")]
    public string A, B, C;

    [HideLabel]
    [PreviewField(50, ObjectFieldAlignment.Right)]
    [HorizontalGroup("row1", 50), VerticalGroup("row1/right")]
    public Object D;

    [HideLabel]
    [PreviewField(50, ObjectFieldAlignment.Left)]
    [HorizontalGroup("row2", 50), VerticalGroup("row2/left")]
    public Object E;

    [VerticalGroup("row2/right"), LabelWidth(-54)]
    public string F, G, H;

    [PreviewField("preview", FilterMode.Bilinear)]
    public Object I;

    private Texture preview;

#if UNITY_EDITOR // Editor-related code must be excluded from builds
    [OnInspectorInit]
    private void CreateData() {
        RegularPreviewField = ExampleHelper.GetTexture();
        D                   = ExampleHelper.GetTexture();
        E                   = ExampleHelper.GetTexture();
        I                   = ExampleHelper.GetMesh();
        preview             = ExampleHelper.GetTexture();
    }

    [InfoBox(
                "These object fields can also be selectively enabled and customized globally " +
                "from the Odin preferences window.\n\n" +
                " - Hold Ctrl + Click = Delete Instance\n" +
                " - Drag and drop = Move / Swap.\n" +
                " - Ctrl + Drag = Replace.\n" +
                " - Ctrl + drag and drop = Move and override.")]
    [PropertyOrder(-1)]
    [Button(ButtonSizes.Large)]
    private void ConfigureGlobalPreviewFieldSettings() {
        Sirenix.OdinInspector.Editor.GeneralDrawerConfig.Instance.OpenInEditor();
    }
#endif
}

2.17 PolymorphicDrawerSettings

提供“多态类型”的绘制选项。

  • bool ShowBaseType

    是否显示基类。

  • bool ReadOnlyIfNotNullReference

    对象一旦被赋值,是否还可以改变。

  • NonDefaultConstructorPreference NonDefaultConstructorPreference

    指定如何处理非默认构造函数。

  • string CreateInstanceFunction

    指定自定义函数,用于创建所选对象的实例。

image-20240718033449480
// PolymorphicDrawerSettingsExampleComponent.cs

using System;
using Sirenix.OdinInspector;
using Sirenix.Serialization;
using Sirenix.Utilities;
using UnityEngine;

public class PolymorphicDrawerSettingsExampleComponent : MonoBehaviour
{
    [ShowInInspector]
    public IDemo<int> Default;
    
    [Title("Show Base Type"), ShowInInspector, LabelText("On")]
    [PolymorphicDrawerSettings(ShowBaseType = true)]
    public IDemo<int> ShowBaseType_On;
    
    [ShowInInspector, LabelText("Off")]
    [PolymorphicDrawerSettings(ShowBaseType = false)]
    public IDemo<int> ShowBaseType_Off;
    
    [Title("Read Only If Not Null Reference"), ShowInInspector, LabelText("On")]
    [PolymorphicDrawerSettings(ReadOnlyIfNotNullReference = true)]
    public IDemo<int> ReadOnlyIfNotNullReference_On;
    
    [ShowInInspector, LabelText("Off")]
    [PolymorphicDrawerSettings(ReadOnlyIfNotNullReference = false)]
    public IDemo<int> ReadOnlyIfNotNullReference_Off;
    
    [Title("Non Default Constructor Preference"), ShowInInspector, LabelText("Exclude")]
    [PolymorphicDrawerSettings(NonDefaultConstructorPreference = NonDefaultConstructorPreference.Exclude)]
    public IVector2<int> NonDefaultConstructorPreference_Ignore;
    
    [ShowInInspector, LabelText("Construct Ideal")]
    [PolymorphicDrawerSettings(NonDefaultConstructorPreference = NonDefaultConstructorPreference.ConstructIdeal)]
    public IVector2<int> NonDefaultConstructorPreference_ConstructIdeal;
    
    [ShowInInspector, LabelText("Prefer Uninitialized")]
    [PolymorphicDrawerSettings(NonDefaultConstructorPreference = NonDefaultConstructorPreference.PreferUninitialized)]
    public IVector2<int> NonDefaultConstructorPreference_PreferUninit;
    
    [ShowInInspector, LabelText("Log Warning")]
    [PolymorphicDrawerSettings(NonDefaultConstructorPreference = NonDefaultConstructorPreference.LogWarning)]
    public IVector2<int> NonDefaultConstructorPreference_LogWarning;
    
    [Title("Create Custom Instance"), ShowInInspector]
    [PolymorphicDrawerSettings(CreateInstanceFunction = nameof(CreateInstance))]
    public IVector2<int> CreateCustomInstance;
    
    private IVector2<int> CreateInstance(Type type)
    {
        Debug.Log("Constructor called for " + type + '.');
    
        if (typeof(SomeNonDefaultCtorClass) == type)
        {
            return new SomeNonDefaultCtorClass(485);
        }
    
        return type.InstantiateDefault(false) as IVector2<int>;
    }
    
    public interface IVector2<T>
    {
        T X { get; set; }
        T Y { get; set; }
    }
    
    [Serializable]
    public class SomeNonDefaultCtorClass : IVector2<int>
    {
        [OdinSerialize]
        public int X { get; set; }
    
        [OdinSerialize]
        public int Y { get; set; }
    
        public SomeNonDefaultCtorClass(int x)
        {
            this.X = x;
            this.Y = (x + 1) * 4;
        }
    }
    
    public interface IDemo<T>
    {
        T Value { get; set; }
    }
    
    [Serializable]
    public class DemoSOInt32 : SerializedScriptableObject, IDemo<int>
    {
        [OdinSerialize]
        public int Value { get; set; }
    }
    
    [Serializable]
    public class DemoSOInt32Target : SerializedScriptableObject, IDemo<int>
    {
        [OdinSerialize]
        public int Value { get; set; }
    
        public int target;
    }
    
    [Serializable]
    public class DemoSOFloat32 : SerializedScriptableObject, IDemo<float>
    {
        [OdinSerialize]
        public float Value { get; set; }
    }
    
    [Serializable]
    public class Demo<T> : IDemo<T>
    {
        [OdinSerialize]
        public T Value { get; set; }
    }
    
    [Serializable]
    public class DemoInt32Interface : IDemo<int>
    {
        [OdinSerialize]
        public int Value { get; set; }
    }
    
    public class DemoInt32 : Demo<int> { }
    
    public struct DemoStructInt32 : IDemo<int>
    {
        [OdinSerialize]
        public int Value { get; set; }
    }
}

2.18 TypeDrawerSettings

提供“类型”的绘制选项。

  • Type BaseType

    指定是否应使用基类型而不是所有类型。

  • TypeInclusionFilter Filter = TypeInclusionFilter.IncludeAll;

    过滤类型的方法。

image-20240718034227737
// TypeDrawerSettingsAttributeExampleComponent.cs

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Sirenix.OdinInspector;
using UnityEngine;

public class TypeDrawerSettingsAttributeExampleComponent : MonoBehaviour
{
    [ShowInInspector]
    public Type Default;

    [Title("Base Type"), ShowInInspector, LabelText("Set")]
    [TypeDrawerSettings(BaseType = typeof(IEnumerable<>))]
    public Type BaseType_Set;

    [ShowInInspector, LabelText("Not Set")]
    [TypeDrawerSettings(BaseType = null)]
    public Type BaseType_NotSet;

    [Title(nameof(TypeDrawerSettingsAttribute.Filter)), ShowInInspector, LabelText("Concrete Types")]
    [TypeDrawerSettings(BaseType = typeof(IBaseGeneric<>), Filter = TypeInclusionFilter.IncludeConcreteTypes)]
    public Type Filter_Default;

    [ShowInInspector, LabelText("Concrete- && Generic Types")]
    [TypeDrawerSettings(BaseType = typeof(IBaseGeneric<>), Filter = TypeInclusionFilter.IncludeConcreteTypes | TypeInclusionFilter.IncludeGenerics)]
    public Type Filter_Generics;

    [ShowInInspector, LabelText("Concrete- && Interface Types")]
    [TypeDrawerSettings(BaseType = typeof(IBaseGeneric<>), Filter = TypeInclusionFilter.IncludeConcreteTypes | TypeInclusionFilter.IncludeInterfaces)]
    public Type Filter_Interfaces;

    [ShowInInspector, LabelText("Concrete- && Abstract Types")]
    [TypeDrawerSettings(BaseType = typeof(IBaseGeneric<>), Filter = TypeInclusionFilter.IncludeConcreteTypes | TypeInclusionFilter.IncludeAbstracts)]
    public Type Filter_Abstracts;

    [ShowInInspector, LabelText("Concrete-, Abstract- && Generic Types")]
    [TypeDrawerSettings(BaseType = typeof(IBaseGeneric<>), Filter = TypeInclusionFilter.IncludeConcreteTypes |
                                                                    TypeInclusionFilter.IncludeAbstracts |
                                                                    TypeInclusionFilter.IncludeGenerics)]
    public Type Filter_Abstracts_Generics;

    [ShowInInspector, LabelText("Concrete-, Interface- && Generic Types")]
    [TypeDrawerSettings(BaseType = typeof(IBaseGeneric<>), Filter = TypeInclusionFilter.IncludeConcreteTypes |
                                                                    TypeInclusionFilter.IncludeInterfaces |
                                                                    TypeInclusionFilter.IncludeGenerics)]
    public Type Filter_Interfaces_Generics;

    [ShowInInspector, LabelText("Concrete-, Interface- && Abstract Types")]
    [TypeDrawerSettings(BaseType = typeof(IBaseGeneric<>), Filter = TypeInclusionFilter.IncludeConcreteTypes |
                                                                    TypeInclusionFilter.IncludeInterfaces |
                                                                    TypeInclusionFilter.IncludeAbstracts)]
    public Type Filter_Interfaces_Abstracts;

    [ShowInInspector, LabelText("All")]
    [TypeDrawerSettings(BaseType = typeof(IBaseGeneric<>), Filter = TypeInclusionFilter.IncludeAll)]
    public Type Filter_All;

    public interface IBaseGeneric<T>
    { }

    public interface IBase : IBaseGeneric<int>
    { }

    public abstract class Base : IBase
    { }

    public class Concrete : Base
    { }

    public class ConcreteGeneric<T> : Base
    { }

    public abstract class BaseGeneric<T> : IBase
    { }

    [CompilerGenerated]
    public class ConcreteGenerated : Base
    { }
}

2.19 SceneObjectsOnly

使目标对象在 Inspector 窗口中只能场景对象,限制拖拽的资源类型。

image-20240715004717713
// SceneAndAssetsOnlyExamplesComponent.cs

using Sirenix.OdinInspector;
using System.Collections.Generic;
using UnityEngine;

public class SceneAndAssetsOnlyExamplesComponent : MonoBehaviour
{
    [Title("Assets only")]
    [AssetsOnly]
    public List<GameObject> OnlyPrefabs;

    [AssetsOnly]
    public GameObject SomePrefab;

    [AssetsOnly]
    public Material MaterialAsset;

    [AssetsOnly]
    public MeshRenderer SomeMeshRendererOnPrefab;

    [Title("Scene Objects only")]
    [SceneObjectsOnly]
    public List<GameObject> OnlySceneObjects;

    [SceneObjectsOnly]
    public GameObject SomeSceneObject;

    [SceneObjectsOnly]
    public MeshRenderer SomeMeshRenderer;
}

2.20 TableList

将 List 绘制成表格形状。

  • bool IsReadOnly = true

    是否在 Inspector 窗口中只读。

  • int NumberOfItemsPerPage

    列表每页的成员个数,超过该个数则会翻页。

  • bool ShowIndexLabels

    是否显示列表每个 item 的下标。

  • bool ShowPaging = true

    是否启用分页显示。

  • bool ShowItemCount = true

    是否显示成员个数。

  • bool HideToolbar = false

    是否隐藏标题栏。

  • bool DrawScrollView = true

    是否绘制滚动条。

  • int MaxScrollViewHeight/MinScrollViewHeight

    滚动条绘制的范围(最大高度/最小高度),单位:像素。

  • bool AlwaysExpanded

    List 是否可折叠。

image-20240715021626235
// TableListExamplesComponent.cs

using Sirenix.OdinInspector;
using System;
using System.Collections.Generic;
using UnityEngine;

#if UNITY_EDITOR // Editor namespaces can only be used in the editor.
using Sirenix.OdinInspector.Editor.Examples;
#endif

public class TableListExamplesComponent : MonoBehaviour
{
    [TableList(ShowIndexLabels = true)]
    public List<SomeCustomClass> TableListWithIndexLabels = new List<SomeCustomClass>() {
        new SomeCustomClass(),
        new SomeCustomClass(),
    };

    [TableList(DrawScrollView = true, MaxScrollViewHeight = 200, MinScrollViewHeight = 100)]
    public List<SomeCustomClass> MinMaxScrollViewTable = new List<SomeCustomClass>() {
        new SomeCustomClass(),
        new SomeCustomClass(),
    };

    [TableList(AlwaysExpanded = true, DrawScrollView = false)]
    public List<SomeCustomClass> AlwaysExpandedTable = new List<SomeCustomClass>() {
        new SomeCustomClass(),
        new SomeCustomClass(),
    };

    [TableList(ShowPaging = true, NumberOfItemsPerPage = 3)]
    public List<SomeCustomClass> TableWithPaging = new List<SomeCustomClass>() {
        new SomeCustomClass(),
        new SomeCustomClass(),
    };

    [Serializable]
    public class SomeCustomClass
    {
        [TableColumnWidth(57, Resizable = false)]
        [PreviewField(Alignment = ObjectFieldAlignment.Center)]
        public Texture Icon;

        [TextArea]
        public string Description;

        [VerticalGroup("Combined Column"), LabelWidth(22)]
        public string A, B, C;

        [TableColumnWidth(60)]
        [Button, VerticalGroup("Actions")]
        public void Test1() { }

        [TableColumnWidth(60)]
        [Button, VerticalGroup("Actions")]
        public void Test2() { }

#if UNITY_EDITOR // Editor-related code must be excluded from builds
        [OnInspectorInit]
        private void CreateData() {
            Description = ExampleHelper.GetString();
            Icon        = ExampleHelper.GetTexture();
        }
#endif
    }
}

2.21 TableMatrix

绘制二维数组。

  1. 单元格绘制。

    • string HorizontalTitle

      水平标题。

    • bool SquareCells

      如果为 true,则每行的高度将与第一个单元格的宽度相同。

image-20240715022259336
// TableMatrixExamplesComponent.cs

using Sirenix.OdinInspector;
using UnityEngine;

#if UNITY_EDITOR // Editor namespaces can only be used in the editor.
using Sirenix.OdinInspector.Editor.Examples;
#endif

public class TableMatrixExamplesComponent : SerializedMonoBehaviour
{
    [TableMatrix(HorizontalTitle = "Square Celled Matrix", SquareCells = true)]
    public Texture2D[,] SquareCelledMatrix;

    [TableMatrix(SquareCells = true)]
    public Mesh[,] PrefabMatrix;

#if UNITY_EDITOR // Editor-related code must be excluded from builds
    [OnInspectorInit]
    private void CreateData() {
        SquareCelledMatrix = new Texture2D[8, 4] {
            { ExampleHelper.GetTexture(), null, null, null },
            { null, ExampleHelper.GetTexture(), null, null },
            { null, null, ExampleHelper.GetTexture(), null },
            { null, null, null, ExampleHelper.GetTexture() },
            { ExampleHelper.GetTexture(), null, null, null },
            { null, ExampleHelper.GetTexture(), null, null },
            { null, null, ExampleHelper.GetTexture(), null },
            { null, null, null, ExampleHelper.GetTexture() },
        };

        PrefabMatrix = new Mesh[8, 4] {
            { ExampleHelper.GetMesh(), null, null, null },
            { null, ExampleHelper.GetMesh(), null, null },
            { null, null, ExampleHelper.GetMesh(), null },
            { null, null, null, ExampleHelper.GetMesh() },
            { null, null, null, ExampleHelper.GetMesh() },
            { null, null, ExampleHelper.GetMesh(), null },
            { null, ExampleHelper.GetMesh(), null, null },
            { ExampleHelper.GetMesh(), null, null, null },
        };
    }
#endif
}
  1. 表格绘制

    • bool IsReadOnly

      如果为 true,则插入、删除和拖动列和行将不可用。但单元格本身仍将是可修改的。

      如果要禁用所有内容,可以使用 ReadOnly 属性。

    • string VerticalTitle

      垂直标题。

image-20240715022550956
// TableMatrixTitleExampleComponent.cs

using Sirenix.OdinInspector;
using UnityEngine;

public class TableMatrixTitleExampleComponent : SerializedMonoBehaviour
{
    [TableMatrix(HorizontalTitle = "Read Only Matrix", IsReadOnly = true)]
    public int[,] ReadOnlyMatrix = new int[5, 5];

    [TableMatrix(HorizontalTitle = "X axis", VerticalTitle = "Y axis")]
    public InfoMessageType[,] LabledMatrix = new InfoMessageType[6, 6];
}
  1. 图形绘制

    • string DrawElementMethod

      覆盖绘制每个单元格的方式。

      输入参数:Rect rect, T value

      输出:T

    • bool ResizableColumns = true

      列是否可调整大小。

    • RowHeight

      行高,0 表示默认高度。

    • bool Transpose

      如果为 true,则表的行/列颠倒绘制(C# 初始化顺序)。

image-20240715023026208
// TransposeTableMatrixExampleComponent.cs

using Sirenix.OdinInspector;
using Sirenix.Utilities;
using UnityEngine;

public class TransposeTableMatrixExampleComponent : SerializedMonoBehaviour
{
    [TableMatrix(HorizontalTitle = "Custom Cell Drawing", DrawElementMethod = nameof(DrawColoredEnumElement), ResizableColumns = false, RowHeight = 16)]
    public bool[,] CustomCellDrawing;

    [ShowInInspector, DoNotDrawAsReference]
    [TableMatrix(HorizontalTitle = "Transposed Custom Cell Drawing", DrawElementMethod = "DrawColoredEnumElement", ResizableColumns = false, RowHeight = 16, Transpose = true)]
    public bool[,] Transposed { get { return CustomCellDrawing; } set { CustomCellDrawing = value; } }

#if UNITY_EDITOR // Editor-related code must be excluded from builds
    private static bool DrawColoredEnumElement(Rect rect, bool value) {
        if (Event.current.type == EventType.MouseDown && rect.Contains(Event.current.mousePosition)) {
            value       = !value;
            GUI.changed = true;
            Event.current.Use();
        }

        UnityEditor.EditorGUI.DrawRect(rect.Padding(1), value ? new Color(0.1f, 0.8f, 0.2f) : new Color(0, 0, 0, 0.5f));

        return value;
    }

    [OnInspectorInit]
    private void CreateData() {
        // =)
        this.CustomCellDrawing        = new bool[15, 15];
        this.CustomCellDrawing[6, 5]  = true;
        this.CustomCellDrawing[6, 6]  = true;
        this.CustomCellDrawing[6, 7]  = true;
        this.CustomCellDrawing[8, 5]  = true;
        this.CustomCellDrawing[8, 6]  = true;
        this.CustomCellDrawing[8, 7]  = true;
        this.CustomCellDrawing[5, 9]  = true;
        this.CustomCellDrawing[5, 10] = true;
        this.CustomCellDrawing[9, 9]  = true;
        this.CustomCellDrawing[9, 10] = true;
        this.CustomCellDrawing[6, 11] = true;
        this.CustomCellDrawing[7, 11] = true;
        this.CustomCellDrawing[8, 11] = true;
    }
#endif
}

2.22 Toggle

将对象放在开关组中绘制。

  • string toggleMemberName

    用于启用或禁用对象的任何 bool 字段或属性的名称。

  • bool CollapseOthersOnExpand = true

    如果为 true,其中一个打开,其他的开关组都将折叠显示。

image-20240718034704631
// ToggleExampleComponent.cs

using Sirenix.OdinInspector;
using System;
using UnityEngine;

public class ToggleExampleComponent : MonoBehaviour
{
    [Toggle("Enabled")]
    public MyToggleable Toggler = new MyToggleable();

    public ToggleableClass Toggleable = new ToggleableClass();

    [Serializable]
    public class MyToggleable
    {
        public bool Enabled;
        public int  MyValue;
    }

    // You can also use the Toggle attribute directly on a class definition.
    [Serializable, Toggle("Enabled")]
    public class ToggleableClass
    {
        public bool   Enabled;
        public string Text;
    }
}

2.23 ToggleGroup

将对象添加进单选框组。

  • string toggleMemberName

    启用或禁用 ToggleGroup 的任何 bool 字段或属性的名称。

  • float order = 0.0f

    单选框组的排序。

  • string groupTitle = null

    Inspector 窗口中显示的标题名称。

image-20240716043131827
// ToggleGroupExamplesComponent.cs

using Sirenix.OdinInspector;
using System;
using UnityEngine;

public class ToggleGroupExamplesComponent : MonoBehaviour
{
    // Simple Toggle Group
    [ToggleGroup("MyToggle")]
    public bool MyToggle;

    [ToggleGroup("MyToggle")]
    public float A;

    [ToggleGroup("MyToggle")]
    [HideLabel, Multiline]
    public string B;

    // Toggle for custom data.
    [ToggleGroup("EnableGroupOne", "$GroupOneTitle")]
    public bool EnableGroupOne = true;

    [ToggleGroup("EnableGroupOne")]
    public string GroupOneTitle = "One";

    [ToggleGroup("EnableGroupOne")]
    public float GroupOneA;

    [ToggleGroup("EnableGroupOne")]
    public float GroupOneB;

    // Toggle for individual objects.
    [Toggle("Enabled")]
    public MyToggleObject Three = new MyToggleObject();

    [Toggle("Enabled")]
    public MyToggleA Four = new MyToggleA();

    [Toggle("Enabled")]
    public MyToggleB Five = new MyToggleB();

    public MyToggleC[] ToggleList = new MyToggleC[] {
        new MyToggleC() { Test = 2f, Enabled = true, },
        new MyToggleC() { Test = 5f, },
        new MyToggleC() { Test = 7f, },
    };

    [Serializable]
    public class MyToggleObject
    {
        public bool Enabled;

        [HideInInspector]
        public string Title;

        public int A;
        public int B;
    }

    [Serializable]
    public class MyToggleA : MyToggleObject
    {
        public float C;
        public float D;
        public float F;
    }

    [Serializable]
    public class MyToggleB : MyToggleObject
    {
        public string Text;
    }

    [Serializable]
    public class MyToggleC
    {
        [ToggleGroup("Enabled", "$Label")]
        public bool Enabled;

        public string Label { get { return this.Test.ToString(); } }

        [ToggleGroup("Enabled")]
        public float Test;
    }
}
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蔗理苦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值