自作音ゲーを旧Siv3DからOpenSiv3Dに移植しました。

# はじめに

### 自作ゲームの紹介コーナー
「ColorfulTone」は2016年にリリースした、色をテーマにした音楽ゲームです。
既に定期更新は終了となりましたが、長年のバージョンアップの末に200曲を越える楽曲が収録されております。
長い間応援ありがとうございました。改めてお礼を申し上げます。

https://www.freem.ne.jp/win/game/14167

ソースコードもGitHubで公開しております。

https://github.com/tyanmahou/ColorfulTone

さて、そんな「ColorfulTone」ですが、今年の春に大型アップデートをして
その際にもともと旧Siv3D製だったのを、おもいきってOpenSiv3Dに移植しました。
その苦労話などをできればと思います。

# きっかけ
そもそも、なぜ新バージョンの開発、そして移植することになったのかというと

ありがたいことに、まだ✨遊んでくださっている方々✨がいらっしゃいまして
要望などの声が届いていたからです。

しかし、2016年にリリースしただけあって様々な問題がありました。

- 旧Siv3D辛い
    - もう更新はないので、ずっと過去のエンジンを使い続けるのがしんどい気持ちがありました
- C++14辛い
    - 旧Siv3DはC++14環境なので、C++17やC++20といった最新C++が使用できないもどかしさがありました。
- そもそも開発環境も消しちゃった
    - 上記の開発環境を残しておくのも面倒なのと、もう触ることないと思って消しちゃったので、再構築が面倒だった
- 2016年の自分のソースコード汚すぎ
    - あまり触りたくない
    - 
まだプログラミング歴2年くらいの学生だったし、こんなもんやろ、許して…許して…


そういった理由から、新機能の開発をするなら、今後も見据えて、いっそOpenSiv3Dに乗り換えるかと決意し
重い腰をあげて移植する戦いが始まりました。

:::note info
ちなみに、OpenSiv3Dが出た当初は、まだ機能が旧Siv3Dに追いついておらず、早めの移植が不可能だったという経緯もあるが、OpenSiv3Dの機能もかなり揃ってきたので移植可能となった
:::

# 移植との戦い

### API変更との戦い
旧Siv3DとOpenSiv3Dは沢山のAPIが変わっており、互換性がない部分が多いです。
この辺りの修正は物量的に大変でした。

一気に修正するのも大変なのでシステム単位やScene単位とかで徐々に対応していきました。

#### 例 Stringの変更
```cpp:旧Siv3D
L"Hello World"
```
```cpp:OpenSiv3D
U"Hello World"
```
#### 例 入力の変更
```cpp:旧Siv3D
Input::KeyEnter.clicked
```
```cpp:OpenSiv3D
KeyEnter::down()
```
#### 例 オーディオの変更
`Sound`⇒`Audio`になり
OpenSiv3Dだと直接`Wave`がとれなかったり
```cpp:旧Siv3D
// wav取得
Sound sound = nowMusic.getSound();
Wave wav = sound.getWave();
```

```cpp:OpenSiv3D
// wav取得
Audio sound = nowMusic.getSound();
Wave wav = [](const Audio& sound)->Wave {
    Array<WaveSample> wavSamples;
    size_t sampleLength = sound.samples();
    wavSamples.reserve(sampleLength);

    const float* left = sound.getSamples(0);
    const float* right = sound.getSamples(1);
    for (size_t i = 0; i < sampleLength; ++i) {
        wavSamples.emplace_back((*left), (*right));
        ++left;
        ++right;
    }
    return Wave{ std::move(wavSamples) };
}(sound);
```

#### 例 Fontの変更
drawKineticが廃止されてglyphを扱えるようになったり
```cpp:旧Siv3D
font(text).drawKinetic(topLeft.x, topLeft.y, [](s3d::KineticTypography& k){
    if (k.ch == ' ')
        k.col.setAlpha(0);
}, color);
```

```cpp:OpenSiv3D
Vec2 penPos{ topLeft };
for (const Glyph& glyph : font.getGlyphs(text)) {
	// 改行文字なら
	if (glyph.codePoint == U'\n') {
		// ペンの X 座標をリセット
		penPos.x = topLeft.x;

		// ペンの Y 座標をフォントの高さ分進める
		penPos.y += font.height();

		continue;
	}

	if (glyph.codePoint != U' ') {
		glyph.texture.draw(Math::Round(penPos + glyph.getOffset()), color);
	}

	penPos.x += glyph.xAdvance;
}
```
などなど
全部は上げきれないのですがこういったこまごま変更が多いです。

### シェーダー調整との戦い
シェーダーも少し変わっているので、全部調整しました。
そこまで変化はないですが

```hlsl:旧Siv3D
Texture2D g_texture0 : register(t0);
SamplerState g_sampler0 : register(s0);

struct VS_OUTPUT
{
	float4 position : SV_POSITION;
	float2 tex : TEXCOORD0;
	float4 color : COLOR0;
};
float4 PS(VS_OUTPUT input) : SV_Target
{
	return g_texture0.Sample(g_sampler0, input.tex) * input.color;
}
```

```hlsl:OpenSiv3D
Texture2D g_texture0 : register(t0);
SamplerState g_sampler0 : register(s0);

cbuffer PSConstants2D : register(b0)
{
    float4 g_colorAdd;
    float4 g_sdfParam;
    float4 g_sdfOutlineColor;
    float4 g_sdfShadowColor;
    float4 g_internal;
}
struct PSInput
{
    float4 position : SV_POSITION;
    float4 color : COLOR0;
    float2 uv : TEXCOORD0;
};
float4 PS(PSInput input) : SV_TARGET
{
    return g_texture0.Sample(g_sampler0, input.uv) * input.color + g_colorAdd;
}
```

### ステンシルマスク修正との戦い
旧Siv3Dはステンシルステートに対応していたので、ステンシルを使ったマスクができました。
OpenSiv3Dはステンシルステートが現状使えないので、シェーダーを使ったマスク表現でカバーします。

```cpp:旧Siv3D
Graphics2D::SetStencilState(StencilState::Replace);
Graphics2D::SetStencilValue(stencilNum);
// いろいろ
Graphics2D::SetStencilState(StencilState::Test(StencilFunc::Equal));
// いろいろ
Graphics2D::SetStencilState(StencilState::Default);
```

```cpp:OpenSiv3D
// ↓は自前実装なのでSiv3DのAPIじゃないよ
auto scopedMask = Shaders::Mask(index).equal([&] {
	// いろいろ
});
// いろいろ
```

Shaderを使ったマスク処理は以前の記事でも紹介してます

https://qiita.com/tyanmahou/items/6a6b6dff7a02738356db

### Font修正との戦い

Siv3Dで用意されているフォントが変わって、同じコードで同じフォントサイズでも
見た目が大きく変わってしまった問題があった。

これはどうすることも出来ないので、目コピでなるべく旧作の見た目に合うように移植した。

また、アウトラインの扱い方や、drawKineticなどのAPIも大きく変わったので、その辺の修正もたくさんあった。

### エンコードとの戦い

旧Siv3DはShift-JISが扱えていて
譜面ファイルなどが全部Shift-JISで作られていた

OpenSiv3Dで譜面ファイルをロードしたら文字化けがおこりまくって
全てUTF-8に修正する羽目になった。

----
ところで、この文字コードの一括修正は既にダウンロード済みのユーザー環境でも自動修正してあげる必要があったので
プログラムで一括変換する処理を実装して、ゲーム起動時に実行するようにしたのだが
エンコード変換処理も頑張って自分で書くはめにになり。。。大変だった

### GUI作り直しとの戦い
旧Siv3DにあったGUIのシステムがなくなって、OpenSiv3Dは全く違った仕組みになっているので
ほぼ完全に作り直しになったのが大変だった

----
そんなこんなで、いろいろと大変だった

# 得られた恩恵

OpenSiv3D/C++20になったことで、得られた恩恵も沢山ありました。

### AudioのMixBus
OpenSiv3Dから使えるようになったMixBusにより、BGMやSEなど種別毎に音量を変更する仕組みが実装できるようになりました。

```cpp
struct MixBusKind
{
    inline static constexpr s3d::MixBus Bgm = s3d::MixBus::Index0;
    inline static constexpr s3d::MixBus Se = s3d::MixBus::Index1;
    inline static constexpr s3d::MixBus InGameMusic = s3d::MixBus::Index2;
    inline static constexpr s3d::MixBus InGameSe = s3d::MixBus::Index3;
};
// SEの音量調整
GlobalAudio::BusSetVolume(MixBusKind::Se, volume);

// SEのバスで再生
AudioAsset(name).playOneShot(MixBusKind::Se, 1.0);
```

### Scoped系が超便利
レンダーステートの変更などを多用するので非常に便利
(旧Siv3Dでも自前に実装することで実現はできた。)

```cpp
ScopedRenderStates2D blend(BlendState::Additive);
```

### 用意されたシェーダー

OpenSiv3D側でいろいろとシェーダーを用意してもらってるので
自前で書いてたやつを使わなくてよくなった

```cpp
Shader::GaussianBlur(region, irt, rt);
```

### Inputの名前がとれるようになった
キーコンフィグとかを実装しているので地味に嬉しかった事

```cpp:OpenSiv3D
//キーの名前取得
const String GetKeyName(const Input& key)
{
    switch (key.deviceType()) {
    case InputDeviceType::Keyboard:
    case InputDeviceType::Gamepad:
    case InputDeviceType::XInput:
        return key.name();

    default:
        break;
    }
    return U"----";
}
```

以前は自前で実装してた…
```cpp:旧Siv3D
String GetKeyboardName(const uint8 code)
{
	static std::unordered_map<uint8, String> dictionary
	{
    	{ 0x00,L"----" },
		{ 0x01,L"MouseL" },
		{ 0x02,L"MouseR" },
		{ 0x04,L"MouseM" },
...
```

### C++20コルーチンによる非同期処理
C++20が使えるのでコルーチンなどを導入できた。

```cpp
// ロード処理
// ↓Fiberは自前の仕組みだからSiv3DのAPIじゃないよ
Coro::Fiber<void> FileLoadScene::updateAsync()
{
	// マイグレーション処理
	co_await Thread::Task(MigrationSystem::Up);

	// ロード開始
	co_await Thread::Task(::LoadContentsData);

	// ロード完了
	m_state = State::LoadCompleted;
	m_view.onCompleted();

	co_await Coro::FiberUtil::WaitForSeconds(0.8s);

	// シーン遷移
	auto& musics = Game::Musics();
	if (musics.size() == 0) {
		// 楽曲なし
		System::Exit();
	} else {
		changeScene(SceneName::Title, 1000);
	}
}
```

コルーチンに関しては以前の記事でも紹介しています

https://qiita.com/tyanmahou/items/1799d80c9e260b2267d5

# おまけ：その他大変だったこと

- 可変フレームレート対応
    - 昔はよくわかっておらず、deltaTimeの考慮ができてなかったので、基本修正した。
- 多少はコード整理とかした
    - リファクタに時間をかけすぎたくもなかったので、ちょこちょこ
- 音ゲーのコア部分もがっつり書き直した
    - ランダム配置や、BPM変化オフ設定に対応できるように仕組みを作り直した。
- Twitter(現X) API使えなくなった問題
    - 画像付きの投稿ができなくなったので、とりあえずテキストだけでもできるようにしておいた。
    - 
Ctrl Shift S でスクショを貼ってくれ

- 新機能盛りだくさん、やりきった:clap:

https://x.com/ColorfulToneJP/status/1782397899546190127

# まとめ
旧Siv3DからOpenSiv3Dに自作ゲームを移植した。
API変更など大変な部分や、つまづきポイントも多かったがなんとかやり切った。
「ColorfulTone」遊んでね🎵

https://www.freem.ne.jp/win/game/14167