유니티 디버깅팁: 어느 게임오브젝트인지 알아내기

같은 고블린 몬스터의 스크립트에서 특정 조건에 Debug.Log를 출력하게 코드를 작성했다고 해도 씬에 고블린이 백마리면 어떨까? 백마리 중 로그를 출력한 인스턴스(고블린)를 알아내려면 단순한 Debug.Log 메시지로는 안되고 인스턴스ID를 함께 출력해서 찾아본다던지, 디벙깅 툴을 사용해서 브레이크포인트를 찍어가며 추적해 볼 수 있을 것이다.

이 글에서는 그런 번거로운 방법보다는 간단한 방법을 소개하려고한다. 콘솔에 출력되는 메시지를 클릭하면 유니티 하이어라키 창에 어떤 게임오브젝트인지 표시해주는 간단한 메서드이다.

프로그래밍에서 가장 기초적이라고 할 수 있는 디버깅은, 유니티에서의 경우 Debug.Log()처럼 로그를 콘솔창에 출력해보는 것이다.그런데 게임개발에서 (게임오브젝트, 컴포넌트등의) 같은 클래스의 많은 ‘인스턴스’를 다룰 일이 많다보니 어느 게임오브젝트에 붙은 클래스(컴포넌트 스크립트)에서 출력한 건지 알 아야할 필요가 있을 때가 빈번하다.

Exception 메시지의 PingObject

간단한 커스텀 클래스를 작성하면되는데 우선 본인이 이걸 고안하게 된 것은 간단한 유니티엔진의 기능을 활용한 것이다. 유니티의 콘솔창에 출력되는 메시지 중에서는 Exception(예외 메시지)가 있는데 대표적으로는 우리가 유니티엔진 개발하면서 자주 보는 NullRefferenceExeption 이 이 예외 메시지이다.

콘솔창에서 이 Excepiton 메시지를 클릭하면 유니티엔진 에디터는 어느게임오브젝트에서 발생한 예외인지 친절하게 하이어라키창에 한번 깜빡이듯 노랗게 표시해준다.

Ping이 찍힌 게임오브젝트

이것은 유니티 Editor API의 PingObject 기능이고 에디터 스크립팅에서는 이것을 ‘핑을 찍는다’라고 표현한다.

반면, 우리가 콘솔에 메시지를 출력 해보기 위해서 많이 쓰는 Debug.Log???()로 찍힌 메시지는 클릭해도 핑을 찍어주지 않는다. 그래서 어느 게임오브젝트에서 출력한 로그메시지인지 디버그로그 메시지만으로는 알기 어렵다.

이렇게 Exception 메시지를 클릭하면 유니티에디터가 하이어라키창에 게임오브젝트에 핑을 찍어주는 기능을 이용하면 에디터 스크립팅으로 PingObject를 호출하지 않고도 예외메시지를 throw 해주는 것만으로도 이같은 기능을 쉽게 구현할 수 있다.

Ping 찍는 커스텀 클래스

우선 아래 코드를 보자면 Ping 네임스페이스 아래 Throw라고하는 정적클래스 안에 두 개의 정적 메서드 Message() 오버로드를 작성했다.

using System;
using UnityEngine;

namespace Ping
{
    public static class Throw
    {
        private const string InfoText = "이 메시지를 클릭하면 하이어라키창에 표시됩니다. 주의: 이 메시지 이후의 코드는 중단되었습니다.";

        /// <summary>
        /// throw new Exception(message); 래퍼.
        /// 콘솔창에 예외를 출력한다. 예외메시지는 콘솔창 메시지를 클릭하면  PingObject를 실행해서 하이어라키에서 어느 오브젝트에서 throw했는지 알 수 있다.
        /// 씬에서 Destroy된 게임오브젝트는 Ping될 수 없다.
        /// 주의: 이 메시지 출력 이후의 코드는 실행되지 않는다.
        /// 에디터상에서만 호출됨.
        /// </summary>
        /// <param name="message">출력할 짧은 커스텀 메시지</param>
        /// <exception cref="Exception">예외메시지를 출력한다.</exception>
        [System.Diagnostics.Conditional("UNITY_EDITOR")]
        public static void Message(string message) => throw new Exception($"{message}. \n{InfoText} ");

        /// <summary>
        /// throw new Exception(message); 래퍼.
        /// 콘솔창에 예외 메시지를 출력하며 게임오브젝트의 이름, 컴포넌트 클래스 Type 명을 함께 출력한다.
        /// 예외메시지는 콘솔창 메시지를 클릭하면  PingObject를 실행해서 하이어라키에서 어느 게임오브젝트에서 throw한 메시지인지 알 수 있다.
        /// 씬에서 Destroy된 게임오브젝트는 Ping될 수 없다.
        /// 주의: 이 메시지 출력 이후의 코드는 실행되지 않는다.
        /// 에디터상에서만 호출됨
        /// </summary>
        /// <param name="message">출력할 짧은 커스텀 메시지.</param>
        /// <param name="gameObject">예외를 출력하는 게임오브젝트</param>
        /// <param name="monoComponent"> 컴포넌트 클래스에서 호출시 this 입력할 것. 예외를 출력하는 컴포넌트 인스턴스 </param>
        /// <exception cref="Exception">예외메시지를 출력한다.</exception>
        [System.Diagnostics.Conditional("UNITY_EDITOR")]
        public static void Message(string message, GameObject gameObject, MonoBehaviour monoComponent)
            => throw new Exception($"{message} from  [{gameObject.name}]의 [{monoComponent.GetType().Name}] 컴포넌트 \n{InfoText}");
    }
}

아래는 필자가 작성한 커스텀 클래스이다. namespace는 빼도 되고, namespace, class, method 이름은 자신이 짓고 싶은대로 지으면된다.

위 코드에서 XML 도큐먼트 주석을 길게 써놓긴 했지만 순수 코드는 아래와 같다.

using System;
using UnityEngine;

namespace Ping
{
    public static class Throw
    {
        private const string InfoText = "이 메시지를 클릭하면 하이어라키창에 표시됩니다. 주의: 이 메시지 이후의 코드는 중단되었습니다.";

        [System.Diagnostics.Conditional("UNITY_EDITOR")]
        public static void Message(string message) 
        {
            throw new Exception($"{message}. \n{InfoText} ");
        }

        [System.Diagnostics.Conditional("UNITY_EDITOR")]
        public static void Message(string message, GameObject gameObject, MonoBehaviour monoComponent)
        {
            throw new Exception($"{message} from  [{gameObject.name}]의 [{monoComponent.GetType().Name}] 컴포넌트 \n{InfoText}");
        }
    }
}

즉, 다음의 두개의 메서드 오버로드가 있다.

void Message(string message)

void Message(string message, GameObject gameObject, MonoBehaviour monoComponent)

호출방법

Message() 메서드의 사용법은 여러분들이 작성하는 MonoBehaviour를 상속받는 컴포넌트 스크립트에서 아래처럼 호출하면된다.

Ping.Throw.Message("Your 텍스트");
Ping.Throw.Message("Your 텍스트", gameObject, this);

두번째 오버로드의 두번째 인자 gameObject는 스크립트 컴포넌트 인스턴스가 붙을 게임오브젝트 인스턴스이고, 세번째 인자 this는 스크립트 컴포넌트 인스턴스 자신이다.

아래처럼 콘솔에 빨간색 예외메시지가 출력되었을때 일시정지하고 클릭해보면 하이어라키창에서 이 메시지를 출력한 게 어느 게임오브젝트인지 알 수 있게된다.

당연히도 밑에 나오는 메시지에서 콜스택을 따라 메시지 출처코드를 추적 할 수도 있다. 다만 이 콜스택의 경우 상속관계가 있고 부모클래스에 작성된 메시지라면 부모클래스 코드로의 링크가 걸리는 것을 염두에 두자.

주의사항

이 클래스가 trhow 하는 System.Exception 메시지는 코드를 중단시킨다는 점에 주의해야한다. 보통 C# 에서 Exception 은 try catch 문등으로 예외처리를 하는 것이 좋은데 앞서 말했듯 위에 필자가 작성한 메서드는 그런 예외처리를 한 것은 아니다.

여러 인스턴스가 동시다발적으로 동작하는 게임개발 특성상 게임은 계속해서 실행될 수는 있다. 메시지 출력 뒤부터는 해당 메서드 뒷부분이나 해당인스턴스는 그 후 코드가 전혀 실행되지 않게 될 것이므로 주의해한다 (코드에 따라 더이상 게임진행은 불가할 수도 있다).

그래서 이 Ping.Throw.Message() 커스텀 메시지가 Debug.Log를 완전히 대체하진 못하므로, 단순히 로그를 출력해 볼 목적으로는 남발하지 말아야한다.

어디까지나 같은 클래스의 인스턴스가 여럿일 때 어느 게임오브젝트에 붙어있는 스크립트 컴포넌트에서 문제가 생겼는지 알아보는 용도로 써야한다. 예를 들어 수 많은 같은 타입의 게임오브젝트 복제품 중에서 유독 오류난 몇몇의 인스턴스를 잡아낼 때, 혹은 반드시 입력되어야 할 인스펙터 필드 값을 체크 할 때 등이다.

