1

问题

当节点层次结构被编码时,正如在应用程序状态保存或“游戏保存”期间常见的那样,运行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 会淡出并从 parent 中移除,但orcDidFinishDying:不会调用 cleanup 方法。

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

示例 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代码块,尤其是在延迟之后。永远不要依赖动画的完成来恢复良好的应用程序状态。(如果您需要以可编码的方式安排未来的事件,请构建自己的事件队列而不使用代码块。)这个解决方案是不完美的,因为它非常有用,而且这将是一种耻辱(并且对于新手来说是一个反复出现的runBlock陷阱customActionWithDuration:actionBlock:) 认为他们是邪恶的。

4

1 回答 1

1

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

以下想法的代码在这里

替代runBlock

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

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

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

  • 轻量级对象符合NSCoding.

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

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

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

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

  • 同样,它由 no-argument 触发[SKAction performSelector:onTarget:],调用指定的execute方法。

  • AcustomActionWithDuration:actionBlock:有一个持续时间。但触发performSelector:onTarget:没有。waitForDuration:如果取决于持续时间,调用者必须将伴随动作插入到她的序列中。

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

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

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

限制

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

于 2016-02-07T03:13:40.027 回答