I've spent a few days working on the animations for my Bejeweled clone and I am pretty happy with the results so I am posting the code for review. This is the most advanced animation code that I have written so far, so there are probably problems with it. Here is a gif of the animation running :
I encountered two major problems when creating this code. The first one was that the update method continually called the animation code for each animation state while it was playing. This could cause weird problems and graphical glitches. I solved this by having a variable keep track of whether an animation was currently playing, and if so it would not run it again. The downside to this is needing to manually set that flag to NO
at the end of each animation. The second major problem was that in order to get the animation state to properly advance, I had to place each piece of animation code inside of an SKAction
block. I am not sure if this is the proper approach, but it seems to be a good solution.
I will not post the entire scene since it is a lot of code.
First off, I need these instance variables to keep track of certain information at the object level:
//animations
DMGameSceneState _state;
BOOL _isAnimationPlaying;
BZDirection _lastDirection;
Next, here are the conditionals inside of the update
method of the Scene:
if (_state == DMGameSceneStatePoppingMatches ||
_state == DMGameSceneStateReplacingMatches ||
_state == DMGameSceneStateSwapFailure ||
_state == DMGameSceneStateSwapSuccess) {
if (!_isAnimationPlaying) {
[self processGameAnimationState];
_isAnimationPlaying = YES;
}
}
if (_state == DMGameSceneStateRunning || _state == DMGameSceneStateBuildingBoard) {
[self renderBoardOrbs];
}
Here is the switch statement that controls which animation will play. Initially all of the animation code was contained in this method, but I have since extracted some of it to smaller methods called from inside here:
#pragma mark - Orb Animations
-(void) processGameAnimationState {
switch (_state) {
case DMGameSceneStateSwapFailure:
[self playAnimationSwapFailure];
break;
case DMGameSceneStateSwapSuccess:
[self playAnimationSwapSuccess];
break;
case DMGameSceneStatePoppingMatches:
[self playAnimationPoppingMatches];
break;
case DMGameSceneStateReplacingMatches:
[self playAnimationReplacingMatches];
break;
default:
break;
}
}
And finally, the animation methods. This is the real meat of the code, and is probably where any major flaws will be found:
-(void) playAnimationSwapFailure {
[self runAction:[SKAction runBlock:^(void) {
DMOrb *firstOrb = _game.firstOrb;
DMOrb *secondOrb = _game.secondOrb;
CGPoint firstOrbOrigin = CGPointMake(firstOrb.boardPosition.x * (_orbWidth + _orbSpacing),
firstOrb.boardPosition.y * (_orbWidth + _orbSpacing));
CGPoint secondOrbOrigin = CGPointMake(secondOrb.boardPosition.x * (_orbWidth + _orbSpacing),
secondOrb.boardPosition.y * (_orbWidth + _orbSpacing));
for (SKSpriteNode *node in _orbLayer.children) {
NSString *firstOrbName = NSStringFromCGPoint(firstOrb.boardPosition);
if ([firstOrbName isEqualToString:node.name]) {
[node runAction:[SKAction moveTo:secondOrbOrigin duration:0.1] completion:^(void) {
[node runAction:[SKAction moveTo:firstOrbOrigin duration:0.1]];
}];
firstOrb = nil;
}
NSString *secondOrbName = NSStringFromCGPoint(secondOrb.boardPosition);
if ([secondOrbName isEqualToString:node.name]) {
[node runAction:[SKAction moveTo:firstOrbOrigin duration:0.1] completion:^(void) {
[node runAction:[SKAction moveTo:secondOrbOrigin duration:0.1]];
}];
secondOrb = nil;
}
}
}] completion:^(void) {
[self runAction:[SKAction waitForDuration:0.2] completion:^(void) {
_state = DMGameSceneStateRunning;
_isAnimationPlaying = NO;
}];
}];
}
-(void) playAnimationSwapSuccess {
[self runAction:[SKAction runBlock:^(void) {
//save local variables because they are used in the blocks
DMOrb *firstOrb = _game.firstOrb;
DMOrb *secondOrb = _game.secondOrb;
CGPoint firstOrbOrigin = CGPointMake(firstOrb.boardPosition.x * (_orbWidth + _orbSpacing),
firstOrb.boardPosition.y * (_orbWidth + _orbSpacing));
CGPoint secondOrbOrigin = CGPointMake(secondOrb.boardPosition.x * (_orbWidth + _orbSpacing),
secondOrb.boardPosition.y * (_orbWidth + _orbSpacing));
//swap the orbs and then replace them with their counterpart once the swap is complete
for (SKSpriteNode *node in _orbLayer.children) {
NSString *firstOrbName = NSStringFromCGPoint(firstOrb.boardPosition);
if ([firstOrbName isEqualToString:node.name]) {
[node runAction:[SKAction moveTo:secondOrbOrigin duration:0.1] completion:^(void) {
[node removeFromParent];
DMOrbNode *newNode = [[DMOrbNode alloc]initWithColor:[self colorForType:firstOrb.type]
size:CGSizeMake(_orbWidth, _orbWidth)
orb:secondOrb];
newNode.position = secondOrbOrigin;
[_orbLayer addChild:newNode];
}];
}
NSString *secondOrbName = NSStringFromCGPoint(secondOrb.boardPosition);
if ([secondOrbName isEqualToString:node.name]) {
[node runAction:[SKAction moveTo:firstOrbOrigin duration:0.1] completion:^(void) {
[node removeFromParent];
DMOrbNode *newNode = [[DMOrbNode alloc]initWithColor:[self colorForType:secondOrb.type]
size:CGSizeMake(_orbWidth, _orbWidth)
orb:firstOrb];
newNode.position = firstOrbOrigin;
[_orbLayer addChild:newNode];
}];
}
}
}] completion:^(void) {
[self runAction:[SKAction waitForDuration:0.2] completion:^(void) {
_state = DMGameSceneStatePoppingMatches;
_isAnimationPlaying = NO;
}];
}];
}
-(void) playAnimationPoppingMatches {
[self runAction:[SKAction runBlock:^(void) {
for (SKSpriteNode *node in _orbLayer.children) {
for (DMOrb *orb in _game.matchedOrbs) {
NSString *orbName = NSStringFromCGPoint(orb.boardPosition);
if ([orbName isEqualToString:node.name]) {
[node runAction:[SKAction fadeOutWithDuration:0.2]];
break;
}
}
}
[_game clearMatchedOrbs];
}] completion:^(void) {
[self runAction:[SKAction waitForDuration:0.2] completion:^(void) {
_state = DMGameSceneStateReplacingMatches;
_isAnimationPlaying = NO;
}];
}];
}
-(void) playAnimationReplacingMatches {
[self runAction:[SKAction runBlock:^(void) {
for (SKSpriteNode *node in _orbLayer.children) {
for (DMOrb *orb in _game.movingOrbs) {
NSString *orbName = NSStringFromCGPoint(orb.previousPosition);
if ([orbName isEqualToString:node.name]) {
CGPoint destination = CGPointMake(orb.boardPosition.x * (_orbWidth + _orbSpacing),
orb.boardPosition.y * (_orbWidth + _orbSpacing));
CGFloat speed;
if (destination.x > orb.previousPosition.x ||
destination.x < orb.previousPosition.x) {
speed = 0.002 * abs(destination.x - orb.previousPosition.x);
} else if (destination.y > orb.previousPosition.y ||
destination.y < orb.previousPosition.y) {
speed = 0.002 * abs(destination.y - orb.previousPosition.y);
}
[node runAction:[SKAction moveTo:destination duration:speed]];
break;
}
}
}
[_game clearMovingOrbs];
for (DMOrb *orb in _game.createdOrbs) {
DMOrbNode *orbNode = [[DMOrbNode alloc]initWithColor:[self colorForType:orb.type]
size:CGSizeMake(_orbWidth, _orbWidth) orb:orb];
[_orbLayer addChild:orbNode];
orbNode.position = [self outsidePositionForDirection:_lastDirection orb:orb];
CGPoint destination = CGPointMake(orb.boardPosition.x * (_orbWidth + _orbSpacing),
orb.boardPosition.y * (_orbWidth + _orbSpacing));
[orbNode runAction:[SKAction moveTo:destination duration:0.4]];
}
[_game clearCreatedOrbs];
}] completion:^(void) {
[self runAction:[SKAction waitForDuration:0.4] completion:^(void) {
_state = DMGameSceneStateRunning;
_isAnimationPlaying = NO;
}];
}];
}
Also, here is the method that calculates the proper position when adding newly created orbs to the scene:
-(CGPoint) outsidePositionForDirection:(BZDirection)direction orb:(DMOrb *)orb {
switch (direction) {
case BZDirectionLeft:
return CGPointMake(9 * (_orbWidth + _orbSpacing), orb.boardPosition.y * (_orbWidth + _orbSpacing));
case BZDirectionRight:
return CGPointMake(-1 * (_orbWidth + _orbSpacing), orb.boardPosition.y * (_orbWidth + _orbSpacing));
case BZDirectionUp:
return CGPointMake(orb.boardPosition.x * (_orbWidth + _orbSpacing), -1 * (_orbWidth + _orbSpacing));
case BZDirectionDown:
return CGPointMake(orb.boardPosition.x * (_orbWidth + _orbSpacing), 9 * (_orbWidth + _orbSpacing));
default:
return CGPointZero;
}
}
Any and all feedback is much appreciated!