Godot引擎实战:2D动作冒险游戏架构设计与开发全流程
1. 项目概述为什么选择Godot复刻TetraForce如果你是一个对2D游戏开发有热情同时又对《塞尔达传说》系列那种俯视角动作冒险游戏着迷的开发者那么“复刻一个自己的TetraForce”这个想法大概率会在你脑海里盘旋过。我当初就是这么入坑的。TetraForce这个源自经典游戏《塞尔达传说众神的三角力量》的粉丝概念其核心魅力在于它构建了一个精巧的、由探索、解谜和战斗驱动的2D世界。而Godot引擎以其轻量、开源、对2D原生支持极佳的特性成为了实现这个梦想的绝佳工具。这个项目远不止是“用Godot做个游戏demo”那么简单。它本质上是一个中型2D动作冒险游戏的项目架构实战。你需要考虑的不仅仅是让林克或者说你的主角挥剑、移动更要构建一整套支撑游戏循环的系统从地图与房间的无缝切换、物品与能力的获取逻辑、到敌人AI与战斗反馈再到那个经典的“解谜-获得钥匙-开启新区域”的循环。Godot的节点Node与场景Scene架构与这种模块化的游戏设计思路天然契合。通过这个项目你能深入理解如何将一个复杂的游戏创意拆解成可管理、可迭代的代码和资源模块这对于任何有志于游戏开发的从业者来说都是一次宝贵的全流程锻炼。2. 核心架构设计从上帝视角规划你的海拉鲁在动手写第一行代码之前花时间在架构设计上是绝对值得的。一个清晰的架构能让你在开发中期避免陷入“屎山”代码的泥潭。对于TetraForce这类游戏我建议采用一种基于场景Scene的、事件驱动的模块化架构。这听起来有点抽象让我用盖房子来类比整个游戏世界Overworld是一栋大楼每个房间Room或区域Dungeon是一个个功能完备的套房而主角、敌人、道具、机关则是可以在不同套房之间移动或复用的家具和电器。2.1 场景树Scene Tree的顶层设计Godot的核心是场景树。我们的顶层设计应该清晰反映游戏的状态流。通常我会建立如下几个核心的“根场景”启动场景Boot负责初始化全局单例Autoload、加载配置文件、检查存档。这是游戏的入口。主菜单场景MainMenu提供新游戏、继续、设置等入口。游戏主场景Game这是核心。它本身可能是一个简单的节点用于挂载和管理两个关键子场景世界管理器WorldManager负责当前游戏世界如大地图、某个迷宫的加载、切换和持久化。它知道当前玩家在哪个“房间”。UI层UILayer始终显示在屏幕上的UI如血量、卢比数量、小地图、道具栏。它通过信号与游戏世界交互而不是直接引用具体游戏对象。这种分离确保了游戏逻辑、UI表现和资源管理的解耦。世界管理器加载一个具体的“房间场景”Room_01.tscn而UI层独立更新。2.2 全局单例Autoload与事件总线Event BusGodot的Autoload功能非常适合创建全局管理器。对于TetraForce项目以下几个单例几乎是必需的GameEvents事件总线这是架构的“神经系统”。它定义了一系列全局信号Signals例如player_health_changed,item_acquired,door_opened,save_game_requested。任何节点都可以发出或连接这些信号而无需直接持有对其他节点的引用。这极大地降低了模块间的耦合度。比如当主角捡起一把钥匙时它只需发出item_acquired信号并附带钥匙ID而对应的门监听这个信号检查ID匹配后自行打开。SaveManager存档管理器负责序列化游戏状态玩家位置、库存、已开启的门、已击败的BOSS到文件。设计存档格式时要考虑到可扩展性。我常用一个字典Dictionary结构为每种可存档对象如玩家、房间、物品集合分配一个唯一的键。InventoryManager库存管理器集中管理玩家获得的道具、武器、钥匙。它提供接口来添加、移除、查询物品并发出库存变更信号驱动UI更新。AudioManager音频管理器统一管理背景音乐BGM和音效SFX的播放、淡入淡出避免多个音频播放器冲突。注意不要滥用Autoload。只有那些真正需要全局访问、生命周期与游戏一致的管理器才适合放在这里。把业务逻辑尽量放在具体的场景节点中。2.3 实体-组件Entity-Component思维的运用Godot本身不是严格的ECS架构但其节点系统鼓励组件化设计。我们可以把主角Player看作一个实体它的能力由挂载的组件子节点提供HealthComponent管理血量、受伤无敌时间。MovementComponent处理输入、移动、碰撞检测。AttackComponent管理攻击动画、碰撞框生成、伤害计算。InteractComponent处理与可交互物体NPC、宝箱的交互。当主角获得新能力如炸弹时我们不是修改Player.gd脚本而是动态实例化一个BombAbilityComponent节点并添加到主角下。这种设计让代码更易维护和扩展。3. 核心模块实现细节与避坑指南有了顶层架构我们来深入几个最关键模块的实现细节。这里有很多“教科书不会告诉你”的坑。3.1 玩家控制器Player Controller手感是灵魂2D俯视角移动的手感至关重要。很多人直接用move_and_slide()就结束了但要做出手感扎实的移动需要更多细节。基础移动与碰撞# 在 _physics_process(delta) 中 var input_direction Input.get_vector(move_left, move_right, move_up, move_down) var desired_velocity input_direction * move_speed # 应用加速度和减速度让移动不那么“僵硬” velocity velocity.lerp(desired_velocity, acceleration * delta) velocity move_and_slide(velocity)这里acceleration控制了加速快慢lerp线性插值让速度变化更平滑。攻击与状态机玩家的行为闲置、移动、攻击、受伤、交互最好用一个有限状态机FSH来管理。Godot的AnimationTree配合AnimationNodeStateMachine可以可视化地管理状态但用代码实现一个轻量级FSH也不复杂。关键是确保同一时间只有一个活跃状态并且状态转换条件清晰例如从“攻击”状态回到“闲置”需要等待攻击动画播放完毕且没有输入。实操心得攻击判定的时机不要绑定在动画帧上除非你非常熟悉Godot的动画播放器回调。更可靠的做法是在攻击状态激活时生成一个Area2D攻击碰撞区域节点并设置一个Timer来控制其存在时间。这样你可以精确控制攻击判定的持续帧数并且易于调整攻击范围。3.2 房间系统与门加载无缝世界的奥秘经典2D塞尔达游戏是分房间Room加载的。实现这个功能的关键是TransitionZone过渡区域。设计过渡区域在房间边缘放置一个不可见的Area2D节点并为其添加一个CollisionShape2D。定义连接数据为这个TransitionZone添加一个自定义资源如TransitionZoneData里面定义目标房间的场景路径target_room_path和玩家在新房间的出生点标识spawn_point_id。触发加载当玩家身体进入TransitionZone触发body_entered信号。在信号处理函数中通过GameEvents发出room_transition_requested信号附带目标房间路径和出生点ID。WorldManager监听这个信号开始异步加载ResourceLoader.load_threaded_request目标房间场景。加载完成后将当前房间场景从树中移除queue_free()实例化新房间场景并添加到WorldManager下。根据出生点ID找到新房间中对应的SpawnPoint节点将玩家位置设置到那里。性能优化点对于大型世界可以考虑使用“房间预加载”策略。当玩家靠近当前房间的出口时提前在后台异步加载相邻房间以实现真正的无缝切换尽管原版游戏是黑屏切换但我们可以做得更好。3.3 物品与交互系统让世界活起来物品系统包括可收集物卢比、心、钥匙和能力道具剑、盾、炸弹袋。实现的关键是统一接口。我建议为所有可交互物体创建一个基类Interactable.gd它定义了一个虚方法interact(interactor: Node)。然后派生出PickupItem用于收集物。被交互时发出GameEvents.item_acquired信号然后自毁。Chest宝箱。有一个状态关闭/打开。首次交互时播放打开动画生成一个PickupItem子节点即里面的物品然后状态变为打开之后不再可交互。NPC非玩家角色。交互时显示对话气泡或打开对话UI。库存系统的实现InventoryManager内部可以用一个字典来存储物品键为物品ID值为数量和自定义数据。当需要持久化时将这个字典序列化保存。UI层通过连接GameEvents.inventory_updated信号来实时刷新道具栏的显示。3.4 敌人AI从呆瓜到挑战者即使是简单的敌人也需要一点AI来增加趣味性。一个经典的“巡逻并追击”的敌人可以这样实现状态IDLE闲置、PATROL巡逻、CHASE追击、ATTACK攻击。视觉检测在敌人前方添加一个锥形的Area2D作为“视野”。当玩家进入该区域且射线检测RayCast2D无遮挡时切换到CHASE状态。巡逻在PATROL状态敌人在几个预设点之间移动。可以使用Path2D和PathFollow2D节点或者简单地用Timer切换目标点。追击与丢失在CHASE状态敌人朝玩家当前位置移动。同时启动一个“丢失计时器”如果一段时间内没有看到玩家则退回PATROL状态。攻击当敌人与玩家距离足够近时进入ATTACK状态播放攻击动画并触发攻击判定。避坑指南不要在敌人的_process或_physics_process里每帧都计算到玩家的路径如使用NavigationServer2D这对性能消耗很大。对于简单2D游戏直接朝玩家当前位置直线移动配合碰撞避免通常就足够了。将复杂的路径计算用在Boss战的特殊阶段。4. 实战开发流程与项目管理有了模块设计我们来看看如何有序地推进整个项目。游戏开发容易陷入“一直在做功能永远看不到完整游戏”的困境因此采用迭代式开发至关重要。4.1 原型验证阶段第一周目标验证核心玩法和手感。任务创建一个最简单的房间场景实现玩家移动、攻击挥剑、生成一个可被攻击消灭的史莱姆敌人、一个可被打开的宝箱。产出一个可运行的.exe或网页版能体验从移动、战斗到获得物品的基本循环。这个阶段要疯狂调整move_speed、attack_range、damage等数值直到手感你觉得“有点意思”。工具使用Godot内置的Remote场景实时修改变量快速迭代。4.2 核心系统搭建阶段第二~四周目标搭建起游戏的主干框架。任务实现GameEvents事件总线定义首批关键信号。完成WorldManager和房间切换逻辑。实现InventoryManager和基础的UI血条、道具栏。设计并实现第一个“能力道具”及其使用逻辑比如炸弹放置、倒计时、爆炸。创建2-3种不同类型的敌人近战、远程、固定炮台。产出一个拥有多个连通房间的小地图玩家可以探索、战斗、获得新能力并用新能力解谜例如用炸弹炸开裂缝墙。4.3 内容生产与整合阶段持续进行目标用已搭建的系统填充游戏内容。任务关卡设计使用Godot的TileMap系统绘制大地图和迷宫。为每个房间设计解谜和敌人配置。敌人与Boss设计为每个敌人设计独特的行为模式。Boss战通常是多个简单AI状态的组合并伴有明显的阶段转换提示。物品与能力扩展添加更多道具弓、箭、回旋镖每种道具都需要配套的获取、使用逻辑以及UI图标。音频与特效为所有动作添加音效和粒子特效打击火花、爆炸烟雾、物品获取光效这是提升游戏质感的廉价方式。项目管理使用Trello或Notion等工具维护一个任务看板。将大任务拆解成可在1-2天内完成的小任务如“实现弓箭的抛物线射击逻辑”、“绘制森林区域TileSet”。4.4 打磨与优化阶段最后阶段目标让游戏从“能玩”变成“好玩”。手感打磨调整所有动作的反馈。攻击命中时是否应该有短暂的帧冻结HitStop受击时是否有屏幕震动和敌人闪红移动时是否有尘土粒子难度曲线检查游戏的前半小时体验。是否循序渐进地引入新机制第一个小Boss战是否有效地教会了玩家基本操作性能分析使用Godot的调试器Debugger和性能分析器Profiler。重点关注_physics_process中的逻辑和绘制调用Draw Calls。对于大量相同的敌人或装饰物考虑使用MultiMeshInstance2D。用户测试找几个朋友来试玩不要给任何指导观察他们在哪里卡住、在哪里感到困惑。这是发现设计问题的最直接方法。5. 常见问题与调试技巧实录开发过程中你一定会遇到各种奇怪的问题。下面是我踩过的一些坑和解决方法。5.1 物理与碰撞的诡异行为问题玩家或敌人有时会卡在TileMap的缝隙里或者穿墙而过。排查首先检查碰撞层Collision Layer和掩码Collision Mask。确保玩家的碰撞掩码包含了墙壁所在的层反之亦然。检查碰撞形状CollisionShape2D或CollisionPolygon2D是否精确贴合精灵图。在编辑器中打开“可见碰撞形状”调试选项。如果使用move_and_slide()确保floor_stop_on_slope和floor_max_angle参数设置正确。对于平台游戏这些参数很重要对于俯视角通常可以保持默认。终极武器在_physics_process中使用print(velocity)和print(get_slide_collision_count())来实时监控移动和碰撞情况。问题Area2D的信号如body_entered有时触发两次或不触发。排查最常见的原因是节点在同一个物理帧内被添加和移除。确保你的逻辑不会导致节点在触发信号的同一帧被queue_free()。检查是否有多个重叠的Area2D。body_entered信号会为每个进入的Area2D各触发一次。使用set_deferred(monitoring, false)在信号处理后禁用监测可以防止同一帧内的重复处理。5.2 资源管理与内存泄漏问题切换房间后内存占用持续上升游戏越来越卡。排查静态资源泄漏确保在移除场景如旧房间时调用了queue_free()。但注意queue_free()是在当前帧结束后才执行。对于立即清理可以使用free()但要确保没有其他地方引用该节点。动态资源泄漏如果你在代码中动态加载资源load()或preload()并存储在全局变量中这些资源会一直留在内存中。对于只在特定场景使用的大资源如Boss的背景音乐应在场景切换时手动释放resource null并让GC回收。使用Godot的性能分析器运行一段时间后查看“对象”计数是否持续增长。定位那些不断创建却未被销毁的节点实例。5.3 存档与读档的坑问题读档后游戏状态不对比如已经打开的门又关上了或者玩家位置错误。排查序列化不完整确保SaveManager保存了所有必要的状态。每个需要持久化的对象房间、机关、敌人都应该有一个唯一的标识符如persistent_id并实现一个返回可序列化字典的方法如func save() - Dictionary。读档顺序读档时应先加载世界和房间然后再根据存档数据恢复房间内各个对象的状态最后再放置玩家并恢复其状态。顺序错乱会导致引用丢失。版本控制在存档数据中加入一个version字段。当游戏更新后存档结构可能改变你需要编写迁移代码将旧版存档转换为新版格式。5.4 跨平台构建的注意事项问题在Windows上运行良好的游戏导出到Web或移动端后出现控制失灵、UI错位或性能问题。解决方案输入处理不要硬编码键盘键位。使用Godot的输入映射Input Map。为“移动”、“攻击”、“交互”等动作定义逻辑名称然后为不同平台绑定不同的物理键键盘、手柄、触摸屏按钮。在代码中只检查动作Input.is_action_pressed(move_right)。UI缩放使用Godot的容器Container和锚点Anchors系统来布局UI而不是绝对坐标。将根Control节点的布局模式设置为“缩放”以适应不同分辨率。性能测试Web和移动端的性能远低于PC。需要更严格地控制绘制调用和粒子数量。使用LOD细节层次技术在远离屏幕或低端设备上使用更简单的精灵和特效。开发这样一个项目最大的收获不是最终做出了一个多像《塞尔达传说》的游戏而是在这个过程中你被迫去思考并实践一套完整的、可维护的游戏架构。你会深刻理解模块解耦、信号通信、资源管理这些概念的重要性。下次当你再启动任何一个新项目无论大小这种系统性的思维方式都会让你事半功倍。最后一个小建议定期备份你的项目Godot的.tscn文件是文本格式用Git进行版本控制会帮你省去无数个追悔莫及的夜晚。

