본문 바로가기
개발/게임 디자인 패턴

게임 디자인 패턴 2. 경량 패턴 (Flyweight Pattern)

by 석시 2023. 10. 24.



들어가며

경량 패턴.

무엇이 가볍길래?

설명에 따르면 사용하는 메모리가 가볍다는 의미이다.

객체의 수가 너무 많아서 메모리의 양에 차지하는 공간이 많을 때, 객체를 가볍게 만들어서 최적화하는 패턴을 경량 패턴이라고 한다.

그래서 가볍게는 어떻게 만드나요?

모든 개체가 똑같은 값을 가지고 있는 데이터를 모아서 개체마다 저장하는 것이 아닌 한 곳에다 저장한 후 모든 개체가 그 데이터를 참조하는 방법으로 만든다!

이러한 공통 데이터를 GoF에서는 고유 상태 (intrinsic state) 라고 부르고 그렇지 않는 개별 데이터는 외부 상태 (extrinsic state) 라고 부른다.

게임 프로그래밍 패턴에서는 자유문맥 (context-free) 상태라고 부르는데, 음 글쎄다… 한글로 어감이 잘 번역이 되지 않은 것 같다.

문맥이 없는 상태?라고 적당히 알아들으면 될 것 같다.

경량 패턴을 설명하는 대부분의 글에서 숲에 있는 나무를 렌더링하는 상황을 예로 든다.

보통 나무들끼리 같은 모델링을 공유하기 때문에 나무가 어디있는지 위치 정보만을 가지고 메시나 머테리얼 같은 모델링은 전부 하나의 데이터를 참조하도록 하는 것이다.

해당 예시의 핵심은 경량 패턴 그 자체보다도 어떤 식으로 자원을 공유하고 있는 지를 그래픽카드도 이해할 수 있는 방식으로 표현해야 한다는 것이다.

이는 Direct3D와 OpenGL에서 인스턴스 렌더링이라는 기능을 통해 기본적으로 지원하고 있다고 한다.


경량 패턴의 구현?

사실 경량 패턴의 자세한 구현 방식은 잘 몰라도 좋다.

유니티에서 기본적으로 지원하는 기능들로 구현이 가능하고, 다른 패턴과 섞어서 사용한는 것이 대부분이기 때문이다.

앞서 언급했듯이 고유 상태의 공유 자원을 어떻게 Implement 할 건지가 경량 패턴의 핵심이겠다.

유니티에서 가능한 방법들을 몇 가지 소개해보고자 한다.

방법 1) 스크립터블 오브젝트 (Scriptable Object)

스크립터블 오브젝트 | 유니티
유니티 엔진에서 제공하는 데이터 컨테이너인 스크립터블 오브젝트(Scriptable Object)에 대해서 알아봅시다. 타임라인 0:00 인트로 0:12 - 스크립터블 오브젝트에 대한 기본 설명 1:53 - 스크립터블 오브젝트 만드는 방법 2:19 - CreataAssetMenu 3:54 - 코드 작성 후 스크립터블 오브젝트 애셋 생성하기 5:07 - ZombieData 스크립터블 오브젝트를 사용하는 Zombie 클래스 만들기 5:44 - ZombieSpawner 만들기 7:42 - 스크립터블 오브젝트의 활용과 주의점 본 영상의 오프닝은 비디오몬스터를 이용해서 만들어졌습니다. https://videomonster.com/ [유니티 어필리에이트 프로그램] 아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다. - 유니티 에셋스토어 : https://prf.hn/l/vJl41D5 - 유니티 플러스 : https://prf.hn/l/wLNyJZG - 유니티 프로 : https://prf.hn/l/qXOybmp [투네이션] https://toon.at/donate/637735212761460238 [Patreon] https://www.patreon.com/WerGameDevChan #유니티강좌 #스크립터블오브젝트 #게임만들기 #베르 #베르의프로그래밍노트
https://youtu.be/7Qt4QNhM4nY?si=1cLyX9cTHZLHtaY3

위 영상의 예시는 좀비이다.

좀비가 제대로 기능하기 위해 개체의 최대체력, 데미지, 시야, 움직이는 속도를 참조할 때 해당 데이터를 스크립터블 오브젝트로 만들어서 일종의 프리셋 형태로 저장해놓자는 방식이다.

