このドキュメントは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は非圧縮のタイルマップのみをサポートしているため、設定が正しいことを確認してください。よりサイズの小さなファイルを生成するためBase64エンコーディングをお勧めしますが実際にはあなた次第です。
次に、新しいタイルセット
ボタンを押下しタイルセットを追加しましょう。Tilesではタイルセットの間隔とマージンを0
に設定してください。
より美しくするために2つのレイヤーを作成します。1つは背景レイヤー、もう1つは前景レイヤーです。
あなたの想像力を自由に使ってあなたがやりたいことを何でもしてください。今回は論理的に背景
と前面
という名前を付けましたが、実際には任意の名前でよいです。
(レベルデザインが)完了後にレベルがどのように見えるかを示しています:
最後にカラーピッカーツールを使用してレベルの背景色を定義し好きな色を指定します。
最後に、このマップを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
ファイルを見てみましょう。
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
ファイルに追記しデータマニフェストにロードする必要があります。
{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
メソッドを編集します(このメソッドは状態変化時に呼び出されます)
レベルロード関数を使用し、デフォルトのレベル名から以前にプリロードされたレベルを表示します。
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
グローバルオブジェクトの定義を実装します。
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
)を使用してキャラクターをアニメーション化します。
基本的な歩行および立ちアニメーションを定義します。もちろん、同じエンティティに対してより複雑なアニメーションを定義することも可能です。(ジャンプ、しゃがみ、傷を負ったとき、など)
しかし、まずは物事をシンプルに進めましょう。
エンティティを作成します。js/renderables/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で名前を指定し残りをコンストラクターで直接管理する方が簡単です)
注:必要な数のプロパティを自由に追加することもできます。これらはすべて、コンストラクターに渡される設定オブジェクトで使用できます。
オブジェクトが作成されたらエンティティをレベルに配置します。
次の例のように、実際のスプライトサイズと一致するようにTiled
でオブジェクトの長方形のサイズも変更していることを確認してください。
衝突レイヤーの定義
ほぼ完了です!最後のステップは衝突レイヤーを定義することです。
これには「衝突」という名前の新しいオブジェクトレイヤーを作成し、それにいくつかの基本的な形状を追加する必要があります。必要なのはそれだけです!
新しいオブジェクトグループレイヤーを追加します。このレイヤーの名前にはエンジンが衝突オブジェクトレイヤーとして認識できるように、キーワードとして「''collision''」が含まれている必要があります。
追加されたレイヤーを選択し、オブジェクトツールバーを使用して任意の形状を追加することによりレベル衝突マップを「描画」します。
melonJSは分離軸定理アルゴリズムを使用して衝突検出を実装していることに注意してください。衝突に使用されるすべてのポリゴンは、時計回りに巻かれているすべての頂点で凸状である必要があります。
内部の2点を結ぶすべての線分が、ポリゴンのどのエッジとも交差しない場合にポリゴンは凸状になります(すべての頂点の角度が180度未満であることを意味します)
ポリゴンの「曲がりくねった」は、頂点(ポイント)が右に曲がっていると宣言されている場合は時計回りです。(上の画像はCOUNTERCLOCKWISE線を示しています。)
また、環境の境界を指定するために複雑な形状が必要な場合は、個別の線分を使用することをお勧めします。これらの線は、たとえば、オブジェクトの特定の側面のみを衝突可能にする必要があるプラットフォームまたは壁の要素を定義するときにも使用できます。
やってみよう
すべてを保存しindex.htmlを再度開くと、次のように表示されます。
ディスプレイが自動的にプレーヤーを追跡し、環境をスクロールしていることに気付くでしょう。
最後に一つだけ。
ブジェクトを作成するとTiled
で定義したオブジェクトのサイズに基づいて、オブジェクト間の衝突を管理するためのデフォルトの衝突形状が自動的に作成されます。
デバッグの目的で、ブラウザのURLバーのURLに#debug
を追加することで、デバッグパネルを有効にできます。
ゲームをリロードして「hitbox」を有効にすると、次のように表示されます。
衝突ボックスは、オブジェクトのサイズを変更して上記の例に一致させることによりタイルから調整できます。
注:デバッグパネルを使用する場合、スプライトの境界線は緑色で描画され、定義された衝突形状は赤色で描画されます。長方形の衝突形状以外のものを使用する場合は、オレンジ色も表示されます。定義されたすべての衝突形状を含む最小の長方形に対応するボックス(エンティティ本体の境界ボックスとも呼ばれます)
Part4: 背景のスクロール
これはとても簡単です。
すべてがTiled
を介して行われるため、コードを追加する必要はありません。
Part1の最後に追加した背景色を削除します(そのためには、TMXファイルをテキスト編集しbackgroundcolor
プロパティを削除する必要があります。)
背景はスクロールレイヤーで塗りつぶされるため、特定の色で表示をクリアする必要はありません(それはいくつかの貴重なフレームを節約します)
次に、次の2つの背景を使用します。
最初の背景レイヤーにはsrc/data/img/background.png
、2番目の背景レイヤーにはsrc/data/img/clouds.png
とします。
Tiled
を起動し、イメージレイヤーを2つ追加します。追加したレイヤーには好きな名前をつけ、レイヤーの順序は正しく調整します。
次に、レイヤーを右クリックしてプロパティを定義し次のプロパティを設定します。
最初のレイヤーのプロパティでは、ブラウズボタンを押下し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
のスプライトシートを使用して、収集可能なコインを追加します(後でスコアに追加するために使用します)
そして、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
クラスのcollisionType
がPLAYER_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
で定義するので、今のところコンストラクタに他に何も追加する必要はありません。
敵の場合は少々長くなります。今回は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.js
のpreload
メソッド)に追加します。
各クラスを定義している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
要素をシミュレートしましょう。
「プラットフォーム」として機能させたい特定の衝突形状は、Tiled
でtype
プロパティを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ファイルタイプはtype
をbinary
に設定する必要があることに注意してください。
以前に使用したボイラープレートには、ゲームのベースとして使用する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(もしくは、ゲーム音楽)
オーディオを最初に初期化した方法を振り返ると、mp3
やogg
パラメーターを初期化関数に渡したことがわかります。これは、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);
}
ジャンプ時の効果音
mainPlayer
のupdate
メソッドでは、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で質問してください。
これはすべて楽しみのためであることを決して忘れないでください、楽しんでください!