Part 2 : 멀티 플레이어 게임 자습서 : 인증 및 P2 물리 서버

소개

저장소는 https://github.com/dci05049/Phaser-Multiplayer-Game-Tutorial/tree/master/Part2에서 확인할 수 있습니다.

이것은 클라이언트의 Phaser와 서버의 Node.js를 사용한 멀티 플레이어 게임 자습서 시리즈의 연속입니다. 첫 번째 부분

- 우리는 성공적으로 개발 환경을 설정하였습니다.
- 우리는 클라이언트와 서버를 Socket.io와 Express.js로 연결했습니다.
- 우리는 서버가 하나의 게임 상태를 갖도록 모든 클라이언트 동작을 동기화했습니다.

그러나 우리의 구현은 서버에 직접 클라이언트 위치를 전송하기 때문에 매우 순진하다는 것을 기억하십시오. 그리고 우리는 연결된 플레이어의 나머지 부분으로 그 위치를 다시 방송합니다.

이게 왜 위험한가요? 클라이언트 측은 쉽게 조작 할 수 있기 때문에 주로. 또한 누구나 자바 스크립트 파일을 변경할 수 있으므로 해커가 자바 스크립트 파일을 변경하여 게임에서 자신의 위치를 쉽게 조작 할 수 있으므로 다른 모든 플레이어의 게임 경험에 해를 끼칠 수 있습니다.

그러면 해결책은 무엇입니까? "서버 권위있는"게임을 만들 수 있습니다. 즉, 중요한 데이터는 모두 서버에 저장되고 계산됩니다. 우리는 위치 대신 서버에 입력을 보내 게임을 보다 안전하게 만들 수 있습니다. 그런 다음 플레이어의 새로운 위치를 계산하여 다른 플레이어에게 방송 할 수 있습니다. 그러나 한 가지 문제가 있습니다. 우리는 마우스 포인터를 따르는 데 물리학을 사용하고 있습니다. 화살표 키를 눌러 플레이어를 움직이는 것만 큼 간단하지 않습니다. 이것은 우리가 서버에서도 물리 시스템을 필요로 한다는 것을 의미합니다!.

서버의 물리학을 위해 우리는 p2 물리학을 사용할 것입니다. 클라이언트에서 p2 물리학을 사용한다는 사실은 클라이언트와 서버 계산이 유사하기 때문에 맨 위에있는 체리입니다.

p2 physics를 설치하려면 npm install p2 --save를 입력하십시오.

이제 코드 작성 준비가되었습니다.

클라이언트 측 : main.js

var socket; 
socket = io.connect();


canvas_width = window.innerWidth * window.devicePixelRatio;
canvas_height = window.innerHeight * window.devicePixelRatio;

game = new Phaser.Game(canvas_width,canvas_height, 
                       Phaser.CANVAS, 'gameDiv');

// 적 플레이어 목록
var enemies = [];

var gameProperties = { 
 gameWidth: 4000,
 gameHeight: 4000,
 game_elemnt: "gameDiv",
 in_game: false,
};

var main = function(game){
};

function onsocketConnected () {
 console.log("connected to server"); 
 createPlayer();
 gameProperties.in_game = true;
 // 서버를 초기 위치에 보내고 우리에게 연결되었음을 알려줍니다.
 socket.emit('new_player', {x: 0, y: 0, angle: 0});
}

// 서버가 클라이언트 연결 끊김을 알리면 연결되지 않은 적을 찾아서 
// 게임에서 제거합니다.
function onRemovePlayer (data) {
 var removePlayer = findplayerbyid(data.id);
 // Player not found
 if (!removePlayer) {
  console.log('Player not found: ', data.id)
  return;
 }
 
 removePlayer.player.destroy();
 enemies.splice(enemies.indexOf(removePlayer), 1);
}

function createPlayer () {
 player = game.add.graphics(0, 0);
 player.radius = 100;

 // set a fill and line style
 player.beginFill(0xffd900);
 player.lineStyle(2, 0xffd900, 1);
 player.drawCircle(0, 0, player.radius * 2);
 player.endFill();
 player.anchor.setTo(0.5,0.5);
 player.body_size = player.radius; 

 // draw a shape
 game.physics.p2.enableBody(player, true);
 player.body.clearShapes();
 player.body.addCircle(player.body_size, 0 , 0); 
 player.body.data.shapes[0].sensor = true;
}

