본문 바로가기

Unity 개발 공부

[내배캠] 본캠 23일차. 싱글턴, 패턴매칭

처음보는 코드 공부
Collider2D hit = Physics2D.OverlapBox(randomPos, obstacleSize, 0f, obstacleLayer);
2d 물리엔진(Physics2d) 에서 사각형 박스영역에 최초로 겹치는 collider2d를 리턴하는 함수이다.

매개변수
randomPos(vector2 또는 vector3)
박스의 중심좌표
obstacleSize(vector2)
박스의 가로세로크기(폭,높이)를 나타냄
0f(float angle)
박스를 얼마나 회전시킬지 지정, 여기서는 회전없이 축정렬된 박스
obstacleLayer(layerMask또는 int)
어떤 레이어에 속한 콜라이더만 검사할지 필터링하는 값

반환값 : 겹치는 콜라이더가 있으면 그 collider2d를 반환한다
없으면 null을 반환한다.

// 1) LayerMask 세팅
// 프로젝트 설정 > Layers 에서 "Obstacles" 라는 레이어를 만들었다면:
int obstacleLayer = LayerMask.GetMask("Obstacles");

// 2) 검사할 박스 정보
Vector2 center   = new Vector2(5f, 2f);      // 검사할 위치
Vector2 size     = new Vector2(1.5f, 0.8f);  // 폭 1.5, 높이 0.8
float   angle    = 0f;                       // 회전 없음

// 3) 겹치는 콜라이더 찾기
Collider2D hit = Physics2D.OverlapBox(center, size, angle, obstacleLayer);

if (hit != null)
{
    Debug.Log($"장애물({hit.name})과 겹침!");
}
else
{
    Debug.Log("겹치는 장애물 없음");
}

반환값의 타입은 Collider2D고,
값은 겹치는콜라이더가 있다면 바로 그 콜라이더다.
int obstacleLayer = LayerMask.GetMask("Obstacles");를 살펴보자면
내부적으로는 비트마스크(int)로 처리되어,
마스크에 포함된 레이어에 속한 콜라이더만 검사 대상으로 삼는다
따라서 Obstacles 레이어에 속하지 않은 콜라이더는 처음부터 아예 리턴 대상이 아니다.

에디터에서 아래의 함수를 붙여둬서 박스시각화도 가능하다.
void OnDrawGizmosSelected()
{
    Gizmos.color = Color.red;
    // OverlapBox와 같은 형태로 그리기
    Gizmos.DrawWireCube(randomPos, obstacleSize);
}

다수의 충돌체를 모두 얻고싶다면
Collider2D[] hits = Physics2D.OverlapBoxAll(center, size, angle, obstacleLayer);
다른모양을 쓰고싶으면
OverlapCircle
OverlapCapsule
이런것들도 있다.

 활용예제로는 이러하다.
// 예시: Inspector에서 레이어 지정
[SerializeField] private LayerMask obstacleLayer;  

private void TrySpawnObstacle(Vector2 randomPos, Vector2 obstacleSize)
{
    // 박스에 겹치는 'Obstacles' 레이어의 콜라이더가 있는지 검사
    Collider2D hit = Physics2D.OverlapBox(randomPos, obstacleSize, 0f, obstacleLayer);

    if (hit != null)
    {
        // ① 해당 오브젝트의 이름을 찍어보고
        Debug.Log($"겹친 장애물: {hit.name}");

        // ② 그 오브젝트의 스크립트나 컴포넌트에 접근 가능
        var obsScript = hit.GetComponent<Obstacle>();
        if (obsScript != null)
        {
            obsScript.HandleCollision();
        }

        // ③ 게임오브젝트 자체를 활성/비활성하거나 파괴
        // hit.gameObject.SetActive(false);
        // Destroy(hit.gameObject);
    }
    else
    {
        Debug.Log("장애물과 겹침 없음, 안전하게 스폰 가능");
        // 실제 장애물 스폰 로직 실행...
    }
}

