技术博客

PackedScene 的序列化格式:.tscn 文本与 .scn 二进制对照

第 102 / 149 篇 · 2026-09-27 · godot · 资源管线场景系统源码分析

场景保存到磁盘有两种格式:.tscn(文本)和 .scn(二进制)。它们在内存里是同一样东西——PackedScene 内部那个 SceneState(实例化那篇拆过的平行数组),落盘只是两种编码。理解格式本身的价值在于:合并冲突时会读 .tscn 了、反编译别人的资源时知道 .scn 怎么解析、以及明白为什么 Godot 默认推荐文本格式。

.tscn 的四个区块

一个最小场景长这样:

[gd_scene load_steps=4 format=3 uid="uid://c8k1x2y3z4w5v"]

[ext_resource type="Texture2D" uid="uid://b3r4s" path="res://hero.png" id="1_ab"]

[sub_resource type="RectangleShape2D" id="RectangleShape2D_x1"]
size = Vector2(32, 64)

[node name="Player" type="CharacterBody2D"]
position = Vector2(100, 200)
texture = ExtResource("1_ab")

[node name="Collision" type="CollisionShape2D" parent="."]
shape = SubResource("RectangleShape2D_x1")

头部行。 [gd_scene load_steps format uid],load_steps 是资源步骤总数(ext_resource 数 + sub_resource 数 + 1),加载器用它预分配数组;format=3 是文本格式版本号,跨大版本迁移时会 bump;uid 就是 UID 那篇讲的全局 ID。

ext_resource。 引用工程里其他文件。id 是场景内的引用键,节点属性里用 ExtResource("1_ab") 指回来。键的随机后缀(_ab)是为了让你手动编辑时不容易撞 id。

sub_resource。 场景私有的资源对象,比如碰撞形状。它们不进 ResourceCache 全局表,随场景一起实例化。id 同样只是场景内的键。

node 段。 每个节点一段,属性是 名字 = 值 的行。两个容易忽略的细节:parent="." 里的点是相对于场景根的 NodePath(场景根自己无 parent 属性);未修改的默认属性根本不写进文件——所以文本场景里看不到的属性不等于没值,只是等于 ClassDB 默认值,加载时少做一步反序列化,这也是 .tscn 能靠"只记增量"控制体积的原因。

实例化与覆盖怎么表示

场景里嵌套了另一个场景(实例化)时,格式用两个标记表达:

[node name="Enemy" instance=ExtResource("2_scene")]
position = Vector2(500, 100)
health = 30

[node name="ExtraGun" type="Sprite2D" parent="." index="1"]

instance=ExtResource(...) 把该节点标记为实例根:它的子树不在本文件里,来自被引用的 PackedScene。其下写的 position、health 是对实例的覆盖,加载时应用在实例化结果之上。index="1" 的用法更隐蔽:给实例加新子节点时,用 index 指明插在源场景哪个位置之后——覆盖不只是改属性,还能改结构。diff 文本场景时,这两类行就是冲突的高发区。

.scn 二进制:RSCC 头与 Variant 流

.scn 由 core/io/resource_format_binary.cpp 读写,结构(以 4.x 为准):

"RSCC"                     4B   magic
version                    4B   格式版本(当前 3)
endian + real64 flags      2B   字节序与双精度开关
project settings hash      16B  影响 Variant 编码的哈希
save_type                  str  "PackedScene"
[external resources]       外部资源表:类型 + 路径 + uid
[internal resources]       内部资源名表 + 结尾偏移表
[property stream]          每个资源:属性名 + Variant 值…
"RSRC"                     4B   结尾 magic

字符串编码是 uint32 长度 + UTF-8 字节,Variant 值带 4 字节类型头(含对象引用的编码标志位)。每个资源的属性段以空属性名终止。和文本版逐字段对照:文本里的 ext_resource 编号对应二进制外部资源表的下标,ExtResource("1_ab") 编码进 Variant 的对象引用标志里,parent="." 就是存了一个相对 NodePath 的 Variant。

选哪种

体积上 .scn 占优:无空白字符、数字定长二进制、无属性名字符串的重复开销,实测同等场景约为文本的 60~70%。加载也快:跳过词法分析,直接内存映射读 Variant。代价是失去文本的全部好处——不能 diff、不能 grep、不能手工修冲突、不能代码评审。

Godot 的默认立场很清楚:场景和资源一律文本(.tscn / .tres),二进制只在明确需求时用。我的实际取舍:场景和策划数据用文本,协作价值远大于那点加载差;发布给玩家的数据如果担心解包篡改,二进制 + PCK 加密(PCK 那篇)组合用,但要认清这只抬高门槛,挡不住认真的反编译——真正的保护在服务器校验,不在文件格式。

同一份 SceneState 的两种落盘编码 SceneState 节点/属性平行数组 .tscn 文本 可 diff / grep / 手改 .scn 二进制 体积小约 30~40%,加载快 .scn 文件结构 RSCC 头 + 版本 + 哈希 外部资源表(路径+uid) 属性流:名字 + Variant 值 RSRC 结尾 magic .tscn 文本编码规则 属性行:名字 = 值;默认属性不落盘;未写的 = ClassDB 默认值 实例化的文本表达 instance=ExtResource() 标记实例根;其下属性行 = 覆盖;index= 插入新子节点