Notice
Recent Posts
Recent Comments
NeuroWhAI의 잡블로그
[Web] Node.js와 Socket.io로 멀티플레이 게임 개발 테스트 본문
만들고 싶은 멀티플레이 게임이 있는데 동기화 문제가 골치일 거라는 것을 알고 있었기에 연습 삼아 개발했습니다.
좋은 글이 있어서 비교적 빨리 구현할 수 있었습니다.
다만 많이 허술합니다. 데모 링크는 서버가 안좋아서 ㅠㅠ
겨우 공 몇개 동기화하는 것도 이렇게 힘든데 참 어떡할지 고민이네요.
아래는 소스코드입니다.
자바스크립트는 정식으로 공부한 적이 없어서 코드 스타일은 거르고 보시면 되겠습니다 ㅋㅋ..
GitHub에도 있습니다!
서버:
var app = require('http').createServer(handler);
var io = require('socket.io')(app);
var fs = require('fs');
app.listen(process.env.PORT);
function handler(req, res) {
fs.readFile(__dirname + '/public/client.html', function(err, data) {
if (err) {
res.writeHead(500);
return res.end('Error loading client.html');
}
res.writeHead(200);
res.end(data);
});
}
function getRandomColor() {
let letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; ++i) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
class InputData {
constructor(num) {
this.num = num;
this.w = false;
this.s = false;
this.a = false;
this.d = false;
}
}
class Ball {
constructor(socket) {
this.socket = socket;
this.x = 0;
this.y = 0;
this.color = getRandomColor();
this.inputMap = {};
this.inputBuffer = [];
this.lastInputNum = 0;
}
get id() {
return this.socket.id;
}
checkKey(key) {
return this.inputMap[key];
}
pushInput(inputData) {
this.inputBuffer.push(inputData);
}
applyInputs() {
let left = this.inputBuffer.length;
while (left > 0) {
left -= 1;
let input = this.inputBuffer.shift();
if (input.num > this.lastInputNum) {
this.lastInputNum = input.num;
this.inputMap.w = input.w;
this.inputMap.s = input.s;
this.inputMap.a = input.a;
this.inputMap.d = input.d;
}
}
}
handleInput(timeRate) {
let vx = 0;
let vy = 0;
if (this.checkKey('w')) {
vy = -4;
}
if (this.checkKey('s')) {
vy = 4;
}
if (this.checkKey('a')) {
vx = -4;
}
if (this.checkKey('d')) {
vx = 4;
}
this.x += vx * timeRate;
this.y += vy * timeRate;
}
}
var balls = [];
var ballMap = {};
function joinGame(socket) {
let ball = new Ball(socket);
balls.push(ball);
ballMap[socket.id] = ball;
return ball;
}
function leaveGame(socket) {
for (let i = 0; i < balls.length; ++i) {
if (balls[i].id == socket.id) {
balls.splice(i, 1);
break;
}
}
delete ballMap[socket.id];
}
function onInput(socket, data) {
let ball = ballMap[socket.id];
let inputData = new InputData(data.num);
inputData.w = data.w || false;
inputData.s = data.s || false;
inputData.a = data.a || false;
inputData.d = data.d || false;
ball.pushInput(inputData);
}
io.on('connection', function(socket) {
console.log(`${socket.id} has joined!`);
socket.on('disconnect', function(reason) {
console.log(`${socket.id} has leaved! (${reason})`);
leaveGame(socket);
socket.broadcast.emit('leave_user', socket.id);
});
socket.on('input', function(data) {
onInput(socket, data);
});
let newBall = joinGame(socket);
socket.emit('user_id', socket.id);
// Send data of users already in game.
for (let i = 0; i < balls.length; ++i) {
let ball = balls[i];
socket.emit('join_user', {
id: ball.id,
x: ball.x,
y: ball.y,
color: ball.color,
});
}
// Send data of a new user.
socket.broadcast.emit('join_user', {
id: socket.id,
x: newBall.x,
y: newBall.y,
color: newBall.color,
});
});
var prevUpdateTime = new Date().getTime();
var stateNum = 0;
function updateGame() {
let currentUpdateTime = new Date().getTime();
let deltaTime = currentUpdateTime - prevUpdateTime;
prevUpdateTime = currentUpdateTime;
let timeRate = deltaTime / (1000 / 60);
for (let i = 0; i < balls.length; ++i) {
let ball = balls[i];
ball.applyInputs();
ball.handleInput(timeRate);
}
setTimeout(updateGame, 16);
}
function broadcastState() {
stateNum += 1;
let data = {};
data.state_num = stateNum;
for (let i = 0; i < balls.length; ++i) {
let ball = balls[i];
data[ball.id] = {
last_input_num: ball.lastInputNum,
x: ball.x,
y: ball.y,
};
}
io.sockets.emit('update_state', data);
setTimeout(broadcastState, 33);
}
updateGame();
broadcastState();
클라이언트(script 부분만 발췌):
function Ball(id) {
this.id = id;
this.color = "#888888";
this.x = 0;
this.y = 0;
}
var balls = [];
var ballMap = {};
var myId;
function joinUser(id, color, x, y) {
let ball = new Ball(id);
ball.color = color;
ball.x = x;
ball.y = y;
balls.push(ball);
ballMap[id] = ball;
return ball;
}
function leaveUser(id) {
for (let i = 0; i < balls.length; ++i) {
if (balls[i].id == id) {
balls.splice(i, 1);
break;
}
}
delete ballMap[id];
}
function updateState(id, x, y) {
let ball = ballMap[id];
if (!ball) {
return;
}
ball.x = x;
ball.y = y;
}
var socket = io();
var lastInputNum = 0;
var lastStateNum = 0;
var stateBuffer = [];
socket.on('user_id', function(data) {
myId = data;
});
socket.on('join_user', function(data) {
joinUser(data.id, data.color, data.x, data.y);
});
socket.on('leave_user', function(data) {
leaveUser(data);
});
socket.on('update_state', function(data) {
let stateNum = data.state_num;
if (stateNum > lastStateNum) {
lastStateNum = stateNum;
// Store server states if valid.
let time = new Date().getTime();
if (stateBuffer.length == 0 || stateBuffer[stateBuffer.length - 1][1] < time) {
stateBuffer.push([data, time]);
if (stateBuffer.length > 256) {
stateBuffer.shift();
}
}
else if (stateBuffer.length > 0) {
stateBuffer[stateBuffer.length - 1] = [data, time];
}
// Update my state if last input state
if (myId) {
let myBall = data[myId];
if (myBall && myBall.last_input_num >= lastInputNum) {
updateState(myId, myBall.x, myBall.y);
}
}
}
});
// Input Cache
// inputMap[KEY] : Press(true), Up(false), None(undefined)
var inputMap = {};
function sendInput() {
let inputs = "wsad";
let anyInput = false;
let inputData = {};
// Copy input map
for (let i = 0; i < inputs.length; ++i) {
let key = inputs.charAt(i);
if (key in inputMap) {
anyInput = true;
inputData[key] = inputMap[key];
}
}
// Send input map if exists
if (anyInput) {
lastInputNum += 1;
inputData.num = lastInputNum;
socket.emit('input', inputData);
}
}
function handleInput(timeRate) {
if (!myId) {
return;
}
let ball = ballMap[myId];
if (!ball) {
return;
}
sendInput();
// Delete none key and Predict client state.
let vx = 0;
let vy = 0;
if (inputMap['w']) {
vy = -4;
}
else {
delete inputMap['w'];
}
if (inputMap['s']) {
vy = 4;
}
else {
delete inputMap['s'];
}
if (inputMap['a']) {
vx = -4;
}
else {
delete inputMap['a'];
}
if (inputMap['d']) {
vx = 4;
}
else {
delete inputMap['d'];
}
ball.x += vx * timeRate;
ball.y += vy * timeRate;
}
function interpolate(now) {
if (stateBuffer.length < 2) {
return;
}
let renderTime = now - 33;
// Find state contains time for render.
let t0Index = -1;
for (let i = stateBuffer.length - 1; i >= 0; --i) {
let time = stateBuffer[i][1];
if (renderTime >= time) {
t0Index = i;
break;
}
}
if (t0Index < 0 || t0Index + 1 >= stateBuffer.length) {
return;
}
let s0 = stateBuffer[t0Index][0];
let s1 = stateBuffer[t0Index + 1][0];
let t0 = stateBuffer[t0Index][1];
let t1 = stateBuffer[t0Index + 1][1];
let deltaState = s1.state_num - s0.state_num;
if (deltaState <= 0 || t0 >= t1) {
return;
}
for (let i = 0; i < balls.length; ++i) {
let ball = balls[i];
if (ball.id == myId) {
continue;
}
if (ball.id in s0 && ball.id in s1) {
let b0 = s0[ball.id];
let b1 = s1[ball.id];
// Interpolation
ball.x = b0.x + (b1.x - b0.x) * (renderTime - t0) / (t1 - t0);
ball.y = b0.y + (b1.y - b0.y) * (renderTime - t0) / (t1 - t0);
}
}
}
var prevUpdateTime = new Date().getTime();
function updateGame() {
let currentUpdateTime = new Date().getTime();
let deltaTime = currentUpdateTime - prevUpdateTime;
prevUpdateTime = currentUpdateTime;
let timeRate = deltaTime / (1000 / 60);
handleInput(timeRate);
interpolate(currentUpdateTime);
}
function renderGame() {
ctx.clearRect(0, 0, board.width, board.height);
// Draw balls
for (let i = 0; i < balls.length; ++i) {
let ball = balls[i];
ctx.fillStyle = ball.color;
ctx.beginPath();
ctx.arc(ball.x, ball.y, 16, 0, Math.PI * 2, false);
ctx.closePath();
ctx.fill();
}
//ctx.font = "16px serif";
//ctx.fillStyle = "black";
//ctx.fillText("Last S : " + lastStateNum, 8, 24);
}
function update() {
updateGame();
renderGame();
window.requestAnimationFrame(update);
}
var board;
var ctx;
function initGame() {
board = document.getElementById('board');
ctx = board.getContext('2d');
document.addEventListener('keydown', function(event) {
if (!inputMap[event.key]) {
inputMap[event.key] = true;
}
});
document.addEventListener('keyup', function(event) {
inputMap[event.key] = false;
});
update();
}
'개발 및 공부' 카테고리의 다른 글
디스코드에 봇으로 키워드 알림 기능을 만들어보았다 (0) | 2019.04.14 |
---|---|
기상청 바람 관측 데이터를 지도에 표시하기 (0) | 2019.04.07 |
[Web] Firebase로 메모 웹앱 개발 실습 (0) | 2019.01.27 |
[Rust] Brainfuck(브레인퍽) 구현 (0) | 2019.01.15 |
수알못의 '딥 드림(Deep Dream)' 원리 파헤치기 (0) | 2019.01.07 |
Comments