news 2026/9/30 11:36:30

2D技术实战全解析:从Unity碰撞检测到医学图像分割

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
2D技术实战全解析:从Unity碰撞检测到医学图像分割

最近网上流传一句很火的“纪录片式”旁白:“大型纪录片《终于是2D的了,就只冲这一点也要狠狠支持》持续为您播出!”配合那标志性的配音腔,评论区里清一色刷“狠狠支持”。

很多人把它当段子看,但我作为一个经常和 Unity、Godot、OpenCV、PyTorch 打交道的开发者,倒觉得这句话背后藏着一个挺真实的技术判断:在不少场景里,2D 就是比 3D 更高效、更稳定、更容易落地。

所以这篇笔记不打算讨论“2D 好还是 3D 好”这种没有结论的话题,而是围绕 2D 在各个技术方向上的真实用法,整理一份能直接落地的实战记录。你会看到:

  • Unity 2D 碰撞检测怎么调;
  • Godot Physics 2D 跨平台回滚为什么老“回滚不干净”;
  • 用 HTML Canvas 做一个“简单 2D 我的世界”;
  • 2D 工业缺陷检测与 2D 医学图像分割的工程思路;
  • 2D 图纸基准选取的基本规则。

每部分都有代码或配置示例,也有我在项目里见过的高频问题。

1. 从“终于是2D的了”说起:2D为什么依然能打

很多玩家看到“终于是 2D 的了”会心一笑,本质上是因为 2D 释放了一种“确定性”:画面简单、逻辑直接、操控反馈即时。对开发者来说,2D 项目同样意味着更小的包体、更简单的美术流程、更低的性能门槛。

举个最简单的例子:同样的“平台跳跃”玩法,3D 版本要考虑摄像机跟随、碰撞体旋转、物理引擎稳定性,而 2D 版本只需要处理 XY 平面内的碰撞和动画帧,调试成本明显低。

当然,2D 也并不是“简单”的代名词。2D 物理碰撞、跨平台网络同步、图像分割里的像素级精度,任何一个方向深入下去都不轻松。

也正因为如此,我决定把它拆成一篇综合笔记,相当于一次“2D 技术巡礼”。内容不追求每个方向都写到论文级别,但会把关键步骤、代码骨架、常见坑点交代清楚。

2. 2D技术基础概念再捋一遍

2.1 2D坐标系:原点到底在哪

很多初学者第一次被 2D 坐标系搞懵,是因为不同平台的原点定义不一样。

  • 数学坐标:原点在左下,X 向右,Y 向上。
  • 屏幕坐标 / Canvas:原点在左上,X 向右,Y 向下。
  • Unity 2D:使用左手坐标系,2D 场景常见 XY 平面,Z 轴用于分层;Sprite 在场景中的位置是 X、Y 坐标,Z 通常保持 0。
  • Godot 2D:同样是 XY 平面,Y 轴向下,与屏幕坐标一致,Node2D 的 position 控制位置。

这个差异直接影响了移动逻辑和碰撞检测。如果你把 Canvas 中的“向下移动”惯性思维带到 Unity 里,很容易发现物体方向反了。解决办法很简单:每次进入新引擎,先看它的坐标文档,别凭记忆写代码。

2.2 像素、精灵与图集

2D 画面里最基本的概念是 Sprite(精灵),本质是一张位图或序列帧。一个角色在 2D 世界中的运动,不过是控制它的精灵图在场景中的位置、旋转、缩放。

为了性能,2D 游戏通常会把多张小图合并到一张图集(Texture Atlas)中,运行时通过 UV 裁剪来减少 Draw Call。这个优化点在 2D 项目里非常实用,尤其是 Canvas 游戏或者 Unity 2D 项目里角色、地图、UI 混合渲染时。

2.3 碰撞、掩码与分割:别把概念搞混

在 2D 游戏里,“碰撞检测”指的是判断两个物体是否在空间中重叠,常用 AABB、圆形碰撞体;在计算机视觉里,“掩码(Mask)”指的是图像中每个像素属于哪个类别;“图像分割”则是输出这样一个像素级掩码。

同样是 2D 数据,不同任务的处理方式差别很大:

  • 游戏碰撞用几何运算;
  • 缺陷检测用特征距离或分类网络;
  • 医学分割用分割网络输出概率图。

