🎮

Phaser3 + TypeScript + Spine で 2D 横スクロールゲームのサンプルを作ってみる

2024/01/04に公開

※本記事は個人の備忘録です。
完全な解決に至らずに妥協している点もありますので、ご了承ください。

本記事を書くにあたり、コードを書いて横スクロールで移動する画面を作る【Phaser3】丸パクリ めちゃめちゃ参考にさせていただきました!
Phaser3 で横スクロールゲームを作成したい時の入門として、超々々有用記事です。

Phaser3 + TypeScript + Spine で 2D 横スクロールゲームを作ってみる

表題通り、Phaser3Spine を組み合わせて、2D 横スクロールを作ってみたいと思い立ちました。
ある程度のカタチにはなったので、備忘録としてまとめようと思った次第です。

spine-phaser runtime released にもある通り、Phaser3 用の Spine ランタイムは公式で提供されるようになりました。
spine-phaser Runtime Documentation にドキュメントがありますので、より詳しい情報を知りたい方は、本記事よりもそちらを参照すると良いでしょう。

2D 横スクロールゲーム サンプルページ

> 2D 横スクロールゲーム サンプルページ URL
※PCで閲覧する想定で作成しています。
スマホでは正しく表示できない可能性がありますので、ご注意ください。

ページが表示後に、少し待機すると 2D 横スクロールゲームのステージが表示されます。
ステージ左端の立っている Spine Boy が操作可能プレイヤーです。
左右の矢印キーを押下すると、それぞれの方向へ移動します。また、Space キーを押下すると shoot アニメーションが発火します。

画面キャプチャ
2D横スクロールゲームのサンプル

リポジトリはコチラです。
> t-tonyo-maru/pub_web_phaser3-spine_2D-platform-game_sample

主となる TypeScript コードはコチラの 2 ファイルです。

主なパッケージのバージョン

Package Version
@esotericsoftware/spine-phaser ^4.2.34
phaser ^3.70.0
typescript ^5.2.2
vite ^5.0.8

Phaser3 + TypeScript + Spine の勘所

2D 横スクロールゲームの実装方法は先達の素晴らしい解説記事がありますので、本記事では主に Phaser3 に Spine アニメーションを導入するところを解説していきます。

Phaser のシーンに Spine アニメーションを追加する

Phaser のシーンに Spine アニメーションを追加するおおまかな流れは、下記の通りです。

  1. @esotericsoftware/spine-phaser をインストールする
  2. Phaser の GameConfigpluginsSpinePlugin をセットする
  3. Spine のエクスポートファイルを読み込む
  4. spine 関数で Spine アニメーションをステージに追加する

1. @esotericsoftware/spine-phaser をインストールする

まずは何がともあれ @esotericsoftware/spine-phaser の導入ですね。
npm i @esotericsoftware/spine-phaser を実行します。

2. Phaser の GameConfigpluginsSpinePlugin をセットする

src/main.ts にある通り、config(Phaser の GameConfig オブジェクト) の plugins.scene 配列に SpinePlugin を追加します。

src/main.ts
import { SpinePlugin } from '@esotericsoftware/spine-phaser'

const config: Phaser.Types.Core.GameConfig = {
  // …略…
  plugins: {
    scene: [
      { key: 'spine.SpinePlugin', plugin: SpinePlugin, mapping: 'spine' }
    ]
  },
  // …略…
}

3. Spine のエクスポートファイルを読み込む

次に Phaser の Scene クラスを編集していきます。

Phaser の Scene クラスのライフサイクルは下記の通りです。

  1. preload 関数でシーンに必要な画像・音声などを読み込む
  2. create 関数でシーンの初期設定を行う
  3. update 関数でシーンの更新を行う

では、はじめに preload 関数で Spine のエクスポートファイルを読み込むようにします。

src/scene.ts
preload = () => {
  // …略…

  this.load.spineJson(
    'spine-ghost-model',
    `${ASSETS_URL}/spine/ghost/model.json`
  )
  this.load.spineAtlas(
    'spine-ghost-atlas',
    `${ASSETS_URL}/spine/ghost/model.atlas`
  )

}

4. spine 関数で Spine アニメーションをステージに追加する

preload 関数にて必要な素材がすべて読み込まれると、create 関数が発火します。
この create 関数にて、読み込んだ Spine エクスポートファイルを調整した後にステージに追加します。

src/scene.ts
create = () => {
  // …略…

  // Spine お化け キャラクターの追加
  this.spineGhost = this.add.spine(
    500, // x
    550, // y
    'spine-ghost-model', // json
    'spine-ghost-atlas' // atlas
  )
  this.spineGhost.scale = 0.5 // スケール
  this.spineGhost.setInteractive() // インタラクション可
  this.physics.add.existing(this.spineGhost) // 当たり判定を付与
  if (this.spineGhost.body instanceof Phaser.Physics.Arcade.Body) {
    // 弾性を調整
    this.spineGhost.body.setBounce(0.2)
    this.spineGhost.body.setCollideWorldBounds(true)
    this.spineGhost.body.setOffset(0, 0)
  }
  this.input.enableDebug(this.spineGhost, 0xff00ff) // デバッグON
  this.spineGhost.animationState.setAnimation(0, 'idle', true) // idle アニメーションをセット

  // …略…
}

