건축 입력 처리 시스템

목차

1. 요구 사항

2. 흐름도

3. 구현

        3.1. 카메라와 레이어 설정

       3.2. 입력 데이터 시스템

       3.3. 마우스 위치 기반 맵 좌표 계산

       3.4. UI 입력 충돌 방지

       3.5. 입력 처리 루프

4. 개발 의도

1. 시스템 요구 사항

Grid 기반 건축 시스템에서는 플레이어의 입력을 통해 구조물을 배치하거나 회전시키고, 배치 작업을 취소하거나 삭제 모드로 전환하는 다양한 상호작용이 발생한다.

이러한 입력은 키보드와 마우스를 통해 발생하며, 건축 시스템은 이러한 입력을 실시간으로 감지하여 적절한 시스템으로 전달해야 한다.

하지만 건축 시스템 내부에서 직접 Unity의 입력 API를 호출하게 되면 입력 처리 로직과 배치 로직이 서로 강하게 결합되는 문제가 발생할 수 있다.

예를 들어 PlacementManager나 PlacementSelector와 같은 시스템이 직접 Input 클래스를 호출하게 되면 입력 처리 방식이 변경될 때 여러 클래스의 코드가 동시에 수정되어야 할 수 있다.

또한 건축 시스템은 단순히 키 입력만 처리하는 것이 아니라 마우스 위치를 기반으로 Grid 좌표를 계산하고, UI 위에서 입력이 발생했는지 판단하며, 마우스 클릭과 키 입력을 다양한 건축 동작으로 변환해야 한다.

이러한 입력 처리 기능은 건축 시스템의 핵심 로직과는 성격이 다른 별도의 책임 영역에 속한다.

따라서 건축 시스템에서는 플레이어 입력을 독립적으로 처리하고, 입력 이벤트를 다른 시스템으로 전달하는 전용 입력 관리 계층이 필요하다.

이 계층은 Unity 입력 시스템과 직접 상호작용하면서도 건축 시스템의 다른 구성 요소들과는 느슨하게 결합된 구조를 유지해야 한다.

이를 위해 InputManager는 다음과 같은 역할을 수행한다.

플레이어의 키보드 및 마우스 입력을 감지하고, 이를 건축 시스템에서 사용할 수 있는 이벤트 형태로 변환한다.

마우스 위치를 기반으로 실제 맵의 선택 위치를 계산한다.

마우스가 UI 위에서 동작하고 있는지를 판단하여 잘못된 입력 처리를 방지한다.

입력 처리 로직을 건축 로직과 분리하여 단일 책임 원칙을 유지한다.

2. 흐름도

플레이어 입력

  ↓

Unity Input System

  ↓

InputManager

  ↓

입력 이벤트 발생

(OnMousePressed / OnRotate / OnCancel 등)

  ↓

PlacementManager

  ↓

건축 시스템 로직 실행

건축 시스템에서 플레이어 입력은 Unity의 Input API를 통해 감지된다.

그러나 InputManager는 단순히 입력을 읽는 역할에서 끝나지 않고, 해당 입력을 건축 시스템이 이해할 수 있는 이벤트 형태로 변환한다.

예를 들어 마우스 왼쪽 버튼을 클릭하면 InputManager는 이를 단순한 입력 값으로 처리하지 않고 OnMousePressed 이벤트로 변환한다.

이후 PlacementManager와 같은 시스템은 이 이벤트를 구독하고 있다가 실제 구조물 배치 로직을 실행하게 된다.

이러한 구조를 사용하면 입력 처리 로직과 건축 로직이 직접 연결되지 않기 때문에 시스템 간 결합도를 크게 낮출 수 있다.

또한 입력 방식이 변경되더라도 InputManager 내부만 수정하면 되므로 유지보수성이 높아진다.

3. 구현

3.1 카메라와 레이어 설정
[SerializeField]
private Camera sceneCamera;

[SerializeField]
private LayerMask placementLayermask;

sceneCamera는 마우스 위치를 기반으로 월드 좌표를 계산하기 위해 사용되는 카메라 참조이다.

Unity에서는 화면 좌표(Screen Position)를 월드 공간의 Ray로 변환할 때 Camera 객체가 필요하다.

SerializeField 속성을 사용하면 private 변수이면서도 Unity Inspector에서 값을 설정할 수 있다.

이 방식은 캡슐화를 유지하면서도 디자이너나 개발자가 에디터에서 쉽게 값을 설정할 수 있도록 해준다.

placementLayermask는 Raycast가 충돌을 검사할 레이어를 제한하기 위해 사용된다.

Unity의 LayerMask는 특정 레이어만 선택적으로 검사할 수 있도록 해주는 기능이다.

이 기능을 사용하면 Raycast가 씬의 모든 오브젝트를 검사하지 않고, 배치 가능한 바닥이나 Grid와 같은 특정 레이어만 검사하도록 제한할 수 있다.

이 방식은 두 가지 장점을 가진다.

첫째는 성능 측면이다.

불필요한 충돌 검사를 줄일 수 있기 때문에 Raycast 연산 비용이 감소한다.

둘째는 정확성이다.

건축 시스템에서는 플레이어가 선택할 수 있는 대상이 제한되어 있기 때문에 특정 레이어만 검사하는 것이 더 안정적인 선택 위치 계산을 가능하게 한다.

3.2. 입력 데이터 시스템
public event Action OnMousePressed, OnMouseReleased, OnCancle, OnUndo;
public event Action<int> OnRotate;
public event Action<bool> OnDelete;

InputManager는 이벤트 기반 구조를 사용하여 입력을 다른 시스템으로 전달한다.

여기서 사용된 Action 타입은 C#에서 제공하는 delegate 타입으로, 특정 시점에 호출될 수 있는 메서드 참조를 저장할 수 있다.

event 키워드는 외부 클래스가 이벤트를 구독할 수 있도록 허용하면서도 직접 이벤트를 호출하지 못하도록 보호하는 역할을 한다.

이 구조를 사용하면 InputManager는 입력을 감지한 후 이벤트를 발생시키기만 하고, 실제 입력 처리 로직은 이벤트를 구독한 다른 시스템이 수행하게 된다.

OnRotate 이벤트는 Action<int> 타입을 사용하고 있는데, 이는 회전 방향 정보를 전달하기 위해서이다.

예를 들어 -1은 왼쪽 회전, 1은 오른쪽 회전을 의미하도록 사용할 수 있다.

OnDelete 이벤트는 bool 값을 전달하는 구조로 설계되어 있다.

Delete 키가 눌린 상태인지 여부를 전달함으로써 삭제 모드의 시작과 종료를 모두 처리할 수 있도록 구성하였다.

이러한 이벤트 기반 구조는 Observer 패턴과 유사한 방식으로 동작한다.

입력 시스템은 이벤트를 발생시키는 역할만 수행하고, 실제 동작은 이를 구독한 시스템이 처리한다.

결과적으로 입력 시스템과 건축 시스템 사이의 의존성이 크게 감소하게 된다.

3.3. 마우스 위치 기반 맵 좌표 계산
public Vector3 GetSelectedMapPosition()
{
    Vector3 mousePos = Input.mousePosition;
    mousePos.z = sceneCamera.nearClipPlane;
 
    Ray ray = sceneCamera.ScreenPointToRay(mousePos);
    RaycastHit hit;
    if (Physics.Raycast(ray, out hit, 100, placementLayermask))
    {
        lastPosition = hit.point;
    }
    return lastPosition;
}

이 함수는 마우스 위치를 기반으로 실제 맵 위의 선택 좌표를 계산하는 역할을 한다.

Unity에서 마우스 위치는 화면 좌표(Screen Position) 형태로 제공되지만, 건축 시스템에서는 월드 좌표가 필요하다.

먼저 Input.mousePosition을 사용하여 현재 마우스 위치를 가져온다.

Input.mousePosition은 화면 기준 픽셀 좌표를 반환하는 Unity API이다.

이후 Camera.ScreenPointToRay 함수를 사용하여 해당 화면 좌표를 월드 공간의 Ray로 변환한다.

ScreenPointToRay는 카메라에서 특정 화면 위치를 향해 발사되는 Ray를 생성하는 함수이다.

이 Ray를 이용해 Physics.Raycast를 수행하면 실제 씬에서 마우스가 가리키는 위치를 계산할 수 있다.

Raycast는 Unity 물리 시스템에서 제공하는 충돌 검사 기능으로, Ray가 특정 Collider와 충돌했는지를 검사한다.

여기서 placementLayermask를 사용하여 Raycast 대상 레이어를 제한한다.

Raycast가 성공하면 hit.point 값을 통해 실제 충돌 위치를 얻을 수 있다.

이 값은 lastPosition 변수에 저장된다.

lastPosition을 사용하는 이유는 Raycast가 실패하는 경우에도 마지막으로 유효했던 위치를 유지하기 위해서이다.

예를 들어 마우스가 잠시 Collider가 없는 공간을 가리키더라도 건축 프리뷰 위치가 갑자기 사라지지 않도록 하기 위한 안정성 설계이다.

3.4. UI 입력 충돌 방지
public bool IsInteractingWithUI()
    => EventSystem.current.IsPointerOverGameObject();

이 함수는 현재 마우스 입력이 UI 위에서 발생했는지를 검사하는 기능을 제공한다.

