[Unity] 마우스와 상호작용하는 오브젝트 구현

Min·2024년 8월 13일

Project일지

목록 보기
8/8

아아 그건 '버튼'이라고 하는거다

타워 건설, 업그레이드 등 UI에 사용 할, 월드좌표에서 작동하는 버튼이 필요했다. 처음엔 당연히 Unity의 시스템을 활용하고 싶었다. Canvas는 당연히 뷰포트 좌표 뿐만 아니라 월드 좌표도 지원하는 기능이 있기 때문이다.

여러 생각을 했다. 일단 월드에서 작동하는 오브젝트에 달려있는 버튼은 당연히 오브젝트를 부모로 가지고 있어야 한다고 생각했다. 그러나 Unity에서 지원하는 UI시스템은 반드시 Canvas를 부모로 가져야 한다. 여기서 모든 의문이 시작된다.

1. 그럼 Canvas를 자식으로 가져야 하나?
실제로 다른사람들은 어떻게 작업하는지 모르겠지만, 오브젝트마다 Canvas를 가지는 것이 너무나 큰 낭비라고 생각되었다. 하나의 Canvas안에서 모든 버튼을 관리할 수 있으면 될 것 같다는 생각을 했기 때문에 좋은 설계로 보이지 않았다.

2. 그럼 버튼은 반드시 Canvas를 부모로 가져야 하는데, 실질적으로 부모 자식 관계를 가져야할(생애주기와 트랜스폼 연산을 함께 해야할) 오브젝트의 관계 설정을 어떻게 해야하지?
이건 그야말로 너무 복잡하다는 생각이 들었다. UiManager를 만들어도, 내가 버튼과 오브젝트의 관계를 직접 설정하고 구현하는것은 복잡하고 많은 논리적 오류를 야기시킬 수 있을거라는 생각이 들었다.

그냥 버튼 만들자

기존 프레임 워크에서 버튼을 만들고 사용한 경험이 있기 때문에, 해당 로직을 활용해서 다시 버튼을 직접 구현하기로 하였다. Canvas에 종속되지 않는 버튼은, 오브젝트와 부모 자식관계를 형성할 수 있기 때문에, 생애주기부터 트랜스폼까지 걱정없이 사용해도 된다.

버튼 개요 설명

  • ReleaseSprite, HoverSprite, PressSprite
    이 게임에서 버튼은 주로 상태에 따라 Sprite가 교체되는 방식으로 작동한다. 때문에 상태에 따른 Sprite를 미리 저장해놓고, 해당 상태가 되면 SpriteRenderer가 해당 상태의 Sprite를 보여주는 형태로 작동한다.
  • ColScale
    말 그대로 버튼의 충돌체 크기이다. 마우스에 달려있는 피직스가 버튼의 충돌체를 감지하도록 설계되어있다.
  • ClickCallBack
    콜백함수를 저장하는 delecate이다. 클릭을 하면, ClickCallBack의 함수가 작동한다.
  • ButtonRenderer
    버튼 렌더러이다. 버튼 상태에 따른 Sprite를 보여줄 수 있다.
  • ButtonState
    버튼의 상태를 Enum으로 설정해놓았다.
  • ButtonCol
    버튼의 박스 콜라이더이다. 마우스에 달려있는 피직스와 상호작용 할 예정이다.

    이 밖에 나중에 추가된 프로퍼티들이 있긴 하나, 중요하지 않기 때문에 필요할 때 설명하면 좋을 것 같다.

마우스 개요 설명

  • ReleaseClickEvent
    아무 버튼과도 상호작용하고 있지 않은채로 클릭할 시, 호출되어야 하는 함수들의 모음(관찰자 패턴 활용)
  • ButtonLayer
    마우스의 피직스가 상호작용 할 레이어마스크
  • WolrdPos
    Update에서 매 프레임 갱신해주는 마우스의 월드 좌표
  • 마우스를 클릭할 시 일어나는 일
  1. 래이캐스트를 통해 충돌한 버튼 콜라이더 정보를 모두 가지고옴.
  2. 버튼의 sortingOrder를 가지고 정렬.
  3. 만약 충돌한 버튼이 없다면, ReleaseClickEvents를 모두 호출(주로 UI를 종료할 때 사용됨)
  4. sortingOrder상 가장 위에있는 버튼의 클릭 함수를 호출(Object Peeking)

코드

  • 버튼
using Unity.VisualScripting;
using UnityEngine;

public enum ButtonState
{
    Null = -1,
    Release,
    Hover,
    Press
}

