정식 서버로 Phaser 3에서 간단한 멀티 플레이어 게임 만들기 - 2 부

이 튜토리얼의 제 1 부에서는 Node.js 서버를 만들고 기본적인 Phaser 게임을 설정하고 헤드리스 모드로 Phaser를 실행하도록 서버를 설정합니다.
이 튜토리얼에서는 게임에 Socket.IO 라이브러리를 추가하고 게임에 플레이어를 추가 및 제거하는 서버 로직을 추가하며 플레이어를 게임에 추가하기위한 클라이언트 측 로직을 추가하는 데 집중할 것입니다.

Socket.IO 추가하기

Phaser가 이제 서버에서 실행되면서 이제 Socket.IO를 게임에 추가 할 것입니다.
Socket.IO는 웹 클라이언트와 서버 간의 실시간 양방향 통신을 가능하게하는 JavaScript 라이브러리입니다.
Socket.IO를 사용하려면 클라이언트와 서버 코드를 업데이트하여 둘 사이의 통신을 가능하게해야합니다.

터미널에서 다음 명령을 실행하십시오.
npm install --save socket.io
서버가 계속 실행중인 경우 새 터미널 창을 열고 프로젝트 폴더에서 코드를 실행하거나 서버를 중지 한 다음 (Ctrl + C) 명령을 실행할 수 있습니다. 이렇게하면 Socket.IO 노드 패키지가 설치되어 package.json 파일에 저장됩니다.

이제 index.js에 var server = require ( 'http').Server (app); 아래에 다음 코드를 추가합니다.
const io = require('socket.io').listen(server);
그런 다음 dom.window.gameLoaded 코드 아래에 다음 행을 추가하십시오.
dom.window.io = io;
그러면 jsdom에 socket.io 인스턴스가 삽입되어 서버에서 실행중인 Phaser 코드에서 이 인스턴스에 액세스 할 수 있습니다. 이제 authoritative_server / js / game.js에서 create 함수에 다음 코드를 추가합니다.
io.on('connection', function(socket) {
console.log('a user connected');
socket.on('disconnect', function() {
console.log('user disconnected');
});
});
위 코드에서 우리는 :
socket.io 모듈을 참조하고 서버 객체를 수신하게했습니다.
연결 및 연결 끊김을 청취하는 논리가 추가되었습니다.

다음으로 Socket.IO 라이브러리를 포함하도록 클라이언트 측 코드를 업데이트합니다. public / index.html을 열고 <body> 요소의 맨 위에 다음 줄을 추가하십시오.
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.js"></script>
그런 다음 public / js / game.js를 열고 create 함수에 다음 코드를 추가합니다.
this.socket = io();
이제 코드 변경 사항을 저장하고 서버를 다시 시작한 다음 브라우저에서 게임을 새로 고침하면 연결된 사용자에 대한 메시지가 표시됩니다. 브라우저에서 게임을 새로 고침하면 사용자 연결 해제에 대한 새 메시지와 사용자 연결에 대한 다른 메시지가 표시됩니다.

플레이어 추가 - 서버

소켓 연결 설정을 통해 플레이어를 게임에 추가 할 수 있습니다. 가장 먼저해야 할 일은 플레이어의 우주선에 사용될 자산을 적재하는 것입니다. 이 튜토리얼에서는 Kenny 's Space Shooter Redux 자산 팩의 일부 이미지를 사용합니다. 게임의 자산은 여기에서 다운로드 할 수 있습니다.

authoritative_server 폴더에서 assets이라는 새 폴더를 만들고 여기에 이미지를 저장합니다. 게임에서 이미지를로드하려면 authoritative_server / js / game.js의 사전로드 함수에 다음 행을 추가해야합니다.
this.load.image('ship', 'assets/spaceShips_001.png');

우주선 이미지가 로드되면 플레이어 추가를 위한 나머지 로직이 추가됩니다. 모든 플레이어의 게임을 동기화 상태로 유지하려면 사용자가 게임에 연결하거나 연결을 끊을 때 모든 플레이어에게 알릴 수있는 방법이 필요합니다. 또한 새 플레이어가 연결되면 새 플레이어가 게임의 다른 모든 플레이어를 알 수있는 방법이 필요합니다.

