입력

Unity Input System 위에 구축된, 컨텍스트 스택과 인터랙티브 리바인딩을 갖춘 액션 기반 입력.

Unity Input System 위의 액션 맵, 컨텍스트 스택, 인터랙티브 리바인딩 · 이벤트 버스로의 선택적 이벤트 발행 · no-op 대체물: NullInputService

CGS는 키보드, 마우스, 게임패드 입력을 IInputService — Unity Input System 패키지 위의 얇은 계층 — 를 통해 읽습니다. 코드 곳곳에 원시 키코드 검사를 흩뿌리는 대신 액션 맵으로 작업합니다: "Gameplay"나 "Menu"처럼 이름 붙은 컨트롤 집합을 .inputactions 에셋에 한 번 정의하는 것입니다. 그 위에 서비스는 Unity가 기본으로 제공하지 않는 세 가지를 더합니다: 어떤 액션 맵이 활성인지 전환하는 컨텍스트 스택, 자동 저장을 갖춘 인터랙티브 리바인딩("키를 눌러 재할당"), 그리고 이벤트 버스로의 선택적 입력 이벤트 발행입니다.

무엇보다 먼저: 프로젝트가 새 입력 백엔드를 쓰고 있어야 합니다. Input System 패키지를 설치하는 것(CGS 의존성 매니페스트가 대신 해 줍니다)은 Active Input Handling이라는 프로젝트 설정을 바꾸지 않습니다. 새 프로젝트에서는 *Input Manager (Old)*로 남아 있으며 — 그러면 데모 씬, 실행 가능한 샘플, 그리고 여러분의 액션 맵이 모든 키 입력을 소리 없이 무시합니다. Welcome 창(Tools > Common Game System > Welcome)이 이를 감지해 원클릭 Enable the new Input System 버튼을 보여 줍니다; 에디터가 한 번 재시작되고 입력이 동작합니다. 수동 경로: Edit > Project Settings > Player > Other Settings > Active Input Handling → "Input System Package (New)" 또는 "Both". 자세한 내용은 FAQ를 참조하십시오.

무엇을 하는가

  • 액션 기반 읽기. 액션을 한 번 조회한 뒤(GetAction("Gameplay", "Jump")) 매 프레임 폴링하거나 이벤트를 구독합니다. 게임플레이 코드는 특정 키나 버튼을 결코 언급하지 않으므로 키보드와 게임패드가 같은 경로로 동작합니다.
  • 컨텍스트 스택. 컨텍스트를 push하면(PushContext("Menu")) 그 액션 맵만 활성화되고 나머지는 모두 비활성화됩니다; pop하면 이전 컨텍스트가 이어받습니다. 가장 새로운 컨텍스트가 항상 이기는데, 이것이 게임플레이 위에 중첩된 메뉴에서 정확히 원하는 동작입니다.
  • 인터랙티브 리바인딩. 리바인드를 시작하고, 플레이어가 새 키를 누르게 하고, 완료 콜백을 받습니다. 재정의는 한 번의 호출로 PlayerPrefs에 저장됩니다.
  • 이벤트 발행(옵트인). 어떤 액션의 눌림을 이벤트 버스로 발행하도록 요청하면, 결합이 끊긴 시스템들이 InputAction 참조를 들지 않고도 입력에 반응할 수 있습니다.

서비스는 선택적 CommonGameSystem.Input 어셈블리로 제공됩니다. 프로젝트에서 Input System 패키지를 제거하면 그 어셈블리는 스스로를 자동으로 제외하며, CGS의 나머지는 여전히 컴파일되고 부팅됩니다.

서비스 가져오기

Awake에서 한 번 resolve해 참조를 캐시하십시오:

using CommonGameSystem.Core;
using UnityEngine;

[DefaultExecutionOrder(100)]
public class MyGameMode : MonoBehaviour
{
    private IInputService _input;

    private void Awake()
    {
        _input = ServiceLocator.Resolve<IInputService>();
    }
}

[DefaultExecutionOrder(100)] 어트리뷰트는 여러분의 Awake가 실행되기 전에 CGS 부트스트래퍼가 서비스 등록을 마치도록 보장합니다. Update() 안에서 Resolve를 절대 호출하지 마십시오 — 딕셔너리 조회입니다; 한 번만 캐시하십시오.

