2024年自作ゲーム総括

## 本題について

この記事は、自作シーティングゲーム・編集ツールのアップデート内容を毎年投稿しており、
2024年も数多くのアップデートしましたが、そのうちの一部を振り返りたいと思います

::: note info
過去記事は以下になります
:::

https://qiita.com/TakahashiMasaya/items/0d8ef354695fd42b2136

https://qiita.com/TakahashiMasaya/items/eb7386aa11f158cb6064

https://qiita.com/TakahashiMasaya/items/6c67b82977ce5d2aadde

https://qiita.com/TakahashiMasaya/items/f578b350603c80f01f85

## 自作シューティングゲームとは

TypeScriptで実装したグラディウス風の横スクロールシューティングのブラウザゲームです
一部制約はありますが、基本的にPC・スマホでゲームを楽しめます
GamepadAPIを使っており、幾つかのゲームパッドやアーケードコントローラーなどと接続してゲームを楽しむこともできます

<iframe width="560" height="315" src="https://www.youtube.com/embed/Aj-HWIWvf8k?si=arNPsCFcq5iXcrzH&amp;start=735" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

この制作目的は
- 好きなシューティングゲームを自分で楽しむ
- 変遷の大きいフロントエンドの技術等を学習する

です

| フレームワーク | React |
| - | - |
| バージョン管理 | Proto |
| ビルド環境 | Vite |
| テストフレームワーク | Bun |
| 言語 | TypeScript<br>Scss |
| WebAPI | CanvasAPI<br>WebAudioAPI<br>GamepadAPI |
| lintツール | Biome<br>Stylelint |
| 対応機種 | Windows PC(Windows 11)<br>Mac(latest macOS)<br>iPhone(latest iOS)<br>iPad(latest iPadOS) |
| 対応ブラウザ | Chrome<br>Edge<br>Safari |
| 接続可能外部コントローラー | 8BitDo Arcade Stick<br>8BitDo ZEROゲームパッド コントローラー<br>Xbox Eliteワイヤレス コントローラー シリーズ 2<br>Backbone One PlayStation®版 |

### 構成

このシューティングゲームは、Nuxtで実装したステージ編集ツールと連携してます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130301/97aed01e-4240-e021-8465-d497585e320f.png)

今年も幾つか改修してますが、主だった内容だけピックアップします

### 1-1. 装備追加

- HANDLE MISSILE
ミサイル発射後、十字キーに併せてミサイルの移動先をコントロールできます
![Monosnap screencast 2024-12-27 15-39-18.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130301/293c8280-aefc-2cb4-d3ba-0453992e0982.gif)

- HANDLE SHIELD
バリアを装備した時に現れたバリアが、十字キーに併せて位置が動くようになってます
![Monosnap screencast 2024-12-27 16-15-12.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130301/82ab452b-bf0e-72c0-3082-b96fd27da999.gif)


### 1-2. ChatGPTを使った既存装備の改修

今年からChatGPTを使って、プロンプトから生成されたコードを参考に、
自分の実装と比較して簡潔で管理しやすいソースであればそちらに改修しました

以下の装備はChatGPTを参考に実装を変更しました

- SWING OPTION
OPTIONは分身装備で、ロープのように振り回す動きをします
![Monosnap screencast 2024-12-30 15-52-30.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130301/125da80e-2bed-b433-35c9-67bf3567395e.gif)


### 1-3. 開発環境をBunに切替え

パッケージマネージャー: pnpm -> Bun

```package.json
{
  ..(略)..
  "scripts": {
    "dev": "vite",
    "preview": "vite preview",
    "build": "vite build",
  }
  ..(略)..
}
```

```bash
# 開発環境起動
$ bun dev
# プレビュー
$ bun preview
# ビルド
$ bun run build
```

#### ユニットテスト: Vitest -> Bun

現時点でユニットテストのテストケースは2000以上ありますが、
実行は約1秒ほどでした

```bash
$ bun test
...
...
 2115 pass
 3 skip
 0 fail
 3708 expect() calls
Ran 2118 tests across 210 files. [1266.00ms]
```

#### テストソースの換装

1. 必要なメソッドをimport
1. `vi.`は全て削除
1. `it.each()`はBunに併せて修正（後述）

