- 유니티로 최적화를 하다보면 Tags and Layers에서 다양한 Tag와 Layer를 추가해서 사용 합니다. 이번에는 그 중에서 다양한 물리연산과 관련된 Layer에 대해서 알아보겠습니다.
- Layer Mask의 스크립트 코드와 Scripting API의 내용을 참고로 하여 작성하였습니다.
Layer Mask 내부구조
- Edit->Settings->Tags and Layers에서 수정을 하고 추가하는데 Tags나 Sorting Layer는 가변길이를 갖기 때문에 계속 추가하여 사용하지만 Layer는 32개의 고정된 길이를 갖기 때문에 이미 셋팅된 값들을 제외하면 최대 27개의 레이어를 추가 할 수 있습니다.
내부 구조를 보면 다음과 같이 LyaerMask는 struct이고 m_Mask라는 정수형 프로퍼티를 하나 가지고 있습니다.
public struct LayerMask
{
[NativeName("m_Bits")]
private int m_Mask;
public static implicit operator int(LayerMask mask) => mask.m_Mask;
public static implicit operator LayerMask(int intVal)
{
LayerMask layerMask;
layerMask.m_Mask = intVal;
return layerMask;
}
}
- 그리고 오퍼레이터를 통해 LayerMask를 int에 대입하면 m_Mask값이 대입되고 int를 LayerMask에 대입하면 LayerMask를 생성하고 m_Mask값에 int값을 대입하게 됩니다. m_Mask를 비트플래그로 활용하면 총 32개의 Layer를 플래그 연산을 통해 간편하게 비교하고 사용 할 수 있습니다.
Layer Mask활용
- 유니티에서는 Physics옵션의 Layer Collision Matrix에서 필요없는 부분을 제거하여 물리연산을 쉽게 최적화 할 수 있습니다.

- 그리고 다음과 같이 여러가지 물리연산에 활용 가능합니다.
LayerMask targetLayer = LayerMask.GetMask("Enemy");
var hitTargets = Physics2D.CircleCastAll(myPos, radius, direction, distance, targetLayer);
value, GetMask, NameToLayer
public int value
{
get => this.m_Mask;
set => this.m_Mask = value;
}
public static unsafe int NameToLayer(string layerName)
{
try
{
ManagedSpanWrapper managedSpanWrapper;
if (!StringMarshaller.TryMarshalEmptyOrNullString(layerName, ref managedSpanWrapper))
{
ReadOnlySpan<char> readOnlySpan = layerName.AsSpan();
fixed (char* begin = &readOnlySpan.GetPinnableReference())
managedSpanWrapper = new ManagedSpanWrapper((void*) begin, readOnlySpan.Length);
}
return LayerMask.NameToLayer_Injected(ref managedSpanWrapper);
}
finally
{
__unpin(begin);
}
}
public static int GetMask(params string[] layerNames)
{
if (layerNames == null)
throw new ArgumentNullException(nameof (layerNames));
int mask = 0;
foreach (string layerName in layerNames)
{
int layer = LayerMask.NameToLayer(layerName);
if (layer != -1)
mask |= 1 << layer;
}
return mask;
}
- 우선 value는 m_Mask을 리턴 합니다. m_Mask는 1 << layer(0~31까지의 인덱스 값)와 같습니다.
LayerMask.NameToLayer는 레이어 이름에 해당하는 레이어 인덱스 값을 리턴합니다. 그리고 LayerMask.GetMask()는 LayerMask.NameToLayer로 인덱스값을 받아 비트연산을 통해 결국 해당 레이어의 value값과 같은 값을 리턴하게 됩니다.
var targetLayers = LayerMask.GetMask("Layer1", "Layer2");
var hitTargets = Physics2D.CircleCastAll(myPos, radius, direction, distance, targetLayer);
- 그리고 input값을 배열로 받기 때문에 LayerMask.GetMask("layer1", "layer2")와 같이 여러 레이어를 입력하면 각 레이어의 인덱스를 비트연산을 통해 합쳐서 하나의 int값으로 리턴하게 됩니다.
주석에 해당하는 부분과 같이 각레이어의 value값을 저장해두고 필요할 때 합쳐서 사용할 수도 있습니다.
int layer1Number = LayerMask.NameToLayer("Layer1");
int layher2Number = LayerMask.NameToLayer("Layer2");
int targetLayers = 1 << layer1Number | 1 << layer2Number;
var hitTargets = Physics2D.CircleCastAll(myPos, radius, direction, distance, targetLayers);
- 그리고 인덱스를 저장해두고 사용할 수도 있습니다.
주의 사항
- GetMask도 결국 NameToLayer를 사용하기 때문에 자주 사용하면 CPU의 부하를 주게됩니다. 그래서 미리 저장 해 두는 것이 좋습니다. 저장할 때 int로 저장 후 필요 시 LayerMask로 대입하게 되면 구조체를 생성하게 되므로 주의가 필요합니다. Parameter가 LayerMask라면 의도치 않은 비용이 발생 할 수 있습니다.
- 레이어를 저장해 둘 때 LayerMask layerMask = LayerMask.GetMask("LayerName");
과 같이 선언과 동시에 불러오게 되면 UnityException: NameToLayer is not allowed to be called from a MonoBehaviour constructor (or instance field initializer), call it in Awake or Start instead. Called from MonoBehaviour 'ObjectManager' on game object 'ObjectPool'.와 같은 에러 메세지를 볼 수 있습니다. Construct에서 초기화 하지 말고 Awake나 Start에서 초기화 해야 합니다.
- LayerMask는 오퍼레이터를 통해 int로 자동 변환 되기 때문에 다양한 연산이 가능합니다. 자주 사용하게 되면 작지만 프로퍼티 접근비용이 발생하기 때문에 주의할 필요가 있습니다. 이전 글 중 관리되는 힙에 배열기반 Unity API부분을 참고 해주세요. 그리고 사용하기 편해서 여려가지를 섞어 쓰다보면 박싱과 언박싱으로 cpu에 영향을 주기 때문에 자주 호출 되는 부분은 주의해서 사용해야 합니다.