从零构建月球着陆模拟器:物理模拟与状态机实战
最近我把一个存了很久的想法落地了做一个纯粹的月球着陆模拟器项目代号就叫 Apollo-11。名字不复杂但它把我的目标说得很清楚——让玩家用有限的燃料手动控制一艘着陆器在起伏的月面上找到一块平地减速、调整姿态最后安全着陆。这个小项目并不追求“大作”级体验而是想做成一个能跑在浏览器里、打开就能玩的技术演示同时把游戏开发里最核心的物理模拟、状态机、碰撞检测这些事扎扎实实地串起来。我自己在写的过程中最大的感受是一个看上去“只有几个按钮”的小游戏拆开之后全是细节。物理公式选哪一套、碰撞检测采样几个点、状态切换谁先谁后都会直接决定最终手感。所以这篇文章想把整个项目的设计思路、关键实现和踩坑过程都摊开来讲。如果你正想做一个类似的物理小游戏或者单纯想看看一个小型模拟器是如何从零长出来的这篇整理应该能给你一份可以直接参考的操作方案。1. 项目整体设计与思路拆解1.1 为什么是浏览器里的着陆模拟器刚动工的时候我列过几个技术方向用游戏引擎做一套 3D 模拟或者用原生语言写桌面窗口程序甚至考虑过硬件套件加传感器的方式。后来全部否掉了最终选择了最朴素的“浏览器 2D Canvas”。原因很简单目标场景不需要复杂的 3D 渲染。月球着陆的核心体验在于“对速度和姿态的控制”2D 平面表现这个主题完全足够而且能大幅降低渲染成本和交互复杂度。浏览器的好处是零安装、零依赖打开 HTML 文件就能跑。对于技术分享和改动演示来说这是最省事的载体。再加上现代浏览器对 Canvas 2D 的支持已经非常稳定性能足够撑起一个常驻的模拟循环完全不需要凑热闹引游戏引擎。另一个考虑点是学习成本。如果用重型引擎工程脚手架会盖过核心逻辑读者很难看清物理模拟是怎么和游戏循环耦合的。而纯 JS Canvas 写出来的代码状态更新、碰撞检测、UI 绘制全部摊在眼前几乎不需要额外解释工具链。当时我给这个项目定的技术标准只有三条逻辑层和渲染层分离物理模拟可配置状态切换清晰可读。1.2 物理模型该简化的地方绝不复杂着陆模拟器最容易犯的错是物理模型越做越复杂。一开始我也想过加入自转修正、多轴惯量、燃料重心偏移甚至在飞船上模拟姿态控制力矩。后来发现这些内容在一个 2D 小游戏里不但难以被玩家感知还会让数值调试变成一场灾难。最后采用的物理模型是平面二维运动只考虑三个核心作用力垂直方向的重力、引擎产生的推力、以及由姿态决定的水平方向分力。月球重力加速度固定为 1.62 m/s²没有空气阻力没有风力。小游戏里玩家能感知到的就是“重力把飞船往下拉引擎推力把飞船往上顶”这两个力的对抗已经足够形成紧张感。公式上每一帧更新速度时使用经典的运动学积分速度更新vx ax * dtvy ay * dt位置更新x vx * dty vy * dt加速度来源g 1.62引擎推力向量按照飞船当前朝向旋转。引擎的推力方向直接由飞船的朝向决定这个设计很直观。玩家按下“左”键时飞船逆时针旋转推力方向也跟着转于是速度中会出现水平分量。如果不调整朝向飞船就会一边飘一边落如果调整过度又会往反方向甩出去。物理模型越简单玩家的“直觉”反而越可靠。1.3 核心状态机从待机到登月整个项目里最容易被忽略又最值得设计的一块是状态机。没有状态机的时候我试过用一堆布尔变量去判断当前是否允许输入、是否显示结算面板结果不到半小时就出现了组合爆炸。飞在天上时按“开始”键能重置飞船失败时还能继续喷引擎这种逻辑混乱很快逼我停下来。最终定义了几组稳定的状态READY显示初始界面飞船停在发射台上等待玩家点击“起飞”。FLYING游戏主阶段玩家可以通过键盘控制飞船的引擎开关、旋转方向和推力大小。LANDED成功着陆状态飞船停稳显示任务完成终止输入响应。CRASHED失败状态飞船已撞击地面显示失败原因终止输入响应。OUT_OF_FUEL燃料耗尽进入失控滑落状态此状态下引擎不再响应但仍保留物理模拟。状态之间的跃迁只允许在明确事件下发生。比如只有 FLYING 状态下燃料耗尽才进入 OUT_OF_FUEL只有 OUT_OF_FUEL 或 FLYING 状态下发生撞击才进入 CRASHED。这样一来任何一帧更新只需要查看当前状态决定要不要执行物理积分、要不要处理输入逻辑路径非常干净。2. 核心细节解析与实操要点2.1 坐标系与渲染层分离这个项目里我踩过最大的一个坑是差点把物理坐标和屏幕像素坐标混在一起。早期原型里直接让飞船的 x、y 用像素值驱动结果想改屏幕尺寸时所有物理参数全部作废飞船动不动就飞出了视野。后来强行约定物理世界使用“米”作为单位渲染层负责把米转换成像素。转换函数集中在两个地方一个是从世界坐标到屏幕坐标另一个是从屏幕坐标到世界坐标。这样做的好处非常明显重力加速度 1.62 m/s² 直接按照物理世界数值写不需要转换成“多少像素每秒平方”碰撞检测全部在世界坐标里做和浏览器窗口大小无关。渲染层在做的事是根据视口中心点和缩放比例把世界坐标的 (x, y) 映射到 Canvas 的 (screenX, screenY)。function worldToScreen(worldX, worldY, camera) { const screenX (worldX - camera.x) * camera.scale canvas.width / 2; const screenY (worldY - camera.y) * camera.scale canvas.height / 2; return { x: screenX, y: screenY }; }画面缩放到什么程度只影响表现不影响模拟。我在实际调参时会直接在代码里手动改 camera.scale让视野从“月球表面大画幅”切到“飞船特写”物理参数完全不用动。这个设计后来帮我节省了大量调试时间。2.2 着陆判定速度、角度、落点模拟器真正核心的玩法点是着陆判定怎么定义“成功”。我参考了很多同类游戏的做法最终没有采用“接触到地面就算着陆”的粗暴逻辑那样玩家会很容易利用高速横擦地面蒙混过关。我的判定规则由三个条件共同构成垂直速度小于 2 m/s。垂直速度是相对月面法线方向而言在这个 2D 场景里就是 vy 的绝对值。水平速度小于 1 m/s。要求飞船横向漂移也要足够小毕竟现实中没有摩擦力让飞船在接触瞬间“刹住”。飞船倾角绝对值小于 10 度。着陆器必须保持基本上直立歪着插进地面不算成功。这三个阈值直接变成常量写在判定模块顶部。每个阈值都不是拍脑袋定的我把 g1.62 代入试算过垂直速度 2 m/s 意味着玩家在最后一秒还有约 1.2 米的减速余地如果阈值设成 3 m/s玩家会明显觉得“明明摔得挺重也算成功”。设为 2 之后手感会紧张但不苛刻。碰撞检测部分我没有用像素级检测而是把飞船抽象成底部两个端点的线段。每一帧取飞船底部的左右两点检测它们和地形的交点。只要有一个点穿越到了地面高度以下就认为发生了接触碰撞。之所以不用飞船中心点是因为中心点离底部还有很长距离看起来明明要触地了中心点仍在地形上方很远处导致撞击判定严重延迟。2.3 燃料、重力与任务进度燃料系统是整个游戏里“压力”的主要来源。我设计燃料总量为 100 单位最大推力设置为 18 m/s²燃料消耗率是推力值乘以固定系数。也就是说玩家长时间满推飞行会快速耗尽燃料而用低推力和滑翔策略则能飞得更久。燃料和推力的关系采用的是线性比例模型const consumptionPerSecond currentThrust * 0.012; fuel - consumptionPerSecond * deltaTime;如果当前推力为 18每秒就要消耗 0.216 单位燃料100 单位大约只能支持 7 分多钟的全马力运行但实际飞行中很少持续全推所以可玩性还行。任务进度的显示也花了一些心思。除了高度和剩余燃料我还增加了“当前垂直速度”“当前水平速度”“倾角”三个读数。目的很直接让玩家在操作时能像看仪表盘一样实时调整策略而不是凭感觉去猜。调试时这些数据也是我的重要参考用文字印在 Canvas 右上角比开 DevTools 去查变量直观得多。3. 实操过程与核心环节实现3.1 主循环固定时间步长是底线游戏循环的第一版我直接用 requestAnimationFrame 驱动每帧计算 dt然后直接更新物理。看起来理所当然但很快就发现问题当电脑性能波动或者浏览器标签页短暂失去焦点再回来时dt 会突然变大。假设某一帧 dt 变成 0.2 秒物理积分会一下子往前推进很多飞船可能直接从天上穿到地下判定直接失败。这种情况在 Web 平台上几乎无法避免最稳妥的解法是固定时间步长 累加器。每次 requestAnimationFrame 回调都计算真实的时间差把它累加到 accumulator 里然后以固定步长比如 1/60 秒循环消费这个时间差。这样无论帧率怎么波动每一步物理更新所看到的时间步长都是稳定的模拟结果可复现。核心循环结构大致是这样function gameLoop(timestamp) { if (!lastTime) lastTime timestamp; let frameTime (timestamp - lastTime) / 1000; lastTime timestamp; if (frameTime 0.1) frameTime 0.1; accumulator frameTime; while (accumulator step) { update(step); accumulator - step; } render(); requestAnimationFrame(gameLoop); }补充说明一下代码里把单帧最大时长限制在 0.1 秒是为了防止标签页长时间挂起后进入“追帧地狱”。如果用户切出去几十秒再回来累加器会积压大量步长物理世界会以极快的速度瞬间跑完这显然不是我们想要的。限制掉最大帧时间后最多只补几帧现象就会自然恢复。3.2 键盘与触屏输入处理输入系统要同时考虑桌面和移动端。键盘上我绑定了方向键和 WASD上/W 增加推力左/A 左转右/D 右转下/S 或空格用来点火也可以作为刹车辅助。触屏上则直接在画面底部绘制三个虚拟按钮左转、点火、右转。这里有一个很容易忽略的设计键盘事件的按下和抬起需要跟踪而不是用 click 一次性触发。玩家按住方向键时飞船应该持续旋转松开后才停止。所以我用了一个 Set 保存当前按下的键在每帧物理更新时查询这个 Set 来决定是否施加力矩。const keys new Set(); window.addEventListener(keydown, (e) { keys.add(e.code); if ([ArrowUp, ArrowDown, ArrowLeft, ArrowRight, Space].includes(e.code)) { e.preventDefault(); } }); window.addEventListener(keyup, (e) { keys.delete(e.code); });随后在 update 里判断if (keys.has(ArrowLeft) || keys.has(KeyA)) ship.angle - turnSpeed * dt; if (keys.has(ArrowRight) || keys.has(KeyD)) ship.angle turnSpeed * dt; if (keys.has(ArrowUp) || keys.has(KeyW) || keys.has(Space)) engineOn true;这样还能够方便地支持快捷键组合尤其“按住方向键同时点火”这种常见操作用 Set 判断比一系列布尔变量要干净得多。触屏虚拟按钮的实现则是在 pointerdown/pointerup 事件里往同一个 Set 里添加或删除对应的虚拟按键值。逻辑完全复用。3.3 地形生成与视觉反馈地形模块直接用高度图实现。最开始的方案是手写一段固定数组但看起来太“假”也没有可重复性。后来采用了简单的分段随机生成将水平方向切成若干段每段给出一个高度值再用线性插值连接。为了不让地形出现直角折线我额外做了一次平滑处理将每个高度点与前后两点取平均。着陆点选择也做了特殊处理。在生成地形时我会强制在画面中间附近保留一段较平缓的高度区间并把这段区间标记为“推荐着陆区”。实际视觉效果就是一片比较明显的平地玩家会自然地把它当作目标。这比让玩家在随机山脉中找一条可能存在也可能不存在的平路要友好得多。视觉反馈上除了飞船的实时角度和推力火焰位置我还加入了几个小细节速度过大时飞船周围会出现红色警示线倾角过大时底部会画出方向箭头燃料低于 20% 时燃料条会开始闪烁。这些反馈全部在渲染层完成不干扰物理判定属于“纯表现”功能但很能提升游玩的紧张感。4. 常见问题与排查技巧实录4.1 引擎没动数值却一直在飘项目做到中期时我遇到一个很奇怪的现象明明没有按任何键飞船的 vy 却在持续增大甚至还会横向漂移。第一反应是“是不是哪里的物理公式写错了”。检查之后发现加速度计算没有任何问题问题出在 requestAnimationFrame 的 dt 单位上。在一段时间里我将 timestamp 的单位当成了秒直接参与计算但 timestamp 是毫秒。于是一步物理更新相当于真实时间 1000 倍的 dt重力加速度自然看起来被放大了无数倍飞船瞬间被“吸”向地面。排查办法很简单在渲染层把 dt 和当前帧序号一起显示在屏幕上观察数值是否合理。当发现 dt 一直在 0.016 附近而物理世界却“跳秒”时立刻就能锁定是单位换算问题。后来我也养成了一个习惯所有物理相关参数都要用世界单位先跑一遍纯逻辑测试再套渲染层。纯逻辑测试时只输出 x、y、vx、vy不画任何图形定位问题会快很多。4.2 明明看起来没碰到怎么判了撞击第二次比较难缠的问题是碰撞检测“提前触发”。飞船从高空下落底部距离地面还有一小段肉眼可见的间隙系统却直接判定 CRASHED。一开始我怀疑是碰撞阈值设置太严格但我需要区分“垂直速度阈值”和“接触检测”。后来加上了调试用的线框渲染把飞船的碰撞盒和地形高度线画出来才看到真相地形的高度数据是离散采样的采样点之间用线段连接而飞船在这个间隔内穿过了连接线导致检测到的接触点位置比视觉表现的地表线“更早”命中。解决方式分成两步。第一步增加地形采样点密度把所有点之间的距离从 1 米压缩到 0.2 米线段长度缩短后穿透误解明显减少。第二步修改碰撞检测逻辑不只检测飞船底部端点是否低于地形高度还检测两个连续帧之间飞船底部线段和地形线段是否存在交点用线段相交测试替代简单的点高度比较。线段相交检测的代价虽然略高一点但考虑到这一步不是每帧对所有线段执行而是先查找飞船水平位置附近的地形段再只检测相邻两段性能完全不是问题。修复之后“看着没碰实际碰了”的误判基本消失了。4.3 状态机卡在“着陆成功”不可重开还有一个很隐蔽的状态问题成功后点击“重新开始”时飞船又飞起来了但 UI 上仍显示“LANDED”按钮无响应只能刷新页面。检查后明确问题出在“重置逻辑不完整”。我在 reset 函数里确实把飞船位置、速度、燃料都重置了却没有重置状态机和输入的按键状态。典型的场景是玩家在空中一直按着方向键着陆成功后松手但键盘事件可能被浏览器吞掉导致 keys 集合里还残留一个按键重置后 update 帧执行检测到该按键存在立刻给飞船加了角速度但状态机仍在 LANDED于是又产生了异常。修复方法是在重置函数里执行三个步骤第一设置状态为 READY第二调用一个方法清空 keys 集合模拟所有按键弹起第三重置累计时间、暂停标志等所有运行时变量。function resetGame() { ship.reset(); keys.clear(); gameState GameState.READY; fuel 100; accumulator 0; lastTime 0; }这个“重置时要清理输入”的教训后来也用在暂停功能上。暂停解除后如果不清空按键就会有一飞机自己乱转的体验。多数这类问题都可以通过强制输入状态重置来避免。做这个小项目的过程中最大的体会是模拟器类游戏把“确定性”看得很重。物理不能用拍脑袋的帧率来驱动碰撞不能靠近似感觉来判定状态切换更不能指望几个布尔变量互相配合。每一个看起来无关紧要的细节最后都可能在某一帧变成玩家面前的 bug。如果你也打算尝试类似的物理模拟项目我建议第一版就把固定步长、状态机和坐标转换写好再把着陆判定阈值做成可调参数这样后续所有手感打磨都会顺利很多。最后再分享一个小技巧给游戏加一个“慢动作模式”把步长从 1/60 降到 1/180调试碰撞和玩家操作时就能看清每一帧到底发生了什么。这个小功能平时不起眼但排查问题时比任何日志都管用。

