本題について
2021年は、自作ゲームを2つ新規・追加改修しました。
- 【追加改修】昨年に引続き、趣味でグラディウス風(特にゲーセン版の3作目を意識して)のシューティングゲーム
- 【新規】TypeScriptや開発環境周りの知識をつけるべく、テトリス風のブロックゲーム(別途記載予定)
今回は以下の内容を記載します。
- 自作シューティングゲームとは
- 自作シューティングゲームでの追加・変更
- TypeScriptへの換装
1. 自作のシューティングゲームとは
グラディウス風の横スクロールシューティングゲームです。
一部制約はありますが、基本的にPC・スマホ(※)でゲームを楽しめます。
言語はTypeScriptで、Canvasで描画しています。
※PCはMacOSの最新版、Windows10 Edge (Chromium)
※スマホはiOS、iPadOSの最新版でSafari対応
※2021/12/26時点で合計ファイルサイズは6.47MB(Webpackによるビルド実行)
※昨年の記事はこちらになります
実際にiPad miniでプレーした動画がこちらになります。
2021年明け時、他の新規自作ゲーム制作に集中しようと思ったのですが、
SNSでフォローした方々の自作シューティングを拝見して、それに触発されまして、
自作ゲームをさらに良い形にしたく、今年もこのシューティングゲームの改修を決意した感じです。
2. 自作シューティングゲームでの追加・変更
今年は以下を追加をしました。
- ステージの追加
- プレーモードの追加
- 自機ショットの追加
- スコアランキングの追加
2-1. ステージの追加
砂ステージをステージ1に追加しました。
難易度の低いステージをゲーム序盤に用意したかったのが目的です。
それまでは、既存ステージが難易度の高い形で仕上がった感があり、
それらのステージから、難易度調整等でカスタムするのが面倒だったので、
いっそのこと新しいステージ作ろうと、砂ステージを作成しました。
敵数も少なめ、かつ敵を倒しやすいように組みましたが、デザイン作成は他ステージに比べて一番時間かかりました。
ただ、昨年から独自でデザインを描いた経験が活かせたせいか、
デザイン自体は他のステージより良い出来だったと思います。
これで全ステージが9つになりました。他にも幾つかのステージでデザインを変更しました。
ステージ | ステージ名 |
---|---|
1 | 砂漠ステージ(←追加分) |
2 | ストーンステージ |
3 | 火山ステージ |
4 | モマイステージ |
5 | 細胞ステージ(←ボスシーンでデザイン変更) |
6 | フレームステージ |
7 | 木ステージ(←ステージ全面デザイン変更) |
8 | ボス集団ステージ |
9 | 要塞ステージ |
今週の進捗はここまで。
— Takahashi Masaya (@ma_taka) April 16, 2021
砂漠は何度も描き直した。
どら焼きっぽくなったけど、明暗差を抑えたことで、自分の理想に近づけたかなぁ。
地形パターンをもう少し増やそうか。
砂竜は、cos()でくねくね動いてくれた。
意外に面白いステージになった。 https://t.co/lTaZi7VQoN pic.twitter.com/Kt6c9STFxl
2-2. プレーモードの追加
元々このゲームはスマホで簡易に楽しむ意味で、
9つのステージから1つ決めてプレーする選択方式だけでしたが、
ステージ1→ステージ9を通しプレーできるる全ステージクリア方式を追加しました。
以下の画像では、オープニング画面で以下2つから選択できます。
- NORMAL START→全ステージクリア方式
- SPECIAL START→ステージ選択方式
ちなみに、私がNORMAL STARTで実際にプレーをして、全ステージクリアするのに、
ノーミスで約30分でした。
2-3. 自機ショットの追加
上記4つの装備に加え、さらに以下の武器を追加しました。
- ミサイル
・UPPER MISSILE(上方向にミサイルを発射し、地上を這う)
・TRACKING MISSILE(ミサイルが自動的に敵に目掛けて進む)
・SMALL SPREAD(上下2方向に放物線を描くように進み、敵衝突時爆風を発生。それに近い敵も巻き込んで倒す)
- ダブル
・VERTICAL(前方と真上にショットする)
・FREEWAY(前方と、ボタン方向にショットする)
・SPREADGUN(前方と上下2方向にショットする)
- レーザー
・REFLECTING LASER(反射レーザー、壁にぶつかったら反射する)
- オプション
・FIXED OPTION(自機上下に分身を固定する)
ショットの数を結構増やしたので、
ミサイル、ダブル、レーザー、オプション、シールドを個別に選択できる、
エディット装備を追加しました。
個人的の会心作はREFLECTING LASER
かと。
反射レーザーを試作。
— Takahashi Masaya (@ma_taka) July 16, 2021
攻撃力がなんか微妙で、分身を全部装備したら、
もう訳がわからず。。。
数多ければ良い、じゃないすね。。なので保留。
レーザー情報を配列で保持していて、
相当な要素数が必要(自分が考えたロジックでは)。
やっぱり動作が重くなるなぁ。 pic.twitter.com/9S1h3mLYbE
2-4. スコアランキングの追加
このゲーム開発は5年目になるのですが、その前半ぐらいに知人からスコアランクもあった方が良いのでは、
とアドバイスを頂きまして、漸くこのタイミングで追加しました。
- オープニング時にトップ10を表示
- ゲームオーバー時に、スコアがトップ10に入れば表示
イニシャルの設定等はしてないですが、スコアのデータ(ランキング・スコア・ランクイン時の日付)はブラウザのLocalStorageに保存するように実装してます。
2021年の半年が終わる。早いねぇ〜。
— Takahashi Masaya (@ma_taka) June 30, 2021
そのタイミングで、スコアランクを追加。
昔、知人からスコアランクあった方が良いよ、
とアドバイスをもらい、随分経ったが一先ず完成。
スコアの保存は、現時点ではFirebase。
後日、他のCMS(MT等)に検証・追加予定。
またファイルサイズ増えたな・・・。 pic.twitter.com/NpQ1rj20p6
3. TypeScriptへの換装
元々JavaScriptで実装したゲームでしたが、以下理由でTypeScript化を決めました。
- TypeScriptの言語、それに関連する開発環境(Webpackや、tsやjest等のconfig関連)の体験・知見を増やしたかった
- Jest(tsファイル)を導入し、できる範囲で単体テストを実施。コマンドラインで確認できるようにしたかった
3-1. TypeScript換装前の事前準備(フォルダ構成、環境構築)
フォルダ構成、環境構築をするにあたり、簡単に方針を決めてました。
- Webpackを使用する
- ビルド時に、アセットファイル(画像)は圧縮する
- Reactをベースに実装する
Webpackですが、以下参考に学習がてら最小構成から少しずつカスタマイズしようと決めてました。
※Reactを使用するので、webpack + TypeScript + Reactの最小構成
を参考にしました。
3-1-1. 各パッケージのバージョン(本記載関連分抜粋)
"copy-webpack-plugin": "^10.2.0",
"html-webpack-plugin": "^5.5.0",
"image-minimizer-webpack-plugin": "^3.2.0",
"jest": "^27.4.5",
"sass": "^1.45.1",
"sass-loader": "^12.4.0",
"style-loader": "^3.3.1",
"webpack": "^5.65.0",
"webpack-cli": "^4.9.1",
"webpack-dev-server": "^4.7.1",
"workbox-webpack-plugin": "^6.4.2"
"react": "^17.0.2",
"react-dom": "^17.0.2"
3-1-2. フォルダ構成(本記載関連分抜粋)
├──dist/
│ ├── audios/ ・・・ src①からコピー
│ ├── images/ ・・・ src②から画像圧縮
│ ├── json/ ・・・ src③からコピー
│ ├── main.js ・・・ src④、⑤からバンドル
│ └── index.html ・・・ src⑥からmain.jsのインポートを追加
├── src/
│ ├── application/
│ │ ├── audios/ ・・・ ①
│ │ ├── images/ ・・・ ②
│ │ ├── json/ ・・・ ③
│ │ ├── scss/ ・・・ ④
│ │ └── ts/ ・・・ ⑤
│ └── html/ ・・・ ⑥
└── tests/ ・・・ Jestで扱うファイル群
├── application/
│ └── ts/
└── utils/
3-2. TypeScript換装前の事前準備(クラス構成)
実装期間は11月中旬から12月中旬の約1ヶ月でした。
元々JavaScriptの時に、以下図のようなクラス構成やフォルダ構成を決めてて、
これらの構成は基本変更しない前提でTypeScript化に着手しました。
DDDやクリーンアーキテクチャを意識した上でに、僕なりに定義した各要素とその役割を簡単に下記します。
-
Device.
キーボード、タッチ入力を検知します。 -
Controller.
Deviceから送られた情報をInteractorに渡します。 -
Interactor.
Controllerから送られた情報を保存・調整して、それをもとにApplicationにアクションを実行します。 -
Application.
以下の役割を果たします。
・各Scene(オープニング、プレー中など)の調整
・Repositoryからステージ、ハイスコア情報を取得
・Sceneがもつ各Domainを制御
・Interactorの情報を取得し、それをもとにDomainを制御
・Domainから情報を取得し、表示や音などを発生するためにPresenterに渡す
・DOM制御(ボタンの表示等) -
Domain.
Parts(自機・敵など)を定義して、PartsManagerがそれらを制御します。 -
Presenter.
Canvasによる画面表示や、サウンドを発生します。 -
Repository.
外部のデータ(Firebase、APIが利用できるCMS等)にアクセスして、
シューティングゲームにおいては、ステージ情報や、ハイスコア情報を取得します。
3-3. TypeScript換装時に苦労したこと
敵クラスを作る際、JavaScriptでは以下方針を基に実装してました。
- 基本機能は親クラスメソッドが実装、ただし親クラスはそれ自身からインスタンスさせない
- 子クラスに、基本+独自機能があれば親クラスメソッドを上書(オーバーライド)する
たとえば、親クラスでは位置x, y
設定を基本機能とし、
ある敵ではさらに角度を設定したい場合、JavaScriptでは以下の実装をしてました。
class EnemyParent {
set = ({ x, y }) => {
this.x = x;
this.y = y;
};
}
class EnemyChild extends EnemyParent {
set = ({ x, y, radian }) => {
this.x = x;
this.y = y;
this.radian = radian; // ←角度を追加
};
}
const childObj = new EnemyChild();
childObj.set({ x: 10, y: 10, radian: 20 });
ところがTypeScriptに換装したところ、以下のエラーに直面します。
親クラスのクラスメソッドset
に対して、引数・戻値の型が異なるのはいけないようです・・・。
上記のサイトに対策があったのですが、とにかく即決で解決したかった思いもあって・・・、
親クラスに対して、子クラスが必要であろうプロパティをオプションとして実装し直すことにしました。
もちろんこれが根幹対策ではないと思ってます・・・。
abstract class EnemyParent {
protected x: number = 0;
protected y: number = 0;
set = ({ x, y, radian }: { x: number, y: number, radian?: number }) => {
this.x = x;
this.y = y;
if (radian) {
// 'radian' is defined but never used.eslint@typescript-eslint/no-unused-vars
// ↑対策でわざわざradianに対してif文書いてます・・・。
}
};
}
class EnemyChild extends EnemyParent {
private radian: number = 0;
set = ({ x, y, radian }: { x: number, y: number, radian?: number }) => {
this.x = x;
this.y = y;
this.radian = radian || 0; // radian未定義の場合0
};
}
const childObj = new EnemyChild();
childObj.set({ x: 10, y: 10, radian: 20 });
これは敵に限らず、上記に該当するクラス全てを改修することになり、結構時間を要してしまいました。
3-4. Jestの導入(Mocha+ChaiからJestへ)
元々、このゲームは全jsファイルをhtmlファイルに<script>
でインポートする、原始的な実装でした。
当時は変数チェックする際、Chormeのデベロッパーツールで、ブレイクポイントやconsole.log
使ったランタイムチェックだけでした。
ただ、60fps上での動作確認は、目的のタイミングに合わせられなかったり、
console.log()の高負荷でブラウザが落ちたりするなど結構不便でした。
何かJestのようなテストツールがないか検索したところ、
Mocha + Chaiによるテスト方法があったので、以下参考に実装しました。
ソースイメージは以下な感じです。
<html>
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/9.0.2/mocha.css">
</head>
<body>
<!-- テスト対象のスクリプト -->
<script src="(ソース1.js)"></script>
...
<script src="(ソースn.js)"></script>
<!-- Mochaを動かすための準備 -->
<div id="mocha"></div>
<script>
const mocha = window.mocha;
mocha.setup('bdd');
</script>
<script>
const chai = window.chai;
const expect = chai.expect;
</script>
<!-- ソース単位にテストコードを作成しインポートする -->
<script src="(ソース1 テストケース.js)"></script>
...
<script src="(ソースn テストケース.js)"></script>
<!-- mochaを実行 -->
<script>
mocha.checkLeaks();
mocha.run();
</script>
</body>
</html>
テスト結果を以下図のように、ブラウザで結果が確認できます。
※全てOKのパターン
ただ、新機能追加でjsファイルを追加・変更するたびにhtmlファイルを変更。
さらにブラウザでの確認が段々億劫に感じ、TypeScript化にあわせてJestに換装するのでした・・・。
3-5. Mocha+Chai→Jestの換装
Jest換装は以下の観点で行ったのですが、大量テスト数の換装以外は特に大変なことはなかったです。
- TypeScript化
-
to.deep.equal(Mocha)
→toEqual(Jest)
- モック部分の改修(
jest.fn()
に換装)
テスト自体、ランタイムで気づけなかった不具合がいくつか発見できました。
(最初からTDDライクな感じで先にテストコードを書くようにしておけばよかった・・・。)
テスト結果によるカバレッジが見られるのは良いですね。
なお、現時点テスト対象は、膨大なコード量もあってDomain
のみとし、
今後、新規追加・変更等があった場合は、都度テストコードを書くようにしてます。
4. まとめ
ここでは自作シューティングの追加機能と、TypeScript換装化について記載しました。
GitではLanguages
にて、TypeScriptが全体を占める割合にすることができました。
TypeScriptのメリットでもある型定義のおかげで、データの取り扱いが分かりやすく、
実際に、vscodeでカーソルに合わせればポップアップで型定義が確認できるので大変助かってます。
また、ESModule化したことで、vscode使って開発の利便・拡張性があがったと思います。
例えば、メソッドの参照をcommandキー(Mac)で参照が意外に便利でした。
(JavaScriptの場合、検索で探すしか知らず・・・。)
今後ですが、直近ではFirebase9を導入したいと思います。
これは、Repository
でステージやスコア情報を保持・アクセスする機能追加のためです。
環境情報を.env
使って実装できたらいいなぁ。
ちなみに、Firebase9では<script>
タグでの利用ができなくなったみたいなんですよね・・・。
(TypeScript換装化した理由の一つでもあります)
また、このシューティングゲームは、ステージをUIで変更できるツール(vue2ベース)も実装ずみで、こちらはNuxt3(現時点ではベータ版)を使って体験しながら換装する予定です。
5. 参考
おまけ -アケステでプレーしてみました(2022/01/03追加)
この自作ゲームをアケステでプレーしたいと思いまして。
しかし、当時Macのみ所有してた身として、アケステ対応Macがあまり見当たらないことで詰まり、Windowsマシン購入を決めるのですが、今後Windowsに触れる機会が少ないことを見越し、場所を取らない省スペースの筐体を選びました。
購入したのは以下です。
- アケステ
8BitDo Arcade Stick - Windowsマシン
MeLE Intel J4125/8GB+128GB/Win10Pro
ブラウザはEdge (Chromium)
- ゲームパッド用キーエミュレーション・ソフト
JoyAdapter
キーマッピングして設定したのち、無事に動作したのでした。
なお、このアケステは、Macに対してキーマッピング設定自体は可能でしたが、
対象はボタンのみで、スティックに対して設定できずで諦めました。
(Karabiner-Elements: 14.3.0を使用しました)