HTML5GAME :: HTML5GAME

달력

82025  이전 다음

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

게임 플레이 설계 및 구현


이제 게임의 게임 플레이 디자인과 구현에 대한 통찰력을 얻고 자합니다.

중력 퀘스트 (Gravity Quest)에서는 우주 비행사의 역할을 맡아 우주에서 미네랄 탐침을 수집하는 동안 이상한 사고 때문에 그녀의 우주 왕복선에서 분리됩니다. 생존을 위해, 플레이어는 25 레벨의 모든 우주 비행후 결국 지구로 돌려 보낼 블랙홀로 이동해야합니다. 

플레이어가 도달하려면 (어떤 끌림) 중력총을 이용해야 합니다. 그것을 발사하는 동안, 우주 비행사는 가능한 가장 가까운 소행성으로 가속합니다. 

따라서 타르 잔은 소행성 주변을 돌아 다니면서 검은 색 전체에 도달하거나, 치명적인 물체와 충돌하거나, 우주로 너무 멀리 떠돌아 다니다가 잃어 버리게됩니다. 다음은 설명 된 게임 플레이의 몇 가지 예를 보여줍니다.


 


아이디어 반복하기


앞서 설명한 게임의 최종 개념을 설정하기 전에 다른 디자인으로 몇 차례 반복했습니다. 예를 들어 이전의 아이디어 중 하나는 플레이어는 검은 색 전체를 제어했습니다. 목표는 플레이어가 블랙홀 쪽으로 항상 떠 다니는 다양한 개체 (행성, 우주선 등)를 흡수하여 적의 블랙홀이 흡수하기 전에 수행하는 것입니다. 

이 적들을 추방하기 위해 플레이어는 스크린 주변에서 블랙홀을 능숙하게 움직여야했습니다. 그러나 이 개념의 프로토 타입을 작성함으로써, 나는 플레이어가 블랙홀을 재배치하기 위해 화면에서 게임 플레이가 손가락을 계속 움직여야 하고, 결과적으로 불만족 스러웠다는 것을 빨리 알게되었습니다. 여기서 중요한 점이 게임 플레이에 대한 테스트를 가능한 한 빨리하면 쉽게 아이디어를 수정하거나 완전히 정리 할 수 있다는 사실을 쉽게 알 수 있습니다.



개념 구현하기


다음에서는 필자가 Phaser 프레임 워크로 게임 플레이를 구현 한 방법을 설명해 보겠습니다 (참고 : 필자는 이 기사 작성 시점 인 최신 버전 2.0.7을 사용하고 있습니다). 저는 우주 비행사가 레벨을 이동하기 위해 사용자 입력시 소행성과 어떻게 상호 작용하는지에 대해 집중적으로 다룰 것입니다. 실행 가능한 데모를 정의하는 데 필요한 모든 부분을 제시 하겠지만 Phaser 프레임 워크를 사용하는 모든 측면에 대해서는 자세히 설명하지 않겠습니다. 관심있는 독자는 토마스 팔레 프 (Thomas Palef)의 광범위한 책 디스 카이저 (Phaser)와 같은 더 포괄적 인 소개를 살펴 봐야합니다.


기초로서 Phaser는 게임 개체의 초기화가 필요합니다. 이 작업을 수행하는 기능은 게임의 의도 된 너비와 높이 (예 : 640 및 480 픽셀)와 렌더링 방법 (Canvas 또는 WebGL)을 정의하고, Phaser.AUTO를 사용하면 Phaser가 가장 적절한 렌더링 방법을 선택할 수 있습니다. 추가 매개 변수를 사용하여 게임의 캔버스가 삽입되는 DOM 요소 (예 : div)를 선택할 수 있습니다.이 경우 id는 'game'입니다.

var game = new Phaser.Game(640, 480, Phaser.AUTO, 'game');

Listing 1 : 페이저 게임 객체를 인스턴스화한다.


Phaser의 게임은 적어도 하나의 State로 구성됩니다. 게임 상태는 일반적으로 고유 한 기능 집합을 나타냅니다. 예를 들어, 한 State에서는 게임 메뉴를 담당하고 다른 State에서는 실제 게임 플레이를 담당합니다. 각 State는 일반적으로 목록 2에 설명 된 preload, create 및 update 세 가지 기능으로 구성됩니다.