그리고 메서드에 [System.Diagnostics.Conditional(“UNITY_EDITOR”)] 특성을 적용했기에 유니티 에디터상에서만 호출되므로 호출 코드를 제거하진 않아도 된다. 아래처럼 컴포넌트를 가져와 ActorInstance라는 필드에 담는 필자의 메서드를 보자.

    private void GetActor()
    {
        if (!TryGetComponent(out ActorInstance)) //현재 게임오브젝트에 Actor가 없을 수도 있다.
            if (!transform.parent.TryGetComponent(out ActorInstance)) // 부모에서 시도
                Ping.Throw.Message("Actor를 찾을 수 없음", gameObject, this); //핑 메시지
    }

이 메서드를 Awake()나 Start() 에서 호출하도록 하는데, Ping.Throw.Message() 호출부분을 제거할 필요는 없는 것이다.

위 코드를 호출하는 스크립트에서는 다른 컴포넌트인 Actor 컴포넌트가 필수이므로 저 메시지가 나온 게임오브젝트는 개발과정에서 반드시 오류를 수정 할 것이다. 그렇게 오류가 수정되고나면 저 명령줄은 다시 호출될 일이 없을 것이기 때문이다.

이 관점에서는 [System.Diagnostics.Conditional(“UNITY_EDITOR”)] 특성이 딱히 필요없긴하다.

인자 및 파라미터에관한 추가 설명

필자의 Ping 메시지 메서드를 다시 한 번 보자

    private const string InfoText = "이 메시지를 클릭하면 하이어라키창에 표시됩니다. 주의: 이 메시지 이후의 코드는 중단되었습니다.";

    [System.Diagnostics.Conditional("UNITY_EDITOR")]
    public static void Message(string message, GameObject gameObject, MonoBehaviour monoComponent)
    {
        throw new Exception($"{message} from  [{gameObject.name}]의 [{monoComponent.GetType().Name}] 컴포넌트 \n{InfoText}");
    }

두 번째 인자에는 Componet라면 가지고있는 gameObject 프라퍼티를 전달하면 gameObject.name 으로 게임오브젝트 이름이 출력되도록했다. 그러니까 현재 스크립트 인스턴스(this)가 붙어있는 게임오브젝트 인스턴스를 넘기는 것이고 Message() 메서드는 이름을 출력한다.

세 번째 인자는 컴포넌트 스크립트의 인스턴스 자신을 가리키는 this를 그대로 입력하면된다. (유니티에서 우리가 작성하는 MonoBehaviour를 상속한 커스텀 클래스를 컴포넌트 스크립트라고 부르기도하는데, 이것은 클래스를 정의하는 것일 뿐 게임오브젝트에 붙이면 각자 다른 ‘인스턴스(개체)’가 된다. 그래서 같은 클래스이더라도 A게임오브젝트에 붙은 스크립트의 this와 B게임오브젝트에 붙은 스크립트의 this는 다른 개체이다.)

Message() 메서드 세번 째 매개변수로 들어와야할 this라는 인자는 MonoBehaviour 타입이고, 메서드구현에서는 Monobehavior.GetType().Name 으로 출력한다. 이러면 해당 컴포넌트 인스턴스의 클래스이름이 출력된다.


이것은 상속관계에 있다고할 때에 게임오브젝트에 붙어있는 컴포넌트가 자식클래스라면 부모에서 Ping.Throw.Message(…)를 호출한다고해도, 부모클래스의 이름이 나오는 게 아닌, 자식클래스의 인스턴스인 this의 타입 이름이 출력된다.

예를들어 다음 세 개의 클래스가 상속관계에 있다고 하고

A : MonoBehavior
B : A
C : B

부모인 A 클래스 안에서 아래처럼 작성 되었다고 보자.

void Start() {
  Ping.Throw.Message("텍스트", gameObject, this);
}

반면 게임오브젝트에는 C 컴포넌트가 붙어있다면

유니티 API 메시지인 Start()를 실행하는 것은 C 인스턴스이기 때문에 this를 전달한 것은 C인스턴스 그 자신이고, Message() 메서드실행 결과로는 C 클래스의 이름이 출력된다. 그렇게 결국 컴포넌트의 이름을 알 수 있는 것이다 ( Start()를 B, C가 override하는 경우에 대해선 별도로 생각치 않음).

참고: 게임오브젝트에 붙어있어야 MonoBehavior의 Start 메시지를 받아 콜백메서드인 Start()가 실행된다.


현재글 목차