GridUnlocker Part 2

더 이야기할만 한 주제들

UnityEvent를 이용해 인스펙터에서 콜백을 걸어주는 방법, 클리어 여부를 검사하는 방법, Prefab 시스템을 이용해 퍼즐을 저장한 메커니즘, 인게임 UI에 대해서는 다음 글에서 다루겠습니다.. 라고 저번 글에서 다루었습니다. 이번 시간에는 이 주제들에 대해 이야기를 해보고, 앞으로 이 프로젝트를 어떻게 더 이끌어 나갈지에 대해 이야기하겠습니다.

UnityEvent

C#에서 함수를 인자로 받아 저장해두었다가 한 번에 실행하는 시스템은 이미 C# delegate와 event라는 이름으로 존재합니다. 퍼포먼스 면에서나 단순성 면에서나 C# native method를 이용하는 것이 낫다고 생각하지만, 이번 프로젝트에서는 인스펙터에서 hook을 걸 수 있다는 장점을 고려해 UnityEvent를 이용하기로 했습니다.

UnityEvent는 위에서 간략하게 언급한 delegate / event 시스템의 wrapper로, 인스펙터 상에서 함수를 등록할 수 있다는 장점(내지는 단점)이 있습니다. 좋게 보자면 스크립트는 그대로 유지한 채로 각 프리팹/게임오브젝트 별로 별개의 함수를 등록할 수 있다는 장점이 있는 것입니다. 하지만 나쁘게 본다면 이렇게 인스펙터 상에서 등록한 함수는 스크립트로 제거가 불가능하고, 함수를 등록할 수 있는 point가 2개가 되어 관리할 지점이 2배가 되고(또 어떤 함수를 등록했는지 알려면 일일히 각 게임오브젝트를 조회해야 하고) 또 퍼포먼스 면에서 손해를 본다는 단점이 있는 것입니다. 일반 delegate를 인스펙터에서 조회할 수 있다면 좋았겠지만 현실은 그러하지 않기에 우리는 공학적인 선택을 해야 하는 것입니다.

각설은 그만 하고, 실제로 어떻게 사용했는지 설명하도록 하겠습니다.


    public UnityEvent OnLaserReceived;
    public UnityEvent OnLaserReceiving;
    public UnityEvent OnLaserStopped;

    private void LaserReceived()
    {
        OnLaserReceived.Invoke();
    }

    private void LaserReceiving()
    {
        // Each frame laser is received
        OnLaserReceiving.Invoke();
    }
    
    private void LaserStopped()
    {
        OnLaserStopped.Invoke();
    }



    public void LaserIsActive()
    {
        if (!isOnLaser)
        {
            isOnLaser = true;
            LaserReceived();
        }
        else
        {
            LaserReceiving();
        }
    }
    public void LaserIsInactive()
    {
        isOnLaser = false;
        LaserStopped();
    }

public 변수로 UnityEvent를 등록하면 인스펙터에서 함수를 지정해 넣을 수 있습니다. 그리고 특정 조건에 따라 Invoke()를 하면 등록된 함수들이 한 번에 실행됩니다. 이를 잘 이용하면,

등등의 일을 하나의 스크립트로 할 수 있게 됩니다.

클리어 여부를 검사하기

당연히, 레이저를 받았을 때 클리어 여부를 검사하는 트리거를 실행하게 만들 수 있습니다. 그렇다면 클리어는 어떻게 판정하면 될까요? 다음과 같은 기준을 삼았습니다.

첫번째 요소는 LaserEvent 컴포넌트 스크립트를 변수로 담아 두고 다음과 같이 설정했습니다. checker 변수의 UnityEvent OnLaserReceived에 대해 AddListner로 CheckVictoryCondition() 함수를 등록했습니다. 이제 checker가 레이저를 받으면 클리어 조건을 검사하게 되겠죠.


    public LaserEvent checker;
    private void OnEnable()
    {
        checker.OnLaserReceived.AddListener(CheckVictoryCondition);

    }

    private void OnDisable()
    {
        checker.OnLaserReceived.RemoveListener(CheckVictoryCondition);

    }

두번째 요소는 간단하게 List를 이용했습니다. 리스트를 순회하면서, 레이저를 모두 받고 있는지를 체크하게 만든 겁니다.

    public bool IsVictoryCondition()
    {
        return checkList.TrueForAll(element => element.isOnLaser);
    }

사실 더 추상적으로 만들기 위해서는 check를 위한 임의의 함수를 만들어서 각자 점검하는 것이 더 낫지만, 일단은 이렇게 했습니다. ‘특정 receiver에는 닿지 않게 하기’ 같은 조건을 넣고 싶을 때 그렇게 바꾸면 되겠군요.

세번째 요소는, 그리 좋아하지는 않지만, Singleton 패턴을 이용했습니다. PuzzleManager는 싱글톤으로, 다른 함수에서 PuzzleManager.instance와 같은 요령으로 접근 가능합니다. 간단한 게임이기 때문에 가능한 일이라 생각합니다.


    public void CheckVictoryCondition()
    {
        if (IsVictoryCondition())
            PuzzleManager.instance.PuzzleSolved();
    }

퍼즐을 저장하는 방법

자료구조

퍼즐을 저장하는 방법은 여러 가지가 있겠습니다만, 크게 나누자면 Prefab을 그대로 저장하는 방법과 조금 더 추상적인 파일 형태(csv나, json 등)로 저장했다가 직렬화/역직렬화를 하는 방법이 있습니다. 양쪽 다 장단점이 있습니다. 이번 프로젝트에서는 전자를 선택했습니다.

가장 큰 이유는 ‘퍼즐의 형태가 3차원이라는 것’과 ‘퍼즐의 모양이 부정형이라는 것’이었습니다. 먼저 퍼즐의 형태가 3차원이기 때문에, x y z축을 모두 저장해주어야 합니다. 이건 그래도 xy 평면에 배열된 형태를 여러 층을 쌓은 것으로 보아 해결한다고 할 수 있습니다. 하지만 회전된 정도를 저장해야 하는데 이것도 쉽지 않은 것입니다. 더 나아가서 다각형이나 원형, 기타 다른 형태의 퍼즐을 만들고 싶다면 어떨까요. 그런 내용을 모두 저장할 수 있는 훌륭한 파일 규격을 만드느니, 그냥 Unity 상에서 편집도 되고 미리보기도 가능한 Prefab을 쓰기로 했습니다.

퍼즐 저장하기

Prefab을 그대로 저장할 경우 종전에는 Prefab 내부에 다른 Prefab을 둘 수 없다거나, 동일한 Prefab의 다른 버전을 만들기 위해 서로 다른 Prefab으로 분할을 하면 공통 수정사항이 생겼을 때 각 Prefab 버전들을 일일히 일치시켜야 하는 크나큰 불편함이 있었습니다. 다행히도 Unity 2018? 2019? 정도에서 그 문제가 해결되었습니다.

이제 Prefab 조각들을 모아서 Prefab을 만들 수 있습니다. 이를 위해 기본 building block이 되는 prefab들을 만들고, 이를 레고 조각처럼 모아서 퍼즐을 구성해주었습니다. 나중에 Outline shader를 적용할 때에도 한 번에 적용이 되어 편리했습니다. 또 기본이 되는 Prefab 형태를 만들고, 마치 상속과 같이 달라진 점만 따로 저장하는 Prefab 기능을 활용해 다양한 퍼즐 조각을 만들 수 있었습니다.

이렇게 만든 퍼즐 덩어리를 Asset 폴더에 넣고, PuzzleManager 정도에 리스트로 등록시켜 두면, 다른 곳에서 함수를 호출하는 것으로 간단하게 퍼즐 로딩이 가능합니다. 이 때에 Coroutine을 활용해 부자연스럽게 ‘한 번에’ 퍼즐이 버벅거리며 나타나는 대신 애니메이션과 함께 퍼즐이 소환되게 만들었습니다.

여기에 DoTween도 활용해주었습니다. DoTween은 무료이면서, 최근까지 업데이트가 되고 있는 tween 라이브러리입니다. 기존의 오브젝트/마테리얼/UI 등을 확장시켜, 간단한 메소드 호출로 애니메이션 실행이 가능하게 만들어주는 라이브러리입니다. 나중에 사운드와 햅틱을 추가할 것을 염두에 두어 설계했습니다.

GridUnlocker의 미래

지금 3월은 꽤나 바빠서, 3월 말~4월 초에나 조금 여유가 날 것 같습니다. 그래서 3월 말 정도에는 유지보수 정도나 할 것 같습니다.

이런 작업들을 위에서 아래 순서로 4월 ~ 5월까지 진행하려고 합니다. 본격적인 개발은 여름방학에 재개할 예정입니다.

과연 이 게임이 상용 게임까지 갈 수 있을까요? 모르겠습니다. 확실한 것은 꽤나 재미있게 굴러가고 있다는 것입니다.

*****
긍정적인 영향을 주는 사람이 됩시다
Served using home raspberry pi 3 B+
☕ Pudhina theme by Knhash 🛠️