[20260209] LayerMask/RenderTexture/UI

SmartBear·2026년 2월 9일
post-thumbnail

LayerMask

Layer

  • 오브젝트는 단 1개의 레이어만 가질 수 있다.
  • LayerMask.NameToLayer("NAME")
  • 이름을 통해 레이어 인덱스(0-31)를 가져옴.

Layer Mask

  • 사용 예시
// 특정 레이어 선택시
int layerMask = 1 << LayerMask.NameToLayer("Emeny");

// 다수의 레이어 선택시
LayerMask mask = LayerMask.GetMask("Name1", "Name2");


// example
if (Physics.Raycast(ray, out hit, distance, layerMask)) { ... }

Layer Mask 함수 종류

  1. LayerMask.GetMask: 레이어 이름 묶음을 가져오는 함수
LayerMask mask = LayerMask.GetMask("Name1", "Name2");
// Name1 이 8번 레이어면?
// 1 << 8 (256)
  1. LayerMask.NameToLayer ; 이름으로 숫자
int layermask = LayerMask.NameToLayer("Name1"); 
  1. LayerMask.LayerToName ; 숫자로 이름
string name = LayerMask.LayerToName(8);
  1. LayerMask.value ; 비트마스크 값을 나타냄. 주로 비트 연산 수행(|&^~).
int bitMask = enemyLayer.value | obstacleLayer.value;

Bit 연산의 원리

  • 1 << n : n번 Shift / n번 레이어의 스위치를 켠다(?)
  • |(OR) : 필터에 레이어를 추가 한다.
  • &(AND) : 오브젝트가 포함되어 있다. (Filter)
  • ~(NOT) : 지정한 레이어 제외한 나머지 다 선택. (잘 안씀)
int layer = 7; // 0, 1, 2 를 말한다.
int layer = 1 << 7; // 이렇게 해야 7번만 선택한다.

Culling Mask

  • 카메라에서 랜더링을 켜고 끄는 역할

Project Setting / Physics Settings

  • Layer 간 충돌 처리 연산 수행 여부 셋팅

특정 Layer 포함 여부 체크 (& AND)

public static class Extentions 
{
    public static bool Contains(this LayerMask mask, int layer) 
    {
        return (mask.value & 1 << layer) != 0;
    }

    public static bool Contains(this LayerMask mask, GameObject obj) 
    {
        return (mask.value & 1 << obj.layer) != 0;
    }
}

//
if (layerMask.Contains(mask, 8)) { ... }

Test Scenario

Monster 를 때렸을 때, 공격 가능 여부 체크

if (mask.Contains('Enemy', 몬스터)) { ... }
  • 결국.. 상호작용 가능 여부.

왜 Bitmask 를 사용할까?

if (target == "" || target == "" || ...) // 너무 많은 비교 연산

if (mask.Contains(target, layer)) // 간단하고 빠른 bit 연산

Render Texture

메인 카메라 외 다른 카메라 하나로 동시에 보는 것.
즉, 랜더링 두배가 될 수도 있어 굉장히 무겁고 비쌀 수 있다.

왜 배울까?

  • CCTV, MiniMap, Scope, Potal 등 다른 화면을 동시에 봐야함.

흐름 도식화

[Sub Camera] -> [Target Texture] -> [Render] -> [Meterial or UI -> RawImage]

예시

  • 미니맵

  • CCTV

  • Scope

간단한 팁?

  • 카메라로 보였다가 카메라만 끄면 바로 전 프레임의 잔상이 그대로 남아있음 (사진 마냥)

UI 기본과 최적화

UI 의 기본 좌표

Rect Transform

  • Anchor: 부모 해상도가 변할 때, 부모 기준으로 상대 위치를 결정.
  • Pivot: 내가 회전하거나 커질 때, 어디를 기준으로 움직일 것인가(자신 기준)

Canvas UI 처리 최소 단위

Canvas Render Mode

  • Screen Space-Ovelay

    • 카메라와 상관없이 화면 가장 위 레이어 그림.
  • Screen Space-Camera⭐⭐

    • 특정 카메라 앞에 일정 거리에 배치.
    • 연출용 UI, 파티클이 섞인 UI
  • World Space⭐⭐

    • 씬내 3D 오브젝트처럼 활용하는 UI
    • 오브젝트 체력, 데미지 표현 등