Unity에서는 UI 시스템이 별도의 이벤트 시스템(EventSystem)을 통해 동작한다.

EventSystem.current.IsPointerOverGameObject 함수는 현재 마우스 포인터가 UI 요소 위에 위치하고 있는지를 검사하는 기능을 제공한다.

건축 시스템에서는 UI 위에서 클릭이 발생했을 때 구조물이 배치되는 상황을 방지해야 한다.

예를 들어 플레이어가 UI 버튼을 클릭했는데 동시에 건축 시스템이 마우스 클릭을 감지하여 구조물을 배치하는 문제가 발생할 수 있다.

따라서 입력 처리 단계에서 UI와 게임 월드 입력을 분리하는 것이 중요하다.

이 함수는 이러한 문제를 방지하기 위한 보호 장치 역할을 수행한다.

3.5. 입력 처리 루프
private void Update()
{
    if (Input.GetKeyDown(KeyCode.Escape))
        OnCancle?.Invoke();

    if (Input.GetKey(KeyCode.LeftControl) && Input.GetKeyDown(KeyCode.Z))
        OnUndo?.Invoke();

    if (Input.GetMouseButtonDown(0))
        OnMousePressed?.Invoke();
    if (Input.GetMouseButtonUp(0))
        OnMouseReleased?.Invoke();

    if (Input.GetKeyDown(KeyCode.Comma))
        OnRotate?.Invoke(-1);
    if (Input.GetKeyDown(KeyCode.Period))
        OnRotate?.Invoke(1);

    if (Input.GetKeyDown(KeyCode.Delete))
        OnDelete?.Invoke(true);
    if (Input.GetKeyUp(KeyCode.Delete))
        OnDelete?.Invoke(false);
}

Update 함수는 Unity의 게임 루프에서 매 프레임 호출되는 함수이다.

Unity에서 키보드 입력이나 마우스 입력은 일반적으로 Update 함수에서 처리된다.

Unity의 Input API는 프레임 단위 입력 감지 방식으로 동작한다.

예를 들어 Input.GetKeyDown이나 Input.GetMouseButtonDown 같은 함수는 특정 프레임에서만 true 값을 반환한다.

따라서 이러한 입력을 정확하게 감지하려면 매 프레임 Update 함수에서 검사해야 한다.

이 함수에서는 건축 시스템에서 필요한 다양한 입력을 감지한다.

Escape 키 입력은 건축 모드를 취소하는 이벤트로 변환된다.

Ctrl + Z 입력은 되돌리기 기능으로 전달된다.

마우스 클릭 입력은 구조물 배치 이벤트로 전달된다.

Comma와 Period 키 입력은 구조물 회전 이벤트로 전달된다.

Delete 키 입력은 삭제 모드의 시작과 종료 이벤트로 변환된다.

이러한 입력 처리 로직을 InputManager 내부에 집중시키면 건축 시스템의 다른 클래스들은 입력 API를 직접 호출할 필요가 없어진다.

결과적으로 시스템 구조가 훨씬 단순해지고 유지보수성이 향상된다.

4. 개발 의도

InputManager의 설계 목표는 건축 시스템에서 입력 처리 책임을 완전히 분리하는 것이다.

건축 시스템의 핵심 로직은 구조물 배치, 공간 검사, Grid 좌표 계산과 같은 기능에 집중해야 하며, 플레이어 입력 처리까지 동시에 담당하게 되면 클래스의 책임이 지나치게 커질 수 있다.

이를 방지하기 위해 입력 처리는 InputManager에서만 수행하도록 설계하였다.

InputManager는 Unity Input API와 직접 상호작용하면서 입력을 감지하고, 이를 이벤트 형태로 다른 시스템에 전달한다.

이벤트 기반 구조를 사용한 이유는 시스템 간 결합도를 낮추기 위해서이다.

PlacementManager나 Selector 시스템은 입력을 직접 감지하지 않고 InputManager의 이벤트를 구독하는 방식으로 동작한다.

이 구조 덕분에 입력 방식이 변경되더라도 건축 시스템의 다른 구성 요소는 영향을 받지 않는다.

또한 Raycast 기반 마우스 위치 계산 기능을 InputManager 내부에 포함시켜 입력 처리와 좌표 계산을 하나의 계층에서 관리하도록 설계하였다.

이를 통해 건축 시스템의 다른 클래스들은 단순히 선택 위치 정보만 사용하면 되도록 만들었다.

결과적으로 InputManager는 단순한 입력 처리 스크립트가 아니라, Unity 입력 시스템과 건축 시스템 사이를 연결하는 입력 해석 계층(Input Interpretation Layer) 역할을 수행하도록 설계된 시스템이다.