일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 3d
- OculusMotionVectorPass
- 개인 바이트
- Windows Build
- Cartoon Rendering
- 메모리 누수
- 게임 수학
- Toon Shader
- ColorGradingLutPass
- working set
- Virtual Byte
- 작업 집합
- Specular
- Three(Two) Tone Shading
- ASW(Application SpaceWarp)
- Cell Shader
- URP로 변경
- C언어
- Private Bytes
- URP
- AppSW
- Cell Look
- 가상 바이트
- VR
- Rim Light
- 프로그래밍 기초
- 벡터
- Today
- Total
WinCNT
Unity의 EditorWindow를 한 번 알아보자 그 첫번째! 덤으로 Reflection과 PropertyDrawer로 List 요소만 표시해보자! 본문
Unity의 EditorWindow를 한 번 알아보자 그 첫번째! 덤으로 Reflection과 PropertyDrawer로 List 요소만 표시해보자!
WinCNT_SSS 2024. 3. 25. 16:03서론
전임자(필자는 만난 적 없음)가 전 프로젝트(필자는 참가한 적 없음)에서 만들었던 툴을 현재 담당하는 프로젝트에 마이그레이션하는 업무를 담당하게 되었다
아쉽게도 무지성으로 스크립트를 복사하는 방법은 당연히 불가능했다…따흐흑
살짝 살펴보니 EditorWindow에 대한 지식이 없으면 옮길 것도 못 옮길 것 같아서 우선 EditorWindow에 대해서 알아보기로 하였다
Hello, World!
역시 Unity, 공식 문서에 튜토리얼이 있었다
위를 참고해서 일단 기본 중 기본인 Hello, World!를 출력해봤다(namespace는 필자가 좀 바꿨다)
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace Editor.EditorWindows
{
public class MyCustomEditor : EditorWindow
{
[MenuItem("Custom Tools/My Custom Editor")]
public static void ShowMyEditor()
{
EditorWindow wnd = GetWindow<MyCustomEditor>();
wnd.titleContent = new GUIContent("My Custom Editor");
}
public void CreateGUI()
{
rootVisualElement.Add(new Label("Hello, World!"));
}
}
}
중요한 것은 EditorWindow를 상속받아서 만든 스크립트가 Editor 폴더 하부에 있어야 한다는 점이다
어…음 사실 asmdef를 제대로 설정하면 다른 폴더여도 상관 없을 것 같지만 그쪽은 일단 생략하자
또한 MenuItem를 설정한 함수는 Static으로 선언해야 한다
CreateGUI()와 OnGUI()
위에서는 CreateGUI()로 Hello, World!를 출력했지만, 사실 사용 빈도가 높은 함수는 OnGUI() 쪽일 것이다
CreateGUI는 에디터를 열 때 한 번만 호출되지만, OnGUI는 반복되서 호출된다
매번 호출되는 것은 아니고, 아마 특정 이벤트(마우스 호버링 등)일 때 호출되는 것으로 보인다
즉 초기화 설정 같은 것은 CreateGUI()에서 하고 갱신되는 렌더링 처리는 OnGUI()에 구현하면 될 것 같다
물론 대부분의 경우는 CreateGUI() 대신에 OnEnable()을 사용해도 별 문제가 없을 것으로 보인다
List를 표시하기
다른 여러 기능들은 다른 사람들의 글에도 잘 정리되어 있으니 생략하도록 하고!
애초에 필자도 그런 글들 보면서 구현한다…
찾아도 잘 안 나와서 필자가 고생하면서 구현했던 List를 EditorWindow에 표시하는 방법을 정리해보고자 한다
라고는 했지만 그냥 표시하고 싶을 뿐이라면 PropertyField()라는 API를 사용하는 것이 제일 편하다
Editor: How to do PropertyField for List elements?
물론 PropertyField()를 사용하기 위해서는 SerializedProperty가 필요하고, SerializedProperty를 사용하기 위해서는 SerializedObject하긴 하다
SerializedObject나 SerializedProperty에 대해서는 아래의 참고 사이트가 잘 정리되어 있으니 궁금한 분들은 읽어보는 것을 추천한다!
[에디터 확장 입문] 번역 5장 SerializedObject에 대해서
아무튼 자세한 설명은 생략하고, 필자가 구현해본 코드는 다음과 같다
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
[Serializable]
public class SceneInfoData
{
[SerializeField] private string assetPath = default;
public SceneInfoData() { }
public SceneInfoData(string assetPath) { this.assetPath = assetPath; }
}
public class MyCustomWindow : EditorWindow
{
[MenuItem("Custom Tools/My Custom Window")]
public static void ShowWindow()
{
GetWindow<MyCustomWindow>("My Custom Window");
}
[SerializeField] private List<SceneInfoData> sceneInfoList = new List<SceneInfoData>();
SerializedObject targetObject;
private void OnEnable()
{
targetObject = new SerializedObject(this);
sceneInfoList.Clear();
sceneInfoList.Add(new SceneInfoData("111"));
sceneInfoList.Add(new SceneInfoData("333"));
}
private void OnGUI()
{
if (targetObject != null)
{
targetObject.Update();
// sceneInfoList는 위에서 선언한 List의 변수명
SerializedProperty prop = targetObject.FindProperty("sceneInfoList");
EditorGUILayout.PropertyField(prop, new GUIContent(prop.displayName));
targetObject.ApplyModifiedProperties();
}
}
}
그러면 다음과 같이 List가 잘 표시된다!
PropertyAttribute와 PropertyDrawer 사용하기!
EditorWindow에도 PropertyAttribute와 PropertyDrawer를 사용할 수 있다
PropertyAttribute와 PropertyDrawer를 MonoBehaviour의 스크립트에 사용하는 예는 많았지만, 신기하게도 EditorWindow에서 사용하는 예제는 찾지 못 했다
그래도 좀 고생해서 어떻게든 작동하는 코드를 구현해봤다!
아래는 커스텀 Disable을 구현해본 코드이다
Custom PropertyAttribute
public class CustomDisableAttribute : PropertyAttribute { }
Custom PropertyDrawer
[CustomPropertyDrawer(typeof(CustomDisableAttribute))]
public class DisableDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginDisabledGroup(true);
EditorGUI.BeginProperty(position, label, property);
EditorGUI.PropertyField(position, property, new GUIContent(property.displayName));
EditorGUI.EndProperty();
EditorGUI.EndDisabledGroup();
}
}
그리고 위의 샘플 코드의 List에 [CustomDisable]를 추가해주면…!
public class MyCustomWindow : EditorWindow
{
[MenuItem("Custom Tools/My Custom Window")]
public static void ShowWindow()
{
GetWindow<MyCustomWindow>("My Custom Window");
}
[CustomDisable][SerializeField] private List<SceneInfoData> sceneInfoList = new List<SceneInfoData>();
SerializedObject targetObject;
// ...(생략)...
}
그러면 다음과 같이 List가 Disable이 된다!
샘플 코드 CustomDisable의 약점
샘플 코드의 CustomDisable는 List로 표시되는 요소들을 Disable로 할 뿐이고 List 자체를 Disable로 하지는 않는다
그게 무슨 소리지?라고 할 수 있는데 다음을 보면 어떤 느낌인지 이해가 잘 될 것이다
List 자체를 Disable로 하고 싶으면 List를 PropertyField하는 처리의 앞뒤로 Disable 설정을 해주면 된긴 한다
만약 그 때 List에 CustomDisable를 안 붙였으면 List의 요소들을 접거나 펼칠 수 있으면서, List의 추가나 교체는 안 되게 된다
List의 요소만 표시하기
예제에는 우연히 Asset Path의 값이 라벨로 표시되고 있지만, 이 부분은 PropertyField가 뭘 표시해야 할지 모르는 데이터면 Element0나 Element1와 같은 형태로 표시되기도 한다
그런데 필자는 더 나아가서 List의 요소들만 표시하고 싶었다
예를 들어 샘플 코드의 SceneInfoData의 Asset Path의 값만 표시하는 등등
하지만 이것이 헬 게이트의 시작인지 몰랐지…
수정 1) 우선 요소만 나오도록 바꿔보자!
할 수 있다! GetEnumerator라면…!
[CustomPropertyDrawer(typeof(CustomDisableAttribute))]
public class DisableDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginDisabledGroup(true);
EditorGUI.BeginProperty(position, label, property);
var enumerator = property.GetEnumerator();
int depth = property.depth;
while (enumerator.MoveNext())
{
var prop = enumerator.Current as SerializedProperty;
if (prop == null || prop.depth > depth + 1)
continue;
EditorGUI.PropertyField(position, prop, new GUIContent(prop.displayName));
}
EditorGUI.EndProperty();
EditorGUI.EndDisabledGroup();
}
}
그러면 위와 같은 결과가 나온다…는 사실 훼이크였다
만약 SceneInfoData에 다른 SerializeField가 있으면 UI가 망가진다
public class SceneInfoData
{
[SerializeField] private string assetPath = default;
[SerializeField] private string AAAAAAAAA = default;
[SerializeField] private string BBBBBBBBB = default;
public SceneInfoData(string assetPath)
{
this.assetPath = assetPath;
this.AAAAAAAAA = "AAAAAAAAA";
this.BBBBBBBBB = "BBBBBBBBB";
}
}
수정 2) List의 요소마다 적절한 높이를 확보하기!
표시할 프로퍼티의 높이는 GetPropertyHeight란 메소드에 의해 정해지는 것 같았다
다시 말해 이 부분을 override해주면 List의 요소마다 적절한 높이를 구할 수 있다
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
float totalHeight = 0f;
// 프로퍼티 한 개의 높이+ 적절한 패딩
var singleLineHeight = base.GetPropertyHeight(property, label) + EditorGUIUtility.standardVerticalSpacing;
// 표시할 프로퍼티의 수
int propertyCount = 0;
var enumerator = property.GetEnumerator();
while (enumerator.MoveNext())
propertyCount++;
totalHeight = propertyCount * singleLineHeight;
return totalHeight;
}
처음에는 프로퍼티의 개수를 구하기 위해 property.Copy().CountInProperty()를 써봤는데, 가끔 1을 반환하는 경우가 있어서 property.GetEnumerator()로 변경했다
수정 3) List의 요소의 프로퍼티를 겹치지 않게 표시하기!
다음은 시행착오를 거치며 적정한 position의 위치를 찾았다
override 하기 전의 GetPropertyHeight로 position.y와 position.height를 적절하게 조절하는 것이 포인트였다
[CustomPropertyDrawer(typeof(CustomDisableAttribute))]
public class DisableDrawer : PropertyDrawer
{
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
// ...생략...
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginDisabledGroup(true);
EditorGUI.BeginProperty(position, label, property);
position.y += EditorGUIUtility.standardVerticalSpacing;
var enumerator = property.GetEnumerator();
int depth = property.depth;
while (enumerator.MoveNext())
{
var prop = enumerator.Current as SerializedProperty;
if (prop == null || prop.depth > depth + 1)
continue;
position.height = base.GetPropertyHeight(prop, null);
EditorGUI.PropertyField(position, prop, new GUIContent(prop.displayName));
position.y += base.GetPropertyHeight(prop, new GUIContent(prop.displayName)) + EditorGUIUtility.standardVerticalSpacing;
}
EditorGUI.EndProperty();
EditorGUI.EndDisabledGroup();
}
}
수정 4) 홀수행, 짝수행의 Background를 다른 색으로 표시하기
이 부분도 조금 고생했지만 EditorGUI.LabelField로 어떻게든 됐다
우선초기화는 한 번만 하고 싶으니 CustomDisableAttribute에 변수를 하나 추가했다
public class CustomDisableAttribute : PropertyAttribute
{
public bool IsInitialized { get; set; }
}
그리고 초기화 처리와 배경색을 변경하는 처리를 추가했다!
[CustomPropertyDrawer(typeof(CustomDisableAttribute))]
public class DisableDrawer : PropertyDrawer
{
// 배경색 변경을 위해 추가한 변수들
private static GUIStyle style = new GUIStyle(EditorStyles.label);
private static Texture2D texture = new Texture2D(1, 1);
private bool isEven = false;
private CustomDisableAttribute attr { get { return (CustomDisableAttribute)attribute; } }
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
// ...생략...
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
// 초기화 처리 추가
if (!attr.IsInitialized)
{
isEven = false;
if (texture != null)
{
texture.SetPixel(0, 0, Color.gray);
texture.Apply();
}
attr.IsInitialized = true;
return;
}
// 홀수, 짝수에 따라 배경색을 변경
if (isEven)
style.normal.background = texture;
else
style.normal.background = null;
isEven = !isEven;
position.y -= EditorGUIUtility.standardVerticalSpacing / 2;
EditorGUI.LabelField(position, GUIContent.none, style);
EditorGUI.BeginDisabledGroup(true);
EditorGUI.BeginProperty(position, label, property);
position.y += EditorGUIUtility.standardVerticalSpacing;
var enumerator = property.GetEnumerator();
int depth = property.depth;
while (enumerator.MoveNext())
{
var prop = enumerator.Current as SerializedProperty;
if (prop == null || prop.depth > depth + 1)
continue;
position.height = base.GetPropertyHeight(prop, null);
EditorGUI.PropertyField(position, prop, new GUIContent(prop.displayName));
position.y += base.GetPropertyHeight(prop, new GUIContent(prop.displayName)) + EditorGUIUtility.standardVerticalSpacing;
}
EditorGUI.EndProperty();
EditorGUI.EndDisabledGroup();
}
}
System.Reflection으로 SerializedObject.FindProperty 대체해보기
이대로 끝내도 됐지만 개인적으로 마음에 안 드는 부분이 아직 있었다
바로 SerializedObject.FindProperty에 변수명(샘플에서는 sceneInfoList)를 쓴 부분이었다
public class MyCustomWindow : EditorWindow
{
// ...생략...
[CustomDisable, SerializeField] private List<SceneInfoData> sceneInfoList = new List<SceneInfoData>();
SerializedObject targetObject;
private void OnGUI()
{
if (targetObject != null)
{
targetObject.Update();
// sceneInfoList는 위에서 선언한 List의 변수명
SerializedProperty prop = targetObject.FindProperty("sceneInfoList");
EditorGUILayout.PropertyField(prop, new GUIContent(prop.displayName));
targetObject.ApplyModifiedProperties();
}
}
}
그냥 어트리뷰트에 CustomDisable가 붙었으면 알아서 표시하도록 하고 싶었다
동적으로 변수에 대한 정보를 알아야 하는 거니 예의 System.Reflection라는 것을 쓰면 가능하지 않을까라고 생각해 한 번 해봤다
여기서 그만두면 좋았을 텐데…그래도 결과적으로는 구현이 가능했으니 다행이라고 치자
Reflection을 이용해서 CustomAttributes가 있는 SerializedProperty만 획득하기
이번에 구현했던 것을 샘플 코드로 정리해봤다
이 부분을 좀 더 추상화하면 MyCustomWindow가 더 깔끔해질 것이다(샘플 코드라 생략)
우선 CustomAttributes가 있는 SerializedProperty만 모아두는 List를 추가했다
public class MyCustomWindow : EditorWindow
{
private readonly List<SerializedProperty> targetList = new List<SerializedProperty>();
// ...생략...
}
그리고 위에서 구현한 CustomDisableAttribute가 설정된 SerializedProperty하고 List에 추가하는 메소드도 만들었다
private void GetTargetProperty(SerializedObject serializedObject, SerializedProperty property)
{
try
{
var type = serializedObject.targetObject.GetType();
// BindingFlags는 매 중요!!
FieldInfo field = type.GetField(property.propertyPath, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
if (field != null)
{
object[] attributes = field.GetCustomAttributes(true);
foreach (var attr in attributes)
{
// CustomDisableAttribute인지 체크하고 List에 추가
if (attr.GetType() == typeof(CustomDisableAttribute))
targetList.Add(property.Copy());
}
}
}
catch (Exception e)
{
Debug.Log(e);
throw;
}
}
참고로 BindingFlags는 제대로 설정해야 한다
예를 들어 샘플 코드의 sceneInfoList는 private이므로 BindingFlags.NonPublic 플래그가 없으면 GetField로 가져올 수 없다
그리고 메소드는 OnEnable에서 다음과 같이 사용했다
private void OnEnable()
{
targetObject = new SerializedObject(this);
targetObject.Update();
var iterator = targetObject.GetIterator();
while (iterator.NextVisible(true))
{
GetTargetProperty(targetObject, iterator);
}
targetObject.ApplyModifiedProperties();
// ...생략...
}
그리고 이제 변수명으로 획득할 필요가 없으니 지워주고, OnGUI에서 targetList의 요소들을 찾아서 그리는 것으로 변경!
private void OnGUI()
{
if (targetObject != null)
{
targetObject.Update();
// 이제 sceneInfoList를 찾을 필요 없음
// SerializedProperty prop = targetObject.FindProperty("sceneInfoList");
// EditorGUILayout.PropertyField(prop, new GUIContent(prop.displayName));
EditorGUI.BeginDisabledGroup(true);
foreach (var item in targetList)
{
EditorGUILayout.PropertyField(item, new GUIContent(item.displayName));
}
EditorGUI.EndDisabledGroup();
targetObject.ApplyModifiedProperties();
}
}
그러면 짜잔~!
겉모습은 변함 없이 제대로 나온다!
이제 다음과 같이 변수을 선언할 때 마음대로 정할 수 있게 되었다
public class MyCustomWindow : EditorWindow
{
// ...생략...
[CustomDisable, SerializeField] private List<SceneInfoData> sceneInfoList = new List<SceneInfoData>();
[CustomDisable, SerializeField] private List<SceneInfoData> sceneInfoList2 = new List<SceneInfoData>();
[CustomDisable, SerializeField] private List<SceneInfoData> sceneInfoList3 = new List<SceneInfoData>();
// ...생략...
}
샘플 코드 전문
MyCustomWindow.cs
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using UnityEditor;
[Serializable]
public class SceneInfoData
{
[SerializeField] private string assetPath = default;
[SerializeField] private string AAAAAAAAA = default;
[SerializeField] private string BBBBBBBBB = default;
public SceneInfoData(string assetPath)
{
this.assetPath = assetPath;
this.AAAAAAAAA = "AAAAAAAAA";
this.BBBBBBBBB = "BBBBBBBBB";
}
}
public class MyCustomWindow : EditorWindow
{
private readonly List<SerializedProperty> targetList = new List<SerializedProperty>();
[MenuItem("Custom Tools/My Custom Window")]
public static void ShowWindow()
{
GetWindow<MyCustomWindow>("My Custom Window");
}
[CustomDisable, SerializeField] private List<SceneInfoData> sceneInfoList = new List<SceneInfoData>();
[CustomDisable][SerializeField] private List<SceneInfoData> sceneInfoList2 = new List<SceneInfoData>();
[CustomDisable(IsInitialized = false), SerializeField] private List<SceneInfoData> sceneInfoList3 = new List<SceneInfoData>();
SerializedObject targetObject;
private void OnEnable()
{
targetObject = new SerializedObject(this);
targetObject.Update();
var iterator = targetObject.GetIterator();
while (iterator.NextVisible(true))
{
GetTargetProperty(targetObject, iterator);
}
targetObject.ApplyModifiedProperties();
sceneInfoList.Clear();
sceneInfoList.Add(new SceneInfoData("111"));
sceneInfoList.Add(new SceneInfoData("222"));
sceneInfoList.Add(new SceneInfoData("333"));
sceneInfoList.Add(new SceneInfoData("444"));
sceneInfoList.Add(new SceneInfoData("555"));
sceneInfoList2.Clear();
sceneInfoList2.Add(new SceneInfoData("222"));
sceneInfoList2.Add(new SceneInfoData("444"));
sceneInfoList2.Add(new SceneInfoData("666"));
sceneInfoList3.Clear();
sceneInfoList3.Add(new SceneInfoData("111"));
sceneInfoList3.Add(new SceneInfoData("333"));
sceneInfoList3.Add(new SceneInfoData("555"));
}
private void OnGUI()
{
if (targetObject != null)
{
// // List
// SerializedProperty prop = targetObject.FindProperty("sceneInfoList");
//
// EditorGUI.BeginDisabledGroup(true);
// EditorGUILayout.PropertyField(prop, new GUIContent(prop.displayName));
// EditorGUI.EndDisabledGroup();
targetObject.Update();
EditorGUI.BeginDisabledGroup(true);
foreach (var item in targetList)
{
EditorGUILayout.PropertyField(item, new GUIContent(item.displayName));
}
EditorGUI.EndDisabledGroup();
targetObject.ApplyModifiedProperties();
}
}
private void GetTargetProperty(SerializedObject serializedObject, SerializedProperty property)
{
try
{
var type = serializedObject.targetObject.GetType();
FieldInfo field = type.GetField(property.propertyPath, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
if (field != null)
{
object[] attributes = field.GetCustomAttributes(true);
foreach (var attr in attributes)
{
if (attr.GetType() == typeof(CustomDisableAttribute))
targetList.Add(property.Copy());
}
}
}
catch (Exception e)
{
Debug.Log(e);
throw;
}
}
}
CustomDisable.cs
using UnityEditor;
using UnityEngine;
public class CustomDisableAttribute : PropertyAttribute
{
public bool IsInitialized { get; set; }
}
[CustomPropertyDrawer(typeof(CustomDisableAttribute))]
public class DisableDrawer : PropertyDrawer
{
private static GUIStyle style = new GUIStyle(EditorStyles.label);
private static Texture2D texture = new Texture2D(1, 1);
private bool isEven = false;
private CustomDisableAttribute attr { get { return (CustomDisableAttribute)attribute; } }
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
float totalHeight = 0f;
var singleLineHeight = base.GetPropertyHeight(property, label) + EditorGUIUtility.standardVerticalSpacing;
int propertyCount = 0;
var enumerator = property.GetEnumerator();
while (enumerator.MoveNext())
propertyCount++;
totalHeight = propertyCount * singleLineHeight;
return totalHeight;
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if (!attr.IsInitialized)
{
isEven = false;
if (texture != null)
{
texture.SetPixel(0, 0, Color.gray);
texture.Apply();
}
attr.IsInitialized = true;
return;
}
if (isEven)
style.normal.background = texture;
else
style.normal.background = null;
isEven = !isEven;
position.y -= EditorGUIUtility.standardVerticalSpacing / 2;
EditorGUI.LabelField(position, GUIContent.none, style);
EditorGUI.BeginDisabledGroup(true);
EditorGUI.BeginProperty(position, label, property);
position.y += EditorGUIUtility.standardVerticalSpacing;
var enumerator = property.GetEnumerator();
int depth = property.depth;
while (enumerator.MoveNext())
{
var prop = enumerator.Current as SerializedProperty;
if (prop == null || prop.depth > depth + 1)
continue;
position.height = base.GetPropertyHeight(prop, null);
EditorGUI.PropertyField(position, prop, new GUIContent(prop.displayName));
position.y += base.GetPropertyHeight(prop, new GUIContent(prop.displayName)) + EditorGUIUtility.standardVerticalSpacing;
}
EditorGUI.EndProperty();
EditorGUI.EndDisabledGroup();
}
}
마무리
사실 구현만 하자면 간단히 끝날 수 있는 부분이었지만, 여러 가지 고려하다보니 중요한 개념들에 대해서 배울 수 있게 되었다
나중에 다시 쓰일 테니 결코 시간 낭비한 게 아니라 믿는다…
참고 사이트
Generic List In Custom Editor Window
[에디터 확장 입문] 번역 10장 PropertyDrawer
'Unity > Unity 관련' 카테고리의 다른 글
Volume의 Bloom에서 추가 속성(Addtional Properties)인 Downscale, Max Iterations를 표시하고 최적화하기! (0) | 2024.04.01 |
---|---|
Unity의 EditorWindow를 한 번 알아보자 그 두번째! 이번엔 Reflection과 Action으로 Method를 실행해보자! (0) | 2024.03.28 |
Unity에서 커스텀 ShaderGUI를 구현해보자! 그 두번째 (0) | 2024.02.09 |
UniTask를 인스톨하고 살짝 사용해봤다! (0) | 2023.11.30 |
Unity 2022.2부터 Navigation이 AI Navigation로 바뀌었다길래 사용해봤다 (0) | 2023.11.14 |