API 레퍼런스

액션 조회

InputAction GetAction(string actionMapName, string actionName)

예컨대 "Gameplay" / "Jump"에 해당하는 InputAction을 반환하며, 그 맵이나 액션이 .inputactions 에셋에 정의되어 있지 않으면 null을 반환합니다. 결과를 캐시하십시오 — 핫 패스에서 액션을 조회하지 마십시오.

폴링 (Update 또는 FixedUpdate에서 호출)

T ReadValue<T>(InputAction action)           // Current-frame value (Vector2, float, ...)
bool IsPressed(InputAction action)           // true while held
bool WasPressedThisFrame(InputAction action) // true only on the press frame
bool WasReleasedThisFrame(InputAction action)// true only on the release frame

이벤트 버스로 발행하기 (옵트인)

IDisposable PublishOnStarted(InputAction action)   // publishes InputActionStartedEvent
IDisposable PublishOnPerformed(InputAction action) // publishes InputActionPerformedEvent
IDisposable PublishOnCanceled(InputAction action)  // publishes InputActionCanceledEvent

각 호출은 토큰을 반환합니다; dispose하면 발행이 멈춥니다. 구독 방법은 이벤트 버스 페이지를 참조하십시오.

컨텍스트 스택

IDisposable PushContext(string actionMapName) // Enable only this map, disable the rest
string CurrentContext { get; }                // Top of the stack (null when empty)
IReadOnlyList<string> ContextStack { get; }   // Bottom-to-top snapshot (allocates; debug use)

PushContext는 토큰을 반환합니다; 토큰을 dispose하면 그 컨텍스트가 pop되고 그 아래에 있던 것이 다시 활성화됩니다. 같은 맵 이름을 두 번 push하면 스택 항목이 두 개 따로 생깁니다.

인터랙티브 리바인딩

IInputRebindOperation StartInteractiveRebind(
    InputAction action,
    int bindingIndex = -1,            // -1 = the action's first binding
    string controlsExcluding = "Mouse") // comma-separated controls to ignore

IsCompleted, IsCanceled, ResultBindingPath, 그리고 Completed 이벤트를 가진 작업 토큰을 반환합니다. 리바인드는 한 번에 하나만 실행할 수 있습니다 — 두 번째를 시작하면 InvalidOperationException이 발생합니다. 플레이어가 타임아웃(기본 5초; InputServiceOptions.RebindTimeoutSeconds로 구성 가능) 안에 아무것도 누르지 않으면 리바인드는 스스로 취소됩니다.

바인딩 저장과 복원

void SaveBindingOverrides()                           // Persist all overrides to PlayerPrefs
void LoadBindingOverrides()                           // Re-apply saved overrides
void ResetBindingOverrides(InputAction action = null) // Clear overrides (null = all actions)

디바이스 조회

bool IsDeviceConnected<TDevice>() where TDevice : InputDevice // Any such device present?
TDevice GetDevice<TDevice>() where TDevice : InputDevice      // First matching device, or null

예를 들어 IsDeviceConnected<Gamepad>()는 게임패드 버튼 프롬프트를 보여줄지 여부를 알려 줍니다.

전체 예제

이동을 폴링하고, 점프 눌림을 이벤트 버스로 발행하고, 게임이 일시정지되면 컨텍스트를 바꾸는 플레이어 컨트롤러:

using System;
using CommonGameSystem.Core;
using UnityEngine;
using UnityEngine.InputSystem;

[DefaultExecutionOrder(100)]
public class PlayerController : MonoBehaviour
{
    [SerializeField] private float _moveSpeed = 5f;

    private IInputService _input;
    private InputAction _moveAction;
    private InputAction _jumpAction;
    private IDisposable _jumpEvents;
    private IDisposable _gameplayToken;
    private IDisposable _menuToken;

    private void Awake()
    {
        _input = ServiceLocator.Resolve<IInputService>();
        _moveAction = _input.GetAction("Gameplay", "Move");
        _jumpAction = _input.GetAction("Gameplay", "Jump");
    }

