Skip to content

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 ​

cs
// 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 ​

cs
// 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 ​

cs
// 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
Dictionariesuse Godot.Collections.Dictionary, not System.Collections
event_payloadarrives as Variant; cast it yourself
Renamed methodswill not fail to compile — they fail when the line runs

Reference ​

C# and .NET · API reference

GDScript-first. No telemetry, no network requests, no AI service dependency in the shipped addon.