OpenSiv3D + PokéAPI でクイズゲームを作りました

# はじめに

この記事では、[OpenSiv3D](https://siv3d.github.io/ja-jp/) と [PokéAPI](https://pokeapi.co/) を使って製作したゲーム[「ポケモンフレーバーはかせ」](https://voidproc.com/blog/archives/636)について、開発にあたり工夫した点を紹介させていただきます。

久しぶりに[ポケモンの新作](https://www.pokemon.co.jp/ex/sv/ja/)で遊んでいたのですが、ふとポケモンのデータセットって誰かがまとめて公開しているのでは？と思い、探してみたところ [PokéAPI](https://pokeapi.co/) なるものがありまして、様々なデータを JSON で取得できるようでした。
そこで、この PokéAPI と [OpenSiv3D](https://siv3d.github.io/ja-jp/) を使ってひとつゲームを作ってみようと思い、またタイミング良く [Siv3D Advent Calendar 2022](https://qiita.com/advent-calendar/2022/siv3d) が開催されていましたので、参加させていただきました。

# どんなゲーム？

ポケモンには「フレーバーテキスト」という、ポケモンについての説明文があるのですが、その文章からどのポケモンかを当てるクイズゲームです。

ランダムに出題される問題を 5 問連続で正解すればクリア、という単純なルールとしました。

https://twitter.com/voidproc/status/1600041139976818689

難易度は「グリーン」「ゴールド」「ダイヤ」「エキスパ」の 4 種類用意しています。
グリーンは初代のみ出題、エキスパは 900 種類くらいの中からの出題となります。腕に自信のある方にはエキスパのはかせを目指してみてほしいです（私はグリーンしか無理でした）。

# ダウンロード

https://voidproc.com/blog/archives/636

# 開発環境

- Windows 10
- Visual Studio 2022 Community 17.4.2
- OpenSiv3D 0.6.6

# PokéAPI とは？

https://pokeapi.co/about

PokéAPI は、ポケモンに関する様々なデータを REST API を通して提供していて、例えば `https://pokeapi.co/api/v2/pokemon-species/pikachu` とリクエストすると、ピカチュウの各言語での名前、説明文、登場した世代、どのポケモンから進化したか……などなど、色々なデータを JSON で取得することができます。
今回製作したゲームでは、ポケモンの日本語での名前・説明文、イラストの URL を取得するために利用させてもらっています。

# ソースコード

MIT ライセンスの下で公開しています。

https://github.com/voidproc/PokemonFlavorHakase

# 工夫した点など

## PokéAPI JSON データの非同期ダウンロード

Web からのデータ取得は環境によっては非常に時間がかかる可能性がある処理なので、ダウンロード中のフリーズ／フレームレート低下を避けるために非同期通信をしたいところです。
OpenSiv3D 0.6.6 では `SimpleHTTP::SaveAsync(url, saveFilePath)` を使ってファイルの非同期ダウンロードができるので、これを利用しました。
将来、`GetAsync` などが整備されればより使いやすくなりそうだと感じました。

`SimpleHTTP` についてチュートリアルのページ⬇️

https://zenn.dev/reputeless/books/siv3d-documentation/viewer/tutorial-http#29.4-%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E9%9D%9E%E5%90%8C%E6%9C%9F%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89%E3%81%99%E3%82%8B%EF%BC%88%E3%82%AD%E3%83%A3%E3%83%B3%E3%82%BB%E3%83%AB%E3%83%BB%E9%80%B2%E6%8D%97%E8%A1%A8%E7%A4%BA%EF%BC%89

## アセット（フォント）の非同期ロードについて

開発中、問題文が表示されるたびにフレームレートが落ちる現象がありました。今回、シーンの解像度を結構大きめ（1600x1200）に設定したため、文章を表示するためのフォントのサイズも大きめになっており、その生成に時間がかかっていると推測し、ゲーム起動時に教育漢字の一部をプリロードしておくことにしました（教育漢字全部をプリロードすると逆に起動時にとても待たされてしまってストレスだったため、読み込む範囲は小２学習分までに留めました）。

アセットの非同期ロードについてのチュートリアル⬇️

https://zenn.dev/reputeless/books/siv3d-documentation/viewer/tutorial-asset#20.6-%E3%82%A2%E3%82%BB%E3%83%83%E3%83%88%E3%81%AE%E9%9D%9E%E5%90%8C%E6%9C%9F%E3%83%AD%E3%83%BC%E3%83%89

## 出題のランダムさについて
例えば 1 問目がゼニガメだったとして、2 問目もゼニガメだったりカメックスだったりするとちょっとガッカリするのではと思います。そこで、次に出題するポケモン ID が直近 10 問で出た ID と近すぎる場合は再度抽選するようにしました。もちろんまた近い ID になる可能性はありますが。

ソースコードとしてはこのあたり⬇️

https://github.com/voidproc/PokemonFlavorHakase/blob/174f471282302f193c6c5c4e2a1b6f03275baa04/PokemonFlavorHakase/MainScene.cpp#L277

これは最近読んだ Web の記事で、テトリス・ザ・グランドマスターの出現テトリミノがそんな感じになっているとの事で、アイデアを拝借してみることにしました。

## テキスト入力欄の描画

日本語入力時には、変換未確定のテキストをグレーで表示すると分かりやすいと思い、実装してみました。`TextInput::GetEditingText()` で未確定のテキストが得られるので、入力済みテキストと合わせて 1 文字ずつ描画しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35599/a2856338-acf1-be9a-8d4f-f748d0a94b54.png)
カーソルもそれっぽく描画してみたり。

ソースコードとしてはこのあたり⬇️

https://github.com/voidproc/PokemonFlavorHakase/blob/174f471282302f193c6c5c4e2a1b6f03275baa04/PokemonFlavorHakase/MainScene.cpp#L587

テキスト入力についてのチュートリアル⬇️

https://zenn.dev/reputeless/books/siv3d-documentation/viewer/tutorial-keyboard#16.7-%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88%E5%85%A5%E5%8A%9B

テキストを 1 文字ずつ表示するチュートリアル⬇️

https://zenn.dev/reputeless/books/siv3d-documentation/viewer/tutorial-font#14.14-%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88%E3%82%92-1-%E6%96%87%E5%AD%97%E3%81%9A%E3%81%A4%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B

## ESC キーの扱い

`ESC` キーを押すと標準ではアプリ終了となるのですが、今回はユーザーに日本語入力をさせる必要があり、`ESC` キーが頻繁に押されることになるため、この動作は無効にしました。

```cpp
void Main()
{
    // ...
	System::SetTerminationTriggers(UserAction::CloseButtonClicked);
```

アプリを終了する手段は、×ボタンクリックや `Alt+F4` 押下など色々あるためそこまで問題ないと思います（本当はタイトル画面でアプリ終了を選択できる UI を用意できればよかったです）。

さて、ゲームプレイ中にタイトルに戻りたくなる事って結構あると思うのですが、そういう場合に（システムメニュー等の表示を期待して）まず押されるのが `ESC` キーじゃないでしょうか。ですので、今回はタイトルに戻る動作を割り当てました。押下後すぐ戻るのではなく、一定時間長押しすると戻るようにしています。
![20221206-224236-380.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35599/09888ede-4afc-b59e-8048-13796313de30.png)
個人的に気に入っています（決してメニューを作るのが面倒だったというわけではない）。

また、そういうインターフェースであるとユーザーに知らせるために、一瞬 `ESC` キーが押されたときには小さく「長押しでタイトルに戻れます」という案内を出しています。

## ウィンドウサイズの調整

まず、このゲームではシーンのサイズを 1600x1200 に設定しました。

```cpp
	Window::SetStyle(WindowStyle::Sizable);
	Scene::SetResizeMode(ResizeMode::Keep);

	const auto sceneSize = Size(1600, 1200);
	Scene::Resize(sceneSize);
```

で、可能なら実ウィンドウサイズをシーンと同じサイズにしたいのですが……

```cpp
    Window::ResizeActual(sceneSize);
```

これだと、モニタの解像度によってはウィンドウがモニタからはみ出してしまいます。なので、モニタから縦または横にはみ出る場合は適当にウィンドウサイズを小さくするようにしました。

```cpp
	// ウィンドウがディスプレイからはみ出す場合は適当に小さくする

	const auto workAreaSize = System::GetCurrentMonitor().workArea.size;

	if (sceneSize.y > workAreaSize.y)
	{
		const auto scale = 0.9 * workAreaSize.y / sceneSize.y;
		Window::ResizeActual(sceneSize.x * scale, sceneSize.y * scale);
	}
	else if (sceneSize.x > workAreaSize.x)
	{
		const auto scale = 0.9 * workAreaSize.x / sceneSize.x;
		Window::ResizeActual(sceneSize.x * scale, sceneSize.y * scale);
	}
	else
	{
		Window::ResizeActual(sceneSize);
	}
```

ウィンドウモードのアプリなので、解像度そのままでなくワークエリア（タスクバーなどを除いた領域）の大きさを使うのがいいと思います。また、ウィンドウのフレームやタイトルバーの分も考慮して、適当に 0.9 掛けのサイズにしています。

ソースコードとしてはこのあたり⬇️

https://github.com/voidproc/PokemonFlavorHakase/blob/174f471282302f193c6c5c4e2a1b6f03275baa04/PokemonFlavorHakase/Main.cpp#L24

## モンスターボールを描画する関数

連続正解数をモンスターボールの数で表示しているのですが、今回はできるだけ PokéAPI 以外の外部リソースを準備しないで何とかするという個人的な裏テーマがありましたので、OpenSiv3D の標準機能（Line, Circle）のみで描画してみました（自己満足）。

```cpp
void drawMonsterball(double r, const Vec2& pos)
{
	// LineとCircleでがんばってモンスターボールを描く…

	// 基本サイズ: r=24
	const double scale = r / 24.0;

	Circle(Arg::center = pos, r).draw(Palette::Red);
	Circle(Arg::center = pos, r).drawPie(90_deg, 180_deg, Palette::White);

	const auto glowShift = -13 * scale;
	const auto glowR = 4 * scale;
	Circle(Arg::center = pos.movedBy(glowShift, glowShift), glowR).draw(ColorF(1, 1));

	const auto centerLineWidth = 4 * scale;
	const auto lineColor = ColorF(0.2);
	Line(pos.movedBy(-r + centerLineWidth / 2, 0), pos.movedBy(r - centerLineWidth / 2, 0)).draw(centerLineWidth, lineColor);

	const auto centerCircleR = 10 * scale;
	Circle(Arg::center = pos.movedBy(0, 4), centerCircleR).draw(ColorF(0, 0.1)); //shadow

	const auto centerCircleLineWidth = 4 * scale;
	Circle(Arg::center = pos, centerCircleR)
		.draw(Palette::White)
		.drawFrame(centerCircleLineWidth, lineColor);

	const auto centerCircleR2 = 6 * scale;
	const auto centerCircleLineWidth2 = 1.5 * scale;
	Circle(Arg::center = pos, centerCircleR2)
		.draw(Palette::White)
		.drawFrame(centerCircleLineWidth2, ColorF(0.6));

	const auto outlineFrameWidth = 3 * scale;
	Circle(Arg::center = pos, r).drawFrame(outlineFrameWidth, lineColor);}
```

任意の大きさで描けます。引数に角度を渡して回転できるようにすれば良かった。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35599/6b608bcc-cc8f-896a-8cb0-3462d2bc57c4.png)

## 実行ファイルのアイコンを自作 (icon.ico)

ポケモンのモンスターボールは、非常にシンプルなデザインで作図しやすく、かつ一目でポケモンだと分かってもらえる素晴らしいシンボルですので、これを実行ファイルのアイコンにすることにしました。

以下の手順がお手軽でした。

- PowerPoint で作図し、グループ化して「図として保存...」で PNG 形式で保存
- Photoshop（なんでもよい）で 256x256 に縮小
- [Aseprite](https://store.steampowered.com/app/431730/Aseprite/) で ICO 形式で保存

![ppt_icon.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35599/4e064bfa-289f-67bb-441e-b67d87a18be0.png)
[Aseprite](https://store.steampowered.com/app/431730/Aseprite/) は高機能なドット絵エディタなのですが、今回は ICO 形式への変換のために使いました。非常に使いやすいソフトなのでとてもおすすめです。

https://www.aseprite.org/

# さいごに

ゲーム作ろう！と決めてから 1 週間足らずでリリースまで到達することができ、また一つ自分の作品が増えてとても満足しています。これは、OpenSiv3D がとても使いやすく、コードを書いていて楽しいライブラリであり、そしてまた Advent Calendar というイベントの相乗効果もあってモチベーションが高くいられたおかげと思っています。

このような素晴らしいライブラリを継続的にメンテナンス・提供していただいている Ryo Suzuki さん、コントリビュータの方々に感謝しつつ、引き続きこのイベントを楽しませていただこうと思います。