Cocos Creator碰撞检测回调顺序详解:从底层机制到实战解决方案
1. 项目概述从一次“诡异”的碰撞Bug说起在Cocos Creator项目中实现一个简单的双人弹球游戏时我遇到了一个让人挠头的Bug。场景中有两个玩家控制的球体PlayerA, PlayerB和一堆障碍物方块。理想逻辑是球碰到障碍物会反弹并播放音效同时记录一次碰撞球与球相撞则触发特殊的互动效果。然而测试时发现当PlayerA同时撞上PlayerB和一个障碍物时有时音效会播放两次碰撞计数却只增加了一次甚至特殊互动效果根本没触发。反复检查碰撞分组、回调函数代码都看似无误。最终经过大半天的逐帧调试问题根源锁定在了碰撞检测的回调顺序上——Cocos引擎内部处理多个碰撞体在同一帧内发生接触时其回调的触发顺序并非我们直觉认为的“同时”或“随机”而是遵循着一套明确的、但文档中并未着重强调的规则。不理解这套规则你的碰撞事件处理逻辑就可能埋下难以察觉的定时炸弹。“Cocos引擎碰撞检测回调顺序详解”这个主题正是为了彻底解决这类问题。它不仅仅是API调用更是深入引擎物理系统内部运作机制的关键。无论是制作复杂的物理谜题、严谨的ARPG战斗受击判定还是包含大量交互物体的模拟游戏对碰撞回调顺序的掌控程度直接决定了游戏逻辑的稳定性和可预测性。本文将带你穿透表象从引擎源码设计思路出发结合大量实测案例完整拆解Cocos Creator中多个碰撞体交互时的回调顺序逻辑并给出针对不同场景的最佳实践方案让你从此对碰撞事件了如指掌。2. 核心机制深度解析回调顺序的底层逻辑要理解回调顺序必须先抛弃“碰撞是瞬间事件”的简单想法。在Cocos Creator以基于Box2D的物理引擎为例中碰撞处理是一个持续、分步骤的流程。每一帧物理步进Physics Step中引擎主要完成以下工作碰撞检测Broad-phase Narrow-phase快速找出所有可能发生碰撞的碰撞体对Pair然后进行精确的几何相交测试。接触点计算为每个发生相交的碰撞体对计算具体的接触点信息。接触流Contact Stream生成与处理这是决定回调顺序的核心环节。引擎会为每一对碰撞体维护一个“接触状态机”状态包括开始接触onBeginContact、持续接触onStayContact、结束接触onEndContact。回调分发根据更新后的接触状态向脚本组件分派相应的回调函数。问题的关键在于第3步当多个碰撞事件在同一帧内被检测到时它们被添加到待处理队列的顺序以及引擎遍历这个队列的方式决定了回调的触发顺序。2.1 决定顺序的核心因素根据对引擎行为的分析和测试影响回调顺序的主要因素有以下几点其优先级通常如下2.1.1 节点在场景树中的顺序渲染顺序的同源影响这是最稳定、最可预测的因素。物理引擎内部在处理碰撞对时其顺序常与场景中节点的遍历顺序存在间接关联。虽然物理引擎独立运作但碰撞体组件依附于节点节点创建、初始化的顺序会影响其在内部数据结构中的索引进而可能影响碰撞对的处理顺序。一个简单的验证方法是创建两个静止的碰撞体A和B让一个运动体C同时撞向它们。通过交换A和B节点在场景层级管理器中的上下位置你可能会观察到onBeginContact回调的触发顺序发生改变。2.1.2 碰撞体的唯一ID或创建顺序引擎内部为每个碰撞体分配了一个唯一标识符如UUID或在物理世界中的索引。这个ID通常与碰撞体组件的实例创建顺序有关。当引擎组织碰撞对列表时可能会依据这些ID进行某种排序如升序以确保跨帧处理的一致性。这意味着先创建的碰撞体可能在后创建的碰撞体之前被处理。2.1.3 物理世界的更新阶段onBeginContact和onEndContact通常在物理步进的特定阶段被集中处理。而onStayContact则是在持续接触的每一帧都会触发。重要的是对于同一对碰撞体onBeginContact总是发生在任何onStayContact之前对于该帧新建立的接触。但对于不同碰撞体之间的onBeginContact其相对顺序则受上述因素影响。2.1.4 碰撞分组与过滤碰撞能否发生首先由分组掩码决定。但请注意分组过滤发生在碰撞检测阶段它决定了哪些碰撞对需要被进一步处理。一旦通过过滤这些碰撞对进入后续流程其回调顺序就不再受分组影响了。分组影响的是“有没有”而不是“谁先谁后”。重要提示引擎并未在官方文档中严格担保一个跨版本不变的、精确的回调顺序。因此最健壮的程序设计不应依赖于一个可能变化的顺序。但理解其潜在规律对于调试和设计容错逻辑至关重要。2.2 一个典型的回调顺序场景模拟假设一帧内发生了三个新的碰撞开始事件碰撞体Player与Wall_A开始接触。碰撞体Player与Enemy开始接触。碰撞体Bullet与Wall_B开始接触。同时上一帧持续接触的Player与Floor仍在接触中。引擎在这一帧可能注意是“可能”基于常见模式按如下顺序处理回调Player-Floor:onStayContact持续接触优先处理或按原有顺序处理Player-Wall_A:onBeginContact假设Wall_A节点顺序或ID先于EnemyPlayer-Enemy:onBeginContactBullet-Wall_B:onBeginContact这个顺序会导致一个关键问题如果Player与Enemy的碰撞回调中脚本逻辑将Player销毁或移除了碰撞体那么对于同一帧内稍后处理的Player与Wall_A的碰撞回调可能会因为Player的碰撞体已失效而引发异常或逻辑错误。3. 多碰撞体事件处理的常见陷阱与应对策略基于上述机制我们在编写碰撞回调函数时会遇到几个典型的陷阱。3.1 陷阱一在回调中修改碰撞体状态导致的后续回调异常这是最危险的陷阱。正如前面例子所述在onBeginContact中销毁节点、移除碰撞组件、或大幅改变物理属性如类型从动态改为静态可能会影响同一帧内其他尚未执行的回调。解决方案延迟处理将可能影响碰撞体状态的操作推迟到当前物理帧之外执行。// 在组件中定义一个数组用于存储待处理的碰撞事件 private _pendingCollisionEvents: Array{type: string, other: Collider2D} []; onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D) { // 不立即处理仅记录事件 this._pendingCollisionEvents.push({type: begin, other: otherCollider}); } update(dt: number) { // 在update中即物理帧之后处理所有累积的事件 if (this._pendingCollisionEvents.length 0) { for (let event of this._pendingCollisionEvents) { this.processCollisionEvent(event); } this._pendingCollisionEvents []; // 清空队列 } } private processCollisionEvent(event: {type: string, other: Collider2D}) { if (event.type begin) { // 在这里安全地执行销毁、状态变更等操作 if (event.other.group Enemy) { this.node.destroy(); // 此时销毁不会影响同一帧的其他碰撞回调 } if (event.other.group Coin) { event.other.node.destroy(); this.addScore(); } } }3.2 陷阱二依赖不可控的回调顺序实现游戏逻辑例如希望玩家先碰到“护盾”道具消除碰撞体再碰到“尖刺”时免受伤害。如果顺序反过来玩家就会先受伤再获得护盾这显然是Bug。解决方案使用状态标志与帧内优先级判断不在回调中立即执行核心逻辑而是收集信息在帧末根据游戏规则而非引擎顺序进行裁决。// PlayerCtrl.ts private _collidedWithShieldThisFrame: boolean false; private _collidedWithSpikeThisFrame: boolean false; private _spikeCollider: Collider2D | null null; onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D) { if (otherCollider.group Shield) { this._collidedWithShieldThisFrame true; // 可以立即隐藏护盾表现但逻辑判断延后 otherCollider.node.active false; } if (otherCollider.group Spike) { this._collidedWithSpikeThisFrame true; this._spikeCollider otherCollider; } } lateUpdate(dt: number) { // 在lateUpdate中所有碰撞回调均已执行完毕 if (this._collidedWithSpikeThisFrame) { if (!this._collidedWithShieldThisFrame) { // 没有护盾受到伤害 this.takeDamage(); // 处理尖刺效果例如击退 if (this._spikeCollider) { this.applyKnockback(this._spikeCollider); } } else { // 有护盾免疫伤害但可以触发其他效果如护盾破碎音效 this.playShieldBreakEffect(); } // 重置标志 this._collidedWithShieldThisFrame false; this._collidedWithSpikeThisFrame false; this._spikeCollider null; } else if (this._collidedWithShieldThisFrame) { // 仅碰到护盾的情况 this.acquireShield(); this._collidedWithShieldThisFrame false; } }3.3 陷阱三onStayContact的频繁调用与性能消耗onStayContact每帧都会触发如果在此回调中执行复杂的计算或频繁的查找、创建操作会带来巨大的性能压力。解决方案节流处理与状态缓存节流不是每一帧onStayContact都需要处理业务。可以设置一个计时器或帧计数器。private _stayProcessCounter: number 0; private readonly STAY_PROCESS_INTERVAL: number 3; // 每3帧处理一次 onStayContact(selfCollider: Collider2D, otherCollider: Collider2D) { this._stayProcessCounter; if (this._stayProcessCounter this.STAY_PROCESS_INTERVAL) { this._stayProcessCounter 0; // 执行实际的持续接触逻辑如持续扣血 this.applyContinuousDamage(otherCollider); } }缓存在onBeginContact中建立联系在onStayContact中直接使用缓存数据避免重复查找。private _contactMap: MapCollider2D, ContactInfo new Map(); onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D) { this._contactMap.set(otherCollider, { node: otherCollider.node, startTime: Date.now(), // 其他初始信息 }); } onStayContact(selfCollider: Collider2D, otherCollider: Collider2D) { const info this._contactMap.get(otherCollider); if (info) { // 使用缓存的信息进行处理效率更高 this.processOngoingContact(info); } } onEndContact(selfCollider: Collider2D, otherCollider: Collider2D) { this._contactMap.delete(otherCollider); }4. 高级实践构建一个健壮的碰撞事件管理系统对于中大型项目散落在各个组件中的碰撞回调会变得难以维护。我们可以设计一个中心化的碰撞事件管理系统统一处理顺序问题和事件分发。4.1 系统设计思路事件队列所有onBeginContact、onStayContact、onEndContact回调不再直接处理业务而是将事件数据碰撞双方、类型、时间戳推入一个全局管理器的队列中。帧末处理在lateUpdate或一个专门的系统更新阶段管理器按自定义规则处理队列中的所有事件。规则引擎管理器内部可以定义优先级规则例如“治疗”优先于“伤害”“触发器”优先于“固体”基于碰撞体的自定义标签或分组来决定处理顺序从而覆盖引擎的底层顺序。事件分发按照确定性的顺序处理完事件后再将结果分发给注册了监听的游戏实体或系统。4.2 简化版管理器实现示例// CollisionEventManager.ts import { _decorator, Component, Collider2D } from cc; type CollisionEvent { type: begin | stay | end; colliderA: Collider2D; colliderB: Collider2D; timestamp: number; }; export class CollisionEventManager extends Component { public static instance: CollisionEventManager null!; private _eventQueue: CollisionEvent[] []; private _listeners: Mapstring, Function[] new Map(); // key: eventKey, value: callback[] onLoad() { if (CollisionEventManager.instance) { this.node.destroy(); return; } CollisionEventManager.instance this; } // 由碰撞体组件调用报告事件 public reportEvent(type: begin | stay | end, colliderA: Collider2D, colliderB: Collider2D) { this._eventQueue.push({ type, colliderA, colliderB, timestamp: Date.now() }); } // 在lateUpdate中处理队列 lateUpdate(dt: number) { if (this._eventQueue.length 0) return; // 1. 排序可以在这里插入自定义排序逻辑例如按分组优先级排序 this._eventQueue.sort((a, b) { // 示例让“PowerUp”分组的事件优先处理 const getPriority (collider: Collider2D) { if (collider.group PowerUp) return 1; if (collider.group Enemy) return 2; return 3; }; const priA Math.min(getPriority(a.colliderA), getPriority(a.colliderB)); const priB Math.min(getPriority(b.colliderA), getPriority(b.colliderB)); return priA - priB; }); // 2. 处理并分发 for (const event of this._eventQueue) { this.processSingleEvent(event); } // 3. 清空队列 this._eventQueue.length 0; } private processSingleEvent(event: CollisionEvent) { // 生成唯一的事件键例如 begin:A_UUID:B_UUID const eventKey ${event.type}:${event.colliderA.uuid}:${event.colliderB.uuid}; const callbacks this._listeners.get(eventKey); if (callbacks) { for (const cb of callbacks) { cb(event); } } // 也可以广播更通用的事件 this.node.emit(collision-${event.type}, event); } // 供其他系统注册监听 public registerListener(eventKey: string, callback: Function) { if (!this._listeners.has(eventKey)) { this._listeners.set(eventKey, []); } this._listeners.get(eventKey)!.push(callback); } public unregisterListener(eventKey: string, callback: Function) { const callbacks this._listeners.get(eventKey); if (callbacks) { const index callbacks.indexOf(callback); if (index -1) callbacks.splice(index, 1); } } }在碰撞体组件中的使用方式// MyCollider.ts onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D) { // 不再直接处理逻辑而是报告给管理器 CollisionEventManager.instance?.reportEvent(begin, selfCollider, otherCollider); } // ... 同理 onStayContact, onEndContact在逻辑系统中监听处理// PlayerLogic.ts onLoad() { // 监听与任何敌人开始的碰撞 CollisionEventManager.instance?.registerListener(begin:*:Enemy, (event) { // 检查event.colliderA或colliderB哪个是自己 if (event.colliderA.node this.node) { this.handleEnemyCollision(event.colliderB); } }); }这个系统将碰撞的“检测”与“响应”解耦给了我们完全掌控处理顺序的能力并且使碰撞逻辑更易于管理和调试。5. 调试技巧与性能优化指南5.1 可视化调试回调顺序在开发阶段我们可以通过简单的绘图或日志来直观观察回调顺序。onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D) { const debugLabel this.node.getComponent(Label); if (debugLabel) { // 在节点上显示最后碰撞的对象和时间 debugLabel.string Hit: ${otherCollider.node.name} at ${Date.now() % 10000}; } // 或者在控制台输出带时间戳和层级信息的日志 console.log([${this.node.name}] BEGIN with ${otherCollider.node.name}, Parent: ${this.node.parent?.name},, SiblingIdx: ${this.node.getSiblingIndex()}); } // 使用不同的颜色区分不同帧的碰撞 private _debugColor: Color Color.WHITE; onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D) { this._debugColor this._debugColor Color.RED ? Color.GREEN : Color.RED; const comp this.getComponent(Sprite); if (comp) comp.color this._debugColor; }5.2 性能优化要点减少回调中的开销避免在碰撞回调中进行getComponent、find、实例化对象、发射大量事件等耗时操作。尽量只设置标志位或推送数据到队列。合理使用onStayContact如前所述对onStayContact进行节流。对于不需要每帧检测的持续接触如角色站在地面上可以考虑用onBeginContact设置状态用onEndContact清除状态而不是依赖onStayContact。简化碰撞体形状复杂多边形碰撞体的检测开销远大于矩形Box或圆形Circle。在满足需求的前提下使用最简单的形状。动态管理碰撞组对于远处或不需要交互的物体可以动态修改其碰撞分组使其不与特定对象发生检测从根本上减少回调触发。例如屏幕外的敌人可以暂时设为与子弹不碰撞的分组。池化与复用频繁创建/销毁的物体如子弹、特效使用对象池。对象池中的物体复用时要特别注意清理其上一轮碰撞残留的状态和数据避免旧数据干扰新的碰撞事件。掌握Cocos引擎碰撞检测的回调顺序本质上是理解引擎物理系统的工作流程并与之合作而非对抗。通过采用延迟处理、状态管理、中心化事件系统等策略我们可以构建出逻辑严谨、性能高效且易于维护的碰撞交互代码。记住永远不要假设回调的顺序而是设计能够适应任何顺序的健壮系统。

相关新闻

字级控制与毫秒级停顿:打造自然语音合成的核心技术解析

字级控制与毫秒级停顿:打造自然语音合成的核心技术解析

1. 项目概述:为什么我们需要“真人感”语音合成?如果你最近用过手机里的语音助手、有声书App,或者刷到过一些AI配音的短视频,可能会隐约觉得哪里不对劲。声音是清晰流畅的,但总感觉少了点“人味儿”,像是一…

2026/9/18 3:39:28 阅读更多 →
Cocos Creator引擎配置与构建发布全解析:从原理到实战优化

Cocos Creator引擎配置与构建发布全解析:从原理到实战优化

1. 项目概述:为什么引擎配置是项目的“地基”做Cocos Creator项目,尤其是准备发布到不同平台时,很多开发者会一头扎进功能逻辑的实现里,直到打包时才发现各种问题:安卓上UI错位、微信小游戏包体超限、Web端加载缓慢。这…

2026/10/9 0:38:38 阅读更多 →
Handy离线语音转文字终极安装指南:深入解决5大核心技术难题

Handy离线语音转文字终极安装指南:深入解决5大核心技术难题

Handy离线语音转文字终极安装指南:深入解决5大核心技术难题 【免费下载链接】Handy A free, open source, and extensible speech-to-text application that works completely offline. 项目地址: https://gitcode.com/GitHub_Trending/handy11/Handy Handy是…

2026/10/9 22:40:39 阅读更多 →

最新新闻

如何安全调整Meta广告预算:meta-ads-mcp 预算管理与预算排期工具清单

如何安全调整Meta广告预算:meta-ads-mcp 预算管理与预算排期工具清单

【免费下载链接】meta-ads-mcp Meta Ads (Facebook/Instagram) MCP server for Claude, ChatGPT, Perplexity & Cursor — the Meta node of Pipeboard’s 5-platform family ( Google, TikTok, Snap, Reddit). Hosted remote MCP, badged Meta Business Partner, free pla…

2026/10/12 20:08:22 阅读更多 →
三相MMC并网逆变器:拓扑、控制与调试要点全解析

三相MMC并网逆变器:拓扑、控制与调试要点全解析

入行电力电子这几年,接触过的拓扑不算少,但三相MMC(模块化多电平换流器)并网逆变器第一次跑通仿真时,我还是忍不住想多说几句——它用一串低压子模块就叠出了中高压等级的输出波形,完全绕开了“单个管子硬扛…

2026/10/12 20:08:22 阅读更多 →
为什么高斯点要用Morton码排序?揭秘splat-transform的空间局部性技巧

为什么高斯点要用Morton码排序?揭秘splat-transform的空间局部性技巧

【免费下载链接】splat-transform CLI tool and library for 3D Gaussian splat processing and conversion 项目地址: https://gitcode.com/gh_mirrors/sp/splat-transform 点击查看 免费下载 如果你处理过大型 3D 高斯 splat 文件,大概率遇到过这样的…

