Sprite Kit 如何实现用户手指移出按钮即停止对应按钮动作
问题核心成因
现有代码存在两个逻辑漏项:
- 仅在触摸完全结束(手指抬起)的瞬间才检测触摸所在节点,没有监听触摸移动过程的位置变化,手指按住按钮滑出按钮区域时,不会触发任何停止逻辑
- 没有维护按钮的按下状态,所有判断都基于事件触发瞬间的位置,无法覆盖"按住移出"这个中间状态
Sprite Kit的触摸事件机制中,只要手指未抬起,即使滑出屏幕任意区域,触摸事件仍会持续传递给场景,不会因为离开按钮区域自动触发touchesEnded,这就是移出按钮后动作一直不停止的根本原因。
具体实现方案
核心思路是维护当前按下按钮的状态标识,在三个关键触摸回调中同步校验状态:
- 触摸按下时记录被选中的按钮节点
- 触摸移动时实时判断手指是否还在按钮范围内,一旦移出立刻执行停止逻辑
- 触摸结束/被系统打断时,基于记录的状态做收尾清理,避免状态残留
步骤1:在SKScene类中新增状态属性
// 记录当前按下的按钮节点和对应名称 private var currentlyPressedButton: SKNode? private var pressedButtonName: String?
步骤2:在touchesBegan中初始化按下状态
补充原有触摸按下逻辑,点中按钮时直接记录对应节点:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { guard let touch = touches.first else { return } let touchPosition = touch.location(in: self) let touchedNode = self.atPoint(touchPosition) // 匹配左右按钮 if let nodeName = touchedNode.name, nodeName == "leftbutton" || nodeName == "rightbutton" { currentlyPressedButton = touchedNode pressedButtonName = nodeName // 执行原有按钮按下、启动玩家移动动作的逻辑 touchedNode.run(buttonPressedAction) // 你的玩家启动移动动作的原有代码 } }
步骤3:重写touchesMoved实现移出即停
这是解决当前问题的核心逻辑:只要手指在移动过程中离开按钮范围,立刻触发停止动作,无需等手指抬起:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { // 没有按下按钮的状态时直接返回,减少无效计算 guard let touch = touches.first, let pressedNode = currentlyPressedButton else { return } let currentTouchPosition = touch.location(in: self) // 校验当前触摸位置是否还在按钮范围内 // 推荐用contains方法而非frame.contains:会自动适配按钮的缩放、旋转变换,判断更准确 if !pressedNode.contains(currentTouchPosition) { print("\(pressedButtonName ?? "") 已停止:触摸移出按钮区域") pressedNode.run(buttonStoppedPressAction) player?.removeAllActions() // 清空状态,后续移动不再触发重复判断 currentlyPressedButton = nil pressedButtonName = nil } }
步骤4:优化原有touchesEnded逻辑,补充touchesCancelled处理
替换原来在touchesEnded里重新查找节点的代码,直接基于记录的状态做收尾:
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { // 没有待处理的按下状态时直接返回 guard let pressedNode = currentlyPressedButton else { return } print("\(pressedButtonName ?? "") 已停止:触摸抬起") pressedNode.run(buttonStoppedPressAction) player?.removeAllActions() // 清空状态 currentlyPressedButton = nil pressedButtonName = nil } // 补充触摸被系统打断的处理(比如来电、弹窗遮挡时系统会取消触摸) override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) { guard let pressedNode = currentlyPressedButton else { return } pressedNode.run(buttonStoppedPressAction) player?.removeAllActions() currentlyPressedButton = nil pressedButtonName = nil }
注意事项
- 替换原有的
atPoint查找逻辑后,不会再出现按钮带动画位移/缩放时,位置判断不准的问题 - 如果按钮本身设置了较大的热区(比实际显示的节点范围大),可以在校验位置时自定义热区frame判断,不需要硬套节点原始范围
- 有状态标识兜底就不会出现动作残留或者重复触发的问题,不需要在多个回调里重复移除玩家动作
内容的提问来源于stack exchange,提问作者Greg McKenna
相关产品推荐
相关产品推荐

