HTML5GAME :: 'PROGRAMMING/Phaser Js' 카테고리의 글 목록 (3 Page)

달력

52024  이전 다음

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

어렸을 때, 나는 게임을하는 것을 좋아했습니다. 나이가 들어감에 따라 직접 만들어 보고 싶었습니다. 그리고 성인이 되어 게임 제작을 직업으로 하고 있습니다.

나는 내 일자리를 정말로 즐깁니다. 그러나 나는 여전히 게임을 만드는 것에 대해 생각합니다. 그러면 간단한 퐁게임을 자바스크립트로 만들어 보겠습니다.

여기서 데모게임을 할수 있습니다.

여기서 게임코드를 찾을수 있습니다.


나는 각각을 위한 데모와 함께 5 개의 섹션으로 게임개발 내용을 나누었습니다.


GameRunner


Part1에서는 GameRunner를 소개합니다. 캔버스 게임을 만드는 데 필요한 최소한의 프레임 워크로 60fps 이중 버퍼 캔버스 게임 루프를 제공하므로 update () 메서드가 호출 될 때마다 게임 상태를 업데이트하고 draw () 메서드가 호출 될 때마다 게임 자체를 렌더링하는 데 집중할 수 있습니다.


Bouncing Balls


Part 2에서는 GameRunner 프레임 워크를 사용하여 간단한 튀는 공 예제를 표시하고 GameRunner와 Pong 게임 자체의 분리를 보여줍니다.


GameLoop


Part 3에서는 GameLoop를 소개합니다. 기본 메뉴로 시작하여 사용자 입력으로 게임을 시작하거나, 승자가있을 때까지 게임을 실행하거나, 사용자가 포기할 때까지 기다린 다음 메뉴로 돌아가서 반복합니다.


충돌 감지


Part 4에서는 패들과 볼 사이의 충돌 감지 기능을 구현하여 처음으로 게임을 실행하게하여 게임 자체의 문제점을 해결했습니다. 컴퓨터 AI는 없지만 이제는 2 명이 즐길 수 있습니다.


컴퓨터 인공 지능


Part 5에서는 컴퓨터 플레이어 용 기본 AI를 구현합니다. 싱글 플레이어 게임을하거나 심지어 컴퓨터 게임을 보는 것도 허용합니다.


결론

Pong은 상당히 사소한 게임이지만 여전히 제대로 작동하려면 인프라가 약간 필요합니다. 이것은 다소 지나치게 엔지니어링 된 버전이지만 여전히 추가 될 수있는 것이 더 있습니다 :


터치 이벤트 - 모바일 브라우저의 경우 사용자 입력에 대해 다른 디자인이 필요합니다. 아마도 패들 이동에 대한 터치 이벤트를 지원할 수 있습니다.


CSS @media 쿼리 - 우리는 아마도 사용자 화면 영역을 기반으로 퐁 게임 캔버스를 확장해야합니다. (업데이트 : 이것은 마지막 순간에 추가되었으며, 브라우저 창 크기로 놀고 무슨 일이 일어나는 지 봅니다)


requestAnimationFrame - 이중 버퍼링 및 60fps로 잠긴 경우에도 애니메이션에 약간의 자국이 남아 있습니다. 가비지 콜렉션 또는 제어가 불가능한 vsync 문제 일 수도 있지만 setInterval과 같은 충실도가 낮은 타이밍 메커니즘을 사용하는 것도 효과가 없습니다. 최신 브라우저에서는 requestAnimationFrame을 구현하여 게임 루프를 통해 미세한 입자 제어가 가능하지만 아직 주요 시간대에는 준비가되어 있지 않습니다.


이 구현은 미래의 다른 간단한 게임으로 성장할 수있는 기본 프레임 워크를 제공합니다. 앞으로 몇 개월 동안 좀 더 작은 게임을 개발하고 싶습니다. 꽤 작 으면 무언가를 완성 할 가능성이 가장 높다고 가정하면 브레이크 아웃, 뱀, 테트리스, 영웅, 돌격, 돌 발치 등 오래된 고전 게임의 캔버스 버전을 만들 수 있다고 상상합니다.



Posted by 마스터킹
|

캐주얼하고 단순한 2D 게임이 때로는 백만 달러가 넘는 복잡 한 게임보다 더 인기가 있습니다. 이것은 좋은 것이 든 나쁜 것이 든, 사람들이 게임 시간을 보내는 것을 선택하는 것입니다.


HML5는 내가 좋아하기 때문에뿐만 아니라, 기존의 웹 개발 기술을 활용하여 단일 코드 기반으로 다양한 플랫폼을 타겟팅 할 수 있기 때문에 집중적으로 다룹니다.


이 튜토리얼에서는 간단한 사이드 스크롤러 모바일 / 크로스 플랫폼 게임을 처음부터 만들어 Phaser 라이브러리를 사용하여 HTML5 모바일 게임 개발의 기초를 배웁니다.


제공된 코드와 Assets는 여러분의 모바일 프로젝트에 자유롭게 사용셔도 됩니다.



소스 코드 파일

소스 코드와 게임 저작물을 zip 파일로 다운로드 하십시오. Github에서 가져올 수도 있습니다. 여기에서 완성 된 게임을 플레이 할 수 있습니다.



튜토리얼 목표

이 튜토리얼을 따르면, Phaser로 2D 모바일 HTML5 게임을 만들 수있는 기초가 있어야합니다.

1. 2 차원 사이드 스크롤러를 만들어 Phaser의 기초를 배웁니다.

2. 무료 Tiled 레벨 편집기를 사용하여 HTML5 게임용 레벨을 만드는 방법을 배웁니다.

3. 기본적인 2D 게임 메카니즘을 배우십시오.



튜토리얼 요구 사항

- JavaScript 에 대한 기초 지식부터 중간 지식이 필요하며, 코드 편집기 또는 IDE, Sublime Text, Netbeans 또는 Gean와 같은 편집기가 필요합니다.

GitHub 레포에서 Phaser를 다운로드하십시오. repo를 복제하거나 ZIP 파일을 다운로드 할 수 있습니다.

- 로컬 또는 원격 웹 서버를 사용하여 코드 및 Phaser 예제를 실행해야합니다. 몇 가지 일반적인 옵션은 Apache (Windows의 경우 WAMP, Mac의 경우 MAMP)입니다. 가벼운 대안은 몽구스 웹 서버와 파이썬의 HTTP 서버입니다. 자세한 내용은이 안내서를 참조하십시오.

- Phaser 설명서와 예제 페이지를 준비하십시오. 또한 Phaser의 소스 코드를 보면서 언제나 해답을 찾을 수 있다는 것을 잊지 마십시오.

- 우리는 Windows, Mac 및 Linux와 호환되는 무료 및 Open Source Tiledmap 편집기를 사용할 타일 기반 게임을 제작할 것입니다.


- 터치 컨트롤의 경우 HTML5 Virtual Game Controller라는 라이브러리를 사용합니다. gamecontroller.js 파일은 게임 코드에 포함되어 있지만 최신 버전을 구해서 설명서를 읽어보십시오.



새 프로젝트

새 폴더, 색인 HTML 파일 및 Phaser 파일로 시작하십시오. 다운로드 한 Phaser zip 또는 복제 된 저장소의 "build"디렉토리에서 가져올 수 있습니다.


개발 모드 (전 세계에 최종 목적지로 게임을 배포하는 생산 방식과 달리)에서는 phaser.js 파일 (phaser.min.js와 반대)을 포함하지 않는 phaser.js 파일을 포함하는 것이 좋습니다. 그 이유는 게임 엔진을 블랙 박스로 취급하는 것은 좋은 습관이 아닙니다. 파일의 내용을 탐색하고 올바르게 디버그 할 수 있기를 원합니다.


모든 답변이 Google이나 StackOverflow에있는 것은 아니므로 가장 좋은 방법은 무엇이 진행되고 있는지를 이해하기 위해 원본 소스 코드를 읽는 것입니다. Google의 index.html 파일은 다음과 같습니다. 자습서가 진행됨에 따라 여기에 표시되는 파일을 추가하고 설명합니다.

<!DOCTYPE html>

<html>

  <head>

    <meta charset="utf-8" />

    <title>Learn Game Development at ZENVA.com</title>

    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <meta name="mobile-web-app-capable" content="yes">

    <meta name="apple-mobile-web-app-capable" content="yes">

    <script type="text/javascript" src="js/gamecontroller.js"></script>

    <script type="text/javascript" src="js/phaser.min.js"></script>

    <script type="text/javascript" src="js/Boot.js"></script>

    <script type="text/javascript" src="js/Preload.js"></script>

    <script type="text/javascript" src="js/Game.js"></script>

    <style>

      body {

        padding: 0px;

        margin: 0px;

      }

      </style>

  </head>

  <body> 

    <!-- include the main game file -->

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

  </body>


</html>


뷰포트 메타 태그를 사용하여 사용자가 화면을 꼬집어 재생하면 확대 / 축소를 방지합니다 (재생시 실수로 할 수 있음). 모바일 웹 앱을 사용할 수 있고 Apple 모바일 웹 앱을 추가하면 휴대 전화 사용자가 휴대 전화의 바로 가기를 webapp에 추가 할 수 있습니다. 아이콘을 추가하고 iOS 및 Android에 대한 자세한 내용을 볼 수도 있습니다).


main.js 파일이 게임을 시작합니다.



States

코드로 진행하기 전에 Phaser의 중요한 개념 인 States에 대해 이야기하고 싶습니다.


페이저에서는 모든 동작이 States에서 발생합니다. 당신은 그것들을 게임의 주요 순간으로 생각할 수 있습니다. 축구 경기를 생각하면 문이 열리고 사람들이 들어 오기 시작할 때가 있습니다. 그러면 게임 전 쇼가 개최되는 States가 있습니다. 그런 다음 경기 전 경기 자료가 현장에서 제거 된 상태입니다. 그런 다음 게임이 시작되는 States 등이 있습니다.


Phaser는 당신이 가질 수있는 States만큼 많은 유연성을 제공하지만, 많은 게임과 튜토리얼에서 사용되는 사실상의 컨벤션이 있습니다. 이름은 조금씩 다를 수 있지만 일반적으로 다음과 같습니다.


Boot States : 일반 게임 설정이 정의되고 사전로드 화면의 자산이 로드됩니다 (예 :로드 바). 사용자에게 아무 것도 표시되지 않습니다.


Preload States : 게임 Assets (이미지, 스프라이트 시트, 오디오, 텍스처 등)이 메모리에 로드됩니다 (디스크에서). PreLoad 화면은 사용자에게 보여지며 일반적으로 진행 상황을 보여주는 로드 바를 포함합니다.


MainMenu State : 게임의 시작 화면. Preload States 후에는 모든 게임 이미지가 이미 메모리에 로드되어 있으므로 빠르게 액세스 할 수 있습니다.


Game State : 게임이 실제로 작동되는 States 입니다.


투토리얼에서는 Boot, Preload 및 Game의 세 가지 States 만 사용합니다. 



게임을 시작하다


다시 코딩으로 돌아갑니다! 게임의 진입 점인 main.js를 살펴 보겠습니다.

var SideScroller = SideScroller || {};

SideScroller.game = new Phaser.Game(746, 420, Phaser.AUTO, '');

SideScroller.game.state.add('Boot', SideScroller.Boot);

SideScroller.game.state.add('Preload', SideScroller.Preload);

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


SideScroller.game.state.start('Boot');


우리가 할 첫 번째 일은 우리 게임의 모든 데이터를 유지할 객체를 만드는 것입니다. 이 객체를 SideScroller라고합니다.


게임이라는 변수를 만든 다음 각 파일에서 정의 할 다른 객체에 대한 새 변수를 만드는 것이 어떻습니까?


진실은 당신이 그렇게 할 수 있다는 것입니다. 그러나 당신은 그렇게 선호합니다.하지만 모든 것을 하나의 단일 객체로 유지하는 것이 좋습니다. 이를 네임 스페이스 패턴이라고 합니다. 일반적으로 원하지 않는 것은 응용 프로그램의 전역 범위를 오염시키는 것입니다. 내가 "game"이라는 변수를 정의하는 외부 라이브러리를 포함한다고 가정 해보십시오. 전역 범위에서 작성하는 변수가 많을수록 더 많은 변수가 발생할 수 있습니다. 이로 인해 게임이 오작동을 일으킬 수 있습니다. 따라서 우리는 일반적으로 "SideScroller"와 같은 다른 곳에서 사용하기는 거의 불가능한 이름을 선택하고 그 안에 모든 것을 넣습니다.


문장 : var SideScroller = SideScroller || {}; SideScroller가 이전에 정의되어 있지 않으면 빈 객체로 만들어집니다.

SideScroller.game = new Phaser.Game(746, 420, Phaser.AUTO, ”);


우리는 화면 해상도로 Phaser 게임을 시작합니다. 게임을 렌더링하기 위해 Phaser는 CANVAS 요소를 사용하거나 WebGL (대부분의 최신 브라우저에있는 OpenGL과 웹용으로 작동하는 사양)에서 사용할 수 있습니다. 렌더링을 Phaser.AUTO로 설정하여 Phaser가 사용 가능한 것을 따라 무엇을 사용할 지 결정합니다.


States를 정의한 후에 (외부 파일로 생성 된 개체) Phaser가 Boot 상태를 시작하도록 지시합니다.



States 메서드

States는 특정 목적에 부합하는 일부 예약 된 방법을 가지고 있습니다. 이것들은 우리가 이 튜토리얼에서 사용할 것들입니다. 여기에서 전체 목록을 찾을 수 있습니다.


- init : States 초기화시 호출됩니다. States에 매개 변수를 전송해야하는 경우 여기에 액세스 할 수 있습니다 (자세한 내용은 나중에 설명합니다)


-preload : Assets이 로드되는 곳입니다.


-create : Assets 로드가 완료되면 호출됩니다.


-update : 이것은 모든 게임 틱에서 호출됩니다. 기본적으로 "초당 여러 번" 호출 되므로 충돌 감지와 같이 지속적으로 테스트해야하는 항목을 포함시키고 자합니다.



Boot State

Boot State에 대해 이야기하기 전에 이론적으로 모든 게임이 수행되는 상태를 사용할 수는 없지만 States를 사용하면 코드를 더 잘 구성 할 수 있다고 말하고 싶습니다.


Boot State는 어두운 곳입니다. 여기서 우리는 화면 크기와 우리가 사용할 물리 엔진과 같은 일반적인 게임 구성 옵션을 정의합니다 (Phaser는 그 중 세 가지가 있으며 우리는 Arcade라는 가장 간단한 것을 사용합니다). 또한 Boot State에서 Preload State로 표시 할 Assets을 로드합니다.


그래서 Boot State는 Preload State의 Assets을 로드하고 Preload State는 게임 Assets을 로드합니다. 왜 게임 Assets을 거기에서 로드하지 않는 것이 좋을까요? 아무 것도 그 일을 막지는 못하지만, 게임 Assets은 미리로드 된 화면 애셋보다 로드하는 데 시간이 오래 걸릴 가능성이 높습니다. 로딩 바 및 로고 일뿐입니다.


프리 로딩 화면 에셋을 먼저 로드하면 (즉, 가벼워 야 함) 빈 화면이있는 시간을 최소화합니다 (블루 스크린보다 좋지만 사용자는 이를 좋아하지 않습니다). 그런 다음 Preload 상태에서 눈을 바쁘게하는 멋지고 매력적인 프리로드 스크린으로 다른 모든 것을 로드 할 시간이 있습니다.


Boot.js의 내용 :

var SideScroller = SideScroller || {};

SideScroller.Boot = function(){};

//setting game configuration and loading the assets for the loading screen

SideScroller.Boot.prototype = {

  preload: function() {

    //assets we'll use in the loading screen

    this.load.image('preloadbar', 'assets/images/preloader-bar.png');

  },

  create: function() {

    //loading screen will have a white background

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

    //scaling options

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

    //have the game centered horizontally

    this.scale.pageAlignHorizontally = true;

    this.scale.pageAlignVertically = true;

    //screen size will be set automatically

    this.scale.setScreenSize(true);

    //physics system

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

    this.state.start('Preload');

  }


};


main.js와 같은 패턴을 따라 시작합니다. SideScroller가 이미 정의 된 경우이를 사용하고 그렇지 않으면 새 객체를 시작합니다.


SideScroller.Boot는 Boot 상태를 포함 할 객체입니다. 이것은 게임에 상태를 추가 할 때 main.js에서 전달한 것과 같은 객체입니다.

SideScroller.game.state.add('Boot', SideScroller.Boot);


preload 메소드에서 우리는 진행 상태를 위한 스프라이트를 로드하고 있습니다. 이것은 단순히 녹색 사각형입니다. 다음과 같이 이미지를 로딩하면 :

this.load.image('preloadbar', 'assets/images/preloader-bar.png');


우리는 게임의 어디에서든 이미지 자산을 참조 할 수 있습니다 (이 경우 "preloadbar").


(이미지는 HTML5 foriOS 및 Android로 만들어진 레트로 게임 Huungree RPG의 스크린 샷입니다.)


우리는 우리 게임이 수평 및 수직 가운데에 위치하기를 원합니다.

this.scale.pageAlignHorizontally = true;

this.scale.pageAlignVertically = true;

// Now, the game area we defined in main.js:


SideScroller.game = new Phaser.Game(746, 420, Phaser.AUTO, '');


게임의 픽셀을 나타냅니다. 또한 화면의 픽셀을 나타낼 수도 있지만 게임을 전체 화면에 맞추기를 원하는 대부분의 경우에 이 줄을 추가하는 이유는 다음과 같습니다.

this.scale.setScreenSize(true);

// Lastly, we initiate the physics engine and launch the Preload state:

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


this.state.start('Preload');



Preload State

Preload 상태는 모든 게임 에셋이 로드되는 곳입니다. 여기에는 게임에서 사용할 수있는 이미지, 오디오, 타일 맵 및 기타 파일이 포함됩니다.

왜 자산을 미리로드해야합니까?

웹 또는 하드 드라이브에서 파일을 로드하는 경우 시간이 오래 걸립니다. 플레이어는 총알을 쏘고 총알 스프라이트가로드 될 때까지 잠시 시간을 내어 게임이 멈추거나 표시해야하는 이미지가 표시되지 않도록합니다. 미리 로드함으로써, 모든 파일을 장치의 RAM에 로드하므로 게임에서 필요에 따라 이미지 / 리소스를 빠르게 액세스 할 수 있습니다. 처음부터 플레이어가 끔찍한 경험을 하는 것보다 기다리는 것이 가장 좋습니다!

var SideScroller = SideScroller || {};

//loading the game assets

SideScroller.Preload = function(){};

SideScroller.Preload.prototype = {

  preload: function() {

    //show loading screen

    this.preloadBar = this.add.sprite(this.game.world.centerX, this.game.world.centerY, 'preloadbar');

    this.preloadBar.anchor.setTo(0.5);

    this.preloadBar.scale.setTo(3);

    this.load.setPreloadSprite(this.preloadBar);

    //load game assets

    this.load.tilemap('level1', 'assets/tilemaps/level1.json', null, Phaser.Tilemap.TILED_JSON);

    this.load.image('gameTiles', 'assets/images/tiles_spritesheet.png');

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

    this.load.image('playerDuck', 'assets/images/player_duck.png');

    this.load.image('playerDead', 'assets/images/player_dead.png');

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

    this.load.audio('coin', 'assets/audio/coin.wav');

  },

  create: function() {

    this.state.start('Game');

  }


};


첫 번째 부분은 "preloadBar"라는 스프라이트의 생성입니다.이 스프라이트는 게임 영역의 중앙에 배치되며 이전에 Boot 상태로 로드 한 키 "preloadbar"가 있는 이미지를 사용합니다. 

그런 다음 이미지의 앵커 포인트를 중간으로 설정합니다. 이미지가 커지고 축소되거나 회전한다고 가정 해보십시오. 고정점이 변형이 발생한 후에 화면의 같은 부분에 남아있는 점입니다.


막대가 조금 작기 때문에 크기를 3 배로 합니다.

this.preloadBar.scale.setTo(3);


Phaser는 우리가 스프라이트를 프리로드 바 (preload bar)로 지정할 수있게 해줍니다. 기본적으로 게임 요소가 로드되면 풀 사이즈가 될 때까지 커집니다.

this.load.setPreloadSprite(this.preloadBar);


그 후에 모든 요소를로드합니다. 로드가 완료되면 게임 상태로 이동합니다.

main.js에서 SideScroller.Game에 대한 참조를 주석 처리하면 이 시점에서 로딩 막대가 보입니다.



Game State

타일 기반 게임과 Tiled 맵 편집기에 대해서도 이야기 할 필요가 있지만 Game.js에 대한 해골을 넣어 콘솔 오류없이 게임을 완벽하게 실행할 수 있게하십시오.

var SideScroller = SideScroller || {};

SideScroller.Game = function(){};

SideScroller.Game.prototype = {

  preload: function() {

      this.game.time.advancedTiming = true;

    },

  create: function() {

    //create player

    this.player = this.game.add.sprite(100, 300, 'player');

 }, 

  update: function() {

  },

  render: function()

    {

        this.game.debug.text(this.game.time.fps || '--', 20, 70, "#00ff00", "40px Courier");  

    }


};

우리는 여기에서 두 가지 작업을 수행합니다. 

첫번째 우리는 Preload에서 "player"라는 이미지를 사용하여 플레이어에 대한 스프라이트를 생성합니다.

두번째로, 우리는 이니셔티브를 통해 게임의 초당 프레임 수 (fps)를 여기에서 보여줍니다.

this.game.time.advancedTiming = true;

그런 다음 render 메서드에 디버그 텍스트를 표시합니다. Phaser는 60fps로 실행하는 것이 가장 좋습니다. 오늘날 HTML5 게임은 상당히 괜찮은 휴대 전화 및 컴퓨터에서도 이 속도로 실행되는 것이 어렵지만 점점 더 많이 개선되고 있습니다. 나는 성능에 주시하고 그 수가 떨어지는 상황 (나쁜 것)을 감지하기 위해이 숫자를 유지하기를 원합니다.

설명서에서 디버깅에 대해 자세히 읽어보십시오.

더 이상 이동하려면 Tiled에 대해 이야기해야합니다.



Get Tiled Face

Tiled는 Thorbjørn Lindeijer가 만든 정말 멋진 레벨 편집기로, 게임에 타일 기반 레벨을 시각적으로 만들 수 있습니다. 이 레벨에서 사용하는 주요 파일 형식을 TMX 및 XML이라고 합니다. Phaser에서 이러한 레벨을 사용하기 위해 이들을 JSON 파일로 내보낼 것입니다.


"타일"과 "타일 기반 게임"에 관해 이야기 할 때, 레벨이란 개별적인 작은 블록이나 조각으로 구성된 게임을 의미합니다. 이 개념에 100 % 익숙하지 않다면 Wikipedia 정의를 확인하십시오.


타일형식의 새지도를 만들려면 파일 -> 새로 만들기 :


레벨의 너비는 50 타일이고 높이는 6 타일입니다. 이 타일의 크기는 70 x 70 픽셀입니다. 오리엔테이션이 "직교"로 설정되고 레이어 포맷이 "CSV"로 설정되었는지 확인하십시오.


성과에 관한 조언 :지도가 커지고 성능이 최악입니다. 지도가 불필요하게 커지지 않도록하십시오. 또한 타일 크기가 작을수록 좋습니다. 심지어 70도 조금 큽니다. 게임 영역의 크기와 동일합니다. 알아야 할 유일한 방법은 게임을 밖으로 시도하는 것입니다. 느린 경우 게임이 잘 돌아갈 때까지 줄입니다.


다음 단계는 레이어를 만드는 것입니다. 레벨은 서로 위에있는 서로 다른 레이어를 가지게 됩니다. 레이어의 이름은 나중에 코드에서 참조해야하므로 중요합니다.


두 가지 유형의 레이어가 있습니다.

- 타일 레이어 : 타일 / 블록으로 이루어진 레이어.


- 객체 레이어 : 메타 데이터를 포함 할 수있는 벡터 객체를 만드는 레이어입니다.


레이어 아래에있는 "+" 버튼을 사용하여 레이어를 만들 수 있으며 새로 만든 레이어의 이름을 클릭하면 이름을 변경할 수 있습니다. 순서가 중요합니다. 화살표를 사용하여 순서를 재정렬 할 수 있습니다.


이 예에서는 backgroundLayer라는 배경 (플레이어와 충돌하지 않음)과 blockedLayer (바닥, 벽)라는 차단 요소를위한 두 개의 타일 레이어가 있습니다. 또한 게임 요소를 나타내는 하나의 오브젝트 레이어 (objectsLayer) (이 경우 동전)가 있지만 원수, 문, 플레이어의 시작 위치가 될 수도 있습니다.

이것은 모두 하나의 제안이며, 내가 정상적으로 그것을 하는 방법은 결코 관습이나 틀의 규칙이 아닙니다!



타일셋 로드하기

타일셋은 개별 타일 집합으로 이미지에 표시됩니다. 타일 맵에 많은 타일 세트를 사용할 수 있습니다. asset / images / tiles_spritesheet.png 중 하나만 사용합니다. 이 타일셋은 공개 도메인이므로 (자신의 상용 프로젝트에 사용할 수 있음) Kenney가 제작했습니다.


타일 세트를 로드하려면 : Map -> New Tileset

타일의 너비와 높이는 70 픽셀입니다 (가능한 경우 작게하려고하면 성능이 더 좋아집니다.). 타일 간격은 2 픽셀이므로 간격에 대해 2를 입력했습니다.


이름에 대해서는 tiles_spritesheets로 남겨 두었습니다. Phaser 내의 타일 시트를 참조 할 때이 이름을 사용하십시오.



레벨 만들기

이제 레벨을 만들 수 있습니다. 이제 당신에게 달려 있습니다! 내가 한 것은 외계인의 하늘을 표현하기 위해 분홍색 타일로 backgroundLayer를 채운 다음 (양동이 도구 사용) 차단 된 타일로 변경하고 일부 바닥과 장애물을 그렸습니다.



오브젝트 레이어

이 튜토리얼에서는 한 가지 유형의 객체만 추가 할 것입니다. 그러나 비슷한 접근 방식으로 원하는 만큼 객체를 추가 할 수 있습니다.


이 객체들은 타일셋의 스프라이트로 표현 될 것입니다. 그러나 그것은 우리의 단순성을 위한 것입니다. Phaser는 Sprite 객체를 만들 때 Sprite를 표시합니다.


쉽게 설명을 하자면, tiles_spritesheet.png 파일에 나는 동전을 넣었고 이 이미지를 사용하는 물체를 레벨에 배치 할 것입니다. 동전 이미지를 추가하는 대신 문자 "C"가있는 타일을 추가 할 수있었습니다. Phaser는 당신이 말한 것을 로드 할 것이지만 레벨이 실제로 어떻게 보이는지를 시각화 할 수 있다면 더 쉽습니다.


새 오브젝트를 삽입하려면 : objectsLayer 레이어를 선택하고 타일 삽입 버튼 (일몰 사진을 표시하는 버튼)을 클릭 한 다음 표시 할 타일을 클릭 한 다음, 지도에서 오브젝트의 위치를 클릭하십시오.



객체에 속성 추가하기

Phaser는 이러한 요소가 무엇을 나타내며 스프라이트를 사용하여 표시해야하는지 어떻게 알 수 있을까요? 이는 객체에 속성을 추가하면 알수 있습니다.


메뉴에서 "개체 선택"단추를 클릭 한 다음 개체를 두 번 클릭하면 속성을 입력 할 수있는 대화 상자가 나타납니다.

값이 "coin"인 "type"이라는 속성을 입력하십시오. "sprite"라는 또 다른 속성을 설정하고 "coin"을 입력하십시오. 이것은 Phaser에게이 오브젝트에 어떤 이미지를 사용해야하는지 알려줄 것입니다 (Preload에서 동전 이미지를로드하고 "동전"이라고 부르므로 여기에 "coin"을 입력하는 이유입니다).


게임에 따라 더 많은 속성을 입력 할 수 있습니다 (실제로 당신에게 달려 있습니다!). 예를 들어, "value"을 사용하여 다른 동전에 다른 값을 주거나 여기에 다른 "sprite"값을 지정하면 동전 오브젝트에 다른 스프라이트를 사용할 수 있습니다.


여러 요소를 만들려면 생성 한 동전을 마우스 오른쪽 버튼으로 클릭하고 "duplicate"를 클릭하면 해당 속성과 함께 개체를 빠르게 복제 할 수 있습니다. 여러 동전을 복제하여 레벨 주위로 끌 수 있습니다.


더 나아 가기 전에,이 튜토리얼 (및 다른 튜토리얼의 일부)으로 구축하고있는 게임 템플릿으로 동작 하는것이 디폴트의 Phaser 기능이 아닌 것을 분명히 하고 싶습니다. Phaser는 동일한 스프라이트 키를 사용하여 객체로부터 여러 스프라이트를 생성하는 기능 만 제공합니다. Phaser가 이 부분에서 제공하는 것을 배우려면 Tilemap 문서를보십시오.


일단 JSON 파일로 레벨을 내보내십시오. 파일 -> 다른 이름으로 내보내기 -> Json 파일, /assets/tilemaps/level1.json에 저장



타일 맵을 게임에 로드하기

이 프로세스 개요는 다음과 같습니다.

- 타일셋 PNG 파일과 레벨 JSON 파일을 미리로드하십시오 (이 시점에서 Phaser는 이들이 함께 속하는지 알지 못합니다).

- Game 구문에 타일 맵 만들기 추가하기

- 타일셋을 타일 맵에 추가합니다 (기본적으로 JSON 데이터를 실제 이미지와 병합하여 Phaser가 함께 사용한다는 것을 알 수 있습니다)

- 타일 레이어를 타일 맵에 추가합니다 (backgroundLayer 및 blockedLayer).

blockedLayer를 충돌 레이어로 만듭니다.

- 바둑판 식 기반으로 만든 레벨과 같은 크기로 게임 세계 크기를 조정하십시오.


- 레이어 objectsLayer에있는 것을 기반으로 게임 개체를 만듭니다.


타일 맵과 관련하여 프리로드 상태에서 로드 한 내용을 검토해 보겠습니다.

this.load.tilemap('level1', 'assets/tilemaps/level1.json', null, Phaser.Tilemap.TILED_JSON);


this.load.image('gameTiles', 'assets/images/tiles_spritesheet.png');


로더의 문서 페이지에서 더 자세히 살펴보십시오. 머리 속에 "레벨 1"과 "gameTiles"라는 키를 사용하십시오. 이 키는 완전히 임의적이며 나중에이 애셋을 나타 내기 위해 사용할 것입니다.


이제 플레이어 생성 전에 Game.js에 create 메소드의 다음을 추가합니다.

this.map = this.game.add.tilemap('level1');

    //the first parameter is the tileset name as specified in Tiled, the second is the key to the asset

this.map.addTilesetImage('tiles_spritesheet', 'gameTiles');

    //create layers

this.backgroundlayer = this.map.createLayer('backgroundLayer');

this.blockedLayer = this.map.createLayer('blockedLayer');

    //collision on blockedLayer

this.map.setCollisionBetween(1, 100000, true, 'blockedLayer');

    //resizes the game world to match the layer dimensions


this.backgroundlayer.resizeWorld();


처음 세 문장은 단계 설명 다음에 설명이 필요합니다. 충돌에 대한 설명 :

this.map.setCollisionBetween(1, 100000, true, 'blockedLayer');


처음 두 매개 변수는 타일 ID의 범위를 지정합니다. 타일을 사용하여 지도를 저장하면 각 타일마다 고유 한 ID가 지정됩니다. level1.json을 열면이 숫자가 표시됩니다. setCollisionBetween을 사용하면 범위 내의 모든 ID에 대해 충돌을 지정할 수 있습니다. 1에서 100000과 같이 큰 숫자를 지정하면 blockedLayer의 모든 타일에 충돌이 발생합니다.


우리는 게임 세계를 배경 레이어의 크기로 조정하고 있습니다. 동일한 크기이므로 차단 된 레이어 일 수도 있습니다. 중요하지 않습니다.


이제는 목록의 8 단계 (객체 계층에서 객체 생성)를 무시합니다.


이것을 실행하면 브라우저에 두 레이어가 모두 표시됩니다. 이제 플레이어에게 생명을 불어 넣을 시간입니다!



플레이어 추가하기

우리 플레이어는 이미 화면에 있지만 많이하지는 않습니다! 우리는 중력을 줌으로써 바보처럼 주위를 떠 다니는 것이 아닙니다.


이를 위해 우리는 물리엔진을 가능하게 해야합니다. 일단 물리엔진이 가능 해지면, 플레이어 스프라이트에는 body라는 속성이 생기고, 모든 물리 속성과 동작을 갖게됩니다.

//create player

this.player = this.game.add.sprite(100, 300, 'player');

//enable physics on the player

this.game.physics.arcade.enable(this.player);

//player gravity

this.player.body.gravity.y = 1000;

//the camera will follow the player in the world


this.game.camera.follow(this.player);

우리는 플레이어에게 육체뿐 아니라 중력도 주었습니다! 이제 플레이어는 땅에 떨어져야하고, 카메라는 플레이어를 따라 레벨을 따라 가야합니다. 플레이어가 지나가는 대신에 땅에 닿도록 해 봅시다.



충돌 감지

충돌 감지는 두 게임 요소가 서로 교차할때 이를 감지하여 이에 따라 조치를 취할 수 있음을 의미합니다.

Phaser는 충돌과 중첩의 두 가지 옵션을 제공합니다. 두 객체 사이에 충돌 메소드를 지정하면 물리적으로 충돌에 도달합니다. 예를 들어 속도가 멈출 것입니다. 반면에 겹치는 동작을 지정하면 서로 영향을주지 않습니다. 마치 홀로그램을 건드린 것과 같습니다.

우리 게임에서는 플레이어와 바닥 사이에 충돌이 발생하기를 원하며, 플레이어와 동전 사이에 겹치는 동작을 원합니다 (그렇지 않으면 동전을 만질 때처럼 벽에 충돌하는 것처럼 될것입니다).

update 메소드는 모든 게임 틱 ( "초당 여러 번")에서 트리거되기 때문에 충돌 감지를 처리 ​​할 장소입니다.

성능이 너무 낮으면 게임에서 충돌 감지가 제 시간에 일어나지 않고 벽과 바닥을 통과 할 가능성이 있습니다. 그러므로 fps를 주시하십시오 !!

update ()에 다음을 추가 할 수 있습니다.

//collision


this.game.physics.arcade.collide(this.player, this.blockedLayer, this.playerHit, null, this);

게임 상태에 playerHit 메서드를 추가합니다. 이 메소드는 플레이어가 blockedLayer와 충돌 할 때 실행됩니다. 지금은 비어있을 것입니다.

playerHit: function(player, blockedLayer) {}

나중에 코드를 추가하면 됩니다.

this.game.physics.arcade.collide(this.player, this.blockedLayer)

우리 플레이어는 이제 타격을 입어야합니다.



기본 사이드 스크롤링

이제 x에서 속도를 주어 오른쪽으로 이동하고 UP 키를 사용하여 점프하는 기능을 추가합니다.

Phaser 및 내가 알고있는 모든 HTML5 게임 프레임 워크에서 X 축은 오른쪽으로 증가하고 왼쪽으로 감소합니다. Y 축은 내려갈 때 증가하고 올라갈 때 감소합니다. 좌표계의 원점은 왼쪽 상단 구석에 있습니다.

create 메소드가 끝나면 커서 키 (화살표 키)를 시작합니다.

[javscript] // 커서 키로 플레이어 이동

this.cursors = this.game.input.keyboard.createCursorKeys (); [/ javascript]


업데이트 메소드에서 다음을 추가하십시오.

this.player.body.velocity.x = 300; 

if(this.cursors.up.isDown) {

    this.playerJump();


}

우리가 하는 첫 번째 작업은 플레이어에게 x의 속도를 300으로 지정하는 것입니다. 플레이어의 물리 엔진을 활성화 했기 때문에 이 작업을 수행 할 수 있습니다.

위쪽 화살표를 누르면 playerJump라는 메서드가 실행됩니다. 게임 상태에서이 메서드를 추가해 보겠습니다.

playerJump: function() {

    if(this.player.body.blocked.down) {

      this.player.body.velocity.y -= 700;

    }   


  },

플레이어가 바닥에 닿아 있는지 여부를 확인하고 있습니다. 그렇다면 우리는 뛰어 넘을 수 있습니다. 점프는 Y에 속도를 설정하는 것으로 구성됩니다 (올라갈 때 음수).

이제 캐릭터를 레벨로 이동시키고 장애물을 뛰어 넘을 수 있어야합니다. 우리는 여전히 오리를 먹을 수는 없지만 다음에 그것을 추가 할 것입니다.



Ducking

점핑 외에도 우리는 장애물을 피할 수 있기를 원합니다. 우리는 점프와 비슷한 접근법을 따를 것입니다. 유일한 차이점은 플레이어가 이미지 "player_duck.png"( "Preload"의 key "playerDuck")로 향할 때 스프라이트 이미지를 변경하려는 것입니다.

create 메서드에서 플레이어의 중력을 설정 한 후

//properties when the player is ducked and standing, so we can use in update()

var playerDuckImg = this.game.cache.getImage('playerDuck');

this.player.duckedDimensions = {width: playerDuckImg.width, height: playerDuckImg.height};

this.player.standDimensions = {width: this.player.width, height: this.player.height};


this.player.anchor.setTo(0.5, 1);

각 코드 행을 살펴보기 전에 우리가하는 일을 설명하겠습니다. 플레이어가 오리를 잡을 때 이미지를 "playerDuck"로 변경하고 싶습니다. 이미지를 변경하는 것도 한 가지이지만, 우리는 또한 물리엔진 객체를 나타내는 플레이어의 바디를 업데이트 해야합니다. 이미지를 더 작은 이미지로 변경하더라도 명시 적으로 말하지 않으면 본문이 변경되지 않습니다.

더욱이, 우리가 오리를 나타낸 후에는 플레이어가 다시 일어나기를 원하기 때문에 플레이어의 몸을 한 번 더 업데이트해야합니다.

첫 번째 줄은 플레이어의 이미지를 가져 와서 크기를 읽을 수 있도록하는 것입니다.

var playerDuckImg = this.game.cache.getImage('playerDuck');

그런 다음 이 이미지의 크기와 서있는 플레이어의 크기를 저장하여 나중에 다시 사용할 수 있습니다.

this.player.duckedDimensions = {width: playerDuckImg.width, height: playerDuckImg.height};


this.player.standDimensions = {width: this.player.width, height: this.player.height};

플레이어가 수축 할 때 발이 바닥에 있어야합니다 (오리를 잡을 때 시도하는 방식입니다). 따라서 앵커 포인트를 스프라이트 맨 아래에 설정해야합니다.

this.player.anchor.setTo(0.5, 1);

이제 update 함수에서 DOWN 화살표를 읽고 플레이어가 오리가 되게합니다. 또한 플레이어가 더킹 된 상태에서 더 이상 DOWN 키를 누르고 있지 않은 경우, 그 사람이 다시 일어 서기를 바랍니다.

this.player.body.velocity.x = 300; 

if(this.cursors.up.isDown) {

this.playerJump();

}

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

this.playerDuck();

}

if(!this.cursors.down.isDown && this.player.isDucked) {

//change image and update the body size for the physics engine

this.player.loadTexture('player');

this.player.body.setSize(this.player.standDimensions.width, this.player.standDimensions.height);

this.player.isDucked = false;


}

그리고 playerDuck 메소드 :

playerDuck: function() {

//change image and update the body size for the physics engine

this.player.loadTexture('playerDuck');

this.player.body.setSize(this.player.duckedDimensions.width, this.player.duckedDimensions.height);

//we use this to keep track whether it's ducked or not

this.player.isDucked = true;


},

이제 이 방법으로 물체를 피할 수 있습니다!



게임 오버

이 게임에서는 절벽에서 떨어지거나 벽 / 장애물을 피하는 대신 패를 당하면 잃게됩니다. 낙하로 인한 사망을 구현하기 위해 업데이트 방법에 새로운 체크를 추가 할 것입니다. 플레이어의 위치가 게임 세계의 가장자리보다 크거나 같으면 게임을 다시 시작하는 gameOver () 메소드를 실행합니다.

//restart the game if reaching the edge

if(this.player.x >= this.game.world.width) {

    this.game.state.start('Game');


}

플레이어가 죽을 수있는 다른 방법은 장애물을 부딪치는 것입니다. 기본적으로 플레이어의 오른쪽에 차단 된 레이어 사이에 충돌이있을 때. 이는 플레이어가 오른쪽으로 이동하기 때문입니다. 이전에 생성 한 playerHit 메소드에서 이 로직을 구현합니다. 다음과 같습니다.

playerHit: function(player, blockedLayer) {

    //if hits on the right side, die

    if(player.body.blocked.right) {

      //set to dead (this doesn't affect rendering)

      this.player.alive = false;

      //stop moving to the right

      this.player.body.velocity.x = 0;

      //change sprite image

      this.player.loadTexture('playerDead');

      //go to gameover after a few miliseconds

      this.game.time.events.add(1500, this.gameOver, this);

    }


  },

우리가 "alive" 속성을 어떻게 확인하는지보십시오. 이 속성은 페이저의 모든 스프라이트에 존재하지만 렌더링에는 영향을 미치지 않지만 게임 요소를 라이브 또는 죽게하는 것이 대부분의 게임에서 공통적이기 때문에 편의상 배치되었습니다.


플레이어가 오른쪽 (player.body.blocked.right)에 도달하면 움직임을 멈추고 플레이어를 "not alive"로 설정하고 이미지를 preloaded에 로드 한 playerDead 이미지로 변경 한 후 게임을 다시 시작합니다. 1.5 초 (바닥에 닿을 시간을 줄 수 있습니다!).

하지만, 만약 플레이어가 죽었다 할지라도 키를 사용한다면 우리는 여전히 뛰어 넘을 수 있습니다. 플레이어가 점프 또는 오리를 만들기 전에 플레이어가 살아 있는지 확인해야합니다. 업데이트를 수정하면 다음과 같이 보입니다.

update: function() {

    //collision

    this.game.physics.arcade.collide(this.player, this.blockedLayer, this.playerHit, null, this);

    this.game.physics.arcade.overlap(this.player, this.coins, this.collect, null, this);

    //only respond to keys and keep the speed if the player is alive

    if(this.player.alive) {

      this.player.body.velocity.x = 300; 

      if(this.cursors.up.isDown) {

        this.playerJump();

      }

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

        this.playerDuck();

      }

      if(!this.cursors.down.isDown && this.player.isDucked) {

        //change image and update the body size for the physics engine

        this.player.loadTexture('player');

        this.player.body.setSize(this.player.standDimensions.width, this.player.standDimensions.height);

        this.player.isDucked = false;

      }

      //restart the game if reaching the edge

      if(this.player.x >= this.game.world.width) {

        this.game.state.start('Game');

      }

    }


  },



타일형식으로 배열 된 레이어에서 객체로드

지금까지 타일링 된 맵에서 타일 레이어 만 로드했습니다. 우리도 객체 레이어를 만들었던 것을 잊지 마십시오. 이러한 객체를 로드하려면 Tilemap 클래스와 함께 제공되는 메서드를 사용할 수 있지만 그렇게 하려면 원하는 각 객체 유형에 대해 객체 레이어를 만들어야합니다 (각 스프라이트 이미지에 대해서도 마찬가지 임) .

내가 선호하는 또 다른 옵션은 게임의 모든 개체에 대해 단일 개체 계층을 사용하고 각 개체의 개체 유형 및 사용할 스프라이트 이미지를 지정하는 것입니다. 우리가 그렇게 할 수 있도록 게임 상태에 추가 할 두 가지 도우미 메서드를 만들었습니다.

첫 번째 메소드 인 findObjectsByType을 사용하면 특정 유형과 일치하는 객체 레이어의 모든 객체가 있는 배열을 얻을 수 있습니다 (객체 계층을 타일로 만들 때 유형을 어떻게 지정했는지 기억하십시오).

//find objects in a Tiled layer that containt a property called "type" equal to a certain value

  findObjectsByType: function(type, map, layerName) {

    var result = new Array();

    map.objects[layerName].forEach(function(element){

      if(element.properties.type === type) {

        //Phaser uses top left, Tiled bottom left so we have to adjust

        //also keep in mind that some images could be of different size as the tile size

        //so they might not be placed in the exact position as in Tiled

        element.y -= map.tileHeight;

        result.push(element);

      }     

    });

    return result;


  },

두 번째 방법 인 createFromTiledObject는 객체에서 스프라이트를 만드는 것입니다. 예를 들어 첫 번째 방법을 사용하여 모든 "coin"을 가져온 다음 두 번째 방법을 사용하여 실제로 스프라이트를 만듭니다.

//create a sprite from an object

createFromTiledObject: function(element, group) {

    var sprite = group.create(element.x, element.y, element.properties.sprite);

//copy all properties to the sprite

Object.keys(element.properties).forEach(function(key){

sprite[key] = element.properties[key];

});


  },



Loading Coin

두 가지 도우미 메서드를 사용하여 게임에서 모든 동전을 만듭니다. create 메소드에서 world resizing 문 다음에 다음을 추가하십시오.

this.createCoins();

Let's implement the createCoins method:

//create coins

  createCoins: function() {

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

    this.coins.enableBody = true;

    var result = this.findObjectsByType('coin', this.map, 'objectsLayer');

    result.forEach(function(element){

      this.createFromTiledObject(element, this.coins);

    }, this);


  },


여기에 무슨 일이 일어날까요? :

우리는 동전이라는 새로운 스프라이트 그룹을 만듭니다. Phaser에서는 그룹을 사용하여 동일하거나 유사한 속성 및 동작을 가진 요소 집합과 쉽게 작업 할 수 있습니다. 예를 들어, 적, 수집품, 아이템 등에 대한 그룹을 만들 수 있습니다.

우리는 전체 그룹을 대상으로 물리 시스템을 활성화합니다.

우리는 objectsLayer에서 모든 동전 오브젝트를 찾습니다.


우리가 가진 것은 일련의 사물들이었습니다. 이제 우리는 이 객체들 각각에 대해 실제 스프라이트를 만들고 싶습니다.

우리는 플레이어가 동전을 수집하기를 원합니다. 플레이어가 동전에 닿으면 플레이어의 속도가 동전의 영향을 받지 않아야 하므로 충돌 대신 겹침 유형을 사용합니다. 업데이트 방법 :

this.game.physics.arcade.overlap(this.player, this.coins, this.collect, null, this);

다음에 collect 메소드를 구현해 보겠습니다.



소리


Phaser에서 사운드를 재생하는 것은 매우 간단하며 다른 애셋을 로드 / 사용하는 것과 동일한 방식을 따릅니다. Preloader에서 먼저 로드해야합니다.

this.load.audio('coin', 'assets/audio/coin.wav');

그런 다음 게임 상태의 create 메소드에서 오디오 객체를 만듭니다.

//sounds


this.coinSound = this.game.add.audio('coin');

사운드를 재생하려면 사운드 객체의 play () 메서드를 사용하면 됩니다. 우리는 collect 메서드에서 동전 사운드를 재생합니다 (사용자가 동전을 쥐었을 때).

collect: function(player, collectable) {

    //play audio

    this.coinSound.play();

    //remove sprite

    collectable.destroy();


  },

이상적으로 동전을 추적하고 총계를 화면에 표시하고 싶지만,이 예에서는 그 부분을 생략합니다. 



사운드 관련 추가 노트

Phaser는 사용 가능한 경우 WebAudio API를 사용하고, 그렇지 않으면 AUDIO 태그를 사용하여 사운드를 나타냅니다. 이것은 자동으로 처리됩니다.

서로 다른 브라우저는 오디오 형식에 대한 지원이 다릅니다. Phaser에서는 동일한 오디오에 대해 여러 개의 오디오 파일을 전달할 수 있으므로 엔진이 브라우저에 맞는 형식으로 재생됩니다. 


가능한 한 오디오 파일을 압축하십시오. 무료 사운드 유틸리티 인 Audacity를 사용하면됩니다. 나는 일반적으로 프로젝트 속도 (HZ)를 가능한 한 많이 내리고 품질은 좋습니다. 그런 다음 파일을 OGG 및 MP3로 내 보냅니다.



터치 컨트롤러

이 섹션에서는 HTML5 Virtual Game Controller라는 외부 라이브러리를 사용하여 터치 컨트롤러를 추가 할 것입니다. 페이지를 체크 아웃하여 다양한 컨트롤러 유형에 대한 스크린 샷과 코드 예제를 확인하십시오.

우리 게임에서 우리는 단지 두 개의 버튼을 원할뿐입니다. 하나는 점핑을 위한 것이고 다른 하나는 더킹을 위한 것입니다. 이 게임을 게시하려면 이 작업을 위해 화면을 스와이프 하거나 터치하는 것을 선호하지만 이 튜토리얼에서는 터치 컨트롤러 사용법을 포함하기를 원했습니다.

우리가 한 첫번째 일은 우리의 인덱스 페이지에 gamecontroller.js (또는 gamecontroller.min.js)를 포함 시켰습니다. GameController라는 전역 개체가 만들어집니다.

이 컨트롤러는 CANVAS에 그려집니다. 우리는 상태를 다시 시작할 때마다 다르게 그려지기를 원합니다. 다시 그려집니다. 게임 상태의 create 메소드에서 add :

//init game controller

this.initGameController();

Lets implement this method:

initGameController: function() {

    if(!GameController.hasInitiated) {

      var that = this;     

      GameController.init({

          left: {

              type: 'none',

          },

          right: {

              type: 'buttons',

              buttons: [

                false,

                {

                  label: 'J',

                  touchStart: function() {

                    if(!that.player.alive) {

                      return;

                    }

                    that.playerJump();

                  }

                },

                false,

                {

                  label: 'D',

                  touchStart: function() {

                    if(!that.player.alive) {

                      return;

                    }

                    that.pressingDown = true; that.playerDuck();

                  },

                  touchEnd: function(){

                    that.pressingDown = false;

                  }

                }

              ]

          },

      });

      GameController.hasInitiated = true;

    }


  },

GameController.hasInitiated는 컨트롤러가 시작되었는지 여부를 추적하여 두 번하지 않도록 추적하는 데 사용할 변수입니다.

우리가 키보드로 했던 것과 비슷한 방식으로 player.alive를 확인하고 어떻게 작동하는지 보십시오.

업데이트에서 변경하고자 하는 한 가지 사항은 다운 키가 눌러지지 않았지만 오리 버튼이 눌려지고있는 경우 플레이어가 일어 서기를 원하지 않기 때문에 다음을 대체하십시오.

//instead of: if(!this.cursors.down.isDown && this.player.isDucked) {


