You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

求助:基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 09:18:50