커맨드 레지스트리
개발자 콘솔의 데이터 코어 — 커맨드 테이블, 토크나이저, 실행, 히스토리, 자동완성. UI는 직접 만듭니다.
화면만 빼고 개발자 콘솔에 필요한 전부 — 커맨드, 파싱, 히스토리, 자동완성.
| 인터페이스 | ICommandRegistry |
| 끄기 스위치 | NullCommandRegistry |
| 어셈블리 | CommonGameSystem.Core |
| 시작 | 부팅 시 자동 등록 (23개 서비스 중 하나) — 별도 설정 불필요 |
하는 일
커맨드 레지스트리는 개발자 콘솔, 치트 메뉴, 자동화 스크립트가 꽂히는 엔진입니다. 데이터를 소유합니다: 커맨드 테이블(이름에서 핸들러로), 원시 입력 줄을 위한 토크나이저(give sword 3를 give, sword, 3으로 나누는 부분), 크기가 제한된 커맨드 히스토리, 그리고 접두사 자동완성. 화면에는 아무것도 그리지 않습니다 — 콘솔 UI(렌더링, 입력 캡처, 토글 키)는 여러분이 만드는 몫이고, 아래 콘솔 직접 만들기 섹션이 그 방법을 보여 줍니다.
런타임 문제는 호출자를 절대 크래시시키지 않습니다. 알 수 없는 커맨드, 잘못된 인자, throw하는 핸들러는 격리되고 로그되며, 실패한 CommandResult로 반환됩니다. 프로그래밍 오류만 throw합니다: null이거나 빈 이름의 등록, null 핸들러, dispose된 후의 사용, 워커 스레드에서의 호출.
빠른 시작
using CommonGameSystem.Core;
using UnityEngine;
[DefaultExecutionOrder(100)] // Run after the framework has started.
public class MyCheats : MonoBehaviour
{
private ICommandRegistry _commands;
private void Awake()
{
_commands = ServiceLocator.Resolve<ICommandRegistry>(); // registered automatically at startup
_commands.Register("give", args =>
{
if (args.Count < 2) return CommandResult.Fail("usage: give <id> <count>");
return CommandResult.Ok($"gave {args[1]}x {args[0]}");
}, new CommandMetadata("Grant an item", "give <id> <count>"));
CommandResult r = _commands.TryExecute("give sword 3"); // tokenizes, dispatches, records history
Debug.Log(r.Message);
}
}
보여 준 대로 Awake()에서 서비스를 한 번 resolve해 캐시하세요. 프레임워크가 시작된 뒤에 실행되도록 클래스에 [DefaultExecutionOrder(100)]를 붙이세요.
API 레퍼런스
등록
| 멤버 | 설명 |
|---|---|
void Register(string name, CommandHandler handler, CommandMetadata metadata = default) | 이름을 핸들러에 매핑합니다. 기존 이름을 다시 등록하면 생성 시 선택한 중복 정책을 따릅니다: 경고와 함께 교체, 조용히 교체, 또는 throw. null/공백 이름이나 null 핸들러는 throw합니다. |
bool Unregister(string name) | 이름을 제거합니다. 있었다면 true, 알 수 없는 이름·null·공백이면 false. 절대 throw하지 않습니다. |
bool IsRegistered(string name) | 이름이 등록되어 있는지. null이나 공백이면 false를 반환합니다. |
bool TryGetMetadata(string name, out CommandMetadata metadata) | true와 해당 항목의 메타데이터, 또는 false와 default. |
IReadOnlyList<string> RegisteredNames { get; } | 등록된 모든 이름의 읽기 전용 정렬 스냅샷. 스냅샷은 수정할 수 없습니다. |
실행
| 멤버 | 설명 |
|---|---|
CommandResult TryExecute(string rawLine) | 줄을 토크나이즈해 첫 토큰을 커맨드 이름으로, 나머지를 인자로 취급하고, 그 줄을 히스토리에 기록한 뒤 핸들러의 결과를 반환합니다. 런타임 입력에 대해서는 절대 throw하지 않습니다. |
CommandResult TryExecute(string name, IReadOnlyList<string> args) | 미리 파싱된 이름과 인자를 직접 디스패치합니다. 토크나이즈를 건너뛰고, 히스토리를 기록하지 않습니다. |
IReadOnlyList<string> Tokenize(string rawLine) | 단일 패스 토크나이저: 공백 분리, 큰따옴표 그룹핑, 백슬래시 이스케이프. 셸 확장은 없습니다. 절대 throw하지 않습니다. |
자동완성
| 멤버 | 설명 |
|---|---|
IReadOnlyList<string> Lookup(string prefix) | prefix로 시작하는 등록된 이름들(정렬됨). 비어 있거나 null인 접두사는 모든 이름을 반환합니다. 절대 throw하지 않습니다. |
히스토리 (데이터만 — UI 없음)
| 멤버 | 설명 |
|---|---|
string Previous() | 커서를 더 오래된 항목 쪽으로 한 칸 옮기고 그 줄을 반환합니다. 가장 오래된 항목에서 멈추며(순환 없음), 히스토리가 비어 있으면 null을 반환합니다. |
string Next() | 더 새로운 항목 쪽으로 이동합니다. 가장 새로운 항목을 지나면(빈 입력 상태) null을 반환합니다. |
void ResetHistoryCursor() | 커서를 가장 새로운 위치로 되돌립니다. |
IReadOnlyList<string> History { get; } | 시간순 스냅샷(오래된 것부터 새것까지). 호출할 때마다 새 리스트입니다. |
지원 타입
| 타입 | 설명 |
|---|---|
delegate CommandResult CommandHandler(IReadOnlyList<string> args) | 파싱된 문자열 인자를 받는 여러분의 핸들러. 커맨드 이름은 제외되며, 리스트는 절대 null이 아닙니다. 타입 있는 값의 파싱은 핸들러의 몫입니다 — 레지스트리는 타입 변환을 하지 않으며, 그것이 IL2CPP/AOT 안전성을 지킵니다. |
readonly struct CommandResult | bool Success와 string Message(절대 null 아님). CommandResult.Ok(msg)나 CommandResult.Fail(msg)로 만드세요. default는 실패로 칩니다. |
readonly struct CommandMetadata(string description, string usage) | Description과 Usage 도움말 문자열(둘 다 절대 null 아님). |
readonly struct CommandRegistryOptions | 생성 정책. 생성자나 .Default로: HistoryCapacity(64, 8–1024로 클램프), CaseSensitiveNames(true), DuplicateRegistrationPolicy, MaxArgumentCount(0 = 무제한), WarnOnUnknownCommand, WarnOnDuplicateRegistration, WarnOnMalformedLine, InitialCommandCapacity. |
enum CommandDuplicatePolicy | ReplaceWithWarning(기본) / ReplaceSilently / Throw. |
콘솔 직접 만들기
레지스트리는 콘솔에 필요한 모든 것을 화면만 빼고 제공합니다: TryExecute가 출력 텍스트를 만들고, Previous/Next가 위/아래 히스토리 recall을 구동하며, Lookup이 자동완성을 뒷받침합니다. 이 셋을 원하는 UI에 바인딩하면 동작하는 인게임 콘솔이 완성됩니다.
밑바닥부터 시작할 필요는 없습니다. Command Console 샘플 — 패키지와 함께 제공되는 여섯 샘플 중 하나이자 실행 가능한 세 샘플 중 하나 — 은 이 서비스 위에 만들어진 완전히 동작하는 콘솔입니다: 출력 뷰가 있는 어두운 UGUI 패널, 흐릿한 자동완성 힌트 줄, 입력 필드, 데모 커맨드 다섯 개(help, echo, add, timescale, clear), 그리고 위/아래 히스토리. **Tools → Common Game System → Welcome → Import "Command Console Sample"**로 임포트한 뒤, 임포트된 CommandConsole.unity를 열고 Play를 누르세요. 권장 출발점입니다: 프로젝트에 복사해 커맨드 목록을 확장하세요.
와이어링 패턴의 핵심만 추리면:
using System.Collections.Generic;
using CommonGameSystem.Core;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
[DefaultExecutionOrder(100)]
public class MyConsole : MonoBehaviour
{
[SerializeField] private InputField _inputField; // your input row
[SerializeField] private Text _outputText; // your output view
private ICommandRegistry _commands;
private bool _fieldFocusedLastFrame;
private void Awake() => _commands = ServiceLocator.Resolve<ICommandRegistry>();
private void Update()
{
Keyboard kb = Keyboard.current;
if (kb == null) return;
// History recall while the field is focused.
if (_inputField.isFocused)
{
if (kb.upArrowKey.wasPressedThisFrame) Recall(_commands.Previous());
else if (kb.downArrowKey.wasPressedThisFrame) Recall(_commands.Next() ?? string.Empty);
}
// Submit on Enter — detected here in Update (see the tip below).
bool enter = kb.enterKey.wasPressedThisFrame || kb.numpadEnterKey.wasPressedThisFrame;
if (enter && (_inputField.isFocused || _fieldFocusedLastFrame))
Submit(_inputField.text);
_fieldFocusedLastFrame = _inputField.isFocused;
}
private void Submit(string line)
{
if (string.IsNullOrWhiteSpace(line)) return;
CommandResult result = _commands.TryExecute(line); // tokenize + dispatch + history
_outputText.text += $"\n> {line}\n{result.Message}";
_commands.ResetHistoryCursor(); // next Up starts from the newest entry
_inputField.text = string.Empty;
_inputField.ActivateInputField(); // refocus for the next command
}
private void Recall(string line)
{
if (line == null) return; // null only when history is empty
_inputField.text = line;
_inputField.caretPosition = line.Length;
_inputField.ActivateInputField();
}
// Autocomplete: call from the field's onValueChanged and render the matches yourself.
public IReadOnlyList<string> Complete(string prefix) => _commands.Lookup(prefix);
}
실용 팁 — Enter는 onEndEdit이 아니라 Update에서 감지하세요. 입력 필드의 onEndEdit 이벤트에서 제출하고 싶어지지만, UI가 onEndEdit을 올릴 때쯤이면 키보드의 enterKey.wasPressedThisFrame은 이미 false로 돌아가 있습니다(Unity 6.3 LTS에서 측정) — onEndEdit 안에서 그 플래그로 제출을 게이트하는 콘솔은 모든 제출을 조용히 삼킵니다. 위 스니펫처럼 Update에서 키보드를 폴링하세요. 미묘한 점이 하나 더 있습니다: 같은 Enter 입력이 프레임의 더 이른 시점에 입력 필드를 비활성화할 수 있으므로, 필드가 지금 포커스되어 있거나 직전 프레임에 포커스되어 있었다면 그 입력을 받아들이세요 — _fieldFocusedLastFrame이 바로 그 용도입니다.
샘플에서 따라 할 만한 습관 두 가지 더:
- 공유 레지스트리에 등록하고, 자기 이름만 정리하세요.
OnDestroy에서 여러분이 추가한 커맨드만 정확히Unregister하세요. 레지스트리를 절대Dispose()하지 마세요 — Bootstrap이 소유한 공유 서비스입니다. - 타이핑이나 Enter에 아무 반응이 없다면, 프로젝트가 아직 Unity의 구 입력 백엔드에 있을 가능성이 큽니다. Tools → Common Game System → Welcome을 열어 Enable the new Input System을 클릭하고(에디터 재시작 한 번), 다시 Play 하세요.
예제
using System.Collections.Generic;
using CommonGameSystem.Core;
using UnityEngine;
[DefaultExecutionOrder(100)]
public class DebugConsoleInput : MonoBehaviour
{
private ICommandRegistry _commands;
private void Awake()
{
_commands = ServiceLocator.Resolve<ICommandRegistry>();
_commands.Register("god", _ => CommandResult.Ok("god mode toggled"),
new CommandMetadata("Toggle invulnerability", "god"));
}
// Called by your own UI when the player submits a console line.
public string Submit(string line)
{
CommandResult r = _commands.TryExecute(line); // recorded in history automatically
return r.Message; // your UI prints it
}
// Up/Down arrow recall — pure data; you bind the keys.
public string RecallPrevious() => _commands.Previous();
public string RecallNext() => _commands.Next();
// Tab autocomplete — you render the candidate list.
public IReadOnlyList<string> Complete(string prefix) => _commands.Lookup(prefix);
}
끄는 방법
ServiceLocator.Replace<ICommandRegistry>(new NullCommandRegistry());
이렇게 하면 모든 커맨드가 무음 처리됩니다. Register/Unregister는 아무것도 하지 않고, TryExecute는 실패한 CommandResult를 반환하며, 모든 조회는 빈 컬렉션이나 null을 반환합니다. 실제 레지스트리에 연결된 게임은 치트와 커맨드가 조용히 비활성화된 채 계속 돌아갑니다. 교체 구현이 생성될 때 경고가 한 번 로그되므로 교체가 조용히 지나가는 일은 없습니다. 프로그래밍 오류는 여전히 throw합니다 — null/공백 이름, null 핸들러, dispose 후 사용 — 프레임워크의 모든 null 구현이 따르는 규칙 그대로입니다.
흔한 함정
- 콘솔 UI는 여러분의 몫입니다. 레지스트리는 데이터 코어만 제공합니다 — print, render, 키 토글 API는 없습니다.
TryExecute(출력),Previous/Next(히스토리 recall),Lookup(자동완성)에 자체 UI를 바인딩하세요. 레지스트리가 만들어 내는 텍스트는CommandResult.Message뿐입니다. - 핸들러가 인자를 직접 파싱합니다. 인자는 커맨드 이름이 제외된
IReadOnlyList<string>으로 도착합니다. 레지스트리는 타입 변환도, 리플렉션 바인딩도 하지 않습니다 —int.Parse등은 직접 호출하세요. 이것이 IL2CPP/AOT 안전성을 지키는 방식입니다. - 두
TryExecute오버로드는 히스토리를 다르게 다룹니다.string rawLine오버로드는 토크나이즈하고 히스토리도 기록합니다. 미리 파싱된(name, args)오버로드는 둘 다 건너뜁니다. 콘솔 입력에는 raw-line 형식을, 프로그래밍 방식 디스패치에는 파싱된 형식을 쓰세요. default(CommandRegistryOptions)는.Default가 아닙니다. 맨new CommandRegistryOptions()(또는default)는 전부 0인 값 묶음입니다: 히스토리 용량 0, 대소문자 구분 없는 이름. 레지스트리는 그 0들에서 합리적인 값을 복구하지만, 표준 정책을 원하면CommandRegistryOptions.Default를 명시적으로 전달하세요.- 메인 스레드 전용입니다. 대부분의 프레임워크 서비스처럼 레지스트리는 메인 스레드를 assert합니다. 워커 스레드에서의 호출은 프로그래밍 오류입니다.