在应用程序状态保存期间,解决SKAction代码块编码限制的好方法是什么?

问题

当节点层次结构被编码时,如在应用程序状态保存或“游戏保存”期间常见的那样,必须专门处理运行具有代码块的SKAction动作的节点,因为代码块不能被编码。

示例1:动画后的延迟回调

在这里,一个兽人被杀死了。 它是动画淡出然后从节点层次结构中删除自己:

 SKAction *fadeAction = [SKAction fadeOutWithDuration:3.0]; SKAction *removeAction = [SKAction removeFromParent]; [orcNode runAction:[SKAction sequence:@[ fadeAction, removeAction ]]]; 

如果对orc节点进行编码然后解码,则动画将按预期正确恢复并完成。

但现在修改了示例以使用在淡入淡出后运行的代码块。 一旦兽人(最终)死了,也许代码会清理一些游戏状态。

 SKAction *fadeAction = [SKAction fadeOutWithDuration:3.0]; SKAction *removeAction = [SKAction removeFromParent]; SKAction *cleanupAction = [SKAction runBlock:^{ [self orcDidFinishDying:orcNode]; }]; [orcNode runAction:[SKAction sequence:@[ fadeAction, removeAction, cleanupAction ]]]; 

不幸的是,代码块不会编码。 在应用程序状态保存(或游戏保存)期间,如果此序列正在运行,将发出警告:

SKAction:运行块操作无法正确编码,Objective-C块不支持NSCoding。

解码后,orc将淡入并从父节点中删除,但不会调用清理方法orcDidFinishDying: .

解决此限制的最佳方法是什么?

示例2:补间

SKAction customActionWithDuration:actionBlock:非常适合补间。 我的样板代码是这样的:

 SKAction *slideInAction = [SKAction customActionWithDuration:2.0 actionBlock:^(SKNode *node, CGFloat elapsedTime){ CGFloat normalTime = (CGFloat)(elapsedTime / 2.0); CGFloat normalValue = BackStandardEaseInOut(normalTime); node.position = CGPointMake(node.position.x, slideStartPositionY * (1.0f - normalValue) + slideFinalPositionY * normalValue); }]; 

不幸的是, customActionWithDuration:actionBlock:无法编码。 如果在动画期间保存游戏,则游戏加载时无法正常恢复。

同样,解决这个限制的最佳方法是什么?

不完美的解决方案

这是我考虑但不喜欢的解决方案。 (也就是说,我很乐意阅读成功捍卫其中一个的答案。)

  • 不完美的解决方案:在动画中使用performSelector:onTarget:而不是runBlock: . 这个解决方案是不完美的,因为参数不能传递给被调用的选择器; 调用的上下文只能由目标和选择器的名称表示。 不是很好。

  • 不完美的解决方案:在编码期间,从任何相关节点中删除SKAction序列并推进程序状态,就像序列已完成一样。 在第一个示例中,这意味着将节点alpha立即设置为0.0 ,从父节点移除orc节点,并调用orcDidFinishDying: . 这是一个令人遗憾的解决方案,至少有两个原因:1)在编码期间需要特殊的处理代码; 2)在视觉上,节点将没有机会完成其动画。

  • 不完美的解决方案:在编码期间,从任何相关节点中删除SKAction代码块,并在解码期间重新创建它们。 这不重要。

  • 不完美的解决方案:永远不要使用SKAction代码块,特别是在延迟之后。 永远不要依赖动画的完成来恢复良好的应用状态。 (如果你需要以可编码的方式安排未来的事件,建立你自己的事件队列而不是使用代码块。)这个解决方案是不完美的,因为runBlockcustomActionWithDuration:actionBlock:真是太有用了,这将是一个耻辱(和新手的反复陷阱)认为他们是邪恶的。

可编码的轻量级对象可以模拟我们想要使用的SKAction代码块(但不能)。

以下思路的代码在这里 。

替换runBlock

第一个可编码的轻量级对象替换了runBlock 。 它可以使用一个或两个参数进行任意回调。

  • 调用者实例化轻量级对象并设置其属性:target,selector和arguments。

  • 轻量级对象由runAction动画通过标准无参数[SKAction performSelector:onTarget:] 。 对于此触发操作,目标是轻量级对象,而选择器是指定的“执行”方法。

  • 轻量级对象符合NSCoding

  • 作为奖励,触发SKAction保留对轻量级对象的强引用,因此两者都将与运行动作的节点一起编码。

  • 可以制作这个轻量级对象的一个​​版本,它可以很好地保留目标,这可能是好的和/或必要的。

这是一个可能的接口草案:

 @interface HLPerformSelector : NSObject  - (instancetype)initWithTarget:(id)target selector:(SEL)selector argument:(id)argument; @property (nonatomic, strong) id target; @property (nonatomic, assign) SEL selector; @property (nonatomic, strong) id argument; - (void)execute; @end 

以及随附的实施:

 @implementation HLPerformSelector - (instancetype)initWithTarget:(id)target selector:(SEL)selector argument:(id)argument { self = [super init]; if (self) { _target = target; _selector = selector; _argument = argument; } return self; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super init]; if (self) { _target = [aDecoder decodeObjectForKey:@"target"]; _selector = NSSelectorFromString([aDecoder decodeObjectForKey:@"selector"]); _argument = [aDecoder decodeObjectForKey:@"argument"]; } return self; } - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:_target forKey:@"target"]; [aCoder encodeObject:NSStringFromSelector(_selector) forKey:@"selector"]; [aCoder encodeObject:_argument forKey:@"argument"]; } - (void)execute { if (!_target) { return; } IMP imp = [_target methodForSelector:_selector]; void (*func)(id, SEL, id) = (void (*)(id, SEL, id))imp; func(_target, _selector, _argument); } @end 

以及使用它的一个例子:

 SKAction *fadeAction = [SKAction fadeOutWithDuration:3.0]; SKAction *removeAction = [SKAction removeFromParent]; HLPerformSelector *cleanupCaller = [[HLPerformSelector alloc] initWithTarget:self selector:@selector(orcDidFinishDying:) argument:orcNode]; SKAction *cleanupAction = [SKAction performSelector:@selector(execute) onTarget:cleanupCaller]; [orcNode runAction:[SKAction sequence:@[ fadeAction, removeAction, cleanupAction ]]]; 

customActionWithDuration:actionBlock:替换customActionWithDuration:actionBlock:

第二个可编码的轻量级对象替换了customActionWithDuration:actionBlock: . 然而,这并不是那么简单。

  • 同样,它由无参数[SKAction performSelector:onTarget:]触发,调用指定的execute方法。

  • customActionWithDuration:actionBlock:具有持续时间。 但是触发performSelector:onTarget:没有。 如果呼叫者取决于持续时间,则必须将伴随的waitForDuration:动作插入到她的序列中。

  • 轻量级对象使用目标,选择器,节点和持续时间进行初始化。

  • 当它被触发时,轻量级对象跟踪它自己的经过时间,并定期调用目标上的选择器,将节点和经过的时间传递给它。

  • 轻量级对象符合NSCoding 。 在解码时,如果已经触发,它将继续在其配置的持续时间的剩余时间内调用选择器。

限制

我已经实现了这些提议类的一个版本 。 通过轻度使用,我已经发现了一个重要的限制: 使用正在运行的SKAction序列编码的节点在解码时从头开始重新启动序列 。