스마트폰으로 PC 게임 컨트롤러 구현하기

스마트폰으로 PC 게임 컨트롤러 구현하기

작성자
태인태인
카테고리
⚗️ 프로젝트
작성일
2023년 11월 19일
태그
Web
Python
PC로 게임을 할 때, 좀 더 실감나고 편하게 플레이하기 위해서 게임 패드(컨트롤러)를 구매해 연결할 수 있다.
개인적으로 게임을 자주하는 편은 아니기 때문에 스마트폰 화면에 게임 컨트롤러를 띄워놓고, 버튼을 누르면 PC에서 정해진 키보드 입력이 실행되도록 구현한다면 비슷한(?) 플레이 경험을 구현할 수 있지 않을까라는 생각이 들어 진행하게 되었다.

구상

동작 시나리오

폰 or 태블릿 화면에 여러 버튼/조이스틱 있음 → 각 버튼/조이스틱을 누르면 그에 맞는 키 값을 PC로 전송 → PC에서 키 값을 수신하면 키보드 입력 동작 수행

어떻게 구현하지

컨트롤러 화면은 웹으로 구현(HTML/CSS/JS)
통신은 WebSocket을 이용하자.
동일한 Wifi 연결 상태에서 PC에서 서버를 열어 해당 PC의 ip 주소로 컨트롤러 웹 페이지와 웹소켓 서버를 실행.
이 서버는 python을 이용하여 제작하자.
→ 그럼 키보드 입력은 어떻게?
pyautogui라는 모듈 존재(DirectX 기반 게임에서는 미동작 이슈)
→ 대체 모듈로 pydirectinput 이용

이제 만들어보자!

컨트롤러 화면 구현

고도의 CSS 기술을 동원해 아래와 같이 컨트롤러 비스무리한 화면을 구성해준다.
닌텐도 스위치 조이콘과 비슷하쥬
닌텐도 스위치 조이콘과 비슷하쥬

웹소켓 연결

