iBeaconでcocos2dと連携する方法 | アドカレ2013 : SP #18

2013.12.18

自己紹介

初めまして、来年度からクラスメソッドで勤務予定の荒川 靖久と申します。

現在はインターンでiOS関係のお仕事をさせて頂いております。ブログ記事もiOSの話題中心になるかと思います。

以前からゲームアプリを作っていたこともあり、ゲーム関連の記事も書けたら良いなと考えています。特にcocos2d frameworkが好きでcocos2d-xの勉強会に最近は出没したりします。

先日【iOS勉強会】iBeaconでできること – Developers.IO Meetup 02の最後のLT枠でiBeaconとcocos2dを絡めたネタをお話しさせて頂きました。

iBeaconは現在多くのiPhoneアプリ開発者から注目を浴びている技術の一つではありますが、まだまだ提案や試作段階の物が多いです。試してみたはいいけれども、よくわからなくてすぐ諦めたという方もいるかもしれません。

そこで数多くいるフロントエンジニア・ゲームエンジニアの方にiBeaconにもっと興味を持ってもらいたいということで今回はLTで紹介したアプリを一から作成する手順を紹介します。

開発環境構築(Macのみ)

  • Xcodeのインストール
  • Provisioningファイルに登録されたテストデバイス(実機2台)を用意
  • cocos2d for iPhoneのインストール

Xcodeのインストールはこちらから行って下さい。

実機の用意は省略します。

cocos2d for iPhoneは公式サイトのDownloadのタブからOlder versions Stable versionの箇所にあるcocos2d-iphone-2.1.tar.gzを任意のディレクトリにダウンロードし、ダブルクリックして解凍します。解凍したディレクトリをFinderで見てみます。

cocos2d-1

そしてターミナル.appを起動(見つからなければmac画面右上の虫眼鏡アイコンを押し、出て来た検索ウィンドウに「ターミナル」と入力しEnter)し、

cd(半角スペース)」と打ちます。

その後Finderで表示されているディレクトリをターミナルへドラッグ&ドロップします。

cocos2d-2

 

cd のあとに~/(ダウンロードしたファイルの場所)/cocos2d-iphoneと表示されたらEnterを押します。

その後「./install-templates.sh」と入力してEnterを押し、インストール完了(ok)のメッセージが表示されれば正常にインストールができています。

これでXcodeのプロジェクトからcocos2d for iPhoneのテンプレートが選択出来るようになります。

プロジェクトを作成する(Hello World)

Xcodeを起動し、Welcome to Xcodeと表示されたら、その下にある「Create a new Xcode project」を選択して下さい。

ここで正常にcocos2dのインストールが行えていればiOSの欄に「cocos2d v2.x」の項目が追加されているはずです。

選択したら一番左の「cocos2d iOS」を選択しNextを押します。以降は通常のプロジェクト作成と同じなので省略します。

プロジェクトが作成できたらまずXcodeメニュー画面左上のRunボタンを押すとこのように表示されます。

cocos2d-3

 

これでcocos2dの基本的な導入はおしまいです。

次にcocos2dのプロジェクトにCoreLocation.frameworkを導入します。

cocos2d-4

 

これでiBeaconを実装するための準備が整いました。

バックグランドでも処理を続けたい場合はGeneralの隣のタブ、Capabillitiesのバックグラウンドモードをオンにして、Location updatesにチェックを入れます。

 

今回は標準で用意されているHelloWorldLayer.h(Hello Worldが表示されているシーン)にCoreLocation.frameworkを追加していきます。

#import <GameKit/GameKit.h>

// When you import this file, you import all the cocos2d classes
#import "cocos2d.h"
#import <CoreLocation/CoreLocation.h>

// HelloWorldLayer
@interface HelloWorldLayer : CCLayer
<GKAchievementViewControllerDelegate,
GKLeaderboardViewControllerDelegate,
CLLocationManagerDelegate>
{
}

@property (nonatomic, strong) CLLocationManager *locationManager_;
@property (nonatomic, strong) NSUUID *proximityUUID_;
@property (nonatomic, strong) CLBeaconRegion *beaconRegion_;

// returns a CCScene that contains the HelloWorldLayer as the only child
+(CCScene *) scene;

@end
  • #importを追加
  • CLLocationManagerDelegateを追加
  • propertyを3つ追加

を行いました。

各プロパティの説明については[iOS 7] 新たな領域観測サービス iBeacon を使ってみるに詳細が書かれていますので、気になる方は参考にして下さい。

次にHelloWorldLayer.mに処理を記述していきます。

// Import the interfaces
#import "HelloWorldLayer.h"

// Needed to obtain the Navigation Controller
#import "AppDelegate.h"

#pragma mark - HelloWorldLayer

