csharp-godot について
Godot 4.3以降の開発者向けC#スキルガイドです。言語の違い、プロジェクト設定、相互運用を例とトラブル対処を交えて説明します。
主な機能
- 01C#とGDScriptの構文比較
- 02Godot C#プロジェクトとIDEの設定
- 03シグナルと非同期処理のパターン
- 04C#とGDScriptの相互運用
- 05パフォーマンスとよくある問題の解説
活用シーン
- 01GDScriptのパターンをC#に置き換える場面
- 02Godot C#プロジェクトとIDEの設定
- 03Godotのシグナルや非同期処理の実装
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jame581/ godotprompter --skill csharp-godotインストール後、Claude Code で/csharp-godotを入力して読み込まれたか確認します。
同梱ファイルreferences/gdscript-interop.md
詳細
Godot 4.3+ の C#
このスキルでは、C# 固有の規約、GDScript との API の違い、プロジェクトのセットアップ、相互運用のパターンを扱います。例はすべて C# のみです。Godot 4.3+ と GodotSharp NuGet パッケージを対象としています。
関連スキル: C# のシグナルパターンについては csharp-signals、C# プロジェクトのひな形については godot-project-setup、gdUnit4 を使った C# のテストについては godot-testing、C# では不十分な場合のネイティブ C++ については gdextension、C# の並行処理については multithreading を参照してください。
1. C# と GDScript の構文比較
| GDScript | C# 相当 | 備考 |
|---|---|---|
var x = 5 | var x = 5; または型を指定した int x = 5; | C# の var はコンパイル時に型を推論します |
func MyMethod() -> void: | public void MyMethod() { } | C# ではメソッド名に PascalCase を使います |
signal health_changed(amount: int) | [Signal] delegate void HealthChangedEventHandler(int amount); | EventHandler サフィックスを付ける必要があります |
@export var speed: float = 100.0 | [Export] public float Speed { get; set; } = 100f; | PascalCase を使い、プロパティ構文で記述します |
@onready var label = $Label | private Label _label; + _label = GetNode<Label>("Label"); を _Ready() 内で実行 | @onready に相当する機能はありません。_Ready() を使います |
match value: | switch (value) { case X: break; } | C# の switch もパターンマッチングに対応しています |
class_name MyClass | [GlobalClass] public partial class MyClass : GodotObject { } | [GlobalClass] 属性が必要です |
extends Node | public partial class MyScript : Node { } | : を使って継承します |
preload("res://scene.tscn") | GD.Load<PackedScene>("res://scene.tscn") | コンパイル時ではなく、実行時に読み込まれます |
push_error("msg") | GD.PushError("msg"); | Godot のエラーログに出力します |
print("msg") | GD.Print("msg"); | GD.PrintS()、GD.PrintT() も使用できます |
node is CharacterBody2D | node is CharacterBody2D | キーワードも意味も同じです |
node as CharacterBody2D | node as CharacterBody2D | どちらも失敗すると null を返します |
await signal_name | await ToSignal(source, SignalName.X); | ToSignal() ラッパーを使う必要があります |
Array | Godot.Collections.Array | System.Collections.Generic.List<T> ではありません |
Dictionary | Godot.Collections.Dictionary | System.Collections.Generic.Dictionary<K,V> ではありません |
2. プロジェクトのセットアップ
.csproj とソリューション
Script > New Script > C# から最初の C# スクリプトを作成すると、Godot が .csproj を自動生成します。生成されたファイル構成は手動で編集しないでください。エディターに管理を任せてください。
MyProject/
├── MyProject.csproj # Auto-generated, edit only for NuGet packages
├── MyProject.sln # Auto-generated solution file
├── project.godot
└── scripts/
└── Player.cs
NuGet パッケージ
<ItemGroup> 内の MyProject.csproj にパッケージを追加します。
<Project Sdk="Godot.NET.Sdk/4.3.0">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>
dotnet restore を実行するか、編集後に IDE が自動で復元するようにします。
Godot.NET プレビュー バインディング(Godot 4.7+)
Godot 4.7 の Godot.NET.Sdk では、オプトインの MSBuild プロパティを指定すると、プロジェクトのバインディングを従来の GodotSharp からプレビュー版の Godot.NET バインディングに切り替えられます(GodotSharp ではなく Godot.Bindings アセンブリを使用します)。
<PropertyGroup>
<EnableGodotDotNetPreview>true</EnableGodotDotNetPreview>
</PropertyGroup>
プレビュー版バインディングは実験的な機能です。本番プロジェクトでは、このプロパティを設定しないでください。詳細は GH-118001 を参照してください。
IDE のセットアップ
| IDE | 必要なセットアップ | 備考 |
|---|---|---|
| JetBrains Rider | Godot プラグインをインストールします(Rider 2023.3+ に同梱) | Godot の C# サポートが最も充実しています。デバッガーはそのまま動作します |
| VS Code | C# Dev Kit と Godot Tools の拡張機能をインストールします | デバッガーをアタッチするには launch.json が必要です |
| Visual Studio | Godot Visual Studio 拡張機能をインストールします | Windows のみ。デバッガーは Tools > Attach to Process から使用します |
すべての IDE で、ソリューション全体を正しく解決するため、フォルダーだけでなく .sln ファイルを開いてください。
3. partial class の要件
Godot の型を継承するすべてのクラスは、partial として宣言する必要があります。これは任意ではありません。
理由
Godot は C# ソースジェネレーターを使い、シグナルの登録、プロパティのバインディング、RPC のコードをクラスと並べて生成します。ソースジェネレーターが同じクラス宣言にコードを挿入するには、partial が必要です。
忘れた場合のエラー
error GD0001: Missing partial modifier on declaration of type 'Player'
that derives from 'Godot.GodotObject'
ビルドが失敗するため、partial の記述漏れを見逃すことはありません。
ルール
// CORRECT
public partial class Player : CharacterBody2D { }
// WRONG — will cause source generator errors
public class Player : CharacterBody2D { }
これは、中間の基底クラスも含め、Godot の型を継承する継承チェーン内のすべてのクラスに適用されます。
4. 命名規則
| 要素 | 規則 | 例 |
|---|---|---|
| メソッド | PascalCase | public void TakeDamage(int amount) |
| プロパティ | PascalCase | public float MaxHealth { get; set; } |
| シグナル(デリゲート) | PascalCase + EventHandler サフィックス | HealthChangedEventHandler |
[Export] プロパティ | PascalCase | [Export] public float Speed { get; set; } |
| プライベートフィールド | 先頭にアンダースコアを付けた _camelCase | private float _currentSpeed; |
| ローカル変数 | camelCase | var newPosition = ... |
| パラメーター | camelCase | void SetHealth(int newHealth) |
| Godot API 名 | Godot の PascalCase に正確に合わせる | global_position ではなく GlobalPosition |
| 列挙型 | 型名とメンバー名は PascalCase | enum State { Idle, Running, Dead } |
| 定数 | チームのスタイルに応じて PascalCase または SCREAMING_SNAKE | const float MaxSpeed = 200f; |
GodotSharp のプロパティ名とメソッド名は、必ず正確に合わせてください。これらは GDScript の snake_case 名を PascalCase に変換したものです(例:is_on_floor() → IsOnFloor())。
5. C# のシグナル
シグナルには、[Signal] 属性を付けたデリゲート宣言が必要です。デリゲート名は EventHandler で終わる必要があります。
using Godot;
public partial class Player : CharacterBody2D
{
// Declaration
[Signal] public delegate void HealthChangedEventHandler(int newHealth);
[Signal] public delegate void DiedEventHandler();
private int _health = 100;
public void TakeDamage(int amount)
{
_health -= amount;
EmitSignal(SignalName.HealthChanged, _health); // Use SignalName.X, not a string
if (_health <= 0)
EmitSignal(SignalName.Died);
}
}
接続と切断
// Connect with +=
player.HealthChanged += OnHealthChanged;
player.Died += OnPlayerDied;
// Disconnect with -=
player.HealthChanged -= OnHealthChanged;
player.Died -= OnPlayerDied;
// Handler signatures must match the delegate
private void OnHealthChanged(int newHealth) { ... }
private void OnPlayerDied() { ... }
ワンショット接続、静的型付きシグナル、言語をまたぐシグナル配線など、シグナルのパターン全般については、csharp-signals スキルを参照してください。
6. Async / Await
GDScript の await は、C# の await ToSignal(...) に対応します。Godot のシグナルは、C# の await と互換性のある SignalAwaiter を返します。
Godot シグナルの待機
public async void StartCutscene()
{
// Wait for a timer to finish
await ToSignal(GetTree().CreateTimer(2.0), SceneTreeTimer.SignalName.Timeout);
// Wait for an animation to finish
var anim = GetNode<AnimationPlayer>("AnimationPlayer");
anim.Play("intro");
await ToSignal(anim, AnimationPlayer.SignalName.AnimationFinished);
GD.Print("Cutscene complete");
}
Task ベースのパターン
CPU 負荷の高い処理には Task.Run を使用します。ただし、メインスレッド以外から Godot オブジェクトにアクセスしてはいけません。
public async void LoadHeavyData()
{
// Off main thread: pure C# computation only
var result = await Task.Run(() => ComputeSomethingExpensive());
// Back on main thread: safe to use Godot API
ApplyResult(result);
}
private int ComputeSomethingExpensive()
{
// No Godot API calls here
return Enumerable.Range(0, 1_000_000).Sum();
}
GDScript の await に相当する記述
| GDScript | C# |
|---|---|
await get_tree().create_timer(1.0).timeout | await ToSignal(GetTree().CreateTimer(1.0), SceneTreeTimer.SignalName.Timeout) |
await animation_player.animation_finished | await ToSignal(animPlayer, AnimationPlayer.SignalName.AnimationFinished) |
await signal_name | await ToSignal(this, SignalName.YourSignal) |
7. GDScript との相互運用
C# から GDScript オブジェクトを呼び出すには Call、Get、Set を使用します。また、C# クラスが [GlobalClass] として登録されていれば、GDScript から直接使用できます。境界を越える値は Variant として扱われます。リファレンスにはマーシャリング時の注意点(null、int[]、List<T>)が記載されています。
Call/Get/Setの例、[GlobalClass]パターン、Variantのマーシャリング表については、references/gdscript-interop.md を参照してください。
8. パフォーマンス
C# と GDScript の比較
| ワークロード | 優位な言語 | 理由 |
|---|---|---|
| 数学処理の多いループ(経路探索、シミュレーション) | C#(大幅に高速) | JIT コンパイルと、インタープリター方式の GDScript の違い |
| 大規模な配列/コレクションの処理 | C# | 値型の配列ではボックス化が発生しない |
| Godot API の呼び出し(move_and_slide など) | ほぼ同等 | どちらも同じ C++ エンジンを介して処理される |
| シーンツリーの操作 | ほぼ同等 | ボトルネックは言語ではなく C++ のオーバーヘッド |
| 迅速なプロトタイピング | GDScript | 定型コードが少なく、再コンパイルせずにホットリロードできる |
| エディターツール(プラグイン、@tool) | GDScript | C# のツールスクリプトはビルド一式が必要 |
ガイダンス
- 物理ソルバー、プロシージャル生成、AI の意思決定ツリー、データ処理など、タイトなループが必要なシステムには C# を使用してください。
- エディタープラグイン、
@toolスクリプト、ゲームプレイロジックの迅速な反復には GDScript を使用してください。 - 同じプロジェクト内での言語の混在がサポートされています。異なる言語間の呼び出しがたまにある程度なら、相互運用のコストは最小限です。
- ホットパスでは
Godot.Collections.ArrayとArray<T>を避けてください。Array<T>は同じエンジン配列をラップしているため、要素にアクセスするたびにVariantのマーシャリングが発生します。代わりにT[]またはList<T>を使用し、境界で変換してください。 StringNameの検索は O(1) ですが、StringNameの構築はそうではありません。頻繁に作成する場合はキャッシュしてください。
9. よくある落とし穴
| 落とし穴 | 問題 | 修正方法 |
|---|---|---|
Variant から C# 型への変換 | (float)v は v.AsSingle() と同じ呼び出しです。どちらも例外はスローされず、int、bool、数値文字列は変換され、それ以外の型は 0 になります | 保存されている型が重要な場合は v.VariantType を確認してください |
null と default(Variant) の違い | 値を渡さない Godot シグナルは、C# の null ではなく default(Variant) を渡します | .VariantType == Variant.Type.Nil を確認してください |
Godot.Collections と System.Collections の違い | Godot API メソッドは Godot.Collections.Array を返します。List<T> は Variant に変換されないため、渡してもコンパイルできません | Godot API との境界では常に Godot.Collections.Array/Dictionary を使用してください |
| ネイティブオブジェクトの破棄 | 解放済みの Godot オブジェクトのメソッドを呼び出すと、ObjectDisposedException がスローされます | 使用前に IsInstanceValid(obj) を確認してください |
ホットループ内での StringName の構築 | new StringName("my_signal") は呼び出しごとにメモリを割り当てます | private static readonly StringName _signalName = "my_signal"; としてキャッシュしてください |
| エクスポート配列の型 | [Export] public Array Items; は型指定のない配列をエクスポートします | [Export] public Godot.Collections.Array<MyResource> Items { get; set; } を使用してください |
| ノードパス文字列 | パスが変更された場合、GetNode("../UI/Label") は実行時に Node not found をログに出力し、null を返します | 一意の名前を使って GetNode<Label>("%Label") を使用するか、型指定された [Export] ノード参照を使用してください |
partial class の指定漏れ | ビルドエラー GD0001 が発生します | Godot の型を継承するクラスはすべて partial にする必要があります |
async void と async Task の違い | async void は await できません。誰からも await されない async Task は例外を隠します(Godot は async void の例外をログに記録します) | Task を返して await してください。async void は Godot が呼び出すトップレベルのイベントハンドラーに限定してください |
⚠️ Godot 4.7 での変更点: 4.7 の C# アセンブリには、いくつかの互換性を破る変更が含まれています。バイナリ互換性のみの変更(ソースは引き続きコンパイルでき、再ビルドだけで対応できます):
OptimizedTranslation.Generate()の戻り値がvoidからboolに変更されました(GH-119563)。バイナリとソースの両方に互換性のない変更(コードの修正が必要です):Animation.Lengthの型メタデータがfloatからdoubleに変更されました(GH-116394)。C# プロパティもdoubleになったため、float len = anim.Length;はコンパイルできなくなりました。また、Control.AccessibilityLiveの列挙型がDisplayServer.AccessibilityLiveModeからAccessibilityServer.AccessibilityLiveModeに変更されました(GH-116839)。宣言をAccessibilityServerの列挙型に更新してください。ソース互換性のない変更(既存のバイナリは引き続き読み込めます):RichTextLabel.ImageUpdateMask.WidthInPercentの名前がWidthUnitに変更されました(GH-112617。GDScript ではUPDATE_WIDTH_IN_PERCENTからUPDATE_WIDTH_UNITに変更)。また、EditorSceneFormatImporterのIMPORT_*定数がImportFlags列挙型に移動しました(GH-115788)。削除されたもの:AudioEffectSpectrumAnalyzer.TapBackPos(GH-114355)。4.7 に対して再ビルドし、該当するシンボルを修正してください。4.7 移行ガイドを参照してください。
10. チェックリスト
- Godot の型を継承するすべてのクラスが
partialとして宣言されている - メソッド名が GodotSharp の PascalCase に従っている(例:
_Ready、_PhysicsProcess、IsOnFloor()) -
[Export]プロパティが PascalCase で、プロパティ構文({ get; set; })を使用している - シグナルデリゲート名が
EventHandlerで終わり、EmitSignal(SignalName.X)を使用している - シグナルを不要になった際に
+=で接続し、-=で切断している - Godot のシグナルには
await ToSignal(...)を使用し、ゲーム内のタイミング制御に生のTask.Delayを使用していない - Godot API の境界では
System.Collections型ではなく、Godot.Collections.Array/Dictionaryを使用している - 解放された可能性のある Godot オブジェクトを使用する前に、
IsInstanceValid(obj)を確認している - ループ内または頻繁に呼び出されるメソッド内で生成する場合、
StringNameインスタンスをキャッシュしている -
Task.Run(...)のコールバック内で Godot API を呼び出していない — エンジンの呼び出しはすべてメインスレッドで行っている - ソリューション全体を正しく解決するため、IDE では
.slnファイル経由で.csprojを開いている - 計算負荷の高いシステムには C# を使用し、エディターツールと
@toolスクリプトには GDScript を引き続き使用している