搞清楚这些区分,后面看代码时才不会乱。

3. Unity 2D碰撞检测实战

3.1 环境与项目创建

这里以 Unity LTS 版本为例,建议使用 2021 LTS 或更高版本。具体版本要根据你本机环境和团队约定来,本文的代码是通用 API,不依赖某个特定版本。

打开 Unity Hub,新建 2D 项目。注意不是先建 3D 项目再切换模式,而是直接选择“2D Core”模板。如果你已经建了 3D 项目,也可以通过 Project Settings 里的 Editor Settings 切换默认行为,但不如直接建 2D 项目干净。

创建完成后,默认场景中有一个 Camera,层级顺序保持默认即可。

3.2 搭建最简单的碰撞场景

我们做一个“地面 + 玩家”的最小场景:

  1. 在 Hierarchy 中创建 Sprite,命名 Ground。
  2. 为 Ground 添加 Sprite Renderer,给一张白色方块或任意测试图片。
  3. 为 Ground 添加 Box Collider 2D。
  4. 调整 Ground 的位置,让它在屏幕下方。
  5. 再创建 Sprite 命名 Player,添加 Box Collider 2D、Rigidbody 2D,把 Rigidbody 的 Body Type 设为 Dynamic。
  6. 把 Player 放到 Ground 上方。

这里需要解释一下:为什么 Player 需要 Rigidbody,而 Ground 不需要?

  • 物理引擎只对带有 Rigidbody 的物体施加力、重力、碰撞响应。
  • 静态物体(Ground)只需要 Collider,不需要 Rigidbody,就能参与碰撞。
  • 两个物体碰撞时,至少有一个是动态 Rigidbody,碰撞事件才会正常触发。

如果把 Ground 也设成 Dynamic,它会受到重力往下掉,这就不是我们想要的结果了。

3.3 用 C# 实现移动与碰撞检测

下面这段是玩家控制脚本。文件路径:Assets/Scripts/PlayerController.cs

using UnityEngine; public class PlayerController : MonoBehaviour { public float moveSpeed = 5f; public float jumpForce = 8f; private Rigidbody2D rb; private bool isGrounded; void Start() { rb = GetComponent<Rigidbody2D>(); } void Update() { float move = Input.GetAxis("Horizontal"); rb.velocity = new Vector2(move * moveSpeed, rb.velocity.y); if (Input.GetButtonDown("Jump") && isGrounded) { rb.velocity = new Vector2(rb.velocity.x, jumpForce); } } private void OnCollisionEnter2D(Collision2D collision) { if (collision.gameObject.CompareTag("Ground")) { isGrounded = true; } } private void OnCollisionExit2D(Collision2D collision) { if (collision.gameObject.CompareTag("Ground")) { isGrounded = false; } } }

核心逻辑:

  • rb.velocity直接修改刚体速度,实现左右移动。
  • Input.GetAxis("Horizontal")拿到键盘 A/D 或左右方向键输入,范围是 -1 到 1。
  • isGrounded用碰撞事件维护,避免玩家在空中无限跳跃。

注意:这里跳起来时直接把 Y 速度设成jumpForce。如果你希望手感更好,可以引入加速度和摩擦系数,但最小示例没必要。

接着处理触发事件。很多游戏里收集道具、触发机关用的是 Trigger 而不是 Collision:

private void OnTriggerEnter2D(Collider2D other) { if (other.CompareTag("Collectible")) { Debug.Log("拾取道具"); Destroy(other.gameObject); } }

Collision 和 Trigger 的区别:

  • Collision:两个碰撞体都未勾选 Is Trigger,物理引擎会阻挡并产生碰撞回调。
  • Trigger:至少一个碰撞体勾选了 Is Trigger,物理引擎不阻挡,只产生触发回调。

3.4 常用配置:层与碰撞矩阵

如果发现碰撞事件一直不触发,先检查两个物体的 Layer。

在 Edit -> Project Settings -> Physics 2D 里,有一张 Layer Collision Matrix。矩阵里每一行代表一个 Layer,每一列代表与哪个 Layer 碰撞。如果 Player 和 Ground 所在层没有互相勾选,物理引擎根本不会计算它们之间的碰撞。

项目实践中建议:

  • Player 单独一层;
  • Ground 放在 Ground 层;
  • 道具放在 Collectible 层,并设置与 Player 的触发关系。

这样在复杂场景里才能快速排查“谁和谁不该碰却碰了”的问题。

3.5 运行验证

点击 Play 后,按下方向键,Player 应能左右移动;跳起后落到 Ground 上会停住;如果给道具物体编了标签、勾选 Is Trigger,走进道具区域后控制台会输出“拾取道具”。

如果 Player 直接穿过 Ground,按这个顺序检查:

  1. Ground 是否有 Collider。
  2. Player 是否有 Rigidbody2D。
  3. 两个物体所在的 Layer 是否参与 2D 碰撞矩阵。
  4. Player 的 Rigidbody2D 是不是被锁定了 Z 轴旋转或约束,导致无法正常下落。

4. Godot Physics 2D跨平台回滚问题

4.1 “回滚”是什么

在网络游戏里,“回滚(Rollback)”是一种网络同步策略。当本机收到服务器的状态更新时,发现本地预测和权威状态不一致,就回退到之前的快照,重新计算最近一段时间内的物理和输入。

这个词在格斗游戏、动作游戏里尤其常见。Godot 作为开源引擎,支持跨平台发布,Godot Physics 2D 也经常被用来做这类带网络同步的 2D 项目。

4.2 为什么 2D 物理回滚“不干净”

网上有人吐槽“Godot Physics 2D 跨平台 rollback 时回滚不干净”,实际操作中确实容易遇到。最典型的几个原因:

  1. 快照保存不完整:只保存了节点的 position 和 rotation,却没保存 PhysicsBody2D 的 linear_velocity、angular_velocity,甚至物理层的禁用状态。
  2. 物理步长不一致:不同客户端如果physics_ticks_per_second设置不同,回滚后的模拟过程会漂移。
  3. 插值干扰:Godot 2D 渲染默认可能做位置插值,回滚后视觉位置和物理位置可能不一致。
  4. 非确定性运算:例如使用了randf()、Time.get_ticks_msec(),不同平台执行结果不同,回滚后无法收敛到同一状态。

4.3 排查与修复思路

先确认你的 Godot 项目是 4.x 还是 3.x。版本差异会影响 API,但排查思路通用:

  • 统一物理频率:在 Project Settings -> Physics -> Common 里设置physics_ticks_per_second = 60,并且所有客户端保持一致。
  • 保存完整快照:至少包括 position、rotation、linear_velocity、angular_velocity。
  • 回滚后不要只改 node 坐标,还要恢复 RigidBody2D 的速度,否则下一帧物理结果依然延续旧状态。
  • 关闭或统一插值:检查是否有设置 physics interpolation,跨平台联机时建议先关闭。
  • 避免在物理回调里使用随机数。

4.4 一个简单的快照示例

下面用 GDScript 写一个“保存-恢复”的最小骨架,帮助理解需要恢复哪些状态:

func save_snapshot() -> Dictionary: return { "position": global_position, "rotation": rotation, "linear_velocity": linear_velocity, "angular_velocity": angular_velocity, } func restore_snapshot(snapshot: Dictionary) -> void: global_position = snapshot["position"] rotation = snapshot["rotation"] linear_velocity = snapshot["linear_velocity"] angular_velocity = snapshot["angular_velocity"]

这段逻辑看起来很简单,但很多回滚不干净的案例就是因为linear_velocity没有被恢复。玩家感觉“人物明明回到了之前位置,却继续往前滑”,多半就是速度没有同步恢复。

如果项目里还用了 CharacterBody2D,那么你还要额外恢复它的motion_mode、platform_on_floor之类的自定义状态;如果是 Area2D,则要恢复信号触发状态或自定义游戏逻辑字段。

5. HTML Canvas“简单2D我的世界”

5.1 需求与设计

“简单 2D 我的世界”是网上经常出现的一个标题,很多初学者用它来练手 2D 游戏开发。我们用纯 HTML + Canvas 实现一个最简版本:一个由方块组成的 2D 世界,玩家可以左右移动、跳跃。

