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

What are good ways to work around the encoding limitation of SKAction code blocks during application state preservation?

问题

当节点层次结构被编码时,在应用程序状态保存或“游戏保存”期间很常见,节点 运行 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: Run block actions can not be properly encoded, Objective-C blocks do not support NSCoding.

解码后,orc 会淡化并从 parent 中移除,但不会调用清理方法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: 无法编码。如果游戏在动画期间保存,则在游戏加载时将无法正确恢复。

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

不完美的解决方案

以下是我考虑过但不喜欢的解决方案。 (也就是说,我很想阅读成功支持其中一个的答案。)

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

以下想法的代码是 here

替代runBlock

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

  • 调用者实例化轻量级对象并设置其属性:目标、选择器和参数。

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

  • 轻量对象符合NSCoding.

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

  • 这个轻量级对象的一个​​版本可以弱保留目标,这可能很好 and/or 必要。

这是一个可能的界面草稿:

@interface HLPerformSelector : NSObject <NSCoding>

- (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:。然而,这不是那么简单。

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

  • A customActionWithDuration:actionBlock: 有持续时间。但是触发 performSelector:onTarget: 不会。如果它取决于持续时间,调用者必须在她的序列中插入伴随 waitForDuration: 动作。

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

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

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

限制

我已经实现了a version of these proposed classes. Through light use I've already found an important limitation: