깊은바다거북
개발 공부 기록
깊은바다거북
전체 방문자
오늘
어제
  • 분류 전체보기 (219)
    • JAVA (9)
    • JavaScript (15)
    • 스파르타코딩클럽 (11)
      • [내일배움단] 웹개발 종합반 개발일지 (5)
      • [내일배움캠프] 프로젝트와 트러블 슈팅 (6)
    • SQL | NoSQL (4)
    • CS 등등 (0)
    • TIL | WIL (173)
    • 기타 에러 해결 (3)
    • 내 살 길 궁리 (4)

인기 글

최근 글

최근 댓글

태그

  • TIT (Today I Troubleshot)
  • 자잘한 에러 해결
  • DFS(깊이우선탐색)
  • TypeScript
  • Binary Tree(이진 트리)
  • 혼자 공부하는 자바스크립트
  • Backtracking(백트래킹)
  • 점화식(Recurrence Relation)
  • tree
  • BST(이진 탐색 트리)
  • POST / GET 요청
  • 트러블 슈팅 Troubleshooting
  • 최대 힙(Max Heap)
  • Linked List
  • 재귀 함수
  • 자료 구조
  • Preorder Traversal(전위 순회)
  • BFS(너비우선탐색)
  • 코딩테스트 연습문제
  • Trie
  • 자바스크립트 기초 문법
  • Til
  • leetcode-cli
  • 프로그래머스
  • 팀 프로젝트
  • Inorder Traversal(중위 순회)
  • 시간 복잡도
  • Leetcode
  • 최소 힙(Min Heap)
  • 01. 미니 프로젝트
hELLO · Designed By 정상우.
깊은바다거북

개발 공부 기록

1/15 (Socket.IO를 활용한 간단 듀얼 게임, 일) TIL
TIL | WIL

1/15 (Socket.IO를 활용한 간단 듀얼 게임, 일) TIL

2023. 1. 15. 23:57

(웹소켓 - 듀얼 게임 예제 공부 중)

서버가 원하는 소켓 클라이언트 그룹에 메세지를 보낼 수 있다는 건 명확히 알겠다. 그런데 클라이언트측에서 수정한 데이터들이 어떻게 서버를 경유해 자기자신이 속한 ‘방’에만 영향을 미칠 수 있는 건지가 아리송하다. 분명히 데이터 자체를 넘겨주지는 않는데…

(여기 보는 중)

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

    'TIL | WIL' 카테고리의 다른 글
    • 1/17 화 (웹 서비스에서 고가용성이란) TIL
    • 1/16 월 (TypeScript, 만나서 반갑소) TIL, TIT
    • 1/13 (Socket.IO “방”의 개념 예제, 금) TIL
    • 1/12 (공부가 느려서 초조한 하루, 목) TIL
    깊은바다거북
    깊은바다거북

    티스토리툴바