목차
유니티 디버그, OnTriggerStay2D 리팩토링, 오브젝트 풀 동기 메서드
디버그: 유니티 연결하기
유니티 개발하다보면 IDE에서 유니티와 연결하기 기능을 쓸 경우가 많다.
아래 이미지와 같이 중단점을 찍고 실행 단계를 확인하게 된다.

위와 같이 줄 중단점에서 Didn't find loaded method for ~~ 라는 말이 나온다.
Visual Studio에서는 아예 빨간 점이 빈 동그라미가 되어 디버깅을 할 수 없는 상황인것처럼 보인다.
하지만 이는 씬에 해당 스크립트(=컴포넌트)가 존재하지 않는다는 의미이고,
에디터에서 실행하다가 해당 스크립트(=컴포넌트)가 생성되면 다시 빨갛게 변한다.
이를 몰라서 뭔가 중단점 자체에 문제가 생긴 줄 알았다.
하필이면 해당 중단점에서 Null Exception이 발생해서 메소드를 중단하는 바람에 중단점 자체에 문제가 있는 줄알았다.
하지만 씬에 해당 컴포넌트가 생성되지 않았기 때문에 생긴 일이었다!
때문에 한동안 디버그가 작동하는 여부를 몰라서 찾느라 고생했었다.
이는 해당 스크립트가 로드되지 않았다는 의미라고 받아들이면 될 것 같다.
OnTriggerStay2D 리팩토링
이전에는 플레이어와 데미지 처리를 위해서 OnTriggerStay2D를 사용했다.
채택한 이유로서는
- 몬스터의 공격 범위에서 벗어나려는 노력이 필요하기 때문에, 벗어나려는 노력 없이는 계속해서 데미지를 입어야함.
- 플레이어는 한번 공격을 당하면 일정시간동안 무적상태여서 OnTriggerStay2D에서 지속되어도 괜찮다!
따라서 아래와 같이 코딩을 했다.
private void OnTriggerStay2D(Collider2D other)
{
if ((playerLayer.value & (1 << other.gameObject.layer)) != 0) //(other.gameObject.layer == playerLayer)
{
bool isPlayer = other.gameObject.TryGetComponent<IDamagable>(out var damagable);
if (isPlayer && damagable != null && currentDamage > 0)
isAttackingPlayer = other.gameObject.TryGetComponent<IDamagable>(out target);
}
}
하지만 그간 영 찝찝했던 부분이었다.
일단 OnTriggerStay2D를 하게 된다면 콜라디어의 물리 충돌이 계속해서 발생하는 걸 캐치하고 실행하는 건데 물리연산이 계속해서 발생하는 것이라 쓸데없는 연산을 하게되는 것이라 부담이 될 것이라고 생각했었다.
마침 튜터님께 다녀올 일이 있었고, 마침 이게 눈에 띄어서 한 번 이야기가 나왔다.
개선 방향을 조언해주셨고 그게 바로 아래와 같은 방식이다.
private bool isAttackingPlayer = false;
private IDamagable target = null; //싱글 플레이어이므로 단일 변수이지만, 추후 싱글이 아닌 멀티이거나, 몬스터끼리 공격하는 상황이 발생한다면 List<IDamagable> 로 바꿀 것.
private void OnTriggerEnter2D(Collider2D other)
{
if ((playerLayer.value & (1 << other.gameObject.layer)) != 0) //(other.gameObject.layer == playerLayer)
{
isAttackingPlayer = other.gameObject.TryGetComponent<IDamagable>(out target);
}
}
private void OnTriggerExit2D(Collider2D other)
{
if ((playerLayer.value & (1 << other.gameObject.layer)) != 0) //(other.gameObject.layer == playerLayer)
{
// 나가는 순간 공격 상태를 무조건 종료하고 대상 null 처리
isAttackingPlayer = false; ;
target = null;
}
}
private void Attack()
{
if (isAttackingPlayer && target != null && currentDamage > 0)
{
target.TakeDamage(currentDamage);
if (counterCoroutine == null)
{
counterCoroutine = StartCoroutine(DelayForNextCounter(PlayerManager.Instance.player.Data.invincibleTime));
}
}
}
위와 같이 변경하고 Attack을 FixedUpdate()에서 호출하게 했다.
이럴 경우 물리연산으로 인해 OnTriggerStay2D를 호출하지 않을 수 있고,
매번 FixedUpdate에서 호출되는 것도 같지만,
레이어 값을 Stay에서 매번 검사하는 것과, Enter과 Exit에서 한번씩만 검사하고 Update 문에서 bool 값을 if 문으로 돌리는 것과는 부담이 다를 것이라고 생각했다.
오브젝트 풀: 동기 GetObject() 메서드 설계
상황
기존의 추상 메서드 내부에서 오브젝트 풀을 이용해야할 일이 있었다.
기존의 오브젝트 풀에서의 Get은 비동기 메서드인 GetObjectAsync를 사용해야했다.
이를 위해서 await 및 async 키워드를 위해 함수의 반환형을 task<T>와 같이 변경해야 했는데,
이 경우에는 추상 메서드까지 변경을 해야했었기 때문에 과도한 리팩토링이 염려되었다.
시도
오브젝트 풀의 동기로 작동하는 GetObject 메서드를 추가하고,
기존의 GetObjectAsync는 비동기 호출이 필요할 때만 사용하도록 설계를 수정하였다.
즉, 기존의 추상 메서드의 반환형이나 특징을 깨지 않을 수 있었다.
하지만 여전히 어드레서블에서 에셋을 가져와 풀을 생성하는 시간이 오래 걸릴까봐 걱정이 되었다.
해결
/// <summary>
/// pool에 등록된 path가 없다면 자동으로 등록 후 오브젝트를 반환한다. (defaultCapacity = 1, maxSize = 100)
/// </summary>
/// <param name="fullPath">Resources 폴더 내부의 Path + prefab name까지 포함한다. </param>
/// <param name="parent">지정할 경우 해당 객체 하위에 생성된다. </param>
/// <param name="position"></param>
/// <returns></returns>
public async UniTask<GameObject> GetObjectAsync(string fullPath, Transform parent = null, Vector3 position = default(Vector3))
{
if (!pools.ContainsKey(fullPath))
{
await RegisterPoolAsync(fullPath);
}
var obj = pools[fullPath].Get();
Debug.Log($"[ObjectPool] GetObject: {fullPath}, obj: {obj}, id: {obj.GetInstanceID()}, activeSelf: {obj.activeSelf}");
if(parent != null)
{
obj.transform.SetParent(parent, false);
obj.transform.localPosition = position;
}
else
{
obj.transform.position = position;
}
return obj;
}
/// <summary>
/// 등록된 풀에서만 오브젝트를 가져올 수 있습니다.
/// parent가 null일 경우 월드 좌표에 생성됩니다.
///
/// </summary>
/// <param name="fullPath"></param>
/// <param name="parent"></param>
/// <param name="position"></param>
/// <returns></returns>
public GameObject GetObject(string fullPath, Transform parent = null, Vector3 position = default(Vector3))
{
if (!pools.ContainsKey(fullPath))
{
Debug.LogError($"풀에 등록되지 않은 경로입니다: {fullPath}");
return null;
}
var obj = pools[fullPath].Get();
if (parent != null)
{
obj.transform.SetParent(parent, false);
obj.transform.localPosition = position;
}
else
{
obj.transform.position = position;
}
Debug.Log($"[ObjectPool] GetObject: {fullPath}, obj: {obj}, id: {obj.GetInstanceID()}, activeSelf: {obj.activeSelf}");
return obj;
}
성능 및 문제 해결을 위해 동기로 작동하는 GetObject를 구현하는 대신,
이 메서드가 풀이 확실히 Register된 후에만 호출 될 수 있도록 책임과 제약 조건을 명확히 정의했다.
이를 위해 GetObject 메서드에 Summary 주석을 작성하여 사용 제약을 명시하고,
풀이 확보 되지 않은 상태에서 호출 될 경우를 대비한 방어적 코드를 추가했다.
이는 추후에 예외 처리를 해주면 더 좋을 것 같다.
+이와 더불어 기존에는 좌표를 설정할 수 없었지만 설정할 수 있도록 변경하였다.
느낀점
오늘 효율이 좋지 않았지만 주말에 나와서 미리 작업한 것들을 합치니 평일 작업량 치고는 많았던 것 같다.
요즘 컨디션이 안 좋았으니 주말에 나와서 작업해야겠다고 느꼈었는데,
주말에 작업을 하게 되어서 다행이었다.
컨디션을 미리 파악하고 그를 위한 대비를 할 수 있어야겠다는 생각도 들었다.
물론 적당한 휴식은 당연히! 필요하지만,
이렇게 작업량이 부족할 때는 어쩔 수 없이 시간으로 몰아부쳐야 하는 상황도 있는 것 같다.
내일 학습 할 것은 무엇인지
오늘! 내일 오전중으로는 해드리겠습니다!!! 였던
이펙트 매니저, 위치 설정할 수 있게 하는 부분을 리팩토링 하는 걸!!
내일 오전 일찍 나와서 스크럼 끝나기 전에 구현할 예정이다.
이렇게 자신있게 이야기할 수 있는 부분은 이미 골조 구상이 다 끝났기 때문!
그리고 내일은 정말로 빌드를 위해 합칠 예정이다.
합치고 나서는 넣으면 좋고 안넣으면 아쉽지만 어쩔수 없는 항목이 되어버린.. 보스 기믹 2를 진행할 예정!
보스 기믹 1이 오래 걸린 이유는 이펙트 매니저 하랴, 오브젝트 풀 하랴, 여기저기 끌려다니느라 였었기 때문에 금방 할 수 있을 것 같다.
물론 이렇게 낙관적으로 생각해도 빌드 때는 예기치 못한 버그가 발생할 확률이 100% 이기 때문에 긴장을 늦추지 말아야겠다.
'부트캠프 > 본캠프' 카테고리의 다른 글
| [내일배움캠프_2025OCT02] 중간 발표 (0) | 2025.10.02 |
|---|---|
| [내일배움캠프_2025SEP30] 빌드 QA, 발표 준비 (0) | 2025.09.30 |
| [내일배움캠프_2025SEP27] 보스 몬스터 특수 스킬 구현 (0) | 2025.09.27 |
| [내일배움캠프_2025SEP26] UniTask 도입 (0) | 2025.09.26 |
| [내일배움캠프_2025SEP25] 코루틴 예외 처리와 자원 정리 (1) | 2025.09.25 |