読者です 読者をやめる 読者になる 読者になる

夏までにiPhone アプリつくってみっか!

趣味でiPhone/Androidアプリを開発し、日々勉強した事を書いています。オープンワールド系レースゲームをUnityで開発中です。

【cocos2d】Core Motionを使ってcocos2dゲームをモーションセンサーで操作

cocos2d Objective-C スペースハリアー TravelShooting JP

前回、
【偽スペースハリアー】自家製スペハリfor iPhone完成!これからが本番 - 夏までにiPhone アプリつくってみっか!
で一応完成した偽スペハリですが、まだまだプロトタイプとして実験に使って行きます。

今回は、CoreMotionフレームワークを導入し、加速度センサーやジャイロセンサーなどのモーションセンサーを使ってキャラクターをコントロールできるようにしてみました。

注:ここに書かれている方法だとだんだん座標がずれて行くかもしれません。修正版ソースを載せた下記の記事もあわせてご参照ください。
【Core Motion】モーションセンサー使用時にだんだん座標がずれていくのを防止する - 夏までにiPhone アプリつくってみっか!

タッチ操作によるコントロールは直感的で非常に良いのですが、肝心のメインキャラクターが指に隠れてよく見えないというデメリットがあります。
モーションセンサーを使えば実に広々と画面を見渡す事ができるようになります。
ただし、デメリットとして基本的に姿勢を固定しての操作が求められます。うっかり体の向きを帰るとキャラクターがあらぬ方向へ行ってしまいます。電車の中でのプレイもどうでしょう。加減速時にスーッと左右にキャラクターが動いてしまいそうです。

さて、iPhoneでモーションセンサーを使うにはCore Motionフレームワークを使用します。
cocos2dのテンプレートから作成したプロジェクトには含まれていないのでTARGETSのGeneralタブからCoreMotion.frameworkを追加します。

ヘッダファイルは です。

まず最初にやる事はCMMotionManagerのインスタンスの作成です。
このインスタンス複数作るとパフォーマンスに影響が出るらしいです。
このCMMotionManagerは加速度センサー、ジャイロセンサーなど個別のセンサーの値を読み出したりできますが、デバイスの向きを調べるにはそのものズバリのCMAttitudeというクラスが用意されていますので、これを使います。CMAttitudeは3種類の方式でデバイスの向きを返してくれますが、ピッチ、ロール、ヨーの3つの角度でデバイスの向き示すオイラー角というやつを使います。他の2つは良くわかりません。

まずは、

_motionManager.deviceMotionAvailable

でデバイスに必要なセンサーが搭載されているかチェックします。搭載されていなければinitは失敗としてnilを返すようにしています。
搭載されていれば、

_motionManager.deviceMotionUpdateInterval = 1/60.0;

でセンサー情報更新の頻度を指定します。
ゲームでは60fpsが基本になると思いますので、それに合わせています。

あとは、

[_motionManager startDeviceMotionUpdates];

とすればモーションセンサーが動き出します。

あとはcocos2dのupdateメソッド内で

CMAttitude* attitude = _motionManager.deviceMotion.attitude;
double rollDegree = CC_RADIANS_TO_DEGREES(attitude.roll);

などとすれはピッチ、ロール、ヨーの角度を度数で知る事ができます。
あとは

CGFloat yMove = rollDegree * (_winCenter.y / MAX_TILT_ANGLE);

などとして、角度を座標に変換します。_winCenterには画面中央の座標が入っています。
MAX_TILT_ANGLEはこの場合片側にどのくらい傾けたときに画面端の座標にするかを決めます。
この値によって操作感を変える事ができます。なお、ピッチ、ロールはiPhoneを立てたときの見方になりますので、横に持つときはx方向がピッチ、y方向がロールになります。
また、同じ横持ちでも右に倒したときと左に倒したときではiPhone本体に対するゲームの座標の向きが反対になりますのでそこはUIDeviceでオリエンテーションを調べて補正する必要があります。

また、忘れていましたが、重要な事がありました。
基準となるデバイスの向きを用意する必要があります。
例えば、ゲームスタート時の向きを基準に現在どれだけ向きが変化したかという感じでその時点のキャラクターの位置を決定します。
私の場合は画面にボタンを置いて、それが押される度にタッチ操作とモーション操作を切り替え、モーション操作にした場合はそのときの向きを基準とするようにしています。

