(웹소켓 - 듀얼 게임 예제 공부 중)
서버가 원하는 소켓 클라이언트 그룹에 메세지를 보낼 수 있다는 건 명확히 알겠다. 그런데 클라이언트측에서 수정한 데이터들이 어떻게 서버를 경유해 자기자신이 속한 ‘방’에만 영향을 미칠 수 있는 건지가 아리송하다. 분명히 데이터 자체를 넘겨주지는 않는데…
(여기 보는 중)
socket.rooms, namespace.rooms 예제 모음 - https://www.tabnine.com/code/javascript/functions/socket.io/Socket/rooms
‘듀얼’ 게임모드가 작동하는 핵심을 알았다.
두 번째 참가자가 방에 입장하면 초당 30번씩 호출되는 emitGameState
루프가 시작되게 된다.
// server.js
function handleJoinGame(roomName) {
...
client.join(roomName);
...
startGameInterval(roomName);
}
function startGameInterval(roomName) {
const intervalId = setInterval(() => {
...
emitGameState(roomName, state[roomName]);
}, 1000 / 30);
}
이 emitGameState
이 해당 ‘방’에 있는 모든 클라이언트(소켓)에게만 paintGame
을 호출하게 된다. 이 paintGame
이 점수판도 업데이트하고 게임판도 그려주는 역할이다.
// server.js
function emitGameState(room, gameState) {
io.of("/").to(room).emit("gameState", JSON.stringify(gameState));
}
// index.ejs
socket.on("gameState", handleGameState)
function handleGmaeState(gameState) {
...
requestAnimationFrame(() => paintGame(gameState));
}
function paintGame(state) {
// 게임판 그리기
// 점수판 업데이트
}
즉 복잡한 과정을 생략하면,
서버 → 클라이언트 : 초당 30번씩 해당 ‘방’에 있는 두 명의 클라이언트에게만 계속해서 게임판을 그려줌(= 게임 진행상태가 자연스럽게 반영되게 됨).
클라이언트 → 서버 : 클라이언트는 단신의 소켓이므로, 어떤 클라이언트가 어떤 ‘신호(=emit)’을 보내든지 그 신호는 항상 그 클라이언트가 속한 ‘방’에만 영향을 미치게 됨.
- 아니 잠깐만.. 정말 어떻게 클라에서 서버로 정보가 오게 되는 거지..?
새 클라이언트가 접속하자마자 신호가 새지 않도록 ‘방’에다 잘 넣어두기만 하면, 이후로는 일사천리인 샘.
- 일시 정지를 구현했다. 1초에 30번씩 렌더링 하는 과부하를 잠깐 멈추게 하고 싶어서.
- ‘일시 정지’ 화면을 덧씌우는 코드를 추가함.
function paintGame(state) {
...(중략)
// (추가) 게임이 일시정지 상태일 때 '일시 정지' 문구를 띄움.
if (state.isPause) {
paintPauseGame(gridsize, size);
}
}
// gridsize = 캔버스의 전체 격자 수(= 20칸)
// size = 캔버스 격자 하나의 크기(= 600px / 20칸 = 30px)
function paintPauseGame(gridsize, size) {
// 화면 전체에 투명 회색 덧씌우기
// fillstyle 속성은 CSS rgba 컬러값을 받을 수 있으므로 이걸로 '투명도'를 지정할 수 있다:
ctx.fillStyle = 'rgba(191, 191, 191, 0.3)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// '일시 정지' 문구와 박스
ctx.fillStyle = 'rgba(108, 122, 137, 0.8)';
ctx.fillRect((gridsize * 0.25) * size, (gridsize * 0.40) * size, (gridsize * 0.5) * size, (gridsize * 0.2) * size,);
ctx.font = '43px NeoDungGeunMo';
ctx.textAlign = 'center' // = 좌우 가운데 정렬 , start(default), end, left, right
ctx.textBaseline = 'middle' // = 상하 가운데 정렬 , alphabetic(default), top, hanging, ideographic, bottom
ctx.fillStyle = '#ffffff';
ctx.fillText('일시 정지', (gridsize * 0.50) * size, (gridsize * 0.5) * size);
}
- 유저 캐릭터가 게임판 바깥으로 벗어나면 반대쪽에서 나타나도록 개선
- Socket.IO의 namespace와 room의 개념을 확실히 알게 됨. namespace란, express의 라우터처럼 접속 url부터 갈라지는 형태라는 것이 가장 기억하기 좋은 포인트.
⇒ 이 듀얼 게임의 경우 굳이 namespace로 크게 갈라주지 않아도 되는 작은 게임이므로 그냥 디폴트 namespace인 ‘/’과 거기서 나뉘는 room들로 구현한 것 같다. namespace까지 가르면 코드가 많이 복잡해짐.
- 게임방을 나갔다 들어왔다 가능하도록 구현중 예외사항 우수수 마주침, 후퇴
socket.on(”disconnecting”, …)
이라고, ‘disconnect’ 되기 직전에 발생하는 이벤트를 활용했다. 클라이언트가 아직 접속을 끊기 전이므로socket.rooms
같은 속성으로 클라이언트의 소켓 ID와 방 ID를 조회할 수 있다는 장점이 있다.
- 참고로
socket.on(disconnect” (reason) ⇒ { … })
은 연결이 끊어지고 난 후 ‘왜 끊어졌는지’ 정도만 조회가 가능한 듯… 아닌가 이 안에서도socket.id
같은 게 호출 가능한가..?
socket.rooms
Set{ }
에 인덱스로 접근하고 싶을 때 배열 구조분해 할당 이용하기 (원래는 인덱스로 접근하는 방법이 없다. for … of든지 forEach든지 하여튼 반복문을 사용하라고 하든가 Object.keys()를 사용하라고 함.)
- 방 id로 해당 방에 참여중인 클라이언트(소켓)들 조회하기
// server.js client.on('disconnecting', handleDisconnecting); function handleDisconnecting() { // roomName 받아오는 게 필요한데... console.log(client.rooms); // => Set(2) { '72UInmIqz3kqeySbAAAN', 'O3GU7' } // => 첫 값은 해당 소켓의 ID이고, 이후로는 이 클라이언트가 접속한 방들의 ID가 된다. const roomName = [...client.rooms][1] const socketId = [...client.rooms][0] if (!roomName) { // 아직 아무 방도 들어가지 않은 상태에서 새로고침한 경우, roomName은 null이다. return; } const room = io.of('/').adapter.rooms.get(roomName); console.log(room) // => Set(2) { '72UInmIqz3kqeySbAAAN', 'DRCzbchbPBcUW5_7AAAP' } // 1. 두 사용자중 한 명이 나가는 경우, // clientRooms['해당 소켓ID']삭제하기 if (room.size == 2) { // 아직 접속을 끊기 전이므로 이 방은 2명인 상태. delete clientRooms[socketId]; } // 2. 마지막 남은 사용자가 나가는 경우, // clientRoom['해당 소켓ID'] 삭제하기 // state['현재 룸 코드'] 깔끔히 폭파하기 if (room.size == 1) { delete clientRooms[socketId]; delete state[roomName]; } }
- keydown 이벤트를 받을 때 기존의
e.keyCode
는 사장되는 추세라고 한다. 새롭게 떠오르는e.key
속성을 활용하라는 조언을 따라 수정하여봄// index.ejs // 1. keydown 소켓 이벤트 격발 => e.key속성을 지원하는 브라우저라면 // e.key 그대로를 넘기고, 아닌 브라우저면 대신 e.keyCode를 넘기도록 수정함. function keydown(e) { console.log("keydown e: ", e); // up 방향키: KeyboardEvent {..., key: 'ArrowUp', code: 'ArrowUp', keyCode: 38, location: 0, ctrlKey: false, …} const key = e.key || e.keyCode; socket.emit('keydown', key); } // server.js // 2. keydown 소켓 이벤트 수신. 넘겨받은 e.key값을 // game.js 파일에서 임포트한 UpdateMovement 함수에 인수로 주어 호출함. const { UpdateMovement } = require('./game.js') io.on('connection', (client) => { client.on('keydown', handleKeydown); ... function handleKeydown(key) { ... // '일시정지' 상태가 아니라면 방향키로 인한 위치를 업데이트 if (!state[roomName].isPause) { UpdateMovement(state[roomName], client.number, key); } } } // game.js // 3. index.ejs -> server.js 를 거쳐 넘겨받은 e.key값에 따라 상태 업데이트. function UpdateMovement(state, clientNumber, key) { ... if (key == "ArrowUp" || key == "38") { // 위 화살표 if (state.players[clientNumber - 1].pos.y > 0) { state.players[clientNumber - 1].pos.y -= 1; } else { state.players[clientNumber - 1].pos.y = GRID_SIZE - 1; } } ... }
Uploaded by N2T