본문 바로가기

Unity/작업방식

Unity. Windows 플랫폼에서 키보드 입력 막기

유니티는 Input 클래스와 비교적 최근 새로 추가된 InputSystem 클래스를 통해서 사용자의 입력을 받고 처리할 수 있습니다.

 

Legacy 방식인 Input 클래스로는 유니티 게임 루프의 Update에서 Input.GetKeyDown, GetKeyUp 등의 이번 루프의 프레임에서 해당 키가 눌러지기 시작했거나 끝났는지 확인 하는 방식을 취합니다. Input System에서는 이벤트 방식으로, 어떤 키 입력에 대해 액션 이벤트를 트리거할지 정해두고, 이벤트에 델리게이트를 등록해두고 들어오는 신호를 토대로 입력을 핸들링합니다.

 

Input System의 경우에는 글로벌한 스위치가 있어, 플레이어의 입력을 의도적으로 모두 차단 할 수 있는데요. Input 클래스는 그런 기능을 따로 제공해주고 있지 않습니다. 따라서, 구현하는 입장에서 이와 같은 기능 구현 및 편의상 입력을 한 곳에서 관리할 수 있도록 매니저 클래스를 만들어 작업하게 됩니다.

 

본문과 무관, 참고 이미지

입력을 선택적으로 막을 수 있게 구현하기 위해서, 유니티에서 글로벌하게 제공하는 스위치가 없기 떄문에 보통은 세부 구현 전 기획적으로 해결을 보아야 하는 부분인데요. 게임 개발 레이어와 관계없이 앞선 타이밍에 관리할 수 있는 방법을 포함하여 몇 가지 방안을 소개해 보려고 합니다.

 

방법 1. 래퍼 클래스 구현 및 사용

가장 흔하게 사용되는 방법은, UnityEngine.Input 클래스의 래퍼 클래스를 사용하는 것입니다.

 

https://github.com/Unity-Technologies/UnityCsReference/blob/4d031e55aeeb51d36bd94c7f20182978d77807e4/Modules/InputLegacy/Input.bindings.cs

 

GitHub - Unity-Technologies/UnityCsReference: Unity C# reference source code.

Unity C# reference source code. Contribute to Unity-Technologies/UnityCsReference development by creating an account on GitHub.

github.com

공개용으로 노출되어있는 API 메서드들을 동일한 이름으로 구현하고, 사용자의 condition에 따라 Input 클래스의 메서드를 호출해주도록 구성하는 방식이 되겠죠.

class ManagedInput {
	public static bool blockAllInputs = true;
	public static bool GetKeyDown(KeyCode keyCode) {
    	if (blockAllInputs) return false;
        
        return UnityEngine.Input.GetKeyDown(keyCode);
    }
}

사용하는 모든 Input 클래스는 ManagedInput으로 교체하고, bool 값을 조절하면서 입력을 중간에서 컨슘해버릴 수 있게 됩니다.

 

방법 2. WindowsHook 사용

유니티 애플리케이션 역시 Win32api 위 레이어에서 작동하는 프로그램이고, 관련된 라이브러리를 자유롭게 활용할 수 있습니다. Window Frame에 들어오는 이벤트를 처리하는 훅을 등록해서, 입력되는 메시지들을 판독하고 선택적으로 패스하거나 소모해 버릴 수 있습니다.

 

주요 api로는, user32.dll에 구현되어있는 SetWindowsHookEx, UnhookWindowsHook 등인데요, 윈도우 전체에 들어오는 메시지를 후킹해서 들여다 본 다음 유니티 C# 코드에서 처리해 줄 것이기 때문에 그렇게 복잡한 방식은 아닙니다.

 

먼저, SetWindowsHookEx로 HookProc을 등록해둡니다.

// https://github.com/seonghwan-dev/unity-input-interceptor/blob/main/Packages/com.seonghwan.windows.interceptor/Runtime/Interceptor.cs#L28
public static bool Hook()
{
    if (bIsHooked) return false;

    int currentThreadId = (int)Kernel32.GetCurrentThreadId();
    if (currentThreadId == 0) return false;
            
    if (pWindowsHook == IntPtr.Zero)
    {

        if (handler == null)
            handler = new DefaultKeyCodeHandler();
                
        pWindowsHook = User32.SetWindowsHookEx(
            EHookId.WH_KEYBOARD,
            HookProc,
            IntPtr.Zero,
            currentThreadId
        );
    }

    return pWindowsHook != IntPtr.Zero;
}

 

메시지가 발생하면 HookProc이 실행되는데요, 들어오는 lParam과 wParam으로부터 키가 Pressed인지 Released인지 구분하고, 어떤 키인지 Virtual Key 밸류를 받아서 확인합니다.

