如何在SpriteKit贪吃蛇游戏中实现兼容SKAction特性的滑动移动?
我正在用SpriteKit开发一款贪吃蛇游戏,原本想借助SKAction的时序、序列、组等特性简化开发,但现在不得不写大量自定义action,显得很冗余。以下是基础代码实现:
基础代码
class SOSnake: SKNode { let size: Double // 蛇由多个方块组成,size代表单个方块的边长 var body: [SOBodyPart] // 存储所有身体部位,方便遍历 init(ofSize size: Double) { self.size = size self.body = [SOBodyPart(type: .head, size: size)] super.init() addChild(head) } var head: SOBodyPart { body.first! } // 快捷访问头部 var tail: SOBodyPart { body.last! } // 快捷访问尾部。当body.count == 1时,头部和尾部是同一个节点 }
// 实际游戏中,SOBodyPart有一个`type`属性,可选值为"head"、"body"或"tail", // 用来决定SKSpriteNode的颜色,这部分和当前问题无关,所以省略了 class SOBodyPart: SKSpriteNode { init(id: Int, size: Double) { // id用来设置节点name super.init(texture: nil, color: .yellow, size: CGSizeMake(size, size)) self.name = String(id) } }
我的滑动移动实现
我写了一个move()方法,返回SKAction,让蛇的每个身体部位朝自身朝向移动:
// 每个身体部位的name是它在body数组中的索引,所以可以用String(indexInBodyArray)来引用 func move() -> SKAction { let actions = body.indices.map { SKAction.run(.move(Direction.of(body[$0]), by: size, duration: 1), onChildWithName: String($0)) } return SKAction.group(actions) }
辅助代码
(可跳过,仅作上下文参考)
enum Direction { case up, down, left, right static func of(_ node: SKNode) -> Direction { // 根据节点的zRotation判断方向: // 0°±45°范围内返回right,90°±45°范围内返回up,以此类推 } } extension SKAction { static func move(_ direction: Direction, by amount: Double, duration: TimeInterval) -> SKAction { switch direction { case .up: return .move(by: .init(dx: 0, dy: amount), duration: duration) case .down: return .move(by: .init(dx: 0, dy: -amount), duration: duration) case .left: return .move(by: .init(dx: -amount, dy: 0), duration: duration) case .right: return .move(by: .init(dx: amount, dy: 0), duration: duration) } } }
问题所在
当我在SKScene中调用move()并修改返回的SKAction属性时:
private func onTap() { let action = snake.move() action.duration = 10.0 action.timingMode = .easeInEaseOut snake.run(action) }
这些修改不会生效,因为SKAction.run(_:onChildWithName:)是瞬时执行的容器action,它的属性无法传递给内部包裹的移动action。我现在被迫用customAction(withDuration:actionBlock:)来实现,但这重复了SpriteKit已有的移动功能。
请问有没有内置方式实现move()方法,让返回的SKAction属性修改能生效?或者只能通过自定义action实现?希望找到最简单的方案,同时能利用SKAction的时序模式、序列等特性。
解决方案
问题核心在于你当前使用的SKAction.run(_:onChildWithName:)是瞬时执行的容器action,它本身的duration和timingMode不会传递给内部的移动action。要让外部修改生效,需要调整action的构造方式,让组的时序属性能同步到每个子action上。
方案1:动态生成带参数的移动action组
不要提前把duration写死在move方法里,而是让move接收duration和timingMode参数,生成的每个移动action都会使用统一的配置:
func move(withDuration duration: TimeInterval, timingMode: SKActionTimingMode = .linear) -> SKAction { let actions = body.indices.map { index -> SKAction in let bodyPart = body[index] let moveAction = SKAction.move(Direction.of(bodyPart), by: size, duration: duration) moveAction.timingMode = timingMode return SKAction.run(moveAction, onChildWithName: String(index)) } let group = SKAction.group(actions) group.duration = duration // 同步组的duration,保持API一致性 return group }
调用时直接传入参数即可:
private func onTap() { let action = snake.move(withDuration: 10.0, timingMode: .easeInEaseOut) snake.run(action) }
方案2:直接操作子action同步属性
如果一定要保留外部直接修改action属性的能力,可以构造action组后,手动同步每个子action的duration和timingMode:
func move() -> SKAction { let actions = body.map { bodyPart -> SKAction in return SKAction.move(Direction.of(bodyPart), by: size, duration: 1.0) } return SKAction.group(actions) }
调用时强制转换为SKActionGroup并同步子属性:
private func onTap() { guard let actionGroup = snake.move() as? SKActionGroup else { return } let newDuration = 10.0 let targetTimingMode: SKActionTimingMode = .easeInEaseOut actionGroup.duration = newDuration // 同步每个子移动action的属性 actionGroup.actions.forEach { subAction in subAction.duration = newDuration subAction.timingMode = targetTimingMode } snake.run(actionGroup) }
这种方式完全复用SpriteKit的原生移动action,没有冗余逻辑,只是多了一步同步子属性的操作。
方案3:自定义action复用原生移动逻辑
如果想让外部修改action属性直接生效,也可以用自定义action,但内部复用原生SKAction.move的方向计算逻辑,避免重复造轮子:
func move() -> SKAction { return SKAction.customAction(withDuration: 1.0) { [weak self] node, elapsedTime in guard let self = self else { return } let action = node.action(forKey: "move") let progress = elapsedTime / (action?.duration ?? 1.0) for bodyPart in self.body { let direction = Direction.of(bodyPart) let totalDelta = self.size * progress let delta: CGPoint switch direction { case .up: delta = CGPoint(x: 0, y: totalDelta) case .down: delta = CGPoint(x: 0, y: -totalDelta) case .left: delta = CGPoint(x: -totalDelta, y: 0) case .right: delta = CGPoint(x: totalDelta, y: 0) } bodyPart.position += delta } } }
此时外部修改action.duration和timingMode都会生效,自定义action会根据总时长和已流逝时间计算进度,再应用到每个身体部位上。
总结
优先选择方案1,它既保留了SKAction的时序特性,又能通过参数直接控制移动的duration和timingMode,代码简洁且不冗余。如果一定要支持外部直接修改action属性,方案2是更贴合需求的选择,完全依赖原生API,没有额外的自定义逻辑。
内容的提问来源于stack exchange,提问作者rayaantaneja

