Lingara Lingara 說明文件 學習指南 API 程式庫 App 建立 嵌入 網頁版
語言: 粵語

Godot

呢頁係由英文翻譯過嚟嘅。如果兩個版本有出入,以英文頁為準。 睇英文頁

Godot 提供兩種語言,Lingara 為每一種都準備咗套件。GDScript 遊戲用 Lingara 外掛程式。Godot C# 遊戲用 Lingara.Embed,即係 Unity 遊戲用嗰個一模一樣嘅 NuGet 套件。一個專案入面淨係用其中一個,永遠唔好兩個一齊用。下面嘅分頁分別示範咗兩者。

安裝

GDScript。 由 Godot Asset Library 安裝 Lingara 外掛程式,然後喺「專案設定」嘅「外掛程式」度啟用佢。啟用之後會加入一個叫 Lingara 嘅自動載入,每個場景都可以透過 /root/Lingara 存取佢。

C#。 將 Lingara.Embed 以套件參照嘅方式加入你個專案。下面嘅範例節點 LingaraEmbedNode 唔屬於嗰個套件:將佢嘅原始碼加入你個專案,再將佢放入一個場景。

由你部伺服器攞權杖

兩個套件都唔會持有密鑰。核心需要權杖嗰陣,會問你自己部伺服器攞,伺服器用伺服器套件簽發一個權杖,再回覆權杖同佢嘅到期時間。

用外掛程式嘅話,喺「專案設定」度設定你部伺服器嘅權杖路由,Lingara 就會喺佢自己嘅執行緒上面呼叫佢。如果嗰條路由需要玩家嘅工作階段,就要改為喺執行期間設定,因為專案設定會跟住你嘅匯出一齊發佈。又或者連接 token_needed,自己回應每一個請求。用 C# 嘅話,每當用戶端要權杖,範例節點都會向你嘅權杖路由傳送 POST 請求。無論用邊個方法,都要用 token_unavailable 或者 TokenUnavailable 回報失敗,等緊嘅呼叫就會即刻失敗。

每個畫格處理事件

核心喺佢自己嘅執行緒上面做網絡 I/O。外掛程式嘅自動載入每個畫格清空一次佇列,每個事件發出一個訊號,所以你只需要連接你個遊戲需要嘅訊號,唔使自己清空任何嘢。C# 節點喺 _process 入面清空佇列,每個畫格最多 256 個事件,再將每個事件重新以 Godot 訊號發出。

同樣嘅訊號亦都會帶住串流嘅 NPC 回覆(睇 NPC 對話)同練習更新(睇練習結果)。

<PackageReference Include="Lingara.Embed" Version="0.1.0-alpha.6" />
程式碼例子嘅語言

Godot C#

// Your own server mints the token: never put a client secret in a game. The
// continuation may run off the main thread, which the client allows.
private async Task FetchToken()
{
    try
    {
        using var response = await Http.PostAsync(TokenUrl, new StringContent("{}"));
        response.EnsureSuccessStatusCode();
        using var body = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
        Client?.ProvideToken(body.RootElement.GetProperty("token").GetString(), body.RootElement.GetProperty("expires_at").GetString());
    }
    catch (Exception e) when (e is HttpRequestException || e is JsonException || e is KeyNotFoundException)
    {
        Client?.TokenUnavailable(e.Message); // fail the waiting calls now
    }
}

GDScript

# The common case: set Project Settings > lingara/token_url to your server's
# token route, which answers exactly {"token", "expires_at"}. Lingara POSTs to
# it on its own thread whenever it needs a token. If the route needs the
# player's session, set it at run time instead (never in ProjectSettings,
# which ships inside the export):
func use_my_session(session_cookie: String) -> void:
	lingara.set_token_endpoint("https://game.example.com/lingara-token", {"cookie": session_cookie})


# Or answer each token_needed yourself, with whatever login your game has.
func _on_token_needed(_request: int, _reason: String) -> void:
	var http := HTTPRequest.new()
	add_child(http)
	http.request("https://game.example.com/lingara-token", [], HTTPClient.METHOD_POST, "{}")
	var reply: Array = await http.request_completed
	http.queue_free()
	var body: Variant = JSON.parse_string((reply[3] as PackedByteArray).get_string_from_utf8())
	if reply[1] == 200 and body is Dictionary:
		lingara.provide_token(body["token"], body["expires_at"])
	else:
		lingara.token_unavailable("the token route answered %d" % reply[1])
程式碼例子嘅語言

Godot C#

public override void _Process(double delta)
{
    if (Client == null) return;
    events.Clear();
    Client.Drain(events); // at most 256 a frame; the rest wait for the next
    foreach (var e in events)
    {
        switch (e)
        {
            case EmbedEvent.TokenNeeded _: _ = FetchToken(); break;
            case EmbedEvent.NpcTurn { Update: NpcTurnUpdate.Fragment f } t:
                EmitSignal(SignalName.NpcTurnFragment, t.Dialogue.Value, t.Request.Value, f.Text); break;
            case EmbedEvent.NpcTurn { Update: NpcTurnUpdate.Finished f } t:
                EmitSignal(SignalName.NpcTurnFinished, t.Dialogue.Value, t.Request.Value, f.Text); break;
            case EmbedEvent.Practice p: EmitSignal(SignalName.PracticeChanged, p.Session.Value, p.Snapshot.Phase.ToString()); break;
            case EmbedEvent.Failed f: EmitSignal(SignalName.Failed, f.Error.KindName, f.Error.Message, f.Error.Request?.Value ?? 0); break;
            case EmbedEvent.Log log: GD.Print($"[lingara] {log.Level} {log.Target}: {log.Message}"); break;
        }
    }
}

GDScript

# The Lingara autoload the plugin adds, reached by path and typed, so every
# call and signal below is checked against LingaraEmbed.
@onready var lingara: LingaraEmbed = get_node("/root/Lingara")

# Enable the plugin (Project > Project Settings > Plugins): it adds the
# Lingara autoload, which drains the core once per frame and emits one signal
# per event. Connect the signals your game needs; you never call drain.
func _ready() -> void:
	lingara.token_needed.connect(_on_token_needed)
	lingara.event_received.connect(_on_event)
	lingara.error_raised.connect(func(error: Dictionary) -> void: push_warning(error.message))
	lingara.practice_changed.connect(_on_practice_changed)
	lingara.practice_finished.connect(_on_practice_finished)
	mei.replied.connect(_on_mei_replied)


func _on_event(_id: String, event_type: String, _created_at: String, data: Dictionary) -> void:
	if event_type == "lesson_plan.ready":
		print("a plan is ready: ", data.get("plan_id"))