본문 바로가기

개발 공부 유니티, C#/스터디

게임 디자인 패턴 정리

이 책에 있는 내용은 예제 코드의 예시가 좋지 않은 경우도 좀 있었음.
예제가 괜찮게 구현됬다기보다는 이런 패턴이 있다를 이해하는 목적으로만 사용

1~3은 서론으로 스킵

 

Chapter 4 싱글턴으로 게임 매니저 구현

싱글턴 

단 하나의 객체만 가지고 있는 클래스

씬이 바뀌어도 파괴되지 않고, Awake에서 다른 인스턴스가 이미 존재한다면 현재 객체 파괴

 

GameManager를 예시로 들었는데, 예제는 생략

Start에서 플레이어 세이브/로드

OnApplicationQuit에서 종료시 마지막 종료시간으로 로그남김

 

Chapter 5 상태 패턴

public class BikeController
{
    switch(state)
    {
        case Stop:
            //...
            break;
        case Start:
            //...
            break;
        case Turn:
            //...
            break;
    }
}

 

Chapter 6 이벤트 버스로 게임 이벤트 관리하기

전역게임 이벤트(발행자) publish() -> 이벤트 버스 -> 구독자

이벤트 버스 클래스에 있는 event객체의 이벤트 발생을 알아야 하는 구독자들이 구독(event멤버에 대입)

이후 이벤트가 발생하면 특장 이벤트버스 클래스에서 실행(콜백)

 

아래에는 이벤트 버스 구현방식인데 그닥 중요하진 않고, Action으로 구현


UnityEngine.Events
UnityEvents : 유니티 에디터에서 이벤트들을 사용하기 위한 클래스
UnityActions

1. Event Listener가 2개 이상인 경우, UnityEvent가 C# Event에 비해 메모리를 덜 Allocation한다. (1개인 경우 그 반대)
2. Event Dispatch의 경우 UnityEvent는 맨 처음 Dispatch할 때 가비지를 발생시킨다. C# Event는 가비지가 발생하지 않는다.
3. UnityEvent는 C# Event에 비해 최소 두 배 느리고, worst case의 경우 40배까지 느렸다.

https://highfence.tistory.com/19

public class Publisher
{
	EventBus eventBus;
    
    void Start()
    {
    	//SubScribe
        Subscriber1 sb1 = new Subscriber1();
        Subscriber1 sb2 = new Subscriber2();
        eventBus += sb1.EventFunc();
        eventBus += sb2.EventFunc();
        
        eventBus.Publish();
    }
}

public class EventBus
{
	public event void thisEvent;
	
    public void Publish()
    {
    	thisEvent?.Invoke();
    }
}

public class Subscriber1
{
	public void EventFunc()
    {
		//구독후 이벤트 발생시 호출될 콜백 함수
    }
}

public class Subscriber2
{
	public void EventFunc()
    {
    	//구독후 이벤트 발생시 호출될 콜백 함수
    }
}

 

Chapter 7 커맨드 패턴으로 리플레이 시스템 구현

 

RPG게임의 스킬 들을 커맨드 패턴으로 구현하는게 좋은 예시

*단점 : 커맨드를 상속하는 모든 클래스를 생성해야함. 복잡해짐

게임, 코드에 대해 익숙하지 않으면 어떤 커맨드들이 있는지 알기 힘듬

using UnityEngine;
using System.Collection;

public class InputHandler : MonoBehaviour
{
    //Controller와 Command를 멤버로 갖고
    //유저 입력 처리
}

public class Controller : MonoBehaviour
{
    //오브젝트가 커맨드에 의해 호출될 함수들
}

public abstract class Command : MonoBehaviour
{
    public abstract void Execute();
}

//Command를 상속하여 구현한 클래스들
//외부에서 받은 controller를 멤버로 갖고 controller의 함수를 실행
public class TurnRight : Command {}
public class TurnLeft : Command {}
public class Turbo : Command {}