var mainState = {
 preload: function() {
  // called once upon instantiation of the state
  // typically used to load required assets
 },
 create: function() {
  // called once after the preload function
  // typically used to set up the game
 },
 update: function() {
  // called 60 times per second
  // typically used to react to user input, define game logic etc.
 }
}

Listing 2 : 상태 정의.


(이 경우) State만 정의하면 게임에 추가 할 수 있으며 목록 3과 같이 즉시 시작될 수 있습니다.


...
game.state.add('main', mainState);
game.state.start('main');

목록 3 : 게임에 상태를 추가하고 시작.


이 기본 설정을 하였으면, 목록 4에 표시된 것처럼 preload 함수 내에서 필요한 스프라이트를 로드 할 수 있습니다. 이 경우 우주 비행사가 소행성에 가장 가까운 소행성에 발사 할수있는 우주 비행사, 소행성 및 중력선에 대한 스프라이트를 로드합니다. 

...
game.load.image('astronaut', 'assets/astronaut.png');
game.load.image('asteroid', 'assets/asteroid.png');
game.load.image('gravityRay', 'assets/gravityray.png');
...

Listing 4 : preload 함수에서 Asset로드하기.


create 함수에서 먼저, Phaser의 아케이드 물리 시스템 (후자의 물리 시스템에 대해 자세히 설명)을 목록 5와 같이 활성화 합니다. 게임 세계에서 스프라이트에 body를 할당 할 수 있습니다. 이 body에서 중력이나 가속과 같은 효과를 적용하고 충돌을 검사 할 수 있습니다.

...
game.physics.startSystem(Phaser.Physics.ARCADE);
...

목록 5 : 아케이드 물리 시스템 활성화.


다음으로, create 함수에서 중력선, 소행성 및 우주 비행사가 미리로드 된 Asset을 사용하여 게임에 배치합니다. 목록 6에서 볼 수 있듯이, 중력선은 게임 월드의 왼쪽 상단 모퉁이에 위치하고 있습니다. 이 위치는 나중에 중력 광선이 시작되는 우주 비행사의 위치와 관련하여 조정됩니다. 중력 광선의 앵커는 수직 중심으로 설정됩니다. 광선은 사용자 입력시에만 표시되어야 하므로 중력 광선의 가시성을 false로 설정합니다.

...
// create gravity ray between astronaut and closest asteroid:
this.gravityRay = game.add.sprite(0, 0, 'gravityray');
this.gravityRay.anchor.setTo(0, 0.5);
this.gravityRay.visible = false;
...

목록 6 : 중력 광선을 게임에 배치.


이 데모에서는 목록 7처럼 7 개의 소행성이 게임에 추가됩니다. 모든 소행성은 Phaser의 rnd.integerInRange를 사용하여 게임 세계에서 무작위로 배치됩니다. 잠재적 인 소행성 위치에서 100 픽셀의 경계가 제외됩니다. (실제 게임에서 모든 소행성의 위치는 모든 레벨에 대해 수동으로 정의됩니다.) 또한 모든 소행성 앵커는 중심에 놓여 우주 비행사가 나중에 소행성의 왼쪽 위 모퉁이로가 아니라 중심쪽으로 가속하게 합니다. 또한 아케이드 물리 시스템은 모든 소행성에 대해 활성화합니다. 소행성은 그 다음 배치 처리를 위해 소행성을 결합한 사전 정의 된 소행성 그룹에 추가됩니다.

...
// (randomly) place asteroids and add to group:
this.asteroids = game.add.group();
for (var i = 0; i < 7; i++) {
 var asteroid = game.add.sprite(
  game.rnd.integerInRange(100, game.world.width - 100),
  game.rnd.integerInRange(100, game.world.height - 100),
  'asteroid');
 asteroid.anchor.setTo(0.5, 0.5);
 game.physics.enable(asteroid, Phaser.Physics.ARCADE);
 this.asteroids.add(asteroid);
};
...

목록 7 : (무작위로) 소행성을 게임에 배치.


마지막으로, create 함수에서 우주 비행사는 목록 8에 표시된대로 게임 세계의 중심에 위치합니다. 모든 소행성의 경우와 마찬가지로, 물리엔진은 활성화됩니다.