// 디버깅용: 씬 뷰에서 박스 시각화
private void OnDrawGizmosSelected()
{
    Gizmos.color = Color.yellow;
    Gizmos.DrawWireCube((Vector3)randomPos, obstacleSize);
}

hit.gameObject, hit.transform, hit.attachedRigidbody 등으로 해당 오브젝트와 상호작용가능하다.


---------------------------
수준별학습 스탠다드반 강의

1. 유니티와 컴포넌트
패턴: 특정한 문제를 해결하기위해 사용되는 일반적이고 재사용가능한 해결책.
소프트웨어 개발에서 패턴은 특정맥락에서 문제를 해결하는데 도움이 되는 테스트된 모범사례들 집합.
디자인패턴, 아키텍쳐패턴, 프로그래밍 패턴등 다양한 유형의 패턴이 있음..

1-1 
디자인패턴
소프트웨어 디자인에서 발생하는 문제들에 대한 해결책을 표준화함
객체지향프로그래밍에서  주로 사용됌

예를들면
-싱글턴패턴
: 오직 하나의 인스턴스만 생성하고 이에 접근하는 패턴
-팩토리 메서드 패턴
: 객체를 생성하고 인터페이스를 정의하고 이를 서브클래스에서 구현하여 객체의 생성을 지연시키는 패턴
-옵저버패턴
:객체간의 일대 다 종속성을 정의하여 어떤 객체의 상태가 변경될때 그 객체에 종속된 모든 객체에게
퀘스트를 만들때 이벤트코드를 받기때문에... 많이씀

알림을 보내는 패턴
-스트래티지패턴
:알고리즘을 정의하고 각각의 알고리즘을 캡슐화하여 교체가능하게 만드는패턴(전략패턴)


아키텍쳐패턴 -- 게임에 적용하기는 쉽지않다.
아키텍쳐 패턴은 소프트웨어의 전체 구조나 시스템 간의 관계를 정의하는 패턴이다.
주로 대규모 소프트웨어의 설계에 사용된다.

예시:
-MVC 패턴 (Model-View-Controller Pattern)
: 모델, 뷰 , 컨트롤러로 구성되며, 사용자 인터페이스를 구성하는 패턴

사실 아키텍쳐패턴은 유니티에서 쓰기에 한계가있음. 연습을 시작은 이걸로할수는 있다.
데이터를 주고받고 유저간 소통하는 걸 구현하려다보면 결국 이 패턴은 깨지게 되기는함
웹에서만 자연스럽게 가능함.

-MVVM 패턴
: 모델, 뷰, 뷰모델로 구성되어 사용자 인터페이스와 비즈니스 로직을 분리하는 패턴

-레이어드 아키텍쳐패턴
:소프트웨어를 여러 레이어로 나누어 각 레이어 간의 의존성을 관리하는 해턴


프로그래밍 패턴
프로그래밍 패턴은 특정 프로그래밍 언어나 도구에 특화된 소프트웨어 디자인패턴이다.
예시:
-callback 패턴:
함수나 메서드를 다른 함수에 전달하여 특정이벤트가 발생할때 호출되도록 하는 패턴
-비동기 프로그래밍 패턴:
비동기적인 작업을 처리하기 위한 패턴, callbakcs, promises, async/await 등이 포함된다.


컴포넌트 패턴과 유니티
기능단위로 만들자. 하나의 오브젝트에 컴포넌트화 해서 처리하겠다라는 뜻
하나의 오브젝트를 구성할때 자유롭게 구성해나갈수있다라는 점
단점은 레고처럼 개별조각이라 컴포넌트끼리는 서로가 서로를 인지하지못한다는점 
스크립트를 통해 이들간 소통을 만들어주는 중화역할을해야하는것이다.

