15.6 前后端通信实现

配套代码:code/chapter15

15.6.1 API 客户端封装

Godot 前端需要与 FastAPI 后端进行 HTTP 通信。我们创建一个 API 客户端脚本api_client.gd,封装所有的 API 调用,并将其设置为 AutoLoad(自动加载)单例,让其他脚本可以方便地使用。

API 客户端使用 Godot 的 HTTPRequest 节点来发送 HTTP 请求。HTTPRequest 是一个异步节点,发送请求后不会阻塞游戏,而是通过信号通知请求完成。这样可以保证游戏的流畅性,即使网络延迟较高也不会卡顿。我们使用信号机制来通知其他脚本 API 响应,而不是使用 await,这样可以让多个脚本同时监听同一个 API 响应。

# api_client.gd
extends Node

# 信号定义
signal chat_response_received(npc_name: String, message: String)
signal chat_error(error_message: String)
signal npc_status_received(dialogues: Dictionary)
signal npc_list_received(npcs: Array)

# HTTP请求节点
var http_chat: HTTPRequest
var http_status: HTTPRequest
var http_npcs: HTTPRequest

func _ready():
    # 创建HTTP请求节点
    http_chat = HTTPRequest.new()
    http_status = HTTPRequest.new()
    http_npcs = HTTPRequest.new()

    add_child(http_chat)
    add_child(http_status)
    add_child(http_npcs)

    # 连接信号
    http_chat.request_completed.connect(_on_chat_request_completed)
    http_status.request_completed.connect(_on_status_request_completed)
    http_npcs.request_completed.connect(_on_npcs_request_completed)

# ==================== 对话API ====================
func send_chat(npc_name: String, message: String) -> void:
    """发送对话请求"""
    var data = {
        "npc_name": npc_name,
        "message": message
    }

    var json_string = JSON.stringify(data)
    var headers = ["Content-Type: application/json"]

    var error = http_chat.request(
        Config.API_CHAT,
        headers,
        HTTPClient.METHOD_POST,
        json_string
    )

    if error != OK:
        print("[ERROR] 发送对话请求失败: ", error)
        chat_error.emit("网络请求失败")

func _on_chat_request_completed(_result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
    """处理对话响应"""
    if response_code != 200:
        print("[ERROR] 对话请求失败: HTTP ", response_code)
        chat_error.emit("服务器错误: " + str(response_code))
        return

    var json = JSON.new()
    var parse_result = json.parse(body.get_string_from_utf8())

    if parse_result != OK:
        print("[ERROR] 解析响应失败")
        chat_error.emit("响应解析失败")
        return

    var response = json.data

    if response.has("success") and response["success"]:
        var npc_name = response["npc_name"]
        var msg = response["message"]
        print("[INFO] 收到NPC回复: ", npc_name, " -> ", msg)
        chat_response_received.emit(npc_name, msg)
    else:
        chat_error.emit("对话失败")

# ==================== NPC状态API ====================
func get_npc_status() -> void:
    """获取NPC状态"""
    # 检查是否正在处理请求
    if http_status.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
        print("[WARN] NPC状态请求正在处理中,跳过本次请求")
        return

    var error = http_status.request(Config.API_NPC_STATUS)

    if error != OK:
        print("[ERROR] 获取NPC状态失败: ", error)

func _on_status_request_completed(_result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
    """处理NPC状态响应"""
    if response_code != 200:
        print("[ERROR] NPC状态请求失败: HTTP ", response_code)
        return

    var json = JSON.new()
    var parse_result = json.parse(body.get_string_from_utf8())

    if parse_result != OK:
        print("[ERROR] 解析NPC状态失败")
        return

    var response = json.data

    if response.has("dialogues"):
        var dialogues = response["dialogues"]
        print("[INFO] 收到NPC状态更新: ", dialogues.size(), "个NPC")
        npc_status_received.emit(dialogues)

# ==================== NPC列表API ====================
func get_npc_list() -> void:
    """获取NPC列表"""
    var error = http_npcs.request(Config.API_NPCS)

    if error != OK:
        print("[ERROR] 获取NPC列表失败: ", error)

func _on_npcs_request_completed(_result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
    """处理NPC列表响应"""
    if response_code != 200:
        print("[ERROR] NPC列表请求失败: HTTP ", response_code)
        return

    var json = JSON.new()
    var parse_result = json.parse(body.get_string_from_utf8())

    if parse_result != OK:
        print("[ERROR] 解析NPC列表失败")
        return

    var response = json.data

    if response.has("npcs"):
        var npcs = response["npcs"]
        print("[INFO] 收到NPC列表: ", npcs.size(), "个NPC")
        npc_list_received.emit(npcs)

这个 API 客户端封装了三个核心功能:发送对话请求(send_chat)、获取 NPC 状态(get_npc_status)和获取 NPC 列表(get_npc_list)。所有的 HTTP 请求都是异步的,通过信号通知响应结果。我们为每个 API 创建了独立的 HTTPRequest 节点,这样可以同时发送多个请求而不会互相干扰。API 的 URL 从 Config 单例中获取,方便统一管理。对话系统监听chat_response_received信号来接收 NPC 回复,主场景监听npc_status_received信号来更新 NPC 对话气泡。

15.6.2 对话 UI 实现

对话 UI 是玩家与 NPC 交互的界面。我们需要设计一个简洁美观的对话框,包含 NPC 名称、职位、对话内容显示、输入框和按钮。

对话 UI 的结构如图 15.13 所示:

图 15.13 对话 UI 结构

对话 UI 的设计非常简洁。DialogueUI 是一个 CanvasLayer 节点,这意味着它会始终显示在游戏画面的最上层,不会被其他游戏对象遮挡。Panel 是对话框的背景,锚定在屏幕底部。Panel 下直接放置了 6 个 UI 元素:NPCName 显示 NPC 的名字,NPCTitle 显示职位,DialogueText 使用 RichTextLabel 显示对话内容(支持富文本格式),PlayerInput 是一个 LineEdit 用于玩家输入,SendButton 和 CloseButton 分别用于发送消息和关闭对话框。

对话 UI 脚本dialogue_ui.gd实现了对话界面的逻辑:

# dialogue_ui.gd
extends CanvasLayer

# UI节点引用
@onready var panel = $Panel
@onready var npc_name_label = $Panel/NPCName
@onready var npc_title_label = $Panel/NPCTitle
@onready var dialogue_text = $Panel/DialogueText
@onready var input_field = $Panel/PlayerInput
@onready var send_button = $Panel/SendButton
@onready var close_button = $Panel/CloseButton

# API客户端
var api_client: Node = null

# 当前对话的NPC
var current_npc_name: String = ""

func _ready():
    # 初始化时隐藏对话框
    visible = false

    # 连接按钮信号
    send_button.pressed.connect(_on_send_button_pressed)
    close_button.pressed.connect(_on_close_button_pressed)
    input_field.text_submitted.connect(_on_text_submitted)

    # 获取API客户端
    api_client = get_node_or_null("/root/APIClient")

func start_dialogue(npc_name: String):
    """开始与NPC对话"""
    current_npc_name = npc_name

    # 设置NPC信息
    npc_name_label.text = npc_name
    npc_title_label.text = get_npc_title(npc_name)

    # 清空对话内容
    dialogue_text.clear()
    dialogue_text.append_text("[color=gray]与 " + npc_name + " 的对话开始...[/color]\n")

    # 清空输入框
    input_field.text = ""

    # 显示对话框
    show_dialogue()

    # 聚焦输入框
    input_field.grab_focus()

func show_dialogue():
    """显示对话框"""
    visible = true

    # 通知玩家进入交互状态(禁用移动)
    var player = get_tree().get_first_node_in_group("player")
    if player and player.has_method("set_interacting"):
        player.set_interacting(true)

func hide_dialogue():
    """隐藏对话框"""
    visible = false
    current_npc_name = ""

    # 通知玩家退出交互状态(启用移动)
    var player = get_tree().get_first_node_in_group("player")
    if player and player.has_method("set_interacting"):
        player.set_interacting(false)

func _on_send_button_pressed():
    """发送按钮点击"""
    send_message()

func _on_close_button_pressed():
    """关闭按钮点击"""
    hide_dialogue()

func _on_text_submitted(_text: String):
    """输入框回车"""
    send_message()

func send_message():
    """发送消息"""
    var message = input_field.text.strip_edges()

    if message.is_empty():
        return

    if current_npc_name.is_empty():
        return

    # 显示玩家消息
    dialogue_text.append_text("\n[color=cyan]玩家:[/color] " + message + "\n")

    # 清空输入框
    input_field.text = ""

    # 禁用输入
    input_field.editable = false
    send_button.disabled = true

    # 发送API请求
    if api_client:
        api_client.send_chat_request(current_npc_name, message)

func on_chat_response_received(npc_name: String, response: String):
    """收到NPC回复"""
    if npc_name == current_npc_name:
        # 显示NPC回复
        dialogue_text.append_text("[color=yellow]" + npc_name + ":[/color] " + response + "\n")

        # 启用输入
        input_field.editable = true
        send_button.disabled = false
        input_field.grab_focus()

func get_npc_title(npc_name: String) -> String:
    """获取NPC职位"""
    var titles = {
        "张三": "Python工程师",
        "李四": "产品经理",
        "王五": "UI设计师"
    }
    return titles.get(npc_name, "")

这个对话 UI 实现了完整的对话功能。玩家可以输入消息并发送,UI 使用 RichTextLabel 的 append_text 方法显示对话内容,支持富文本格式(颜色、粗体等)。所有的 API 调用都是异步的,在等待响应时会禁用输入框,防止重复发送。对话框显示时会通知玩家进入交互状态,禁用移动,关闭时恢复移动。

15.6.3 主场景整合

最后,我们需要在主场景中整合所有的功能:玩家控制、NPC 交互、对话 UI 和 NPC 状态更新。主场景脚本main.gd负责协调这些组件,并定时从后端获取 NPC 状态,更新 NPC 的对话气泡。

# main.gd
extends Node2D

# NPC节点引用
@onready var npc_zhang: Node2D = $NPCs/NPC_Zhang
@onready var npc_li: Node2D = $NPCs/NPC_Li
@onready var npc_wang: Node2D = $NPCs/NPC_Wang

# API客户端
var api_client: Node = null

# NPC状态更新计时器
var status_update_timer: float = 0.0

func _ready():
    print("[INFO] 主场景初始化")

    # 获取API客户端
    api_client = get_node_or_null("/root/APIClient")
    if api_client:
        api_client.npc_status_received.connect(_on_npc_status_received)

        # 立即获取一次NPC状态
        api_client.get_npc_status()
    else:
        print("[ERROR] API客户端未找到")

func _process(delta: float):
    # 定时更新NPC状态
    status_update_timer += delta
    if status_update_timer >= Config.NPC_STATUS_UPDATE_INTERVAL:
        status_update_timer = 0.0
        if api_client:
            api_client.get_npc_status()

func _on_npc_status_received(dialogues: Dictionary):
    """收到NPC状态更新"""
    print("[INFO] 更新NPC状态: ", dialogues)

    # 更新各个NPC的对话
    for npc_name in dialogues:
        var dialogue = dialogues[npc_name]
        update_npc_dialogue(npc_name, dialogue)

func update_npc_dialogue(npc_name: String, dialogue: String):
    """更新指定NPC的对话"""
    var npc_node = get_npc_node(npc_name)
    if npc_node and npc_node.has_method("update_dialogue"):
        npc_node.update_dialogue(dialogue)

func get_npc_node(npc_name: String) -> Node2D:
    """根据名字获取NPC节点"""
    match npc_name:
        "张三":
            return npc_zhang
        "李四":
            return npc_li
        "王五":
            return npc_wang
        _:
            return null

主场景脚本的核心功能是定时从后端获取 NPC 状态。在_ready()中,我们获取 APIClient 单例的引用,并连接npc_status_received信号。然后立即调用get_npc_status()获取一次 NPC 状态。在_process()中,我们使用计时器每隔Config.NPC_STATUS_UPDATE_INTERVAL秒(默认 30 秒)调用一次get_npc_status()。当收到 NPC 状态更新时,_on_npc_status_received()回调函数会遍历所有 NPC,调用它们的update_dialogue()方法更新对话气泡。这样,即使玩家不与 NPC 交互,也能看到 NPC 之间的自主对话。

整个前后端通信流程如图 15.14 所示:

图 15.14 前后端通信完整流程

至此,前后端通信的所有功能都已实现。玩家可以在游戏中自由移动,与 NPC 互动,进行自然语言对话。同时,主场景会定时从后端获取 NPC 状态,更新 NPC 的对话气泡,展示 NPC 之间的自主对话。整个系统使用信号机制进行通信,各个组件之间松耦合,易于维护和扩展。