Lingara Lingara 문서 학습 가이드 API 라이브러리 앱 만들기 임베드 웹 앱
언어: 한국어

Godot

이 페이지는 영어에서 번역되었습니다. 두 내용이 다를 경우 영어 페이지가 정확합니다. 영어 페이지 읽기

Godot는 두 가지 언어를 제공하며, Lingara에는 각각을 위한 키트가 있습니다. GDScript 게임은 Lingara 애드온을 사용합니다. Godot C# 게임은 Unity 게임이 쓰는 것과 같은 패키지인 Lingara.Embed를 사용합니다. 한 프로젝트에서는 둘 중 하나만 쓰고, 둘 다 쓰지 마세요. 아래 탭에서 각각을 보여 줍니다.

설치

GDScript. Godot Asset Library에서 Lingara 애드온을 설치한 다음, Project Settings의 Plugins에서 활성화합니다. 활성화하면 Lingara라는 이름의 오토로드가 추가되며, 모든 씬에서 /root/Lingara로 접근할 수 있습니다.

C#. 프로젝트에 Lingara.Embed를 패키지 참조로 추가합니다. 아래 샘플 노드인 LingaraEmbedNode는 패키지에 포함되어 있지 않습니다. 그 소스를 프로젝트에 추가하고 씬에 배치하세요.

서버에서 토큰 받기

어느 키트도 시크릿을 보관하지 않습니다. 코어가 토큰이 필요하면 여러분 자체의 서버에 요청하고, 서버는 서버 키트로 토큰을 발급해 토큰과 그 만료 시각으로 응답합니다.

애드온에서는 Project Settings에 서버의 토큰 라우트를 설정하면 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"))