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

달력

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

멀티 플레이어 게임을 제작하는 것은 여러 가지 이유로 어려움을 겪습니다. 호스트 비용이 많이 들고 설계가 까다 롭고 구현하기 어려울 수 있습니다. 이 자습서를 통해 마지막 장벽을 해결할 수 있기를 바랍니다.


이것은 게임을 만드는 방법을 알고 자바 스크립트에 익숙하지만 온라인 멀티 플레이어 게임을 만든 적이없는 개발자를 대상으로합니다. 일단 끝나면 기본적인 네트워킹 구성 요소를 모든 게임에 구현하고 거기에서 구축 할 수 있어야합니다.


이것이 우리가 구축 할 것입니다.

Screenshot of the Final Game - Two Ships Attacking Each Other


여기서 라이브 버전의 게임을 사용해 볼 수 있습니다! W 또는 위로 이동하여 마우스를 향해 이동하고 클릭하여 촬영하십시오. 온라인에 다른 사람이 없다면 같은 컴퓨터에서 두 개의 브라우저 창을 열거 나 휴대 전화에서 두 개의 브라우저 창을 열어 멀티 플레이어 작동 방식을 확인하십시오. 로컬에서 실행하는 데 관심이 있다면 완전한 소스 코드를 GitHub에서도 사용할 수 있습니다.


나는 Kenney 's Pirate Pack 아트 Asset과 Phaser 게임 프레임 워크를 사용하여 이 게임을 한데 모았습니다. 이 자습서에서는 네트워크 프로그래머 역할을 맡을 것입니다. 출발점은 이 게임의 모든 기능을 갖춘 싱글 플레이어 버전이 될 것이며, 네트워킹 부분에 Socket.io를 사용하여 Node.js에 서버를 작성하는 것은 여러분의 일이 될 것입니다. 이 튜토리얼을 관리하기 쉽게 하기 위해 멀티 플레이어 부분에 초점을 맞추고 Phaser 및 Node.js의 특정 개념을 살펴 보겠습니다.


1. 설정

Glitch.com의 스타터 키트를 설치했습니다.


몇 가지 빠른 인터페이스 팁 : 언제든지 Show 버튼 (왼쪽 상단)을 클릭하여 앱의 실시간 미리보기를 볼 수 있습니다.

The show button is at the top left on the Glitch interface


왼쪽의 수직 사이드 바에는 앱의 모든 파일이 포함됩니다. 이 응용 프로그램을 편집하려면 "리믹스"해야합니다. 이렇게하면 귀하의 계정에 복사본이 생성됩니다 (또는 git lingo에서 포크로 작성). Remix this button을 클릭하십시오.

The remix button is at the top of the code editor


이 시점에서 익명 계정으로 앱을 수정하게됩니다. 로그인하여 (오른쪽 상단) 작업 내용을 저장할 수 있습니다.


이제 더 나아 가기 전에 멀티 플레이어를 추가하려는 게임의 코드에 익숙해지는 것이 중요합니다. index.html을 살펴보십시오. 플레이어 오브젝트 (35 행) 외에 preload (99 행), create (115 행), GameLoop (142 행)의 세 가지 중요한 기능을 알고 있어야 합니다.


게임을 통해 배운다면 다음과 같은 도전 과제를 시도하여 게임이 어떻게 작동하는지 요점을 확인하십시오.


 - 세계를 더 크게 만든다. (29 행) - 페이지 내 실제 캔버스에 게임 내 세계와 창 크기에 대해 별도의 세계 크기가 있음을 알린다.

 - 스페이스 바를 앞으로 돌리십시오 (53 행).

 - 플레이어 선종을 변경하십시오 (라인 129).

 - 총알이 느리게 움직 이도록하십시오 (줄 155).



Socket.io 설치하기

Socket.io는 웹 소켓을 사용하여 브라우저에서 실시간 통신을 관리하기위한 라이브러리 입니다 (멀티 플레이어 데스크톱 게임을 만드는 경우 UDP와 같은 프로토콜 사용과 반대). 또한 WebSocket이 지원되지 않는 경우에도 여전히 작동하는지 확인해야합니다. 따라서 메시징 프로토콜을 처리하고 사용자가 사용할 수있는 훌륭한 이벤트 기반 메시지 시스템을 제공합니다.


우리가해야 할 첫 번째 일은 Socket.io 모듈을 설치하는 것입니다. Glitch에서는 package.json 파일로 이동하여 종속성에서 원하는 모듈을 입력하거나 패키지 추가를 클릭하고 "socket.io"를 입력하여이 작업을 수행 할 수 있습니다.

The add package menu can be found at the top of the code editor when selecting the file packagejson


이것은 서버 로그를 지적하기에 좋은 시간입니다. 왼쪽에있는 Logs 버튼을 클릭하여 서버 로그를 불러 오십시오. Socket.io를 모든 종속 항목과 함께 설치해야합니다. 여기서 오류 또는 서버 코드의 출력을 보러 갈 것입니다.

The Logs button is on the left side of the screen


이제 server.js로 이동하십시오. 이것이 서버 코드가 있는 곳입니다. 지금 당장은 HTML을 제공하기 위한 몇 가지 기본적인 상용구가 있습니다. Socket.io를 포함하도록 맨 위에 다음 행을 추가하십시오.

var io = require('socket.io')(http);


이제 클라이언트에 Socket.io를 포함시켜야하므로 index.html로 돌아가서 <head> 태그의 맨 위에 추가하십시오.

<!-- Load the Socket.io networking library -->

<script src="/socket.io/socket.io.js"></script>


참고 : Socket.io는 자동으로 해당 경로에서 클라이언트 라이브러리 제공을 처리하므로 폴더에 /socket.io/ 디렉토리가 없는데도이 행이 작동하는 이유입니다.


이제 Socket.io가 포함되고 준비 되었습니다!


2. 플레이어 탐지 및 생성

첫 번째 단계는 서버에서 연결을 수락하고 클라이언트에서 새 플레이어를 생성하는 것입니다.


서버에서 연결 수락

server.js의 맨 아래에 다음 코드를 추가하십시오.

// Tell Socket.io to start accepting connections

io.on('connection', function(socket){

    console.log("New client has connected with id:",socket.id);

})


이것은 클라이언트가 연결할 때 자동으로 트리거 되는 모든 연결 이벤트를 수신 대기하도록 Socket.io에 지시합니다. 각 클라이언트에 대해 새로운 소켓 객체를 생성합니다. 여기서 socket.id는 해당 클라이언트의 고유 식별자입니다.


이 작업이 제대로 작동하는지 확인하려면 클라이언트 (index.html)로 돌아가서이 함수를 create 함수의 어딘가에 추가하십시오.

var socket = io(); // This triggers the 'connection' event on the server


게임을 시작한 다음 서버 로그를 보면 (로그 버튼을 클릭하면) 해당 연결 이벤트가 기록됩니다.


이제 새로운 플레이어가 연결될 때, 우리는 그들이 우리에게 그들의 상태에 관한 정보를 보내길 기대합니다. 이 경우 적절한 위치에 올바르게 생성하려면 x, y 및 각도를 알아야합니다.


연결 이벤트는 Socket.io가 우리를 위해 시작하는 내장 이벤트였습니다. 우리는 우리가 원하는 모든 정의 된 이벤트를 들을 수 있습니다. 저는 new-player를 부를 것이고, 클라이언트가 그들의 위치에 관한 정보에 연결하자 마자 그것을 보내 줄 것으로 기대합니다. 이것은 다음과 같습니다.

// Tell Socket.io to start accepting connections

io.on('connection', function(socket){

    console.log("New client has connected with id:",socket.id);

    socket.on('new-player',function(state_data){

// Listen for new-player event on this client

      console.log("New player has state:",state_data);

    })

})


이 프로그램을 실행하면 서버 로그에 아무 것도 표시되지 않습니다. 우리가 클라이언트에게 이 새로운 플레이어 이벤트를 아직 내 보내지 않았기 때문입니다. 그러나 잠시 돌보고 서버를 계속 사용한다고 가정 해 봅시다. 우리가 접속한 새로운 플레이어의 위치를 받은 후에는 어떻게 해야합니까?


우리는 새로운 플레이어가 접속했다는 것을 알리기 위해 연결된 모든 다른 플레이어에게 메시지를 보낼 수 있습니다. Socket.io는 이렇게 하기위한 편리한 함수를 제공합니다 :

socket.broadcast.emit('create-player',state_data);


socket.emit을 호출하면 해당 클라이언트로 메시지가 다시 전송됩니다. socket.broadcast.emit을 호출하면 호출 된 하나의 소켓을 제외하고 서버에 연결된 모든 클라이언트로 소켓을 보냅니다.


io.emit을 사용하면 예외없이 서버에 연결된 모든 클라이언트에 메시지를 보냅니다. 게임을 시작할 때 이미 자신의 플레이어의 배를 만들었 기 때문에 자신의 배를 만들 것을 요청하는 서버에서 메시지를 받은 경우에는 중복 된 스프라이트가 생기기 때문에 현재 설정으로는 이를 원하지 않습니다. 다음은 이 튜토리얼에서 사용할 다양한 종류의 메시징 기능에 대한 유용한 정보 시트입니다.


서버 코드는 다음과 같아야합니다.

// Tell Socket.io to start accepting connections

io.on('connection', function(socket){

    console.log("New client has connected with id:",socket.id);

    socket.on('new-player',function(state_data){

// Listen for new-player event on this client

      console.log("New player has state:",state_data);

      socket.broadcast.emit('create-player',state_data);

    })

})


따라서 플레이어가 연결할 때마다 위치 데이터가 포함 된 메시지를 보내고 다른 모든 플레이어에게 해당 데이터를 보내서 해당 스프라이트를 생성 할 수있게합니다.


클라이언트에서 생성

이제 이주기를 완료하기 위해 클라이언트에서 두 가지 작업을 수행해야합니다.

  1. 우리가 연결되면 우리의 위치 데이터로 메시지를 내 보냅니다.

  2. 플레이어 생성 이벤트를 듣고 그 위치에있는 플레이어를 스폰합니다.


첫 번째 작업에서는 create 함수 (135 행 주변)에서 플레이어를 생성 한 후 다음과 같이 보내려는 위치 데이터가 포함 된 메시지를 내보낼 수 있습니다.

socket.emit('new-player',

{x:player.sprite.x,y:player.sprite.y,angle:player.sprite.rotation})

보내시는 데이터를 직렬화하는 것에 대해 걱정할 필요가 없습니다. 어떤 종류의 객체라도 전달할 수 있으며 Socket.io가 처리 할 수 있습니다.

New player has state: { x: 728.8180247836519, y: 261.9979387913289, angle: 0 }


우리 서버가 새로운 플레이어가 연결되었다는 발표와 더불어 위치 데이터를 올바르게 가져 오는 중임을 알고 있습니다!


다음으로, 우리는 새로운 플레이어를 만들기위한 요청을 듣고 싶습니다. 우리는 이 코드를 방출 한 직후에 배치 할 수 있으며 다음과 같이 보일 것입니다 :

socket.on('create-player',function(state){

  // CreateShip is a function I've already defined to create and return a sprite

  CreateShip(1,state.x,state.y,state.angle)

})


이제 테스트 해보세요. 게임의 두 창을 열고 작동하는지 확인하십시오.


두 개의 클라이언트를 연 후 첫 번째 클라이언트에는 두 개의 생성 된 배가 있고 두 번째 클라이언트에는 두 개의 클라이언트 만 표시됩니다.


첫 번째 플레이어가 연결되었을 때 서버가 다른 모든 플레이어에게 플레이어 생성 이벤트를 보냈지 만 수신 할 다른 플레이어가 없었습니다. 두 번째 플레이어가 연결되면 서버는 다시 브로드 캐스트를 전송하고 플레이어 1이 수신하여 올바르게 스프라이트를 생성하지만 플레이어 2는 플레이어 1의 초기 연결 브로드 캐스트를 놓쳤습니다.


따라서 플레이어 2가 게임의 후반에 참가하여 게임의 상태를 알아야 하기 때문에 문제가 발생합니다. 우리는 어떤 플레이어가 이미 존재하는지 (또는 이미 세계에서 일어난 일)를 연결하는 새로운 플레이어에게 그들이 따라 잡을 수 있도록 말할 필요가 있습니다. 이 문제를 해결하기 전에 간단한 경고가 있습니다.


게임 상태 동기화에 대한 경고

모든 플레이어의 게임을 동기화 상태로 유지하는 데는 두 가지 방법이 있습니다. 첫 번째는 네트워크를 통해 변경된 사항에 대한 최소한의 정보 만 전송하는 것입니다. 따라서 새 플레이어가 연결될 때마다 새 플레이어의 정보 만 다른 플레이어에게 보내고 (새 플레이어에게 전 세계 모든 다른 플레이어의 목록을 보냅니다) 연결을 끊으면 다른 모든 플레이어에게 이 개별 클라이언트가 연결이 끊어졌습니다.


두 번째 방법은 전체 게임 상태를 보내는 것입니다. 이 경우 연결 또는 연결 끊기가 발생할 때마다 모든 플레이어의 전체 목록을 모든 사람에게 보냅니다.


첫 번째는 네트워크를 통해 전송되는 정보를 최소화 한다는 점에서 더 좋지만 매우 까다로울 수 있으며 플레이어가 동기화되지 않을 위험이 있습니다. 두 번째 옵션은 플레이어가 항상 동기화되지만 각 메시지와 함께 더 많은 데이터를 보내는 것을 보장합니다.


우리의 경우, 새로운 플레이어가 연결되어 있을 때 메시지를 보내려고 하지 않고, 연결을 끊어서 삭제할 때, 그리고 자신의 위치를 업데이트하기 위해 이동했을 때, 그 모든 것을 하나의 업데이트 이벤트로 통합 할 수 있습니다 . 이 업데이트 이벤트는 모든 사용 가능한 플레이어의 위치를 항상 모든 클라이언트에게 보냅니다. 그것이 서버의 전부입니다. 클라이언트는 받은 상태로 최신 정보를 유지해야 합니다.


이를 구현하기 위해 다음과 같이 할 것입니다.

  1. 키가 자신의 ID이고 값이 위치 데이터 인 플레이어 사전을 보관하십시오.

  2. 업데이트 이벤트를 연결하고 보낼 때 플레이어를 이 사전에 추가하십시오.

  3. 플레이어가 연결을 끊고 업데이트 이벤트를 보낼 때이 사전에서 플레이어를 제거합니다.


이 단계는 매우 간단하기 때문에 스스로 구현할 수 있습니다 (치트 시트가 유용 할 수 있습니다). 전체 구현은 다음과 같습니다.

// Tell Socket.io to start accepting connections

// 1 - Keep a dictionary of all the players as key/value

var players = {};

io.on('connection', function(socket){

    console.log("New client has connected with id:",socket.id);

    socket.on('new-player',function(state_data){

// Listen for new-player event on this client

      console.log("New player has state:",state_data);

      // 2 - Add the new player to the dict

      players[socket.id] = state_data;

      // Send an update event

      io.emit('update-players',players);

    })

    socket.on('disconnect',function(){

      // 3- Delete from dict on disconnect

      delete players[socket.id];

      // Send an update event

    })

})


클라이언트 측은 조금 까다 롭습니다. 한 편, 업데이트 - 플레이어 이벤트에 대해서만 걱정할 필요가 있습니다. 그러나 서버가 우리가 알고있는 것보다 더 많은 배를 보내거나 너무 많은 배를 보내면 더 많은 배를 만드는 것에 대해 고려해야합니다 .


다음은 클라이언트에서 이 이벤트를 처리 한 방법입니다.

// Listen for other players connecting

// NOTE: You must have other_players = {} defined somewhere

socket.on('update-players',function(players_data){

    var players_found = {};

    // Loop over all the player data received

    for(var id in players_data){

        // If the player hasn't been created yet

        if(other_players[id] == undefined && id != socket.id){

// Make sure you don't create yourself

            var data = players_data[id];

            var p = CreateShip(1,data.x,data.y,data.angle);

            other_players[id] = p;

            console.log("Created new player at (" + data.x + ", " + data.y + ")");

        }

        players_found[id] = true;

         

        // Update positions of other players

        if(id != socket.id){

// Update target, not actual position, so we can interpolate

          other_players[id].x  = players_data[id].x;

          other_players[id].y  = players_data[id].y;

          other_players[id].rotation  = players_data[id].angle;

        }

                 

    }

    // Check if a player is missing and delete them

    for(var id in other_players){

        if(!players_found[id]){

            other_players[id].destroy();

            delete other_players[id];

        }

    }    

})


나는 스크립트의 맨 위에 정의 된 other_players라는 사전에서 클라이언트의 배송 정보를 추적합니다 (여기에 표시되지 않음). 서버가 모든 플레이어에게 플레이어 데이터를 전송하기 때문에 클라이언트가 별도의 스프라이트를 작성하지 않도록 체크를 추가해야합니다. (구조화에 어려움이있는 경우 여기에 index.html에있는 전체 코드가 있습니다.)


이제 이것을 시험해보십시오. 여러 클라이언트를 만들고 닫을 수 있어야 하며 올바른 위치에 출현하는 선박의 수를 파악할 수 있어야합니다.


3. 클라이언트 위치 동기화

여기 재미있는 부분으로 갑니다. 우리는 실제로 현재 모든 클라이언트에 걸쳐 배송 위치를 동기화 하려고합니다. 이것은 우리가 지금까지 구축 한 구조의 단순성이 실제로 보여주는 곳입니다. 모든 사용자의 위치를 동기화 할 수있는 업데이트 이벤트가 이미 있습니다. 우리가해야 할 일은 다음과 같습니다.


  1. 클라이언트가 새 위치로 이동할 때마다 클라이언트를 내보내도록 하십시오.

  2. 서버가 해당 이동 메시지를 수신 대기하게 하고 플레이어 사전에서 해당 플레이어의 항목을 업데이트합니다.

  3. 모든 클라이언트에 업데이트 이벤트를 내 보냅니다.


힌트가 필요한 경우 최종 완성 프로젝트를 참조 할 수 있습니다.


네트워크 데이터 최소화에 대한 참고 사항

이를 구현하는 가장 간단한 방법은 모든 플레이어가 새로운 플레이어로 이동 메시지를 받을 때마다 모든 플레이어를 새로운 위치로 업데이트하는 것입니다. 이는 플레이어가 가능한 한 빨리 최신 정보를 수신한다는 점에서 훌륭하지만 네트워크를 통해 전송되는 메시지의 수는 프레임 당 수백 개까지 쉽게 증가 할 수 있습니다. 10 명의 플레이어가 있고 각 플레이어가 매 프레임마다 이동 메시지를 보내는 경우 서버가 모든 10 명의 플레이어에게 릴레이를 다시 수행해야한다고 가정 해보십시오. 그것은 이미 프레임 당 100 개의 메시지입니다!


더 좋은 방법은 모든 정보를 포함하는 큰 업데이트를 모든 플레이어에게 보내기 전에 서버가 플레이어로부터 모든 메시지를 수신 할 때까지 기다리는 것입니다. 그런 식으로 당신은 당신이 게임에 가지고있는 플레이어의 수로 보내는 메시지의 수를 스쿼시합니다 (그 수의 제곱이 아닌). 그러나 그 문제는 모든 사람이 게임에서 가장 느린 연결을 가진 플레이어만큼 많은 시간을 경험하게된다는 것입니다.


또 다른 방법은 플레이어가 지금까지 받은 메시지 수에 관계없이 서버가 업데이트를 일정한 속도로 보내도록하는 것입니다. 초당 30 회 정도 서버를 업데이트하는 것이 일반적인 표준처럼 보입니다.


그러나 서버를 구조화 하기로 결정한 후에는 게임을 개발할 때마다 모든 프레임을 얼마 전 보내는 메시지에 주의해야합니다.


4. 총알 동기화

마지막 큰 조각은 총알을 네트워크를 통해 동기화 하는 것입니다. 우리는 플레이어를 동기화하는 것과 같은 방식으로 할 수 있습니다.

  - 각 클라이언트는 매 프레임마다 모든 글 머리 기호의 위치를 보냅니다.

  - 서버는 그것을 모든 플레이어에게 전달합니다.


그러나 문제가 있습니다.


속임수에 대하여 확보

총알의 진정한 위치로 클라이언트가 보낸 메시지를 전달하면 플레이어는 다른 선박이있는 곳으로 텔레 포트하는 총알과 같은 가짜 데이터를 보내도록 클라이언트를 수정하여 속일 수 있습니다. 웹 페이지를 다운로드하고 JavaScript를 수정 한 다음 다시 실행하면 쉽게이 문제를 해결할 수 있습니다. 브라우저 용 게임의 경우 문제가 아닙니다. 일반적으로 클라이언트에서 오는 데이터는 절대 신뢰할 수 없습니다.


이를 막기 위해 다른 계획을 세웁니다.

  - 클라이언트는 총알의 위치와 방향을 발사 할 때마다 내 보낸다.

  - 서버는 총알의 움직임을 시뮬레이션합니다.

  - 서버는 각 클라이언트를 모든 총알의 위치를 업데이트합니다.

  - 클라이언트는 서버가 수신 한 위치에 총알을 렌더링합니다.


이 방법은 클라이언트가 총알이 어디에서 생기는지 알려주지 만 움직이는 속도가 빠르거나 어디로가는 지 추적하지 않습니다. 클라이언트는 자신의 보기에서 총알의 위치를 변경할 수 있지만 다른 클라이언트가 보는 것을 변경할 수는 없습니다.


자, 이것을 구현하기 위해, 나는 당신이 쏠 때 방출을 추가 할 것입니다. 나는 실제 스프라이트를 만들지 않을 것입니다. 왜냐하면 그것의 존재와 위치가 서버에 의해 완전히 결정되기 때문입니다. index.html의 새로운 총알 코드는 다음과 같아야 합니다.

// Shoot bullet

if(game.input.activePointer.leftButton.isDown && !this.shot){

    var speed_x = Math.cos(this.sprite.rotation + Math.PI/2) * 20;

    var speed_y = Math.sin(this.sprite.rotation + Math.PI/2) * 20;

    /* The server is now simulating the bullets,

clients are just rendering bullet locations, so no need to do this anymore

    var bullet = {};

    bullet.speed_x = speed_x;

    bullet.speed_y = speed_y;

    bullet.sprite = game.add.sprite(this.sprite.x +

bullet.speed_x,this.sprite.y +

bullet.speed_y,'bullet');

    bullet_array.push(bullet);

    */

    this.shot = true;

    // Tell the server we shot a bullet

    socket.emit('shoot-bullet',{x:this.sprite.x,

y:this.sprite.y,

angle:this.sprite.rotation,

speed_x:speed_x,

speed_y:speed_y})

}


또한 클라이언트의 총알을 업데이트하는 이 전체 섹션을 주석 처리 할 수 있습니다.

/* We're updating the bullets on the server,

so we don't need to do this on the client anymore

// Update bullets

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

    var bullet = bullet_array[i];

    bullet.sprite.x += bullet.speed_x;

    bullet.sprite.y += bullet.speed_y;

    // Remove if it goes too far off screen

    if(bullet.sprite.x < -10 ||

bullet.sprite.x > WORLD_SIZE.w ||

bullet.sprite.y < -10 ||

bullet.sprite.y > WORLD_SIZE.h){

        bullet.sprite.destroy();

        bullet_array.splice(i,1);

        i--;

    }

}

*/


마지막으로, 클라이언트에게 총알 업데이트를 수신하도록 요청해야합니다. 플레이어가 bullet-update라고 하는 이벤트에서 모든 총알 위치의 배열을 전송하는 것과 동일한 방식으로 처리합니다. 클라이언트는 총알을 만들거나 파괴하여 동기화 상태를 유지합니다. 다음은 그 모습입니다.

// Listen for bullet update events

socket.on('bullets-update',function(server_bullet_array){

  // If there's not enough bullets on the client, create them

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

      if(bullet_array[i] == undefined){

          bullet_array[i] = game.add.sprite(server_bullet_array[i].x,

server_bullet_array[i].y,

'bullet');

      } else {

          //Otherwise, just update it!

          bullet_array[i].x = server_bullet_array[i].x;

          bullet_array[i].y = server_bullet_array[i].y;

      }

  }

  // Otherwise if there's too many, delete the extra

  for(var i=server_bullet_array.length;i<bullet_array.length;i++){

       bullet_array[i].destroy();

       bullet_array.splice(i,1);

       i--;

   }

                   

                })


그것은 클라이언트의 모든 것입니다. 나는 이 부분을 어디에 넣을 지, 그리고 이 시점에서 모든 것을 함께 모으는 방법을 알고 있다고 가정하고 있지만, 어떤 문제에 부딪히는 경우 참조를 위해 항상 최종 결과를 살펴볼 수 있다는 것을 기억하십시오.


이제 server.js에서 총알을 추적하고 시뮬레이션 해야 합니다. 먼저 우리가 플레이어와 동일한 방법으로 총알을 추적 할 수 있는 배열을 만듭니다.

var bullet_array = []; // Keeps track of all the bullets to update them on the server


다음으로 우리는 총알 발사 이벤트를 작성합니다.

// Listen for shoot-bullet events and add it to our bullet array

  socket.on('shoot-bullet',function(data){

    if(players[socket.id] == undefined) return;

    var new_bullet = data;

    data.owner_id = socket.id; // Attach id of the player to the bullet

    bullet_array.push(new_bullet);

  });


이제 초당 60 번 총알을 시뮬레이션합니다.

// Update the bullets 60 times per frame and send updates

function ServerGameLoop(){

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

    var bullet = bullet_array[i];

    bullet.x += bullet.speed_x;

    bullet.y += bullet.speed_y;

     

    // Remove if it goes too far off screen

    if(bullet.x < -10 || bullet.x > 1000 || bullet.y < -10 || bullet.y > 1000){

        bullet_array.splice(i,1);

        i--;

    }         

  }   

}

 

setInterval(ServerGameLoop, 16);


그리고 마지막 단계는 그 함수의 어딘가에 업데이트 이벤트를 보내는 것입니다 (그러나 확실히 for 루프 바깥 쪽).

// Tell everyone where all the bullets are by sending the whole array

  io.emit("bullets-update",bullet_array);


이제 실제로 테스트 할 수 있습니다! 모든 것이 잘 진행되면 총알이 클라이언트간에 올바르게 동기화되는지 확인해야합니다. 우리가 서버에서 이 작업을 수행했다는 사실은 더 많은 작업 일뿐 아니라 훨씬 더 많은 제어 기능을 제공합니다. 예를 들어 총알 발사 이벤트가 발생하면 총알의 속도가 특정 범위 내에 있는지 확인할 수 있습니다. 그렇지 않으면 이 플레이어가 속임수 임을 알 수 있습니다.


5. 총알 충돌 판정

이것이 우리가 구현할 마지막 핵심 기술자입니다. 이제는 구현을 계획하는 절차에 익숙해 져서 클라이언트 구현을 완전히 끝내기 전에 먼저 서버로 이동하는 것이 좋을 것입니다. 이것은 구현할 때 앞뒤로 전환하는 것보다 오류가 발생하기 쉬운 방법이 아닙니다.


충돌을 확인하는 것은 중요한 게임 플레이 메커닉이므로 치트 프루프가 되고 싶습니다. 우리는 총알에 대해서도 서버에서 구현할 것입니다. 우리가해야 할 일은 :


  - 총알이 서버의 모든 플레이어와 충분히 가까웠는지 확인하십시오.

  - 특정 플레이어가 공격을받을 때마다 모든 클라이언트에게 이벤트를 내 보냅니다.

  - 클라이언트가 히트 이벤트를 듣고 우주선이 맞았을 때 우주선이 깜박이도록 하십시오.


이 작업을 직접 수행 할 수 있습니다. 히트시 플레이어가 깜박이도록 하려면 알파 값을 0으로 설정하면됩니다.

player.sprite.alpha = 0;


그리고 다시 완전한 알파로 다시 돌아갑니다 (이것은 플레이어 업데이트에서 수행됩니다). 다른 플레이어의 경우 비슷한 일을 할 것이지만 업데이트 기능에서 다음과 같이 알파를 다시 가져와야합니다.

for(var id in other_players){

 if(other_players[id].alpha < 1){

        other_players[id].alpha += (1 - other_players[id].alpha) * 0.16;

    } else {

        other_players[id].alpha = 1;

    }

}


당신이 처리해야 할 까다로운 부분은 플레이어 자신의 총알이 그들을 맞출 수 없는지 확인하는 것입니다 (그렇지 않으면 당신은 항상 당신이 발사 할 때마다 자신의 총알로 명중 할 수 있습니다).


이 구성표에서 클라이언트가 속임수를 쓰려고 시도하고 서버가 전송 한 적중 메시지를 확인하지 않더라도 자신의 화면에서 볼 수있는 내용 만 변경합니다. 다른 모든 플레이어는 여전히 그 플레이어가 맞았다는 것을 볼 것입니다.


6. 매끄러운 이동

이 단계까지 모든 단계를 수행했다면, 축하드립니다. 방금 멀티 플레이어 게임을 만들었습니다! 친구에게 보내고 온라인 멀티 플레이어들을 지켜보십시오!


게임은 완벽하게 작동하지만 우리의 작업이 멈추지 않습니다. 우리가 해결해야 할 플레이어의 경험에 영향을 미칠 수있는 몇 가지 문제가 있습니다.

  - 모든 플레이어가 빠르게 연결되지 않으면 다른 플레이어의 움직임이 고르지 않게 보일 것입니다.

  - 총알이 즉시 발사되지 않기 때문에 총알이 반응이 느껴질 수 있습니다. 클라이언트의 화면에 나타나기 전에 서버에서 메시지를 다시 기다립니다.


우리는 클라이언트에있는 배에 대한 위치 데이터를 보간하여 첫 번째 것을 고칠 수 있습니다. 그래서 우리가 충분히 빠른 업데이트를받지 못한다고해도, 우리는 우주선을 순간 이동하는 것과 반대되는 방향으로 부드럽게 움직일 수 있습니다.


총알은 조금 더 정교해질 것입니다. 우리는 서버가 총알을 관리하기를 원합니다. 왜냐하면 그것은 치트 프루프 (cheat-proof) 방식이기 때문입니다. 그러나 우리는 총알을 발사하고 총을 쏘는 것을 즉각 피드백하고 싶습니다. 가장 좋은 방법은 하이브리드 방식입니다. 서버와 클라이언트 모두 총알 위치를 업데이트하는 서버와 함께 총알을 시뮬레이트 할 수 있습니다. 동기화가 안되면 서버가 맞다고 클라이언트의 총알 위치를 무시하십시오.


위에 설명 된 글 머리 기호 시스템을 구현하는 것은이 자습서의 범위를 벗어나지 만이 방법이 존재한다는 것을 알고있는 것이 좋습니다.


배의 위치에 대한 간단한 보간법은 매우 쉽습니다. 새 위치 데이터를 처음 수신하는 업데이트 이벤트에서 직접 위치를 설정하는 대신 목표 위치를 저장하기 만하면됩니다.

// Update positions of other players

if(id != socket.id){

  other_players[id].target_x  = players_data[id].x;

// Update target, not actual position, so we can interpolate

  other_players[id].target_y  = players_data[id].y;

  other_players[id].target_rotation  = players_data[id].angle;

}


그런 다음 업데이트 기능 (여전히 클라이언트에 있음)에서 다른 모든 플레이어를 반복하여이 대상을 향해 푸시합니다.

for(var id in other_players){

    var p = other_players[id];

    if(p.target_x != undefined){

        p.x += (p.target_x - p.x) * 0.16;

        p.y += (p.target_y - p.y) * 0.16;

        // Interpolate angle while avoiding the positive/negative issue

        var angle = p.target_rotation;

        var dir = (angle - p.rotation) / (Math.PI * 2);

        dir -= Math.round(dir);

        dir = dir * Math.PI * 2;

        p.rotation += dir * 0.16;

    }

}


이 방법으로 서버가 초당 30 번 업데이트를 보내도록 할 수 있지만 여전히 60fps로 게임을 실행하면 매끄럽게 보입니다!


결론

휴! 우리는 방금 많은 것을 다루었습니다. 간단히 요약하면 클라이언트와 서버간에 메시지를 보내는 방법과 서버가 모든 플레이어에게 메시지를 전달하도록 하여 게임 상태를 동기화하는 방법을 살펴 보았습니다. 이것은 온라인 멀티 플레이 경험을 만드는 가장 간단한 방법입니다.


또한 서버의 중요한 부분을 시뮬레이션하고 클라이언트에게 결과를 알리는 방법으로 치팅에 대해 게임을 보안 할 수있는 방법을 알아 냈습니다. 당신이 당신의 클라이언트를 신뢰할수록, 게임은 더 안전해질 것입니다.


마지막으로, 우리는 클라이언트에서 보간함으로써 랙을 극복하는 방법을 보았습니다. 랙 보상은 광범위한 주제이며 매우 중요합니다 (일부 게임은 충분히 지연없이 충분히 재생할 수 없게됩니다). 서버에서 다음 업데이트를 기다리는 동안 보완하는 것은 그것을 완화하는 한 가지 방법 일뿐입니다. 또 다른 방법은 다음 몇 개의 프레임을 미리 예측하고 서버에서 실제 데이터를 받으면 수정하는 것입니다. 물론 이것은 매우 까다로울 수 있습니다.


지연의 영향을 완화하는 완전히 다른 방법은 그 주위를 설계하는 것입니다. 배가 천천히 움직이게하는 이점은 고유 한 이동 메커니즘과 이동의 갑작스러운 변화를 막는 방법으로 작용합니다. 따라서 느린 연결을 하더라도 여전히 경험을 망치지는 않습니다. 이와 같이 게임의 핵심 요소를 디자인하는 동안 지연에 대한 설명은 큰 차이를 만들 수 있습니다. 때로는 최상의 솔루션은 전혀 기술적이지 않습니다.


유용 할 수도있는 Glitch의 마지막 기능 중 하나는 왼쪽 상단의 고급 설정으로 이동하여 프로젝트를 다운로드하거나 내보낼 수 있다는 것입니다.

The advanced options menu allows you to import export or download your project


멋진 소식을 올리려면 아래의 의견에서 공유하십시오! 또는 질문이나 명확한 설명이 있으면 언제든지 도와 드리겠습니다.



Posted by 마스터킹
|

이것은 JavaScript 및 Phaser로 Slither.io를 만드는 튜토리얼 시리즈의 네 번째 파트입니다! 


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


충돌 개념

이 부분에서는 snake.js를 수정합니다. 본질적으로, 우리가 하고 싶은 것은 다른 뱀의 섹션과 충돌 할 수 있는 뱀의 전면에 포인트를 추가하는 것입니다. 다음과 같이 보입니다.

각 뱀의 앞쪽에서 원형 물리 바디 "포인트"를 볼 수 있습니다. 게임에서 이 피직스 바디를 보려면 뱀의 디버그 속성을 true로 설정하십시오.


Part 2의 뱀 섹션에 이미 물리 바디를 추가 했으므로 뱀 앞에 서클 본문을 만드는 것이 필요합니다. 이 원을 뱀의 "가장자리"라고 할 것 이므로 뱀의 가장자리가 섹션 바디와 충돌 할 때 관심이 있습니다. 가장자리를 머리 앞쪽에 유지하려면 잠금 제한을 사용하여 뱀이 회전하거나 움직일 수 있도록하고 이 가장자리는 앞으로 유지됩니다.


가장자리 만들기

먼저 snake.js 소스의 Snake 함수에 다음 줄을 추가했습니다.


// 가장자리는 다른 뱀과 충돌 할 수있는 앞쪽 몸체입니다.

// 이 뱀의 머리에 고정되어있다.

this.edgeOffset = 4;

this.edge = this.game.add.sprite (x, y - this.edgeOffset, this.spriteKey);

this.edge.name = "edge";

this.edge.alpha = 0;

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

this.edge.body.setCircle (this.edgeOffset);


// 가장자리를 머리 앞쪽으로 제한합니다.

this.edgeLock = this.game.physics.p2.createLockConstraint (

     this.edge.body, this.head.body, [0, -this.head.width * 0.5-this.edgeOffset]

);


this.edge.body.onBeginContact.add (this.edgeContact, this);


우리는 스프라이트를 보이지 않게 만들고, 그런 다음 가장자리에 원형 물리바디를 줍니다. 이 바디를 특정 오프셋에서 머리 앞에 고정시키는 잠금 구속 조건을 만듭니다. 이 가장자리가 다른 본문과 접촉하기 시작할 때 콜백을 추가합니다.


에지 연결

가장자리가 무언가와 충돌 할 때 우리가 하는 일을 살펴 봅시다.


edgeContact : function (phaserBody) {

     // 가장자리가 다른 뱀의 섹션을 치는 경우이 뱀을 파괴합니다.

     if (phaserBody && this.sections.indexOf (phaserBody.sprite) == -1) {

         this.destroy ();

     }

     // 가장자리가이 뱀 자신의 섹션에 닿는 경우 글리치를 피하는 간단한 해결책은

     // 가장자리를 중심으로 이동하는 것입니다.

     // 그러면 잠금 제한으로 인해 맨 앞으로 이동합니다

     else if (phaserBody) {

         this.edge.body.x = this.head.body.x;

         this.edge.body.y = this.head.body.y;

     }

}


우리는 가장자리가 다른 뱀의 단면 또는이 뱀의 단면을 공격했는지 알아 낼수 있습니다. 그것이 다른 뱀을 치면 우리는 파괴 방법을 사용하여 이 뱀을 파괴합니다. 이 뱀을 때렸다면, 우리는 머리를 단순히 재설정하기 위해 머리의 중심으로 이동시킵니다. 이 솔루션은 충돌 그룹을 사용하여 이 뱀의 가장자리가 자신의 섹션을 치는 것을 방지하기 위한 대안입니다. 우리가 사용하는 솔루션은 간단하지만 구현하기가 훨씬 쉽습니다.


파괴

뱀이 파괴되면, 우리는 또한 가장자리를 파괴해야합니다. destroy 메소드에 이 코드를 추가했습니다.


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

this.edge.destroy ();


그들이 제한하는 스프라이트 바디와 함께 제약 조건을 파괴하는 것이 항상 중요합니다.


스케일링

뱀의 머리가 커지면 그 엣지는 더욱 더 제약을 받아야 합니다. 이 코드를 setScale 메서드에 추가합니다.


// p2 physics로 엣지 잠금 위치 업데이트

this.edgeLock.localOffsetB = [

     0, this.game.physics.p2.pxmi (this.head.width * 0.5 + this.edgeOffset)

];


순수한 P2 물리를 사용하여 잠금 구속 조건을 업데이트하고 새로운 헤드 스케일을 기반으로 헤드에서 추가 오프셋에 에지를 배치합니다. P2 잠금 제한 등록 정보는 여기에서 읽을 수 있습니다.


그리고 그것은 게임에서 뱀 충돌을 구현하는 데 필요한 모든 것입니다. 우리의 작업은 대부분 물리학 자들로 철저한 스네이크 수업을 만들고 방법을 파괴함으로써 이미 완료되었습니다. 제 5 부에서는 우리가 뱀에게 눈을 보충하기 위해 노력할 것입니다.





Posted by 마스터킹
|

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


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


스네이크 확장하기

제 2 부에서 우리의 뱀은 앞으로 나아갈 수 있습니다. 이제 우리는 뱀 머리를 왼쪽과 오른쪽으로 돌리는 작업을해야합니다. Snake 클래스에서 이 기능을 추가하는 대신, Snake를 확장 할 BotSnake 및 PlayerSnake에 추가 할 것입니다. 왜 우리는 이것을 갈라 놓아야 합니까? 플레이어의 뱀은 커서 위치 또는 화살표 키로 제어되기 때문에 봇은 단순히 임의로 회전합니다. 이러한 차별화는 코드에 들어갈 때 명확 해집니다.



BOT SNAKE

botSnake.js의 BotSnake 클래스를 먼저 살펴 보겠습니다. 우리는 함수를 생성 한 다음 뱀을 상속받습니다.


BotSnake = function (게임, spriteKey, x, y) {

     Snake.call (this, game, spriteKey, x, y);

     this.trend = 1;

}

BotSnake.prototype = Object.create (Snake.prototype);

BotSnake.prototype.constructor = BotSnake;


함수 호출 메서드를 사용하여 Snake를 상속받습니다. 우리는 BotSnake에 고유 한 trend라는 필드를 추가합니다. 이 상속에는 Snake 프로토 타입이 포함되어 있지 않으므로 Prototype을 Object.create ()로 복제하고 BotSnake 프로토 타입과 동일한 값으로 설정해야합니다.


이제 BotSnake는 Snake와 똑같은 기능을하므로 기능을 확장해야 합니다. 특히 update 메서드에 추가하려고합니다. 우리가 어떻게 하는지 보시죠:


BotSnake.prototype.tempUpdate = BotSnake.prototype.update;

BotSnake.prototype.update = function () {

     this.head.body.setZeroRotation ();


     // 로봇이 한 방향으로 계속 회전하도록합니다.

     // 길 찾기 전환 전의 상당한 시간

     if (Util.randomInt (1,20) == 1) {

         this.trend * = -1;

     }

     this.head.body.rotateRight (this.trend * this.rotationSpeed);

     this.tempUpdate ();

}


먼저 tempUpdate라는 원본 업데이트 메서드의 복사본을 만듭니다. 우리는 실제 업데이트 방법에 추가 할 수 있도록이 작업을 수행 한 다음 끝에 tempUpdate를 호출합니다. Part 2에서 보았 듯이, Snake update 메소드는 우리가 잃고 싶지 않은 중요한 기능을 가지고 있습니다.


새로운 업데이트 방법에서는 뱀의 머리를 왼쪽이나 오른쪽으로 돌린 다음 특정 시간 후에 회전 방향을 전환하는 것입니다.


Player Snake

이제 playerSnake.js에서 PlayerSnake를 살펴 보겠습니다.


PlayerSnake = function (game, spriteKey, x, y) {

     Snake.call (this, game, spriteKey, x, y);

     this.cursors = game.input.keyboard.createCursorKeys();


     // 플레이어의 뱀이 속도를 낼 수 있도록 스페이스 키를 처리합니다.

     var spaceKey = this.game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR);

     var self = this;

     spaceKey.onDown.add(this.spaceKeyDown, this);

     spaceKey.onUp.add(this.spaceKeyUp, this);

     this.addDestroyedCallback(function () {

         spaceKey.onDown.remove(this.spaceKeyDown, this);

         spaceKey.onUp.remove(this.spaceKeyUp, this);

     }, this);

}


PlayerSnake.prototype = Object.create (Snake.prototype);

PlayerSnake.prototype.constructor = PlayerSnake;


// 스페이스 키가 눌려지면 이 뱀을 켜고 속도를 높이십시오.

PlayerSnake.prototype.spaceKeyDown = function () {

     this.speed = this.fastSpeed;

}

// 스페이스 키가 다시 올라 오면 뱀을 느리게 만듭니다.

PlayerSnake.prototype.spaceKeyUp = function () {

     this.speed = this.slowSpeed;

}


우리가 BotSnake에서 했던 것처럼, 우리는 Snake를 상속 받고 프로토 타입을 복제합니다. 그런 다음 공간 키가 작아지면 이 플레이어의 뱀이 더 빨리 움직 이도록 만듭니다. 뱀에 대해 Part 2에서 작성한 addDestroyedCallback 메소드를 사용하는 방법에 주목하십시오.


이제 우리는 봇 스네이크에서했던 것처럼 새 업데이트 메서드를 만듭니다.


PlayerSnake.prototype.tempUpdate = PlayerSnake.prototype.update;

PlayerSnake.prototype.update = function () {

    // 머리가 회전해야하는 각도를 찾습니다.

    // 마우스를 마주 치기 위해 통과

    var mousePosX = this.game.input.activePointer.worldX;

    var mousePosY = this.game.input.activePointer.worldY;

    var headX = this.head.body.x;

    var headY = this.head.body.y;

    var angle = (180 * Math.atan2 (mousePosX-headX, mousePosY-headY) /Math.PI);

    if (angle> 0) {

        angle = 180- angle;

    }

    else {

        angle = -180-angle;

    }

    var dif = this.head.body.angle - angle;

    this.head.body.setZeroRotation ();

    // 화살표 키 사용 허용

    if (this.cursors.left.isDown) {

        this.head.body.rotateLeft (this.rotationSpeed);

    }

    else if (this.cursors.right.isDown) {

        this.head.body.rotateRight (this.rotationSpeed);

    }

    // 왼쪽 또는 오른쪽으로 회전하면 머리를 기울일 지 여부를 결정합니다.

    // 화살표 키를 사용하지 않으면 마우스가 빨라집니다.

    else if (dif <0 && dif> -180 || dif> 180) {

        this.head.body.rotateRight (this.rotationSpeed);

    }

    else if (dif> 0 && dif <180 || dif <-180) {

        this.head.body.rotateLeft (this.rotationSpeed);

    }


    // 원래 뱀 업데이트 메서드를 호출합니다.

    this.tempUpdate ();

}


플레이어 뱀의 업데이트 메소드에 추가 한 것은 화살표 키를 기준으로 처음으로 전환하는 기능입니다. 화살표 키가 눌러지지 않은 경우, 뱀은 커서를 향해 더 빨리 가리키는 방향으로 회전합니다. 본질적으로, 그것은 뱀의 현재 방향과 머리와 커서가 형성하는 선 사이의 각도가 작은면을 찾습니다.


game.js

마지막으로, 우리는 게임에 이 뱀을 추가해야합니다! 우리는 게임 상태의 생성 메소드에서 이것을 해야합니다 :


// 플레이어를 만듭니다.

var snake = new PlayerSnake (this.game, 'circle', 0, 0);

this.game.camera.follow (snake.head);


// 봇을 만듭니다.

new BotSnake (this.game, 'circle', -200, 0);

new BotSnake (this.game, 'circle', 200, 0);


그리고 우리 선수와 로봇이 작동합니다! Part 4에서는 뱀 사이의 충돌을 처리 할 것입니다.


Posted by 마스터킹
|

이제는 인터넷 브라우저 용 게임을 개발하는 개발자들에게 대단히 흥미 진진한 시간이되었습니다. 이 게임 프레임 워크는 게임 개발을위한 친숙하고 직관적 인 생태계를 제공하고 우수한 Canvas 및 WebGL 렌더링 성능을 위한 우수한 Pixi.js를 활용합니다.


- 시작하기

Yeoman이 컴퓨터에 설치되어 있어야합니다. 따라서 가지고 있지 않다면 웹 사이트의 지침을 따르십시오. Yeoman이 게임을 시작하면 Yeoman 프로젝트 템플릿을 설치하여 게임 개발을 시작하십시오. 게임을 만들 때 많은 반복을 줄일 수 있습니다.

Yeoman 프로젝트 템플릿 생성기를 설치하려면 새 터미널 창을 열고 다음을 입력하십시오 (Mac의 경우 sudo로 접두사를 붙이십시오).

npm install -g yo generator-phaser-official


우리가해야 할 첫 번째 일은 모든 것을 테스트하기 위해 프로젝트를 생성하고 컴파일하는 것입니다. 게임에 사용할 새 폴더를 만들고 현재 터미널 디렉토리에서이 폴더로 이동 한 후 다음을 입력하십시오.

yo phaser-official


Yeoman은 게임 이름 (우리는 'netmag-phaser'를 사용했습니다), 사용하려는 Phaser 버전 (2.0.0) 및 게임의 너비 (900)를 템플릿 작성 중에 몇 가지 질문에 대한 답을 묻습니다. ) 및 높이 (480). Yeoman이 프로젝트 생성을 마쳤 으면 명령 줄에서 grunt를 실행하여 모든 것이 올바르게 설정되었는지 확인하십시오.

브라우저가 열리고이 프로젝트에 포함 된 기본 게임이 실행되기 시작합니다. 


- 구조와 상태

프로젝트의 구조를 살펴보면 다음과 같이 몇 가지 주요 폴더를 만들 수 있습니다.

/assets - 이미지 애셋을 포함하는 폴더

/game - 게임의 JavaScript 파일

/game/state - 게임의 상태


/dist - 온라인 호스팅을위한 컴파일 된 게임


게임과 상태 디렉토리에서 대부분의 시간을 보내 게됩니다. 이들은 핵심 프로젝트 파일입니다. 각 게임은 여러 가지 상태로 구성되어 있습니다.  게임의 여러 단계로 생각할 수 있습니다. 게임에서는 Preload, Menu, Play, Gameover 등 네 가지 상태 만 사용합니다. 이 JavaScript 파일은 /game/states 폴더에서 볼 수 있습니다.


각 상태는 공통 레이아웃으로 구성됩니다. 템플릿 상태를 보면 가장 일반적인 메소드가 Preload, Create 및 Update라는 것을 알 수 있습니다.


Preload - 상태가 생성되기 전에 자산 또는 데이터 사전로드를 배치 할 수있는 장소를 제공합니다.

Create - 우리가 객체를 인스턴스화하고, 그래픽을 배치하고, 재생할 준비가 된 상태를 설정합니다.


Update - 주 상태의 틱카운트로 생각할 수 있습니다. 이 함수는 초당 60 회 실행되며 게임 논리의 핵심을 포함합니다


이제 이 튜토리얼의 GitHub 저장소를 다운로드하거나 복제하기 좋은 시간입니다. 내부에는 이 게임의 이미지가 포함 된 저작물이라는 제목의 게임을 포함하여 게임의 여러 단계를위한 여러 폴더가 있습니다. 먼저 이 폴더의 전체 내용을 게임의 /assets 폴더에 복사합니다.


- 식별자

Preload.js 상태로 점프하면 게임이 시작되기 전에 복사 된 애셋을 미리 로드하도록 설정할 수 있습니다.

preload.js preload () 메소드에 다음을 추가하십시오.

this.load.image('menu', 'assets/menu.jpg');

this.load.image('background', 'assets/background.jpg');

this.load.image('player', 'assets/player.png');

this.load.image('creature_1', 'assets/creature_1.png');

this.load.image('creature_2', 'assets/creature_2.png');

this.load.image('creature_3', 'assets/creature_3.png');


this.load.image('gameover', 'assets/gameover.jpg');


load.image 메서드는 두 개의 매개 변수를 사용합니다. 첫 번째는 이미지가 로드 된 인스턴스를 생성해야하고 두 번째 매개 변수가 이미지의 경로 인 경우 이미지를 참조하는 데 사용할 문자열 기반 식별자입니다.


다음은 게임의 메인 상태입니다. 목표는 가능한 한 많은 생물을 스쿼시하는 것입니다.


다음은 게임의 메인 메뉴 장면입니다. Menu.js 상태를 열고 create () 메서드의 전체 내용을 제거하고 다음 내용으로 바꿉니다.

this.title = this.game.add.sprite(0, 0, 'menu');


Preload에서 자산을 제공 한 식별자를 기억하십니까? 우리는 단순히 game.add.sprite ()를 사용하고 수평 및 수직 위치 0을 부여한 다음 기본 메뉴 이미지의 사전로드 상태에서 지정한 메뉴 식별자를 사용하기만 하면됩니다.


터미널에서 Grunt를 실행하여 진행 상황을 테스트하여 브라우저에서 결과가로드되는지 확인하십시오. 게임은 그래픽이있는 새로운 메뉴로 부팅되어야 합니다.


- 기능적 구조

이 섹션에서는 게임구조를 살펴 보겠지만, 먼저 기본 게임 클래스를 만들었습니다. 함께 제공되는 소스 코드에서 3-Play라는 폴더에서 Play.js를 복사하고 현재 Play.js를 /game/states에 덮어 씁니다.

코드를 살펴보면, 프레임 워크의 몇 가지 중요한 라인이 지적됩니다. 10, 13, 14 행은 객체를 유지하기 위한 경계로 사용되는 직사각형을 만듭니다. 이 라인들은 Phaser의 내장형 아케이드 물리 시스템을 가능하게합니다.


17 번에서 21 번 라인은 우리 플레이어를 만듭니다. 이전과 같은 방식으로 이미지를 로드하지만 이번에는 20 번째 줄에 앵커를 설정합니다. 이는 본질적으로 대상의 x 및 y 위치가 오프셋 될 수 있음을 의미합니다. 따라서 플레이어의 중심점 (수평선)을 기준으로 플레이어의 위치를 지정하고 왼쪽 상단 점의 위치는 지정하지 않습니다.


우리는 game.physics.arcade.enable ()을 사용하고 경계와 충돌하도록 플레이어의 몸체를 설정하여 물리 시스템에 플레이어를 추가합니다. 이 단계에서 플레이어는 중력에 반응하고 바닥과 충돌합니다.


24 ~ 32 행은 위와 같은 방법으로 적군을 만듭니다. 점프 기능에 사용되는 각각의 적에 몇 가지 추가 속성이 있습니다. 이것은 점프 높이에 대한 강도와 각 점프 사이의 지연을 설정합니다. 점프 논리는 Statesupdate () 메소드에 있습니다.


39 ~ 54 행은 점수와 시간에 대한 변수를 만듭니다. 또한 텍스트 렌더링에 Phaser의 다른 기능을 사용합니다. game.add.text ()를 사용하여 글꼴, 채우기 및 정렬을 지정하면서 화면에 텍스트 문자열을 만들 수 있습니다. Phaser는 텍스트 기반 기능이 뛰어납니다.


- 업데이트 방법

이제 메인 게임의 로직이 있는 update () 메소드로 이동하겠습니다. 59 행과 60 행은 프레임 워크의 입력 시스템에 대한 바로 가기를 만듭니다. cursors 변수를 사용하면 cursors.right.isDown ()을 사용하여 키가 다운되었는지 확인할 수 있습니다.


66 번에서 73 번까지는 어떤 키가 눌러 졌는지 확인하고, 이전에 보았던 경계 물체와 플레이어가 충돌하지 않는 경우 player.body.onFloor ()를 사용하여 false를 반환하면 플레이어가 공중에 있으면 player.body.velocity.x를 사용하여 플레이어에게 속도 변경을 부여합니다. 우리는 실제 x-y 값을 사용할 필요가 없습니다. x-y에 속도를 부여하고 나머지는 Phaser의 물리 시스템에서 처리합니다. 이제 플레이어는 점프하는 동안 이동할 수 있습니다.


83 행에는 Phaser 텍스트 필드를 업데이트하는 방법의 예가 있습니다. 우리는 단순히 setText ()와 렌더링 하고자하는 텍스트 내용을 설정합니다. 우리가 인스턴스화 한 기존의 텍스트 스타일 속성을 사용합니다.


물리 기반 충돌이 아닌 두 스프라이트 간의 충돌을 테스트하는 방법의 예는 113 행에 따라 Phaser.Rectangle.intersectsmethod ()를 사용하는 것입니다. 두 개의 sprites.bounds ()를 전달하면 충돌 여부에 따라 true 또는 false가 반환됩니다. 만약 false를 반환하면 적 타이머가 새 타이머를 만들 때까지 적을 숨깁니다.


- 게임 끝

나머지는 대부분 표준 자바 스크립트이지만, 게임을 강조 할 Phaser의 마지막 부분이 하나 있습니다. state.start () 메소드는 129 행에 있습니다. 일단 이것이 호출되면 현재 상태를 끝내고 문자열로 지정한 상태로 이동합니다. 이 경우 타이머가 0에 도달하면 Gameover 상태로 점프합니다.


Gameover.js에서 create () 메소드의 내용을 지우십시오. Menu 상태와 마찬가지로 배경 이미지를 만들지만 이번에는 동적 텍스트 필드를 추가하여 사용자의 점수를 표시합니다.

create () 메소드에 다음을 입력하십시오.

this.gameoverbg = this.game.add.sprite(0, 0, 'gameover');

this.score_text = this.game.add.text(this.game.world.centerX, 325, this.game.score, { font: '32px Arial', fill: '#ffffff', align: 'center'});


this.score_text.anchor.setTo(0.5, 0.5);


이제 Gameover.js 파일을 저장하고 다시 Grunt를 실행하십시오. 이번에는 전체 메뉴 화면, 게임 및 훨씬 향상된 Gameover 화면을 가져와야합니다.


지금까지 Phaser로 게임 개발에 대한 경험을 얻었습니다. 환상적인 게임을 만드는 데 도움이되는 훌륭한 프레임 워크입니다.

Posted by 마스터킹
|

Phaser.io JavaScript 라이브러리를 사용하여 게임을 만드는 방법을 보여주는 3 포스트 시리즈 중 첫 번째 시리즈에 오신 것을 환영합니다.


이 게임은 예전의 미사일 커맨드 게임과 유사하지만 조금 변형되었습니다. 미사일을 쏘는 대신에 낙하산 낙하산들에게 화살을 쏴서 피라미드에 닿지 않게 할 것입니다.


게임이 완료되면 어떻게 보이는지 다음 이미지에서 확인할 수 있습니다.


재미있어 보이지 않습니까? 자 이제 게임을 개발하는 데 사용할 기술부터 설명하겠습니다.


Phaser

최근에 게임을 개발하는 자바 스크립트 라이브러리가 늘어났습니다. 왜 Phaser가 필요합니까? 

Phaser의 기능 목록은 다음과 같습니다.

- WebGL 및 Canvas 지원

- 물리엔진

- 파티클

- 모바일 브라우저 지원

... 기타 유용한 기능


그리고 제가이 라이브러리를 선택한 이유는 GitHub 페이지에서 볼 수 있듯이 지속적인 업데이트 및 업그레이드 때문입니다.

필요한 Phaser 파일을 다운로드하고 Phaser 웹 사이트에서 설명서를 읽을 수도 있습니다.


다음 단계는 게임에 필요한 파일을 만드는 것입니다. index.html 파일을 작성하여 시작하겠습니다.


index.html

<head>

    <title>Phaser Mayan Defense</title>

    <script src="lib/phaser.min.js"></script>

    <script src="src/boot.js"></script>

    <script src="src/preloader.js"></script>

    <script src="src/main_menu.js"></script>

    <script src="src/game.js"></script>"

</head>


이 파일은 우리 프로젝트의 출발점입니다. 여기서 우리는 필요한 파일을 포함시킵니다. 첫 번째 스크립트 라인에는 Phaser 라이브러리가 있고, 다른 파일은 게임에서 사용할 화면이나 상태입니다.


기본적으로 상태는 게임에서 스크린이나 장면으로 볼 수 있습니다. 해당 화면에서 사용할 자원을 초기화하고, 렌더링 및 갱신 로직을 수행하며,이 화면에서 사용 된 자원을 해제 할 수 있습니다.


이 게임에서 우리가 사용할 상태는 다음과 같습니다.

- Boot

- Preloader

- MainMenu

- Game

이 파일들 각각을 자세히 검토 할 것입니다. 또한 게시물 끝에있는 소스 코드와 리소스 파일을 다운로드 할 수있는 링크를 찾을 수 있습니다.


boot.js

게임의 시작점은 boot.js 파일입니다. 이 파일에서 우리는 데스크탑 및 모바일 클라이언트 모두에 대해 몇 가지 설정을 적용했습니다. 또한 이 파일에서 우리는 다음 게임 상태 (또는 화면)에서 사용할 자원 (이미지)을 로드 할 수 있습니다.


init 함수에서 각 유형의 장치에 대해 서로 다른 옵션을 구성합니다.

init: function() {

    // 멀티 터치 지원이 필요하지 않으므로 여기서 1을 지정합니다.

    this.input.maxPointers = 1;


    if (this.game.device.desktop) {

        // 이 섹션에서는 데스크톱 별 설정에 대해 설명합니다.

        this.scale.pageAlignHorizontally = true;

    } else {

        // 이 모바일 설정 섹션

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

        this.scale.setMinMax(480, 260, 1024, 768);

        this.scale.forceLandscape = true;

        this.scale.pageAlignHorizontally = true;

    }


},


preload 함수를 사용하여 다음 상태에 대한 모든 리소스를로드합니다.

preload: function() {

    this.load.image('preloaderBackground', 'res/images/preloader_background.png');

    this.load.image('preloaderBar', 'res/images/preloader_bar.png');

},


preloader.js

이 파일은 게임에 필요한 모든 리소스를 로드 할 수있는 곳입니다. 또한 리소스가 로드되는 동안 표시 할 배경 이미지와 애니메이션을 설정할 수 있습니다.


프리로드 함수에서 boot.js에 미리 로드한 배경 및 진행률 막대를 추가합니다. 우리는 또한 이 함수를 사용하여 게임에서 사용할 다른 모든 리소스를 로드합니다.

preload: function () {

    this.background = this.add.sprite(0, 0, 'preloaderBackground');

    this.preloadBar = this.add.sprite(300, 400, 'preloaderBar');


    this.load.setPreloadSprite(this.preloadBar);


    this.load.image('titlepage', 'images/title.jpg');

    this.load.atlas('playButton', 'images/play_button.png', 'images/play_button.json');


    this.load.image('gameBackground', 'res/images/game_background.jpg');

}


위의 코드에서 볼 수 있듯이 이미지 (jpg 및 png 파일)가 로드 되지만 json 파일도 로드됩니다. 이 json 파일에는 버턴에 대한 아틀라스 또는 스프라이트 시트에 대한 정보가 들어 있습니다. 기본적으로 아틀라스는 단일 텍스처에 포함 된 이미지 그룹입니다. 이 방법으로 자원의 로딩은 그래픽 프로세서에 의해 최적화 될 수 있습니다.


아틀라스를 만들려면 TexturePacker라는 훌륭한 소프트웨어를 사용하고 있습니다. TexturePacker의 이미지 편집 기능을 사용하여 이미지를 하나씩 만들 수 있으며, 생성된 이미지 파일들을 하나의 파일로 만들 수 있습니다. TexturePacker에서 생성 된 단일 텍스처를 아래에서 볼 수 있습니다. 재생 버튼에 이 텍스처를 사용합니다.


다음은 TexturePacker에 의해 생성 된 json 파일의 예입니다.

  {"frames": [


  {

    "filename": "play_button_click.png",

    "frame": {"x":2,"y":2,"w":252,"h":100},

    "rotated": false,

    "trimmed": true,

    "spriteSourceSize": {"x":6,"y":6,"w":252,"h":100},

    "sourceSize": {"w":270,"h":110},

    "pivot": {"x":0.5,"y":0.5}

  },


update 함수에서는 다음 단계만 호출하면 됩니다. 나중에 게임에 음악을 추가 할 것이므로 update 함수를 사용하여 음악이 디코딩되어 다음 상태로 돌아갈 때까지 기다릴 수 있습니다.


main_menu.js

이것은 우리 게임의 메뉴 화면입니다. 이 화면에서 버튼을 추가하여 높은 점수를보고 설정을 변경하고 소셜 네트워크를 사용하여 게임에서 업적을 공유 할 수있는 버튼을 추가 할 수 있습니다. 그러나 지금은 재생 버튼 만 있으면됩니다.


create 함수를 사용하여 주 메뉴에 대한 스프라이트와 버튼을 추가합니다.

create: function () {

    this.add.sprite(0, 0, 'titlepage');

 this.playButton = this.add.button(400, 600, 'playButton', this.startGame, this, 'buttonOver', 'buttonOut', 'buttonOver');


},

위의  코드에서 먼저 배경 이미지를 먼저 추가합니다. 두 번째 줄에는 버튼 객체가 추가됩니다. 버튼 객체는 기본적으로 특정 유형의 이벤트, 특히 포인터 이벤트에 응답 할 수있는 스프라이트입니다.


- 버튼 이벤트

Over : 포인터가 버튼 위로 움직일 때 트리거됩니다.

Out : 이전에 Button 위에 있던 포인터가 밖으로 나올 때.

Down : 버튼에서 포인터를 누를때.

Up : 버튼에서 눌려진 포인터를 다시 놓을 때.


다음 함수는 플레이 버튼에 대한 이벤트 핸들러입니다.

startGame: function (pointer) {

    this.state.start('Game');


}

이 이벤트 핸들러에서는 현재 상태를 게임 화면으로 변경합니다.


이것이 바로 메뉴가 표시되는 방식입니다.


game.js

우리 게임의 마지막 상태는 게임 상태입니다.


아래 나열된 특성은 Phaser의 모든 상태에 존재하며 자동으로 설정됩니다. 우리는 상태 내의 모든 함수에서 사용할 수 있습니다.

MayanDefenseGame.Game = function (game) {

    this.game;      //  현재 실행중인 게임 (Phaser.Game)에 대한 참조

  this.add;       //  스프라이트, 텍스트, 그룹 등을 추가하는 데 사용됩니다 (Phaser.GameObjectFactory).

    this.camera;    //  게임 카메라 (Phaser.Camera)에 대한 참조

    this.cache;     //  게임 캐시 (Phaser.Cache)

    this.input;     //  전역 입력 관리자 이 input.keyboard, this.input.mouse도 액세스 할 수 있습니다. (Phaser.Input)

    this.load;      //  for preloading assets (Phaser.Loader)

    this.math;      //  유용한 수학 연산 (Phaser.Math)

  this.sound;     //  사운드 매니저 - 사운드 추가, 하나 재생, 마커 설정 등 (Phaser.SoundManager)

    this.stage;     //  the game stage (Phaser.Stage)

    this.time;      //  the clock (Phaser.Time)

    this.tweens;    //  the tween manager (Phaser.TweenManager)

    this.state;     //  the state manager (Phaser.StateManager)

    this.world;     //  the game world (Phaser.World)

    this.particles; //  the particle manager (Phaser.Particles)

    this.physics;   //  the physics manager (Phaser.Physics)

 this.rnd;       //  the repeatable random number generator (Phaser.RandomDataGenerator)


};


지금은 Game 상태에서 유일하게 유용한 함수는 create 함수입니다. 여기에 게임 배경 이미지를 캔버스에 추가합니다.

create: function () {

    // Here we add the game background image

    this.add.sprite(0, 0, 'gameBackground');


},


이제 우리는 게임을 테스트 할 준비가 되었습니다. index.html 파일을 직접 열거 나 웹 서버 (예 : Apache)를 사용하여 코드를 호스팅하고 실행할 수 있습니다. 다른 웹 브라우저에서 코드를 실행하는 것에 대한 몇 가지 참고 사항은 아래에서 확인할 수 있습니다.


게임을 실행하기위한 고려 사항

- Chrome을 사용하여 게임을 테스트하려면 Apache와 같은 웹 서버를 사용해야합니다.

- 파일을로드 할 때 Firefox의 제한이 적기 때문에 Firefox를 사용하여 웹 서버없이 게임을 테스트 할 수 있습니다.


- Glyph Designer를 사용하여 사용자 정의 글꼴의 png 및 xml 파일을 생성 할 수 있습니다.


최종 노트

자습서의 첫 번째 부분입니다. 다음 단계에서는 게임의 기본 기능을 추가 할 예정이므로 계속 지켜봐 주시기 바랍니다.


여기에서 코드를 다운로드하거나 복제 할 수 있습니다.



Posted by 마스터킹
|

이것은 JavaScript 및 Phaser로 Slither.io를 만드는 튜토리얼 시리즈의 두 번째 부분입니다! Part 1을 아직 보지 않았다면 살펴보십시오.

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


Assets 폴더를 엽니다. 이것은 우리의 모든 이미지가 있는 곳입니다. 우리는 이 부분에있는 모든 것을 사용하지 않을 것입니다. 배경에는 tile.png 만 사용하고 뱀 섹션에는 circle.png 만 사용합니다.


이제 index.html을 엽니 다. 다음은 자체 실행 익명 함수에서 게임을 초기화 하는 예제입니다.

(function() {

    var game = new Phaser.Game(800, 500, Phaser.AUTO, null);

    game.state.add('Game', Game);

    game.state.start('Game');

})();

'Game'이라는 상태를 추가하고 Game 함수를 전달한 다음 시작합니다.


src 폴더에서 game.js를 엽니다. 게임 함수는 다음과 같이 정의됩니다.

Game = function(game) {}

Game.prototype = {

   ...

}


게임 프로토 타입에서는 preload 단계로 Assets을 로드합니다.

preload: function() {

    //load assets

   this.game.load.image('circle','asset/circle.png');

   this.game.load.image('background', 'asset/tile.png');

}


생성 단계에서는 월드 경계를 설정하고 배경을 추가하고 P2 물리를 시작하고 뱀을 추가합니다.

create: function() {

    var width = this.game.width;

    var height = this.game.height;


    this.game.world.setBounds(-width, -height, width*2, height*2);

    this.game.stage.backgroundColor = '#444';


    //add tilesprite background

    var background = this.game.add.tileSprite(-width, -height,

        this.game.world.width, this.game.world.height, 'background');


    //initialize physics and groups

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


    this.game.snakes = [];


    //create player

    var snake = new Snake(this.game, 'circle', 0, 0);

    this.game.camera.follow(snake.head);

}


우리는 큰 세계를 만들었기 때문에 카메라가 뱀의 머리를 따라 가게합니다. 뱀 객체가 생성되면 this.game.snakes 배열에 추가됩니다. 이 배열은 여러 위치에서 현재의 뱀에 액세스하는 데 편리합니다.


마지막으로 뱀 클래스에서 update 메소드를 사용할 계획이므로 게임의 모든 뱀에 대해 메인 업데이트 루프에서 호출해야 합니다.

update: function() {

    //update game components

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

        this.game.snakes[i].update();

    }

}


이제 Snake 클래스를 살펴 보겠습니다.

snake.js를 엽니다. 먼저 게임 객체, 스프라이트 키 및 머리 위치를 매개 변수로 가져 와서 많은 변수를 초기화합니다.

Snake = function(game, spriteKey, x, y) {

    this.game = game;

    //create an array of snakes in the game object and add this snake

    if (!this.game.snakes) {

        this.game.snakes = [];

    }

    this.game.snakes.push(this);

    this.debug = false;

    this.snakeLength = 0;

    this.spriteKey = spriteKey;


    //various quantities that can be changed

    this.scale = 0.6;

    this.fastSpeed = 200;

    this.slowSpeed = 130;

    this.speed = this.slowSpeed;

    this.rotationSpeed = 40;


    //initialize groups and arrays

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

    this.sections = [];

    //the head path is an array of points that the head of the snake has

    //traveled through

    this.headPath = [];

    this.food = [];


    this.preferredDistance = 17 * this.scale;

    this.queuedSections = 0;


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

    //add the head of the snake

    this.head = this.addSectionAtPosition(x,y);

    this.head.name = "head";

    this.head.snake = this;


    this.lastHeadPosition = new Phaser.Point(this.head.body.x, this.head.body.y);

    //add 30 sections behind the head

    this.initSections(30);


    this.onDestroyedCallbacks = [];

    this.onDestroyedContexts = [];

}

가장 중요한 것은 addSectionAtPosition을 호출하여 헤드를 추가하는 것입니다.

initSections를 호출하여 헤드 바로 아래에 30 개의 섹션을 추가합니다. initSections 메서드는 헤드를 만든 후에 한 번만 호출 할 수 있습니다. 나중에 우리는 편리한 메소드 addSectionsAfterLast를 사용하므로 새로운 위치에 대해 걱정할 필요가 없습니다. 이 방법들을 한 번에 하나씩 살펴 보겠습니다.


addSectionAtPosition 메소드를 사용하여 단일 섹션을 추가 할 수 있습니다.

addSectionAtPosition: function(x, y) {

    //initialize a new section

    var sec = this.game.add.sprite(x, y, this.spriteKey);

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

    sec.body.setCollisionGroup(this.collisionGroup);

    sec.body.collides([]);

    sec.body.kinematic = true;


    this.snakeLength++;

    this.sectionGroup.add(sec);

    sec.sendToBack();

    sec.scale.setTo(this.scale);


    this.sections.push(sec);


    //add a circle body to this section

    sec.body.clearShapes();

    sec.body.addCircle(sec.width*0.5);


    return sec;

}


이제 뱀 뒤에 적절한 위치에 섹션을 쉽게 추가 할 수 있어야 합니다. 먼저 initSections 메서드를 사용하여 뱀의 머리 바로 뒤에 섹션을 추가합니다.

initSections: function(num) {

    //create a certain number of sections behind the head

    //only use this once

    for (var i = 1 ; i <= num ; i++) {

        var x = this.head.body.x;

        var y = this.head.body.y + i * this.preferredDistance;

        this.addSectionAtPosition(x, y);

        //add a point to the head path so that the section stays there

        this.headPath.push(new Phaser.Point(x,y));

    }

}


this.headPath는 뱀의 머리가 통과 한 점들의 배열입니다. 처음에는 아무 지점도 통과하지 못했기 때문에 초기 섹션을 추가 한 지점을 추가합니다. 그러나 우리는 또한 뱀의 뒤쪽에 새로운 섹션을 추가하는 편리한 방법이 필요합니다. addSectionsAfterLast 메소드를 사용합니다.

addSectionsAfterLast: function(amount) {

    this.queuedSections += amount;

}

나중에 queuedSections 속성이 0보다 클 때 새 섹션을 추가하는 위치를 알 수 있습니다.


업데이트 함수를 확인하겠습니다. 이것은 메인 업데이트 루프에서 호출되는 메소드입니다. 이 방법으로 우리는 앞으로 뱀을 움직일 것입니다.


헤드의 경로 (headPath 배열)를 사용하여 섹션을 배치 할 위치를 결정합니다. update 메소드가 호출 될 때마다, 배열의 앞부분에 새 머리 위치 인 점을 추가합니다. 섹션 사이에 preferredDistance 번호가 있으므로 나머지 섹션을 경로에 배치 할 위치를 결정할 수 있습니다. 다음은 이를 설명하는 그림입니다.


일반적으로 섹션은 headPath 내에 2 포인트 이상 떨어져 배치되지만 명확한 예제를 위해이 작업을 수행했습니다. 또한 "L"은 매번 계산해야하는 작은 십진수입니다. 우리가 그것들을 더할 때, 그것들은 preferredDistance에 완벽하게 들어 맞지 않을 것입니다, 그래서 우리는 선호하는 것과 가장 가까운 거리를 제공하는 점을 간단히 선택해야 합니다.


이제 update 메소드를 시작하는 방법을 살펴 보겠습니다. 먼저 head를 이동하고 headPath의 마지막 점을 제거한 다음 새로운 머리 위치를 배열의 앞쪽에 배치합니다.

var speed = this.speed;

this.head.body.moveForward(speed);

var point = this.headPath.pop();

point.setTo(this.head.body.x, this.head.body.y);

this.headPath.unshift(point);


다음으로, 헤드 경로를 따라 적절한 지점에 섹션을 배치해야합니다.

var index = 0;

var lastIndex = null;

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


    this.sections[i].body.x = this.headPath[index].x;

    this.sections[i].body.y = this.headPath[index].y;


    //hide sections if they are at the same position

    if (lastIndex && index == lastIndex) {

        this.sections[i].alpha = 0;

    }

    else {

        this.sections[i].alpha = 1;

    }


    lastIndex = index;

    //this finds the index in the head path array that the next point

    //should be at

    index = this.findNextPointIndex(index);

}

우리가 호출 한 findNextPointIndex 메소드는 점의 거리 공식을 사용하여 이전 섹션이 있던 위치를 기준으로 다음 섹션을 배치 할 위치를 확인합니다. 우리는 그 방법을 잠시 살펴볼 것입니다.


다음으로 업데이트 루프에서 배열이 너무 짧으면 headPath에 점을 추가하고 길이가 너무 길면 점을 제거합니다 (섹션을 배치 할 때 배열의 마지막 인덱스에 도달하지 않기 때문에).

//continuously adjust the size of the head path array so that we

//keep only an array of points that we need

if (index >= this.headPath.length - 1) {

    var lastPos = this.headPath[this.headPath.length - 1];

    this.headPath.push(new Phaser.Point(lastPos.x, lastPos.y));

}

else {

    this.headPath.pop();

}


이제 우리는 두 번째 섹션이, 이 메서드의 이전 호출에서 첫 번째 섹션 (head)이 있던 위치에 도달 할 때마다 onCycleComplete 메서드를 호출하려고합니다.

var i = 0;

var found = false;

while (this.headPath[i].x != this.sections[1].body.x &&

this.headPath[i].y != this.sections[1].body.y) {

    if (this.headPath[i].x == this.lastHeadPosition.x &&

    this.headPath[i].y == this.lastHeadPosition.y) {

        found = true;

        break;

    }

    i++;

}

if (!found) {

    this.lastHeadPosition = new Phaser.Point(this.head.body.x, this.head.body.y);

    this.onCycleComplete();

}


하지만 왜 우리가 이것을 필요로 합니까? 마지막에 섹션을 추가 할 때 동시에 섹션을 추가하고 싶지는 않습니다. 모든 섹션이 상당한 거리만큼 앞으로 움직일 때마다 섹션을 추가하려고합니다. 이것이 우리가 queuedSections 변수를 가지는 이유이며, onCycleComplete 메소드에 대기중인 섹션 중 하나를 추가합니다 :

onCycleComplete: function() {

    if (this.queuedSections > 0) {

        var lastSec = this.sections[this.sections.length - 1];

        this.addSectionAtPosition(lastSec.body.x, lastSec.body.y);

        this.queuedSections--;

    }

}


이제 이전에 사용한 findNextPointIndex 메서드를 살펴 보겠습니다.

findNextPointIndex: function(currentIndex) {

    var pt = this.headPath[currentIndex];

    // 우리는 거리 이전에있는 지점으로부터이 

    // 거리만큼 떨어져있는 지점을 찾으려고합니다.     

    //여기서 거리는 두 지점을 연결하는 모든 선의 총 길이입니다

    var prefDist = this.preferredDistance;

    var len = 0;

    var dif = len - prefDist;

    var i = currentIndex;

    var prevDif = null;

    // 이 루프는 함수의 주어진 인덱스에서 시작하는 헤드의 경로상의 점 사이의 

    // 거리를 합한 다음의 합계가 두 개의 뱀 섹션 사이의 선호 거리에 

    // 근접 할 때까지 계속됩니다

    while (i+1 < this.headPath.length && (dif === null || dif < 0)) {

        //get distance between next two points

        var dist = Util.distanceFormula(

            this.headPath[i].x, this.headPath[i].y,

            this.headPath[i+1].x, this.headPath[i+1].y

        );

        len += dist;

        prevDif = dif;

        // 우리는 현재 합계와 선호 거리 사이의 차이를 0에 가깝게하려고합니다.

        dif = len - prefDist;

        i++;

    }


    // 루프가 완료되면 차이를 0에 가깝게 만드는 인덱스를 선택하십시오.

    if (prevDif === null || Math.abs(prevDif) > Math.abs(dif)) {

        return i;

    }

    else {

        return i-1;

    }

}

Util.distanceFormula를 호출하여 점 사이의 거리를 얻은 다음 누적 합계에 추가합니다. 이 합계를 선호 거리와 비교하는 데 사용합니다. 우리는 Util 객체를 곧 살펴볼 것입니다.


뱀의 전체 크기를 변경하고자 하므로 setScale 메서드를 사용합니다.

setScale: function(scale) {

    this.scale = scale;

    this.preferredDistance = 17 * this.scale;


    //scale sections and their bodies

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

        var sec = this.sections[i];

        sec.scale.setTo(this.scale);

        sec.body.data.shapes[0].radius = this.game.physics.p2.pxm(sec.width*0.5);

    }

}


자, 다음과 같이 뱀 크기를 증가시킬 수 있습니다 :

incrementSize: function() {

    this.addSectionsAfterLast(1);

    this.setScale(this.scale * 1.01);

}


우리는 아직 뱀을 파괴하는 것이 없지만 나중에 뱀을 처리해야합니다. 우리 프로그램의 다른 부분은 나중에 뱀이 언제 파괴되는지 알고 싶어하기 때문에 뱀이 파괴 될 때 콜백을 처리해야합니다. 먼저 콜백을 배열하여 콜백을 설정할 수 있습니다.

addDestroyedCallback: function(callback, context) {

    this.onDestroyedCallbacks.push(callback);

    this.onDestroyedContexts.push(context);

}


그런 다음, 우리는 모든 것을 파괴하고 콜백을 다음과 같이 호출 할 수 있습니다.

destroy: function() {

    this.game.snakes.splice(this.game.snakes.indexOf(this), 1);

    this.sections.forEach(function(sec, index) {

        sec.destroy();

    });


    //call this snake's destruction callbacks

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

        if (typeof this.onDestroyedCallbacks[i] == "function") {

            this.onDestroyedCallbacks[i].apply(

                this.onDestroyedContexts[i], [this]);

        }

    }

}


마지막으로 util.js를 살펴보십시오. Util은 유용한 기능을 가진 객체 일뿐입니다. 이 객체에는 거리 공식과 임의의 정수 생성기가 있습니다.

const Util = {

    /**

     * Generate a random number within a closed range

     * @param  {Integer} min Minimum of range

     * @param  {Integer} max Maximum of range

     * @return {Integer}     random number generated

     */

    randomInt: function(min, max) {

        min = Math.ceil(min);

        max = Math.floor(max);

        return Math.floor(Math.random() * (max - min + 1)) + min;

    },

    /**

     * Calculate distance between two points

     * @param  {Number} x1 first point

     * @param  {Number} y1 first point

     * @param  {Number} x2 second point

     * @param  {Number} y2 second point

     */

    distanceFormula: function(x1, y1, x2, y2) {

        var withinRoot = Math.pow(x1-x2,2) + Math.pow(y1-y2,2);

        var dist = Math.pow(withinRoot,0.5);

        return dist;

    }

};


우리의 뱀은 기능적이지만 아직 돌 수 없습니다! 걱정하지 마십시오. 그것은 뱀의 머리를 돌리는 것입니다. 3 부에서 컨트롤을 돌려서 처리 할 것입니다. 그 동안 뱀을 테스트하십시오. 속도를 변경하면 섹션 사이의 거리가 거의 동일하게 유지됩니다. 3 부에서는 뱀을 플레이어의 뱀 또는 봇으로 확장하는 방법을 보여줍니다!


Posted by 마스터킹
|

이 튜토리얼은 https://loonride.com/learn/phaser/slither-io-part-1  를 구글 번역기를 통해서 번역한 것입니다. 저작권 및 기타 문제 발생시 댓글 달아 주시기 바랍니다. 

감사합니다.


많은 사람들이 현재 "io"게임 중 하나 인 Slither.io 게임을 들었을 것입니다. 게임을 했다면 아마 그 단순함에 놀랐을 것입니다. 이 게임에는 물리학, 조명 효과 및 퍼포먼스 등이 들어가기 때문에 매우 재미있습니다.


이 튜토리얼은 Slither.io와 같은 게임을 만드는 방법을 보여줍니다. 이 연재물을 주의 깊게 읽으면 게임 아이디어를 어떻게 시각화 할 수 있는지를 보여줄 것입니다. 내가 말했듯이, 단지 아이디어 만으로 끝낼것이 아니라 제작할수 있어야 합니다. 다음 "io"게임에 대한 훌륭한 아이디어가 나왔을 때,이 시리즈를 읽은 후 즉시 프로토 타입을 만들수 있을것입니다!


이 튜토리얼은 7 개의 파트로 구성이 되어 있으며,  여기서 완성된 소스를 확인할수 있습니다.


이 튜토리얼은 Phaser 프레임 워크를 사용합니다. 이 시리즈 중 어느 것도 Slither.io 소스 코드를 보고 작성된 것은 없습니다. 이 시리즈의 모든 코드는 순수하게 게임에 대한 나의 해석에 기반을 두고 있으며, 여기서 만든 게임의 메커니즘은 실제 게임에서와 같은 방식으로 작동하지 않습니다.


이 튜토리얼에서는 Slither.io의 멀티 플레이 기능에 대해서는 다루지 않습니다. 게임을 디자인 방식 때문에 Node.js와 socket.io를 사용하여 게임을 위한 간단한 서버 구현을 하는 것은 어렵지 않습니다. 서버구현에 관심이 있으시면 해당 기술에 대한 조사를 해보십시오. 서버로 뱀 머리의 위치를 보내고 가져 오는 방법을 알아낼 수 있습니다.


이 튜토리얼의 첫 번째 부분에서는 프로젝트 구성 방법과 시작하기 전에 필요한 사항을 확인해 보겠습니다.


모든 소스 코드는 Github에서 찾을 수 있습니다. 최종 버전은 slither-io 폴더에 있습니다. 자신의 컴퓨터에서 게임을 테스트하려면 로컬 웹 서버로 소스 코드를 제공 할 수 있어야합니다. Phaser 게임 프레임 워크에 익숙해야합니다. 만약 Phaser 프레임 워크에 익숙하지 않으면, 자습서 "Phaser 시작하기"를 참조하십시오.


이 튜토리얼는 JavaScript 및 Phaser의 고급 개념에 대해 설명합니다.

JavaScript의 객체, 함수 및 함수 프로토 타입에 익숙해야 합니다. 또한 기본적인 객체 상속을 다루게 될 것이고, 나중에 자세히 설명 할 것입니다. this 키워드를 반드시 이해하십시오.  이해하기 힘들때는 온라인에서 이 주제에 관한 정보를 쉽게 찾을 수 있습니다.


Phaser에서 다뤄야 할 복잡한 주제는 P2 물리학입니다. 가장 중요한 것은 Phaser가 핵심 P2 물리 엔진과 인터페이스하는 메소드 계층을 추가한다는 것입니다. Phaser의 메소드 레이어에 대한 문서는 Phaser.Physics.P2에서 확인할 수 있습니다.


Phaser.Physics.P2와 P2 Physics의 주요 차이점 : Phaser.Physics.P2는 픽셀을 치수로 사용하지만 P2 Physics는 미터를 사용합니다. 우리는 게임에서 독립형 P2 물리를 몇 번 사용하므로 픽셀에서 미터로 변환해야합니다. 다행스럽게도 Phaser.Physics.P2는 pxm 및 pxmi 또는 "pixels to meters"및 "pixels to metered meter"라는 메소드를 제공합니다.


Phaser.Physics.P2.Body에는 여기에 있는 문서에서 볼 수있는 속성 데이터가 있습니다. 이 속성에는 순수 P2 물리 물리 체의 문서가 들어있는 실제 P2 물리 본문이 포함되어 있습니다. Phaser 문서는 Phaser.Physics.P2.Body 제한 사항을 피하기 위해이 속성의 사용을 권장하지 않지만 조심해서 사용해야합니다.


다음은 이러한 복잡한 P2 개념을 모두 사용할 수있는 방법의 예입니다 (실제로 프로젝트에서 이 작업을 수행함).

var circle = this.game.add.sprite(0, 0, "circle");

this.game.physics.p2.enable(circle, false);

//give circle sprite a circular physics body with the proper radius

circle.body.setCircle(circle.width*0.5);

//scale the circle up

circle.scale.setTo(2);

//scale up the circle's P2 physics body with the proper radius

circle.body.data.shapes[0].radius = this.game.physics.p2.pxm(circle.width*0.5);

이 코드에서는 스프라이트 원을 초기화 한 다음 적절한 반경을 가진 순환 물리 바디를 추가합니다. 그런 다음 원의 스프라이트를 확대합니다. 물론 원의 몸체도 확대하고 싶습니다. Phaser 솔루션은 오래된 바디를 새로운 바디로 대체하는 것입니다. 순수한 P2 해법은 실제 몸의 반경을 변경하는 것입니다. circle.body의 data 속성을 사용합니다. 그런 다음 pxm 메서드를 사용하여 P2에 대한 미터로 변환합니다.


이제는 모든 일반적인 정보를 얻었으므로 프로젝트에 곧바로 뛰어들 수 있습니다!


게임의 그래픽 측면에 초점을 맞추기보다는 우리가 재창조하려는 핵심 역학 및 기능에 중점을 둘 것입니다.


첫째, 우리는 세그먼트로 구성된 기능적인 뱀이 필요합니다. 이 세그먼트는 뱀 머리의 정확한 경로를 따라야하며 균등하게 간격을 유지해야합니다. 뱀은 길이를 늘리고 규모를 늘리며 속도를 바꿀 수 있어야합니다. 그것은 봇으로 행동 할 수 있어야하며, 그렇지 않으면 커서가 그것을 제어 할 수 있어야합니다.


뱀의 세그먼트는 서로 충돌 할 수 없어야 하며, 서로 겹쳐져야 합니다. 뱀은 다른 뱀의 세그먼트와 충돌 할 수있는 머리 앞면에 단일 지점을 가져야합니다. 만약 충돌일 일어나면, 뱀은 파괴되어야 합니다.


게임에는 뱀 머리의 중심쪽으로 끌어 당길 수있는 음식이 있어야 하며, 그 곳에서 뱀의 머리가 파괴되어야합니다. 뱀이 파괴 된 곳에서는 음식을 떨어 뜨려야합니다.


그리고 약간의 미적 요구 사항에 대해서, 뱀은 커서를 따라가는 눈을 가져야합니다. 그리고 뱀은 뱀의 속도가 올라가면 빛을 낼 수있는 그림자가 있어야합니다.


소스 코드를 살펴보십시오. 우리 게임은 index.html 내에서 초기화됩니다. 모든 자바 스크립트 소스 파일은 src 폴더에 있습니다.


일반적인 수학 연산과 유틸리티의 경우 util.js에 Util 객체를 만들었습니다.


Phaser 게임은 index.html에서 시작하여 게임을 실행하는 Game이라는 단일 상태를 가지고 있습니다. 이 상태는 game.js에서 찾을 수 있습니다. 이 상태는 먼저 Assets 폴더에있는 Assets을 로드합니다. 그런 다음 다양한 Snake 및 Food 객체를 초기화합니다. 그 외에도 주요 업데이트 루프가 포함되어 있으며 뱀이 파괴되었는지 확인합니다.


Snake 함수는 snake.js에서 찾을 수 있습니다. 그것은 뱀을 형성하는 뱀 섹션 스프라이트의 그룹을 만듭니다. 그것은 뱀의 머리에 일정한 속도를 주고 머리가 통과 한 위치 배열을 유지합니다. 그런 다음 이 경로를 따라 다른 섹션을 배치합니다.


 코어 Snake 클래스는 BotSnake 및 PlayerSnake에 의해 상속되고 확장됩니다. BotSnake는 무작위로 뱀의 머리를 왼쪽이나 오른쪽으로 돌립니다. PlayerSnake는 화살표 키나 마우스의 위치를 기반으로 뱀의 머리를 회전시킵니다.


Snake 클래스는 EyePair를 초기화합니다. EyePair 클래스는 두 개의 Eye 객체를 생성하고이를 머리의 적절한 위치에 고정시킵니다. EyePair 컨트롤러를 제거하여 뱀에 하나 또는 3 개의 눈을 주는 것이 더 쉽기 때문에 Eye와 EyePair를 분리했습니다.


Snake는 Shadow 객체도 초기화 합니다. Shadow 클래스는 쉐도우 스프라이트 배열을 보유하고 있으며 뱀의 섹션 스프라이트 아래에 해당 스프라이트를 배치하는 업데이트 메소드가 있습니다. 뱀 섹션이 추가 될 때마다 그림자 스프라이트가 그림자 인스턴스를 통해 추가됩니다.


마지막으로, Food 스프라이트를 포함하는 Food 클래스가 있습니다. 음식 스프라이트의 몸체가 뱀의 머리와 충돌하는지 확인합니다. 그럴 때, 음식은 제약 조건을 사용하여 뱀의 머리 중심으로 끌려 가게되고, 그 곳에서 그것은 파괴됩니다.


이 튜토리얼의 다음 부분에서는 Snake 클래스의 디자인을 살펴 보겠습니다.



Posted by 마스터킹
|

일반 전략

Pong은 간단한 게임이므로 컴퓨터 AI에 간단한 2 단계 전략을 제공 할 수 있습니다.

모델 반응 시간 - 의사 결정을 하기 전에 일정 기간 기다리기

모델 정확도 - 컴퓨터가 볼의 착륙 지점을 정확하게 알고 컴퓨터에 오류가없는 것처럼 보이는 임의의 오류 요인을 추가합니다.

우리는 게임이 느슨해지기 시작하면 컴퓨터 기술을 더 잘 발달 시키거나 컴퓨터가 지배적 일 경우 더 나쁘게 만들어 게임의 균형을 유지하려고 할 수 있습니다.

Levels: [
  {aiReaction: 0.2, aiError:  40}, // 0:  ai is losing by 8
  {aiReaction: 0.3, aiError:  50}, // 1:  ai is losing by 7
  {aiReaction: 0.4, aiError:  60}, // 2:  ai is losing by 6
  {aiReaction: 0.5, aiError:  70}, // 3:  ai is losing by 5
  {aiReaction: 0.6, aiError:  80}, // 4:  ai is losing by 4
  {aiReaction: 0.7, aiError:  90}, // 5:  ai is losing by 3
  {aiReaction: 0.8, aiError: 100}, // 6:  ai is losing by 2
  {aiReaction: 0.9, aiError: 110}, // 7:  ai is losing by 1
  {aiReaction: 1.0, aiError: 120}, // 8:  tie
  {aiReaction: 1.1, aiError: 130}, // 9:  ai is winning by 1
  {aiReaction: 1.2, aiError: 140}, // 10: ai is winning by 2
  {aiReaction: 1.3, aiError: 150}, // 11: ai is winning by 3
  {aiReaction: 1.4, aiError: 160}, // 12: ai is winning by 4
  {aiReaction: 1.5, aiError: 170}, // 13: ai is winning by 5
  {aiReaction: 1.6, aiError: 180}, // 14: ai is winning by 6
  {aiReaction: 1.7, aiError: 190}, // 15: ai is winning by 7
  {aiReaction: 1.8, aiError: 200}  // 16: ai is winning by 8
],

패들 예측

패들 AI는이 전략을 따릅니다.

- 공이 떨어져 나가면 아무것도하지 마라.

- 그렇지 않으면 공이 코트의 패들 가장자리와 만나는 지점을 예측하십시오.


- 우리가 예측을 한다면 위아래로 움직여 그것을 만난다.

ai: function(dt, ball) {
  if (((ball.x < this.left) && (ball.dx < 0)) ||
      ((ball.x > this.right) && (ball.dx > 0))) {
    this.stopMovingUp();
    this.stopMovingDown();
    return;
  }
  this.predict(ball, dt);
  if (this.prediction) {
    if (this.prediction.y < (this.top + this.height/2 - 5)) {
      this.stopMovingDown();
      this.moveUp();
    }
    else if (this.prediction.y > (this.bottom - this.height/2 + 5)) {
      this.stopMovingUp();
      this.moveDown();
    }
    else {
      this.stopMovingUp();
      this.stopMovingDown();
    }
  }
},

예측 코드는 약간 까다 롭습니다.

첫째, 반응 시간에 도달 할 때까지 아무것도 하지 않습니다.

둘째, 무한히 큰 코트를 가정 한 공의 절편을 계산하십시오.

셋째, 패들쪽에 도달 할 때까지 위쪽 및 아래쪽 벽에서 반복적으로 예측을 바운스합니다.


마지막으로 우리의 오차 요인을 적용하여 이것이 '추측'합니다.

predict: function(ball, dt) {
  // only re-predict if the ball changed direction, or its been some amount of time since last prediction
  if (this.prediction &&
      ((this.prediction.dx * ball.dx) > 0) &&
      ((this.prediction.dy * ball.dy) > 0) &&
      (this.prediction.since < this.level.aiReaction)) {
    this.prediction.since += dt;
    return;
  }
  var pt  = Pong.Helper.ballIntercept(ball, {left: this.left, right: this.right, top: -10000, bottom: 10000}, ball.dx * 10, ball.dy * 10);
  if (pt) {
    var t = this.minY + ball.radius;
    var b = this.maxY + this.height - ball.radius;
    while ((pt.y < t) || (pt.y > b)) {
      if (pt.y < t) {
        pt.y = t + (t - pt.y);
      }
      else if (pt.y > b) {
        pt.y = t + (b - t) - (pt.y - b);
      }
    }
    this.prediction = pt;
  }
  else {
    this.prediction = null;
  }
  if (this.prediction) {
    this.prediction.since = 0;
    this.prediction.dx = ball.dx;
    this.prediction.dy = ball.dy;
    this.prediction.radius = ball.radius;
    this.prediction.exactX = this.prediction.x;
    this.prediction.exactY = this.prediction.y;
    var closeness = (ball.dx < 0 ? ball.x - this.right : this.left - ball.x) / this.pong.width;
    var error = this.level.aiError * closeness;
    this.prediction.y = this.prediction.y + Game.random(-error, error);
  }
},

이 게임은 이제 이 데모가있는 1 인 및 2 인 게임으로 거의 완벽합니다.




Posted by 마스터킹
|

공이동

게임을 하기 위해서는 공을 가속화 해야합니다. 위치, 속도, 가속도 및 시간 간격 dt가 주어지면 볼의 새로운 위치와 속도는 다음과 같이 계산 될 수 있습니다.

accelerate: function(x, y, dx, dy, accel, dt) {
  var x2  = x + (dt * dx) + (accel * dt * dt * 0.5);
  var y2  = y + (dt * dy) + (accel * dt * dt * 0.5);
  var dx2 = dx + (accel * dt) * (dx > 0 ? 1 : -1);
  var dy2 = dy + (accel * dt) * (dy > 0 ? 1 : -1);
  return { nx: (x2-x), ny: (y2-y), x: x2, y: y2, dx: dx2, dy: dy2 };
},

가속화가되면 ball의 update () 메소드는 다음을 수행해야 합니다.

- 공의 새로운 위치와 속도를 계산하십시오.

- 상단 또는 하단 벽에서 튀어 나오면 감지 (단순 경계 검사)

- 패들에서 튀었는지 탐지합니다 (다음 섹션 참조).


- 공이 움직이는 외륜을 치면 스핀을 시뮬레이트하기 위해 y 속도를 조정하십시오.

update: function(dt, leftPaddle, rightPaddle) {
  pos = Pong.Helper.accelerate(this.x, this.y, this.dx, this.dy, this.accel, dt);

  if ((pos.dy > 0) && (pos.y > this.maxY)) {
    pos.y = this.maxY;
    pos.dy = -pos.dy;
  }
  else if ((pos.dy < 0) && (pos.y < this.minY)) {
    pos.y = this.minY;
    pos.dy = -pos.dy;
  }
  var paddle = (pos.dx < 0) ? leftPaddle : rightPaddle;
  var pt     = Pong.Helper.ballIntercept(this, paddle, pos.nx, pos.ny);
  if (pt) {
    switch(pt.d) {
      case 'left':
      case 'right':
        pos.x = pt.x;
        pos.dx = -pos.dx;
        break;
      case 'top':
      case 'bottom':
        pos.y = pt.y;
        pos.dy = -pos.dy;
        break;
    }
    // add/remove spin based on paddle direction
    if (paddle.up)
      pos.dy = pos.dy * (pos.dy < 0 ? 0.5 : 1.5);
    else if (paddle.down)
      pos.dy = pos.dy * (pos.dy > 0 ? 0.5 : 1.5);
  }
  this.setpos(pos.x,  pos.y);
  this.setdir(pos.dx, pos.dy);
},

볼 및 패들 교차점

위 update () 메서드에서 사용 된 ballIntercept () 메서드는 프레임 간격 (dt) 동안 볼이 패들에 충돌하는지 여부를 정확하게 감지해야 합니다.

공이 시간 간격 (dt) 동안 p1에서 p2로 이동하고 패들 가장자리가 p3에서 p4로 늘어나고 충돌하는지 확인해야합니다.

우리는 공이 배트의 명백한면에 충돌 하는지 알 수 있기를 원합니다.

- 공이 왼쪽으로 움직이는 경우 플레이어 1의 오른쪽 가장자리를 확인하십시오.


- 공이 오른쪽으로 움직이는 경우 플레이어 2의 왼쪽 가장자리를 확인하십시오.

그러나 우리는 또한 공이 '빗나 갔다'는 눈에 띄지 않는 가장자리가 있는지를 알기 위해 위쪽과 아래쪽과 같은 명백하지 않은 부분을 확인하려고 하지만 목표를 향해 나가기 전에 위쪽이나 아래쪽으로 튀어 나옵니다. 대부분의 pong 게임은 이것으로 귀찮게 하지 않지만, 우리는 완성을 위해 노력하고 있습니다. 그래서 간단한 라인 intercept() 메서드가 있다고 가정하면 ballIntercept ()는 다음과 같이 구현 될 수 있습니다 :

ballIntercept: function(ball, rect, nx, ny) {
  var pt;
  if (nx < 0) {
    pt = Pong.Helper.intercept(ball.x, ball.y, ball.x + nx, ball.y + ny,
                               rect.right  + ball.radius,
                               rect.top    - ball.radius,
                               rect.right  + ball.radius,
                               rect.bottom + ball.radius,
                               "right");
  }
  else if (nx > 0) {
    pt = Pong.Helper.intercept(ball.x, ball.y, ball.x + nx, ball.y + ny,
                               rect.left   - ball.radius,
                               rect.top    - ball.radius,
                               rect.left   - ball.radius,
                               rect.bottom + ball.radius,
                               "left");
  }
  if (!pt) {
    if (ny < 0) {
      pt = Pong.Helper.intercept(ball.x, ball.y, ball.x + nx, ball.y + ny,
                                 rect.left   - ball.radius,
                                 rect.bottom + ball.radius,
                                 rect.right  + ball.radius,
                                 rect.bottom + ball.radius,
                                 "bottom");
    }
    else if (ny > 0) {
      pt = Pong.Helper.intercept(ball.x, ball.y, ball.x + nx, ball.y + ny,
                                 rect.left   - ball.radius,
                                 rect.top    - ball.radius,
                                 rect.right  + ball.radius,
                                 rect.top    - ball.radius,
                                 "top");
    }
  }
  return pt;
}

선 교차점

ballIntercept () 메서드는 일반적인 intersect 메서드를 사용합니다.

공이 (x1, y1)에서 (x2, y2)로 이동하고 패들 가장자리가 (x3, y3)에서 (x4, y4)로 이동한다고 가정하면 intercept () 메서드는 다음과 같이 구현할 수 있습니다.

intercept: function(x1, y1, x2, y2, x3, y3, x4, y4, d) {
  var denom = ((y4-y3) * (x2-x1)) - ((x4-x3) * (y2-y1));
  if (denom != 0) {
    var ua = (((x4-x3) * (y1-y3)) - ((y4-y3) * (x1-x3))) / denom;
    if ((ua >= 0) && (ua <= 1)) {
      var ub = (((x2-x1) * (y1-y3)) - ((y2-y1) * (x1-x3))) / denom;
      if ((ub >= 0) && (ub <= 1)) {
        var x = x1 + (ua * (x2-x1));
        var y = y1 + (ua * (y2-y1));
        return { x: x, y: y, d: d};
      }
    }
  }
  return null;
},

게임은 이제 이 데모와 함께 2 인 게임으로 즐길 수 있습니다.



Posted by 마스터킹
|

Game.Runner는 update () + draw ()에서 60fps 프레임 루프를 제공합니다. 해당 프로세스 내에서 (이 경우 퐁에서) 게임 자체, 기본 메뉴로 시작, 게임을 시작이 승자, 또는 사용자가 이를 포기 때까지 게임을 실행하는 사용자 입력을 기다립니다, 그런 다음 메뉴로 돌아가서 반복하십시오.



초기화

Pong 게임의 경우 사용자에게 다음과 같은 정보를 제공하는 '메뉴'를 표시하기 위해 일부 이미지를 사용합니다.

- 싱글 플레이어 게임의 경우 '1'을 누르십시오.


- 더블 플레이어 게임의 경우 '2'를 누르십시오.

이것은 GameRunner와 Pong 관계에 미묘한 비틀기를 도입합니다. 이미지로드는 비동기 프로세스이므로 이미지가 로드 될 때까지 이미지를 표시 할 수 없으므로 이미지가 완전히로드 될 때까지 GameRunner를 루프에 넣지 않아도됩니다.

이 문제를 해결하기 위해 Pong initialize 메서드는 콜백 패턴을 사용하여 이미지로드가 완료된 시점을 알 수 있으며 해당 프로세스가 완료 될 때까지 GameRunner에 start ()를 알리지 않습니다.

initialize: function(runner, cfg) {
  Game.loadImages(Pong.Images, function(images) {
    this.cfg         = cfg;
    this.runner      = runner;
    this.width       = runner.width;
    this.height      = runner.height;
    this.images      = images;
    this.playing     = false;
    this.scores      = [0, 0];
    this.menu        = Object.construct(Pong.Menu,   this);
    this.court       = Object.construct(Pong.Court,  this);
    this.leftPaddle  = Object.construct(Pong.Paddle, this);
    this.rightPaddle = Object.construct(Pong.Paddle, this, true);
    this.ball        = Object.construct(Pong.Ball,   this);
    this.runner.start();
  }.bind(this));
},

키보드 입력


키보드 이벤트가 발생하면 GameRunner는 게임의 onkeydown () 또는 onkeyup () 메소드 (있는 경우)를 자동으로 호출하므로 Pong 게임이 적절한 키를 감지하고 적절하게 게임을 시작하거나 중지 할 수 있습니다.

onkeydown: function(keyCode) {
  switch(keyCode) {
    case Game.KEY.ZERO: this.startDemo();            break;
    case Game.KEY.ONE:  this.startSinglePlayer();    break;
    case Game.KEY.TWO:  this.startDoublePlayer();    break;
    case Game.KEY.ESC:  this.stop(true);             break;
    case Game.KEY.Q:    this.leftPaddle.moveUp();    break;
    case Game.KEY.A:    this.leftPaddle.moveDown();  break;
    case Game.KEY.P:    this.rightPaddle.moveUp();   break;
    case Game.KEY.L:    this.rightPaddle.moveDown(); break;
  }
},
onkeyup: function(keyCode) {
  switch(keyCode) {
    case Game.KEY.Q: this.leftPaddle.stopMovingUp();    break;
    case Game.KEY.A: this.leftPaddle.stopMovingDown();  break;
    case Game.KEY.P: this.rightPaddle.stopMovingUp();   break;
    case Game.KEY.L: this.rightPaddle.stopMovingDown(); break;
  }
},

또한 사용자가 패들의 위나 아래를 움직일 수있는 입력을 감지합니다.



게임 시작하기


이제 키보드 입력을 감지 할 수 있으므로 게임을 시작할 수 있습니다.

startDemo:         function() { this.start(0); },
startSinglePlayer: function() { this.start(1); },
startDoublePlayer: function() { this.start(2); },
start: function(numPlayers) {
  if (!this.playing) {
    this.scores = [0, 0];
    this.playing = true;
    this.ball.reset();
    this.runner.hideCursor();
  }
},

경기 도중


게임이 진행되는 동안 update () 메소드는 채점 된 시점과 승자를 선언하고 게임을 중단할 시기를 감지해야합니다.

update: function(dt) {
  this.leftPaddle.update(dt, this.ball);
  this.rightPaddle.update(dt, this.ball);
  if (this.playing) {
    var dx = this.ball.dx;
    var dy = this.ball.dy;
    this.ball.update(dt, this.leftPaddle, this.rightPaddle);
    if (this.ball.left > this.width)
      this.goal(0);
    else if (this.ball.right < 0)
      this.goal(1);
  }
},
goal: function(playerNo) {
  this.scores[playerNo] += 1;
  if (this.scores[playerNo] == 1) {
    this.menu.declareWinner(playerNo);
    this.stop();
  }
  else {
    this.ball.reset(playerNo);
  }
},

게임 중지


우승자가 선언되면 게임이 중지되고 ESC 키를 치는 사용자에 대한 응답으로 게임을 중지 할 수도 있습니다.

stop: function(ask) {
  if (this.playing) {
    if (!ask || this.runner.confirm('Abandon game in progress ?')) {
      this.playing = false;
      this.runner.showCursor();
    }
  }
},

여기에서 데모를 통해 진행중인 게임 루프를 볼 수 있습니다.



Posted by 마스터킹
|