Blender & Godot 3D 教程 - 2026-07-06 - Godot 4 联机传输层零基础入门:WebSocket + WebRTC,让浏览器、移动端、P2P 低延迟都能联机

零基础搞懂 Godot 4.6 的另外两条联机通道:用 Python 起一个 WebSocket 信令服务器,让 Godot 客户端(包括 Web 导出)能跨平台联机;再用 WebRTCMultiplayerPeer 跑一个 P2P Demo,3D 角色位置延迟从 ENet 的 80ms 压到 20ms,适合 FPS / 格斗 / 任何对操作手感敏感的游戏。

cover

上一篇回顾:2026-06-15 我们讲了联机架构总览(C/S / P2P / 权威服务器),2026-06-16 用 Godot 4 的 ENetMultiplayerPeer 跑通了第一个双窗口 3D Demo。但 ENet 只走原生 UDP——想做浏览器联机、想做超低延迟位置同步,ENet 就顶不住了

Godot 4 联机传输层零基础入门:WebSocket + WebRTC,让浏览器、移动端、P2P 低延迟都能联机

今天这一篇,是「Godot 4 联机三件套」的最后一块拼图:

  • ENet(06-16):原生 UDP,速度快、API 简单,但不支持浏览器
  • WebSocket(今天上半场):基于 TCP,浏览器天然支持,适合大厅、信令、移动端。
  • WebRTC(今天下半场):P2P 直连,延迟能压到 20ms 以内,适合 FPS / 格斗 / 任何对操作手感敏感的游戏。

读完这一篇,你就能在自己的游戏里同时支持 PC、移动端、浏览器三种客户端,并且知道什么时候该用哪条传输通道。


一、为什么学完 ENet 还要学 WebSocket / WebRTC?

先回答一个常见的疑问:「我都用 ENet 跑通 Demo 了,为什么还要折腾 WebSocket 和 WebRTC?」

看这张对比表就清楚了:

传输层协议浏览器支持移动端支持延迟适用场景
ENetMultiplayerPeerUDP(自实现可靠层)❌ 不支持30–80msPC 原生客户端、大厅内战斗
WebSocketPeerTCP(HTTP Upgrade)✅ 天然支持50–150ms浏览器联机、信令、聊天、移动端
WebRTCMultiplayerPeerUDP(P2P 直连 + DTLS/SRTP)✅ 天然支持10–30msFPS / 格斗 / 任何对手感敏感的场景

一句话总结

  • ENet 是「PC 原生客户端的瑞士军刀」。
  • WebSocket 是「让浏览器跑起来的唯一选择」(浏览器没有裸 UDP)。
  • WebRTC 是「低延迟场景的杀手锏」(走 P2P,不经过服务器中转)。

💡 为什么这样做:现实里 60% 的玩家会用浏览器打开你的游戏链接试玩 5 分钟,决定要不要下载客户端。如果你的游戏只能在 PC 原生客户端联机,这一关直接劝退。


二、准备工作:装好 Godot 4 和 Python

今天的 Demo 需要两个东西:

  1. Godot 4.6官方下载),带 Web 和 WebRTC 模板
  2. Python 3.10+,用来跑 WebSocket 信令服务器(不到 60 行代码)。

2.1 验证 Godot 4 的 WebRTC 支持

打开 Godot,新建项目,按 F5 之前先在编辑器里跑一下:

1
2
godot --version
# Godot Engine v4.6.stable.official

WebRTC 是 Godot 4 内置模块,不需要额外安装,但Web 导出模板必须用「HTML5 with WebAssembly」,否则浏览器跑不起来。

2.2 安装 Python WebSocket 库

打开终端:

1
python -m pip install websockets==12.0

💡 为什么这样做websockets 是 Python 生态里最干净的 WebSocket 实现,异步写、几行就能起一个服务。如果你在公司里已经有 Node.js / Go 的信令服务器,可以跳过这一节,直接看 Godot 端的代码。


三、WebSocket 部分:跑通一个能跨平台的聊天大厅

我们先做最实用的部分——用 Python 写一个 WebSocket 信令服务器,Godot 4 客户端连上去互相发消息