相关新闻

解决AirSim编译报错:Missing UnrealBuildTool.exe的完整指南

解决AirSim编译报错:Missing UnrealBuildTool.exe的完整指南

1. 项目概述:当AirSim遇上UnrealBuildTool如果你正在尝试将微软的AirSim无人机仿真平台集成到Unreal Engine项目中,并且卡在了编译这一步,屏幕上赫然显示着“Missing UnrealBuildTool.exe”这个令人头疼的错误,那么你来对地方了。…

2026/10/9 12:32:56 阅读更多 →
Windows 11部署OpenClaw集成1Password技能指南

Windows 11部署OpenClaw集成1Password技能指南

1. 项目背景与核心需求在Windows 11环境下部署OpenClaw并安装1Password技能,本质上是在构建一个本地化的智能助手系统。OpenClaw作为开源AI助手框架,其模块化设计允许用户通过"技能"扩展功能。1Password作为知名密码管理工具,将其集…

2026/9/29 8:26:41 阅读更多 →
大数据学习心路:从Java基础到Hadoop生态的实战闭环

大数据学习心路:从Java基础到Hadoop生态的实战闭环

1. 从迷茫到清晰:我的大数据学习心路历程四年前,当我第一次在专业导论课上听到“大数据”这个词时,脑子里一片空白。它听起来既宏大又遥远,像是只存在于科技新闻头条里的概念。身边的同学有的已经开始讨论Hadoop、Spark&#xff0…

