Phaser, NodeJS 및 Ethereum-Pt를 사용하여 멀티 플레이어 블록 체인 게임 만들기. 2

이 프로젝트의 첫 번째 단계는 서버입니다. 아주 간단한 노드 환경을 설정하고 planck-js, socket.io 및 web3을 설치했습니다.

첫 번째 단계는 Imports 를 초기화 하는 것입니다.




var pl = require('planck-js'); var Vec2 = pl.Vec2
var server = require('http').createServer();
var io = require('socket.io')(server);
var Web3 = require('web3');
step1.js hosted with ❤ by GitHub
이것은 우리에게 중요한 플랙 변수 (우리의 물리 엔진), 우리의 서버, websocket.io(socket.io) 및 Web3(Ethereum)에 대한 액세스를 제공합니다.

이제 우리는 물리 엔진을 설정하고 Web3을 초기화하고 클라이언트를 추적 할 수있는 변수를 만들어야합니다.


// Initialize Web3 var web3 = new Web3(new Web3.providers.HttpProvider('https://ropsten.infura.io/'));
// The time step for the game loop and physics
var FIXED_TIME_STEP = 1/20
// Create a physics world, where bodies and constraints live
var world = new pl.World({
gravity : Vec2(0, 10)
});
// Create an infinite ground plane body
var ground = world.createBody();
ground.createFixture(pl.Edge(Vec2(0, 400), Vec2(500, 400)));
// Default stats for players
var DEFAULT_STATS = {
maxVel: 20
}
// Object to hold client information
var clients = {}
step2.js hosted with ❤ by GitHub

먼저 Ropsten testnet을 사용하여 Web3을 초기화합니다. 이것으로 실제로 Ethereum을 쓰지 않고 테스트 할 수 있습니다.

그런 다음 서버에 고정 된 시간 단계를 설정하고 물리 엔진을 초기화합니다. 

중력은 10으로 설정되지만 Planck의 기본값은 -9입니다. 우리는 Phaser와 함께 훌륭하게 연주하려면 긍정적 이어야 합니다.

우리는 클라이언트 정보를 보유 할 수있는 객체를 생성합니다.. 

이것은 연결 ID (socket.io에 의해 우리에게 주어진)를 Key로, 클라이언트 객체를 Value 로 사용하는 Map으로 작동합니다. 

따라서 특정 클라이언트와 상호 작용하는 것이 훨씬 쉬워집니다.

이제 게임에서 몇몇 플레이어를 얻고 Web3를 설정하십시오!

// Listen for connections io.on('connection', function(client){
// Let us know someone connected
console.log("Client connected!")
// When a player sends the join message
client.on('join', function(data) {
// TODO 1: Do something about a player joining
})
// TODO 2: Listen for more packets
});
part3.js hosted with ❤ by GitHub

누군가가 우리 서버에 연결할 때 io.on ( 'connection')이 호출됩니다. 

일단 접속 이벤트가 발생하면 접속된 클라이언트와 관련된 특정 패킷에 대해 호출하는 함수를 설정할 수 있습니다. 

이 중 첫 번째 메시지는 Join 메시지입니다.

플레이어가 조인 할 때 클라이언트 객체에 정보를 추가하기만 하면됩니다. 

이제 플레이어가 단순히 연결될 때, 이 모든 작업을 수행 할 수 있지만 기다려야 할 것이 있습니다. 

특히 우리는 접속된 클라이언트가 유효한 이더리움 (ethereum) 계정을 갖고 있는지 확인하고, 

실제로 계정 정보를 보내어 게임에 참여하도록 해야 합니다.



클라이언트가 Join 메시지를 보낼 때

나머지는 TODO 1이 있는 곳으로 갑니다.

먼저, 플레이어의 상태 및 물리 엔진을 설정할 수 있습니다.

// Set the player's stats client.stats = DEFAULT_STATS
// Setup client body
client.body = world.createBody({fixedRotation: true}).setDynamic();
client.body.createFixture(pl.Box(1, 1), {friction: 2});
client.body.setPosition(Vec2(50, 50));
client.body.setMassData({
mass : 2,
center : Vec2(),
I : 1
})
statsAndPhysics.js hosted with ❤ by GitHub

일단 클라이언트가 Join 하면 우리는 위에서 설정 한 기본값으로 상태를 설정하고 (아직 스마트 계약을 설정하지 않았으므로) ,

새로운 물리 엔진을 설정합니다.

// Setup eth account info client.ethAccount = data.ethAccount
// test balance
web3.eth.getBalance(client.ethAccount).then( balance => {
var value = web3.utils.fromDecimal(balance);
var balanceInEther = web3.utils.fromWei(value, "ether")
client.balance = balanceInEther
})
ethBalance.js hosted with ❤ by GitHub

클라이언트가 Ethereum 계좌 주소를 보내면 속성으로 추가합니다.

우리의 Web3 기능성을 테스트하기 위해 계좌잔액을 속성으로 추가합니다. 나중에 우리는 클라이언트에게 보내어 표시 할 수 있습니다.

// add client to clients object clients[client.id] = client

// broadcast connection to players
client.broadcast.emit('playerConnected', {id: client.id});
addAndBroadcast.js hosted with ❤ by GitHub

접속된 클라이언트는 클라이언트 객체에 추가 한 다음, 다른 모든 클라이언트에 브로드 캐스팅합니다. 

그러면 다른 클라이언트에게 방금 접속된 플레이어가 표시됩니다.

client.broadcast.emit를 호출하면 socket.io는 현재 접속한 클라이언트를 제외하고 연결된 모든 클라이언트에게 메시지를 보냅니다.


다음 단계

다음 단계는 TODO 2가 있는 곳으로 갑니다.


입력

// add listener to input client.on('input', function(data){
clients[client.id].input = data
});
input.js hosted with ❤ by GitHub

이렇게하면 입력 패킷에 대한 데이터 처리가 추가됩니다. 이것은 각 방향 키 (WASD)에 대한 Bool 객체입니다. 

클라이언트는 입력이 변경 될 때만이 패킷을 보냅니다.  

이를 통해 우리는 매 20 분의 1 초마다 플레이어로부터 어떤 키가 눌려 있는지 추적 할 수 있습니다.


플레이어 상태 요청

// add listener to player state request client.on('playerStateRequest', function(data){
client.emit('requestedPlayerState', {id: client.id, state: {x: client.body.getPosition().x, y: client.body.getPosition().y}})
});
playerStateRequest.js hosted with ❤ by GitHub

일부 플레이어는 다른 플레이어 보다 나중에 접속하게 됩니다.  

우리는 플레이어의 위치 같은 것을 업데이트하기 위해 플레이어의 전체 상태를 끊임없이 보내고 싶지는 않습니다. 

클라이언트가 연결되면 다른 플레이어가 연결되어 있음을 볼 수 있지만 정보가 없습니다. 

클라이언트는 정보가없는 플레이어를 감지하면 playerStateRequest 패킷을 보냅니다.

이렇게 하면 해당 플레이어의 상태가 클라이언트에 전송되어 최신 상태가 될 수 있습니다.



플레이어 접속 해제

// add listener to disconnect client.on('disconnect', function(){
// broadcast player disconnect
client.broadcast.emit('playerDisconnected', {id: client.id});
// safely remove player
world.destroyBody(client.body)
delete clients[client.id]
console.log("Client disconnected!")
});
disconnect.js hosted with ❤ by GitHub

플레이어가 연결을 끊으면 몇 가지 작업을 수행해야합니다. 먼저, 다른 모든 플레이어에게 연결이 끊어 졌음을 알릴 필요가 있습니다. 

이것은 클라이언트에 의해 일종의 타임 아웃을 추가하여 추측 할 수 있었습니다 (플레이어가 잠시 동안 업데이트되지 않았을 수도 있음). 

플레이어가 잠시 동안 메시지를받지 못하면 단순히 플레이어를 삭제하지 말고 각 클라이언트의 접속유지 신호(Heart Beat)를 확인후 처리해 합니다.

이것은 UDP를 사용할 때 분명히 유용하지만 WebSocket (본질적으로 TCP)에서는 필요 없습니다.

우리가 보내는 모든 메시지는 믿을만 하므로 모든 플레이어를 신뢰할 수있게 만듭니다!

역자주) WebSocket 에서는 Keep Alive, Heart Beat 가 필요 없을 만큼 신뢰한다는 내용인것 같습니다. 

          그래도 모바일 환경에서 처럼 잦은 접속 변경 및 끊김에서는 신뢰 하기 힘들겠죠?



게임 루프

이 코드는 이제 io.on ( 'connection') 함수 외부에 있습니다.

function gameLoop() { // Add horizontal spring force
var clientIds = Object.keys(clients)
for (i = 0; i < clientIds.length; i++) {
// If we are trying to access a client who's recently been removed, do nothing
if (!clients[clientIds[i]] || !clients[clientIds[i]].body) {
continue
}
// Get the client and their input
var client = clients[clientIds[i]]
var { input } = client
// Update client with input
if (input.w) {
var f = client.body.getWorldVector(Vec2(0, -10));
var p = client.body.getWorldPoint(Vec2(0.0, 0.0));
client.body.applyLinearImpulse(f, p, true); }
if (input.a) {
var f = client.body.getWorldVector(Vec2(-0.5, 0));
var p = client.body.getWorldPoint(Vec2(0.0, 0.0));
client.body.applyLinearImpulse(f, p, true);
}
if (input.d) {
var f = client.body.getWorldVector(Vec2(0.5, 0));
var p = client.body.getWorldPoint(Vec2(0.0, 0.0));
client.body.applyLinearImpulse(f, p, true);
}
// Limit the players velocity
var linVel = client.body.getLinearVelocity()
if (Math.abs(linVel.x) > client.stats.maxVel) {
linVel.x = 10 * Math.sign(linVel.x)
}
if (Math.abs(linVel.y) > client.stats.maxVel) {
linVel.y = 10 * Math.sign(linVel.y)
}
// Emit the updated player's state to everyone
io.sockets.emit('updatePlayerState', {id: client.id, state: {...client.body.getPosition(), balance: client.balance} });
}
// Move physics forward in time
world.step(FIXED_TIME_STEP/2);
}
gameLoop.js hosted with ❤ by GitHub


이것이 우리 서버의 강점입니다. 1/20 초마다 실행됩니다 (FIXED_TIME_STEP). 

그것은 아주 간단합니다. 그래서 나는 단지 그것을 간단히 보겠습니다.


Lines 4-5 번 라인 모든 클라이언트 객체의 모든 키를 얻고 루프를 반복합니다.

Lines 7-9 클라이언트가 시간 내에 잠재적으로 제거되었을 수 있으므로  클라이언트가 여전히 존재하는지 확인합니다.

Lines 14-27 현재의 입력 상태에 따라 플레이어 물리 엔진을 조정합니다.

Lines 30-36 모든 플레이어가 플래시가 아닌지 확인하기 위해 속도를 제한합니다.

Lines 39 모든 클라이언트에게 새로운 상태를 전송합니다.  여기에는 우리가 방금 사용했던 클라이언트도 포함됩니다. 
           우리는 클라이언트의 속도를 높여야하기 때문입니다!

Line 42 물리엔진의 속도를 절반으로 줄여 게임을 조금 느리게 했다. 이 부분은 단지 환경 설정입니다.



마지막


setInterval(gameLoop, FIXED_TIME_STEP); server.listen(2217);
finalTouches.js hosted with ❤ by GitHub

우리가 작성한 모든 작업은 매 1/20 초마다 gameLoop 함수를 호출하도록 만든 다음 서버를 시작합니다!


결론

이제 우리는 Web3를 사용하여 Ethereum에 연결된 게임 서버를 갖게 되었습니다!

Part 3에서는 클라이언트를 설정하고 게임을 업로드하여 실제로 유저가 사용하는 방법을 보여줍니다.

댓글 없음:

댓글 쓰기