if(!this.cursors.down.isDown && this.player.isDucked && !this.pressingDown) {

이젠 모바일에서도 키보드를 사용하지 않고도 사용할수 잇습니다.


이 튜토리얼의 게임 소스는 링크로 접속하여 다운 받으시면됩니다.

Posted by 마스터킹
|

Phaser는 tweens와 파티클 덕택에 타이틀 스크린에 생명을 불어 넣을 수있게 해줍니다. 그리고 몇 줄의 코드로 우리는 약 1 분 안에 간단하고 심플한 타이틀 화면을 다이나믹한 화면으로 만들수 있습니다.


왼쪽에는 정적인 타이틀 화면이며, 오른쪽은 트윈과 파티클을 이용한 다이나믹한 화면입니다.


 


어느 것이 더 전문적인 것 같습니까? 게임 타이틀을 왼쪽에서 오른쪽으로 바꾸는데 걸리는 시간은 오래 걸리지 않습니다.


다음 코드는 왼쪽 화면을 생성하는 코드 입니다.

var GlobezGame = GlobezGame || {};

GlobezGame.GameTitle = function(){

startGame = false;

};

GlobezGame.GameTitle.prototype = {

   create: function(){

   console.log("%cStarting game title", "color:white; background:red");

this.add.image(0,0,"background");

          //

var gameTitleSeaLife = this.add.image(160,70,"gametitle_sealife");

          gameTitleSeaLife.anchor.setTo(0.5,0.5);

//

var gameTitleVs = this.add.image(190,120,"gametitle_vs");

          gameTitleVs.anchor.setTo(0.5,0.5);

//

var gameTitleMines = this.add.image(160,160,"gametitle_mines");

          gameTitleMines.anchor.setTo(0.5,0.5);

//

   var playButton = this.add.button(160,320,"playbutton",this.playTheGame,this)

playButton.anchor.setTo(0.5,0.5);

},

playTheGame: function(){

if(!startGame){

startGame = true

alert("Start the game!!");

}

}


}


다음 코드는 오른쪽 화면을 생성하는 코드 입니다.

var GlobezGame = GlobezGame || {};

GlobezGame.GameTitle = function(){

startGame = false;

};

GlobezGame.GameTitle.prototype = {

   create: function(){

   console.log("%cStarting game title", "color:white; background:red");

this.add.image(0,0,"background");

          //

          var bubblesEmitter = this.add.emitter(160, 500, 50);

          bubblesEmitter.makeParticles("bubble");

          bubblesEmitter.maxParticleScale = 0.6;

          bubblesEmitter.minParticleScale = 0.2;

          bubblesEmitter.setYSpeed(-30, -40);

          bubblesEmitter.setXSpeed(-3, 3);

          bubblesEmitter.gravity = 0;

          bubblesEmitter.width = 320;

          bubblesEmitter.minRotation = 0;

          bubblesEmitter.maxRotation = 40;

          bubblesEmitter.flow(15000, 2000)

//

var gameTitleSeaLife = this.add.image(160,70,"gametitle_sealife");

          gameTitleSeaLife.anchor.setTo(0.5,0.5);

          gameTitleSeaLife.angle = (2+Math.random()*5)*(Math.random()>0.5?1:-1);

          var seaLifeTween = this.add.tween(gameTitleSeaLife);

seaLifeTween.to({

angle: -gameTitleSeaLife.angle

},5000+Math.random()*5000,Phaser.Easing.Linear.None,true,0,1000,true);

//

var gameTitleVs = this.add.image(190,120,"gametitle_vs");

          gameTitleVs.anchor.setTo(0.5,0.5);

          gameTitleVs.angle = (2+Math.random()*5)*(Math.random()>0.5?1:-1);

          var vsTween = this.add.tween(gameTitleVs);

vsTween.to({

angle: -gameTitleVs.angle

},5000+Math.random()*5000,Phaser.Easing.Linear.None,true,0,1000,true);

//

var gameTitleMines = this.add.image(160,160,"gametitle_mines");

          gameTitleMines.anchor.setTo(0.5,0.5);

          gameTitleMines.angle = (2+Math.random()*5)*(Math.random()>0.5?1:-1);

          var minesTween = this.add.tween(gameTitleMines);

minesTween.to({

angle: -gameTitleMines.angle

},5000+Math.random()*5000,Phaser.Easing.Linear.None,true,0,1000,true);

//

   var playButton = this.add.button(160,320,"playbutton",this.playTheGame,this)

playButton.anchor.setTo(0.5,0.5);

          playButton.angle = (2+Math.random()*5)*(Math.random()>0.5?1:-1);

          var playTween = this.add.tween(playButton);

playTween.to({

angle: -playButton.angle

},5000+Math.random()*5000,Phaser.Easing.Linear.None,true,0,1000,true);

          //

          var blackFade = this.add.sprite(0,0,"blackfade");

          var fadeTween = this.add.tween(blackFade);

fadeTween.to({

alpha:0

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

},

playTheGame: function(){

if(!startGame){

startGame = true

alert("Start the game!!");

}

}


}


오른쪽 화면 생성 코드에서 볼 수 있듯이 입자 방아쇠와 일부 컷 / 과거 트윈을 추가 했으므로 코드를 원하는대로 변경할 수 있습니다. 일부 좌표 만 변경하면됩니다.


그 결과가 더 좋고, 노력이 최소한이며, 정적 타이틀 화면이 더 이상 보이지 않기를 바랍니다. 두 프로젝트의 소스 코드를 다운로드 할 수 있습니다.



Posted by 마스터킹
|

레벨 디자인

이전 문서에서 나는 Gravity Quest의 비주얼과 사운드를 어떻게 만들 었는지 설명했습니다. 게임 플레이, 영상 및 사운드를 제작했으면 다음 단계는 레벨을 제작하는 것입니다. Gravity Quest의 레벨 생성은 도전적이었습니다. 단일 레벨 디자이너는 반복적 인 레벨로 인해 특정 스타일을 고수 할 위험이 있으므로 플레이어가 빠르게 지루해집니다. 아직도 게임을 합리적으로 판매하기 위해 적어도 20 레벨 (완전히 주관적으로 선택한 숫자)을 특징으로했습니다. 따라서 레벨을 만들고 편집하고 표현할 수있는 효율적인 방법을 찾아야 했습니다. 그리고 궁극적으로 이것이 내가 레벨을 설계하는데 적 접근 방식을 시도하기로 결정한 이유입니다.



레벨 표현


Gravity Quest의 세계를 만들기 시작하기 전에 한 가지 결정을 내려 레벨 데이터를 표현하는 방법을 결정했습니다. 플랫폼 게임에서 타일 맵은 Phaser가 지원하는 일반적인 접근 방식입니다. 타일 맵에서 레벨은 거친 타일 (예 : 16x16, 32x32 또는 64x64 픽셀 크기의 사각형)로 나뉩니다. 모든 타일에 대해 렌더링 할 텍스처와 타일이 플레이어 또는 적과 충돌하는지 여부가 지정됩니다. Tile 맵은 Phaser에서의 지원과 Tiled 맵 에디터와 같은 툴을 생성 할 수있는 Gravity Quest의 유일한 접근 방법이었습니다.


그러나 Gravity Quest의 요구 사항은 타일 맵을 사용하여 제외되었습니다. 레벨의 요소는 다양한 크기로 되어 있으며 거친 격자에 꼭 맞을 필요는 없습니다. 게다가, 그들의 위치는 세밀하게 아주 정교하게 정의 될 필요가 있습니다. 예를 들어, 레벨 13에서 우주 비행사는 노바와 충돌하기 전에 마지막 순간에 소행성으로 가속하기 위해 중력총을 사용해야합니다. 이 레벨을 생성하기 위해 소행성과 왜소 노바가 픽셀 단위로 정확하게 배치되었습니다.


결과적으로, Gravity Quest에있는 레벨 데이터를 JavaScript 객체에 저장하기로 결정했습니다 (목록 1 참조). 레벨의 모든 요소는 최소한 x 및 y 좌표를 나타냅니다. 게임 플레이 상태의 create 함수에서 레벨을 설정할 때 JavaScript 객체가 반복되고 모든 요소가이 좌표에 따라 배치됩니다. 일부 요소는 사용중인 스프라이트의 이름을 지정하는 키와 충돌 감지에 사용되는 반경을 나타냅니다 (게임 플레이 구현 : 충돌 감지 관련 도움말 참조). 예를 들어, 소행성이나 nova가 다른 크기를 가질 수 있기 때문에이 정보가 필요합니다. maxDistance 값은 우주 비행사가 손실되기 전에 가장 가까운 소행성에서 얼마나 멀어 질 수 있는지를 결정합니다. 

Game.Level_02 = {
 player: {
  x: 431,
  y: 607,
 },
 asteroids: [
  {
   x: 680,
   y: 410,
   key: "asteroid_32",
   radius: 16
  },
  {
   x: 465,
   y: 548,
   key: "asteroid_32",
   radius: 16
  }
 ],
 target: {
  x: 606,
  y: 307,
  key: "target",
  radius: 18
 },
 goodies: [
  {
   x: 687,
   y: 514
  },
  {
   x: 776,
   y: 361
  },
  {
   x: 517,
   y: 460
  }
 ],
 maxDistance: 250
}

목록 1 : Gravity Quest의 레벨 2를 정의하는 JavaScript 객체.



레벨 편집기 구현하기

Gravity Quest의 레벨 빌드를 시작할 때 처음에는 텍스트 편집기를 기본 도구로 사용했습니다. 즉, 코드를 작성하여 레벨 요소와 위치를 정의했습니다. 이것은 쉽게 문제가 되는 것으로 판명되었습니다 : 레벨이 많은 요소로 구성되어있을 때 코드의 위치를 ​​추적하기가 어려워지며 코드를 이동하는 것이 번거로워집니다. 생산성을 높이기 위해 필자는 시각화 및 최종 편집 작업을 시작했습니다. 그래서 레벨 편집기를 개발하기 시작했습니다. 필자는 HTML5 캔버스 요소의 드래그 앤 드롭 기능을 지원하는 Fabric.js를 사용하여 레벨을 표시하고 요소를 편집했습니다. 나는 다음과 같은 기능 세트에 도달 할 때까지 편집기를 끊임없이 수정했습니다.

- 편집기는 blueprint로 기존 레벨 중 하나를 로드하여 새로운 레벨을 생성 할 수 있습니다.

- 팔레트에서 요소를 추가 및 선택하고 휴지통 버튼을 눌러 제거 할 수 있습니다.

- 드래그 앤 드롭을 사용하여 요소를 이동할 수 있습니다.

- 설계된 레벨은 편집기에서 테스트 할 수 있습니다. 그렇게 하기 위해, Gravity Quest는 팝업 창에서 시작되고 편집기의 레벨은 해당 팝업 창으로 전달되어 즉시 시작됩니다. 이 기능을 사용하면 레벨을 신속하게 테스트하고 설계를 반복 할 수 있습니다.

- 디자인 수준의 양방향 데이터 바인딩 JSON 표현이 지속적으로 표시됩니다. a)는 레벨 요소 (예 : 좌표)를 프로그래밍 방식으로 조정하고 b) 복사 및 붙여 넣기 레벨을 허용합니다.

- 브라우저의 로컬 저장소는 변경 사항을 지속적으로 저장하여 작업을 일시 중지하고 나중에 계속할 수 있습니다.


비디오 1은 Gravity Quest의 레벨 편집기에 대한 간단한 데모를 보여줍니다.

 
비디오 1 : Gravity Quest의 레벨 편집기 데모 

참여 수준 디자인

처음에는 레벨을 생성하기 위해 내 노트북에서만 실행되는 편집기 (임시 버전)를 사용했습니다. 편집기는 디자인하는 과정을 가속화했습니다. 그러나 20 레벨 이상을 만드는 것은 지루하고 반복적 인 레벨을 만들 수 있다는 것을 깨달았습니다. 따라서 편집자가 친구와 가족이 게임에 자신의 레벨을 제공 할 수 있도록 허용하기로 결정했습니다.


편집기로 참여 레벨 디자인을 사용하려고 약간 확장했습니다. 완성 된 레벨의 이름을 지정하고 편집자로부터 검토하여 최종 게임에 포함시킬 수 있습니다. 그런 다음 Heroku에 편집기를 배치하여 웹에서 액세스 할 수 있도록했습니다. 편집자가 (대부분) 안정 버전으로 배포되면 선택한 친구에게 전자 메일을 보내서 게임에 레벨을 기여하도록 요청했습니다. 처음에 16 명에게 요청을 보냈습니다. 일부 업데이트 및 알리미에서는 반복적으로이 숫자를 늘려 궁극적으로 25 명이 해결되었습니다.


Gravity Quest의 친구들이 레벨을 만들도록 허용하는 것은 여러모로 긍정적인 효과가 있었습니다. 첫째, 최종 게임을 완성 할 수있는 수준으로 발전했습니다. 대체로 5 명의 친구로부터 10 레벨을 받았습니다. 게임 레벨의 40 %만이 아니라, 모두 아주 즐겁고 재미있게 플레이 할 수 있습니다! 둘째, 참여 방식은 게임을 베타 테스트 할 수있는 견고한 수단임을 입증했습니다. 레벨 디자이너가 레벨을 테스트 할 수있게되면서 게임 플레이, 영상 및 사운드를 자연스럽게 검토했습니다. 레벨 디자이너의 피드백은 게임을 개선하는 데 매우 중요했습니다. 예를 들어, 우주 비행사가 움직이는 속도에 대한 힌트, 중력 총에 의해 목표 된 경우 우주 비행사에게 소행성을 이동하라는 제안, 시차 스크롤을 구현하기위한 제안, 또는 버전 1.1에서 3 가지 보너스 레벨을 추가하라는 제안이 포함됩니다. 게임. 셋째, 레벨 디자인에 대한이 참여 방식은 조기 마케팅 조치로 작용하여 진행중인 게임에 대한 단어를 전파했습니다.

물론 모든 공헌자는 Gravity Quest의 출품작에 대해 감사를 표합니다.



결론

게임을 개발할 때 많은 단계를 생각해내는 것이 어려운 작업입니다. Gravity Quest의 경우, 레벨 편집기는 레벨 설계에 대한 시각적 피드백을 제공하고 레벨 편집 속도를 높이며 아이디어와 디자인을 신속하게 테스트 할수 있어 작업을 쉽게 수행 할수 있었습니다.


웹을 통해 배포되고 액세스 할 수있는 편집기는 가족과 친구들을 레벨 생성에 추가 할수 있었습니다. 이것은 훌륭한 수준일뿐 아니라 귀중한 피드백과 게임에 대한 토론에도 귀결되었습니다. 그리고 궁극적으로 개발 과정을 보다 사회적이고 재미있게 만들어주는 것으로 입증되었습니다. 게임에 수준을 수동으로 만들어야하는 경우이 방법을 적극 권장합니다.


이 시리즈의 다음 기사에서는 Apple App Store에서 Gravity Quest의 배포에 대해 설명합니다.



Posted by 마스터킹
|

비주얼 및 사운드

이전 문서에서, Gravity Quest에서 사용 된 충돌 감지에 대해 논의 했으므로 (게임의 게임 플레이에 대한 부분을 마무리 함) 이 문서에서는 Gravity Quest의 비주얼과 사운드가 어떻게 생겨 났는지 설명합니다.


Gravity Quest의 경우 영상과 사운드에 관한 저의 목표는 매우 낮았습니다. 나는 결코 예술가가 아니 었습니다. 솔직히 뛰어난 시각 효과와 사운드를 만드는 데 드는 노력을 두려워했습니다. 따라서 내 목표는 일관된 경험을 창조하는 것 - Gravity Quest에 뚜렷한 느낌을 주기위한 것입니다.



스프라이트 페인팅


여러 가지 이유로 픽셀 아트 스타일을 추구하기로 결정했습니다. 첫째, 픽셀 아트는 제약을 유도하며 응집력있는 스타일을 만드는 데 도움이됩니다. 둘째, 픽셀 아트는 많은 주목을 받고있는 이 글의 일부이며 결국에는 게임의 미래로 여겨지기도합니다. 셋째, 제한된 양의 픽셀로 인해 비주얼을 만드는 데 필요한 노력이 줄어들 것이라고 추론했습니다 (이는 매우 순진한 생각이었습니다). 이 훌륭한 기사는 픽셀 아트와 어떤 관계가 있는지 설명합니다.


게임의 스프라이트를 만들기 위해 필자는 주로 Mac OS 용 Pixen 응용 프로그램을 사용했습니다. Apple App Store에서 구하거나 GitHub에서 무료로 구할 수있는 xCode 프로젝트에서 컴파일 할 수 있습니다. Pixen은 잘 선택된 드로잉 도구 세트, 작업 할 레이어 및 애니메이션 제작을 지원하는 픽셀 아트를 제작하도록 설계되었습니다.


아트 워크 만들기는 연속적인 반복으로 표시되었습니다. 예를 들어, 이미지 1은 우주 비행사의 일부 버전을 보여줍니다. 

이미지 1 : 우주 비행사의 일부 버전


게임의 스프라이트에 사용할 모양과 색상에 영감을 얻기 위해 Google의 이미지 검색을 사용하여 소행성, 폭발물 또는 우주선과 같은 이러한 객체의 사진을 얻었습니다.



애니메이션


애니메이션에 많은 노력을 기울였습니다. 확실히 제한적 임에도 불구하고 게임에 애니메이션이 있습니다. Novae는 이미지 2에서 보여지는 것처럼 깜박임 현상이 나타나며, 이미지가 항상 활성화 되도록 합니다. 반 중력력 필드는 이미지 3 에서 처럼 중력파 확대를 지속적으로 방출하며 Pixen의 원 도구로 구속력이 필요합니다. 확장된 광선의 투명도가 높아져서 사라지는 것을 시뮬레이트하기 때문에 마지막 두 프레임이 흐리게 보입니다. 미네랄은 이미지 4에서 볼 수 있듯이 자주 번쩍 거립니다. 외계인은 이미지 5에서 볼 수있는 것처럼 살아 있다는 것을 암시하기 위해 조명을 돌립니다. UFO가 우주 비행사를 따라 가면이 표시등이 빨간색으로 바뀝니다. 마지막으로, 우주 비행사는 이미 이미지 1에 표시된 몇 가지 최소 애니메이션을 특징으로합니다.

이미지 2 : 노바 애니메이션


이미지 3 : 반 중력 영역 애니메이션



이미지 4 : 광물 애니메이션


이미지 5 : 외계인 애니메이션


애니메이션은 목록 1의 nova 및 우주 비행사에 표시된 것처럼 preload 함수에서 애니메이션 프레임을 포함하는 스프라이트 시트를 먼저 로드하여 Phaser에서 사용할 수 있습니다. 모든 프레임의 너비와 높이가 32 픽셀임을 추가로 정의합니다. 이 코드와 다음 코드는 문서에서 제시 한 게임 플레이 구현, 특히 충돌 감지에 대한 내용을 확장합니다.

...
// instead of: game.load.image('nova', 'assets/nova.png');
game.load.spritesheet('nova', 'assets/nova_animation.png', 32, 32);
// instead of: game.load.image('astronaut', 'assets/astronaut.png');
game.load.image('astronaut', 'assets/astronaut_animation.png', 20, 32);
...

목록 1 : 우주 비행사와 우주 비행사를위한 애니메이션 프레임을 포함하는 스프라이트 시트 로딩.


그런 다음 create 함수 내에서 목록 2와 같이 해당 스프라이트를 만들 때 애니메이션이 정의됩니다. 애니메이션을 만들 때 프레임을 재생해야하는 순서를 정의 할 수 있습니다. nova의 경우, 정의 된 애니메이션이 즉시 시작됩니다. 애니메이션을 시작할 때 프레임 속도 (여기 : 5)와 애니메이션을 반복할지 여부 (여기 : true)를 지정할 수 있습니다.

...
// (randomly) place novae and add to group:
this.novae = game.add.group();
for (var i = 0; i < 3; i++) {
 var nova = game.add.sprite(
  game.rnd.integerInRange(100, game.world.width - 100),
  game.rnd.integerInRange(100, game.world.height - 100),
  'nova');
 nova.anchor.setTo(0.5, 0.5);

 // define and start animation:
 nova.animations.add('flicker', [0,1,2,3,2,1]);
 nova.animations.play('flicker', 5, true);

 this.novae.add(nova);
};
...
// create astronaut at the center of the world:
this.astronaut = game.add.sprite(game.world.width * 0.5, game.world.height * 0.5, 'astronaut');
this.astronaut.animations.add('idle', [0]);
this.astronaut.animations.add('firing', [1]);
...

목록 2 : 애니메이션 정의 및 시작.


우주 비행사의 경우 update 함수에서 중력총이 발사 될 때 애니메이션이 재생됩니다. 따라서 코드는 목록 3과 같이 중력총이 발사되었는지 여부를 확인하는 데 사용되는 if-else 문에 배치됩니다.

...
if(game.input.activePointer.isDown){
 ...
 // animate astronaut:
 this.astronaut.animations.play('firing', 1, false);
} else {
 ...
 // animate astronaut:
 this.astronaut.animations.play('idle', 1, false);
}
...

목록 3 : 중력 총 발사시 우주 비행사 애니메이션 재생


전반적으로 나는 픽셀 아트에 대한 나의 순진한 아이디어가 틀렸다고 Gravity Quest의 비주얼을 제작함으로써 배웠습니다. 픽셀 수가 제한되어 있어도 반드시 노력이 줄어들지는 않습니다. 오히려 유도 된 구속 조건은 디자이너가 원하는 모양을 만들고, 관련된 세부 사항에 초점을 맞추고, 적절한 비율로 못 박는 것을 더욱 어렵게 만듭니다.



타이포그래피


타이포그래피의 경우, 나는 pixelart-esque 모양을 위해 무료 (상업용으로도 사용 가능) 글꼴 실크 스크린을 선택했습니다. Silkscreen은 TrueType 형식으로 제공되고 Phaser는 비트 맵 글꼴이 필요합니다. 트루 타입 글꼴에서 비트 맵 글꼴을 만들려면 littera 비트 맵 글꼴 생성기를 사용했습니다. 주어진 글꼴 파일에 대해 비트 맵 글꼴 스프라이트 시트를 구성 가능한 크기 및 색상으로 출력합니다. 색상 (흰색 또는 검은 색)과 크기가 다른 여러 가지 변형 된 실크 스크린 비트 맵 글꼴을 만들었습니다.



시각 효과


시각적 효과가 제대로 적용되면 덤덤한 게임을 진정으로 흥미로운 것으로 만들 수 있습니다. 여기에 Gravity Quest에서 사용 된 시각 효과에 대한 설명이 있습니다.



파티클 이미터


일부 효과는 Phaser의 파티클 이미터에 달려 있습니다. 파티클 이미터는 개발자 정의 방식으로 여러 개의 스프라이트 인스턴스를 생성합니다. 비, 폭발, 연기, 스파크 링 등과 같은 효과를 구현하는 데 사용할 수 있습니다. Gravity Quest에서는 중력 총의 사용을 나타내는 데 하나의 입자 방사체가 사용됩니다. 다음 코드는이 시리즈의 두 번째 기사에서 Gravity Quest의 게임 플레이 구현을 논의 할 때 제시되는 코드를 확장합니다. Listing 4는 preload 함수에서 필요한 스프라이트를 로드하는 방법을 보여준다. 두 입자는 이미지 6에 나와 있습니다.



이미지 6 : 중력총 이미터의 입자.

game.load.image('gun_particle_1', 'assets/gun_particle_1.png');
game.load.image('gun_particle_2', 'assets/gun_particle_2.png');

목록 4 : 중력총 이미터용 입자로드.


Listing 5는 gunEmitter가 create 함수에서 정의된 방법을 보여준다. 처음 두 매개 변수는 이미터 위치를 지정합니다. 이미터는 위치가 지속적으로 업데이트되므로 처음에는 게임에 위치합니다. thrid 매개 변수는 이미 터에서 최대 입자 수를 정의합니다. 다음 줄에서는 이미로드 된 입자 스프라이트가 이미터에 사용되도록 정의됩니다.

...
this.gunEmitter = game.add.emitter(0, 0, 25);
this.gunEmitter.makeParticles(['gun_particle_1', 'gun_particle_2']);
...

목록 5 : create 함수에서 중력총용 파티클 이미터 정의하기


이미터를 정의하면 우주 비행사가 중력총을 발사하는 데 사용할 수 있습니다 (Listing 6 참조). 제시된 코드는 중력총이 발사되었는지 여부를 결정하는 데 사용되는 if-else 문으로 update 함수내에서 실행됩니다. 우선 이미터의 위치는 우주 비행사에게 가장 가까운 소행성의 가장자리 (중력총에 의해 목표 된 것)로 업데이트됩니다. 다음으로 이미터가 이미 실행 중이 아니면 시작됩니다. 마지막으로 이미터의 알파와 모든 입자의 알파는 우주 비행사가 가장 가까운 소행성까지의 거리에 따라 설정됩니다. 이렇게하면 중력총이 멀리에서 발사되는 경우 빛이 반짝 일 수 있습니다.

...
if(game.input.activePointer.isDown){
 ...
 // emit particles:
 this.gunEmitter.x = closestAsteroid.x - Math.cos(angle) * closestAsteroid.width * 0.5;
 this.gunEmitter.y = closestAsteroid.y - Math.sin(angle) * closestAsteroid.width * 0.5;
 if(!this.gunEmitter.on){
  this.gunEmitter.start(false, 100, 15);
 }
 this.gunEmitter.alpha = 1 - distance / 250; // make emitter visible
 ...
} ...

목록 6 : 우주 비행사가 중력총을 발사 할 때 이미터 시작하기.


이 효과를 완료하기 위해 이미터는 중력총이 더 이상 발사되지 않으면 중지됩니다 (7). 다시 이 코드는 중력 총 발사 여부를 결정하는 데 사용되는 if-else 문이update 함수에서 사용됩니다 .

...
if(game.input.activePointer.isDown){
 ...
} else {
 ...
 // stop particle emitter:
 this.gunEmitter.on = false;
}
...

목록 7 : 중력총이 더 이상 발사되지 않을 때 이미터를 멈춤.


Gravity Quest의 파티클 이미터는 외계인이 충돌 할 때 우주 비행사가 노벨, 열 활성 소행성 또는 외계인에 의해 살해되거나 우주선의 연기를 만들때 사용됩니다. 전반적으로, 그들은 Phaser로 만든 게임에 생명을 불어 넣는 다목적 도구입니다.



시차 스크롤링


2D 게임에서 일반적으로 사용되는 또 다른 효과는 시차 스크롤입니다. 즉, 카메라가 움직일 때 배경이 전경의 대상과 다른 속도로 움직여 심도있는 효과를 만들어냅니다. Phaser는 수평 시차 스크롤링 (종종 platformer 스타일의 게임에서 사용됨)을 구현하는 기능을 제공합니다. 그러나 Gravity Quest에서는 우주 비행사가 수평 또는 수직으로 움직일 수 있으므로 내 자신의 솔루션을 구현하게되었습니다.


작동하려면 먼저 카메라 동작을 활성화해야 합니다. 그렇게 하기 위해서, 게임 세계의 범위는 목록 8에있는 것처럼 create 함수에서 실제 게임보다 크게 만들어야한다. (참고 : 이것은 무작위 적으로 데모에서 훨씬 더 분산 된 소행성과 nova로 이어질 것입니다).

...
// make the world larger than the actual canvas (for camera to be able to follow):
game.world.setBounds(0, 0, 1280, 960);
...

목록 8 : 게임월드 경계 설정.


다음으로, create 함수에서 게임의 카메라는 목록 9와 같이 우주 비행사를 따라 가도록 설정 했으며, Phaser에는 여러 가지 FOLLOW 모드가 있습니다. FOLLOW_TOPDOWN_TIGHT는 우주 비행사의 움직임이 카메라에 의해 수평 및 수직으로 밀접하게 따르는 것을 의미합니다.

...
// make camera follow player:
game.camera.follow(this.astronaut, Phaser.Camera.FOLLOW_TOPDOWN_TIGHT);
...

목록 9 : 우주 비행사를 따라가는 카메라 설정.


시차 스크롤링이 작동하려면 배경 스프라이트를 preload 함수에서 로드하고 목록 10에 표시된 것처럼 작성 함수에 타일 스프라이트로 배치해야합니다. 타일 스프라이트에서 정의 된 스프라이트는 수평 및 수직으로 반복됩니다.

...
// load background sprite in the preload function:
game.load.image('background', 'assets/background.png');
...
// place background in the create function:
this.background = game.add.tileSprite(0, 0, game.world.width, game.world.height, 'background');
...

목록 10 : 배경로드 및 배치


시차 스크롤링을 구현하기 위해, 먼저, create 함수에서 카메라 위치는 목록 11에 표시된대로 저장됩니다. 이 마지막 위치는 시차 스크롤을 구현하기 위해 다음 update 루프에서 사용되기 때문에 cameraLastX 및 cameraLastY로 표시됩니다.

...
// store cameras position:
this.cameraLastX = this.camera.x;
this.cameraLastY = this.camera.y;
...

목록 11 : create 함수에 카메라 위치를 저장.


update 함수에서 카메라의 현재 위치는 목록 12에 표시된 마지막 저장된 위치와 비교할 수 있습니다. 위치가 변경되면 배경이 이동되고 새 위치가 저장됩니다.

...
// compare current camera position with last one and shift background if it differs:
if(game.camera.x !== this.cameraLastX){
 this.background.x -= 0.2 * (this.cameraLastX - game.camera.x);
 this.cameraLastX = game.camera.x;
}
if(game.camera.y !== this.cameraLastY){
 this.background.y -= 0.2 * (this.cameraLastY - game.camera.y);
 this.cameraLastY = game.camera.y;
}
...

목록 12 : 현재 카메라 위치를 마지막 저장된 위치와 비교하여 배경을 이동.



소리

소리 때문에 나는 주로 몇 달 동안 Gravity Quest에서 수집한 품질 원천에 의존했습니다. 내 Assets은 다음과 같습니다.


UniversalSoundFX는 (상용) 프로젝트 / 게임에서 로열티 없이 사용할 수있는 3000 가지 이상의 사운드 라이브러리입니다. Richard Davey (Phaser의 제작자)가 한 번 그것에 대해 트윗하기 때문에 나는 그것에 대해 알게되었습니다. 나는 다양한 게임에 맞는 다양한 사운드를 고려하여 20$를 좋은 투자로 생각합니다.

Sfrx와 그 기반의 Bfrx는 임의로 특정 스타일의 사운드를 만들고 다양한 속성과 관련하여 미세 조정할 수있는 도구입니다. 나는 이 도구들을 익히지 못했지만 그들이 생성 한 무작위 소리 중 2 개가 최종 게임으로 만들어졌습니다.

CC 믹서는 크리에이티브 커먼즈 라이선스에 따라 음악을 저장하는 곳입니다. 이 사이트에서는 제목이나 제작자에 관한 노래를 검색하고 노래를 올바르게 사용하는 데 필요한 것이 무엇인지 명확하게 명시합니다 (상업적으로). Gravity 퀘스트는 Javolenus의 Nickleus가 게임 메뉴를 제공하는 'C95 - 일상 유지 임무'와 소개를위한 Karsten Holy Moly의 'Space Intro'를 제공합니다. 두 곡 모두 엔딩 점수에 나열됩니다.

Nosoapradio는 Deceased Superior Technician (DST)의 음악 보관소입니다. 노래는 (상업용) 게임에서 사용할 수 있으며 웹 사이트에는 적합한 노래를 선택하기위한 매우 훌륭한 필터링 인터페이스가 있습니다. Gravity 퀘스트는 Outro에서 'ALightIntro'를 특징으로하며,이 곡은 엔딩 스코어에 표시됩니다.


확실히 게임에서 사용할 수있는 (무료) 오디오 파일의 훨씬 좋은 소스가 있습니다. 


플레이 가능한 데모



결론

게임을 위한 훌륭한 비주얼과 사운드를 만드는 일은 마찬가지로 어렵습니다. Gravity Quest를 만들기 퀘스트의 비주얼과 사운드는 완벽하지는 않지만, 게임의 논리를 코딩하는 것보다 시간이 더 걸렸습니다. 이 문서가 시작하기에 흥미로운 자료를 제공하기 바랍니다. 이 기사에서 링크 된 데모의 완전한 주석 소스 코드는 GitHub에서 일반적으로 제공되는 것과 같습니다. 다음 문서에서는 참여 방식을 사용하여 Gravity Quest의 레벨을 만드는 방법에 대해 설명하겠습니다.



Posted by 마스터킹
|

게임 플레이 구현 : 충돌 감지

이 시리즈의 이전 문서에서는 Gravity Quest의 게임 플레이의 기본 디자인과 구현에 대해 설명했습니다. 이 문서에서는 충돌 감지에 초점을 맞춰 논의를 하겠습니다. 충돌 감지는 Gravity Quest 개발의 기본 요소로 입증되었으며 다른 많은 게임에서도 필요합니다.


Gravity Quest에서는 다양한 충돌이 발생할 수 있습니다. 우주 비행사는 중력 총에 의해 표적화 될 수있는 열 활동성 소행성과 충돌 할 수 있으며, 중력 총에 의해 표적이 될 수없는 난장이 또는 우주 비행사를 추적하는 외계인과 충돌 할 수 있습니다. 이 모든 경우에 우주 비행사는 사망할수 있으니 충돌 감지가 필요합니다.



Phaser의 물리 엔진

Phaser에는 충돌 감지가 포함 된 세 가지 물리 엔진이 있습니다.

Phaser의 자체 Arcade 엔진은 일반적으로 빠른 성능을 위해 최적화되어 있습니다. 이 기능은 게임 개발자가 가지고 있는 많은 기본 요구 사항을 다루지만 더 복잡한 요구가 발생할 때는 충분하지는 않습니다.

Phaser는 P2 물리엔진 (P2 물리엔진의 GitHub 저장소)을 추가로 패키지합니다. 충돌 감지, 구속 조건 또는 스프링을 위한 다각형 몸체와 같은 다양한 고급 기능을 제공합니다. 이 기능에 대한 데모는 P2 홈페이지에서 찾을 수 있습니다.


마지막으로 Phaser에는 Ninja 물리 엔진이 포함되어 있습니다. 경사면이나 둥근 모서리가 있는 가상의 풍경을 만들 때 빛납니다. 닌자 엔진에서 이익을 얻는 전형적인 게임 플랫폼 입니다 (슈퍼 마리오 또는 소닉의 구 버전을 생각해보십시오).



P2를 초기 선택으로

Gravity Quest의 목적을 위해, 처음에는 P2가 다른 것들을 배제하여 선택한 엔진이었습니다. Ninja 엔진은 다른 스타일의 게임을 목표로하기 때문에 배제되었습니다. Arcade 엔진은 Gravity Quest에 필요한 많은 기능을 제공합니다. 

예를 들어, 서로 가속하는 기능, 그리고 Gravity Quest에서는 충돌 감지 기능입니다. 

...
accelerateToObject: function (obj1, obj2, speed) {
 if (typeof speed === 'undefined') { speed = 60; }
 var angle = Math.atan2(obj2.y - obj1.y, obj2.x - obj1.x);
 obj1.body.force.x = Math.cos(angle) * speed;
 obj1.body.force.y = Math.sin(angle) * speed;
}
...

목록 1 : P2 바디에 대한 accelerateToObject 함수.


Gravity Quest의 개발 과정 전반에 걸쳐 나는 P2의 선택에 행복하게 매달 렸습니다. 나는 테스트 할 장치가 비교적 적기 때문에 처음에는 항상 iOS 용 게임을 게시하려고 했습니다. 게임을 개발하는 동안, 나는 대부분 내 iPhone 5에서 테스트를 해 보았습니다. 그러나 게임이 거의 완료되면 iPhone 4 및 iPhone 4와 같은 일부 내 친구의 Apple 장치에서도 게임을 테스트하기 시작했습니다. 그리고 불행히도 성능이 떨어지는 것을 발견했습니다. 프레임 속도가 30 이하로 내려갔습니다 (테스트 한 수준의 복잡성에 따라). 그래 Gravity Quest가 느리게 느껴지도록 만들었습니다. 초기에 업데이트 기능에서 빈번한 거리 계산을 의심하면서 성능 병목 현상을 확인하기 위해 몇 가지 테스트를 수행했습니다 (이전 기사에서 설명한 구현 참조). 그러나 이러한 테스트 결과에 따르면 P2는 이전 장치에 비해 너무 무거웠습니다. (참고 사항 : 나는 이 내용이 일반화 되기를 결코 바라지 않는다 - 이것은 단지 나의 경험이었고 다른 경우에는 적용이 되서는 안되며 확실히 P2의 높은 품질에 반대하지 않는다.)


Gravity Quest가 물리 엔진을 필요로 할 때, 예를 들어 물체를 가속화 할 때 Phaser의보다 빠른 Arcade 엔진을 사용하기로 했습니다. 그러나 직사각형과 원 또는 원과 원 사이에 충돌 감지 기능을 제공하지 않으므로 필자 만의 솔루션을 구현했습니다. 이에 대해서는 다음에서 설명합니다.



원과 원 간의 충돌

Gravity Quest의 가능한 충돌 유형은 둘 다 외계인의 충돌, 둘 다 원형, 그리고 폭발이 발생합니다.


목록 2는 두 원 간의 충돌 감지를 구현하는 방법을 보여줍니다. collidesCircleCircle과 getPowDistance의 두 함수는 필요한 객체의 메소드로 정의 할 수 있습니다. collidesCircleCircle 함수는 body1과 body2라는 두 개의 아케이드 물리엔진 입력으로 사용합니다. 

참고 :이 몸체는 직사각형이며 아케이드 물리 엔진이 지원하는 유일한 유형의 몸체입니다. 그러나 이 몸체는 원형 스프라이트에 속해 있으며, 몸체가 사각형 (높이가 같음)임을 암시합니다. 또한이 몸체의 앵커는 수직 및 수평 모두 가운데에 위치한다고 가정합니다.

...
/* 
 * Determines whether two circles collide or not.
 * Input: 2 square Arcade physics bodies with centered anchors
 * Output:
 * - true, if collision is detected
 * - false, if no collision is detected
 */
collidesCircleCircle: function(body1, body2){
 var radius1 = body1.width * 0.5;
 var radius2 = body2.width * 0.5;
 if(Math.abs(body1.x - body2.x) < radius1 + radius2 &&
  Math.abs(body1.y - body2.y) < radius1 + radius2){
  var distance = this.getPowDistance(body1.x, body1.y, body2.x, body2.y);
  if (distance < (radius1 + radius2) * (radius1 + radius2)){
      return true;
  }
 }
 return false;
},

/* 
 * Helper function to determine the distance between
 * two points using Pythagorean theorem 
 */
getPowDistance: function(fromX, fromY, toX, toY){
 var a = Math.abs(fromX - toX);
 var b = Math.abs(fromY - toY);
 return (a * a) + (b * b);
}
...

목록 2 : 두 개의 원형 몸체 사이의 충돌 감지.


처음에는 목록 2에 표시된 것처럼 이 몸체의 반경은 폭에서 파생됩니다. 첫 번째 if 문에서 충돌이 가능한지, 즉 두 몸체 사이의 수평 거리와 수직 거리가 반경의 합보다 작거나 같은지 여부를 확인하기 위해 빠른 검사가 수행됩니다. 이 조건이 성립되지 않으면 두 개의 원이 충돌 할 수 없습니다 (예 : 이미지 1의 사례 1에 표시됨). 조건이 성립하면 충돌이 발생할 수 있습니다. 예를 들어 두 개의 몸체가 이미지 1의 사례 2에서와 같이 수직 또는 수평으로 정렬 된 경우 그러나 이미지 1의 경우 3에서 보여지는 것처럼해서는 안됩니다. 따라서 두 번째 조건이 필요합니다. 그것은 피타고라스의 정리를 사용하여 두 몸체의 닻 사이의 거리를 결정합니다. 결정된 거리가 두 반지름의 합보다 작 으면 충돌 감지는 참을 반환합니다. 이 제 2 조건에 기초하여, 이미지 1의 경우 3의 충돌은 배제되고 이미지 1의 경우 4의 충돌이 검출됩니다.


이미지 1 : 원형 충돌의 예.



회전 된 직사각형과 원 사이의 충돌

Gravity Quest의 다른 유형의 충돌은 회전 된 직사각형 물체와 원형 물체입니다. 예를 들어, 우주 비행사는 직사각형이며 원형의 열 활동성 소행성, 난쟁이 노벨 또는 외계인과 충돌 할 수 있습니다.


목록 3은 회전 된 사각형과 원 간의 충돌 감지가 어떻게 결정되는지 보여줍니다. 함수 collidesRectCircle은 그것이 필요한 곳에서 상태 객체의 메소드로 정의 될 수 있습니다.

...
/* 
 * Determines whether two circles collide or not.
 * Input: 
 * - rect: an Arcade physics body with centered anchor
 * - circle: a square Arcade physics bodies with centered anchor
 * Output:
 * - true, if collision is detected
 * - false, if no collision is detected
 */
collidesRectCircle: function(rect, circle){
 var radius = circle.width * 0.5;
 var upperRectRadius = Math.max(rect.width, rect.height) * 0.75;

 // quick check, whether collision is actually possible:
 if(Math.abs(circle.x - rect.x) < radius + upperRectRadius &&
  Math.abs(circle.y - rect.y) < radius + upperRectRadius){

  // adjust radians:
  var rotation = rect.rotation > 0 ? -1 * rect.rotation : -1 * rect.rotation + Math.PI;

  // rotate circle around origin of the rectangle:
  var rotatedCircleX = Math.cos(rotation) * (circle.x - rect.x) - 
    Math.sin(rotation) * (circle.y - rect.y) + rect.x;
  var rotatedCircleY  = Math.sin(rotation) * (circle.x - rect.x) + 
    Math.cos(rotation) * (circle.y - rect.y) + rect.y;

  // get upper left position of the rectangle:
  var rectX = rect.x - (rect.width * 0.5);
  var rectY = rect.y - (rect.height * 0.5);

  // find closest point in the rectangle to the rotated circle's center:
  var closestX, closestY;

  if (rotatedCircleX  < rectX){
   closestX = rectX;
  } else if (rotatedCircleX  > rectX + rect.width){
   closestX = rectX + rect.width;
  } else {
   closestX = rotatedCircleX;
  }

  if (rotatedCircleY < rectY){
   closestY = rectY;
  } else if (rotatedCircleY > rectY + rect.height) {
   closestY = rectY + rect.height;
  } else {
   closestY = rotatedCircleY;
  }

  // check distance between closest point and rotated circle's center:
  var distance = this.getPowDistance(rotatedCircleX, rotatedCircleY, closestX, closestY);
  if (distance < radius * radius){
   return true; // Collision
  }
 }
 return false;
}
...

목록 3 : 회전 된 직사각형과 원형 몸체 사이의 충돌 감지.


처음에 목록 3에 표시된 것처럼 주어진 원의 반경이 결정됩니다. 또한, 주어진 사각형을 에워싸는 원의 반경의 대략적인 상한 upperRectRadius가 결정됩니다. 이 반지름을 사용하여 두 원 사이의 충돌 감지와 유사하게 빠른 점검이 수행되어 충돌이 가능한지 여부를 결정합니다.


다음과 같이 비용이 많이 드는 충돌 감지는 Circle 및 Rotated Rectangle Collision Detection에 대한 게시물에 설명 된 방법의 JavaScript 번역입니다. 기본 아이디어는 사각형의 중심을 중심으로 원의 중심을 동일한 양만큼 이동하여 사각형의 회전을 보정하는 것입니다.


이렇게하려면 Brad Greens가 Circle and Rotated Rectangle Collision Detection에서 언급 한 라디안 회전을 조정 한 후 원의 앵커를 사각형의 앵커 주위로 회전시켜 기본 대수를 사용하여 사각형의 회전을 보정합니다. 이 프로시저를 수행하면 rotateCircleX 및 rotatedCircleY로 표시된 회전 된 원의 중심 좌표가 됩니다.


다음으로 회전 된 원의 중심에 대한 직사각형 내의 가장 가까운 점의 X 및 Y 좌표가 if-else 문에서 결정됩니다. 마지막으로 피타고라스 정리를 다시 사용하여 직사각형의 결정된 가장 가까운 점과 회전 된 원의 중심 사이의 거리를 결정합니다. 이 거리가 원의 반경보다 작으면 충돌이 감지됩니다.



게임에서 맞춤 충돌 감지 사용

기본 메커니즘을 제자리에 두고 충돌 감지에 사용할 수 있습니다. 이를 보완하기 위해 이전 기사의 소스 코드가 확장되어 직사각형 우주 비행사와 일부 비명 (novae) 간의 충돌 감지를 지원합니다. 충돌시 게임의 단일 상태가 다시 시작됩니다.


목록 4에서 볼 수 있듯이 해당 Asset이 프리로드됩니다.

...
game.load.image('nova', 'assets/nova.png');
...


다음으로, 작성 함수에서, 목록 5에 나타난 바와 같이, 세 개의 비 순례가 게임에 무작위로 배치되고 비 순응 그룹에 추가됩니다. 이전 기사에서와 같이, 100 픽셀의 경계선은 노베의 위치 지정을 위해 생략되었습니다.

...
this.novae = game.add.group();
for (var i = 0; i < 3; i++) {
 var nova = game.add.sprite(
  game.rnd.integerInRange(100, game.world.width - 100),
  game.rnd.integerInRange(100, game.world.height - 100),
  'nova');
 nova.anchor.setTo(0.5, 0.5);
 this.novae.add(nova);
};
...

목록 5 : novae를 임의의 위치에 배치.


마지막으로, 갱신 함수에서, 충돌은 목록 6에 도시 된 바와 같이 체크됩니다. 노벨 그룹이 반복되고, 모든 노바는 상술 한 collidesRectCircle 함수에서 우주 비행사와의 충돌에 대해 체크됩니다. 충돌이 감지되면 main 상태가 다시 시작됩니다.

...
this.novae.forEach(function(nova){
 if(this.collidesRectCircle(this.astronaut, nova)){
  game.state.start('main');
 }
}.bind(this));
...

목록 6 : 업데이트 기능에서 충돌 확인.



데모

우주 비행사가 노벨 중 하나와 충돌하면 게임이 다시 시작됩니다.



결론

충돌 탐지는 많은 게임 개발 프레임 워크의 표준 기능이며, 물론 Phaser도 마찬가지입니다. 그러나 Gravity Quest의 경우 경량 물리 시스템에 대한 요구 사항과 원형 체 사이의 충돌 감지가 조합되어 사용자 정의 솔루션을 만들었습니다. 여기 제시된 구현은 완전히 최적화되지는 않았지만 원하는 결과를 생성합니다. 게임은 구형 iOS 장치에서도 잘 수행되고 충돌 감지가 제대로 작동합니다. 충돌 감지 데모의 소스 코드는이 GitHub 저장소에서 사용할 수 있습니다. 다음 기사에서는 Gravity Quest의 비주얼과 사운드를 어떻게 만들 었는지 설명 할 것입니다.


Posted by 마스터킹
|

게임 플레이 설계 및 구현


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

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

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

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


 


아이디어 반복하기


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

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



개념 구현하기


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


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

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

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


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

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

Listing 2 : 상태 정의.


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


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

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


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

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

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


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

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

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


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

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

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


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

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

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


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

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

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


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


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

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

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


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

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

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


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

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

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

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


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

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

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


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

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

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



데모

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



Posted by 마스터킹
|

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




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


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


* P2 물리학

* 중력과 배상

* 연락 청취자


* 정적 및 동적 몸체


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

window.onload = function() {

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

     // the hero!!

var hero;

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

// not so close to each others

     var gameItems=[];

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

     var gameText = "";

     // group which will contain all level assets

     var levelGroup;

   

     // starting level

     var level=1;

     // starting experience

     var exp=0;

     // starting lives

     var lives=3;

     // starting floor

     var floor=1;

     // starting gold

     var gold=0;

     function onPreload() {

      // preloading images

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

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

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

}

// going fullscreen

function goFullScreen(){

          game.scale.pageAlignHorizontally = true;

      game.scale.pageAlignVertically = true;

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

game.scale.setScreenSize();

     }

function onCreate() {

goFullScreen();

// dark grey background color

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

// starting P2 physics system

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

// setting a soft gravity...

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

// ... but a high restitution

game.physics.p2.restitution = 0.9;

// level creation

createLevel();

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

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

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

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

// adding the HUD text

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

// updating the text

updateText();

}

function createLevel(){

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

levelGroup = game.add.group();

// the array of items starts as an empty array

gameItems = [];

// placing 18 items per level

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

// skulls will be placed more and more often

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

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

gameItems[i].name = "skull";

}

// adding coins

else{

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

gameItems[i].name = "coin";

}

// adding the last created item to levelgroup

levelGroup.add(gameItems[i]);

// setting its registration point in the middle

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

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

do{

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

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

} while(itemsOverlap());

// enabling item to react to physics

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

// setting it as a 24 pixels radius circle

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

// setting items as static

gameItems[i].body.static=true;

}

}