이렇게 하기 위해 소켓 연결을 사용하여 각 클라이언트에 메시지를 보낼 수 있습니다. 플레이어 데이터의 경우 연결하고 끊는 각 플레이어를 추적해야 합니다.

authoritative_server / js / game.js 파일의 맨 위에 다음 코드를 추가하십시오.
const players = {};

이 오브젝트를 사용하여 현재 게임중인 모든 플레이어를 추적합니다. 그런 다음 create 함수의 맨 위에 다음 코드를 추가합니다.
const self = this;
this.players = this.physics.add.group();

그런 다음 socket.io 연결 이벤트의 콜백 함수에서 console.log ( 'a user connected') 아래에 다음 코드를 추가합니다.
// create a new player and add it to our players object
players[socket.id] = {
rotation: 0,
x: Math.floor(Math.random() * 700) + 50,
y: Math.floor(Math.random() * 500) + 50,
playerId: socket.id,
team: (Math.floor(Math.random() * 2) == 0) ? 'red' : 'blue'
};
// add player to server
addPlayer(self, players[socket.id]);
// send the players object to the new player
socket.emit('currentPlayers', players);
// update all other players of the new player
socket.broadcast.emit('newPlayer', players[socket.id]);
방금 추가 한 코드를 살펴 보겠습니다.

- 우리는 self라고 불리는 새로운 변수를 만들었고이 변수를 사용하여 이 Phaser Scene에 대한 참조를 저장합니다.
- 우리는 우리 게임에서 모든 플레이어를 관리하는 데 사용될 새로운 Phaser 물리 그룹을 만들었습니다. Phaser의 그룹에 익숙하지 않은 경우, 유사한 게임 개체를 관리하고 하나의 단위로 제어 할 수있는 방법입니다. 한 가지 예는 각 게임 개체의 충돌을 개별적으로 확인하지 않고 그룹과 다른 게임 개체 간의 충돌을 검사 할 수 있다는 것입니다.
- 플레이어가 웹 소켓에 연결하면 플레이어에 대한 데이터로 플레이어 개체를 업데이트하고 이 데이터를 개체로 저장하고 소켓의 ID를 개체의 키로 사용합니다.
- 우리는 플레이어의 회전, x 및 y 위치를 저장하고 있으며 클라이언트 측에서 스프라이트를 작성하고 이 데이터를 사용하여 각 플레이어 게임을 업데이트하는 데 사용합니다.
- 우리는 또한 playerId를 저장하여 게임에서 참조 할 수 있으며 나중에 사용할 팀 속성을 추가했습니다.
- 우리는 socket.emit과 socket.broadcast.emit을 사용하여 클라이언트 측 소켓에 이벤트를 발생시켰다. socket.emit은 이 특정 소켓 (방금 연결 한 새 플레이어)으로 이벤트를 방출합니다. socket.broadcast.emit은 이벤트를 다른 모든 소켓 (기존 플레이어)에게 보냅니다.
- currentPlayers 이벤트에서 플레이어 개체를 새 플레이어로 전달합니다. 이 데이터는 새 플레이어의 게임에서 모든 플레이어 스프라이트를 채우는 데 사용됩니다.
- newPlayer 이벤트에서 우리는 새로운 플레이어의 데이터를 다른 모든 플레이어에게 전달하므로 새로운 스프라이트를 게임에 추가 할 수 있습니다.
- 마지막으로 addPlayer라는 새로운 함수를 호출했습니다.이 함수는 서버에서 플레이어를 만드는 데 사용됩니다.

