Calling TalkKit from C#
Verified
The wrapper below is compiled against the real Godot .NET SDK and run against the addon by tools/verify_csharp.sh, on Godot 4.7.2 .NET with .NET SDK 8.0.425.
The constraint
C# cannot reference a GDScript class_name as a type. There is no NPCTalkKit type available to C#; you hold the node as Node and go through Call(), Get() / Set() and Connect() with string names.
That means typos become run-time errors rather than compile errors — which is why the wrapper below is worth the ten minutes.
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) { }
}Listening
// 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) { }
}Signal names and argument types match the GDScript API exactly.
Getting typed code back
// 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) { }
}Wrap once, and the rest of your game talks to a normal C# class.
Why the surface is small
Everything authored in the editor — conversations, styles, anchors, templates, transitions — never touches C#. The only things that cross the boundary are starting a conversation and reacting to a couple of signals. For most games that is play(), event_blocked and request_player_lock.
What to watch for
Call("play", "greeting") | the name is a StringName in GDScript; a C# string converts fine |
| Dictionaries | use Godot.Collections.Dictionary, not System.Collections |
event_payload | arrives as Variant; cast it yourself |
| Renamed methods | will not fail to compile — they fail when the line runs |