Каковы хорошие способы обойти ограничение кодирования блоков кода SKAction во время сохранения состояния приложения?

Проблема

Когда иерархия узлов закодирована, как это обычно бывает при сохранении состояния приложения или «сохранении игры», узлы, выполняющие SKAction действия с блоками кода, должны обрабатываться особым образом, поскольку блоки кода не могут быть закодированы.

Пример 1: отложенный обратный вызов после анимации

Здесь убит орк. Он анимирован, чтобы исчезнуть, а затем удалить себя из иерархии узлов:

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

Если узел орка закодирован, а затем декодирован, анимация восстановится правильно и завершится, как и ожидалось.

Но теперь пример изменен, чтобы использовать блок кода, который запускается после затухания. Возможно, код очищает состояние игры после того, как орк (наконец) мертв.

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: Твининг

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: просто чертовски полезны, и это было бы позором (и повторяющейся ловушкой). для новичков) считать их злом.


person Karl Voskuil    schedule 07.02.2016    source источник


Ответы (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:. Однако это не так просто.

  • Опять же, он запускается без аргументов [SKAction performSelector:onTarget:], вызывая назначенный метод execute.

  • customActionWithDuration:actionBlock: имеет продолжительность. Но срабатывания performSelector:onTarget: нет. Вызывающий должен вставить сопутствующее действие waitForDuration: в свою последовательность, если оно зависит от продолжительности.

  • Облегченный объект инициализируется с целью, селектором, узлом и длительностью.

  • Когда он срабатывает, легковесный объект отслеживает собственное прошедшее время и периодически вызывает селектор цели, передавая ему узел и прошедшее время.

  • Облегченный объект соответствует NSCoding. При декодировании, если он уже запущен, он возобновляет вызов селектора на оставшуюся часть настроенной длительности.

Ограничения

Я реализовал версию предложенных классов. Благодаря легкому использованию я уже обнаружил важное ограничение: Узлы, закодированные с помощью текущей последовательности SKAction, перезапускают последовательность с самого начала при декодировании.

person Karl Voskuil    schedule 07.02.2016