2018年4月25日水曜日

Phaser3 のチュートリアルを Sinatra 上で動かしてみた

概要

Phaser3 は HTML + Javascript でゲームを開発することができるフレームワークです
公式のチュートリアルがあったのでやってみました
また今回は Sinatra 上で動かしてみました
成果物は以下の通りです

$ tree . -I vendor
.
├── Gemfile
├── Gemfile.lock
├── app.rb
├── config.ru
├── public
│   ├── assets
│   │   ├── bomb.png
│   │   ├── dude.png
│   │   ├── platform.png
│   │   ├── sky.png
│   │   └── star.png
│   └── js
│       └── game1.js
└── views
    └── index.erb

4 directories, 11 files

環境

  • macOS 10.13.2
  • Ruby 2.4.1p111
  • sinatra 2.0.1
  • Phaser 3.4

準備

  • bundle init
  • vim Gemfile
gem "sinatra"
  • bundle install --path vendor
  • mkdir -p public/assets
  • mkdir -p public/js
  • mkdir views

Sinatra アプリの作成

あとで作成するテンプレートファイルを呼び出すだけです

  • vim app.rb
require 'sinatra/base'

class GameApp < Sinatra::Base
  get '/' do
    erb :index
  end
end
  • vim config.ru
require './app.rb'
run GameApp

リソースファイルの取得

ゲームに必要な画像などのリソースファイルを取得します

  • cd public
  • wget 'http://phaser.io/tutorials/making-your-first-phaser-3-game/phaser3-tutorial-src.zip'
  • unzip phaser3-tutorial-src.zip

とりあえずほしいのは assets ディレクトリ配下にある画像ファイルです
他にも html ファイルや js ファイルがありますが今回は使いません

テンプレートファイルの作成

ここでゲーム用の JavaScript を参照することでゲームを開始します

  • vim views/index.erb
<!DOCTYPE html>
<html>
<head>
  <script src="http://cdn.jsdelivr.net/npm/phaser@3.4.0/dist/phaser.min.js"></script>
  <script src="../js/game1.js"></script>
</head>
<body>
</body>
</html>

ゲーム用の JavaScript ファイル作成

ここが本題です
Phaser を使ってゲームを作成しています

  • vim public/js/game1.js
var score = 0;
var scoreText;

var config = {
  type: Phaser.AUTO,
  width: 800,
  height: 600,
  physics: {
    default: 'arcade',
    arcade: {
      gravity: { y: 300 },
      debug: false
    }
  },
  scene: {
    preload: preload,
    create: create,
    update: update,
  }
};

var game = new Phaser.Game(config);

function preload () {
  this.load.image('sky', '../assets/sky.png');
  this.load.image('star', '../assets/star.png');
  this.load.image('platform', '../assets/platform.png');
  this.load.image('bomb', '../assets/bomb.png');
  this.load.spritesheet(
    'dude',
    '../assets/dude.png',
    { frameWidth: 32, frameHeight: 48 }
  );
}

function create () {
  this.add.image(400, 300, 'sky');
  platforms = this.physics.add.staticGroup();
  platforms.create(400, 568, 'platform').setScale(2).refreshBody();
  platforms.create(600, 400, 'platform');
  platforms.create(50, 250, 'platform');
  platforms.create(750, 220, 'platform');

  dude = this.physics.add.sprite(100, 450, 'dude');
  dude.setBounce(0.2);
  dude.setCollideWorldBounds(true);
  this.anims.create({
    key: 'left',
    frames: this.anims.generateFrameNumbers('dude', { start: 0, end: 3 }),
    frameRate: 10,
    repeat: -1
  });
  this.anims.create({
    key: 'turn',
    frames:[ { key: 'dude', frame: 4 } ],
    frameRate: 20
  });
  this.anims.create({
    key: 'right',
    frames: this.anims.generateFrameNumbers('dude', { start: 5, end: 8 }),
    frameRate: 10,
    repeat: -1
  });
  dude.body.setGravityY(300)
  this.physics.add.collider(dude, platforms);

  cursors = this.input.keyboard.createCursorKeys();

  stars = this.physics.add.group({
    key: 'star',
    repeat: 11,
    setXY: { x: 12, y: 0, stepX: 70 }
  });
  stars.children.iterate(function (child) {
    child.setBounceY(Phaser.Math.FloatBetween(0.4, 0.8));
  });
  this.physics.add.collider(stars, platforms);
  this.physics.add.overlap(dude, stars, collectStar, null, this);

  scoreText = this.add.text(16, 16, 'score: 0', { fontSize: '32px', fill: '#000'});

  bombs = this.physics.add.group();
  this.physics.add.collider(bombs, platforms);
  this.physics.add.overlap(dude, bombs, hitBomb, null, this);
}

