起因
想让用户每次存钱都有丢硬币进玻璃罐的爽感,结果光调物理参数就花了两周。
我在做一个储蓄 App「聚沙攒钱」,核心交互就一个:点击存钱按钮后,硬币从罐口掉进去,互相碰撞、弹开、最终堆叠在罐底。存得越多,罐子越满。
听起来简单,实际上从"能跑"到"看着舒服"之间的距离,比我预想的远得多。
金额到硬币数的映射
第一个要解决的问题:存 10 块掉几个硬币?存 1000 块掉几个?
线性映射肯定不行——存 1000 元掉 1000 个,直接卡死。我用了对数映射,存 10 元掉 3 个,存 100 元掉 6 个,存 1000 元掉 10 个,上限卡在 15。
func coinCount(for amount: Int64) -> Int {
let base = log2(Double(max(amount, 1))) * 1.5
return min(max(Int(base), 1), 15)
}
func spawnCoins(amount: Int64, jarWidth: CGFloat) {
let count = coinCount(for: amount)
for i in 0..<count {
let coin = createCoinNode(amount: amount)
let randomX = CGFloat.random(in: -jarWidth * 0.35...jarWidth * 0.35)
coin.position = CGPoint(x: frame.midX + randomX, y: frame.maxY + CGFloat(i) * 20)
coin.run(.sequence([.wait(forDuration: Double(i) * 0.08), .unhide()]))
coin.isHidden = true
addChild(coin)
}
}
每个硬币初始 Y 递增 20pt,配合 0.08 秒延迟,视觉上是一个接一个往下掉。这个 0.08 试了很多值——0.05 太快像倒豆子,0.15 太慢没爽感。
罐子边界:Edge Loop 做物理容器
罐子是上宽下窄的弧形,用 SKPhysicsBody(edgeLoopFrom: path) 把轮廓变成静态物理边界:
func createJarBoundary() -> SKNode {
let jar = SKNode()
let path = CGMutablePath()
path.move(to: CGPoint(x: -90, y: 0))
path.addLine(to: CGPoint(x: 90, y: 0))
path.addQuadCurve(to: CGPoint(x: 110, y: 300), control: CGPoint(x: 105, y: 150))
path.addLine(to: CGPoint(x: -110, y: 300))
path.addQuadCurve(to: CGPoint(x: -90, y: 0), control: CGPoint(x: -105, y: 150))
jar.physicsBody = SKPhysicsBody(edgeLoopFrom: path)
jar.physicsBody?.friction = 0.6
jar.physicsBody?.restitution = 0.1 // 低弹性,不然硬币撞壁像打弹珠
return jar
}
踩过一个坑:路径不闭合的话硬币会从缝隙漏出去,debug 了半天才发现是 path 最后没有连回起点。
硬币物理属性:调了两周的那些数
说实话最耗时间的就是调硬币本身的参数。几个关键值:
restitution = 0.3:弹性系数。设成 0.5 硬币弹跳太剧烈,看着不像金属;设成 0.1 又像黏土直接粘住了。linearDamping = 0.3:空气阻力。没有阻尼的话硬币落下去弹跳好几秒才静止,用户等不了这么久。加到 0.3 大概 1.5 秒就能稳定。density = 1.2:密度稍微调高,硬币落下去有"沉甸甸"的感觉,而不是飘。
这三个值的组合我大概试了二三十种,最后靠录屏对比才定下来。
SpriteView 嵌入 SwiftUI 的三个坑
这部分网上讨论很少,但对做工具类 App 的人来说挺实用。
ScrollView 里 SpriteView 疯狂重绘。 我的主界面是 ScrollView,存钱罐在顶部。每次滑列表,SpriteView 的 update 被频繁调用,CPU 飙到 40%。解法是动画结束后设 .isPaused = true,只在硬币掉落期间激活物理模拟。
frame 变化导致场景重建。 SwiftUI 在键盘弹起、横竖屏切换时重算布局,SpriteView 每次 frame 变化都重建 SKScene。我用 @StateObject 持有 scene 实例 + .ignoresSafeArea() 固定 frame,才绕过去。
内存问题最要命。 SpriteKit 纹理缓存不自动释放,存 50 次后罐子里几百个节点,内存涨到 200MB+。我的处理是设 80 个节点上限,超过后移除最底层的硬币,替换成一张静态"已堆满"背景图。用户看到的还是满罐,但实际参与物理模拟的只有最近掉落的那些。
关于节点数量管理
这其实是我现在还没完全满意的地方。现在的方案是粗暴地"移除旧节点+静态图兜底",但切换回来时偶尔能看到一帧闪烁。我在想是不是应该用对象池来管理——硬币节点预创建好,掉落时从池子取,稳定后回收到池底但不销毁,只是设为不可见。
你们处理这种大量同类节点的场景时,一般是用对象池还是直接 removeFromParent?有没有更好的方案?