ここまで正しく実装できていれば、ステージ上に Spine アニメーションが表示されると思います。

Spine アニメーションに当たり判定を付与する

Spine アニメーションに当たり判定を付与するには、physics.add.collider 関数を使います。
Phaser の通常の当たり判定付与の処理と同様ですね。
今回のサンプルでは create 関数にて、当たり判定を付与しています。

src/scene.ts
create = () => {
  // …略…

  // 当たり判定を設定
  this.physics.add.collider(
    [this.spineBoyPlayer, this.spineGhost],
    this.platforms
  )
  this.physics.add.collider([this.spineGhost], this.spineBoyPlayer)

  // …略…
}

上記の当たり判定の処理があるおかげで、プレイヤーである Spine Boy が地面に立つことができますし、Spine アニメーション同士で衝突するようにもなっています。

Spine アニメーションにコールバック関数を設定する

Spine アニメーションの任意のタイミングに合わせて、コールバック関数を発火するには animationStateaddListener 関数を利用します。

2D 横スクロールゲーム サンプルページでは、create 関数にて、アニメーションが完了した時 (complete) にのみコールバック関数を設定しています。

src/scene.ts
create = () => {
  // …略…

  // Spine アニメーションにコールバックを設定する
  this.spineBoyPlayer.animationState.addListener({
    start: (entry) => {},
    end: (entry) => {},
    interrupt: (entry) => {},
    dispose: (entry) => {},
    complete: (entry) => {
      // トラック:1 の shoot が再生された後に空アニメーションにする
      if (entry.animation?.name === 'shoot') {
        this.spineBoyPlayer!.animationState.setEmptyAnimation(1)
      }
    }
    event: (entry) => {}
  })

  // …略…
}

上記では shoot アニメーション完了時にトラック 1 に空アニメーションをセットしています。
Spine アニメーションはループを指定しない限り、一度再生したら、再生されたきりで止まってしまいます。
shoot アニメーションが再生されたきりの状態を解消するために、shoot アニメーションがセットされているトラック1を空アニメーションにすることで、一度再生した shoot アニメーションを初期化しています。

このコールバック関数のおかげで、下記のアニメーションサイクルが実現できています。

  1. Space キーを押下する
  2. shoot アニメーション発火
  3. shoot アニメーション完了
  4. トラック1を空アニメーションにして、shoot アニメーションを初期化する
  5. 再び Space キーを押下すると、再度 shoot アニメーションが発火する。以降ループする

他にも startend など、様々なタイミングでコールバック関数を設定できます。

Spine アニメーションを反転させる(※妥協した点)

2D 横スクロールゲーム サンプルページでは、左右の矢印キーを押下すると、押下した方向へプレイヤーが体を向けるようになっています。
この処理は、かなり強引な手段で実現しています。

src/scene.ts
update = () => {
  // 左矢印キー押下:
  if (this.cursors.left.isDown) {
    if (this.spineBoyPlayer.body instanceof Phaser.Physics.Arcade.Body) {
      // …
    }
    if (
      this.spineBoyPlayer.animationState.getCurrent(0)?.animation?.name !==
      'run'
    ) {
      // Spine boy を反転
      this.spineBoyPlayer.scaleX = //  ←ココ
        this.spineBoyPlayer.scaleX > 0
          ? this.spineBoyPlayer.scaleX * -1
          : this.spineBoyPlayer.scaleX * 1
      this.spineBoyPlayer.animationState.setAnimation(0, 'run', true)
    }
  } // …略…
}

はじめは this.spineObject.setFlipX(true) で簡単に実現できるかと思いましたが、うまくいかず。。
ドキュメントを軽く見てみましたが、パッと解決できる案が見つからなかったため、妥協案として現在のカタチになっています。

しかしながら、この強引な反転処理のせいでプレイヤーが左を向くと当たり判定からはみ出してしまいます。
これはゲームとしては致命的ですね。。

プレイヤーが左を向いた時のスクリーンショット
ピンクが当たり判定です。当たり判定からプレイヤーがはみ出しています

左右の矢印キー押下時に、scaleX を変更するのではなく、それぞれの向きのアニメーションを適用するカタチにすれば、この不具合は解消できると思います。
(左向きのアニメーションを新しく作成するのが面倒くさいので、とりあえず現在のカタチにしています。)

Spine アニメーションの黒い線を取り除く

Spine エクスポートファイルは、テクスチャパッキング設定によっては黒い線が発生する時があります。

黒い線が入った Spine エクスポートファイル
よく見ると黒い線が口周りに入ってます

この現象をパッと解決するには、テクスチャパッキング設定の「出力」の「乗算済みアルファ」のチェックを外せば OK です。
Spine テクスチャパッキング設定
赤枠が「乗算済みアルファ」です

「乗算済みアルファ」をチェックするかしないかは、ランタイム上でどのようにレンダリングされるかに依拠するようですので、お使いに環境に合わせるのが無難そうです。
(正直、「乗算済みアルファ」なるモノが何なのか正しく理解できていないので、時期を見て調べてみようと思います。)

> テクスチャパッキング設定

まとめ

以上。
「Phaser3 + TypeScript + Spine で 2D 横スクロールゲームのサンプルを作ってみる」でした。

次回も Spine の記事を書いてみようと思います。
では、また!

Discussion