2022年自作ゲーム総括

# 本題について

2022年は、自作ゲームを2つ新規・追加改修しました。

- **昨年に引続き、趣味でグラディウス風（特にゲーセン版の3作目を意識して）のシューティングゲーム**
- **Vue3ベース15Puzzle**


今回は、シューティングゲームの内容をお伝えしたいと思います。

1. 自作シューティングゲームとは
2. 2022年自作シューティングゲーム追加・変更
2. まとめ

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

TypeScriptで実装したグラディウス風の横スクロールシューティングゲームです。
一部制約はありますが、基本的にPC・スマホでゲームを楽しめます。

<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/JGqlmu5i600" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

※この動画ではiPad Pro2021, ゲームコントローラー（Xbox Elite ワイヤレス コントローラー シリーズ 2）使ってプレーしてます。

- PCはMacOSの最新版、Windows11 Edge (Chromium)
- スマホはiOS、iPadOSの最新版でSafari対応
- スマホではゲームコントローラーを使ってゲーム可能（後述）
- 2022/12/5時点で合計ファイルサイズは6.812MB（Viteによるビルド実行）
- フレームワークはReact
- 言語はTypeScript（2021年〜）でCanvasで描画、AudioContextで音声出力

過去にも自作ゲームに関する記事を投稿してます。

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

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

この開発は約6年ぐらいになりますが、今でも改修を続けてます。

# 2. 2022年自作シューティングゲーム追加・変更

## 2-1. ゲームコントローラー対応

GamepadAPIを実装し、ゲームコントローラーでプレーできるよう改修をしました。

iOSが16.xになり、Joy-Conサポートされました。
基本的に動作はしましたが、いくつか課題もありました。

### 2-1-1. 使用機材
- [8Bitdo アーケードスティック for Switch & Windows](https://www.amazon.co.jp/8Bitdo-%E3%82%A2%E3%83%BC%E3%82%B1%E3%83%BC%E3%83%89%E3%82%B9%E3%83%86%E3%82%A3%E3%83%83%E3%82%AF-for-Switch-Windows/dp/B08GJC5WSS)
- [Nintendo Switch（有機EL）](https://www.nintendo.co.jp/hardware/detail/switch-oled/index.html)のJoy-Con
- [Razer Kishi V2 For iPhone](https://www.razer.com/jp-jp/mobile-controllers/razer-kishi-v2-for-iphone)
- [Xbox Elite ワイヤレス コントローラー シリーズ 2](https://www.xbox.com/ja-jp/accessories/controllers/elite-wireless-controller-series-2)


### (1). Windowsと8Bitdo アーケードスティック

PCからUSB経由でアケステに接続し、Edgeブラウザでプレーできます。
Windows側にはキーマッピングするソフトが必要になります。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">この小型PCをWindows11にアップデート。<br>アケステの動作は問題なく、自作のをEdgeでプレーしても、<br>スムーズに動いてくれて良かった。<br><br>最近、この自作の改修を再開してるけど、<br>tsでの型定義等の学習には丁度良いソースなんすよね。<br>とはいえ、ブロックゲームも再開しないと。 <a href="https://t.co/0kN1IA4k9I">pic.twitter.com/0kN1IA4k9I</a></p>&mdash; Takahashi Masaya (@ma_taka) <a href="https://twitter.com/ma_taka/status/1511369810676170752?ref_src=twsrc%5Etfw">April 5, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

### (2). iPhoneとJoy-Con(これはサポート対象外)
初めて外部コントローラーに接続した感動を覚えたのですが、
致命的だったのが、ゲーム開始後操作遅延が発生し、暫くして処理が低下。
最終的にはSafariが落ちてしまう現象が発生しました。

なので、この自作ゲームではJoy-Conは対象外にしました。
iOSか、Safariアップデートによる改善を期待しつつ・・・。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">原因がわかって、Switchの場合、左右のJoy-Con両方ペアリングが必要だったようで・・・。なるほどです。<br><br>ひとまずiPhoneでなんとか動かせた。<br>あとは、cssで画面調整すれば良いか。<br><br>iPadOS16が楽しみになってきた。<br><br>実際のこんな操作はちょっと面倒なので、<br>MFI認証のコントローラー欲しいかも。 <a href="https://t.co/JLPyVUwI5O">https://t.co/JLPyVUwI5O</a> <a href="https://t.co/cfIcDs8Pq3">pic.twitter.com/cfIcDs8Pq3</a></p>&mdash; Takahashi Masaya (@ma_taka) <a href="https://twitter.com/ma_taka/status/1575469715094646784?ref_src=twsrc%5Etfw">September 29, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

### (3). iPhoneとRazer Kishi V2
Joy-Conでの課題を改善するために、Bluetoothではなく、
Lightning接続コントローラーを購入。
遅延はほとんど（自分感覚）なく、スムーズにプレーできます。
ただ、状況によっては長時間プレーすると、ブラウザ（Safari）が落ちることもあります。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">諦めきれなくて、lightning接続コントローラー購入して検証。<br><br>スティックがSwitchより(すごく)操作しやすいお陰もあって、ほぼ遅延なしで、上積みした自機スピードでも、狭い通路を簡単に操作できて驚いてる。<br><br>コントローラー処理箇所をずっと修正してたのはなんだったのか。。。 <a href="https://t.co/K5jNAMPifG">pic.twitter.com/K5jNAMPifG</a></p>&mdash; Takahashi Masaya (@ma_taka) <a href="https://twitter.com/ma_taka/status/1582701300365811712?ref_src=twsrc%5Etfw">October 19, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


### (4). iPadとXbox Elite ワイヤレス コントローラー シリーズ 2

当初3,000円程度のゲームコントローラーを使ってたのですが、
あまりの操作遅延で使い物にならず。
また、iPadでの有線接続方法もなかなか見つからず。

ダメもとでXboxコントローラーを購入してBluetooth接続しても、殆ど遅延は発生しませんでした。
時間経過による劣化での遅延も想定してたのですが、今のところ遅延は発生してません。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">先日知人と秋葉原へ、ゲーミングPC散策。<br>それに触発され、🎮を購入。<br>(ELITE Series2)<br><br>iPadでのBT接続では、注意文言解消されなかったけど、<br>Apple Pencil接続状態でも、遅延は殆どない感じで操作できて驚いてる。どういう仕掛けなんだろ。<br>使用劣化も含め暫く様子見。<br><br>明日出社なので早めに就寝。 <a href="https://t.co/oKCrRBTfim">pic.twitter.com/oKCrRBTfim</a></p>&mdash; Takahashi Masaya (@ma_taka) <a href="https://twitter.com/ma_taka/status/1592131837014740992?ref_src=twsrc%5Etfw">November 14, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


### 2-1-2. ゲームコントローラーの処理フロー

mdnにある[ゲームパッドAPIの使用](https://developer.mozilla.org/ja/docs/Web/API/Gamepad_API/Using_the_Gamepad_API)を参考に実装しました。
`requestAnimationFrame`を使ってゲームコントローラーからのキー情報を取得するようにしてます。

当初、ゲーム処理とゲームコントローラーそれぞれに`requestAnimationFrame`を用意したのですが、処理負担を考慮（個人見解）して1つにまとめました。

```mermaid
graph TD;
    requestAnimationFrame --> ゲーム処理;
    ゲーム処理 --> ゲームコントローラー
    ゲームコントローラー --> requestAnimationFrame

```

### 2-1-3. ゲームコントローラー実行時の課題点

#### (1). Nintendo Switch(JoyCon)との接続では途中でSafariが落ちることがある
Nintendo Switch(JoyCon)との接続では5分も経過せずに処理がスローになり、最終的にSafariが落ちてしまいます。
`requestAnimationFrame`でゲームコントローラー側で処理
GamepadAPIからのキー情報取得を間引いたりしたのですが、結局変わらず・・・。
ただ、他のコントローラーではこの事象が発生しないので、Switchにおいてはゲームプレー対象外にしてます。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">iPhoneでAirPlayして検証(Safari、iOS16.0.2)。<br><br>結構重い処理のせいか、60fpsでのボタン情報取得では確実に落ちる。<br><br>なので15回取得/secに間引き、約40分経過しても落ちずに済んでる。<br><br>jsでここまでできたのは素晴らしいけども、このゲームでは、🎮はおまけな感じかなぁ。 <a href="https://t.co/5fOUKeFzn7">https://t.co/5fOUKeFzn7</a> <a href="https://t.co/RCvWQCk9yA">pic.twitter.com/RCvWQCk9yA</a></p>&mdash; Takahashi Masaya (@ma_taka) <a href="https://twitter.com/ma_taka/status/1577653519061962752?ref_src=twsrc%5Etfw">October 5, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

iOSのアップデートによって改善される可能性があるのかな・・・。

また、iPhoneとRazer Kishiとの組み合わせでもフリーズしてSafariが落ちることがありますが、
落ちるタイミングの幅が広い（だいたい30分か、それ以降）ので、善処方法が不明といった感じです。

:::note info
2023/12/20
iPhone14 Plus、iOS17.1.xで検証したところ、JoyConでも動作できるようになりました。
またプロダクトコードでfpsの間引きをせず、長時間プレーでは端末に熱は発生するものの概ね問題ありませんでした。
:::

#### (2). スマホで、タスク切替や画面ロックによってゲームコントローラーが反応しないことがある

iPhone、iPadで発生する事象で、特にSafariでゲームプレー中 -> Chromeでタスク切替後、再びSafariに戻すとコントローラーが全く効かなくなります。
結局Safariを再起動させるしか方法がない感じです。

## 2-2. 開発環境をwebpackからViteに移行

元々はwebpackを使って開発してたのですが、サーバの起動・開発においてもう少し処理時間を短縮したく。
Viteはその要望に応えられそうと思い、Viteへの移行にトライしてみました。

![screencast 2022-12-22 21-55-55.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130301/db8a482c-f392-c78a-b00d-404b7249fe02.gif)
ファイル保存後にすぐにプレビューできて、開発の進みが早くなった気がします。
ただ、開発環境を意識した実装も考慮しないといけないなとも思いました。（後述）


### 2-2-1. package.jsonの変更
`package.json`で、webpackからの移行による差分を下記します。
webpack、Vite共に自身とその関連するパッケージを差し替えてます。
関連パッケージでは以下の機能を引き継いでます。
- html、アセット系の圧縮
- Eslintによるチェック

```diff_json
package.json(抜粋)

  "scripts": {
     ...
-    "build": "webpack",
-    "dev": "webpack serve",
+    "dev": "vite",
+    "preview": "vite preview",
+    "build": "vite build",
     ...
  },
  "devDependencies": {
     ...
-    "compression-webpack-plugin": "^10.0.0",
-    "copy-webpack-plugin": "^11.0.0",
-    "dotenv": "^16.0.2",
-    "dotenv-cli": "^6.0.0",
-    "dotenv-webpack": "^8.0.1",
-    "eslint-import-resolver-webpack": "^0.13.2",
-    "html-loader": "^4.1.0",
-    "html-webpack-plugin": "^5.5.0",
-    "image-minimizer-webpack-plugin": "^3.3.0",
-    "imagemin-gifsicle": "^7.0.0",
-    "imagemin-pngquant": "^9.0.2",
-    "imagemin-svgo": "^10.0.1",
-    "imagemin-webpack-plugin": "^2.4.2",
-    "jpegtran-bin": "^7.0.0",
-    "webpack": "^5.74.0",
-    "webpack-bundle-analyzer": "^4.6.1",
-    "webpack-cli": "^4.10.0",
-    "webpack-dev-server": "^4.10.1",
-    "workbox-webpack-plugin": "^6.5.4"
     ...
  }
  "dependencies": {
     ...
+    "@vitejs/plugin-react": "^2.0.1",
+    "vite": "^3.0.9",
+    "vite-plugin-compression": "^0.5.1",
+    "vite-plugin-eslint": "^1.8.1",
+    "vite-plugin-html": "^3.2.0",
+    "vite-plugin-imagemin": "^0.6.1"
     ...
  },

```
`vite.config.ts`も追加設定してますが、
上記パッケージを使った設定をしてるだけなので、ここでは省略します。

### 2-2-2. フォルダ構成 
[前回の投稿](https://qiita.com/TakahashiMasaya/items/6c67b82977ce5d2aadde#3-1-2-%E3%83%95%E3%82%A9%E3%83%AB%E3%83%80%E6%A7%8B%E6%88%90%E6%9C%AC%E8%A8%98%E8%BC%89%E9%96%A2%E9%80%A3%E5%88%86%E6%8A%9C%E7%B2%8B)よりViteの基本設定に準じてフォルダ構成を変更しました。

```diff
├──dist/
│   ├── audios/ ･･･ ①からコピー
│   ├── images/ ･･･ ②から画像圧縮
│   ├── images-dom/ ･･･ ③から画像圧縮
│   ├── icons/ ･･･ ④から画像圧縮
│   ├── index-xxxxxxx.js ･･･ ⑥、⑦、⑧から圧縮、バンドル
│   ├── index-xxxxxxx.css ･･･ ⑤をバンドル
│   └── index.html ･･･ ⑨から圧縮
├── public/
│       ├── audios/ ･･･ ①
│       ├── images/ ･･･ ②
│       ├── images-dom/ ･･･ ③
│       └── icons/ ･･･ ④
├── src/
│   └── application/
│       ├── scss/ ･･･ ⑤
│       ├── json/ ･･･ ⑥
│       ├── ts/ ･･･ ⑦
│       └── main.tsx ･･･ ⑧
├── index.html ･･･ ⑨
└── tests/ ･･･ Jestで扱うファイル群
```

### 2-2-3. HMRを考慮した改修

webpackではソース改修後に再読込してたのですが、
ViteになってHMRにしたので、必要な箇所のみ再読込する形になりました。
この自作ゲームでは、再読込前提の実装だったので、「後始末」を全く意識してませんでした。

例を示すために簡単なソースを、以下に記載しますが、
モジュールを更新する度に、`App.tsx`のイベント処理が都度登録されてました。

```App.tsx
window.addEventListener('touchStart', () => {
  ... // イベント処理
}, false)

export const App: React.FC = () => {
  useEffect(() => {
    ...
  }, [])
  return (
    <div>
      ...
    </div>
  )
}
```

そこで、イベント登録が重複されないよう、
[副作用フックの利用法](https://ja.reactjs.org/docs/hooks-effect.html)を参考に、クリーンアップを使ってイベント処理を見直しました。
ランタイム確認で、イベントの登録・削除の所作が思惑通りになりました。

```diff_typescript
- window.addEventListener('touchStart', () => {
-   // イベント処理
- }, false)

export const App: React.FC = () => {
-  useEffect(() => {
-    ...
-  }, [])
+  useEffect(() => {
+    const doEvent = () => {
+      // イベント処理
+    }
+    // イベント登録
+    window.addEventListener('touchStart', doEvent, false)
+    return () => {
+      // イベント削除
+      window.removeEventListener('touchStart', doEvent)
+    }
+  }, [])  
  return (
    <div>
      ...
    </div>
  )
}
```


## 2-3. テストフレームワークをJestからVitestに移行

元々テストはJestを使用して、TypeScriptで実装しました。
2022/12/05時点でのテストファイル、テスト項目数です。
- テストファイル 119
- テスト項目 724

今回Vitestに移行したのは、テスト実施時間の短縮が目的です。
今年だけでテストフレームワークは以下の変遷でした。
Vite導入に併せてVitestに決定した感じです。

```
1. Jest + ts-config
2. Jest + swc
3. Vitest ← 現在
```
この自作ゲームはクラスの集まりなので、テスト方針はクラスメソッドのIN・OUTチェックがベースです。

## 2-3-1. Vitestの設定

Vitestへの移行にあたり、ここではVitestに必要なパッケージと追加設定ファイルの内容を記載します。

```diff_json
package.json(抜粋)

"scripts": {
  ...
+  "test": "vitest",
+  "coverage": "vitest run --coverage",
  ...
}
"devDependencies": {
  ...
+  "jsdom": "^20.0.3",
+  "vitest": "^0.25.3",
+  "@vitejs/plugin-react": "^2.2.0",
  ...
}
```

```vitest.config.ts
import { defineConfig } from 'vitest/config';
import path from 'path';

export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom',
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      '~': path.resolve(__dirname, './tests'),
    },
  },
});

```

## 2-3-2. テストソース改修

Jestと互換性があり、大幅な変更はなかったのですが、
`describe`や`it`等を`vitest`から`import`する必要があり、
全テストファイルに追加しました。
※もしかしたら追加不要の方法があったのかも・・・。

```diff_typescript
+ import { beforeEach, describe, it, expect, vi } from 'vitest';
import { SceneAnimator } from '@/application/ts/animator';

describe('SceneAnimator', () => {
  let anim: SceneAnimator;
  beforeEach(() => {
    anim = new SceneAnimator();
  });
  describe('アニメーション登録・削除', () => {
    it('アニメーションが削除されないこと', () => {
      anim.addAnimation({
        keyName: 'test',
        act: () => {},
      });
      anim.removeAnimation('test2');
      expect((<any>anim).animations.length).toBe(1);
    });
  });
});
```

## 2-3-3. テスト所要時間 (Jest+swcとVitestの比較)

Jest + swc 82.857ms
![Screenshot 2022-11-23 at 15.27.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130301/9c06cbbe-0c41-dafb-ba86-f71ea6032f37.png)

Vitest 47.68ms
![Screenshot 2022-11-23 at 15.22.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130301/00dda1ba-f80f-4442-5641-7a03708d25e1.png)

※それぞれ2回計測してほぼ同じ経過時間でした。

## 2-3-4. Vitest(VSCodeプラグイン)

自分の開発環境ではVitestでのコンソール実行は設けてますが、
VSCodeのプラグインでテスト実施できるということで、公式のプラグインを導入[^1][^2]してみました。
(2022/12/05時点ではPreview版でした)

![Screenshot 2022-12-05 at 9.05.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130301/e6b29b5c-20f6-9345-f619-bd2e0321156a.png)

ファイル単体でのチェック実行は問題ないですが、全ファイルで実行しようとしても動かないようです。。（2022/12/25時点）

![screencast 2022-12-05 09-20-53.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130301/f400d367-2adc-6aca-264b-bd4136c09e57.gif)

# 3. まとめ

スマホではスクリーンにボタン表示させてプレーしてたのですが（以下動画参照）、
ゲームコントローラーの操作に慣れると、スクリーンボタンによる操作が億劫に感じてしまいますね・・・。

ただ、完全にサポートできたわけではないので、今回記載した問題は可能な限り対応したいなと。
あと、デバッグ方法がもう少し簡単だったらなぁ。

<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/MDSR4CA7Smg" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

このゲームでは、フロントエンドに関するいろんなツールを試し、理解を深める意図もあって作成・改修を行なってます。
今回は、Vite・Vitestの導入で開発環境をまるっと差替え、それぞれの体験を通してメリット・デメリットを理解したつもりです。

2023年も、新しい技術等あればこの自作ゲームに取り込んでみたいと思ってます。


[^1]: [Vitest公式のVSCodeプラグインがリリースされました](https://dev.classmethod.jp/articles/vitest-vscode-plugin/)
[^2]: [Vitest](https://marketplace.visualstudio.com/items?itemName=ZixuanChen.vitest-explorer)