//리플레이 저장중엔
//유저의 입력을 받은 시간들을 저장하고, 리플레이 재생시 그대로 처리
//같은 입력이라도 랜덤값에 따라 요소들로 결과가 다를 수 있는 경우 고정해주어 같은 결과가 나오도록 해주어야함

Chapter 8 오브젝트 풀로 최적화

예측가능한 메모리 사용

새로운 객체 생성을 줄여 성능 향상

 

캐시처럼 객체를 재사용하는 목적이 있지만, 풀의 크기에 따라 객체를 생성/삭제한다는 차이가 있다.

 

Chapter 9 옵저버 패턴으로 컴포넌트 분리

주체에 필요한 만큼 객체를 옵저버로 추가할 수 있고, 런타임 중 동적 제거도 가능

일대다 : 주체 - 옵저버

*단점 : 주체에서 옵저버에게 알리는 순서를 보장하지 않음

 

HUD, Camera, 같은 주체에 대한 정보를 보여주는 UI들에 적합

 

Chapter 10 방문자 패턴

파워업 시스템을 예제로 들고 있음

플레이어, 닿았을때 기능을 향상시키는 객체의 구현

 

아래처럼 변경이 필요한 주체를 함수 매개변수로 보내어 함수내에서 값을 변경하는 구조

함수에 객체 참조로 넘어가는걸 방문자라고 표현

public void Visit(BikeEngine bikeEngine)
{
    float boost = bikeEngine.turboBoost = 0.0f;

        if(boost < 0.0f)
            bikeEngine.turboBoost = 0.0f;
        
        if(boost >= bikeEngine.maxTurboBoost)
            bikeEngine.turboBoost = bikeEngine.maxTurboBoost;
}

ScriptableObject로 필요 특정값들을 변경하는 예제들이 있음

[Range(0.0f, 50f)]
public float turboBoost;
[Range(0.0f, 25f)]
public float weaponRange;
[Range(0.0f, 100f)]
public float weaponStrength;

 

Chapter 11 전략 패턴

적 드론 디자인 예제

드론의 행동 패턴

bobbing(상하로 움직임), weaving(좌우로 움직임), fallback(뒤로 움직임)

예제에서는 특정 시간마다 랜덤하게 한 가지 전략을 취해줬는데, 조건에 따른 전략 선택으로 활용하는 경우가 많음

//전략 클래스들이 상속할 인터페이스
public interface IManeuverBehaviour
{
    void Maneuver(Drone drone)
}

public class Drone : MonoBehaviour
{
    //광선 파라미터
    private RaycastHit _hit;
    private Vector3 _rayDirection;
    private float _rayAngle = -45.0f;
    private float _rayDistance = 15.0f;

    //이동 파라미터
    public float speed = 1.0f;
    public float maxHeight = 5.0f;
    public float weavingDistance = 1.5f;
    public float fallbackDistance = 20.0f;

    void Start()
    {
        //rayDirection 초기화
    }

    public void ApplyStrategy(IManeuverBehaviour strategy)
    {
        strategy.Maneuver(this);
    }

    void Update()
    {
        //오브젝트에서 Raycast 쏘고 맞으면 출력 (공격 판정으로 간주)
    }
}

//전략 기술을 구현 보빙, 위빙, 폴백
public class BoppingManeuver : MonoBehaviour, IManeuverBehaviour
{
    public void Maneuver(Drone drone)
    {
        StartCoroutine(Bopple(drone));
    }

    IEnumerator Bopple(Drone drone)
    {
        //일정 시간동안 드론의 위치를 위, 아래(y)로 움직임
    }
}

public class WeavingManeuver : MonoBehaviour, IManeuverBehaviour
{    
    public void Maneuver(Drone drone)
    {
        StartCoroutine(Weave(drone));
    }

    IEnumerator Weave(Drone drone)
    {
        //일정 시간동안 드론의 위치를 좌우(x)로 움직임
    }
}

public class FallbackManeuver : MonoBehaviour, IManeuverBehaviour
{    
    public void Maneuver(Drone drone)
    {
        StartCoroutine(Weave(drone));
    }
     
    IEnumerator Fallback(Drone drone)
    {
        //일정 시간동안 드론의 위치를 뒤(z)로 움직임
    }
}
 
public ClientStrategy : MonoBehaviour
{
    public GameObject _drone;
    private List<IManeuverBehaviour> _components = new List<IManeuverBehaviour>();
 
    private void SpawnDrone()
    {
        _drone = GameObject.CreatePrimitive(PrimitiveType.Cube);
        _drone.AddComponent<Drone>();
        _drone.tranform.position = Random.insideUnitSphere * 10;
        ApplyRandomStrategies();
    }
 
    private void ApplyRandomStrategies()
    {
        _components.Add(_drone.AddCompoent<BoppingManeuver>());
        _components.Add(_drone.AddCompoent<WeavingManeuver>());
        _components.Add(_drone.AddCompoent<FallbackManeuver>());
 
        int index = Random.Range(0, _components.Count);
        _drone.GetComponent<Drone>().ApplyStrategy(_components[index]);
    }
}

 

Chapter 12 데커레이터 패턴

 

기존 객체를 변경하지 않고 새로운 기능을 추가

장점
서브 클래싱의 대안 : 원하는 동작이 있는 같은 부모 클래스의 인스턴스만 다른 인스턴스로 교체할 수 있다.
런타임 다이내믹 : 객체에 데커레이터를 추가하여 런타임에 기능을 추가/제거 가능

단점
복잡한 관계 : 객체 주변에 다양한 계층의 데커레이터가 있다면 초기화 체인, 데커레이터 간의 관계 추적 복잡
복잡해지는 코드 : 작은 데커레이터 클래스들의 유지/관리로 코드가 복잡해 질 수 있음.
여기서는 각 에셋으로 저장하는 ScriptableObject인스턴스 이므로 문제는 되지 않음.

데커레이터 패턴을 사용하는 경우
무기에 다양한 부착물 부착 가능
런타임 추가/삭제 가능

무기 시스템 디자인 하기
인젝터 : 데미지 +20
쿨러 : 쿨다운 -1
안정 장치 : 사정거리 + 10


위 부착물들을 오토바이 무기에 적용하는 예제

public class BikeWeapon : MonoBehaviour
{
    public WeaponConfig weaponConfig;
    public WeaponAttachment mainAttachment;
    public WeaponAttachment secondaryAttachment;
     
    private bool _isFiring;
    private IWeapon _weapon;
    private bool _isDecorated;
 
    void Start()
    {
        _weapon = new Weapon(weaponConfig);
    }
 
    void OnGUI()
    {
        //weapon. Range, Strengh, Cooldown, Rate, isFiring 출력
        //mainAttachment, secondaryAttachment name출력
    }
 
    public void ToggleFire()
    {
        isFiring = !_isFiring;
 
        if(_isFiring)
            StartCoroutine(FireWeapon());
    }
 
    IEnumerator FireWeapon()
    {
        float firingRate = 1.0f / _weapon.Rate;
 
        while(_isFiring)
        {
            yield return new WaitForSeconds(firingRate);
            Debug.Log("fire");
        }
    }
 
    public void Decorate()
    {
        if(mainAttachment && !secondaryAttachment)
        {
            _weapon = new WeaponDecorator(_weapon, mainAttachment);
        }  
 
        if(mainAttachment && secondaryAttachment)
        {
            _weapon = new WeaponDecorator(new WeaponDecorator(_weaponm, mainAttachment), secondaryAttachment);
        }   
 
        _isDecorated = !_isDecorated;
    }
}
 
 
public interface IWeapon
{
    float Range { get; }
    float Rate { get; }
    float Strength { get; }
    float Cooldown { get; }
}
 
