.NETによるヘッドレスかつ決定論的な工場シミュレーションの構築に向けて

製造現場のデジタルツインや物流最適化において、既存の巨大な工場シミュレーター(Siemens Plant Simulation等)を導入するには、膨大なライセンス費用と専門教育による運用コストが大きな障壁となっています。
一方で、近年『Factorio』や『Satisfactory』といった工場自動化ゲームが証明したのは、複雑なサプライチェーンの構築や物流網の最適化というプロセスが、本来は極めて直感的で、熱狂を伴うほどに面白い知的作業であるという事実でした。
私たちは、こうした「重すぎる産業用ソフト」の代替となり得る基礎研究として、ゲームのような操作性と、研究開発に耐えうる厳密さを両立したプロトタイプをC#(.NET 9)で開発しました。
3種類の原材料から製品を生産する工場で、5体のロボットが働く。彼らは疲れるとベッドで(画面右上の緑のタイル)一定時間休憩する。
今はまだプロトタイプのごく初期段階ですが、最終的に目指すのは、シミュレーションにおける「Figma」のような存在です。
開発における技術的な条件は、「ヘッドレス」であること、そして「決定論的」であることです。
「ヘッドレス」のヘッドとは「画面」のことです。
つまり、このプロトタイプは、グラフィックの表示なしで、データ同士の関係性だけで工場のシミュレーションを行うことができます。
言い換えれば、特定の重量級ゲームエンジンやGUIに依存せず、純粋なロジックのみで「工場という名の計算モデル」を構築しています。
グラフィックは、その計算結果を後知恵的に説明するための手段として描画されているに過ぎません。
また、「決定論的」とは、何度シミュレーションを繰り返しても、同じ条件下では同じ結果が得られることを意味します。
以上の条件を持って、小規模なプロセスの検証から大規模な物流網のシミュレーションまで、ブラウザ上で軽快に、かつ数学的な正しさを持って実行できる基盤を追求しています。
シミュレーション部分は純粋なC#(.NET 9)で構築されました。重量級のゲームエンジンから完全に切り離されており、状態の可視化にはシンプルなHTML/JSフロントエンドを使用しています。
本記事では、このシミュレーションの背後にあるコア・アーキテクチャのパターンを探り、開発の過程で直面した興味深いデバッグの課題について紹介します。
アーキテクチャ:不変の構成 vs 可変の状態
シミュレーションのパフォーマンスとスレッドセーフを維持するため、このアーキテクチャでは、「不変の構成(工場の設計図)」と「可変の状態(特定の『ティック』におけるシミュレーションの正確なステータス)」を厳格に分離しています。
構成(Immutable Records)
マシンの配置、レシピ、コンベアベルトの接続、最大容量など、工場のレイアウトはC# 9.0以降のrecord型およびreadonly record struct型によって強固に定義されています。これらをレコードとして厳密かつ動的に定義することで、コンパイラレベルで数学的に不変性を強制しています。つまり、工場のレイアウトは一度宣言されたら、実行中に変更されることはありません。
ティック・エンジン(Mutable State)
SimulationEngineは、実行中の可変状態(RobotState、TruckStateなど)を管理します。エンジンは毎ティック、コンポーネントを順番に処理します。この決定論的なアプローチにより、競合状態(レースコンディション)のない予測可能な動作が保証されます。
public void Tick()
{
State.TickCount++;
UpdateMachines(); // 原材料を生産物に加工
UpdateConveyors(); // トラックに沿ってアイテムを移動
UpdateRobots(); // BFS(幅優先探索)による経路探索、アイテムの回収・搬入
UpdateTrucks(); // 外部物流の到着と出発を処理
}
レイアウトと実行時の可変状態を切り離すことで、全状態を軽量なJSONペイロードとして簡単にシリアライズできるようになっています。
APIと可視化レイヤー
エンジンは「ヘッドレス」であるため、メカニクスは純粋にメモリ内のみで実行されます。バックグラウンドサービスがシミュレーションを継続的に進行させ、ASP.NET Core APIが現在のフレームを接続クライアントに公開します。
// SimulationService.cs の APIエンドポイント
[HttpGet("/api/state")]
public IResult GetState(string scenario = "default")
{
var engine = _activeEngines[scenario];
var state = engine.State;
return Results.Ok(new {
tick = state.TickCount,
robots = state.Robots,
trucks = state.Trucks,
// ...
});
}
フロントエンドは完全に標準的なindex.htmlファイルで、グリッド座標(t.x * 64px)をCSSのleft/top絶対オフセットに直接バインドする静的なJSオブジェクトを保持しています。
教訓:震えが止まらないロボット
現実的な工場のロジック・アルゴリズムは、すぐに複雑化します。私たちは、10台以上のロボットが一斉に1つのアイテムを掴むために在庫へダッシュし、その後、無限に「震える(アイテムを高速で拾っては即座に落とす動作を繰り返す)」という、奇妙で目を引くバグに遭遇しました。
欠陥
出発しようとするトラックのためにアイテムを回収しているロボットの数を評価する際、システムはロボットのTargetEntity(目標物)が「トラックの停留所(TruckStop)」に割り当てられているかどうかを確認していました。
しかし、ロボットが実際にアイテムを「回収」しに行く段階では、一時的にTargetEntityを向かっている先の「在庫(Stockpile)」に設定していました。この時、厳密にトラックをターゲットにしているロボットは「0」と判定されるため、マップ上のすべてのアイドル状態のロボットが「トラックが助けを求めている!」と判断し、一斉に在庫に群がってしまったのです。
解決策
一時的なエンティティ座標のチェックを廃止し、代わりに「アイテムに対する明確な意図(Intent)」を数学的に検証するようにしました。ロボットが物理的にどこを歩いているかに関わらず、_robotStates[j].TargetItem.Value == 3(ゴールドCPU)を追跡することで、エンジンはアクティブな回収任務を完璧に監視し、重複したジョブの割り当てをきれいに回避できるようになりました。
// 修正された割り当てチェック
int robotsFetching = 0;
for (int j = 0; j < _robotStates.Length; j++)
{
// ロボットが内部的に「要求されたアイテムを拾う」というジョブを確定させているかを確認
if (tTask != RobotTask.Idle && _robotStates[j].TargetItem.Value == requiredItem)
{
robotsFetching++;
}
}
結論
C#でゼロからヘッドレスな工場シミュレーションエンジンを構築するための基礎技術が出来上がりました。
これにより、魅力的で高機能な物流グリッドシステムを作るために、必ずしもUnityやUnrealを必要としないことが証明されました。
クリーンなアルゴリズムの分離と予測可能な逐次ティック処理は、複雑で拡張性の高い自動化システムのための素晴らしい基盤となります。
1,000社以上の事業成長を支えた圧倒的な『スピードと柔軟性』で、御社のアイデアを最短で形にします。まずは無料で壁打ちしませんか?
無料相談はこちら

