📖학습 내용


클로저와 변수 캡쳐

for 루프 내에서 이벤트 핸들러를 등록할 떄, 반복 변수를 매개변수로 사용할 경우 이상한 문제가 발견되었다. 매개변수로 등록된 값이 1씩 증가하여 들어가는 것이 아닌, 최대 값만 들어가는 것이다.

실제 오류 발생 코드

for (int j = 0; j < objectPrefabs.Length; j++)
{
    for (int i = 0; i < poolSize; i++)
    {
        GameObject obj = Instantiate(objectPrefabs[j]);
        obj.GetComponent<EnemyStat>().OnDie += () => ReturnObject(obj, j);
        obj.SetActive(false);  /
        obj.transform.SetParent(gameObject.transform);
        pool[j].Enqueue(obj);
    }
}

몬스터의 객체를 생성할 떄 마다 각 몬스터의 사망이벤트에 리턴 함수를 추가해주는 구조인데, 이떄 j 값이 0 부터 들어가는 것이 아니라, 반복문의 최대값으로만 들어갔다.

알아본 결과, 이 현상은 클로저(Closure) 의 동작 방식 때문에 발생한다고 한다.

문제의 원인: 클로저와 변수 캡쳐

  1. 클로저(Closure): 클로저는 자신이 정의될 떄 접근 가능했던 주변 환경(변수 등)을 “기억” 하고, 나중에 실행될 때도 그 환경에 접근할 수 있는 함수를 의미하며, C#에서 람다식이나 무명 메소드를 사용하면 생성된다
  2. 변수 캡쳐 방식 : for 루프 안에서 이벤트 핸들러를 정의하고 이떄 루프 변수를 사용하면, 클로저는 i의 값을 캡쳐하는 것이 아니라 i라는 변수 자체(메모리 위치에 대한 참조)를 캡쳐하게 된다.
  3. 루프 실행 완료: for 루프는 이벤트 핸들러가 실제로 실행되기 훨씬 전에 모든 반복을 완료합니다. 이떄 i 변수는 루프의 최종 값을 가지게 된다.
for (int j = 0; j < objectPrefabs.Length; j++)
    {
        for (int i = 0; i < poolSize; i++)
        {
            GameObject obj = Instantiate(objectPrefabs[j]);
            int temp = j;
            obj.GetComponent<EnemyStat>().OnDie += () => ReturnObject(obj, temp);
            obj.SetActive(false);
            obj.transform.SetParent(gameObject.transform);
            pool[j].Enqueue(obj);
        }
    }

이런 식으로 지역변수를 따로 만들어서 넣어주니 정상적으로 작동되었다.


Rigidbody의 Sleeping Mode

플레이어가 함정을 밟을 시 데미지 처리를 OnTrigger 를 통해 구현하였는데, 함정 위에 가만히 서있을 시 데미지 처리가 지속적으로 되지 않는 현상이 발견되었다.

이는 OnTriggerStay에서 충돌 오브젝트가 움직이지 않을 시, 일정 시간이 지나면 Sleeping Mode 에 들어가기 때문이다.

Sleeping Mode 는 유니티 물리 엔진의 성능 최적화 기능으로, Rigidbody가 일정 시간동안 거의 움직이지 않거나 에너지가 특정 임계값 아래로 떨어지면, 물리 계산에서 제외되어 CPU 자원을 절약할 수 있다.

해결 방법

  1. RigidbodySleeping ModeNever Sleep으로 바꿔준다.
  2. 지속적인 데미지 처리를 OnTriggerStay 가 아닌 Update 에서 해준다.

1번 방법은 간단하지만, 성능 저하가 심하게 일어날 가능성이 높기 때문에 고려하지 않은 방식이다. 따라서 그냥 Update 에서 데미지 처리를 담당하기로 했다.

   protected virtual void Update()
    {
        if (player != null && damageDelay <= timer)
        {
            TryDealDamage();
            timer = 0;
        }
        timer += Time.deltaTime;
    }

댓글남기기