본문 바로가기
개발일지/게임개발

Project_DT - 카드 드래그 가이드 개발

by 라이티아 2025. 9. 25.
  1. 레이어 설계
  • Enemy 레이어 생성. 모든 적 루트 오브젝트에 지정. 콜라이더는 적 루트나 하위에 1개만 유지.
  • (선택) EnemyTarget 레이어 추가. 적 머리 위 큰 히트박스(타깃 전용 콜라이더)에 지정해 드랍 성공률을 높임.
  • 드래그 프리뷰와 손패 UI는 서로 다른 목적:
    • 프리뷰: Ignore Raycast 레이어. 콜라이더 없음.
    • 손패 UI: 기본 레이어이든 UI 레이어든 상관없음. 단, “드랍하면 취소”가 되도록 UI 판정을 받게 둠.

우선 해당 부분을 구현하려 한다

 

 

적에게 사용할 적 레이어를 정의한다

 

해당 레이어에 ray를 마우스 좌표에서 인식하도록 한다

 

ray 충돌 감지를 위해서 collider와

카메라에 physics 2d raycaster를 세팅한다

https://docs.unity3d.com/Packages/com.unity.ugui@1.0/manual/script-Physics2DRaycaster.html

 

2d에서 ray를 사용하려면 해당 컴포넌트가 필수로 필요하다

 

원래라면 projectsetting에서 ray에 맞을 layer만 세팅해야 하지만, 현재는 미완전 단계이기에 스킵하고 진행한다

 

using TMPro;
using UnityEngine;

public class Card : MonoBehaviour
{
    [Header("Card ID")]
    [SerializeField]
    private int _cardID = -1; // 초기값, -1인 상태로 사용되면 예외처리 핸들링
    // public int CardID => _cardID;

    /// <summary>
    /// 카드의 id를 세팅 = card manager에서 call해서 사용
    /// </summary>
    /// <param name="id"></param>
    public void SetCardID(int id)
    {
        _cardID = id;
        transform.GetChild(0).GetComponent<TextMeshProUGUI>().text = _cardID.ToString();
    }

    public int GetCardID()
    {
        return _cardID;
    }
}

현재 카드 컴포넌트이다

여기에 카드가 드래그 되는 기능을 작성한다

카드 프리팹에 layout element를 추가한다

이는 부모의 layout group에서 간섭을 피하게 해준다

 

이제 이전 글에서 작성한 코드인 cardarrowlinemaker를 사용한다

https://nonamed02.tistory.com/191

 

Unity 2D 게임 개발 - 객체 A B를 잇는 화살표 그리기

즐거운 라비닝을 들으며 개발하는 무언가 현재 덱빌딩 게임을 기획하고 있는데, 그중 카드를 끌어서 원하는 목표에 움직일시 위치를 시각적으로 보여주어야 하기에 그때 사용되는 기능이다 현

nonamed02.tistory.com

 

현재 몇몇 부분을 자신이 관리하게 되어 있는데, 이를 카드에 넘기도록 세팅한다

 

private bool _isDragging = false;

현재 해당 변수값이 입력을 인식하고 있는데 이를 card가 관리하도록 스왑해준다

....

 

근데 지금 보니까 구조가 잘못 되어 있다

 

    public void OnBeginDrag(PointerEventData eventData)
    {
        Debug.Log("OnBeginDrag");
    }

    // 드래그 중 처리
    public void OnDrag(PointerEventData eventData)
    {
        Debug.Log("OnDrag");
    }

    // 드래그 종료 처리
    public void OnEndDrag(PointerEventData eventData)
    {
        Debug.Log("OnEndDrag");
    }

기본적으로 

https://docs.unity3d.com/kr/2021.3/Manual/EventSystem.html

 

이벤트 시스템 - Unity 매뉴얼

이벤트 시스템은 키보드, 마우스, 터치, 커스텀 입력 등 입력 기반 애플리케이션의 오브젝트에 이벤트를 전송하는 방법입니다. 이벤트 시스템은 이벤트를 전송에 함께 작용하는 일부 컴포넌트

docs.unity3d.com

유니티 event system을 활용해서 사용해야 하는데

 

    void Update()
    {
        if (Input.GetMouseButtonDown(0) && IsPointerOverUI("Card"))
        {
            _isDragging = true;
        }
        if (Input.GetMouseButtonUp(0))
        {
            _isDragging = false;
        }
        UpdateArrowEndPoint();
        _middlePoint = CalculateMiddlePointVertex();
        DrawLine();
    }

update로 그리고 있다....

진심인가?

 

다시 생각을 정의한다

 

카드에서 시작을 정의해서 드로어에게 Call을 날리고

 

이를 드로어에서 ing - end를 정의한다

 

    // 드래그 시작 처리
    public void OnBeginDrag(PointerEventData eventData)
    {
        Debug.Log("OnBeginDrag");

        PointerEventData pointerData = new PointerEventData(EventSystem.current);
        pointerData.position = Input.mousePosition;

        List<RaycastResult> results = new List<RaycastResult>();
        EventSystem.current.RaycastAll(pointerData, results);

        foreach (RaycastResult result in results)
        {
            if (result.gameObject == gameObject)
            {
                CardArrowLineMaker.Instance.SetStartPoint(gameObject.transform);
            }
        }
    }
    // 드래그 중 처리
    private void OnDrag(PointerEventData eventData)
    {
        _targets[1] = Input.mousePosition;
    }

    // 드래그 종료 처리
    private void OnEndDrag(PointerEventData eventData)
    {
        Debug.Log("OnEndDrag");
    }

    /// <summary>
    /// set linedrawer start point == selected card
    /// </summary>
    public void SetStartPoint(Transform cardPoint)
    {
        _targets[0] = cardPoint;
    }

 

이때 드래그중 transform은 vector로 받을 수 없기에 world값으로 변경 해주어야 한다

 

    // 드래그 중 처리
    private void OnDrag(PointerEventData eventData)
    {
        Vector3 worldPos;
        RectTransformUtility.ScreenPointToWorldPointInRectangle(
            _targets[1].GetComponent<RectTransform>(),
            Input.mousePosition,
            Camera.main,
            out worldPos);
        
        _targets[1].position = worldPos;
    }

recttransformutility를 사용한다

 

    // 드래그 종료 처리
    public void OnEndDrag(PointerEventData eventData)
    {
        Debug.Log("OnEndDrag");
        for (int i = 0; i < _targets.Length; i++)
        {
            _targets[i] = null;
        }
    }

 

드래그 종료시 시작점, 끝점을 버리도록 한다

 

    public void SetIsDragging(bool value)
    {
        _isDragging = value;
    }

그릴지 말지 결정하는 flag를 세팅할 수 있도록 하고

 

    // 드래그 시작 처리
    public void OnBeginDrag(PointerEventData eventData)
    {
        Debug.Log("OnBeginDrag");

        PointerEventData pointerData = new PointerEventData(EventSystem.current);
        pointerData.position = Input.mousePosition;

        List<RaycastResult> results = new List<RaycastResult>();
        EventSystem.current.RaycastAll(pointerData, results);
        foreach (RaycastResult result in results)
        {
            if (result.gameObject == gameObject)
            {
                CardArrowLineMaker.Instance.SetIsDragging(true);
                CardArrowLineMaker.Instance.SetStartPoint(gameObject.transform);
            }
        }
    }

    // 드래그 종료 처리
    public void OnEndDrag(PointerEventData eventData)
    {
        Debug.Log("OnEndDrag");
        CardArrowLineMaker.Instance.SetIsDragging(false);
    }

이를 연결해 준다

 

GPT에게 점검을 받는다

더보기

문제 핵심 6개.

  1. 드래그 이벤트 수신 주체가 잘못됨
    IDragHandler는 UI 오브젝트에서만 동작합니다. CardArrowLineMaker는 UI가 아니니 OnDrag가 호출되지 않습니다.
    → 드래그 이벤트는 Card에서만 처리하세요. 드로어는 Begin/UpdateEnd/End만.
  2. _targets[1] Null 참조 위험
    드로어 OnDrag에서 _targets[1].GetComponent<RectTransform>() 호출하지만 _targets[1]은 어디서도 세팅되지 않음.
    → 끝점은 Transform 쓸 필요 없습니다. Vector3로 받고 내부에만 보관하세요.
  3. 좌표 변환 기준 RectTransform이 잘못됨
    ScreenPointToWorldPointInRectangle의 첫 파라미터에 _targets[1]의 RectTransform을 넘기면 안 됩니다. 보통 Canvas의 RectTransform을 넘깁니다.
  4. Update()에서 항상 그리기 시도
    드래그 안 해도 DrawLine()을 호출합니다.
    → 드로어 내부에 _active 체크로 가드하거나, 아예 이벤트 기반으로만 그리세요.
  5. 배열 null 초기화 코드 오작동
    foreach (var target in _targets) { target = null; }는 원소가 안 바뀝니다.
    → for (int i=0;i<_targets.Length;i++) _targets[i]=null;
  6. Card.OnBeginDrag의 RaycastAll 남용
    이미 그 카드에서 콜백이 왔습니다. 결과 리스트 검사 불필요.
    → 바로 CardArrowLineMaker.Instance.Begin(startWorld) 호출.

??? event가 ui에서만 작동한다고??????

 

다시 책임을 찟어야 한다

 

using System.Collections.Generic;
using Unity.VisualScripting.Dependencies.NCalc;
using UnityEngine;
using UnityEngine.EventSystems;

public class CardDragController : MonoBehaviour, IDragHandler, IEndDragHandler
{
    // 드래그 중 처리
    public void OnDrag(PointerEventData eventData)
    {
        Vector3 worldPos;
        RectTransformUtility.ScreenPointToWorldPointInRectangle(
            CardArrowLineMaker.Instance.Targets[1].GetComponent<RectTransform>(),
            Input.mousePosition,
            Camera.main,
            out worldPos);

        CardArrowLineMaker.Instance.Targets[1].position = worldPos;
    }
    // 드래그 종료 처리
    public void OnEndDrag(PointerEventData eventData)
    {
        Debug.Log("OnEndDrag");
        for (int i = 0; i < CardArrowLineMaker.Instance.Targets.Length; i++)
        {
            CardArrowLineMaker.Instance.Targets[i] = null;
        }
    }
}

드래그를 하는 부분만 따로 UI오브젝트에 할당해 준다

 

더보기

문제 요약과 해결안.

  1. 드래그 이벤트 분산
  • IDragHandler는 “포인터 다운된 그 UI 오브젝트”에만 간다. Card는 Begin/End, CardDragController는 Drag로 쪼개면 같은 오브젝트에 둘 다 붙어있지 않으면 OnDrag가 오지 않는다.
    → 한 오브젝트(Card)가 IBeginDragHandler, IDragHandler, IEndDragHandler를 모두 받게 하거나, 이벤트를 Card→매니저로 이벤트로 중계하라.
  1. Drawer가 내부 상태를 외부에 노출
  • Targets 배열을 외부에서 직접 건드린다 → NRE·순서 오류 위험.
    → Begin(start) / UpdateEnd(end) / End() 메서드만 공개.
  1. 좌표 변환 대상 오류
  • ScreenPointToWorldPointInRectangle의 첫 매개변수에 Targets[1]의 RT를 넣음. 보통 Canvas의 RectTransform이나 카드의 RT로 변환한다.
  1. 비활성 시 그리기
  • _isDragging이 false여도 _targets null 접근 가능.
    → 드로어는 활성 플래그로 가드하고 LineRenderer.enabled를 on/off.
  1. 불필요한 UI 레이캐스트
  • Card.OnBeginDrag에서 RaycastAll로 자기 자신을 다시 찾는다.
    → 이미 해당 카드에서 콜백이 왔으니 직접 Begin 호출.

... 이번에는 분산되었다고 문제점을 지적한다

 

using System.Collections.Generic;
using Unity.VisualScripting.Dependencies.NCalc;
using UnityEngine;
using UnityEngine.EventSystems;

public class CardDragController : MonoBehaviour, IDragHandler, IEndDragHandler
{
    // 드래그 중 처리
    public void OnDrag(PointerEventData eventData)
    {
        Debug.Log("OnDrag");
        Vector3 worldPos;
        RectTransformUtility.ScreenPointToWorldPointInRectangle(
            CardArrowLineMaker.Instance.Targets[1].GetComponent<RectTransform>(),
            Input.mousePosition,
            Camera.main,
            out worldPos);

        CardArrowLineMaker.Instance.Targets[1].position = worldPos;
    }
    // 드래그 종료 처리
    public void OnEndDrag(PointerEventData eventData)
    {
        Debug.Log("OnEndDrag");
        for (int i = 0; i < CardArrowLineMaker.Instance.Targets.Length; i++)
        {
            CardArrowLineMaker.Instance.Targets[i] = null;
        }
    }
}

그래서 그걸 총괄하는 매니저를 만들었는데

이게 canvas 하위에 빈 오브젝트에 들어가는 것이다

 

근데....

더보기

드래그 이벤트는 반드시 UI 그래픽 오브젝트에서 시작해야 한다.

  • Image, Text, Button 같은 Graphic이 있어야 EventSystem이 PointerDown을 잡는다.
  • 그냥 빈 오브젝트에 RectTransform만 있으면 이벤트가 절대 안 옴.

으아아아악

 

무조건 그래픽 요소가 있어야 작동하는 무언가이다

으아악

 

그냥 카드에 넣는게 최선이라는 결과값이다

using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;

public class Card : MonoBehaviour, IBeginDragHandler, IEndDragHandler
{
    [Header("Card ID")]
    [SerializeField]
    private int _cardID = -1; // 초기값, -1인 상태로 사용되면 예외처리 핸들링
    // public int CardID => _cardID;

    /// <summary>
    /// 카드의 id를 세팅 = card manager에서 call해서 사용
    /// </summary>
    /// <param name="id"></param>
    public void SetCardID(int id)
    {
        _cardID = id;
        transform.GetChild(0).GetComponent<TextMeshProUGUI>().text = _cardID.ToString();
    }

    public int GetCardID()
    {
        return _cardID;
    }

    // 드래그 시작 처리
    public void OnBeginDrag(PointerEventData eventData)
    {
        Debug.Log("OnBeginDrag");

        PointerEventData pointerData = new PointerEventData(EventSystem.current);
        pointerData.position = Input.mousePosition;

        List<RaycastResult> results = new List<RaycastResult>();
        EventSystem.current.RaycastAll(pointerData, results);
        foreach (RaycastResult result in results)
        {
            if (result.gameObject == gameObject)
            {
                CardArrowLineMaker.Instance.SetIsDragging(true);
                CardArrowLineMaker.Instance.SetStartPoint(gameObject.transform);
            }
        }
    }

    // 드래그 중 처리
    public void OnDrag(PointerEventData eventData)
    {
        Debug.Log("OnDrag");
        Vector3 worldPos;
        RectTransformUtility.ScreenPointToWorldPointInRectangle(
            CardArrowLineMaker.Instance.Targets[1].GetComponent<RectTransform>(),
            Input.mousePosition,
            Camera.main,
            out worldPos);

        CardArrowLineMaker.Instance.Targets[1].position = worldPos;
    }

    // 드래그 종료 처리
    public void OnEndDrag(PointerEventData eventData)
    {
        Debug.Log("OnEndDrag");
        CardArrowLineMaker.Instance.SetIsDragging(false);


        for (int i = 0; i < CardArrowLineMaker.Instance.Targets.Length; i++)
        {
            CardArrowLineMaker.Instance.Targets[i] = null;
        }
    }
}

카드에 드래그와 관련된 모든 코드를 박아넣는다

 

진짜로?

 

일단 카드에 넣고 확인하니 라인도 안그려지고 드래그도 작동하지 않는다

 

뭔가 잘못 짠 매니저에 있던 코드를 그대로 박아서 생기는 문제 같다

 

    // 드래그 시작 처리
    public void OnBeginDrag(PointerEventData eventData)
    {
        Debug.Log("OnBeginDrag");

        
        CardArrowLineMaker.Instance.SetIsDragging(true);
        CardArrowLineMaker.Instance.SetStartPoint(gameObject.transform);
    }

 

드래그 시작시, 애초에 그래픽을 눌러야 처리가 되니, 그냥 조건 없이 바로 처리하면 된다

 

    // 드래그 중 처리
    public void OnDrag(PointerEventData eventData)
    {
        Debug.Log("OnDrag");

        Vector3 worldPos;
        RectTransformUtility.ScreenPointToWorldPointInRectangle(
            CardArrowLineMaker.Instance.Targets[1].GetComponent<RectTransform>(),
            Input.mousePosition,
            Camera.main,
            out worldPos);

        CardArrowLineMaker.Instance.Targets[1].position = worldPos;
    }

RectTransform기준으로 작성해야 하는데 그냥 vec3으로 받아서 처리하고 있다

 

    private Canvas _canvas;

    public void Init(Canvas canvas)
    {
        _canvas = canvas;
    }

recttransform에서 마우스 좌표를 가저올 수 있는 canvas를 받아와서 처리한다

 

    /// <summary>
    /// 카드의 여러 값을 생싱시 세팅
    /// </summary>
    /// <param name="id"></param>
    public void Init(Canvas canvas, int id)
    {
        _canvas = canvas;
        _cardID = id;
        transform.GetChild(0).GetComponent<TextMeshProUGUI>().text = _cardID.ToString();
    }

이를 생성시 받도록 한다

    // 드래그 중 처리
    public void OnDrag(PointerEventData eventData)
    {
        Debug.Log("OnDrag");

        RectTransform canvasRT = _canvas.GetComponent<RectTransform>();
        Camera cam = null;

        Vector3 worldPos;
        RectTransformUtility.ScreenPointToWorldPointInRectangle(
            canvasRT,      // 변환 기준 RectTransform
            eventData.position,    // 현재 마우스/터치 스크린 좌표
            cam,           // 사용할 카메라 (또는 null)
            out worldPos   // 변환된 월드 좌표
        );
        CardArrowLineMaker.Instance.SetEndPoint(worldPos);
    }

이렇게 세팅한 값을 바탕으로 드래그시 마우스 좌표값을 받아서 라인을 그린다

 

끔찍한 null의 지옥이 벌어진다

 

    // 드래그 종료 처리
    public void OnEndDrag(PointerEventData eventData)
    {
        Debug.Log("OnEndDrag");
        CardArrowLineMaker.Instance.SetIsDragging(false);


        for (int i = 0; i < CardArrowLineMaker.Instance.Targets.Length; i++)
        {
            CardArrowLineMaker.Instance.Targets[i] = null;
        }
    }

아마 종료시 null로 만들면 안되는 것 같다

 

    // 드래그 시작 처리
    public void OnBeginDrag(PointerEventData eventData)
    {
        Debug.Log("OnBeginDrag");

        CardArrowLineMaker.Instance.ActiveLineDrawer(true);

        CardArrowLineMaker.Instance.SetIsDragging(true);
        CardArrowLineMaker.Instance.SetStartPoint(gameObject.transform);
        
    }

    // 드래그 중 처리
    public void OnDrag(PointerEventData eventData)
    {
        Debug.Log("OnDrag");

        RectTransform canvasRT = _canvas.GetComponent<RectTransform>();
        Camera cam = null;

        Vector3 worldPos;
        RectTransformUtility.ScreenPointToWorldPointInRectangle(
            canvasRT,      // 변환 기준 RectTransform
            eventData.position,    // 현재 마우스/터치 스크린 좌표
            cam,           // 사용할 카메라 (또는 null)
            out worldPos   // 변환된 월드 좌표
        );
        CardArrowLineMaker.Instance.SetEndPoint(worldPos);
    }

    // 드래그 종료 처리
    public void OnEndDrag(PointerEventData eventData)
    {
        Debug.Log("OnEndDrag");
        CardArrowLineMaker.Instance.SetIsDragging(false);


        CardArrowLineMaker.Instance.ActiveLineDrawer(false);
    }

대충 시작과 끝에서 컴포넌트 자체를 끄고 켜는 것으로 처리한다

 

드래그시 언사인 레퍼런스가 나온다

 

그냥 세팅은 안해준 것이였다

 

세팅후 확인해보니

뭔가 마우스 끝 좌표가 이상하게 그려지고 있다

 

더보기

이렇게 카메라를 null로 두면 ScreenSpaceOverlay 모드에서만 맞는 값이 나와.
그런데 네 씬은 **월드 좌표(LineRenderer)**와 연결돼 있어서 화면 좌표가 월드에서 말도 안 되는 위치(수만 단위)로 변환되는 거야.


해결 방법

  1. UI 좌표 그대로 쓰려는 경우
    LineRenderer도 UI용(UILineRenderer 같은 패키지)으로 바꿔야 해.
    → 하지만 지금은 3D 라인으로 그리니까 이건 아님.
  2. 게임 월드 좌표로 쓰려는 경우
    메인 카메라 기준으로 ScreenToWorldPoint를 써야 해.
 
Vector3 screenPos = eventData.position; screenPos.z = 10f; // 카메라로부터의 거리 (카메라 near/far 안쪽 값) Vector3 worldPos = Camera.main.ScreenToWorldPoint(screenPos); CardArrowLineMaker.Instance.SetEndPoint(worldPos);
  1. Canvas 모드 확인
  • Canvas가 Screen Space - Overlay라면: RectTransformUtility는 UI 안에서 좌표 얻는 용도. → 월드 오브젝트용으로는 안 맞음.
  • Canvas가 World Space라면: 카메라 지정해줘야 정상 변환됨.

카메라 세팅의 문제라고 한다

 

현재 z가 -10으로 세팅되어 있는데 이것이 연산에 오류를 만들어 낸다

-10만큼 보정이 들어가야 한다

 

    // 드래그 시작 처리
    public void OnBeginDrag(PointerEventData eventData)
    {
        Debug.Log("OnBeginDrag");


        CardArrowLineMaker.Instance.ActiveLineDrawer(true);
        CardArrowLineMaker.Instance.SetIsDragging(true);

        var cam = Camera.main;
        float depth = 0f - cam.transform.position.z;
        Vector3 sp = new Vector3(eventData.position.x, eventData.position.y, depth);
        Vector3 start = cam.ScreenToWorldPoint(sp);
        start.z = 0f;
        CardArrowLineMaker.Instance.SetStartPoint(start);
        
    }

    // 드래그 중 처리
    public void OnDrag(PointerEventData eventData)
    {
        Debug.Log("OnDrag");

        var cam = Camera.main;
        float depth = 0f - cam.transform.position.z; // 목표 z=0
        Vector3 sp = new Vector3(eventData.position.x, eventData.position.y, depth);
        Vector3 end = cam.ScreenToWorldPoint(sp);
        end.z = 0f;
        CardArrowLineMaker.Instance.SetEndPoint(end);
    }

해당 보정을 처리해 준다

 

 

잘 그려지는 것을 확인할 수 있다