안녕하세요! 코코네에서 클라이언트 엔지니어로 일하고 있는 SN입니다.
이전에 UnrealEngine으로 1년 정도 개발을 했지만, 현재는 Unity 클라이언트 엔지니어로 활동 중입니다. Unity 엔지니어링 자체는 UnrealEngine 이전에 약 7년 정도 경험이 있어, 1년 만에 다시 다루게 되었습니다.
이번 개발 블로그에서는 Unity 프로젝트에 직접 제작한 에디터 확장을 도입한 사례를 소개합니다.
「게임뷰와 스크린샷을 겹치는 도구」와 「우클릭으로 임의의 오브젝트를 선택하는 도구」라는 두 가지 에디터 확장입니다.
각각의 특징, 구현 포인트, 그리고 고생했던 부분에 대해 자세히 설명드리겠습니다!
1. 게임뷰와 스크린샷을 겹치는 도구
개요
이 에디터 확장은 게임뷰 상에 임의의 PNG 이미지를 겹쳐서 표시할 수 있는 기능입니다. 주로 디자이너가 전달한 UI 이미지를 비교하면서 위치 조정 및 구현 확인을 효율화할 수 있습니다.
이 기능은 과거 개인적으로 개발한 적이 있었지만, Unity 버전 업그레이드로 사용할 수 없게 되어 이번에 수정했습니다. (마지막 참고 링크에 이전에 제작한 기사를 링크로 첨부해 두었습니다.)
유용한 포인트
– 드래그 앤 드롭으로 손쉽게 이미지 로드
– 투명도를 조정해 이미지 겹침 가능
– UI 위치 조정 및 최종 확인이 정확하고 간단
기존에는 게임 오브젝트를 생성해 임시 이미지를 붙이는 방식이었지만, 이 방법은 시간도 많이 걸리고 실수가 발생하기 쉬웠습니다. 이 확장을 통해 더욱 원활한 UI 확인이 가능합니다.
구현 방법
구현 흐름은 아래 다이어그램과 같습니다.
– EditorWindow를 사용해 구현하며, 에디터 창에서 GameView와 PNG 합성 결과를 표시합니다.
– GameView 표시는 RenderTexture를 가져와 임의의 PNG 이미지와 합성하여 표시합니다.
– GameView 획득에는 Reflection을 사용하여 Unity의 private 메서드에 접근하여 데이터를 가져옵니다.
– GameView와 PNG의 합성은 전용 셰이더를 준비해 합성 결과를 에디터에 표시합니다.
GameView 창 획득
Type gameViewType = Type.GetType("UnityEditor.GameView,UnityEditor");
MethodInfo getMainPlayModeView = gameViewType.GetMethod(
"GetMainPlayModeView",
BindingFlags.NonPublic | BindingFlags.Static
);
RenderTexture 획득
FieldInfo renderTextureField = gameViewType.GetField(
"m_RenderTexture",
BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.GetField |
BindingFlags.FlattenHierarchy | BindingFlags.SetField
);
이 방식으로 GameView에서 RenderTexture를 가져와 합성 처리에 사용합니다.
포인트: Reflection을 사용한 데이터 획득
var mainPlayModeView = GetMainPlayModeView();
var renderTexture = m_RenderTexture; // Unity 2021 이후의 획득 방법
Unity 버전별로 메서드명이나 변수명이 다르기 때문에, 프리프로세서를 사용해 전환하여 대응했습니다.
고생했던 포인트
1.Unity 버전별 사양 변경
– 아래와 같은 버전에 따른 처리가 필요했습니다.
– Unity 2020: GetMainGameView → GetMainPlayModeView
– Unity 2021: m_TargetTexture → m_RenderTexture
2.Unity 2019.3 이후의 시뮬레이터 기능 대응
시뮬레이터용 뷰가 도입되면서 게임 뷰를 가져오는 것이 복잡해졌습니다. 이를 해결하기 위해 시뮬레이터 모드 감지를 추가하고, 적절히 null을 반환하도록 구현했습니다.
프로그램 전체
아래 파일을 `Assets/Editor` 폴더 아래에 배치하세요.
`ImageOverlayGameViewWindow.cs`
ImageOverlayGameViewWindow.cs
using System;
using System.IO;
using System.Reflection;
using UnityEditor;
using UnityEngine;
///
/// EditorWindow를 사용하여 GameView에 원하는 이미지를 겹칠 수 있는 확장
/// PNG 이미지만 지원
///
public class ImageOverlapGameViewWindow : EditorWindow { // 셰이더 ID ///
/// 두 번째 텍스처로 설정할 매개변수
///
private static readonly int SubTex = Shader.PropertyToID(“_SubTex”); ///
/// 텍스처의 투명도
///
private static readonly int TexAlpha = Shader.PropertyToID(“_TexAlpha”); ///
/// 이미지 데이터의 파일 경로
///
private string imageFilePath; ///
/// 게임 뷰 텍스처
///
private RenderTexture gameViewTexture; ///
/// 읽어들인 이미지 데이터 텍스처
///
private Texture2D imageFileTexture; ///
/// 합성을 위한 머티리얼
///
private Material imageMaterial; ///
/// 이미지 투명도
///
private float imageAlpha = 0.5f; // 에디터 윈도우 스크롤 및 윈도우 위치 유지 등의 매개변수 ///
/// 스크롤 좌표
///
private Vector2 scrollPosition = Vector2.zero; ///
/// 윈도우에 스냅 여부
///
private bool isSnapWindow = true; ///
/// 이미지 로드 완료 여부
///
private bool isOpenedImage; ///
/// 메뉴 오픈 시 처리
///
[MenuItem(“Custom Tools/Common/ImageOverlapWindow”)] static void Open() { GetWindow(true, “ImageOverlapWindow”); } ///
/// PNG 이미지를 로드하고 텍스처로 변환
///
///이미지 경로 void GetTextureForPng(string path) { if (!path.Contains(“.png”)) { Debug.Log(“PNG 형식이 아니어서 읽을 수 없습니다!”); return; } var readBinary = ReadBytesFile(path); // 헤더 정보를 건너뛰고 16바이트부터 시작 var pos = 16; var width = 0; for (int i = 0; i < 4; i++) { width = width * 256 + readBinary[pos++]; } var height = 0; for (int i = 0; i < 4; i++) { height = height * 256 + readBinary[pos++]; } imageFileTexture = new Texture2D(width, height); imageFileTexture.LoadImage(readBinary); isOpenedImage = true; }
`EditorOverlayImage.shader`
Shader "EDITOR/EditorOverlayImage"
{
Properties
{
[PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
[PerRendererData] _SubTex ("Sprite Texture", 2D) = "white" {}
_Color ("Tint", Color) = (1,1,1,1)
}
SubShader
{
Tags
{
"Queue"="Transparent"
"IgnoreProjector"="True"
"RenderType"="Transparent"
"PreviewType"="Plane"
"CanUseSpriteAtlas"="True"
}
Cull Off
Lighting Off
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Pass
{
Name "Default"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 2.0
#include "UnityCG.cginc"
#include "UnityUI.cginc"
struct appdata_t
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
};
struct v2f
{
float4 vertex : SV_POSITION;
float2 texcoord : TEXCOORD0;
float2 subTexcoord : TEXCOORD1;
float2 clipUV : TEXCOORD2;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _SubTex;
float _TexAlpha;
v2f vert(appdata_t v)
{
v2f OUT;
OUT.vertex = UnityObjectToClipPos(v.vertex);
OUT.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
OUT.subTexcoord = v.texcoord;
return OUT;
}
fixed4 frag(v2f IN) : SV_Target
{
half4 color = tex2D(_MainTex, IN.texcoord);
color = lerp(color,tex2D(_SubTex, IN.subTexcoord),_TexAlpha);
color.a = 1;
return color;
}
ENDCG
}
}
}
2. 우클릭으로 임의의 오브젝트를 선택하는 도구
개요
이 에디터 확장은 우클릭한 위치에 존재하는 게임 오브젝트를 리스트업하여 선택할 수 있는 기능입니다.
UI의 레이어 구조가 복잡할 경우, **”어떤 오브젝트를 선택했는지 알 수 없다”**는 상황이 자주 발생합니다.
이 확장을 사용하면
투명한 판정 오브젝트
텍스트 및 아이콘
등 클릭 위치에 있는 모든 오브젝트를 일괄적으로 리스트화하여, 쉽게 찾을 수 있습니다.
유용한 포인트
– 복잡한 UI 계층 구조를 파악하기 쉬움
– 게임 오브젝트를 빠르게 찾아낼 수 있음
구현 방법
Unity의 HandleUtility.PickGameObject를 사용했습니다.
이 함수는 클릭 위치에 있는 단일 객체만 가져올 수 있으므로, 매개변수로 설정 가능한 제외 리스트를 활용하여 여러 객체를 가져옵니다.
객체 획득 처리 흐름
GameObject gameObject = null;
do
{
gameObject = HandleUtility.PickGameObject(point, false, objects.ToArray(), null);
if (gameObject != null)
{
objects.Add(gameObject);
}
} while (gameObject != null);
return objects;
null이 반환될 때까지 이 처리를 반복하여 모든 객체를 리스트업합니다.
고생했던 포인트
처음에는 Physics.Raycast로 획득을 시도했지만, UI 요소에는 콜리전이 없어서 잘 작동하지 않았습니다.
결국 HandleUtility가 있다는 것을 알게 되었고, Unity 쪽에서 구현한 처리를 참고하면서 구현했습니다.
프로그램 전체
아래 파일을 `Assets/Editor` 폴더 아래에 배치하세요.
※ 우클릭 드래그로 인한 SceneView 카메라 이동이 객체 우클릭 시 작동하지 않으니, 필요에 따라 수정하시기 바랍니다.
`SceneViewGameObjectSelector.cs`
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
///
/// 우클릭 시 SceneView의 GameObject를 리스트에서 선택할 수 있는 확장
///
[InitializeOnLoad] public class SceneViewGameObjectSelector : Editor { ///
/// 생성자
///
static SceneViewGameObjectSelector() { SceneView.duringSceneGui += OnDuringSceneGUI; } ///
/// GUI 업데이트 시 처리
///
///대상 씬 static void OnDuringSceneGUI(SceneView sceneView) { Event currentEvent = Event.current; bool isDownRightClick = currentEvent.type == EventType.MouseDown && currentEvent.button == 1; if (!isDownRightClick) { return; } // 히트한 GameObject 리스트 생성 IReadOnlyList hitObjects = PickGameObjectsOnPointer(currentEvent.mousePosition); if (hitObjects.Count == 0) { return; } // 컨텍스트 메뉴 생성 GenericMenu menu = new GenericMenu(); foreach (GameObject hitGameObject in hitObjects) { string text = GetMenuItemText(hitGameObject); menu.AddItem(new GUIContent(text), false, () => Selection.activeGameObject = hitGameObject); } // 메뉴 표시 menu.ShowAsContext(); // 이벤트 사용 처리 currentEvent.Use(); } ///
/// 선택한 위치에 존재하는 게임 오브젝트를 획득
///
///획득할 좌표 /// 획득한 게임 오브젝트 리스트 static IReadOnlyList PickGameObjectsOnPointer(Vector2 point) { List objects = new List(); GameObject gameObject = null; do { gameObject = HandleUtility.PickGameObject(point, false, objects.ToArray(), null); if (gameObject != null) { objects.Add(gameObject); } } while (gameObject != null); return objects; } ///
/// 선택한 게임 오브젝트의 부모 자식 관계를 표시
///
///대상 GameObject /// 부모 자식 관계의 표시 텍스트 static string GetMenuItemText(GameObject gameObject) { string objectName = gameObject.name; Transform parent = gameObject.transform.parent; while (parent != null) { objectName = parent.name + “\\” + objectName; parent = parent.parent; } return gameObject.name + ” : ” + objectName; } }
요약
이번에 소개한 두 가지 에디터 확장은 일상의 개발 작업을 효율화하는 도구입니다.
「게임 뷰와 스크린샷을 겹치는 도구」는 UI 배치를 쉽게 만들었고,
「우클릭으로 임의의 객체를 선택하는 도구」는 복잡한 UI 계층을 파악하기 쉽게 했습니다.
앞으로도 더 편리한 확장 도구를 제공하여 개발 환경을 더욱 개선하고 싶습니다.
그럼, 모두 즐거운 개발 생활 되시길 바랍니다!
참고 링크
과거 「게임 뷰와 스크린샷을 겹치는 도구」를 제작했을 때의 해설(일본어)
「우클릭으로 임의의 객체를 선택하는 도구」 구현 시 참고한 Unity의 처리(일본어)
Cocone Engineering에서 함께 일할 동료를 모집하고 있습니다.
관심이 있으신 분들은 엔지니어 채용 사이트를 참고해 주세요.
→Cocone Engineering 주식회사 엔지니어 채용 공고 (코코네 주식회사를 통해 채용 중)