...
// create astronaut at the center of the world:
this.astronaut = game.add.sprite(game.world.width * 0.5, game.world.height * 0.5, 'astronaut');
this.astronaut.anchor.setTo(0.5, 0.5);
game.physics.enable(this.astronaut, Phaser.Physics.ARCADE);
...

Listing 8 : 우주 비행사를 세계에 배치.


다음에서는 create 함수에서 게임의 Scene을 설정한 후, update 함수에서 플레이어 컨트롤을 활성화하는 데 필요한 로직으로 작성하겠습니다.


먼저, 플레이어에게 가장 가까운 소행성과 그 거리가 목록 9에 표시 된 바와 같이 결정됩니다. 초기 거리는 가능한 가장 높은 값으로 설정됩니다. 모든 소행성에 대해 우주 비행사와의 거리는 아케이드 물리시스템 인 distanceBetween에서 사용할 수있는 도우미 함수를 사용하여 계산됩니다. distanceBetween은 2 개의 아케이드 물리 객체를 입력하면 앵커 포인트 사이의 거리를 반환합니다. 결정된 거리가 현재 가장 작은 거리보다 작은 경우이 거리가 저장되고 해당 소행성이 가장 가까운 거리로 설정됩니다.

...
var distance = Number.MAX_VALUE;
var closestAsteroid;
this.asteroids.forEach(function(ast){
 var tempDistance = this.distanceBetween(ast, this.astronaut);
 if(tempDistance < distance){
  distance = tempDistance;
  closestAsteroid = ast;
 }
}, this);
...

목록 9 : 우주 비행사에게 가장 가까운 소행성과 그 거리를 결정.


다음으로, 목록 10에 표시된 바와 같이, 결정된 최소 거리가 특정 임계 값보다 250 아래에 있는지 검사합니다. 이때 우주 비행사가 너무 멀리 떨어져 나가고 게임이 종료되고 리스타트 하게 됩니다.

if(distance > 250){
 game.state.start('main');
}
...

목록 10 : 우주 비행사가 지나치게 멀리 떨어진 경우 게임을 다시 시작.


다음으로, 업데이트 기능 내에서, 목록 11의 코드 내용처럼 사용자 입력를 행하는지를 체크합니다. Gravity Quest에서, 사용자 입력은 포인터 (컴퓨터상의 마우스 또는 터치 장치상의 손가락)를 누르고 있는것으로 판단합니다. 사용자 입력이 탐지되면, 우주 비행사를 가장 가까운 소행성으로 가속시키는 힘이 결정됩니다. 이 경우, 우주 비행사가 가장 가까운 소행성과 얼마나 가까운 지에 따라 가속도가  30 이상으로 설정됩니다. 중력 총을 더 강하게 만들어서 그것이 발사되는 대상으로 가깝게 만드는 것입니다. 플레이어는 아케이드 물리 시스템의 accelerateToObject 기능을 사용하여 계산 된 힘으로 가장 가까운 소행성으로 가속됩니다. 또한 우주 비행사는 아케이드 물리 시스템의 angleBetween 기능을 사용하여 가장 가까운 소행성을 향해 회전합니다. 우주 비행사의 앵커가 의도 한 회전을 받기 위해 중심에 놓이는 것이 중요합니다 (목록 8 참조).

...
if(game.input.activePointer.isDown){
 var force = Math.min(30, 30 * (1 - distance / 250));
 game.physics.arcade.accelerateToObject(this.astronaut, closestAsteroid, force);

 this.astronaut.rotation = game.physics.arcade.angleBetween(this.astronaut, closestAsteroid);
}
...

목록 11 : 사용자 입력에 따라 우주 비행사를 가장 가까운 소행성으로 가속.


