6
\$\begingroup\$

I have a simple strategy game, and I implemented a simple counter object to count down a number whenever the game ticks. I have heard about something called NSTimer but I haven't messed with it at all yet. I have also heard that you should use Core Animations to move objects around when rendering rather than a tick based system, because tick based movement would be jerky. However I wanted to make sure to handle animation and rendering separately from the underlying game model, so that part doesn't matter as much. This is just to be used in the game model, and the actual animations of everything will be handled by the scene.

I realize I am probably reinventing the wheel here, but I wanted to build something dead simple that I could use in a few places right away, and I wanted the experience. I am hoping most of all that the class is easy to understand, so let me know what you think.

The scene updates the game object according to the following method:

- (void)update:(NSTimeInterval)currentTime {
    CFTimeInterval timeSinceLast = currentTime - self.lastUpdateTimeInterval;

    //update game and hud counters every quarter second
    if (!_game.isPaused) {
        self.countdownInterval += timeSinceLast;
        if (self.countdownInterval > 0.25) {
            [self updateGame];
            [self buildResourceCounters];
            self.countdownInterval = 0;
        }
    }

    self.lastUpdateTimeInterval = currentTime;
    if (timeSinceLast > 1) { // more than a second since last update
        self.lastUpdateTimeInterval = currentTime;
    }
}

And here is the Countdown class which the game will update each tick:

typedef NS_ENUM(NSInteger, CountdownState){
    CountdownNotStarted = 0,
    CountdownCounting,
    CountdownPaused,
    CountdownFinished
};

DTCountdown.h:

#import <Foundation/Foundation.h>
#import "DTCountdownStates.h"

@interface DTCountdown : NSObject

-(id) initWithCount:(int)startCount;

-(void) update;
-(void) startCountdown;
-(void) pauseCountdown;
-(void) restartCountdown;
-(void) cancelCountdown;

@property CountdownState state;

@end

DTCountdown.m:

#import "DTCountdown.h"

@implementation DTCountdown {
    int _currentCount;
    int _startCount;
}

-(id) initWithCount:(int)startCount {
    self = [super init];
    if (self) {
        _startCount = startCount;
        _currentCount = _startCount;
        _state = CountdownNotStarted;
    }
    return self;
}

#pragma mark - Update Loop
-(void) update {
    switch (self.state) {
        case CountdownNotStarted:
            break;
        case CountdownCounting:
            [self doCountdown];
            break;
        case CountdownPaused:
            break;
        case CountdownFinished:
            break;
        default:
            break;
    }
}
-(void) doCountdown {
    if (_currentCount > 0) {
        _currentCount--;
    } else {
        self.state = CountdownFinished;
    }
}

-(void) startCountdown {
    self.state = CountdownCounting;
}
-(void) pauseCountdown {
    self.state = CountdownPaused;
}
-(void) restartCountdown {
    _currentCount = _startCount;
    self.state = CountdownCounting;
}
-(void) cancelCountdown {
    _currentCount = _startCount;
    self.state = CountdownNotStarted;
}

@end
\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

There's one style issue I want to address, and then I'll talk bigger picture.

This comment may turn irrelevant in this specific case, because bigger picture, the better option is probably going to be to use NSTimer, and I'll go over some samples of how to use that, but this comment is still useful for other scenarios, so here it goes:

-(void) update {
    switch (self.state) {
        case CountdownNotStarted:
            break;
        case CountdownCounting:
            [self doCountdown];
            break;
        case CountdownPaused:
            break;
        case CountdownFinished:
            break;
        default:
            break;
    }
}

We have 4 possible conditions (not including default:), but only 2 possible results. What you're doing here is on par with something like this:

NSUInteger foo;
// stuff

switch (foo) {
    case 0: break;
    case 1: [self doCountdown];
    case 2: break;
    case 3: break;
    case 4: break;
    // etc ad naseum
    case NSUIntegerMax: break;
    default: break;
}

Listing EVERY possible value for an NSUInteger in a switch statement in which we only care about 1 (or even a couple) value seems absurd, right? It's no less absurd to do this for an NS_ENUM.

After all, your NS_ENUM is actually backed by some sort of primitive data type. And in this case we have only 4 different values, but it doesn't mean there can't be more. For my current project at work, one of my enums already has 100+ defined values, and will easily have hundreds more by the time the project is done, yet most of the time when I switch on variables of this type, I only care about one specific value, sometimes two or three more.

There are three possible options on how to rewrite this into something better.

  • A simple if-else. If the specific scenario means it's extraordinarily unlikely that we'll only ever care whether or not the variable matches this one specific variable, and we don't need an if-else if-else or anything more complex than a simple if-else, then we're fine to just use an if-else:
if (self.state == CountdownCounting) {
    [self doCountdown];
}

And in this specific case, we don't even need the else.

  • Let conditions with that should execute the same block of code simply fallthrough:
switch(self.state) {
    case CountdownCounting:
        [self doCountdown];
        break;
    case CountdownNotStarted:
    case CountdownPaused:
    case CountdownFinished:
    default:
        break;
}

This one explicitly lists each condition, and this is fine if the enum is well defined, small, and unlikely to contain more than a handful of values. However, this specific syntax will not work with switch statement. You'd have to add the fallthrough keyword to each of those empty cases to achieve the exact same code, and at that point, we're not any better than the original situation of just break-ing from each. Since iOS development will likely move quickly to , I'd recommend against getting into the habit of this sort of switch structure if you're not already in it.

  • Let default: handle the default cases. Any case for which the code you want to be executed matches exactly to the code in default: can simply be omitted.
switch (self.state) {
    case CountdownCounting:
        [self doCountdown];
    default:
        break;
}

Now this is the same logic as the if-else above. It makes it more clear that we only care about this specific case. However, compared to the if-else above, this is easier to add other values and scenarios. And this structure will work perfectly fine exactly as-is barring syntax discrepancies if/when you make the transition to . This is absolutely the style I would recommend, although all three options are better than what you currently have I believe.


Now then, as I mentioned before (and as you mentioned in the question), NSTimer is probably the answer here.

You said you'd heard of it, but not messed with it yet. NSTimer is no different from any of the other Apple classes. It may seem a little confusing or overwhelming at first, but the documentation is great, and it really only takes using it for yourself once or twice to get pretty settled in and comfortable with how the class works, so here's a few pointers on NSTimer.

There are 5 methods for creating a timer. Of these 5, I personally only ever use 1 of them. One of the methods is an init method, where the other 4 are factory methods, convenience methods that do some extra set up for you.

Of these 4 factory methods, there's two groups, scheduled and unscheduled. The two methods that start with the phrase timerWithTimeInterval: create unscheduled timers. The two that start with the phrase scheduledTimerWithTimeInterval: are scheduled timers. The only difference between creating timers with these two groups is that the former need to be scheduled after they've been created while the latter do not. The latter are scheduled to start counting time from the instant they're instantiated.

So let's focus on the two scheduled timers as these are the ones that are really the most useful.

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds
                                 invocation:(NSInvocation *)invocation
                                    repeats:(BOOL)repeats;

+ (NSTimer *)scheduledTimerWithTimerInverval:(NSTimeInterval)seconds
                                      target:(id)target
                                    selector:(SEL)aSelector
                                     repeats:(BOOL)repeats;

So, there are three parts to these timers.

  1. The timer interval between now and when the timer should fire.
  2. The action that should be taken when the timer fires.
  3. Whether or not the action should repeat.

The first method solves part two with a single argument, an NSInvocation class, something I'm not familiar with, but it's basically an Objective-C message rendered static--an action turned into an argument.

The second method solves part two with two arguments. A target and a selector. The target is a reference to the object you want to call a method on, and the selector is the method you want the timer to call on that target.

The method should take a single argument of type NSTimer as the timer object sends itself as the argument to the method it calls. This is particularly useful because NSTimer has a NSDictionary property called userInfo in which you can store any information you want when you create the timer so that the method that gets called can retrieve the information stored.

So, a quick rewrite of your class using NSTimer might look something like this:

In interface:

@property (nonatomic,strong) NSTimer *timer;

In implementation:

- (void)startTimer {
    self.state = CountdownCounting;
    self.timer = [NSTimer scheduledTimerWithInterval:0.25f
                                              target:self
                                            selector:@selector(update:)
                                             repeats:YES];
} 

- (void)restartTimer {
    [self cancelTimer];
    [self startTimer];
}

- (void)cancelTimer {
    if (self.timer.isValid) {
        [self.timer invalidate];
    }
    self.state = CountdownNotCounting;
}

- (void)update:(NSTimer *)timer {
    // logic for whatever you want to happen each time the timer fires
}

In the update: method, you don't need any special logic relating to the timer.

When we call invalidate on the timer, it stops the timer. Because we sent YES for the repeats argument, it will automatically recall update: every 0.25 seconds until we stop the timer.

Although, as you can see, NSTimer is really quite simple to use, and arguably doesn't warrant a wrapper class.

One important thing to watch out for with NSTimer however. While the timer is valid, the timer object holds a strong reference to the value sent for the target: argument. This is important to note because in most cases the timer is a property on a given object with a strong reference to it (so the timer doesn't deallocate) and the object will typically send itself as the target argument. So we're knowingly and intentionally creating a retain cycle for the timer period during which the timer is valid.

This is mostly fine. If we sent NO as the argument for repeats:, then the retain cycle only lasts until the timer fires, at which point the method is called on the object (which is better than a method being called on a deallocated object, which would crash the app). Once the method returns, NSTimer releases its strong reference to the object, the retain cycle is broken, the object deallocates, leaving no strong references to the NSTimer object, so it deallocates, and now everything is fine.

NSTimer also breaks its strong reference when it is invalidated however, so all you have to do to break an NSTimer retain cycle is simply call invalidate on it. This is important to remember when you've sent YES for the repeats: argument.

\$\endgroup\$
1
  • \$\begingroup\$ Wow, that's very thorough @nhgrif impressive! \$\endgroup\$
    – Phrancis
    Jul 11, 2014 at 20:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.