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

如何通过UIPanGestureRecognizer持续移动SKSpriteNode?

解决SKScene中精灵随手势持续移动的问题

我来帮你搞定这个问题!首先得说清楚你当前代码的核心问题:每次手势触发.changed状态时,你只是给精灵添加了一个固定距离的moveTo动作,这就导致它只会执行一次移动,没法跟着手势全程持续动起来。下面我先给你改好UIPan的实现,再聊聊更适合游戏的摇杆替代方案。

一、改进UIPan手势实现持续移动

我们需要利用手势的速度(velocity)或者位移增量,在每次.changed触发时实时更新精灵位置,而不是用固定时长的moveTo动作。这样就能实现拖动手势全程的持续移动了:

@objc func didPan(gesture: UIPanGestureRecognizer) {
    guard let sceneView = view else { return }
    let panVelocity = gesture.velocity(in: sceneView)
    // 调整这个系数可以控制移动速度,根据你的场景需求来改
    let moveSpeed: CGFloat = 0.05

    switch gesture.state {
    case .began:
        // 手势开始时先停止之前的移动动作,避免动作冲突
        for sprite in levelSprites {
            sprite.removeAction(forKey: "backgroundMove")
        }
        // 开始播放对应方向的循环行走动画
        if panVelocity.x > abs(panVelocity.y) {
            player.run(SKAction.repeatForever(SKAction(named: "WalkRight")!), withKey: "playerWalk")
        } else if panVelocity.x < -abs(panVelocity.y) {
            player.run(SKAction.repeatForever(SKAction(named: "WalkLeft")!), withKey: "playerWalk")
        } else if panVelocity.y > abs(panVelocity.x) {
            player.run(SKAction.repeatForever(SKAction(named: "WalkUp")!), withKey: "playerWalk")
        } else if panVelocity.y < -abs(panVelocity.x) {
            player.run(SKAction.repeatForever(SKAction(named: "WalkDown")!), withKey: "playerWalk")
        }
    case .changed:
        // 计算每帧的移动增量,背景向手势相反方向移动
        let deltaX = -panVelocity.x * moveSpeed
        let deltaY = -panVelocity.y * moveSpeed
        
        for sprite in levelSprites {
            // 实时更新位置,实现持续移动
            sprite.position = CGPoint(x: sprite.position.x + deltaX, y: sprite.position.y + deltaY)
        }
    case .ended, .cancelled:
        // 手势结束时停止玩家动画
        player.removeAction(forKey: "playerWalk")
        // 给背景加个减速停止的动作,让过渡更自然
        let stopAction = SKAction.moveBy(x: 0, y: 0, duration: 0.2)
        for sprite in levelSprites {
            sprite.run(stopAction, withKey: "backgroundMove")
        }
    default: break
    }
}

这里的关键是:在.changed状态下直接修改精灵的position属性,而不是添加一次性的移动动作。这样每次手势更新时,精灵都会跟着移动,完美实现持续拖动的效果。

二、替代UIPan的更优方案:虚拟摇杆

既然你后续打算加入摇杆控制,那直接实现虚拟摇杆才是最适合游戏的方案——它的操作逻辑更符合玩家习惯,能持续提供方向输入,比手势控制直观得多。下面是一个简单的自定义摇杆实现:

1. 自定义摇杆类

class Joystick: SKNode {
    private let backgroundNode: SKSpriteNode
    private let knobNode: SKSpriteNode
    private var inputVector = CGVector.zero
    // 摇杆最大拖动半径,根据你的UI调整
    private let maxRadius: CGFloat = 50
    
    // 摇杆输入变化的回调,给外部场景传递方向信息
    var onInputChanged: ((CGVector) -> Void)?
    
