크리티컬섹션 :: 'Phaser JS' 카테고리의 글 목록

달력

112024  이전 다음

  • 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

MOTO XM POOL PARTY

Phaser JS 2019. 5. 3. 15:33


Moto XM is an ultimate online bike racing game which is very suitable for players who love extreme sports With the different levels in challenges the extremely dangerous but no less attractive high speed the game will blow you away with its exciting features It can help you to have interesting relax time Welcome to the special Pool Party version of this awesome bike stunt game Have fun with Moto XM Pool Party


Moto XM은 익스트림 스포츠를 좋아하는 플레이어에게 매우 적합한 최고의 온라인 자전거 경주 게임입니다. 도전 수준이 다른 여러 레벨에서 극도의 위험은 있지만 매력적이지 않은 고속으로 게임을하면 흥미 진진한 기능이 사라집니다. 재미있는 휴식 시간이 멋진 자전거 스턴트 게임의 특별한 풀 파티 버전에 오신 것을 환영합니다 Moto XM Pool Party

Posted by 마스터킹
|



Play mini games featuring the characters from Dumb Ways to Die. Dumb Ways to Die is a worldwide brand with over 300 million downloads on Apple App Store and Google Play combined. Test your reflexes in this challenging series of mini games, where a milisecond can make the difference between winning and losing. Score coins for each challenge, and use the to repair the once great town of Dumbville.


Dumb Ways from Die의 캐릭터가 등장하는 미니 게임을 즐겨보십시오. Dumb Ways to Die는 Apple App Store 및 Google Play에서 3 억회가 넘는 다운로드를 기록한 세계적 브랜드입니다. 미니 게임의 도전적인 시리즈에서 반사 신경을 테스트하십시오. 밀리 초는 성공과 실패의 차이를 만들 수 있습니다. 각 도전 과제에 대해 점수를 매기고 한 번 위대한 마을 인 덤 빌 (Dumbville)을 수리하십시오.

Posted by 마스터킹
|
Posted by 마스터킹
|

10 년 전에 출시 된 이전 게임 인 Mass Attack이라는 프로토 타입을 만들려고했습니다. 게시물 Mass Attack과 같은 플래시 게임을 만들고 Tween을 사용하여 Mass Attack과 같은 HTML5 게임을 만듭니다 

필자는 Phaser CE v2.8.7 및 Phaser v3.0.0을 사용하여 프로토 타입을 다시 작성했으며 이는 아마도 Phaser 3 작동 첫 번째 프로토 타입 일 것입니다.


2.8.7 버전부터 시작해 보겠습니다.


클릭하여 길게 누르면 구가 생기고, 놓으면 구가 튀어 나오고 그에 따라 움직입니다.


소스 코드는 아직 댓글을 달지 않았습니다. 특히 시리즈의 다른 게시물을 읽는다면 매우 정직합니다.

var game;

 

var gameOptions = {

    maxDiameter: 50,

    ballGrowingSpeed: 0.5,

    balanceFriction: 400

}

 

window.onload = function(){

    game = new Phaser.Game(320, 480, Phaser.CANVAS);

    game.state.add("PlayGame", playGame, true);

}

 

var playGame = function (){};

 

playGame.prototype = {

    preload: function (){

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

        game.scale.pageAlignHorizontally = true;

        game.scale.pageAlignVertically = true;

game.stage.disableVisibilityChange = true;

        game.stage.backgroundColor = 0x222222;

        this.load.image("ball", "ball.png");

        this.load.image("balance", "balance.png");

    },

    create: function (){

        this.growBall = false;

        this.canPlay = true;

        this.balance = [];

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

            this.balance[i] = game.add.group();

            this.balance[i].weight = 0;

            var balanceSprite = game.add.sprite(game.width / 2 * i, 240, "balance");

            balanceSprite.anchor.set(0, 0.5);

            this.balance[i].add(balanceSprite);

        }

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

        game.input.onUp.add(this.dropBall, this);

    },

    placeBall: function(e){

        if(!this.growBall && this.canPlay){

            this.ball = game.add.sprite(e.x, 0, "ball");

            this.ball.anchor.set(0.5);

this.ball.width = 1;

            this.ball.height = 1;

            this.ball.balance = Math.floor(this.ball.x / (game.width / 2));

            this.balance[this.ball.balance].add(this.ball);

            this.ball.y = 30 - this.balance[this.ball.balance].y

     this.growBall = true;

        }

    },

    dropBall: function(){

        if(this.growBall){

            this.growBall = false;

            this.canPlay = false;

            var ballDestination = game.height / 2 - this.balance[this.ball.balance].getChildAt(0).height / 2 - this.ball.height / 2;

            this.balance[this.ball.balance].weight += (4 / 3) * Math.PI * Math.pow((this.ball.width / 2), 3);

            var ballTween = game.add.tween(this.ball).to({

y: ballDestination

}, 2000, Phaser.Easing.Bounce.Out, true);

            ballTween.onComplete.add(this.adjustBalances, this)

        }

    },

    adjustBalances: function(){

        var weightDifference = (this.balance[0].weight - this.balance[1].weight) / gameOptions.balanceFriction;

        var maxDifference = game.height / 3;

        if(weightDifference > maxDifference){

weightDifference =maxDifference;

}

if(weightDifference < -maxDifference){

weightDifference = -maxDifference;

}

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

            var balanceTween = game.add.tween(this.balance[i]).to({

                y: weightDifference - (2 * i * weightDifference)

            }, 2000, Phaser.Easing.Quadratic.Out, true);

            balanceTween.onComplete.add(function(){

                this.canPlay = true;

            }, this);

        }

    },

    update: function(){

        if(this.growBall && this.ball.width < gameOptions.maxDiameter){

            this.ball.width += gameOptions.ballGrowingSpeed;

            this.ball.height += gameOptions.ballGrowingSpeed;

        }

    }

}


Phaser 3 버전은 어떻습니까?



게임이 똑같아 보이더라도 코드를 작동시키기 위해 코드를 다양하게 변경해야했습니다. Phaser 3 버전을 가능한 한 Phaser CE 버전에 가깝게 유지하려고 했으므로 최적화되지 않았으며 API 문서가 거의 없습니다. 어쨌든 여러 가지 차이점을 나열 해 보겠습니다.


GAME CONSTRUCTOR : 생성자에는 이제 단 하나의 인수 만 있습니다.이 인수는 빠른 사용자 정의를 허용하는 객체입니다.


SCALING : 나는 게임을 확장 할 수 없었습니다. 또한 배경색으로 카메라를 사용해야 했습니다. 어쩌면 내가 뭔가를 놓친 것 같습니다.


GAME STATUS : 이제 장면이라고 부르며,이 기능을 거의 사용하지 않았더라도 빠른 설정이 가능합니다.


THIS INSTANCE : 콜백 함수에서 this를 추적 할 수 없어서 _this 변수로 저장해야 했습니다.


INPUT LISTENERS : 이름이 바뀌었지만 모두 똑같은 것처럼 보입니다.


TWEENS : 정의를 편하게 할수 있도록 되었습니다.


GROUP : 그들은 고통 스러웠습니다. 나는 그 (것)들을 움직일 수 없었다, 나는 x 또는 y 위치를 TWEEN 할 수없고, 평형 운동을 창조하기 위하여 많은 workaround 및 어리석은 수학을해야했다. 어쩌면 나는 무엇인가 놓쳤다.


SPRITES : 나는 비행 중에 폭과 높이를 변경할 수 없었기 때문에 크기를 조정하는 대신 스프라이트 크기를 조정해야했습니다.


