1. 서론
나는 c언어(cpp)로 소켓을 사용해 게임을 만들어본 적은 있었다. winapi로 체스, 크레이지 아케이드 모작을 만들었었다.
하지만 이번에는 웹소켓을 사용해 게임을 만들어보고 싶었다.
체스, 오목같이 실시간으로 바쁘게 통신하지 않는 게임은 만들기 쉬울 것 같았다. 물론 게임 로직을 만드는 것은 쉽지 않겠지만 통신이 쉬울 것 같다는 뜻이다. socket.io가 이벤트 기반으로 통신하기 때문에 이벤트가 발생하면 차례가 넘어가는 등으로 처리하면 될 것이다.
하지만 크레이지 아케이드 또는 스타크래프트 또는 fps게임처럼 실시간으로 바쁘게 통신하는 게임은 어떻게 게임 정보를 다수의 클라이언트가 동기화하는가? 이는 나의 오랜 고민이자 망상이었다. 하지만 이번에는 망상으로 끝나지 않고 chatgpt에 물어보고 구글에 검색하니 많은 기법이 나왔다. 정말 고마워요 chatgpt! 머리가 말끔해지는 기분이다.
2. 멀티플레이 게임의 동기화
2.1. 동기화 기법 선정 - lockstep synchronization
멀티플레이 게임의 동기화에 대해 잘 정리된 네이버 블로그 글이 있다. 해당 글에 따르면 동기화 방법은 시간 동기화와 이벤트 동기화로 나눌 수 있는데, 이중에서 socket.io로 통신하는 멀티플레이 게임에서 사용할 만한 초보적인 방법은 lockstep Synchronization이다. lockstep이 어떤 동기화 방법인지는 아래 참고에 주소를 붙여놓았으니 궁금하면 참고 바란다.
2.2. 동기화 구현 방법
의도하지 않았지만, 과거의 나는 cpp로 크레이지 아케이드 모작을 만들었을 때, lockstep을 사용한 것이었다. client1에서 키보드 정보를 서버에 보내고 client2에서 키보드 정보를 서버에 보내고 마지막으로 서버에서 두 클라이언트에게 키보드 정보를 보낸것이다. cpp로 구현할 때는 이 과정이 쉬웠다. 클라이언트에서 send함수 호출 후 recv함수를 호출하는데, 서버에서 정보가 전달될 때까지 idle이 되는것이다. 따로 생각할 것이 없이 내가 원하는 순서대로 프로그램 흐름이 지나는 것이다.
하지만 웹에서 통신하는 socket.io는 이벤트 기반이다. cpp에서 소켓에서 사용하는 send함수는 emit함수로, recv함수는 on함수로 대체할 수 있다. 그러나 다른점은 send, recv함수는 순차적으로 진행되는데 반해 emit, on함수는 이벤트 기반이라서 순서를 보장 할 수 없다는 것이다.
그렇다면 어떻게 순서를 보장할 것인가? ①두 클라이언트가 서버에 정보를 전송한다. ②서버는 각각의 정보를 cross해서 클라이언트에게 다른 클라이언트 정보를 전송한다. ③클라이언트는 서버가 전송한 데이터를 바탕으로 로직 계산 및 화면을 갱신한다.
위 세 과정을 반복할 것이다.
2.3. 동기화 게임 구현 차트(마우스 위치 전송)
처음부터 복잡한 게임을 만들 수 없으므로, 간단하게 서로의 마우스 위치를 전송하고 전달받아 화면에 띄우는 프로그램을 만들어보자. 서버의 역할은 클라이언트에서 마우스 정보를 받는다면, 다른 클라이언트에게 마우스 정보를 전송하는 것이다.
그림 1은 flow는 위의 상태이다. 하지만 socket.io는 이벤트 기반이기 때문에 아래줄에 코드를 쓴다고 순서를 보장받지 않는다. 그렇다면 어떻게 해야할까?
그림2는 js문법에 맞게 실행시킬 수 있는 차트이다. setInterval로 서버에 마우스 정보를 전송하는 함수를 주기적으로 호출하고, 서버에서 마우스 정보를 받자마자 다른 클라이언트에게 마우스 정보를 전달하여, 서버에게서 다른 클라이언트 우스 정보를 받아 화면을 갱신하면 그림1처럼 순서가 지켜진다.
하지만 여기서 발생할 수 있는 문제가 무엇인가? 네트워크나 브라우저 환경에 따라 지연될 수 있다는 것이다. 서버에 마우스 정보를 전송 후 다음 단계는 다른 클라 마우스 정보 받기인데, 또 서버에 마우스 정보를 전송한다면? 다른 클라이언트에서 화면 갱신이 2번 일어나는 것이다. 이번 글에서는 화면 갱신만 하여 문제가 되지 않는다.하지만 마우스 정보를 이용해 다른 로직을 처리한다고 하면, 동기화가 되지 않는것이다. flag를 추가하여 동기화 할 것이다.
flag를 추가해 true일 때만 서버에 마우스 정보를 전송한다. 이렇게 하면 서로 클라이언트가 동기화가 되지 않는 문제가 발생하지 않게 된다.
2.4. 코드 및 결과 화면
//index.js
const express = require('express');
const app = express();
const http = require('http');
const server = http.createServer(app);
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
// 메인 화면
app.get('/', (req, res) => {
res.render('index');
});
server.listen(8880, () => {
console.log('listening on *:8880');
});
require('./socket')(server);
//socket.js
const { Server } = require("socket.io");
let population = 0;
module.exports = (server) => {
const io = new Server(server);
io.on('connection', (socket) => {
socket.on('ready', () => {
population += 1;
console.log(population);
//접속인원을 확인해서 모두 접속하면 game start emit
if (population === 2) {
io.emit("game start");
}
});
socket.on('clientMouseData', (data) => {
//다른 클라이언트에게 마우스 데이터를 전송
console.log(data);
socket.broadcast.emit('serverMouseData', data);
});
});
};
<!--index.ejs-->
<!DOCTYPE html>
<html>
<head>
<link rel="shortcut icon" href="https://blackbearwow.github.io/favicon/favicon.ico">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
<script>
const socket = io();
let can; //canvas 객체
let ctx; //context객체를 얻어야 drawimage와 filltext를 할 수 있음
let mainInterval; //interval변수
let receiveFlag = true; //서버로부터 정보를 받았는지 확인하는 플래그. true라면 서버에 정보를 전송, 아니라면 전송하지 않는다.
let mouseX, mouseY; //현재 마우스 x, y좌표
let mX2, mY2; //서버의 마우스 x, y좌표
let mouseImage; //마우스 이미지
mouseImage = new Image();
mouseImage.src = ("https://blackbearwow.github.io/image/cursor.png");
socket.on('game start', () => {
mainInterval = setInterval(sendData, 20);
})
socket.on('disconnect', ()=> {
clearInterval(mainInterval);
})
socket.on('serverMouseData', (data)=>{
mX2 = data[0]; mY2 = data[1];
receiveFlag = true;
update();
})
$(document).ready(() => {
document.getElementById("can").addEventListener('mousemove', function (e) { mouseX = e.offsetX; mouseY = e.offsetY; })
initialize();
})
function initialize() {
can = document.getElementById("can");
ctx = can.getContext("2d");
//모든 준비가 끝나면 ready라고 서버에 전송
socket.emit('ready');
}
function sendData() {
if(receiveFlag === true) {
socket.emit('clientMouseData', [mouseX, mouseY]);
receiveFlag = false;
}
}
function update() {
ctx.fillStyle = '#aaaaaa';
ctx.fillRect(0, 0, 500, 700);
ctx.drawImage(mouseImage, mouseX, mouseY, 30, 30);
ctx.drawImage(mouseImage, mX2, mY2, 30, 30);
}
</script>
</head>
<body style="background-color:black">
<center>
<canvas id="can" width="500" height="700" style=" cursor:none;" />
</center>
</body>
</html>
'Node.js' 카테고리의 다른 글
크레이지 아케이드 멀티게임(모작) (0) | 2023.08.26 |
---|---|
웹소켓(WebSocket) 8장 - socket.io와 lockstep (0) | 2023.08.24 |
nodejs의 file system (node:fs) (0) | 2023.07.29 |
nodejs에서 session사용하기 - express-session 모듈 (0) | 2023.07.27 |
웹소켓(WebSocket) 6장 - socket.io를 이용한 여러 채팅방 구현 (0) | 2023.07.26 |