[Unity] Destroy

Jihoon·2022년 4월 27일
0

MMO_Unity

목록 보기
20/22

Destroy

게임의 오브젝트들의 체력이 0이 되었을 때 만약 게임오브젝트를 무작정 제거한다면, 오브젝트는 제거됐지만 다른 객체에서 계속해서 제거된 오브젝트에 접근하려고 하기 때문에(다른 객체에서 플레이어의 컴포넌트에 접근하려고 한다든지) 에러가 발생한다.

따라서 게임 내의 오브젝트를 제거해야 되는 상황이 온다면 단순히 오브젝트를 제거하는것이 아닌 제 3의 오브젝트에서 오브젝트 제거/삽입을 관리해야 한다.

C#의 null 관리

if (targetStat.Hp <= 0)
{
    GameObject.Destroy(targetStat.gameObject);
}
GameObject _player = null;
if (_player == null)
    return;

몬스터 컨트롤러에 플레이어의 체력이 0이하가 되면 플레이어 객체를 삭제하는 코드와 카메라 컨트롤러에 만약 플레이어가 null이 된다면 return 하라는 코드를 추가해서 테스트해보았다.
그러면 플레이어의 체력이 0이 되는 순간 return이 된다. 이는 원래라면 불가능한 일이다.

C#은 참조를 통해서 객체에 접근한다. 즉 카메라 컨트롤러의 _player 변수는 플레이어의 객체 그 자체가 아닌 객체를 가리키는 주소값이 된다.

그렇기 때문에 만약 플레이어의 객체가 null이 된다고 해도 플레이어의 객체를 나타내던 주소값은 null이 되지 않는다. 따라서 원래라면 플레이어의 객체가 null이 됐을 때 _player에 접근한다면 _player 변수는 null이 아니기 때문에 위에 작성한 코드처럼 _player 객체의 null 체크를 할 수가 없다. (C++같은 언어에서는 실제로 그런 오류가 발생한다.)

하지만 테스트 해본 결과는 에러 없이 정상적으로 작동한다. 이는 중단점을 걸고 디버깅을 해보면 이유를 알 수 있다.
위 스크린샷을 보면 _player의 값이 "null" 인것을 알 수 있다. 얼핏 보면 _player를 null로 인식하고 있는 것으로 보이나, 알고 보면 이는 null값을 나타내는 것이 아닌 "null"이라는 문자열을 값을 나타내는 것이다.
즉, 메모리 상에는 _player가 남아있으나 null체크를 위해 null로 인식되도록 C#이 처리한 것이다.

위 사실을 통해 우리는 연산자 오버로딩을 사용하고 있다는 것을 알 수 있다. 실제로 GameObect의 부모 클래스인 Object 클래스에 접근하면 == 연산자의 오버로딩을 찾을 수 있다.

GameManagerEx

뒤에 Ex가 붙는 이유는 유니티 버그로 인해 GameManager라는 이름의 스크립트를 만들면 툴에서의 아이콘이 이상하게 나타나는 현상이 있다. 따라서 뒤에 Ex라는 이름을 추가하도록 한다.(기능은 완전히 동일하다)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManagerEx
{
    GameObject _player;
    HashSet<GameObject> _monsters = new HashSet<GameObject>();

    public GameObject Spawn(Define.WorldObject type, string path, Transform parent = null)
    {
        GameObject go = Managers.Resource.Instantiate(path, parent);
        switch (type)
        {
            case Define.WorldObject.Monster:
                _monsters.Add(go);
                break;
            case Define.WorldObject.Player:
                _player = go;
                break;
        }

        return go;
    }

    public Define.WorldObject GetWorldObjectType(GameObject go)
    {
        BaseController bc = go.GetComponent<BaseController>();
        if (bc == null)
            return Define.WorldObject.Unknown;

        return bc.WorldObjectType;
    }

    public void Despawn(GameObject go)
    {
        Define.WorldObject type = GetWorldObjectType(go);

        switch (type)
        {
            case Define.WorldObject.Monster:
                {
                    if (_monsters.Contains(go))
                        _monsters.Remove(go);
                }
                break;
            case Define.WorldObject.Player:
                {
                    if (_player == go)
                        _player = null;
                }
                break;
        }

        Managers.Resource.Destroy(go); 
    }
}

이제 처음부터 적과 플레이어의 프리팹을 배치한 상태로 게임을 실행하는 것이 아닌 게임이 실행되면 그때 플레이어와 적이 스폰되는 방식을 사용할 것이다.

Define 클래스의 정의한 enum을 통해 게임 오브젝트가 적인지 플레이어인지 구분하고 스폰과 디스폰을 한다. 그리고 GameScene 클래스에서 게임 매니저를 통해 스폰을 시키면 된다.

이후에 몬스터 컨트롤러에서 OnHitEvent() 함수에서 Destroy가 아닌 Despawn을 하도록 수정한다.

이후에 게임을 실행해보니 정상적으로 작동하지만, 플레이어가 사망한 이후에 비활성화 된 오브젝트가 애니메이션을 사용하고 있다는 경고가 발생한다.

public void Destroy(GameObject go)
{
    if (go == null)
        return;

    Poolable poolable = go.GetComponent<Poolable>();
    if (poolable != null)
    {
        Managers.Pool.Push(poolable);
        return;
    }    

    Object.Destroy(go);
}

이는 우리가 플레이어를 풀링해서 사용하고 있기 때문이다.

리소스 매니저에서 오브젝트가 제거될 때 만약 오브젝트가 풀링된 오브젝트라면 오브젝트를 제거하는 것이 아닌 비활성화 하는 방식을 사용하고 있기 때문에 플레이어는 제거가 아닌 비활성화 되고, 비활성화 된 상태에서 Idle같은 애니메이션을 계속해서 사용하고 있기 때문에 경고가 발생하는 것이다.

if (_player == null || !_player.activeSelf)
    return;

카메라 컨트롤러의 코드를 수정하여 플레이어가 null이거나 비활성화 상태일 때 처리하도록 하면 된다.

profile
클라이언트 개발자 지망생

0개의 댓글