그렇게 되면 좀비가 200마리, 300마리가 스폰되어도 공통된 스탯을 저장하기 위해 메모리에 2~300개의 공간을 할당할 필요가 없이 단 한 번의 메모리 할당만으로 모든 좀비들의 스탯을 읽어올 수 있게 되는 것이다.

스크립터블 오브젝트의 주의점으로는 에디터에서는 스크립터블 오브젝트를 언제든 생성하고 관리할 수 있지만, 빌드 버전에서는 그것이 불가능하다.

스크립터블 오브젝트는 애셋 파일 형식으로 관리되기 때문에 애셋 번들로 빌드하고 배포하는 방식으로 게임 데이터를 관리하고 있다.

자세한 내용은 다음의 유튜브를 참고하자.

재빌드 없이 밸런스패치 가능할까?
구글스프레드 시트에서 스크립터블 오브젝트로 서버에서 데이터를 가져오는 방법입니다 빌드를 안하고도 서버의 밸런스패치 내용이 바로 반영됩니다
https://youtu.be/72NSxjWt6mM?si=2Jo_SarcVCWF1inP

추가로 스크립터블 오브젝트에 데이터를 저장하면 이것이 빌드된 게임에 포함되어 배포가 되기 때문에 사용자가 마음만 먹는다면 데이터를 조작할 수 있기 때문에 멀티플레이 게임에서는 다른 방식을 사용하는 것이 좋다.

방법 2) 정적 배칭 (Static Batching)

정적 게임 오브젝트 - Unity 매뉴얼
런타임 시점에 움직이지 않는 게임 오브젝트는 정적 게임 오브젝트이고, 런타임 시점에 움직이는 게임 오브젝트는 동적 게임 오브젝트입니다.
https://docs.unity3d.com/kr/current/Manual/StaticObjects.html

유니티 오브젝트를 인스펙터 상에서 보면 오른쪽 위 구석탱이에 Static이라는 것을 활성화 시킬 수 있는 것을 볼 수 있다.

이는 오브젝트를 정적 오브젝트 (Static Object) 로 만들어주는 체크박스이다.

런타임에 절대로 움직이지 않는 객체를 정적 오브젝트로 지정할 수 있는데, 정적 오브젝트로 지정되면 유니티 엔진이 시스템을 사전 계산할 때 해당 오브젝트를 포함할 수가 있게 된다.

미리 계산하여 로드를 줄이는 효과도 있지만, 가장 중요한 것은 유니티가 같은 머테리얼을 가진 정적 오브젝트들을 한 그룹으로 묶어 한 번에 렌더링하기 때문에 드로우콜이 줄어든다는 것이다!

즉, 머테리얼을 여러 번 불러올 필요가 없으니 GPU 입장에서 상당히 많은 수준의 최적화를 이룰 수 있는 것이다.

주의점이라면 유니티에서 지원하는 인스턴스 렌더링 기능인 GPU 인스턴싱을 정적 오브젝트에는 사용할 수 없다는 점이다.

렌더링해야하는 머테리얼의 종류가 다양하다GPU 인스턴싱, 같은 머테리얼을 공유하는 대규모 객체 그룹이 존재한다정적 배칭으로 최적화를 하면 되겠다.


경량 패턴을 언제 사용할지 모르겠어요

경량 패턴을 언제 사용해야 하는지 감이 잡히지 않을 경우가 있다.

게임 프로그래밍 패턴에서는 필요할 때 만드는 것이 낫다고 한다.

공유 기능을 유지하고 싶다면, 인스턴스를 요청받았을 때 이전에 만들어 놓은 것이 있는지 확인하고, 있다면 그걸 반환해주는 방식으로 구현하면 된다.

using System.Collections.Generic;
using UnityEngine;

public class ObjectContainer : MonoBehaviour {

    static Dictionary<string, GameObject> dict = new Dictionary<string, GameObject>();
    static GameObject objectPrefab;

    public static GameObject getPerson(string key)
    {
        if (!dict.ContainsKey(key))
        {
            GameObject tmp = Instantiate(objectPrefab);
            dict.Add(key, tmp);
        }
        return dict[name];
    }
}


Uploaded by N2T