// ターミナルから $ uuidgen
static NSString * const kBeaconUUID         = @"AA9D2576-7392-4763-AB84-6C30F8A8D366";
// 任意の識別子
static NSString * const kBeaconIdentifier   = @"com.classmethod.testCocosBeacon";
// 建物などの識別子(16ビットまでの任意の自然数)
static NSUInteger const kTargetBeaconMajor   = 1;
// 区画などの識別子(16ビットまでの任意の自然数)
static NSUInteger const kTargetBeaconMinor   = 1;

// HelloWorldLayer implementation
@implementation HelloWorldLayer

// Helper class method that creates a Scene with the HelloWorldLayer as the only child.
+(CCScene *) scene
{
	// 'scene' is an autorelease object.
	CCScene *scene = [CCScene node];
	
	// 'layer' is an autorelease object.
	HelloWorldLayer *layer = [HelloWorldLayer node];
	
	// add layer as a child to scene
	[scene addChild: layer];
	
	// return the scene
	return scene;
}

// on "init" you need to initialize your instance
-(id) init
{
	// always call "super" init
	// Apple recommends to re-assign "self" with the "super's" return value
	if( (self=[super init]) ) {
		
		// create and initialize a Label
		CCLabelTTF *label = [CCLabelTTF labelWithString:@"Hello World" fontName:@"Marker Felt" fontSize:64];

		// ask director for the window size
		CGSize size = [[CCDirector sharedDirector] winSize];
	
		// position the label on the center of the screen
		label.position =  ccp( size.width /2 , size.height/2 );
		
		// add the label as a child to this Layer
		[self addChild: label];
		
		
		
		//
		// Leaderboards and Achievements
		//
		
		// Default font size will be 28 points.
		[CCMenuItemFont setFontSize:28];
		
		// to avoid a retain-cycle with the menuitem and blocks
		__block id copy_self = self;
		
		// Achievement Menu Item using blocks
		CCMenuItem *itemAchievement = [CCMenuItemFont itemWithString:@"Achievements" block:^(id sender) {
			
			
			GKAchievementViewController *achivementViewController = [[GKAchievementViewController alloc] init];
			achivementViewController.achievementDelegate = copy_self;
			
			AppController *app = (AppController*) [[UIApplication sharedApplication] delegate];
			
			[[app navController] presentModalViewController:achivementViewController animated:YES];
			
			[achivementViewController release];
		}];
		
		// Leaderboard Menu Item using blocks
		CCMenuItem *itemLeaderboard = [CCMenuItemFont itemWithString:@"Leaderboard" block:^(id sender) {
			
			
			GKLeaderboardViewController *leaderboardViewController = [[GKLeaderboardViewController alloc] init];
			leaderboardViewController.leaderboardDelegate = copy_self;
			
			AppController *app = (AppController*) [[UIApplication sharedApplication] delegate];
			
			[[app navController] presentModalViewController:leaderboardViewController animated:YES];
			
			[leaderboardViewController release];
		}];

		
		CCMenu *menu = [CCMenu menuWithItems:itemAchievement, itemLeaderboard, nil];
		
		[menu alignItemsHorizontallyWithPadding:20];
		[menu setPosition:ccp( size.width/2, size.height/2 - 50)];
		
		// Add the menu to the layer
		[self addChild:menu];
        
        [self startMonitoring];

	}
	return self;
}

// on "dealloc" you need to release all your retained objects
- (void) dealloc
{
	// in case you have something to dealloc, do it in this method
	// in this particular example nothing needs to be released.
	// cocos2d will automatically release all the children (Label)
	
	// don't forget to call "super dealloc"
	[super dealloc];
}

#pragma mark GameKit delegate

-(void) achievementViewControllerDidFinish:(GKAchievementViewController *)viewController
{
	AppController *app = (AppController*) [[UIApplication sharedApplication] delegate];
	[[app navController] dismissModalViewControllerAnimated:YES];
}

-(void) leaderboardViewControllerDidFinish:(GKLeaderboardViewController *)viewController
{
	AppController *app = (AppController*) [[UIApplication sharedApplication] delegate];
	[[app navController] dismissModalViewControllerAnimated:YES];
}

#pragma mark - CLLocationManagerDelegate methods

// 画面を表示した時に通知を受け取る
- (void)locationManager:(CLLocationManager *)manager didDetermineState:(CLRegionState)state forRegion:(CLRegion *)region
{
    CCLOG(@"Determine State");
    
    // 観測可能領域内かつ測定可能
    if(state == CLRegionStateInside && [CLLocationManager isRangingAvailable]) {
        [self.locationManager_ startRangingBeaconsInRegion:self.beaconRegion_];
    }
}