// this is the enemy class. 
var remote_player = function (id, startx, starty, start_angle) {
 this.x = startx;
 this.y = starty;
 // 이것이 유일한 소켓 ID입니다. 
    // 우리는 그것을 적의 유일한 이름으로 사용한다.
 this.id = id;
 this.angle = start_angle;
 
 this.player = game.add.graphics(this.x , this.y);
 this.player.radius = 100;

 // set a fill and line style
 this.player.beginFill(0xffd900);
 this.player.lineStyle(2, 0xffd900, 1);
 this.player.drawCircle(0, 0, this.player.radius * 2);
 this.player.endFill();
 this.player.anchor.setTo(0.5,0.5);
 this.player.body_size = this.player.radius; 

 // draw a shape
 game.physics.p2.enableBody(this.player, true);
 this.player.body.clearShapes();
 this.player.body.addCircle(this.player.body_size, 0 , 0); 
 this.player.body.data.shapes[0].sensor = true;
}

// 서버는 새로운 적 플레이어가 서버에 연결할 때 알려줍니다.
// 우리는 우리 게임에서 새로운 적을 창조합니다.
function onNewPlayer (data) {
 //enemy object 
 var new_enemy = new remote_player(data.id, data.x, 
                                      data.y, data.angle); 
 enemies.push(new_enemy);
}

// 서버는 새로운 적의 움직임이 있음을 알려줍니다. 
// 이동 된 적을 찾고 서버와 적의 움직임을 동기화합니다.
function onEnemyMove (data) {
 console.log("moving enemy");
 
 var movePlayer = findplayerbyid (data.id); 
 
 if (!movePlayer) {
  return;
 }
 
 var newPointer = {
  x: data.x,
  y: data.y, 
  worldX: data.x,
  worldY: data.y, 
 }
 
 var distance = distanceToPointer(movePlayer.player, newPointer);
 speed = distance/0.05;
 
 movePlayer.rotation = movetoPointer(movePlayer.player, speed, 
                                        newPointer);
}

// 우리는 서버에서 계산 된 위치를 받고 플레이어 위치를 변경합니다.
function onInputRecieved (data) {
 
 // 우리는 새로운 위치를 가진 새로운 포인터를 만들고 있습니다.
 var newPointer = {
  x: data.x,
  y: data.y, 
  worldX: data.x,
  worldY: data.y, 
 }
 
 var distance = distanceToPointer(player, newPointer);
 //우리는 50ms마다 플레이어 위치를 얻고 있습니다. 
    // 현재 위치와 새 위치 사이를 보정합니다.
 speed = distance/0.05;
 
 // 새로운 위치로 이동하십시오.
 player.rotation = movetoPointer(player, speed, newPointer);

}

// 여기서 우리는 소켓 ID를 사용합니다.
// 적의 목록을 검색하여 찾습니다.
function findplayerbyid (id) {
 for (var i = 0; i < enemies.length; i++) {
  if (enemies[i].id == id) {
   return enemies[i]; 
  }
 }
}

main.prototype = {
 preload: function() {
  game.stage.disableVisibilityChange = true;
  game.scale.scaleMode = Phaser.ScaleManager.RESIZE;
  game.world.setBounds(0, 0, gameProperties.gameWidth, 
                                  gameProperties.gameHeight, 
                                  false, false, false, false);
  game.physics.startSystem(Phaser.Physics.P2JS);
  game.physics.p2.setBoundsToWorld(false, false, false, 
                                         false, false)
  game.physics.p2.gravity.y = 0;
  game.physics.p2.applyGravity = false; 
  game.physics.p2.enableBody(game.physics.p2.walls, false); 
  // physics start system
  //game.physics.p2.setImpactEvents(true);

    },
 
 create: function () {
  game.stage.backgroundColor = 0xE1A193;;
  console.log("client started");
  socket.on("connect", onsocketConnected); 
  
  // listen to new enemy connections
  socket.on("new_enemyPlayer", onNewPlayer);
  // listen to enemy movement 
  socket.on("enemy_move", onEnemyMove);
  // when received remove_player, remove the player passed; 
  socket.on('remove_player', onRemovePlayer); 
  // when the player receives the new input
  socket.on('input_recieved', onInputRecieved);
 },
 
 update: function () {
  // 플레이어 입력을 내 보낸다.
  
  // 플레이어가 만들어지면 플레이어를 움직인다.
  if (gameProperties.in_game) {
  
   // 우리는 새로운 마우스 포인터를 만들고 
            // 이 입력을 서버에 보냅니다.
   var pointer = game.input.mousePointer;
     
   // 새로운 위치 데이터를 서버에 보냅니다.
   socket.emit('input_fired', {
    pointer_x: pointer.x, 
    pointer_y: pointer.y, 
    pointer_worldx: pointer.worldX, 
    pointer_worldy: pointer.worldY, 
   });
  }
 }
}