구성인원
-게임오브젝트
-컴포넌트종류
transform 위치,회전, 크기정보를 가지며 게임오브젝트의 위치를 결정
renderer 렌더링과 관련된 기능을 제공하며 시각적인 표현을 담당
collider 충돌과 관련된 기능을 제공하며 물리엔진과 상호작용
script 사용자가 작성한 스크립트를 부착하여 게임오브젝트의 동작을 제어
그외에도 다양한 컴포넌트 존재함

컴포넌트패턴의 장점
a: 모듈화와 재사용성 : 각 컴포넌트는 독립적으로 동작하며 필요한 기능을 제공한다
이로써 모듈화와 재사용성이 높아지게된다
b: 유연성과 확장성 : 새로운 기능이나 컴포넌트를 추가하기 쉽다. 이미 존재하는 게임오브젝트에
새로운 기능을 부여하기위해 단순히 새로운 컴포넌트를 추가하면된다.
c: 가독성과 유지보수성: 각 컴포넌트가 특정한 역할을 수행하기 때문에 코드가 간결, 유지보수가 쉽다.

많은 정보들은 Unity documentation에 사실 다 정리가 되어있는편이다.
Unity documentation을 확인하고
scripting api에 있는 기능들의 역할을 보고 작업하면 너무 유용하고 공부가 잘된다.
Rigidbody.Addforce 이런거 눌러보면 뜻, 활용예제 이런거 다있음..
https://docs.unity.com/
https://docs.unity3d.com/ScriptReference/index.html

취향것하지만 클래스네이밍을 이런식으로할수는있다.
컨트롤러: 주체적인동작이있을때.
핸들러: 컨트롤러를 지원해줄때.



2. 벡터와 좌표계
Vector3로 사용하는게 수월하기는하지만
프로젝트에 따라 vector2를씀

덧셈
Vector3 sum = vectorA + vectorB;
두좌표의 끝점을 이어서 도달하는 지점을 말한다.

뺄셈
A -B  -> A + (-B) 
마찬가지로 끝점을 이어붙여서 도달하는 지점을 말하는데 이것은 벡터를 옮겨도 똑같다
뺀 값에서 시작한 값(A)를 바라본다라고 인지하면된다. 

노름 (Norm)
벡터의 길이를 1로 만든다. 정규화
float magnitude = vectorA.magnitude; 이건 길이, 크기

제곱근은 코드에서 생각보다 무거움
그래서 제곱근을 안푸는것이
float sqrMagnitude = vectofA.sqrMagnitude;

sqrMagnitude > 5 * 5 이냐 물어보는게 위에 magnitude > 상수값으로 비교하는것보다 저렴하다.


좌표계, 로컬좌표, 월드좌표
Transform.localPosition을 자주써주자.

3.싱글턴패턴을쓰기위해서
static은 데이터영역에 저장된다. 데이터영역은 정적변수와 전역변수가 저장되는공간이다.
프로그램 시작시 할당되고 종료될때 소멸된다.

manager급만 주로 붙여서 쓰는게좋다.
예를들면 player가 싱글톤으로 관리해야하는구조로 갔다면 썩좋은구조는아니다.

일반적인싱글톤중에 제네릭싱글톤도 있다.
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 PlayerSound (string soundName)
{
// 사운드 재생로직
}

}

뭐 이런식으로 상속시켜서 쓸수있다.

4.유니티안에는 라이프 사이클 존재, start와 awake 구분사용

awake, start 둘다 한번만 호출되며, 초기화 및 시작로직을 담당한다.
awake는 외부참조를 이용한 초기화는 하지않음, 자기자신만 초기화하는게 안꼬이고 좋다.
a,b 클래스 둘다 서로를 초기화하길 원하는상황에서 awake에서 초기화하면 순서가 꼬일확률이 높다.
내가 가진값을 초기화한다거나, getcomponent를 한다거나 하는것은 awake에서해주고
그이외에 싱글턴 객체를 참조해야한다거나, 내가 가지고온 컴포넌트에서 새로운값을 참조해야한다거나, 다른클래스
참조를해야하는경우는 start에서 이루어지게하는게좋다.

나중에 초기화 새로운 시점을 만드는경우도많다.
init 관련된 코드를 통해 직접 손수 초기화하는경우도 매우 많음.

nullreferenceexception 오류를 방지하는 필수적인 방법이다.

참고로 안쓰는 유니티메서드들은 지워야한다. 계속 비어있어도 호출하기때문에 최적화를 위해서말이다.

5. 델타타임
전프레임이 완료된후 현재프레임이 시작된 시점부터 현재까지의 시간을 나타낸다.
초당프레임수가 높을수록 Time.deltaTime 값은 작아진다.
따라서 프레임 레이트의 변화에 따라 게임이 너무 빠르거나 느린경우를 방지한다.
예를들면 30fps면 간격은 1/30 초
15fps면 1/15초로 간격이 좀더 넓어진다.


update에서만 쓸수있음 - fixedupdate 같은데서 다른데서도 돌아는가는데 부정확하게 흘러갈것

FIxedUpdate에 물리적인 처리를 많이넣는다. 고정된 시간을 이용하기때문에 안정적이다.
fixedDeltatime 를 쓸수있다.
 
6. 프리팹
재사용성있는 애를 프리팹화했고 복제본으로 사용하기위하여 설정함

한씬에서 여러명이작업하기어려움.
프리팹을 잘활용하자. 씬에서 변동사항이없으면 원본에서 수정하면 자동반영된다.
내작업물을 씬에 저장하는게아니라
프리팹화해서 넣으면됌. 그걸통해 내작업물을 보존하는것.

프리팹화해서 수정하면 나중에 프리팹을 씬에 추가할때 좌표나 스케일도 저장한 그대로 씬에출력된다, 자식오브젝트들도
단 UI는 해상도 좌표를 따온다, 월드에 생성하고 UI canvas로 넘겼을때 스케일이달라진다.
그래서 canvas로 부모를 바꿀때 앵커처리를하는등 초기화해줘야한다.

7. 씬전환시 데이터유지

PlayerPrefs를 이용함 - 윈도우로따지자면 registry 의 느낌
Player preferences 를 줄인말이며
어디에 저장되는지
어떤 메서드들이 있는지 documents를 보고 활용해보자.

DontDestroyOnLoad 메서드를 이용해서 씬전환시 파괴되지않도록 설정할수도있음
항상 기능에 집중하면좋을듯. 너무 편의적으로 하려고
이걸 남용하진말자.
static변수를 사용할수도있지만 마찬가지다. 남용하지말자.
계속 변해야하는 기능,값들이라면 따로 관리하자.

8. 레이어마스크
충돌과 관련된 주요한 내용이다.

충돌을 구분해내는 용도다.

레이어마스크설정
레이어 생성 및할당
edit > project settings > tags and layers 에서 새로운 레이어를 생성하고
각 게임 오브젝트의 layer속성에서 해당 레이어를 선택한다.

레이어마스크 설정
코드에서 사용할 레이어 마스크값을 정의한다.
int layerMask = 1 << LayerMask.NameToLayer("YourLayerName");
코드에서는 2진값으로 변환해서 쓴다. 


레이케스트와 레이어 마스크사용
Physics.Raycast를 통한 레이캐스트
Physics.Raycast 함수를 사용하여 레이캐스트를 수행하고 마스크를 설정한다.

Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
int layerMask = 1 << LayerMask.NameToLayer("YourLayerName");

if(Physics.Raycast(ray, out hit, Mathf.Infinity, layerMask)) // 내가 원하는 레이어마스크와만 충돌할수있다.
{
//특정 레이어와 충돌한경우
Debug.Log("Collided with the specified layer.");
}

예를들어보자
유니티상 3번레이어라고할때
1000 // 오른쪽 가장 작은 값 기준 0의 세번째 자리수에 1이뜨게된다
충돌했을때
1000과 비교하여
and든 or 연산이든 수행하여
충돌해도되는지 안되는지 구분해내는것이다.

3, 2번 두개의 레이어를 원할때는
1000
100
뭐 이런상태일거다.
그렇다면 3번 2번 레이어를 더해줘야하는데 이진수에서의 덧셈을하면 값이 바뀌어버린다.
그게아니라 or 처리를해준다
둘중에 하나라도 1이면 켜지는거다
1100 이렇게되면 되는것이다.
그리고 충돌을했을때
100 이런애랑 만났다고쳐보자
그럴때 또 and 연산을하면
둘다 1일때만 1을 반환하니까
100이 나온다 그렇다면 아 100이랑 100이랑 똑같네, 충돌할수있는애구나 하고 판단하게된다.

Tag는 스트링비교니까 추천을 하지는않는편...
layer를 쓰는걸 추천하는데 layer는 팀이라고생각하기, 나중에 팩션이라는 개념이 생길수있기때문에 크게 만드는걸 생각하기.
layer를 생명체라고 크게 생각하고 만들어서 충돌판정나게하고 그안에서 player, enemy를 구분하는 값은따로
작게 코드화할수도있다.
생명체를 인식하고, 공통된 코드에서 어떤 진영인지알게하고, 나랑 다른진영이라면 공격하겠다 라는식으로
구조를 짤수있다. 

다시돌아와 아래와같은 예제를 해석해보자. 모르는 부분이많으니 하나씩 부분부분 공부한다.
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
int layerMask = 1 << LayerMask.NameToLayer("YourLayerName");

if(Physics.Raycast(ray, out hit, Mathf.Infinity, layerMask)) // 내가 원하는 레이어마스크와만 충돌할수있다.
{
//특정 레이어와 충돌한경우
Debug.Log("Collided with the specified layer.");
}

전체적인의미는 화면상의 좌표(마우스위치)를 3d 월드 공간의 레이(광선)으로 바꿔서 그 레이가
어떤 오브젝트와 부딪히는 검사하는 흐름이다.

Ray ray = Camer.main.ScreenPointTORay(Input.mousePosition);

Camera.main은 씬에서 MainCamera 태그가 붙은 카메라 컴포넌트를 뜻한다.
Input.mousePosition은 화면좌표계에서 마우스 포인터의 위치를 Vector(x,y,0)으로 바꿔준다
화면왼쪽아래가 좌표 (0,0) 오른쪽위가 (screen.width, scree.right) 인걸 기준으로한다.

ScreenPointToRay(...)
스크린 좌표를 받아서
카메라 위치에서 그 화면 지점을 통과하는 광선(ray)를 만들어준다.

결과물인 ray 구조체는
struct Ray {
  public Vector3 origin;    // 광선이 시작되는 3D 위치
  public Vector3 direction; // 그 지점에서 쏘아올려지는 방향 벡터
}
처럼되어있따.
마우스 위치 → 화면 좌표 → 카메라에서 뻗어나가는 3D 광선
좀더 얘기해보자면, Input.mousePosition은 스크린좌표다. 
ScreenToWordPoint 이 마우스좌표를 월드좌표로 변환해주는 역할을하는걸 본적이있다
ScreenPointToRay는 단순히 픽셀좌표를 월드좌표 한점으로 변환해주는게아니라
그 픽셀위치를 통과하는 광선 자체를 만들어주는 메서드다
스크린 좌표가 카메라의 투영행렬을 통과해 월드 공간에서 어디에 대응되는지 계산하고
카메라의 월드위치를 origin으로, 그 대응지점과 카메라 위치를 잇는 방향벡터를 distance으로 갖는 ray를 리턴한다.
“마우스가 찍힌 픽셀” → 2) “카메라가 보는 가상의 화면 면(near clip plane) 위의 점” → 3) “카메라 위치에서 그 점을 향해 뻗어나가는 광선”

Camera.ScreenToWorldPoint(new Vector3(x,y,z))는
스크린 (x,y)와
z축 깊이값(카메라부터 얼마나 떨어진 지점인지, 예를들어 z= 10)
를 줘야만 월드의 한점을 리턴한다.

반면 ScreenPointToRay는
깊이를 정해주지않아도,
카메라 위치에서 화면을 통과하는 "무한한" 광선을 만들어주기때문에
그 뒤에 Physics.Raycast나 Plane.Raycast 등을 써서 실제 충돌지점을 얻어올수있다.
정리하자면 ScreenPointToRay는
픽셀좌표 -> 월드좌표한점 변환도 포함되는내용이지만 실제로는
카메라위치(origin)와 그 픽셀점을 향한 방향(direction)을 함께 뱉어내서 광선형태로 만들어준다.

따라서 그냥 ScreenToWordPoint로 깊이값을 찾아낼수도있지만
// 예: 마우스가 가리키는 지점이 지면(y=0) 위라면
Plane ground = new Plane(Vector3.up, Vector3.zero);
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
float enter;
if (ground.Raycast(ray, out enter))
{
    Vector3 worldPosOnGround = ray.GetPoint(enter);
    Debug.Log($"지면 위 좌표: {worldPosOnGround}");
}
이렇게도할수있다.

2d예제중에
Vector2 worldPos = camera.ScreenToWorldPoint(mousePosition);
lookDirection = (worldPos - (Vector2)transform.position);

if (lookDirection.magnitude < .9f)
{
    lookDirection = Vector2.zero;
}
else
{
    lookDirection = lookDirection.normalized;
}
그냥 이렇게 쓰는경우도있는데

mousePosition이 Vector2이지만, 내부적으로는 Vector3(mouseX, mouseY, 0)이 된다.
즉 z = 0을 깊이로 삼아 월드 좌표를 계산한다
2D 프로젝트에서 보통 카메라가 z = –10, 게임 오브젝트들이 z = 0에 놓이기 때문에
“깊이 0” → “z = 0 평면”
에서 x,y 좌표를 돌려주는 셈이라
따로 z를 지정하지 않아도 원하는 대로 작동했다.


계속 이어나가보자..
RaycastHit hit;
RaycastHit는 충돌정보를 담는 구조체(struct)이다.
Ray를 던졌을때 무엇에, 어느지점에서, 법선(normal)은 어떤 방향인지 등 다양한 정보를 나중에 꺼내쓸수있다.
hit.point // 충돌지점(vector3)
hit.normal // 표면의 법선 벡터
hit.collider // 부딪힌 collider 컴포넌트
hit.distance // 레이 시작점에서 충돌 지점까지의 거리
hit.transform // 부딪힌 오브젝트의 trnasform