- (void)locationManager:(CLLocationManager *)manager didStartMonitoringForRegion:(CLRegion *)region
{
    CCLOG(@"Start Monitoring Region");
    
    if([CLLocationManager isRangingAvailable]) {
        [self.locationManager_ startRangingBeaconsInRegion:self.beaconRegion_];
    }
}

- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region
{
    CCLOG(@"Enter Region");
    
    // 測定開始
    if ([region isMemberOfClass:[CLBeaconRegion class]] && [CLLocationManager isRangingAvailable]) {
        [self.locationManager_ startRangingBeaconsInRegion:(CLBeaconRegion *)region];
    }
}

- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region
{
    CCLOG(@"Exit Region");
    
    // 測定停止
    if ([region isMemberOfClass:[CLBeaconRegion class]] && [CLLocationManager isRangingAvailable]) {
        [self.locationManager_ stopRangingBeaconsInRegion:(CLBeaconRegion *)region];
    }
}

- (void)locationManager:(CLLocationManager *)manager didRangeBeacons:(NSArray *)beacons inRegion:(CLBeaconRegion *)region
{
    CLBeacon *nearestBeacon = beacons.firstObject;
    
    CCLOG(@"%@", [NSString stringWithFormat:@"major:%@\n minor:%@\n accuracy:%f\n rssi:%d",
                  nearestBeacon.major, nearestBeacon.minor, nearestBeacon.accuracy, nearestBeacon.rssi]);
}

- (void)locationManager:(CLLocationManager *)manager monitoringDidFailForRegion:(CLRegion *)region withError:(NSError *)error
{
    if(error.code == kCLErrorRegionMonitoringFailure) {
        CCLOG(@"Fail for Region");
    } else {
        [[[UIAlertView alloc] initWithTitle:@"error"
                                    message:error.description
                                   delegate:nil
                          cancelButtonTitle:@"OK"
                          otherButtonTitles:nil] show];
    }
}

#pragma mark - Private methods

// Beaconによる領域観測を開始
- (void)startMonitoring
{
    if ([CLLocationManager isMonitoringAvailableForClass:[CLBeaconRegion class]]) {
        
        // CLLocationManagerの生成とデリゲートの設定
        self.locationManager_ = [CLLocationManager new];
        self.locationManager_.delegate = self;
        
        // 生成したUUIDからNSUUIDを作成
        self.proximityUUID_ = [[NSUUID alloc] initWithUUIDString:kBeaconUUID];
        // CLBeaconRegionを作成
        self.beaconRegion_ = [[CLBeaconRegion alloc] initWithProximityUUID:self.proximityUUID_
                                                                     major:kTargetBeaconMajor
                                                                identifier:kBeaconIdentifier];
        
        [self.locationManager_ startMonitoringForRegion:self.beaconRegion_];
    }
}

// 指定ビーコンの測定・監視を止める
- (void)stopMonitoring:(CLBeaconRegion *)region
{
    if ([region isMemberOfClass:[CLBeaconRegion class]] && [CLLocationManager isRangingAvailable]) {
        [self.locationManager_ stopMonitoringForRegion:self.beaconRegion_];
        [self.locationManager_ stopRangingBeaconsInRegion:region];
        self.locationManager_.delegate = nil;
        self.locationManager_ = nil;
        self.proximityUUID_ = nil;
        self.beaconRegion_ = nil;
    }
}

@end

以下を順番に実装します。

  • static定数(UUID、BeaconのID、major、minor値)を追加
  • -(id) initメソッドの末尾に[self startMonitoring];を追加
  • CoreBluetoothのデリゲードメソッド6つを実装
  • - (void)startMonitoring- (void)stopMonitoring:(CLBeaconRegion *)regionの追加
  • 実機でデバッグ

    ここで実行する時の注意点を挙げます。

  • 必ず実機で実行してください。シミュレータだと動きません。
  • [iOS 7] 新たな領域観測サービス iBeacon を使ってみるで紹介されているPeripheralをもう1台のデバイスで起動させておきます。そちらがBeaconの送信側、今回のアプリが受信側となります。2台の距離間をBluetooth Low Energyの電波強度で測定できます。
  • デバイスのBluetooth設定をONにして下さい。どんなにコードが正しくてもBluetoothがOFFだと動きません。
  • 送信側と受信側アプリのstatic定数(UUID、BeaconのID、major)は同じにして下さい。観測対象が別々だと検知しません。
  • 上記を満たして成功すると

    cocos2d-5

    こんな感じにログが表示されます。これでPeripheralデバイス(Beacon送信側)との距離をBluetooth Low Energyで測定し、ログを表示することができるようになりました。iBeaconの基本的な実装はこれでおしまいです。

    更に発展させるにはiBeacon Advent Calender - iBeaconとcocos2dを連携してみる - iBeaconでゲームアプリを作ってみたに書きましたので、興味を持って頂いた方はご覧になってみて下さい。