이 튜토리얼의 목표는 기본 멀티 플레이어 HTML5 게임을 만드는 방법을 보여주는 것입니다. 게임 개발을 위해 Phaser를 사용하고 클라이언트 / 서버 통신을 위해 Eureca.io를 사용합니다. 이 튜토리얼에서는 HTML5 게임 개발에 대한 지식 (Preference에서 Phaser로 이미 알고있는 것으로 생각합니다)을 가정합니다. 또한 nodej에 대한 지식이 있고 이미 설치했다고 가정합니다. 내가 사용할 게임 코드는 페이저 웹 사이트의 Tanks 예제 게임을 기반으로합니다. 이 비디오는 최종 결과를 보여줍니다.
첫 번째 단계 : 코드 리팩터링
멀티 플레이어에 적합하도록 수정 및 단순화 되었습니다. 멀티 플레이어 모드에서 적 탱크는 단지 원격 플레이어이기 때문에 Tank라는 클래스의 플레이어와 적 탱크 코드를 분해했습니다 (이것은 Phaser 예제 코드에서 EnemyTank 클래스로 이름이 변경되었습니다). 생성 및 업데이트 코드가 Tank 클래스로 이동되었습니다.
Tank = function (index, game, player) {
this.cursor = {
left:false,
right:false,
up:false,
fire:false
}
this.input = {
left:false,
right:false,
up:false,
fire:false
}
var x = 0;
var y = 0;
this.game = game;
this.health = 30;
this.player = player;
this.bullets = game.add.group();
this.bullets.enableBody = true;
this.bullets.physicsBodyType = Phaser.Physics.ARCADE;
this.bullets.createMultiple(20, 'bullet', 0, false);
this.bullets.setAll('anchor.x', 0.5);
this.bullets.setAll('anchor.y', 0.5);
this.bullets.setAll('outOfBoundsKill', true);
this.bullets.setAll('checkWorldBounds', true);
this.currentSpeed =0;
this.fireRate = 500;
this.nextFire = 0;
this.alive = true;
this.shadow = game.add.sprite(x, y, 'enemy', 'shadow');
this.tank = game.add.sprite(x, y, 'enemy', 'tank1');
this.turret = game.add.sprite(x, y, 'enemy', 'turret');
this.shadow.anchor.set(0.5);
this.tank.anchor.set(0.5);
this.turret.anchor.set(0.3, 0.5);
this.tank.id = index;
game.physics.enable(this.tank, Phaser.Physics.ARCADE);
this.tank.body.immovable = false;
this.tank.body.collideWorldBounds = true;
this.tank.body.bounce.setTo(0, 0);
this.tank.angle = 0;
game.physics.arcade.velocityFromRotation(this.tank.rotation, 0, this.tank.body.velocity);
};
Tank.prototype.update = function() {
for (var i in this.input) this.cursor[i] = this.input[i];
if (this.cursor.left)
{
this.tank.angle -= 1;
}
else if (this.cursor.right)
{
this.tank.angle += 1;
}
if (this.cursor.up)
{
// The speed we'll travel at
this.currentSpeed = 300;
}
else
{
if (this.currentSpeed > 0)
{
this.currentSpeed -= 4;
}
}
if (this.cursor.fire)
{
this.fire({x:this.cursor.tx, y:this.cursor.ty});
}
if (this.currentSpeed > 0)
{
game.physics.arcade.velocityFromRotation(this.tank.rotation, this.currentSpeed, this.tank.body.velocity);
}
else
{
game.physics.arcade.velocityFromRotation(this.tank.rotation, 0, this.tank.body.velocity);
}
this.shadow.x = this.tank.x;
this.shadow.y = this.tank.y;
this.shadow.rotation = this.tank.rotation;
this.turret.x = this.tank.x;
this.turret.y = this.tank.y;
};
또한 게임 장면에서 탱크를 제거하기 위해 Tank.kill 메서드를 추가하고 fire () 코드를 Tank.fire로 이동했습니다.
Tank.prototype.fire = function(target) {
if (!this.alive) return;
if (this.game.time.now > this.nextFire && this.bullets.countDead() > 0)
{
this.nextFire = this.game.time.now + this.fireRate;
var bullet = this.bullets.getFirstDead();
bullet.reset(this.turret.x, this.turret.y);
bullet.rotation = this.game.physics.arcade.moveToObject(bullet, target, 500);
}
}
Tank.prototype.kill = function() {
this.alive = false;
this.tank.kill();
this.turret.kill();
this.shadow.kill();
}
또한 단순화를 위해 피해를 처리하는 부분을 제거 했으므로 다른 탱크를 쏘아도 그들을 죽지 않을 것입니다. 하나의 마지막 변경은 플레이어 입력이 처리되는 방식으로 이루어졌습니다. 예제 코드는 (game.input.keyboard.createCursorKeys ()를 통해) 직접 phaser.input 네임 스페이스를 사용하지만, 우리의 경우 클라이언트가 직접 입력을 처리해서는 안됩니다. 그래서 플레이어 입력을 처리하기 위해 Tank.input 객체와 Tank.cursor 객체를 만들었습니다. 다음은 수정 된 update () 함수의 코드입니다.
function update () {
player.input.left = cursors.left.isDown;
player.input.right = cursors.right.isDown;
player.input.up = cursors.up.isDown;
player.input.fire = game.input.activePointer.isDown;
player.input.tx = game.input.x+ game.camera.x;
player.input.ty = game.input.y+ game.camera.y;
turret.rotation = game.physics.arcade.angleToPointer(turret);
land.tilePosition.x = -game.camera.x;
land.tilePosition.y = -game.camera.y;
for (var i in tanksList)
{
if (!tanksList[i]) continue;
var curBullets = tanksList[i].bullets;
var curTank = tanksList[i].tank;
for (var j in tanksList)
{
if (!tanksList[j]) continue;
if (j!=i)
{
var targetTank = tanksList[j].tank;
game.physics.arcade.overlap(curBullets, targetTank, bulletHitPlayer, null, this);
}
if (tanksList[j].alive)
{
tanksList[j].update();
}
}
}
}
여기에서 리팩토링 된 코드를 다운로드하면 다음 튜토리얼 단계를 수행하는 데 도움이됩니다.
이제는 간단한 코드 만 만들어서 멀티 플레이어 게임으로 변환 해 보겠습니다.
***************************************************************************************
내부 요구 사항을 위해 개발 한 RPC 라이브러리 인 Eureca.io를 사용하고 소스 코드 (https://github.com/Ezelia/eureca.io에서 사용할 수있는 소스 코드)를 열어보기로했습니다. 여러 네트워킹 라이브러리 (socket.io, engine.io ... 등)가 있지만 Eureca.io가 일을 더 단순하게 만드는 방법을 보게됩니다 🙂
***************************************************************************************
웹 서버
게임을위한 기본적인 웹 서버를 만드는 것으로 시작하겠습니다. 여기서 nodejs 를 위한 익스프레스 라이브러리를 설치하고, 파일 서비스를 더 간단하게 만들면, eureca.io와 호환됩니다. Express는 멀티 플레이어 게임 (동적 인 웹 페이지, 세션, 쿠키, 양식 등을 처리 할 수있는 웹 페이지)을 구축하는 경우에도 도움이됩니다.
npm install express
Tank 게임의 루트 디렉토리에 server.js 파일을 만들고 다음 코드로 편집하십시오
var express = require('express')
, app = express(app)
, server = require('http').createServer(app);
// serve static files from the current directory
app.use(express.static(__dirname));
server.listen(8000);
브라우저를 열고 http : // localhost : 8000 /로 이동하십시오. Tank 게임이 잘 작동한다면 다음 단계로 넘어가거나 여기에서 코드를 다운로드 하면됩니다.
eureca.io 설치 및 준비
이제 Eureca.io로 게임을 시작해보십시오. engine.io가 사용되는 엔진 전송 레이어로 engine.io 또는 sockjs를 사용할 수 있습니다. eureca.io를 기본 구성으로 사용하려면 eureca.io와 engine.io를 설치해야합니다.
npm install engine.io
npm install eureca.io
이제 eureca.io를 추가하기 위해 서버 코드를 수정합니다 : server.listen (8000)
eureca.io 서버를 인스턴스화하고 다음 코드를 사용하여 HTTP 서버에 연결합니다.
//get EurecaServer class
var EurecaServer = require('eureca.io').EurecaServer;
//create an instance of EurecaServer
var eurecaServer = new EurecaServer();
//attach eureca.io to our http server
eurecaServer.attach(server);
그런 다음 클라이언트 연결과 연결 해제를 감지하는 이벤트 리스너를 추가합니다
//detect client connection
eurecaServer.onConnect(function (conn) {
console.log('New Client id=%s ', conn.id, conn.remoteAddress);
});
//detect client disconnection
eurecaServer.onDisconnect(function (conn) {
console.log('Client disconnected ', conn.id);
});
클라이언트 측에서는 tanks.js 스크립트보다 먼저 index.html에 다음 행을 추가합니다.
<script src="/eureca.js"></script>
이렇게 하면 클라이언트가 eureca.io를 사용할 수 있게됩니다. 이제 tanks.js 파일을 편집하고 처음에 다음 코드를 추가하십시오
var ready = false;
var eurecaServer;
//this function will handle client communication with the server
var eurecaClientSetup = function() {
//create an instance of eureca.io client
var eurecaClient = new Eureca.Client();
eurecaClient.ready(function (proxy) {
eurecaServer = proxy;
//we temporary put create function here so we make sure to launch the game once the client is ready
create();
ready = true;
});
}
여기서 우리가 하는 일은 eureca.io 클라이언트를 인스턴스화하고 클라이언트가 준비 될 때까지 기다린 다음 클라이언트가 초기화 될 때까지 기다리는 클라이언트 초기화 메소드 "eurecaClientSetup"을 작성한 다음 create () 메소드가 Phaser에 의해 처음 호출 된 게임 작성 메소드 (create ())를 호출하는 것입니다.
Game () 인스턴스화 메소드는 이 행을 수정하여 eurecaClientSetup을 호출합니다.
var game = new Phaser.Game(800, 600, Phaser.AUTO, 'phaser-example', { preload: preload, create: eurecaClientSetup, update: update, render: render });
중요 : 멀티 플레이어 게임을 만드는 경우 일반적으로 게임 코드를 시작하기 전에 서버를 사용할 수 있는지 확인해야합니다. 이것이 바로 eurecaClientSetup에서 수행하는 작업입니다.
마지막 한가지. ready 변수가 false로 설정된 것을 보았을 때 클라이언트 / 서버 초기화가 완료되었는지를 알 수 있습니다.
게임이 만들어졌습니다. 우리는 phaser가 create () 전에 update methode를 호출하는 것을 막기 위해 이를 사용합니다. 그래서 우리는 update () 메소드에 다음을 추가 할 필요가있다.
function update () {
//do not update if client not ready
if (!ready) return;
}
여기서 결과 코드를 다운로드 할 수 있습니다 : 탱크 게임 다운로드 2 단계 코드
server.js를 다시 (노드 server.js) 실행하고 http : // localhost : 8000 / 게임을 시작하면 서버가 클라이언트 연결을 감지했음을 알 수 있습니다. 이제 페이지를 새로 고침하면 클라이언트가 연결 해제 된 후 다시 연결되었음을 알 수 있습니다. 이것이 작동하면 다음 단계로 넘어갈 준비가되었습니다.
원격 플레이어 스폰/죽음
기본 멀티 플레이어 게임의 경우 서버는 연결된 모든 클라이언트를 추적해야합니다. 모든 클라이언트를 구별하기 위해 우리는 또한 각 플레이어에 대해 고유 식별자를 가질 필요가 있습니다 (우리는 eureca.io가 생성 한 고유 ID를 사용합니다). 이 고유 ID는 클라이언트와 서버간에 공유됩니다. 플레이어 데이터를 동기화하고 원격 클라이언트와 탱크를 연결할 수 있습니다.
구현은 다음과 같습니다.
- 새 클라이언트가 연결되면 서버는 uniq id (여기서는 eureca.io 세션 ID가 사용됨)
- 서버는이 uniq id를 클라이언트에 보낸다.
- 클라이언트는 플레이어의 탱크로 게임 장면을 만들고 그 고유의 ID를 탱크에 할당합니다.
- 클라이언트는 클라이언트 측에서 모든 것이 준비되었다는 것을 서버에 알립니다 (우리는 이것을 핸드 쉐이크라고 부릅니다)
- 서버는 각 연결된 플레이어에 대해 알림 및 호출 클라이언트 Spawn 메서드를 가져옵니다.
- 클라이언트는 연결된 각 플레이어에 대해 Tank 인스턴스를 생성합니다.
- 클라이언트가 연결을 끊으면 서버가이를 식별하여 연결된 클라이언트 목록에서 제거합니다.
- 모든 연결된 클라이언트의 서버단에서 Kill () 메서드 호출
- 각 클라이언트는 연결이 끊긴 플레이어의 Tank 인스턴스를 제거합니다.
클라이언트 사이드
Eureca.io 인스턴스에는 "exports"라는 특수한 네임 스페이스가 있습니다.
이 네임 스페이스 아래 정의 된 모든 메소드는 RPC에서 사용할 수있게됩니다. 우리는 그것을 어떻게 사용하는지 보게 될 것이다. 이를 위해 eurecaClientSetup 메소드를 수정해야합니다.
var eurecaClientSetup = function() {
//create an instance of eureca.io client
var eurecaClient = new Eureca.Client();
eurecaClient.ready(function (proxy) {
eurecaServer = proxy;
});
//methods defined under "exports" namespace become available in the server side
eurecaClient.exports.setId = function(id)
{
//create() is moved here to make sure nothing is created before uniq id assignation
myId = id;
create();
eurecaServer.handshake();
ready = true;
}
eurecaClient.exports.kill = function(id)
{
if (tanksList[id]) {
tanksList[id].kill();
console.log('killing ', id, tanksList[id]);
}
}
eurecaClient.exports.spawnEnemy = function(i, x, y)
{
if (i == myId) return; //this is me
console.log('SPAWN');
var tnk = new Tank(i, game, tank);
tanksList[i] = tnk;
}
}
위의 예제에서 setId, kill 및 spawnEnemy와 같이 서버 측에서 호출 할 수있는 세 가지 메소드가 있습니다.
클라이언트가 원격 서버 기능 : eurecaServer.handshake ()를 호출 중임을 참고하십시오.
서버 측
클라이언트 메소드 (setId, kill 및 spawnEnemy)가 신뢰할 수있는 클라이언트 함수라는 것을 Eureca.io에게 알려주는 첫 번째 사항은 그렇지 않습니다. 그렇지 않으면 eureca.io는 클라이언트 / 서버 개발에서 클라이언트 메소드를 호출하지 않으므로 클라이언트를 맹목적으로 신뢰해서는 안됩니다. 다음 코드는 Eureca.io에게 이러한 메소드를 신뢰하고 클라이언트 데이터를 보유 할 clientList 객체를 생성하도록 지시합니다.
var eurecaServer = new EurecaServer({allow:['setId', 'spawnEnemy', 'kill']});
var clients = {};
이제 onConnect 및 onDisconnect 메소드를 수정 해 봅시다.
//detect client connection
eurecaServer.onConnect(function (conn) {
console.log('New Client id=%s ', conn.id, conn.remoteAddress);
//the getClient method provide a proxy allowing us to call remote client functions
var remote = eurecaServer.getClient(conn.id);
//register the client
clients[conn.id] = {id:conn.id, remote:remote}
//here we call setId (defined in the client side)
remote.setId(conn.id);
});
//detect client disconnection
eurecaServer.onDisconnect(function (conn) {
console.log('Client disconnected ', conn.id);
var removeId = clients[conn.id].id;
delete clients[conn.id];
for (var c in clients)
{
var remote = clients[c].remote;
//here we call kill() method defined in the client side
remote.kill(conn.id);
}
});
서버가 원격 클라이언트 기능을 호출하는 방법에 유의하십시오 : remote.setId (conn.id) 및 remote.kill (conn.id);
기억한다면 클라이언트도 서버 측 메서드를 호출합니다. 여기에 선언하는 방법이 있습니다.
eurecaServer.exports.handshake = function()
{
//var conn = this.connection;
for (var c in clients)
{
var remote = clients[c].remote;
for (var cc in clients)
{
remote.spawnEnemy(clients[cc].id, 0, 0);
}
}
}
이제 서버를 시작하고 http : // localhost : 8000에서 첫 번째 브라우저 창을 열고, 탱크를 조금 옮기고 http : // localhost : 8000 /에서 다른 브라우저 창을 엽니다.
첫 번째 창에서 탱크가 생성되는 것을 볼 수 있습니다. 마지막 창을 닫으면 탱크가 사라집니다. 이것은 꽤 좋았지만 여전히 멀티 플레이어 게임은 아닙니다. 탱크 운동은 아직 재결합되지 않았고, 이것은 우리가 다음 단계에서 할 것입니다. 그건 그렇고, 여기에 위의 단계 😀의 전체 코드입니다
입력 처리 / 상태 동기화
멀티 플레이어 게임에서 서버는 클라이언트 상태를 제어해야 하며 유일한 신뢰할 수있는 엔터티는 서버입니다. (P2P 게임과 같은 몇 가지 다른 변종이 있지만 여기에서는 설명하지 않겠습니다 🙂)
클라이언트 / 서버 게임의 이상적인 구현은 클라이언트와 서버가 모두 동작을 시뮬레이션하면서 서버가 클라이언트에 상태 데이터를 보냅니다.
현지 지위를 수정 / 보완 할 것입니다. 이 예에서는 최소한의 정보 만 동기화 할것입니다.
플레이어가 입력 (이동 또는 공격)을 하면 지역 코드로 직접 처리되지 않습니다.
대신 서버에 보내면 서버는 연결된 모든 클라이언트에서 처리하여 다시 클라이언트 입력을 보냅니다.
각 클라이언트는 이 입력을 탱크의 클라이언트 측 사본에 적용합니다.
탱크는 로컬 입력에 의해 발행 된대로 서버가 보낸 입력을 처리합니다.
이 외에도 입력 정보가 전송 될 때마다 탱크 위치에 대한 정보를 보내고, 이 정보는 탱크 상태를 연결된 모든 클라이언트와 동기화하는 데 사용됩니다. 이것을 처리 할 코드를 작성해 보겠습니다.
eurecaClient.exports.updateState = function(id, state)
{
if (tanksList[id]) {
tanksList[id].cursor = state;
tanksList[id].tank.x = state.x;
tanksList[id].tank.y = state.y;
tanksList[id].tank.angle = state.angle;
tanksList[id].turret.rotation = state.rot;
tanksList[id].update();
}
}
중요포인트 : exports 메소드를 서버에서 호출 할 수 있습니다. updateState 메서드는 공유 플레이어 입력으로 Tank.cursor를 업데이트하지만 탱크 위치와 각도도 수정합니다. 이제 우리는 Tank.update 메소드에서 이것을 처리 할 필요가 있습니다. Tank.prototype.update를 편집하고 다음 라인을 대체하십시오
for (var i in this.input) this.cursor[i] = this.input[i];
이 코드와 함께
var inputChanged = (
this.cursor.left != this.input.left ||
this.cursor.right != this.input.right ||
this.cursor.up != this.input.up ||
this.cursor.fire != this.input.fire
);
if (inputChanged)
{
//Handle input change here
//send new values to the server
if (this.tank.id == myId)
{
// send latest valid state to the server
this.input.x = this.tank.x;
this.input.y = this.tank.y;
this.input.angle = this.tank.angle;
this.input.rot = this.turret.rotation;
eurecaServer.handleKeys(this.input);
}
}
여기에서 로컬 플레이어가 입력 (마우스 클릭 또는 키보드 왼쪽 / 오른쪽 / 위로)을 입력 한 경우 서버에서 직접 처리하는 대신 eurecaServer.handle을 사용하여 서버 쪽 handleKeys 메서드가 입력을 다시 전송하여 모든 연결된 클라이언트에게 알려줍니다.
서버 측
먼저 새로 선언 된 클라이언트 메소드 (updateState)를 허용해야합니다.
var eurecaServer = new EurecaServer({allow:['setId', 'spawnEnemy', 'kill', 'updateState']});
그런 다음 handleKeys 메소드를 선언합니다.
eurecaServer.exports.handleKeys = function (keys) {
var conn = this.connection;
var updatedClient = clients[conn.id];
for (var c in clients)
{
var remote = clients[c].remote;
remote.updateState(updatedClient.id, keys);
//keep last known state so we can send it to new connected clients
clients[c].laststate = keys;
}
}
그리고 기존의 핸드 쉐이크 방식에 대에 약간 수정합니다.
eurecaServer.exports.handshake = function()
{
for (var c in clients)
{
var remote = clients[c].remote;
for (var cc in clients)
{
//send latest known position
var x = clients[cc].laststate ? clients[cc].laststate.x: 0;
var y = clients[cc].laststate ? clients[cc].laststate.y: 0;
remote.spawnEnemy(clients[cc].id, x, y);
}
}
}
모든 단계를 따르거나 (link 링크에서 최종 코드를 다운로드 한 경우) 서버를 시작하고 두 개 이상의 창을 엽니다. 이제 한 클라이언트에서 탱크를 움직이거나 발사체를 발사하면 다른 창으로 이동합니다.
다음은?
이제 기본 코드와 멀티 플레이어 게임 개념을 갖게되었습니다.
이 튜토리얼을 공유하고 싶다면 물론 코멘트와 제안을 환영합니다.
'Phaser JS' 카테고리의 다른 글
Phaser 프레임 워크를 사용하여 'Gravity Quest'를 어떻게 개발 했나요? - Part 1 (0) | 2017.07.16 |
---|---|
Phaser로 HTML5 물리엔진 기반 게임을 만드는 법 (0) | 2017.07.16 |
Phaser 로 "2048"게임을 만드는 방법 (0) | 2017.07.11 |
Phaser Js로 Roguelike 만드는 법 (0) | 2017.07.10 |
Phaser Js 로 Runner 게임 만들기 Part - 3 (0) | 2017.07.10 |