유니티 강좌 중에 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) 이 존재하고 있었습니다.
이렇게 구현된 곳으로 이동하고 나니 이제야 인터페이스를 모두 이해한 느낌이 들었습니다.
'Unity' 카테고리의 다른 글
Unity 에서 물체를 숨기는 가장 효율적인 방법은? (0) | 2025.06.24 |
---|---|
로그를 찍을 때, 시간도 같이 남기는 방법 (0) | 2025.06.16 |
유니티 시네머신 3.x : 추적 카메라 설정 방법 (0) | 2025.05.28 |
유니티에서 FBX 파일 가져왔는데 텍스처가 안 보일 때 (0) | 2025.05.10 |
Unity 에서 Empty object 를 Scene 창에서 보이게 하는 방법 (0) | 2025.04.30 |