2026/10/12 20:08:22 阅读更多 →
Kanidm OAuth2 Refresh Token 设计解析:NIB 机制、重放检测与复制一致性

Kanidm OAuth2 Refresh Token 设计解析:NIB 机制、重放检测与复制一致性

后端认证鉴权安全 【免费下载链接】kanidm Kanidm: A simple, secure, and fast identity management platform 项目地址: https://gitcode.com/gh_mirrors/ka/kanidm 点击查看 免费下载 本文围绕 Kanidm 身份管理平台中 OAuth2 刷新令牌(refresh token…

2026/10/12 20:08:22 阅读更多 →
Cats Free Monad 实战指南:用嵌入式 DSL 构建可组合、可重定向的纯函数式程序

Cats Free Monad 实战指南:用嵌入式 DSL 构建可组合、可重定向的纯函数式程序

后端 【免费下载链接】cats Lightweight, modular, and extensible library for functional programming. 项目地址: https://gitcode.com/gh_mirrors/ca/cats 点击查看 免费下载 本文是 Cats 函数式编程库中 cats-free 模块的完整技术指南。自由单子(F…

2026/10/12 20:08:21 阅读更多 →
可靠性数据统计方法与R语言实战:从配套数据集到Weibull建模

可靠性数据统计方法与R语言实战:从配套数据集到Weibull建模

简介:《Statistical Methods for Reliability Data》配套数据集面向可靠性建模学习者和从业者,是这本经典著作的实操补充。包内围绕寿命分布、生存分析、故障率函数、可靠性增长、多组件系统可靠性、寿命试验设计与统计推断等核心知识点,提供…

2026/10/12 20:07:21 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器: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 阅读更多 →