이제 우리는 addPlayer 함수를 생성 할 것입니다. 이 함수는 새로운 플레이어 게임 개체를 만드는 데 사용되며 방금 만든 플레이어 그룹에 해당 게임 개체를 추가합니다. 업데이트 함수 아래에 다음 코드를 추가하십시오.
function addPlayer(self, playerInfo) {
const player = self.physics.add.image(playerInfo.x, playerInfo.y, 'ship').setOrigin(0.5, 0.5).setDisplaySize(53, 40);
player.setDrag(100);
player.setAngularDrag(100);
player.setMaxVelocity(200);
player.playerId = playerInfo.playerId;
self.players.add(player);
}
위 코드에서 우리는 :
- 이전에 생성 한 x와 y 좌표를 사용하여 새 플레이어의 배를 만들었습니다.
- 플레이어의 배를 만들기 위해 self.add.image를 사용하는 대신 self.physics.add.image를 사용하여 게임 객체가 아케이드 물리를 사용할 수있게했습니다.
- setOrigin ()을 사용하여 게임 객체의 원점을 왼쪽 상단 대신 객체의 중간에 설정했습니다. 우리가 게임 객체를 회전시킬 때 원점을 중심으로 회전하기 때문에 이 작업을 수행 한 이유입니다.
- setDisplaySize ()를 사용하여 게임 객체의 크기와 크기를 변경했습니다. 원래 우주선 이미지는 106 × 80 픽셀이었습니다. setDisplaySize ()를 호출 한 후 이미지가 이제 게임에서 53x40 픽셀입니다.
- 마지막으로 setDrag, setAngularDrag 및 setMaxVelocity를 사용하여 게임 객체가 아케이드 물리에 반응하는 방식을 수정했습니다. setDrag 및 setAngularDrag는 객체가 움직일 때 저항하는 양을 제어하는 ​​데 사용됩니다. setMaxVelocity는 게임 개체가 도달 할 수있는 최대 속도를 제어하는 ​​데 사용됩니다.

플레이어 제거 - 서버

서버에 플레이어를 추가하는 로직을 사용하여 서버에서 플레이어를 제거하는 로직을 추가합니다. 플레이어가 연결을 끊으면 플레이어 개체에서 해당 플레이어의 데이터를 제거해야하며,이 사용자에 대한 다른 모든 플레이어에게 메시지를 내 보내야합니다. 그러면 플레이어의 스프라이트가 클라이언트 게임에서 제거 될 수 있습니다.

socket.io 연결 해제 이벤트의 콜백 함수에서 console.log ( 'user disconnected') 아래에 다음 코드를 추가하십시오.
// remove player from server
removePlayer(self, socket.id);
// remove this player from our players object
delete players[socket.id];
// emit a message to all players to remove this player
io.emit('disconnect', socket.id);

그런 다음 addPlayer 함수 아래에 다음 코드를 추가합니다.
function removePlayer(self, playerId) {
self.players.getChildren().forEach((player) => {
if (playerId === player.playerId) {
player.destroy();
}
});
}
위 코드에서 우리는 :

- removePlayer라는 새 함수를 만들었습니다. 이 함수는 연결이 끊어진 플레이어의 socket.id를 가져오고 Phaser 그룹에서 플레이어의 게임 개체를 찾아 파괴합니다.
- 우리는 플레이어 그룹에서 getChildren () 메서드를 호출하여이를 수행합니다. getChildren () 메서드는 해당 그룹에있는 모든 게임 객체의 배열을 반환합니다. 일치하는 ID를 가진 게임 객체를 찾을 때까지 forEach () 메서드를 사용하여 배열을 반복합니다.
- 서버에서 게임 개체가 파괴되면 delete 개체를 사용하여 해당 개체를 제거하여 플레이어 개체에서 해당 개체를 삭제합니다.
- 마지막으로 io.emit을 사용하여 모든 소켓에 메시지를 보냈습니다. 이 경우 연결을 끊은 사용자의 socket.id를 전달하여 클라이언트 측 코드에서 해당 플레이어의 스프라이트를 제거 할 수 있습니다.

authoritative_server / js / game.js 파일은 다음과 같아야합니다.
const players = {};

const config = {
type: Phaser.HEADLESS,
parent: 'phaser-example',
width: 800,
height: 600,
physics: {
default: 'arcade',
arcade: {
debug: false,
gravity: { y: 0 }
}
},
scene: {
preload: preload,
create: create,
update: update
},
autoFocus: false
};

function preload() {
this.load.image('ship', 'assets/spaceShips_001.png');
}

