Godot场景实例化:从基础原理到性能优化的完整指南
1. 项目概述如果你刚开始用Godot引擎或者已经用它做过几个小Demo那么“场景实例化”这个概念你迟早会碰到而且会反复用到。它不是什么高深莫测的黑科技但却是构建任何稍具规模游戏项目的基石。简单来说场景实例化就是“复制粘贴”的工业化、可编程版本。它让你能把一个精心设计好的游戏对象比如一个敌人、一颗子弹、一个宝箱打包成一个独立的、可重复使用的模板也就是场景文件.tscn然后在游戏运行时随时随地、按需生成这个模板的副本。听起来很简单对吧但新手最容易犯的错误就是把场景实例化和直接创建节点new Sprite2D()混为一谈。后者是“从零开始组装一个零件”而前者是“直接调用一条成熟的生产线生产出一个完整的产品”。这篇教程就是要帮你彻底搞懂这条“生产线”的运作原理、最佳实践以及那些官方文档里可能没明说但实际开发中一定会遇到的“坑”。我会从最基础的“为什么需要实例化”讲起一直深入到动态管理、性能优化和内存释放让你不仅能“会用”更能“用好”。2. 场景实例化的核心价值与设计哲学2.1 为什么是场景而不是脚本很多从其他引擎转过来的开发者习惯把所有逻辑都写在一个巨大的脚本里用代码“new”出各种组件。这在Godot里是行得通的但绝非最佳实践。Godot的核心设计哲学是“基于节点的场景树”和“组合优于继承”。一个场景本质上就是一个预配置好的节点树及其所有属性、脚本、信号的集合。场景作为模板的优势可视化编辑你可以在编辑器中直观地摆放精灵、碰撞体、粒子效果设置属性连接信号而无需写一行代码。这极大地提升了原型设计和迭代速度。封装与复用一个“敌人”场景内部可能包含Sprite2D外观、CollisionShape2D碰撞、AnimationPlayer动画、HealthBarUI等多个节点以及控制它们交互的脚本。通过实例化你可以一键生成这个复杂的复合体所有内部关联都已就绪。资源管理场景文件.tscn本身也是一种资源。Godot的资源系统能高效地管理它们的加载和引用。预加载preload机制可以让资源在游戏启动时就进入内存避免运行时卡顿。继承与变体你可以基于一个基础场景如BaseEnemy.tscn创建多个变体场景如FastEnemy.tscn、TankEnemy.tscn只需修改少数属性或添加额外节点就能实现差异化这比在代码里写一堆if-else要清晰得多。2.2 实例化 vs. 直接添加节点一个生动的比喻想象你要开一家汉堡店。直接添加节点就像每次有订单你都现跑去市场买面包、肉饼、生菜、酱料然后回到厨房一片片组装。慢且容易出错忘加酱料。场景实例化你提前设计好“经典汉堡”的完整配方和组装流程这就是场景。当有订单时你直接从仓库里调取一份“经典汉堡”套餐实例化稍作加热初始化即可出餐。高效、标准、可批量生产。在Godot中PackedScene就是这个“套餐包”而instantiate()就是“拆包并组装”的过程。3. 场景实例化的基础操作全解析3.1 第一步创建你的模板场景我们以一个简单的“子弹”为例。新建一个场景根节点选择Area2D因为子弹需要检测碰撞。将其重命名为Bullet。为Bullet节点添加子节点一个Sprite2D用于显示子弹图片一个CollisionShape2D用于碰撞检测。为Sprite2D设置纹理Texture为CollisionShape2D设置形状如矩形。为根节点Bullet附加一个脚本如bullet.gd。在脚本里你可以定义子弹的速度、伤害以及_physics_process中的移动逻辑。将场景保存为bullet.tscn。现在bullet.tscn就是你的子弹模板。在编辑器中它看起来就是一个完整的、可运行的子弹对象。3.2 第二步在代码中加载与实例化假设你有一个“玩家”脚本需要发射子弹。3.2.1 加载场景资源有两种方式运行时加载 (load)在需要时才从磁盘读取。适用于不频繁使用或体积较大的资源。var bullet_scene load(res://bullet.tscn)预加载 (preload)在脚本编译阶段就加载。适用于游戏启动时就必须可用、或会频繁实例化的核心资源。这是GDScript独有的优化。var bullet_scene preload(res://bullet.tscn)实操心得对于像子弹、敌人、特效这类会大量、高频生成的对象务必使用preload。这能彻底消除运行时因磁盘I/O导致的瞬间卡顿。load更适合用于按需加载的关卡场景或大型资源。3.2.2 实例化并添加到场景树加载得到的是一个PackedScene资源你需要调用其instantiate()方法来创建一个实际的节点实例。func shoot(): var bullet_instance bullet_scene.instantiate() # 此时 bullet_instance 是一个 Node具体是 Bullet 场景的根节点 Area2D这个bullet_instance目前还游离在场景树之外不会被渲染也不会处理物理或输入。你必须将其添加为某个已存在于场景树中的节点的子节点。func shoot(): var bullet_instance bullet_scene.instantiate() # 通常将新实例添加到当前场景的根节点或一个专门的容器节点下 get_tree().current_scene.add_child(bullet_instance) # 或者如果玩家节点是子弹的逻辑父级 # add_child(bullet_instance)add_child()的调用才真正让这个子弹“活”起来进入游戏循环。3.3 第三步配置新实例刚实例化的子弹其位置、旋转等属性都是模板场景中保存的默认值。你通常需要根据发射者的状态进行配置。func shoot(): var bullet_instance bullet_scene.instantiate() # 1. 设置位置通常从玩家枪口位置发射 bullet_instance.global_position $GunTip.global_position # $GunTip 是玩家身上的一个Marker2D节点 # 2. 设置旋转朝向鼠标或移动方向 bullet_instance.rotation global_rotation # 3. 设置速度方向假设子弹脚本有一个 direction 属性 bullet_instance.direction (get_global_mouse_position() - global_position).normalized() # 4. 设置归属例如用于区分玩家子弹和敌人子弹 bullet_instance.owner self add_child(bullet_instance)注意事项直接修改position和修改global_position有本质区别。position是相对于父节点的局部坐标而global_position是世界坐标。在将实例添加到场景树之前其父节点尚未确定此时只能设置global_position。添加到场景树之后则可以根据父子关系灵活选择。一个常见的做法是先设置global_position再add_child。4. 动态实例化的高级模式与内存管理4.1 对象池模式应对高频实例化如果你需要每帧发射数十发子弹频繁地instantiate()和queue_free()会产生内存碎片和GC垃圾回收压力。对象池Object Pool是解决此问题的经典模式。核心思想预先创建一批对象子弹并禁用放入一个“池子”数组。需要时从池中取一个激活使用用完后再放回池子并禁用而不是销毁。extends Node2D export var bullet_scene: PackedScene export var pool_size: int 20 var bullet_pool: Array [] func _ready(): # 预初始化对象池 for i in range(pool_size): var bullet bullet_scene.instantiate() bullet.visible false bullet.process_mode Node.PROCESS_MODE_DISABLED # 彻底禁用物理和处理 add_child(bullet) bullet_pool.append(bullet) func get_bullet_from_pool(): for bullet in bullet_pool: if not bullet.visible: # 或者用一个更明确的状态标志如 bullet.is_active # 重置子弹状态 bullet.global_position $GunTip.global_position bullet.visible true bullet.process_mode Node.PROCESS_MODE_INHERIT # ... 其他初始化 return bullet # 如果池子用尽可以选择动态扩容新建一个或返回null print(Bullet pool exhausted!) return null func _on_bullet_finished(bullet): # 子弹生命周期结束回收入池 bullet.visible false bullet.process_mode Node.PROCESS_MODE_DISABLED bullet.position Vector2.ZERO # 移出屏幕外避免意外碰撞实操心得对象池特别适合粒子、子弹、伤害数字等生命周期短、生成频繁的对象。关键在于彻底禁用回收的对象不仅仅是visible false要将其process_mode设为DISABLED并确保其碰撞体也被禁用否则它们仍在后台消耗性能。4.2 场景树中的层级管理与清理不加管理地实例化对象会导致场景树越来越臃肿即使对象已不可见或功能结束。你需要一个清晰的清理策略。4.2.1 使用专门的容器节点不要把所有实例都直接挂在根节点下。创建一些空节点作为逻辑容器。# 在游戏主场景中 Node2D ├── Player ├── UI ├── World # 世界容器存放地图、平台等静态物 └── Entities # 动态实体容器专门存放子弹、敌人、掉落物等实例化时将对象添加到对应的容器中get_node(/root/Game/Entities).add_child(enemy_instance)这样做的好处是逻辑清晰便于按类别管理如批量暂停所有敌人。易于清理可以通过遍历容器节点的子节点来统一处理。4.2.2 自动清理超出边界的实例对于子弹、飞行道具等通常需要在它们飞出屏幕后自动销毁。# 在 bullet.gd 中 func _process(delta): # 假设有一个全局的屏幕矩形区域 var viewport_rect get_viewport().get_visible_rect() if not viewport_rect.has_point(global_position): queue_free()更优雅的方式是利用VisibleOnScreenNotifier2D节点。将其作为子弹的子节点并连接其screen_exited信号。# 在 bullet.gd 的 _ready() 中 $VisibleOnScreenNotifier2D.screen_exited.connect(queue_free)4.3 信号通信解耦实例与创建者实例化后的对象需要与游戏世界其他部分通信如子弹击中敌人后敌人需要扣血玩家需要加分。绝对要避免通过“获取创建者节点然后直接调用方法”这种紧耦合的方式。正确做法使用信号Signals。在子弹场景的脚本中定义信号。# bullet.gd signal hit_something(body: Node2D, damage: int)在子弹碰撞检测到目标时发出信号。# bullet.gd func _on_body_entered(body): if body.is_in_group(enemies): hit_something.emit(body, damage) queue_free()在创建子弹的脚本中如玩家脚本连接这个信号。# player.gd func shoot(): var bullet bullet_scene.instantiate() bullet.hit_something.connect(_on_bullet_hit) add_child(bullet) func _on_bullet_hit(body: Node2D, damage: int): body.take_damage(damage) add_score(10)这种方式实现了完全的解耦。子弹不需要知道谁创建了它也不需要知道敌人具体怎么处理伤害。它只负责报告事件由关心此事件的系统玩家、记分板、音效管理器去处理。5. 性能优化与常见陷阱排查5.1 性能优化要点预加载是关键重申一遍对于高频使用的场景必须preload。控制同时存在的实例数量即使是对象池也要根据设备性能设定合理的池大小。对于无限刷怪的关卡可以设置一个最大敌人数量上限。简化实例化时的初始化逻辑_ready()函数里的操作要轻量。避免在实例化瞬间进行复杂的计算或资源加载。可以将非必要的初始化延迟到第一帧_process中。使用MultiMeshInstance进行大批量渲染如果你需要渲染成千上万个相同的简单物体如草地、星星、子弹尾迹MultiMeshInstance比实例化上千个独立场景节点性能高出几个数量级。它通过一次绘制调用渲染所有实例。注意物理层的开销每个RigidBody2D或CharacterBody2D的实例都会增加物理世界的计算负担。对于大量简单的、行为可预测的物体考虑用代码模拟其运动而非使用物理引擎。5.2 常见问题与排查技巧问题1实例化后对象没有出现或者属性不对。检查点是否调用了add_child()这是最常被遗忘的一步。实例的位置是否正确在add_child前设置global_position在add_child后设置position要分清。场景的根节点类型是否正确确保你实例化后得到的节点类型是你期望的。可以用print(bullet_instance.get_class())调试。导出export的属性是否在实例化后被错误覆盖确保你的初始化代码没有在_ready()里意外地覆盖掉在编辑器中设置好的值。问题2游戏运行一段时间后变卡内存持续增长。排查内存泄漏确认对象被正确释放在对象的_ready()中print(self.name, spawned)在_exit_tree()或queue_free()前print(self.name, freed)。观察日志看是否每个“spawned”都有对应的“freed”。检查循环引用如果两个对象通过信号或引用互相持有对方且没有正确断开连接即使调用queue_free()它们也可能无法被垃圾回收。使用弱引用weakref()或确保在适当时候disconnect()。使用Godot的性能分析器Profiler查看“对象计数Object Count”图表如果某些类型的节点数量只增不减就是泄漏的明确信号。问题3实例化瞬间造成帧率骤降卡顿。原因与解决首次加载成本如果是load卡顿来自磁盘I/O。换成preload。复杂的_ready()即使场景已加载首次执行_ready()中的复杂逻辑如生成大量子节点、计算路径也会卡。优化_ready()或将工作分摊到多帧。Shader编译卡顿如果实例化的场景使用了新的着色器GPU需要时间编译。Godot 4.x 提供了着色器异步编译和缓存机制来缓解此问题。问题4信号连接后回调函数被多次触发。典型场景一个敌人被击中后其hit_something信号连接了UI更新函数。如果这个敌人在一帧内被实例化并快速销毁多次而信号连接没有断开可能导致回调累积。解决方案在连接信号时使用CONNECT_ONE_SHOT标志如果只期望触发一次或者在对象销毁前tree_exiting信号中主动断开所有它发出的信号连接。6. 实战构建一个简单的敌人生成系统让我们把上面的知识整合起来创建一个可配置的敌人波次生成器。1. 创建敌人场景 (enemy.tscn)根节点CharacterBody2D(名为Enemy)子节点Sprite2D,CollisionShape2D脚本enemy.gd包含移动逻辑和health属性以及一个died信号。2. 创建生成器脚本 (spawner.gd)extends Node2D export var enemy_scene: PackedScene export var spawn_points: Array[Marker2D] [] # 在编辑器中拖入多个Marker2D export var waves: Array[Dictionary] [ # 在编辑器中配置波次 {enemy_count: 5, spawn_interval: 1.0}, {enemy_count: 10, spawn_interval: 0.7}, # ... ] export var max_concurrent_enemies: int 15 var current_wave_index: int 0 var enemies_alive: int 0 var spawn_timer: Timer func _ready(): spawn_timer Timer.new() spawn_timer.one_shot false spawn_timer.timeout.connect(_on_spawn_timer_timeout) add_child(spawn_timer) start_wave(current_wave_index) func start_wave(wave_index: int): if wave_index waves.size(): print(All waves completed!) return var wave waves[wave_index] print(Starting wave , wave_index 1) spawn_timer.wait_time wave.get(spawn_interval, 1.0) spawn_timer.start() # 我们可以用一个计数器来控制生成总数这里简化处理由定时器控制直到数量达标 func _on_spawn_timer_timeout(): if enemies_alive max_concurrent_enemies: return # 达到上限暂停生成 if spawn_points.is_empty(): return var spawn_point spawn_points.pick_random() var enemy enemy_scene.instantiate() enemy.global_position spawn_point.global_position enemy.died.connect(_on_enemy_died) # 连接死亡信号 get_parent().add_child(enemy) # 假设生成器是放在世界容器里的 enemies_alive 1 # 检查当前波次是否完成这里需要根据波次配置逻辑判断例如已生成数量 # 如果完成则 spawn_timer.stop() 并准备下一波 func _on_enemy_died(): enemies_alive - 1 # 可以在这里检查当前波次敌人是否全部死亡如果是则启动下一波 if enemies_alive 0: current_wave_index 1 start_wave(current_wave_index)这个生成器展示了如何结合导出变量进行编辑器配置、使用定时器控制生成频率、管理同时存在的实例数量以及通过信号与生成的实例进行通信。掌握场景实例化你就掌握了Godot动态游戏世界的钥匙。从简单的子弹到复杂的敌人AI群落从随机生成的地图元素到动态加载的关卡区块其核心都是这套“模板-实例”的思维。记住最佳实践预加载、善用信号、及时清理、池化优化。多动手实验利用Godot编辑器的可视化调试工具观察场景树的变化你很快就能驾轻就熟构建出丰富而高效的动态游戏体验。

相关新闻

深入解析Spring SPI机制:从JDK ServiceLoader到Spring Boot自动配置原理

深入解析Spring SPI机制:从JDK ServiceLoader到Spring Boot自动配置原理

在实际 Java 后端开发中,尤其是面试和框架深度使用场景,我们经常听到“SPI”这个词。很多开发者知道它和“服务发现”有关,也大概了解 JDK 的ServiceLoader,但当被问到“Spring 是如何实现和运用 SPI 机制的?”时&…

2026/10/2 3:15:39 阅读更多 →
B站m4s视频转换工具:简单三步实现缓存视频永久保存

B站m4s视频转换工具:简单三步实现缓存视频永久保存

B站m4s视频转换工具:简单三步实现缓存视频永久保存 【免费下载链接】m4s-converter 一个跨平台小工具,将bilibili缓存的m4s格式音视频文件合并成mp4 项目地址: https://gitcode.com/gh_mirrors/m4/m4s-converter 你是否曾经遇到过B站视频突然下架…

2026/10/8 14:17:10 阅读更多 →
AI协同办公2026趋势:从工具到伙伴,重塑工作流与组织形态

AI协同办公2026趋势:从工具到伙伴,重塑工作流与组织形态

1. 项目概述:从工具到伙伴,AI如何重塑协同办公的底层逻辑如果你在2024年还在讨论“用AI写个周报”、“让AI做个PPT”,那可能已经有点落伍了。过去两年,AI在办公领域的渗透,已经从“点状工具”升级为“系统性重构”。我…

2026/10/10 12:17:12 阅读更多 →

最新新闻

鱼书深度学习代码实战:MNIST反向传播与优化器调试全解

鱼书深度学习代码实战:MNIST反向传播与优化器调试全解

简介:《深度学习入门:基于Python的理论与实现》(鱼书)的配套源代码,面向刚接触深度学习、希望在读理论的同时亲手验证代码的Python学习者,解决“书上公式能懂,动手实现不知从哪开始”的问题。资…

2026/10/12 20:37:17 阅读更多 →
QNX vmstat 内存分析实战:字段解读、排查方法与避坑指南

QNX vmstat 内存分析实战:字段解读、排查方法与避坑指南

1. 为什么内存分析绕不开 vmstat做嵌入式开发的人,尤其是碰过 QNX 的,大概都有过这种经历:系统跑着跑着突然变慢,某个进程响应时间从几毫秒飙到几百毫秒,日志里什么异常都没有,CPU 占用看起来也不高。这时候…

2026/10/12 20:37:17 阅读更多 →
Win7下Python 3.7项目打包:Cython与PyInstaller实战指南

Win7下Python 3.7项目打包:Cython与PyInstaller实战指南

1. 为什么要在Win7上折腾这套打包方案先把场景说清楚。你手上有一个在Windows 7上跑得好好的Python项目,可能是给工厂产线写的数据采集工具,可能是给财务部门做的小型报表系统,也可能是某个老设备配套的上位机软件。这些场景有个共同点&#…

2026/10/12 20:37:17 阅读更多 →
Claude Code 终端隐藏参数:一键批量生成单元测试与接口文档妙招|TaoToken 统一 Key 通道

Claude Code 终端隐藏参数:一键批量生成单元测试与接口文档妙招|TaoToken 统一 Key 通道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 20:37:17 阅读更多 →
Cookie中文乱码排查指南:EL表达式为何不解码及应对

Cookie中文乱码排查指南:EL表达式为何不解码及应对

页面上显示一排%E5%BC%A0%E4%B8%89,第一反应是 JSP 的pageEncoding没配对,contentType改了好几遍,问题纹丝不动。实际上,EL 表达式获取 Cookie 中的中文值乱码,绝大多数时候不是页面编码的问题,而是这串值从…

2026/10/12 20:37:17 阅读更多 →
在线判题系统特判程序(SPJ)实战:LDU与HUSTOJ配置指南

在线判题系统特判程序(SPJ)实战:LDU与HUSTOJ配置指南

1. 从一次“答案不唯一”的判题事故说起如果你维护过在线判题系统,大概率遇到过这种场景:选手提交的代码逻辑完全正确,输出也符合题目要求,但系统就是给了一个刺眼的红色 Wrong Answer。你反复检查标程,发现标程输出的…

2026/10/12 20:36:16 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天,画面可以做到绝对的锐利、平滑与无瑕。然而,当一张秋日手账插画或拍立得照片过于“平整无瑕”时,往往会散发出一种冰冷生硬的“数码塑料感(Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中,横排(Horizontal Layout)早已经成为了绝对的主流。然而,当我们翻开泛黄的线装古籍、宋版木刻诗集,或是欣赏一张茶道雅集的手写便签时,那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点,很多人心里都会悄悄亮起一盏警示灯。 在心理学上,这种现象有一个专门的称谓——“周日夜晚焦虑症(Sunday Scaries)”。明天又是周一,闹钟又要重新在七点响彻卧房;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →