Charles Hache(Unity 인증 전문 프로그래머)가 쓴 "모든 유니티 게임 개발자가 알아야할 상위 7가지 디자인 패턴"
[원문]
[번역본_1]
[번역본_2]
순서
1. Singleton Pattern (싱글톤 패턴)
2. Observer Pattern (관찰자 패턴)
3. Command Pattern (명령 패턴)
4. Strategy Pattern (전략 패턴)
5. Factory Pattern (공장 패턴)
6. Decorator Pattern
7. State Pattern (상태 패턴)
싱글톤 패턴은 Class에 Instance가 하나만 있도록 하고, 해당 Instance에 대한 전역 엑세스 지점을 제공한다.
이 패턴은 단일 엑세스 포인트와 영구 상태가 필요한 게임 시스템 및 서비스를 관리하는 데 특히 유용하다.
Singleton은 Game Manager나 Audio Manager와 같이 특정 클래스가 게임의 전체 Life동안 하나의 Instacne만 갖도록 해야 할 때 유용한 패턴이다.
제어를 중앙 집중화 하여 리소스, 설정, 게임 상태를 쉽게 관리할 수 있다.
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
private void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
}
추가 자료
옵저거 패턴은 객체 간의 일대다(One To Many) 종속성을 정의하는 행동 디자인 패턴이다.
이 패턴을 사용하면 주체(Subject)라고 하는 객체가 관찰자(Observer)라고 하는 다른 객체에게 상태 변화를 알릴 수 있다.
옵저버 패턴은 여러 객체가 긴밀하게 결합되지 않은 상태에서 다른 객체의 상태 변화에 반응해야 하는 상황에 이상적.
이 패턴은 모듈식 분리형 아키텍쳐를 구축하여 코드를 더욱 유연하고 쉽게 유지 관리할 수 있게된다.
public interface IObserver
{
void OnNotify();
}
public class Subject : 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 NotifyObservers()
{
foreach (IObserver observer in _observers)
{
observer.OnNotify();
}
}
}
추가 자료
커맨드 패턴은 요청을 객체로 캡슐화하여 요청하는 객체와 요청을 실행하는 객체를 분리할 수 있는 디자인 패턴이다.
이는 느슨한 결합을 촉진하며 실행 취소 / 다시 실행 기능이나 유연한 입력 시스템을 구현하는 데 특히 유용하다.
커맨드 패턴은 사용자 입력, AI 결정, 실행 취소/다시 실행 시스템과 같은 복잡한 게임 상호작용을 관리하는 데 유용하다.
이러한 동작을 커맨드 객체로 캡슐화하면 코드가 더욱 모듈화되고 유지 관리 및 확장이 쉬워진다.
// Command 인터페이스
public interface ICommand
{
void Execute(); // 명령 실행
void Undo(); // 명령 취소
}
// Receiver: 실제 행동을 수행하는 객체
public class PlayerController : MonoBehaviour
{
public void Move(Vector3 direction)
{
transform.position += direction;
Debug.Log($"Player moved to: {transform.position}");
}
public void MoveBack(Vector3 direction)
{
transform.position -= direction;
Debug.Log($"Player moved back to: {transform.position}");
}
}
// ConcreteCommand: 특정 행동을 구현한 명령
public class MoveCommand : ICommand
{
private PlayerController player; // Receiver
private Vector3 direction;
public MoveCommand(PlayerController player, Vector3 direction)
{
this.player = player;
this.direction = direction;
}
public void Execute()
{
player.Move(direction);
}
public void Undo()
{
player.MoveBack(direction);
}
}
public class CommandInvoker
{
private Stack<ICommand> commandHistory = new Stack<ICommand>(); // 실행된 명령 기록
public void ExecuteCommand(ICommand command)
{
command.Execute();
commandHistory.Push(command);
}
public void UndoCommand()
{
if (commandHistory.Count > 0)
{
ICommand lastCommand = commandHistory.Pop();
lastCommand.Undo();
}
}
}
using UnityEngine;
public class GameManager : MonoBehaviour
{
public PlayerController player;
private CommandInvoker invoker;
private void Start()
{
invoker = new CommandInvoker();
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.W))
{
ICommand moveForward = new MoveCommand(player, Vector3.forward);
invoker.ExecuteCommand(moveForward);
}
if (Input.GetKeyDown(KeyCode.S))
{
ICommand moveBackward = new MoveCommand(player, Vector3.back);
invoker.ExecuteCommand(moveBackward);
}
if (Input.GetKeyDown(KeyCode.Z)) // Undo
{
invoker.UndoCommand();
}
}
}
추가 자료
전략 패턴은 알고리즘 군(Family)을 정의하고 각 알고리즘을 캡슐화하여 상호 교환할 수 있도록 하는 디자인 패턴이다.
이를 통해 런타임에 알고리즘을 선택할 수 있으므로 코드의 유연성과 적응성을 높일 수 있다.
(A Family of Algorithms -> 비슷한 알고리즘끼리 묶는다)
전략 패턴은 특정 작업에 대해 다양한 알고리즘이나 동작을 구현해야 하고, 런타임 중에 쉽게 교체하려는 상황에 이상적이다.
이 패턴을 사용하면 새로운 동작을 더 쉽게 추가할 수있고, 코드를 깔끔하고 유지 관리하기 쉽게 유지할 수 있다.
public interface IMovementStrategy
{
void Move(Character character);
}
public class WalkMovement : IMovementStrategy
{
public void Move(Character character)
{
// Walk movement logic
}
}
public class FlyMovement : IMovementStrategy
{
public void Move(Character character)
{
// Fly movement logic
}
}
public class Character : MonoBehaviour
{
private IMovementStrategy _movementStrategy;
public void SetMovementStrategy(IMovementStrategy strategy)
{
_movementStrategy = strategy;
}
private void Update()
{
_movementStrategy.Move(this);
}
}
추가 자료
팩토리 패턴은 슈퍼 클래스(Super Class)에서 객체를 생성하기 위한 Interface를 제공하여 SubClass가 Instance화할 클래스를 결정할 수 있도록 하는 생성 디자인 패턴이다.
이 패턴은 관련 클래스 집합이 있고, 생성할 클래스를 명확히 기입하지 않고, 해당 클래스의 인스턴스를 생성하려는 경우에 유용함
SuperClass는 부모 클래스라고 인지하고 있으면 된다. 다른 클래스가 파생되는 클래스
팩토리 패턴은 공통 Interface나 Base Class를 공유하는 적이나 아이템과 같은 다양한 유형의 객체를 만들어야 할 때 특히 유용하다.
객체 생성 프로세스를 캡슐화하여 코드를 더 쉽게 유지 관리하고 확장할 수 있도록 도와줌.
public abstract class Enemy
{
// Common enemy behavior and properties
}
public class Zombie : Enemy
{
// Zombie-specific behavior and properties
}
public class Ghost : Enemy
{
// Ghost-specific behavior and properties
}
public static class EnemyFactory
{
public static Enemy CreateEnemy(string type)
{
switch (type)
{
case "Zombie":
return new Zombie();
case "Ghost":
return new Ghost();
default:
throw new ArgumentException("Invalid enemy type");
}
}
}
public class EnemySpawner : MonoBehaviour
{
private void SpawnEnemy(string type)
{
Enemy enemy = EnemyFactory.CreateEnemy(type);
// Spawn and initialize the enemy
}
}
추가 자료
데코레이터 패턴은 구조를 수정하지 않고도 객체에 새로운 책임을 동적으로 부여할 수 있는 구조적 디자인 패턴이다.
이 패턴은 같은 클래스의 다른 인스턴스에 영향을 주지 않고 객체에 기능을 추가해야할 때 유용하다.
데코레이터 패턴은 원래 구조를 변경하거나 다른 Instance에 영향을 주지 않고 객체의 기능을 확장하고자 할 때 유용하다.
특히 런타임에 객체에 기능을 추가하고 코드 재사용성을 높이는 데 유용하다.
public abstract class Weapon
{
public abstract int GetDamage();
}
public class Sword : Weapon
{
public override int GetDamage()
{
return 10;
}
}
public abstract class WeaponDecorator : Weapon
{
protected Weapon _weapon;
public WeaponDecorator(Weapon weapon)
{
_weapon = weapon;
}
public override int GetDamage()
{
return _weapon.GetDamage();
}
}
public class FireEnchantment : WeaponDecorator
{
public FireEnchantment(Weapon weapon) : base(weapon) { }
public override int GetDamage()
{
return base.GetDamage() + 5;
}
}
추가 자료
상태 패턴은 내부 상태가 변경될 때 객체의 동작을 변경할 수 있는 디자인 패턴이다.
이 패턴은 FSM을 깔끔하고 유지 관리하기 쉬운 방식으로 구현하는데 특히 유용하다
-> 상태를 객체화하여 필요에 따라 다르게 구현
스테이트 패턴은 캐릭터 AI나 게임 상태 전환과 같이 복잡한 상태 종속적 동작을 관리할 때 특히 유용하다.
각 상태를 별도의 객체로 캡슐화하면 코드가 더 모듈화되고 유지 관리가 쉬워지며 새로운 상태로 확장하기가 더 쉬워진다.
public interface IState
{
void Enter();
void Execute();
void Exit();
}
public class IdleState : IState
{
public void Enter() { /* Enter idle state logic */ }
public void Execute() { /* Execute idle state logic */ }
public void Exit() { /* Exit idle state logic */ }
}
public class MovingState : IState
{
public void Enter() { /* Enter moving state logic */ }
public void Execute() { /* Execute moving state logic */ }
public void Exit() { /* Exit moving state logic */ }
}
public class StateMachine
{
private IState _currentState;
public void ChangeState(IState newState)
{
_currentState?.Exit();
_currentState = newState;
_currentState.Enter();
}
public void Update()
{
_currentState.Execute();
}
}
추가 자료