var gameBootstrapper = {
    init: function(gameContainerElementId){
  game.state.add('main', main);
  game.state.start('main'); 
    }
};;

gameBootstrapper.init("gameDiv");
클라이언트 측에서 추가 한 새로운 기능은 onInputRecieved입니다. 튜토리얼의 파트 1에서는 클라이언트에서 플레이어 자체를 이동하고 현재 위치를 서버로 보냈습니다. 대신 서버에서 새로운 위치를 기다릴 것입니다. 우리는 50ms마다 데이터를받습니다. 따라서 새로운 속도를 계산하여 시작 위치와 끝 위치를 보간합니다. socket.on에 의한 onInputReceived ( 'input_recieved', onInputRecieved)를 기다리십시오.

서버 : 새 파일, playermovement.js

서버 폴더에 "physics"라는 새 폴더를 만듭니다. playermovement.js라는 새 javascript 파일을 만듭니다.
function movetoPointer (displayObject, speed, pointer, maxTime) 
{
 pointer = pointer;
 if (maxTime === undefined) { maxTime = 0; }
 var angle = angleToPointer(displayObject, pointer);
 if (maxTime > 0)
 {
  // 얼마나 많은 픽셀을 이동해야 하는지 알지만 
        // 속도는 얼마나 빠릅니까?
  speed = distanceToPointer(displayObject, pointer) / 
                                 (maxTime / 1000);
 }
 displayObject.playerBody.velocity[0] = Math.cos(angle) * 
                                          speed;
 displayObject.playerBody.velocity[1] = Math.sin(angle) * 
                                          speed;
 return angle;
}

function distanceToPointer (displayObject, pointer, world) {
    if (world === undefined) { world = false; }
    var dx = (world) ? displayObject.world.x - pointer.worldX 
      : displayObject.playerBody.position[0] - pointer.worldX;
    var dy = (world) ? displayObject.world.y - pointer.worldY 
      : displayObject.playerBody.position[1] - pointer.worldY;
    return Math.sqrt(dx * dx + dy * dy);
}

function angleToPointer (displayObject, pointer, world) {
      
        if (world === undefined) { world = false; }

        if (world)
        {
            return Math.atan2(pointer.worldY - 
                              displayObject.world.y, 
                              pointer.worldX - 
                              displayObject.world.x);
        }
        else
        {
            return Math.atan2(pointer.worldY - 
                      displayObject.playerBody.position[1], 
                      pointer.worldX - 
                      displayObject.playerBody.position[0]);
        }
}

// 우리는이 세 가지 기능을 제공한다.
module.exports = {
 movetoPointer: movetoPointer,
 distanceToPointer: distanceToPointer,
 angleToPointer: angleToPointer
}
Node.js에서 다른 사람들이 사용할 함수를 내보내려면 module.exports를 사용해야합니다. 그렇지 않으면 파일을 필요로 할 때 사용할 수 없습니다. 파일을 "요구"하면 export 된 함수를 사용할 수 있습니다 .

플레이어 이동 함수는 클라이언트에서 동일한 메커니즘을가집니다. 그러나 우리는 playerBody.position [0]과 같은 p2 구문을 사용합니다. 구문은 나중에 설명 하겠지만 x 및 y 위치를 지정하는 p2 방법 일뿐입니다.

서버 : App.js 

var express = require('express');
// 서버에 p2 물리 라이브러리가 필요합니다.
var p2 = require('p2'); 

var app = express();
var serv = require('http').Server(app);
// 서버에서 플레이어를 이동하는 데 필요한 기능을 얻습니다.
var physicsPlayer = require('./server/physics/playermovement.js');

app.get('/',function(req, res) {
 res.sendFile(__dirname + '/client/index.html');
});
app.use('/client',express.static(__dirname + '/client'));

serv.listen(process.env.PORT || 2000);
console.log("Server started.");

var player_lst = [];

// 물리 업데이트에 필요
var startTime = (new Date).getTime();
var lastTime;
var timeStep= 1/70; 

// 서버의 물리 세계. 이것은 모든 현상이 일어나는 곳입니다.
// 우리는 마우스 포인터를 따라 가기 때문에 중력을 0으로 설정합니다.
var world = new p2.World({
  gravity : [0,0]
});

// a player class in the server
var Player = function (startX, startY, startAngle) {
  this.x = startX
  this.y = startY
  this.angle = startAngle
  this.speed = 500;
  // We need to intilaize with true.
  this.sendData = true;
}

// 물리 처리기를 60fps라고 부릅니다. 여기서 물리 계산됩니다.
setInterval(physics_hanlder, 1000/60);

// Steps the physics world. 
function physics_hanlder() {
 var currentTime = (new Date).getTime();
 timeElapsed = currentTime - startTime;
 var dt = lastTime ? (timeElapsed - lastTime) / 1000 : 0;
    dt = Math.min(1 / 10, dt);
    world.step(timeStep);
}


// 새 플레이어가 연결되면 플레이어 개체의 새 인스턴스를 만들고 
// 새 플레이어 메시지를 클라이언트에 보냅니다.
function onNewplayer (data) {
 console.log(data);
 // 새 플레이어 인스턴스
 var newPlayer = new Player(data.x, data.y, data.angle);
 
 // 플레이어 본문의 인스턴스 만들기
 playerBody = new p2.Body ({
  mass: 0,
  position: [0,0],
  fixedRotation: true
 });
 
 // 플레이어 객체에 playerbody 추가
 newPlayer.playerBody = playerBody;
 world.addBody(newPlayer.playerBody);
 
 console.log("created new player with id " + this.id);
 newPlayer.id = this.id;  
 // 발신자를 제외한 모든 클라이언트에게 보낼 정보
 var current_info = {
  id: newPlayer.id, 
  x: newPlayer.x,
  y: newPlayer.y,
  angle: newPlayer.angle,
 }; 
 
 // 이미 연결되어있는 모든 사람에 대해 새 플레이어에게 보냅니다.
 for (i = 0; i < player_lst.length; i++) {
  existingPlayer = player_lst[i];
  var player_info = {
   id: existingPlayer.id,
   x: existingPlayer.x,
   y: existingPlayer.y, 
   angle: existingPlayer.angle,   
  };
  console.log("pushing player");
  // 보낸 사람 - 클라이언트에게만 메시지 보내기
  this.emit("new_enemyPlayer", player_info);
 }
 
 // 발신자를 제외한 모든 연결된 클라이언트에게 메시지 보내기
 this.broadcast.emit('new_enemyPlayer', current_info);
 

 player_lst.push(newPlayer); 
}


// 우리는 이것을 더 이상 사용하지 않고있다.
function onMovePlayer (data) {
 var movePlayer = find_playerid(this.id); 
 movePlayer.x = data.x;
 movePlayer.y = data.y;
 movePlayer.angle = data.angle; 
 
 var moveplayerData = {
  id: movePlayer.id,
  x: movePlayer.x,
  y: movePlayer.y, 
  angle: movePlayer.angle
 }
 
 // 발신자를 제외한 모든 연결된 클라이언트에게 메시지 보내기
 this.broadcast.emit('enemy_move', moveplayerData);
}

