내일배움캠프 73일차

박나연·2025년 7월 18일

내배캠

목록 보기
62/69

최종프로젝트 4주차 - 키 리바인딩

오늘의 키워드 : 유저 키 커스텀 기능 만들기

<오늘 작업> 7/18

  • enum InputIndex 새로 작성
  • KeyCustomSlot.cs 작성
  • SaveManager.cs 수정 : InputAction 정보 저장
  • PlayerSkillManager.cs 수정 : 스킬 키 바인딩 부분만 수정
  • PlayerStatHandler.cs 수정 : 포션 키 바인딩 부분만 수정
  • 새 폰트 찾기
  • 새 UI 에셋 구매한걸로 UI 전면 교체

키 커스텀 기능 사전 작업

//public enum InputIndex
//{
//    Q,
//    E,
//    Space,
//    LeftClick,
//    RightClick,
//}
public enum InputIndex
{
    Skill1,
    Skill2,
    Skill3,
    Skill4,
    Interact,
    Escape,
    Potion1,
    Potion2
}

먼저 기존 enum을 바꿔주었다. 이제 직관적으로 어떤 것의 키를 바꿀 것인지 볼 수 있게 되었다.

SaveManager

    //키 설정
    public PlayerInputActions inputActions { get; private set; }
    public Dictionary<InputIndex, string> keyBindings = new();
    
    protected override void Awake()
    {
        base.Awake();
        DontDestroyOnLoad(gameObject);

        path = Path.Combine(Application.persistentDataPath, "saves.json");
        //BuildDataMap();
        LoadAll();

        //InputActions 생성 및 활성화
        inputActions = new PlayerInputActions();
        inputActions.Enable();

        //기본 바인딩 읽어서 딕셔너리에 채우기
        foreach (InputIndex idx in Enum.GetValues(typeof(InputIndex)))
        {
            var act = GetActionFor(idx);
            if (act != null && act.bindings.Count > 0)
                keyBindings[idx] = act.bindings[0].effectivePath;
        }
    }

    public void ApplyKeyBindings()
    {
        foreach (var kv in keyBindings)
        {
            var act = GetActionFor(kv.Key);
            if (act != null)
                act.ApplyBindingOverride(0, kv.Value);
        }
    }
    // InputIndex 에 대응하는 InputAction 추출
    public InputAction GetActionFor(InputIndex idx)
    {
        var p = inputActions.Player;
        switch (idx)
        {
            case InputIndex.Skill1: return p.Use_Skill_Q;
            case InputIndex.Skill2: return p.Use_Skill_E;
            case InputIndex.Skill3: return p.Use_Skill_Space;
            case InputIndex.Skill4: return p.Use_Skill_LeftClick;
            case InputIndex.Potion1: return p.Use_Potion_1;
            case InputIndex.Potion2: return p.Use_Potion_2;
            case InputIndex.Interact: return p.NPCInteration;
            case InputIndex.Escape: return p.Paused;
            default: return null;
        }
    }

먼저

    public PlayerInputActions inputActions { get; private set; }
    public Dictionary<InputIndex, string> keyBindings = new();

이 코드를 추가해

  • InputAction을 코드로 다루기 위한 인스턴스를 보관
  • InputIndex에 대응하는 바인딩 경로를 런타임에 저장, 관리하기 위해 keyBindings를 선언

GetActionFor에서 inputindex에 대응하는 inputAction을 추출하였다. 실제 사용하는 action의 이름을 적어야한다. 앞으로 상호작용 키가 추가되면 여기에 또 넣어주면 된다. 키를 바꿀 필요가 없는 I(인벤토리)는 넣지 않았다.

ApplykeyBindingskeyBindings 딕셔너리에 저장된 모든 경로를 액션의 첫 번째 바인딩 인덱스(0번)에 오버라이드로 적용한다. 이 메서드를 호출하면 런타임에서 누르는 키가 즉시 변경된 바인딩 대로 입력을 받아 처리하도록 해준다.

그리고 Awake에서 Enable을 호출해 키 입력을 받을 준비를 마치고 foreach문에서 에디터에 설정된 기본 바인딩(effectivePath)를 읽어 keyBindings에 채워 넣는다. 이렇게 해야 사용자가 아직 리바인딩을 하지 않은 상태에서도 어떤 키가 할당되어 있는지 코드가 알고 있게 된다.

KeyCustomSlot.cs

슬롯 프리팹에 붙여준 스크립트이다. 인스펙터 창에서 SkillIndex와 그에 맞게 Text를 수정해주어야 한다. 동작과정은 다음과 같다.

  1. KeyBtn을 눌러 원하는 키 입력 (이미 사용 중인 키나 WASD, anyKey 등은 후보에서 제외)
  2. 원하는 키가 입력되면 (확인 대기) 상태로 표시
  3. 확인 버튼을 눌러 최종 적용 → 런타임에 즉시 바인딩 반영
    private void StartRebind()
    {
        // 리바인드 시작 전 disable
        action.Disable();
        bindingText.text = "입력 대기";
        rebindButton.interactable = false;

        action.PerformInteractiveRebinding(0)
        // 마우스 포인터 입력 제외
        .WithControlsExcluding("<Mouse>/position")
        .WithControlsExcluding("<Mouse>/delta")
        // WASD 제외
        .WithControlsExcluding("<Keyboard>/w")
        .WithControlsExcluding("<Keyboard>/a")
        .WithControlsExcluding("<Keyboard>/s")
        .WithControlsExcluding("<Keyboard>/d")
        // anyKey 제외
        .WithControlsExcluding("<Keyboard>/anyKey")
        .OnComplete(op => HandleRebindComplete(op))
        .Start();
    }
  • 리바인딩 중 원치 않는 이벤트 발생 방지
  • 원치 않는 입력을 리바인딩 후보에서 제외
  • HandleRebindComplete로 제어 흐름 이동
    private void RefreshDisplay()
    {
        var current = Bindings[slotIndex];
        var toShow = pendingPath ?? current;
        var human = InputControlPath.ToHumanReadableString(
            toShow,
            InputControlPath.HumanReadableStringOptions.OmitDevice
        );
        bindingText.text = pendingPath != null
            ? $"{human}\n확인 대기"
            : human;
    }
  • 현재 바인딩(current) 또는 대기중 경로(pendingPath)를 사람이 읽기 좋은 문자열로 변환
  • 대기 중이면 확인 대기 문구를 표시
    private void HandleRebindComplete(InputActionRebindingExtensions.RebindingOperation op)
    {
        var newPath = action.bindings[0].effectivePath;

        // 중복 검사
        bool isDuplicate = Bindings
            .Any(kv => kv.Key != slotIndex && kv.Value == newPath);

        if (isDuplicate)
        {
            Debug.LogWarning("이미 사용 중인 키입니다. 다시 입력해주세요.");
            op.Dispose();
            action.RemoveBindingOverride(0);
            action.Enable();
            // 다시 리바인드 대기 상태로 진입
            StartRebind();
            return;
        }

        // 정상 후보
        pendingPath = newPath;

        op.Dispose();
        action.Enable();

        confirmButton.interactable = true;
        RefreshDisplay();
    }
  • 이미 다른 슬롯에서 사용중인 키 입력 시 경고 로그를 출력하고 오버라이드 제거 후 액션 재활성화
  • StartRebind 재호출로 재입력을 유도
  • 정상적으로 입력했다면 pendingPath에 저장 -> 확인 대기 상태로 넘어감
    private void ConfirmRebind()
    {
        if (pendingPath == null) return;

        // Dictionary 업데이트
        Bindings[slotIndex] = pendingPath;
        //런타임에 바로 적용
        SaveManager.Instance.ApplyKeyBindings();

        // 상태 초기화
        pendingPath = null;
        rebindButton.interactable = true;
        confirmButton.interactable = false;
        RefreshDisplay();
    }
  • keyBindings를 갱신해 slotIndex에 새 경로 저장
  • ApplyKeyBingings를 호출해 모든 액션에 오버라이드를 반영해 즉시 바뀐 키를 적용

아직 해야 할 기능은 많이 남았다. 기억나는 걸로는
1. 한번에 여러 키를 수정가능한 현상 막기
2. 확인 버튼을 누르지 않으면 계속 입력 대기 상태인 현상 막기
3. 키 설정이 저장되게 하기
4. 확인 버튼을 눌렀을 때 이미지가 달라지게 하기
정도가 되겠다.


마무리하며

작업을 하면서 생각했던 건 현재 스킬, 포션, 상호작용이렇게 크게 세가지가 키 입력이 되는 상태인데 다른 방식으로 코드가 작성되어 있어 통일할 필요가 있어보인다는 점이었다. 그런데 통일하느라 고칠 코드가 연계되어 너무 많아져 버린다면... 그냥 지금 동작 잘만 되는데 냅둬도 되지 않을까?

0개의 댓글