소개
저장소는 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);
});
문서 : 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 게임 메 커닉을 추가 할 것입니다. 레벨 업을위한 경험 막대도 추가 할 것입니다.
댓글 없음:
댓글 쓰기