这个 Demo 跑通之后,你就能:

  • 让浏览器玩家和 PC 玩家在同一个大厅里。
  • 把 06-28 的 Matchmaking / Lobby 系统搬过来用。
  • 用 WebSocket 当 WebRTC 的信令通道(后面下半场会用到)。

3.1 Python 信令服务器(60 行)

新建 signalling_server.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import asyncio
import websockets
import json

# 所有在线客户端:{websocket: {"id": int, "name": str}}
clients: dict = {}
next_id = 1


async def broadcast(message: dict, exclude=None):
    """把消息发给所有客户端,可选排除某个连接。"""
    if exclude is None:
        exclude = set()
    data = json.dumps(message)
    await asyncio.gather(
        *[ws.send(data) for ws in clients if ws not in exclude],
        return_exceptions=True,
    )


async def handler(ws):
    """每个客户端连接的处理函数。"""
    global next_id
    my_id = next_id
    next_id += 1
    clients[ws] = {"id": my_id, "name": f"Player_{my_id}"}

    # 1) 告诉新人"你是几号、现在大厅里都有谁"
    await ws.send(json.dumps({
        "type": "welcome",
        "your_id": my_id,
        "peers": [{"id": info["id"], "name": info["name"]}
                  for info in clients.values()],
    }))

    # 2) 告诉所有人"新人来了"
    await broadcast({
        "type": "peer_joined",
        "id": my_id,
        "name": clients[ws]["name"],
    })

    try:
        async for raw in ws:
            msg = json.loads(raw)

            # 客户端可以改自己的昵称
            if msg.get("type") == "rename":
                clients[ws]["name"] = msg["name"]
                await broadcast({"type": "peer_renamed",
                                 "id": my_id,
                                 "name": msg["name"]})

            # 转发聊天消息给所有人
            elif msg.get("type") == "chat":
                await broadcast({
                    "type": "chat",
                    "id": my_id,
                    "name": clients[ws]["name"],
                    "text": msg["text"],
                })

    finally:
        # 客户端断开,告诉所有人"他走了"
        del clients[ws]
        await broadcast({"type": "peer_left", "id": my_id})


async def main():
    async with websockets.serve(handler, "0.0.0.0", 8765):
        print("✅ WebSocket signalling server on ws://localhost:8765")
        await asyncio.Future()  # 永久运行


if __name__ == "__main__":
    asyncio.run(main())

跑起来:

1
2
python signalling_server.py
# ✅ WebSocket signalling server on ws://localhost:8765

💡 为什么这样做:真实的游戏信令服务器要处理「鉴权、重连、消息加密」,但今天我们聚焦传输层,所以用最简版本。生产环境把这层换成 gRPC-WebNATS 都行,关键是 Godot 端的 WebSocketPeer 代码基本不用改。

3.2 Godot 4 客户端(GDScript 80 行)

新建 Godot 项目,根节点用一个 Control(脚本挂在它上面):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# res://scripts/lobby.gd
extends Control

const URL := "ws://localhost:8765"

var _socket := WebSocketPeer.new()
var _my_id: int = -1
var _peer_names: Dictionary = {}

@onready var _log: RichTextLabel = %Log
@onready var _input: LineEdit = %Input


func _ready() -> void:
    _socket.connect_to_url(URL)
    set_process(true)


func _process(_delta: float) -> void:
    _socket.poll()
    var state := _socket.get_ready_state()
    if state == WebSocketPeer.STATE_OPEN:
        while _socket.get_available_packet_count() > 0:
            var packet := _socket.get_packet().get_string_from_utf8()
            _handle_message(JSON.parse_string(packet))


func _handle_message(msg: Dictionary) -> void:
    match msg.get("type"):
        "welcome":
            _my_id = msg["your_id"]
            _log_line("🟢 你连上了,ID = %d" % _my_id)
            for peer in msg["peers"]:
                _peer_names[peer["id"]] = peer["name"]

        "peer_joined":
            _peer_names[msg["id"]] = msg["name"]
            _log_line("👋 %s 加入了" % msg["name"])

        "peer_renamed":
            _peer_names[msg["id"]] = msg["name"]
            _log_line("✏️  %s 改名了" % msg["name"])

        "peer_left":
            var name = _peer_names.get(msg["id"], "#%d" % msg["id"])
            _peer_names.erase(msg["id"])
            _log_line("🚪 %s 离开了" % name)

        "chat":
            _log_line("[%s] %s" % [msg["name"], msg["text"]])


func _on_send_pressed() -> void:
    if _input.text.is_empty():
        return
    _socket.send_text(JSON.stringify({
        "type": "chat",
        "text": _input.text,
    }))
    _input.clear()


func _log_line(s: String) -> void:
    _log.append_text(s + "\n")

场景树大致这样:

Control (挂 lobby.gd)
├── VBoxContainer
│   ├── RichTextLabel (unique_name: %Log) —— 占据大部分空间
│   ├── HBoxContainer
│   │   ├── LineEdit (unique_name: %Input) —— placeholder="说点什么…"
│   │   └── Button (text="发送", pressed -> _on_send_pressed)

💡 为什么这样做WebSocketPeer 在 Godot 4 里是非阻塞的,必须每帧调一次 poll() 才能收到消息。这是和 06-16 的 ENet 一样的写法——Godot 的所有网络 API 都遵循「主线程 poll,子线程别碰节点」的约定。

3.3 验证:开两个窗口互相聊天

  1. 起 Python 服务器。
  2. 在 Godot 里按 F5 跑两个实例(Project → Run Instances → 设置成 2)。
  3. 两个窗口都会显示「🟢 你连上了」。
  4. 一边发消息,另一边立刻看到。

到这一步,浏览器联机已经打通了。下一步用 Godot 的 Web 导出,就能让一个 Chrome 标签页和一个 PC 客户端在同一个大厅里聊天。


四、WebRTC 部分:跑一个 P2P 低延迟 3D 位置同步

WebSocket 解决了「能联上」的问题,但延迟还是 50ms+。

要做 FPS / 格斗游戏,你得用 WebRTC:客户端之间直接走 P2P,服务器只当「告诉对方我的 IP/端口」的中间人。

4.1 WebRTC 的工作流程(30 秒看懂)

  浏览器 A                          Python 信令服务器                     浏览器 B
     │                                   │                                  │
     │──── "我要连 B" ────────────────────>                                  │
     │                                   │──── "A 想连你" ──────────────────>│
     │                                   │                                  │
     │<────── "B 同意,把他的 SDP 给你" ──┴─────── "B 的 SDP" ───────────────│
     │                                                                          │
     │═══════ P2P 直连(不走服务器)═════════════════════════════════════════>│
     │                              延迟 10–30ms                                  │

关键术语:

  • SDP(Session Description Protocol):一段「我能接收/发送什么编码、IP/端口是什么」的字符串。
  • ICE Candidate:本机可能的网络路径(局域网 IP、公网 IP、STUN 反射地址)。
  • 信令服务器:只负责搬运 SDP 和 ICE,不参与实际游戏数据传输。

4.2 扩展信令服务器:支持 WebRTC 握手

signalling_server.py 里加几行(完整的差分,加在 handler() 函数里):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
async def handler(ws):
    global next_id
    my_id = next_id
    next_id += 1
    clients[ws] = {"id": my_id, "name": f"Player_{my_id}"}

    await ws.send(json.dumps({
        "type": "welcome",
        "your_id": my_id,
        "peers": [{"id": info["id"], "name": info["name"]}
                  for info in clients.values()],
    }))
    await broadcast({"type": "peer_joined",
                     "id": my_id,
                     "name": clients[ws]["name"]})

    try:
        async for raw in ws:
            msg = json.loads(raw)

            if msg.get("type") == "rename":
                clients[ws]["name"] = msg["name"]
                await broadcast({"type": "peer_renamed",
                                 "id": my_id,
                                 "name": msg["name"]})

            elif msg.get("type") == "chat":
                await broadcast({"type": "chat",
                                 "id": my_id,
                                 "name": clients[ws]["name"],
                                 "text": msg["text"]})

            # ★ 新增:把 SDP / ICE 转发给指定 peer
            elif msg.get("type") in ("sdp_offer", "sdp_answer", "ice_candidate"):
                target_id = msg["to"]
                for other_ws, info in clients.items():
                    if info["id"] == target_id:
                        msg["from"] = my_id  # 让对方知道是谁发的
                        await other_ws.send(json.dumps(msg))
                        break

    finally:
        del clients[ws]
        await broadcast({"type": "peer_left", "id": my_id})

💡 为什么这样做:信令服务器本质就是「两个人之间的消息中转站」,所以加 30 行就能复用之前的代码。

4.3 Godot 4 WebRTC 客户端(核心 90 行)

新建 res://scripts/webrtc_lobby.gd

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# res://scripts/webrtc_lobby.gd
extends Node3D

const SIGNAL_URL := "ws://localhost:8765"
const POS_TOPIC := 1   # MultiplayerAPI.RPC_CHANNEL 外的自定义 channel id

var _ws := WebSocketPeer.new()
var _my_id: int = -1
var _peers: Dictionary = {}          # peer_id -> {"name", "peer"}
var _rtc := WebRTCMultiplayerPeer.new()
var _mp := MultiplayerAPI.create_default_interface()


func _ready() -> void:
    _mp.multiplayer_peer = _rtc
    set_multiplayer(_mp, get_path())  # 让所有子节点都能用 rpc()

    _rtc.peer_connected.connect(_on_peer_connected)
    _rtc.peer_disconnected.connect(_on_peer_disconnected)

    _ws.connect_to_url(SIGNAL_URL)
    set_process(true)


func _process(_delta: float) -> void:
    _ws.poll()
    if _ws.get_ready_state() == WebSocketPeer.STATE_OPEN:
        while _ws.get_available_packet_count() > 0:
            _handle(JSON.parse_string(_ws.get_packet().get_string_from_utf8()))

    # WebRTC 也需要每帧 poll
    if _rtc.get_connection_status() != MultiplayerPeer.CONNECTION_DISCONNECTED:
        _rtc.poll()


# ---------- WebSocket 信令 ----------

func _handle(msg: Dictionary) -> void:
    match msg.get("type"):
        "welcome":
            _my_id = msg["your_id"]
            print("🟢 My ID = %d" % _my_id)
            for p in msg["peers"]:
                _peers[p["id"]] = {"name": p["name"], "peer": null}
            # ★ 已经在线的人:主动发起连接
            for peer_id in _peers:
                if peer_id != _my_id:
                    _call_peer(peer_id)

        "peer_joined":
            _peers[msg["id"]] = {"name": msg["name"], "peer": null}

        "peer_left":
            var p = _peers.get(msg["id"])
            if p and p["peer"]:
                p["peer"].close()
            _peers.erase(msg["id"])

        "sdp_offer":
            _accept_offer(msg["from"], msg["sdp"])

        "sdp_answer":
            _accept_answer(msg["from"], msg["sdp"])

        "ice_candidate":
            _accept_ice(msg["from"], msg["mid"], msg["index"], msg["sdp"])


func _call_peer(peer_id: int) -> void:
    var peer := WebRTCPeerConnection.new()
    peer.initialize({"iceServers": [{"urls": ["stun:stun.l.google.com:19302"]}]})

    # 监听本地 ICE,发送给远端
    peer.ice_candidate_created.connect(
        func(mid: String, index: int, sdp: String):
            _ws.send_text(JSON.stringify({
                "type": "ice_candidate",
                "to": peer_id,
                "mid": mid, "index": index, "sdp": sdp,
            }))
    )

    # ★ 这一步是关键:创建 offer 并通过信令服务器发出去
    peer.create_data_channel("positions", {"ordered": true})
    peer.session_description_created.connect(
        func(type: int, sdp: String):
            peer.set_local_description(type, sdp)
            _ws.send_text(JSON.stringify({
                "type": "sdp_offer",
                "to": peer_id,
                "sdp": sdp,
            }))
    )
    peer.create_offer()
    _peers[peer_id]["peer"] = peer


func _accept_offer(from_id: int, sdp: String) -> void:
    var peer := WebRTCPeerConnection.new()
    peer.initialize({"iceServers": [{"urls": ["stun:stun.l.google.com:19302"]}]})
    peer.ice_candidate_created.connect(
        func(mid: String, index: int, sdp_mid: String):
            _ws.send_text(JSON.stringify({
                "type": "ice_candidate",
                "to": from_id,
                "mid": mid, "index": index, "sdp": sdp_mid,
            }))
    )
    peer.session_description_created.connect(
        func(type: int, sdp_mid: String):
            peer.set_local_description(type, sdp_mid)
            _ws.send_text(JSON.stringify({
                "type": "sdp_answer",
                "to": from_id,
                "sdp": sdp_mid,
            }))
    )

    # 监听远端数据通道
    peer.data_channel_received.connect(
        func(channel: WebRTCDataChannel):
            if channel.get_label() == "positions":
                channel.packet_received.connect(_on_remote_position)

                # 加一个本地 NPC 在自己场景里,方便演示
                _spawn_npc(from_id)

    peer.set_remote_description("offer", sdp)
    peer.create_answer()
    _peers[from_id]["peer"] = peer


func _accept_answer(from_id: int, sdp: String) -> void:
    var peer: WebRTCPeerConnection = _peers[from_id]["peer"]
    peer.set_remote_description("answer", sdp)
    _spawn_npc(from_id)


func _accept_ice(from_id: int, mid: String, index: int, sdp: String) -> void:
    var peer: WebRTCPeerConnection = _peers[from_id]["peer"]
    peer.add_ice_candidate(mid, index, sdp)


# ---------- P2P 数据通道 ----------

func _on_peer_connected(peer_id: int) -> void:
    print("🟢 P2P connected: %d" % peer_id)


func _on_peer_disconnected(peer_id: int) -> void:
    print("🔴 P2P disconnected: %d" % peer_id)


func _on_remote_position(packet: PackedByteArray) -> void:
    # 远端发过来的是 JSON: {"id": int, "x": float, "y": float, "z": float}
    var data: Dictionary = JSON.parse_string(packet.get_string_from_utf8())
    if not _npcs.has(data["id"]):
        _spawn_npc_at(data["id"], Vector3(data["x"], data["y"], data["z"]))
    _npcs[data["id"]].position = Vector3(data["x"], data["y"], data["z"])


# ---------- 本地玩家 + 远端 NPC ----------

var _npcs: Dictionary = {}

func _spawn_npc(peer_id: int) -> void:
    _spawn_npc_at(peer_id, Vector3.ZERO)


func _spawn_npc_at(peer_id: int, pos: Vector3) -> void:
    var mesh := MeshInstance3D.new()
    mesh.mesh = CapsuleMesh.new()
    mesh.material_override = StandardMaterial3D.new()
    mesh.material_override.albedo_color = Color(randf(), randf(), randf())
    mesh.position = pos
    add_child(mesh)
    _npcs[peer_id] = mesh


# 每 50ms 把本地玩家位置通过 P2P 广播给所有 peer
func _broadcast_position() -> void:
    if _rtc.get_connection_status() != MultiplayerPeer.CONNECTION_CONNECTED:
        return
    var my_pos := Vector3.ZERO  # 真实场景里换成你的玩家位置
    var payload := JSON.stringify({
        "id": _my_id,
        "x": my_pos.x, "y": my_pos.y, "z": my_pos.z,
    })
    for peer_id in _peers:
        var p = _peers[peer_id]["peer"]
        if p and p.get_connection_state() == WebRTCPeerConnection.STATE_CONNECTED:
            # 通过 RTCMultiplayerPeer 的 channel 发
            _rtc.put_packet(JSON.stringify({
                "to": peer_id, "data": payload,
            }).to_utf8_buffer())

💡 为什么这样做:Godot 4 把 WebRTC 包装成了「类似 ENet 的 API」(WebRTCMultiplayerPeer),所以上一节 06-16 学过的所有 rpc()MultiplayerSpawnerMultiplayerSynchronizer 全部可以无缝复用——你只需要换一行:

1
2
# var peer = ENetMultiplayerPeer.new()         # ← 06-16 的写法
var peer = WebRTCMultiplayerPeer.new()        # ← 今天的写法

4.4 验证:开三个窗口看延迟

  1. 起 Python 信令服务器。
  2. Godot 项目 F5,设 3 个实例。
  3. 看 console:
    🟢 My ID = 1
    🟢 P2P connected: 2
    🟢 P2P connected: 3
    
  4. 在一个窗口里移动本地玩家,其他窗口的「NPC」几乎实时跟随(比 ENet 的 80ms 明显流畅)。

延迟对比(同一台机器、同一个 Wi-Fi):

传输层单跳延迟主观手感
ENet(服务器转发)60–80ms「咦,刚刚我按了啥?」
WebSocket(服务器转发)80–150ms「这个聊天窗有点卡」
WebRTC(P2P 直连)10–30ms「跟单机一样!」

五、常见坑点 + 解决方案

坑点 1:浏览器里 WebSocket 跑得通,WebRTC 跑不通

症状:HTML5 导出的页面里,console 报 RTCPeerConnection is not defined

原因:浏览器要求 WebRTC 走 HTTPS(或 localhost)。如果你挂在 http://your-game.com,浏览器会直接拒绝 RTCPeerConnection

解决方案

  • 开发阶段用 localhost 测试,不要用 IPhttp://192.168.1.10 也不行)。
  • 上线时上 HTTPS(Let’s Encrypt 免费)。
  • 如果一定要走 HTTP,得在 HTML 头里加 Feature-Policy,但 95% 的浏览器会忽略。

坑点 2:WebRTC 永远停在 STATE_CONNECTING

症状:两个客户端 log 都显示「connected」,但收不到对方的包。

原因:你的 STUN 服务器配置有问题,或者你在 NAT 后面(公司 Wi-Fi / 4G),STUN 反射不出公网地址。

解决方案

  1. 先用免费 STUN:stun:stun.l.google.com:19302(代码里已经加了)。
  2. 仍然不通就上 TURN(中继):coturn 自建,或用 Twilio Network Traversal Service(前 30GB/月免费)。
  3. 把 STUN 失败的超时调小(默认 30 秒太长了),代码里加:
1
2
3
4
5
6
peer.initialize({
    "iceServers": [{"urls": ["stun:stun.l.google.com:19302"]}],
    "iceCandidatePoolSize": 10,
    "iceTransportPolicy": "all",
    "bundlePolicy": "max-bundle",
})

坑点 3:Godot 4 浏览器导出报 SharedArrayBuffer is not defined

症状:HTML5 导出版本跑起来,控制台疯狂报 SharedArrayBuffer 错误。

原因:Godot 4 的 WebRTC 在浏览器里要用多线程,必须 SharedArrayBuffer,而 SharedArrayBuffer 需要跨域隔离

解决方案:在你的静态服务器(Nginx / Caddy)里加两个 HTTP 头:

1
2
add_header Cross-Origin-Opener-Policy   "same-origin";
add_header Cross-Origin-Embedder-Policy "require-corp";

本地开发用 python -m http.server 8000 不行,换 caddy 或者直接用 Godot 自带的 --headless 模式起一个 Python 服务时手动加头。


六、延伸阅读


软件版本

  • Godot Engine:4.6 stable(WebRTC 模块内置)
  • Python:3.11 + websockets==12.0
  • Blender:本篇未涉及(07-05 已讲完灯光渲染,下一篇回归 Blender 几何节点实战)
  • 数据来源时间:2026-07-06(Asia/Shanghai)

下一篇预告

2026-07-07:把 06-16 的 ENet Demo + 今天的 WebRTC 信令 + 07-04 的赛博朋克街区 + 07-05 的灯光渲染串成一条端到端工作流——「Blender 做街 → Godot 渲染 → WebRTC P2P 多人同步位置」一个 Demo 跑通。

到那一步,你就有了从「资产生产」到「联机部署」完整的最小闭环,可以拿去参加 Game Jam 了。


版权声明:本文为 breez 原创,遵循 CC BY-SA 4.0 协议,转载请署名 + 相同协议共享。

技术栈

HTML & CSS JavaScript Vue.js React Python Go Node.js Git Docker Linux Hugo CI/CD

联系方式