소스 코드를보고, 약간의 변화를보고 두려워하지 말고, 여기에 새로운 언어를 배우려고 할 때 도움이 될 10 가지 팁이 있습니다.


var game;

var _this;

 

var gameOptions = {

    maxDiameter: 1,

    ballGrowingSpeed: 0.015,

    balanceFriction: 400

}

 

var config = {

    type: Phaser.CANVAS,

    width: 320,

    height: 480

};

 

window.onload = function(){

    game = new Phaser.Game(config);

    game.scene.add("PlayGame", playGame, true);

}

 

var playGame = function (){

    _this = this;

};

 

playGame.prototype = {

    preload: function (){

        var camera = _this.cameras.add(0, 0, game.width, game.height);

        camera.setBackgroundColor("0x222222");

        _this.load.image("ball", "ball.png");

        _this.load.image("balance", "balance.png");

    },

    create: function (){

        _this.growBall = false;

        _this.canPlay = true;

        _this.balance = [];

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

            _this.balance[i] = _this.add.group();

            _this.balance[i].weight = 0;

            _this.balance[i].saveYPosition = 0;

            var balanceSprite = _this.add.sprite(config.width / 2 * i, 240, "balance");

            balanceSprite.setOrigin(0, 0.5);

            _this.balance[i].add(balanceSprite);

        }

        _this.input.events.on("POINTER_DOWN_EVENT", _this.placeBall);

        _this.input.events.on("POINTER_UP_EVENT", _this.dropBall);

    },

    placeBall: function(e){

        if(!_this.growBall && _this.canPlay){

            var side = Math.floor(e.x / (config.width / 2));

            _this.ball = _this.add.sprite(e.x, 30, "ball");

            _this.ball.balance = side;

     _this.ball.scaleX = 0.1;

            _this.ball.scaleY = 0.1;

            _this.balance[_this.ball.balance].add(_this.ball);

     _this.growBall = true;

        }

    },

    dropBall: function(){

        if(_this.growBall){

            _this.growBall = false;

            _this.canPlay = false;

            var ballDestination =  config.height / 2 + _this.balance[_this.ball.balance].saveYPosition - _this.balance[_this.ball.balance].children.entries[0].height / 2 - _this.ball.height * _this.ball.scaleY / 2;

            _this.balance[_this.ball.balance].weight += (4 / 3) * Math.PI * Math.pow((_this.ball.width * _this.ball.scaleX / 2), 3);

            var ballTween = _this.tweens.add({

                targets: _this.ball,

                y: ballDestination,

                duration: 2000,

                ease: "Bounce",

                onComplete: _this.adjustBalances

            });

        }

    },

    adjustBalances: function(){

        var weightDifference = (_this.balance[0].weight - _this.balance[1].weight) / gameOptions.balanceFriction;

        var maxDifference = config.height / 3;

        if(weightDifference > maxDifference){

weightDifference = maxDifference;

}

if(weightDifference < -maxDifference){

weightDifference = -maxDifference;

}

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

            var difference = - _this.balance[i].saveYPosition + weightDifference - (2 * i * weightDifference)

            _this.balance[i].saveYPosition += difference;

            var balanceTween = _this.tweens.add({

                targets: _this.balance[i].children.entries,

                y: "+=" + difference.toString(),

                duration: 2000,

                ease: "Quad",

                onComplete: function(){

                    _this.canPlay = true;

                }

            })

        }

    },

    update: function(){

        if(_this.growBall && _this.ball.scaleX < gameOptions.maxDiameter){

            _this.ball.scaleX += gameOptions.ballGrowingSpeed;

            _this.ball.scaleY += gameOptions.ballGrowingSpeed;

        }

    }

}


여전히 많은 부분이 있지만, 일반적인 "Hello World"보다 훨씬 깁니다. 프로토 타입을 개선하기위한 제안이 있다면 환영합니다. 두 프로토 타입의 소스 코드를 다운로드하십시오.


Posted by 마스터킹
|

2 년 전 Phaser를 사용하여 1 + 2 = 3 게임의 100 줄 html5 프로토 타입을 게시했습니다.


몇 달 후, 프로토 타입을 선택하고, 일종의 스토리와 132 레벨 (예, 132)을 추가하고 Softgames가 후원 한 Matt Vs Math를 발표했습니다.


프로토 타입은 주석 처리되지 않은 상태로 남아 있었고 일부 독자는 대부분 내가 어려움이 증가하는 무작위 질문을 생성 할 수 있었던 방법을 알기 위해 주로 의견을 추가하라고했습니다.


오늘 필자는 최신 Phaser CE 버전 (2.8.5)으로 업데이트 된 원본 프로토 타입을 다시 작성하고 게임이 끝나면 다시 시작할 수있는 게임을 다시 작성합니다.




표시된 식에 따라 1, 2 또는 3의 버튼을 클릭하거나 터치하십시오. 너 가장 좋은 점수는 뭐니? 모바일 장치가있는 경우 이 링크에서 직접 재생할 수 있습니다.


마지막으로 이것은 소스 코드이며 주석을 달고 업데이트합니다.


// the game itself

var game; 

var gameOptions = {

// 합계의 최대 길이

maxSumLen: 5, 

// 높은 점수를 저장하는 데 사용되는 로컬 저장소 이름

localStorageName: "oneplustwo",

// 질문에 대답 할 수있는 시간 (밀리 초)

timeToAnswer: 3000,

// 난이도를 높이기 위해 필요한 점수

nextLevel: 400

}

// 일단 창이 완전히 로드되면 ...

window.onload = function() {

// CANVAS 렌더링을 사용하여 500x500 픽셀 게임 만들기

game = new Phaser.Game(500, 500, Phaser.CANVAS);

// "PlayGame" 상태를 만들고 시작하십시오.

game.state.add("PlayGame", playGame, true);

}

// "PlayGame"상태

var playGame = function(game){}

playGame.prototype = {

// 사전로드 상태

    preload: function(){

        // 모든 콘텐츠를 표시하면서 가능한 한 가장 큰 창 영역을 게임 커버 만들기

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

        game.scale.pageAlignHorizontally = true;

        game.scale.pageAlignVertically = true;

// 초점을 잃을 때 게임을 일시 중지하지 않기.

game.stage.disableVisibilityChange = true; 

        // 배경색 변경

        game.stage.backgroundColor = 0x444444;

// preloading images

game.load.image("timebar", "timebar.png"); 

// preloading a spritesheet where each sprite is 400x50 pixels

game.load.spritesheet("buttons", "buttons.png", 400, 50);

 

    },

// when the state has been created...

    create: function(){

// it's not game over yet...

this.isGameOver = false; 

// current score is set to zero

this.score = 0;

// we'll also keep track of correct answers

this.correctAnswers = 0;  // topScore는 로컬 저장소에 이전에 저장된 값을 가져오고 그렇지 않으면 0을 가져옵니다.

this.topScore = localStorage.getItem(gameOptions.localStorageName) == null ? 0 : localStorage.getItem(gameOptions.localStorageName);

// sumsArray는 가능한 모든 질문이있는 배열입니다.

this.sumsArray = []   // 매번 무작위로 질문을 던지기보다는 모든 가능한 질문을 배열에 저장하는 것이 // 더 쉬워졌으며 매번 무작위로 질문을 뽑았습니다. 가능한 모든 질문을 생성하는 // 알고리즘이 필요합니다.

// 루프로 가능한 모든 질문을 만들기 시작하십시오.  // 1 (1 + 1과 같은 연산자 만)에서 maxSumLen // (이 경우 5, 1 + 1 + 1 + 1-1-1과 같이)까지 다양합니다.

for(var i = 1; i < gameOptions.maxSumLen; i++){

// sumsArray [i]를 3 개의 빈 배열로 정의

this.sumsArray[i]=[[], [], []];

// 각 합계의 가능한 결과 인 1에서 3으로 반복

for(var j = 1; j <= 3; j++){ 

// buildTrees는 스크립트의 핵심이며, 설명되어 있습니다.

this.buildThrees(j, 1, i, j);

}

}

// 가능한 모든 조합을 볼 수 있습니다.

console.log(this.sumsArray);

// questionText는 질문을 표시 할 텍스트 객체입니다.

this.questionText = game.add.text(250 , 160, "-", {

font: "bold 72px Arial"

});

// 설정 질문 등록 포인트

this.questionText.anchor.set(0.5);

// scoreText는 현재 점수를 유지합니다.

this.scoreText = game.add.text(10, 10, "-", {

font: "bold 24px Arial"

});

// 루프를 사용하여 세 가지 응답 버튼을 만듭니다.

for(i = 0;i < 3; i++){

// 응답 버튼의 생성, 프레임 "i"로 설정.

// 한 번 트리거 된 checkAnswer 콜백 함수를 호출합니다.

var numberButton = game.add.button(50, 250 + i * 75, "buttons", this.checkAnswer, this).frame = i;

}

// 타임 바 추가

var numberTimer =  game.add.sprite(50, 250, "timebar");

// 세 개의 대답 버튼을 덮는 그래픽 마스크 생성

this.buttonMask = game.add.graphics(50, 250);

this.buttonMask.beginFill(0xffffff);

this.buttonMask.drawRect(0, 0, 400, 200);

this.buttonMask.endFill();

numberTimer.mask = this.buttonMask;

// 다음 질문

this.nextNumber();

}, 

// buildThrees 메서드를 사용하면 가능한 모든 합계를 찾습니다.

// arguments:

// initialNumber: 첫 번째 숫자. 각 질문은 항상 양수로 시작합니다.

// currentIndex: 이미 합계에 배치 된 피연산자의 양입니다.

// limit: 질문에 허용 된 최대 피연산자 수

// currentString: 지금까지 생성 된 문자열

buildThrees: function(initialNummber, currentIndex, limit, currentString){

// 0을 제외하고 -3에서 3까지 가능한 피연산자

var numbersArray = [-3, -2, -1, 1, 2, 3];

// 0에서 numbersArray의 길이로 루핑하기

for(var i = 0; i < numbersArray.length; i++){

// "sum"은 첫 번째 숫자와 현재 numberArray 항목 사이의 합계입니다.

var sum = initialNummber + numbersArray[i];

// 출력 문자열은 현재 문자열을 현재 numbersArray 항목과 연결하여 // 생성됩니다. 항목이 0보다 큰 경우 "+"를 추가하고 그렇지 않으면 // 이미 "-"가 있습니다.

var outputString = currentString + (numbersArray[i] < 0 ? "" : "+") + numbersArray[i];

// 합계가 1에서 3 사이이고 우리가 원하는 피연산자 수에 도달하면 ...

if(sum > 0 && sum < 4 && currentIndex == limit){

// 그런 다음 출력 문자열을 sumsArray로 푸시합니다. // [피연산자 수][결과]

this.sumsArray[limit][sum - 1].push(outputString);

}

// 피연산자의 양이 우리가 원하는 양보다 여전히 적다면 ...

if(currentIndex < limit){

// 재귀 적으로 buildThrees를 호출하여 인수로 전달 :

// 현재 합계

// 새로운 양의 피연산자

// 우리가 원하는 피연산자의 양

// 현재 출력 문자열

this.buildThrees(sum, currentIndex + 1, limit, outputString);

}

}

},

// 이 방법은 다음 질문을한다.

nextNumber: function(){

// 점수 텍스트 업데이트

this.scoreText.text = "Score: " + this.score.toString() + "\nBest Score: " + this.topScore.toString();

// 우리가 이미 하나 이상의 질문에 대답했다면 ...

if(this.correctAnswers > 1){

// 정지 시간 트윈

this.timeTween.stop();

// 마스크 수평 위치 재설정

this.buttonMask.x = 50;

}

// 우리가 이미 적어도 한 가지 질문에 대답했다면 ...

if(this.correctAnswers > 0){

// 트위스트가 마스크를 밀어 내고 뒤에 무엇이 있는지 밝히기.

this.timeTween = game.add.tween(this.buttonMask).to({

x: -350

}, gameOptions.timeToAnswer, Phaser.Easing.Linear.None, true);

// 트윈이 끝날 때 트리거되는 콜백

this.timeTween.onComplete.add(function(){

// "gameOver"메서드 호출. "?" 표시 할 문자열입니다.

this.gameOver("?");

}, this);

}

// 0과 2 사이의 무작위 결과 그리기 (1에서 3까지)

this.randomSum = game.rnd.between(0, 2);

// 현재 점수에 따라 질문 길이 선택

var questionLength = Math.min(Math.floor(this.score / gameOptions.nextLevel) + 1, 4) 

// 질문 텍스트 업데이트 중

this.questionText.text = this.sumsArray[questionLength][this.randomSum][game.rnd.between( 0, this.sumsArray[questionLength][this.randomSum].length - 1)];

},

// 대답을 확인하는 메소드, 인수는 눌려진 버튼입니다.

checkAnswer: function(button)

// 아직 게임이 끝나지 않은 경우에만 답변을 확인합니다.

if(!this.isGameOver){

// 버튼 프레임은 randomSum과 같습니다. 대답은 정확합니다.

if(button.frame == this.randomSum) 

// 응답에 소요 된 시간에 따라 점수가 증가합니다.

     this.score += Math.floor((this.buttonMask.x + 350) / 4);

// 하나 더 정답

this.correctAnswers++;

// 다음 질문으로 이동

this.nextNumber();

     }

// 잘못된 답변

     else{

// 첫 번째 질문이 아니라면 ...

     if(this.correctAnswers > 1) {

// 트윈을 중지 시키십시오.

this.timeTween.stop();

     }

// "gameOver"메서드 호출. // "button.frame + 1"은 표시 할 문자열입니다.

     this.gameOver(button.frame + 1);

}

}

},

// 게임을 끝내는 방법. 인수는 쓸 문자열입니다.

gameOver: function(gameOverString){

// changing background color

game.stage.backgroundColor = "#ff0000";

// displaying game over text

this.questionText.text = this.questionText.text + " = " + gameOverString;

// now it's game over

this.isGameOver = true;

// updating top score in local storage

localStorage.setItem(gameOptions.localStorageName, Math.max(this.score, this.topScore));

// restart the game after two seconds

game.time.events.add(Phaser.Timer.SECOND * 2, function(){

game.state.start("PlayGame");

}, this);

}

}


Posted by 마스터킹
|

소개

저장소는 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);
});

우리는 먼저 물리 계산을 위해 p2 물리학을 요구합니다. 우리는 또한 플레이어 이동을 위한 playermovement 파일이 필요합니다. 우리는 새로운 p2 물리 세계를 만들고 physics_handler로 단계를 밟습니다.

문서 : 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 게임 메 커닉을 추가 할 것입니다. 레벨 업을위한 경험 막대도 추가 할 것입니다.