相关新闻

ESP32修复USB4000光谱仪变砖:EEPROM固件恢复实战

ESP32修复USB4000光谱仪变砖:EEPROM固件恢复实战

1. 项目背景与核心需求拆解1.1 一台“变砖”的光谱仪到底意味着什么手里有一台USB4000光谱仪,插上电脑之后系统毫无反应,设备管理器里连未知设备都不弹出来,这种状态在硬件圈子里通常被叫做“变砖”。光谱仪这类精密光学仪器,内部…

2026/10/15 16:47:54 阅读更多 →
905 nm激光测距模块UART驱动与ROS 2集成实战

905 nm激光测距模块UART驱动与ROS 2集成实战

1. 从一束不可见光说起:905 nm 测距模块到底在解决什么问题如果你玩过扫地机器人、做过无人机定高、或者给机械臂加过避障,大概率绕不开一个东西:激光测距模块。市面上常见的方案有超声波、红外三角测距、ToF 摄像头,还有一类就是…

2026/10/15 16:47:54 阅读更多 →
C# WinForms接口调试工具:HttpClient+SQLite工程化实践

C# WinForms接口调试工具:HttpClient+SQLite工程化实践

简介:这是一款基于C#开发的轻量级Windows桌面接口测试工具,面向.NET初学者、Web API开发者及测试人员,用于快速验证HTTP接口功能与响应行为。工具采用WinForm框架构建图形界面,原生支持GET、POST、PUT、DELETE四种标准请求方法&am…

