Pheser3 エンドレスランナーチュートリアルを読む
Pheser3のチュートリアルのどれを試そうかとみていたらエンドレスランナーというのがあったので読んでみた。
Stepを5段階で説明してくれている。
- Step1: playerとplatformが白い四角で表現。platformはリサイクルしている。
- Step2: playerアニメーション
- Step3: コインが登場。platformも画像に。
- Step4: 背景に山。遠いので移動速度が他より遅く。
- Step5: 炎登場。当たり判定を表示より小さく。
ゲーム内容
- タッチあるいはクリックによりジャンプする。
- ジャンプはジャンプ中にもう一度だけジャンプできる。
- 炎に当たると焼け死ぬ。
- コインを取れる。
- 画面下まで落ちると終わり。ゲームが自動的に開始する。
次のページで遊べる。
コード
game.jsのコードにコメントを追加した。
// ゲームインスタンス
let game;
// ゲームオプション
// global game options
let gameOptions = {
// プラットフォームの速度範囲
// platform speed range, in pixels per second
platformSpeedRange: [300, 300],
// 山の速度
// mountain speed, in pixels per second
mountainSpeed: 80,
// プラットフォーム間距離の範囲(プラットフォームの右端と画面の右端の距離)
// spawn range, how far should be the rightmost platform from the right edge
// before next platform spawns, in pixels
spawnRange: [80, 300],
// プラットフォームの横幅範囲
// platform width range, in pixels
platformSizeRange: [90, 300],
// プラットフォームの高さ位置範囲
// a height range between rightmost platform and next platform to be spawned
platformHeightRange: [-5, 5],
// プラットフォーム高さ位置単位
// a scale to be multiplied by platformHeightRange
platformHeighScale: 20,
// プラットフォームの位置範囲
// platform max and min height, as screen height ratio
platformVerticalLimit: [0.4, 0.8],
// 重力
// player gravity
playerGravity: 900,
// ジャンプ力
// player jump force
jumpForce: 400,
// プレイヤーの位置
// player starting X position
playerStartPosition: 200,
// 連続ジャンプ可能数
// consecutive jumps allowed
jumps: 2,
// コインの出現率
// % of probability a coin appears on the platform
coinPercent: 25,
// 炎の出現率
// % of probability a fire appears on the platform
firePercent: 25
}
// ロード時の処理
window.onload = function() {
// ゲーム設定
// object containing configuration options
let gameConfig = {
type: Phaser.AUTO, // 表示方法
width: 1334, // 幅
height: 750, // 高さ
scene: [preloadGame, playGame], // シーン
backgroundColor: 0x0c88c7, // 背景色
// 物理設定
// physics settings
physics: {
default: "arcade" // 標準
}
}
// ゲームインスタンス生成
game = new Phaser.Game(gameConfig);
// ウィンドウにフォーカス
window.focus();
// ウィンドウサイズに合わせてキャンバスをリサイズ
resize();
// リサイズイベントにresizeハンドラ登録
window.addEventListener("resize", resize, false);
}
// 画像のロード用シーン
// preloadGame scene
class preloadGame extends Phaser.Scene{
// コンストラクタ
constructor(){
super("PreloadGame");
}
// ロード
preload(){
// プラットフォーム画像
this.load.image("platform", "platform.png");
// プレイヤースプレッドシート
// player is a sprite sheet made by 24x48 pixels
this.load.spritesheet("player", "player.png", {
frameWidth: 24,
frameHeight: 48
});
// コインスプレッドシート
// the coin is a sprite sheet made by 20x20 pixels
this.load.spritesheet("coin", "coin.png", {
frameWidth: 20,
frameHeight: 20
});
// 炎スプレッドシート
// the firecamp is a sprite sheet made by 32x58 pixels
this.load.spritesheet("fire", "fire.png", {
frameWidth: 40,
frameHeight: 70
});
// 山スプレッドシート
// mountains are a sprite sheet made by 512x512 pixels
this.load.spritesheet("mountain", "mountain.png", {
frameWidth: 512,
frameHeight: 512
});
}
// 初期化
create(){
// プレイヤーが走る
// setting player animation
this.anims.create({
key: "run",
frames: this.anims.generateFrameNumbers("player", {
start: 0,
end: 1
}),
frameRate: 8,
repeat: -1
});
// コインが回転する
// setting coin animation
this.anims.create({
key: "rotate",
frames: this.anims.generateFrameNumbers("coin", {
start: 0,
end: 5
}),
frameRate: 15,
yoyo: true,
repeat: -1
});
// 炎が燃える
// setting fire animation
this.anims.create({
key: "burn",
frames: this.anims.generateFrameNumbers("fire", {
start: 0,
end: 3
}),
frameRate: 15,
repeat: -1
});
// メインのシーンへ
this.scene.start("PlayGame");
}
}
// ゲームのメインシーン
// playGame scene
class playGame extends Phaser.Scene{
// コンストラクタ
constructor(){
super("PlayGame");
}
// 初期化
create(){
// 山グループ
// group with all active mountains.
this.mountainGroup = this.add.group();
// プラットフォームグループ
// group with all active platforms.
this.platformGroup = this.add.group({
// 削除したらリサイクル用プールへ
// once a platform is removed, it's added to the pool
removeCallback: function(platform){
platform.scene.platformPool.add(platform)
}
});
// プラットフォームのリサイクルプール
// platform pool
this.platformPool = this.add.group({
// 削除したらプラットフォームグループへ
// once a platform is removed from the pool, it's added to the active platforms group
removeCallback: function(platform){
platform.scene.platformGroup.add(platform)
}
});
// コイングループ
// group with all active coins.
this.coinGroup = this.add.group({
// 削除したらリサイクルプールへ
// once a coin is removed, it's added to the pool
removeCallback: function(coin){
coin.scene.coinPool.add(coin)
}
});
// コインのリサイクルプール
// coin pool
this.coinPool = this.add.group({
// 削除したらコイングループへ
// once a coin is removed from the pool, it's added to the active coins group
removeCallback: function(coin){
coin.scene.coinGroup.add(coin)
}
});
// 炎グループ
// group with all active firecamps.
this.fireGroup = this.add.group({
// 削除したらリサイクルプールへ
// once a firecamp is removed, it's added to the pool
removeCallback: function(fire){
fire.scene.firePool.add(fire)
}
});
// 炎のリサイクルプール
// fire pool
this.firePool = this.add.group({
// 削除したら炎グループへ
// once a fire is removed from the pool, it's added to the active fire group
removeCallback: function(fire){
fire.scene.fireGroup.add(fire)
}
});
// 山々の追加
// adding a mountain
this.addMountains()
// プラットフォームの数初期化
// keeping track of added platforms
this.addedPlatforms = 0;
// 連続ジャンプ数初期化
// number of consecutive jumps made by the player so far
this.playerJumps = 0;
// プラットフォームの追加(横幅、中心、高さ)
// adding a platform to the game, the arguments are platform width, x position and y position
this.addPlatform(game.config.width, game.config.width / 2, game.config.height * gameOptions.platformVerticalLimit[1]);
// プレイヤーの生成、重力設定
// adding the player;
this.player = this.physics.add.sprite(gameOptions.playerStartPosition, game.config.height * 0.7, "player");
this.player.setGravityY(gameOptions.playerGravity);
this.player.setDepth(2);
// Depth 山を手前に表示しない
// 0 or 1 山
// 2 プレイヤー、プラットフォーム、コイン、炎
// 焼け死んだフラグ
// the player is not dying
this.dying = false;
// プレイヤーとプラットフォームの衝突判定をする
// setting collisions between the player and the platform group
this.platformCollider = this.physics.add.collider(this.player, this.platformGroup, function(){
// 着地したら走る
// play "run" animation if the player is on a platform
if(!this.player.anims.isPlaying){
this.player.anims.play("run");
}
}, null, this);
// プレイヤーとコインの重なり判定をする
// setting collisions between the player and the coin group
this.physics.add.overlap(this.player, this.coinGroup, function(player, coin){
// アニメーション
this.tweens.add({
targets: coin, // コインを
y: coin.y - 100, // ちょっと上へ移動
alpha: 0, // 非表示
duration: 800, // 時間
ease: "Cubic.easeOut", // 動きの種類
callbackScope: this,
onComplete: function(){ // リサイクルへ
this.coinGroup.killAndHide(coin);
this.coinGroup.remove(coin);
}
});
}, null, this);
// プレイヤーと炎の重なり判定をする
// setting collisions between the player and the fire group
this.physics.add.overlap(this.player, this.fireGroup, function(player, fire){
this.dying = true; // 焼け死ぬ
this.player.anims.stop(); // アニメ停止
this.player.setFrame(2); // 黒こげ画像のフレームへ
this.player.body.setVelocityY(-200); // 上に
this.physics.world.removeCollider(this.platformCollider); // プレイヤーの衝突判定削除
}, null, this);
// クリック・タッチはjumpハンドラへ
// checking for input
this.input.on("pointerdown", this.jump, this);
}
// 山々の追加
// adding mountains
addMountains(){
// 一番右の山の位置
let rightmostMountain = this.getRightmostMountain();
// 一番右の山の位置が画面横幅2倍より左であれば山を追加する
if(rightmostMountain < game.config.width * 2){
// 山画像を一番右の山から右へ100~350離れて下に配置し0~100下に下げる
let mountain = this.physics.add.sprite(rightmostMountain + Phaser.Math.Between(100, 350), game.config.height + Phaser.Math.Between(0, 100), "mountain");
mountain.setOrigin(0.5, 1); // x中央、y下から
mountain.body.setVelocityX(gameOptions.mountainSpeed * -1)
this.mountainGroup.add(mountain);
if(Phaser.Math.Between(0, 1)){
mountain.setDepth(1);
}
mountain.setFrame(Phaser.Math.Between(0, 3)); // 4種類のどれか
this.addMountains(); // 再帰呼び出しし右に追加する
}
}
// 一番右の山を取得する
// getting rightmost mountain x position
getRightmostMountain(){
let rightmostMountain = -200;
this.mountainGroup.getChildren().forEach(function(mountain){
rightmostMountain = Math.max(rightmostMountain, mountain.x);
})
return rightmostMountain;
}
// プラットフォームを追加する
// the core of the script: platform are added from the pool or created on the fly
addPlatform(platformWidth, posX, posY){
this.addedPlatforms ++;
let platform;
if(this.platformPool.getLength()){
// リサイクルプールにあればそれを使う
platform = this.platformPool.getFirst();
platform.x = posX;
platform.y = posY;
platform.active = true;
platform.visible = true;
this.platformPool.remove(platform);
let newRatio = platformWidth / platform.displayWidth;
platform.displayWidth = platformWidth;
platform.tileScaleX = 1 / platform.scaleX;
}
else{
// 新しいタイルスプライトを追加する
platform = this.add.tileSprite(posX, posY, platformWidth, 32, "platform");
this.physics.add.existing(platform);
platform.body.setImmovable(true);
platform.body.setVelocityX(Phaser.Math.Between(gameOptions.platformSpeedRange[0], gameOptions.platformSpeedRange[1]) * -1);
platform.setDepth(2);
this.platformGroup.add(platform);
}
// 次のプラットフォームへの距離を決める
this.nextPlatformDistance = Phaser.Math.Between(gameOptions.spawnRange[0], gameOptions.spawnRange[1]);
// はじめのプラットフォームでなければコインや炎を追加を確率で決める
// if this is not the starting platform...
if(this.addedPlatforms > 1){
// コインの追加
// is there a coin over the platform?
if(Phaser.Math.Between(1, 100) <= gameOptions.coinPercent){
if(this.coinPool.getLength()){
// リサイクルプールにあればそれを使う
let coin = this.coinPool.getFirst();
coin.x = posX;
coin.y = posY - 96;
coin.alpha = 1;
coin.active = true;
coin.visible = true;
this.coinPool.remove(coin);
}
else{
// 新しくコインを追加
let coin = this.physics.add.sprite(posX, posY - 96, "coin");
coin.setImmovable(true);
coin.setVelocityX(platform.body.velocity.x);
coin.anims.play("rotate");
coin.setDepth(2);
this.coinGroup.add(coin);
}
}
// 炎
// is there a fire over the platform?
if(Phaser.Math.Between(1, 100) <= gameOptions.firePercent){
if(this.firePool.getLength()){
// リサイクルプールにあればそれを使う
let fire = this.firePool.getFirst();
fire.x = posX - platformWidth / 2 + Phaser.Math.Between(1, platformWidth);
fire.y = posY - 46;
fire.alpha = 1;
fire.active = true;
fire.visible = true;
this.firePool.remove(fire);
console.log("fire", fire.depth);
}
else{
// 新しく炎を追加
let fire = this.physics.add.sprite(posX - platformWidth / 2 + Phaser.Math.Between(1, platformWidth), posY - 46, "fire");
fire.setImmovable(true);
fire.setVelocityX(platform.body.velocity.x);
//fire.setSize(8, 2, true); // 物理ボディを設定(表示サイズより当たり判定を小さくする)
fire.setBodySize(2, 2, true); // 物理ボディを設定(表示サイズより当たり判定を小さくする)
fire.anims.play("burn");
fire.setDepth(2);
this.fireGroup.add(fire);
}
}
}
}
// ジャンプ
// the player jumps when on the ground, or once in the air as long as there are jumps left and the first jump was on the ground
// and obviously if the player is not dying
jump(){
// 焼け死んでなく、地面に接しているか連続ジャンプ可能数に達していなければジャンプする
if((!this.dying) && (this.player.body.touching.down || (this.playerJumps > 0 && this.playerJumps < gameOptions.jumps))){
if(this.player.body.touching.down){
this.playerJumps = 0; // 着地していれば連続ジャンプ数クリア
}
this.player.setVelocityY(gameOptions.jumpForce * -1); // 上に向かう
this.playerJumps ++;
// アニメは停止
// stops animation
this.player.anims.stop();
}
}
update(){
// 画面境界まで落ちたらメインシーンへ
// game over
if(this.player.y > game.config.height){
this.scene.start("PlayGame");
}
// プレイヤーのx位置はプラットフォームと一緒に移動しない
this.player.x = gameOptions.playerStartPosition;
// プラットフォームのリサイクル
// recycling platforms
let minDistance = game.config.width;
let rightmostPlatformHeight = 0;
this.platformGroup.getChildren().forEach(function(platform){
// プラットフォームの右端から画面右境界までの距離
let platformDistance = game.config.width - platform.x - platform.displayWidth / 2;
if(platformDistance < minDistance){
minDistance = platformDistance; // 最小の距離
rightmostPlatformHeight = platform.y; // そのy座標
}
if(platform.x < - platform.displayWidth / 2){ // 画面から全部出たら削除する
this.platformGroup.killAndHide(platform);
this.platformGroup.remove(platform);
}
}, this);
// コインのリサイクル
// recycling coins
this.coinGroup.getChildren().forEach(function(coin){
if(coin.x < - coin.displayWidth / 2){ // 画面から出たら削除
this.coinGroup.killAndHide(coin);
this.coinGroup.remove(coin);
}
}, this);
// 炎のリサイクル
// recycling fire
this.fireGroup.getChildren().forEach(function(fire){
if(fire.x < - fire.displayWidth / 2){ // 画面から出たら削除
this.fireGroup.killAndHide(fire);
this.fireGroup.remove(fire);
}
}, this);
// 山のリサイクル
// recycling mountains
this.mountainGroup.getChildren().forEach(function(mountain){
if(mountain.x < - mountain.displayWidth){ // 画面から出たら
let rightmostMountain = this.getRightmostMountain();
mountain.x = rightmostMountain + Phaser.Math.Between(100, 350); // 右端の山の右側に配置
mountain.y = game.config.height + Phaser.Math.Between(0, 100);
mountain.setFrame(Phaser.Math.Between(0, 3)); // 4種類の山から選ぶ
if(Phaser.Math.Between(0, 1)){
mountain.setDepth(1);
}
console.log("mountainGroup", mountain.depth);
}
}, this);
// 次のプラットフォーム追加
// adding new platforms
if(minDistance > this.nextPlatformDistance){
let nextPlatformWidth = Phaser.Math.Between(gameOptions.platformSizeRange[0], gameOptions.platformSizeRange[1]);
let platformRandomHeight = gameOptions.platformHeighScale * Phaser.Math.Between(gameOptions.platformHeightRange[0], gameOptions.platformHeightRange[1]);
let nextPlatformGap = rightmostPlatformHeight + platformRandomHeight;
let minPlatformHeight = game.config.height * gameOptions.platformVerticalLimit[0];
let maxPlatformHeight = game.config.height * gameOptions.platformVerticalLimit[1];
let nextPlatformHeight = Phaser.Math.Clamp(nextPlatformGap, minPlatformHeight, maxPlatformHeight);
this.addPlatform(nextPlatformWidth, game.config.width + nextPlatformWidth / 2, nextPlatformHeight);
}
}
};
// ウィンドウのリサイズ時の処理
function resize(){
// ウィンドウの縦横比に合わせてキャンバスサイズを変更する
// ゲームの縦横比より横長なら横を短く、縦長なら縦を短く
let canvas = document.querySelector("canvas");
let windowWidth = window.innerWidth;
let windowHeight = window.innerHeight;
let windowRatio = windowWidth / windowHeight;
let gameRatio = game.config.width / game.config.height;
if(windowRatio < gameRatio){
canvas.style.width = windowWidth + "px";
canvas.style.height = (windowWidth / gameRatio) + "px";
}
else{
canvas.style.width = (windowHeight * gameRatio) + "px";
canvas.style.height = windowHeight + "px";
}
}