C# and .NET
Verified
tools/verify_csharp.sh builds the shipped wrapper against the real Godot .NET SDK and runs it against the addon: C# starts a conversation with parameters, receives a blocking event, branches from inside the handler, and reads the player lock back. Checked on Godot 4.7.2 .NET with .NET SDK 8.0.425.
What does not work
C# cannot reference a GDScript class_name as a type. There is no NPCTalkKit type in C#; you hold the node as Node.
Calls go through Call(), properties through Get() / Set(), and signals through Connect() with string names. Mistakes surface at run time, not at compile time.
Playing a conversation
// Docs: /guide/csharp
//
// The addon is GDScript. It loads and runs in a Godot .NET project, but C#
// cannot reference a GDScript class_name as a type: you hold the node as Node
// and go through Call(), Get()/Set() and Connect(). Mistakes surface at run
// time, not at compile time.
//
// Compiled by tools/verify_csharp.sh against the real Godot .NET SDK, so a
// signature that stops existing fails there rather than in your game.
using Godot;
public partial class Blacksmith : Node2D
{
private Node _talk;
public override void _Ready()
{
_talk = GetNode("NPCTalkKit");
ConnectTalk();
}
// #region play
public void Greet(string playerName)
{
var args = new Godot.Collections.Dictionary { { "player_name", playerName } };
_talk.Call("play", "greeting", args);
}
// #endregion
// #region signals
private void ConnectTalk()
{
_talk.Connect("event_blocked",
Callable.From((StringName id, Variant payload) => OnBlocked(id, payload)));
_talk.Connect("request_player_lock",
Callable.From((bool locked) => SetPlayerLocked(locked)));
}
private void OnBlocked(StringName eventId, Variant payload)
{
if (eventId == "offer_job")
_talk.Call("play", PlayerAccepts() ? "accepted" : "declined");
}
// #endregion
// #region wrapper
// A thin wrapper puts the rest of your code back in typed C#. The surface
// is small because v0.4 authors content in the editor, not in code.
private bool IsTalking() => _talk.Call("is_running").AsBool();
private bool IsBlocked() => _talk.Call("is_blocked").AsBool();
private void Resume() => _talk.Call("resume");
// #endregion
private bool PlayerAccepts() => true;
private void SetPlayerLocked(bool locked) { }
}Signals
// Docs: /guide/csharp
//
// The addon is GDScript. It loads and runs in a Godot .NET project, but C#
// cannot reference a GDScript class_name as a type: you hold the node as Node
// and go through Call(), Get()/Set() and Connect(). Mistakes surface at run
// time, not at compile time.
//
// Compiled by tools/verify_csharp.sh against the real Godot .NET SDK, so a
// signature that stops existing fails there rather than in your game.
using Godot;
public partial class Blacksmith : Node2D
{
private Node _talk;
public override void _Ready()
{
_talk = GetNode("NPCTalkKit");
ConnectTalk();
}
// #region play
public void Greet(string playerName)
{
var args = new Godot.Collections.Dictionary { { "player_name", playerName } };
_talk.Call("play", "greeting", args);
}
// #endregion
// #region signals
private void ConnectTalk()
{
_talk.Connect("event_blocked",
Callable.From((StringName id, Variant payload) => OnBlocked(id, payload)));
_talk.Connect("request_player_lock",
Callable.From((bool locked) => SetPlayerLocked(locked)));
}
private void OnBlocked(StringName eventId, Variant payload)
{
if (eventId == "offer_job")
_talk.Call("play", PlayerAccepts() ? "accepted" : "declined");
}
// #endregion
// #region wrapper
// A thin wrapper puts the rest of your code back in typed C#. The surface
// is small because v0.4 authors content in the editor, not in code.
private bool IsTalking() => _talk.Call("is_running").AsBool();
private bool IsBlocked() => _talk.Call("is_blocked").AsBool();
private void Resume() => _talk.Call("resume");
// #endregion
private bool PlayerAccepts() => true;
private void SetPlayerLocked(bool locked) { }
}A wrapper ships with the addon
addons/npc_talkkit/csharp/TalkKit.cs is a typed face over the calls above:
_talk = TalkKit.Of(this);
_talk.EventBlocked += OnBlocked;
_talk.Play("greeting", TalkKit.Params("player_name", "Alex"));Run the check yourself with:
tools/verify_csharp.sh /path/to/Godot_mono.app/Contents/MacOS/GodotIt builds the wrapper in an otherwise empty project and drives the addon with it, so a signature that stops existing fails there rather than in your game.
A .cs file in a standard Godot build has no importer and is ignored. That was verified by importing this project on 4.7.2 standard with the file present: no errors, no warnings.
Writing your own instead
// Docs: /guide/csharp
//
// The addon is GDScript. It loads and runs in a Godot .NET project, but C#
// cannot reference a GDScript class_name as a type: you hold the node as Node
// and go through Call(), Get()/Set() and Connect(). Mistakes surface at run
// time, not at compile time.
//
// Compiled by tools/verify_csharp.sh against the real Godot .NET SDK, so a
// signature that stops existing fails there rather than in your game.
using Godot;
public partial class Blacksmith : Node2D
{
private Node _talk;
public override void _Ready()
{
_talk = GetNode("NPCTalkKit");
ConnectTalk();
}
// #region play
public void Greet(string playerName)
{
var args = new Godot.Collections.Dictionary { { "player_name", playerName } };
_talk.Call("play", "greeting", args);
}
// #endregion
// #region signals
private void ConnectTalk()
{
_talk.Connect("event_blocked",
Callable.From((StringName id, Variant payload) => OnBlocked(id, payload)));
_talk.Connect("request_player_lock",
Callable.From((bool locked) => SetPlayerLocked(locked)));
}
private void OnBlocked(StringName eventId, Variant payload)
{
if (eventId == "offer_job")
_talk.Call("play", PlayerAccepts() ? "accepted" : "declined");
}
// #endregion
// #region wrapper
// A thin wrapper puts the rest of your code back in typed C#. The surface
// is small because v0.4 authors content in the editor, not in code.
private bool IsTalking() => _talk.Call("is_running").AsBool();
private bool IsBlocked() => _talk.Call("is_blocked").AsBool();
private void Resume() => _talk.Call("resume");
// #endregion
private bool PlayerAccepts() => true;
private void SetPlayerLocked(bool locked) { }
}The surface C# has to touch is small, because v0.4 authors content in the editor rather than in code. For most games it is play() and two signals.
What is unaffected
Everything authored in the TalkKit workspace — conversations, styles, anchors, templates — works identically. None of it goes through C#.