Adding, Removing, and Caching SKNodes

2019-08-05 06:55发布

问题:

Background info: I'm creating a SpriteKit 2D platform-style game with multiple "floors". When the user enters a "portal" he is transported to another floor (either one up or one down). If the user dies at a different floor from where the most recent spawnpoint is, he is transported back to the floor with the spawnpoint.

Problem: After upgrading to iOS8, this scenario causes the game to crash with an EXC_BAD_ACCESS exception/ error. Even after going through multiple tutorials on how to debug such errors, I cannot seem to find the problem.

So I'd really appreciate if someone could take a look at my code and tell me if they see a better way of doing what I'm doing, preferably without causing the game to freeze.

Game layout: I have a custom class TileMapLayer which is based on the same class from Ray Wenderlich's book iOS Games By Tutorials. It is basically a custom SKNode class containing multiple 32x32p tiles, combined creating the background for my game.

I load the appropriate floor when the game begins, and then call to load another floor when my user goes up/down. In this scenario, I cache the current floor first in a NSDictionary so I can access it later if the user returns to this floor.

// Cache the current floor
[_allFloors setObject:_bgLayer forKey:[NSString stringWithFormat:@"floor_%tu", (unsigned)_currentFloor]];

// Remove the current floor
[_bgLayer removeFromParent];

// Get the cached (next) floor if it exists, if not create it
TileMapLayer *cachedFloor = [_allFloors objectForKey:[NSString stringWithFormat:@"floor_%tu", (unsigned)(_currentFloor + 1)]];
if (!cachedFloor) {
    _bgLayer = [self createScenery:[NSNumber numberWithInteger:(_currentFloor + 1)]];
    NSLog(@"creating new floor");
} else {
    _bgLayer = cachedFloor;
    NSLog(@"getting cached floor");
}

// Display the new floor
[_worldNode addChild:_bgLayer];

// Increment the floor number
_currentFloor++;

(I have a similar method for going down a floor as well). This works perfectly, both before and after upgrading to iOS8.

When the user dies, he is transported back to the last spawnpoint. If the last spawnpoint is on a different floor, the floor also changes appropriately. I call this custom SKAction on the ball itself as a part of an animation:

SKAction *changeFloor = [SKAction runBlock:^{
  if (self.spawnFloor != _currentFloor) { 
    [_bgLayer removeFromParent];
    _bgLayer = [_allFloors objectForKey:[NSString stringWithFormat:@"floor_%tu", (unsigned)self.spawnFloor]];
    [_worldNode addChild:_bgLayer];
    _currentFloor = self.spawnFloor;
    NSLog(@"Made it here");
  }
}];

As you can see there isn't much difference. The Made it here gets logged to the console, but the game freezes immediately after. The next method I call (to move the ball to the correct location) is not executed at all.

Just for fun I tried caching the _bgLayer before removing it from its parent like so:

if (self.spawnFloor != _currentFloor) {
  [_allFloors setObject:_bgLayer forKey:[NSString stringWithFormat:@"floor_%tu", (unsigned)_currentFloor]];
  ...

And for some reason the game does not freeze when I do this. However, the floors end up being mixed as if the _bgLayer never was removed from its parent (note: the "old" tiles does no longer react to any physics-simulations, they're just in the background doing nothing).

I also tried [_bgLayer removeAllChildren] (removing the individual tiles) right after removing from the parent:

[_bgLayer removeFromParent];
[_bgLayer removeAllChildren];

But this causes the _bgLayer to be empty when I return to this floor after respawning. As if I removed all the nodes before I stored it in the dictionary. My guess is that the dictionary only references the location of the _bgLayer, not the content itself. So when I remove all the children on screen, I effectively remove all of them in the cache as well.

Wrapup: I know this is a long question, and I'd like to say thank you if you made it this far. If you have any questions, please don't hesitate to ask them in the comments below. Ultimately, what I'd like to know is this: How can I resolve my problem so the game doesn't freeze? What's the best practice for caching the floors without causing memory problems? What's the best practice for removing and adding the nodes on screen, and always referring to the current floor (the one that's on screen) as _bgLayer?

Again, thank you so much!

回答1:

iOS8 seems to behave different when removing nodes inside blocks, as if you tried to do it on a background thread so sometimes it causes strange crashes. It might be a bug but until then I'd suggest two things:

1.- Mark the node to be removed inside the block but do it on the update: loop. You will not notice any difference.

2.- Make sure the removeFromParent happens in the main thread. But not sure this will fix the problem.

__strong typeof(_bgLayer) strongLayer = _bgLayer;
dispatch_async(dispatch_get_main_queue(), ^{
        [strongLayer removeFromParent];
    });