개발/하스스톤+전장

하스+전장) 인터페이스,제네틱 쓰는 이유

kimchangmin02 2026. 1. 26. 12:06

공격취소되엇다면, 다시 공격할수잇도록[   ]

 

공격하고 되돌아옴[  ]

<아마 예외처리 많이 필요하겟지 ,아닌가

이미 필드에 잇는 하수인을 한번 클릭하면 화살표가 나오게 하고싶은데, 그리고 목적지를 선택하면 그곳으로 돌진햇다가, 원래 자리로 돌아오는,

아마 draw관련 움직이는 스크립트 추가한것처럼 스크립트 추가하면 되나 

 

[   ]카드 내려고했다가 취소되면 원래 손패위치로 되돌아가게 

<아마 추가적인 변수가 필요하겟지, 젤 마지막에 선택한 카드의 레이아웃 순서 번호 ?


미니언 attack.cs 해석

 

draggable .cs 해석[   ]

 

카드 사이에 놓고싶을때, 카드 빠져나갈때 어떻게 자연스러운 이미지 [  ]

 

스크립트간에 상호작용은 어케 하는거지[   ]

 

손패 코드 해석[  ]

 


📌 dropzon.cs  해석[  완 ]

  • OnPointerEnter (진입): 마우스 커서가 해당 UI 영역 안으로 들어가는 순간 딱 한 번 실행됩니다.
    • 코드에서의 역할: "아, 카드가 내 영역 위로 왔네? 그러면 카드가 들어갈 빈 자리를 미리 보여주자."
  • OnPointerExit (퇴장): 마우스 커서가 해당 UI 영역 밖으로 나가는 순간 딱 한 번 실행됩니다.
    • 코드에서의 역할: "카드가 내 영역을 벗어났네? 빈 자리 표시를 지우고 원래 있던 곳(손패 등)으로 돌려보내자."
  • OnDrop (놓기): 마우스 버튼을 누르고 드래그하다가 이 영역 위에서 버튼을 뗐을 때 실행됩니다.
    • 코드에서의 역할: "드디어 카드를 여기에 놓았구나! 이제 이 카드의 부모(주인)는 공식적으로 나(Field)다."

 

 

 

자바 GUI(Swing) 등에서는 마우스 좌표를 실시간으로 계산해서 "좌표 (x, y)가 사각형 안에 들어왔나?"를 직접 체크해야 하는 경우가 많지만, 유니티는 **Raycasting(광선 쏘기)**이라는 방식을 씁니다.

  • 유니티 내부 과정:
    1. EventSystem이 매 프레임 마우스 커서 위치에서 화면 안쪽으로 투명한 레이저를 쏩니다.
    2. 그 레이저에 맞는 UI 오브젝트를 찾습니다.
    3. 지난 프레임에는 안 맞았는데, 이번 프레임에 레이저에 맞은 물체가 있다면? -> OnPointerEnter 실행!
    4. 지난 프레임엔 맞았는데, 이번 프레임에 레이저에서 벗어났다면? -> OnPointerExit 실행!

 

 

 

 

 

1. 유니티가 어떻게 호출하나요? (Event System)

유니티 하이어라키(Hierarchy) 창에 보면 UI를 만들 때 자동으로 생기는 **EventSystem**이라는 오브젝트가 있죠? 이 친구가 "관리자" 역할을 합니다.

  1. 감시: EventSystem은 매 프레임 마우스의 움직임과 클릭을 감시합니다.
  2. 레이캐스팅: 마우스 위치에서 화면 아래로 레이저를 쏩니다.
  3. 대상 확인: 레이저에 어떤 UI(예: DropZone)가 맞으면, 유니티는 그 오브젝트에 붙어있는 스크립트들을 다 뒤져봅니다.
  4. 메시지 전달: "어? 이 스크립트(DropZone)는 IDropHandler라는 '자격증'을 가지고 있네? 그럼 내가 OnDrop 함수를 실행해줘야겠다!" 하고 유니티가 함수를 강제로 실행시킵니다.

 

 

유니티(EventSystem)는 매 프레임마다 모든 오브젝트를 검사하는 게 아니라, 사건(클릭, 드랍 등)이 발생했을 때 그 지점에 있는 오브젝트가 "자격증(인터페이스)"이 있는지 확인합니다.

 

 

 

// --- 유니티 엔진 내부 어딘가의 가상 코드 ---

void 무언가_마우스를_뗐을_때_실행되는_함수(PointerEventData eventData) {
    // 1. 마우스 밑에 있는 물체를 찾는다
    GameObject hitObject = 레이저_쏴서_맞은_놈;

    // 2. 그 물체에 "IDropHandler" 자격증(인터페이스)이 있는지 검사한다
    // (이게 포인트! 여기서 일종의 조건문 처리가 내부적으로 일어납니다)
    IDropHandler handler = hitObject.GetComponent<IDropHandler>();

    // 3. 자격증이 있다면? 우리가 구현한 그 함수를 유니티가 대신 호출한다!
    if (handler != null) {
        handler.OnDrop(eventData); // <--- 여기서 우리가 짠 코드가 실행됨!
    }
}

 

 

우리는 함수를 만들어두기만 하고, 언제 실행할지는 결정하지 않습니다. 유니티가 마우스 드롭이라는 "사건"이 터졌을 때, 우리가 만든 그 함수의 이름을 보고 "똑똑똑, 약속하신 OnDrop 실행할 시간입니다" 하고 호출해주는 구조입니다.

 

 

 

 

1. 제일 밑바닥에는 무엇이 있나? (Hardware & OS)

마우스를 클릭하면 마우스 센서가 전기 신호를 만들고, 이걸 Windows나 Android 같은 OS가 받습니다.

  1. OS 레벨: OS는 매 순간 마우스 좌표와 버튼 상태를 체크합니다. (여기엔 당연히 if (버튼눌림)이 있습니다.)
  2. 유니티 엔진 레벨: 유니티는 OS로부터 "지금 마우스 왼쪽 버튼 눌렸음, 좌표는 (500, 300)임"이라는 정보를 매 프레임 받아옵니다.
    • 유니티 내부 코드 어딘가에는 if (Input.GetMouseButtonDown(0)) 같은 코드가 무조건 돌고 있습니다.

2. EventSystem: "중앙 통제소"의 등장

우리가 코드를 짤 때 if를 안 쓰는 이유는, 유니티가 **EventSystem**이라는 거대한 중앙 통제소를 만들어 놨기 때문입니다.

  • 직접 짤 때 (비효율): 100장의 카드가 각자 자기 Update() 함수 안에서 if (마우스가 내 위에 있나?)를 물어봅니다. (총 100번의 체크)
  • 유니티 방식 (효율): EventSystem이라는 딱 한 명의 감시자 Update()를 돌립니다.

 

 

 

 

 

 

 

 

 

📌 hitObject.GetComponent<IDropHandler>().OnDrop(data);

의미

 

  1. hitObject: 레이저에 맞은 그 물체(GameObject)야.
  2. .GetComponent<IDropHandler>(): "야, 너한테 붙어있는 여러 스크립트 중에 IDropHandler 자격증(인터페이스) 구현한 놈 하나만 꺼내봐."
  3. .OnDrop(data): "그 자격증 있으면 무조건 OnDrop이라는 함수가 들어있겠네? 그거 지금 실행해!"

 

 

📌 제네릭 <T>을 왜 쓰나?

GetComponent<T>()에서 < > 제네릭(Generic) 문법입니다. 자바의 ArrayList<String> 할 때 그 꺽쇠와 같습니다.

  • 역할: "내가 찾고 싶은 타입이 바로 이거야!"라고 명시하는 것입니다.
  • 편의성: 만약 제네릭이 없다면 예전 방식처럼 형변환(Casting)을 수동으로 해야 했습니다.C#
    // 옛날 방식 (불편)
    IDropHandler handler = (IDropHandler)hitObject.GetComponent(typeof(IDropHandler));
  • 요즘 방식 (깔끔):
    // 제네릭 사용 (컴파일러가 미리 타입을 체크해줘서 안전하고 깔끔함)
    IDropHandler handler = hitObject.GetComponent<IDropHandler>();

 

 

유니티 엔진을 만든 개발자들은 여러분이 스크립트 이름을 무엇으로 지을지 전혀 모릅니다. (Field라고 할지 MyDropZone이라고 할지 모르죠.)

그래서 그들은 **"이름은 마음대로 짓되, 내 이벤트를 받고 싶으면 IDropHandler라는 인터페이스를 상속받아라. 그러면 내가 GetComponent<IDropHandler>()로 찾아서 실행해줄게!"**라는 규칙을 정해놓은 것입니다.

한 줄 요약:
"누군지 이름(클래스명)은 몰라도, **특정 능력(인터페이스)**을 가진 놈을 콕 집어서(제네릭) 불러내기 위해" 저런 형태의 코드를 쓰는 것입니다!

 


 

"어떻게 한 번에" 처리하나요? (내부 메커니즘)

유니티가 if문을 100번 돌리지 않고 한 번에 처리하는 핵심 기술은 Graphic Raycaster입니다.

  1. 마우스 클릭 발생: (딱 1번의 if (Input.GetMouseButtonDown))
  2. 레이저(Ray) 발사: 마우스 좌표에서 화면 안쪽으로 레이저를 딱 한 번 쏩니다.
  3. 충돌체 확인: 이 레이저에 맞은 UI가 무엇인지 리스트를 뽑습니다. (이때 유니티는 내부적으로 최적화된 수학 계산을 써서 좌표를 순식간에 계산합니다.)
  4. 인터페이스 필터링 (가장 중요한 부분):
    레이저에 맞은 물체(예: DropZone)에게 딱 한 번 물어봅니다.
  5. "너 IDropHandler라는 인터페이스(자격증) 가지고 있니?"

 

 

 

 

📌 인터페이스가 왜 필요햇더라

 

1. 인터페이스가 없을 때 (instanceof 1000번)

만약 유니티에 IDropHandler라는 인터페이스가 없다면, 유니티 개발자들은 전 세계 사람들이 만들 모든 클래스 이름을 다 예상해서 코드를 짜야 합니다.

// 유니티 엔진 내부 코드라고 가정
void 마우스뗐을때(GameObject hitObject) {
    // 이 놈이 피카츄(Field)인가?
    if (hitObject is Field) { 
        ((Field)hitObject).OnDrop(); 
    }
    // 이 놈이 파이리(TrashCan)인가?
    else if (hitObject is TrashCan) { 
        ((TrashCan)hitObject).OnDrop(); 
    }
    // 이 놈이 꼬부기(Inventory)인가?
    else if (hitObject is Inventory) { 
        ((Inventory)hitObject).OnDrop(); 
    }
    // ... 새로운 클래스 나올 때마다 else if 추가 ... (망함)
}

