水シミュレーション

昔販売されていた AQUAZONE 風のゲームを作ろうと思い、仕様を練っています。アクアリウムの主役は「魚」ですが、シミュレータの仕様を決めるにあたってキモとなるものは、タイトルにある通り「水」(水質)です。

そこで AQUAZONE ではどうなっていたかアプリを実行して確認してみました。

AQUAZONE ではこのように水質の情報を見ることができます。今回は新しい水槽を立ち上げたときのパラメータを確認しておきます。

ラベル名称初期値最小値最大値
Temp/℃水温26.5 ℃0.050.0
pH水素イオン濃度7.00.014.0
GH総硬度4.00.020.0
O2酸素8.000 mg/l0.010.0
CO2二酸化炭素15.000 mg/l0.0100.0
HNO3硝酸0.000 mg/l0.0100.0
NH3アンモニア0.000 mg/l0.010.0
Cl塩素1.100 mg/l0.020.0
Mgマグネシウム0.000 mg/l0.0300.0
Caカルシウム42.000 mg/l0.0300.0
Proteinタンパク質0.000 mg/l0.0100.0
Carbohydrate炭水化物0.000 mg/l0.0100.0
Fat脂質0.000 mg/l0.0100.0
Vitaminビタミン0.000 mg/l0.0100.0

現実で水槽を管理するときに気を付ける成分以外にも、食べ物に含まれる成分(タンパク質・炭水化物・脂質・ビタミン)が細かくパラメータ化されていました。これは餌が水質に与える影響を計算するためでしょうか。自分が考えていた仕様よりも細かい。。。他に気になるのは最初から水温が 26.5 ℃ で温水を汲んでいるのかとか、塩素濃度が 1.100 mg/l で国内では超えないようにされている目標値の 1.000 mg/l を超えている点とか。

水温は水換えのたびに調整するのは面倒そうだし、塩素濃度は薄すぎてもゲーム性が欠けるというのもあり、単純に現実に合わせればいいというわけでもないのでどう調整しようかといったところ。。。

C++からC#をスクリプトとして実行

この記事は 「プロ生ちゃん Advent Calender 2016」 6日目の記事です。

はじめに

ゲーム開発で C++ のコードからスクリプト言語を利用するとき Lua や AngelScript といった選択肢がありますが、そーいえば C# 自身が cs ファイルのコンパイル機能を持ってたなーと思い、テスト的に作ってみて COM を作ったりだとか面倒だった構成をどこまでシンプルにできるか挑戦してみました。

ここでは最適化の過程は省略して、今回得られた手順について説明していきます。

1. CLI でスタティックライブラリを作る

cs ファイルのコンパイル機能は C# の機能のため C++ からは直接呼び出せません。そこで、CLI で作ったライブラリを中継させます。CLI をスタティックライブラリにするのは正規の使い方から外れている気がしますが、動けばよしという方針でやっていきます。

とりあえず空の CLR プロジェクト (.Net Framework 4) を作ります。
プロジェクト名は LibCli としておきました。

プロジェクトのプロパティを開いて、
構成プロパティ >> 全般 >> 構成の種類
を アプリケーション (.exe) から スタティックライブラリ(.lib) に変えます。

cs スクリプトのコンパイル&実行を行うコードを書きます。

ヘッダに CLI 形式のコードが紛れていると C++ の方でエラーになってしまうので cpp とヘッダはきちんと分離しておきます。
IScript は cs スクリプトで実装するインターフェースです。
cs スクリプトのファイル名は “TestScript.cs” 、クラス名は “Test.TestScript” と決め打ちにしています。

あと、この3つのコードを追加しただけではビルドが失敗します。コメントに書いた
Microsoft.CSharp
System
System.Windows.Forms
をプロジェクトの参照に追加してからビルドします。

ここまでやってビルドが通らないときは .Net Framework が 4 意外になっている疑いがあります。

2. C++ で exe を作る

C++ で exe を作って、CLI のライブラリを組み込みます。
ソリューションに Win32 コンソールアプリケーションを新規追加します。
アプリケーション設定は、コンソールアプリケーションと、空のプロジェクトにチェック。Security Development Lifecycle のチェックは外した状態にします。
プロジェクト名は CppMain にしました。

CLI で作ったスタティックライブラリをリンクするようにします。
追加した CppMain のプロジェクト設定を開いて
構成プロパティ >> リンカー >> 入力 >> 追加の依存ファイル
に ..\$(Configuration)\LibCli.lib を追加します。
$(Configuration) はマクロで、ビルド構成に合わせて Debug や Release に置換されます。

LibCli.lib は LibCli をビルドした後に生成されるので、LibCli をビルドした後に CppMain をビルドするようにします。
CppMain のプロジェクトを右クリックして
ビルド依存関係 >> プロジェクト依存関係
を選択し、LibCli にチェックを入れます。

プロジェクトの設定は以上で、残りは CLI で書いた関数の呼び出しコードを書くだけです。

CppMain をスタートアッププロジェクトに設定して、ビルドして、実行して、例外が出れば成功です。

3. cs スクリプトを書く

さきほどの例外は cs スクリプトを書いてなかったせいです。cs スクリプトさえ書いてやれば問題なく動きます。

これを CppMain プロジェクトのフォルダ、main.cpp と同じ所に置いて実行すると cs スクリプトに書いたメッセージボックスが出るようになります。

ここまで作業を行ったソリューションファイルです -> CsScriptTest_0.zip

4. cs スクリプトから C++ の関数を呼び出す

今までの説明で cs スクリプトのビルド&実行が最低限動くようになりましたが、cs スクリプトから C++ の関数の呼び出しができないと実用性が薄れるのでそこも試しにやってみます。Lua や AngelScript でいうバインディングってやつですね。

関数を共有するためには CppMain と LibCli から参照する新しいプロジェクトを追加するのが正攻法ですが、今回は最小限に抑えるため LibCli に共有インターフェースを追加してしまいます。

SharedInterface は CppMain と LibCli で共有するインターフェース。SharedWrapper は C# から SharedInterface を使うためのラッパークラスです。

SharedInterface を利用するように LibCli を書き換えます。

CppMain の方には SharedInterface の実装を追加します。

LibCliMain() の呼び出し時に SharedData を渡すようにします。

最後に、cs スクリプトを書き換えて完了です。

どこかのタイミングで dll を配置しないといけなくなるかと思っていましたが、終わりまで exe 1個で済んでしまいました。

あと、説明なしにさらっと回避処理を紛れ込ませていましたが、普通に cs ファイルのコンパイル・実行コードを書いた場合は TestScript.cs から LibCli への参照が登録されておらず LibCli.IScript や LibCli.SharedWrapper を参照できません。
でも LibCli はスタティックライブラリになってるしどうすっぺという感じですが、LibCli.cpp で

とやっている部分がミソです。実は exe も dll 的なふるまいをするので、ここで exe を dll として登録しています。

最終的にできあがったソリューションファイルです -> CsScriptTest_1.zip

おわりに

この方法は、設計的にはきれいにまとまりましたが、CLI でスタティックライブラリを使う場合にいろいろと問題が出てしまいました。

まず、.Net Framework 4 と指定していた所ですが、どうやら新しい .Net Framework ではスタティックライブラリが作れないようです。Debug ビルドはできても Release ビルド時に mscorlib.dll が見つからないとエラーになったりします。また無理にリンクして動かしたとしても、アプリケーション実行時にマネージド領域が初期化されないとか何かでC#のメソッドが正しく動きませんでした。

さらに、ソースコードの編集でインテリセンスが効かなくなるという問題もありました。これは結構きついです。

CLI でスタティックライブラリを作れるとこういう面白いことができるなーと思ったのですが、MS さん的にはサポートしていかない感じなんですかね。DLL にすれば解決する話でもあるのですが、やっぱりスタティックリンクがいいなー