LambFerret's Blog
건파밍 #1 게임 시스템 (1) : Singleton, GameManager 본문
들어가기앞서, 글을 쓸때 '내가생각하기엔..' , '~라고 생각한다.' 류의 말은 생략하겠습니다. 모든 제 글은 제1대전제로 온전한 개인적 주장이기에 틀릴 수 있습니다. 감사합니다.
Singleton : 프로젝트 내에서 하나의 인스턴스만 보장되는 게임 디자인 패턴이다. 이는 코드상에서 싱글턴에 유연한 접근을 가능케하기에 보통은 게임 핵심 시스템에 많이 사용하는 편이다.
게임은 Manager란 이름으로 정말 많은 관리용 클래스가 필요하기에 이들 모두를 싱글턴화 시켜두었다. 주 사용 방법으로는
AudioManager.Instance.PlaySfx(shotSound);
위와 같은 방법으로 간편하게 오디오 소리만 내고 싶을때 사용한다.
싱글턴의 사용방법은 이정도 중요도가 좋다. 그 이유는 접근의 용의성과 그 중요도는 반비례해야 하기 때문이다. 어디서든지 핵심에 접근 수 있다면 잠재 위험을 감수하기 힘들기 때문이다. 그 매니저와 아주 유기적인 관계를 가진 어떤 클래스라면, 그 클래스를 합치는 방법이라던지, 논리를 변경하는게 좋다. 싱글턴끼리의 혹은, 싱글턴 - 인스턴스 간의 결합도의 상승은 더 심도있게 경계해야할 부분이다.
말은 이렇게 했지만 사용처는 아주 많다. 우선 기초적으로 싱글턴을 만드는 방식을 보자면
public class Singleton : MonoBehaviour
{
public static Singleton instance;
private void Awake()
{
if(instance == null)
{
instance = this;
DontDestroyOnLoad(gameobject);
}
else
{
Destroy(gameobject);
}
}
}
이런식으로 매 클래스마다 static Instance를 지정해주었지만 하나하나 하기엔 무리가 있다. 그래서 내가 사용한 방법으로는
using UnityEngine;
namespace Systems.tool
{
/// <summary>
/// 타입의 객체를 싱글톤화 하는 추상 클래스
/// </summary>
/// <typeparam name="T">컴포넌트 타입</typeparam>
public abstract class AbstractSingleton<T> : MonoBehaviour where T : Component
{
public static T Instance { get; protected set; }
protected virtual void Awake()
{
if (Instance == null)
{
Instance = this as T;
}
}
}
}
이후 사용할땐
public class AudioManager : AbstractSingleton<AudioManager>
이렇게 상속만 받으면 편하다. (출처 : https://gist.github.com/whitebull/a5262e57579f42333899)
이중 편의를 위해 변경한 부분이 있는데 DontDestroyOnLoad 부분이다. Manager들은 Scene전환시에도 같은 상태가 유지되어야 하기에 DontDestroyOnLoad가 꼭 필요하다. 하지만 이를 위한 제약사항으로 DontDestroyOnLoad는 Hierarchy 최상위에 있어야한다. 이는 Manager가 늘어나면 늘어날수록 Hierarchy 의 가독성이 떨어지기에 Singleton 과 DontDestroyObject 로 책임을 나누었다.
using UnityEngine;
namespace Systems.tool
{
public class DontDestroyObject : MonoBehaviour
{
private static DontDestroyObject _instance;
private void Awake()
{
if (_instance == null)
{
_instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
GameObject o;
(o = gameObject).SetActive(false);
Destroy(o);
}
}
}
}
이를 최상위로 두고, 나머지 싱글턴들을 이 GameObject의 자식으로 두면, 씬이동시의 파괴방지도 보장되며 각 매니저의 싱글턴도 유지할 수 있었다. 그리고 가장 좋았던건 Hierarchy가 깨끗해졌다. (중요)
간단하게 제일 기초가 되는 GameManager를 살펴보겠다.
public class GameManager : AbstractSingleton<GameManager>
{
[Header("UI")]
public TextMeshProUGUI fps;
[Header("Config")]
public bool isDebug;
[Header("Info")]
// 고민중인 부분.. GameManager에서 Player를 들고있는것은 타당하다고 볼 수 있으나 확신은 없음
// 사용시 편리한건 사실. Player를 사용하는 곳은 맞으나 결합도가 생각보다 높아짐
// 사용시 GameManager.Instance.player와 같이 길어지는것도 꺼려짐의 원인중 하나
[ReadOnly] public Player player;
protected override void Awake()
{
base.Awake();
Init();
}
private void Init()
{
AddComponentIfNotExists<IInteractable, Interactable>();
AddComponentIfNotExists<IProcessingDialogue, ProcessingDialogue>();
DOTween.SetTweensCapacity(200000, 50);
// screen into window mode
Screen.SetResolution(2560, 1440, true);
// fps 제한
Application.targetFrameRate = 60;
player = FindFirstObjectByType<Player>();
}
private void Update()
{
UpdateFPS();
// set fullscreen
if (Input.GetKeyDown(KeyCode.F11))
{
Screen.fullScreen = !Screen.fullScreen;
}
}
#region calculate FPS
private readonly Queue<float> _frameTimes = new();
private const int FrameRange = 60; // 최근 60프레임을 기준으로 평균 계산
private void UpdateFPS()
{
// 현재 프레임 시간 추가
_frameTimes.Enqueue(Time.deltaTime);
// 저장된 프레임 시간이 설정된 범위를 초과하면 가장 오래된 값 제거
if (_frameTimes.Count > FrameRange)
{
_frameTimes.Dequeue();
}
// 평균 프레임 시간 계산
float averageFrameTime = 0f;
foreach (float frameTime in _frameTimes)
{
averageFrameTime += frameTime;
}
averageFrameTime /= _frameTimes.Count;
// FPS 계산 및 표시
float averageFPS = 1.0f / averageFrameTime;
fps.text = $"{averageFPS:F1} FPS";
}
#endregion
}
}
Player에 저 장문의 주석이 적힌게 보이시나요? 저 당시조차도 되게 고민이 많던부분.
어떤 코드는 이 GameManager에 mainCamera, Player 등등 게임에 중심이 되는 중요 오브젝트를 미리 연결해두고 어디서든 접근하기 쉽게 하도록 하기도 한다. 하지만 너무 접근성이 높아지는게 아닌가 해서, (하지만 역시 Player가 접근이 쉬운건 포기할수없었다.. ) 이 필드에 연결을 최소화 하였다.
이렇게 하면 생각보다 GameManager 에 넣을게 없어진다. 그래서 현재 사용중인건 FPS를 볼 수 있는 UI의 추가와, F11을 눌러서 전체화면 화 시키는 기능 및 해상도설정을 넣어두었다.
쓰고보니 게임에 관한건 없었다.
그래도 백엔드에서 좀 있던 느낌때문인지 이런 코드를 만지는것도 충분히 재밌다. 오히려 주되게 재밌다. 그리고 System끝나려면 앞으로 적어도 3번은 남았으니 또 재밌게 써야겠다.
'게임 개발 > #2 건파밍 (2D 탑뷰슈터)' 카테고리의 다른 글
건파밍 #5 게임 시스템 (5) : Dialogue System (1) | 2025.02.10 |
---|---|
건파밍 #4 게임 시스템 (4) : Localization (0) | 2025.01.27 |
건파밍 #3 게임 시스템 (3) : Excel to ScriptableObject (1) | 2025.01.21 |
건파밍 #2 게임 시스템 (2) : Data Persistence (0) | 2025.01.18 |
건파밍 #0 개요 (0) | 2025.01.18 |