여기서 좀 생소한 법선(normal)에 대해서 공부해보자.
법선은 충돌이 일어난 지점에서 그 표면에 수직인방향 벡터를 말한다 
수직방향
- 평면위에서라면 법선은 그 평면을 직각으로 뻗어나가는 방향
예를들어 2d 평면의 법선은 보통 위쪽을 가르키는 (0,1,0) 벡터이다
단위벡터
보통 길이가 1인 정규화된 벡터로 반환되기 때문에
hit.normal.magnitude를 해보면 거의 항상 1이 나온다.
활용예시로는
반사: 레이가 부딪히고 튕겨나가는 방향을 구할때
Vector3 incoming = ray.direction;
Vector3 reflectDir = Vector3.Reflect(incoming, hit.normal);
여기서 Vector3.Reflect는 벡터를 법선에 반사시키는 새로운 벡터를 계산해주는 유틸함수
incoming : 원래 방향벡터(레이가 날아온방향)
normal : 충돌한 표면의 법선 벡터
일때
출력은 : 표면에 맞고 튕겨나가는(반사되는) 새로운 방향 벡터이다.
reflected=incoming−2×(incoming⋅normal)×normal
여기서 incoming ⋅ normal 은 두 벡터의 내적(inner product)은 두 벡터의 내적이다.
그 결과에 법선 벡터를 곱하고, 두 배로 뺌으로써 “거울에 반사된” 방향을 구한다.
예제를 한번띄워보자면,,
// 1) 레이의 방향(rayDirection)과 충돌 표면의 법선(hit.normal)을 얻었다면
Vector3 incoming = ray.direction;      // 레이가 들어온 방향
Vector3 normal   = hit.normal;         // 충돌한 표면의 수직 방향
// 2) 반사 방향 계산
Vector3 reflectDir = Vector3.Reflect(incoming, normal);
// 3) 반사된 방향을 시각화해 보기 (디버그용)
Debug.DrawRay(hit.point, reflectDir * 2f, Color.cyan, 1f);
이렇게 하면 레이가 표면에 부딪힌 후 “튕겨나가는” 방향을 reflectDir에 담아
굴절, 반사 미러 효과, 공 튕기기 같은 물리·그래픽 연출에 활용할 수 있다.
또한,
물리반응: 충돌시 오브젝트의 반발력이나 회전토크계산
쉐이딩/라이팅: 조명계산에서 표면이 빛을 얼마나 받는지판단 
같은것들도 normal을 이용해서 가능하다.

레이저나 시선이반사되는효과, 볼이나 탄환이 벽에 부딪혀 튕길때의 궤적
라인렌더러로 반사광선 시뮬레이션
시뮬레이션게임의 반사물리구현등이 가능하다.

그다음으로나아가자
int layerMask = 1 << LayerMask.NameToLayer("YourLayerName");
레이어 마스크를 만들어서 레이캐스트가 어떤 레이어(collision layer)에만 반응하게 필터링하는 방법이다.
LayerMask.NameToLayer("YourLayerName") 은 문자열로 된 레이어 이름을 0~31 사이의 레이어 인덱스로 바꿔준다.

int idx = LayerMask.NameToLayer("Enemies"); // 예: idx = 8
int mask = 1 << idx;                        // mask = 1<<8 = 0x00000100

이렇게하면 8번 레이어만 검사대상이된다.

마지막으로 
Physics.Raycast(ray, out hit, Mathf.Infinity, layerMask)에 대해서 알아보자

bool hitSomething = Physics.Raycast(
    Ray    ray,         // 쏠 광선
    out RaycastHit hit, // 맞았을 때 정보 받기
    float  maxDistance, // 최대 사거리 (여기선 무한대)
    int    layerMask    // 검사할 레이어 필터
);

리턴값은 bool 형태다
true라면 지정된 레이어의 어떤 콜라이더든 최초로 맞았다.
false 라면 아무것도 맞지않았다.

out hit 
out 키워드 덕분에 레이가 뭔가에 맞으면 hit 변수에 자동으로 충돌 정보가 채워진다.
out 변수는 메서드 호출전에 초기화할 필요가없다.

이런예제도 가능하다
void Update()
{
    // ① 화면 클릭 지점에서 레이 생성
    Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

    // ② 맞은 정보 받을 구조체
    RaycastHit hit;

    // ③ "Interactable" 레이어만 검사하도록 마스크 생성
    int layerMask = 1 << LayerMask.NameToLayer("Interactable");

    // ④ 레이 캐스트: 맞으면 true, 안 맞으면 false
    if (Physics.Raycast(ray, out hit, Mathf.Infinity, layerMask))
    {
        // 충돌 지점의 오브젝트 이름 출력
        Debug.Log($"Hit: {hit.collider.name} at {hit.point}");

        // 예) 맞은 오브젝트의 스크립트 함수 호출
        var target = hit.collider.GetComponent<MyInteractable>();
        if (target != null)
            target.OnClick();
    }
}