```diff_typescript
+ import { spyOn, describe, it } from 'bun:test';

describe('case', () => {
  it('case 1', () => {
-   vi.spyOn(className, 'classMethod');
+   spyOn(className, 'classMethod'); // クラスメソッドの動作をモック
+   expect(true).toBe(true); // Bunではexpectedではなくexpect
  });
});
```

`it.each()`の変更点

Vitestでeach()メソッドはテンプレートリテラルの実装が可能で、
項目名を使ってまとまりのある見易いコードが、Bunではなぜかできない...
（調べる限り、[テンプレートリテラル未対応のため、オブジェクト配列形式で記述が必要みたいです](https://github.com/oven-sh/bun/issues/6364)）

試行錯誤の末、オブジェクト配列に変更しましたが、
Bunにもテンプレートリテラル対応が実現すれば、より便利になると感じました

```diff_typescript
+ import { describe, expect, it } from 'bun:test';

describe('getDistance', () => {
-    it.each`
-      a                  | b                 | result
-      ${{ x: 1, y: 0 }}  | ${{ x: 0, y: 0 }} | ${1}
-    `('getDistance $a, $b, $result', ({ a, b, result }) => {
+    it.each([
+      [{ x: 1, y: 0 }, { x: 0, y: 0 }, 1],
+    ])('getDistance %p, %p, %p', (a, b, result) => {
      expect(COMMON_GAME.getDistance(a, b)).toEqual(result);
    });
});
```

#### GitHub Actionsにて設定ファイルもBunに切替

```diff_yaml
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: snyk/actions/setup@master
      - uses: moonrepo/setup-toolchain@v0
        with:
          auto-install: true
-      - name: pnpm install
-        run: pnpm install
-      - name: pnpm check:all
-        run: pnpm check:all
-      - name: pnpm knip
-        run: pnpm knip
-      - name: pnpm test
-        run: pnpm test
+      - name: bun install
+        run: bun install
+      - name: bun check:all
+        run: bun check:all
+      - name: bun knip
+        run: bun knip
+      - name: bun test
+        run: bun test
```
先に記載したBun切替によるユニットテストがかなり貢献してくれて、
全体の実行時間も1分ほど短縮できました


#### Amplifyのホスティングにおけるビルド設定
こちらもBunに対応できたようで、以下に設定しました
(編集ツールも同様です)

```amplify.yml
version: 1.1
frontend:
  phases:
    preBuild:
      commands:
        - curl -fsSL https://bun.sh/install | bash
        - source /root/.bashrc
        - bun install
    build:
      commands:
        - bun run build
  artifacts:
    # IMPORTANT - Please verify your build output directory
    baseDirectory: dist
    files:
      - '**/*'
```


## 2. ステージ編集ツール

Nuxtで実装したシューティングゲームのステージを作成・編集するツールです
現時点ではPCのみの対応です

| フレームワーク | Nuxt3 |
| - | - |
| テストフレームワーク | Vitest |
| lintツール | Eslint<br>Prettier<br>Stylelint |
| 対応機種 | Windows PC(Windows 11)<br>Mac(latest macOS) |
| 対応ブラウザ | Chrome<br>Edge<br>Safari |

編集の主な機能です

| 機能 | 操作イメージ |
|-|-|
| ステージの作成 | ![Monosnap screencast 2023-12-30 17-02-02.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130301/4686a678-f4dd-b5a8-8055-85556d32c772.gif) |
| ステージの編集 | ![Monosnap screencast 2023-12-30 17-05-20.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130301/53df9fb7-ea44-5f1e-e7f9-e023d315b8ff.gif) |
| ステージの削除 | ![Photos Library.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130301/852850a4-6dac-7f0a-c2fb-da31c563e3f8.gif) |
| ステージの並替え | ![test.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130301/1a3d5abc-f64e-aac6-86e5-1b4952944e85.gif) |

このステージ編集ツールは、シューティングゲームと連携しており、
ゲームを起動して、編集したステージをプレーすることができます

また、ステージの保存先は現時点で以下を対応しており、ゲーム側でクエリパラメータを使って編集したステージをプレーすることができます
| リポジトリ | URL |
|-|-|
| firebase | https://(ゲームのパス)?repository=firebase |
| microCMS | https://(ゲームのパス)?repository=microcms |
| MovableType | https://(ゲームのパス)?repository=movabletype |

以下に今年改修した内容を記載します

### 2-1. プレビュー機能

編集中のステージを実際にプレーして検証するプレビュー機能を追加しました

![Monosnap screencast 2024-12-21 14-55-07.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130301/f8c6c76f-a35e-ccf1-ff59-2813613e0605.gif)

#### プレビュー機能の概要
- 編集中のステージをプレイして確認可能
- Loop（難易度）や復帰位置を切り替えながらリアルタイムで確認可能
- ゲームコントローラーを利用した操作もサポート

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130301/b78fff6e-b4f3-795a-8846-90a0ed705ebf.png)

