간단하게 정의를 설명하자면 다음과 같다.
상이한 두 인터페이스를 호환할 수 있도록 하는 중간 객체를 만들어 주는 과정
-기존 코드를 수정하지 않기 때문에 해당 코드가 변경 불가능하거나 업데이트 될 때에도 대응할 수 있다.
-최소한의 변경으로 기존 코드를 재사용 할 수 있다.
-약간의 성능 저하가 발생한다. (물론, 이슈가 되지 않을 만큼 미비한 성능 저하이다)
-어댑터가 너무 많아지면 복잡해진다
-해당 스크립트만 봤을 때 상호작용 원리를 파악하기 힘들다 -> 디버깅이 어렵다.
사실상 팀원과의 의사소통이 잘 되는 편이라면 어댑터를 사용하는 것보다 인터페이스로 연결하는 것이 좋다.
하지만 아래와 같은 경우는 어댑터를 쓰는 게 좋다
어댑터 패턴에 대해 알아보기 앞서, 유니티에서 인터페이스가 어떻게 작용하는지 알아보자.
다음과 같이, 총알이 몬스터에게만 데미지를 주도록 인터페이스로 연결해보자.
IDamageable 인터페이스부터 만들어보자.
using UnityEngine;
public interface IDamageable
{
public void TakeDamage(GameObject dealer, int damage);
}
매개번수로 데미지를 준 상태의 게임 오브젝트와, 데미지 수치를 요구한다.
이제, Bullet에 인터페이스를 가져와서 데미지를 줄지 여부를 판단한다.
public class Bullet : MonoBehaviour
{
...
private void OnCollisionEnter(Collision collision)
{
Destroy(gameObject);
Instantiate(explosionEffetPrefeb, transform.position, transform.rotation);
// 충돌할 시에 게임 오브젝트에서 IDamageble 인터페이스가 있는지 가져온다.
IDamageable damageable = collision.gameObject.GetComponent<IDamageable>();
// 해당 오브젝트에 IDamageable 인터페이스가 있을 경우
if(damageable != null)
{
// 데미지를 준다.
Attack(damageable);
}
}
// 인터페이스를 직접 가져올 수 있다.
private void Attack(IDamageable damagable)
{
// 인터페이스가 있으면 데미지를 준다.
damagable.TakeDamage(gameObject, attackPoint);
}
}
이제 이와 같이 작성하고, 데미지를 받을 대상에게 IDamageable 인터페이스를 달아주면 된다.
이를 위해 몬스터 스크립트를 수정할 것인데, 기존의 몬스터 체력을 깎는 코드는 다음과 같았다.
private void OnCollisionEnter(Collision other)
{
if (other.gameObject.tag == "PlayerBullet")
{
TakeDamage();
}
}
private void TakeDamage()
{
monsterHp--;
}
이런 방법으로도 데미지를 구현할 수 있지만, 다음과 같은 이유로 문제가 생길 수 있다.
따라서 인터페이스를 통해서 관리할 수 있다.
using UnityEngine;
public class MonsterMover : MonoBehaviour, IDamageable
{
...
public void TakeDamage(GameObject dealer, int damage)
{
Debug.Log($"{gameObject.name} 이/가 {dealer.name}에게 피해를 {damage} 받았습니다.");
monsterHp -= damage;
}
...
}
이와 같이 작성할 수 있다.
이와 같이 작성하였을 때, 총알이 벽이나 바닥 같은 곳에 충돌했을 경우 IDamageable을 찾으려 할 것이고, 없는 오브젝트에는 데미지를 입히지 않으며 있는 있는 오브젝트에는 데미지를 줄 것이다.
어댑터 패턴의 예시로 아래와 같은 상황을 생각해 보자.
스위치를 조작하면 문을 열 수 있고, 한 번 더 조작하면 문이 닫히는 시스템을 구현했다고 치자. 하지만 탱크로 스위치를 공격했을 때에도 문을 열 수 있는 기능을 만들고 싶다. 여기에서 문제가 생긴다.
우선은 스위치를 만들어보자.
using UnityEngine;
public class Switch : MonoBehaviour
{
[SerializeField] GameObject door;
private bool IsDoorOpen => door.activeSelf == false;
public void SwitchAction()
{
if(IsDoorOpen)
{
Close();
}
else
{
Open();
}
}
public void Open()
{
door.SetActive(false);
}
public void Close()
{
door.SetActive(true);
}
}
이와 같이 만들고 씬 뷰에서 문과 스위치를 만들어보자.
인스펙터에서도 설정해준다.
이와 같이 설정한 후 탱크로 스위치를 사용할 수 있는 어댑터를 만들어 보고자 한다.
아까 전, 우리는 위에서 IDamageable 인터페이스를 만들었다. 이 인터페이스를 사용하여 DamageAdapter를 만든다.
public class DamageAdapter : MonoBehaviour, IDamageable
{
public UnityEvent<GameObject, int> Ondamaged;
public void TakeDamage(GameObject dealer, int damage)
{
Ondamaged?.Invoke(dealer, damage);
}
}
데미지를 받았을 때 Invoke를 일으키는 클래스를 만들었다. 이걸 이제 스위치에 다시 넣는다.
스위치에 DamageAdapter를 넣었을 때, 데미지를 넣는 칸은 없는 것을 확인할 수 있다. 여기에서 사용할 함수를 선택하여 놓으면 되며, 스위치의 온오프를 위해 스위치 액션으로 설정하기로 했다.
잘 작동되는지 확인해보자.
이와 같이 작동하는 것을 확인할 수 있다.
스위치를 작동시키는 예 외에도, 여러 게임과 상황에서 유용하게 사용할 수 있는 상호작용 어댑터, InteractAdapter도 만들어보자.
먼저 간단하게 IInteractable 인터페이스를 만들어보자.
public interface IInteractable
{
public void Interact();
}
이후 이 인터페이스를 사용한 어댑터와 해당 어댑터를 사용할 NPC 클래스를 만들어 보자.
using UnityEngine;
using UnityEngine.Events;
public class InteractAdapter : MonoBehaviour, IInteractable
{
public UnityEvent OnInteracted;
public void Interact()
{
OnInteracted?.Invoke();
}
}
using UnityEngine;
public class NPC : MonoBehaviour
{
[SerializeField] string name;
public void Talk()
{
Debug.Log($"{name} : 안녕하세요.");
}
public void Die()
{
Debug.Log($"{name} : 으악!");
}
}
이와 같이 작성하고서 인스펙터로 돌아가보자.
아까 만들었던 DamageAdapter와 InteractAdapter 둘 다 붙여서 작용을 확인해보았다.
이와 같이 작용을 확인할 수 있다.
앞으로 게임 프로그래밍을 하는 상황에서, 모든 게임에서 절대로 빼먹지 않고 쓰게 될 패턴이다.
게임 매니저, 점수 및 데이터 관리에 필수적인 패턴으로, 사용을 금지하는 일부 회사를 제외하고선 앞으로 매우 많이 쓰게 될 기능이니 잘 기억해 두자.
어떤 클래스로 하여금 오직 한 개의 클래스 인스턴스만을 갖도록 보장하는 것으로, 클래스에 대한 전역적인 접근점을 제공한다.
구현 원리는 다음과 같다.
public class Singleton
{
private static Singleton instance;
public static Singleton GetInstance()
{
if (instance == null)
{
instance = new Singleton();
}
return instance;
}
private Singleton() { }
}
장점
1. 하나뿐인 존재로 주요 클래스 & 관리자 역할에 적합함
2. 전역적 접근으로 참조에 필요한 작업이 없이 빠른 접근이 가능함
3. 인스턴스들이 싱글톤을 통하여 데이터를 공유하기 쉬워짐
주의점
1. 싱글톤은 너무 많은 책임을 짊어지는 경우가 많으며 단일책임원칙을 위반
2. 싱글톤은 전역접근으로 코드의 결합도를 높이므로 남발하지 않아야 함
3. 싱글톤은 단위 테스트를 하기 어렵게 함
아래 구조가 유니티에서 가장 기초적으로 만들 수 있는 싱글톤 패턴이다.
다만 기초적인 구조인 만큼 허점이 많으므로, 실제로는 이것보다 훨씬 더 견고한 구조로 만들어야 한다.
using UnityEngine;
public class GameManager : MonoBehaviour
{
private static GameManager instance;
// 게임 매니저가 아예 없는 경우도 오류가 생길 수 있으므로 이렇게 써 주는 것도 방법이다.
public static GameManager Instance
{
get
{
if(instance == null)
{
GameObject gameObject = new GameObject("GameManager");
instance = gameObject.AddComponent<GameManager>();
}
{ return instance; }
}
}
private void Awake()
{
CreateInstance();
}
private void CreateInstance()
{
if (instance == null)
{
instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
}
실무에서 사용하는 싱글톤 패턴의 예시 또한 참고용으로 남겨놓고자 한다. 다만 이 또한 보완해야 할 부분이 있는 패턴이며 구조적으로 정해진 패턴이 아님을 기억해 두도록 한다.
먼저 해당 구조를 만들기 위해서 Resources 폴더를 만들어야 한다.
*오탈자가 없도록 주의한다
이와 같은 폴더를 만들고, 이 안에 게임매니저를 포함한 싱글톤 패턴의 프리팹을 넣어준다.
(필드에 생성해 둔 프리팹은 제거하도록 한다.)
그리고 싱글톤 패턴을 만들기 위한 코드는 세 개로 분리한다.
1) Manager Static Class
using UnityEngine;
public static class Manager
{
public static GameManager Game => GameManager.Instance;
// 씬이
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Initailize()
{
GameManager.CreateInstance();
}
}
2) GameManager
게임매니저에는 선언할 변수와 초기값만 넣는다.
public class GameManager : SingleBehaviour<GameManager>
{
public int score;
}
3) SingleBehaviour 제네릭 클래스
using UnityEngine;
public class SingleBehaviour<T> : MonoBehaviour where T : MonoBehaviour
{
private static T instance;
public static T Instance { get { return instance; } }
public static void CreateInstance()
{
if (instance == null)
{
T prefab = Resources.Load<T>("GameManager");
instance = Instantiate(prefab);
DontDestroyOnLoad(instance.gameObject);
}
}
public static void ReleaseInstance()
{
if (instance != null)
{
Destroy(instance.gameObject);
instance = null;
}
}
}
옵저버 패턴은 언뜻 보면 낯선 것처럼 보일 수 있지만, 이미 배워본 것이다.
옵저버 패턴은 주시 대상(Subject)이 되는 객체가 자신의 데이터 변경 시 등록된 관찰자(Observer)들에게 알려주는 디자인 패턴이다. 이를 통해 주기적으로 확인하지 않아도 데이터 변화에 대응할 수 있으므로 게임 최적화에 도움 되는 패턴이다.
이는 C# 프로그래밍에서 배운 델리게이트와 이벤트이며, 콜백 함수를 말하는 것이다.
Subject
주시대상이 되는 데이터와 옵저버들을 가지고 있는 주체이다. 데이터 변경시 등록된 여러 옵저버들에게 메서드를 통해 메시지를 전달한다.
Observer
Subject를 주시하고 있는 관찰자이며, 데이터 변경에 대한 메시지 수신 시 자신이 해야 할 동작을 수행한다.
더 이상 사용하지 않는 객체는 구독을 해지하기
옵저버패턴은 주시 대상에게 관찰자를 '등록'하는 방식이다. 앞으로 사용하지 않을 객체에서 할당 해제를 하지 않는다면, 불필요한 메모리 공간을 차지한다다.
옵저버가 많아질수록 무거워진다
성능적으로 이득을 보는 것은 사실이지만, 관찰자가 늘어날수록 관찰자 목록을 순회하며 호출 함수를 실행하기 때문에 관찰자의 수와 성능이 반비례한다.
구조 복잡도 증가
옵저버 패턴은 객체 간의 결합력이 낮아지는 이점이 있지만 객체간의 관계가 불명확할 수 있다. 간결하고 명확한 코드를 작성해야 하며, 이는 설계 단계에서 중요하게 생각해야 할 문제이다.
먼저 게임매니저에 일시정지 함수를 추가한다. 여기서 이벤트를 사용한다.
public class GameManager : MonoBehaviour
{
private static GameManager instance;
public event Action OnPaused;
...
public void OnApplicationPause(bool pause)
{
Time.timeScale = 0f;
OnPaused?.Invoke();
}
...
이제 이와 같이 적용했을 때 플레이어 움직임 클래스에 이와 같이 내용을 추가해주면 된다.
public class TankMovementUpgrade : MonoBehaviour
{
...
private void Start()
{
Manager.Game.OnPaused += (함수);
}
private void OnDestroy()
{
Manager.Game.OnPaused -= (함수);
}
...
-Resource 폴더
https://velog.io/@byin99/Resources-%ED%8F%B4%EB%8D%94