function collectStar(dude, star) {
  star.disableBody(true, true);
  score += 10;
  scoreText.setText('score: ' + score);
  if (stars.countActive(true) == 0) {
    stars.children.iterate(function (child) {
      child.enableBody(true, child.x, 0, true, true);
    });
    var x = (dude.x < 400) ? Phaser.Math.Between(400, 800) : Phaser.Math.Between(0, 400);
    var bomb = bombs.create(x, 16, 'bomb');
    bomb.setBounce(1);
    bomb.setCollideWorldBounds(true);
    bomb.setVelocity(Phaser.Math.Between(-200, 200), 20);
    bomb.allowGravity = false;
  }
}

function hitBomb(dude, bomb) {
  this.physics.pause();
  dude.setTint('0xff0000');
  dude.anims.play('turn');
  gameOver = true;
}

function update() {
  if (cursors.left.isDown) {
    dude.setVelocityX(-160);
    dude.anims.play('left', true);
  } else if (cursors.right.isDown) {
    dude.setVelocityX(160);
    dude.anims.play('right', true);
  } else {
    dude.setVelocityX(0);
    dude.anims.play('turn');
  }
  if (cursors.up.isDown && dude.body.touching.down) {
    val = -530;
    console.log(val);
    dude.setVelocityY(val);
  }
}

上記は完成版になります

少しだけ説明

詳細は後述にある参考サイトの URL を確認してください

流れとしては config を設定し、その configを元にゲームを作成 new Phaser.Game(config) するだけです
実装しなければならないのは config.scene で定義した preload, create, update のそれぞれの関数です

名前の通りですがそれぞれには役割があり preload はゲーム開始前に行うことを記載し、create はゲーム開始時に作成するべきキャラクターや背景、敵などを作成し、update はイベントなどフレームが更新されたときに随時呼ばれるような処理を記載します
今回だと preload はリソースファイルの読み込み、create は各種スプライトや背景などの作成、update はカーソルが押されたときの処理を実装しています

Phaser には物理エンジンがあります
物理エンジンを使ったスプライトを作成したい場合には this.physics.add を使用します
物理エンジンには static と dynamic の 2 種類があり static は重力の影響を受けません
dynamic は重量の影響を受けるので自然落下します

物理エンジンを使ったスプライト同士は当たり判定を行うことができます
当たり判定を行う場合には this.physics.add.collider を使います
また衝突時に何かしら処理を行いたい場合には this.physics.add.overlap(sprite1, sprite2, callback, null, this) を使います
3 つ目の引数でコールバックとして呼び出す関数名を指定することで sprite1 と sprite2 が衝突した際にその関数が呼ばれるようになります

アニメーションの作成は this.anims.create を使います
今回やプライヤーの方向展開をするアニメーションを定義するのに使っています
定義したアニメーションはグローバルに使うことができるようになります

物理エンジンを動的に割り当てたい場合は disableBody/enableBody を使います

動作確認

  • bundle exec rackup config.ru

localhost:9292 にアクセスするとゲームが開始されます
星をすべて取ると爆弾が生成され爆弾に当たると終了です
phaser_demo.gif

とりあえずこんな感じで動作するようになればチュートリアルは完了です

最後に

ブラウザゲームが作成できる Phaser3 を試してみました
Sinatra で動かした理由としては CORS のエラー対策で公式でも説明があるのですがファイルをブラウザで開くだけだと Uncaught DOMException: Failed to execute 'texImage2D' on 'WebGLRenderingContext': The image element contains cross-origin data, and may not be loaded. のエラーが発生します
なので必ず何かしらの Web サーバ上で動作させる必要があり今回は Sinatra を使いました
nginx だけでも動作するので面倒な場合は nginx のドキュメントルートに配置しても OK です

Sinatra で動かしておけばサーバ側のコードが必要になったときもそのまま使えるので良いかなと思います
本当にブラウザだけで簡潔するゲームなのであれば不要だとは思いますが

参考サイト

0 件のコメント:

コメントを投稿