// 플레이어 위치를 확인하는 대신 사용자 입력을 확인합니다.
function onInputFired (data) {
 var movePlayer = find_playerid(this.id, this.room); 
 
 
 if (!movePlayer) {
  return;
  console.log('no player'); 
 }

 // sendData가 true이면 데이터를 클라이언트에 다시 보냅니다.
 if (!movePlayer.sendData) {
  return;
 }
 
 // 50ms마다 데이터를 전송합니다.
 setTimeout(function() {movePlayer.sendData = true}, 50);
 // 데이터를 보낼 때 sendData를 false로 설정합니다.
 movePlayer.sendData = false;
 
 // 클라이언트로부터의 새로운 입력으로 새로운 포인터 만들기
 // 서버에 플레이어 위치를 포함합니다.
 var serverPointer = {
  x: data.pointer_x,
  y: data.pointer_y,
  worldX: data.pointer_worldx,   
  worldY: data.pointer_worldy
 }
 
 // 플레이어로 부터 새로운 입력으로 플레이어를 이동.
 if (physicsPlayer.distanceToPointer(movePlayer, serverPointer) <= 30) 
 {
  movePlayer.playerBody.angle = 
  physicsPlayer.movetoPointer(movePlayer, 0, serverPointer, 1000);
 } else {
  movePlayer.playerBody.angle = 
  physicsPlayer.movetoPointer(movePlayer, 
                              movePlayer.speed, serverPointer); 
 }
 
 // 새로운 플레이어 위치가 클라이언트로 다시 전송됩니다.
 var info = {
  x: movePlayer.playerBody.position[0],
  y: movePlayer.playerBody.position[1],
  angle: movePlayer.playerBody.angle
 }

 // 보낸 클라이언트 (모든 클라이언트가 아닌)로 보냅니다.
 this.emit('input_recieved', info);
 
 // 발신자를 제외한 모든 사용자에게 다시 전송할 데이터
 var moveplayerData = {
  id: movePlayer.id, 
  x: movePlayer.playerBody.position[0],
  y: movePlayer.playerBody.position[1],
  angle: movePlayer.playerBody.angle,
 }
 
 // 발신자를 제외한 모든 사람에게 보내기
 this.broadcast.emit('enemy_move', moveplayerData);
}

// 클라이언트가 연결을 끊을 때 호출을하고 발신자를 제외한 
// 클라이언트에게 연결이 끊긴 플레이어를 제거하라고 알립니다.
function onClientdisconnect() {
 console.log('disconnect'); 

 var removePlayer = find_playerid(this.id); 
  
 if (removePlayer) {
  player_lst.splice(player_lst.indexOf(removePlayer), 1);
 }
 
 console.log("removing player " + this.id);
 
 // 발신자를 제외한 모든 연결된 클라이언트에게 메시지 보내기
 this.broadcast.emit('remove_player', {id: this.id});
 
}

// 고유 소켓 ID로 플레이어 찾기
function find_playerid(id) {

 for (var i = 0; i < player_lst.length; i++) {

  if (player_lst[i].id == id) {
   return player_lst[i]; 
  }
 }
 
 return false; 
}

 // io connection 
var io = require('socket.io')(serv,{});

io.sockets.on('connection', function(socket){
 console.log("socket connected"); 
 
 // listen for disconnection; 
 socket.on('disconnect', onClientdisconnect); 
 
 // listen for new player
 socket.on("new_player", onNewplayer);
 /*
 //we dont need this anymore
 socket.on("move_player", onMovePlayer);
 */
 //listen for new player inputs. 
 socket.on("input_fired", onInputFired);
});
우리는 먼저 물리 계산을 위해 p2 물리학을 요구합니다. 우리는 또한 플레이어 이동을 위한 playermovement 파일이 필요합니다. 우리는 새로운 p2 물리 세계를 만들고 physics_handler로 단계를 밟습니다.
문서 : http://schteppe.github.io/p2.js/docs/classes/Body.html.

우선 우리가 플레이어를 만들 때 플레이어 바디가 추가됩니다. Phaser에서는 p2.js와 같지만 구문은 다릅니다. 우리는 OnNewPlayer에 새로운 p2.Body가 있는 본문을 만듭니다. 우리는 OnMovePlayer를 제거하고 onInputFired를 추가했습니다. 가장 큰 차이점은 다음과 같습니다. 또한 새로운 위치를 발신자에게 보냅니다. 서버에서 몸체의 x 위치를 지정하려면 playerbody [0]을 수행하고 y 위치를 지정하려면 playerbody [1]을 지정합니다. 속도에 대해서도 마찬가지입니다. x는 playerbody.velocity [0], y velocity는 playerbody.velocity [1]입니다. world.addBody (playerbody)를 사용하여 플레이어 바디를 세계에 추가하는 것을 잊지 마십시오 !! 그렇지 않으면 플레이어의 물리 연산이 계산되지 않습니다.

이제 "node app"을 입력하여 실제 게임을보십시오!

새로운 튜토리얼에서 우리는 더 많은 게임 메 커닉 측면에 집중할 것입니다. 우리는 음식과 다른 플레이어를 먹는 agar.io 게임 메 커닉을 추가 할 것입니다. 레벨 업을위한 경험 막대도 추가 할 것입니다.

댓글 없음:

댓글 쓰기