HiveBrain v1.2.0
Get Started
← Back to all entries
patternMinor

Animated Score Amounts for Game

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
animatedscoregameamountsfor

Problem

This is a simple class for a label with a score that animates counting up or down. When someone in the game scores points, the numbers count up or down to the new total.

Here is an example of what it looks like:

I ran into a few problems when building this class. It was important to prevent the building of further animations while a current animation was playing, because otherwise the score would be incremented too many times. It was also important to to set the correct values at the end of the sequence of animations in order to make sure that it was always accurate when it completed.

BZAnimatedScoreLabel.h

#import 

@interface BZAnimatedScoreLabel : SKLabelNode

+(BZAnimatedScoreLabel *) labelWithText:(NSString *)text score:(int)score size:(int)fontSize;

-(void) updateForScore:(int)newScore;

@end


BZAnimatedScoreLabel.m

```
#import "BZAnimatedScoreLabel.h"

@implementation BZAnimatedScoreLabel {
int _score;
SKLabelNode *_scoreLabel;
NSMutableArray *_actionQueue;
BOOL _isAnimationPlaying;
}

#pragma mark - Initialization
+(BZAnimatedScoreLabel ) labelWithText:(NSString )text score:(int)score size:(int)fontSize {
return [[BZAnimatedScoreLabel alloc]initWithText:text score:score size:fontSize];
}
-(instancetype) initWithText:(NSString *)text score:(int)score size:(int)fontSize {
self = [super initWithFontNamed:@"Arial"];
if (self) {
self.text = text;
self.fontSize = fontSize;
self.fontColor = [SKColor whiteColor];
_score = score;

_scoreLabel = [[SKLabelNode alloc]initWithFontNamed:@"Arial"];
_scoreLabel.fontSize = fontSize;
_scoreLabel.fontColor = [SKColor whiteColor];
_scoreLabel.text = [NSString stringWithFormat:@"%i", _score];
_scoreLabel.position = CGPointMake((fontSize * 4), 0);
[self addChild:_scoreLabel];

_isAnimationPlaying = NO;
_actionQueue = [[NSMutableArray alloc]init];
}
return self;
}

#pragma mark - A

Solution

-
If the score label is updated while the animation is still running, then
the update is simply ignored. For example, with

[_pointsLabel updateForScore:100];
[_pointsLabel updateForScore:200];


the label will animate to 100 and stay there, instead of animating to 200.

Instead of pre-computing all actions from the current score to the final value,
I would start only a single action that will display the next intermediate score, e.g. from 100 to 110. When that action has completed, start a new action.

This approach solves the problem of simultaneously running actions, and makes
both _isAnimationPlaying and the _actionQueue obsolete. Each time an action is created,
it can check whether the counter has to be incremented or decremented.

-
The animation is always in steps of 10, e.g. an update from 13 to 51 will
display 13, 23, 33, 43, 51. It would look nicer if multiples of 10 are displayed
where possible, in this case 13, 20, 30, 40, 50, 51.

-
The animation is too fast. At last on my Simulator it was running so fast that
not all intermediate steps can be recognized. I would add an small delay
between the actions.

-
The updateForScore: method is not really necessary. I would make score
a (public) property and override the setter method, so that

_pointsLabel.score = newValue;


updates the score and starts the animation.

Then your implementation could look like this:

BZAnimatedScoreLabel.h

@interface BZAnimatedScoreLabel : SKLabelNode
+(BZAnimatedScoreLabel *) labelWithText:(NSString *)text score:(int)score size:(int)fontSize;
@property (nonatomic) int score;
@end


BZAnimatedScoreLabel.m

#import "BZAnimatedScoreLabel.h"

@implementation BZAnimatedScoreLabel {
    SKLabelNode *_scoreLabel;
    int _currentScore; // The currently displayed score
}

#pragma mark - Constants

static NSString *kAnimationKey = @"BZLabelAnimationKey";
static const NSTimeInterval kAnimationDelay = 0.02;

#pragma mark - Initialization

+(BZAnimatedScoreLabel *) labelWithText:(NSString *)text score:(int)score size:(int)fontSize {
    return [[BZAnimatedScoreLabel alloc]initWithText:text score:score size:fontSize];
}

-(instancetype) initWithText:(NSString *)text score:(int)score size:(int)fontSize {
    self = [super initWithFontNamed:@"Arial"];
    if (self) {
        self.text = text;
        self.fontSize = fontSize;
        self.fontColor = [SKColor whiteColor];

        _currentScore = _score = score;
        _scoreLabel = [[SKLabelNode alloc] initWithFontNamed:@"Arial"];
        _scoreLabel.fontSize = fontSize;
        _scoreLabel.fontColor = [SKColor whiteColor];
        _scoreLabel.position = CGPointMake(fontSize * 4, 0);
        _scoreLabel.text = [NSString stringWithFormat:@"%i", score];
        [self addChild:_scoreLabel];
    }
    return self;
}

#pragma mark - Animation

-(void)setScore:(int)score {
    _score = score;
    [self updateDisplay];
}

// Compute next multiple of 10 from _currentScore in the direction of _score:
-(int)computeNextScore {
    int next;
    if (_score > _currentScore) {
        if (_currentScore >= 0) {
            next = ((_currentScore + 10)/ 10) * 10;
        } else {
            next = ((_currentScore + 1)/ 10) * 10;
        }
        if (next > _score) {
            next = _score;
        }
    } else if (_score < _currentScore) {
        if (_currentScore <= 0) {
            next = ((_currentScore - 10) / 10) * 10;
        } else {
            next = ((_currentScore - 1) / 10) * 10;
        }
        if (next < _score) {
            next = _score;
        }
    } else {
        next = _score;
    }
    return next;
}

-(void)updateDisplay {
    if (_score != _currentScore) {
        SKAction *wait = [SKAction waitForDuration:kAnimationDelay];
        SKAction *update = [SKAction runBlock:^() {
            _currentScore = [self computeNextScore];
            _scoreLabel.text = [NSString stringWithFormat:@"%i", _currentScore];
        }];
        SKAction *checkAgain = [SKAction performSelector:@selector(updateDisplay) onTarget:self];
        [self runAction:[SKAction sequence:@[wait, update, checkAgain]] withKey:kAnimationKey];
    } else {
        [self removeActionForKey:kAnimationKey];
    }
}

@end


_currentScore is the currently displayed score, and the computeNextScore
method computes the next value to be displayed. It looks a bit complicated,
but it works correctly for both positive and negative scores.

updateDisplay starts a sequence of three actions (if necessary): wait
is for the delay, update computes the next score to be displayed and
updates the label, and checkAgain causes the updateDisplay method to
be called again.

The sequence action is created with a key. This has two advantages:
The action can be removed, and starting a new action with the same key
will automatically stop the previous action.

Code Snippets

[_pointsLabel updateForScore:100];
[_pointsLabel updateForScore:200];
_pointsLabel.score = newValue;
@interface BZAnimatedScoreLabel : SKLabelNode
+(BZAnimatedScoreLabel *) labelWithText:(NSString *)text score:(int)score size:(int)fontSize;
@property (nonatomic) int score;
@end
#import "BZAnimatedScoreLabel.h"

@implementation BZAnimatedScoreLabel {
    SKLabelNode *_scoreLabel;
    int _currentScore; // The currently displayed score
}

#pragma mark - Constants

static NSString *kAnimationKey = @"BZLabelAnimationKey";
static const NSTimeInterval kAnimationDelay = 0.02;


#pragma mark - Initialization

+(BZAnimatedScoreLabel *) labelWithText:(NSString *)text score:(int)score size:(int)fontSize {
    return [[BZAnimatedScoreLabel alloc]initWithText:text score:score size:fontSize];
}

-(instancetype) initWithText:(NSString *)text score:(int)score size:(int)fontSize {
    self = [super initWithFontNamed:@"Arial"];
    if (self) {
        self.text = text;
        self.fontSize = fontSize;
        self.fontColor = [SKColor whiteColor];

        _currentScore = _score = score;
        _scoreLabel = [[SKLabelNode alloc] initWithFontNamed:@"Arial"];
        _scoreLabel.fontSize = fontSize;
        _scoreLabel.fontColor = [SKColor whiteColor];
        _scoreLabel.position = CGPointMake(fontSize * 4, 0);
        _scoreLabel.text = [NSString stringWithFormat:@"%i", score];
        [self addChild:_scoreLabel];
    }
    return self;
}

#pragma mark - Animation

-(void)setScore:(int)score {
    _score = score;
    [self updateDisplay];
}

// Compute next multiple of 10 from _currentScore in the direction of _score:
-(int)computeNextScore {
    int next;
    if (_score > _currentScore) {
        if (_currentScore >= 0) {
            next = ((_currentScore + 10)/ 10) * 10;
        } else {
            next = ((_currentScore + 1)/ 10) * 10;
        }
        if (next > _score) {
            next = _score;
        }
    } else if (_score < _currentScore) {
        if (_currentScore <= 0) {
            next = ((_currentScore - 10) / 10) * 10;
        } else {
            next = ((_currentScore - 1) / 10) * 10;
        }
        if (next < _score) {
            next = _score;
        }
    } else {
        next = _score;
    }
    return next;
}

-(void)updateDisplay {
    if (_score != _currentScore) {
        SKAction *wait = [SKAction waitForDuration:kAnimationDelay];
        SKAction *update = [SKAction runBlock:^() {
            _currentScore = [self computeNextScore];
            _scoreLabel.text = [NSString stringWithFormat:@"%i", _currentScore];
        }];
        SKAction *checkAgain = [SKAction performSelector:@selector(updateDisplay) onTarget:self];
        [self runAction:[SKAction sequence:@[wait, update, checkAgain]] withKey:kAnimationKey];
    } else {
        [self removeActionForKey:kAnimationKey];
    }
}

@end

Context

StackExchange Code Review Q#79254, answer score: 4

Revisions (0)

No revisions yet.