Fork me on GitHub

プラットフォームゲームのチュートリアル

このチュートリアルでは、シンプルなプラットフォームゲームを作ります。Tiled をレベルエディタとして使い、動作させるゲームの基本的な要素を作ることに主な焦点を当てていきます。

はじめに

このチュートリアルでは、以下のプログラムが必要になります:

  • Tiled Map Editor:インストール済み・動作中(0.9.0 以降)
  • melonJS ボイラープレート:このチュートリアルではデフォルトのテンプレートプロジェクトとして使います。
  • チュートリアルのデータファイル:(上記)テンプレートデータのディレクトリに解凍され、以下の内容が含まれます。
    • レベルのタイルセット1つ
    • パララックス・レイヤー用の背景2つ
    • 基本的なスプライトシート
    • 基本的な音声 SFX と音楽
    • タイトル画面の背景
  • melonJS ライブラリ: /lib ディレクトリの下にコピーします(ミニフィケーション版およびプレーン版の両方を必ずダウンロードしてください。デバッグ目的でプレーン版が必要になる可能性があります。)
  • より詳細な内容を知るためのmelonJS ドキュメンテーション

テスト/デバッグ:
そのファイルシステムを使いたいだけの場合には、"cross-origin request" のセキュリティエラーに直面することが問題となります。Chrome の場合は、"--disable-web-security" パラメータを使う必要がある、または、ブラウザの起動時に"--allow-file-access-from-files" した方が良いことになります。ローカルのコンテンツをテストするためにはこれを行わねばならず、これを行わない場合には XHR を通じてアセットをロードしようとする時にブラウザがエラーを出します。このオプションを有効にしている限りは、ご自分の使用環境にセキュリティ上の脆弱性を追加することになりますので、この方法は推奨されるものではありません。

2つ目のより簡単なオプションは、例えば、melonJS のボイラープレートの README にある grunt serve ツールを使って、ローカルのウェブサーバーを使うことです。これで http://localhost:8000 URL を使ってブラウザ上でゲームをテストできるようになります。

クレジットの追加:

気兼ねなく、お好きなように改変してください。みなさんはすでに Tiled には馴染みがあると思っていますが、このツールに関するサポートがさらに必要な場合は、Tiled のホームページや wiki により詳しい情報が掲載されているので、そちらをご確認ください。

その1:Tiled を使ってレベルを作る

まず、 Tiled を開いて、新しいマップを作りましょう。:このチュートリアルでは、 640x480 サイズのキャンバスと 32x32 サイズのタイルを使用するため、マップサイズとしては最低でもそれぞれ 20 枚と 15 枚を指定する必要があります。この例では、 40x15 サイズのレベルを定義することで、後でスクロールする背景を楽しむことができるようにします。

Step 1 of creating a new map

また、melonJS は 非圧縮のタイルマップだけにしか対応していないため、正しく設定されていることを確認してください。小さいサイズのファイルを作成することから、私たちは Base64 エンコーディングを推奨していますが、ご自由にお選び下さい。

次に、Map/New Tileset を使ってタイルセットを追加しましょう。Tiled でタイルセットの間隔やマージンがゼロに設定されていることを確認してください。

Adding a tileset

美しく作るために、背景レイヤー1つと前景レイヤー1つ、合計2つのレイヤーを作成します。ご自分の想像力を活用して、お好きなようにしてください。私は論理的に "background" と "foreground" と名前をつけましたが、自由に好きな名前をつけてください。

ここまで完了すると、私のレベルはこのように見えるようになりました。:

Tiled level design

最後に、カラーピッカーツール(Map/Map Properties)を使って、好きな色を指定して、レベルの背景色を定義しましょう。

Setting a background color in Tiled

終了させるために、この新しいマップを "/data/map/" フォルダの下にある "area01" に保存し(resources.js のファイルを構成するためのGrunt タスクは、マップの特定の場所だけを確認します)、これで最初のステップは終了です。

その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 のスケルトンを見てみましょう:

/* 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() 関数が呼び出され、画面と音声が初期化され、すべてのゲームリソースのロードが始まります。また、すべてを使用する準備ができた時に呼び出されるコールバックも定義します。コールバックの中では、ゲームイベント(リセットなど)を管理するために使われる PlayScreen オブジェクトとともに、ゲーム内で使われる新しいステートを定義しています。

このチュートリアルでは640x480 サイズのキャンバスを作成するため、デフォルトのプロジェクトテンプレートから、唯一 `me.video.init()` 関数に与えられる映像の解像度だけは変更します。また、プラットフォームゲームにより適しているため、scaleMethod を "flex-width" に変更します。(利用可能な様々なスケーリングモードに関する詳しい情報については、`me.video.init` のドキュメンテーションをご覧ください。)

このボイラープレートは自動的にリソースリストを構築し、grunt serve タスクを使う時には、game.resources (build/js/resources.js) としてそのリストをアプリにエクスポーズします。

警告: ボイラープレートを使わない場合は、手動で resources.js を管理する(時間がかかり、エラーが起こりやすくなります)必要があります。 resources.js を手動で管理する場合には、 git repo にある事例をご覧ください。

また、ここでは tmx ファイルを直接使っていますが、ゲーム制作をする際には、 JSON フォーマット(Tiled から直接エクスポートすることができます)を使うことを推奨していることをご留意ください。これはファイルサイズが小さく、レベルのロードがずっと速く、.txm 拡張子によるサーバー上の問題を予防するためです。

最後に、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 は自動的にそれに対応したビューポイントを作成します。次のステップで "main player" を追加すると、マップ上を移動できるようになります。

その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;
  }
});

上のコードを理解するのはかなり簡単だと思います。基本的には、 Entity を拡張して、プレイヤーのデフォルト速度を設定し、カメラを調整し、何らかのキーが押されたかどうかを試し、プレイヤーの動きを管理します(プレイヤーの速度を設定し、エンティティ Body の update 関数を呼び出す)。また、気づいたかもしれませんが、オブジェクトの最終的な速度(this.body.vel.x と this.body.vel.y)をテストしており、これによって、オブジェクトが実際に動いたかを知り、スプライトアニメーションを実行したいかどうかを調節することができます。

その後、デフォルトの game.PlayerEntity はすでにボイラープレートの中で宣言されていますが、オブジェクト プールの中で新しいエンティティを実際に宣言し(オブジェクトのインスタンスを作成するためにエンジンがこれを使います)、最終的にプレイヤーの動きに使うキーをマッビングするように、"main" を修正しなくてはなりません。 そのため 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(またはオブジェクトをオブジェクトプールに登録した時に使った名前と同じ名前)と名前を付け(大文字か小文字かは無関係)、そのオブジェクトに2つのプロパティを追加します:

  • image:gripe_run_right の値(リソースの名称)
  • framewidth:スプライトシートの1つのスプライトのサイズである64 という値
  • frameheight:シングルラインのスプライトシートを使用しており、その場合エンジンは実際の画像の高さを数値として採用するため、ここではこの値を定義しません。

これら2つのパラメーターは、そのオブジェクトが作成される時に、パラメーター(この上の設定オブジェクトはコンストラクタに使用されます)として渡されます。ここで、これらの項目を Tiled で指定することもできますし、コードの中で直接指定することもできます。(複数のオブジェクトを処理する時には、Tiled では名前だけを指定し、残りを直接コンストラクタで管理した方が簡単な場合があります。)

注意:好きなだけ多くプロパティを追加することもでき、コンストラクタに手渡される設定オブジェクトの中ですべて利用することができます。

Adding an entity

オブジェクトが作成されたら、エンティティをレベルの中に位置付けます。下の例にあるように、実際のスプライトのサイズに合うように、Tiled の中でオブジェクトの長方形をサイズ変更することを忘れないようにしましょう。

positioning an entity

衝突レイヤーの定義

もうほとんど終わりです!最後のステップは、衝突レイヤーの定義です。ここでは、"collision" という名前の新しいレイヤーを作成して、そこに基本的なシェイプを追加する必要があるだけです。それだけで大丈夫です!

ここで、新しいオブジェクトグループレイヤーを追加します。エンジンが衝突オブジェクトレイヤーだと認識できるように、このレイヤーの名前には必ず "collision" というキーワードを含むようにします。

レイヤーが追加されたら、選択して、オブジェクトツールバーを使っていずれかのシェイプを追加することによって、レベルの衝突マップを "描画" するだけです。

object tool bar

melonJS は Separating Axis Theorem アルゴリズムを使って衝突検知を実装していることをご留意ください。衝突に使われる全ての多角形は、時計回りに探索して定義されたすべての頂点が凸である必要があります。以下に示されている通り、内部の2点を結ぶすべての線分が多角形のどの辺とも交差しない(全ての内角が180度未満であることを意味しています)時に、その多角形は凸であるということになります:

多角形の頂点を右へ順番に宣言する場合には、多角形の "探索" が時計回りだということになります。(補注:上の画像は反時計回りに探索しています。)

また、その環境の周囲の長さを特定するために複雑なシェイプが必要な場合には、切り離された線分を使うことが推奨されます。直線は、例えばプラットフォームや壁エレメントを定義する際などにも使われ、その場合には衝突するオブジェクトの特定の側面だけが必要になります。

試してみよう

すべてを保存して、index.html を再度開くと、このようなものが見えるはずです。(イメージの上をクリックすると、ブラウザ上で動作するのが見えます)

Step 3 Results

ディスプレイが自動的にプレイヤーを追いかけ、その環境をスクロールしていくことにも気づくでしょう。

最後に1つ。オブジェクトを作る時に、デフォルトの衝突シェイプは、Tiled で定義されたオブジェクトのサイズに基づいて、オブジェクト同士の衝突を管理するために、自動的に作成されます。デバッグ目的の時には、ブラウザの URL バーで URL に #debug と追加することで、デバッグパネルを有効にすることができます。

ゲームをリロードして、"hitbox" を有効にすると、これを見ることができます:

Enabling the debug panel

この衝突ボックスは、Tiled で オブジェクトのサイズを変更することで調整でき、上の例に合わせることができます。(エンティティボディシェイプのプロパティにアクセスすることで、 衝突シェイプを手動で調整することもできます。)

注意:デバッグパネルを使う時には、スプライトボーダーが緑で描画され、定義された衝突シェイプは赤で描画されます。長方形の衝突シェイプ以外またはそれ以上の何かを使う場合には、全ての定義された衝突シェイプを含む最小の長方形に対応して、オレンジ色のボックス(エンティティボディ・バウンディングボックスと呼ばれます)も表示されるはずです。

その4:スクロールする背景を追加する

これはとても簡単です。Tiled 経由で全てできるので、1列のコードも追加する必要がありません。

まず、その1の最後で追加した背景色を削除します。(そのために、TMX ファイルを編集して、`backgroundcolor` プロパティを削除する必要があります。) 背景はスクロールするレイヤーで埋められるため、特定の色に表示する必要はありません。(それに、貴重なフレームを節約することにもなります。)

次に、以下の2つの背景を使います:

/data/img/area01_bkg0.png を最初の背景レイヤーにします

Parallax background 1

/data/img/area01_bkg1.png を2つ目の背景レイヤーにします

Parallax background 2

Tiled を開き、2つの新しい画像レイヤーを追加して、好きな名前を付けて、レイヤーの順番が正しくなる(下から順に表示される順番)ように調整します。

Layering parallax layers

ここでレイヤーを右クリックして、そのプロパティを定義して、以下のプロパティを設定します:

  • ブラウズボタンをクリックして、 画像 area01_bkg0 を最初のレイヤーとして選択します。(写真の Parallax_layer1)
  • 2つ目のレイヤーでも同じように繰り返します(Parallax_layer2)
Configuring Image Layer properties

最後に、 ratioプロパティを追加して、各レイヤーのスクロール速度を指定します。最初のレイヤー(写真の Parallax_layer1)では0.25 という値、2つ目のレイヤーには 0.35 という値を指定します。(ratio が小さいほど、スクロール速度が遅くなることをお忘れなく)

イメージレイヤーに対するデフォルトのビヘイビアは、x 軸 y 軸 の両方について自動的に繰り返すことになっており、これはパララックス効果を作るためにここで設定したい内容と一致します。

試してみよう

"じゃーん!" index.html を開くと、このように見えるはずです:

Step 4 results

プレイヤーを動かしてみて、ビューをお楽しみください :)

その5:基本的なオブジェクトや敵を追加する

ここでは、spinning_coin_gold.png スプライトシートを使って、集められるコイン(あとでスコアに追加するために使います)を追加します:

Spinning gold coin

そして、wheelie_right.png スプライトシートを使って、基本的な敵を追加します:

Wheelie right sprite

コイン自体はかなり簡単です。ただ me.CollectableEntity を拡張するだけです。実際には、Tiled の中で直接使う(ここで CoinEntity を作成する必要がない)ことができますが、コインを集めた時のスコアや音声 SFX を後で追加するため、この方法で直接行いましょう。

/**
 * 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 の中で Coin オブジェクトのプロパティを定義して、ここではコンストラクタに他のものを追加する必要がないようにします。:

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;
  }
});

ここでわかるように、As you can see here, I specified the コンストラクタで直接 settings.image と settings.framewidth のプロパティを指定しましたが、これは Tiled の中でこれらのプロパティをオブジェクトに追加しなくて良いことを意味します。(繰り返しになりますが、どのように使うのかはあなた次第です。)

また、この敵が走る経路を指定するために Tiled が与えた width プロパティを使っています。最後に、onCollision メソッドの中で、何かが敵の上にジャンプした時に、敵が点滅するようにしています。

オブジェクトエンティティの描画可能なコンポーネント(アニメーションのシングルスプライトのどちらか)がエンティティの `renderable` プロパティを通じてアクセス可能であることに注意してください。これが次の内容をここで行う理由を説明しています: `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 でレベルを完成させる準備ができました。新しいオブジェクトレイヤーを作成し、コインや敵を好きな場所に追加するために、Insert Object ツールを使います。各オブジェクトの上で右クリックして、名前を必ず 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);
},

最後に、これも大切なことで、レベル内にいくつかのプラットフォームを追加しましたが、"WORLD_SHAPE" タイプに対するカスタマイズされたビヘイビアを追加するために、onCollision ハンドラーを修正し、以下に示されているように、"platform" エレメントをシミュレーションしましょう。

Tiled でそれらのタイププロパティを "platform" と設定することで、"platform" としてふるまって欲しい特定の衝突シェイプが指定されることにご注意ください。(両方で同じ値を使う限りは、ご自由に好きな値を使ってください)

/**
 * 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` フォルダの下には、2つのファイルがあります: a .PNG(実際のテクスチャ)と a .FNT (フォント定義ファイル)で、ここで提供されるフォントの例には、"PressStart2P" と名前がついており、プレロードするには、既存のアセットリストに以下のラインを追加する必要があるだけです:

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

ここでは .FNT のファイルタイプをバイナリに設定する必要があるため、気を付けてください。

先ほど使ったボイラープレートにはすでに HUD スケルトン が含まれており、ゲームのベースとして使います。スケルトンはかなりシンプルで、以下を含んでいます:

  • game.HUD.Container と呼ばれるオブジェクト(me.Containerから継承)
  • game.HUD.ScoreItem と呼ばれる基本スコアオブジェクト(me.Renderableから継承)

HUD コンテナは基本的にはただのオブジェクトコンテナであり、persistent と定義されており(レベルが変化しても変わらない)、すべての他のオブジェクトより前に表示され(z プロパティは無限大に設定)、衝突不可にするため、衝突チェックの間は無視されます。

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 は、ゲームがスタートした時には、すでに追加された後に削除されているため、ここでは何もすることがありません。 me.Container オブジェクトはデフォルトで z の値を自動的に 設定する(autoDepth 機能を通じて)ため、レベルのロード後にゲーム世界に HUD を追加していることにご注意してください。こうすることで、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 オブジェクトを修正しましょう:

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 関数の中で、単に game.data.score プロパティに値を追加することでこれを変更し、その後、オブジェクトを重複して集められないことを確認し、コインを削除しています。

試してみよう

ここで結果を確認すると、画面の右下にスコアが表示されているはずです:

Step 6 results

その7:音声を追加する

このセクションでは、ゲームに音声を追加します:

  • コインを集めたときの音
  • ジャンプした時の音
  • 敵を踏みつけた時の音
  • BGM (またはゲーム内音楽)

最初にどのように音声を初期化したかを振り返ると、"mp3,ogg" パラメーターを initialization 関数に渡し、mp3 と ogg の2つの音声ファイルフォーマットで提供することを示唆していることがわかると思います。こうすることで、melonJS はブラウザの能力に基づいてその権利を使用します。

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

それではゲームを修正して見ましょう:

コインの収集

以前獲得したポイントを管理した CoinEntity コードの中で、me.audio.play() の新たな呼び出しを追加し、"cling" 音声リソースを使用する必要があるだけです。

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);
}

ジャンプ

mainPlayer の update() 関数の中では、me.audio.play()の呼び出しを追加し、"jump" 音源を使いました。doJump() が返す値に対するテストを追加したことにも注意してください。ジャンプできない場合(すでにジャンプしている、など)には、doJump は false を返し、この場合、音声 SFX を再生する必要はありません。

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");
  }
}

ふみつける

これについても同様ですが、"ふみつけ" のリソースを使い、今回は mainPlayer の衝突ハンドラー関数の中に入れます:

/**
 * 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;
}

ゲーム内音楽

main の中の 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:2つ目のレベルを追加する

もうレベルを作る方法はわかったはずです。しかし、ここでは次のレベルに進む方法をお見せします。

このために、melonJS には me.LevelEntity という名前のオブジェクトがあり、Tiled の Entities レイヤーに追加し、メインプレイヤーがそれに当たると何をするのか指定します:

Creating an object to go to next level

新しいレベルが "area02" と呼ばれるとすると、"to" プロパティに "area02" という値を追加するだけです。プレイヤーがオブジェクトに当たると、エンジンが自動的にレベル "area02" をロードします。

オプションとして、Optionally we can also ask the engine to add a fadeOut/fadeIn effect when changing level by adding the "fade" の色 と "duration" (ミリ秒単位) のプロパティを追加する(画像参照)ことで、レベルが変わる時のフェードアウト/フェードインのエフェクトの追加をエンジンに求めることもできます。

ここをクリックして、最後の状態を確認してください。

その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 () {
    // ...
  }
});

ここでやりたいことは:

  • 上の背景画像を表示すること
  • 画面中央にテキストを追加すること ("Enter を押してゲーム開始")
  • ユーザーのインプットを待つ(Enter を押す)

さらに、このチュートリアルに関して、短いスクロールするテキストを追加したいです。

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. onResetEvent 関数の中で、2つのレンダリング可能なコンポーネントを作成し、ゲーム世界に追加します。1つ目はタイトルの背景画像を表示する基本的なスプライトオブジェクトで、2つ目は トゥイーンオブジェクトに基づいて "press ENTER" のメッセージとスクローラーを処理します。注意:フォントに関しては、対応するアセット(32x32_font.png)を注意深く確認すると、大文字しか含まれていないことがわかるため、テキストの中では大文字だけを使うようにしてください。
  2. 押した時に PLAY ステートに自動的に切り替わるように、キーイベントやマウス/タップイベントも登録します。
  3. destroy では、キーやポインターのイベントをアンバインドします。

そして、もちろん、一番最後には、新しいオブジェクトを作成し、それを対応するステート(ここでは MENU)に関連づけることをエンジンに示唆します。また、me.state の transition 関数を使って、ステート変更の間でフェードするエフェクトを追加するようにエンジンに伝えています。

最後に、loaded 関数の最後で PLAY ステートに切り替える代わりに、 MENU ステートに切り替えています:

/*
 * 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 で質問してください。

すべては楽しさのためであることを忘れずに。だから楽しんでください!