//get 프로퍼티 모음
public class Weapon : IWeapon
{
    Range
    Rate
    Strength
    Cooldown
    WeaponConfig
}
 
public class WeaponDecorator : IWeapon
{
    private readonly IWeapon _decorateWeapon;
    private readonly WeaponAttachment _attachment;
         
    public WeaonDecorator(IWeapon weapon, WeaponAttachment attachment)
    {
        _attachment = attachment;
        _decorateWeapon = weapon;
    }
     
    //get 프로퍼티
    // 무기의 원래 값과 attachment의 값을 합산해서 리턴
    public float Rate
    public float Range
    public float Strength
    public float Cooldown
}
 
public class WeaponAttachment : ScriptableObject, IWeapon
{
    //무기에 상승시켜줄 멤버 값들을 Range로 각자 제한을 걸고 갖고 있음
    //해당 값을 get하는 프로퍼티
}
 
public class WeaponConfig : ScriptableObject, IWeapon
{
    //attachment에 사용할 config값
}
 
public class ClientDecorator : MonoBehaviour
{
    private BikeWeapon _bikeWeapon;
    private bool _isWeaponDecorated;
 
    void Start()
    {
        _bikeWeapon = (BikeWeapon)FindObjectOfType(typeof(BikeWeapon));
    }
 
    void OnGUI()
    {
        if(!_isWeaponDecorated)
            if(GUILayout.Button("Decorate Weapon"))
            {
                _bikeWeapon.Decorate();
                _isWeaponDecorated = !_isWeaponDecorated;
            }
 
        if(!_isWeaponDecorated)
            if(GUILayout.Button("Reset Weapon"))
            {
                _bikeWeapon.Reset();
                _isWeaponDecorated = !_isWeaponDecorated;
            }   
 
          if(GUILayout.Button("Toggle Fire"))
             _bikeWeapon.ToggleFire();
    }
}

 

Chapter 13 공간 분할로 레벨 에디터 구현

 

방대한 맵의 요소들은 전부 로드하면 과부하가 걸리기 때문에 공간을 분할해서 구현.

현재 플레이어가 속한 특정 공간내에서의 상호작용 만을 하도록 하여, 부하를 줄임

 

Chapter 14 어댑터로 시스템 조정

클린 코드에서도 경계 처리에서 나왔던 내용

*구조적 패턴의 패밀리 
퍼사드, 브리지, 컴포지트, 등도 구조적 패턴
서로 호환되지 않는 2개의 인터페이스 조정
리팩터링 할 수 없는 레거시 코드, 업그레이드했을 때 써드 파티 라이브러리에 기능을 추가할 경우 용이


객체 어댑터 : 어댑터는 조정한 객체의 래퍼처럼 동작
원래 메서드를 가져와 필요한 것으로 조정 후 사용

클래스 어댑터 : 기존 클래스의 인터페이스를 다른 클래스의 인터페이스에 적용하기 위해 상속
아래 예제에서 이 방식으로 구현

장점
수정 없이 조정 가능 : 오래된 코드 서드파티코드를 수정없이 조정
재사용성 및 유연성 : 새로운 시스템에서 최소한의 변경만으로 레거시 코드를 계속 사용할 수 있다. 

단점
지속적인 레거시 사용 : 개발 비용 측면에서 효율적이지만..(이걸 효율적이라고 표현하는게 맞는지..), 사용되지 않는 오래된 코드, 새로운 유니티 버전업, 서드파티 라이브러리의 호환에 문제가 될 수 있음.
약간의 성능 저하 : 객체 간 호출을 리다이렉션하는 비용, 약간의 성능 저하 미미한 단점

using UnityEngine;
using System.Collections.Generic
 
//라이브러리
public class InventorySystem
{
    public void AddItem(InventoryItem item) //
    public void RemoveItem(InventoryItem item) //
    public List<InvetoryItem> GetInventory() //
}
 
public class InventorySystemAdapter : InventorySystem, IInventorySystem
{
    //_cloudInventory객체를 갖고 필요한 행동을 취하는 어댑터
    private List<InventoryItem> _cloudInventory;
 
    public void SynInventories()
    {
            var _cloudInventory = GetInvetory();
 
            Debug.Log("Synchronizeing local drive and cloud inventories");
    }
 
    public void AddItem(InventoryItem item, SaveLocation location)
    {
        if(location == SaveLocation.Cloud)
            AddItem(item); // 부모의 InventorySystem의 AddItem을 호출
 
        if(location == SaveLocation.Local)
            //
        if(location == SaveLocation.Both)
            //
    }
 
    public void RemoveItem(InventoryItem item, SaveLocation location)
 
    public List<InventoryItem> GetInventory(SaveLocation location)
    {
        return new List<InvetoryItem>();
    }
}
 
public interface IInventorySystem
{
    void SyncInventories();
    void AddItem(InventoryItem item, SaveLocation location );
    void RemoveItem(InventoryItem item) //
    List<InvetoryItem> GetInventory(SaveLocation location) //
}
 
public class ClientAdapter
{
    public class ClientAdapter : MonoBehaviour
    {
        public InventoryItem item;
        private InventorySystem _inventorySystem;
        private IInventorySystem _inventorySystemAdapter;
 
        void Start()
        {
            _inventorySystem = new InventorySystem();
            _inventorySystemAdapter = new InventorySystemAdapter();
        }
 
        void OnGUI()
        {
            if(GUILayout.Button("Add item (no adapter)"))
                _inventorySystem.AddItem(item);
 
            if(GUILayout.Button("Add item (with adapter)"))
                _inventorySystemAdapter.AddItem(item, SaveLocation.Both);
        }
    }
}

 

Chapter 15 퍼사드 패턴으로 복잡성 숨기기

복잡한 내부 구조를 숨기는 패턴
p176 서브 시스템들의 내용을 밖에서 래핑된 단일 클래스로의 호출

장점 : 복잡한 코드 본문에 단순화된 인터페이스 제공, 쉬운 리팩터링
단점 : 지저분한 코드를 쉽게 숨길 수 있다 → ? 위에 장점으로 쓴 내용 같은데..
너무 많은 퍼사드 : 전역 접근이 가능한 매니저 클래스지만 남용되는 경우 매니저들이 남발, 디버깅, 리팩터링, 단위테스트가 어려워짐

기존 클래스 FuelPump, CoolingSystem, TurboCharger,를 퍼사드 클래스 BikeEngine에서 멤버로 갖고
필요에 따른 해당 클래스들을 호출
앞의 어댑터 예제와는 다르게 상속이 아닌 다른 클래스의 내용을 불러와서 사용

 

Chapter 16 서비스 로케이터 패턴으로 종속성 관리

장점
런타임 최적화 : 특정 서비스를 완료하고자 더 최적화된 라이브러리나 컴포넌트를 동적으로 감지하여 최적화 할 수 있다.
단순성 : 간단해서 프로젝트에서 빠르게 사용하고 동료에게 패턴을 가르쳐 줄 수 있다.

단점
블랙박스화 : 클래스 종속성을 읽기 어렵게 함, 종속성을 잃거나 잘못 등력했다면 런타임에 이슈가 발생할 수 있다.
전역적 종속성 : 잘못 남용하면 관리하기 힘든 전역 종속성이 될 수 있다. 코드가 과하게 의존하게 되고 쉽게 분리할 수 없게 됨.

동적으로 접근해야 하는 서비스 목록을 서비스를 얻는 것과 관련된 과정을 캡슐화 하고 싶을 때 사용

 

사용 예시

로거 : 중앙 집중식 로깅 시스템의 퍼사드 역할
분석 : 플레이어 행동에 대한 통찰력을 제공하기 위해 백엔드에 커스텀 분석 정보를 보냄
광고