function addHero(){

// once the hero is added, remove the listener

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

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

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

// adding the hero to the group

levelGroup.add(hero);

// enabling the hero to react to physics

game.physics.p2.enable(hero);

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

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

}

function heroHit(body){

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

if(body){

switch(body.sprite.name){

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

case "coin":

gold+=(level*floor);

body.sprite.kill();

break;

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

case "skull":

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

if(hero.y<body.y){

body.sprite.kill();

exp+=1;

if(exp>level*level/2){

level++;

lives++;

exp=0;

}

}

else{

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

lives--;

game.stage.backgroundColor = "#ff0000";

}

break;

}

}

else{

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

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

lives--;

game.stage.backgroundColor = "#ff0000";

}

if(lives==0){

// no lives = game over

hero.kill();

}

// updating HUD text

updateText();

}

function updateText(){

// just writing a string

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

}

function onUpdate() {

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

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

// if the hero is in game...

if(hero){

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

if(hero.y>500){

// preparing for next level

hero.y=0;

levelGroup.destroy(true);

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

floor++;

lives++;

updateText();

createLevel();

}

}

}

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

function itemsOverlap(){

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

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

if(distance<50){

return true;

}

}

return false;

}

// manhattan distance.

function manhattanDistance(from,to){

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

}

};


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





Posted by 마스터킹
|

이 튜토리얼의 목표는 기본 멀티 플레이어 HTML5 게임을 만드는 방법을 보여주는 것입니다. 게임 개발을 위해 Phaser를 사용하고 클라이언트 / 서버 통신을 위해 Eureca.io를 사용합니다. 이 튜토리얼에서는 HTML5 게임 개발에 대한 지식 (Preference에서 Phaser로 이미 알고있는 것으로 생각합니다)을 가정합니다. 또한 nodej에 대한 지식이 있고 이미 설치했다고 가정합니다. 내가 사용할 게임 코드는 페이저 웹 사이트의 Tanks 예제 게임을 기반으로합니다. 이 비디오는 최종 결과를 보여줍니다.



첫 번째 단계 : 코드 리팩터링

멀티 플레이어에 적합하도록 수정 및 단순화 되었습니다.  멀티 플레이어 모드에서 적 탱크는 단지 원격 플레이어이기 때문에 Tank라는 클래스의 플레이어와 적 탱크 코드를 분해했습니다 (이것은 Phaser 예제 코드에서 EnemyTank 클래스로 이름이 변경되었습니다). 생성 및 업데이트 코드가 Tank 클래스로 이동되었습니다.

Tank = function (index, game, player) {

this.cursor = {

left:false,

right:false,

up:false,

fire:false

}

  this.input = {

left:false,

right:false,

up:false,

fire:false

}

    var x = 0;

    var y = 0;

    this.game = game;

    this.health = 30;

    this.player = player;

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

    this.bullets.enableBody = true;

    this.bullets.physicsBodyType = Phaser.Physics.ARCADE;

    this.bullets.createMultiple(20, 'bullet', 0, false);

    this.bullets.setAll('anchor.x', 0.5);

    this.bullets.setAll('anchor.y', 0.5);

    this.bullets.setAll('outOfBoundsKill', true);

    this.bullets.setAll('checkWorldBounds', true);

    this.currentSpeed =0;

    this.fireRate = 500;

    this.nextFire = 0;

    this.alive = true;

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

    this.tank = game.add.sprite(x, y, 'enemy', 'tank1');

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

    this.shadow.anchor.set(0.5);

    this.tank.anchor.set(0.5);

    this.turret.anchor.set(0.3, 0.5);

    this.tank.id = index;

    game.physics.enable(this.tank, Phaser.Physics.ARCADE);

    this.tank.body.immovable = false;

    this.tank.body.collideWorldBounds = true;

    this.tank.body.bounce.setTo(0, 0);

    this.tank.angle = 0;

    game.physics.arcade.velocityFromRotation(this.tank.rotation, 0, this.tank.body.velocity);

};


Tank.prototype.update = function() {      

    for (var i in this.input) this.cursor[i] = this.input[i];    

    if (this.cursor.left)

    {

        this.tank.angle -= 1;

    }

    else if (this.cursor.right)

    {

        this.tank.angle += 1;

    }  

    if (this.cursor.up)

    {

        //  The speed we'll travel at

        this.currentSpeed = 300;

    }

    else

    {

        if (this.currentSpeed > 0)

        {

            this.currentSpeed -= 4;

        }

    }

    if (this.cursor.fire)

    {  

        this.fire({x:this.cursor.tx, y:this.cursor.ty});

    }

    if (this.currentSpeed > 0)

    {

        game.physics.arcade.velocityFromRotation(this.tank.rotation, this.currentSpeed, this.tank.body.velocity);

    }  

    else

    {

        game.physics.arcade.velocityFromRotation(this.tank.rotation, 0, this.tank.body.velocity);

    }  

    this.shadow.x = this.tank.x;

    this.shadow.y = this.tank.y;

    this.shadow.rotation = this.tank.rotation;

    this.turret.x = this.tank.x;

    this.turret.y = this.tank.y;

};

또한 게임 장면에서 탱크를 제거하기 위해 Tank.kill 메서드를 추가하고 fire () 코드를 Tank.fire로 이동했습니다.

Tank.prototype.fire = function(target) {

if (!this.alive) return;

        if (this.game.time.now > this.nextFire && this.bullets.countDead() > 0)

        {

            this.nextFire = this.game.time.now + this.fireRate;

            var bullet = this.bullets.getFirstDead();

            bullet.reset(this.turret.x, this.turret.y);

  bullet.rotation = this.game.physics.arcade.moveToObject(bullet, target, 500);

        }

}

Tank.prototype.kill = function() {

this.alive = false;

this.tank.kill();

this.turret.kill();

this.shadow.kill();

}

또한 단순화를 위해 피해를 처리하는 부분을 제거 했으므로 다른 탱크를 쏘아도 그들을 죽지 않을 것입니다. 하나의 마지막 변경은 플레이어 입력이 처리되는 방식으로 이루어졌습니다. 예제 코드는 (game.input.keyboard.createCursorKeys ()를 통해) 직접 phaser.input 네임 스페이스를 사용하지만, 우리의 경우 클라이언트가 직접 입력을 처리해서는 안됩니다. 그래서 플레이어 입력을 처리하기 위해 Tank.input 객체와 Tank.cursor 객체를 만들었습니다. 다음은 수정 된 update () 함수의 코드입니다.

function update () {

player.input.left = cursors.left.isDown;

player.input.right = cursors.right.isDown;

player.input.up = cursors.up.isDown;

player.input.fire = game.input.activePointer.isDown;

player.input.tx = game.input.x+ game.camera.x;

player.input.ty = game.input.y+ game.camera.y;

turret.rotation = game.physics.arcade.angleToPointer(turret);

    land.tilePosition.x = -game.camera.x;

    land.tilePosition.y = -game.camera.y;

    for (var i in tanksList)

    {

if (!tanksList[i]) continue;

var curBullets = tanksList[i].bullets;

var curTank = tanksList[i].tank;

for (var j in tanksList)

{

if (!tanksList[j]) continue;

if (j!=i)

{

var targetTank = tanksList[j].tank;

game.physics.arcade.overlap(curBullets, targetTank, bulletHitPlayer, null, this);

}

if (tanksList[j].alive)

{

tanksList[j].update();

}

}

    }

}

여기에서 리팩토링 된 코드를 다운로드하면 다음 튜토리얼 단계를 수행하는 데 도움이됩니다.

이제는 간단한 코드 만 만들어서 멀티 플레이어 게임으로 변환 해 보겠습니다.


***************************************************************************************

내부 요구 사항을 위해 개발 한 RPC 라이브러리 인 Eureca.io를 사용하고 소스 코드 (https://github.com/Ezelia/eureca.io에서 사용할 수있는 소스 코드)를 열어보기로했습니다.  여러 네트워킹 라이브러리 (socket.io, engine.io ... 등)가 있지만 Eureca.io가 일을 더 단순하게 만드는 방법을 보게됩니다 🙂

***************************************************************************************



웹 서버

게임을위한 기본적인 웹 서버를 만드는 것으로 시작하겠습니다. 여기서 nodejs 를 위한 익스프레스 라이브러리를 설치하고, 파일 서비스를 더 간단하게 만들면, eureca.io와 호환됩니다. Express는 멀티 플레이어 게임 (동적 인 웹 페이지, 세션, 쿠키, 양식 등을 처리 할 수있는 웹 페이지)을 구축하는 경우에도 도움이됩니다.

npm install express



Tank 게임의 루트 디렉토리에 server.js 파일을 만들고 다음 코드로 편집하십시오

var express = require('express')

  , app = express(app)

  , server = require('http').createServer(app);

// serve static files from the current directory

app.use(express.static(__dirname));

server.listen(8000);

브라우저를 열고 http : // localhost : 8000 /로 이동하십시오. Tank 게임이 잘 작동한다면 다음 단계로 넘어가거나 여기에서 코드를 다운로드 하면됩니다.



eureca.io 설치 및 준비

이제 Eureca.io로 게임을 시작해보십시오. engine.io가 사용되는 엔진 전송 레이어로 engine.io 또는 sockjs를 사용할 수 있습니다. eureca.io를 기본 구성으로 사용하려면 eureca.io와 engine.io를 설치해야합니다.

npm install engine.io

npm install eureca.io


이제 eureca.io를 추가하기 위해 서버 코드를 수정합니다 : server.listen (8000)

eureca.io 서버를 인스턴스화하고 다음 코드를 사용하여 HTTP 서버에 연결합니다.

//get EurecaServer class

var EurecaServer = require('eureca.io').EurecaServer;

//create an instance of EurecaServer

var eurecaServer = new EurecaServer();

//attach eureca.io to our http server

eurecaServer.attach(server);


그런 다음 클라이언트 연결과 연결 해제를 감지하는 이벤트 리스너를 추가합니다

//detect client connection

eurecaServer.onConnect(function (conn) {  

    console.log('New Client id=%s ', conn.id, conn.remoteAddress);

});

//detect client disconnection

eurecaServer.onDisconnect(function (conn) {  

    console.log('Client disconnected ', conn.id);

});


클라이언트 측에서는 tanks.js 스크립트보다 먼저 index.html에 다음 행을 추가합니다.

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


이렇게 하면 클라이언트가 eureca.io를 사용할 수 있게됩니다. 이제 tanks.js 파일을 편집하고 처음에 다음 코드를 추가하십시오

var ready = false;

var eurecaServer;

//this function will handle client communication with the server

var eurecaClientSetup = function() {

//create an instance of eureca.io client

var eurecaClient = new Eureca.Client();

eurecaClient.ready(function (proxy) {

eurecaServer = proxy;

//we temporary put create function here so we make sure to launch the game once the client is ready

create();

ready = true;

});

}


여기서 우리가 하는 일은 eureca.io 클라이언트를 인스턴스화하고 클라이언트가 준비 될 때까지 기다린 다음 클라이언트가 초기화 될 때까지 기다리는 클라이언트 초기화 메소드 "eurecaClientSetup"을 작성한 다음 create () 메소드가 Phaser에 의해 처음 호출 된 게임 작성 메소드 (create ())를 호출하는 것입니다.

Game () 인스턴스화 메소드는 이 행을 수정하여 eurecaClientSetup을 호출합니다.

var game = new Phaser.Game(800, 600, Phaser.AUTO, 'phaser-example', { preload: preload, create: eurecaClientSetup, update: update, render: render });


중요 : 멀티 플레이어 게임을 만드는 경우 일반적으로 게임 코드를 시작하기 전에 서버를 사용할 수 있는지 확인해야합니다. 이것이 바로 eurecaClientSetup에서 수행하는 작업입니다.


마지막 한가지. ready 변수가 false로 설정된 것을 보았을 때 클라이언트 / 서버 초기화가 완료되었는지를 알 수 있습니다.

게임이 만들어졌습니다. 우리는 phaser가 create () 전에 update methode를 호출하는 것을 막기 위해 이를 사용합니다. 그래서 우리는 update () 메소드에 다음을 추가 할 필요가있다.

function update () {

//do not update if client not ready

if (!ready) return;

}


여기서 결과 코드를 다운로드 할 수 있습니다 : 탱크 게임 다운로드 2 단계 코드

server.js를 다시 (노드 server.js) 실행하고 http : // localhost : 8000 / 게임을 시작하면 서버가 클라이언트 연결을 감지했음을 알 수 있습니다. 이제 페이지를 새로 고침하면 클라이언트가 연결 해제 된 후 다시 연결되었음을 알 수 있습니다. 이것이 작동하면 다음 단계로 넘어갈 준비가되었습니다.



원격 플레이어 스폰/죽음

기본 멀티 플레이어 게임의 경우 서버는 연결된 모든 클라이언트를 추적해야합니다. 모든 클라이언트를 구별하기 위해 우리는 또한 각 플레이어에 대해 고유 식별자를 가질 필요가 있습니다 (우리는 eureca.io가 생성 한 고유 ID를 사용합니다). 이 고유 ID는 클라이언트와 서버간에 공유됩니다. 플레이어 데이터를 동기화하고 원격 클라이언트와 탱크를 연결할 수 있습니다.

구현은 다음과 같습니다.

- 새 클라이언트가 연결되면 서버는 uniq id (여기서는 eureca.io 세션 ID가 사용됨)

- 서버는이 uniq id를 클라이언트에 보낸다.

- 클라이언트는 플레이어의 탱크로 게임 장면을 만들고 그 고유의 ID를 탱크에 할당합니다.

- 클라이언트는 클라이언트 측에서 모든 것이 준비되었다는 것을 서버에 알립니다 (우리는 이것을 핸드 쉐이크라고 부릅니다)

- 서버는 각 연결된 플레이어에 대해 알림 및 호출 클라이언트 Spawn 메서드를 가져옵니다.

- 클라이언트는 연결된 각 플레이어에 대해 Tank 인스턴스를 생성합니다.

- 클라이언트가 연결을 끊으면 서버가이를 식별하여 연결된 클라이언트 목록에서 제거합니다.

- 모든 연결된 클라이언트의 서버단에서 Kill () 메서드 호출

- 각 클라이언트는 연결이 끊긴 플레이어의 Tank 인스턴스를 제거합니다.



클라이언트 사이드

Eureca.io 인스턴스에는 "exports"라는 특수한 네임 스페이스가 있습니다.

이 네임 스페이스 아래 정의 된 모든 메소드는 RPC에서 사용할 수있게됩니다. 우리는 그것을 어떻게 사용하는지 보게 될 것이다. 이를 위해 eurecaClientSetup 메소드를 수정해야합니다.

var eurecaClientSetup = function() {

//create an instance of eureca.io client

var eurecaClient = new Eureca.Client();

eurecaClient.ready(function (proxy) {

eurecaServer = proxy;

});

//methods defined under "exports" namespace become available in the server side

eurecaClient.exports.setId = function(id)

{

//create() is moved here to make sure nothing is created before uniq id assignation

myId = id;

create();

eurecaServer.handshake();

ready = true;

}

eurecaClient.exports.kill = function(id)

{

if (tanksList[id]) {

tanksList[id].kill();

console.log('killing ', id, tanksList[id]);

}

}

eurecaClient.exports.spawnEnemy = function(i, x, y)

{

if (i == myId) return; //this is me

console.log('SPAWN');

var tnk = new Tank(i, game, tank);

tanksList[i] = tnk;

}

}

위의 예제에서 setId, kill 및 spawnEnemy와 같이 서버 측에서 호출 할 수있는 세 가지 메소드가 있습니다.

클라이언트가 원격 서버 기능 : eurecaServer.handshake ()를 호출 중임을 참고하십시오.



서버 측

클라이언트 메소드 (setId, kill 및 spawnEnemy)가 신뢰할 수있는 클라이언트 함수라는 것을 Eureca.io에게 알려주는 첫 번째 사항은 그렇지 않습니다. 그렇지 않으면 eureca.io는 클라이언트 / 서버 개발에서 클라이언트 메소드를 호출하지 않으므로 클라이언트를 맹목적으로 신뢰해서는 안됩니다. 다음 코드는 Eureca.io에게 이러한 메소드를 신뢰하고 클라이언트 데이터를 보유 할 clientList 객체를 생성하도록 지시합니다.

var eurecaServer = new EurecaServer({allow:['setId', 'spawnEnemy', 'kill']});

var clients = {};


이제 onConnect 및 onDisconnect 메소드를 수정 해 봅시다.

//detect client connection

eurecaServer.onConnect(function (conn) {  

    console.log('New Client id=%s ', conn.id, conn.remoteAddress);

//the getClient method provide a proxy allowing us to call remote client functions

    var remote = eurecaServer.getClient(conn.id);  

//register the client

clients[conn.id] = {id:conn.id, remote:remote}

//here we call setId (defined in the client side)

remote.setId(conn.id);

});

//detect client disconnection

eurecaServer.onDisconnect(function (conn) {  

    console.log('Client disconnected ', conn.id);

var removeId = clients[conn.id].id;

delete clients[conn.id];

for (var c in clients)

{

var remote = clients[c].remote;

//here we call kill() method defined in the client side

remote.kill(conn.id);

}

});


서버가 원격 클라이언트 기능을 호출하는 방법에 유의하십시오 : remote.setId (conn.id) 및 remote.kill (conn.id);


기억한다면 클라이언트도 서버 측 메서드를 호출합니다. 여기에 선언하는 방법이 있습니다.

eurecaServer.exports.handshake = function()

{

//var conn = this.connection;

for (var c in clients)

{

var remote = clients[c].remote;

for (var cc in clients)

{

remote.spawnEnemy(clients[cc].id, 0, 0);

}

}

}


이제 서버를 시작하고 http : // localhost : 8000에서 첫 번째 브라우저 창을 열고, 탱크를 조금 옮기고 http : // localhost : 8000 /에서 다른 브라우저 창을 엽니다.

첫 번째 창에서 탱크가 생성되는 것을 볼 수 있습니다. 마지막 창을 닫으면 탱크가 사라집니다. 이것은 꽤 좋았지만 여전히 멀티 플레이어 게임은 아닙니다. 탱크 운동은 아직 재결합되지 않았고, 이것은 우리가 다음 단계에서 할 것입니다. 그건 그렇고, 여기에 위의 단계 😀의 전체 코드입니다



입력 처리 / 상태 동기화

멀티 플레이어 게임에서 서버는 클라이언트 상태를 제어해야 하며 유일한 신뢰할 수있는 엔터티는 서버입니다. (P2P 게임과 같은 몇 가지 다른 변종이 있지만 여기에서는 설명하지 않겠습니다 🙂)

클라이언트 / 서버 게임의 이상적인 구현은 클라이언트와 서버가 모두 동작을 시뮬레이션하면서 서버가 클라이언트에 상태 데이터를 보냅니다.

현지 지위를 수정 / 보완 할 것입니다. 이 예에서는 최소한의 정보 만 동기화 할것입니다.

플레이어가 입력 (이동 또는 공격)을 하면 지역 코드로 직접 처리되지 않습니다.

대신 서버에 보내면 서버는 연결된 모든 클라이언트에서 처리하여 다시 클라이언트 입력을 보냅니다.

각 클라이언트는 이 입력을 탱크의 클라이언트 측 사본에 적용합니다.

탱크는 로컬 입력에 의해 발행 된대로 서버가 보낸 입력을 처리합니다.


이 외에도 입력 정보가 전송 될 때마다 탱크 위치에 대한 정보를 보내고, 이 정보는 탱크 상태를 연결된 모든 클라이언트와 동기화하는 데 사용됩니다. 이것을 처리 할 코드를 작성해 보겠습니다.

eurecaClient.exports.updateState = function(id, state)

{

if (tanksList[id])  {

tanksList[id].cursor = state;

tanksList[id].tank.x = state.x;

tanksList[id].tank.y = state.y;

tanksList[id].tank.angle = state.angle;

tanksList[id].turret.rotation = state.rot;

tanksList[id].update();

}

}



중요포인트 : exports 메소드를 서버에서 호출 할 수 있습니다. updateState 메서드는 공유 플레이어 입력으로 Tank.cursor를 업데이트하지만 탱크 위치와 각도도 수정합니다. 이제 우리는 Tank.update 메소드에서 이것을 처리 할 필요가 있습니다. Tank.prototype.update를 편집하고 다음 라인을 대체하십시오

for (var i in this.input) this.cursor[i] = this.input[i];


이 코드와 함께

var inputChanged = (

this.cursor.left != this.input.left ||

this.cursor.right != this.input.right ||

this.cursor.up != this.input.up ||

this.cursor.fire != this.input.fire

);

if (inputChanged)

{

//Handle input change here

//send new values to the server

if (this.tank.id == myId)

{

// send latest valid state to the server

this.input.x = this.tank.x;

this.input.y = this.tank.y;

this.input.angle = this.tank.angle;

this.input.rot = this.turret.rotation;

eurecaServer.handleKeys(this.input);

}

}

여기에서 로컬 플레이어가 입력 (마우스 클릭 또는 키보드 왼쪽 / 오른쪽 / 위로)을 입력 한 경우 서버에서 직접 처리하는 대신 eurecaServer.handle을 사용하여 서버 쪽 handleKeys 메서드가 입력을 다시 전송하여 모든 연결된 클라이언트에게 알려줍니다.



서버 측

먼저 새로 선언 된 클라이언트 메소드 (updateState)를 허용해야합니다.

var eurecaServer = new EurecaServer({allow:['setId', 'spawnEnemy', 'kill', 'updateState']});


그런 다음 handleKeys 메소드를 선언합니다.

eurecaServer.exports.handleKeys = function (keys) {

var conn = this.connection;

var updatedClient = clients[conn.id];

for (var c in clients)

{

var remote = clients[c].remote;

remote.updateState(updatedClient.id, keys);

//keep last known state so we can send it to new connected clients

clients[c].laststate = keys;

}

}


그리고 기존의 핸드 쉐이크 방식에 대에 약간 수정합니다.

eurecaServer.exports.handshake = function()

{

for (var c in clients)

{

var remote = clients[c].remote;

for (var cc in clients)

{

//send latest known position

var x = clients[cc].laststate ? clients[cc].laststate.x:  0;

var y = clients[cc].laststate ? clients[cc].laststate.y:  0;

remote.spawnEnemy(clients[cc].id, x, y);

}

}

}

모든 단계를 따르거나 (link 링크에서 최종 코드를 다운로드 한 경우) 서버를 시작하고 두 개 이상의 창을 엽니다. 이제 한 클라이언트에서 탱크를 움직이거나 발사체를 발사하면 다른 창으로 이동합니다.


다음은?

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

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

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

Posted by 마스터킹
|

HTML5 게임 엔진 인 Phaser로 만든 다중 플랫폼 게임 인 Monster Wants Candy의 소스를 분석해 보겠습니다. 이 방법으로 엔진에 대한 실질적인 소개를 얻게되며 자신 만의 HTML5 모바일 및 브라우저 게임을 제작하는 데 사용할 수있는 개념을 배우게됩니다.


소개

HTML5 게임을 만들려면 프레임 워크 또는 엔진을 선택하는 것이 좋습니다. 물론 평범한 JavaScript로 할 수도 있지만 프레임 워크를 사용하면 개발 속도가 크게 빨라지고 편하게 개발할수 있습니다.


Phaser는 헌신적 인 커뮤니티가있는 최신 HTML5 게임 개발 프레임 워크로, 아직 들어 본 적이 없다면 반드시 시도해야합니다.


Phaser 란 무엇입니까?

Phaser는 데스크톱 및 모바일 HTML5 게임을 제작하기위한 프레임 워크입니다. 그것은 Photon Storm에 의해 만들어졌습니다. 이 프레임 워크는 순수한 JavaScript로 작성되었지만, 어떤 경우에는 TypeScript 정의 파일을 포함합니다.


Phaser 코드는 Flash gamedev 플랫폼 Flixel을 기반으로하므로 Flash 개발자는 바로 느낄 수 있습니다. 내부적으로 Pixi.js 엔진을 사용하여, 가능한 경우 Canvas 또는 WebGL을 사용하여 화면의 모든 부분을 렌더링합니다.



그것은 아주 새롭지 만 HTML5GameDevs 포럼에서 활발한 커뮤니티의 도움으로 빠르게 성장하고 있습니다. 사용할 수있는 자습서와 기사가 이미 많이 있으며, 공식 문서와 개발 중에 매우 도움이 될 수있는 수많은 예제를 확인할 수도 있습니다. 오픈 소스이며 GitHub에서 무료로 사용할 수 있으므로 소스 코드로 직접 들어가서 배울 수 있습니다.


Monster Wants Candy 무엇입니까?

게임을 제작할 때 먼저 핵심 아이디어를 생각하고 작동하는 프로토 타입을 신속하게 설정하려고합니다. 이 사례 연구에서는 Monster Wants Candy라고 불리는 게임의 간단한 데모부터 시작합니다.


프로토 타입으로 작업하는 대신 프로젝트의 구조를 먼저 보여 주므로 전체 아이디어를 이해할 수 있습니다. 그런 다음 Assets로드에서 부터 메인 메뉴 제작 및 실제 게임 루프에 이르기까지 게임의 개발 단계를 거치게됩니다. 지금 Monster Wants Candy 데모를 보시면 우리가 함께 일하는 것을 보실 수 있습니다.


코딩은 Enclave Games의 Andrzej Mazur가 담당했으며  모든 그래픽 Assets은 Blackmoon Design의 Robert Podgórski가 만들었습니다.



Monster Wants Candy의 이야기는 간단합니다. 사악한 왕이 당신의 사랑을 납치했고 그녀를 되찾기 위해 충분한 사탕을 모아야합니다. 게임 플레이도 간단합니다 : 과자가 떨어지고 있고 그들을 먹을 수 있습니다. 캔디를 먹으면 얻는 점수가 많을수록 좋습니다. 당신이 무엇이든 놓치고 화면에서 떨어지면 생명을 잃고 게임은 끝납니다.


보시다시피 매우 단순한 게임이지만 구조가 완벽합니다. 프레임 워크의 가장 중요한 용도는 이미지로드, 스프라이트 렌더링 및 사용자 활동 감지와 같은 작업에 해당합니다. 또한 코드를 복사하고 코드를 시작하고 자신 만의 게임을 만들 수있는 좋은 출발점이됩니다.


프로젝트 설정 및 구조

Phaser를 시작하는 방법에 대해 프레임 워크 작성자가 직접 작성한 이 유용한 기사를 읽거나 GitHub phaser.min.js 파일을 프로젝트 디렉토리에 복사하여 처음부터 작업을 시작할 수 있습니다. 

IDE가 필요하지 않습니다. 브라우저에서 index.html 파일을 실행하기 만하면 소스 코드에서 변경 한 내용을 즉시 볼 수 있습니다.


우리 프로젝트 폴더에는 HTML5 구조와 필요한 모든 JavaScript 파일을 포함하는 index.html 파일이 들어 있습니다. 두 가지 하위 폴더가 있습니다. img는 모든 그래픽 애셋을 저장하고 src는 게임의 소스 코드를 저장합니다.


폴더 구조가 어떻게 보이는지는 다음과 같습니다.



src 폴더에는 JavaScript 파일이 있습니다. 이것은 마술이 일어나는 곳입니다. 이 튜토리얼에서는 그 폴더에있는 모든 파일의 목적과 내용을 설명합니다.


이 튜토리얼의 GitHub 에서 각 파일의 소스를 볼 수 있습니다.


Index.html

index.html 파일부터 시작하겠습니다. 기본 HTML5 웹 사이트처럼 보이지만 텍스트와 많은 HTML 요소를 추가하는 대신, Canvas 요소로 렌더링하는 Phaser 프레임 워크를 초기화합니다.


<!DOCTYPE html>

<html>

<head>

    <meta charset="utf-8" />

    <title>Monster Wants Candy demo</title>

    <style> body { margin: 0; background: #B4D9E7; } </style>

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

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

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

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

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

</head>

<body>

<script>

(function() {

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

    game.state.add('Boot', Candy.Boot);

    game.state.add('Preloader', Candy.Preloader);

    game.state.add('MainMenu', Candy.MainMenu);

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

    game.state.start('Boot');

})();

</script>

</body>

</html>


doctype을 가진 HTML 문서의 일반적인 구조와 <head>의 일부 정보 : charset 인코딩, 페이지 제목 및 CSS 스타일을 정의합니다. 

일반적으로 우리는 모든 스타일을 넣은 외부 CSS 파일을 참조 할 것이지만 여기서는 필요하지 않습니다. 앞에서 언급했듯이 모든 것이 Canvas 요소에 렌더링되므로 스타일을 지정할 HTML 요소가 없습니다. .


마지막으로해야 할 일은 Phaser.min.js 파일에서부터 Phaser 프레임 워크의 소스 코드와 함께 모든 자바 스크립트 파일을 게임의 코드가 포함 된 모든 파일에 포함시키는 것입니다. 모든 자바 스크립트 파일을 하나로 결합하여 브라우저의 요청 수를 줄이는 것이 좋으므로 게임로드가 빨라지지만 이 자습서에서는 별도로 로드 할 것입니다.


<body> 태그의 내용으로 이동하여 프레임 워크를 초기화하고 게임을 시작하십시오. 이 코드는 자체 호출 함수 내에 있습니다. 첫 번째 줄은 다음과 같습니다.

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


이 코드는 몇 가지 기본값으로 페이저를 초기화합니다 :


640은 게임의 캔버스 너비 (픽셀)이고 960은 게임의 높이입니다.


Phaser.AUTO는 Canvas에 게임을 렌더링하는 방법을 프레임 워크에 알려줍니다. 세 가지 옵션이 있습니다 : CANVAS, WEBGL, AUTO. 

첫 번째는 캔버스의 2D 컨텍스트에서 게임을 실행합니다. 

두 번째는 가능한 경우 WebGL을 사용하여 렌더링합니다 (주로 현재 데스크톱이지만 모바일 지원은 개선되고 있습니다). 

세 번째는 WebGL이 지원이되면 WebGL, 그렇지 않으면 2D 캔버스 렌더링이 사용됩니다.


프레임 워크 초기화는 game이라는 단일 객체에 지정되며, Phaser 인스턴스를 참조 할 때 사용하게됩니다.


다음 줄은 게임에 상태를 추가하는 것입니다.

game.state.add('Boot', Candy.Boot);


'Boot'는 상태 이름이고 Candy.Boot는 그 상태를 시작할 때 실행될 객체입니다 (다음 단계에서 정의 됨). 

Boot (구성), Preloader (자산로드), MainMenu (우리 게임의 메인 메뉴) 및 Game (게임의 기본 루프)에 대한 상태를 추가합니다. 

마지막 줄인 game.state.start ( 'Boot')는 Boot 상태를 시작하여 Candy.Boot 객체의 적절한 함수가 실행되도록합니다.


보시다시피, 하나의 주요 자바 스크립트 게임 객체가 만들어졌으며 많은 다른 객체가 특수 목적으로 할당되었습니다. 우리 게임에서는 Boot, Preloader, MainMenu 및 Game 객체가 게임 상태가되며 프로토 타입을 사용하여 정의합니다. 프레임 워크 자체 (preload (), create (), update () 및 render ()) 용으로 예약 된 객체에는 몇 가지 특수 함수 이름이 있지만, 자체 startGame (), spawnCandy managePause ()). 이 모든 것을 잘 이해하지 못한다면 걱정하지 마십시오. 나중에 코드 예제를 사용하여 모든 것을 설명 할 것입니다.


게임

이제 Boot, Preloader 및 MainMenu 상태를 잊어 버리세요. 자세한 내용은 나중에 설명합니다. 부트 상태가 게임의 기본 설정을 처리하고, Preloader가 모든 그래픽 Assets을 로드하며 MainMenu가 게임을 시작할 수 있는 화면을 보여줍니다.


게임 자체에 초점을 맞추고 Gamestate의 코드가 어떻게 되어있는지 봅시다. 전체 Game.js 코드를 살펴보기 전에 개발자의 관점에서 게임 자체의 개념과 로직의 가장 중요한 부분에 대해 이야기 해 봅시다.


세로 모드


이 게임은 세로 모드로 재생됩니다. 즉, 플레이어가 휴대 전화를 세로로 들고 게임을 재생합니다.



이 모드에서는 화면의 너비가 가로보다 커지는 가로 모드와 달리 화면의 높이가 너비보다 큽니다. 몬스터 원츠 캔디 (Monster Wants Candy)와 같은 세로 모드에서 더 잘 작동하는 게임 종류 (Craigen과 같은 플랫폼 게임 포함) 및 두 모드에서 작동하는 일부 유형이 있습니다. 일반적으로 가로 모드에서 더 잘 작동하는 유형이 있습니다. 그러한 게임을 코드화 하십시오.


Game.js


game.js 파일의 소스 코드를 살펴보기 전에 구조에 대해 이야기 해 보겠습니다. 우리를 위해 창조 된 세계가 있으며, 그 안에는 사탕을 잡는 플레이어 캐릭터가 있습니다.


게임 세계 : 괴물 뒤의 세상은 움직이지 않습니다. 배경에 Candyland의 이미지가 있습니다. 배경에서 괴물을 볼 수 있으며, 사용자 인터페이스도 있습니다.


플레이어 캐릭터 : 이 데모는 매우 간단하고 기본이기 때문에 작은 괴물은 사탕을 기다리는 것 외에는 아무 것도하지 않습니다. 플레이어의 주요 임무는 사탕을 수집하는 것입니다. 
캔디 : 게임의 핵심 메카닉은 최대한 많은 캔디를 잡는 것입니다. 사탕은 화면의 상단 가장자리에서 출현하며, 플레이어는 떨어지는대로 클릭해야 합니다. 사탕이 화면 하단에서 떨어지면 제거되고 플레이어 캐릭터가 손상을 입습니다. 우리는 생명 시스템을 구현하지 않았기 때문에 게임이 즉시 종료되고 적절한 메시지가 표시됩니다.
이제 Game.js 파일의 코드 구조를 살펴 보겠습니다.

Candy.Game = function(game) {

    // ...

};

Candy.Game.prototype = {

    create: function() {

        // ...

    },

    managePause: function() {

        // ...

    },

    update: function() {

        // ...

    }

};

Candy.item = {

    spawnCandy: function(game) {

        // ...

    },

    clickCandy: function(candy) {

        // ...

    },

    removeCandy: function(candy) {

        // ...

    }

};


Candy.Game 프로토 타입에는 다음 세 가지 기능이 정의되어 있습니다.

- create ()는 초기화를 처리합니다.

- managePause ()는 게임을 일시 중지하고 일시 정지 해제합니다.


- update ()는 틱마다 주 게임 루프를 관리합니다.


하나의 캔디를 나타내는 item이라는 편리한 객체를 만들 것입니다. 다음과 같은 유용한 함수가 있습니다.

- spawnCandy ()는 게임 세계에 새로운 캔디를 추가합니다.

- clickCandy ()는 사용자가 캔디를 클릭하거나 탭하면 시작됩니다.

- removeCandy ()는 그것을 제거합니다.


다음 코드를 봅시다.

Candy.Game = function(game) {

    this._player = null;

    this._candyGroup = null;

    this._spawnCandyTimer = 0;

    this._fontStyle = null;

    Candy._scoreText = null;

    Candy._score = 0;

    Candy._health = 0;

};


여기서 우리는 나중에 코드에서 사용할 모든 변수를 설정하고 있습니다.


this._name을 정의함으로써, 변수의 사용을 Candy.Game 범위로 제한합니다. 즉, 다른 상태에서는 사용할 수 없다는 의미입니다. 즉, 필요하지 않으므로 왜 노출합니까?


Candy._name을 정의하면 다른 상태 및 객체에서 이러한 변수를 사용할 수 있으므로 예를 들어 Candy._score를 Candy.item.clickCandy () 함수에서 사용 수 있습니다.


객체는 null로 초기화되고 계산에 필요한 변수는 0으로 초기화됩니다.


Candy.Game.prototype의 내용으로 이동해서:

create: function() {

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

    this.physics.arcade.gravity.y = 200;


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

    this.add.sprite(-30, Candy.GAME_HEIGHT-160, 'floor');

    this.add.sprite(10, 5, 'score-bg');

    this.add.button(Candy.GAME_WIDTH-96-10, 5, 'button-pause', this.managePause, this);


    this._player = this.add.sprite(5, 760, 'monster-idle');

    this._player.animations.add('idle', [0,1,2,3,4,5,6,7,8,9,10,11,12], 10, true);

    this._player.animations.play('idle');


    this._spawnCandyTimer = 0;

    Candy._health = 10;


    this._fontStyle = { font: "40px Arial", fill: "#FFCC00", stroke: "#333", strokeThickness: 5, align: "center" };

    Candy._scoreText = this.add.text(120, 20, "0", this._fontStyle);


    this._candyGroup = this.add.group();

    Candy.item.spawnCandy(this);


},



create () 함수의 시작 부분에서 우리는 ARCADE 물리 시스템을 설정했습니다. Phaser에서는 몇 가지가 있지만 가장 간단한 방법입니다. 그 후, 게임에 수직 중력을 추가합니다. 그런 다음 배경, 괴물이 서있는 바닥 및 점수 UI 배경을 추가합니다. 우리가 추가하는 네 번째 항목은 일시 중지 버튼입니다. Candy.Preloader ()에 정의되어 있지만 게임 코드 전체에서 사용할 수있는 Candy.GAME_WIDTH 및 Candy.GAME_HEIGHT 변수를 사용하고 있습니다.


그런 다음 플레이어의 아바타 인 괴물을 만듭니다. 프레임 있는 스프라이트 (스프라이트 시트)입니다. 그가 침착하게 서 있고 호흡하는 것처럼 보이게하려면, 우리는 그를 움직일 수 있습니다.


animations.add () 함수는 사용 가능한 프레임에서 애니메이션을 만들고 함수는 다음 네 개의 매개 변수를 사용합니다.

- 애니메이션의 이름 (나중에 참조 할 수 있습니다)

- 우리가 사용하고자 하는 모든 프레임이 있는 테이블 (우리가 원하는 경우에만 사용할 수 있습니다)

- 프레임 속도

- 애니메이션을 반복하여 무한정 재생할지 여부를 지정하는 플래그.

애니메이션을 시작하려면 animations.play () 함수를 지정된 이름으로 사용해야합니다.


그런 다음 spawnCandyTimer를 0 (카운트 준비)으로 설정하고 괴물의 상태를 10으로 설정합니다.



텍스트 스타일 지정하기

다음 두 줄은 화면에 텍스트를 보여줍니다. this.add.text () 함수는 네 개의 매개 변수를 사용합니다. 

화면의 왼쪽 및 위쪽 절대 위치, 실제 텍스트 문자열 및 config 객체입니다. config 객체의 CSS와 유사한 구문을 사용하여 텍스트 서식을 지정할 수 있습니다.


우리 글꼴의 설정은 다음과 같습니다.

this._fontStyle = { 

    font: "40px Arial", 

    fill: "#FFCC00", 

    stroke: "#333", 

    strokeThickness: 5, 

    align: "center"


};


이 경우 글꼴은 Arial, 높이가 40 픽셀, 색상이 노란색, 획이 정의되어 있습니다 (색상 및 두께). 텍스트는 가운데 맞춤입니다.


그런 다음 candyGroup을 정의하고 첫 번째 캔디를 생성합니다.



게임 일시 중지


일시 중지 기능은 다음과 같습니다.

managePause: function() {

    this.game.paused = true;

    var pausedText = this.add.text(100, 250, "Game paused.\nTap anywhere to continue.", this._fontStyle);

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

        pausedText.destroy();

        this.game.paused = false;

    }, this);


},


일시 중지 버튼을 클릭 할 때마다 this.game.paused의 상태를 true로 변경하고, 해당 프롬프트를 플레이어에 표시하고, 플레이어의 클릭 또는 화면의 탭에 대한 이벤트 리스너를 설정합니다. 해당 클릭 또는 탭이 감지되면 텍스트를 제거하고 this.game.paused를 false로 설정합니다.


게임 개체의 일시 중지 된 변수는 게임에서 애니메이션이나 계산을 중지하기 때문에 Phaser에서 특별하므로 게임을 일시 중지 할 때까지 모든 것이 고정되어 false로 설정됩니다.



업데이트 루프


update () 함수 이름은 Phaser의 예약어 중 하나입니다. 그 이름을 가진 함수를 작성할 때, 그것은 게임의 모든 프레임에서 실행될 것입니다. 다양한 조건에 따라 계산을 관리 할 수 있습니다.

update: function() {

    this._spawnCandyTimer += this.time.elapsed;

    if(this._spawnCandyTimer > 1000) {

        this._spawnCandyTimer = 0;

        Candy.item.spawnCandy(this);

    }

    this._candyGroup.forEach(function(candy){

        candy.angle += candy.rotateMe;

    });

    if(!Candy._health) {

        this.add.sprite((Candy.GAME_WIDTH-594)/2, (Candy.GAME_HEIGHT-271)/2, 'game-over');

        this.game.paused = true;

    }

}

게임 세계의 모든 틱은 이전 틱 이후 경과 된 시간을 spawnCandyTimer 변수에 추가하여 추적합니다. if 문은 타이머를 재설정하고 게임 세계에 새 캔디를 스폰 할 시간인지 여부를 확인합니다. 

매 초마다 이렇게 처리합니다 (즉, spawnCandyTimer가 1000 밀리 초를 경과 할 때마다). 그런 다음 forEach를 사용하여 내부에있는 모든 캔디 객체가있는 캔디 그룹을 반복합니다.

캔디의 angle 변수에 추가합니다(캔디 객체의 rotateMe 값에 저장 됨). 그들은 떨어지는 동안 각각이 고정 된 속도로 회전합니다. 마지막으로 생명 상태가 0으로 떨어졌는지 확인하고, 0이면 화면 위에 게임오버을 표시하고 게임을 일시 중지합니다.



캔디 이벤트 관리


캔디 로직을 메인 게임과 분리하기 위해 우리는 spawnCandy (), clickCandy () 및 removeCandy ()와 같은 함수를 포함하는 item이라는 객체를 사용합니다. 사탕과 관련된 변수 중 일부는 Game 개체에 사용하기 쉽도록 유지하는 반면 다른 항목은 더 나은 유지 관리를 위해 항목 기능에만 정의됩니다.

spawnCandy: function() {

    var dropPos = Math.floor(Math.random()*Candy.GAME_WIDTH);

    var dropOffset = [-27,-36,-36,-38,-48];

    var candyType = Math.floor(Math.random()*5);

    var candy = game.add.sprite(dropPos, dropOffset[candyType], 'candy');

    candy.animations.add('anim', [candyType], 10, true);

    candy.animations.play('anim');


    game.physics.enable(candy, Phaser.Physics.ARCADE);

    candy.inputEnabled = true;

    candy.events.onInputDown.add(this.clickCandy, this);


    candy.checkWorldBounds = true;

    candy.events.onOutOfBounds.add(this.removeCandy, this);

    candy.anchor.setTo(0.5, 0.5);

    candy.rotateMe = (Math.random()*4)-2;

    game._candyGroup.add(candy);


},

이 함수는 세 가지 값을 정의하여 시작합니다.

- 캔디를 떨어 뜨리는 랜덤 화 된 x 좌표 (캔버스의 너비와 너비 사이)

- 높이 (우리는 사탕의 유형에 따라 나중에 결정 함)에 따라 사탕을 떨어 뜨리는 y 좌표,


- 무작위로 추출한 캔디 유형 (우리는 다섯 가지 다른 이미지를 사용합니다)


그런 다음, 위에서 정의한 시작 위치와 이미지를 사용하여 하나의 캔디를 스프라이트로 추가합니다. 이 블록에서 마지막으로 수행 할 작업은 캔디가 생성 될 때 사용할 새 애니메이션 프레임을 설정하는 것입니다.


다음으로 중력이 설정되면 화면 상단에서 자연스럽게 떨어질 수 있도록 물리 엔진 용 캔디 바디를 활성화합니다. 그런 다음 캔디의 클릭작업에 대한 이벤트 리스너를 설정합니다.


캔디가 화면 경계를 벗어날 때 이벤트를 발생 시키도록하려면 checkWorldBounds를 true로 설정하십시오. events.onOutOfBounds ()는 캔디가 화면을 벗어날때 호출 될 함수입니다. 그리고 removeCandy ()를 호출하도록합니다. 정확히 중간에 캔디에 앵커를 설정하여 축 주위로 앵커를 회전시켜 자연스럽게 회전합니다. 우리는 rotateMe 변수를 여기에 설정하여 update () 루프에서 이를 변경하면 사탕을 회전시킬 수 있습니다. -2와 +2 사이의 값을 선택합니다. 마지막 줄은 우리가 새로 만든 사탕을 캔디 그룹에 추가하여 모든 것을 반복 할 수 있도록합니다.


다음 함수 인 clickCandy ()로 넘어 갑시다.

clickCandy: function(candy) {

    candy.kill();

    Candy._score += 1;

    Candy._scoreText.setText(Candy._score);


},


하나의 캔디를 매개 변수로 사용하고 Phaser 메소드 kill ()을 사용하여 제거합니다. 또한 점수를 1 씩 늘리고 점수 텍스트를 업데이트합니다.


사탕을 재설정하는 것은 짧고 쉽습니다.

removeCandy: function(candy) {

    candy.kill();

    Candy._health -= 10;


},


캔디가 클릭되지 않고 화면 아래에 사라지면 removeCandy () 함수가 시작됩니다. 캔디 오브젝트가 제거되고 플레이어의 체력이 10만큼 감소합니다. (그는 처음에는 10 점이 있었기 때문에 떨어지는 캔디 한 점을 잃어 버리더라도 게임이 끝납니다.)



프로토 타입 및 게임 상태

게임 메커닉, 핵심 아이디어 및 게임 플레이의 모습에 대해 배웠습니다. 이제 화면의 크기 조정, 에셋로드, 버튼 누르기 관리 등 코드의 다른 부분을 볼 차례입니다.


우리는 이미 게임 상태에 대해 알고 있습니다. 그래서 게임의 상태를 정확히 보도록하겠습니다.


Boot.js


Boot.js는 게임 객체를 정의하는 JavaScript 파일입니다.

Boot.js 파일의 소스 코드는 다음과 같습니다.

var Candy = {};

Candy.Boot = function(game) {};

Candy.Boot.prototype = {

    preload: function() {

        this.load.image('preloaderBar', 'img/loading-bar.png');

    },

    create: function() {

        this.input.maxPointers = 1;

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

        this.scale.pageAlignHorizontally = true;

        this.scale.pageAlignVertically = true;

        this.scale.setScreenSize(true);

        this.state.start('Preloader');

    }


};


보시다시피, 우리는 게임을 위한 전역 객체를 생성하는 var Candy = {}로 시작하였습니다. 모든 것이 내부에 저장되므로 전역 네임 스페이스를 부 풀리지 않을 것입니다.


Candy.Boot = function (game) {}은 게임 객체를 매개 변수 (index.html의 프레임 워크로도 생성)로 받는 Boot ()라는 새로운 함수를 만듭니다 (index.html에서 사용됨).


Candy.Boot.prototype = {} 코드는 프로토 타입을 사용하여 Candy.Boot의 내용을 정의하는 방법입니다.

Candy.Boot.prototype = {

    preload: function() {

        // code

    },

    create: function() {

        // code

    }

};


앞에서 언급했듯이 Phaser의 기능에는 몇 가지 예약 된 이름이 있습니다. preload () 및 create ()는 두 가지입니다. preload ()는 모든 에셋을 로드하는 데 사용되며 create ()는 정확히 한 번 (preload () 후) 호출되므로 변수 정의 또는 스프라이트 추가와 같이 객체의 설정으로 사용될 코드를 배치 할 수 있습니다 .


우리의 Boot 객체에는 이 두 함수가 포함되어 있으므로 Candy.Boot.preload () 및 Candy.Boot.create ()를 각각 사용하여 참조 할 수 있습니다. Preload () 함수는 Boot.js 파일의 전체 소스 코드에서 볼 수 있듯이 프리 로더 이미지를 프레임 워크에 로드합니다.

preload: function() {

    this.load.image('preloaderBar', 'img/loading-bar.png');


},


this.load.image ()의 첫 번째 매개 변수는 로딩 바 이미지에 지정한 이름이고 두 번째 매개 변수는 프로젝트 구조의 이미지 파일에 대한 경로입니다.



Preload.js 파일에서 로드되는 다른 모든 이미지의 상태를 표시하는 로드바 이미지가 필요합니다. 그러므로 Boot.js에서 다른 모든 이미지보다 먼저 로드해야합니다.



비율 옵션

create () 함수는 입력 및 크기 조정을 위한 몇 가지 Phaser 설정을 포함합니다.

create: function() {

    this.input.maxPointers = 1;

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

    this.scale.pageAlignHorizontally = true;

    this.scale.pageAlignVertically = true;

    this.scale.setScreenSize(true);

    this.state.start('Preloader');


}

input.maxPointers를 1로 설정하는 첫 번째 줄은 우리가 게임에서 필요로 하지 않기 때문에 멀티 터치를 사용하지 않을 것을 정의합니다.


scale.scaleMode 설정은 게임의 스케일링을 제어합니다. 사용 가능한 옵션은 EXACT_FIT, NO_SCALE 및 SHOW_ALL입니다. 그것들을 열거 해, 각각 0, 1, 또는 2의 값을 사용할 수 있습니다. 

첫 번째 옵션은 사용 가능한 모든 공간 (100 % 너비와 높이, 보존 비율 없음)으로 게임의 크기를 조정합니다. 

두 번째는 스케일링을 완전히 비활성화합니다. 

세 번째 플레이어는 게임이 주어진 차원에 맞는지 확인하지만 단편을 숨기지 않고 모든 내용이 화면에 표시됩니다 (비율은 그대로 유지됩니다).


scale.pageAlignHorizontally 및 scale.pageAlignVertically를 true로 설정하면 게임이 가로 및 세로로 정렬되므로 Canvas 요소의 왼쪽과 오른쪽에 같은 양의 여유 공간이 생깁니다. 상단과 하단도 동일합니다.


scale.setScreenSize (true)를 호출하면 크기 조절이 "활성화"됩니다.


마지막 줄인 state.start ( 'Preloader')는 다음 상태 (이 경우 Preloader 상태)를 실행합니다.



Preloader.js


방금 진행 한 Boot.js 파일에는 간단한 (한 줄짜리) preload () 함수와 create () 함수에 많은 코드가 있지만 Preloader.js는 완전히 다른 것처럼 보입니다. create () 함수는 모든 에셋이 로드 될 때 다른 상태로 이동하는 데 사용됩니다.


Preloader.js 파일의 코드는 다음과 같습니다.

Candy.Preloader = function(game){

    Candy.GAME_WIDTH = 640;

    Candy.GAME_HEIGHT = 960;

};


Candy.Preloader.prototype = {

    preload: function() {

        this.stage.backgroundColor = '#B4D9E7';

        this.preloadBar = this.add.sprite((Candy.GAME_WIDTH-311)/2,

            (Candy.GAME_HEIGHT-27)/2, 'preloaderBar');

        this.load.setPreloadSprite(this.preloadBar);


        this.load.image('background', 'img/background.png');

        this.load.image('floor', 'img/floor.png');

        this.load.image('monster-cover', 'img/monster-cover.png');

        this.load.image('title', 'img/title.png');

        this.load.image('game-over', 'img/gameover.png');

        this.load.image('score-bg', 'img/score-bg.png');

        this.load.image('button-pause', 'img/button-pause.png');


        this.load.spritesheet('candy', 'img/candy.png', 82, 98);

        this.load.spritesheet('monster-idle',

            'img/monster-idle.png', 103, 131);

        this.load.spritesheet('button-start',

            'img/button-start.png', 401, 143);

    },

    create: function() {

        this.state.start('MainMenu');

    }


};


이전 Boot.js 파일과 유사하게 시작됩니다. Preloader 객체를 정의하고 프로토 타입에 두 개의 함수 (preload () 및 create ())에 대한 정의를 추가합니다. Prototype 객체 안에는 두 개의 변수 인 Candy.GAME_WIDTH와 Candy.GAME_HEIGHT가 정의되어 있습니다. 이것들은 게임 화면의 기본 너비와 높이를 설정합니다. 이 너비와 높이는 코드의 다른 곳에서 사용될 것입니다.


preload () 함수의 처음 세 줄은 스테이지의 배경색을 설정하고 (# B4D9E7, 하늘색), 게임의 스프라이트를 표시하고, setPreloadSprite라는 특수 함수의 기본 색상으로 정의합니다 ) 이미지로딩 진행 상태를 나타냅니다.


add.sprite () 함수를 살펴 보겠습니다.

this.preloadBar = this.add.sprite((640-311)/2, (960-27)/2, 'preloaderBar');


보시다시피 세 가지 값을 전달합니다. 이미지의 절대 왼쪽 위치 (화면의 중심은 스테이지의 너비에서 이미지의 너비를 빼고 결과를 반으로 나눔으로써 얻습니다.), 이미지의 절대 상단 위치 (비슷하게 계산 된), 이미지의 이름 (Boot.js 파일에 이미로드 된 이미지의 이름).



스프라이트 시트로드 중

다음 몇 줄은 load.image () (이미 보았 듯이)를 사용하여 모든 그래픽 애셋을 게임에 로드하는 것입니다.


마지막 3 가지는 약간 다릅니다.

this.load.spritesheet('candy', 'img/candy.png', 82, 98);


단일 이미지를 로드하는 대신 load.spritesheet () 함수는 하나의 파일 (스프라이트 시트) 내의 이미지 전체를 관리합니다. 스프라이트에서 단일 이미지의 크기를 함수에 알리기 위해 두 개의 추가 매개 변수가 필요합니다.


이 경우 하나의 candy.png 파일 안에 5 가지 유형의 사탕이 있습니다. 전체 이미지는 410x98px이지만 단일 항목은 82x98px로 설정되어 load.spritesheet () 함수에 입력됩니다. 플레이어 스프라이트 시트도 비슷한 방식으로 로드됩니다.


두 번째 함수 인 create ()는 게임의 다음 상태 인 MainMenu를 시작합니다. 즉, preload () 함수의 모든 이미지가 로드 된 직후에 게임의 기본 메뉴가 표시됩니다.



MainMenu.js


이 파일은 게임 관련 이미지를 렌더링하는 곳이며, 사용자가 시작 버튼을 클릭하여 게임 루프를 시작하고 게임을하는 곳입니다.


Candy.MainMenu = function(game) {};

Candy.MainMenu.prototype = {

    create: function() {

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

        this.add.sprite(-130, Candy.GAME_HEIGHT-514, 'monster-cover');

        this.add.sprite((Candy.GAME_WIDTH-395)/2, 60, 'title');

        this.add.button(Candy.GAME_WIDTH-401-10, Candy.GAME_HEIGHT-143-10,

            'button-start', this.startGame, this, 1, 0, 2);

    },

    startGame: function() {

        this.state.start('Game');

    }


};

구조는 이전 JavaScript 파일과 유사합니다. MainMenu 객체의 프로토 타입에는 preload () 함수가 없습니다. 모든 이미지가 이미 Preload.js 파일에서 로드되었기 때문입니다.


프로토 타입에는 create () 와 startGame ()이라는 두 가지 함수가 정의되어 있습니다. 앞에서 언급했듯이, 첫 번째 이름은 Phaser에만 국한되며 두 번째 이름은 우리 자신의 이름입니다.


startGame ()을 먼저 살펴 보겠습니다.

startGame: function() {

    this.state.start('Game');


}

이 함수는 게임 루프를 시작하는 것만을 처리하지만 자동으로 또는 애셋이로드 된 후에 실행되지 않습니다. 버턴에 할당하고 사용자 입력을 기다립니다.


create: function() {

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

    this.add.sprite(-130, Candy.GAME_HEIGHT-514, 'monster-cover');

    this.add.sprite((Candy.GAME_WIDTH-395)/2, 60, 'title');

    this.add.button(Candy.GAME_WIDTH-401-10, Candy.GAME_HEIGHT-143-10,

        'button-start', this.startGame, this, 1, 0, 2);


},

create () 메소드는 우리가 이미 잘 알고 있는 3 개의 add.sprite () Phaser 함수를 가지고 있습니다 : 이미지를 절대 위치에 배치하여 보이는 스테이지에 이미지를 추가합니다. 

우리의 메인 메뉴는 배경, 모서리에있는 작은 괴물, 그리고 게임의 제목을 포함합니다.



버튼

Game 상태에서 이미 사용했던 객체도 있습니다.

this.startButton = this.add.button(Candy.GAME_WIDTH-401-10, Candy.GAME_HEIGHT-143-10,


    'button-start', this.startGame, this, 1, 0, 2);

이 버튼은 지금까지 보아 왔던 방법보다 훨씬 복잡해 보입니다. 

왼쪽 위치, 위쪽 위치, 이미지 (또는 스프라이트)의 이름, 버튼을 클릭 한 후 실행할 함수, 이 함수가 실행되는 컨텍스트 및 이미지의 인덱스를 만들기 위해 8 개의 다른 인수를 전달합니다.


다음은 버튼 스프라이트 시트가 표시되는 상태로 레이블이 지정된 상태입니다.



이것은 이전에 사용한 candy.png 스프라이트 시트와 매우 유사합니다.


함수에 전달되는 된 마지막 세 자리는 오버 (호버), 아웃 (일반) 및 다운 (터치 / 클릭)과 같은 버튼의 여러 상태임을 기억하는 것이 중요합니다. 

우리는 button.png 스프라이트 시트에서 normal, hover 및 click 상태를 각각 가지므로 add.button () 함수의 전달되는 순서를 0, 1, 2에서 1, 0, 2로 변경하여야 합니다.


이제 Phaser 게임 프레임 워크의 기본 사항을 알게되었습니다.



완성 된 게임

이 문서에서 사용 된 데모 게임은 여기서 플레이 할 수있는 완성 된 게임으로 발전했습니다. 보시다시피 생명, 업적, 높은 점수 및 기타 흥미로운 기능이 구현되었지만 대부분이 튜토리얼을 통해 배웠던 지식을 기반으로 합니다.



마치며

지금까지 Monster Wants Candy 데모의 모든 코드 행을 통한 긴 여행 이었지만 Phaser를 배우는 데 도움이되기를 바랍니다. 가까운 장래에 프레임 워크를 사용하여 멋진 게임을 만들 수 있기를 바랍니다.

Posted by 마스터킹
|