phina.jsで音ゲーを作ってみる【前編】

## はじめに
今年こういうものを公開しました。

**[PxB Map Editor](http://pentamania.hatenablog.com/entry/2017/03/16/155547)**

こちらは[某社初の本格的音楽ゲーム](http://www.capcom.co.jp/smartphone/crossbeats/)が元ネタです。

厳密にはゲームでは無いですが、コア部分にてphina.jsを使用しており、作った譜面を実際にプレイすることも可能です。

phina.jsにはマルチタップ機能、サウンド再生機能があるのでこういったタッチ系音ゲーがそこそこラクに作れます。
というわけで、簡単に以下のようなスクールなアイドルがフェスティバルしてそうな雰囲気の音ゲーも作ってみました。

![screenshot.gif](https://qiita-image-store.s3.amazonaws.com/0/81823/1d917655-2e03-3277-5f50-1e39a6657b1b.gif)

**[こちらからプレイできます（iOS端末でのプレイがおすすめ）](https://pentamania.github.io/sukuphina/index.html)**

**[リポジトリはこちら](https://github.com/pentamania/sukuphina)**

本記事ではこちらのゲームの作り方のポイントだけ、かいつまんで説明していこうと思います。
上のリポジトリのソースコードを見ながら読むとわかりやすいのではないかと思います。

## 前提：必要なもの
#### ローカルサーバー環境
xampp等。CORS対策に必要です。
個人的にはnode.js(npm)がインストール済みなら[BrowserSync](https://browsersync.io/)がおすすめです。

#### webAudioサポート環境＆対策
webAudioAPIをサポートしていないブラウザ（IE等）ではプレイできませんので新しめのブラウザを用意しましょう。
モバイル環境についてはまず大丈夫なはずです。

ちなみに未サポートブラウザに対しては以下のような感じで対策できます。

```js
if (!phina.util.Support.webAudio) {
  alert('webAudioに対応していません。最新のブラウザを使用して下さい！');
}
```

### モバイルサウンド制限のアンロック
まずtitlescene.jsを見ると、何か怪しげなことをしている箇所があると思います。

```titlescene.js
this.on('enter', function() {
  var event = "touchstart";
  var dom = this.app.domElement;
  dom.addEventListener(event, (function() {
    return function f() {
      var context = phina.asset.Sound.getAudioContext();
      var buf = context.createBuffer(1, 1, 22050);
      var src = context.createBufferSource();
      src.buffer = buf;
      src.connect(context.destination);
      src.start(0);

      dom.removeEventListener(event, f, false)
    }
  }()), false);

  // シーン遷移
  this.on('pointend', function() {
    this.exit();
  });
});
```

これは、モバイル端末では最初のオーディオ再生はユーザーイベントによって行わないといけないという制約があるため、タイトル画面（厳密に言うとアプリ本体のCanvas要素）タップ時に~~各音源毎に~~無音再生を行うよう仕込んでいます。
以後は、こちらの好きなタイミングで音を鳴らせるようになります。

ちなみにphina独自のユーザーイベントとしてpointstart, pointend等がありますが、これらは厳密にはユーザーイベントではない（内部的にはsetTimeoutによる非同期処理？）ため、アンロックには使えません。

### タイマー
判定時間が命の音ゲーでは時間の管理が重要です。
本ゲームでは時間を管理する変数を２つ用意しています。

- sceneの経過時間を管理するelapseTime
- 描画や入力判定用のgameTime

両者は初期値だけ違います。

```js
phina.define('MainScene', {
  superClass: 'phina.display.DisplayScene',

  init: function(options) {
    this.superInit(options);
    // ~中略
    var AM = phina.asset.AssetManager;
    var beatmap = AM.get('json', 'beatmap').data;

    // タイマーのセット
    this.elapsedTime = 0;
    this.gameTime = 0 - MUSIC_START_DELAY + beatmap.offset;
    // ~中略
  },
    // ~中略
});

```

MUSIC_START_DELAYは後述しますが、音楽再生の遅延を行った分をあらかじめ差し引いてます。

beatmapはロード時に読み込んでおいたjson形式の譜面データで、beatmap.offsetは音楽再生位置と、実際の判定位置に合わせるための差分値です。
（音源によっては冒頭に無音時間があったりしてズレるため。サンプルの曲は0なのであまり関係ないですが）

#### 時間の経過
各フレーム毎の経過時間はapp.deltaTimeで取得することができます。
なのでupdateで毎回それを加算していけばオーケー。

```js
    // ~中略

update: function(app) {
  // ~中略

  // タイマー加算
  this.elapsedTime += app.deltaTime;
  this.gameTimer += app.deltaTime;

  // ~中略
}

```

## 音楽の遅延再生
メインシーンに移っていきなり音楽を流れてゲームが始まるのはちょっとアレなので、
数秒待機してから再生を行いたいと思います。

そこで便利なのがイベント発火を一度だけを行ってくれるoneメソッドです。

```js
// config.js
var MUSIC_START_DELAY = 2000;

// ～中略

// mainscene.js
init: function(options) {

// ～中略
  
  // イベントをセット
  this.one('musicstart', function() {
    // 非ループ再生
    SoundManager.playMusic('music', null, false);
  });
  
  // ～中略
},

update: function(app) {

  // 時間経過で発火
  if (this.has('musicstart') && this.elapsedTime > MUSIC_START_DELAY) {
    this.flare('musicstart');
  }
 
  // ～中略
}

```

これで指定したミリ秒経過で一度だけ音楽の再生を行ってくれます。

ちなみに名前と機能は似てますが、**"on"**と書き間違えないように注意しましょう。
大変なことになります。（耳が）

SoundManager.playMusicはその名の通り音楽を鳴らすためのメソッドです。
引数で鳴らす音源や、フェードインするか、ループするか等、色々指定できますが、ループせずフェード処理しない場合は以上のような感じで指定します。

## まとめ
とりあえず前準備とタイマー、音源の鳴らし方について解説してみました。
長くなりそうなので、判定やノーツの描画処理については別記事にまとめようと思います。
:point_right: [**後編へ**](https://qiita.com/pentamania/items/edb5eee1c7c7268431b2)

その他、「ここ何してんの？」と気になるところがありましたらコメントや[twitter](https://twitter.com/ptmnia)等で気軽にどうぞ。