abstract public class SC_MyButton : MonoBehaviour
{
    //리소스 캐싱을 위해 만들면 좋은 인터페이스(권장합니다)
    //static Sprite CacheReleaseSprite = null;
    //static Sprite CacheHoverSprite = null;
    //static Sprite CachePressSprite = null;

    virtual protected void Awake()
    {
        ButtonRenderer = gameObject.AddComponent<SpriteRenderer>();
        ButtonRenderer.sprite = ReleaseSprite;
        SettingButtonRenderOrder();

        ButtonCol = gameObject.AddComponent<BoxCollider2D>();
        ButtonCol.size = new Vector2(ColScale.x, ColScale.y);
        gameObject.tag = "MyButton";
        gameObject.layer = LayerMask.NameToLayer("MyButton");
    }

    // Update is called once per frame
    void Update()
    {
        Vector2 MousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        RaycastHit2D HitInfo = Physics2D.Raycast(MousePos, Vector2.zero);

        if (HitInfo.collider == ButtonCol)
        {
            if (Input.GetMouseButtonDown((int)MouseButton.Left))
            {
                State = ButtonState.Press;
                ButtonRenderer.sprite = PressSprite;
            }
            else
            {
                if (State != ButtonState.Hover)
                {
                    State = ButtonState.Hover;
                    ButtonRenderer.sprite = HoverSprite;
                    if(HoverEvent != null)
                    {
                        HoverEvent();
                    }
                }
            }
        }
        else
        {
            if (State != ButtonState.Release)
            {
                State = ButtonState.Release;
                ButtonRenderer.sprite = ReleaseSprite;
                if (ReleaseEvent != null)
                {
                    ReleaseEvent();
                }
            }
        }
    }
    
    protected abstract void SettingButtonRenderOrder();
    
    protected Sprite ReleaseSprite;
    protected Sprite HoverSprite;
    protected Sprite PressSprite;
    protected Vector4 ColScale = Vector4.one;
    protected System.Action ClickCallBack;
    public System.Action Click
    {
        get
        {
            return ClickCallBack;
        }
        set
        {
            ClickCallBack = value;
        }
    }

    protected SpriteRenderer ButtonRenderer;
    private ButtonState state = ButtonState.Null;
    protected ButtonState State
    {
        get
        {
            return state;
        }

        private set
        {
            state = value;
        }
    }
    private BoxCollider2D ButtonCol;

    private System.Action hoverEvent = null;
    public System.Action HoverEvent
    {
        get
        {
            return hoverEvent;
        }

        set
        {
            hoverEvent = value;
        }
    }

    private System.Action releaseEvent = null;
    public System.Action ReleaseEvent
    {
        get
        {
            return releaseEvent;
        }

        set
        {
            releaseEvent = value;
        }
    }
}
  • 마우스
using System.Collections.Generic;
using System.Linq;
using Unity.VisualScripting;
using UnityEngine;

public class SC_MyMouseBase : MonoBehaviour
{
    static public SC_MyMouseBase MouseSetting = null;

    private void Awake()
    {
        MouseSetting = this;
        ButtonLayer |= (1 << LayerMask.NameToLayer("MyButton"));
    }

    // Update is called once per frame
    void Update()
    {
        WorldPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        if (Input.GetMouseButtonUp((int)MouseButton.Left))
        {
            ButtonClick();
        }
    }

    private void ButtonClick()
    {
        RaycastHit2D[] HitInfos = Physics2D.RaycastAll(WorldPos, Vector2.zero, 10.0f, ButtonLayer);
        HitInfos.OrderBy(HitInfos => 
        HitInfo.collider.GetComponent<SpriteRenderer>().sortingOrder
        );

        if (HitInfos.Length == 0)
        {
            for(int i = 0; i < ReleaseClickEvents.Count; i++)
            {
                ReleaseClickEvents[i]();
            }
            return;
        }

        HitInfos[0].collider.gameObject.GetComponent<SC_MyButton>().Click();
    }

    public void RegistReleaseClickEvent(System.Action Event)
    {
        ReleaseClickEvents.Add(Event);
    }

    //관찰자 패턴 활용?
    private List<System.Action> ReleaseClickEvents = new List<System.Action>();

    private LayerMask ButtonLayer;
    private Vector2 WorldPos = Vector2.zero;
    private RaycastHit2D HitInfo;
    private Physics2D MousePhysics;
    
}

사용 후기

흠 다시 보니까 버그가 생길 여지가 있을 수 있겠네요. 특히 sortingOrder에 따른 로직이 걱정되네요. 잘 작동해서 의식하지 않았는데... 나중에 다시 수정해봐야겠습니다.

profile
티내는 청년

0개의 댓글