Godot 2D游戏开发第5期:打通场景切换、敌人AI与UI音效的可玩闭环

做2D游戏做到第五期,最容易遇上的状态就是:玩家已经能跑能跳了,但游戏还不像游戏。前四期我们一路把角色动画、基础TileMap关卡、金币收集勉强拼了出来,可一旦想打通从"进入关卡"到"死亡重来"再到"结算展示"的完整流程,就发现处处欠着债。本期系列一第5篇,我打算集中把这笔债还掉:把场景结构理清楚、把游戏状态接起来、把敌人从木桩改成会巡逻会攻击的活物、再把UI和音效补上。这期结束之后,你手里的Godot 2D工程才算真正有了一个"可玩闭环"的雏形。

如果你是从前几期一直跟下来的,那这期会很舒服,因为每一步都是在前面的工程上做加法。如果你是半路看到这篇,只要手头有玩家控制、基础平台关卡的2D工程,也可以直接照着改。

1. 进度盘点:前四期留下了一个"能跑不能玩"的Demo

1.1 当前场景树长什么样

按照前几期的搭法,你的主场景大概长这样:

Main(Node2D) ├── Player(CharacterBody2D) │ ├── CollisionShape2D │ └── AnimatedSprite2D ├── LevelRoot(Node2D) │ └── TileMapLayer(或旧版TileMap) │ ├── 地面/平台 │ ├── 金币(Area2D实例) │ └── 终点门 └── Camera2D

玩家脚本里塞了移动、跳跃、重力处理,金币用Area2D检测到玩家后调用queue_free()销毁并加一分。看起来没有问题,但只要你稍微往后想一步,就会发现三件大事完全没着落:

  • 角色死亡后没有重生机制;
  • 金币分数没有全局保存;
  • 从一关到另一关的切换逻辑是空的。

这些问题如果不先解决,后面加敌人、加机关、加Boss都只是往一个地基都没打稳的房子上添砖。

1.2 排查清单:距离可玩还有哪些缺口

我习惯在每个阶段结束时列一份"可玩性缺口清单",比盲目堆功能要高效得多。第5期开动前,我给自己列的清单是这样的:

模块当前状态本期目标
场景切换完全没写支持关卡重载与下一关跳转
死亡逻辑玩家掉出地图就结束扣生命、重生、游戏结束
分数与生命存在某个临时变量里放进全局单例
敌人没有巡逻+追击+攻击
UI没有分数、生命、关卡提示
音效没有背景音乐与关键反馈音效

这是很典型的"第5期该做的事":不搞新花样,先把核心循环打通。这条循环一旦通了,后面再加任何玩法都是往框架里填东西。

2. 场景切换与全局数据:用Autoload把状态接住

2.1 GameState单例的数据设计

Godot里跨场景保存数据最直接的办法就是Autoload单例。在Project Settings -> Autoload里注册一个脚本,比如叫GameState,生命周期从游戏启动一直持续到退出,天然就是全局数据中心。

我通常会在里面放这些内容:

extends Node signal score_changed(new_score) signal lives_changed(new_lives) var score: int = 0 var lives: int = 3 var current_level: int = 1 func add_score(amount: int) -> void: score += amount score_changed.emit(score) func lose_life() -> void: lives -= 1 lives_changed.emit(lives) func reset_game() -> void: score = 0 lives = 3 current_level = 1

为什么要把分数和生命放在这里而不是放在关卡场景里?因为场景在切换时会释放,那个瞬间除了Autoload和场景树本身,其他东西全都保不住。前四期你可能侥幸没遇到过切场景掉数据的坑,但只要开始做多关卡,这个坑迟早踩。与其到时候手忙脚乱,不如现在就把状态抽出去。

2.2 重生、场景切换与退出流程

状态放好了,接下来是"怎么用"。玩家死亡的处理可以统一放在Player脚本里,但决策交给GameState:

func _on_player_died() -> void: GameState.lose_life() if GameState.lives <= 0: get_tree().change_scene_to_file("res://scenes/game_over.tscn") else: get_tree().reload_current_scene()

这里关键点有两个:

一是reload_current_scene()。它不仅重新加载关卡,还会把关卡里所有子节点重置回来,金币、敌人位置、机关状态都会回到初始状态。这就是为什么我们不用手动去"清空场上敌人"——只要场景所有可变化状态都能从头开始重建,重生就是免费的。

