超絶久々の Cities Box.cpp 開発日記です。前回はなんと4年前。えぇ…

こんなに空いてしまって申し訳ないです。個人プロジェクトはゴールと期日を定めてないとこうなります。趣味の開発だろうとクライテリアは設定しておかないとダメですね。

ぶっちゃけ新機能の実装など目新しい変更は現時点ではまだないです。ただ、一部処理を Rust に移行し始めており、今回はその辺の話を書きたいと思います。

HLQfUeeaQAAr26F

Rust 移行への道のり

そもそもなぜ Rust か

一番の理由は Rust が好きだから、C++ を書きたくないから、これに尽きます。…いや、別に言語の宗教論争をしたいわけではありません。ただ、「個人的に」これまでコードを書いてきた結果として書きやすい&開発しやすいのが Rust だった、というだけです。

Cities Box.cpp の開発にあたって、これまでに何度もメモリリークやらぬるぽやらに悩まされてきました。もちろん私のコードの書き方が悪いのはそうなんですが、2019年からと長年の開発を続けていくとどうしても技術的負債やら過去の自分を殴りたくなるような実装やらがあって、規模もそれなりに大きくなってくると、管理保守がしづらくなってくるものです。そうなると、ぬるぽエラーが発生してもエラーの原因の特定が難しい。コールスタックを辿っても、あれこれどこで代入してるんだっけ、とコードを辿っていくのが億劫です。

Rust ならこういったエラーに悩まされる心配がない…とは言い切れません。ただ、コンパイルエラーが親切でわかりやすいですし、unsafe なコードを書かない限りはコンパイル段階である程度検証してくれます。だから開発が楽なのは確かです。また副次的なメリットとして、Codex など生成AIにコードを書かせても、コンパイルさえ通ればとりあえずはメモリ関連のバグの心配はなさそうだな、という安心感が得られます。もちろんコードレビューは必須という前提の上での話です。

実装方針

Cities Box.cpp は GUI 描画を OpenSiv3D に頼っています。この方針は今後も変える予定はありません。OpenSiv3D は直感的に GUI オブジェクトを配置できる優れたフレームワークですし、GUI 部分まで Rust に移植するのは学習コストと実装コストがかかります。

ですので、マップ関連処理を始めとするデータ処理を中心に Rust に移植することとしました。要は GUI 描画をはじめとするフロントエンド処理は OpenSiv3D / C++ で継続し、マップ関連のデータ処理やゲームの進行処理などバックエンド処理を Rust に移行します。

現在の構成はこんな感じです。

src
├── AbstractSetting.cpp
├── AbstractSetting.hpp
├── AddonDirectionStruct.hpp
├── AddonLayer.cpp
(中略)
├── UnitaryTools.cpp
├── UnitaryTools.hpp
├── citiesbox-rs
│   ├── Cargo.lock
│   ├── Cargo.toml
│   ├── citiesbox-rs.code-workspace
│   └── src
├── cxx.h
└── ...

AbstractSetting.cpp をはじめとする旧来の C++ ファイルと同じ場所に citiesbox-rs というクレートが配置されています。これが移植先の Rust クレートです。

citiesbox-rscxx.h を通して C++ から参照可能なライブラリとして実装しています。現状は C++ 側から Rust 側のライブラリを呼び出すような形になっていますし、最終的なマップの状態などを保持するのは C++ 側になっています。このあたりも Rust 側から呼び出せるようにできたら素敵ですが、OpenSiv3D との共存が難しそう…。

現在の状況

1. 都市シミュレーション (citiesbox-rs/src/simulation/)

Rust側の SimulationState で都市の状態を一元管理しています。

  • ゲーム内日時
  • 総人口
  • 所持金
  • 気温
  • RCOIF需要
  • 警察・消防・郵便・教育予算
  • 住宅・商業・オフィス・工業・農業の税率

日付をまたぐ更新では citiesbox-rs でこれらの処理を順番に回します。

  1. 人口更新
  2. 就職・就学更新
  3. 月初なら税収支更新
  4. RCOIF需要更新

個別には次の処理が移行済みです。

  • 日時の進行 (citiesbox-rs/src/simulation/time.rs)
    • 分・時・日・月・年の繰り上げ
    • 月末・年末処理
    • 平年と閏年の2月判定
  • 人口増減 (citiesbox-rs/src/simulation/population.rs)
    • 住宅への転入・転出
    • 定員の考慮
    • 住民の年齢・性別データ管理
    • 年始の加齢と死亡処理
    • 総人口の再集計
    • 転出時の勤務先情報との整合
  • 就職・就学 (citiesbox-rs/src/simulation/employment.rs)
    • 年齢に基づく勤務先割当
    • 小学校から大学までの就学先割当
    • 施設定員の制御
    • 消滅した施設や不適格な割当の解除
    • 同一住民への就職・就学の重複防止
    • 複数タイル施設への人数反映
  • 税・予算・建設費 (citiesbox-rs/src/simulation/finance.rs)
    • 月初の税収計算
    • 公共施設の維持費支出
    • 複数月経過時の月ごとの収支処理
    • 建設成功時に5を徴収
  • 気温 (citiesbox-rs/src/simulation/temperature.rs)
    • 時間帯別の上昇・下降確率
    • 季節ごとの最低・最高気温
  • RCOIF需要 (citiesbox-rs/src/simulation/demand.rs)
    • 地価・犯罪率・教育率から住宅需要を算出
    • 商業・オフィス・工業・農業需要の更新
    • 就職時の需要減少
    • HSP版由来の乱数・下限制御を維持