PC에서 실행할 웹소켓 서버에 연결하는 코드를 먼저 작성해주자.
function connectToPC() { const defaultServerIP = window.location.hostname; const serverIP = prompt(`PC의 IP 주소 입력`, defaultServerIP) || defaultServerIP; const serverPort = 8888; // 사용할 포트 번호 // WebSocket을 사용하여 PC와 연결 socket = new WebSocket(`ws://${serverIP}:${serverPort}`); // 연결이 열리면 메시지를 보냄 socket.addEventListener("open", (event) => { console.log(`Received from PC: ${event.data}`); Toastify({ text: "연결 성공", duration: 1000, position: "center", style: { borderRadius: '20px', fontSize: "20px", } }).showToast(); document.documentElement.webkitRequestFullscreen(); }); // 연결이 닫히면 콘솔에 출력 socket.addEventListener("close", (event) => { Toastify({ text: "연결 끊김", duration: 1000, position: "center", style: { borderRadius: '20px', fontSize: "20px", } }).showToast(); }); // 에러가 발생하면 콘솔에 출력 socket.addEventListener("error", (event) => { console.log("Error: " + event); }); }
 

일반 버튼 클릭 이벤트 지정

조이스틱을 제외한 다른 버튼들은 눌렀을 때 단순히 하나의 키 값을 전송하면 된다.
<div class="button arrow up" ontouchstart="send('up')"></div> <div class="button arrow right"ontouchstart="send('right')"></div> <div class="button arrow down" ontouchstart="send('down')"></div> <div class="button arrow left" ontouchstart="send('left')"></div>
이런 식으로 각 버튼을 클릭했을 때 send 함수가 실행되도록 HTML을 작성해준다.
그럼 아래의 send 함수가 실행되어 키 값이 websocket 통신을 통해 PC로 전송된다.
function send(message) { socket.send(message); }
 

조이스틱 구현

조이스틱을 구현하기 위해 nippleJS라는 라이브러리를 사용하였다.
var leftJoyStick = nipplejs.create({ zone: document.getElementById('leftJoyStick'), mode: 'static', position: { left: '50%', top: '50%' }, color: '#ff5e52' });
조이스틱을 생성해주고, 조이스틱을 움직였을 때 WASD 키값을 전송하는 코드를 작성해야 한다.
leftJoyStick.on('move', function (evt, data) { let x = data.vector.x; let y = data.vector.y; let message = ''; if (y < -0.5) { message += 's'; //뒤 } else if (y > 0.5) { message += 'w'; //앞 } if (x < -0.5) { message += 'a'; //왼 } else if (x > 0.5) { message += 'd'; //오 } if (message && message !== leftPrevMessage) { leftPrevMessage = ''; send_arrow('[arrow-stop]'); send_arrow('[arrow]' + message); startVibrate(5); leftPrevMessage = message; } }); leftJoyStick.on('end', function (evt, data) { if (leftPrevMessage) { send_arrow('[arrow-stop]'); startVibrate(5); leftPrevMessage = ''; } });
조이스틱에서 손을 떼거나 방향이 전환되었을 때 [arrow-stop] 값을 전달하고, WASD 방향키값은 앞에 [arrow]를 붙여 전송하는데, 이 이유는 뒤에 기술하겠다.

웹소켓 서버&키매핑 구현

PC로 전송된 키값을 수신하고, 이를 키보드 입력으로 옮기기 위해 python으로 코드를 작성해주어야 한다.
아래와 같이 작성하면 8888번 포트에 웹소켓 수신을 위한 서버를 오픈하고, 수신되는 값을 확인할 수 있다.
async def server(websocket, path): print("Connection from the Web") await websocket.send("connected") async for data in websocket: print(f"Received from Web: {data}") start_server = websockets.serve(server, "0.0.0.0", 8888) asyncio.get_event_loop().run_until_complete(start_server) asyncio.get_event_loop().run_forever()

일반 버튼 키 입력 구현

우선 조이스틱이 아닌 일반 버튼을 눌렀을 때 수신되는 키값에 따라 키보드 입력을 수행하는 코드를 추가해보자.
pydirectinput 모듈에서 제공하는 keydown, keyup 메서드를 사용하면 된다.
async for data in websocket: print(f"Received from Web: {data}") pydirectinput.keyDown(data) pydirectinput.keyUp(data)
 

조이스틱 키 입력 구현

조이스틱은 좀 더 복잡하다. WASD는 보통 이동에 사용되는 키인데, 한번 누른 키를 조이스틱의 방향이 바뀌지 않는 이상 계속 누르고 있어야 하기 때문이다. 그러다 조이스틱의 방향이 바뀌거나 조이스틱에서 손을 떼면 누르고 있던 키를 떼고 새로운 키를 눌러야 한다.
이를 위해 컨트롤러에서 조이스틱을 움직였을 때 앞에 [arrow]를, 뗐을 때 [arrow-stop]을 붙여 전송한 것이다.
 
그런데, 변수가 하나 있다
조이스틱에는 대각선 이동도 가능하다. 즉 W, A, S, D뿐만 아니라
두 키를 조합한 WA, WD와 같은 키 입력도 가능해야 하는 것이다.
이를 위해 [arrow]로 전송된 키값을 한글자씩 잘라 keys_pressed 배열에 저장해 눌러야 한다.
 
key_thread = None key_thread_stop_flag = False def hold_key(keys): global key_thread_stop_flag while not key_thread_stop_flag: keys_copy = keys.copy() for key in keys_copy: pydirectinput.keyDown(key) def release_keys(keys): for key in keys: pydirectinput.keyUp(key) async def server(websocket, path): global key_thread, key_thread_stop_flag print("Connection from the Web") await websocket.send("connected") keys_pressed = set() async for data in websocket: print(f"Received from Web: {data}") if '[arrow]' in data: keys = data.replace('[arrow]', '') keys_pressed.update(keys) if key_thread is None: key_thread_stop_flag = False key_thread = threading.Thread(target=hold_key, args=(keys_pressed,)) key_thread.start() elif '[arrow-stop]' in data: if key_thread is not None: key_thread_stop_flag = True key_thread.join() key_thread = None print(set(keys_pressed)) release_keys(set(keys_pressed)) keys_pressed.clear() else: pydirectinput.keyDown(data) pydirectinput.keyUp(data) if '[arrow]' not in data: release_keys(set(keys_pressed))
 
정리하자면 이렇다.
[arrow] 데이터가 수신되면, 눌러야 하는 키들을 keys_pressed에 저장한다.
이후 key_thread를 이용해 hold_key 함수를 계속 실행하여 입력된 키를 계속 누르도록 한다.
이후 새로운 조이스틱 방향이 입력되거나 손을 떼는 경우 수신되는 [arrow-stop] 에 대해서는 key_thread를 초기화하고, release_keys 함수를 이용해 keys_pressed(누르고 있는 키)들을 다시 모두 뗀다. 그리고 다시 keys_pressed를 초기화한다.

컨트롤러 조작 시 진동 구현

을 웹페이지에서?
가능하다.
Vibration API를 이용하면 되는데, 아래와 같이 ios를 제외한 대부분의 모바일 환경에서 지원하고 있다.
notion image
 
아래의 코드를 실행하면 된다.
navigator.vibrate(duration);
duration에는 ms 단위의 초를 입력할 수 있는데, 배열 형태의 입력도 가능하다.
값 배열은 장치가 진동하는 주기와 진동하지 않는 주기를 번갈아 가며 나타낸다.
window.navigator.vibrate(200); //200ms 진동 window.navigator.vibrate([200, 100, 200]); //200ms 진동 -> 100ms 대기 -> 200ms 진동
햅틱 피드백같은 느낌을 주기 위해서는 5나 10 정도의 ms 만큼 진동을 발생시키면 된다.
참고로 진동 기능은 스마트폰의 소리 설정이 진동 모드로 되어 있어야 동작한다.
 
이제 버튼을 클릭하거나 조이스틱을 조작할 때마다 적절한 길이로 진동을 발생시켜주면 좀 더 실감나는 조작이 가능하다.

 
완성되었으니, PC에 연결해 게임을 즐겨보자.
딜레이가 살짝 있긴 하지만 높은 수준의 실시간성을 요구하는 FPS 게임류가 아니라면 무리없이 플레이가 가능한 수준이다.
 
websocket-game-controller
IceCream0910Updated Nov 19, 2023
 

댓글

guest