Fork me on GitHub

플랫포머(Platformer) 튜토리얼

이 튜토리얼에서, 우리는 간단한 플랫포머를 만들 것입니다. 이 튜토리얼은 주로 타일드(Tiled)를 레벨 에디터로 사용하는 작동하는 게임의 기본 요소를 만드는 것에 초점을 둘 것입니다.

도입

이 튜토리얼을 따라가기 위해, 다음 사항들이 필요합니다:

  • 타일드 맵 에디터(Tiled Map Editor), 설치 후 실행 중이어야 합니다(0.9.0 버전 이상)
  • 우리가 튜토리얼에서 기본 템플릿 프로젝트로 사용할 melonJS 보일러 플레이트(boilerplate)
  • 튜토리얼 데이터 파일을, 압축 해제 후 템플릿 데이터 디렉토리에 두십시오. 이 폴더에는 다음과 같은 것들이 있어야 합니다:
    • 레벨 타일셋
    • 시차 레이어들의 두 배경
    • 기본 스프라이트시트
    • 오디오 특수효과 및 음악
    • 타이틀 화면 배경
  • melonJS 라이브러리, /lib 디렉토리 아래에 복사되어야 합니다 (디버그 과정에서 아마 일반 버전이 필요할 수 있으므로 최소 버전과 일반 버전 모두를 다운로드하는 것을 잊지 마세요)
  • 더 많은 정보를 보시려면 melonJS 문서

테스트/디버그 :
만약 당신이 파일 시스템을 사용하기를 원하신다면, 문제는 당신이 "교차-원본 요청(cross-origin request)" 보안 오류를 경험하게 될 것입니다. 크롬을 쓰신다면, 당신은 브라우저를 실행하실 때, "--disable-web-security" 파라미터를 사용하시거나 "--allow-file-access-from-files"를 사용하셔야 합니다. 이것은 특정 로컬 컨텐츠를 테스트 하려면 반드시 수행되어야 합니다. 그렇지 않으면 브라우저는 자산을 XHR을 통해 로드할 때 오류를 일으킬 것입니다. 비록 이 방법이 추천되는 방법은 아니지만, 당신이 옵션을 실행하는 한, 당신은 당신의 환경에 보안 취약성을 더하게 됩니다.

두 번째이자 더 쉬운 방법은 로컬 웹 서버를 사용하는 것으로, 예를 들어 melonJS 보일러 플레이트(boilerplate) 도움말에 설명된 것과 같이, 그런트 서브(grunt serve) 툴을 사용함으로써, 그것은 당신이 당신의 게임을 당신의 브라우저에서 http://localhost:8000 url을 사용해 테스트하는 것을 허용할 것입니다.

추가 참고 자료 :

당신이 수정하고싶은 것은 무엇이든 수정하세요. 저희는 당신이 이미 타일드(Tiled)에 친숙할 것이라고 여기에서 추측합니다; 만약 이 툴에 대해 더 많은 도움이 필요하시다면, 당신은 타일드 Tiled 홈페이지와 위키를 확인하셔서 더 많은 도움을 얻으실 수 있습니다.

파트 1: 타일드(Tiled)를 사용해 레벨 생성

먼저 타일드(Tiled)를 열어 새 맵을 만들어 봅시다: 이 튜토리얼에서, 우리는 640x480 캔버스를 사용할 것이고, 우리가 32x32 타일을 가지고 있기에, 우리는 맵 크기를 최소 20X15로 설정해야 합니다. 제 예시에서 저는 40x15 레벨을 정의할 것이고, 우리는 이후에 스크롤링 배경으로 플레이할 수 있을 것입니다.

Step 1 of creating a new map

또한, melonJS가 압축되지 않은 타일맵만을 지원하므로, 당신의 설정이 맞는지 확인하십시오. 저희는 그것이 더 작은 파일을 만들기 때문에 Base64 인코딩을 추천합니다만 당신이 원하는 대로 하시면 됩니다.

그럼 우리의 타일셋을 맵/새 타일셋(Map/New Tileset)을 이용해 추가해 봅시다. 타일셋 간격과 여백을 0으로 설정했는지 확인하세요.

Adding a tileset

아름다움을 위해, 우리는 두 개의 레이어를 생성할 것입니다 - 하나는 배경 레이어고, 하나는 전경 레이어입니다. 당신의 상상력을 활용해 원하시는 것을 무엇이든 해 보세요. 저는 논리적으로 "배경"과 "전경"으로 이름 붙였습니다만, 당신은 당신이 원하는 어떤 이름도 붙일 수 있습니다.

이것이 완료했을 때 제 레벨의 모습입니다:

Tiled level design

마침내, 이제 맵/맵 속성(Map/Map Properties)에서 색상 선택 툴(color picker tool)을 사용해 당신이 원하는 색을 고름으로써 우리 레벨의 배경 색을 설정해 봅시다.

Setting a background color in Tiled

완료하려면, 우리의 새 맵을 "/data/map/" 폴더 아래에 (Grunt가 the resources.js 파일을 구축할 때 맵이 이 위치에 있는지 확인할 것입니다) "area01"로 저장합시다, 그럼 우리는 첫 단계를 마친 것입니다!

파트 2: 레벨 로드

우선, 튜토리얼 자료들을 보일러 플레이트 디렉토리 구조에 압축해제하신 후, 당신은 이와 같은 것들을 가질 것입니다:

data/
  bgm/
    dst-inertexponent.mp3
    dst-inertexponent.ogg
  img/
    font/
     32x32_font.png
    gui/
     title_screen.png
    map/
     area01_level_tiles.png
     license.txt
    sprite/
     gripe_run_right.png
     spinning_coin_gold.png
     wheelie_right.png
    area01_bkg0.png
    area01_bkg1.png
   map/
   sfx/
    cling.mp3
    cling.ogg
    jump.mp3
    jump.ogg
    stomp.mp3
    stomp.ogg
js/
  game.js
  resources.js
  entities/
    HUD.js
    entities.JS
  screens/
    play.js
    title.js
index.html
index.css

보일러 플레이트는 또한 기본 코드들을 제공합니다만, 먼저 우리의 js/game.js skeleton을 봅시다:

/* game namespace */
var game = {
  /**
   * an object where to store game global data
   */
  data : {
    score : 0
  },

  // Run on page load.
  onload : function () {
    // Initialize the video.
    if (!me.video.init(640, 480, {wrapper : "screen", scale : "auto", scaleMethod : "flex-width"})) {
      alert("Your browser does not support HTML5 canvas.");
      return;
    }

    // add "#debug" to the URL to enable the debug Panel
    if (me.game.HASH.debug === true) {
      window.onReady(function () {
        me.plugin.register.defer(this, me.debug.Panel, "debug", me.input.KEY.V);
      });
    }

    // Initialize the audio.
    me.audio.init("mp3,ogg");

    // Set a callback to run when loading is complete.
    me.loader.onload = this.loaded.bind(this);

    // Load the resources.
    me.loader.preload(game.resources);

    // Initialize melonJS and display a loading screen.
    me.state.change(me.state.LOADING);
  },

  // Run on game resources loaded.
  loaded : function () {
    me.state.set(me.state.MENU, new game.TitleScreen());
    me.state.set(me.state.PLAY, new game.PlayScreen());

    // add our player entity in the entity pool
    me.pool.register("mainPlayer", game.PlayerEntity);

    // Start the game.
    me.state.change(me.state.PLAY);
  }
};

이것은 매우 간단합니다. 일단 페이지가 로드되면, onload() 함수가 호출되면, 화면과 오디오가 초기화되고, 모든 게임 리소스가 로드되기 시작합니다. 우리는 또한 모든 것이 사용될 준비가 되었을 때 호출될 콜백을 정의합니다. 콜백 안에서, 우리가 게임 이벤트를 관리하기 위해 사용할(리셋 등...) 플레이화면 오브젝트와 함께 우리는 게임 안에서 사용될 새로운 상태를 정의합니다.

우리가 기본 프로젝트 템플릿에서 수행할 유일한 변화는 `me.video.init()` 함수에서 주어진 비디오 해상도를, 튜토리얼에서처럼 바꾸어 우리는 640x480 캔버스를 만들 것입니다. 또한 우리는 플랫포머 게임에 더 잘 맞도록 (다양한 스케일 모드에 대한 추가 정보를 얻으시려면 `비디오 초기화(me.video.init)` 문서를 참조하세요)스케일 메소드(scaleMethod)를 "flex-width"로 바꿀 것입니다.

보일러 플레이트는 자동적으로 리소스 리스트를 구축하고 그것을 그런트 서브(grunt serve) 태스크를 사용할 때 당신의 앱에 game.resources (build/js/resources.js)로 노출시킵니다.

경고: 만약 당신이 보일러 플레이트를 사용하고 있지 않으시다면, 당신은 resources.js를 수동으로 (시간이 소요되고, 에러가 자주 발생하는)관리해야 할 것입니다. 만약 resources.js를 수동으로 관리한다면, 당신은 깃 리포(git repo)의 사례를 볼 수 있습니다.

또한 비록 우리가 여기 tmx 파일을 직접 사용하고 있지만, 프로덕션을 위해 저희는 더 조그만 파일 크기를 가지고, 훨씬 빠른 레벨 로딩을 허용하고 .tmx 확장자에 서버 이슈가 발생하는 것을 막기 위해 json 포맷을 사용하실 것을 추천한다는 것을 알아두세요(타일드(Tiled)로부터 직접 추출될 수 있는).

마지막으로, js/screens/play.js 파일을 열어보고 onResetEvent() 함수(상태 변화에서 호출되는)에서, 우리는 loadLevel 함수와 우리의 기본 레벨 이름에 대한 호출을 추가함으로써 레벨 디렉터가 이전에 우리가 먼저 로드해두었던 레벨을 보여주도록 요청합니다:

game.PlayScreen = me.ScreenObject.extend({
  /**
   * action to perform on state change
   */
  onResetEvent : function () {
    // load a level
    me.levelDirector.loadLevel("area01");

    // reset the score
    game.data.score = 0;

    // add our HUD to the game world
    this.HUD = new game.HUD.Container();
    me.game.world.addChild(this.HUD);
  },

  /**
   * action to perform when leaving this screen (state change)
   */
  onDestroyEvent : function () {
    // remove the HUD from the game world
    me.game.world.removeChild(this.HUD);
  }
});

이상입니다! 만약 모든 것을 제대로 하셨다면, 당신의 index.html를 열어보세요 (만약 당신이 웹 브라우저를 사용하고 있지 않으시다면 당신의 브라우저가 로컬 파일에 접근하는 것을 허용할 필요가 있음을 기억하세요, 필요하시다면 자세한 사항은 이 튜토리얼 시작 부분의 “테스트/디버그”를 참조하세요).

시도해보기

(당신의 브라우저에서 이것이 작동하는지 보시려면 이미지를 클릭하세요), 당신은 이와 같은 것을 보시게 될 것입니다

Step 2 results

네, 아직 멋지진 않지만, 이건 시작일 뿐입니다!

또한 만약 당신이 알아채지 못하셨을 수도 있는데, 우리가 우리의 프로그램을 640x480 해상도로 설정했기에, 일반적인 상황인 우리는 지도의 일부만을 볼 것입니다(정확히는 절반). melonJS는 자동으로 이에 상응하는 뷰포트를 생성하고, 우리는 "메인 플레이어"를 추가했을 때 다음 단계에서 맵을 통해 둘러볼 수 있을 것입니다.

파트 3: 메인 플레이어 추가

여기에서 우리는 우리의 플레이어를 생성하기 위해 기본적인 me.Entity를 확장해 새 오브젝트를 생성할 수 있습니다. 우리는 우리 캐릭터를 움직이게 하고 기본적인 걷고 서는 움직임을 정의하하기 위해 제공된 단순한 (gripe_run_right.png) 스프라이트시트를 사용할 것입니다. 동일한 엔티티에 더 복잡한 움직임(점프, 다치는 등의 상황에서의 튕김, 등...)을 정의하는 것도 물론 가능합니다만, 지금은 단순한 것들만 합시다.

Gripe run right

이제 우리의 엔티티를 추가하고 `js/entities/entities.js` 예제 파일을 열 시간입니다. 이것을 다음과 맞추어 봅시다 :

/**
 * a player entity
 */