    init(backgroundTexture: SKTexture, knobTexture: SKTexture) {
        backgroundNode = SKSpriteNode(texture: backgroundTexture)
        knobNode = SKSpriteNode(texture: knobTexture)
        super.init()
        
        backgroundNode.position = .zero
        backgroundNode.isUserInteractionEnabled = false
        addChild(backgroundNode)
        
        knobNode.position = .zero
        knobNode.isUserInteractionEnabled = false
        addChild(knobNode)
        
        isUserInteractionEnabled = true
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }
        updateKnobPosition(with: touch)
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }
        updateKnobPosition(with: touch)
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        resetKnobPosition()
    }
    
    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
        resetKnobPosition()
    }
    
    private func updateKnobPosition(with touch: UITouch) {
        let touchLocation = touch.location(in: self)
        let distance = hypot(touchLocation.x, touchLocation.y)
        
        if distance <= maxRadius {
            knobNode.position = touchLocation
            // 将输入归一化到-1到1的范围
            inputVector = CGVector(dx: touchLocation.x / maxRadius, dy: touchLocation.y / maxRadius)
        } else {
            // 限制摇杆在最大半径内
            let angle = atan2(touchLocation.y, touchLocation.x)
            knobNode.position = CGPoint(x: maxRadius * cos(angle), y: maxRadius * sin(angle))
            inputVector = CGVector(dx: cos(angle), dy: sin(angle))
        }
        
        onInputChanged?(inputVector)
    }
    
    private func resetKnobPosition() {
        knobNode.run(SKAction.move(to: .zero, duration: 0.1))
        inputVector = .zero
        onInputChanged?(inputVector)
    }
}

2. 在SKScene中使用摇杆

class GameScene: SKScene {
    private var joystick: Joystick!
    // 背景移动速度,根据场景调整
    private let moveSpeed: CGFloat = 100
    // 记录帧间隔时间,用于平滑移动
    private var dt: CGFloat = 0
    private var lastUpdateTime: TimeInterval = 0
    
    override func didMove(to view: SKView) {
        // 初始化摇杆,替换成你的纹理资源
        let bgTexture = SKTexture(imageNamed: "joystick_bg")
        let knobTexture = SKTexture(imageNamed: "joystick_knob")
        joystick = Joystick(backgroundTexture: bgTexture, knobTexture: knobTexture)
        // 把摇杆放在屏幕左下角,位置可以自定义
        joystick.position = CGPoint(x: 100, y: 100)
        addChild(joystick)
        
        // 监听摇杆输入变化
        joystick.onInputChanged = { [weak self] vector in
            guard let self = self else { return }
            
            // 根据摇杆输入,计算背景移动增量(相反方向)
            let deltaX = -vector.dx * self.moveSpeed * self.dt
            let deltaY = -vector.dy * self.moveSpeed * self.dt
            
            // 更新所有背景精灵的位置
            for sprite in self.levelSprites {
                sprite.position.x += deltaX
                sprite.position.y += deltaY
            }
            
            // 播放对应方向的玩家动画
            let isMoving = vector.dx != 0 || vector.dy != 0
            if isMoving {
                if abs(vector.dx) > abs(vector.dy) {
                    vector.dx > 0 ? self.playAnimation("WalkRight") : self.playAnimation("WalkLeft")
                } else {
                    vector.dy > 0 ? self.playAnimation("WalkUp") : self.playAnimation("WalkDown")
                }
            } else {
                self.player.removeAction(forKey: "playerWalk")
            }
        }
    }
    
    override func update(_ currentTime: TimeInterval) {
        // 计算帧间隔时间,保证移动速度不受帧率影响
        if lastUpdateTime == 0 {
            lastUpdateTime = currentTime
        }
        dt = CGFloat(currentTime - lastUpdateTime)
        lastUpdateTime = currentTime
    }
    
    private func playAnimation(_ name: String) {
        // 避免重复播放相同动画
        if player.action(forKey: "playerWalk")?.key != name {
            player.removeAction(forKey: "playerWalk")
            player.run(SKAction.repeatForever(SKAction(named: name)!), withKey: "playerWalk")
        }
    }
}

这种摇杆方案的优势很明显:玩家可以按住摇杆保持持续移动,操作逻辑和传统游戏完全一致,体验比手势控制好太多,也更符合你后续的功能规划。

总结

  • 如果想继续用UIPan手势,核心是在.changed状态下实时更新精灵position,而不是用固定的moveTo动作;
  • 考虑到你后续要做摇杆,直接实现虚拟摇杆是更优的选择,操作体验更贴合游戏场景。

内容的提问来源于stack exchange,提问作者amq

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.29 08:25:09