function create() {
const self = this;
this.players = this.physics.add.group();

io.on('connection', function (socket) {
console.log('a user connected');
// create a new player and add it to our players object
players[socket.id] = {
rotation: 0,
x: Math.floor(Math.random() * 700) + 50,
y: Math.floor(Math.random() * 500) + 50,
playerId: socket.id,
team: (Math.floor(Math.random() * 2) == 0) ? 'red' : 'blue'
};
// add player to server
addPlayer(self, players[socket.id]);
// send the players object to the new player
socket.emit('currentPlayers', players);
// update all other players of the new player
socket.broadcast.emit('newPlayer', players[socket.id]);

socket.on('disconnect', function () {
console.log('user disconnected');
// remove player from server
removePlayer(self, socket.id);
// remove this player from our players object
delete players[socket.id];
// emit a message to all players to remove this player
io.emit('disconnect', socket.id);
});
});
}

function update() { }

function addPlayer(self, playerInfo) {
const player = self.physics.add.image(playerInfo.x, playerInfo.y, 'ship').setOrigin(0.5, 0.5).setDisplaySize(53, 40);
player.setDrag(100);
player.setAngularDrag(100);
player.setMaxVelocity(200);
player.playerId = playerInfo.playerId;
self.players.add(player);
}

function removePlayer(self, playerId) {
self.players.getChildren().forEach((player) => {
if (playerId === player.playerId) {
player.destroy();
}
});
}

const game = new Phaser.Game(config);
window.gameLoaded();

이제 코드 변경 사항을 저장하고 서버를 다시 시작한 다음 브라우저에서 게임을 새로 고침하면 콘솔에 오류 메시지가 표시됩니다.


오류 메시지의 모양에서 jsdom은 URL.createObjectURL 메소드를 지원하지 않는 것 같습니다. 이 정적 메서드는 지정된 소스 객체의 내용을 참조하는 데 사용할 수있는 객체 URL이 포함 된 DOMString을 만드는 데 사용됩니다. 이 오류를 해결하려면 비슷한 값을 반환하는 메서드를 구현해야합니다.

이를 위해 datauri 패키지를 사용하여 데이터 URI를 반환합니다. 이 패키지를 사용하려면 프로젝트에 추가해야합니다. 터미널에서 서버를 중지하고 다음 명령을 실행하십시오.
npm install -save datauri

그런 다음 서버 코드에 라이브러리를 포함시켜야합니다. server / index.js를 열고 const {JSDOM} = jsdom;
const Datauri = require('datauri');
const datauri = new Datauri()