game.PlayerEntity = me.Entity.extend({
  /**
   * constructor
   */
  init : function (x, y, settings) {
    // call the constructor
    this._super(me.Entity, 'init', [x, y, settings]);

    // set the default horizontal & vertical speed (accel vector)
    this.body.setVelocity(3, 15);

    // set the display to follow our position on both axis
    me.game.viewport.follow(this.pos, me.game.viewport.AXIS.BOTH);

    // ensure the player is updated even when outside of the viewport
    this.alwaysUpdate = true;

    // define a basic walking animation (using all frames)
    this.renderable.addAnimation("walk",  [0, 1, 2, 3, 4, 5, 6, 7]);

    // define a standing animation (using the first frame)
    this.renderable.addAnimation("stand",  [0]);

    // set the standing animation as default
    this.renderable.setCurrentAnimation("stand");
  },

  /*
   * update the player pos
   */
  update : function (dt) {
    if (me.input.isKeyPressed('left')) {
      // flip the sprite on horizontal axis
      this.renderable.flipX(true);

      // update the entity velocity
      this.body.vel.x -= this.body.accel.x * me.timer.tick;

      // change to the walking animation
      if (!this.renderable.isCurrentAnimation("walk")) {
        this.renderable.setCurrentAnimation("walk");
      }
    }
    else if (me.input.isKeyPressed('right')) {
      // unflip the sprite
      this.renderable.flipX(false);

      // update the entity velocity
      this.body.vel.x += this.body.accel.x * me.timer.tick;

      // change to the walking animation
      if (!this.renderable.isCurrentAnimation("walk")) {
        this.renderable.setCurrentAnimation("walk");
      }
    }
    else {
      this.body.vel.x = 0;

      // change to the standing animation
      this.renderable.setCurrentAnimation("stand");
    }

    if (me.input.isKeyPressed('jump')) {
      // make sure we are not already jumping or falling
      if (!this.body.jumping && !this.body.falling) {
        // set current vel to the maximum defined value
        // gravity will then do the rest
        this.body.vel.y = -this.body.maxVel.y * me.timer.tick;

        // set the jumping flag
        this.body.jumping = true;
      }
    }

    // apply physics to the body (this moves the entity)
    this.body.update(dt);

    // handle collisions against other shapes
    me.collision.check(this);

    // return true if we moved or if the renderable was updated
    return (this._super(me.Entity, 'update', [dt]) || this.body.vel.x !== 0 || this.body.vel.y !== 0);
  },

  /**
   * colision handler
   * (called when colliding with other objects)
   */
  onCollision : function (response, other) {
    // Make all other objects solid
    return true;
  }
});

제 생각에 위의 코드는 이해하기 쉽습니다. 기본적으로, 우리는 extend the 엔티티를 확장, 기본 플레이어 스피드 구성, 카메라 구성, 몇몇 키들이 눌려는지 확인 및 우리 플레이어 움직임 관리 (플레이어 스피드 설정 및 엔티티 바디 update 함수 호출)를 해 보았습니다. 또한, 당신은 제가 제 오브젝트가 실제로 움직였는지 알기 위해 제 오브젝트의 최종적인 속도(this.body.vel.x와 this.body.vel.y)를 테스트하고 있고 제가 스프라이트 애니메이션을 원하는 지 아닌지를 통제한다는 것을 아셨을 지도 모릅니다.

그러면, 비록 기본 game.PlayerEntity가 이미 보일러 플레이트에서 정의되었지만, 우리는 우리의 "메인"이 오브젝트 풀 안에 실제로 선언되도록 수정해야 하고 (엔진에 의해 오브젝트의 예를 들어주는), 마침내 우리가 플레이어 움직임에서 사용할 키를 놓아봅시다. 그러면 우리의 loaded() 함수는 다음처럼 될 것입니다:

/**
 * callback when everything is loaded
 */
loaded : function () {
  // set the "Play/Ingame" Screen Object
  me.state.set(me.state.PLAY, new game.PlayScreen());

  // register our player entity in the object pool
  me.pool.register("mainPlayer", game.PlayerEntity);

  // enable the keyboard
  me.input.bindKey(me.input.KEY.LEFT,  "left");
  me.input.bindKey(me.input.KEY.RIGHT, "right");
  me.input.bindKey(me.input.KEY.X,     "jump", true);

  // start the game
  me.state.change(me.state.PLAY);
}

이제 우리는 레벨에 우리의 엔티티를 추가했습니다! 타일드(Tiled)로 돌아가, 새 오브젝트와 새 엔티티를 추가합니다. 새 엔티티를 추가하는 것은 오브젝트 레이어에 직사각형을 더하기 위해 "직사각형 삽입(Insert Rectangle)" 툴을 사용합니다. 그리고 당신은 마우스 우클릭을 하고 아래의 속성을 추가하기에 들어가면 됩니다.

그것의 이름을 mainPlayer라고 붙여봅시다. (대소문자 무관, 오브젝트 풀에 우리의 오브젝트를 등록할 때 쓰셨던 것을 사용해도 됩니다) 그리고 오브젝트에 두 가지 속성을 추가합시다:

  • 이미지 : gripe_run_right로 설정 (우리 리소스의 이름)
  • 프레임 너비 : 스피라이트시트의 하나의 스프라이트 크기64로 설정
  • 프레임 높이 : 우리는 한 줄의 스프라이트시트를 사용할 것이고 이 때 엔진은 실제 이미지 높이를 값으로 설정할 것이기에 이 값을 정의하지 않습니다.

이 두 파라미터는 오브젝트가 생성될 때 구축기에 의해 위에서 사용된 (세팅 오브젝트) 파라미터로서 전달될 것입니다. 이제 당신은 이 필드들을 타일드(Tiled)에서, 또는 당신의 코드에서 직접 정의할 수 있습니다(많은 오브젝트를 다룰 때, 이름만 타일드(Tiled)에서 정의하고, 나머지는 직접 구축기에서 관리하는 것이 더 쉽습니다).

주의: 당신은 또한 자유롭게 당신이 원하는 대로 많은 속성들을 추가할 수 있고, 그들은 당신의 구축기에서 전달된 오브젝트 설정에서 모두 이용 가능할 것입니다.

Adding an entity

일단 오브젝트가 생성되면 당신의 엔티티를 레벨에 두시고, 아래 예시에서처럼 실제 스프라이트 사이즈와 맞추기 위해 당신이 오브젝트 직사각형을 타일드(Tiled)에서 크기 조정을 했는지 확인하십시오.

positioning an entity

충돌 레이어 정의

거의 다 되었습니다! 마지막 단계는 충돌 레이어를 정의하는 것입니다. 이를 위해 우리는 단순히 "collision"라는 이름의 새 오브젝트 레이어를 만들고 기본 모양을 추가하기만 하면 됩니다. 여기까지입니다!

이제 새 오브젝트 그룹 레이어를 추가합시다. 이 레이어의 이름은 반드시 키워드 "collision"을 포함해야 엔진이 이를 충돌 오브젝트 레이어로 인식합니다.

일단 레이어가 추가되면, 그것을 선택하고, 오브젝트 툴바를 이용해 어떤 모양이든 당신의 레벨 충돌 지도에 "그려주면" 됩니다.

object tool bar

melonJS가 분할 축 이론 알고리즘(Separating Axis Theorem algorithm)을 사용해 충돌 감지를 실행한다는 것을 알아두세요. 충돌을 위해 사용된 모든 다각형이 시계방향으로 정의된 모든 꼭지점들이 볼록해야 합니다. 다각형은 아래 그림에서 보이는 것처럼 내부에서 두 점을 연결하는 모든 선분들이 다각형의 모서리를 지나지 않을 때 볼록합니다 (모든 각들이 180도 이내여야 함을 의미합니다):

