在应用程序状态保存期间解决 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:
无法编码。如果游戏在动画期间保存,则在游戏加载时将无法正确恢复。
同样,解决此限制的最佳方法是什么?
不完美的解决方案
以下是我考虑过但不喜欢的解决方案。 (也就是说,我很想阅读成功支持其中一个的答案。)
不完美的解决方案:在动画中使用performSelector:onTarget:
而不是runBlock:
。这个解决方案是不完美的,因为不能将参数传递给被调用的选择器;调用的上下文只能由目标和选择器的名称来表达。不太好。
不完美的解决方案:在编码过程中,从任何相关节点中删除SKAction
序列,并像序列已完成一样推进程序状态。在第一个示例中,这意味着将节点 alpha
立即设置为 0.0
,从父节点中删除 orc 节点,然后调用 orcDidFinishDying:
。这是一个不幸的解决方案,至少有两个原因:1)它在编码期间需要特殊的处理代码; 2) 在视觉上,节点将没有机会完成它的动画。
不完美的解决方案:在编码过程中,从任何相关节点中删除SKAction
代码块,并在解码过程中重新创建它们。这很重要。
不完美的解决方案:永远不要使用 SKAction
代码块,尤其是在延迟之后。永远不要依赖动画的完成来恢复良好的应用程序状态。 (如果您需要以可编码的方式安排未来的事件,请构建您自己的事件队列而不是使用代码块。)这个解决方案是不完美的,因为 runBlock
和 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: 。
问题
当节点层次结构被编码时,在应用程序状态保存或“游戏保存”期间很常见,节点 运行 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:
无法编码。如果游戏在动画期间保存,则在游戏加载时将无法正确恢复。
同样,解决此限制的最佳方法是什么?
不完美的解决方案
以下是我考虑过但不喜欢的解决方案。 (也就是说,我很想阅读成功支持其中一个的答案。)
不完美的解决方案:在动画中使用
performSelector:onTarget:
而不是runBlock:
。这个解决方案是不完美的,因为不能将参数传递给被调用的选择器;调用的上下文只能由目标和选择器的名称来表达。不太好。不完美的解决方案:在编码过程中,从任何相关节点中删除
SKAction
序列,并像序列已完成一样推进程序状态。在第一个示例中,这意味着将节点alpha
立即设置为0.0
,从父节点中删除 orc 节点,然后调用orcDidFinishDying:
。这是一个不幸的解决方案,至少有两个原因:1)它在编码期间需要特殊的处理代码; 2) 在视觉上,节点将没有机会完成它的动画。不完美的解决方案:在编码过程中,从任何相关节点中删除
SKAction
代码块,并在解码过程中重新创建它们。这很重要。不完美的解决方案:永远不要使用
SKAction
代码块,尤其是在延迟之后。永远不要依赖动画的完成来恢复良好的应用程序状态。 (如果您需要以可编码的方式安排未来的事件,请构建您自己的事件队列而不是使用代码块。)这个解决方案是不完美的,因为runBlock
和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: