ODIN
개발을 하다보면 인스펙터 창이 상당히 비가시적이 되거나 복잡해지는 경우가 많은데 Odin 에셋은 이에 꽤나 도움이 된다. 인스펙터 창을 직접 사용자화 함으로써 일단 구현을 해놓으면 후에 매우 편리하게 값들을 변경할 수 있게된다.밑은 구매링크이다. 세일 기간을 노리면 50프로까지 할인을 받을 수 있으니 좋은 때를 노려보자.https://assetstore.unity.com/packages/tools/utilities/odin-inspector-and-serializer-89041
Odin Inspector and Serializer | 유틸리티 도구 | Unity Asset Store
Use the Odin Inspector and Serializer from Sirenix on your next project. Find this utility tool & more on the Unity Asset Store.
assetstore.unity.com
Attribute
하지만 이렇게 편리한 에셋임에도 한국어 정보가 상당히 부족해보인다.
내가 편리하게 쓰기 위해서라도 이번 포스트에는 많고 많은 Odin Attribute 중 Essetials 부분만 정리하려 한다.
1. [AssetsOnly], [SceneObjectsOnly]
- [AssetsOnly] : 프로젝트 에셋으로만 할당을 제한한다.
- [SceneObjectsOnly] : 씬 오브젝트로만 할당을 제한한다.
[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. [CustomValueDrawer]
그림과 같이 슬라이더를 통해 범위를 즉시 조절할 수 있으며, 실행 취소/다시 실행 및 다중 선택을 지원한다.
public float From = 2, To = 7;
[CustomValueDrawer("MyCustomDrawerStatic")]
public float CustomDrawerStatic;
[CustomValueDrawer("MyCustomDrawerInstance")]
public float CustomDrawerInstance;
[CustomValueDrawer("MyCustomDrawerAppendRange")]
public float AppendRange;
[CustomValueDrawer("MyCustomDrawerArrayNoLabel")]
public float[] CustomDrawerArrayNoLabel = new float[] { 3f, 5f, 6f };
private static float MyCustomDrawerStatic(float value, GUIContent label)
{
return EditorGUILayout.Slider(label, value, 0f, 10f);
}
private float MyCustomDrawerInstance(float value, GUIContent label)
{
return EditorGUILayout.Slider(label, value, this.From, this.To);
}
private float MyCustomDrawerAppendRange(float value, GUIContent label, Func<GUIContent, bool> callNextDrawer)
{
SirenixEditorGUI.BeginBox();
// Value값을 연동시키기 위한 작업
callNextDrawer(label);
var result = EditorGUILayout.Slider(value, this.From, this.To);
SirenixEditorGUI.EndBox();
return result;
}
private float MyCustomDrawerArrayNoLabel(float value)
{
return EditorGUILayout.Slider(value, this.From, this.To);
}
3. [DelayedProperty]
DelayedProperty을 사용하면 사용자가 값을 변경한 후에도 특정 조건이 충족되기 전까지 값이 반영되지 않는다. 대신 사용자가 "Apply" 또는 "Revert" 버튼을 클릭하거나 다른 조건이 충족될 때까지 값이 유지되는데, 이를 통해 사용자가 여러 개의 값을 변경한 후 한 번에 반영하고자 할 때 유용하다.
// Delayed and DelayedProperty attributes are virtually identical...
[Delayed]
[OnValueChanged("OnValueChanged")]
public int DelayedField;
// ... but the DelayedProperty can, as the name suggests, also be applied to properties.
[ShowInInspector, DelayedProperty]
[OnValueChanged("OnValueChanged")]
public string DelayedProperty { get; set; }
private void OnValueChanged()
{
Debug.Log("Value changed!");
}
// Delayed and DelayedProperty attributes are virtually identical...
[Delayed]
[OnValueChanged("OnValueChanged")]
public int DelayedField;
// ... but the DelayedProperty can, as the name suggests, also be applied to properties.
[ShowInInspector, DelayedProperty]
[OnValueChanged("OnValueChanged")]
public string DelayedProperty { get; set; }
private void OnValueChanged()
{
Debug.Log("Value changed!");
}
4. [DetailedInfoBox]
Info를 추가함으로써 관련된 정보를 자세하게 적는다.
[DetailedInfoBox("Click the DetailedInfoBox...",
"... to reveal more information!\n" +
"This allows you to reduce unnecessary clutter in your editors, and still have all the relavant information available when required.")]
public int Field;
5. [Enable GUI]
Get만 있는 경우에도 해당 변수가 Enable 처리를 할 수 있게된다. 하지만 숫자를 바꿔도 도로 10으로 돌아간다.
[ShowInInspector]
public int GUIDisabledProperty { get { return 10; } }
[ShowInInspector, EnableGUI]
public int GUIEnabledProperty { get { return 10; } }
6. [GUIColor]
버튼의 색을 마음대로 바꿀 수 있는 프로퍼티이다. 직접 헥사코드나 RGBA, RGB, 심지어는 깜빡임도 구현 가능하다.
[GUIColor(0.3f, 0.8f, 0.8f, 1f)]
public int ColoredInt1;
[GUIColor(0.3f, 0.8f, 0.8f, 1f)]
public int ColoredInt2;
[GUIColor("#FF0000")]
public int Hex1;
[GUIColor("#FF000077")]
public int Hex2;
[GUIColor("RGB(0, 1, 0)")]
public int Rgb;
[GUIColor("RGBA(0, 1, 0, 0.5)")]
public int Rgba;
[GUIColor("orange")]
public int NamedColors;
[ButtonGroup]
[GUIColor(0, 1, 0)]
private void Apply()
{
}
[ButtonGroup]
[GUIColor(1, 0.6f, 0.4f)]
private void Cancel()
{
}
[InfoBox("You can also reference a color member to dynamically change the color of a property.")]
[GUIColor("GetButtonColor")]
[Button("I Am Fabulous", ButtonSizes.Gigantic)]
private static void IAmFabulous()
{
}
// 여기서 @는 문자열을 표현식으로 바꿔주는 기능을 함으로써 시간에 따른 Color값을 반환한다.
[Button(ButtonSizes.Large)]
[GUIColor("@Color.Lerp(Color.red, Color.green, Mathf.Abs(Mathf.Sin((float)EditorApplication.timeSinceStartup)))")]
private static void Expressive()
{
}
private static Color GetButtonColor()
{
Sirenix.Utilities.Editor.GUIHelper.RequestRepaint();
return Color.HSVToRGB(Mathf.Cos((float)UnityEditor.EditorApplication.timeSinceStartup + 1f) * 0.225f + 0.325f, 1, 1);
}
7. [HideLabel]
변수 이름을 감춰주는 역할을 한다. 밑에 예제를 보게되면 WiderColor1이라는 변수명이 있지만 인스펙터 창에서는 나타나지 않는다. 해당 이유가 바로 [HideLabel] 어트리뷰트 덕분이다.
[Title("Wide Colors")]
[HideLabel]
[ColorPalette("Fall")]
public Color WideColor1;
[HideLabel]
[ColorPalette("Fall")]
public Color WideColor2;
[Title("Wide Vector")]
[HideLabel]
public Vector3 WideVector1;
[HideLabel]
public Vector4 WideVector2;
[Title("Wide String")]
[HideLabel]
public string WideString;
[Title("Wide Multiline Text Field")]
[HideLabel]
[MultiLineProperty]
public string WideMultilineTextField = "";
8. [PropertyOrder]
인스펙터에 표시되는 프로퍼티의 순서를 정할 수 있다. 기본적으로 작은 숫자가 더 높은 우선순위를 가진다. 즉, 값이 작을수록 먼저 표시된다. 음수 값을 사용할 수도 있으며, 음수 값은 기본적으로 제공되는 Odin Inspector의 프로퍼티 이전에 표시된다.
[PropertyOrder(1)]
public int Second;
[InfoBox("PropertyOrder is used to change the order of properties in the inspector.")]
[PropertyOrder(-1)]
public int First;
9. [PropertySpace]
인스펙터에서 특정 프로퍼티의 간격을 조절하는 역할을 한다. 이를 통해 인스펙터의 레이아웃을 조정하고 가독성을 향상시킬 수 있다.
// PropertySpace 과 Space는 사실상 동일한 어트리뷰트이다.
[Space]
[BoxGroup("Space", ShowLabel = false)]
public int Space;
// 앞뒤 간격 조절도 가능하다.
[PropertySpace(SpaceBefore = 30, SpaceAfter = 60)]
[BoxGroup("BeforeAndAfter", ShowLabel = false)]
public int BeforeAndAfter;
// 프로퍼티에서도 적용 가능하다
[PropertySpace]
[ShowInInspector, BoxGroup("Property", ShowLabel = false)]
public string Property { get; set; }
10. [ReadOnly]
말 그대로 인스펙터 창에서 편집은 불가하고 읽기만 가능하게 하는 어트리뷰트이다.
[ReadOnly]
public string MyString = "This is displayed as text";
[ReadOnly]
public int MyInt = 9001;
[ReadOnly]
public int[] MyIntList = new int[] { 1, 2, 3, 4, 5, 6, 7, };
11. [Required]
필수적으로 값을 가져야 하는 프로퍼티를 지정하는 역할을 한다. 이 어트리뷰트를 사용하면 인스펙터에서 해당 프로퍼티가 누락되었을 때 경고를 표시하고, 런타임 중에 값이 비어 있을 경우 예외를 발생시킬 수 있다.
[Required]
public GameObject MyGameObject;
[Required("Custom error message.")]
public Rigidbody MyRigidbody;
[InfoBox("Use $ to indicate a member string as message.")]
[Required("$DynamicMessage")]
public GameObject GameObject;
public string DynamicMessage = "게임 오브젝트가 필요합니다.";
12. [RequiredIn]
특정 조건에 따라 필수적으로 값을 가져야 하는 프로퍼티를 지정하는 역할을 한다. 이 어트리뷰트를 사용하면 인스펙터에서 해당 조건을 만족하지 않는 경우 프로퍼티가 누락되었을 때 경고를 표시하고, 런타임 중에 값이 비어 있을 경우 예외를 발생시킨다.
public class MyComponent : MonoBehaviour
{
public enum ObjectType
{
Cube,
Sphere,
None
}
public ObjectType objectType;
// objectType이 Cube로 설정됐을 경우, Cube가 할당되지 않았을 때 오류가 난다.
[RequiredIn("objectType", ObjectType.Cube)]
public GameObject cubeObject;
[RequiredIn("objectType", ObjectType.Sphere)]
public GameObject sphereObject;
}
12. [Searchable]
인스펙터에서 검색 기능을 적용하는 역할을 합니다. 이 어트리뷰트를 사용하면 해당 프로퍼티가 인스펙터의 검색 결과에 포함되도록 설정할 수 있습니다.
// 새로운 클래스 ExampleClass 인스턴스 생성
[Searchable]
public ExampleClass searchableClass = new ExampleClass();
// 구조체 ExampleStruct 리스트 생성
[Searchable]
public List<ExampleStruct> searchableList = new List<ExampleStruct>(Enumerable.Range(1, 10).Select(i => new ExampleStruct(i)));
// 구조체 FilterableBySquareStruct 리스트 생성
// 필터옵션 : 이 옵션은 검색 가능한 요소들 중에서 ISearchFilterable 인터페이스를 구현한 요소들에 대해서만 필터링을 수행하도록 지정한다.
[Searchable(FilterOptions = SearchFilterOptions.ISearchFilterableInterface)]
public List<FilterableBySquareStruct> customFiltering = new List<FilterableBySquareStruct>(Enumerable.Range(1, 10).Select(i => new FilterableBySquareStruct(i)));
// ExampleClass 클래스(string, int, 클래스 DataContainer 인스턴스)
[Serializable]
public class ExampleClass
{
public string SomeString = "Saehrimnir is a tasty delicacy";
public int SomeInt = 13579;
public DataContainer DataContainerOne = new DataContainer() { Name = "Example Data Set One" };
public DataContainer DataContainerTwo = new DataContainer() { Name = "Example Data Set Two" };
}
// DataContainer 클래스 : 이름과, ExampleStruct 구조체 리스트로 가진다
[Serializable, Searchable] // You can also apply it on a type like this, and it will become searchable wherever it appears
public class DataContainer
{
public string Name;
public List<ExampleStruct> Data = new List<ExampleStruct>(Enumerable.Range(1, 10).Select(i => new ExampleStruct(i)));
}
[Serializable]
public struct FilterableBySquareStruct : ISearchFilterable
{
public int Number;
[ShowInInspector, DisplayAsString, EnableGUI]
public int Square { get { return this.Number * this.Number; } }
public FilterableBySquareStruct(int nr)
{
this.Number = nr;
}
public bool IsMatch(string searchString)
{
return searchString.Contains(Square.ToString());
}
}
[Serializable]
public struct ExampleStruct
{
public string Name;
public int Number;
public ExampleEnum Enum;
public ExampleStruct(int nr) : this()
{
this.Name = "Element " + nr;
this.Number = nr;
this.Enum = (ExampleEnum)ExampleHelper.RandomInt(0, 5);
}
}
public enum ExampleEnum
{
One, Two, Three, Four, Five
}
13. [ShowInInspector]
해당 멤버를 인스펙터 창에 표시하는 기능
[ShowInInspector]
private int myPrivateInt;
[ShowInInspector]
public int MyPropertyInt { get; set; }
[ShowInInspector]
public int ReadOnlyProperty
{
get { return this.myPrivateInt; }
}
[ShowInInspector]
public static bool StaticProperty { get; set; }
14. [Title]
Title 어트리뷰트는 Header와 달리 구분선이 존재하여 좀 더 효과적인 구분이 가능하다.
[Title("Titles and Headers")]
public string MyTitle = "My Dynamic Title";
public string MySubtitle = "My Dynamic Subtitle";
15. [TypeFilter]
이 어트리뷰트를 사용하면 인스펙터에서 특정 타입의 객체만 필터링하여 표시할 수 있다.
[TypeFilter("GetFilteredTypeList")]
public BaseClass A, B;
[TypeFilter("GetFilteredTypeList")]
public BaseClass[] Array = new BaseClass[3];
// 필터링된 타입 리스트를 생성하는 로직 포함
public IEnumerable<Type> GetFilteredTypeList()
{
// BaseClass를 상속받는 타입들을 필터링 조건에 따라 가져온다.
// Assembly : 어셈블리는 프로그램의 실행에 필요한 코드와 리소스를 포함하고 있는 컨테이너
var q = typeof(BaseClass).Assembly.GetTypes()
.Where(x => !x.IsAbstract) // 추상클래스 타입 제외
.Where(x => !x.IsGenericTypeDefinition) // 제네릭 타입 제외(C1)
.Where(x => typeof(BaseClass).IsAssignableFrom(x)); // Base Class 상속 받는거 아니면 제외
// C1<T>에 여러 타입 변형 추가
// AppendWith : Collection 마지막에 해당 item추가
// MakeGenericType : 제네릭 타입을 ()안의 타입으로 변환
q = q.AppendWith(typeof(C1<>).MakeGenericType(typeof(GameObject)));
q = q.AppendWith(typeof(C1<>).MakeGenericType(typeof(AnimationCurve)));
q = q.AppendWith(typeof(C1<>).MakeGenericType(typeof(List<float>)));
return q;
}
public abstract class BaseClass
{
public int BaseField;
}
public class A1 : BaseClass { public int _A1; }
public class A2 : A1 { public int _A2; }
public class A3 : A2 { public int _A3; }
public class B1 : BaseClass { public int _B1; }
public class B2 : B1 { public int _B2; }
public class B3 : B2 { public int _B3; }
public class C1<T> : BaseClass { public T C; }
16. [TypeInfoBox]
정보 전달을 위한 박스이다.
public MyType MyObject = new MyType();
[Serializable]
[TypeInfoBox("The TypeInfoBox attribute can be put on type definitions and will result in an InfoBox being drawn at the top of a property.")]
public class MyType
{
public int Value;
}
17. [ValidateInput]
이 어트리뷰트는 필드 또는 속성 위에 적용하면 해당 필드 또는 속성의 값을 검증하는 메서드를 지정할 수 있다. 이 메서드는 값을 입력하거나 변경할 때마다 호출되어 입력된 값을 검증하고 유효성 여부에 따라 오류 메시지를 표시할 수 있다.
[HideLabel]
[Title("Default message", "You can just provide a default message that is always used")]
[ValidateInput("MustBeNull", "This field should be null.")]
public MyScriptyScriptableObject DefaultMessage;
[Space(12), HideLabel]
[Title("Dynamic message", "Or the validation method can dynamically provide a custom message")]
[ValidateInput("HasMeshRendererDynamicMessage", "Prefab must have a MeshRenderer component")]
public GameObject DynamicMessage;
[Space(12), HideLabel]
[Title("Dynamic message type", "The validation method can also control the type of the message")]
[ValidateInput("HasMeshRendererDynamicMessageAndType", "Prefab must have a MeshRenderer component")]
public GameObject DynamicMessageAndType;
[Space(8), HideLabel]
[InfoBox("Change GameObject value to update message type", InfoMessageType.None)]
public InfoMessageType MessageType;
[Space(12), HideLabel]
[Title("Dynamic default message", "Use $ to indicate a member string as default message")]
[ValidateInput("AlwaysFalse", "$Message", InfoMessageType.Warning)]
public string Message = "Dynamic ValidateInput message";
private bool AlwaysFalse(string value)
{
return false;
}
private bool MustBeNull(MyScriptyScriptableObject scripty)
{
return scripty == null;
}
private bool HasMeshRendererDefaultMessage(GameObject gameObject)
{
if (gameObject == null) return true;
return gameObject.GetComponentInChildren<MeshRenderer>() != null;
}
private bool HasMeshRendererDynamicMessage(GameObject gameObject, ref string errorMessage)
{
if (gameObject == null) return true;
if (gameObject.GetComponentInChildren<MeshRenderer>() == null)
{
// If errorMessage is left as null, the default error message from the attribute will be used
errorMessage = "\"" + gameObject.name + "\" must have a MeshRenderer component";
return false;
}
return true;
}
private bool HasMeshRendererDynamicMessageAndType(GameObject gameObject, ref string errorMessage, ref InfoMessageType? messageType)
{
if (gameObject == null) return true;
if (gameObject.GetComponentInChildren<MeshRenderer>() == null)
{
// If errorMessage is left as null, the default error message from the attribute will be used
errorMessage = "\"" + gameObject.name + "\" should have a MeshRenderer component";
// If messageType is left as null, the default message type from the attribute will be used
messageType = this.MessageType;
return false;
}
return true;
}
18. [ValueDropdown]
해당 어트리뷰트를 사용하면 필드 또는 속성의 값을 드롭다운 목록으로 제한할 수 있다.
Fields
- AppendNextDrawer : true인 경우 Drawer을 넓은 드롭다운 필드로 교체하는 대신 드롭다운 버튼이 다른 Drawer옆에 그려진 작은 버튼이 된다.
- DisableGUIInAppendedDrawer : 추가된 Drawer에 대한 GUI를 비활성화한다. 기본적으로 거짓이다.
[ValueDropdown("TextureSizes")]
public int SomeSize1;
[ValueDropdown("FriendlyTextureSizes")]
public int SomeSize2;
[ValueDropdown("FriendlyTextureSizes", AppendNextDrawer = true, DisableGUIInAppendedDrawer = true)]
public int SomeSize3;
[ValueDropdown("GetListOfMonoBehaviours", AppendNextDrawer = true)]
public MonoBehaviour SomeMonoBehaviour;
[ValueDropdown("KeyCodes")]
public KeyCode FilteredEnum;
[ValueDropdown("TreeViewOfInts", ExpandAllMenuItems = true)]
public List<int> IntTreview = new List<int>() { 1, 2, 7 };
[ValueDropdown("GetAllSceneObjects", IsUniqueList = true)]
public List<GameObject> UniqueGameobjectList;
[ValueDropdown("GetAllSceneObjects", IsUniqueList = true, DropdownTitle = "Select Scene Object", DrawDropdownForListElements = false, ExcludeExistingValuesInList = true)]
public List<GameObject> UniqueGameobjectListMode2;
// 트리형태의 드롭다운 표현
private IEnumerable TreeViewOfInts = new ValueDropdownList<int>()
{
{ "Node 1/Node 1.1", 1 },
{ "Node 1/Node 1.2", 2 },
{ "Node 2/Node 2.1", 3 },
{ "Node 3/Node 3.1", 4 },
{ "Node 3/Node 3.2", 5 },
{ "Node 1/Node 3.1/Node 3.1.1", 6 },
{ "Node 1/Node 3.1/Node 3.1.2", 7 },
};
private IEnumerable<MonoBehaviour> GetListOfMonoBehaviours()
{
return GameObject.FindObjectsOfType<MonoBehaviour>();
}
private static IEnumerable<KeyCode> KeyCodes = Enumerable.Range((int)KeyCode.Alpha0, 10).Cast<KeyCode>();
private static IEnumerable GetAllSceneObjects()
{
Func<Transform, string> getPath = null;
getPath = x => (x ? getPath(x.parent) + "/" + x.gameObject.name : "");
return GameObject.FindObjectsOfType<GameObject>().Select(x => new ValueDropdownItem(getPath(x.transform), x));
}
private static IEnumerable GetAllScriptableObjects()
{
return UnityEditor.AssetDatabase.FindAssets("t:ScriptableObject")
.Select(x => UnityEditor.AssetDatabase.GUIDToAssetPath(x))
.Select(x => new ValueDropdownItem(x, UnityEditor.AssetDatabase.LoadAssetAtPath<ScriptableObject>(x)));
}
private static IEnumerable GetAllSirenixAssets()
{
var root = "Assets/Plugins/Sirenix/";
return UnityEditor.AssetDatabase.GetAllAssetPaths()
.Where(x => x.StartsWith(root))
.Select(x => x.Substring(root.Length))
.Select(x => new ValueDropdownItem(x, UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(root + x)));
}
private static IEnumerable FriendlyTextureSizes = new ValueDropdownList<int>()
{
{ "Small", 256 },
{ "Medium", 512 },
{ "Large", 1024 },
};
private static int[] TextureSizes = new int[] { 256, 512, 1024 };
'유니티' 카테고리의 다른 글
유니티 회전값 총 정리(Rotation, Quaternion, Euler) (0) | 2022.08.03 |
---|---|
유니티 오류 : UnityEditor.Graphs.Edge.WakeUp () (0) | 2022.07.10 |