그 꼭지점들이 오른쪽으로 도는 것으로 정의될 때 다각형의 "방향이" 시계방향입니다. (보조 설명: 위의 이미지는 반시계 방향입니다.)

또한 당신이 복잡한 모양을 환경의 파라미터로 정의할 필요가 있으시다면, 분리된 선분을 이용하는 것을 추천합니다. 선은 오브젝트의 한 쪽 면만이 충돌 가능한 것으로 정의해야 할 필요가 있을 때 플랫폼이나 벽 요소 등에도 사용될 수 있습니다.

시도해 보기

모든 것을 저장하고, 이제 당신의 index.html를 다시 여시면, 다음과 같은 것이 보일 것입니다: (당신의 브라우저에서 그것이 작동하는 것을 보시려면 이미지를 클릭하세요)

Step 3 Results

당신은 또한 우리 플레이어를 따라 환경을 스크롤하면서 자동적으로 따라가며 보이는 것을 보실 수 있습니다.

마지막 한 가지 - 오브젝트를 만들 때, 기본 충돌 모양은 오브젝트 사이의 충돌을 관리하기 위해 타일드(Tiled)에서 정의된 오브젝트 크기를 바탕으로 자동적으로 만들어집니다. 디버그를 위해, 당신은 #debug를 당신의 브라우저의 URL에 추가함으로써 디버그 패널을 활성화할 수 있습니다.

만약 당신이 게임을 다시 로드하고, "hitbox"를 활성화하시면 이것을 보실 수 있을 것입니다:

Enabling the debug panel

충돌 박스는 타일드(Tiled)에서 오브젝트의 크기를 바꾸고 위의 예시와 맞춤으로써 조정될 수 있습니다. (충돌 모양은 또한 엔티티 바디의 모양 속성에 접근해 수동으로 조정할 수도 있습니다.).

주의 : 디버그 패널을 사용하실 때, 스프라이트 경계는 초록색이 될 것이고, 정의된 충돌 모양은 빨간색이 될 것이며, 만약 직각 모양 이외의 다른 충돌 모양을 사용하신다면, 당신은 모든 정의된 충돌 모양을 포함하는 것에 상응하는 오렌지 박스를 보실 것입니다(그리고 또한 엔티티 바디를 묶는 박스를 호출할 것입니다).

파트 4: 스크롤링 배경 추가

이것은 정말 쉽습니다. 모든 것이 타일드(Tiled)를 통해 이루어지기에 우리는 심지어 한 줄의 코드를 더할 필요가 없습니다.

먼저, 우리가 파트 1 막바지에 추가했던 배경색을 제거하십시오. (이를 위해, 당신은 TMX 을 수정하고 `backgroundcolor` 속성을 제거할 필요가 있을 것입니다). 배경이 우리의 스크롤링 레이어로 채워질 것이기에, 우리는 특정한 색으로 깨끗하게 하기 위해 보여줄 필요가 없습니다 (게다가 그것은 몇몇 가치 있는 프레임을 저장할 것입니다).

그리고 우리는 두 가지 다음의 배경을 사용할 것입니다:

첫 번째 배경 레이어로 /data/img/area01_bkg0.png

Parallax background 1

두 번재 배경 레이어로 /data/img/area01_bkg1.png

Parallax background 2

타일드(Tiled)를 열고, 두 개의 새로운 이미지 레이어(Image Layers)를 추가하고, 이들에 당신이 원하는 이름을 붙이시고 레이어 순서를 정확하게 조정하였는지 확인하세요 (보여주는 순서는 위에서 아래입니다)

Layering parallax layers

이제 그들의 속성을 정의하고 다음의 속성을 설정하기 위해 레이어들에 우클릭하세요 :

  • 찾아보기 버튼을 클릭하고 area01_bkg0 이미지를 첫 번째 레이어로 선택합니다 (그림의 시차_레이어1)
  • 두 번째 레이어에도 반복합니다 (시차_레이어2)
Configuring Image Layer properties

그리고 마지막으로 각 레이어의 속도 스크롤을 정의하기 위해 비율(ratio) 속성을 추가합니다 : 우리는 첫 번째 레이어에 0.25를 정의하고 (그림의 시차_레이어1) 두 번째에 0.35를 추가합니다 (비율이 작아질수록 스크롤 속도가 늦어질 것이라는 점을 명심하십시오).

이미지 레이어의 기본 행동은 시차 효과를 만들기 위해 우리가 여기에서 하고자 하는 것과 같이 x축과 y축에서 자동적으로 반복된다는 것을 알아두세요.

시도해 보기

"Et voila!". 이제 당신의 index.html를 여신다면, 당신은 다음을 보실 것입니다:

Step 4 results

당신의 플레이어로 둘러보시고 뷰를 즐기세요 :)

파트 5: 기본 오브젝트와 적 추가

이 파트에서, 우리는 모을 수 있는 코인 (나중에 우리의 점수를 더하는 것으로 사용할), spinning_coin_gold.png 스프라이트 시트를 사용할 것입니다:

Spinning gold coin

그리고 wheelie_right.png 스프라이트시트를 활용해 기본적인 적도 추가합니다:

Wheelie right sprite

코인 자체는 매우 쉽습니다; 저희는 me.CollectableEntity를 확장할 것입니다. 사실, 우리는 그것을 타일드(Tiled)에서 직접 사용할 수 있습니다 (여기의 CoinEntity를 만들 필요 없이), 그러나 코인이 수집될 때 우리가 몇몇 점수와 오디오 특수효과를 추가할 것이기 때문에, 이 방법으로 직접 해봅시다.

/**
 * a Coin entity
 */
game.CoinEntity = me.CollectableEntity.extend({
  // extending the init function is not mandatory
  // unless you need to add some extra initialization
  init: function (x, y, settings) {
    // call the parent constructor
    this._super(me.CollectableEntity, 'init', [x, y , settings]);

  },

  // this function is called by the engine, when
  // an object is touched by something (here collected)
  onCollision : function (response, other) {
    // do something when collected

    // make sure it cannot be collected "again"
    this.body.setCollisionMask(me.collision.types.NO_OBJECT);

    // remove it
    me.game.world.removeChild(this);

    return false
  }
});

또한, 당신이 이것이 가능하도록 하는 모든 방법을 알아두는 것을 확실히 하세요, 저희는 타일드(Tiled)에서 직접 코인 오브젝트 속성을 정의할 것이므로, 우리는 지금 구축기에 다른 것을 더할 필요가 없습니다:

Spinning gold coin

우리의 적을 위해서는, 약간 더 깁니다 :

/**
 * an enemy Entity
 */
game.EnemyEntity = me.Entity.extend({
  init: function (x, y, settings) {
    // define this here instead of tiled
    settings.image = "wheelie_right";

    // save the area size defined in Tiled
    var width = settings.width;
    var height = settings.height;

    // adjust the size setting information to match the sprite size
    // so that the entity object is created with the right size
    settings.framewidth = settings.width = 64;
    settings.frameheight = settings.height = 64;

    // redefine the default shape (used to define path) with a shape matching the renderable
    settings.shapes[0] = new me.Rect(0, 0, settings.framewidth, settings.frameheight);

    // call the parent constructor
    this._super(me.Entity, 'init', [x, y , settings]);

    // set start/end position based on the initial area size
    x = this.pos.x;
    this.startX = x;
    this.endX   = x + width - settings.framewidth
    this.pos.x  = x + width - settings.framewidth;

    // to remember which side we were walking
    this.walkLeft = false;

    // walking & jumping speed
    this.body.setVelocity(4, 6);

  },

  /**
   * update the enemy pos
   */
  update : function (dt) {

    if (this.alive) {
      if (this.walkLeft && this.pos.x <= this.startX) {
        this.walkLeft = false;
      }
      else if (!this.walkLeft && this.pos.x >= this.endX) {
        this.walkLeft = true;
      }

      // make it walk
      this.renderable.flipX(this.walkLeft);
      this.body.vel.x += (this.walkLeft) ? -this.body.accel.x * me.timer.tick : this.body.accel.x * me.timer.tick;
    }
    else {
      this.body.vel.x = 0;
    }

    // update the body movement
    this.body.update(dt);

    // handle collisions against other shapes
    me.collision.check(this);

    // return true if we moved or if the renderable was updated
    return (this._super(me.Entity, 'update', [dt]) || this.body.vel.x !== 0 || this.body.vel.y !== 0);
  },

  /**
   * colision handler
   * (called when colliding with other objects)
   */
  onCollision : function (response, other) {
    if (response.b.body.collisionType !== me.collision.types.WORLD_SHAPE) {
      // res.y >0 means touched by something on the bottom
      // which mean at top position for this one
      if (this.alive && (response.overlapV.y > 0) && response.a.body.falling) {
        this.renderable.flicker(750);
      }
      return false;
    }
    // Make all other objects solid
    return true;
  }
});

여기서 볼 수 있듯이, 저는 settings.image와 settings.framewidth 속성을 구축기에서 직접 정의했고, 이는 타일드(Tiled)에서는, 제가 이 속성들을 제 오브젝트에 더할 필요가 없었음을 의미합니다 (다시 한 번, 사용 방법을 결정하는 것은 당신 몫입니다).

또한, 저는 타일드(Tiled)에 의해 주어진 너비 속성을 사용해 적이 달릴 경로를 정의합니다. 마침내, onCollision 메소드에서, 저는 만약 무엇이 그것 위로 뛴다면 적을 깜빡거리게 합니다.

여기에서 우리가 이것을 왜 하는지 설묭하는 그릴 수 있는 오브젝트 엔티티가 (애니메이션의 어떤 스프라이트이든지) 엔티티 `렌더러블` 속성을 통해 접근 가능하다는 것을 알아두세요 : `this.renderable.flicker(750);`

그리고 다시, 우리는 이들 새 오브젝트를 오브젝트 풀에 추가합니다.

// register our object entities in the object pool
me.pool.register("mainPlayer", game.PlayerEntity);
me.pool.register("CoinEntity", game.CoinEntity);
me.pool.register("EnemyEntity", game.EnemyEntity);

그리고 우리는 타일드(Tiled)에서 우리 레벨을 완료할 준비가 되었습니다. 새 오브젝트 레이어를 생성하고, 오브젝트 툴 삽입을 사용해 동전과 적들을 당신이 원하는 곳에 추가합니다. 각 오브젝트에 우클릭을 하고 그들의 이름을 CoinEntity 또는 EnemyEntity로 설정했는지 확인합니다.

Step 5

테스트 전, 우리는 우리의 플레이어의 다른 엔티티와의 충돌을 체크하기 위해 수정할 필요가 있습니다. 이를 위해, 만약 완료되지 않았다면 우리는 우리의 mainPlayer 코드의 me.collision.check(this) 함수에 대한 호출을 추가할 필요가 있습니다, 아래를 참조하세요 :

/**
 * update the player pos
 */
update : function (dt) {

  if (me.input.isKeyPressed('left')) {
    // flip the sprite on horizontal axis
    this.renderable.flipX(true);

    // update the entity velocity
    this.body.vel.x -= this.body.accel.x * me.timer.tick;

    // change to the walking animation
    if (!this.renderable.isCurrentAnimation("walk")) {
      this.renderable.setCurrentAnimation("walk");
    }
  }
  else if (me.input.isKeyPressed('right')) {
    // unflip the sprite
    this.renderable.flipX(false);

    // update the entity velocity
    this.body.vel.x += this.body.accel.x * me.timer.tick;

    // change to the walking animation
    if (!this.renderable.isCurrentAnimation("walk")) {
      this.renderable.setCurrentAnimation("walk");
    }
  }
  else {
    this.body.vel.x = 0;

    // change to the standing animation
    this.renderable.setCurrentAnimation("stand");
  }

  if (me.input.isKeyPressed('jump')) {
    if (!this.body.jumping && !this.body.falling) {
      // set current vel to the maximum defined value
      // gravity will then do the rest
      this.body.vel.y = -this.body.maxVel.y * me.timer.tick;

      // set the jumping flag
      this.body.jumping = true;
    }
  }

  // apply physics to the body (this moves the entity)
  this.body.update(dt);

  // handle collisions against other shapes
  me.collision.check(this);

  // return true if we moved or if the renderable was updated
  return (this._super(me.Entity, 'update', [dt]) || this.body.vel.x !== 0 || this.body.vel.y !== 0);
},

마지막으로, 우리가 레벨에 몇몇 플랫폼을 추가한 것처럼, the onCollision 핸들러를 수정해 "WORLD_SHAPE" 타입을 위한 맞춤 행동을 추가하고 "플랫폼" 요소를 아래에서처럼 시뮬레이션 해 봅시다.

우리가 "플랫폼"처럼 행동하도록 하는 입자 충돌 모양이 그들의 타입 속성을 타일드(Tiled)에서 "플랫폼"으로 설정함으로써 여기에서 확인된다는 것을 알아두세요. (양쪽 끝에 같은 값을 사용하는 한 당신이 필요한 것은 자유롭게 사용하세요).

/**
 * colision handler
 */
onCollision : function (response, other) {
  switch (response.b.body.collisionType) {
    case me.collision.types.WORLD_SHAPE:
      // Simulate a platform object
      if (other.type === "platform") {
        if (this.body.falling &&
          !me.input.isKeyPressed('down') &&

          // Shortest overlap would move the player upward
          (response.overlapV.y > 0) &&

          // The velocity is reasonably fast enough to have penetrated to the overlap depth
          (~~this.body.vel.y >= ~~response.overlapV.y)
        ) {
          // Disable collision on the x axis
          response.overlapV.x = 0;

          // Repond to the platform (it is solid)
          return true;
        }

        // Do not respond to the platform (pass through)
        return false;
      }
      break;

    case me.collision.types.ENEMY_OBJECT:
      if ((response.overlapV.y>0) && !this.body.jumping) {
        // bounce (force jump)
        this.body.falling = false;
        this.body.vel.y = -this.body.maxVel.y * me.timer.tick;

        // set the jumping flag
        this.body.jumping = true;
      }
      else {
        // let's flicker in case we touched an enemy
        this.renderable.flicker(750);
      }

      // Fall through

    default:
      // Do not respond to other objects (e.g. coins)
      return false;
  }

  // Make the object solid
  return true;
}

시도해 보기

그리고 이것이 당신이 얻어야 할 것입니다 (제가 플랫폼 등을 추가해 레벨을 약간 완성했다는 것을 기억하세요):

Step 5 results

당신의 동전을 모으고 적을 피하거나 그 위로 점프하려고 노력하세요!

파트 6: 기본 HUD 정보 추가

우리가 동전을 모을 때 점수를 보여줄 시간입니다.

우리는 비트맵 폰트를 사용해 우리의 점수를 표시할 것입니다 ! 편리함을 위해 저희는 필요한 비트맵과 데이터 정보를 제공하고 있습니다만, 스스로 필요한 파일을 생성하는 것도 매우 단순한데, 단지 조그만 방법을 따라하기만 하시면 됩니다. 여기

`data\fnt` 폴더 아래에서, 당신은 두 개의 파일을 찾을 수 있을 것입니다 : .PNG (실제 질감) 파일과 .FNT (글꼴 정의 파일)파일, 그리고 저희가 제공한 글꼴 예시는 "PressStart2P"라는 이름을 가집니다. 우리는 단지 다음 라인을 우리의 존재하는 자산 리스트에 추가해 이들을 사전에 로드해야 합니다 :

// game font
{ name: "PressStart2P", type:"image", src: "data/fnt/PressStart2P.png" },
{ name: "PressStart2P", type:"binary", src: "data/fnt/PressStart2P.fnt"},

여기서 주의하세요, .FNT 파일 타입은 바이너리로 설정될 필요가 있습니다.

우리가 이전에 사용했던 보일러 플레이트는 우리가 게임의 베이스로 사용할 HUD 스켈레톤을 포함합니다. 스켈레톤은 매우 단순하고 다음으로 구성됩니다:

  • me.Container로부터 상속되는 game.HUD.Container로 불리는 오브젝트
  • me.Renderable로부터 상속되는 game.HUD.ScoreItem라 불리는 기본 점수 오브젝트

HUD 컨테이너는 기본적으로 오브젝트 컨테이너로, 지속적인 것(persistent)으로 정의되고 (그것이 레벨 변화에도 살아남을 수 있도록), 다른 모든 오브젝트 위에 표시되며 (z 속성이 무한대(Infinity)로 설정), 우리는 또한 충돌 체크 중 무시하기 위해 그것을 충돌 불가능한 것으로 만듭니다.

점수 오브젝트(Score Object)는 소수(floating)로 (그것을 우리의 HUD 컨테이너에 추가했을 때 우리가 화면 조정을 사용하기 위해) 그리고 현재로서는 점수 값의 현재 캐시 값으로 (game.data 하에 정의된) 정의됩니다.

/**
 * a HUD container and child items
 */
game.HUD = game.HUD || {};

game.HUD.Container = me.Container.extend({
  init: function () {
    // call the constructor
    this._super(me.Container, 'init');

    // persistent across level change
    this.isPersistent = true;

    // make sure we use screen coordinates
    this.floating = true;

    // give a name
    this.name = "HUD";

    // add our child score object
    this.addChild(new game.HUD.ScoreItem(-10, -10));
  }
});

/**
 * a basic HUD item to display score
 */
game.HUD.ScoreItem = me.Renderable.extend({
  /**
   * constructor
   */
  init : function (x, y) {
      // call the parent constructor
      // (size does not matter here)
      this._super(me.Renderable, 'init', [x, y, 10, 10]);

      // local copy of the global score
      this.score = -1;
  },

  /**
   * update function
   */
  update : function (dt) {
    // we don't do anything fancy here, so just
    // return true if the score has been updated
    if (this.score !== game.data.score) {
      this.score = game.data.score;
      return true;
    }
    return false;
  },

  /**
   * draw the score
   */
  draw : function (renderer) {
    // draw it baby !
  }
});

이제 우리의 현재 점수를 표시해봅시다 ! 이를 위해 우리는 로컬 글꼴 설정을 만들어냄으로써 (이전의 비트맵 글꼴을 사용) 단지 주어진 ScoreItem 오브젝트를 완성할 것입니고, 단순히 우리의 비트맵 글꼴을 사용해 점수를 보여줄 것입니다 :

/**
 * a basic HUD item to display score
 */
game.HUD.ScoreItem = me.Renderable.extend( {
  /**
   * constructor
   */
  init : function (x, y) {
    // call the parent constructor
    // (size does not matter here)
    this._super(me.Renderable, 'init', [x, y, 10, 10]);

    // create the font object
    this.font = new me.BitmapFont(me.loader.getBinary('PressStart2P'), me.loader.getImage('PressStart2P'));

    // font alignment to right, bottom
    this.font.textAlign = "right";
    this.font.textBaseline = "bottom";

    // local copy of the global score
    this.score = -1;
  },

  /**
   * update function
   */
  update : function (dt) {
    // we don't draw anything fancy here, so just
    // return true if the score has been updated
    if (this.score !== game.data.score) {
      this.score = game.data.score;
      return true;
    }
    return false;
  },

  /**
   * draw the score
   */
  draw : function (renderer) {
        // this.pos.x, this.pos.y are the relative position from the screen right bottom
		this.font.draw (renderer, game.data.score, me.game.viewport.width + this.pos.x, me.game.viewport.height + this.pos.y);
  }
});

HUD는 이미 우리가 게임을 시작할 때 추가되고 제거되었으므로, 여기에서 할 일은 없습니다. 우리가 레벨을 로드한 다음 게임 세계에 HUD를 추가한다는 것을 주의하세요, me.Container 오브젝트가 기본 값으로 자동적으로 z값을 설정하기 때문에 (자동 깊이(autoDepth) 특성을 통해) 이것은 HUD가 나머지의 위에 정확하게 표시되는 지를 확인할 것입니다.

game.PlayScreen = me.ScreenObject.extend({
  /**
   * action to perform on state change
   */
  onResetEvent : function () {
    // load a level
    me.levelDirector.loadLevel("area01");

    // reset the score
    game.data.score = 0;

    // add our HUD to the game world
    this.HUD = new game.HUD.Container();
    me.game.world.addChild(this.HUD);
  },

  /**
   * action to perform when leaving this screen (state change)
   */
  onDestroyEvent : function () {
    // remove the HUD from the game world
    me.game.world.removeChild(this.HUD);
  }
});

마지막 단계는 물론 동전이 수집되었을 때 점수를 실제로 바꾸는 것입니다 ! 이제 우리의 코인 오브젝트(Coin Object)를 수정해봅시다:

onCollision : function () {
  // do something when collected

  // give some score
  game.data.score += 250;

  // make sure it cannot be collected "again"
  this.body.setCollisionMask(me.collision.types.NO_OBJECT);

  // remove it
  me.game.world.removeChild(this);
}

당신이 보실 수 있듯이, onCollision 함수에서, 우리는 값을 더해줌으로써 우리의 our game.data.score 속성을 변화시켰고, 우리는 오브젝트가 다시 수집될 수 없도록 확인했고, 동전을 제거했습니다

시도해 보기

우리는 이제 결과를 확인할 수 있고, 우리는 이제 우리의 점수가 화면 오른쪽 아래 구석에 표시되도록 만들었을 것입니다:

Step 6 results

파트 7: 오디오 추가

이 섹션에서, 우리는 오디오를 우리 게임에 추가할 것입니다:

  • 동전 수집 시에 나는 소리
  • 점프할 때 나는 소리
  • 적을 밟을 때 나는 소리
  • 배경 음악 (또는 인 게임 음악)

만약 우리가 우리가 처음에 오디오를 어떻게 초기화했는지를 다시 본다면, 당신은 우리가, 당신의 브라우저 능력에 기초해 melonJS가 제대로 사용할 수 있도록 하나는 mp3로 하나는 ogg로 두 개의 오디오 파일 포맷을 제공했음을 알려주기 위해, "mp3,ogg" 파라미터를 초기화 함수(initialization function)로 보내줬음을 알 수 있습니다.

// initialize the "audio"
me.audio.init("mp3,ogg");

이제 우리 게임을 수정합시다 :

코인 수집

우리가 이전에 점수 획득을 관리헸던 CoinEntity 코드에서, 우리는 단지 me.audio.play()에 대한 새 호출을 추가하고 "cling" 오디오 리소스를 사용하면 됮니다. that's all!

onCollision : function () {
  // do something when collected

  // play a "coin collected" sound
  me.audio.play("cling");

  // give some score
  game.data.score += 250;

  // make sure it cannot be collected "again"
  this.body.setCollisionMask(me.collision.types.NO_OBJECT);

  // remove it
  me.game.world.removeChild(this);
}

점프

메인 플레이어의 update() 함수에서, 우리는 me.audio.play()를 호출하는 것을 더하고 "점프" 오디오 리소스를 사용할 것입니다. 당신은 doJump()의 결과 값에 대한 테스트를 추가했다는 것 또한 알아두셔야 합니다. doJump는 당신이 점프(이미 점프 하고 있는 등)하는 것이 허용되지 않은 경우 거짓(false) 값을 돌려주고 그 경우 사운드를 플레이할 필요가 없을 것입니다.

if (me.input.isKeyPressed('jump')) {
  if (!this.body.jumping && !this.body.falling) {
    // set current vel to the maximum defined value
    // gravity will then do the rest
    this.body.vel.y = -this.body.maxVel.y * me.timer.tick;

    // set the jumping flag
    this.body.jumping = true;

    // play some audio
    me.audio.play("jump");
  }
}

밟기

이번에는 비슷하지만, "짓밟기" 리소스를 사용해, 메인 플레이어의 충돌 핸들러 함수를 볼 것입니다:

/**
 * colision handler
 */
onCollision : function (response, other) {

      // ...

      case me.collision.types.ENEMY_OBJECT:
        if ((response.overlapV.y>0) && !this.body.jumping) {
          // bounce (force jump)
          this.body.falling = false;
          this.body.vel.y = -this.body.maxVel.y * me.timer.tick;

          // set the jumping flag
          this.body.jumping = true;

          // play some audio
          me.audio.play("stomp");
        }
        else {
          // let's flicker in case we touched an enemy
          this.renderable.flicker(750);
        }

        // Fall through

      default:
        // Do not respond to other objects (e.g. coins)
        return false;
    }

  // Make the object solid
  return true;
}

인-게임 음악

우리의 메인에서, onResetEvent() 함수를 사용해, 우리는 사용될 오디오 트랙을 보기 위해 me.audio.playTrack() 함수 호출을 추가합니다:

onResetEvent : function () {
  // play the audio track
  me.audio.playTrack("dst-inertexponent");

  // ...
},

그리고 우리는 또한 게임에서 나갈 때 현재의 트랙을 멈추기 위해 onDestroyEvent() 함수를 수정할 필요가 있습니다:

onDestroyEvent : function () {

  // ...

  // stop the current audio track
  me.audio.stopTrack();
}

여기까지입니다! 최종 결과를 보시려면, 여기를 클릭하세요.

파트 8: 두 번째 레벨 추가

당신은 레벨을 만드는 법을 이제 알아야 합니다. 그렇지만, 여기서 저는 다른 레벨로 가는 방법을 보여드릴 것입니다.

이를 위해, melonJS는 레벨 엔티티(me.LevelEntity)라는 이름의 오브젝트를 가지고 있고 그것을 우리는 Tiled의 엔티티 레이어에 추가하고 우리의 메인 플레이어가 그것을 쳤을 때 무엇을 할 지를 정할 것입니다 :

Creating an object to go to next level

우리의 새로운 레벨을 "area02"라고 부른다고 가정하면, 우리는 "area02"의 속성 값으로 "to"를 추가할 필요가 있습니다. 우리의 플레이어가 오브젝트를 쳤을 때, 엔진이 자동적으로 "area02" 레벨을 로드하도록 하기 위해서입니다.

또한, 우리는 "페이드" 색상과 "지속 시간" (밀리 초 단위로) 속성(이미지에서처럼)을 추가함으로써 엔진에게 레벨이 바뀔 때 페이드 아웃/페이드 인 효과를 추가하도록 요청할 수 있습니다.

최종 결과를 보시려면, 여기를 클릭하세요.

파트 9: 타이틀 화면 추가

끝내기 위해, "/data/img/gui/" 폴더 안의 title_screen.png 파일들을 활용해 타이틀 화면을 우리 게임에 추가해 봅시다. (물론 이들은 리소스 리스트에 있어야 합니다. 왜냐하면 우리는 그것을 다른 이미지를 이용해서 해보았습니다.):

Title screen

그리고 그 위에 우리는 몇몇 메시지를 추가할 것이고 시작하기 위해 사용자의 입력을 기다릴 것입니다!

먼저 새로운 오브젝트를 정의해, me.ScreenObject를 확장해봅시다:

/**
 * A title screen
 */
game.TitleScreen = me.ScreenObject.extend({
  // reset function
  onResetEvent : function () {
    // ...
  },

  // destroy function
  onDestroyEvent : function () {
    // ...
  }
});

그래서 이제 우리는 다음을 하려고 합니다:

  • 위의 배경 이미지 시현
  • 스크린 중앙에 텍스트 추가 ("플레이하려면 엔터 키를 누르세요")
  • 사용자 입력(엔터 키 입력)을 기다림

추가적으로, 저는 이 튜토리얼에 조그만 스크롤링 텍스트를 추가하고자 합니다.

game.TitleScreen = me.ScreenObject.extend({
  /**
   * action to perform on state change
   */
  onResetEvent : function () {
    // title screen
    var backgroundImage = new me.Sprite(0, 0, {
            image: me.loader.getImage('title_screen'),
        }
    );

    // position and scale to fit with the viewport size
    backgroundImage.anchorPoint.set(0, 0);
    backgroundImage.scale(me.game.viewport.width / backgroundImage.width, me.game.viewport.height / backgroundImage.height);

    // add to the world container
    me.game.world.addChild(backgroundImage, 1);

    // add a new renderable component with the scrolling text
    me.game.world.addChild(new (me.Renderable.extend ({
      // constructor
      init : function () {
        this._super(me.Renderable, 'init', [0, 0, me.game.viewport.width, me.game.viewport.height]);

        // font for the scrolling text
        this.font = new me.BitmapFont(me.loader.getBinary('PressStart2P'), me.loader.getImage('PressStart2P'));

        // a tween to animate the arrow
        this.scrollertween = new me.Tween(this).to({scrollerpos: -2200 }, 10000).onComplete(this.scrollover.bind(this)).start();

        this.scroller = "A SMALL STEP BY STEP TUTORIAL FOR GAME CREATION WITH MELONJS       ";
        this.scrollerpos = 600;
      },

      // some callback for the tween objects
      scrollover : function () {
        // reset to default value
        this.scrollerpos = 640;
        this.scrollertween.to({scrollerpos: -2200 }, 10000).onComplete(this.scrollover.bind(this)).start();
      },

      update : function (dt) {
        return true;
      },

      draw : function (renderer) {
        this.font.draw(renderer, "PRESS ENTER TO PLAY", 20, 240);
        this.font.draw(renderer, this.scroller, this.scrollerpos, 440);
      },
      onDestroyEvent : function () {
        //just in case
        this.scrollertween.stop();
      }
    })), 2);

    // change to play state on press Enter or click/tap
    me.input.bindKey(me.input.KEY.ENTER, "enter", true);
    me.input.bindPointer(me.input.pointer.LEFT, me.input.KEY.ENTER);
    this.handler = me.event.subscribe(me.event.KEYDOWN, function (action, keyCode, edge) {
      if (action === "enter") {
        // play something on tap / enter
        // this will unlock audio on mobile devices
        me.audio.play("cling");
        me.state.change(me.state.PLAY);
      }
    });
  },

  /**
   * action to perform when leaving this screen (state change)
   */
  onDestroyEvent : function () {
    me.input.unbindKey(me.input.KEY.ENTER);
    me.input.unbindPointer(me.input.pointer.LEFT);
    me.event.unsubscribe(this.handler);
  }
});

위에서 우리가 무엇을 가지고 있을까요?

  1. 1) onResetEvent 함수에서, 우리는 두 렌더러블 구성 요소를 생성하고 그들을 저희 게임 세상에 추가했습니다. 첫 번째는 기본 스프라이트 오브젝트로 우리 타이털 배경 이미지를 보여줄 것이고 두번째는 "ENTER 누르기" 메시지와 트윈 오브젝트 기반 스크롤러를 관리합니다. 주의: 폰트와 관련하여, 만약 당신이 주의깊게 관련된 파일(32x32_font.png)을 확인하신다면, 당신은 그것이 대문자만 포함한다는 것을 알아차리실 것입니다. 그러므로 당신도 당신의 텍스트에 대문자만 포함하도록 하십시오.
  2. 2) 우리는 또한 키 이벤트 또는 마우스/탭 이벤트에 등록하고, 만약 입력되면 "플레이" 상태로 자동적으로 전환합니다.
  3. 3) 파괴할 때, 우리는 키와 포인터 이벤트의 묶음을 풉니다.

그리고 물론 가장 마지막 것은 우리가 새로 만든 오브젝트를 엔진에 지시하고 그것을 연동된 상태(여기서는, 메뉴)에 연결하는 것입니다. 또한, me.state 전이 함수를 이용해, 상태 변화 사이에 엔진에 페이딩 효과를 더하라고 말할 것입니다.

마침내, 로드 함수의 끝에 도달했을 때, 플레이 상태로 전환하는 대신, 이제 메뉴 상태로 전환할 것입니다:

/*
 * callback when everything is loaded
 */
loaded : function () {
  // set the "Play/Ingame" Screen Object
  me.state.set(me.state.MENU, new game.TitleScreen());

  // set the "Play/Ingame" Screen Object
  me.state.set(me.state.PLAY, new game.PlayScreen());

  // set a global fading transition for the screen
  me.state.transition("fade", "#FFFFFF", 250);

  // register our player entity in the object pool
  me.pool.register("mainPlayer", game.PlayerEntity);
  me.pool.register("CoinEntity", game.CoinEntity);
  me.pool.register("EnemyEntity", game.EnemyEntity);

  // enable the keyboard
  me.input.bindKey(me.input.KEY.LEFT, "left");
  me.input.bindKey(me.input.KEY.RIGHT, "right");
  me.input.bindKey(me.input.KEY.X, "jump", true);

  // display the menu title
  me.state.change(me.state.MENU);
}

시도해 보기

축하합니다! 당신은 이 튜토리얼의 끝에 도달하셨습니다. 이제 이것을 테스트해 보시면, 이와 같은 것을 갖게될 것입니다:

Your completed game

파트 10: 결론

음, melonJS에 대한 이 약간의 소개를 즐기셨기를 바라고, 어떻게 추가적으로 발저니킬 수 있는지 당신 스스로 알아보실 수 있기를 바랍니다. 이것은 프로그래밍과 게임 개발의 중요한 부분입니다.

만약 본 튜토리얼의 일부나 도전의 어떤 부분에서 어려움을 겪으신다면, 문제를 검색하시거나, 그 질문을 저희 포럼 @html5gamedevs에 보내주세요.

이것이 즐거움을 원한 것이라는 것을 잊지 마세요, 즐기세요!