이게 바로 질문하신 instanceof(C#에서는 is)를 1000번 써야 하는 상황입니다. 새로운 종류의 UI(포켓몬)를 만들 때마다 유니티 엔진 코드를 수정해야 하니 불가능한 일이죠.


2. 인터페이스가 있을 때 (다형성)

인터페이스는 **"공통의 버튼"**을 만드는 것과 같습니다. 피카츄든 파이리든 꼬부기든, 전부 **IPokemon**이라는 인터페이스를 상속받게 하고 Attack()이라는 함수를 만들게 강제하는 거죠.

그러면 유니티(엔진)는 이렇게 딱 한 줄만 써도 됩니다.

void 마우스뗐을때(GameObject hitObject) {
    // "너 포켓몬(IDropHandler) 자격증 있니?" 라고 딱 한 번 물어봄
    IDropHandler pokemon = hitObject.GetComponent<IDropHandler>();

    if (pokemon != null) {
        pokemon.OnDrop(); // "누구든 상관없으니 공격(OnDrop)해!"
    }
}

3. "이게 인터페이스랑 뭔 상관이더라?"에 대한 답

인터페이스를 썼기 때문에 **GetComponent<IDropHandler>()**라는 코드가 작동할 수 있는 것입니다.

  • 상속의 힘: Field 클래스가 IDropHandler를 상속받는 순간, FieldField 타입이기도 하지만 IDropHandler 타입이기도 하게 됩니다. (다형성)
  • 검색의 힘: GetComponent<IDropHandler>()는 "클래스 이름이 뭐야?"라고 묻지 않습니다. 대신 **"너 IDropHandler라는 타입을 가지고 있니?"**라고 묻습니다.
  • 결과: 유니티는 여러분이 만든 클래스가 Field인지 TrashCan인지 몰라도, 그게 IDropHandler의 일종이라는 것만 확인되면 OnDrop을 실행할 수 있습니다.

4. 요약 (포켓몬 버전)

  • 인터페이스 없이: "피카츄면 백만볼트 쓰고, 파이리면 불꽃세례 써!" (타입 하나하나 다 체크해서 함수 호출)
  • 인터페이스 있게: "포켓몬이면 일단 공격()해!" (피카츄는 백만볼트가 나갈 것이고, 파이리는 불꽃세례가 나갈 것임. 나는 뭔지 몰라도 됨.)

유니티의 OnPointerEnter, OnDrop 등은 전부 이 "포켓몬 공격" 시스템으로 되어 있습니다. 유니티는 여러분의 스크립트 이름을 몰라도 됩니다. 그냥 IDropHandler 자격증이 있는지만 확인하고 그 안의 함수를 실행할 뿐입니다.

그래서 우리가 if (instanceof...)를 안 써도 되는 것입니다! 유니티가 이미 인터페이스라는 규격으로 그 과정을 생략해버렸으니까요.

 

 

 

 

📌 draggable.placeholderParent = this.transform;

어떻게 2번, 3번 카드 사이로 들어가는가? (좌표 계산)

부모(this.transform)만 바꿔준다고 자동으로 순서가 생기지는 않습니다. 보통 이 로직은 DropZone이 아니라, 카드를 직접 잡고 움직이는 CardDraggable 스크립트의 OnDrag 이벤트에서 계산합니다.

작동 원리:

  1. Layout Group: 필드(DropZone)에는 보통 Horizontal Layout Group이 붙어 있습니다. 이 컴포넌트는 자식(Children)들의 **순서(Sibling Index)**에 따라 왼쪽부터 오른쪽으로 배치합니다.
  2. Placeholder(빈 공간): 드래그를 시작할 때 투명한 가짜 카드(Placeholder)를 만듭니다.
  3. 실시간 계산: 드래그 중인 동안, 마우스 좌표가 현재 필드에 있는 카드들 중 누구보다 왼쪽에 있는지 매 프레임 체크합니다.
  4. SetSiblingIndex: 체크 결과에 따라 placeholder.transform.SetSiblingIndex(새로운_인덱스)를 호출하면, Layout Group이 실시간으로 "아, 2번과 3번 사이에 공간을 만들어야겠구나" 하고 카드를 옆으로 밀어냅니다.

 

 

 

 

  • 통로 (PointerEventData): DropZone 스크립트의 OnPointerEnter(PointerEventData eventData) 함수에서 eventData.pointerDrag를 확인합니다.
  • 직접 수정: DropZone은 "내 위로 카드가 올라왔네?"라고 판단하면, eventData.pointerDrag에 붙어있는 CardDraggable 컴포넌트를 가져와서 그 안에 있는 공용 변수(placeholderParent)를 직접 수정해 버립니다.
  • 결과: CardDraggable은 가만히 있어도 DropZone이 자기 변수를 바꿔주니까 "아, 지금 내 빈자리는 필드(DropZone)로 옮겨져야 하는구나"라고 알게 됩니다.

 

 


⚠️ 주의할 점 (코드 개선 팁)

현재 DropZone 스크립트에서도 부모를 바꾸고 있고, CardDraggable OnDrag에서도 RectTransformUtility로 부모를 바꾸고 있습니다.

둘 중 하나만 쓰는 것이 좋습니다.

  1. DropZone 방식: "마우스가 들어왔다/나갔다"는 이벤트 기반이라 성능에 좋고 코드가 깔끔합니다.
  2. OnDrag 방식(현재 코드): 매 프레임 좌표를 계산하므로 아주 정확하지만, 영역이 복잡해지면 코드가 지저분해질 수 있습니다.

추천: CardDraggable 내의 OnDrag에 있는 RectangleContainsScreenPoint 로직을 지우고, 처음에 보여주신 DropZone 스크립트가 parentToReturnTo를 관리하게 하면 역할 분담이 확실해집니다.

  • CardDraggable: 드래그 구현, 빈칸(Placeholder) 순서 계산.
  • DropZone: 마우스가 들어오면 "여기는 필드야!"라고 알려줌. (부모 설정)

 

 

 

 

1. "사공이 많으면 배가 산으로 간다" (로직의 중복과 충돌)

이전 코드에서는 필드 영역인지 확인하는 코드가 두 군데에 다 있었습니다.

  • DropZone: "마우스 들어왔네? 내가 부모가 되어줄게!" (OnPointerEnter)
  • CardDraggable: "잠깐, 내가 매 프레임 좌표 계산해볼게... 음, 필드 위 맞네! 내가 부모 설정할게!" (OnDrag 내의 RectangleContainsScreenPoint)

문제점: 두 스크립트가 동시에 같은 변수(placeholderParent)를 건드리고 있습니다. 만약 유니티의 PointerEnter 판정과 RectangleContainsScreenPoint 판정이 1픽셀이라도 오차가 생기면, 빈칸(Placeholder)이 핸드와 필드 사이에서 미친 듯이 깜빡거리는 버그가 발생할 수 있습니다.

2. "매 프레임 불필요한 수학 계산" (성능 낭비)

이전 CardDraggable OnDrag는 마우스를 움직이는 동안 초당 60~120번 실행됩니다.

  • 이전 방식: "마우스 위치가 Rect 안에 있나?"라는 수학 계산을 초당 100번씩 계속 수행합니다.
  • 수정 방식: 마우스가 필드 경계선을 넘어가는 그 순간(딱 한 번) 유니티 엔진이 OnPointerEnter를 실행해줍니다.

문제점: 카드가 한 장일 때는 괜찮지만, 나중에 카드가 많아지거나 복잡한 연산이 추가되면 게임이 버벅거리는 원인이 됩니다. 유니티가 이미 "영역에 들어왔다"는 이벤트를 주는데, 굳이 코드로 좌표 계산을 또 할 필요가 없는 것이죠.

3. "확장성의 한계" (하드코딩 문제)

이전 코드의 가장 큰 설계적 결함입니다.

// 이전 코드의 문제점: 특정 이름을 직접 찾아야 함
GameObject pField = GameObject.Find("PlayerField");
  • 이전 방식: CardDraggable 스크립트 안에 "PlayerField"라는 이름이 박혀 있습니다. 만약 게임에 '무덤', '상점', '특수 소환 구역' 등 새로운 구역이 추가된다면? CardDraggable 스크립트를 계속 수정해서 if-else를 추가해야 합니다.
  • 수정 방식: CardDraggable은 아무것도 모릅니다. 그냥 어떤 영역이든 DropZone 스크립트만 붙어 있다면, 그 영역이 "내가 부모야!"라고 알려주는 대로 따를 뿐입니다.

 

 

카드(CardDraggable): "나는 끌려다니고, 내 빈칸 위치만 계산할래.

"필드(DropZone): "누가 내 영역에 들어오면 내 자식으로 받아줄 준비를 할래."



역할이 다른가봄

 

📌 근데 수정햇더니, 아예 필드에 카드를 못내게 됫음

 

필드(DropZone)에 Image 컴포넌트가 있나요?

유니티 UI에서 IDropHandler IPointerEnter 같은 이벤트가 작동하려면, 해당 오브젝트에 반드시 Image 컴포넌트가 있어야 하고, **Raycast Target**이 켜져 있어야 합니다.

  • 해결법: PlayerField 오브젝트에 Image 컴포넌트를 추가하세요. (이미지가 보이기 싫다면 투명도(Alpha)를 0으로 낮추거나, Canvas Renderer Cull Transparent Mesh를 체크하세요.)
  • 이유: 투명하더라도 "면적"이 있어야 마우스가 그 위에 올라왔다는 것을 유니티가 인식합니다.

 

 

1. 왜 필드의 Raycast Target을 켜야만 하나요?

IDropHandler, IPointerEnterHandler 등 유니티의 이벤트 인터페이스는 'Raycast'를 기반으로 작동합니다.

  • Raycast Target OFF: 유니티 엔진이 "어? 여기는 아무것도 없네(투명하네)"라고 판단해서 이벤트를 아래로 통과시켜 버립니다. 그래서 OnPointerEnter OnDrop 함수 자체가 아예 실행되지 않습니다.
  • 결론: 카드를 필드에 내려놓으려면 PlayerField는 반드시 마우스에 걸려야(Raycast Target ON) 합니다.

2. 화살표 공격 시 필드가 방해되지 않을까요?

공격할 때 배경(필드)이 마우스에 걸려서 하수인 클릭을 방해할까 봐 걱정하시는 거죠? 다행히 이미 작성하신 MinionAttack.cs의 코드가 이 문제를 해결할 수 있는 RaycastAll 방식을 쓰고 있습니다.

RaycastAll은 마우스 위치에 있는 모든 UI를 리스트로 다 가져옵니다. 즉, 필드가 가로막고 있어도 그 뒤에 있는 적 하수인까지 다 찾아낼 수 있습니다.

 

 

이전 코드에서 Raycast Target이 꺼져 있어도 작동했던 이유는, 사용하셨던 방식이 유니티의 이벤트 시스템을 사용한 게 아니라 "순수 수학 계산(기하학)" 방식이었기 때문입니다.

두 방식의 차이를 비교해 보면 명확해집니다.


1. 이전 코드의 방식: "수학적 좌표 체크"

이전 CardDraggable 코드에는 이 줄이 있었습니다:

if (RectTransformUtility.RectangleContainsScreenPoint(playerFieldRect, eventData.position, ...))
  • 작동 원리: "마우스의 현재 XY 좌표가 playerFieldRect라는 사각형의 네 꼭짓점 범위 안에 들어와 있는가?"를 수학적으로 계산합니다.
  • 특징: 이 함수는 해당 오브젝트가 투명하든, 이미지가 없든, Raycast Target이 꺼져 있든 상관하지 않습니다. 오직 "영역(Rect)" 데이터만 가지고 숫자를 비교하기 때문입니다.
  • 그래서: 이전 코드에서는 Raycast Target을 꺼두어도 계산상 "범위 안"이었기 때문에 작동했던 것입니다.

2. 현재 코드의 방식: "유니티 이벤트 시스템"

새로 바꾼 DropZone 방식은 인터페이스를 사용합니다:

public class DropZone : MonoBehaviour, IDropHandler, IPointerEnterHandler ...
  • 작동 원리: 유니티 엔진의 Graphic Raycaster가 마우스 위치에서 화면 아래로 광선(Ray)을 쏩니다. 이때 **"마우스에 부딪힌 UI가 누구니?"**라고 물어봅니다.
  • 특징: 유니티 엔진은 Raycast Target이 켜진 이미지만 "물체"로 인식합니다. 꺼져 있으면 유니티는 그곳에 아무것도 없다고 판단하고 광선을 통과시켜 버립니다.
  • 그래서: OnPointerEnter, OnDrop 같은 함수는 유니티가 "충돌"을 감지했을 때만 실행해주기 때문에 반드시 Raycast Target이 켜져 있어야 합니다.

 

 

 

 

현재 코드 구조상 손패는 조금 독특합니다.

  • 작동 원리: OnBeginDrag를 할 때 이미 parentToReturnTo를 손패로 저장해둡니다. 그리고 필드(DropZone)에서 마우스가 나가면(OnPointerExit) 다시 손패를 부모로 설정하게 되어 있습니다.
  • 이미지가 없어도 되는 이유: 필드 밖으로 나가는 순간(필드의 Exit 이벤트) 코드가 자동으로 손패로 위치를 옮겨주기 때문입니다.
  • 이미지가 있으면 좋은 이유: 만약 나중에 "필드에 이미 내놓은 카드를 다시 손패로 드래그해서 넣고 싶다"거나, "손패 위로 마우스를 가져갔을 때 손패가 벌어지는 연출"을 더 정교하게 하고 싶다면 손패에도 DropZone과 이미지가 있어야 합니다.