SpriteKit中SKAction如何不调用setter修改节点属性?
SpriteKit官方文档明确提到:动作通常不会调用节点的公开方法。比如子类化SKSpriteNode并给position属性添加didSet观察器,用来响应move(to:duration:)动作时,观察器并不会触发。
示例代码:
class MovingNode: SKSpriteNode { override var position: CGPoint { didSet { // 这里的代码不会在move动作执行时触发 } } }
产生的困惑点:
- 如果不通过
someNode.position = newPosition这种调用setter的方式,节点的位置是怎么改变的? - 为什么每帧打印position能看到数值变化,却完全绕过了didSet观察器?
- SKAction的底层实现逻辑到底是什么?
底层实现原理
1. SKAction直接操作底层存储,绕过属性setter
SpriteKit的核心底层基于Objective-C和C++实现,为了保证高性能(尤其是在每帧更新大量节点的场景下),SKAction不会通过调用Objective-C的属性setter方法来修改节点属性。
以position为例,在OC中position是一个@property,但它对应着一个私有实例变量(比如_position)。SKAction在执行时,会直接修改这个私有实例变量的值,完全跳过了setter方法的调用流程。
2. Swift属性观察器依赖setter触发
Swift的didSet/willSet观察器是Swift语言层面的特性,它的触发必须依赖通过Swift的属性setter来修改值。当底层OC代码直接修改私有实例变量时,Swift的属性访问机制完全没被触发,自然不会调用didSet。
而我们读取position时能看到变化,是因为读取操作走的是属性getter方法,getter会直接返回底层实例变量的当前值,所以能拿到最新的位置数据。
3. 性能优先的设计选择
SpriteKit作为游戏框架,性能是核心考量点。Objective-C的消息发送(包括调用setter方法)本身存在一定开销,如果每个SKAction都通过setter来修改属性,在大量节点同时执行动作的场景下,会产生显著的性能损耗。直接操作底层存储能把这部分开销降到最低,保证游戏的流畅运行。
替代监听方案
如果需要响应SKAction带来的位置变化,可以考虑以下几种方式:
- 在每帧调用的
update(_:)方法中,手动对比当前position和上一帧的position,自行处理变化逻辑 - 使用SKAction的
completion闭包,在动作结束时执行相关逻辑(仅适用于动作结束的场景) - 利用Objective-C的KVO机制监听position属性(KVO能监测到底层实例变量的变化,不过在Swift中需要正确处理KVO的相关操作)
内容的提问来源于stack exchange,提问作者rayaantaneja