Canvas Scaler

  • Constant Pixel Size

    • 해상도가 바뀌어도 UI가 사용하는 픽셀 수 유지.
  • Scale With Screen Size ⭐⭐⭐

    • 주로 모바일 게임에서 많이 사용 (사실상 표준)
  • Constant Physical Size

    • 화면 해상도와 상관없이 cm/inch 크기를 유지.

주의 컴포넌트

  1. Layout Group; 고정적이지 않으면 되도록 사용하는 것은 자제.
  2. Text -> 대안; Text Mesh Pro (Polygon 을 새로 굽지 않아 성능적으로 좋음.)
    • UI가 변화하는 상황에 Legacy Text 대비 효율적으로 변하였음.

Profiler 사용법 - 최적화를 하자

  • ProfileUI 파트 확인.
  • Layer: UI의 위치와 크기를 계산
  • Render: 실제 Mesh 생성 시간
  • 체크 요소
    • Canvas.SendWillRenderCanvases

정리

  • Anchor vs Pivot 개념
  • Rect Transform
  • Canvas Render Mode
  • Canvas Scale Mode
  • Device Simulator
  • (+) Profiler 로 UI 병목 현상 확인

실습 주제

기본

  • Mission1
    • Object Cube, Sphere 생성
    • LayerMask 선언 (서로 다르게)
    • Capsule (Player) 가 Ray 를 forward 방향으로 쐈을 때, Cube 와 Sphere 의 맞는 대상을 1개만 하기.
using UnityEngine;

public class Missions : MonoBehaviour
{
    private Ray _ray;
    private Vector3 _rayPosOffset;

    private void Start()
    {
        _rayPosOffset = new Vector3(0, 0.5f, 0);
        _ray = new Ray(transform.position + _rayPosOffset, transform.forward);
    }
    
    private void Update()
    {
        DetectCube();
    }

    private void OnDrawGizmos()
    {
        Gizmos.color = Color.red;
        Gizmos.DrawRay(_ray.origin, _ray.direction * 20f);
    }

    private void DetectCube()
    {
        _ray.origin = transform.position + _rayPosOffset;
        _ray.direction = transform.forward;
        
        LayerMask layerMask = LayerMask.GetMask("Cube");
        RaycastHit hit;
        if (Physics.Raycast(_ray, out hit, 20f, layerMask))
        {
            Debug.Log(hit.collider.gameObject.name);
        }
    }
}
  • Mission2
    • Project Setting 에 Layer 간 충돌 설정에 의한 동작 확인 하기
  • Mission3
    • Extentions 로 제작한 코드 활용 예제 해보기.
      -> 심화와 엮어 진행.

심화

  • Mission4
    • 원 2개 (Sphere Layer).
    • 큐브 2개 (Cube Layer).
    • 키보드 1번을 누르면 Cube/Sphere 에게 독데미지 부여 (코루틴 활용. 디버그 로그 활용)
    • 키보드 2번을 누르면 Cube 만 독이 풀린다.
// mission.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

public class Missions : MonoBehaviour
{
    private List<(Coroutine cor, Collider col)> _poisionCorouts;

    private void Awake()
    {
        _poisionCorouts = new List<(Coroutine, Collider)>();
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Alpha1)) AllTargetsForPoision();
        if (Input.GetKeyDown(KeyCode.Alpha2)) CurePoisionOnlyCube();
    }
    
    private void Start()
    {
        _rayPosOffset = new Vector3(0, 0.5f, 0);
        _ray = new Ray(transform.position + _rayPosOffset, transform.forward);
    }
       
    private void AllTargetsForPoision()
    {
        LayerMask cubeSphereLayer = LayerMask.GetMask("Cube", "Sphere");
        LayerMask layerMask;
        Collider[] colliders = Physics.OverlapSphere(transform.position, 20f);
        foreach (Collider col in colliders)
        {
            layerMask = col.gameObject.layer;
            if (layerMask.Contains(cubeSphereLayer))
            {
                Status status = col.GetComponent<Status>();
                if (!status.IsPoision)
                {
                    _poisionCorouts.Add((StartCoroutine(Poision(col.gameObject)), col));
                    status.IsPoision = !status.IsPoision;
                }
            }
        }
    }
    
    private void CurePoisionOnlyCube()
    {
        LayerMask layerMask;
        int cubeLayer = LayerMask.NameToLayer("Cube");
        foreach ((Coroutine cor, Collider col) in _poisionCorouts)
        {
            layerMask = col.gameObject.layer;
            if (layerMask == cubeLayer)
            {
                StopCoroutine(cor);
            }
        }
    }

    private IEnumerator Poision(GameObject obj)
    {
        while (true)
        {
            Debug.Log($"{obj.name}은(는) 중독되었다.");
            yield return new WaitForSeconds(0.5f);
        }
    }
}