二是切换场景建议用change_scene_to_file还是change_scene_to_packed。前者每次加载需要重新从磁盘读取,资源大了以后有明显卡顿;后者把PackedScene预先加载好,切换时基本是瞬时的。我的习惯是一旦项目资源多起来,立刻改用后者:

const GAME_OVER_SCENE: PackedScene = preload("res://scenes/game_over.tscn") func go_game_over() -> void: get_tree().change_scene_to_packed(GAME_OVER_SCENE)

顺带说一个新手容易忽略的点:如果玩家是从平台上掉下去触发了死亡,一定要先把玩家的碰撞处理关闭或者做一个死亡动画,否则切场景前的一两帧里可能触发第二次死亡信号。我踩过这个坑,反应是死亡画面刚弹出来就闪退回关卡,非常奇怪。最简单的规避办法是在死亡函数开头加一个set_deferred("process_mode", Node.PROCESS_MODE_DISABLED),让玩家在切换场景前暂停一切处理。

3. 敌人从"木桩"到"巡逻者"的改造

3.1 往返巡逻的几种实现与选型

敌人是很多2D游戏教程里最容易糊弄过去的部分,但恰恰是这一步决定玩家觉得"游戏有内容"还是"游戏很空"。最简单的敌人实现方式是"站在一个点上不动",这只能算障碍物,不能算敌人。我们要做的是让它动起来。

巡逻逻辑本质上就是"在两点之间来回走"。Godot里有两种主流做法:

  • 用Path2D/PathFollow2D:适合敌人走固定路线、多节点折线路径的场景;
  • 手写左右边界判断:适合平台跳跃游戏,敌人沿水平方向在一个区间内往返。

平台跳跃关卡里手写边界更直接。原因是玩家与平台交互时,敌人的巡逻区间经常受地形影响,Path2D的路径要一个个点去摆,手写边界只需要两个数字:

extends CharacterBody2D @export var patrol_left: float = 100.0 @export var patrol_right: float = 300.0 @export var speed: float = 60.0 var direction: int = -1 func _physics_process(delta: float) -> void: if global_position.x <= patrol_left: direction = 1 elif global_position.x >= patrol_right: direction = -1 velocity.x = direction * speed move_and_slide()

这里需要提醒一点:patrol_left和patrol_right尽量不要用绝对坐标硬编码,我更喜欢在场景里摆好两个Marker2D子节点来标记边界,然后取它们的global_position.x。后期调整敌人巡逻范围不用改数字,直接拖Marker2D就行。

3.2 追击与攻击判定:Area2D触发器的正确用法

光会巡逻的敌人还是木桩,得让它在玩家靠近时切换行为。这里要用到Area2D而不是直接在敌人身上改碰撞范围。

最合理的节点结构是这样的:

Enemy(CharacterBody2D) ├── CollisionShape2D(身体碰撞) ├── AnimatedSprite2D ├── DetectRange(Area2D) │ └── CollisionShape2D(检测范围) └── AttackRange(Area2D) └── CollisionShape2D(攻击判定)

DetectRange负责告诉敌人"玩家来了",AttackRange负责触发伤害。两个Area2D各管一摊,比在一个Area2D里既检测接近又检测攻击要清爽得多。

追击代码很好写:

func _on_detect_range_body_entered(body: Node2D) -> void: if body is Player: is_chasing = true func _on_detect_range_body_exited(body: Node2D) -> void: if body is Player: is_chasing = false

当is_chasing为true时,敌人的水平移动从"巡逻区间"切换成"朝玩家方向移动",边界也不再限制它。追击时记得保留物理层里敌人的碰撞,否则它会直接穿过平台。

攻击判定要配套一套碰撞层规范。Godot 4的碰撞层在项目设置里配置,我的习惯是:

层对象说明
1玩家玩家身体
2环境地面、平台、墙体
3敌人敌人身体与检测触发
4玩家攻击玩家挥剑/射击的判定
5敌人攻击敌人碰撞玩家的伤害判定

AttackRange的collision_mask只需要勾选第1层玩家,玩家方面则要确保collision_layer=1。然后写:

func _on_attack_range_body_entered(body: Node2D) -> void: if body is Player and not _has_attacked: _has_attacked = true body.take_damage(1) _attack_timer.start()