2026/10/15 16:47:54 阅读更多 →

最新新闻

herdr-gpui Teleport 深度解析:带着未提交改动、运行中进程和 Agent 会话跨主机迁移工作树

herdr-gpui Teleport 深度解析:带着未提交改动、运行中进程和 Agent 会话跨主机迁移工作树

【免费下载链接】herdr-gpui Native client for Herdr on macOS, Linux, and Windows, built with Rust and GPUI. View terminal sessions, workspaces, Git worktrees, and agent activity through your local Herdr daemon. 项目地址: https://gitcode.com/gh_mi…

2026/10/15 17:31:51 阅读更多 →
共享儿女填补孤独缺口,却替代不了子女的那通电话

共享儿女填补孤独缺口,却替代不了子女的那通电话

(知潮网)它能递给老人一把伞,却补不上子女那通电话——共享陪伴是温情,还是被明码标价的生意? 晚上七点,很多家庭的厨房里,菜凉了,筷子摆了两双,最后一双始终没动过。不是…

2026/10/15 17:31:51 阅读更多 →
Aspose.Words.Cpp 18.11:无Office依赖的C++原生Word文档处理库

Aspose.Words.Cpp 18.11:无Office依赖的C++原生Word文档处理库

简介:Aspose.Words.Cpp_18.11.zip 是面向C开发者的专业级Word文档处理SDK资源包,适用于需在无Office依赖环境下实现文档自动化生成、编辑、格式转换与批量邮件合并的中高级开发场景。资源共1150个文件,涵盖1082个头文件(h&#xf…

2026/10/15 17:31:51 阅读更多 →
Flutter时间解析避坑指南:DateTime.parse与millisecondsSinceEpoch本质解析

Flutter时间解析避坑指南:DateTime.parse与millisecondsSinceEpoch本质解析

1. 为什么看似简单的字符串转时间,却让 Flutter 开发者反复栽跟头?在某跨平台系统重构项目中,我接手了一个已上线半年的订单时间展示模块。用户反馈“部分订单时间显示为 1970-01-01”,后台日志里却明确记录着2023-08-15T14:22:37…

2026/10/15 17:31:51 阅读更多 →
Matlab概率分布与分位数实战:从选型到异常值检测

Matlab概率分布与分位数实战:从选型到异常值检测

1. 从"这组数据像什么形状"开始聊概率分布做数据分析的人都有一个共同的体会:拿到一组数据,第一件事往往不是算均值方差,而是先画个直方图看看它长什么样。这个"长什么样",在统计学里有个正式的名字&#xff…

2026/10/15 17:31:51 阅读更多 →
C++运算符避坑指南:从整数除法到短路求值,这些细节你注意了吗?

C++运算符避坑指南:从整数除法到短路求值,这些细节你注意了吗?

1. 为什么运算符值得单独写一篇笔记 刚学 C 那会儿,我总觉得运算符这东西太基础了,加减乘除谁不会啊,翻书的时候都是直接跳过的。直到后来在项目里踩了几个不大不小的坑——比如整数除法把小数部分悄悄吃掉导致金额计算差了几分钱、自增运算符…

2026/10/15 17:30:51 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 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/15 0:14:36 阅读更多 →
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/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练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/15 9:49:14 阅读更多 →