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

如何在SpriteKit贪吃蛇游戏中实现兼容SKAction特性的滑动移动?

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 17:54:56