1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

melonJS Platformer Tutorial

Last updated at Posted at 2022-04-14

このドキュメントはmelonJSのチュートリアルを元に機械翻訳をベースとした翻訳を行い、melonJS バージョン10.5系のソースコードに書き換えたドキュメントとなります。元ドキュメントとソースコードも含めて多くの部分が異なっており、翻訳とソースコードのアップグレードを並行して行ったため本文とコードの内容が異なる箇所があるかもしれません。


このチュートリアルでは、簡単なプラットフォーマーを作成します。このチュートリアルでは、主にTiledをレベルエディタとして使用して、動作するゲームの基本要素を作成することに焦点を当てます。

イントロダクション

このチュートリアルの実行では次のものが必要となります。

  • Tiled(Tiles Map Editor) ※バージョン0.9.0以降のもの
  • テンプレートプロジェクト一式
  • チュートリアル用データファイル
    このファイルは圧縮ファイルを展開後、src/dataフォルダ内に配置してください。
    内容物は以下の通りです。
    • レベルタイルセット(area01_level_tiles.png)
    • 2枚の背景画像(background.png, clouds.png)
    • 基本的なスプライトシート(プレイヤーと敵のアニメーションスプライトシート、コイン)
    • 効果音と音楽
    • タイトル画像(title_screen.png)
  • melonJSドキュメント(リファレンス)

テストとデバッグ
ファイルシステムを使用して実行した場合、問題は「クロスオリジンリクエスト」のセキュリティエラーが発生することです。
使用しているブラウザがChromeの場合は--disable-web-securityパラメータか--allow-file-access-from-filesパラメータを引数に指定してブラウザを起動してください。
これはローカルコンテンツをテストするために実行する必要があります。これを怠った場合XHRを介してアセットを読み込もうとしたときにブラウザが警告します。
この方法はお勧めしませんが、オプションが有効になっている限り、環境にセキュリティの脆弱性が発生します。

2番目の簡単なオプションは、たとえばmelonJSボイラープレートREADMEで詳しく説明されているように、ローカルWebサーバーを使用することです。

Part1: Tiledによるレベル作成

注:開始する前に、Tiledを初めて使用する場合は、ここでこの概要を読むことを強くお勧めします。
この概要では、開始方法とエディターの動作の基本について説明しています。

まずTiledを起動し、新しいマップを作成しましょう。

このチュートリアルでは640×480のキャンバスを使用します。また、タイルは32px×32pxであるため、マップサイズは少なくとも20と15を指定する必要があります。
このチュートリアルでは40×15レベルを定義するので、後で背景をスクロールして遊ぶことができます。

melonJS_TTP1_Tiled_fig1.jpg

また、melonJSは非圧縮のタイルマップのみをサポートしているため、設定が正しいことを確認してください。よりサイズの小さなファイルを生成するためBase64エンコーディングをお勧めしますが実際にはあなた次第です。

次に、新しいタイルセットボタンを押下しタイルセットを追加しましょう。Tilesではタイルセットの間隔とマージンを0に設定してください。

melonJS_TTP1_Tiled_fig2.jpg

より美しくするために2つのレイヤーを作成します。1つは背景レイヤー、もう1つは前景レイヤーです。
あなたの想像力を自由に使ってあなたがやりたいことを何でもしてください。今回は論理的に背景前面という名前を付けましたが、実際には任意の名前でよいです。

(レベルデザインが)完了後にレベルがどのように見えるかを示しています:

melonJS_TTP1_Tiled_fig3.jpg

最後にカラーピッカーツールを使用してレベルの背景色を定義し好きな色を指定します。

melonJS_TTP1_Tiled_fig4.jpg

最後に、このマップをsrc/data/mapフォルダの下にarea01.tmxというファイル名で保存します。これで最初のステップは完了です。

Part2: レベルの読み込み

まずチュートリアルアセットを定型的なsrc / dataディレクトリ構造に解凍すると、次のようになります。


src
└── data
│    ├── bgm
│    │    ├── dst-inertexponent.mp3
│    │    └── dst-inertexponent.ogg
│    ├── fnt
│    │    ├── PressStart2P.png
│    │    └── PressStart2P.fnt
|    ├── img
|    |    ├── gui
|    │    │    └──  title_screen.png
|    |    ├── map
|    │    │    └── area01_level_tiles.png
|    |    ├── sprite
|    │    │    ├── gripe_run_right.png
|    │    │    ├── spinning_coin_gold.png
|    │    │    └── wheelie_right.png
│    │    ├── background.png
│    │    └── clouds.png
|    ├── map
|    ├── sfx
│    │    ├── cling.mp3
│    │    ├── cling.ogg
│    │    ├── jump.mp3
│    │    ├── jump.ogg
│    │    ├── stomp.mp3
│    │    └── stomp.ogg
└── js
|    ├── renderables
|    └── stage
├── index.js
├── index.css
├── index.html
└── manifest.js

ボイラープレートには一連のデフォルトコードも用意されていますが、最初にindex.jsファイルを見てみましょう。

index.js
import * as me from 'melonjs/dist/melonjs.module.js';

// import TitleScreen from './js/stage/title.js';  // 後で追加
import PlayScreen from './js/stage/play.js';
import PlayerEntity from "./js/renderables/player-entity.js";
// import CoinEntity from "./js/renderables/coin-entity.js";  // 後で追加
// import EnemyEntity from "./js/renderables/enemy-entity.js"; // 後で追加

import DataManifest from './manifest.js';

/* Game namespace */
me.device.onReady(() => {

    // Initialize the video.
    if (!me.video.init(640, 480, {parent : "screen", scale : "auto"})) {
        alert("Your browser does not support HTML5 canvas.");
        return;
    }

    // initialize the debug plugin in development mode.
    import('./js/plugin/debug/debugPanel.js').then((plugin) => {
        // automatically register the debug panel
        me.utils.function.defer(me.plugin.register, this, plugin.DebugPanelPlugin, "debugPanel");
    });


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

    // allow cross-origin for image/texture loading
    me.loader.crossOrigin = "anonymous";

    // set and load all resources.
    me.loader.preload(DataManifest, function() {
        // set the user defined game stages
        // me.state.set(me.state.MENU, new TitleScreen()); // 後で追加
        me.state.set(me.state.PLAY, new PlayScreen());

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

		    // add our player entity in the entity pool
	      me.pool.register("mainPlayer", PlayerEntity);
		    // me.pool.register("CoinEntity", CoinEntity); // 後で追加
		    // me.pool.register("EnemyEntity", EnemyEntity); // 後で追加


        // enable the keyboard
        me.input.bindKey(me.input.KEY.LEFT,  "left");
        me.input.bindKey(me.input.KEY.RIGHT, "right");
        // map X, Up Arrow and Space for jump
        me.input.bindKey(me.input.KEY.X,      "jump", true);
        me.input.bindKey(me.input.KEY.UP,     "jump", true);
        me.input.bindKey(me.input.KEY.SPACE,  "jump", true);

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

これは非常に単純です。ページが読み込まれると表示と音声が初期化され、すべてのゲームリソースがプリロードされるように設定されます。
プリロードが完了したら、ゲームイベント(リセットなど)の管理に使用するPlayScreenステージと共にゲーム内での状態を定義します。

ここで行う唯一の変更点はme.video.initメソッドで指定されているビデオ解像度を変更することです。このチュートリアルでは640×480のキャンバスを使用します。
またscaleMethodパラメータをflex-widthに変更します。これはプラットフォーマーゲームにより適しているためです(利用可能なさまざまなスケーリングモードの詳細についてはme.video.initのドキュメントを参照してください。)

次に最初のステップで作成したTMXレベルとそのタイルセットの両方をアセットのリストに実際に追加するため、以下の2行をmanifest.jsファイルに追記しデータマニフェストにロードする必要があります。

manifest.js
{name: "area01_level_tiles",  type: "image", src: "data/img/map/area01_level_tiles.png"},
{name: "area01_level_tiles",  type: "tsx",   src: "data/img/map/area01_level_tiles.tsx"},
{name: "area01",              type: "tmx",   src: "data/map/area01.tmx"}

また、ここでは(TMXレベルのファイルに)tmx形式を使用していますが、ファイルサイズを小さくしレベルを高速で読み込めるようにするため、本番環境ではjson形式を使用することをお勧めします。

最後にjs/stage/play.jsファイル内のonResetEventメソッドを編集します(このメソッドは状態変化時に呼び出されます)
レベルロード関数を使用し、デフォルトのレベル名から以前にプリロードされたレベルを表示します。

play.js
import * as me from 'melonjs/dist/melonjs.module.js';
import game from '../data.js'

export default class PlayScreen extends me.Stage {
	/**
	 *  action to perform on state change
	 */
	onResetEvent() {
		// load a level
		me.level.load("area01");

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

	/**
	 *  action to perform when leaving this screen (state change)
	 */
	onDestroyEvent() {
		;
	}
}

また、スコアを保持するためにdata.jsを作成し、静的なインスタンスとしてgameグローバルオブジェクトの定義を実装します。

data.js
var game = {
    /**
     * object where to store game global scole
     */
    data : {
        // score
        score : 0
    },
};
export default game;

よくできました!!
すべてを正しく行った場合はindex.htmlを開きます。(ウェブサーバーを使用しない場合は、ブラウザがローカルファイルにアクセスできるようにする必要があることに注意してください。必要に応じてチュートリアルの冒頭にある「テストとデバッグ」を参照してください。)

やってみよう

(画像をクリックしてブラウザで実行されていることを確認してください)
次のように表示されます。

はい。
まだ特別なことは何もありませんが、それはほんの始まりにすぎません!

また、アプリケーションを640x480のディスプレイで定義したため地図の一部しか見えていないことに気づいたでしょうか。
これは正常です。melonJSでは対応するビューポートを自動的に作成します。そして、次のステップでメインプレーヤーを追加するとマップ内を移動できるようになります。

Part3: メインプレーヤーの追加

ここではプレーヤーを作成するために、デフォルトのme.Entityを拡張して新しいオブジェクトを作成します。
チュートリアルで提供している単純なスプライトシート(gripe_run_right.png)を使用してキャラクターをアニメーション化します。
基本的な歩行および立ちアニメーションを定義します。もちろん、同じエンティティに対してより複雑なアニメーションを定義することも可能です。(ジャンプ、しゃがみ、傷を負ったとき、など)
しかし、まずは物事をシンプルに進めましょう。

gripe_run_right.png

エンティティを作成します。js/renderables/player.jsを開き、以下のようにします。

player.js
import * as me from 'melonjs/dist/melonjs.module.js';

/**
 * Player Entity
 */
export default class PlayerEntity extends me.Entity {
    constructor(x, y, settings) {
        super(x, y, settings);

        // max walking & jumping speed
        this.body.setMaxVelocity(3, 15);
        this.body.setFriction(0.4, 0);

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

        // 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 Entity
     *
     * @param dt
     * @returns {any|boolean}
     */
    update(dt) {
        if (me.input.isKeyPressed('left')) {

            // flip the sprite on horizontal axis
            this.renderable.flipX(true);
            // update the default force
            this.body.force.x = -this.body.maxVel.x;
            // 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.force.x = this.body.maxVel.x;
            // change to the walking animation
            if (!this.renderable.isCurrentAnimation("walk")) {
                this.renderable.setCurrentAnimation("walk");
            }
        } else {
            this.body.force.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.force.y = -this.body.maxVel.y
            }
        } else {
            this.body.force.y = 0;
        }

        return (super.update(dt) || this.body.vel.x !== 0 || this.body.vel.y !== 0);
    }

    /**
     * Collision Handler
     *
     * @returns {boolean}
     */
    onCollision() {
        return true;
    }
}

上記のコードは非常に理解しやすいと思います。
基本的にEntityを拡張し、デフォルトのプレーヤー速度を構成し、カメラを微調整し、いくつかのキーが押されているかどうかをテストし、(プレイヤーの速度を設定することによって)プレーヤーの動きを管理します。
また、オブジェクトの最終速度(this.body.vel.xおよびthis.body.vel.y)をテストしていることに気付くかもしれません。これによりオブジェクトが実際に移動したかどうかを確認し、必要に応じてスプライトアニメーションを実行するかどうかを制御できます。

次にデフォルトのゲームですが、PlayerEntityはすでにボイラープレートで宣言されています。オブジェクトプールで新しいエンティティを実際に宣言するには、「メイン」を変更する必要があります(これは、オブジェクトをインスタンス化するためにエンジンによって使用されます)
最後に、プレイヤーの動きに使用するキーをマップします。したがって、preloadメソッドは次のようになります。

me.loader.preload(DataManifest, function() {
    me.state.set(me.state.PLAY, new PlayScreen());

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

    // enable the keyboard
    me.input.bindKey(me.input.KEY.LEFT,  "left");
    me.input.bindKey(me.input.KEY.RIGHT, "right");
    // map X, Up Arrow and Space for jump
    me.input.bindKey(me.input.KEY.X,      "jump", true);
    me.input.bindKey(me.input.KEY.UP,     "jump", true);
    me.input.bindKey(me.input.KEY.SPACE,  "jump", true);

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

これでエンティティをレベルに追加できます。

Tiledに戻り新しいオブジェクトレイヤーを追加し、最後に新しいエンティティを追加します。
新しいエンティティを作成するには「Insert Rectangle」ツールを使用してオブジェクトレイヤーに長方形を追加し、オブジェクトを右クリックして以下のプロパティを追加します。

:image|gripe_run_rightとします。(リソース名)
:framewidth|スプライトシート内の単一スプライトサイズである64を設定します。
:frameheight|単一行のスプライトシートを使用しているため、ここではこの値を定義しません。エンジンは実際の画像の高さを値として使用します。

これらの2つのパラメーターは、オブジェクトが作成されるときに設定パラメーターとして(コンストラクターに)渡されます。
これらのフィールドをTiledで指定することも、コードで直接指定することもできます(複数のオブジェクトを処理する場合は、Tiledで名前を指定し残りをコンストラクターで直接管理する方が簡単です)

注:必要な数のプロパティを自由に追加することもできます。これらはすべて、コンストラクターに渡される設定オブジェクトで使用できます。

melonJS_TTP3_Tiled_fig1.jpg

オブジェクトが作成されたらエンティティをレベルに配置します。
次の例のように、実際のスプライトサイズと一致するようにTiledでオブジェクトの長方形のサイズも変更していることを確認してください。

melonJS_TTP3_Tiled_fig2.jpg

衝突レイヤーの定義

ほぼ完了です!最後のステップは衝突レイヤーを定義することです。
これには「衝突」という名前の新しいオブジェクトレイヤーを作成し、それにいくつかの基本的な形状を追加する必要があります。必要なのはそれだけです!

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

追加されたレイヤーを選択し、オブジェクトツールバーを使用して任意の形状を追加することによりレベル衝突マップを「描画」します。

melonJSは分離軸定理アルゴリズムを使用して衝突検出を実装していることに注意してください。衝突に使用されるすべてのポリゴンは、時計回りに巻かれているすべての頂点で凸状である必要があります。
内部の2点を結ぶすべての線分が、ポリゴンのどのエッジとも交差しない場合にポリゴンは凸状になります(すべての頂点の角度が180度未満であることを意味します)

convex_polygon.png

ポリゴンの「曲がりくねった」は、頂点(ポイント)が右に曲がっていると宣言されている場合は時計回りです。(上の画像はCOUNTERCLOCKWISE線を示しています。)

また、環境の境界を指定するために複雑な形状が必要な場合は、個別の線分を使用することをお勧めします。これらの線は、たとえば、オブジェクトの特定の側面のみを衝突可能にする必要があるプラットフォームまたは壁の要素を定義するときにも使用できます。

やってみよう

すべてを保存しindex.htmlを再度開くと、次のように表示されます。

ディスプレイが自動的にプレーヤーを追跡し、環境をスクロールしていることに気付くでしょう。

最後に一つだけ。
ブジェクトを作成するとTiledで定義したオブジェクトのサイズに基づいて、オブジェクト間の衝突を管理するためのデフォルトの衝突形状が自動的に作成されます。

デバッグの目的で、ブラウザのURLバーのURLに#debugを追加することで、デバッグパネルを有効にできます。
ゲームをリロードして「hitbox」を有効にすると、次のように表示されます。

melonJS_TTP3_Play_fig1.jpg

衝突ボックスは、オブジェクトのサイズを変更して上記の例に一致させることによりタイルから調整できます。

注:デバッグパネルを使用する場合、スプライトの境界線は緑色で描画され、定義された衝突形状は赤色で描画されます。長方形の衝突形状以外のものを使用する場合は、オレンジ色も表示されます。定義されたすべての衝突形状を含む最小の長方形に対応するボックス(エンティティ本体の境界ボックスとも呼ばれます)

Part4: 背景のスクロール

これはとても簡単です。
すべてがTiledを介して行われるため、コードを追加する必要はありません。

Part1の最後に追加した背景色を削除します(そのためには、TMXファイルをテキスト編集しbackgroundcolorプロパティを削除する必要があります。)
背景はスクロールレイヤーで塗りつぶされるため、特定の色で表示をクリアする必要はありません(それはいくつかの貴重なフレームを節約します)

次に、次の2つの背景を使用します。

最初の背景レイヤーにはsrc/data/img/background.png、2番目の背景レイヤーにはsrc/data/img/clouds.pngとします。

Tiledを起動し、イメージレイヤーを2つ追加します。追加したレイヤーには好きな名前をつけ、レイヤーの順序は正しく調整します。

melonJS_TTP4_Tiled_fig1.jpg

次に、レイヤーを右クリックしてプロパティを定義し次のプロパティを設定します。
最初のレイヤーのプロパティでは、ブラウズボタンを押下しbackground.pngファイルを選択します。
同様に、2番目のレイヤーのプロパティでも、ブラウズボタンを押下しclouds.pngファイルを選択します。

最後にそれぞれのレイヤーのプロパティにraitoプロパティを追加し、最初のレイヤーには値を0.25に設定し、2番目のレイヤーには値を0.35に設定します。(raito 値が小さいほど、スクロール速度が遅くなることに注意してください。)

画像レイヤーのデフォルトの動作は、x軸とy軸の両方で自動的に繰り返されることに注意してください。これは、視差効果を作成するためにここで必要な動作です。

また、manifest.jsには忘れずにアセットが読み込まれるように、識別子の宣言を追加してください。

{ name: "clouds",             type: "image",  src: "data/img/clouds.png" },
{ name: "background",         type: "image",  src: "data/img/background.png" },

Part5: 敵とオブジェクトを追加

このパートではspinning_coin_gold.pngのスプライトシートを使用して、収集可能なコインを追加します(後でスコアに追加するために使用します)

spinning_coin_gold.png

そして、wheelie_right.pngのスプライトシートを使用して、敵を追加します。

wheelie_right.png

忘れずにmanifest.jsに使用するアセットの識別子の宣言を追加してください。

{ name: "spinning_coin_gold", type: "image",  src: "data/img/sprite/spinning_coin_gold.png" },
{ name: "gripe_run_right",    type: "image",  src: "data/img/sprite/gripe_run_right.png" },
{ name: "wheelie_right",      type: "image",  src: "data/img/sprite/wheelie_right.png" },

まずPlayerEntityクラスのcollisionTypePLAYER_OBJECTであることをmelonJSに通知する必要があります。
次にファイルに3つのエンティティが含まれるように、エクスポートを再配置する必要があります。

// player.js
import * as me from 'melonjs/dist/melonjs.module.js';
export class PlayerEntity extends me.Entity {
    constructor(x, y, settings) {
        super(x, y, settings);

        // ...

        // we need to tell the game that this is a PLAYER_OBJECT, now that there are other entities that can collide
        // with a player
        this.body.collisionType = me.collision.types.PLAYER_OBJECT;

        // ...
    }

    // ...
}
export default PlayerEntity;

コイン自体はとても簡単で、me.Collectableを拡張するだけです。
実際にはTiledで直接使用できます(ここでCoinEntityクラスを作成する必要はありません)が、後でコインが収集されたときにスコアと効果音サウンドを追加するので、この方法で直接実行しましょう。

import * as me from 'melonjs/dist/melonjs.module.js';

export class CoinEntity extends me.Collectable {
    // extending the init function is not mandatory
    // unless you need to add some extra initialization
    constructor(x, y, settings) {
        // call the parent constructor
        super(x, y , settings);

        // this item collides ONLY with PLAYER_OBJECT
        this.body.setCollisionMask(me.collision.types.PLAYER_OBJECT);
    }

    // this function is called by the engine, when
    // an object is touched by something (here collected)
    onCollision(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
    }
}
export default CoinEntity;

また、これを行う両方の方法が可能であることを明確にするために、CoinオブジェクトのプロパティをTiledで定義するので、今のところコンストラクタに他に何も追加する必要はありません。

melonJS_TTP5_Tiled_fig1.jpg

敵の場合は少々長くなります。今回はme.Entityをベースオブジェクトとして、物理的なボディを「手動で」追加します。

import * as me from 'melonjs/dist/melonjs.module.js';

export class EnemyEntity extends me.Entity  {
    /**
     *
     * @param x
     * @param y
     * @param settings
     */
    constructor(x, y, settings) {
        // save the area size as defined in Tiled
        let width = settings.width;

        // define this here instead of tiled
        settings.image = "wheelie_right";

        // 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;
        settings.shapes[0] = new me.Rect(0, 0, settings.framewidth, settings.frameheight);

        // call the parent constructor
        super(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;

        // enemies are not impacted by gravity
        this.body.gravityScale = 0;

        this.walkLeft = false;

        // body walking & flying speed
        this.body.force.set(settings.velX || 1, settings.velY || 0);
        this.body.setMaxVelocity(settings.velX || 1, settings.velY || 0);

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

        // make it "alive"
        this.alive = true;

        // set a "enemyObject" type
        this.body.collisionType = me.collision.types.ENEMY_OBJECT;
        // don't update the entities when out of the viewport
        this.alwaysUpdate = false;

        // a specific flag to recognize these enemies
        this.isMovingEnemy = true;
    }


    // manage the enemy movement
    update(dt) {
        if (this.alive)
        {
            if (this.walkLeft && this.pos.x <= this.startX)
            {
                this.walkLeft = false;
                this.body.force.x = this.body.maxVel.x;
            }
            else if (!this.walkLeft && this.pos.x >= this.endX)
            {
                this.walkLeft = true;
                this.body.force.x = -this.body.maxVel.x;
            }

            this.flipX(this.walkLeft);
        }
        else
        {
            this.body.force.x = 0;
        }

        // return true if we moved or if the renderable was updated
        return (super.update(dt) || this.body.vel.x !== 0 || this.body.vel.y !== 0);
    }

    /**
     * colision handler
     * (called when colliding with other objects)
     */
    onCollision(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, () => {
                    me.game.world.removeChild(this);
                });
            }
            return false;
        }
        // Make all other objects solid
        return true;
    }
}
export default EnemyEntity;

ここでわかるように、コンストラクタでsettings.imageプロパティとsettings.framewidthプロパティを直接指定しました。つまり、Tiledではこれらのプロパティをオブジェクトに追加する必要はありません(繰り返しになりますが、方法を決定するのはあなた次第です)
また、Tiledによって指定されたwidthプロパティを使用して、この敵が実行されるパスを指定しています。最後にonCollisionメソッドでは、何かがその上にジャンプしている場合、敵を点滅させます。

次にこれらの新しいオブジェクトをオブジェクトプール(index.jspreloadメソッド)に追加します。
各クラスを定義しているJSファイルのインポートも忘れずに行ってください。

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

そして、Tiledでレベルを編集する準備ができました。新しいオブジェクトレイヤーを作成し、オブジェクトの挿入ツールを使用して、必要な場所にコインと敵を追加します。各オブジェクトを右クリックし、それらの名前をCoinEntityまたはEnemyEntityのいずれかに設定してください。

最後になりましたが、レベルにプラットフォームを追加したので、onCollisionメソッドをイベントハンドラに変更して、以下に示すようにWORLD_SHAPEタイプのカスタム動作を追加しplatform要素をシミュレートしましょう。
「プラットフォーム」として機能させたい特定の衝突形状は、Tiledtypeプロパティをplatformに設定することで識別されることに注意してください(両方で同じ値を使用する限り、必要なものは何でも自由に使用できます)

/**
 * collision handler
 */
onCollision(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.falling) {
        // bounce (force jump)
        this.body.vel.y = -this.body.maxVel.y;
      }
      else {
        // let's flicker in case we touched an enemy
        this.renderable.flicker(750);
      }
      
    default:
      // Do not respond to other objects (e.g. coins)
      return false;
  }

  // Make the object solid
  return true;
}

やってみよう

そして、これはあなたが得るべきものです(私がレベルを少し完了したこと、プラットフォームを追加することなどに注意してください...)

コインを集めたり、敵を避けたり、ジャンプしたりしてみてください!

Part6: 基本的なHUDを追加

集めたコインのスコアを表示する時が来ました。

スコアを表示するためにビットマップフォントを使用します!
必要なビットマップとデータ情報の両方を提供していますが、必要なファイルを自分で生成するのは非常に簡単です。ここにある小さなハウツーに従ってください。

data/fntフォルダの下に、PNG(実際のテクスチャ)とFNT(フォント定義ファイル)の2つのファイルがあり、提供するフォントの例は「PressStart2P」という名前で追加する必要があります。既存のアセットリストに次の行を追加して、それらをプリロードします。

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

FNTファイルタイプはtypebinaryに設定する必要があることに注意してください。

以前に使用したボイラープレートには、ゲームのベースとして使用するHUDスケルトンがすでに含まれています。スケルトンは非常にシンプルで、次のもので構成されています。

  • me.Containerから継承したgHUDContainerオブジェクト
  • me.Renderableから継承したgame.HUD.ScoreItemという基本的なスコアオブジェクト

HUDコンテナは基本的にオブジェクトコンテナであり、永続的として定義され(レベルの変更に耐えられるように)、他のすべてのオブジェクトの上に表示されます(Z座標はInfinityに設定されます)
また、衝突しないようにします。衝突チェック中は無視してください。

スコアオブジェクトはフローティングとして定義し(HUDコンテナに追加するときに画面座標を使用するため)、今のところスコア値(game.dataで定義)をキャッシュします。

import * as me from 'melonjs/dist/melonjs.module.js';
import ScoreItem from "./score";

export class HUDContainer extends me.Container {
    constructor() {
        super();

        // 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 at the top left corner
        this.addChild(new ScoreItem(5, 5));
    }
}
export default HUDContainer;

それでは現在のスコアを表示しましょう。

ローカルフォントプロパティを作成して(以前のビットマップフォントを使用して)、指定されたScoreItemオブジェクトを完成させ、ビットマップフォントを使用してスコアを描画します。

import * as me from 'melonjs/dist/melonjs.module.js';
import game from '../data';

/**
 * Code to draw the score to the HUD
 */
export class ScoreItem extends me.Renderable {
	/**
	 *
	 * @param x
	 * @param y
	 */
	constructor(x, y) {
		super(x, y, 10, 10);

		// create the font object
		this.font = new me.BitmapText(0, 0, {font: "PressStart2P"});

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

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

	/**
	 *
	 * @returns {boolean}
	 */
	update() {
		// 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(renderer) {
		this.font.draw (renderer, game.data.score, me.game.viewport.width + this.pos.x, me.game.viewport.height + this.pos.y);
	}
}

HUDはゲームの開始時にすでに追加および削除されているため、ここでは何もする必要はありません。レベルをロードした後、ゲームの世界にHUDを追加していることにも注意してください。コンテナオブジェクトはデフォルトで自動的にZ座標を設定します(autoDepth機能を使用)。これによりHUDが残りの上に正しく表示されます。

export default class PlayScreen extends me.Stage {

	/**
	 *  action to perform on state change
	 */
	onResetEvent() {

		// load a level
		me.level.load("area01");

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

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

	}

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

最後のステップはもちろん、コインが集められたときに実際にスコアを変更することです!
次に、コインオブジェクトを変更しましょう。

onCollision() {
  // 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プロパティに値を追加して変更しオブジェクトが再度収集されないようにして、コインを削除します。

やってみよう

これで結果を確認でき、画面の右下隅にスコアが表示されます。

Part7: 音声追加

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

  • コイン衝突時の効果音
  • ジャンプ時の効果音
  • 敵を踏みつけた時の効果音
  • BGM(もしくは、ゲーム音楽)

オーディオを最初に初期化した方法を振り返ると、mp3oggパラメーターを初期化関数に渡したことがわかります。これは、2つのオーディオファイル形式(1つはmp3形式、もう1つはogg形式)を提供することを示しています。
melonJSはブラウザの機能に従い使用します。

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

また、manifest.jsには使用する音声アセットを読み込むための識別子の宣言を追加します。

{ name: "dst-inertexponent",  type: "audio", src: "data/bgm/" },
{ name: "cling",           type: "audio",  src: "data/sfx/" },
{ name: "stomp",           type: "audio",  src: "data/sfx/" },
{ name: "jump",            type: "audio",  src: "data/sfx/" },

それではゲームを変更しましょう:

コイン衝突時の効果音

獲得したポイントを管理していたCoinEntityのソースコードにあるme.audio.playメソッドに新しい呼び出しを追加し、clingオーディオリソースを使用する必要があります。それで全部です!

onCollision() {
  // 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);
}

ジャンプ時の効果音

mainPlayerupdateメソッドでは、me.audio.playメソッドへの呼び出しを追加しjumpオーディオリソースを使用します。
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;

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

敵を踏みつけた時の効果音

これも同じですが、mainPlayerの衝突ハンドラー関数でstompオーディオリソースを使用するために次のようにします。

/**
 * collision handler
 */
onCollision(response, other) {

      // ...

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

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

BGM

メインのonResetEventメソッドでは、使用するオーディオトラックを指定してme.audio.playTrackメソッドへの呼び出しを追加するだけです。

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

  // ...
},

また、ゲームを終了するときに現在のトラックを停止するようにonDestroyEventメソッドを変更する必要があります。

onDestroyEvent() {

  // ...

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

それで全部です!最終結果を確認するには、ここをクリックしてください。

Part9: タイトルスクリーン

最後にsrc/data/img/guiフォルダー内のtitle_screen.pngファイルを使用して、ゲームにタイトル画面を追加しましょう(もちろん、他の場合と同様にリソースリストに追加されます)

その上にメッセージを追加し、ユーザー入力がゲームを開始するのを待ちます!

stage/title.jsファイルを作成し、me.Stageを拡張して新しいオブジェクトを宣言しましょう。

import * as me from 'melonjs/dist/melonjs.module.js';

export default class TitleScreen extends me.Stage {

	onResetEvent() {
		// new sprite for the title screen, position at the center of the game viewport
		var backgroundImage = new me.Sprite(me.game.viewport.width / 2, me.game.viewport.height / 2, {
				image: me.loader.getImage('title_screen'),
			}
		);

		// scale to fit with the viewport size
		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);

		// 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.on(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() {
		me.input.unbindKey(me.input.KEY.ENTER);
		me.input.unbindPointer(me.input.pointer.LEFT);
		me.event.off(me.event.KEYDOWN, this.handler);
	}
}

これには何が含まれていますか?

  • onResetEventメソッドには、2つのレンダリング可能なコンポーネントを作成しそれらをゲームの世界に追加します。
    1つ目はタイトルの背景画像を表示するSpriteオブジェクトで、2つ目は「Enterキーを押す」メッセージとTweenオブジェクトに基づくスクローラーを処理します。
    注: フォントに関しては、対応するアセット(PressStart2P.png)を注意深く確認すると、大文字のみが含まれていることがわかります。テキストには大文字のみを使用するようにしてください。
  • キーイベント、またはマウス/タップイベントに登録して、押すと自動的にPLAY状態に切り替わります。
  • 破棄時に、キーイベントとポインターイベントのバインドを解除します。

そしてもちろん、最後に、新しいオブジェクトを作成したことをエンジンに示し、それを対応する状態(ここではMENU)に関連付けます。また、me.stateの遷移関数を使用して、状態の変化の間にフェード効果を追加するようにエンジンに指示しています。

最後に、ロードされた関数の最後でPLAY状態に切り替える代わりにここでMENU状態に切り替えています。

/*
 * callback when everything is loaded
 */
me.loader.preload(DataManifest, 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", Entities.PlayerEntity);
  me.pool.register("CoinEntity", Entities.CoinEntity);
  me.pool.register("EnemyEntity", Entities.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);
};

Part10: 結論

さて、このmelonJSの小さな紹介と一緒に過ごした時間を楽しんでいただければ幸いです。これで、自分でさらに進んでいく方法を探ることができます。これは、プログラミングとゲーム開発の重要な部分です。

チュートリアルの課題や部分に行き詰まった場合は問題を検索するか、discordで質問してください。
これはすべて楽しみのためであることを決して忘れないでください、楽しんでください!

1
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?