第6部:C#によるプログラミング
FreeTrainの機能をさらに深く拡張したい場合、XMLの記述だけではなくC#によるコーディングが必要になります。本章では、プラグインから独自の機能や画面を実装するための基礎知識を解説します。
開発環境について FreeTrainは .NET Framework 2.0 上で動作しています。 プラグインを開発する際は、必ずコンパイラのターゲットバージョンを .NET 2.0 (v2.0.50727) に設定してください。新しいバージョンの.NETでビルドしたDLLは読み込めません。
ソースコードの入手: 本体やライブラリのソースコードは、現在 Software Heritage archive 等のアーカイブサイトから取得可能です。内部APIの調査にはソースコードの参照が欠かせません。
6.1 コントリビューションの自作
XMLで指定したクラスを自作することで、既存の枠組みにとらわれない新しい要素をゲームに追加できます。
Contributionクラスの実装
すべての拡張要素は freetrain.framework.plugin.Contribution クラスを継承します。
using freetrain.framework.plugin;
public class MyNewPlugin : Contribution {
// XMLから動的にロードされる場合、このシグネチャのコンストラクタが必須です
public MyNewPlugin(XmlElement e) : base(e) {
// XMLからの読み込み処理
}
}
ファクトリの登録
新しい type を定義したい場合は、system/plugin.xml に倣って ContributionFactory を実装・登録する必要がありますが、通常は既存の type(menu や specialStructure など)を拡張するのが一般的です。
6.2 コントローラー(操作画面)の作成
プレイヤーがマップをクリックして何かを配置したり、情報を表示したりする画面を作るには、AbstractControllerImpl を継承します。
基本構造
public class MyController : AbstractControllerImpl {
// マップがクリックされた時の処理
public override void onClick(MapViewWindow source, Location loc, Point ab) {
// loc: クリックされたボクセルの座標
// ここに建設処理などを記述
}
// マウス移動時のプレビュー処理など
public override void onMouseMove(MapViewWindow view, Location loc, Point ab) {
// ...
}
}
メインウィンドウへのアタッチ
コントローラーを表示し、マップ操作を有効にするには、MainWindow.mainWindow.attachController(this) を呼び出します。これにより、マップ上のマウスイベントがそのコントローラーに送られるようになります。
6.3 世界(World)へのアクセス
ゲーム世界の情報は freetrain.world.World.world シングルトンオブジェクトを通じて操作します。
ボクセルの操作
- 取得:
Voxel v = World.world[x, y, z]; - 設置:
World.world[x, y, z] = new MyVoxel(...); - 範囲操作:
Cubeクラスを使うことで、矩形範囲のボクセルをまとめて扱うことができます。
地形と地価
- 地表の高さ:
int z = World.world.getGroundLevel(x, y); - 地価の取得:
int value = World.world.landValue[x, y];
6.4 グラフィックスプログラミング
独自の描画を行いたい場合は、freetrain.DirectXWrapper を使用します。
- Surface: 画像データを保持するオブジェクトです。
- Sprite:
Surfaceから特定範囲を切り出し、マップ上に描画するためのオブジェクトです。 - 描画:
QuarterViewDrawerを通じて、クォータービューの正しい前後関係で描画が行われます。
6.5 注意事項
- フレームワークのバージョン: 前述の通り、.NET Framework 2.0 が必須です。Visual Studioやcsc.exeのバージョンに注意してください。
- シリアル化: FreeTrainのセーブデータは .NET のバイナリシリアル化を使用しています。独自に作成したクラスをセーブデータに含める場合は、
[Serializable]属性を付与し、フィールドの変更には細心の注意を払ってください。 - スレッド: 描画や更新の多くはメインスレッドで行われます。重い処理を行う場合は、ゲームのレスポンスを損なわないよう配慮が必要です。
6.6 既存プラグインの実装事例分析
既存のプラグインがどのように機能を実装しているかを見ることは、新しいプラグインを開発する上で非常に参考になります。ここでは5つの事例を分析します。
事例1:ブルドーザー
plugins\org.kohsuke.freetrain.land.bulldoze
このプラグインは、指定された範囲内の地形や構造物を撤去して更地化を行う機能を持ちます。
- 継承クラス:
LandBuilderContribution- 土地造成系プラグインの基底クラスです。矩形範囲選択のUIなどが提供されます。
- 実装のポイント: 段階的な削除処理
createメソッド内で、選択された矩形範囲(x1, y1からx2, y2)を二重ループで走査します。- 各座標の
Voxelを取得し、その種類に応じてremove()メソッドやWorld.world.remove()を呼び出します。 - 山(MountainVoxel)の処理: 一度目は
removeTrees()を呼び出して木を取り除き、既に平坦化済みの場合はWorld.world.remove(x,y,z)で山自体を削除するという段階的な処理を行っています。 - コスト計算: 削除処理ごとに
AccountGenre.SUBSIDIARIES.spend(...)を呼び出し、撤去費用を計上しています。
事例2:柵
plugins\jp.co.tripod.chiname.lib.fence
ボクセルの境界線上に柵を設置するプラグインです。
- 継承クラス:
LandBuilderContribution(インターフェースとしてFenceも実装) - 実装のポイント: DummyVoxelと独自コントローラー
- DummyVoxel: 柵は通常、ボクセル(土地や建物)の「属性」として付与されます。しかし、何もない空中や地面の上に柵だけを設置したい場合、属性を付与する対象がありません。そこで、透明で当たり判定以外の実体を持たない
DummyVoxelを生成して配置し、そのボクセルに対してsetFenceメソッドで柵情報を付与する設計になっています。 - 独自コントローラー: 通常の土地造成は矩形範囲を使いますが、柵は「線」で引く操作が適しています。そのため、
createBuilderメソッドをオーバーライドし、BorderLineSelectorControllerを継承した独自のコントローラー (LogicL) を返すことで、ドラッグによる線描画操作を実現しています。
- DummyVoxel: 柵は通常、ボクセル(土地や建物)の「属性」として付与されます。しかし、何もない空中や地面の上に柵だけを設置したい場合、属性を付与する対象がありません。そこで、透明で当たり判定以外の実体を持たない
事例3:柵の撤去
plugins\jp.co.tripod.chiname.lib.fence
- 継承クラス:
FenceBuilder - 実装のポイント: 継承による差分実装
- 建設用クラス
FenceBuilderを継承し、setFenceメソッドのみをオーバーライドしています。 - 建設時は
World.world[loc].setFence(d, this)として柵オブジェクトを登録していた部分を、撤去時はsetFence(d, null)とすることで削除を行っています。 - このように、建設クラスを継承して「逆操作」だけを上書きすることで、コードの重複を抑えています。
- 建設用クラス
事例4:鉄橋
core\world\rail\BridgeRailContributionImpl.cs
列車が中をくぐることができる鉄橋の実装です。
- 継承クラス:
SpecialRailContribution- 通常の線路 (
RailRoadContribution) とは異なり、描画や建設処理をC#で細かく制御するためのクラスです。
- 通常の線路 (
- 実装のポイント: 描画順序の制御と自動生成
- 描画順序 (drawBefore/drawAfter): トラス橋は、列車の「手前」と「奥」に鉄骨が存在します。
drawBefore: 列車の描画「前」に呼び出され、奥側の鉄骨を描画します。- (ここで列車が描画される)
drawAfter: 列車の描画「後」に呼び出され、手前側の鉄骨を描画します。これにより、列車が鉄骨の間を走行しているように見せています。
- 橋脚の自動生成:
buildメソッド内で、線路の高さから地面の高さ (getGroundLevel) を検索し、その差分だけBridgePierVoxel(橋脚) を積み上げる処理が実装されています。
- 描画順序 (drawBefore/drawAfter): トラス橋は、列車の「手前」と「奥」に鉄骨が存在します。
事例5:サッカースタジアム
plugins\org.kohsuke.freetrain.soccerStadium
単なる建物ではなく、試合が行われ、チームの強さや人気が変動するシミュレーション要素を持つ建物です。
- 継承クラス:
PThreeDimStructure- 複数のボクセルを占有する巨大構造物の基底クラスです。
- 実装のポイント: 時間イベントと内部状態
- ClockHandler: コンストラクタで
World.world.clock.registerRepeatedを呼び出し、定期実行されるメソッド (onClock) を登録しています。これにより、定期的にチームの人気や強さを変動させたり、試合スケジュール(futureGames)を管理したりしています。 - 内部状態管理:
_strength(チームの強さ)や_popularity(人気)といった独自のフィールドを持ちます。これらの値はプロパティダイアログで確認できるほか、試合の勝敗判定や収入計算に利用されます。 - remove時の後始末:
removeメソッドでClockHandlerの登録解除 (unregister) を行い、建物が削除された後にイベントが走り続けないようにしています。
- ClockHandler: コンストラクタで
6.7 チュートリアル:動的な乗降需要の実装(ショッピングモール)
本節では、「テナントを入れることで魅力度が変わり、駅の利用客数が増減するショッピングモール」を実際に作成する手順を解説します。
概要
作成するプラグインの主な機能は以下の通りです。
- テナント管理: 設定画面からテナント(ファストフード、映画館など)を追加・削除できる。
- 動的な需要: テナントの構成によって「魅力度」が変化し、乗降客数が変動する。
- 収支計算: 売上とコストを計算し、利益(または赤字)を計上する。
Step 1: 開発環境の準備
pluginsフォルダ内に以下のディレクトリ構造を作成します。
plugins\freetrain.structure.shopping_mall\
├── bin\ (DLL出力先)
├── src\ (ソースコード)
├── plugin.xml (定義ファイル)
├── build.bat (ビルドスクリプト)
└── stadium.bmp (画像ファイル:今回はサッカースタジアムを流用)
plugin.xml
建設メニュー登録用の specialStructure と、建物実体用の anonymous を分けて登録します。
<plug-in>
<title>ショッピングモール</title>
<!-- メニュー登録用 -->
<contribution type="specialStructure" id="{...MALL}">
<class name="freetrain.world.shoppingmall.MallContributionImpl" codebase="bin/FreeTrain.ShoppingMall.dll" />
</contribution>
<!-- 建物実体用 -->
<contribution type="anonymous" id="{...MALL_S}">
<class name="freetrain.world.shoppingmall.StructureContributionImpl" codebase="bin/FreeTrain.ShoppingMall.dll" />
<name>ショッピングモール</name>
<price>500000000</price>
<size>10,6</size>
<height>2</height>
<sprite origin="0,0" offset="78"><picture src="stadium.bmp"/></sprite>
<!-- 独自の人口計算クラスを指定 -->
<population>
<class name="freetrain.world.shoppingmall.MallPopulation" codebase="bin/FreeTrain.ShoppingMall.dll"/>
</population>
</contribution>
</plug-in>
Step 2: データ構造の定義
テナント情報を管理するクラスです。カテゴリや売上情報も持ちます。
[Serializable]
public class Tenant {
public string Name;
public TenantCategory Category; // enum定義が必要
public double AttractivenessBonus;
public long BaseSales;
public long BaseCost;
// コンストラクタ等は省略
}
Step 3: 動的な人口計算
Population クラスを継承し、魅力度に応じた乗降客数を計算します。
重要: プラグイン読み込み時にシステムがこのクラスをインスタンス化するため、XmlElement を引数に取るコンストラクタが必須です。
[Serializable]
public class MallPopulation : Population {
private MallStructure owner;
public MallPopulation(MallStructure owner) { this.owner = owner; }
// システム読み込み用コンストラクタ(必須!)
public MallPopulation(XmlElement e) {}
public override int calcPopulation(Time t) {
if (owner == null) return 0;
// 時間帯ごとの基本人数 × 建物の魅力度
int baseP = (t.hour >= 10 && t.hour <= 20) ? 50 : 0;
return (int)(baseP * owner.Attractiveness);
}
}
Step 4: 構造体の実装
PThreeDimStructure を継承します。
[Serializable]
public class MallStructure : PThreeDimStructure {
private MallPopulation _population;
private SpecialStationListenerImpl _listener; // 駅との接続役
public ArrayList Tenants = new ArrayList();
public double Attractiveness = 1.0;
public MallStructure(StructureContributionImpl type, WorldLocator wloc) : base(type, wloc) {
_population = new MallPopulation(this);
// 駅からの集客を受け付けるリスナーを作成
_listener = new SpecialStationListenerImpl(_population, wloc.location);
// 定期イベント
World.world.clock.registerRepeated(new ClockHandler(OnClock), TimeLength.fromDays(30));
}
// 駅からの問い合わせに対し、リスナーを返す(最重要)
public override object queryInterface(Type aspect) {
if (aspect == typeof(SpecialStationListener))
return _listener;
return base.queryInterface(aspect);
}
// クリック時に設定画面を開く
public override bool onClick() {
using(TenantConfigForm form = new TenantConfigForm(this)) {
form.ShowDialog(MainWindow.mainWindow);
}
return true;
}
public void OnClock() {
// テナントの売上計算や競合ペナルティ計算を行い、収支を計上する
long profit = UpdateStatus();
AccountManager.theInstance.spend(-profit, AccountGenre.SUBSIDIARIES);
}
}
Step 5: UIの実装
Windows Forms を使用して、テナントの設定画面を作成します。System.Windows.Forms.Form を継承したクラス (TenantConfigForm) を作成し、リストボックスやボタンを配置して MallStructure の Tenants リストを操作するようにします。
Step 6: コントリビューションとビルド
src/MallContributionImpl.cs
メニューから選択された時に、建物を配置するコントローラーを起動します。
public override void showDialog() {
// MallController は FixedSizeStructController を継承したクラス
MallController.create();
}
build.bat
FreeTrainは .NET Framework 2.0 上で動作しているため、コンパイラもバージョンを合わせる必要があります。
@echo off
set CSC="C:\Windows\Microsoft.NET\Framework\v2.0.50727\csc.exe"
set OUT="..\bin\FreeTrain.ShoppingMall.dll"
set SRC="src\*.cs"
set REFS="/r:System.dll"
set REFS=%REFS% "/r:System.Windows.Forms.dll"
set REFS=%REFS% "/r:System.Drawing.dll"
set REFS=%REFS% "/r:..\..\..\FreeTrain.Core.dll"
set REFS=%REFS% "/r:..\..\..\FreeTrain.Controls.dll"
%CSC% /target:library /out:%OUT% %REFS% %SRC%
※パスは環境に合わせて調整してください。
以上の手順により、シミュレーション要素を持つ独自の建物を実装できます。
6.8 コーディングエージェントを活用した開発
大規模言語モデル(LLM)やコーディングエージェント(AI)を活用してFreeTrainのプラグインを開発する手法(いわゆるVibe Coding)は、APIドキュメントが少ないFreeTrain開発において非常に強力な武器となります。ここでは、AIと協働してスムーズに開発を進めるための手順とノウハウをまとめます。
1. 準備:AIにコンテキストを与える
AIはFreeTrainのAPIを知りません。開発を始める前に、AIがコードベースを理解できる環境を整える必要があります。
- Coreソースコードの提示: FreeTrain本体のソースコード(
coreフォルダやtrunkフォルダ)のパスをAIに教えます。AIはgrepやファイル検索機能を使って、必要なAPI(クラス名、プロパティ名、メソッドシグネチャ)を自力で調査できるようになります。- 例: 「FreeTrainのソースコードは
C:\Projects\FreeTrain\trunkにあります。APIが不明な場合はここを検索してください。」
- 例: 「FreeTrainのソースコードは
- Gitの利用: AIは大胆なコード修正を行うことがあります。いつでも元に戻せるよう、Gitでリポジトリ管理を行い、作業前にブランチを作成することを指示しましょう。
- AGENTS.md の作成: プロジェクトのルートディレクトリに
AGENTS.mdというファイルを作成し、プロジェクト固有の制約(.NET 2.0、WinForms限定、特定の命名規則など)を記載しておくと、エージェントが最初からその規約を理解した状態で開発を始めてくれます。
注意: 以下の記述例はあくまで一例です。実際のフォルダ構成やプロジェクト独自のルールに合わせて、内容は適宜書き換えて使用してください。
AGENTS.md の記述例
# FreeTrain プラグイン開発エージェントへの指示
このプロジェクトは非常に古い技術スタック(2000年代中盤)で構成されています。
エージェントは以下の制約を厳守してください。
## 開発環境と制約
- **Gitの利用**: 大胆なコード修正を行う際、いつでも元に戻せるようGitでリポジトリ管理を行ってください。大きな変更の前にはブランチを作成することを推奨します。
- **ランタイム**: .NET Framework 2.0
- **言語**: C# 2.0
- `var`(型推論)、LINQ、ラムダ式(`=>`)は使用不可。
- 匿名メソッド(`delegate`)を使用すること。
- **UIフレームワーク**: Windows Forms (System.Windows.Forms) 限定。
- WPFやWinUIなどのモダンなフレームワークは動作しません。
## 実装の作法
- **リソースアクセス**: 画像や独自の設定ファイルを読み込む際は、直接パスを指定せず、`freetrain.framework.plugin.Plugin` クラスが提供するパス解決メソッドを介すること。
- **シリアル化**: セーブデータに保存されるクラスには `[Serializable]` 属性が必須。
- **調査**:
- APIが不明な場合は、必ず `trunk/core` 以下のソースコードを確認すること。
- 実装パターンに迷った場合は、`plugins` フォルダ内の既存プラグイン(C#ソースが含まれるもの)を参考にすること。
## 禁止事項
- 本体ソース(`core/` や `trunk/`)への書き込みは厳禁です。調査にのみ使用してください。
- 修正が必要な場合は、必ずプラグイン側のソースコードを編集してください。
2. 開発フロー
AIとの対話による開発は、以下のサイクルで行います。
- 指示 (Prompt): 「何を」「どこに」「どうしたいか」を伝えます。
- 良い例: 「時計塔プラグイン
ClockTower.csを作成し、毎正時にステータスバーにメッセージを表示するようにしてください。coreフォルダを検索して、定期実行イベントの適用方法を調べてください。」
- 良い例: 「時計塔プラグイン
- 実装 (Implementation): AIがコードを書き換え、ビルドコマンドを実行します。
- エラー解決 (Fix): FreeTrain開発ではビルドエラーが頻発します。AIは最新のC#文法を使いがちですが、FreeTrainは古い環境のためエラーになります。エラーログをそのままAIに貼り付けて修正を促します。
- 動作確認 (Verify): ゲームを起動して動作を確認し、結果(「メッセージが表示されない」「ログに例外が出ている」など)をAIにフィードバックします。
3. よくある落とし穴と回避策 (Tips)
AI開発をスムーズに進めるための重要なポイントです。
- APIの幻覚 (Hallucination):
- AIは「あるはずだ」と思って存在しないプロパティを使うことがあります。
- 対策: 「コンパイルエラーになりました。ソースコードを検索して正しいアクセス方法を探してください」と指示すると、AIは実際のソースから代替手段を見つけ出してくれます。
- C#のバージョン制限:
- FreeTrainは .NET Framework 2.0 (C# 2.0) です。最新の文法は使えません。
- 対策: プロンプトの最初に「開発環境は .NET 2.0 / C# 2.0 です。LINQやラムダ式、varなどは使えません」と明記しておくと手戻りが減ります。
- コンテキストの維持:
- 長い会話になるとAIは前の指示を忘れることがあります。
- 対策: 重要なファイル(
plugin.xmlや修正中の.csファイル)のパスを定期的に提示したり、直近の修正意図を再認識させたりすることで精度を維持できます。
