EditorWindow
ScriptableObject를 상속받아 구현된 클래스이며 UnityEditor의 네임스페이스에 속한다. 이 클래스를 사용하여 유니티 에디터의 기본 창과 유사하게 독립적으로 띄울 수있거나 탭에 부착 가능한 Editor window를 만들 수 있다.
다음 다이어그램은 에디터 윈도우의 실행 순서이다.
- OnEnable: 스크립트가 로드되거나 오브젝트가 활성화될 때 호출된다.
- EditorApplication.isUpdating: true라면, 에디터는 현재 AssetDatabase를 새로고침 중이다.
- CreateGUI: 에디터가 업데이트 중이 아니라면 GUI를 생성한다.
- Update: 프레임당 한 번 호되어 스크립트의 로직을 업데이트 한다.
- OnGUI: GUI 이벤트를 렌더링하고 핸들링하기 위해 한 프레임 당 여러번 호출된다.
- OnDisable: 스크립트가 비활성화 될 때 호출되거나 오브젝트가 파괴될 때 마무리하고 리소스를 정리하기 위해 호출된다.
Editor
namespace는 UnityEditor에 속하여 ScriptableObject를 상속받아 구현된 클래스이다. 해당 클래스를 사용하여 커스텀 오브젝트에 대해 커스텀 인스펙터나 에디터를 만들 수 있다.
예를 들어 custom edotr를 통해 인스펙터에서 스크립트의 모양을 변경할 수 있고, CustomEditor attribute를 사용하여 에디터 클래스가 어떤 런타임 유형을 위한에디터인지 알려주어 커스텀 컴포넌트에 연결할 수 있다.
Message 종류
유니티 에디터 관련으로 스크립팅을 할 때 사용할 수 있는 Message는 다음과 같다.
Awake()
`ScriptableObject`의 인스턴스가 가 생성될 때 호출된다. 이러한 시나리오는 다음과 같다.
- 에디터 상에서 Create Asset Menu를 통해 새로운 ScriptableObject가 에셋으로 생성됨
- ScriptableObject.CreateInstance를 통해 프로그래밍 방식으로 새 ScriptableObject가 생성됨
- 런타임에 AssetBundle에서 Scriptable Object가 로드됨
- ScriptableObject 에셋에 대한 참조를 가지고 있는 Scene은 Hiearchy창에서 처음 로드되거나, 원본 인스턴스가 Resources.UnloadUnusedAssets을 통해 정리되어 후속 로드가 생길 경우 로드함
- Project 창에서 ScriptableObject 에셋이 처음 선택되거나 원본 인스턴스가 Resources.UnloadUnusedAssets을 통해 정리된 경우에 선택 할 때
참고: Edit 모드에서 에셋으로 생성된 ScriptableObject는 Play 모드로 진입할 때 다시 생성되지 않는다. 재생 모드로 전환할 때 스크립터블 오브젝트에서 초기화 작업을 수행하려면 `ScriptableObject.OnEnable`을 대신 사용하면 된다.
Editor 클래스의 메시지 호출 순서
호출 순서를 정리하자면 다음과 같다.
- Awake: 게임 오브젝트가 꺼져있어도 호출된다. OnEnable전에 한 번 호출된다. 커스텀 인스펙터에 들어갈 때마다 한 번씩 소출된다.
- OnEnable: 게임 오브젝트가 꺼져있어도 호출된다. Awake이후에 한 번 호출된다. 커스텀 인스펙터에 들어갈 때마다 한 번씩 소출된다.
- OnInspectorGUI: Monobehaviour의 Update처럼 계속 호출된다.
- OnSceneGUI: 커스텀 인스펙터가 존재하는 게임 오브젝트가 꺼져있으면 호출되지 않는다.
- OnDisable
- CustomEditor로 지정한 스크립트 컴포넌트가 부착되어 있는 게임 오브젝트의 인스펙터에서 벗어날 때 호출된다.
- OnValidate
- OnValidate()는 Monobehaviour.OnValidate()와 ScriptableObject.OnValidate()가 나누어져 있다. 여기서 설명하는 것은 ScriptableObject.OnValidate()이며, 스크립트가 로드되거나 인스펙터에서 값이 변경될 때 Unity가 호출하는 에디터 전용 함수라고 한다.
- 스크립트가 자신 이외의 다른 스크립트의 값을 수정하는 것은 지원되지 않는다고 한다. 아마 다른 스크립트의 변동 사항을 OnValidate()로 알 수는 없다는 의미같다.
- 그래서 커스텀 인스펙터의 값을 바꾸었을 때 Ediotr 클래스의 OnValidate()는 호출되지 않았다.
- 우선 Monobehaviour의 public 변수는 이미 그 자체로 serialized되어있다. 그래서 다음과 같이 Property를 찾아올 수 있다.
serializedObject.Update(); // SerializedObject의 최신 상태 반영
SerializedProperty width = serializedObject.FindProperty("필드 이름") // 프로퍼티 가져오기
serializedObject.ApplyModifiedProperties() // 적용
추가 Message
OnSceneGUI
Editor 스크립트를 통해 씬 뷰에서 이벤트를 처리할 수 있도록 해준다. 에디터 스크립트가 가리키는 스크립트 컴포넌트가 활성화되었을 때 한 번 활성화 된다.
OnInsepctorGUI()
Editor를 상속받은 클래스는 `OnInspectorGUI()`를 오버라이딩하여 사용할 수 있다. 이 함수를 통해 특정 객체 클래스의 인스펙터에 대한 사용자 지정 IMGUI 기반 GUI를 추가할 수 있다.
[CustomEditor(typeof(HCGeneration))]
public class HexGridGeneratorEditor : Editor
{
static HexGridGenerator hexGridGenerator = null;
private void Awake()
{
if (hexGridGenerator == null)
{
HCGeneration hCGeneration = (HCGeneration)target;
hexGridGenerator = new HexGridGenerator(hCGeneration.HexPrefab, hCGeneration.HexSize);
}
}
public override void OnInspectorGUI()
{
HCGeneration hCGeneration = (HCGeneration)target;
DrawDefaultInspector();
if (GUILayout.Button("Generate Grid"))
{
hexGridGenerator.GenerateGrid(hCGeneration.N);
//serializedObject.ApplyModifiedProperties();
}
if (GUILayout.Button("Clear Grid"))
{
hexGridGenerator.ClearGrid();
}
if (GUILayout.Button("Save Grid"))
{
string path = EditorUtility.SaveFilePanelInProject("Save Hex Grid", "HexGridData", "asset", "Test");
Debug.Log($"generator.HexTiles: {hexGridGenerator.HexTiles.Length}");
if (!string.IsNullOrEmpty(path))
{
HexGridData gridData = ScriptableObject.CreateInstance<HexGridData>();
gridData.HexTiles = hexGridGenerator.HexTiles; // 그리드 데이터 반환
gridData.TestValue = 255;
AssetDatabase.CreateAsset(gridData, path);
AssetDatabase.SaveAssets();
EditorUtility.SetDirty(gridData); // 변경사항을 저장
}
}
}
}
CustomEditor에서 type지정은 커스텀에디터를 사용할 클래스의 이름을 적어준다. 또한 `target`은 Editor 클래스에 있는 프로퍼티며, inspecting되어 있는 오브젝트 중 현재 가장 우선순위가 높은 오브젝트를 가리킨다.
public UnityEngine.Object target
{
get
{
return m_Targets[referenceTargetIndex];
}
set
{
throw new InvalidOperationException("You can't set the target on an editor.");
}
}
internal void InternalSetTargets(UnityEngine.Object[] t)
{
m_Targets = t;
}
internal virtual void OnForceReloadInspector()
{
if (m_SerializedObject != null)
{
m_SerializedObject.SetIsDifferentCacheDirty();
InternalSetTargets(m_SerializedObject.targetObjects);
}
}
EditorUtility.SetDirty
오브젝트를 Dirty 상태로 표기한다.
EditorApplication
EditorApplication.update
일반 업데이트를 위한 delegate다. 업데이트를 받으려면 해당 델리게이트에 함수를 추가하자. 이것은 고정된 rate로 호출되지 않고 최소 한 번 이상 호출되지만 일반적으로 1초당 여러 번 호출된다.
Handle
Handles.DrawLine
Handles.DrawLine(p1, p2);
Handles의 DrawLine() 함수는 SceneView에 Line을 그려주지만, 현재 GUI 이벤트 중 Repaint상태일 때만 DrawLine이 호출되며, 그 외의 상황에서는 무시된다.
Handles.DrawAAPolyLine
Handles.DrawAAPolyLine(5f, p1, p2);
선의 굵기를 지정해주고 싶으면 DrawAAPolyLine을 사용할 수 있다.
그리고 만약 현재 카메라에서 Line을 긋고 싶을 때, offset을 추가해주지 않으면 카메라 거리의 문제가 있는지, 그려진 Line이 보이지 않기에 주의해야한다.
// offset을 추가하지 않으면 그려진 Line이 3D화면에선 보이지 않음
Vector3 offsetPosition = camera.transform.position + camera.transform.forward * 0.3f; //
Handles.DrawAAPolyLine(5f, offsetPosition, point);
HandleUtility.GUIPointToWorldRay
SceneView상에서 Raycasting을 사용하려면 이와 같은 방법으로 사용할 수 있다. 이 함수를 사용해서 2D GUI 포지션을 world space의 ray로 변환가능하다. 계산은 현재 camera를 기준으로 계산된다.
if (Event.current.type == EventType.MouseDown)
{
Vector2 mousePosition = Event.current.mousePosition;
Ray ray = HandleUtility.GUIPointToWorldRay(mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit))
{
point = hit.point;
SceneView.RepaintAll();
}
}
Event
Unity GUI의 이벤트이다. 유저의 입력(키 누르기, 마우스 동작) 또는 UnityGUI 레이아웃, 렌더링 이벤트등이 이에 해당한다.
각 이벤트에 대해 OnGUI가 호출되므로, OnGUI는 한 프레임에서 여러번 호출될 수 있다. Event.current는 OnGUI 호출 내부의 "현재"이벤트에 해당한다. Event 클래스는 UnityEngine.IMGUIModule에 구현되어 있다.
EventType.Repaint
매 프레임마다 하나씩 전송되는 이벤트이다. 다른 모든 이벤트들이 처리된 후 repaint 이벤트가 전송된다.
IMGUI, OnGUI
Immediate Mode GUI(IMGUI)는 Unity의 기본 게임오브젝트 기반의 UI와는 완전히 별개의 기능으로, 코드 기반 GUI 시스템이다. 이를 구현하는 모든 스크립트에서 OnGUI 함수를 호출하여 구동된다.
공식 매뉴얼을 살펴보면 다음과 같은 세 가지 용도르 예시로 들어놓았다.
- 인게임 디버깅 디스플레이 및 도구 생성
- 스크립트 컴포넌트에 대해 커스텀 인스펙터 생성
- 새로운 에디터 창과 도구를 유니티 자체에서 생성하여 확
IMGUI는 일반적으로 플레이어가 사용하고 상호작용할 수 있는 사용자 인터페이스에 사용하기 위한 것이 아니다. 게임 플레이어를 위한 인터페이스는 Unity UI(uGUI)를 사용해야 한다. UGUI는 게임 오브젝트 기반의 Ui 시스템으로 사용자 인터페이스를 정렬, 배치, 스타일 지정이 가능하다. Unity 에디터에서는 UGUI를 사용하여 사용자 인터페이스를 생성하거나 변경할 수 없다.
"Immediate Mode"란 IMGUI가 생성되고 그려지는 방식을 의미하며, IMGUI 요소를 만들려면 OnGUI라는 특수 함수에 들어가는 코드를 작성해야 한다.
인터페이스를 표시하는 코드는 매 프레임마다 실행되어 화면에 그려지며, OnGUI 코드에 연결된 객체나 hierachy와 관련있는 시각 요소로 그려진 다른 타입의 오브젝트 외에는 영구적인 게임 오브젝트는 없다.
OnGUI
OnGUI는 GUI 이벤트를 렌더링하고 처리하기 위해 호출된다. OnGUI 함수는 IMGUI 시스템을 구현할 수 있는 유일한 함수이다. 이는 프레임당 여러번 호출 될 수 있다(이벤트당 한 번 호출을 하기 때문에, 한 프레임안에 여러 이벤트가 호출을 해버리는 경우). MonoBehaviour의 프로퍼티가 false로 설정되어 있으면 OnGUI()가 호출되지 않는다.
SceneView
이 클래스를 사용하여 SceneView 세팅을 관리하고, SceneView 카메라 속성을 바꾸고, 이벤트를 구독하고, SceneView 메소드를 호출하고 열려 있는 씬을 렌더링 할 수 있다.
SceneView의 카메라 가져오기
에디터의 씬 뷰에서 현재 활성화된 카메라를 가져오려면 다음과 같은 코드를 작성하면 된다.
Camera camera = Camera.current;
if (!camera && SceneView.lastActiveSceneView != null)
{
camera = SceneView.lastActiveSceneView.camera;
}
SceneView.duringSceneGui
이 이벤트를 구독하여 Scene view가 OnGUI 메소드를 호출할때마다 콜백을 받을 수 있다. 커스텀 핸들과 유저 인터페이스를 구현할 때 이 이벤트를 사용할 수 있다. 만약 Scene view에 어떤 요소를 그리고 싶다면 'Graphics.DrawMeshNow"와 같은 것을 사용할 수 있다. 그리고 "EventType.Repaint"단계에서만 그리는 작업을 시도해야 한다. 그 외의 단계에서는 무시된다.
SceneView.RepaintAll
열려있는 ScenView를 모두 다시 repaint 시킨다.
GUILayout
GUILayout 클래스는 유니티 GUI용 인터페이스이며, 이를 통해 자동화된 레이아웃을 구현할 수 있다.
GUILayout.Button
인스펙터에 버튼을 그리고 싶으면, Button() 함수를 사용하여 그릴 수 있다. 해당 버튼을 클릭 시 true값을 반환한다.
if (GUILayout.Button("Generate Grid"))
{
Debug.Log("그리드 생성");
}
Control ID
EditorGUI
EditorGUIUtility.ShowObjectPicker
함수 내부적으로는 Generic으로 전달된 타입을 통해 `SetupObjectSelector`가 호출되어 ObjectSelector를 이용하게 된다. 유니티 6.0에서 ObjectSelector은 Obsolated되었지만, 유니티 2022까지는 해당 방식으로 동작하는 듯하다.
if (GUILayout.Button("Select Material"))
{
// 매터리얼 선택창을 연다.
EditorGUIUtility.ShowObjectPicker<Material>(null, false, "", 0);
showMaterialPicker = true;
}
이런 방식으로 사용할 수 있고, Generic으로 넘길 수 있는 타입은 Sprite, Material과 같이 다른 UI창이 있는 것에서 동작 가능하며 Camera, Transform같은 클래스를 넣었을 때는 아무런 내용이 없는 창이 뜨게 된다.
공식 문서를 살펴보면,
public static void ShowObjectPicker<T>(UnityEngine.Object obj, bool allowSceneObjects, string searchFilter, int controlID) where T : UnityEngine.Object
{
Type objType = typeof(T);
if (Event.current?.commandName == "ObjectSelectorClosed")
{
EditorApplication.delayCall = (EditorApplication.CallbackFunction)Delegate.Combine(EditorApplication.delayCall, (EditorApplication.CallbackFunction)delegate
{
SetupObjectSelector(obj, objType, allowSceneObjects, searchFilter, controlID);
});
}
else
{
SetupObjectSelector(obj, objType, allowSceneObjects, searchFilter, controlID);
}
}
EditorGUILayout.ObjectField
모든 객체 유형을 받을 수 있는 필드를 만든다. 드래그 앤 드롭으로 객체를 할당하거나 Object Picker를 사용해서 객체를 선택할 수 있다. 오브젝트 참조가 에셋의 일부로 저장된 경우 allowSceneObject 매개 변수가 false인지 확인한다. 에셋은 씬에서 오브젝트에 대한 참조를 저장할 수 없기 때문이다. 이를 true로 설정하면, 하이어라키에서 바로 오브젝트 필드로 끌어놓을 수 있다. 객체 참조가 에셋의 일부분으로 저장되는 경우는 프리팹을 의미한다고 한다. (참조)
ObjectField가 스크립트 컴포넌트에 대한 Custom Editor일 경우, EditorUtility.IsPersistent()를 사용하여 컴포넌트가 에셋 또는 씬 오브젝트에 있는지 확인한다.
참고자료
- https://docs.unity3d.com/ScriptReference/ScriptableObject.Awake.html
- https://docs.unity3d.com/ScriptReference/Handles.DrawLine.html
- https://docs.unity3d.com/ScriptReference/GUILayout.html
- https://docs.unity3d.com/ScriptReference/Event.html
- https://www.reddit.com/r/Unity3D/comments/cqlni9/raycast_editor_tool/
- https://docs.unity3d.com/Manual/GUIScriptingGuide.html
- https://docs.unity3d.com/ScriptReference/MonoBehaviour.OnGUI.html
- https://algorfati.tistory.com/23
- https://docs.unity3d.com/ScriptReference/SceneView-duringSceneGui.html
- https://docs.unity3d.com/ScriptReference/EventType.Repaint.html
- https://docs.unity3d.com/ScriptReference/SceneView.RepaintAll.html
- https://rito15.github.io/posts/unity-update-game-view/
- https://rito15.github.io/posts/unity-editor-refresh-windows/
- https://docs.unity3d.com/ScriptReference/CustomEditor.html
- https://docs.unity3d.com/ScriptReference/EditorWindow.html
- https://stackoverflow.com/questions/32575754/unity-is-it-possible-to-access-onvalidate-when-using-a-custom-inspector
- https://blog.naver.com/hammerimpact/220776179812
- https://docs.unity3d.com/ScriptReference/EditorUtility.SetDirty.html
https://blog.naver.com/cdw0424/221537771726