Posted by 마스터킹
|



설정, 클라이언트 이동 동기화.

소개

제목에서 알 수 있듯이 이 튜토리얼 시리즈에서는 기본 멀티 플레이어 게임 아키텍처를 소개하고 Node.js를 사용하여 기존 io 게임 (agar.io 및 slither.io)과 유사한 브라우저 기반 멀티 플레이어 게임을 만드는 방법에 대한 전체 자습서를 실행합니다. 서버 쪽은 Node.js, 클라이언트 쪽은 Phaser.js입니다. Node.js 서버 측은 클라이언트와 서버 간의 통신을 위해 Socket.io와 Express.js를 사용합니다. 클라이언트 측은 더 나은 충돌 감지를 위해 특별히 p2 물리를 사용합니다. 이 시리즈의 첫 번째 파트에서는 개발 환경을 설정하고 여러 클라이언트를 서버에 연결하며 마우스 포인터를 사용하여 플레이어 동작을 구현합니다. 이 튜토리얼의 마지막에는 모든 플레이어의 움직임이 동기화됩니다.

설정

Node.Js가 설치되어 있지 않은 경우 여기에서 다운로드 할 수 있습니다 (https://nodejs.org/en/). Node를 설치 한 후, Node.js 명령 프롬프트를 실행하고 프로젝트 폴더로 이동하십시오. 그런 다음 npm init을 입력하여 package.json을 작성하십시오. 내 프로젝트 폴더는 멀티 플레이어 게임이라고합니다.

우선 socket.io가 필요합니다. Socket.io를 사용하면 클라이언트와 서버 측 사이에 실시간 통신을 구현할 수 있습니다. Socket.io는 Node.js와 같이 이벤트 중심적입니다. 예를 들어, 클라이언트가 "공격"이라는 메시지를 보낸다면 서버는 "공격"메시지를 듣고 그에 대한 조치를 취합니다.


npm install socket.io --save를 입력하십시오. --save는 패키지를 package.json에 종속성으로 포함시킬 것입니다. package.json은 나중에 배포 할 때 필요합니다.


그 다음으로 Express.js가 필요합니다. Express.js는 Node.js 프레임 워크로서 Node.js http 모듈을 사용할 필요없이 웹 응용 프로그램을보다 쉽게 만들 수 있습니다. 이 모듈에서는 Express.js가 이미 가지고 있는 많은 것을 다시 구현해야합니다. . 멀티 플레이어 게임을 보다 쉽게 개발할 수 있습니다.


Socket.io와 마찬가지로 Express.js를 설치하려면 npm install express --save를 입력하십시오.

이제 코드를 작성할 준비가되었습니다.


index.html

    <body>
  <div id="gameDiv">
  </div>
 </body>
 <script src="client/lib/phaser.js"></script>
 <script src="/socket.io/socket.io.js"></script>
 <script src="client/player.js"></script>
 <script src="client/main.js"></script>

index.html에서 페이저 게임을 위한 컨테이너를 설정하십시오. 여기, "gameDiv"입니다. 페이저 게임 프레임 워크, 클라이언트 용 socket.io 라이브러리 및 게임 파일을 참조하십시오.


1 단계:
클라이언트 측 : main.js

var socket; // 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 gameProperties = { 
 // 이것은 세계의 경계를 결정하는 실제 게임 크기입니다.
 gameWidth: 4000, 
 gameHeight: 4000,
};

// 이것은 주 게임 상태입니다.
var main = function(game){
};
// add the 
main.prototype = {
 preload: function() {


    },
 // 이 함수는 게임을로드 할 때 한 번 실행됩니다.
 create: function () {
  console.log("client started");
  // 서버에서 "연결"메시지를 듣습니다.
        // 클린트가 연결되면 서버는 자동으로 "연결"메시지를 내 보냅니다.
        // 클라이언트가 연결되면 onsocketConnected를 호출하십시오.
  socket.on("connect", onsocketConnected); 

 }
}

// 이 함수는 우리가 연결할 때 시작된다.
function onsocketConnected () {
 console.log("connected to server"); 
}

// 게임 상태를 감싼다.
var gameBootstrapper = {
    init: function(gameContainerElementId){
  game.state.add('main', main);
  game.state.start('main'); 
    }
};;

// 래퍼에서 init 함수를 호출하고 division ID를 지정합니다.
gameBootstrapper.init("gameDiv");

코멘트는 그것이 어떻게 작동하는지 간단히 설명합니다. 중요한 라인은 io.connect ()입니다. 기본적으로 클라이언트는 서버에 대한 연결을 요청할 수 있습니다. 서버는 이 연결 요청을 수신하고 성공적으로 연결할 때 "연결"메시지를 다시 클라이언트로 내 보냅니다. 그래서 우리는 socket.on ( "connect", onsocketConnected)이라는 줄을 가지고 있습니다. 클라이언트가 연결 메시지를 받으면 나중에 게임을 초기화 할 수있는 onsocketConnected 함수를 호출합니다.


서버 : app.js :

// import express.js 
var express = require('express');
// 변수 app에 할당
var app = express();
// 서버를 만들고 요청 처리기로 응용 프로그램에 전달.
var serv = require('http').Server(app); //Server-11
// 주어진 경로에 get 요청이 발생하면 index.html 
   파일을 보내십시오.이 경우 '/'입니다.
app.get('/',function(req, res) {
 res.sendFile(__dirname + '/client/index.html');
});
// 이것은 get 요청이 '/client'에 도달 할 때 모든 
   정적 파일을 클라이언트 폴더에 넣는 것을 의미합니다
app.use('/client',express.static(__dirname + '/client'));

// listen on port 2000
serv.listen(process.env.PORT || 2000);
console.log("Server started.");

 // 우리가 만든 serv 객체를 socket.io에 바인드한다.
var io = require('socket.io')(serv,{});

// 모든 클라이언트의 연결 요청 수신 대기
io.sockets.on('connection', function(socket){
 console.log("socket connected"); 
 // 고유 한 socket.id를 출력하십시오.
 console.log(socket.id);
});

서버 측 코드에서 먼저 익스프레스 모듈을 가져 와서 변수 앱에 할당합니다. 우리는 다음이 응용 프로그램을 사용하여 서버를 만듭니다.


Server11 : 이것이 무엇인지 궁금 할 것입니다. Node Http 모듈에서 require ( 'http'). Server (function (requestListener))는 서버에 요청할 때마다 requestListener를 실행하여 응답과 요청을 처리합니다. app)를 requestListener로 사용하여 응답 및 요청을 처리합니다.


그런 다음 client라는 정적 파일을 설정합니다. app.use ( '/ client', express.static (__ dirname + '/ client'))가 없으면 기본적으로 Node.js는 index.html의 클라이언트 폴더에서 파일을 참조하려고 할 때 수행 할 작업을 알 수 없습니다. Express.js는 express.static이라는 편리한 함수를 제공하여 정적 파일에 쉽게 액세스 할 수있게 해줍니다.


예를 들어 index.html에서 <script src = "client/main"></script>를 호출하여 javascript 게임 파일에 액세스했습니다. 이 파일은 localhost:2000/client/main.js에 있습니다. app.use( '/ client', express.static(__ dirname + '/ client'))는 클라이언트 폴더에있는 모든 정적 파일을 localhost : 2000/client에 저장하여 액세스 할 수 있도록합니다.


우리의 게임을 실제로 볼 시간입니다 !! 노드 app.js를 입력하여 서버를 실행하십시오. 브라우저로 이동하여 localhost:2000을 입력하십시오. 콘솔에 연결된 소켓 메시지와 긴 임의의 문자 (소켓 ID)가 표시되면 정상적으로 작동합니다.


와우, 몇 가지 연결 메시지를 출력 할 수 있지만 실제 게임을 만들고 싶습니다! 이제는 socket.io에 들어가기에 좋은시기입니다. 이 클라이언트와 서버 코드에서는 클라이언트가 서버에 연결할 때마다 플레이어 객체를 생성합니다


2 단계:
클라이언트 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 gameProperties = { 
 gameWidth: 4000,
 gameHeight: 4000,
 game_elemnt: "gameDiv",
 in_game: false,
};

var main = function(game){
};

// 플레이어가 서버에 연결할 때 이 함수를 호출하십시오.
function onsocketConnected () {
 // 연결된 사용자가 제어 할 수 있도록 기본 플레이어 개체를 만듭니다.
 createPlayer();
 gameProperties.in_game = true;
 // 서버에 새로운 플레이어 객체가 생성되었음을 알리는 
    // "new_player"메시지를 보내십시오.
 socket.emit('new_player', {x: 0, y: 0, angle: 0});
}

// CLIENT의 "main"플레이어 클래스. 
// 이 플레이어는 사용자가 제어하는 플레이어입니다. 
// 이 예제를 사용하여 그래픽을 사용하여 그림을 그릴 수 있습니다. 
function createPlayer () {
 // Phaser의 그래픽을 사용하여 원을 그립니다.
 player = game.add.graphics(0, 0);
 player.radius = 100;

 // 채우기 및 선 스타일 설정
 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; 

 // 모양을 그리다
 game.physics.p2.enableBody(player, true);
 player.body.addCircle(player.body_size, 0 , 0); 
}

main.prototype = {
 preload: function() {
  game.scale.scaleMode = Phaser.ScaleManager.RESIZE;
  game.world.setBounds(0, 0, gameProperties.gameWidth, 
  gameProperties.gameHeight, false, false, false, false);
  // 저는 물리 시스템에 P2JS를 사용하고 있습니다. 
  game.physics.startSystem(Phaser.Physics.P2JS);
  game.physics.p2.setBoundsToWorld(false, false, false, false, false)
  // y 중력을 0으로 설정합니다. 
        // 이것은 플레이어가 중력에 의해 떨어지지 않는다는 것을 의미합니다.
  game.physics.p2.gravity.y = 0;
  // 중력을 끄다
  game.physics.p2.applyGravity = false; 
  game.physics.p2.enableBody(game.physics.p2.walls, false); 
  // 충돌 감지 켜기
  game.physics.p2.setImpactEvents(true);

    },
 
 create: function () {
  game.stage.backgroundColor = 0xE1A193;;
  console.log("client started");
  // 클라이언트가 서버에 성공적으로 연결하고 onsocketConnected를 
        // 호출하면 수신 대기합니다.
  socket.on("connect", onsocketConnected); 
 },
 
 update: function () {
  // 플레이어 입력을 내 보낸다.
 
  // 그가 게임에 있을 때 플레이어를 움직이십시오.
  if (gameProperties.in_game) {
   // phaser의 마우스 포인터를 사용하여 사용자의 마우스 위치를 
            // 추적합니다.
   var pointer = game.input.mousePointer;
   
   // distanceToPointer를 사용하면 마우스 포인터와 플레이어 
            // 개체 사이의 거리를 측정 할 수 있습니다.
   if (distanceToPointer(player, pointer) <= 50) {
    // 플레이어는 특정 속도로 마우스 포인터로 이동할 수 있습니다.
                // 이것이 어떻게 구현되는지 player.js를보십시오.
    movetoPointer(player, 0, pointer, 100);
   } else {
    movetoPointer(player, 500, pointer);
   } 
  }
 }
}

var gameBootstrapper = {
    init: function(gameContainerElementId){
  game.state.add('main', main);
  game.state.start('main'); 
    }
};;

gameBootstrapper.init("gameDiv");

중요한 줄은 socket.emit ( 'new_player', {x : 0, y : 0, angle : 0}) ;입니다. 이것이 socket.io를 사용하여 서버에 메시지를 보내는 방법입니다. 첫 번째 매개 변수는 메시지 이름이고 두 번째 매개 변수는 보내려는 데이터입니다. 클라이언트가 서버에 연결할 때 플레이어가 제어 할 원형 개체를 추가하기만 하면됩니다. movetoPointer를 사용하면 특정 속도로 마우스 포인터를 향해 이동할 수 있습니다. 구현을 위해 player.js를 볼 수 있습니다.


서버 : app.js

var express = require('express');

var app = express();
var serv = require('http').Server(app);


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 Player = function (startX, startY, startAngle) {
  var x = startX
  var y = startY
  var angle = startAngle
}

// 서버가 클라이언트로부터 "new_player"라는 메시지를받을 때마다 
// onNewplayer 함수가 호출됩니다.
function onNewplayer (data) {
 // 새로운 플레이어 오브젝트를 형성하다
 var newPlayer = new Player(data.x, data.y, data.angle);
 console.log("created new player with id " + this.id);
 player_lst.push(newPlayer); 

}

// io connection 
var io = require('socket.io')(serv,{});

io.sockets.on('connection', function(socket){
 console.log("socket connected"); 

 // 클라이언트에서 "new_player"메시지 청취
 socket.on("new_player", onNewplayer);
});

새 서버 측 코드에서 추가 한 중요한 기능은 socket.on ( "new_player", onNewplayer)입니다. onNewPlayer 함수 내에서 "this.id" 줄을 확인하십시오. 함수에서 "this"는 io.sockets.on의 "socket"을 참조합니다. 콜백의 "this.id"는 io.sockets.on의 "socket.id"와 같습니다. Socket.id는 고유하므로 모든 연결마다 다른 ID가 있습니다.


게임을 시작하면 이제 서클을 제어 할 수 있습니다! 하지만 ... 다른 플레이어를 보지 못하면 멀티 플레이어 게임이 아닙니다. 실시간으로 적을 추가 할 시간입니다!


3 단계
클라이언트 : 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;
}

// 이것이 적 클래스 입니다.
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) {
 console.log(data);
 // 적 개체
 var new_enemy = new remote_player(data.id, data.x, 
                                   data.y, data.angle); 
 enemies.push(new_enemy);
}

// 서버는 새로운 적의 움직임이 있음을 알려줍니다. 
// 이동 된 적을 찾고 서버와 적의 움직임을 동기화합니다.
function onEnemyMove (data) {
 console.log(data.id);
 console.log(enemies);
 var movePlayer = findplayerbyid (data.id); 
 
 if (!movePlayer) {
  return;
 }
 movePlayer.player.body.x = data.x; 
 movePlayer.player.body.y = data.y; 
 movePlayer.player.angle = data.angle; 
}

// 여기서 우리는 소켓 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);
  
  // remove_player를 받으면 플레이어를 제거합니다.
  socket.on('remove_player', onRemovePlayer); 
 },
 
 update: function () {
  // 플레이어 입력을 내 보낸다.  
  // 플레이어가 만들어지면 플레이어를 움직인다.
  if (gameProperties.in_game) {
   var pointer = game.input.mousePointer;
   
   if (distanceToPointer(player, pointer) <= 50) {
    movetoPointer(player, 0, pointer, 100);
   } else {
    movetoPointer(player, 500, pointer);
   }
   
     
   //Send a new position data to the server 
   socket.emit('move_player', {x: player.x, y: player.y, 
                               angle: player.angle});
  }
 }
}

var gameBootstrapper = {
    init: function(gameContainerElementId){
  game.state.add('main', main);
  game.state.start('main'); 
    }
};;

gameBootstrapper.init("gameDiv");

클라이언트 측 구조는 매우 복잡하지 않습니다. 우리는 서버로부터 새로운 플레이어 메시지를 기다리고 적 대상의 새로운 인스턴스를 생성하고, 움직일 때 적 개체의 위치를 올바른 위치 (서버에서 전송)로 이동시킵니다. findPlayerbyId 함수는 id에 의해 corrent 적을 찾는 데 사용됩니다. 플레이어가 연결을 끊으면 onRemovePlayer 함수를 사용하여 적 개체를 찾아 게임에서 제거합니다. game.stage.disableVisibilityChange = true 행을 추가하십시오. 즉, 커서가 브라우저를 떠날 때 브라우저를 잠자기 하지 않습니다. 즉, 개발을 위해 두 개의 브라우저를 동시에 모니터링 할 수 있습니다.


var express = require('express');

var app = express();
var serv = require('http').Server(app);

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 Player = function (startX, startY, startAngle) {
  this.x = startX
  this.y = startY
  this.angle = startAngle
}

// 새 플레이어가 연결되면 플레이어 개체의 새 인스턴스를 만들고 
// 새 플레이어 메시지를 클라이언트에 보냅니다.
function onNewplayer (data) {
 console.log(data);
 //new player instance
 var newPlayer = new Player(data.x, data.y, data.angle);
 console.log(newPlayer);
 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 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);
 // listen for player position update
 socket.on("move_player", onMovePlayer);
});


서버 측에서는 플레이어의 위치를 업데이트하는 onMovePlayer를 추가했습니다. 우리에게 "this.broadcast.emit"이 있음을 주목하십시오. 즉, 보낸 사람을 제외한 모든 소켓에 데이터를 전송합니다. 우리가 가진 또 다른 유형의 메시지 전송은 "this.emit"입니다. 이것은 "발신자에게만 보내기"를 의미합니다. onNewPlayer에서는 두 가지 작업을 수행합니다. 첫째, 새 플레이어가 연결되면 이전에 이미 게임에 연결된 모든 사람에 대해 특정 새 플레이어에게 보내야합니다. 두 번째로, 우리는 새로운 플레이어에 관해 이미 연결되어있는 모든 플레이어 (새로운 플레이어가 아닌)를 보내야합니다.


이 socket.io 치트 시트를 사용하여 서버의 특정 클라이언트에게 보낼 수 있습니다 : https://socket.io/docs/emit-cheatsheet/


그러나 클라이언트가 클라이언트에서 직접 위치를 전송하기 때문에 이것은 멀티 플레이어의 매우 순진하고 위험한 구현입니다. 해커가 이 순진한 구현을 파악하고 서버와 다른 위치를 전송하려고 시도하는 경우를 상상해 보십시오. 모두는 사기꾼으로부터 가난한 게임 플레이 경험을 겪습니다! 우리는 다음 튜토리얼에서이 순진 구현을 수정하려고 노력할 것입니다!






Posted by 마스터킹
|

카드 게임을 만들 계획이라면 카드를 보여주고 등을 보여야 할 때가 왔습니다. 그런 다음 카드를 뒤집어서 가치를 보여줄 필요가 있습니다.


일반적으로 3D 엔진을 사용하면 축 중 하나를 따라 플립하는 기본 요소 일뿐 아니라 정말 쉬운 작업이됩니다.


우리가 3D 엔진을 사용하지 않을 때해야 할 일은 무엇입니까? Phaser 트윈을 사용하여 플립 운동을 가짜로 만들 수 있습니다.


수직축을 따라 카드를 확대하여 카드가 올라가는 느낌을주고, 카드가 수평선에 가까워 질 때까지 수평축을 따라 축소합니다. 카드가 테이블에 수직 인 순간입니다.


그런 다음 카드 프레임을 변경하고 수평축을 따라 확대하면서 새로운 프레임을 천천히 보여주기 위해 y 축척에 따라 카드를 내립니다.




카드를 클릭하거나 가볍게 두 드립니 다.


다음은 완전히 주석 처리 된 소스 코드입니다.

// 게임 그 자체

var game;

// 게임 옵션이있는 전역 개체

var gameOptions = {

    // 밀리 초 단위로 플 리핑 속도

    flipSpeed: 200, 

    // 줌 비율을 뒤집기. 뒤집을 때 올릴 카드를 시뮬레이트합니다.

    flipZoom: 1.2

}

window.onload = function() { 

    // 500x500 픽셀 게임 제작

    game = new Phaser.Game(500, 500); 

    // 게임 상태

    game.state.add("PlayGame", playGame);

    game.state.start("PlayGame");

}

var playGame = function(game){}

playGame.prototype = {

    preload: function(){

        // 모든 콘텐츠를 표시하면서 가능한 한 가장 큰 창 영역을 게임 커버 만들기

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

        game.scale.pageAlignHorizontally = true;

        game.scale.pageAlignVertically = true; 

        // 배경색 변경

        game.stage.backgroundColor = 0x448844;

        // 카드 스프라이트 시트

        game.load.spritesheet("cards", "cards.png", 167, 243);

    },

    create: function(){

        // 카드 추가

        this.card =  game.add.sprite(game.width / 2, game.height / 2, "cards", 0); 

        // 카드 앵커 포인트를 중앙에 설정

        this.card.anchor.set(0.5); 

        // 카드가 넘겨 지는지 알려주는 맞춤 속성, 지금은 아닙니다.

        this.card.isFlipping = false; 

        // 플레이어 입력 대기 중

        game.input.onDown.add(function(){

            // 카드가 뒤집히지 않으면 ...

            if(!this.card.isFlipping){ 

                // 이제 뒤집기.

                this.card.isFlipping = true; 

                // 두 개의 뒤집기 애니메이션 중 첫 번째 애니메이션 시작

                this.flipTween.start();

            }

        }, this); 

        // 첫 번째 트윈 : 카드를 올리고 뒤집습니다.

        this.flipTween = game.add.tween(this.card.scale).to({

            x: 0,

            y: gameOptions.flipZoom

        }, gameOptions.flipSpeed / 2, Phaser.Easing.Linear.None); 

        // 카드가 뒤집 히면 프레임을 변경하고 두 번째 트윈을 호출합니다.

        this.flipTween.onComplete.add(function(){

            this.card.frame = 1 - this.card.frame;

            this.backFlipTween.start();

        }, this); 

        // 두 번째 트윈 : 뒤집기를 완료하고 카드를 내립니다.

        this.backFlipTween = game.add.tween(this.card.scale).to({

            x: 1,

            y: 1

        }, gameOptions.flipSpeed / 2, Phaser.Easing.Linear.None);

 

        // 일단 카드를 테이블 위에 놓으면 다시 뒤집을 수 있습니다.

        this.backFlipTween.onComplete.add(function(){

            this.card.isFlipping = false;

        }, this);

    }

}


이제 2D 카드 게임에 사실감을 더할 수 있습니다. 다음에는 그림자와 다른 흥미로운 효과를 추가하는 방법을 보여줄 것이며 소스 코드를 다운로드 할 것입니다.



Posted by 마스터킹
|

이것은 JavaScript 및 Phaser로 Slither.io를 만드는 튜토리얼 시리즈의 일곱 번째 파트입니다! Part 1을 처음 보시거나 Part 6으로 돌아 가시면됩니다.


예제를 살펴보고 이 부분의 소스 코드를 살펴보십시오.


음식 개념

이 시리즈의 마지막 부분에서는 뱀이 먹을 수 있는 음식을 추가 할 것입니다. 이것이 우리의 마지막 부분이기 때문에 데모는 완성 된 버전입니다!


우리의 음식 스프라이트는 작은 육각형 일 것입니다. 이 스프라이트에는 원형 물리바디가 있습니다. 음식 스프라이트가 뱀 머리와 충돌 할 때, 음식이 머리 중심을 향해 중력을 가하는 것처럼 보이는 구속 조건이 만들어집니다. 음식이 뱀 머리의 중심에 있으면 파괴되고 뱀의 크기가 커집니다. 육각 이미지는 흰색이므로 hex.png에서 볼 수 있듯이 어떤 색상의 음식이라도 만들 수 있습니다.


음식

food.js의 Food 함수를 살펴보십시오.

Food = function(game, x, y) {

    this.game = game;

    this.debug = false;

    this.sprite = this.game.add.sprite(x, y, 'food');

    this.sprite.tint = 0xff0000;


    this.game.physics.p2.enable(this.sprite, this.debug);

    this.sprite.body.clearShapes();

    this.sprite.body.addCircle(this.sprite.width * 0.5);

    // 무언가가 음식에 부딪 칠 때 콜백을 설정합니다.

    this.sprite.body.onBeginContact.add(this.onBeginContact, this);


    this.sprite.food = this;


    this.head = null;

    this.constraint = null;

}

여기서는 음식 스프라이트를 만들고, 원형 물리 바디를 만들고,이 몸체가 뱀 머리와 접촉하기 시작할 때 콜백을 설정하고, 마지막으로 음식 머리를 만질 때 사용할 속성 머리와 구속 조건을 만듭니다.


처음 접촉

음식이 뭔가를 만졌을 때 콜백을 보자.

onBeginContact: function(phaserBody, p2Body) {

    if (phaserBody && phaserBody.sprite.name == "head" && this.constraint === null) {

        this.sprite.body.collides([]);

        // 음식과 충돌 한 뱀 머리 사이의 제약 조건을 만듭니다.

        // 음식은 머리 스프라이트의 중심으로 가져옵니다.

        this.constraint = this.game.physics.p2.createRevoluteConstraint(

            this.sprite.body, [0,0], phaserBody, [0,0]

        );

        this.head = phaserBody.sprite;

        this.head.snake.food.push(this);

    }

}

먼저 음식이 충돌 한 뱀 머리인지 확인합니다. 그렇다면 우리는 이 음식에 대한 모든 충돌을 차단합니다. 우리는 실제로 그것이 닿았 던 뱀 머리쪽으로 음식을 끌어 당기는 회전 제한을 만듭니다. 우리는 음식이 충돌 한 뱀 머리에 머리 속성을 설정하고, 우리는 잠시 후에 설명 할 뱀의 음식 배열에 이 음식을 밀어 넣습니다.


업데이트

업데이트 방법을 살펴보십시오.

update: function() {

    // 일단 음식이 뱀 머리의 중심에 도달하면, 그것을 파괴하고 뱀의 크기를 증가시킵니다.

    if (this.head && Math.round(this.head.body.x) == Math.round(this.sprite.body.x) &&

    Math.round(this.head.body.y) == Math.round(this.sprite.body.y)) {

        this.head.snake.incrementSize();

        this.destroy();

    }

}

여기서 우리는 음식이 대략 머리 중심에 있는지 확인합니다. 그것이되면, 우리는 뱀 크기를 증가시키고 이 음식을 파괴합니다.


파괴

우리 게임에서 그 밖의 모든 것과 마찬가지로, 음식에는 파괴 방법이 필요합니다.

destroy: function() {

    if (this.head) {

        this.game.physics.p2.removeConstraint(this.constraint);

        this.sprite.destroy();

        this.head.snake.food.splice(this.head.snake.food.indexOf(this), 1);

        this.head = null;

    }

}

일단 머리에 부딪치더라도 우리는 단지 그것을 파괴 할뿐입니다. 그런 다음 제약 조건을 제거하고 스프라이트를 제거한 다음 이전에 언급 한 뱀의 해당 배열에서 음식을 제거합니다.


뱀 음식 배열

Snake 클래스에서 다음 줄을 추가했습니다.

this.food = [];


이 배열은 뱀이 현재 소비하고있는 모든 음식을 포함합니다. 즉, 뱀과 충돌했지만 아직 파괴되지 않은 음식을 의미합니다. 왜 우리가 이것을 필요로합니까? 뱀이 갑자기 파괴되면 음식이 뱀에게 강요되므로 뱀이 모두 파괴되어야합니다. 이것을 처리하지 않으면 오류가 발생합니다. 자 이제 이것을 뱀 파괴 방법에 추가해 봅시다.

// 뱀 머리에 구속 된 음식을 파괴하라.

for (var i = this.food.length - 1 ; i >= 0 ; i--) {

    this.food[i].destroy();

}


게임에 음식 추가하기

마지막으로 실제로 음식을 추가하고 올바르게 충돌해야합니다. game.js를 한번보세요. 우리는 Create 상태에 몇몇 그룹과 음식을 추가 할 것입니다 :

this.foodGroup = this.game.add.group();

this.snakeHeadCollisionGroup = this.game.physics.p2.createCollisionGroup();

this.foodCollisionGroup = this.game.physics.p2.createCollisionGroup();

// add food randomly

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

    this.initFood(Util.randomInt(-width, width), Util.randomInt(-height, height));

}


마지막 몇 라인은 임의의 위치에서 initFood 메소드를 호출합니다. 이 방법을 살펴보십시오.

initFood: function(x, y) {

    var f = new Food(this.game, x, y);

    f.sprite.body.setCollisionGroup(this.foodCollisionGroup);

    this.foodGroup.add(f.sprite);

    f.sprite.body.collides([this.snakeHeadCollisionGroup]);

    return f;

}


우리는 음식을 일부 그룹에 추가 한 다음 뱀 머리와 만 충돌하는지 확인합니다. create 메서드에 뱀을 추가 한 후에는 자신에게 충돌 그룹을 제공해야합니다.

// initialize snake groups and collision

for (var i = 0 ; i < this.game.snakes.length ; i++) {

    var snake = this.game.snakes[i];

    snake.head.body.setCollisionGroup(this.snakeHeadCollisionGroup);

    snake.head.body.collides([this.foodCollisionGroup]);

    //callback for when a snake is destroyed

    snake.addDestroyedCallback(this.snakeDestroyed, this);

}

우리는 뱀 머리를 음식과 충돌하도록 설정 한 다음 파괴 콜백을 추가합니다.


Dropping Food on Destruction

마지막으로, 우리 게임의 가장 중요한 부분 : 그들이 죽을 때 음식을 떨어지는 뱀! 뱀이 파괴되었을 때 game.js에서 콜백 메소드를 봅시다

snakeDestroyed: function(snake) {

    //place food where snake was destroyed

    for (var i = 0 ; i < snake.headPath.length ;

    i += Math.round(snake.headPath.length / snake.snakeLength) * 2) {

        this.initFood(

            snake.headPath[i].x + Util.randomInt(-10,10),

            snake.headPath[i].y + Util.randomInt(-10,10)

        );

    }

}

