Unity | 어댑터, 싱글톤, 옵저버 패턴

Clean·2025년 4월 21일

Unity

목록 보기
9/24
post-thumbnail

🧩어댑터 패턴 (Adapter Pattern)

기존 코드를 수정하지 않고 다른 코드와 호환되게 해주는 중간 다리

어댑터 패턴은 기존 코드의 동작을 유지하면서,

다른 객체와 상호작용할 수 있도록 중간에서 기능을 변환해주는 구조다.

예를 들면,

포탄은 몬스터에게 대미지를 주는 역할을 하지만,

어댑터 패턴을 활용하면 같은 포탄이 타워에 맞았을 때는

몬스터들이 추격을 시작하거나 멈추는 행동으로 작동하게 만들 수 있다.


public interface IAttackable
{
    public void Hit(GameObject gameObject, int damage);
}

IAttackable 을 상호작용을 할 클래스에 상속하면 된다.

현재 MonsterTower 클래스에 상속했다.


void OnTriggerEnter(Collider other)
	{
		// 다른 기능들...

		IAttackable attackable = other.GetComponent<IAttackable>();

		if (attackable != null)
		{
			attackable.Hit(gameObject, damage);
		}
	}

총알이 OnTriggerEnter 이벤트함수가 작동하면 닿은 객체에

GetComponent를 통해 IAttackable 인터페이스가 상속된 컴포넌트(클래스) 를 가져올 수 있다.


상속된 컴포넌트가 있으면 예외처리를 통해 인터페이스의 Hit 함수를 실행하는데

// Monster Class
public void Hit(GameObject gameObject, int damage)
{
	Debug.Log("대미지입음");
	hp -= damage;

	if (hp <= 0)
	{
		Debug.Log("몹주금");
		pool.Release(this);
	}
}

포탄이 몬스터 클래스에 닿으면 포탄의 대미지만큼 체력이 감소되고,


// Tower Class
public void Hit(GameObject gameObject, int damage)
{
	if (monsterManager == null) return;
	
	bool nextState = !monsterManager.isChase;
	monsterManager.SetChase(nextState);

	if (nextState)
		meshRen.material = states[0];
	else
		meshRen.material = states[1];
}

포탄이 타워 클래스에 닿으면 MonsterManager 와 연동해서 몬스터들이 플레이어를 추격한다.


즉, 총알은 항상 같은 방식으로 작동하지만,

타워나 몬스터에서 각각의 코드대로 기능을 하게 된다.

이렇게 하면 기존 코드를 바꾸지 않고도 다양한 기능을 구현할 수 있다.


🌞싱글톤 패턴 (Singleton Pattern)

하나의 인스턴스를 전역에서 공유하고, 중복 생성을 방지하는 구조

싱글톤 패턴은 게임 내에서 하나만 존재하고,

여러 스크립트에서 접근해야 하기 때문에 전역 접근이 가능해야 한다.

주로 게임 매니저같은 게임을 관리하는 매니저를 만들 때 사용하며,

게임에 사용하는 데이터들을 보관할 수도 있다.


public class GameManager : MonoBehaviour
{
	public static GameManager Instance { get; private set; }

	void Awake()
	{
		if (Instance != null && Instance != this)
		{
			Destroy(gameObject);
			return;
		}

		Instance = this;
		DontDestroyOnLoad(gameObject);
	}
}

Instance 정적(static) 변수를 통해

어디서든 GameManager.Instance 로 접근 가능하며,

Awake에서 중복된 인스턴스를 제거하여 단일 인스턴스를 보장한다.

DontDestroyOnLoad()는 씬이 전환돼도 오브젝트가 사라지지 않도록 해준다.


단점은 모든 코드에서 접근이 가능하므로

데이터가 의도치 않게 변경될 가능성이 있고,

여러 객체가 이 인스턴스에 의존하게 되면 구조가 복잡해질 수 있다.

또한 너무 많은 싱글톤을 만들면 정적 메모리 사용량 증가하므로

필요한 최소한의 객체만 싱글톤으로 사용하는 것이 좋다.


📡옵저버 패턴 (Observer Pattern)

어떤 객체의 상태 변화가 있을 때, 다른 객체들이 자동으로 반응할 수 있도록 해주는 구조

전에 배웠던 Unity Event, Delegate 들이 옵저버 패턴이다.


public class Field : MonoBehaviour
{
	public UnityEvent chaseEvent;
	public UnityEvent stopEvent;

	[SerializeField] GameObject displayer;
	[SerializeField] Material[] states;

	void OnTriggerEnter(Collider other)
	{
		if (other.gameObject.tag == "Player")
		{
			Debug.Log("몹들이 추격합니다");
			SetState(true);
		}
	}

	void OnTriggerStay(Collider other)
	{
		if (other.gameObject.tag == "Player")
		{
			SetState(true);
		}
	}

	void OnTriggerExit(Collider other)
	{
		if (other.gameObject.tag == "Player")
		{
			Debug.Log("추격을 종료합니다");
			SetState(false);
		}
	}

	void SetState(bool isChase)
	{
		if (isChase)
		{
			chaseEvent.Invoke();
			displayer.GetComponent<MeshRenderer>().material = states[0];
		}
		else
		{
			stopEvent.Invoke();
			displayer.GetComponent<MeshRenderer>().material = states[1];
		}
	}
}

플레이어가 Field 클래스가 있는 오브젝트와 닿았을 경우

void ChasePlayer()
{
	isChase = true;
	while (poolMonsters.Count < maxMobCount)
	{
		pool.Get();
	}
	foreach (var monster in poolMonsters)
	{
		monster.GetComponent<Monster>().target = player;
	}
}

chaseEvent.Invoke() 이벤트를 실행하고,

오브젝트와 떨어질 경우

void ChaseStop()
{
	isChase = false;
	foreach (var monster in poolMonsters)
	{
		monster.GetComponent<Monster>().target = null;
	}
}

stopEvent.Invoke() 이벤트를 실행한다.


옵저버의 기본 구조는 주시 대상(Subject)관찰자(Observer) 구조지만,

중간에 중계자(Mediator) 를 추가하여 유연하고 확장 가능한 구조로 확장할 수 있다.

Subject ↔ Mediator ↔ Observer


유니티에서 인터페이스를 어떻게 사용할 수 있을지 잘 생각이 안났지만

어댑터 패턴을 보니 되게 유연하게 사용할 수 있을 것 같다.

0개의 댓글