へろへろもへじ

(ブログタイトル募集中)

【Objective-C,iOS】ActionScriptでお馴染みのenterframeライクなコードをObjective-Cで書いてみた

ActionScript(以下AS)で言うところの「enterframe」ってObjective-Cでどうやって書くんだろ?と調べたところ、NSTimerを使って実現するということがわかりました。enterframeってなにそれおいしいの?って方は、こちらのサイトに詳しく書いてありますのでご参照ください。
ActionScript入門Wiki - ENTER_FRAMEイベントとフレームレートを設定)

例えば、60FPS(1秒間に60フレーム更新)を実現しようとしたら実装はこんな感じになるかと思います。

[NSTimer scheduledTimerWithTimeInterval:1.0 / 60
                target:self selector:@selector($_tick:) userInfo:nil repeats:YES];

これだけでも十分といえば十分なのですが、経過時間を取ったり、フレーム数取ったり、停止したりといった処理を毎回個々に実装するのはアレなので、この辺の処理を担い、ASライクに使えるようなクラスを書いてみました。

◆当方の環境
OS X 10.8.2
XCode 4.5.2
・ARCは使っていません。

■TimeLine.h

#import <Foundation/Foundation.h>

@protocol TimeLineDelegate;

/**
 * TimeLineのインターフェースです。
 */
@interface TimeLine : NSObject

/** デリゲートクラス */
@property (assign, nonatomic) id<TimeLineDelegate> delegate;
/** 現在のフレーム数 */
@property (readonly) NSInteger frame;
/** 間隔 */
@property NSTimeInterval interval;
/** タイマーが動いてるかどうか */
@property (readonly) BOOL playing;

/**
 * 初期化を行います。
 *
 * @param fps FPS
 * @return 自身のオブジェクト
 */
- (id)initWithFps:(NSInteger)fps;

/**
 * タイムラインを開始します。
 */
- (void)start;

/**
 * タイムラインを停止します。
 */
- (void)stop;

/**
 * タイムライン開始からの経過時間を返します。
 *
 * @return 開始からの経過時間
 */
- (NSTimeInterval) startIntervalSinceDate;

@end

/**
 * TimeLineのデリゲートプロトコルです。
 */
@protocol TimeLineDelegate <NSObject>

/**
 * フレームが再生される度に実行されるイベントです。
 * フレームレートはfpsプロパティで調整してください。
 */
- (void)onEnterFrame;

/**
 * 指定の間隔毎に実行されるイベントです。
 * デフォルトは1.0secです。
 * 実行間隔をデフォルトから変更したい場合はintervalプロパティで調整してください。
 */
- (void)onInterval;

@end

■TimeLine.m

#import "TimeLine.h"

/** デフォルトインターバル */
const NSTimeInterval DEFAULT_TIME_LNE_INTERVAL = 1.0;

/**
 * TimeLineの無名カテゴリです。
 */
@interface TimeLine ()
{
    /** FPS */
    NSInteger _fps;
    /** タイマー */
    NSTimer *_timer;
    /** 前回のインターバルイベント開始時間 */
    NSDate *_oldTime;
    /** タイムライン開示時間 */
    NSDate *_startTime;
}
@end

/**
 * TimeLineの実装クラスです。
 */
@implementation TimeLine

- (id)initWithFps:(NSInteger)fps
{
    self = [super init];
    if (self == NO){
        return self;
    }
    self.delegate = nil;
    _fps = fps;
    _interval = DEFAULT_TIME_LNE_INTERVAL;
    
    return self;
}

- (void)dealloc
{
    [_oldTime release];
    [_startTime release];
    
    [super dealloc];
}

- (void)start
{
    _startTime = [[NSDate dateWithTimeIntervalSinceNow:0.0] retain];
    _oldTime = [[NSDate dateWithTimeIntervalSinceNow:0.0] retain];
    _playing = YES;
    _timer = [NSTimer scheduledTimerWithTimeInterval:1.0 / _fps
                target:self selector:@selector($_tick:) userInfo:nil repeats:YES];
}

- (void)stop
{
    _playing = NO;
    _oldTime = nil;
    _startTime = nil;
    _frame = 0;
    [_timer invalidate];
}

- (NSTimeInterval)startIntervalSinceDate
{
    return [[NSDate dateWithTimeIntervalSinceNow:0.0] timeIntervalSinceDate:_startTime];
}

#pragma mark --private method
/**
 * FPSに従い、周期的に実行される処理です。
 */
- (void)$_tick:(id)sender
{
    if (_timer.isValid == NO) {
        return;
    }
    
    _frame++;
    
    NSDate *now = [NSDate dateWithTimeIntervalSinceNow:0.0];
    NSTimeInterval since = [now timeIntervalSinceDate:_oldTime];
    
    if (self.interval <= since) {
        // 指定の間隔以上の経過の場合
        [self.delegate onInterval];
	_frame = 0;
	_oldTime = [[NSDate dateWithTimeIntervalSinceNow:0.0] retain];
    }
    
    [self.delegate onEnterFrame];
}

@end

使い方の手順ですが、

1.TimeLineDelegateの実装クラス(デリゲート先)を作成
2.TimeLineのインスタンスを生成する(alloc, initWithFps)
3.TimeLineインスタンス.delegateにデリゲート先(1.で実装したクラス)を設定する
4.startメソッドを呼び出す

をすると、「onEnterFrame」「onInterval」が周期的に呼び出されるようになります。
また、停止したい場合は「stop」メソッドを呼び出します。ざっくりですがこんな感じです。(※詳細はこちらをご参照ください。fukuiretu/enterframe-trial · GitHub

また、NSTimerの使い方のポイントはこちらの記事が大変参考になりました。
A-Liaison BLOG: NSTimerは基本的にretainせずassignでよい


以上、実装してみたものの大して便利ではないかもしれないですが、NSTimerよりも直感的に使えるかと思います。ちなみに、当方ASが達者なわけではございませんorz