입력
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 해결법 단계별 안내