우리는 뱀의 경로 근처에서 무작위로 음식을 떨어 뜨려 음식물이 고르게 흩어 지도록 경로를 반복합니다. 그리고 그걸로 우리 게임이 완성되었습니다!


결론

Slither.io 여행이 끝났습니다. 이제 자신의 IO 게임을 만들거나 이 게임에 추가 할 시간입니다. 이 시리즈와 같은 다른 시리즈를보고 싶다면 알려주세요. 충분한 관심이 있다면이 게임 멀티 플레이어 제작에 대한 자습서를 작성하는 것을 고려할 것입니다.


이 시리즈는 Phaser의 지원 없이는 가능하지 않았습니다. 잠시 시간을내어 Twitter에서 나를 따라 가고이 튜토리얼 시리즈의 소스 코드에 Github의 별표를 올려주세요!

Posted by 마스터킹
|

이것은 자바 스크립트와 페이저로 Slither.io를 만드는 튜토리얼 시리즈의 여섯 번째 부분입니다! Part 1을 처음 보시거나 Part 5로 돌아 가시면됩니다.


예제를 살펴보고 이 부분의 소스 코드를 살펴보십시오.


그림자 개념 및 목표

시리즈의 이 부분에서는 뱀 아래에 회색 그림자를 추가 할 것입니다. 우리는 이 그림자가 색을 바꿀 수 있다는 점에서 유연 할 것이므로 플레이어가 스페이스 바를 눌러 속도를 높이면 그림자가 켜질 수 있습니다. 그것 이외에, 우리는 단순히 뱀 아래에 머물러있는 그림자가 필요합니다.


이미지

Assets 폴더를 보고 white-shadow.png를 보십시오. 왜 흰색입니까? 우리는 이제 스프라이트에 어떤 색조도 줄 수 있으므로 곧 살펴 보겠습니다. 뱀의 섹션 아래에 이러한 이미지 스프라이트 그룹을 추가 한 다음 뱀과 함께 이동합니다.


그림자

이제 shadow.js를보세요. 먼저 Shadow 함수를 만듭니다.

Shadow = function(game, sections, scale) {

    this.game = game;

    this.sections = sections;

    this.scale = scale;

    this.shadowGroup = this.game.add.group();

    this.shadows = [];

    this.isLightingUp = false;


    this.lightStep = 0;

    this.maxLightStep = 3;


    this.lightUpdateCount = 0;

    this.updateLights = 3;


    this.darkTint = 0xaaaaaa;

    this.lightTintBright = 0xaa3333;

    this.lightTintDim = 0xdd3333;

}

여기서 그림자 배열을 만들고, 색상 변경 시퀀스를 처리하기 위해 일부 변수를 초기화하고, 실제로 다른 그림자 색조에 대한 일부 속성을 설정합니다.


그림자 추가

다음으로 쉐도우 스프라이트를 추가하는 메소드가 필요합니다. 그림자 프로토 타입의 add 메서드는 다음과 같습니다.

add: function(x, y) {

    var shadow = this.game.add.sprite(x, y, "shadow");

    shadow.scale.setTo(this.scale);

    shadow.anchor.set(0.5);

    this.shadowGroup.add(shadow);

    this.shadows.push(shadow);

}

지정된 위치에 그림자 스프라이트를 추가 한 다음 그룹에 추가합니다. Snake 클래스를 돌아 보면 각 뱀 섹션마다 하나의 그림자가 추가 된 것을 볼 수 있습니다.


업데이트

이제 뱀 섹션 아래로 그림자를 이동하기 위해 업데이트 메소드가 필요합니다.

update: function() {

    var lastPos = null;

    for (var i = 0 ; i < this.sections.length ; i++) {

        var shadow = this.shadows[i];

        var pos = {

            x: this.sections[i].body.x,

            y: this.sections[i].body.y

        };


        // 이전 그림자가 같은 위치에 있으면 그림자를 숨 깁니다.

        if (lastPos && pos.x == lastPos.x && pos.y == lastPos.y) {

            shadow.alpha = 0;

            shadow.naturalAlpha = 0;

        }

        else {

            shadow.alpha = 1;

            shadow.naturalAlpha = 1;

        }

        // 각 그림자를 뱀 섹션 아래에 배치합니다.

        shadow.position.x = pos.x;

        shadow.position.y = pos.y;


        lastPos = pos;

    }


    // 밝은 색조로 그림자를 연다.

    if (this.isLightingUp) {

        this.lightUpdateCount++;

        if (this.lightUpdateCount >= this.updateLights) {

            this.lightUp();

        }

    }

    // 그림자를 어둡게 만든다.

    else {

        for (var i = 0 ; i < this.shadows.length ; i++) {

            var shadow = this.shadows[i];

            shadow.tint = this.darkTint;

        }

    }

}

코드의 첫 번째 부분은 각 그림자를 뱀 섹션과 같은 위치로 이동시킵니다. 두 개의 그림자가 같은 위치에 있으면 알파를 0으로 설정하여 그림자 중 하나를 숨 깁니다. 이렇게 하면 섹션이 추가 될 때 뱀의 뒤쪽에 그림자 스프라이트가 스태킹 되지 않습니다. 그런 다음 isLightingUp 속성을 기반으로 그림자 스프라이트 색조를 변경합니다.


Lighting Up

업데이트에서 호출하는 lightUp 메서드를 살펴 보겠습니다.

lightUp: function() {

    this.lightUpdateCount = 0;

    for (var i = 0 ; i < this.shadows.length ; i++) {

        var shadow = this.shadows[i];

        if (shadow.naturalAlpha > 0) {

            // 그림자가 일정하지 않게 교번 효과를 만듭니다.

            if ((i - this.lightStep) % this.maxLightStep === 0 ) {

                shadow.tint = this.lightTintBright;

            }

            else {

                shadow.tint = this.lightTintDim;

            }

        }

    }

    // 그림자 색조를 대체하는 방법을 결정하기 위해 카운터를 사용합니다.

    this.lightStep++;

    if (this.lightStep == this.maxLightStep) {

        this.lightStep = 0;

    }

}

이 방법은 밝은 색조로 그림자를 밝게 합니다. 더 어두운 색과 밝은 색으로 번갈아 가며 실제 게임과 같이 그림자가 돋보입니다.


스케일과 파괴

뱀의 다른 부분과 마찬가지로 그림자도 크기를 조정하고 파괴해야 합니다. 다음은 스케일 방법입니다.

setScale: function(scale) {

    this.scale = scale;

    for (var i = 0 ; i < this.shadows.length ; i++) {

        this.shadows[i].scale.setTo(scale);

    }

}

그리고 여기 파괴 방법이 있습니다 :

destroy: function() {

    for (var i = this.shadows.length - 1 ; i >= 0 ; i--) {

        this.shadows[i].destroy();

    }

}


Snake 클래스에 그림자를 구현해 보겠습니다.

this.shadow = new Shadow(this.game, this.sections, this.scale);


Snake의 addSectionAtPosition 메소드에서 각 섹션에 하나씩 그림자 스프라이트를 추가해야합니다.

this.shadow.add(x,y);


그런 다음 뱀 업데이트 메서드에서 그림자 업데이트 메서드가 필요합니다.

this.shadow.update();


그리고 그림자는 뱀 파괴 방법에 있는 방법을 파괴한다 :

this.shadow.destroy();


그리고 그것으로 그림자가 구현되었습니다. 마지막 부분에서 우리는 뱀을 위한 음식을 구현하고 시리즈를 마무리 할 것입니다.

Posted by 마스터킹
|