求助:基于SceneKit实现固定点拖拽旋转带阻尼减速的转轮功能
嘿,我之前刚好在SceneKit里做过类似的转轮拖拽旋转功能,从SpriteKit转过来确实得调整下思路,给你分享下具体的实现步骤和代码,应该能直接用上!
一、先给转轮节点做好基础设置(推荐用物理引擎)
首先得给你的转轮SCNNode配置合适的物理体,这样能轻松实现动量保持和自动减速,比自己写动画省心多了。核心是锁定转轮的位置,只允许它绕固定轴旋转:
// 假设你已经创建了转轮的几何体,比如圆柱 let wheelNode = SCNNode(geometry: SCNCylinder(radius: 1.0, height: 0.1)) // 创建物理体,设置为kinematic(拖拽时我们手动控制,松开后转成dynamic让物理引擎接管) wheelNode.physicsBody = SCNPhysicsBody( type: .kinematic, shape: SCNPhysicsShape(geometry: wheelNode.geometry!) ) // 只允许绕Y轴旋转(根据你的场景换成X/Z轴) wheelNode.physicsBody?.lockedAxes = [.x, .z] // 控制减速快慢的参数,值越大停得越快,0的话会一直转 wheelNode.physicsBody?.angularDamping = 0.6
二、处理拖拽手势:计算旋转角度
接下来给你的SCNView添加UIPanGestureRecognizer,关键是把屏幕触摸点转换成转轮平面上的角度差,同时记录拖拽时的角速度,松开后传给物理引擎:
var lastTouchAngle: CGFloat = 0 var lastRotationTimestamp: TimeInterval = 0 var currentAngularVelocity: CGFloat = 0 // 给SCNView添加手势 let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handleWheelPan(_:))) sceneView.addGestureRecognizer(panGesture) @objc private func handleWheelPan(_ gesture: UIPanGestureRecognizer) { guard let sceneView = gesture.view as? SCNView else { return } let touchPos = gesture.location(in: sceneView) // 碰撞测试,确保触摸的是转轮节点 let hitResults = sceneView.hitTest(touchPos, options: [.boundingBoxOnly: true]) guard let hitResult = hitResults.first, hitResult.node == wheelNode else { return } // 把转轮中心转换成屏幕坐标,用来计算触摸点相对中心的位置 let wheelCenter3D = sceneView.projectPoint(wheelNode.position) let wheelCenter2D = CGPoint( x: CGFloat(wheelCenter3D.x), y: CGFloat(wheelCenter3D.y) ) // 计算触摸点相对转轮中心的向量,再转成角度 let touchOffset = CGPoint( x: touchPos.x - wheelCenter2D.x, y: touchPos.y - wheelCenter2D.y ) let currentAngle = atan2(touchOffset.y, touchOffset.x) switch gesture.state { case .began: // 拖拽开始时记录初始角度和时间 lastTouchAngle = currentAngle lastRotationTimestamp = CACurrentMediaTime() currentAngularVelocity = 0 // 确保物理体是kinematic,我们手动控制旋转 wheelNode.physicsBody?.isKinematic = true case .changed: // 计算角度差,更新转轮旋转 let angleDelta = currentAngle - lastTouchAngle wheelNode.eulerAngles.y += Float(angleDelta) // 计算当前角速度(弧度/秒) let now = CACurrentMediaTime() let timeDelta = now - lastRotationTimestamp currentAngularVelocity = angleDelta / CGFloat(timeDelta) // 更新记录值 lastTouchAngle = currentAngle lastRotationTimestamp = now case .ended, .cancelled: // 松开后,让物理引擎接管,设置初始角速度 wheelNode.physicsBody?.isKinematic = false wheelNode.physicsBody?.angularVelocity = SCNVector4(0, 1, 0, Float(currentAngularVelocity)) default: break } }
三、不用物理引擎的替代方案
如果你不想依赖物理引擎,也可以用CADisplayLink自己实现减速动画,原理是每帧按比例降低角速度,直到速度趋近于0:
private var decelerationLink: CADisplayLink? private var ongoingVelocity: CGFloat = 0 // 松开手势时调用这个方法 private func startDeceleration(initialVelocity: CGFloat) { ongoingVelocity = initialVelocity // 移除之前的displayLink(防止重复) decelerationLink?.invalidate() decelerationLink = CADisplayLink(target: self, selector: #selector(updateDeceleratingRotation)) decelerationLink?.add(to: .main, forMode: .common) } @objc private func updateDeceleratingRotation() { // 每帧衰减速度,0.95是衰减系数,可调整 ongoingVelocity *= 0.95 // 更新转轮旋转(0.016是大概的每帧时间,接近60fps的间隔) wheelNode.eulerAngles.y += Float(ongoingVelocity * 0.016) // 当速度足够小时停止动画 if abs(ongoingVelocity) < 0.01 { decelerationLink?.invalidate() decelerationLink = nil } }
几个关键注意点
- 旋转轴要和你的场景匹配:如果转轮是水平旋转,就把锁定轴改成
.y, .z,旋转轴用X轴。 - 碰撞测试的选项:如果转轮有子节点,记得调整hitTest的参数,确保只检测到转轮本身,避免误触。
- 物理体的
angularDamping值可以反复调试,找到你觉得最自然的减速效果。
内容的提问来源于stack exchange,提问作者Nolan Ranolin
相关产品推荐
相关产品推荐