setupAuthoritativePhaser 함수에서 다음 코드를 dom.window.gameLoaded = () => {line :
dom.window.URL.createObjectURL = (blob) => {
if (blob) {
return datauri.format(blob.type, blob[Object.getOwnPropertySymbols(blob)[0]]._buffer).content;
}
};
dom.window.URL.revokeObjectURL = (objectURL) => { };
방금 추가 한 코드를 살펴 보겠습니다.

- 첫째, 우리는 datauri 패키지를 포함 시켰고 새로운 인스턴스를 만들었습니다.
그런 다음 jsdom에 구현되어 있지 않은 createObjectURL 및 revokeObjectURL 함수를 만들었습니다.
- revokeObjecURL 함수의 경우 함수에 아무 것도하지 않습니다.
- createObjectURL 함수의 경우 datauri.format 메서드를 사용하여 blob을 필요한 형식으로 포맷합니다.
이제 코드 변경 사항을 저장하고 서버를 시작하면 모든 것이 잘 시작됩니다.

플레이어 추가 - 클라이언트

플레이어를 추가하는 서버 코드를 사용하여 클라이언트 측 코드를 작성합니다. 가장 먼저해야 할 일은 플레이어에게 사용될 자산을 로드하는 것입니다. 공용 폴더에서 assets이라는 새 폴더를 만들고 이 폴더에서 다른 assets 폴더의 spaceShips_001.png 이미지를 복사합니다.

게임에서 이미지를 로드하려면 public / js / game.js의 preload 함수에 다음 줄을 추가해야합니다.
this.load.image('ship', 'assets/spaceShips_001.png');
우주선 이미지가 로드되면 이제 게임에서 플레이어를 만들 수 있습니다. 이전에 새 플레이어가 게임에 연결할 때마다 currentPlayers 이벤트를 발생하도록 Socket.IO를 설정했으며 이 이벤트가 발생하면 현재 플레이어의 데이터가 포함 된 플레이어 개체도 전달했습니다.

public / js / game.js의 create 함수를 다음과 일치하도록 업데이트하십시오.
function create() {
var self = this;
this.socket = io();
this.players = this.add.group();

this.socket.on('currentPlayers', function (players) {
Object.keys(players).forEach(function (id) {
if (players[id].playerId === self.socket.id) {
displayPlayers(self, players[id], 'ship');
}
});
});
}
방금 추가 한 코드를 살펴 보겠습니다.

먼저 클라이언트 측의 모든 게임 개체를 관리하는 데 사용되는 새로운 Phaser 그룹을 만들었습니다.
우리는 currentPlayers 이벤트를 수신하기 위해 socket.on을 사용했으며, 이 이벤트가 트리거되면 우리가 제공 한 함수가 서버에서 전달한 players 객체로 호출됩니다.
이 함수가 호출되면 각 플레이어를 반복하고 해당 플레이어의 ID가 현재 플레이어의 소켓 ID와 일치하는지 확인합니다.
플레이어를 통해 반복하기 위해 우리는 Object.keys ()를 사용하여 전달 된 Object의 모든 키 배열을 만듭니다. 반환되는 배열의 경우 forEach () 메서드를 사용하여 배열의 각 항목을 반복합니다 .
마지막으로 displayPlayers () 함수를 호출하여 현재 플레이어의 정보와 현재 장면에 대한 참조를 전달했습니다.

이제 public / js / game.js에 displayPlayer 함수를 추가해 보겠습니다. 파일의 맨 아래에 다음 코드를 추가하십시오.
function displayPlayers(self, playerInfo, sprite) {
const player = self.add.sprite(playerInfo.x, playerInfo.y, sprite).setOrigin(0.5, 0.5).setDisplaySize(53, 40);
if (playerInfo.team === 'blue') player.setTint(0x0000ff);
else player.setTint(0xff0000);
player.playerId = playerInfo.playerId;
self.players.add(player);
}
위 코드에서 우리는 :

우리 서버 코드에서 생성 한 x와 y 좌표를 사용하여 플레이어의 배를 만들었습니다.
setOrigin ()을 사용하여 게임 객체의 원점을 왼쪽 상단 대신 객체의 중간에 설정했습니다.
setDisplaySize ()를 사용하여 게임 객체의 크기와 크기를 변경했습니다.
우리는 setTint ()를 사용하여 배 게임 객체의 색상을 변경했으며 서버에 플레이어 정보를 생성 할 때 생성 된 팀에 따라 색상을 선택합니다.
나중에 id로 게임 객체를 찾을 수 있도록 playerId를 저장했습니다.
마지막으로 플레이어의 게임 개체를 우리가 만든 Phaser 그룹에 추가했습니다.

브라우저를 새로 고침하면 플레이어의 배가 화면에 표시됩니다.


또한 게임을 새로 고침하면 우주선이 다른 위치에 나타나고 우주선이 빨간색이나 파란색으로 무작위로 표시되어야 합니다.

결론

플레이어를 표시하기위한 클라이언트 측 코드를 사용하면이 자습서 시리즈의 제 2 편이 끝납니다. 3 부에서는 멀티 플레이어 게임을 계속 진행합니다.

다른 플레이어를 게임에 추가하기위한 클라이언트 측 로직 추가.
플레이어 입력에 대한 논리 추가.
수집품에 대한 논리 추가.
나는 당신이 우리 두 번째 할부를 즐겁게하고 그것이 도움이되기를 바랐다. 우리가 다음에 다루어야 할 것에 대해 질문이나 제안이 있으면, 아래의 의견에 저희에게 알려주십시오.

댓글 없음:

댓글 쓰기