원클릭으로
godot-csharp
Godot C# specialist for .NET 8 integration, Godot 4.6.1 C# API patterns, async/await, signals, and modern C# idioms.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Godot C# specialist for .NET 8 integration, Godot 4.6.1 C# API patterns, async/await, signals, and modern C# idioms.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
| name | godot-csharp |
| description | Godot C# specialist for .NET 8 integration, Godot 4.6.1 C# API patterns, async/await, signals, and modern C# idioms. |
| license | MIT |
Modern C# patterns for Godot 4.6.1 with .NET 8.
Use this skill when:
MUST follow: .opencode/docs/coding-standards.md (C# section)
All C# code must comply with:
Performance Threshold: >1000 calls/frame → delegate to godot-gdextension
让 C# 类直接出现在编辑器的"添加节点"列表中:
// ✅ Modern: 直接在编辑器中作为节点使用
[GlobalClass]
public partial class PlayerController : CharacterBody2D
{
// 这个类现在可以直接在编辑器中添加
}
[GlobalClass]
public partial class HealthComponent : Node
{
// 可作为独立组件添加到任意节点
}
// ❌ Legacy: 需要先创建 Node 再挂脚本
public partial class PlayerController : CharacterBody2D { }
好处:
❌ Legacy (Godot 3 风格):
[Export] public NodePath HealthBarPath { get; set; }
private ProgressBar _healthBar;
public override void _Ready()
{
_healthBar = GetNode<ProgressBar>(HealthBarPath);
}
✅ Modern (Godot 4.6.1):
// 方式 1: 直接导出节点类型
[Export]
public ProgressBar HealthBar { get; set; }
// 方式 2: 使用 Scene Unique Nodes (场景唯一节点)
// 在编辑器中将节点设为 Unique Name (%HealthBar)
private ProgressBar _healthBar;
public override void _Ready()
{
_healthBar = GetNode<ProgressBar>("%HealthBar");
}
// 方式 3: 使用 GetNode<T> 缓存
private ProgressBar _healthBar;
public override void _Ready()
{
_healthBar = GetNode<ProgressBar>("HealthBar");
}
推荐:
[Export] 节点类型%NodeName)GetNode<T>()// 基础导出
[Export]
public float Speed { get; set; } = 200.0f;
// 范围导出
[Export(PropertyHint.Range, "0,100,0.1")]
public float Health { get; set; } = 100.0f;
// 节点导出 (Modern)
[Export]
public ProgressBar HealthBar { get; set; }
// 资源导出
[Export]
public Texture2D Icon { get; set; }
// 枚举导出
public enum EnemyType { Melee, Ranged, Boss }
[Export]
public EnemyType Type { get; set; }
// 数组导出
[Export]
public WeaponData[] Weapons { get; set; } = Array.Empty<WeaponData>();
// 文件导出
[Export(PropertyHint.File, "*.tscn")]
public string LevelPath { get; set; }
// 定义信号 (Godot 4 方式)
[Signal]
public delegate void HealthChangedEventHandler(float newHealth, float maxHealth);
[Signal]
public delegate void PlayerDiedEventHandler();
// 触发信号
public void TakeDamage(float amount)
{
Health -= amount;
EmitSignal(SignalName.HealthChanged, Health, MaxHealth);
if (Health <= 0)
{
EmitSignal(SignalName.PlayerDied);
}
}
// 连接信号 (Godot 4.6.1)
public override void _Ready()
{
// C# 事件方式 (推荐)
Button.Pressed += OnButtonPressed;
// 自定义信号
HealthChanged += OnHealthChanged;
}
private void OnButtonPressed()
{
GD.Print("Button pressed!");
}
private void OnHealthChanged(float newHealth, float maxHealth)
{
UpdateHealthBar(newHealth, maxHealth);
}
// 断开连接 (防止内存泄漏)
public override void _ExitTree()
{
Button.Pressed -= OnButtonPressed;
HealthChanged -= OnHealthChanged;
}
// ✅ 使用 Godot 的日志系统
GD.Print("普通日志");
GD.PushWarning("警告");
GD.PushError("错误");
// 调试专用
GD.PrintRich("[color=red]红色文本[/color]");
GD.PrintErr("错误输出");
// ❌ 不要用 Console (不会显示在 Godot 输出)
Console.WriteLine("..."); // 无效
// ✅ 正确 — 等待 Godot 定时器
await ToSignal(GetTree().CreateTimer(1.0), SceneTreeTimer.SignalName.Timeout);
// ✅ 正确 — 等待动画完成
await ToSignal(AnimationPlayer, AnimationPlayer.SignalName.AnimationFinished);
// ✅ 正确 — 等待自定义信号
await ToSignal(this, SignalName.HealthChanged);
// ❌ 错误 — Task.Delay 会在 Godot 主循环外运行,导致帧同步问题
await Task.Delay(1000); // 不要这样用!
节点可能在 await 期间被释放:
public async void PerformAttack()
{
IsAttacking = true;
AnimationPlayer.Play("attack");
await ToSignal(AnimationPlayer, AnimationPlayer.SignalName.AnimationFinished);
// ⚠️ 必须检查!节点可能已被释放
if (!IsInstanceValid(this)) return;
DealDamage();
IsAttacking = false;
}
// fire-and-forget(事件回调用)
public async void OnButtonPressed()
{
await LoadDataAsync();
}
// 可测试的异步方法(返回 Task)
public async Task LoadLevelAsync(string levelPath)
{
var resource = GD.Load<PackedScene>(levelPath);
var instance = resource.Instantiate<Node>();
GetTree().CurrentScene.AddChild(instance);
}
// ✅ C# 内部使用 — 标准 .NET 集合(性能更好)
private List<Enemy> _activeEnemies = new();
private Dictionary<string, float> _stats = new();
private HashSet<Item> _collectedItems = new();
// ⚠️ 仅在与 GDScript 交互或导出到检查器时使用 Godot 集合
[Export] public Godot.Collections.Array<Item> StartingItems { get; set; } = new();
[Export] public Godot.Collections.Dictionary<string, int> ItemCounts { get; set; } = new();
| 场景 | 使用类型 | 原因 |
|---|---|---|
| C# 内部逻辑 | List<T>, Dictionary<K,V> | 性能最优,无封送开销 |
| 导出到检查器 | Godot.Collections.Array<T> | Godot 序列化要求 |
| 传递给 GDScript | Godot.Collections.* | 跨语言兼容 |
| 大量数值数据 | Godot.Collections.Packed*Array | 内存连续,性能优化 |
using System.Text.Json;
using System.Text.Json.Serialization;
// 序列化
public class PlayerData
{
public string Name { get; set; }
public int Level { get; set; }
public float Health { get; set; }
}
var player = new PlayerData { Name = "Hero", Level = 10, Health = 100f };
// 序列化为 JSON
string json = JsonSerializer.Serialize(player);
// 美化输出
var options = new JsonSerializerOptions { WriteIndented = true };
string prettyJson = JsonSerializer.Serialize(player, options);
// 反序列化
var loaded = JsonSerializer.Deserialize<PlayerData>(json);
// 异步文件读写
public async Task SaveAsync(string path, PlayerData data)
{
await using var stream = File.Create(path);
await JsonSerializer.SerializeAsync(stream, data, options);
}
public async Task<PlayerData?> LoadAsync(string path)
{
await using var stream = File.OpenRead(path);
return await JsonSerializer.DeserializeAsync<PlayerData>(stream);
}
| 特性 | System.Text.Json | Newtonsoft.Json |
|---|---|---|
| 性能 | ✅ 更快 | ⚠️ 较慢 |
| 内存 | ✅ 更少 | ⚠️ 更多 |
| 依赖 | ✅ .NET 内置 | ❌ 需要 NuGet |
| .NET 8 | ✅ 原生支持 | ⚠️ 兼容 |
只有当需要 Newtonsoft 特有功能时才使用它。
C# 原生支持翻译提取:
// Godot 4.6+ 支持 C# 字符串翻译提取
public void UpdateUI()
{
// 翻译文本
var text = Tr("UI_START_GAME");
StartButton.Text = text;
// 带参数的翻译
var message = Tr("PLAYER_HEALTH").Format(Health, MaxHealth);
// 复数形式
var itemText = TrN("ITEM_COUNT", "ITEM_COUNT_PLURAL", itemCount);
}
// 在编辑器中: Project → Localization → Extract Translatable Strings
// 现在可以自动提取 C# 中的 Tr() 字符串
// GameManager.cs - 添加为 Autoload
public partial class GameManager : Node
{
public static GameManager Instance { get; private set; }
public override void _Ready()
{
Instance = this;
}
}
// 使用
GameManager.Instance.DoSomething();
public interface IState
{
void Enter();
void Exit();
void Update(double delta);
}
public class StateMachine : Node
{
private IState? _currentState;
public void ChangeState(IState newState)
{
_currentState?.Exit();
_currentState = newState;
_currentState.Enter();
}
public override void _Process(double delta)
{
_currentState?.Update(delta);
}
}
public partial class BulletPool : Node
{
[Export] public PackedScene? BulletScene { get; set; }
[Export] public int PoolSize { get; set; } = 50;
private readonly Queue<Bullet> _pool = new();
public override void _Ready()
{
if (BulletScene == null) return;
for (int i = 0; i < PoolSize; i++)
{
var bullet = BulletScene.Instantiate<Bullet>();
bullet.SetProcess(false);
AddChild(bullet);
_pool.Enqueue(bullet);
}
}
public Bullet? Get()
{
if (_pool.Count == 0) return null;
var bullet = _pool.Dequeue();
bullet.SetProcess(true);
return bullet;
}
public void Return(Bullet bullet)
{
bullet.SetProcess(false);
bullet.Hide();
_pool.Enqueue(bullet);
}
}
| GDScript | C# |
|---|---|
func _ready(): | public override void _Ready() |
func _process(delta): | public override void _Process(double delta) |
signal health_changed(val) | [Signal] delegate void HealthChangedEventHandler(float val); |
@export var speed: float | [Export] public float Speed { get; set; } |
@onready var sprite = $Sprite | [Export] public Sprite2D Sprite { get; set; } |
get_node("Path") | GetNode<Node>("Path") |
$Sprite | GetNode<Sprite2D>("Sprite") 或 %Sprite |
queue_free() | QueueFree() |
print() | GD.Print() |
randf() | GD.Randf() |
randi_range(0, 10) | GD.RandRange(0, 10) |
Vector2(1, 2) | new Vector2(1, 2) |
Color.RED | Colors.Red |
await get_tree().create_timer(1.0).timeout | await ToSignal(GetTree().CreateTimer(1.0), SceneTreeTimer.SignalName.Timeout) |
src/
├── Core/
│ └── GameManager.cs
├── Gameplay/
│ ├── Player/
│ │ └── PlayerController.cs
│ └── Enemies/
├── UI/
│ └── HUD.cs
└── Utils/
└── Extensions.cs
<Project Sdk="Godot.NET.Sdk/4.6.1">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
<RootNamespace>MyGame</RootNamespace>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
</PropertyGroup>
</Project>
| 错误 | 修正 |
|---|---|
Console.WriteLine() | GD.Print() |
缺少 partial | public partial class Player : Node |
使用 yield | await ToSignal() |
| NodePath + GetNode | 直接 [Export] 节点类型 |
| 硬编码路径 | 使用 Scene Unique Nodes (%Name) |
| 事件未取消订阅 | 在 _ExitTree() 中取消 |
| 使用 Newtonsoft.Json | 使用 System.Text.Json |
缺少 [GlobalClass] | 添加以注册节点 |
LLM-generated scene files are prone to errors:
After generating C# script, output:
## Scene Assembly Instructions
### Node Tree
[RootNode] (type) ├── [Child1] (type) — purpose ├── [Child2] (type) — purpose │ └── [SubChild] (type) — purpose
### Required Components
| Node | Properties | Script |
|------|------------|--------|
| Player | position: (100, 200) | PlayerController.cs |
### Assembly Steps
1. Create root: Add Node → [NodeType]
2. Attach script: Drag file to node
3. Add children: Add Node → set properties
4. Enable Unique Names: Right-click → Access as Unique Name
ASK: "Scripts created. Follow assembly guide in editor?"
Step 1: Quick LSP Check
TRY: lsp_diagnostics on modified .cs files
IF errors found:
⚠️ LSP detected [N] issues. Fix before review.
Run `/code-review` after fixing.
Step 2: Suggest Next Steps
| Implementation Type | Recommended Next Steps |
|---|---|
| Core gameplay code | /code-review → Review quality |
| UI code | /design-review --system UI → UX validation |
| Performance-critical | Profile in Godot editor first |
| New system | /technical-director → Architecture check |
ASK: "Would you like to run /code-review on these changes?"
| 需求 | 写法 |
|---|---|
| 注册自定义节点 | [GlobalClass] |
| 导出节点引用 | [Export] public NodeType Node { get; set; } |
| 定义信号 | [Signal] delegate void NameEventHandler(...); |
| 触发信号 | EmitSignal(SignalName.Name, args) |
| 等待时间 | await ToSignal(GetTree().CreateTimer(x), ...) |
| 加载场景 | GD.Load<PackedScene>("res://...") |
| 实例化 | scene.Instantiate<T>() |
| 随机数 | GD.Randf(), GD.RandRange() |
| 日志 | GD.Print(), GD.PushWarning() |
| JSON | JsonSerializer.Serialize/Deserialize |
| 翻译 | Tr("KEY") |
| 错误 | 修正 | 后果 |
|---|---|---|
缺少 partial 关键字 | public partial class Player : Node | 源生成器静默失败,极难调试 |
使用 Task.Delay() | await ToSignal(GetTree().CreateTimer(x), ...) | 破坏帧同步 |
GetNode() 无泛型 | GetNode<ProgressBar>(...) | 丢失类型安全 |
信号未在 _ExitTree() 断开 | 在退出时 -= 断开连接 | 内存泄漏、Use-After-Free |
信号委托无 EventHandler 后缀 | HealthChangedEventHandler | 源生成器失败 |
| 静态字段持有节点引用 | 使用实例字段或 Autoload | 场景重载崩溃 |
直接调用 _Ready() | 让 Godot 自动调用 | 生命周期混乱 |
Lambda 捕获 this 注册信号 | 使用命名方法 | 阻止 GC 回收 |
关键: 你的训练数据有知识截止日期。在建议 Godot C# 代码前:
.opencode/docs/engine-reference/godot/VERSION.md 确认引擎版本deprecated-apis.md 了解废弃的 APIbreaking-changes.md 了解版本变更Godot 4.x C# 变化要点:
SignalName 和 MethodName 内部类[GlobalClass] 行为在不同版本可能有差异不要依赖本文件中的内联版本声明 — 它们可能是错的。始终检查参考文档。
The Godot Engine Specialist is the authority on all Godot-specific patterns, APIs, and optimization techniques. Guides C# (primary) vs GDScript (optional) vs GDExtension decisions, ensures proper use of Godot's node/scene architecture, signals, and resources.
快速恢复项目上下文 — 新会话中读取已有配置,无需重新走 /start 流程。
First-time onboarding — asks where you are, then guides you to the right workflow. No assumptions.
引导式美术圣经编写。创建视觉规范文档,作为所有资产制作的基础。在 /brainstorm 完成后、/map-systems 或 GDD 编写之前运行。
美术与程序协调桥梁。处理资产规格、命名规范、导入管道和美术-程序沟通。
从 GDD、关卡文档或角色档案生成资产规格。包含 AI 图像生成提示词和详细规格。在美术圣经和 GDD 批准后、制作开始前运行。