2024. 2. 23. 21:36ㆍ내일배움캠프
2월 23일
오늘은 프로그래밍 디자인 패턴에 대해서 정리해보도록 하자
디자인 패턴은 모듈의 세분화된 역할이나 모듈들 간의
인터페이스 구현 방식을 설계할때 참조할 수 있는 해결 방식을 말한다.
패턴을 통해 설계문제라던지 문제에 대한 해결방법 해결방법을 언제 적용할지 결과는 어떻게 되는지를 알 수 있다.
옵저버 패턴
스타크래프트의 옵저버를 생각하자
옵저버 패턴은 객체 간의 일대다 종속성을 정의하는 디자인 패턴이다.
주체의 상태가 변경되면 관찰자 객체들에게 알리고 자동으로 상태를 업데이트 할 수 있다.
지난번에 말했던 Action,Func 다 옵저버 패턴이라고 할 수 있을것이다.
using UnityEngine;
using System;
using System.Collections.Generic;
// 주체 클래스
public class PlayerHealth : MonoBehaviour {
public event Action<int> OnHealthChanged; // 체력 변화 이벤트
private int currentHealth = 100;
public void TakeDamage(int damage) {
currentHealth -= damage;
// 체력 변화 이벤트 발생
OnHealthChanged?.Invoke(currentHealth);
}
public void Heal(int amount) {
currentHealth += amount;
// 체력 변화 이벤트 발생
OnHealthChanged?.Invoke(currentHealth);
}
}
// 관찰자 클래스
public class HealthBar : MonoBehaviour {
public PlayerHealth playerHealth; // 주체
public RectTransform healthBarFill; // 체력바의 채워지는 부분
private void Start() {
// 주체의 체력 변화 이벤트에 대한 구독
playerHealth.OnHealthChanged += UpdateHealthBar;
// 초기화
UpdateHealthBar(playerHealth.currentHealth);
}
private void UpdateHealthBar(int currentHealth) {
float fillAmount = (float)currentHealth / 100f;
healthBarFill.localScale = new Vector3(fillAmount, 1f, 1f);
}
}
// 테스트용 클라이언트 코드
public class GameManager : MonoBehaviour {
public PlayerHealth playerHealth; // 주체
private void Update() {
// 체력 테스트: 매 프레임마다 플레이어 체력 감소
if (Input.GetKeyDown(KeyCode.Space)) {
playerHealth.TakeDamage(10);
}
}
}
싱글톤 패턴
싱글톤 패턴은 많이 사용해서 익숙하다
어떤 클래스가 오직 하나의 인스턴스만 갖도록 보장함과 전역적인 접근지점을 제공하는 디자인 패턴이다.
게임 매니저 혹은 사운드 매니저 같은 매니저 급의 클래스 등을 싱글톤으로 구현하면 굉장히 편리하게
접근하여 사용할 수 있다는 장점도 있지만 의존성이 높아진다거나 아무래도 static을 이용한 패턴이기 때문에
런타임 동안 메모리를 계속 차지하고 있기에 많은 클래스를 싱글톤 패턴으로 디자인 하게 되면 성능 저하의 원인이
된다. 그 외 여러 단점들도 존재하지만 잘 사용한다면 단점을 모두 상쇄하고도 남을만큼의 편의성을 제공해주기
때문에 꼭 이 클래스를 싱글톤으로 디자인해야 하는 이유를 곰곰히 생각해보고 활용하도록 하자.
using UnityEngine;
public class GameManager : MonoBehaviour {
private static GameManager instance;
// 다른 스크립트에서 GameManager.Instance로 접근할 수 있도록 함
public static GameManager Instance {
get {
if (instance == null) {
instance = FindObjectOfType<GameManager>();
if (instance == null) {
GameObject obj = new GameObject();
obj.name = typeof(GameManager).Name;
instance = obj.AddComponent<GameManager>();
}
}
return instance;
}
}
// 게임 매니저의 기능들...
public void StartGame() {
Debug.Log("Game Started!");
}
public void EndGame() {
Debug.Log("Game Ended!");
}
private void Awake() {
// 여러 개의 GameManager 인스턴스가 생성되는 것을 방지
if (instance != null && instance != this) {
Destroy(gameObject);
} else {
instance = this;
DontDestroyOnLoad(gameObject);
}
}
}
팩토리 메서드 패턴
팩토리 메서드 패턴은 객체 생성을 서브 클래스에 위임하여 객체의 유형을 결정하는 디자인 패턴이다.
클래스를 생성하고 사용하는 로직을 분리하여 결합도를 낮추고 유지보수가 편리하고
팩토리 메서드를 통해 객체의 생성 후 공통적으로 할 일을 수행하도록 지정해 줄 수 있다 등등 여러 장점이 있지만
내가 원하는 구현체 마다 팩토리 객체를 모두 구현해 주어야 하기 떄문에 옵션이 늘어갈 때마다 팩토리 클래스가 증
가하여 수십개의 서브 클래스를 가지게 되고 그 결과 코드의 복잡성이 증가한다는 단점이 있다.
using UnityEngine;
// 추상 생성자 클래스
public abstract class EnemyFactory {
public abstract Enemy CreateEnemy();
}
// 구체적인 생성자 클래스
public class OrcFactory : EnemyFactory {
public override Enemy CreateEnemy() {
return new OrcEnemy();
}
}
public class GoblinFactory : EnemyFactory {
public override Enemy CreateEnemy() {
return new GoblinEnemy();
}
}
// 추상 제품 클래스
public abstract class Enemy {
public abstract void Attack();
}
// 구체적인 제품 클래스
public class OrcEnemy : Enemy {
public override void Attack() {
Debug.Log("Orc attacks with a club!");
}
}
public class GoblinEnemy : Enemy {
public override void Attack() {
Debug.Log("Goblin attacks with a dagger!");
}
}
// 클라이언트 코드
public class Client : MonoBehaviour {
private EnemyFactory factory;
void Start() {
// 원하는 적 캐릭터를 생성
factory = new OrcFactory();
Enemy enemy = factory.CreateEnemy();
enemy.Attack();
factory = new GoblinFactory();
enemy = factory.CreateEnemy();
enemy.Attack();
}
}
커맨드 패턴
커맨드 패턴은 요청을 객체로 캡슐화하여 매개변수화하고
메서드 호출, 연산 수행 또는 작업을 지연할 수 있는 디자인 패턴이다.
명령을 저장하거나 여러개의 커맨드를 조합하여 복잡한 명령을 생성하거나 취소 실행 기능을 제공하여
유연하고 확장성있는 애플리케이션을 만드는데 유용한 방법이나
명령이 많이 추가될수록 구조가 복잡해지고 클래스의 수가 증가할 수 있고
커맨드 객체는 작업을 수행하는 비용이 높을 수 있다는 단점이 있다
using UnityEngine;
using System;
// Command 인터페이스
public interface ICommand {
void Execute();
}
// 구체적인 커맨드 클래스
public class JumpCommand : ICommand {
private Player player;
public JumpCommand(Player player) {
this.player = player;
}
public void Execute() {
player.Jump();
}
}
// 수신자 클래스
public class Player : MonoBehaviour {
public void Jump() {
Debug.Log("Player jumps!");
}
}
// 클라이언트 클래스
public class InputHandler : MonoBehaviour {
private ICommand command;
public void SetCommand(ICommand command) {
this.command = command;
}
void Update() {
if (Input.GetKeyDown(KeyCode.Space) && command != null) {
command.Execute();
}
}
}
// 게임 매니저
public class GameManager : MonoBehaviour {
private Player player;
private InputHandler inputHandler;
void Start() {
player = FindObjectOfType<Player>();
inputHandler = FindObjectOfType<InputHandler>();
// JumpCommand를 할당하여 키보드 입력에 반응하도록 함
inputHandler.SetCommand(new JumpCommand(player));
}
}
컴포지트 패턴
컴포지트 패턴은 객체를 트리 구조로 구성하여 단일 객체와 복합 객체를 동일하게 다룰 수 있게 해주는 디자인 패턴
객체들 사이의 계층 구조를 표현하여 복잡한 자료구조를 다룰 수 있다 또한
객체들 간의 관계를 동적으로 변경할 수도 있다는 장점이 있지만 복잡한 구조를 가질수록 구현이 복잡해지고
일부 구현에서는 모든 메서드를 구현해야 한다는 단점이 있다.
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 + " Operation");
}
}
// Composite 클래스
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 + " Operation");
foreach (Component child in children) {
child.Operation();
}
}
}
// Client 클래스
public class Client : MonoBehaviour {
void Start() {
Composite root = new Composite();
root.name = "Root";
Composite branch1 = new Composite();
branch1.name = "Branch 1";
Composite branch2 = new Composite();
branch2.name = "Branch 2";
Leaf leaf1 = new Leaf("Leaf 1");
Leaf leaf2 = new Leaf("Leaf 2");
Leaf leaf3 = new Leaf("Leaf 3");
root.Add(branch1);
root.Add(branch2);
branch1.Add(leaf1);
branch1.Add(leaf2);
branch2.Add(leaf3);
root.Operation();
}
}
- 게임 오브젝트의 계층 구조 관리: 게임에서는 종종 오브젝트들의 계층 구조가 복잡하게 구성될 수 있다. 예를 들어, RPG 게임에서는 캐릭터가 파티를 형성하고, 각 파티는 다시 미션에 참여할 수 있다. 이런 경우에 컴포지트 패턴을 사용하여 캐릭터, 파티, 미션 등을 각각 Leaf와 Composite 객체로 나타낼 수 있다.
- UI 구성 요소: 게임 UI에서도 컴포지트 패턴을 활용할 수 있다. 예를 들어, UI 창은 여러 개의 UI 요소로 구성될 수 있다. 이러한 UI 요소들은 단일한 UI 창이 되는 Composite로 구성될 수 있다. 각 UI 요소는 Leaf가 될 수 있다.
- 스킬 트리 또는 아이템 트리: 게임에서 스킬 트리나 아이템 트리는 종종 여러 가지 선택지와 분기를 가지고 있다. 각 선택지는 Leaf가 될 수 있고, 여러 선택지가 결합되어 스킬 트리 전체를 형성하는 Composite가 될 수 있다.
- AI 동작 트리: 게임에서 NPC의 행동은 종종 복잡한 트리 구조를 가진다. 각 행동은 Leaf가 되고, 이러한 행동들의 조합으로 NPC의 전체 행동 패턴을 결정하는 Composite가 될 수 있다.
스테이트 패턴
얼마전에 골머리를 앓았던 스테이트 패턴이다 스테이트 패턴은 객체의 내부 상태에 따라
객체의 행동을 변경할 수 있게 해주는 디자인 패턴이다
장점으로 말하자면 여러 행동, 동작 등을 구현해야 할때 상태 객체만 수정하면 되서 추가 삭제 수정 같이 유지 보수가
굉장히 편리하다는 장점이 있다.
단점은 내가 표현해야할 상태 행동이 많아지게 되면 객체가 증가하여 관리해야할 클래스의 수가 증가한다.
using UnityEngine;
// State 인터페이스
public interface IState {
void HandleInput();
void UpdateState();
}
// 구체적인 상태 클래스
public class IdleState : IState {
private Player player;
public IdleState(Player player) {
this.player = player;
}
public void HandleInput() {
if (Input.GetKeyDown(KeyCode.Space)) {
player.ChangeState(new JumpingState(player));
}
}
public void UpdateState() {
Debug.Log("Idle State");
}
}
public class JumpingState : IState {
private Player player;
public JumpingState(Player player) {
this.player = player;
}
public void HandleInput() {
// 점프 중에는 입력을 받지 않음
}
public void UpdateState() {
Debug.Log("Jumping State");
// 점프 동작 처리...
}
}
// Context 클래스
public class Player : MonoBehaviour {
private IState currentState;
void Start() {
currentState = new IdleState(this);
}
void Update() {
currentState.HandleInput();
currentState.UpdateState();
}
public void ChangeState(IState newState) {
currentState = newState;
}
}
프로토타입 패턴
프로토 타입 패턴은 객체를 복제하여 새로운 객체를 생성하는 디자인 패턴이다.
객체 생성 과정이 복잡하거나 비용이 많이 들어갈때 유용하게 쓰인다.
장점이라고 하면 복제하는 과정이 새로운걸 만들어내는 것보다 비용적으로 효율적일 수 있다는 장점이 있지만
객체를 복제하는 과정 자체가 복잡할 수 있다 즉 구현 자체가 힘들어 질 수 있다는 단점이 있다.
using UnityEngine;
// 프로토 타입 클래스
public abstract class EnemyPrototype : MonoBehaviour {
public abstract EnemyPrototype Clone();
}
// 구체적인 프로토 타입 클래스
public class OrcPrototype : EnemyPrototype {
public override EnemyPrototype Clone() {
return Instantiate(this);
}
}
public class GoblinPrototype : EnemyPrototype {
public override EnemyPrototype Clone() {
return Instantiate(this);
}
}
// 클라이언트 클래스
public class GameManager : MonoBehaviour {
public OrcPrototype orcPrototype;
public GoblinPrototype goblinPrototype;
void Start() {
// 적 캐릭터 복제
EnemyPrototype orc1 = orcPrototype.Clone();
orc1.transform.position = new Vector3(0, 0, 0);
EnemyPrototype orc2 = orcPrototype.Clone();
orc2.transform.position = new Vector3(2, 0, 0);
EnemyPrototype goblin1 = goblinPrototype.Clone();
goblin1.transform.position = new Vector3(-2, 0, 0);
EnemyPrototype goblin2 = goblinPrototype.Clone();
goblin2.transform.position = new Vector3(-4, 0, 0);
}
}
- 적 캐릭터나 적 AI의 복제: 게임에서 여러 개의 적 캐릭터나 적 AI가 필요한 경우, 각각을 일일이 새로 작성하는 대신 프로토 타입 패턴을 사용하여 기존의 적을 복제할 수 있다. 이렇게 하면 적의 속성과 행동을 쉽게 재사용하고 조정할 수 있다.
- 아이템의 복제: 게임에서 플레이어가 획득할 수 있는 다양한 아이템이 있는 경우, 각각의 아이템을 프로토 타입으로 정의하고 필요할 때마다 복제하여 사용할 수 있다. 이를 통해 플레이어에게 다양한 아이템을 제공할 수 있다.
- 타일맵 또는 레벨의 복제: 게임에서 유사한 패턴이나 구조를 갖는 다수의 타일맵이나 레벨이 필요한 경우, 기존의 타일맵이나 레벨을 프로토 타입으로 정의하고 필요에 따라 복제하여 사용할 수 있다. 이를 통해 빠르게 새로운 레벨을 만들 수 있다.
- 파티클 시스템의 복제: 게임에서 파티클 시스템을 사용하여 효과를 만드는 경우, 각각의 효과를 프로토 타입으로 정의하고 필요에 따라 복제하여 사용할 수 있다. 이를 통해 다양한 효과를 만들고 조합할 수 있다.
- 유저 인터페이스의 복제: 게임에서 유사한 디자인이나 기능을 갖는 여러 개의 UI 요소가 필요한 경우, 기존의 UI 요소를 프로토 타입으로 정의하고 필요에 따라 복제하여 사용할 수 있다. 이를 통해 일관된 UI를 쉽게 만들고 관리할 수 있다.
플라이웨이트 패턴
플라이웨이트 패턴은 많은 수의 유사한 객체를 효율적으로 공유하여 메모리 사용을 최적화하는 디자인 패턴이다.
많은 객체(FPS 게임의 총알 등)를 만들때 성능을 향상시킬수 있고 스테이트 패턴과 쉽게 결합이 가능하다
하지만 특정 인스턴스의 공유 컴포넌트를 다르게 행동하게 하는게 불가능하고 공통된 자원이기에
공통된 부분을 싱글톤처럼 사용하고 서로 다른 일부 객체는 다르게 사용해야 한다.
using UnityEngine;
using System.Collections.Generic;
// Flyweight 팩토리
public class MeshFactory {
private Dictionary<string, Mesh> sharedMeshes = new Dictionary<string, Mesh>();
public Mesh GetMesh(string key) {
if (!sharedMeshes.ContainsKey(key)) {
sharedMeshes[key] = Resources.Load<Mesh>(key);
}
return sharedMeshes[key];
}
}
// 클라이언트 클래스
public class Client : MonoBehaviour {
private MeshFactory meshFactory;
void Start() {
meshFactory = new MeshFactory();
// 여러 게임 오브젝트가 같은 메쉬를 사용할 때
Mesh mesh = meshFactory.GetMesh("CubeMesh");
GameObject cube1 = GameObject.CreatePrimitive(PrimitiveType.Cube);
cube1.GetComponent<MeshFilter>().mesh = mesh;
GameObject cube2 = GameObject.CreatePrimitive(PrimitiveType.Cube);
cube2.GetComponent<MeshFilter>().mesh = mesh;
}
}
using UnityEngine;
using System.Collections.Generic;
// 플라이웨이트 인터페이스
public interface IBullet {
void Shoot(Vector3 position, Quaternion rotation);
}
// 구체적인 플라이웨이트 클래스
public class Bullet : IBullet {
private GameObject bulletPrefab;
public Bullet(GameObject prefab) {
bulletPrefab = prefab;
}
public void Shoot(Vector3 position, Quaternion rotation) {
GameObject bullet = Object.Instantiate(bulletPrefab, position, rotation);
// 총알 초기화
bullet.GetComponent<Rigidbody>().velocity = bullet.transform.forward * 10f;
}
}
// 플라이웨이트 팩토리
public class BulletFactory {
private Dictionary<string, IBullet> bullets = new Dictionary<string, IBullet>();
public BulletFactory(GameObject bulletPrefab) {
bullets["NormalBullet"] = new Bullet(bulletPrefab);
// 필요한 경우 다른 유형의 총알을 추가로 등록할 수 있음
}
public IBullet GetBullet(string type) {
if (bullets.ContainsKey(type)) {
return bullets[type];
} else {
Debug.LogError("Bullet type not found: " + type);
return null;
}
}
}
// 클라이언트 클래스
public class GameManager : MonoBehaviour {
public GameObject bulletPrefab;
private BulletFactory bulletFactory;
void Start() {
bulletFactory = new BulletFactory(bulletPrefab);
}
void Update() {
// 키 입력 등에 따라 총알 생성
if (Input.GetKeyDown(KeyCode.Space)) {
// 총알 발사
IBullet bullet = bulletFactory.GetBullet("NormalBullet");
bullet.Shoot(transform.position, transform.rotation);
}
}
}
오늘은 게임 프로그래밍에 유용한 디자인 패턴에 대해 간단한 예제 코드와 함께 살펴보았다.
실제로 기능을 알고 사용하는 경우도 모르고 사용했지만? 지금 패턴에 대해서 알고 나니깐
내가 짠 코드가 어떠한 디자인 패턴의 형태와 유사한 경우
혹은 정말 개판처럼 짜놔서 어떤 디자인패턴을 가지고 구현했으면 좋았을 코드 같은 것들이 보이는것 같다.
모든 디자인 패턴에는 장점과 함께 단점도 존재하기 때문에 특정 디자인 패턴을 선택해서 구현하려고 하는 경우
꼭 장점과 단점을 따져보아서 내가 정말 이렇게 구현해야 할까?를 염두해 두고 작업을 해야한다는 것도 느꼈다.
이미 터져버린 머리를 부여잡고
2월 23일 TIL은 여기서 마치도록 하겠다.
'내일배움캠프' 카테고리의 다른 글
240227-TIL (0) | 2024.02.27 |
---|---|
240226-TIL (0) | 2024.02.26 |
240221-TIL (0) | 2024.02.21 |
240220-TIL (0) | 2024.02.20 |
240219-TIL (0) | 2024.02.19 |