注意_has_attacked这个标记非常重要。Area2D在玩家持续站在范围内时会不断触发body_entered,如果不加冷却,玩家的血量会在一瞬间被扣光。用计时器做攻击间隔是常规解法,我习惯给敌人一个attack_cooldown,默认1秒。

4. TileMap的图层规划与机关配置

4.1 图层编号与渲染遮挡

前四期的TileMap可能只有一个图层,所有东西堆一起。到了第5期,机关、平台、装饰逐渐多起来,图层规划立刻变成刚需。

Godot 4里推荐使用多个TileMapLayer节点,而不是在旧版TileMap里管理多个图层。每个TileMapLayer独立渲染、独立设置物理属性,改起来不会牵一发动全身。我的规划通常四层:

LevelRoot(Node2D) ├── GroundLayer(TileMapLayer) # YSort基准层,承载物理碰撞 ├── PropsLayer(TileMapLayer) # 装饰,无碰撞 ├── CollisionLayer(TileMapLayer) # 单向平台、跳台等特殊物理 └── FrontLayer(TileMapLayer) # 前景遮挡,渲染在最前面

这四层的顺序不是随便排的。底层贡献物理碰撞,装饰层负责让场景不单调,特殊物理层放"只能从下面跳上来"的single direction平台,前景层则承担"玩家从树后走过时被树冠遮挡"的纵深效果。

牵涉到渲染遮挡时,记得把需要排序的节点放在同一个YSort容器下:如果地面层和玩家都不属于YSort,玩家走过去时可能被错误地画在装饰物上面,画面层次出问题。很多新人在这卡住,表现是"人都走到树后面了,树却在人后面",其实就是忘了继承YSort。

4.2 移动平台与一次性机关的连接方式

移动平台在2D平台跳跃里几乎是必需品。最常见实现是AnimatableBody2D,配上脚本控制位置:

extends AnimatableBody2D @export var move_distance: Vector2 = Vector2(200, 0) @export var move_duration: float = 2.0 var _start_pos: Vector2 var _t: float = 0.0 func _ready() -> void: _start_pos = global_position func _physics_process(delta: float) -> void: _t += delta var weight = (sin(_t / move_duration * PI * 2) + 1.0) / 2.0 global_position = _start_pos + move_distance * weight

这段代码里最值得留意的是AnimatableBody2D这个名字。如果用的是普通的StaticBody2D,它没有任何"把玩家带起来"的能力,玩家站在上面就会原地往下掉。用AnimatableBody2D后,引擎会自动处理与玩家的相对速度,玩家站在上面就跟坐电梯似的自然移动。

一次性机关的连接逻辑也很典型:开关放Area2D,目标是门或者移动平台。关键在用Export导出节点引用,而不是硬编码NodePath:

extends Area2D @export var target_door: AnimatableBody2D # 或 NodePath func _ready() -> void: body_entered.connect(_on_body_entered) func _on_body_entered(body: Node2D) -> void: if body is Player: target_door.open()

这里有个小经验:从场景面板把目标节点拖到Export变量上,比在代码里写get_node("../Door")要安全得多。一旦场景结构调整,字符串路径会断,Export引用的容错率更高。

5. UI接入与音效:别让游戏在"看得见反馈"上露怯

5.1 CanvasLayer里的HUD组织

很多Godot新手做UI,第一反应是把Label直接挂在主场景的Node2D下。这样做的结果是UI会跟随摄像机运动——主场景的Node2D节点如果被Camera2D移动影响,UI就飘走了。正确做法一定是用CanvasLayer。

我惯用的HUD场景结构:

HUD(CanvasLayer) ├── MarginContainer │ └── VBoxContainer │ ├── HBoxContainer │ │ ├── ScoreLabel(Label) │ │ └── LivesLabel(Label) │ └── LevelLabel(Label) └── CenterContainer └── MessageLabel(Label,用于弹出"Level Start"提示)

5.2 分数、生命值、游戏结束的联动

是时候把第2章里GameState的signal用起来了。HUD脚本只需要连接信号,然后更新Label:

extends CanvasLayer @onready var score_label: Label = %ScoreLabel @onready var lives_label: Label = %LivesLabel func _ready() -> void: GameState.score_changed.connect(_on_score_changed) GameState.lives_changed.connect(_on_lives_changed) _on_score_changed(GameState.score) _on_lives_changed(GameState.lives) func _on_score_changed(new_score: int) -> void: score_label.text = "分数:%d" % new_score func _on_lives_changed(new_lives: int) -> void: lives_label.text = "生命:%d" % new_lives

这里有一个我之前常常忽略、后来才想明白的点:连接到signal之后,记得在_ready里手动刷新一次UI,否则场景加载后、玩家捡到第一枚金币前,屏幕上的分数会停留在初始值不对。很多教程没提这个,但实际交接工程时,这个问题会让第一次运行的人以为UI坏了。

游戏结束画面不用花哨,一个简单的Control场景,包含"你的得分"、"重新开始"按钮就行。按钮的处理:

func _on_restart_button_pressed() -> void: GameState.reset_game() get_tree().change_scene_to_file("res://scenes/level_1.tscn")

重置不掉的话,玩家下次开始游戏会带着上一局剩的命和分继续,那属于硬伤。

5.3 音效接入与资源加载注意事项

音效接入这事儿说难不难,但细节特别多。

先在场景里放几个AudioStreamPlayer节点,我通常放两个:一个BGMPlayer用来循环播放背景音乐,一个SFXPlayer用来播放金币、跳跃、受伤这些短音效。注意BGM的音频导入设置里,一定要在Import面板勾上"Loop"循环模式。不然一段音乐播完就戛然而止,很尴尬。

短音效的play写法有讲究:

sfx_player.stream = coin_sfx sfx_player.play()

如果连续捡金币,这样写每次都会因为重新设置stream而打断上一次播放。做连击收集时,我会用AudioStreamPlaybackPolyphonic或者干脆放一组SFXPlayerPool,按顺序轮换播放。对第5期而言,简单点可以用:

func play_sfx(stream: AudioStream, pitch_scale: float = 1.0) -> void: sfx_player.pitch_scale = randf_range(0.9, 1.1) sfx_player.stream = stream sfx_player.play()

给每次播放加个微小的随机音调变化,能让连续触发的声音听着不那么机械。这个技巧很多音效细节里都会用到,但教程很少说,属于那种"你注意到了才觉得高级"的小处理。

最后提一个很多人都会踩的坑:切场景时音效突然断了。原因是AudioStreamPlayer挂在关卡场景里,场景一切就被释放。解法很简单——把BGMPlayer和全局音效播放器都放在Autoload单例里,或者放在单独的AudioManager节点里做成Autoload。这样音乐才能跨场景连续播放,不会每进一关就断一次。

顺带聊下资源加载,音效文件格式别直接用MP3。Godot对OGG和WAV的支持更稳,尤其环形循环的BGM,OGG质量好体积小。短音效用WAV加载速度更快。一个2D游戏如果素材库里全是MP3,导入阶段就可能出现解码延迟,影响播放响应。

接着上面的思路,现在把敌人、UI、音效都挂进了现有的项目里,再回头看第1章列的那张缺口清单,会发现每一项都已经落地了。剩下的事情不是再加功能,而是打磨手感:跳跃参数、攻击判定范围、敌人追踪速度、UI字体大小……这些数值的调整才是让游戏从"能玩"变成"好玩"的关键。我个人的习惯是每次改完一个数值,立刻实际跑一遍关卡,别一次改一堆参数,否则手感坏了都不知道是哪个参数出了问题。

第6期开始,我们可以放心地把视野放大去做更复杂的玩法了,比如Boss战、多关卡之间的进度保存,或者商店系统。这些内容有一个共同前提:基础循环够稳。这期做完的工程,就是后面的地基。

相关新闻

AI辅助论文平台全拆解:五类能力画像与六周实战工作流

AI辅助论文平台全拆解:五类能力画像与六周实战工作流

如果你最近在准备毕业论文&#xff0c;一定已经发现&#xff1a;事情的关键往往不是“写不出来”&#xff0c;而是“不知道写到什么程度才算够”。我随手翻过某工科院系33份开题报告&#xff0c;其中24份在资料搜集阶段用过AI辅助工具&#xff0c;但最后能顺利过盲审的只有6份。…

2026/10/20 2:36:35 阅读更多 →
RAG查询优化实战:查询转换与分解提升检索命中率

