본문 바로가기
Unity

호출 순서가 문제가 될 때 : SendMessage 또는 Interface 활용 방법

by Oz Driver 2025. 6. 27.

유니티 강좌 중에 object 의 충돌을 판정하고 tag 를 활용하여 점수가 중복되지 않도록 다루는 방법을 보게 되었습니다. tag 를 이렇게도 활용할 수 있구나하고 예제를 따라해보던 중에 한가지 의문이 들었습니다. 

그렇게 시작한 궁금증은 SendMessage() 를 거쳐 Interface 를 활용하는 방법으로까지 코드를 개선하게 되었습니다.

 

다음은 누군가에게 조금이나마 도움이 되었으면 하는 마음으로  제가 겪은 과정을 시간의 흐름에 따라 쓴 글입니다. 

 

1. Tag 를 이용한 중복 처리 필터링

Player 오브젝트

Player GameObject 는 Score 스크립트를 컴포넌트로 가지고 있는데 어떤 물체에 부딪혔을 때, 그 물체의 tag 가 "Hit" 가 아니라면 점수를 추가하는 기능입니다.

 

// Player GameObject 스크립트
public class Score : MonoBehaviour
{
    private int score = 0;

    private void OnCollisionEnter(Collision collision)
    {        
        if (collision.gameObject.tag != "Hit")
        {
            score++;
            Debug.Log($"You've bumped into a thing this many times : {score}");
        }
    }
}

 

주변 오브젝트

주변 오브젝트에는 ObjectHit 스크립트가 추가되어 있으며, 어떤 오브젝트와 충돌했을 때 그 물체의 tag 가 "Player" 라면 자신의 tag 를 "Hit" 로 설정합니다. 

 

// 주변 오브젝트 스크립트
public class ObjectHit : MonoBehaviour
{
    private void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.tag == "Player")
        {
            MeshRenderer mr = GetComponent<MeshRenderer>();
            mr.material.color = Color.black;
            gameObject.tag = "Hit";
        }
    }
}

 

진행 순서

1. Player 오브젝트의 OnCollisionEnter() 가 호출됩니다.

2. tag 를 조사해서 "Hit" 가 아니라면, 점수를 추가합니다.

3. 주변 오브젝트의 OnCollisionEnter() 가 호출됩니다.

4. 충돌한 오브젝트의 tag 가 "Player" 라면 자신의 tag 를 곧바로 "Hit" 로 변경합니다.

 

이러한 이유로 Player 오브젝트는 방금 전 충돌한 오브젝트에 다시 부딪혀도 이미 그 오브젝트의 tag 가 "Hit" 로 변했기 때문에 여러 번 반복해서 부딪혀도 score 가 중복으로 증가하지 않는 비교적 간단한 코드입니다.

 

 if (collision.gameObject.tag != "Hit")
 {
     score++;
     Debug.Log($"You've bumped into a thing this many times : {score}");
 }

 

문제점

Player Object 에 있는 Score 스크립트의 OnCollisionEnter() 가 호출된 후에, ObjectHit 클래스의 OnCollisionEnter() 가 호출되는 순서가 보장되어야 정상적으로 동작하게 됩니다. 

( 이 코드를 실행하면, 다행히도 이 순서대로 진행됩니다. )

그런데 만약 순서가 뒤바뀌어 ObjectHit 의 OnCollisionEnter() 가 먼저 호출된다면 어떻게 될까요?

                                                                

1. 주변 오브젝트의 OnCollisionEnter() 가 호출됩니다.

2. 충돌한 오브젝트의 tag 가 "Player" 라면 자신의 tag 를 곧바로 "Hit" 로 변경합니다.

3. Player 오브젝트의 OnCollisionEnter() 가 호출됩니다.

4. tag 를 조사해서 "Hit" 가 아니라면, 점수를 추가합니다.

 

주변 오브젝트의 tag 가 이미 "Hit" 로 변했기 때문에 Player 의 score 는 영원히 증가하지 않게 됩니다. 

 

2. SendMessage 를 이용하여 호출 순서 개선

호출 순서만 올바르게 유지된다면 이러한 버그는 발생하지 않습니다.

Unity 에는 SendMessage() 라는 함수가 있는데, 이 함수를 이용하여 Player 의 OnCollisionEnter() 함수가 호출된 후에, 상대편 오브젝트에 충돌되었음을 알리는 함수를 호출하여, 그 함수 내에서 tag 를 "Hit" 로 바꿔준다면 순서가 뒤바뀌어 발생하는 버그는 사라지게 됩니다. 

 

// Player GameObject 스크립트
public class Score : MonoBehaviour
{
    private int score = 0;

    private void OnCollisionEnter(Collision collision)
    {
        // 상대 오브젝트가 아직 Hit 처리되지 않았다면
        if (collision.gameObject.tag != "Hit")
        {
            score++;
            Debug.Log($"You've bumped into a thing this many times : {score}");

            // 상대 오브젝트에게 "OnHitByPlayer" 라는 이름의 함수를 호출하라고 요청
            collision.gameObject.SendMessage("OnHitByPlayer", 
            SendMessageOptions.DontRequireReceiver);
        }
    }
}

 

// 주변 오브젝트 스크립트
public class ObjectHit : MonoBehaviour
{
    public void OnHitByPlayer()
    {
        MeshRenderer mr = GetComponent<MeshRenderer>();
        mr.material.color = Color.black;
        gameObject.tag = "Hit";
    }
}

 