우주 비행사의 가속에 이어 중력총의 발사가 시각화 되어야합니다. 이를 처리하기 위해 중력선을 목록 12와 같이 사용자 입력시 보이도록 표시하고, 크기를 조정하고 회전합니다. 먼저 중력 광선을 표시합니다 (목록 6에서는 처음에는 보이지 않도록 설정). 그런 다음, 그 위치는 광선이 원점 인 우주 비행사의 위치와 일치합니다. 중력 광선의 회전은 우주 비행사와 마찬가지로 angleBetween 함수를 사용하여 설정됩니다. 중력선의 너비는 우주 비행사와 가장 가까운 소행성에서이 소행성 반경을 뺀 거리로 설정됩니다. 이렇게하면 우주선이 중심이 아닌 소행성의 표면에서 끝나도록 할 수 있습니다. 마지막으로, 중력선의 높이는 우주 비행사가 가장 가까운 소행성까지의 거리를 고려하여 설정됩니다. 여기에서의 의도는 거리가 멀면 광선을 작게 만들어 우주 비행사가 가속하는 힘을 낮추는 것입니다.

...
if(game.input.activePointer.isDown){
 ...
 this.gravityRay.visible = true;
 this.gravityRay.x = this.astronaut.x;
 this.gravityRay.y = this.astronaut.y;
 this.gravityRay.rotation = game.physics.arcade.angleBetween(this.astronaut, closestAsteroid);
 this.gravityRay.width = distance - closestAsteroid.width * 0.5;
 this.gravityRay.height = Math.min(15, 15 * (1 - distance / 250));
}
...

목록 12 : 우주 비행사가 가장 가까운 소행성으로 중력 광선을 렌더링.


마지막으로, 우주 비행사의 가속을 중지하고 중력 광선을 사용자 입력단에 한 번 숨겨야 할 필요가 있습니다 (목록 13 참조).

...
if(game.input.activePointer.isDown){
 ...
} else {
 game.physics.arcade.accelerateToObject(this.astronaut, closestAsteroid, 0);
 this.gravityRay.visible = false;
}
...

목록 13 : 사용자 입력이 멈출 때 중력선을 제거하고 가속을 중지.



데모

위에서 설명한 내용을 정리후, 아래의 버튼을 눌러 데모를 재생하십시오. 데모에서는 우주 비행사가 가장 가까운 소행성을 향해 가속화 할 수 있도록 아무 곳이나 누르십시오.



Posted by 마스터킹
|

프로토 타입은 One Tap RPG 게임을 기반으로 정했습니다.




게임이 맘에 들지 않았지만 물리엔진으로 구동되는 작은 Phaser 프로토 타입을 개발했습니다.


게임은 간단하지만 다음과 같은 흥미로운 기능을 포함합니다.


* P2 물리학

* 중력과 배상

* 연락 청취자


* 정적 및 동적 몸체


다음은 완전히 주석 처리 된 소스 코드입니다. 이해하기 쉽고 개선하기 쉽습니다.

