유니티에서 레이어에 대한 처리를 훨씬 효율적으로 하는 방법으로 레이어 마스크를 활용한다
유니티에서는 총 32개(0~31)의 레이어를 활용할 수 있으며, 이를 한번에 처리하기 위해 정수형 변수의 각 비트(32비트)를 할당하여 처리한다. 1개의 변수를 여러개의 bool값처럼 처리하는 방법
예를 들어, 카메라에서 특정 레이어에 있는 객체만 촬영하는 Culling Mask를 생각해보자. 이를 이렇게 구현할 수도 있다.
// 1바이트인 bool로 저장하는 의사(pseudo)코드
bool[] layerList;
foreach(bool i in LayerList){ // bool : 1바이트
if(i) Render();
}
하지만 이렇게 하는 경우, 32개의 데이터를 저장할 때 32바이트가 필요하며, 32개의 연산을 하게 되어 매우 불필요하게 연산량이 커질 수 있다.
그래서 이를 2진법으로 표현하여 훨씬 더 편리하게 효율적으로 처리할 수 있다.
즉, 하나의 큰 수에 여러 개의 0과 1값을 저장하는 기법이라고 생각하면 된다.
2진법은 0~9로 숫자를 표현하는 10진법과 다르게, 0~1로 숫자를 표현하는 방법이다. 컴퓨터는 전기 신호의 켜짐(1)과 꺼짐(0)을 이용해 정보를 처리하고 저장한다.
따라서 2진법은 컴퓨터가 이해하고 처리할 수 있는 가장 디본적인 데이터 표현 방식이다.
그리고 이렇게 2진법으로 표현한 수를 2진수라고 한다.
코드로 나타내기
Debug.Log($"십진수 10을 이진수로 변경 : {Convert.ToString(10, 2)}")
class Program
{
static string DecimalToBinary(int decimalNumber)
{
if (decimalNumber == 0)
return "0";
string binaryString = "";
while (decimalNumber > 0)
{
int remainder = decimalNumber % 2;
binaryString = remainder + binaryString;
decimalNumber = decimalNumber / 2;
}
return binaryString;
}
static void Main()
{
int decimalNumber = 15; // 변환하고 싶은 십진수
string binaryString = DecimalToBinary(decimalNumber);
Console.WriteLine("Binary Representation: " + binaryString);
}
}
// 8번 레이어 활성화
// 8번 : 보스 6번 : 일반
int layerMask = 1 << 8;
layerMask |= 1 << 6; // |= 6번도 포함(OR)
// 사용 예시: 8번 레이어의 오브젝트만 감지하는 Raycast
if (Physics.Raycast(ray, out hit, 100, layerMask)) {
// 8번 레이어의 오브젝트와 충돌했을 때의 처리
}
OR연산은 한쪽이라도 1인 경우 1이 되도록 하는 연산이다.
이를 통해 어떤 한 비트가 1이 되도록 할 수 있다.
// 8번과 10번 레이어 결합
int layerMask = (1 << 8) | (1 << 10);
101000000000
// 사용 예시: 8번 또는 10번 레이어의 오브젝트만 감지하는 Raycast
if (Physics.Raycast(ray, out hit, 100, layerMask)) {
// 8번 또는 10번 레이어의 오브젝트와 충돌했을 때의 처리
}
AND는 A, B 모두 1이 아니면 0을 반환하므로,
자신이 1일 때 상대가 1인지 아닌지 판단 할 수 있다.
bool isLayer8Included = (layerMask & (1 << 8)) != 0;
// 사용 예시: layerMask에 8번 레이어가 포함되어 있는지 확인
if (isLayer8Included) {
// 8번 레이어가 포함된 경우의 처리
}
// 예시
// 1000000000 & 1001010101
// => 1000000000 != 0 => true
// 100000000 & 010101010 => 000000000 != 0 => false
// 8번 레이어 제외
int layerMask = ~(1 << 8);
// 사용 예시: 8번 레이어를 제외한 모든 레이어의 오브젝트를 감지하는 Raycast
if (Physics.Raycast(ray, out hit, 100, layerMask)) {
// 8번 레이어를 제외한 오브젝트와 충돌했을 때의 처리
}
LayerMask mask = LayerMask.GetMask("Monster") | LayerMask.GetMask("Wall");
// int mask = (1 << 8) | (1 << 9); 와 동일
RaycastHit hit;
if (Physics.Raycast(ray, out hit, 100.0f, mask))
{
Debug.Log(hit1.collider.gameObject.name);
}
LayerMask.value
비트마스크값을 도출한다. (실제 이진수를 십진수로 바꿔서 계산) 예) 1025 = 1024 + 1
LayerMaks.NameToLayer(string layerName)
레이어의 이름을 통해 레이어의 인덱스(비트마스크값 아님)을 도출한다. 예) 9
LayerMask.Contains(int layerIndex)
LayerMask가 특정한 레이어 인덱스를 포함하고 있는지 확인한다.
System.Flags를 사용해서 중첩 비트마스크를 부여할 수 있다.
이를 활용해 아래와 같은 상태이상 시스템을 구현할 수 있다.
[System.Flags] // 비트 중첩 가능
public enum StatusEffects
{
None = 0,
Poisoned = 1 << 0, // 0001
Burned = 1 << 1, // 0010
Frozen = 1 << 2, // 0100
Paralyzed = 1 << 3 // 1000
}
public class StatusEffectManager
{
private StatusEffects currentEffects = StatusEffects.None;
public void AddEffect(StatusEffects effect)
{
currentEffects |= effect;
}
public void RemoveEffect(StatusEffects effect)
{
currentEffects &= ~effect;
}
public void ClearEffects()
{
currentEffects = StatusEffects.None;
}
public bool HasEffect(StatusEffects effect)
{
// 0100 & 0100 => 0100 != 0000 -> true
return (currentEffects & effect) != StatusEffects.None;
}
public void PrintEffects()
{
Console.WriteLine("Current Status Effects: " + currentEffects);
}
}
class Program
{
static void Main()
{
StatusEffectManager manager = new StatusEffectManager();
// 상태 이상 추가
manager.AddEffect(StatusEffects.Poisoned);
manager.AddEffect(StatusEffects.Frozen);
// 현재 상태 이상 출력
manager.PrintEffects(); // Poisoned, Frozen
// 상태 이상 제거
manager.RemoveEffect(StatusEffects.Poisoned);
// 상태 이상 확인
if (manager.HasEffect(StatusEffects.Frozen))
{
Console.WriteLine("The character is frozen.");
}
// 모든 상태 이상 제거
manager.ClearEffects();
}
}
싱글톤 패턴은 한 개의 인스턴스만 생성하고, 어디서든 그 인스턴스에 접근할 수 있는 디자인 패턴이다.
대부분 사람들이 싱글톤을 쓰는 이유 : 객체 간 접근이 편해서 / 중앙 집중식 관리가 머리 안아픔
전역적인 상태나 리소스에 접근 : 싱글톤은 어디서든 접근할 수 있는 전역적인 상태나 리소스에 대한 중앙 집중적인 접근을 제공한다. 예를 들어, 게임의 설정, 오디오 관리자, 이벤트 매니저 등을 싱글토으로 구현할 수 있다. 한 가지의 상태로, 하나가 주가 되어서 관리
중복 인스턴스 방지 : 싱글톤 패턴을 사용하면 오직 한 개의 인스턴스만 생성되므로, 중복 인스턴스를 방지할 수 있다. 이는 리소스 낭비나 예기치 않은 동작을 방지하는 데 도움이 된다.
객체 간 편한 통신 : 싱글톤 인스턴스는 어디서든 접근할 수 있으므로, 객체 간의 통신이 편리해진다. 다른 객체에서 싱글톤 인스턴스를 사용해 데이터를 공유하거나 메서드를 호출할 수 있다. (!!싱글톤에 너무 의존하면, 단일책임원칙이 망가지고, 유지보수성이 떨어지고, 결합도가 높아진다.)
유지보수 및 확장성 : 싱글톤 패턴은 코드의 유지보수성과 확장성을 향상시킨다. 인스턴스에 대한 접근이 중앙 집중화되므로, 코드의 변경이나 기능의 추가/변경이 용이해진다. 또한, 싱글톤 인스턴스를 사용하는 객체들 사이의 결합도를 낮출 수 있어 유지보수성을 향상시킨다.
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
void Awake()
{
if (Instance == null)
{
Instance = this;
}
else
{
Destroy(gameObject); // 중복 인스턴스가 생성될 경우 제거
}
}
}
씬 전환시 주의점 : 라이프사이클은 씬마다 발생하지 않는다.
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
void Awake() // 게임오브젝트가 처음으로 켜진 그 순간
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject); // 씬 전환 시 파괴되지 않도록 설정
}
else
{
Destroy(gameObject); // 중복 인스턴스가 생성될 경우 제거
}
}
public void Init(){
// 초기 세팅
}
}
using UnityEngine;
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
private static T _instance; // Lazy Loading
public static T Instance
{
get
{
if (_instance == null)
{
_instance = FindObjectOfType<T>();
if (_instance == null)
{
GameObject obj = new GameObject();
// obj.name = typeof(T).Name;
_instance = obj.AddComponent<T>();
}
}
return _instance;
}
}
}
public class AudioManager : Singleton<AudioManager>
{
public void PlaySound(string soundName)
{
// 사운드 재생 로직
}
}
상호 의존성 증가 : 싱글톤 인스턴스가 여러 부분에서 사용될 경우, 클래스 간의 상호 의존성이 증가하며, 이는 시스템의 결합도를 높이고 응집도를 낮출 수 있다.
코드의 복잡성 : 싱글톤을 과도하게 사용하면 시스템 전반에 걸쳐 복잡도가 증가할 수 있으며, 이는 유지보수를 어렵게 만든다.
여러 개 존재하는 싱글톤 (static, 중복검사 등을 제외하여 중복이 발생하는 싱글톤 지양)
일반적인 클래스에 Manager라는 이름 붙이기 (싱글톤은 대부분 XManager 처럼 작성)
모든 문제를 싱글톤으로 해결하려고 하는 경우 (SRP)