multiplyByInverseOfAttitude:メソッドにより、現在の向きattitudeを基準の向き、_referenceAttitudeからの変化量に変換することができます。(このメソッドを実行するとattitudeの内容が変わります。)

[attitude multiplyByInverseOfAttitude:_referenceAttitude];

もう一つ重要なのがデバイスのロック防止です。基本的にタッチ操作をしていないときはユーザーの操作が無いと見なされて画面が暗くなり、ロックされてしまうので、モーション操作中はその機能を切ってあげると親切です。

[UIApplication sharedApplication].idleTimerDisabled = YES; //画面ロックを防止

なお、モーション操作中でないときは速やかにNOに戻すようAppleはアドバイスしていますのでご注意ください。

モーションセンサーも使わないときは止めておくとバッテリーの消費を抑えられますが、画面のボタン操作でいつでもモーションセンサーを使えるゲームの場合は動かしっぱなしの方が良いと思います。
基準の向きを取得するタイミングを上手く工夫してあげないと前回の向きを引きずり誤動作するかもしれません。私はここでハマりました。コンフィグ画面でON/OFFするゲームでは多分問題ないと思います。

参考までに、私の実装です。タッチ操作用のクラスとインターフェースを統一しています。
これらを管理する操作マネージャークラスが操作モードにより座標を参照するインスタンスを切り替えているので、使う側では現在どちらのモードになっているかを意識する必要はありません。

#import <CoreMotion/CoreMotion.h>
#import "AttitudeManager.h"

#define MAX_TILT_ANGLE 10.0

@implementation AttitudeManager {
    CMMotionManager* _motionManager;
    CMAttitude* _referenceAttitude;
    CGPoint _winCenter; //画面中央の座標
    UIDeviceOrientation _orientation; // デバイスの向き
    NSInteger _counter;
}

- (id)init
{
    if (self = [super init]) {
        _motionManager = [[CMMotionManager alloc] init];
        if (_motionManager.deviceMotionAvailable) {
            _motionManager.deviceMotionUpdateInterval = 1/60.0;
        } else {
            return nil; //モーションセンサーが使えないデバイスの場合はnilを返す
        }
        CGSize winSize = [CCDirector sharedDirector].winSize;
        _winCenter = ccp(winSize.width/2.0, winSize.height/2.0);
        _orientation = UIDeviceOrientationLandscapeLeft; // とりあえずどっちか入れておく
    }
    return self;
}

- (void)startMotionUpdate
{
    [_motionManager startDeviceMotionUpdates];
}

- (void)stopMotionUpdate
{
    [_motionManager stopDeviceMotionUpdates];
}

- (void)startUsing
{
    _referenceAttitude = _motionManager.deviceMotion.attitude;
    [self scheduleUpdate];
}

- (void)stopUsing
{
    [self unscheduleUpdate];
}

- (void)update:(ccTime)delta
{
    CMAttitude* attitude = _motionManager.deviceMotion.attitude;
    [attitude multiplyByInverseOfAttitude:_referenceAttitude];
    double rollDegree = CC_RADIANS_TO_DEGREES(attitude.roll);
    double pitchDegree = CC_RADIANS_TO_DEGREES(attitude.pitch);
    CGFloat xMove = pitchDegree * (_winCenter.x / MAX_TILT_ANGLE);
    CGFloat yMove = rollDegree * (_winCenter.y / MAX_TILT_ANGLE);
    

    UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation];
    if ((orientation == UIDeviceOrientationLandscapeRight) ||
        (orientation == UIDeviceOrientationLandscapeLeft)) {
        _orientation = orientation;
    }

    if (_orientation == UIDeviceOrientationLandscapeRight) {
        xMove *= -1;
        yMove *= -1;
    }
    xMove = MIN(_winCenter.x, MAX(-_winCenter.x, xMove));
    yMove = MIN(_winCenter.y, MAX(-_winCenter.y, yMove));
    self.location = ccpAdd(ccp(xMove, yMove), _winCenter);
}

@end