在应用程序状态保存期间,有哪些好方法可以解决 SKAction 代码块的编码限制?

2023-12-07

Problem

当节点层次结构被编码时(这在应用程序状态保存或“游戏保存”期间很常见),运行的节点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。

解码后,兽人会消失并从父级中移除,但清理方法orcDidFinishDying:不会被调用。

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

示例 2:补间

The 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 and customActionWithDuration:actionBlock:实在是太有用了,如果认为它们是邪恶的,那将是一种耻辱(对于新手来说也是一个反复出现的陷阱)。


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

以下想法的代码是here.

替代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:。然而,这件事并不那么简单。

  • 再次,它是由无争论触发的[SKAction performSelector:onTarget:],调用指定的execute method.

  • A customActionWithDuration:actionBlock:有持续时间。但触发的performSelector:onTarget:才不是。调用者必须插入一个同伴waitForDuration:行动进入她的序列,如果它取决于持续时间。

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

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

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

局限性

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

在应用程序状态保存期间,有哪些好方法可以解决 SKAction 代码块的编码限制? 的相关文章

随机推荐