2. マップの読込 (citiesbox-rs/src/citymap/load.rs)

マップファイルの読み込みと解析処理。

  • 現行バージョン形式のマップ読込
  • Release 142の元形式の読込
  • 旧形式から現行状態への変換
  • アドオン情報を参照した検証
  • オブジェクト、タイル、住民、各種レート、勤務先、学校の復元
  • 時刻・人口・資金・需要・予算・税率などの復元

3. マップの保存 (citiesbox-rs/src/citymap/save.rs)

保存処理もRust側へ移行済みです。

  • C++ 側で保持しているタイル・オブジェクト状態をRustへ同期
  • JSON保存形式の生成
  • 従来形式に合わせたXOR処理
  • 一時ファイルを使ったアトミック保存
  • 直前のセーブデータを .bak として保持
  • 時刻・人口・資金・気温・需要・予算・税率の保存

現在の役割分担

C++側

  • OpenSiv3Dによる描画・UI・入力
  • タイルやゲームオブジェクトの実体
  • 現在のマップ情報の収集
  • Rustの更新結果を画面上のオブジェクトへ反映

Rust側

  • 都市シミュレーションの状態と計算
  • マップファイルの解析・検証
  • セーブデータ生成と安全な書込み

マップの読み込み・保存とシミュレーション状態の更新は Rust に、描画処理などは C++ 側にと、わりと綺麗に分かれてきたかなぁと思います。

ただ、タイルやゲームオブジェクトの実体はまだ C++ 側で持っているので、こちらも Rust 側で持たせられるようにしたいです。

テストの実装

これまでの Cities Box.cpp にはテストが一切実装されていませんでした。これは由々しき事態です。というわけで citiesbox-rs 側にテストフレームワークも実装し、GitHub Actions で回せるようにしました。テスト内容はこんな感じ。

  • マップ入出力 (citiesbox-rs/src/citymap/mod.rs)
    • JSON ファイル生成
    • 建設費の更新・反映
    • セーブデータ保存
    • マップ読み込み
    • 旧バージョンフォーマットのマップ読み込み
    • 旧バージョンフォーマットのマップの変換
    • マップの読み込み→更新→保存→再読み込みをしてみて、一連のマップ状態が保持されているか
  • 日時関連(citiesbox-rs/src/simulation/time.rs
    • 60分進めたときの時刻チェック
    • 24時間進めたときの時刻チェック
    • 月末・年末の繰り上げ
    • 平年と閏年の2月
  • シミュレーション関連(citiesbox-rs/src/simulation/state.rs
    • 複数日を一度に進めた場合、経過日数分の日時処理が走ること
    • 日付をまたがない場合、日時処理が走らないこと
  • 人口関連(citiesbox-rs/src/simulation/population.rs
    • 空き住宅に対する転入判定
    • 入居済み住宅に対する人口変動判定
    • 転出処理
    • 住民の年齢更新処理
    • 総人口集計
    • 転出した住民の勤務先情報と、勤務先側の労働者数の整合
  • 就学・就職関連(citiesbox-rs/src/simulation/employment.rs
    • 存在しない施設、不適格な年齢、定員超過の割当を解除
    • 年齢に応じた就職確率と、就職によるRCOIF需要減少
    • 小学校・中学校・高校・大学への年齢別割当
    • 就職と就学が同じ住民に重複しないこと
    • 複数タイルで構成された同一施設に人数を正しく反映
  • 税金・予算・建設費関連(citiesbox-rs/src/simulation/finance.rs
    • 月初に税収と公共施設の維持費を計算
    • 月初以外には収支を更新しないこと
    • 複数月をまたいだ場合、各月の収支を処理
    • 建設費計算
  • RCOIF 需要(citiesbox-rs/src/simulation/demand.rs
    • 空の都市や対応する業務地区がない場合の需要
    • 地価・犯罪率・教育率による住宅需要
    • 商業・オフィス・工業・農業需要の更新
  • 気温関連(citiesbox-rs/src/simulation/temperature.rs
    • 気温変更判定に外れた場合は変化しないこと
    • 昼夜の気温上昇・下降確率
    • 春夏秋冬それぞれの最低・最高気温
    • 1回のマップ更新につき最大1回だけ変化すること
    • 経過時間が0または負なら変化しないこと

現状 C++ 側はテスト実装できていないですが、まあこっちは GUI 処理だけを残す予定なので良いかな。

動作上の変化

年齢変化や就職・就学、建物を建てたことによる資金現象といった処理はまだ Cities Box.cpp に実装していなかったので、このあたりは HSP3 版 Cities Box から移植しました。

おわりに

まだ Cities Box.cpp の開発は中断していません。かなり更新頻度は低いですが、着実に理想のものを作っていきたいと思います。