// Status.cs
using UnityEngine;

public class Status : MonoBehaviour
{
    public bool IsPoision;
}

번외

이왕에 독 데미지를 전역으로 주는거 데미지를 띄워보자 라는 생각으로 AI 함께 진행해 본 것

// DamageController.cs -> 싱글톤
using System.Collections.Generic;
using UnityEngine;

public class DamageController : MonoBehaviour
{
    public static DamageController Instance; // 어디서든 접근 가능하게 싱글톤

    public GameObject textPrefab; 
    public Transform canvasParent; 

    private List<OnOffDamage> pool = new List<OnOffDamage>();

    void Awake()
    {
        Instance = this;
    } 

    public void ShowDamage(int amount, Vector3 worldPos)
    {
        OnOffDamage targetText = null;

        foreach (var t in pool)
        {
            if (!t.gameObject.activeSelf)
            {
                targetText = t;
                break;
            }
        }

        if (targetText == null)
        {
            GameObject obj = Instantiate(textPrefab, canvasParent);
            targetText = obj.GetComponent<OnOffDamage>();
            pool.Add(targetText);
        }

        targetText.Setup(amount, worldPos);
    }
}

// OnOffDamage.cs -> 실제 Text 띄우는 것
using TMPro;
using UnityEngine;

public class OnOffDamage : MonoBehaviour
{
    private TextMeshProUGUI textMesh;

    void Start()
    {
        textMesh = GetComponent<TextMeshProUGUI>();
    }

    public void Setup(int amount, Vector3 worldPos)
    {
        if (textMesh == null)
        {
            Debug.LogWarning("TextMeshProUGUI was not assigned!");
            textMesh = GetComponent<TextMeshProUGUI>();
        }
        textMesh.text = $"- {amount}";
        transform.position = Camera.main.WorldToScreenPoint(worldPos + Vector3.up * 0.5f);
        gameObject.SetActive(true);
        Invoke("DisableText", 1f);
    }

    void DisableText()
    {
        gameObject.SetActive(false);
    }
}

// status.cs
using UnityEngine;

public class Status : MonoBehaviour
{
    public bool IsPoision;
    
    public void TakeDamage() // 간단히 추가
    {
        DamageController.Instance.ShowDamage(10, transform.position);
    }
}

Object Polling싱글톤을 활용하였음. 데미지로 보여져야하기 때문에 Font 등이 모두 같은 크기로 보여지는 것이 좋을 것이라 생각하여 Canvas 의 RenderMode 는 Screen Space - Overlay 를 채택.

진행하며 의문 사항에 대해 몇가지 AI 에게 질문하였고, 그 중 유의미한 질문과 정리는 아래와 같음

Q. Object Polling 에 의해 캐쉬로 생성된 Object 들은 결국 CleanUp 이 되어야 할 텐데, 어떤 기준으로 어느 타이밍에 하는 것이 적절 한가?
A. "시간"에 따른 CleanUp 보다는 "사건"에 따른 CleanUp 이 더 유효하다.

Q. 자료 구조는 보통 어떻게 가져가는게 좋나?
A. Queue 가 가장 많이 선호되며, LinkedList 가 쓰이는 경우도 있다. 동일한 Object Type 당 하나의 Queue 로 관리하는 것이 보통이다. Dictionary<Object, Queue>

profile
Python Dev with Infra -> Game Programmer

0개의 댓글