[AOT.MonoPInvokeCallback(typeof(User32.HOOKPROC))]
private static int HookProc(int nCode, IntPtr wParam, IntPtr lParam)
{
    if (nCode < 0)
    {
        // Skip this event
        return User32.CallNextHookEx(
            pWindowsHook, nCode, wParam, lParam);
    }

    bool consume = HandleKey(wParam, lParam);
    return consume ? 1 : User32.CallNextHookEx(pWindowsHook, 0, wParam, lParam);
}

 

 

유니티에서 등록해 둔 규칙에 따라, 해당 이벤트를 소모시킬지 또는 값 참조만 한 뒤 유니티에게 넘겨줄지 결정합니다.

private static bool HandleKey(IntPtr wParam, IntPtr lParam)
{
    int nativeKeyDown = (int)lParam;
    ushort nativeKeyCode = (ushort)wParam;
            
    bool isKeyDown = IsKeyDown(nativeKeyDown);
    int keyCode = GetKey(nativeKeyCode);

    if (isKeyDown)
    {
        if (OnKeyDown == null)
        {
            return false;
        }

        if (bManagePressedKey)
        {
            bool nowAdded = keys.Add(nativeKeyCode);
            if (nowAdded)
            {
                bool consume = OnKeyDown(keyCode);
                cachedConsumePolicy[nativeKeyCode] = consume;

                return consume;
            }
            else
            {
                return cachedConsumePolicy[nativeKeyCode];
            }
        }
        else
        {
            return OnKeyDown(keyCode);
        }
    }
    else
    {
        if (OnKeyUp == null)
        {
            return false;
        }

        if (bManagePressedKey)
        {
            keys.Remove(nativeKeyCode);
            cachedConsumePolicy.Remove(nativeKeyCode);
        }
                
        return OnKeyUp(keyCode);
    }
}

 

코드 전문은 아래 레포에서 확인하실 수 있습니다.

https://github.com/seonghwan-dev/unity-input-interceptor

 

GitHub - seonghwan-dev/unity-input-interceptor: this plugin allows your script to determine when to intercept a player's keyboar

this plugin allows your script to determine when to intercept a player's keyboard input on Windows. - GitHub - seonghwan-dev/unity-input-interceptor: this plugin allows your script to determine...

github.com

 

이 방법을 사용하면, 유니티 앱이 특정한 키의 이벤트를 무시하도록 앞선 레벨에서 제어할 수 있습니다.

추가적으로, 특정한 키의 입력을 다른 키로 바꾸어 입력시킬 수도 있습니다.

 

하지만 공개된 방식으로 모듈에 메시지 후킹을 설치하는 것이므로, 애플리케이션이 보안 솔루션을 적용하고 있다면 유관부서와 추가적인 도입 가능 여부 확인을 진행하셔야 됩니다.

 

방법 3. 유니티 코어 함수 후킹

이 방법은 1번과 2번 방법 사이에 존재하는 방법인데요, 실질적으로는 게임에 대한 공격이 이루어질 때 사용되는 방법입니다. 유니티는 C++로 제작된 엔진 코어에 닷넷 IL 코드에서 메서드를 매핑해 호출해주는 식으로 동작하게 되는데요, C++ 엔진코어의 EndPoint이자 C# 레이어에서 사용하게 되는 부분의 함수를 후킹해, 유니티 게임 어셈블리 안쪽의 코드가 먼저 호출되도록 장치하는 방법입니다.

 

스크립팅 백엔드가 Mono인 경우, Managed.dll 을 직접 사용하고, Managed DLL을 수정하면 쉽게 구현이 가능한데요. 이 부분은 IL 코드를 디컴파일하고 리컴파일 해주는 도구를 통해서도 충분히 외부에 public Func<KeyCode, bool> 을 노출해, 키를 처리할지 여부에 대한 결정은 게임쪽 코드로 위임하는 인터페이스를 노출하는게 어렵지 않습니다.

 

IL2CPP 백엔드 기준으로 보아서는, GameAssembly.dll이 아닌, UnityPlayer.dll 쪽의 export 된 함수들의 시그니처를 토대로 함수를 찾고, Detours, MinHook 또는 그에 준하는 메서드 후킹 라이브러리를 통해 2번과 유사한 방식의 훅을 설치하고 사용할 수 있습니다.

 

마찬가지로 허용되지 않은 방법으로 특정한 메서드에서 코드를 호출하는 것이므로, 애플리케이션이 보안 솔루션을 적용하고 있다면 유관부서와 추가적인 도입 가능 여부 확인을 진행하셔야 됩니다.