window.onload = function() {

var game = new Phaser.Game(320,480,Phaser.CANVAS,"",{preload:onPreload, create:onCreate, update:onUpdate});

     // the hero!!

var hero;

// an array which will contain all game items. Used just to be sure items will be placed

// not so close to each others

     var gameItems=[];

     // game text displaying floor,level, lives and so on

     var gameText = "";

     // group which will contain all level assets

     var levelGroup;

   

     // starting level

     var level=1;

     // starting experience

     var exp=0;

     // starting lives

     var lives=3;

     // starting floor

     var floor=1;

     // starting gold

     var gold=0;

     function onPreload() {

      // preloading images

game.load.image("skull","assets/skull.png");

game.load.image("coin","assets/coin.png");

game.load.image("hero","assets/hero.png");

}

// going fullscreen

function goFullScreen(){

          game.scale.pageAlignHorizontally = true;

      game.scale.pageAlignVertically = true;

      game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;

game.scale.setScreenSize();

     }

function onCreate() {

goFullScreen();

// dark grey background color

game.stage.backgroundColor = '#2d2d2d';

// starting P2 physics system

game.physics.startSystem(Phaser.Physics.P2JS);

// setting a soft gravity...

game.physics.p2.gravity.y = 200;

// ... but a high restitution

game.physics.p2.restitution = 0.9;

// level creation

createLevel();

// listener for input down, to call addHero when triggered

game.input.onDown.add(addHero, this);

// setting physics world boundaries, only left and right (the first two "true")

game.physics.p2.setBoundsToWorld(true, true, false, false, false);

// adding the HUD text

gameText = game.add.text(0,0, "",{font:"normal 12px arial",fill: "#ffffff"})

// updating the text

updateText();

}

function createLevel(){

// level creation is very easy, first we add a group

levelGroup = game.add.group();

// the array of items starts as an empty array

gameItems = [];

// placing 18 items per level

for(var i=0;i<18;i++){

// skulls will be placed more and more often

if(Math.random()<0.6+(level/1000)){

gameItems[i] = game.add.sprite(0,0,"skull");

gameItems[i].name = "skull";

}

// adding coins

else{

gameItems[i] = game.add.sprite(0,0,"coin");

gameItems[i].name = "coin";

}

// adding the last created item to levelgroup

levelGroup.add(gameItems[i]);

// setting its registration point in the middle

gameItems[i].anchor.setTo(0.5,0.5);

// keep moving the item until id does not ovelap with other items

do{

gameItems[i].x = Math.random()*300+10;

gameItems[i].y = Math.random()*360+100;

} while(itemsOverlap());

// enabling item to react to physics

game.physics.p2.enable(gameItems[i]);

// setting it as a 24 pixels radius circle

gameItems[i].body.setCircle(24);

// setting items as static

gameItems[i].body.static=true;

}

}

function addHero(){

// once the hero is added, remove the listener

game.input.onDown.remove(addHero, this);

// placing hero sprite x = horizontal coordinate of your input, y = outside the stage at the top

hero = game.add.sprite(game.input.worldX,-50,"hero");

// adding the hero to the group

levelGroup.add(hero);

// enabling the hero to react to physics

game.physics.p2.enable(hero);

// listener for hero contacts, heroHit to be called when triggered

hero.body.onBeginContact.add(heroHit, this);

}

function heroHit(body){

// if the hero hits an ACTUAL body (not a world boundary)

if(body){

switch(body.sprite.name){

// if it's a coin, remove the body, update the score

case "coin":

gold+=(level*floor);

body.sprite.kill();

break;

// if it's a skull, remove the body, update experience...

case "skull":

// ... but ONLY if the hero is above the skull

if(hero.y<body.y){

body.sprite.kill();

exp+=1;

if(exp>level*level/2){

level++;

lives++;

exp=0;

}

}

else{

// otherwise decrease the lives and show a bit of "blood"

lives--;

game.stage.backgroundColor = "#ff0000";

}

break;

}

}

else{

// if the hero do not hit an ACTUAL body (that is it hit a world boundary)

// decrease the lives and show a bit of "blood"

lives--;

game.stage.backgroundColor = "#ff0000";

}

if(lives==0){

// no lives = game over

hero.kill();

}

// updating HUD text

updateText();

}

function updateText(){

// just writing a string

gameText.setText("Floor: "+floor+" - Lives: "+lives+" - Level: "+level+" - Exp: "+exp+" - Gold: "+gold);

}

function onUpdate() {

// set background color to dark grey. Should be optimized

game.stage.backgroundColor = "#2d2d2d";

// if the hero is in game...

if(hero){

// and its y position is more than 500 (outside the bottom of the screen)

if(hero.y>500){

// preparing for next level

hero.y=0;

levelGroup.destroy(true);

game.input.onDown.add(addHero, this);

floor++;

lives++;

updateText();

createLevel();

}

}

}

// just a function to see if the latest item overlaps with previous ones

function itemsOverlap(){

for(var i=0;i<gameItems.length-1;i++){

var distance = manhattanDistance(gameItems[i],gameItems[gameItems.length-1]);

if(distance<50){

return true;

}

}

return false;

}

// manhattan distance.

function manhattanDistance(from,to){

return Math.abs(from.x-to.x)+Math.abs(from.y-to.y)

}

};


소스 코드를 다운로드하십시오.





Posted by 마스터킹
|

이 튜토리얼의 목표는 기본 멀티 플레이어 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 링크에서 최종 코드를 다운로드 한 경우) 서버를 시작하고 두 개 이상의 창을 엽니다. 이제 한 클라이언트에서 탱크를 움직이거나 발사체를 발사하면 다른 창으로 이동합니다.


다음은?

이제 기본 코드와 멀티 플레이어 게임 개념을 갖게되었습니다.

이 튜토리얼을 공유하고 싶다면 물론 코멘트와 제안을 환영합니다.

멀티 플레이어 탱크 게임 최종 코드 다운로드

Posted by 마스터킹
|