Code 설명

1. Player 쪽에서는 점수를 증가시킨 후에 SendMessage("OnHitByPlayer") 를 통해 상대방에게 알려줍니다.

2. ObjectHit 스크립트는 OnHitByPlayer()라는 함수를 가지고 있어, 메시지를 수신하면 자신을 "Hit" 상태로 변경합니다.

3. SendMessageOptions.DontRequireReceiver 는 해당 함수가 없더라도 오류가 나지 않도록 해줍니다.

 

이 구조는 충돌 처리 순서와 무관하게 항상 Player → Object 의 순서로 동작하므로 안정적입니다.

 

이슈

SendMessage() 는 내부적으로 C# 의 리플렉션을 활용해 구현된 함수입니다.

리플렉션은 런타임에서 해당 함수의 이름을 문자열로 찾아서 호출하는 구조입니다. 

Invoke() 도 리플렉션을 이용한 함수입니다.

이런 류의 함수들은 런타임에서 탐색하고 호출하기 때문에 호출 비용이 들게 됩니다. 

간단한 함수 호출이라면 상관없으나, 빈번한 호출은 성능에 문제가 될 수 있습니다. 

이를 해결하기 위해 Interface 를 활용한 방식으로 개선해 보았습니다. 

 

3. Interface 를 활용한 충돌 처리 개선

인터페이스 정의

public interface IHittable
{
    void OnHitByPlayer();
}

 

ObjectHit 클래스에 인터페이스 구현

public class ObjectHit : MonoBehaviour, IHittable
{
    public void OnHitByPlayer()
    {
        MeshRenderer mr = GetComponent<MeshRenderer>();
        mr.material.color = Color.black;
        gameObject.tag = "Hit";
    }
}

 

Score 클래스에서 인터페이스로 직접 호출

public class Score : MonoBehaviour
{
    private int score = 0;

    private void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.tag != "Hit")
        {
            score++;
            Debug.Log($"You've bumped into a thing this many times : {score}");

            IHittable hittable = collision.gameObject.GetComponent<IHittable>();
            if (hittable != null)
            {
                hittable.OnHitByPlayer();
            }
        }
    }
}

 

•  SendMessage()는 문자열 기반 함수 호출로 리플렉션이 사용되며, 오타에 취약하고 성능도 떨어집니다.

•  인터페이스는 컴파일 타임에 타입 검사를 할 수 있으며, 함수 호출도 더 빠릅니다.

•  GetComponent<IHittable>() 는 Unity 내부적으로 is 체크로 인터페이스 여부를 확인하고, 구현체가 있다면 반환합니다.

 

이 구조로 바꾸면 호출 순서 문제도 해결되고, 성능도 더 안정적입니다.

 

GetComponent<>()

유니티는 GameObject 에 MonoBehaviour 를 상속한 클래스만 컴포넌트로 부착할 수 있습니다.

그리고 GetComponent<T>() 는 그 중에서 T 타입과 호환되는 첫 번째 컴포넌트를 반환한다. T 타입은 제네릭이고 Interface 타입한 종류이므로 GetComponent<> 로 얻어올 수 있습니다.

내부 코드는 비공개이지만, 다음과 같은 유사한 방식으로 동작할 거라 생각됩니다.

 

public T GetComponent<T>() where T : class
{
    foreach (var component in this.GetAllAttachedComponents())
    {
        if (component is T matchedComponent)
        {
            return matchedComponent;
        }
    }
    return null;
}

 

오브젝트에 존재하는 컴포넌트들을 순회하면서 is 로 타입을 검사하고 조건이 맞는 경우 그  컴포넌트를 반환합니다.

여기서는 IHittable Interface 가 됩니다. 풀어쓰자면 다음과 같은 코드가 될 것입니다.

 

if( component is IHittable )
{
    IHittable hittable = component as IHittable;
    if( hittable != null )
        hittable.OnHitByPlayer();
}​

 

정리

 간단한 예제이므로 Interface 는 좀 과한 면이 있습니다. 

 따라서 이 경우에는 SendMessage() 로도 충분합니다.

 다만 게임의 규모가 커지거나 성능과 확정성을 고려해 한다면 Interface 이 훨씬 효율적입니다.

  IHittable 를 다른 class 에서도 상속받아서 사용할 경우 특히 더 유용합니다.

 

 

추가로 알게 된 것

인터페이스를 구현하고 나서, 구현된 함수로 이동하려고 평소 습관대로 F12 로 이동해봤습니다.

그랬더니 인터페이스가 있는 곳으로 이동되는 것입니다. 

 

그럼 실제 구현된 곳으로 이동하는 메뉴는 뭘까?

인터페이스는 계속해서 구현 이라는 단어를 써 왔다는 것이 생각났습니다.

 

이전까지는 정의로, 기본으로, 구현으로, ... 이런 것들을 구분하지 않고 F12 로 이동했었는데, 이 메뉴를 보고 나니 머리에 뭔가 번뜩하고 지나가는 것이 느껴졌습니다.

그렇습니다.!! 

메뉴에 친절하게도 구현으로 이동 (Ctrl + F12) 이 존재하고 있었습니다. 

 

이렇게 구현된 곳으로 이동하고 나니 이제야 인터페이스를 모두 이해한 느낌이 들었습니다.