# 게임 개발에 자주 사용되는 디자인 패턴만을 다룰 것
- 소프트 웨어 개발 중 자주 발생하는 문제들에 대한 해결책
- 미친놈들이 만든 방법을 사용하게 된다.
- 수세기동안 고수들이 개발
- 시도되고 검증됨
- 개발자간 효율적인 의사소통
- 디자인 패턴은 의도와 목적에 따라 분류된다.
- 디자인 패턴에선 다형성 패턴을 많이 애용
- 부모 포인터 <- 자식 객체(가능)
- 자식 포인터 <- 부모 객체(불가능)
- 다형성 구현에 virtual or abstract 지정 필수
//부모 포인터 <- 자식 객체 할당 Super obj = new Sub(); //자식 포인터 <- 부모 객체 할당 불가 //Sub obj = new Super();
- 팩토리: 객체 생성을 담당하는 추상 클래스 or 인터페이스
- 주어진 입력에 따른 객체를 반환하는 메소드 존재
class HumanFactory { Human CreateHuman(HumanType type) { switch(type) { case HumanType.Teacher: return new Teacher(); break; case HumanType.Student: return new Student(); break; } } }; //사용 시 Human human1 = HumanFactory.CreateHuman(Human.Teacher); Human human2 = HumanFactory.CreateHuman(Human.Student);
- 추상 클래스 or 인터페이스의 팩토리 클래스를 통해 객체 생성 메소드(팩토리 매서드)를 선언하고, 실제 객체 생성을 서브 클래스에서 구현
- 0-1. 팩토리 클래스 생성
- 객체 생성을 다루는 클래스 생성
- 0-2. 팩토리 메소드 생성
- 객체를 실재로 할당하는 메소드 생성
- 부모 포인터 배열 생성
- 자식 객체 할당 후 관리
abstract class StageGenerator { abstract void CreateCreatures(); }; class Stage1Generator : StageGenerator { override void CreateCreatures(){ //몬스터 10마리 생성 } }; class Stage2Generator : StageGenerator { override void CreateCreatures(){ //몬스터 15마리 생성 } }; class StageFactoryMethod { StageGenerator[] stageGenerator = null; StageFactoryMethod() { stageGenerator = new StageGenerator[STAGE_SIZE]; stageGenerator[0] = new Stage1Generator(); stageGenerator[1] = new Stage2Generator(); } void MakeStage1() { stageGenerator[0].CreateCreatures(); } void MakeStage2() { stageGenerator[1].CreateCreatures(); } };
- 추상 팩토리(abstract 부모 클래스 or 인터페이스) 선언
- 해당 클래스엔 abstract 객체 생성 함수 선언
- 자식 클래스에서 해당 함수 override
//프린터 객체 class Printer {...}; class PrinterA : Printer {...}; class PrinterB : Printer {...}; //스캐너 객체 class Scanner {...}; class ScannerA : Scanner {...}; class ScannerB : Scanner {...}; //Factory Method class FactoryPrinter{ //A회사 -> A소프트웨어, B회사 -> B소프트웨어 생성 및 반환 }; class FactoryScanner{ //A회사 -> A소프트웨어, B회사 -> B소프트웨어 생성 및 반환 }; //복합기 객체 class MultifunctionPrinter{ void UseFactory(CompanyType type) { Printer printer = FactoryPrinter.MakePrinter(type); Scanner scanner = FactoryScanner.MakeScanner(type); } }; class MultifunctionPrinter {...}; class PrinterA : MultifuncitonPrinter {...}; class PrinterB : MultifunctionPrinter {...};
- 위 상황에서 Printer, Scanner 뿐만 아닌 더 다양한 객체를 담당할 때, 이를 모두 수정해야하므로 불편함
abstract class MultiPrinterFactory { abstract Printer MakePrinter(); abstract Scanner MakeScanner(); }; class AFactory : MultiPrinterFactory { override Printer MakePrinter() { return new PrinterA(); } override Scanner MakeScanner() { return new ScannerA(); } }; class BFactory : MultiPrinterFactory { override Printer MakePrinter() { return new PrinterB(); } override Scanner MakeScanner() { return new ScannerB(); } };
- 사용법
MultiPrinterFactory factory = null; CompanyType type = CompanyType.A; if(type == CompanyType.A) { factory = new AFactory(); } else if(type == CompanyType.B) { factory = new BFactory(); } Printer printer = factory.MakePrinter(); Scanner scanner = factory.MakeScanner();
- 빌더(Builder): 객체 생성을 담당하는 인터페이스
- 객체 생성 메서드 제공
- 구체적 빌더(Concrete Builder): 실제로 객체를 조립하는 클래스
- 제품(Product): 최종적으로 생성되는 복합 객체
- 감독자, 옵션(Director): Builder를 사용해 객체를 생성하는 클래스
using UnityEngine; //제품(Product) public class Character { public string Name; public int HP; public int ATK; public Character(string name, int hp, int atk) { Name = name; HP = hp; ATK = atk; } } //추상 빌더 public abstract class CharacterBuilder { protected Character character; public Character Character { get { return character; } }; public abstract void BuildName(); public abstract void BuildHP(); public abstract void BuildATK(); } //구체적인 빌더 public class WarriorBuilder : CharacterBuilder { public WarriorBuilder() { character = new Character("Warrior", 100, 50); } public override void BuildName(){ character.Name = "Warrior"; } public override void BuildHP(){ character.HP = 100; } public override void BuildATK(){ character.ATK = 10; } } //구체적인 빌더 public class MageBuilder : CharacterBuilder { public MageBuilder() { character = new Character("Mage", 100, 50); } public override void BuildName(){ character.Name = "Mage"; } public override void BuildHP(){ character.HP = 20; } public override void BuildATK(){ character.ATK = 50; } } //감독자 public class CharacterCreator : MonoBehaviour { private CharacterBuilder characterBuilder; public void SetCharacterBuilder(CharacterBuilder builder){ createrBuilder = builder; } public Character GetCharacter(){ return characterBuilder.Character; } }
- 프로토 타입 객체를 미리 생성
- 프로토 타입 객체을 클론해 객체를 생성
- 높은 비용의 객체 생성 시 효과적
interface IPlayer { IPlayer Clone(); } class Character : IPlayer { Data data; IPlayer Clone() { return this.MemberwiseClone() as IPlayer;//얕은 복사 //깉은 복사 필요 시, 구현 필요 } } Character c1 = new Character(); c1.data = GetDataFromServer(); //클론을 통한 객체 복사 Character c2 = (Character)c1.Clone();
- 클래스당 한 개의 객체를 갖도록 보장하는 것
- 멀티 쓰레드 상황에서 race condition 발생 가능
- 주의해서 사용
public class GameManager : MonoBehaviour { public static GameManager instance = null; private void Start() { if(instance == null) { instance = this; } else { Destroy(instance); } } }
- 한 클래스의 인터페이스를 사용하고자 하는 다른 인터페이스로 변환하는 디자인 패턴
- 외부 라이브러리(LegacyAudioPlayer) 클래스를 Unity AudioSource 클래스와 호환되는 인터페이스로 변환하는 어댑터 구현
using UnityEngine; //외부 라이브러리에서 제공되는 클래스(Adaptee) public class LegacyAudioPlayer { public void PlaySound(string soundName) { Debug.Log("Legacy Audio Player plays sound: " + soundName); } } //Unity AudioSource와 호환된느 인터페이스(target) public interface IAudioSource { void Play(); } //LegacyAudioPlayer를 IAudioSource로 변환하는 어댑터 클래스(Adapter) public class LegacyAudioAdapter : MonoBehaviour, IAudioSource { private LegacyAudioPlayer legacyAudioPlayer; public LegacyAudioAdapter() { legacyAudioPlayer = new LegacyAudioPlayer(); } public void Play() { legacyAudioPlayer.PlaySound("Adapted Sound"); } } //클라이언트 public class AudioManager : MonoBehaviour { public IAudioSource audioSource; void Start() { //어댑터를 통해 Legacy의 기능을 사용 audioSource.Play(); } }
- 객체들을 트리 구조로 구성해 부분-전체 계층을 표현하는 디자인 패턴
- Unity에서는 GameObject를 트리 구조로 구성해 부모-자식 관계로 나타내고, 이를 통해 그룹화된 오브젝트 하나를 단일 오브젝트로 취급한다.
using UnityEngine; using System.Collections.Generic; //Component: 모든 요소에 대한 공통 인터페이스 정의 public abstract class Component { public string name; public abstract void Operation(); } //Leaf: 복합체 안에 들어가는 개별 객체 public class Leaf : Component { public Leaf(string name) { this.name = name; } public override void Operation() { Debug.Log("Leaf " + name + " is performing operation"); } } //Composite: 복합 객체, Leaf 또는 복합 객체 모두 포함 가능 public class Composite : Component { private List<Component> children = new List<Component>(); public void Add(Component component) { children.Add(component); } public void Remove(Component component) { children.Remove(component); } public override void Operation() { Debug.Log("Composite " + name + " is performing operation"); foreach(var child in chilren) { child.Operation(); } } } //Client: 복합체를 사용하는 클라이언트 public class Client : MonoBehaviour { void Start() { //복합 객체 생성 Composite root = new Composite(); root.name = "Root"; //개별 객체 생성 Leaf leaf1 = new Leaf("Leaf1"); Leaf leaf2 = new Leaf("Leaf2"); //복합 객체에 개별 객체 추가 root.Add(leaf1); root.Add(leaf2); //복합 객체에 다른 복합 객체 추가 Composite subComposite = new Composite(); subComposite.name = "SubCompoeite"; Leaf leaf3 = new Leaf("Leaf3"); subComposite.Add(leaf3); root.Add(subComposite); //모드 객체에 대한 작업 수행 root.Operation(); } }
- 이미 생성된 객체는 공유하는 패턴
- 생성된 객체가 없는 경우에만 객체 생성
- 그 외의 경우 객체 재사용
//유닛 생성을 관리하는 Unity Factory Class. class UnityFactory { static Dictionary<string, Unit> dic = new Dictionary<string, Unit>(); static Unit GetUnit(string name) { if(!dic.ContainsKey(name)) { Unit temp = new Unit(name); dic.Add(name, temp); } return dic[name]; } } //사용법 Unit u1 = UnitFactory.GetUnit("aaa"); Unit u2 = UnitFactory.GetUnit("bbb"); Unit u3 = UnitFactory.GetUnit("aaa"); //u1 = u3(같은 객체 = 공유 데이터를 공유)
- 매서드 호출을 실체화 : 객체로 캡슐화하여 요청자와 수신자 사이 의존성을 제거하는 패턴
- 요청을 객체로 감싸고, 요청을 매개변수화하여 여러 작업을 실행, 취소 및 재실행
using UnityEngine; using System.Collections.Generic; //명령 인터페이스 public interface ICommand { void Execute(); void Undo(); } //이동 명령 public class MoveCommand : ICommand { private GameObject gameObject; private Vector3 startPosition; private Vector3 endPosition; public MoveCommand(GameObject gameObject, Vector3 endPosition) { this.gameObject = gameObject; this.endPosition = endPosition; } public void Excute() { startPosition = gameObject.transform.position; gameObject.transform.position = endPosition; } public void Undo() { gameObject.transform.position = startPosition; } } //회전 명령 public class RotateCommand : ICommand { private GameObject gameObject; private Quaternion startRotation; private Quaternion endRotation; public RotateCommand(GameObject gameObject, Quaternion endRotation) { this.gameObject = gameObject; this.endRotation = endRotation; } public void Excute() { startRotation = gameObject.transform.rotation; gameObject.transform.rotation = endRotation; } public void Undo() { gameObject.transform.rotation = startRotation; } } //명령 실행 관리자 public class CommandManager : MonoBehaviour { private Stack<ICommand> undoStack = new Stack<ICommand>(); public void ExcuteCommand(ICommand command) { command.Execute(); undoStack.Push(command); } public void UndoLaskCommand() { if(undoStack.Count > 0) { ICommand command = undoStack.Pop(); command.Undo(); } } } //클라이언트 사용 예시 public class Client : MonoBehaviour { public GameObject cube; void Start() { CommandManager commandManager = GetComponent<CommandManager>(); ICommand moveCommand = new MoveCommand(cube, new Vector3(3f, 0f, 0f)); ICommand rotateCommand = new RotateCommand(cube, Quaternion.Euler(0f, 90f, 0f)); commandManager.ExecuteCommand(moveCommand); commandManager.ExecuteCommand(rotateCommand); commandManager.UndoLastCommand(); } }
- 객체의 상태 변화를 관찰하는 객체(옵저버) 목록을 가지는 디자인 패턴
- 객체가 상태 변화 시, 옵저버에게 자동으로 알림을 보내 상호작용 수행
- Subject & Observer로 구분
- Subject: 상태를 관찰하는 객체
- 상태가 변경되면 등록된 옵저버에게 알림을 보냄
- Observer: Subject의 상태를 관찰하는 객체
- 알림을 받으면 특정 동작 수행
using UnityEngine; using System; using System.Collections.Generic; //Subject: 상태를 관찰하는 플레이어 클래스 public class Player : MonoBehaviour { //옵저버들을 저장할 리스트 private List<IObserver> observers = new List<IObserver>(); //옵저버 등록 매서드 public void RegisterObserver(IObserver observer) { observers.Add(observer); } //옵저버 해제 매서드 public void UnregisterObserver(IObserver observer) { observers.Remove(observer); } //플레이어 이동 매서드 public void Move() { //이동 로직... //이동 후 옵저버들에게 알림 observer.NotifyObservers(); } //옵저버들에게 알림을 보내는 메서드 private void NotifyObservers() { foreach (var observer in observers) { observer.OnPlayerMoved(); } } } //Observer: 변화를 관찰하는 인터페이스 public interface IObserver { void OnPlayerMoved(); } //Observer 1: 적 클래스 public class Enemy : MonoBehaviour, IObserver { 플레이어 이동 시, 호출되는 메서드 public void OnPlayerMoved() { Debug.Log("Enemy: Player had moved. Updating AI..."); //AI 업데이트 로직... } } public class UIElement : MonoBehaviour, IObserver { //플레이어 이동 시 호출되는 매서드 public void OnPlayerMoved() { Debug.Log("UIElement: Player had moved. Updating UI..."); //UI 업데이트 로직... } } //클라이언트 사용법 public class Client : MonoBehaviour { void Start() { Player player = FindObjectOfType<Player>(); Enemy enemy = FindObjectOfType<Enemy>(); UIElement uiElement = FindObjectOfType<UIElement>(); //옵저버들을 플레이어에 등록 player.RegisterObserver(enemy); player.RegisterObserver(uiElement); //플레이어 이동 시마다 등록된 옵저버들에게 알람 player.Move(); } }
- 상태 패턴은 객체의 내부 상태에 따라 객체의 행동이 달라지는 상황에 사용하는 디자인 패턴
- 게임 캐릭터 및 AI의 상태 관리에 사용
- Context(컨텍스트): 상태를 가지고 있는 객체로, 내부 상태가 변경될 때마다 적절한 행동을 수행
- 게임 캐릭터 및 AI 등이 해당
- State(상태): 상태 패턴에서 상태를 나타내는 인터페이스
- 컨텍스트에서 수행할 동작을 정의
- ConcreteState(구체적 상태): State 인터페이스를 구현한 클래스로 실제 특정 상태에서 실행될 동작을 구현
using UnityEngine; // State 인터페이스 public interface IState { void EnterState(); void UpdateState(); void ExitState(); } // ConcreteState 1: 걷는 상태 public class WalkingState : IState { private readonly Player player; public WalkingState(Player player) { this.player = player; } public void EnterState() { Debug.Log("Player enters Walking state"); } public void UpdateState() { // 걷는 동작 수행 player.Move(); } public void ExitState() { Debug.Log("Player exits Walking state"); } } // ConcreteState 2: 점프 상태 public class JumpingState : IState { private readonly Player player; public JumpingState(Player player) { this.player = player; } public void EnterState() { Debug.Log("Player enters Jumping state"); } public void UpdateState() { // 점프 동작 수행 player.Jump(); } public void ExitState() { Debug.Log("Player exits Jumping state"); } } // Context 클래스: 상태를 관리하는 컨텍스트 public class Player : MonoBehaviour { private IState currentState; public void SetState(IState state) { // 현재 상태를 종료하고 새로운 상태로 변경 if (currentState != null) { currentState.ExitState(); } currentState = state; currentState.EnterState(); } void Update() { // 현재 상태의 동작을 실행 if (currentState != null) { currentState.UpdateState(); } } // 실제 게임 플레이어의 동작 메서드들 public void Move() { Debug.Log("Player is walking..."); // 이동 로직... } public void Jump() { Debug.Log("Player is jumping..."); // 점프 로직... } } // 클라이언트 예시 public class Client : MonoBehaviour { private Player player; void Start() { player = GetComponent<Player>(); // 걷는 상태로 시작 player.SetState(new WalkingState(player)); // 점프 상태로 변경 player.SetState(new JumpingState(player)); } }
- Enum 클래스와 상태패턴은 병행됨
- 클래스를 별도로 생성할 이유가 없어짐
- 단 유지보수 면에서 코드의 독립성이 떨어짐
using UnityEngine; // 열거형으로 정의된 상태 public enum PlayerState { Walking, Jumping, Idle } // 컨텍스트 클래스: 상태를 관리하는 플레이어 클래스 public class Player : MonoBehaviour { private PlayerState currentState; void Start() { // 초기 상태 설정 currentState = PlayerState.Idle; } void Update() { // 현재 상태에 따른 동작 실행 switch (currentState) { case PlayerState.Walking: Move(); break; case PlayerState.Jumping: Jump(); break; case PlayerState.Idle: // Idle 상태에서의 동작 (예: 대기) break; default: break; } } // 상태 변경 메서드 public void ChangeState(PlayerState newState) { currentState = newState; } // 실제 게임 플레이어의 동작 메서드들 private void Move() { Debug.Log("Player is walking..."); // 이동 로직... } private void Jump() { Debug.Log("Player is jumping..."); // 점프 로직... } } // 클라이언트 예시 public class Client : MonoBehaviour { private Player player; void Start() { player = GetComponent<Player>(); // 걷는 상태로 변경 player.ChangeState(PlayerState.Walking); // 점프 상태로 변경 player.ChangeState(PlayerState.Jumping); } }
- 여러 알고리즘을 하나의 추상적인 접근점(인터페이스)를 만들어 접근점에서 알고리즘이 서로 교환 가능하도록 하는 디자인 패턴
- 객체의 동작을 클래스로 캡슐화하고, 이를 런타임에 변경하도록 해주는 디자인 패턴
- 게임 캐릭터의 무기 교체 및 전략 선택 등에 사용
class Weapon { IWeapon _weapon; void SetWeapon (IWeapon weapon) {_weapon = weapon; } void Shoot() { weapon.Shoot(); } }; class Bullet : Weapon { void Shoot() { Print("Bullet"); } }; class Missile : Weapon { void Shoot() { Print("Missile"); } }; class Arrow : Weapon { void Shoot() { Print("Arrow"); } }; class WeaponManager { Weaopn _w; void ChangeBullet() { _w.SetWeapon(new Bullet()); } void ChangeMissile() { _w.SetWeapon(new Missile()); } void ChangeArrow() { _w.SetWeapon(new Arrow()); } void Fire() { _w.Shoot(); } };
- 메모리 공간에 사용할 객체를 미리 만들어 두고, 필요할 때 가져다 사용하는 패턴
- 생성 & 삭제 [X]
- 생성 -> 활성화 & 비활성화[O]
- 최초 생성 객체 수를 넘어가는 경우 중간에 확장해서 생성할 수 있다.
- 플레이웨이트와 비슷하지만, 플라이웨이트는 하나의 공유 데이터를 여러 객체가 공유해서 사용하여 메모리 중복을 피하는 것
- 오브젝트 풀은 이미 만들어진 여러 객체를 필요할 때 사용하능한 객체를 가져다 사용하는 방식으로 1 : 1로 사용하고, 다 쓰면 반환하는 형식을 가진다.
- 즉, 플라이웨이트처럼 여러 곳에서 동시에 사용하려는 목적이 아님
//게임 필드에 최대 몬스터 수 만큼 메모리 풀에 만들어두고, 비활성화 해줌 Monster[] monsterPool = new Monster[FIELD_MONSTER_MAX_SIZE]; for(int i = 0; i < FIELD_MONSTER_MAX_SIZE; ++i) { monsterPool[i] = Instantiate(monster); monsterPool[i].SetActive(false); } //객체가 필요해 사용할때, 모두 활성화 되어있다면 객체 추가 생성 및 continue를 수행해야 함(자율 코드 작성) for(int i = 0; i < FIELD_MONSTER_MAX_SIZE; ++i) { if(monsterPool[i].GetActive) continue; monsterPool[i].position = GetRandomPositionInField(); monsterPool[i].SetActive(true); break; }