    private void Start()
    {
        // Publish Jump presses to the event bus.
        _jumpEvents = _input.PublishOnPerformed(_jumpAction);

        // Enable the Gameplay action map. Keep the token so we can
        // pop the context when the game pauses.
        _gameplayToken = _input.PushContext("Gameplay");
    }

    private void Update()
    {
        var move = _input.ReadValue<Vector2>(_moveAction);
        transform.Translate(move * _moveSpeed * Time.deltaTime);
    }

    public void OnPause()
    {
        _gameplayToken?.Dispose();               // Pop Gameplay
        _menuToken = _input.PushContext("Menu"); // Push Menu
    }

    public void OnResumeGame()
    {
        _menuToken?.Dispose();                   // Pop Menu
        _gameplayToken = _input.PushContext("Gameplay");
    }

    private void OnDestroy()
    {
        _jumpEvents?.Dispose();
        _menuToken?.Dispose();
        _gameplayToken?.Dispose();
    }
}

"Gameplay""Menu" 맵, 그리고 "Move""Jump" 액션은 여러분 자신의 .inputactions 에셋에서 옵니다 — 서비스는 여러분의 에셋을 읽을 뿐, 맵을 만들어 주지 않습니다.

끄는 방법

ServiceLocator.Replace<IInputService>(new NullInputService());

null 구현이 자리하면:

호출결과
ReadValue<T>()default(T) (Vector2.zero, 0f, ...)
IsPressed() / WasPressedThisFrame() / WasReleasedThisFrame()false
GetAction()null
PushContext() / PublishOn*()빈 토큰(아무것도 하지 않음)
StartInteractiveRebind()이미 취소된 상태의 스텁 작업
Save/Load/ResetBindingOverrides()no-op
IsDeviceConnected() / GetDevice()false / null

컷씬 전용 모드, 헤드리스 서버, 테스트 하네스에 사용하십시오. 그 외 모든 것(UI, 씬 플로우)은 입력 없이도 여전히 컴파일되고 실행됩니다.

흔한 함정

  • 새 프로젝트에서 입력이 먹통 = 잘못된 백엔드. 키보드나 마우스에 아무것도 반응하지 않으면 먼저 Active Input Handling을 확인하십시오 — 이 페이지 상단의 콜아웃과 FAQ를 참조하십시오.

  • 컨텍스트 토큰은 최신 것부터 pop됩니다. PushContext()가 반환한 토큰을 (보통 OnDestroy에서) dispose해 그 컨텍스트를 pop하십시오. 오브젝트가 순서 없이 파괴되어도 서비스는 우아하게 대처하지만, 맨 위가 아닌 컨텍스트를 pop하는 것은 로그에 남지 않습니다.

  • 액션 맵은 '여러분의' 에셋에서 옵니다. 서비스는 맵이나 액션을 만들지 않습니다. "Gameplay", "Menu", "Dialogue" 등을 여러분의 .inputactions 에셋에 정의하십시오.

  • 인터랙티브 리바인드는 한 번에 하나. 리바인드가 이미 실행 중이면 StartInteractiveRebind()InvalidOperationException을 던집니다. 하나가 활성인 동안에는 설정 UI의 다른 리바인드 버튼을 비활성화하십시오.

  • 인터랙션 처리는 여러분의 에셋이 제어합니다. 데드존, 홀드 인터랙션, 멀티 탭 — 전부 인스펙터에서 여러분의 .inputactions 에셋에 구성합니다. 서비스는 가이드용 기본값(예: InputServiceOptions.StickDeadzoneMin = 0.125f)을 노출하지만 여러분 에셋의 구성을 결코 재정의하지 않습니다.

관련 페이지

  • 이벤트 버스 — 이 서비스가 발행할 수 있는 입력 이벤트: InputActionStartedEvent, InputActionPerformedEvent, InputActionCanceledEvent, InputContextPushedEvent, InputContextPoppedEvent, InputDeviceConnectedEvent, InputDeviceDisconnectedEvent, InputRebindStartedEvent, InputRebindCompletedEvent
  • UI 프레임워크 — 패널이 열리고 닫힐 때 입력 컨텍스트를 자동으로 push/pop
  • 로거 — 컨텍스트 push/pop, 디바이스 연결, 리바인드 타임아웃이 Input 카테고리로 로그에 남습니다
  • FAQ — Active Input Handling 해결법 단계별 안내