`iframe`と`postMessage`を選択した理由
- ゲームと編集ツールを一緒にするとプロダクトサイズが大きくなるし、総合的に管理が大変（な気がした）のを踏まえ
  - `iframe`を使用することで、ゲームと編集ツールを疎結合に保ちながら連携可能
  - `postMessage`を使うことで、異なるオリジン間でも安全にデータをやり取り可能

#### 実装の仕組み
iframeを使ってシューティングゲームを起動し、編集ツールからステージデータを[postMessage](https://developer.mozilla.org/ja/docs/Web/API/Window/postMessage)で送信することで、プレビューを実現しています

以下図はプレビュー起動、Loop・復帰位置選択時のフローです

```mermaid

sequenceDiagram
  Actor Editor
  participant line_2 as Edit
  participant line_3 as Game
  opt 初回プレビュー
    Editor ->> line_2: プレビュー実行
    activate line_2
    line_2 ->> line_3: iframeを使ってGameをload
    activate line_3
    line_3 ->> line_3: messageイベント作成
    Note over line_2, line_2: iframeのloadが完了
    line_2 ->> line_3: 編集中のステージをpostMessageで送信
    line_3 ->> line_3: プレビュー準備・画面表示
    Editor ->> line_3: ウェポンを選択しプレー確認
  end
  opt Loopの変更
    Editor ->> line_2: Loop変更
    line_2 ->> line_3: Loop値・復帰位置と編集中のステージをpostMessageで送信
    line_3 ->> line_3: プレビュー準備・画面表示
    Editor ->> line_3: ウェポンを選択しプレー確認
  end
  opt 復帰位置の変更
    Editor ->> line_2: 復帰位置変更
    line_2 ->> line_3: Loop値・復帰位置と編集中のステージをpostMessageで送信
    line_3 ->> line_3: プレビュー準備・画面表示
    Editor ->> line_3: ウェポンを選択しプレー確認
    deactivate line_2
    deactivate line_3
  end
```

- **初回プレビュー**: iframeをロードし、編集中のステージデータを送信してプレビューを開始
- **Loop/復帰位置変更**: 編集ツールから変更データを送信し、リアルタイムでプレビューを更新

#### 実装例

##### Edit（編集ツール側）

```send.vue
<template>
  <!-- シューティングゲームを表示するiframe -->
  <iframe
    ref="iframedom"
    :src="iFrameURL"
  />
  <!-- メッセージ送信ボタン -->
  <button @click.stop="sendMessage" />
</template>

<script type="ts" setup>
import { useEventListener } from '@vueuse/core'
const iframedom = ref<HTMLIFrameElement | null>(null)
const iFrameURL = computed(() => `(シーティングゲーム側のパス)`)

// 編集中のステージデータをiframeに送信
const sendMessage = () => {
  iframedom.value?.contentWindow?.postMessage(
    JSON.stringify('(送信するステージデータ)'),
    '(ゲーム側のホスト)'
  )
}

onMounted(() => {
  // iframeが読み込まれた際にメッセージを送信
  useEventListener(iframedom, 'load', () => {
    sendMessage()
  })
})
</script>
```

##### Game（ゲーム側）

```recieved.ts
// ゲーム側で受信したメッセージを処理
let recievedMessage = ''
window.addEventListener('message', (event: MessageEvent<stringifiedStages>) => {
  if (event.origin === '(ステージ編集側のホスト)') {
    // 編集機能からのメッセージの場合、受信データをセット
    recievedMessage = event.data
  }
})
```

## 3. まとめ
今年も自作シーティングゲーム、ステージ編集ツールを紹介しました
2017年からシーティングゲームを作成してたので、気づいたらもう7年以上も続けてました

フロントエンドの激しい変遷の中で、
今回の体験を通じ、実務に活かせればと思ってます