Unity에서 게임을 개발할 때, 처음에 구조를 잘 설계하면 유지보수성과 확장성이 크게 향상된다. 반대로 처음에 구조설계를 안하고 대충 개발하면 프로젝트가 진행될 수록 코드가 복잡해져 관리가 어려워지면서 협업할 때 효율성이 매우 떨어질 수 있다. 그래서 오늘은 여러가지 유용한 구조설계에 대해 알아보려고 한다.
Enum을 활용하면 코드의 가독성이 높아지고, 값의 무결성을 유지할 수 있다. 특히 문자열이나 숫자 값으로 직접 비교하는 방식과 비교했을 때, 실수를 줄이고 유지 보수가 쉬워진다.
<장점>
<단점>
<예제>
namespace EnumTypes { public enum AttackTypes { None, Melee, Range } public enum CardRanks { Normal, Special, Rare } public enum CardHowToUses { Normal, TargetGround, TargetEntity } public enum CardAfterUses { Discard, Destruct, Spawn } public enum GameFlowState { InitGame, SelectStage, Setting, Wave, EventFlow, Ending } }
Struct는 Class와 다르게 값 타입이므로 힙이 아닌 스택 메모리에 저장되며, GC(가비지 컬렉션)의 부담이 줄어든다. 따라서 참조 타입인 클래스보다 메모리 할당이 빠르고, 값 복사가 이루어지므로 데이터의 변경이 독립적으로 이루어진다.
<장점>
<단점>
<예시>
namespace Structs { [Serializable] public struct AttackData { public AttackTypes attackType; public int attackAnimationIndex; } [Serializable] public struct StatModifierData { public StatTypes statType; public ModifierTypes modifierType; public float value; } }
유틸리티 함수는 자주 사용되는 기능을 하나의 클래스로 모아서 재사용성을 높이는 방식이다. 일반적으로 static 클래스로 정의하여 인스턴스 생성을 방지하고, 전역적으로 접근할 수 있도록 한다.
<장점>
<단점>
static 함수로만 구성되므로 다형성을 활용하기 어렵다.<예시>
public static class Utils { public static float DirectionToAngle(float x, float y) { return Mathf.Atan2(y, x) * Mathf.Rad2Deg; } public static int GenerateID<T>() { return Animator.StringToHash(typeof(T).Name); } }
전역 변수를 한 곳에서 관리하면 여러 곳에서 동일한 값을 사용할 때 유지보수가 쉬워진다. 하지만 변하는 값을 전역으로 관리하면 프로그램의 흐름을 예측하기 어려워지므로, 반드시 불변 값만 글로벌 변수로 지정해야 한다.
<장점>
<단점>
<예시>
public static class Globals { public const int WorldSpaceUISortingOrder = 1; public static class LayerName { public static readonly string Default = "Default"; public static readonly string UI = "UI"; } }
Unity에서는 게임의 다양한 오브젝트를 효율적으로 관리하기 위해 관리자 클래스를 도입하는 것이 일반적이다. GameManager 를 최상위 관리자로 두고, 하위 관리자 클래스를 계층적으로 배치하면 모든 오브젝트 간 관계를 명확하게 정리할 수 있다.
<장점>
<단점>
<예시>
public class GameManager : MonoBehaviour { [SerializeField] private CharacterManager _characterManager; [SerializeField] private UIManager _uiManager; } public class CharacterManager : MonoBehaviour { private GameManager _gameManager; public void Init(GameManager gameManager) { _gameManager = gameManager; } }
Scene이 변경되더라도 유지되어야하는 사용자 설정, 계정 정보, 네트워크 상태 같은 데이터는 DonDestroyOnLoad 를 사용하여 객체가 Scene전환 시에도 삭제되지 않도록 설정할 수 있다. 이를 관리하기 위해 일반적으로 싱글톤(Singleton)패턴 을 활용한다.
<장점>
<단점>
<예시>
public class GameInstance : Singleton<GameInstance> { private LogGUI _logGUI; private DebugStatGUI _debugStatGUI; private ScriptableObjects.GamePrefabs _gamePrefabs; private HttpManager _httpManager; } public abstract class Singleton<T> : MonoBehaviour where T : Component { private static T instance; public static T Instance { get { if (instance == null) { instance = FindObjectOfType<T>(); if (instance == null) { GameObject obj = new GameObject { name = typeof(T).Name }; instance = obj.AddComponent<T>(); } } return instance; } } protected virtual void Awake() { if (instance == null) { instance = this as T; DontDestroyOnLoad(gameObject); } else { Destroy(gameObject); } } }
이렇게 하면 GameInstance 라는 객체는 Scene이 변경되더라도 유지된다. 하지만 모든 데이터를 싱글톤으로 관리하면 결합도가 높아지고 유지보수가 어려워질 수 있으므로, 필요한 경우에만 싱글톤 패턴을 적용하는 것이 중요하다.
ScriptableObject는 에디터에서 데이터를 수정하고 런타임에서는 읽기 전용으로 사용하는 정적 데이터 관리 방식에 적합하다. 이는 게임 내 아이템 정보, 레벨 디자인 데이터, 캐릭터 능력치 등의 변하지 않는 데이터를 관리할 때 유용하다.
<장점>
<단점>
<예시>
namespace ScriptableObjects { [CreateAssetMenu(fileName = "ItemData", menuName = "ScriptableObjects/ItemData", order = 1)] public class ItemData : ScriptableObject { public string itemName; public int itemID; public Sprite itemIcon; } }
게임 개발을 하면 여러 데이터를 한곳에 모아 관리할 필요가 있다. 이를 위해 데이터 덩어리 객체(Data Container Object) 를 활용할 수 있다. 이는 여러 ScriptableObject를 한곳에서 관리할 수 있도록 설계된다.
<장점>
<단점>
<예시>
namespace ScriptableObjects { [CreateAssetMenu(fileName = "Data", menuName = "ScriptableObjects/GameData", order = 1)] public class GameData : ScriptableObject { public List<CharacterStats> characters; public List<ItemData> items; } }
게임 개발을 할 때 필연적으로 여러 개의 리소스를 효율적으로 관리해야한다. 예를 들면 스프라이트, 사운드, 파티클 등이 있다. 이러한 리소스들을 그룹으로 묶어두면 코드에서 쉽게 참조할 수 있다.
<장점>
<단점>
<예시>
namespace ScriptableObjects { [CreateAssetMenu(fileName = "Data", menuName = "ScriptableObjects/ResourceGroup", order = 1)] public class ResourceGroup : ScriptableObject { public List<Sprite> characterSprites; public List<AudioClip> soundEffects; } }
이 코드를 사용하면 캐릭터 스프라이트와 사운드 효과를 쉽게 관리할 수 있다. 이를 아까 싱글톤으로 만든 GameInstance 에서 참조하면 프로젝트 전체에서 일관된 리소스 관리를 할 수 있다.
Unity에서 Update 함수는 각 프레임마다 호출되며, 게임 로직, 사용자 입력 처리, 타이머, 상태 관리 등에 사용된다. 하지만 수많은 Update 함수가 존재할 경우 성능 저하가 발생할 수 있다. 이는 Unity가 모든 MonoBehaviour 스크립트를 순회하며 Update 함수를 호출하기 때문이다.
<안쓰는 Unity 메시지 함수를 제거해야 하는 이유>
Unity의 MonoBehaviour는 Update, FixedUpdate, LateUpdate, OnEnable, OnDisable 등 다양한 메시지 함수를 제공하는데 Unity는 이러한 함수가 존재하기만 해도 검사하기 때문에 필요하지 않은 함수들은 제거해야한다.
따라서, 사용하지 않는 메시지 함수는 반드시 제거해야 한다.
<대안 및 관리법>
Update가 필요한 경우만 이벤트를 발생시키는 방식으로 불필요한 호출을 최소화한다.
예를 들어, 상태 변화가 있을 때만 특정 함수를 호출하도록 한다.
개별 객체의 Update 호출을 통합하여 관리하면 불필요한 함수 호출을 줄일 수 있다.
물리 연산이 필요한 경우 FixedUpdate를 사용하여 일정한 간격으로 실행되도록 한다. 렌더링 후 처리해야할 작업(카메라 위치 보정 등)은 LateUpdate를 활용하여 불필요한 Update 호출을 방지한다.
Update 대신 필요할 때만 실행하는 코루틴을 사용하여 불필요한 함수 호출을 줄일 수 있다.
오브젝트 풀링은 자주 생성 및 파괴되는 객체를 미리 생성하여 재사용하는 방식이다. 이는 GC의 부담을 줄이고, 성능을 최적화하는데 효과적이다.
<장점>
<대안 방법>
Instantiate 를 호출하는 방식 대신, 미리 생성된 객체를 활성화/비활성화하는 방식으로 사용한다.<예시>
public class ObjectPool<T> where T : MonoBehaviour { private Queue<T> pool = new Queue<T>(); private T prefab; private Transform parent; public ObjectPool(T prefab, int initialSize, Transform parent = null) { this.prefab = prefab; this.parent = parent; for (int i = 0; i < initialSize; i++) { var obj = GameObject.Instantiate(prefab, parent); obj.gameObject.SetActive(false); pool.Enqueue(obj); } } public T Get() { if (pool.Count > 0) { var obj = pool.Dequeue(); obj.gameObject.SetActive(true); return obj; } var newObj = GameObject.Instantiate(prefab, parent); return newObj; } public void Return(T obj) { obj.gameObject.SetActive(false); pool.Enqueue(obj); } }
Unity에서는 여러 개의 씬을 사용하여 게임을 구성할 수 있으며, 씬을 효율적으로 관리하는 것이 성능과 유지보수에 중요한 요소이다.
<씬 관리 전략>
<예시>
IEnumerator LoadSceneAsync(string sceneName) { AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName); while (!asyncLoad.isDone) { yield return null; } }
비동기 로딩을 사용하면 로딩 중에도 게임이 끊기지 않고 부드럽게 진행될 수 있다.
Unity의 UI 시스템은 잘못 설계하면 불필요한 재렌더링과 성능 저하를 초래할 수 있다.
UI 최적화 전략
LayoutRebuilder.ForceRebuildLayoutImmediate() 를 호출하자.<예시>
public class UIButtonHandler : MonoBehaviour { public Button myButton; private void Start() { myButton.onClick.AddListener(OnButtonClick); } private void OnButtonClick() { Debug.Log("버튼이 클릭됨!"); } }
게임 데이터 저장 방식에는 여러 가지가 있으며, 각 방식의 장단점이 다르므로 적절하게 선택해야 한다.
<PlayePrefs(간단한 설정 저장)>
PlayerPrefs.SetInt("HighScore", 100); int score = PlayerPrefs.GetInt("HighScore", 0);
<JSON 파일 저장 (플레이어 데이터, 설정 데이터)>
string json = JsonUtility.ToJson(playerData); File.WriteAllText(Application.persistentDataPath + "/playerData.json", json);
<ScriptableObject활용 (에디터 수정용 데이터)>
[CreateAssetMenu(fileName = "Data", menuName = "Game/StageData")] public class StageData : ScriptableObject { public int stageNumber; public string stageName; }
데이터 저장 방식은 목적에 맞게 선택하는 것이 중요하며, 보안이 필요한 경우 암호화된 저장 방식을 고려해야 한다.