RAG查询优化实战:查询转换与分解提升检索命中率

做过RAG落地的人应该都有这种体验&#xff1a;检索链路搭好了、向量化了、TopK也调了&#xff0c;但用户一问某些问题&#xff0c;召回结果就是不对味。比如用户问“那个能折叠的蓝牙键盘多少钱”&#xff0c;文档里明明写着“便携式折叠键盘&#xff0c;支持蓝牙连接&#xff…

2026/10/20 2:36:35 阅读更多 →
Coze技能上架全流程:从OpenAPI设计到商店审核与运营实战

Coze技能上架全流程:从OpenAPI设计到商店审核与运营实战

说实话&#xff0c;多数人第一次接触Coze商店里的Skills技能&#xff0c;都是从“用”开始的——在Bot编辑页面挑一个现成技能&#xff0c;填几个参数就完事&#xff0c;完全没想过这东西还能自己造。直到我自己把第一个自用的查询工具封装成技能、提交审核、正式上架之后&…

2026/10/20 2:36:35 阅读更多 →

最新新闻

SQL布尔盲注实战:从sqli-labs第十五、十六关拆解无回显渗透

SQL布尔盲注实战:从sqli-labs第十五、十六关拆解无回显渗透

1. 项目概述&#xff1a;从“看不见的输入框”开始理解布尔盲注的真实战场你有没有遇到过这样的页面&#xff1a;一个登录框&#xff0c;输入错误的账号密码&#xff0c;它不告诉你“用户名不存在”或“密码错误”&#xff0c;只冷冷地回一句“登录失败”&#xff1b;再试一次&…

2026/10/20 3:28:05 阅读更多 →
免费高精度IP定位查询入口:数据源拆解、实测对比与评估指南

免费高精度IP定位查询入口:数据源拆解、实测对比与评估指南

这些年我见过太多人带着“免费高精度IP地址定位查询入口”这个想法找上门&#xff0c;搜了一圈之后拿回来的要么是某个商业平台体验版&#xff0c;要么是挂着“全球精准定位”横幅、实际只能定位到城市甚至省份的普通接口。今天不绕弯子&#xff0c;直接把这几个问题拆开聊&…

2026/10/20 3:28:05 阅读更多 →
推理引擎原理与优化:从计算图到性能调优的完整指南

推理引擎原理与优化:从计算图到性能调优的完整指南

做推理引擎这几年&#xff0c;我最大的感受是&#xff1a;很多人手上的模型跑得起来&#xff0c;但跑不快、跑不稳&#xff0c;出了问题也不知道该从哪一层开始查。根本原因是把“AI 模型”和“推理引擎”当成两个隔离的黑盒——模型文件一丢&#xff0c;引擎一跑&#xff0c;结…

2026/10/20 3:28:05 阅读更多 →
模型压缩 - 量化剪枝边缘部署实战

模型压缩 - 量化剪枝边缘部署实战

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

2026/10/20 3:28:05 阅读更多 →
SpringBoot+Vue全栈实战:服装销售平台管理系统设计

SpringBoot+Vue全栈实战:服装销售平台管理系统设计

不是毕业设计模板&#xff0c;也不是网上那些CtrlC就能跑的教学项目。我最近刚把一个服装销售平台管理系统从零搭完&#xff0c;项目代号就叫“衣依”&#xff0c;技术栈是SpringBoot Vue MyBatis MySQL。从需求梳理到数据库设计&#xff0c;再到前后端联调、部署上线&#…

2026/10/20 3:28:05 阅读更多 →
调教Stable Diffusion注意力机制,让出图更清晰

调教Stable Diffusion注意力机制,让出图更清晰

1. 先别急着加高清修复&#xff0c;问题多半出在注意力分配上先说个不少人踩过的坑&#xff1a;用Stable Diffusion出图&#xff0c;总感觉边缘发虚、细节糊成一团&#xff0c;第一反应往往是把分辨率从512拉高到1024&#xff0c;或者直接开Hires. fix&#xff0c;结果要么显存…

2026/10/20 3:27:05 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

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

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

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

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

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

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/19 10:02:04 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 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/19 2:54:50 阅读更多 →
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/19 9:10:22 阅读更多 →
黑夜航拍船只数据集训练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/18 12:16:16 阅读更多 →