2026/10/17 11:33:13 阅读更多 →

最新新闻

ASP.NET WebForms 入门实战:30分钟跑通登录页

ASP.NET WebForms 入门实战:30分钟跑通登录页

简介:本资源是面向零基础初学者的《ASP.Net入门经典》系统性学习包,聚焦Web开发核心技能培养,帮助开发者快速掌握ASP.Net Web Forms、MVC架构、Razor语法、页面生命周期、数据绑定及IIS部署等关键能力,适用于高校计算机专业学生、…

2026/10/17 13:49:47 阅读更多 →
Flutter跨端实战:OpenHarmony智能穿搭推荐应用开发全记录

Flutter跨端实战:OpenHarmony智能穿搭推荐应用开发全记录

一个人面对一柜子衣服却总觉得没衣服穿,这个几乎每天都会上演的场景,其实非常适合用代码来解决。最近我在折腾一套方案:用Flutter把智能穿搭推荐应用跑上OpenHarmony设备,做一个能收录所有衣物、根据天气和场合自动给出搭配的个性…

2026/10/17 13:49:47 阅读更多 →
Postman 直连 MySQL:用 xmysql 自动生成 REST 接口调试数据库

Postman 直连 MySQL:用 xmysql 自动生成 REST 接口调试数据库

1. 为什么要在 Postman 里直连 MySQL很多做后端接口调试的朋友都有过这种体验:接口跑不通,第一反应是代码写错了,结果排查半天发现是数据库里的数据不对——字段值被别的流程改了、某条记录被软删了、关联表的数据压根没插进去。这时候如果手…

2026/10/17 13:49:47 阅读更多 →
大模型可观测与评测闭环:Langfuse+LangSmith+OTel实战

大模型可观测与评测闭环:Langfuse+LangSmith+OTel实战

大模型上线以后,最让人头疼的不是“报错了”,而是“看着都正常,但效果就是不对”。报错可以打日志、查异常,有明确的技术栈去处理;效果不对,你根本说不清到底是大模型本身的问题、Prompt写得不对、还是下游…

2026/10/17 13:49:47 阅读更多 →
Claude Sonnet 4.5 编程新王降临:30小时持续编码、0%错误率,TaoToken 统一 Key 接入 Claude Code 实测

Claude Sonnet 4.5 编程新王降临:30小时持续编码、0%错误率,TaoToken 统一 Key 接入 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/17 13:49:47 阅读更多 →
AI科技风PPT模板:工程师的技术表达结构化接口

AI科技风PPT模板:工程师的技术表达结构化接口

简介:本资源是一份专为科技类汇报、AI主题演讲及数字化办公场景设计的高质量PPT模板,面向产品经理、技术讲师、高校师生及企业培训师等需高频制作专业演示文稿的用户,有效解决科技风视觉统一性弱、AI主题素材匮乏、内容排版效率低等痛点。压缩…

2026/10/17 13:48:47 阅读更多 →

日新闻

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

简介:《DeepSeek使用方法.pdf》是一份面向DeepSeek入门及进阶用户的完整使用指南,重点围绕V3与R1两款模型的能力差异与适用场景展开,帮助读者根据任务类型与成本选择合适的模型。资源共1个PDF文件,压缩包大小约311KB,篇…

2026/10/17 0:00:25 阅读更多 →
200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

简介:这是一份聚焦DeepSeek提示词优化的《200Deepseek润色指令》PDF文档,共收录两百余条可直接套用的润色指令,涵盖语法修正、风格统一、逻辑梳理、表达精炼等维度,并针对商务写作、学术论文、广告文案等场景给出差异化建议。文档…

2026/10/17 0:00:25 阅读更多 →
OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

简介:本资源是一份基于OpenCV的本科课程设计完整报告,面向计算机视觉初学者、图像处理学习者及高校通信/信息类专业学生,解决物体智能计数与检测结果结构化记录的实际问题。项目覆盖图像预处理、轮廓检测、边界框绘制、轨迹栏交互式参数调节、…

2026/10/17 0:00:25 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 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/16 21:54:38 阅读更多 →
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/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练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/16 14:23:36 阅读更多 →