注意:这只是一个最小可运行示例,不是完整游戏。生产环境建议用 Unity、Godot 等成熟引擎,或者至少对碰撞、动画状态机做模块化设计。

5.2 完整代码

把下面的内容保存成index.html,浏览器直接打开就能玩:

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>简单2D我的世界</title> <style> canvas { border: 2px solid #333; display: block; margin: 20px auto; background: #87CEEB; } </style> </head> <body> <canvas id="game" width="800" height="600"></canvas> <script> const canvas = document.getElementById('game'); const ctx = canvas.getContext('2d'); const TILE = 40; const COLS = 20; const ROWS = 15; // 地图:0 表示空气,1 表示草块,2 表示泥土 const map = []; for (let y = 0; y < ROWS; y++) { map[y] = []; for (let x = 0; x < COLS; x++) { if (y < 10) map[y][x] = 0; else if (y === 10) map[y][x] = 1; else map[y][x] = 2; } } const player = { x: 4 * TILE, y: 8 * TILE, width: 32, height: 48, vx: 0, vy: 0 }; const keys = {}; document.addEventListener('keydown', e => { keys[e.code] = true; }); document.addEventListener('keyup', e => { keys[e.code] = false; }); const SPEED = 4; const GRAVITY = 0.4; const JUMP = -10; function solid(tx, ty) { if (tx < 0 || tx >= COLS || ty < 0 || ty >= ROWS) return false; return map[ty][tx] !== 0; } function update() { if (keys['ArrowLeft'] || keys['KeyA']) player.vx = -SPEED; else if (keys['ArrowRight'] || keys['KeyD']) player.vx = SPEED; else player.vx = 0; // 水平碰撞 player.x += player.vx; if (solid(Math.floor(player.x / TILE), Math.floor(player.y / TILE)) || solid(Math.floor((player.x + player.width) / TILE), Math.floor(player.y / TILE))) { player.x -= player.vx; } // 垂直碰撞 + 重力 player.vy += GRAVITY; player.y += player.vy; if (solid(Math.floor(player.x / TILE), Math.floor((player.y + player.height) / TILE)) || solid(Math.floor((player.x + player.width) / TILE), Math.floor((player.y + player.height) / TILE))) { player.y -= player.vy; player.vy = 0; if (keys['Space'] || keys['ArrowUp'] || keys['KeyW']) { player.vy = JUMP; } } } function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); for (let y = 0; y < ROWS; y++) { for (let x = 0; x < COLS; x++) { if (map[y][x] === 0) continue; if (map[y][x] === 1) ctx.fillStyle = '#5cb85c'; else if (map[y][x] === 2) ctx.fillStyle = '#8B5A2B'; ctx.fillRect(x * TILE, y * TILE, TILE, TILE); ctx.strokeStyle = '#333'; ctx.strokeRect(x * TILE, y * TILE, TILE, TILE); } } ctx.fillStyle = 'red'; ctx.fillRect(player.x, player.y, player.width, player.height); } function loop() { update(); draw(); requestAnimationFrame(loop); } loop(); </script> </body> </html>

5.3 关键逻辑说明

地图用二维数组表示,这是 2D 游戏里最基础的数据结构。每个值代表一个方块类型,渲染时按数组坐标乘以 TILE 得到像素坐标。

碰撞检测采用了最直接的“逐格检测”:

  • 水平移动后,检测玩家左上角和右上角是否进入固体格子;
  • 垂直移动后,检测玩家脚底左右两格是否进入固体格子;
  • 如果进入,就把位置回退一帧的增量,并把速度清零。

这种做法的优点是逻辑直观、容易调试;缺点是没有处理快速移动导致的“隧道效应”。如果物体一帧移动距离超过一个格子,可能直接穿墙。解决思路是分步移动,或者增加连续碰撞检测。

性能方面,requestAnimationFrame(loop)提供每帧回调,draw()里每帧清空并重绘整个地图。这样在 800x600 画布下没问题;如果地图变大,建议用离屏 Canvas 先把静态地图渲染一次,然后每帧只重绘动态物体。

6. 2D工业缺陷检测方案(DINO-MX思路)

6.1 为什么缺陷检测首选 2D

工业视觉里,相机拍摄的表面图像大多都是 2D 灰度图或彩色图。无论是 PCB 板、钢板、电池表面,缺陷检测的本质都是判断图像某个局部是否“异常”。相比 3D 点云,2D 图像采集成本低、标注容易、生产环境部署也更简单。

“dinomaly 1 和 2 在 2d缺陷检测对比”指的是一类基于 DINO 特征做异常检测的方法。DINO 本身是视觉自监督学习的一种思路,用大量无标注图像学到的特征,在缺陷检测任务里迁移性很好。

6.2 方法整体流程

无论是 v1 还是 v2,这类方法的整体流程都可以拆成三步:

  1. 特征提取:用预训练 Vision Transformer 把 2D 图片切成 Patch,得到每个 Patch 的特征向量。
  2. 正常分布建模:在训练集里只放正常样本,计算正常 Patch 特征的高斯分布。
  3. 异常评分:在推理时,计算新样本 Patch 特征与正常分布的偏差,偏差越大的区域越可能是缺陷。

三步各有一堆实现细节,比如:

  • 特征层级怎么选;
  • Patch 大小怎么设置;
  • 分布建模用均值向量还是协方差矩阵;
  • 异常分数是用欧氏距离还是马氏距离;
  • 最终要不要做窗口化后处理。

具体应该以官方仓库的代码为准,不要轻信“调一个阈值就能落地”的说法。

6.3 数据准备与标注建议

2D 缺陷检测项目的核心往往不是模型,而是数据。

一个最小可用的数据集应该包含:

  • 正常样本:数量不能太少,最好覆盖光照、角度、批次差异。
  • 缺陷样本:如果走无监督路线,训练时不需要;如果走有监督分割路线,需要逐像素标注掩码。

建议目录结构如下:

dataset/ train/ good/ img_001.png img_002.png val/ good/ bad/ scratch_img_001.png scratch_mask_001.png

标注时常见问题:

  • 掩码边界不统一;
  • 同一类缺陷不同操作员的标注差异大;
  • 缺陷太小,容易被淹没在背景噪声里。

6.4 推理代码思路

下面是核心推理思路,用 PyTorch 风格的伪代码展示。具体接口需要对接你自己的模型:

# 伪代码:基于DINO特征的缺陷检测推理 import torch import torch.nn.functional as F def extract_patch_features(model, image_tensor): # image_tensor: [1, 3, H, W] # 输出: [num_patches, feature_dim] return model.forward_features(image_tensor) def mahalanobis_distance(patch_feature, mean, cov_inv): diff = patch_feature - mean return torch.sqrt(torch.einsum("...i,ij,...j->...", diff, cov_inv, diff)) def infer(model, image, normal_mean, normal_cov_inv, threshold): patches = extract_patch_features(model, image) score = mahalanobis_distance(patches, normal_mean, normal_cov_inv) anomaly_map = score.reshape(grid_h, grid_w) return (anomaly_map > threshold).float()

实际应用中还要做阈值选择。阈值不是拍脑袋定的,而是要在验证集上计算 AUROC 或 F1,找到最优值。

6.5 落地注意事项

  • 相机固定后,先做畸变校正和 ROI 裁切,减少背景干扰。
  • 光源不稳定会导致误检,建议采集不同亮度下的正常样本。
  • 不要只追求 AUROC,还要关注缺陷掩码的连通域大小,避免一个个噪点被当成缺陷。
  • 小缺陷检测要控制输入分辨率,Patch 尺寸过大会漏检。

7. 2D医学图像分割(Missformer思路)

7.1 任务背景

医学影像里,很多数据本身就是 2D 切片。CT、MRI、内窥镜图像都需要医生逐层观察,所以“对 2D 切片做像素级分类”也就是 2D 医学图像分割,是临床辅助诊断里非常重要的任务。

传统的 U-Net 用卷积编码器和解码器提取多尺度特征,效果稳定。但卷积的感受野是局部的,遇到器官边界大范围模糊时,容易漏掉远距离上下文。于是出现了 Missformer 这类基于 Transformer 的 2D 分割方法,核心思想是让模型直接建模图像块之间的长距离依赖。

7.2 模型结构思路

这类方法通常沿用一个“编码器-解码器”框架:

  • 编码器:把输入图像分割成固定大小的 Patch,拉平成 Token 序列,送入 Transformer 编码器。
  • 解码器:把编码后的 Token 特征重排成分割图,再逐步上采样恢复到原始分辨率。
  • 跳跃连接:如果是从 U-Net 演变来的结构,通常会保留编码器浅层特征,辅助解码器恢复细节。

因为医学图像尺寸通常较大,直接把整张图塞进 Transformer 会很费显存。常见做法:

  • 输入固定为 224 / 256 / 512 尺寸;
  • Patch 大小为 16 或 32;
  • 使用混合精度训练;
  • 如果原图更大,先做 ROI 裁切,再按块预测最后拼接。

7.3 预处理与数据增强

2D 医学分割任务中,预处理直接影响效果。常见操作:

  • 灰度归一化到 [0, 1] 或 z-score 标准化;
  • 重采样到统一 spacing(如果是 CT 这类物理单位敏感的影像);
  • 镜像翻转、随机旋转、随机缩放;
  • 有时加入弹性形变增强,模拟器官形态差异。

7.4 训练与评估思路

训练损失用 Dice Loss + Cross Entropy 的组合比较常见,因为医学分割里正负样本比例悬殊,单独用 Cross Entropy 容易偏向背景。

# 伪代码:训练主循环 for images, masks in dataloader: images = images.to(device) # [B, 1, H, W] masks = masks.to(device) # [B, 1, H, W] preds = model(images) # [B, C, H, W] loss = dice_loss(preds, masks) + ce_loss(preds, masks) loss.backward() optimizer.step() scheduler.step()

评估指标通常包括:

  • Dice 系数:预测掩码和真值掩码的重叠度;
  • IoU:交并比;
  • HD95:边界距离,适合评估轮廓精度;
  • 类别平均指标:多器官或多组织分割要看平均而不是只看整体。

7.5 典型坑点

  • 数据泄露:有些人把 data augmentation 写在验证集里,导致验证指标虚高。
  • 类别不平衡:某个类别只占全图的 1%,模型容易直接预测成背景。
  • 显存不够:先降低 batch size,再考虑降低输入分辨率,注意保持验证和训练时预处理一致。
  • 后处理:可以用连通域分析滤掉小噪点,但不要手工规则写得太死,否则泛化差。

8. 2D图纸基准选取原则

8.1 什么是基准

机械制图里,基准是确定被测要素或尺寸位置的起点。最常见的基准是平面、轴线、中心平面。标注时用带字母的基准符号表示,例如基准 A、基准 B。

2D 图纸基准选取,直接决定了尺寸链怎么建立、加工和测量时以哪个面作为定位依据。选得不好,会导致零件合格率低、测量争议大。

8.2 基准类型与标注位置

从性质上分:

  • 设计基准:设计时根据零件功能和装配关系确定的基准,图纸上标注尺寸的起点。
  • 工艺基准:加工时用来定位的基准,可能是夹具上的定位面。
  • 测量基准:检验时用来对准测量设备的基准。

从要素类型上分:

  • 平面基准:常用于板类零件,选底面或侧面。
  • 轴线基准:常用于轴类、孔类零件,选中心轴线。
  • 中心平面基准:对称类零件常用。

标注规则可以参照 GB/T 1182 等产品几何技术规范标准。实际制图软件中,用标注工具选择要素并命名基准字母即可。

8.3 选取原则

一条总原则:设计基准、工艺基准、测量基准尽量统一。

具体可以拆成几条:

  1. 基准要优先选择加工精度高、面积足够的表面,便于夹具定位和测量。
  2. 一个方向上的尺寸尽量使用同一个基准,避免多个基准传递误差。
  3. 基准顺序要反映装配关系,例如先选与配合件接触的面,再选定位孔轴线。
  4. 尽量选择稳定的特征,避免薄壁、毛坯面等容易变形的区域。
  5. 基准数量不是越多越好,过约束反而会让制造和检测扯皮。
场景推荐基准说明
板件上的孔位底面(设计基准)+ 一侧平面(定位基准)保证孔距和装配面一致
轴类零件两端中心孔连线作为公共轴线基准方便车加工和检测
对称壳体中心平面作为基准保证左右对称度

8.4 常见错误

  • 基准选在加工后会变形的薄壁上,测量一堆冲突。
  • 同一方向标注多个不相关基准,导致尺寸链闭环。
  • 基准字母不标或重名,图纸评审时扯皮。
  • 设计基准和工艺基准不一致,又没有尺寸换算,导致加工偏差。

这些错误在 2D 图纸评审阶段最容易暴露。建议新人画图时先对着实体模型想清楚“这个零件装在哪、怎么定位”,再下手标基准。

9. 常见问题汇总与排查清单

下面把前文涉及的高频问题整理成一张速查表:

问题现象常见原因解决思路
Unity 2D碰撞不触发Player 缺少 Rigidbody2D给移动物体加 Rigidbody2D
Unity 2D碰撞不触发碰撞层没有勾选 Layer Collision Matrix到 Physics 2D 设置里勾选对应层
Unity 2D触发回调不执行没有勾选 Is Trigger至少一个碰撞体勾选 Is Trigger
Godot跨平台回滚不干净没有恢复 RigidBody2D 速度快照里加入 linear_velocity、angular_velocity
Godot跨平台回滚不干净物理步长不一致统一 physics_ticks_per_second
Canvas游戏穿墙单帧移动超过一个格子分步移动或做连续碰撞检测
Canvas游戏卡顿每帧重绘全部地图用离屏 Canvas 缓存静态地图
2D缺陷检测误检高正常样本覆盖不够补充光照和角度变化样本
2D缺陷检测阈值不合理未在验证集上调参用 AUROC/F1 选最优阈值
2D医学分割显存不足输入尺寸过大降低输入分辨率、降低 batch size、混合精度
论文指标与复现不符预处理不一致保证训练和验证预处理完全相同
2D图纸基准争议设计基准和工艺基准不统一评审时统一基准并写明要求

这张表也可以当清单用。遇到问题,先看是哪一类,再按对应行去排查,比满世界搜报错更高效。

10. 2D项目最佳实践与工程建议

10.1 项目结构先做好

2D 项目虽然比 3D 简单,但项目结构依然重要。

以 Unity 为例:

Assets/ Scripts/ Scenes/ Sprites/ Prefabs/ Audio/

文件命名建议统一风格:

  • 脚本用 PascalCase:PlayerController.cs
  • 素材用蛇形或小写:player_idle.png
  • 场景按关卡命名:Scene_Level_01.unity

Godot 项目同理:

project.godot scenes/ scripts/ assets/

良好的结构直接降低协作成本,也让后面定位 Bug 更快。

10.2 物理参数与确定性

2D 游戏里物理调参很容易靠“感觉”改,但要注意:

  • 修改 Rigidbody2D 的 gravity scale、mass、drag 之后,记得记录变更原因。
  • 网络同步项目里,物理必须有确定性,依赖浮点计算、Time.deltaTime 的代码要谨慎。
  • 固定步长时,逻辑更新放在_physics_process()里,避免视觉帧和物理帧混用。

10.3 AI训练数据管理

2D 视觉项目里,AI 模型只占不到一半工作量,数据管理才是大头。

  • 数据集打版本:data_v1、data_v2,不要直接覆盖。
  • 记录数据来源:相机型号、拍摄时间、光照条件。
  • 标注文件定期备份,建议用平台管理。
  • 划分训练集、验证集、测试集时,要按“设备/批次”隔离,避免同一批次图像既在训练又在验证。

10.4 性能优化

2D 不等于没有性能问题。

  • 游戏里大量同屏精灵时,优先开图集和批处理。
  • Canvas 游戏优先考虑减少绘制调用,静态层用离屏 Canvas 缓存。
  • 缺陷检测推理时,如果 CPU 设备比较多,考虑 TensorRT、ONNX Runtime 转换。
  • 医学图像分割推理时,控制输入尺寸,避免无意义放大。

10.5 生产环境注意事项

涉及缺陷检测、医学图像分割这类生产应用时,安全边界要特别明确:

  • 先在小批量试运行,对比人工质检结果;
  • 模型不能替代最终判定,尤其是医疗和工业高价值场景;
  • 异常检测需要记录模型置信度、输入图像、运行环境版本;
  • 模型更新要回滚机制,保存历史模型备份;
  • 涉及图像数据的隐私合规,人员权限最小化。

这些不是漂亮口号,而是上生产时必须考虑的工程问题。

10.6 日志与可观测性

2D 视觉任务里,打印“loss: 0.3”是远远不够的。建议在项目里记录:

  • 训练参数(batch size、lr、优化器);
  • 数据版本;
  • 每个 epoch 的验证指标;
  • 推理耗时;
  • 部署环境的 SDK 版本。

有了这些,后面排查“模型在 A 机正常,在 B 机结果不同”才能找到线索。

11. 总结与下一步

这篇笔记从“终于是 2D 的了”这个梗切入,实际上是把 2D 相关方向里的几个关键问题串了一遍:Unity 碰撞检测、Godot 物理回滚、Canvas 小游戏、2D 缺陷检测、2D 医学分割、图纸基准。

如果你刚入门,建议先把 Unity 或 Godot 的 2D 最小场景跑通,再对照本文代码做扩展。如果你已经做了 2D 游戏,可以重点看网络同步回滚那部分。如果你在 AI 视觉方向,可以先从数据管理和阈值评估入手,而不是急着换模型。

下一步可以参考这些路线:

  • 2D 游戏:学完碰撞检测后,试着做一个带状态机、音效、UI 的完整小游戏。
  • 2D 视觉:跑通一个开源的缺陷检测仓库,替换成自己的数据,理解每个指标的含义。
  • 2D 制图:结合实际零件练习基准标注,多对比优秀图纸。

2D 看起来“浅”,真正深入后会发现每个方向都有一套扎实的工程方法。这篇笔记只是一个起点,关键还是打开编辑器,把地图、代码、模型跑起来。

如果本文对你有帮助,可以收藏备用,也欢迎在评论区聊聊你遇到的 2D 坑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 11:33:50

让决策贴近数据:衡石企业级 BI 的订阅触达与权限管理

企业决策通常从数据发现开始&#xff0c;再进入业务沟通、确认与行动。衡石企业级 BI 可通过订阅触达、阈值预警、应用发布、权限管理和指标管理&#xff0c;帮助企业在可控范围内分发分析结果、管理数据访问与指标资产。本文以公开产品能力为边界&#xff0c;说明这些能力适用…

作者头像 李华
网站建设 2026/9/30 11:33:37

板级适配 · 链接脚本 lds(下)①:逐段解剖各内存段

系列目录&#xff1a;本篇是板级适配系列第 4 篇&#xff08;下&#xff09;的第 1 部分。承接第 10 篇&#xff08;上&#xff09;的「256KB 公寓楼」规则&#xff0c;带你逐间房进去看——.isr_vector/.text/.data/.bss 每段怎么布置、门牌号&#xff08;VMA&#xff09;怎么…

作者头像 李华
网站建设 2026/9/30 11:28:17

合并两个有序链表:迭代、递归与原地合并的面试全攻略

1. 这道题为什么值得反复刷&#xff1a;合并有序链表的本质与常见误区 如果只让我推荐三道链表入门题&#xff0c;LeetCode 21“合并两个有序链表”一定在其中。它的题干极短&#xff1a;给定两个升序链表 list1 和 list2 &#xff0c;把它们合并成一个新的升序链表并返回。…

作者头像 李华
网站建设 2026/9/30 11:25:43

linux kernel struct 之 ptdesc

struct ptdesc 的定义在 Linux 内核的 include/linux/mm_types.h 文件中&#xff08;早期版本曾放在 include/linux/pgtable.h&#xff09;。它的设计目标是将页表元数据从 struct page 中拆分出来&#xff0c;目前通过完全覆盖&#xff08;overlay&#xff09; struct page 的…

作者头像 李华
网站建设 2026/9/30 11:25:15

C++高性能算法

多线程编程&#xff1a;C11/14/17 TL;DR 多线程优先任务并行&#xff0c;线程数约等于核数SIMD 先自动向量化&#xff0c;热点再用 intrinsics内存池适合高频小对象分配场景并发结构优先使用成熟库性能优化必须测量先行&#xff0c;再优化多线程编程&#xff1a;C11/14/17 核心…

作者头像 李华