资讯动态

用Xcode与SpriteKit从零开发Flappy Bird:物理碰撞与上架实战

发布时间:2026/10/1 10:13:58 来源:尧图企业网站定制
简介一份基于Xcode开发的Flappy Bird完整工程面向iOS开发者与游戏编程初学者示范如何借助SpriteKit框架实现2D休闲游戏的核心玩法。资源包共58个文件涵盖15张png图片素材、8个m与6个h的Objective-C源码、7个json配置、3个mp3音效以及sks粒子特效、plist属性列表、strings本地化文件等总大小仅507KB目录结构清晰便于按模块查阅。目前已有177人学习下载。工程内实现了物理引擎SKPhysicsWorld的碰撞检测、touchesBegan触摸控制、游戏循环状态更新、随机管道生成、计分与结束判定等完整逻辑同时涵盖Interface Builder界面布局与真机调试配置可直接运行参考从中还能直观理解重力、速度与碰撞体的参数调优过程。适合希望复刻经典小游戏、系统学习SpriteKit游戏开发流程的读者作为练手项目。1. 用 Xcode 把 Flappy Bird 跑起来为什么这个“过气”小游戏还是值得自己写一遍一个已经被做烂到几乎没有新意的复刻项目却是检验你有没有真正理解 SpriteKit 物理世界和 iOS 工程配置的试金石。Flappy Bird 的玩法一句话能说完点击屏幕让小鸟跳起来穿过管道间隙碰一下就死。但就是这个“碰一下就死”牵扯出重力数值、物理体形状、碰撞掩码、状态机流转、内存释放、证书打包上架这一整条 iOS 开发链路。这篇文章不教你抄一份现成代码而是带你用 Xcode 从空工程开始把每个参数为什么这么设、每个坑为什么这么踩讲透。适合两类人刚入门 iOS 开发、想做第一个完整游戏的新手以及写过一堆 UI 界面但从没碰过 SpriteKit 渲染管线的客户端工程师。2. 搭 Xcode 工程与 SpriteKit 物理世界先定重力规则再谈游戏手感2.1 新建工程用 SpriteKit Game 模板还是空模板Xcode 新建项目时有一个现成的 SpriteKit Game 模板打开就能看到一个旋转的飞船 demo里面带一个 GameScene.sks 场景文件和一个 GameScene.swift 控制器。这个模板一开始确实省事但我不推荐用它做 Flappy Bird。原因很现实sks 文件把场景内容序列化了一份而你后续所有代码都在动态生成节点两套逻辑并存反而制造麻烦。你这边在代码里 addChild 了一只鸟那边 sks 文件里还挂着一个没用的 demo 节点运行起来场景里莫名其妙多出东西排查起来非常别扭。我一般会先建一个 “App” 类型下的空工程然后手动引入 SpriteKit。这个选择让整个场景构建全部落在代码里逻辑透明调试物理体时哪里不对一眼就能看到。工程名用 FlappyBirdBundle ID 先填 com.example.FlappyBird后面上架前再改成你自己的域名反写。新建空工程后在 ViewController 里把视图切换成 SKView这是最标准的承载 SpriteKit 的方式。模板工程会自动生成 GameViewController很多教程会让你直接改 stroyboard 的 class但更可控的方式是在 viewDidLoad 里手动加载import UIKit import SpriteKit class GameViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // SKView 是 SpriteKit 渲染的入口承载所有场景 if let view self.view as? SKView { let scene GameScene(size: view.bounds.size) scene.scaleMode .resizeFill view.presentScene(scene) view.ignoresSiblingOrder true // 不按添加顺序强制渲染提高绘制效率 } } }这段代码里 scaleMode 用 .resizeFill 而不是 .aspectFill是给这个游戏定下的第一个重要选型。Flappy Bird 是固定分辨率玩法的竖屏游戏但不同 iPhone 的屏幕宽高比差异很大如果用 .aspectFill两侧画面会被裁掉小鸟碰到屏幕边缘的位置在每台设备上都不一样。用 .resizeFill 后场景尺寸跟随视图实时变化地面和管道的位置要么按比例约束要么在 didChangeSize 里重新摆后面生成管道时的随机高度就都基于 scene 的当前高度计算不会出界。ignoresSiblingOrder 设置为 true 后SpriteKit 不再保证按节点添加顺序绘制而是按 zPosition 的数值来画。这样一些被俯视的装饰层就可以完全不设 zPosition性能更好。2.2 物理世界的重力默认 -9.8 不是给手机屏幕准备的SKView 搭好之后进入 GameScene.swift 里配置物理世界。SpriteKit 没有 Editor 碰撞编辑那样的可视化物理世界编辑器一切都靠代码里的 physicsWorld 对象设置。物理世界决定了所有物理体的共同行为比如重力、速度倍率。import SpriteKit class GameScene: SKScene { override func didMove(to view: SKView) { super.didMove(to: view) // 重力是 Flappy Bird 手感的第一决定性参数 physicsWorld.gravity CGVector(dx: 0, dy: -7.5) physicsWorld.speed 1.0 view.showsPhysics true // 开发期打开所有物理体会以线框形式绘制 view.showsFPS true } }这里 gravity 用 -7.5 而不是系统默认的 -9.8是第一个要划重点的参数。SpriteKit 的物理单位虽然是真实物理单位但屏幕显示上 1 点对应 1 米的直觉是错误的。iPhone 的屏幕高约 844 个 point如果按真实重力加速度 9.8 来算物体从屏幕顶部落到地面不到一秒作为休闲游戏节奏太快玩家根本来不及做出两次点击。调到 -7.5 后下落更“绵”玩家能从视觉上预测落点这在 Flappy Bird 这种靠预判的游戏里决定生死。physicsWorld.speed 是另一个值得注意的参数。它控制的是整个物理世界的模拟倍率调低它可以让物理、碰撞、接触回调全部慢放看起来像是子弹时间。但要注意管道移动如果用 SKAction 驱动是不受 physicsWorld.speed 影响的这就导致慢放时管道还在常速移动小鸟却飘在半空玩法体验彻底割裂。我见过有人因为游戏太难直接把 physicsWorld.speed 调到 0.5 试图降低难度结果鸟和管道不同步撞管判定完全对不上这就是“玄学调参”里最典型的翻车。真想降低难度后面调跳跃高度和管道间距比调 speed 靠谱得多。2.3 画一只鸟物理体形状决定手感上限物理世界准备好后接下来是游戏主角。看起来只是一个圆形的鸟但节点头、物理体形状和材质参数这三件事决定手感上限。我习惯用 SKSpriteNode 加一张自己画的圆形贴图而不是 SKShapeNode。SKShapeNode 也可以做圆形物理体但它是对矢量路径实时栅格化的CPU 开销比普通贴图节点高将来还要给鸟加全套动画帧这套动画资源最终还是要走贴图纹理路线。物理体则用 SKPhysicsBody 的 circleOfRadius 方法生成。let hero SKSpriteNode(imageNamed: bird) hero.size CGSize(width: 68, height: 48) hero.position CGPoint(x: frame.width * 0.3, y: frame.height * 0.6) hero.zPosition 10 let body SKPhysicsBody(circleOfRadius: hero.size.width * 0.24) body.isDynamic true body.categoryBitMask 0x1 0 body.collisionBitMask 0x1 1 body.contactTestBitMask 0x1 2 body.affectedByGravity true body.restitution 0.0 // 撞到地面直接判定失败不要弹性 hero.physicsBody body hero.name hero addChild(hero)这里 radius 用 hero.size.width * 0.24 而不是直接取一半 24是我调试了很多次得出的经验值。视觉上鸟有一圈羽毛透明边真实可碰撞区域如果按整个图片宽度的 0.5 来算鸟还没碰到管道边缘系统就会判定碰撞玩家会觉得“明明还差一大截怎么就死了”这种体验是非常劝退的。保留 4% 左右的视觉余量让碰撞体比视觉略小一圈玩家的补偿心理会舒服很多。restitution恢复系数设成 0 是必须的。鸟撞到地面后如果带弹跳你会看到小鸟在地面颠一下再滑行这在 Flappy Bird 里非常出戏。地面碰撞后应该立刻触发 GameOver物理体不再模拟后续行为这才是游戏要的反馈。2.4 地面和天空静态边界用 edge 还是 body场景里除了鸟还有地面和一组需要避开的障碍物。地面这个边界很多新手会用 SKShapeNode 加一个矩形物理体来搭但矩形 body 是有质量的它必须是动态的才能参与碰撞计算。更标准的做法是用 SKPhysicsBody 的 edge 类型从任一顶点到另一顶点拉一条无限质量的边界线这条线不参与重力模拟、不会被推走但对其他动态物体有阻挡效果。let ground SKNode() ground.position .zero let groundBody SKPhysicsBody(edgeFrom: CGPoint(x: 0, y: 90), to: CGPoint(x: frame.width, y: 90)) groundBody.isDynamic false groundBody.categoryBitMask 0x1 1 groundBody.collisionBitMask 0x0 ground.physicsBody groundBody addChild(ground)edge 类型的 body 天然适合做场景静态障碍。注意我专门加了 isDynamic false 和 collisionBitMask 0x0。edge 本身不会被碰撞推出但如果不把 collisionBitMask 清空鸟撞到地面时物理引擎仍然会做碰撞求解鸟的速度会被瞬间清零并挤压一段位移。这个挤压过程有时会让鸟在地面上抖动一小段距离看起来不够干脆。把碰撞掩码和接触掩码分开控制是这里的关键collisionBitMask 决定谁互相推挤contactTestBitMask 决定谁触发回调。我的做法是鸟和所有障碍物之间只保留 contact 检测不保留实际的碰撞推挤游戏结束后玩家看到的是鸟直接穿过管道再摔到地面视觉上比“鸟被管道挡住还挂在半空”流畅得多。这种取舍后续在碰撞章节还会细说。3. 实现核心玩法跳跃手感、管道对生成与碰撞判定3.1 点击跳跃为什么 applyImpulse 不如直接赋值 velocityFlappy Bird 的核心交互只有一个点击屏幕让鸟向上跳。iOS 里监听点击有 UITapGestureRecognizer 和 touchesBegan 两种方式SpriteKit 游戏里我一般直接重写 touchesBegan。但实现跳跃的物理方式有两种细节差距很大。第一种是用 applyImpulse 给鸟一个瞬间冲量。冲量的效果是立刻改变速度但改变多少取决于物理体的质量。而 SKPhysicsBody 的质量由 density默认 1.0乘以面积算出。圆形的 radius 改成多少、图片尺寸缩放一档都会让跳到的高度完全不一样。同一套代码换一张图鸟就从跳 400 点变成跳 200 点手感全部推翻。这才是真正的黑匣子。第二种方式是直接给 physicsBody.velocity 赋一个固定值彻底绕开质量这个变量。Flappy Bird 里跳跃就是让垂直速度变成某个固定值跟物理冲量关系不大直接操作速度是更可控的建模方式。override func touchesBegan(_ touches: SetUITouch, with event: UIEvent?) { // 状态机控制游戏结束或未开始时禁止操作 guard state .playing, let body hero.physicsBody else { return } // 直接设定垂直速度绕开质量的推算过程 body.velocity.dy 420 body.angularVelocity 0 // 跳跃时头朝上视觉能立刻响应 let rotateUp SKAction.rotate(toAngle: 0.6, duration: 0.15) rotateUp.timingMode .easeOut hero.run(rotateUp) }velocity.dy 420 意味着我们只改垂直方向速度水平方向的速度不受影响。420 这个数不是拍脑袋拍的它是在 gravity -7.5 下经过反复试错得到的。垂直速度最大时鸟能上升到的高度大约由 v² / (2 * g) 计算即 420² / (2 * 7.5) ≈ 11760 个点这个值远超屏幕高度看起来不直觉。但游戏里玩家不会一直按住不放每次点击只给瞬时速度在每次点击的间隔里鸟都会下落实际飞行轨迹是锯齿状的。420 这个初始速度让鸟每次点击后爬升约 200 点的距离正好能穿过 180 点高的管道间隙这个速度是“能过但有点紧张”的平衡点。这里还有一个小细节body.angularVelocity 0。SpriteKit 的物理体自带旋转模拟鸟受重力下落时会因为力矩而产生角速度。如果不清零鸟会在下坠过程中持续旋转失控翻到头上。每次跳跃重置角速度后再手动把 zRotation 固定到合适角度鸟的姿态就不会漂移。3.2 管道对生成一套代码生成上下两根随机高度与回收机制管道是这个游戏里唯一需要持续生成的障碍物并且必须是上下两根配套出现中间留一个可以让鸟飞过的空隙。空隙的高度是游戏难度最核心的旋钮。我取的值是 170 点在 Gravity 值 -7.5 、跳跃速度 420 的组合下这个空隙刚好够鸟垂直穿过基本没有容错空间。如果你给朋友试玩时对方喷你“怎么这么难”第一件事是把这里换到 190 到 200 之间。func spawnPipePair() { let gapCenterY CGFloat.random(in: 220...(frame.height - 380)) let gapHeight: CGFloat 170 let pipeWidth: CGFloat 62 let topPipe SKSpriteNode(imageNamed: pipe_top) topPipe.size CGSize(width: pipeWidth, height: frame.height) topPipe.position CGPoint(x: frame.width pipeWidth / 2, y: gapCenterY gapHeight / 2 topPipe.size.height / 2) topPipe.zPosition 5 topPipe.name pipe let bottomPipe SKSpriteNode(imageNamed: pipe_bottom) bottomPipe.size CGSize(width: pipeWidth, height: frame.height) bottomPipe.position CGPoint(x: frame.width pipeWidth / 2, y: gapCenterY - gapHeight / 2 - bottomPipe.size.height / 2) bottomPipe.zPosition 5 bottomPipe.name pipe let bodyForPipe SKPhysicsBody(rectangleOf: CGSize(width: pipeWidth, height: frame.height)) bodyForPipe.isDynamic false bodyForPipe.collisionBitMask 0x0 bodyForPipe.contactTestBitMask 0x1 0 topPipe.physicsBody bodyForPipe bottomPipe.physicsBody bodyForPipe addChild(topPipe) addChild(bottomPipe) }管道高度直接写 frame.height但上下位置是把整个管道体的一半挪到屏幕外边只把缺口留在屏幕可见区域。这种“超长吞边”的做法比动态计算管道长度更省代码而且不管屏幕尺寸怎么变缺口位置只由 gapCenterY 决定视觉和碰撞都稳定。随机范围 220 到 frame.height - 380 的意义是上管道的下沿和下管道的上沿都保留至少离屏幕顶部和地面 110 点的安全距离不会生成一个几乎贴着地面或几乎顶着天花板的地狱难度。管道移动我用 SKAction 完成。每根管道从屏幕右边缘生成后向左移动固定距离到屏幕左外侧再移除let moveDistance frame.width 360 let moveAction SKAction.moveBy(x: -moveDistance, y: 0, duration: 3.2) let removeAction SKAction.removeFromParent() let sequence SKAction.sequence([moveAction, removeAction]) topPipe.run(sequence) bottomPipe.run(sequence)moveDuration 我设 3.2 秒配合 screen width 约 390 点时移动速度约 120 点/秒。这个速度配合 170 点缺口单手可玩。想要更紧凑的节奏把 duration 降到 2.6 秒就行但这时管道的水平间隔也记得缩短否则屏幕上同时出现的管道数量会变多内存和碰撞回调压力变大。3.3 碰撞掩码与回调顺序这游戏的死亡判定其实很简单Flappy Bird 的碰撞判定只有两类鸟撞到管道死鸟撞到地面死。逻辑简单但掩码配置却能在新手手里翻车无数次。需要先理解 categoryBitMask、collisionBitMask、contactTestBitMask 三个概念的差异。category 是身份collision 指的是物理体之间要发生真实的物理推挤contact 则只是让引擎触发接触回调不改变任何物体的运动轨迹。我曾经见过一段被人传烂的代码给鸟的 collisionBitMask 设成了管道掩码结果鸟撞上管道后会直接挂在管壁上不再下落画面里出现一直贴在管子上的死鸟非常诡异。正确做法是鸟的 collisionBitMask 只和地面做碰撞管道只触发 contactTest 回调遇到管道后鸟穿过去但 gameOver() 已经执行。hero.physicsBody?.collisionBitMask 0x1 1 // 只与地面产生位移碰撞 hero.physicsBody?.contactTestBitMask 0x1 2 // 探测管道即可管道那侧的 physicsBody 保持 isDynamic false其 contactTestBitMask 是 0x1 0鸟的 category这样鸟经过管道时系统会自动触发 didBegin 回调。SpriteKit 的接触回调触发有明确的顺序先调用 didBegin(_ contact:)然后接触结束后调用 didEnd。如果 GameOver 时直接把场景暂停要注意 didEnd 可能不会再触发如果此刻你在 didEnd 里写逻辑做分数清零之类的操作那就会遗漏。保险做法是把所有判断都放在 didBegin 里。3.4 分数与节点管理用 childNode 取回节点而不是遍历管道上方要飘一个计分数字这个问题看着简单真做起来却有不少隐藏的性能陷阱。我见过有人用一个标签节点每一帧都手动刷新数字的 text但 SKLabelNode 的重新赋值 text 会重新触发纹理创建在低端机上掉帧非常明显。更常见的问题是管道被移除后它的得分逻辑需要同步判定。我比较推荐给每个管道对加一个子节点专门做“计分触发区”把判定点独立出来let scoreNode SKNode() scoreNode.position CGPoint(x: frame.width 150, y: 0) let scoreBody SKPhysicsBody(rectangleOf: CGSize(width: 20, height: 400)) scoreBody.isDynamic false scoreBody.contactTestBitMask 0x1 0 scoreNode.physicsBody scoreBody scoreNode.name scoreGap addChild(scoreNode)当 bird 经过 scoreGap 时didBegin 触发且 matched 的 contact body 中包含 scoreGap就执行 score 1。注意这时的物理体位置是相对管道对的中心但管道会移动scoreGap 作为子节点也会跟随着同步移动这样不用手动计算跨越位置接触回调自然会带着跨越的信息。从节点树取回时用 childNode(withName:) 就可以不要每次遍历整个场景的 children那个开销在管道数量多时会被放大成明显的 CPU 高峰。4. 从 Xcode 工程到 App Store证书配置、描述文件与打包提速4.1 证书、描述文件与 Bundle ID三个概念一对齐签名不炸本地跑通后把一个 Flappy Bird 游戏推到 App Store 全流程是很多只做过模拟器调试的开发者最没底的一段。第一步就是证书和描述文件。这个概念不搞清楚Xcode 的 Signing 报错能把人折磨疯一会说 “No signing certificate found”一会说 “App ID not found”。Apple 的签名体系里三个对象分别承担不同职责证书Certificate证明你的开发者身份Bundle Identifier 是 App 在 App Store 的唯一身份证描述文件Provisioning Profile把证书、Bundle ID 和某个 App 的能力绑定在一起。三者必须完全匹配缺一个都不行。比如确认 Bundle ID 在 Apple Developer 后台注册为 com.yourcompany.FlappyBird但 Xcode 工程里随手写成了 com.yourcompany.FlappyBird123签名校验立刻失败。对象创建方式常见错误证书(.cer)钥匙串 - 证书助理 - 从证书颁发机构请求证书上传到 Apple Developer 后台下载后安装到钥匙串换电脑后没导出 .p12 私钥钥匙串里只有公钥Xcode 报 no signing certificate foundBundle IDApple Developer 后台注册反向域名后台为 com.foo.Flappy工程里写 com.foo.FlappyBirdApp ID not found描述文件(.mobileprovision)后台选择 App ID 与证书后生成证书私钥在旧电脑新电脑装描述文件没有私钥签名总失败我给所有做过 iOS 上架的朋友的硬建议是第一次配置成功后立刻从钥匙串里把证书对应的私钥导出为 .p12 文件并让后台下载一份描述文件存档。换电脑时只需要装 .p12 和新下载的描述文件即可否则你迟早有一天会在新电脑上干瞪眼半天。开发阶段在 Xcode 里勾上 Automatically manage signingXcode 会自动创建描述文件。但真机调试时记得在手机上信任开发者证书设置里找到“设备管理”点一下信任否则安装到手机上会直接失败。4.2 归档与导出为什么 Xcode 打包 iOS 突然很慢代码写完、证书配好后最后一个环节是 Product - Archive 做归档然后导出 ipa 上传到 App Store Connect。这个环节里最影响心态的就是“Xcode 从某天开始打包突然很慢”慢到让人以为是电脑坏了。大多数情况下不是电脑坏了而是 DerivedData 积累太多。Xcode 会把编译中间产物缓存到 ~/Library/Developer/Xcode/DerivedData 下一个项目迭代几十个版本后这个目录里的无用手工缓存可能到几个 GB。每次点 Archive 时 Xcode 需要扫描这些缓存判断哪部分可以增量编译扫描过程占满磁盘 IO 就会非常慢。另外 Flappy Bird 这种 SpriteKit 游戏贴图资源往往是一整张 SKTextureAtlasAtlas 生成时会做 PNG 压缩和 GPU 格式转换这个压缩环节 CPU 会瞬间拉满看起来也像是“卡死”了。# 清理指定项目的派生数据是打包慢的第一招 rm -rf ~/Library/Developer/Xcode/DerivedData/Flappy* # 全量清理一次再归档等于给 Xcode 一个干净的编译起点 xcodebuild clean -project FlappyBird.xcodeproj -scheme FlappyBird清理后再 Archive 通常立竿见影。如果仍然慢再开一下时间的排查方向是OpenGL 的调试器被打开、Xcode 的 Views Debugging 在后台运行把 Run 和 Archive 的 scheme 里 Debug 相关的选项全关掉。但最稳定的做法是养成每周做一次 DerivedData 清理的习惯不要攒到上架前才处理。4.3 内购和广告位现在别做最小上架版只留什么Flappy Bird 想上架可能有人思考加入广告展示或内购去广告。但我的建议是第一个版本不要碰这些哪怕是 Game Center 排行榜、新手引导动画这些看起来很核心的功能也放到 v1.1 再去补。原因是 Xcode 工程里每加一项能力对应的描述文件里就会多一个 App ID 配置比如 In-App Purchase 需要到后台开启对应的 capability再来回重新生成描述文件上架审核如果功能页面不完整还会被拒。第一版只保留最核心的单机可玩、记录历史最高分、暂停按钮、重开按钮。这四个功能加起来不足 600 行代码但从零到审核通过整个流程少则五天多则两周这还是只跑一次流程的情况下。所有额外功能都在上架之后通过版本迭代再补这样你就能在一个很小的风险面上走通全链路。5. Flappy Bird 在 Xcode 里的翻车现场五个高频踩坑与排查5.1 小鸟按一下飞半年gravity 与 velocity 的比例失调现象把源码下载下来跑点一下屏幕鸟窜出屏幕外掉也掉不下来一个游戏体验瞬间崩溃。原因gravity 被设成了 -2 左右velocity 却仍然保留 500 的高数值。解决先确认 gravity 和 velocity 的比例关系。按 v² / (2 * g) 粗略估算跳跃高度在 -7.5 的重力下给 420 的速度大致爬升 120 点但如果 gravity 只有 -2同速度下爬升到 250 点以上。调试时把两个参数锁定住让鸟在固定屏幕位置反复起跳观察轨迹不要单独调一个值。5.2 柱子穿模物理体比视觉宽度窄看不见现象鸟从管道中间视觉缝隙飞过但系统判定得分时认为没有碰到有时候明明看着擦到管壁却死活不判定死亡玩家情绪开始失控。原因管道图片宽度 62 点但物理体矩形宽度写成了 30 点。这种情况最常见于从复制粘贴代码改尺寸物理体宽度没跟着图片变。解决把 view.showsPhysics 打开可以直观看到红绿线框和贴图的脱节程度然后让物理体的尺寸直接取 SKSpriteNode 的 size而不是手写常量。开发期开着 showsPhysics 不丢人上架前记得关。5.3 GameOver 后还能穿过去掩码改得太晚现象游戏已经判定失败但鸟还能继续往前飞穿过管道。原因gameOver() 只弹了结算面板但没有关闭鸟的物理体继续模拟运动。解决gameOver 里直接 hero.physicsBody nil让物理体瞬间退出引擎鸟停在半空。注意要在调用 super 之前做接触回调还在处理的时候如果先把 body 拿掉didBegin 可能拿不到 node。5.4 “you should download the command line tools for xcode 26.3”工具链坏了现象Xcode 升级或换了 beta 版本后在终端跑 xcodebuild 突然报 “You should download the Command Line Tools for Xcode 26.3”但 Xcode 明明已经装好。原因macOS 系统里 xcode-select 指向的 CommandLineTools 路径被重置了或者当前生效的是旧的独立 CommandLineTools 目录而不是 Xcode.app 内置的那套。解决先用 xcode-select -p 查看当前路径再用 sudo xcode-select --switch 切回 Xcode 内置 Developer 目录。这是一个非常典型的换完 Xcode 版本后的环境翻车比重新装 CommandLineTools 要快得多。xcode-select -p # 如果输出不是 /Applications/Xcode.app/Contents/Developer就执行下面这条 sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer5.5 模拟器上正常、真机上飘物理步进不同步现象模拟器里鸟跳得频繁流畅真机上一顿一顿的或干脆手感完全不同。原因SpriteKit 的物理模拟在低帧率下会自动降低模拟精度真机如果掉到 30 fps物理步进拉长Bird 的单次跳跃在屏幕上呈现的面和模拟器完全不一样。解决不要开低电量模式去调参物理手感要以真机为准。同时在实际设备上跑一遍时注意不要再开着模拟器的 Slow Animations 模式。这个调试开关是模拟器菜单里的 Debug - Slow Animations打开后物理世界也会慢速模拟真机上没有对应效果新手调完手感一拿到真机又炸这不叫玄学叫流程错误。6. 用 Memory Graph 做个体检顺手治掉循环引用和后台刷分Flappy Bird 这种小游戏场景复杂度低但正因为小很多人忽略内存管理。游戏里最容易出的内存问题是“重开游戏后旧场景没释放”。最常见原因是 GameOver 后有节点闭包持有了 GameScene比如加分动画的 SKAction.run 闭包里访问了 selflet wait SKAction.wait(forDuration: 0.2) let block SKAction.run { [weak self] in self?.scoreLabel.text \(self?.score ?? 0) }如果漏了 weak 修饰SKAction 的闭包持有场景场景持有节点树节点树持有 SKAction一个完美的循环引用就形成了。这也是 Memory Graph 这个调试工具的用武之地运行游戏进入 GameOver回到开始界面的那一刻点 Xcode 调试条上的 Memory Graph 图标一个圆点带三条连线等待几秒展开 GameScene 节点看有没有出现两个 GameScene 实例或者场景已经被 present 新场景后旧实例还挂在 Memory Graph 里。这种情况就是有对象持住了它。排查逻辑很简单先暂停场景中所有定时器和动作再尝试把 GameScene 的引用清零。如果调试出来 Scene 还在就从 GameScene 内部持有的对象往回逐一排查最常见的就是 SKAction.run、Timer 闭包、以及 GameOver 时自己加的重开按钮的回调闭包。重开按钮如果需要点击时访问 scene 的成员闭包里的 self 务必写成 weak否则重开的次数多了内存里堆积一打旧场景机子迟早被拖垮。另外记得在场景的 willMove(from:) 里关闭 view 的 isPaused 恢复机制。很多人在游戏切后台回来后发现鸟已经在屏幕最底部自动结束了游戏或者干脆跨过了好多管道分数暴涨。搞明白原因SpriteKit 默认在 App 进入后台时不暂停物理模拟。在 AppDelegate 的 applicationDidEnterBackground 里加上 gameScene.view?.isPaused true回前台再恢复 false这一行代码避免所有后台挂机刷分问题也防止低端机上切后台再回来的物理跳变。我后来做每个 SpriteKit 或 SceneKit 项目都会先让场景自己处理 focus 变化而不是等用户主动反馈。按这个顺序走完你会发现自己不只是在抄一个 Flappy Bird而是把从空工程到上架的整条 iOS 开发管道完整走了一遍。这套流程里最值得沉淀的不是写代码而是“每个奇怪手感背后到底哪个参数在起作用”的排查思路。以后你再碰到任何 SpriteKit 手感不对首先打开 showsPhysics 看碰撞区域再调 gravity 和 velocity 的比例这两个动作基本能解决八成问题。真心希望这篇笔记帮你在自己的工程里省下几天玄学排错时间。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