工場裏のアーカイブス

素人によるiPhoneアプリ開発の学習記 あと機械学習とかM5Stackとか

SpriteKitでのサウンド再生

ゲーム作成の際にはBGMや効果音などのサウンドも、ゲームを盛り上げるためには是非付けたいものです。本記事では、SpriteKitでサウンドを再生するための方法について調べたことをメモします。

SKActionによるサウンド再生

実はSpriteKitでは、アクションの一つとしてサウンドの再生機能が用意されています。これを利用すると、非常にお手軽に効果音などを鳴らすことが可能です。

具体的な利用法としては、まずプロジェクトにサウンドファイル(例えば”sound.caf”)を追加します。そして以下のようにplaySoundFileNamed:メソッドでサウンド再生アクションを生成し、実行させるだけです。対応しているサウンドファイルの形式については、SpriteKitの公式リファレンスにも記載が無かったのですが、自分が試した限りではcaf、mp3は問題なく再生可能であるようです。

SKAction *playSoundAction = [SKAction playSoundFileNamed:@"sound.caf" 
waitForCompletion:NO];
        
[self runAction:playSoundAction];


なお、メソッドのwaitForCompletion: 引数をNOにすると、 サウンドの再生が開始した瞬間にアクションは完了したとみなされます。引数をYESにすると、サウンドファイルの末尾までの再生が完了して、はじめてアクションは完了したとみなされます。

また、これもアクションの一種である以上、他のアクションと組み合わせることも可能です。例えばrepeatActionForever:と組み合わせると、同じサウンドを延々とループ再生させ続けることも可能です。

音量を調節したり、サウンドファイルの途中から再生したり…といった細かな設定は出来ないようですが、とにかくお手軽なのが良いところです。

通知センターを利用して、ViewController上の AVAudioPlayer をシーンから制御するサンプル

SKActionによるサウンド再生は便利なのですが、例えばアプリ起動中はずっと同じBGMをループ再生し続けて、シーン間の移動が起こる場合にもBGMが途切れないようにする……というケース(拙作のTricolorなど)では実装が困難で悩みました。

色々調べてみたところ、このようなケースは、例えばViewController上でサウンド再生機構(ここではSKActionは利用出来ないので、AVAudioPlayerなど)を用意して、各シーンの処理とは独立してBGMを再生するようにすれば実現出来ることが分かりました。

…しかし、実際にやってみると確かに目的は達せられたのですが、今度はシーン上からBGMのオン、オフを切り替える機能を付けたいと考えたときにまた悩みました。つまりシーンからViewController上のAVAudioPlayerを制御したいのですが、これが一筋縄では行きませんでした。シーンからViewControllerへの参照を取得するような方法が真っ先に浮かびましたが、それはMVCパターンの設計を壊すため推奨されないようです。

そこで、また色々調べてみたところ、通知センター(NSNotification)を利用して、シーンからViewController上の要素を参照なしで制御する方法というのが見つかりました。この方法がベストなのかは分かりませんが、同じようなケースで悩んでいる方の参考になるかもしれませんので、サンプルを作成してみました。

プロジェクトの作成

まず、Xcode(本記事執筆時点:Version 5.1.1)で「SpriteKit」テンプレートを選択してプロジェクトを作成します。そしてプロジェクトに適当なサウンドファイル(例えば”bgm.caf”)を追加します。

ViewControllerの書き換え

ViewController.m を以下のコードに書き換えます(ViewController.h は書き換え不要)

#import "ViewController.h"
#import "MyScene.h"

@import AVFoundation; //AVAudioPlayerを利用するために必要

@interface ViewController()

@property(nonatomic)AVAudioPlayer *bgmPlayer; //プロパティとしてAVAudioPlayerを追加

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    //通知センターの追加
    [[NSNotificationCenter defaultCenter]
     addObserver:self
     selector:@selector(switchBGM:)
     name:@"BGMButton"
     object:nil];
    
    //AVAudioPlayerによるBGM再生開始(サウンドファイル名は適宜書き換え)
    NSURL *bgmURL = [[NSBundle mainBundle] URLForResource:@“bgm" withExtension:@“caf"];
    self.bgmPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:bgmURL error:nil];
    self.bgmPlayer.volume = 0.5;
    self.bgmPlayer.numberOfLoops = -1;
    [self.bgmPlayer prepareToPlay];
    [self.bgmPlayer play];

    // Configure the view.
    SKView * skView = (SKView *)self.view;
    
    // Create and configure the scene.
    SKScene * scene = [MyScene sceneWithSize:skView.bounds.size];
    scene.scaleMode = SKSceneScaleModeAspectFill;
    
    // Present the scene.
    [skView presentScene:scene];
}

//通知センターが呼び出すBGMのオンオフ切り替えメソッドの追加
-(void)switchBGM:(NSNotification *)notification{
    NSDictionary *userInfo = [notification userInfo];
    NSNumber *isBGMPlay = (NSNumber *)[userInfo objectForKey:@"isBGMPlay"];
    
    //MyScene.m からの通知(userInfo に格納されたBOOL値)でBGMをオンオフ
    if([isBGMPlay boolValue] == YES){
        self.bgmPlayer.currentTime = 0;
        [self.bgmPlayer play];
    }
    else [self.bgmPlayer stop];
}

- (BOOL)shouldAutorotate
{
    return YES;
}

- (NSUInteger)supportedInterfaceOrientations
{
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
        return UIInterfaceOrientationMaskAllButUpsideDown;
    } else {
        return UIInterfaceOrientationMaskAll;
    }
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Release any cached data, images, etc that aren't in use.
}

@end


MySceneの書き換え

MyScene.m を以下のコードに書き換えます(MyScene.h は書き換え不要)

#import "MyScene.h"

@interface MyScene(){
    BOOL isPlayBGM; //BGMのオン or オフの状態を表す変数
}

@end

@implementation MyScene

-(id)initWithSize:(CGSize)size {
    if (self = [super initWithSize:size]) {
        self.backgroundColor = [SKColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0];
        isPlayBGM = YES;
        
        //BGMのオン or オフを切り替えるボタン(SKLabelNodeを利用して実装)
        SKLabelNode *BGMButton = [SKLabelNode labelNodeWithFontNamed:@"Gill Sans Light"];
        BGMButton.name = @"BGM";
        BGMButton.text = @"BGM : On";
        BGMButton.fontSize = 50;
        BGMButton.fontColor = [SKColor blackColor];
        BGMButton.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame));
        
        [self addChild:BGMButton];
    }
    return self;
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    SKLabelNode *BGMButton = (SKLabelNode *)[self childNodeWithName:@"BGM"];
    
    CGPoint point = [[touches anyObject] locationInNode:self];
    
    //ボタンがタップされたことを検知したときの処理
    if ([BGMButton containsPoint:point]) {
        if (isPlayBGM == YES) {
            isPlayBGM = NO;
            BGMButton.text = @"BGM : Off";
            
            //ViewController.m の通知センターに、BGMのオフを通知(userInfo に格納したBOOL値で)
            NSDictionary *userInfo = [NSDictionary dictionaryWithObject:
                                      [NSNumber numberWithBool:NO] forKey:@"isPlayBGM"];
            [[NSNotificationCenter defaultCenter]
             postNotificationName:@"BGMButton" object:self userInfo:userInfo];
        }else{
            isPlayBGM = YES;
            BGMButton.text = @"BGM : On";
            
            //同様に、ViewController.m の通知センターに、BGMのオンを通知
            NSDictionary *userInfo = [NSDictionary dictionaryWithObject:
                                      [NSNumber numberWithBool:YES] forKey:@"isBGMPlay"];
            [[NSNotificationCenter defaultCenter]
             postNotificationName:@"BGMButton" object:self userInfo:userInfo];
        }
    }
}

@end


サンプルの実行

ViewController.m、MyScene.mを書き換えてサンプルを実行すると下図左のようになり、BGMも再生されます。そして画面中央のボタン(文字ラベル)をタップするごとに、BGMのオン、オフが切り替わります。

f:id:fleron:20140812005657p:plain

ViewController上で通知センター(NSNotification)を用意し、MySceneでBGMのオン、オフの切り替え操作があったときには、userInfoという変数名のNSDictionaryを介してViewControllerに通知とBGMのオンオフ指示情報が送られるようにしています。そして通知を受け取るごとにswitchBGM:メソッドが実行されて、AVAudioPlayerのオンオフが切り替わります。

SpriteKitでビットマップフォント

※2020/03/07追記:本記事の続きとして、「BMGlyph」というソフトウェアを用いたビットマップフォントの扱い方についてまとめた記事も作成しております。

chemicalfactory.hatenablog.com
 
 
SpriteKit でビットマップフォントを扱う方法がないか模索していたのですが、実はSKLabelNode を用いて、通常の文字ラベルと(ほぼ)同じように扱えることが分かりました。以下ではその方法についてメモしていきます。


まずビットマップフォントの作成ですが、これは何らかのツールを用いる必要があります(自分は「GlyphDesigner」というソフトウェアを用いています。有料ですが、その分使い勝手は非常に良いと思います)。
f:id:fleron:20140509001923p:plain
今回は、↑のスクリーンショットにあるビットマップフォントを、そのまま「Myfont」という名前で出力して用います(ここでは「Myfont.fnt」「Myfont.png」という2つのファイルが出力されます)。


SpriteKitでこれを用いる方法ですが、まず先程出力された2つのファイル(「Myfont.fnt」および「Myfont.png」)をプロジェクトに追加します。後はSKLabelNode のインスタンスを初期化する際に、追加したビットマップフォントをフォント名として指定するだけです。これで通常の文字ラベルと(ほぼ)同じように、ビットマップフォントを扱うことが出来ます。

SKLabelNode *myLabel = [SKLabelNode labelNodeWithFontNamed:@"MyFont"];
myLabel.text = @"Hello, World!";
myLabel.position = CGPointMake(CGRectGetMidX(self.frame),
                                       CGRectGetMidY(self.frame));
[self addChild:myLabel];

試しに、SpriteKitのテンプレートで背景に「Hello, World!”」と白文字で表示する部分を、上記のコードに書き換えて実行してみます。
f:id:fleron:20140509004705p:plain
上図のように、文字がビットマップフォントによって見事に表示されました。…ただし、画面から文字が盛大にはみ出してしまっています。そこでフォントサイズを調整する必要があるのですが、通常の文字ラベルと違って、ビットマップフォントでは例えば以下のコードのようにfontSizeプロパティを設定しても効果が無いようです。

myLabel.fontSize = 30; //ビットマップフォントには効果なし


代わりに、ビットマップフォントでは xScale、yScale プロパティを用いることによって、フォントサイズを調節することが出来るようです。これらのプロパティは、名前通りに文字ラベル全体をx方向、y方向に拡大縮小します。

myLabel.xScale = 0.5;
myLabel.yScale = 0.5;

例えば上記のコードを用いて、先程の「Hello, World!”」をxy方向にそれぞれ0.5倍に縮小してみると、今度は文字がきちんと画面内に収まるようになりました。
f:id:fleron:20140509005827p:plain

ゲームアプリの簡単な骨組み的なモノ(SpriteKit)

(※2020/01/12追記:本記事はObjective-Cを用いた、やや古い記事となります。同構成のアプリをSwiftで作成するための最新記事も公開しています)
chemicalfactory.hatenablog.com



SpriteKitではある画面(シーン)から別の画面へと、アニメーション付きで移動するような処理を、容易に実装可能な仕組みが備わっています。本記事ではそれらを活用して、下図のようにゲームアプリの簡単な骨組み的なモノを作成してみます。

f:id:fleron:20140426002350p:plain

このアプリでは最初タイトル画面が表示され、「情報」をタップすると説明画面に移動します(アプリの説明などを書くことを想定)。また「スタート」をタップするとゲーム画面に移動します。ゲーム画面では10秒のカウントダウンが赤字で表示され、0になるとゲームオーバー画面に強制移動します(制限時間のあるゲームを想定)。ゲームオーバー画面ではゲームをリトライする(ゲーム画面に再移動する)か、タイトル画面に戻るかを選べます。たったこれだけですが、これに肉付けをすれば単純なゲームなら作成出来ると思います。

アプリの実作

SpriteKitテンプレートの準備

まず、Xcode(本記事執筆時点:Version 5.1.1)で「SpriteKit」テンプレートを選択してプロジェクトを作成します。デフォルトで用意されている「MyScene.h/MyScene.m」および「Spaceship.png」は不要なので、削除してしまって構いません。まずプロジェクトにクラスを4つ追加します。名前はそれぞれ「TitleScene」「InfoScene」「GameScene」「GameOverScene」とし、全て SKScene のサブクラスとします。名前の通り、それぞれでタイトル画面、情報画面、ゲーム画面、ゲームオーバー画面を実装します。

タイトル画面(TitleScene)の実装

まず TitleScene.m を以下のコードに書き換えます(TitleScene.h は書き換え不要)

//TitleScene.m

#import "TitleScene.h"
#import "InfoScene.h"
#import "GameScene.h"

@implementation TitleScene

-(id)initWithSize:(CGSize)size {
    if (self = [super initWithSize:size]) {
        self.backgroundColor = [SKColor colorWithRed:0.15 green:0.15 blue:0.5 alpha:1.0];
        
        SKLabelNode *titleLabel = [SKLabelNode labelNodeWithFontNamed:@"Hiragino Kaku Gothic ProN W3"];
        
        titleLabel.text = @"タイトル";
        titleLabel.fontSize = 50;
        titleLabel.fontColor = [UIColor yellowColor];
        titleLabel.position = CGPointMake(CGRectGetMidX(self.frame), self.frame.size.height - 200);
        
        SKAction *action1 = [SKAction scaleTo:1.5 duration:2.0];
        SKAction *action2 = [SKAction scaleTo:1.0 duration:2.0];
        SKAction *sequence = [SKAction sequence:@[action1, action2]];
        [titleLabel runAction:[SKAction repeatActionForever:sequence]];

        
        SKLabelNode *startButton = [SKLabelNode labelNodeWithFontNamed:@"Hiragino Kaku Gothic ProN W3"];
        startButton.name = @"start";
        startButton.text = @"スタート";
        startButton.fontSize = 30;
        startButton.position = CGPointMake(CGRectGetMidX(self.frame), 200);
        
        SKLabelNode *infoButton = [SKLabelNode labelNodeWithFontNamed:@"Hiragino Kaku Gothic ProN W3"];
        infoButton.name = @"info";
        infoButton.text = @"情報";
        infoButton.fontSize = 30;
        infoButton.position = CGPointMake(CGRectGetMidX(self.frame), 130);
        
        [self addChild:titleLabel];
        [self addChild:startButton];
        [self addChild:infoButton];
    }
    return self;
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    SKNode *startButton = [self childNodeWithName:@"start"];
    SKNode *infoButton = [self childNodeWithName:@"info"];
    
    CGPoint point = [[touches anyObject] locationInNode:self];
    
    if (startButton != nil && [startButton containsPoint:point]) {
        SKScene *scene = [[GameScene alloc] initWithSize:self.size];
        SKTransition *transition = [SKTransition crossFadeWithDuration:1.0];
        [self.view presentScene:scene transition:transition];
    }
    if (infoButton != nil && [infoButton containsPoint:point]) {
        SKScene *scene = [[InfoScene alloc] initWithSize:self.size];
        SKTransition *transition = [SKTransition pushWithDirection:SKTransitionDirectionRight duration:0.5];
        [self.view presentScene:scene transition:transition];
    }
}

-(void)update:(CFTimeInterval)currentTime {
    /* Called before each frame is rendered */
}

@end

初期化時にはタイトルラベル(無駄に拡大縮小のアクションを付けています)、およびボタン代わりとなる2つのラベルを生成しています。SpriteKit ではボタン作成用の特別なクラスなどは用意されておらず、ラベルやスプライト画像へのタップをtouchesBegan: メソッドなどで検知し、ボタン代わりにするのがセオリーであるようです。ここでは単純に、タップ位置にラベルが存在するか、その名前(nameプロパティ)は何か、で場合分けをしています。

そして、画面移動の処理を実装しているのは以下のようなコードです。

SKScene *scene = [[GameScene alloc] initWithSize:self.size];
SKTransition *transition = [SKTransition crossFadeWithDuration:1.0];
[self.view presentScene:scene transition:transition];

最初に移動先となるシーンのインスタンスを生成します。また SKTransition クラスでは移動時のアニメーション(種類および時間)を設定します。ここでは crossFadeWithDuration: メソッド(クロスフェード)を設定していますが、それ以外にも様々な種類のアニメーションが用意されています(以下の外部サイト様なども参照)。
Developers.IO   Sprite Kit の画面遷移アニメーションまとめ
そして、presentScene:メソッドによって画面移動が実行されます。このとき、移動前のシーンのインスタンスに対する参照は外れて(特に別のオーナーが存在していない限り)破棄されるようです。

情報画面(InfoScene)の実装

InfoScene.m は以下のコードに書き換えます(InfoScene.h は書き換え不要)

//InfoScene.m

#import "InfoScene.h"
#import "TitleScene.h"

@implementation InfoScene

-(id)initWithSize:(CGSize)size {
    if (self = [super initWithSize:size]) {
        self.backgroundColor = [SKColor colorWithRed:0.15 green:0.15 blue:0.15 alpha:1.0];
        
        SKLabelNode *titleLabel = [SKLabelNode labelNodeWithFontNamed:@"Hiragino Kaku Gothic ProN W3"];
        
        titleLabel.text = @"説明";
        titleLabel.fontSize = 50;
        titleLabel.fontColor = [UIColor greenColor];
        titleLabel.position = CGPointMake(CGRectGetMidX(self.frame), self.frame.size.height - 200);
        
        SKLabelNode *backButton = [SKLabelNode labelNodeWithFontNamed:@"Hiragino Kaku Gothic ProN W3"];
        backButton.name = @"back";
        backButton.text = @"戻る";
        backButton.fontSize = 30;
        backButton.position = CGPointMake(CGRectGetMidX(self.frame), 200);
        
        [self addChild:titleLabel];
        [self addChild:backButton];
    }
    return self;
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    SKNode *backButton = [self childNodeWithName:@"back"];
    CGPoint point = [[touches anyObject] locationInNode:self];
    
    if (backButton != nil && [backButton containsPoint:point]) {
        SKScene *scene = [[TitleScene alloc] initWithSize:self.size];
        SKTransition *transition = [SKTransition pushWithDirection:SKTransitionDirectionLeft duration:0.5];
        [self.view presentScene:scene transition:transition];
    }
}
@end

やっていることはTitleScene.mとほとんど同じです。

ゲーム画面(GameScene)の実装

GameScene.m は以下のコードに書き換えます(GameScene.h は書き換え不要)

//GameScene.m

#import "GameScene.h"
#import "GameOverScene.h"

static const NSTimeInterval gameInterval = 10.0;

@implementation GameScene{
    bool isLastUpdateTimeInitialized;
    bool isTransition;
    NSTimeInterval lastUpdateTime;
    NSTimeInterval gameTime;
}

-(id)initWithSize:(CGSize)size {
    if (self = [super initWithSize:size]) {

        self.backgroundColor = [SKColor colorWithRed:0.8 green:0.8 blue:1.0 alpha:1.0];
        isLastUpdateTimeInitialized = NO;
        isTransition = NO;
        
        SKLabelNode *titleLabel = [SKLabelNode labelNodeWithFontNamed:@"Hiragino Kaku Gothic ProN W3"];
        
        titleLabel.text = @"ゲーム画面";
        titleLabel.fontSize = 50;
        titleLabel.fontColor = [UIColor blueColor];
        titleLabel.position = CGPointMake(CGRectGetMidX(self.frame), self.frame.size.height - 200);
        
        SKLabelNode *timerLabel = [SKLabelNode labelNodeWithFontNamed:@"Hiragino Kaku Gothic ProN W3"];
        timerLabel.name = @"timer";
        timerLabel.text = [NSString stringWithFormat:@"%d", (int)ceil(gameInterval)];
        timerLabel.fontColor = [UIColor redColor];
        timerLabel.fontSize = 30;
        timerLabel.position = CGPointMake(CGRectGetMidX(self.frame), 200);
        
        [self addChild:titleLabel];
        [self addChild:timerLabel];
    }
    return self;
}

-(void)update:(NSTimeInterval)currentTime{
    if(!isLastUpdateTimeInitialized){
        isLastUpdateTimeInitialized = YES;
        lastUpdateTime = currentTime;
    }
    
    NSTimeInterval timeSinceLast = currentTime - lastUpdateTime;
    lastUpdateTime = currentTime;
    
    
    gameTime += timeSinceLast;
    NSTimeInterval timeRemaining = gameInterval - gameTime;
    SKLabelNode *timerLabel = (SKLabelNode *)[self childNodeWithName:@"timer"];
    timerLabel.text = [NSString stringWithFormat:@"%d", (int)ceil(timeRemaining)];
    
    if(timeRemaining <= 0.0){
        if(!isTransition){
            isTransition = YES;
            SKScene *scene = [[GameOverScene alloc] initWithSize:self.size];
            SKTransition *transition = [SKTransition fadeWithDuration:1.0];
            [self.view presentScene:scene transition:transition];
        };
    }
}
@end

初期化時に各ラベル(ここでは「ゲーム画面」という文字、およびカウントダウン表示用)を生成するのはこれまでと同様です。そしてここではupdate:メソッドを用いて「10秒のカウントダウンを行い、0になったらゲームオーバー画面に強制移動する」という処理を実装していますが、その手続きは案外面倒です。以下では具体的なメソッドの中身について説明していきます。

update:メソッドはフレーム毎に呼ばれますが、処理の重さなどによってFPSは変動し得るため、その時間間隔は必ずしも一定になるとは限りません。そのため、単純にupdate:メソッドの呼び出し回数で経過秒数をカウントするような方法は使えません。しかしupdate:メソッドにはcurrentTime引数が存在し、ここにはメソッドが呼ばれた時点でのシステム時刻が渡されます。そこで、前回呼び出し時の時刻を記録しておき、今回呼び出し時の時刻との差分を求めれば、正確な経過秒数をカウントすることが可能になります。

具体的にこれらの処理を実装しているのが、update:メソッド内の以下の部分です。

if(!isLastUpdateTimeInitialized){
    isLastUpdateTimeInitialized = YES;
    lastUpdateTime = currentTime;
}
    
NSTimeInterval timeSinceLast = currentTime - lastUpdateTime;
lastUpdateTime = currentTime;

前回呼び出し時の時刻を記録する lastUpdateTime 変数を用意しておき、初回のupdate:メソッド呼び出し時のみ初期化します(currentTimeを代入)。そして時刻の差分、すなわち前回呼び出し時からの経過秒数を timeSinceLast 変数に代入し、あとは lastUpdateTime を currentTime で更新します。

update:メソッドの続く部分では、カウントダウンを表示する処理を実装しています。

gameTime += timeSinceLast;
NSTimeInterval timeRemaining = gameInterval - gameTime;
SKLabelNode *timerLabel = (SKLabelNode *)[self childNodeWithName:@"timer"];
timerLabel.text = [NSString stringWithFormat:@"%d", (int)ceil(timeRemaining)];

総経過秒数を記録する gameTime 変数を用意しておき、先程の timeSinceLast を加算します。あとは、あらかじめ定数として設定した10秒の制限時間(gameInterval)から、gameTimeを差し引いてラベルに(整数として)表示しているだけです。

update:メソッドの最後の部分では、カウントダウンが0になったときに、ゲームオーバー画面に強制移動する処理を実装しています。

if(timeRemaining <= 0.0){
    if(!isTransition){
        isTransition = YES;
        SKScene *scene = [[GameOverScene alloc] initWithSize:self.size];
        SKTransition *transition = [SKTransition fadeWithDuration:1.0];
        [self.view presentScene:scene transition:transition];
    };
}

ここでのポイントは、フラグ(isTransition)を用いて、カウントダウンが0になった後に presentScene: メソッドが「1回だけ」呼び出されるようにしていることです。こうしないと、意図した通りに画面移動が実行されません。

ゲームオーバー画面(GameOverScene)の実装

GameOverScene.m は以下のコードに書き換えます(GameOverScene.h は書き換え不要)

//GameOverScene.m
#import "GameOverScene.h"
#import "TitleScene.h"
#import "GameScene.h"

@implementation GameOverScene

-(id)initWithSize:(CGSize)size {
    if (self = [super initWithSize:size]) {
        self.backgroundColor = [SKColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:1.0];
        
        SKLabelNode *titleLabel = [SKLabelNode labelNodeWithFontNamed:@"Hiragino Kaku Gothic ProN W3"];
        
        titleLabel.text = @"ゲームオーバー";
        titleLabel.fontSize = 40;
        titleLabel.fontColor = [UIColor whiteColor];
        titleLabel.position = CGPointMake(CGRectGetMidX(self.frame), self.frame.size.height - 200);
        
        SKLabelNode *retryButton = [SKLabelNode labelNodeWithFontNamed:@"Hiragino Kaku Gothic ProN W3"];
        retryButton.name = @"retry";
        retryButton.text = @"リトライ";
        retryButton.fontSize = 30;
        retryButton.position = CGPointMake(CGRectGetMidX(self.frame), 200);
        
        SKLabelNode *backButton = [SKLabelNode labelNodeWithFontNamed:@"Hiragino Kaku Gothic ProN W3"];
        backButton.name = @"back";
        backButton.text = @"タイトルへ戻る";
        backButton.fontSize = 30;
        backButton.position = CGPointMake(CGRectGetMidX(self.frame), 130);
        
        [self addChild:titleLabel];
        [self addChild:retryButton];
        [self addChild:backButton];
    }
    return self;
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    SKNode *retryButton = [self childNodeWithName:@"retry"];
    SKNode *backButton = [self childNodeWithName:@"back"];
    CGPoint point = [[touches anyObject] locationInNode:self];
    
    if (retryButton != nil && [retryButton containsPoint:point]) {
        SKScene *scene = [[GameScene alloc] initWithSize:self.size];
        SKTransition *transition = [SKTransition crossFadeWithDuration:1.0];
        [self.view presentScene:scene transition:transition];
    }
    if (backButton != nil && [backButton containsPoint:point]) {
        SKScene *scene = [[TitleScene alloc] initWithSize:self.size];
        SKTransition *transition = [SKTransition crossFadeWithDuration:1.0];
        [self.view presentScene:scene transition:transition];
    }
}
@end

ここでもやっていることは TitleScene.m とほとんど同じです。

ViewControllerの書き換え

最後に ViewController.m の中身を以下のように書き換えます(ViewController.h は書き換え不要)

//ViewController.m

#import "ViewController.h"
#import "TitleScene.h"

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    SKView * skView = (SKView *)self.view;

    SKScene * scene = [TitleScene sceneWithSize:skView.bounds.size];
    scene.scaleMode = SKSceneScaleModeAspectFill;

    [skView presentScene:scene];
}

- (BOOL)shouldAutorotate
{
    return YES;
}

- (NSUInteger)supportedInterfaceOrientations
{
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
        return UIInterfaceOrientationMaskAllButUpsideDown;
    } else {
        return UIInterfaceOrientationMaskAll;
    }
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Release any cached data, images, etc that aren't in use.
}
@end

書き換えといっても、ヘッダファイルを追加し、viewDidLoadメソッドで最初にタイトル画面(TitleScene)を表示するようにしただけです。これでこの骨組みアプリは完成です。

SpriteKitに触れてみる(2)

SpriteKit Game テンプレートの内容

まずはXcode 5で、SpriteKit Game テンプレートを選んでプロジェクトを作成してみます。
f:id:fleron:20140107230910p:plain

作成されたプロジェクトをビルドして実行してみると、最初は下図左のように、深青色の背景に「Hello, World!」という語句がラベルにより表示されています。そして画面をタップすると、下図右のように戦闘機(?)の画像(スプライト)がその位置に出現して一定速度で回転を続けます。そして画面下部にはノード(文字ラベルやスプライトなど、SpriteKitで描画される要素の総体)の数、およびFPSが表示されています(下図はシミュレータからのキャプチャなのでFPSの値が低いですが、iPhone 5sの実機では、もっと多数のスプライトを表示させてもFPS 60でスムーズに動作します)。

f:id:fleron:20140109233839p:plain

このテンプレートの中身を読み解いてみます。
(参考:SpriteKit の公式リファレンス
 

SpriteKit Game テンプレートの中身の解読

プロジェクトの設定周りについて

まずテンプレートで生成されたプロジェクトを眺めてみると、「SpriteKit.framework」というフレームワークがリンクされています。名前の通りSpriteKitを利用時に必要なフレームワークです。例えばテンプレートを用いずに作成した他プロジェクトでSpriteKitを利用したい場合は、これをリンクする必要があるようです。
f:id:fleron:20140110000522p:plain

またstoryboardを見ると、View のクラスとして「SKView」が設定されています。SKView は UIView の派生クラスであり、SpriteKit を用いたコンテンツを表示するための部品です。
f:id:fleron:20140110002600p:plain

プロジェクトの設定周りで、SpriteKit Game テンプレートの他と違う大きな特徴はこのぐらいです。

ViewController の内容について

いよいよソースコードの中身を見てみます。AppDelegate には、特にSpriteKit特有の記述は無いようですので、まずは ViewController.h に目を向けます。

//ViewController.h

#import <UIKit/UIKit.h>
#import <SpriteKit/SpriteKit.h>

@interface ViewController : UIViewController

@end


「SpriteKit.h」というヘッダをインポートしていますが、それ以外は、ごく標準的に UIViewController を継承した ViewController となっています。特別な派生クラスなどを継承しているわけではありません。続いて ViewController.m に目を向けます。

//ViewController.m

#import "ViewController.h"
#import "MyScene.h"

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    // Configure the view.
    SKView * skView = (SKView *)self.view;
    skView.showsFPS = YES;
    skView.showsNodeCount = YES;
    
    // Create and configure the scene.
    SKScene * scene = [MyScene sceneWithSize:skView.bounds.size];
    scene.scaleMode = SKSceneScaleModeAspectFill;
    
    // Present the scene.
    [skView presentScene:scene];
}

- (BOOL)shouldAutorotate
{
    return YES;
}

- (NSUInteger)supportedInterfaceOrientations
{
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
        return UIInterfaceOrientationMaskAllButUpsideDown;
    } else {
        return UIInterfaceOrientationMaskAll;
    }
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Release any cached data, images, etc that aren't in use.
}

@end


いくつかメソッドがありますが、SpriteKit に関連する部分は「MyScene.h」というヘッダのインポートと、viewDidLoad メソッドの中身だけです。このメソッドの中身を少しずつ見ていくと、まず SKView* 型の変数 skView を宣言して、self.view をキャストして代入しています。

 SKView * skView = (SKView *)self.view;
 skView.showsFPS = YES;
 skView.showsNodeCount = YES;

 
このようなキャストおよび代入が可能なのは、前述のようにstoryboardで、View のクラスとして SKView があらかじめ設定されているためです。以降は skView 変数を介して、self.view を SKView として取り扱うことが可能になります。そして showsFPS および showsNodeCount プロパティでは、アプリ画面下部にノードの数、およびFPSを表示するか否か設定出来ます(試しに NO を代入して実行すると、当然これらの表示は消えます)。

続きを見てみると、今度は MyScene というクラスのインスタンスを生成しています。

// Create and configure the scene.
SKScene * scene = [MyScene sceneWithSize:skView.bounds.size];
scene.scaleMode = SKSceneScaleModeAspectFill;


このクラスは、プロジェクトにデフォルトで存在しているMyScene.h/MyScene.m で 定義されています(「MyScene.h」というヘッダがインポートされている所以)。詳しくは後述しますが、このクラスは SKScene というクラスから派生しており、1つの「シーン」を定義しています。ゲームで言えば「タイトル画面」「実際のゲーム画面」「ゲームオーバー画面」など、それぞれの画面(場面)に当たるのがシーンです。

そして前述したように、各シーンで表示される文字ラベルや画像(スプライト)などの要素に当たるのがノード(SKNode クラスから派生)です。SpriteKit では主にこれらの「シーン」「ノード」という概念によって描画内容を構築して行きます。

そして scaleMode というプロパティでは、シーンと View のサイズが異なるときに、シーンをどのように拡大縮小して View に貼付けるのかを設定出来るようです。ここでは 貼付け先の View(縦画面時)と同じサイズのシーンを生成しているため、そのままでは設定による違いは見えませんが、例えば横画面にしたとき(Viewの幅と高さが入れ替わり、サイズが変わる)には違いが生じます。

そして最後の行では、presentScene メソッドによって、実際に View 上にシーンを表示します。

// Present the scene.
[skView presentScene:scene];


まとめると、 ViewController がやっているのは、MyScene クラスで定義されたシーンを View(SKViewとして扱い) に表示させることだけです。

MySceneの内容について

今度は MyScene クラスの中身に目を向けて、シーンの定義がどのようになっているか見ていきます。まずは MyScene.h から。

//MyScene.h

#import <SpriteKit/SpriteKit.h>

@interface MyScene : SKScene

@end


前述のように、MySceneクラスは SKScene クラスから派生しています。続いて MyScene.m を見てみます。

//MyScene.m

#import "MyScene.h"

@implementation MyScene

-(id)initWithSize:(CGSize)size {    
    if (self = [super initWithSize:size]) {
        /* Setup your scene here */
        
        self.backgroundColor = [SKColor colorWithRed:0.15 green:0.15 blue:0.3 alpha:1.0];
        
        SKLabelNode *myLabel = [SKLabelNode labelNodeWithFontNamed:@"Chalkduster"];
        
        myLabel.text = @"Hello, World!";
        myLabel.fontSize = 30;
        myLabel.position = CGPointMake(CGRectGetMidX(self.frame),
                                       CGRectGetMidY(self.frame));
        
        [self addChild:myLabel];
    }
    return self;
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    /* Called when a touch begins */
    
    for (UITouch *touch in touches) {
        CGPoint location = [touch locationInNode:self];
        
        SKSpriteNode *sprite = [SKSpriteNode spriteNodeWithImageNamed:@"Spaceship"];
        
        sprite.position = location;
        
        SKAction *action = [SKAction rotateByAngle:M_PI duration:1];
        
        [sprite runAction:[SKAction repeatActionForever:action]];
        
        [self addChild:sprite];
    }
}

-(void)update:(CFTimeInterval)currentTime {
    /* Called before each frame is rendered */
}


まずは初期化メソッド(initWithSize:)の中身を見ていきます。ここではまず backgroundColor プロパティによってシーンの背景色(あの深青色)を設定しています。SKColor なるものが色指定に用いられていますが、これは開発対象(iOS向けかどうか)によって UIColor と NSColor を切り替えるだけのマクロのようです。試しに UIColor に書き換えても、全く何事もなく動作します。

self.backgroundColor = [SKColor colorWithRed:0.15 green:0.15 blue:0.3 alpha:1.0];


その続きの部分では、文字ラベル(画面中央に表示されていた「Hello, World!」)を生成しています。

SKLabelNode *myLabel = [SKLabelNode labelNodeWithFontNamed:@"Chalkduster"];
        
myLabel.text = @"Hello, World!";
myLabel.fontSize = 30;
myLabel.position = CGPointMake(CGRectGetMidX(self.frame),
                                       CGRectGetMidY(self.frame));


文字ラベルもノードの一種であり、SKLabelNode というクラス(もちろん SKNode から派生)を用いて扱います。初期化のときにフォントの種類を選択し、プロパティでテキストの内容、フォントサイズ、表示位置を設定し…と、やっていることは分かり易いと思います。ここで用いられている以外にも様々なプロパティがあり、たとえば fontColor というプロパティでは文字の色を変えることが出来ます。

あとは addChild: メソッドによって、ノードをシーンに追加します。シーンを表示した際に、追加した各ノードが設定に従って描画されるようになります。

[self addChild:myLabel];



続いて、touchesBegan: メソッドの中身を見ていきます。ここでは、画面をタップした位置に戦闘機の画像(スプライト)が出現し、回転する動作を定義しています。

SKSpriteNode *sprite = [SKSpriteNode spriteNodeWithImageNamed:@"Spaceship"];
        
sprite.position = location;


スプライトは SKSpriteNode というクラス(もちろん SKNode から派生)を用いて扱います。あらかじめプロジェクトに追加されている「Spaceship.png」をスプライト画像として初期化し、position プロパティによってその表示位置を、画面のタップされた箇所(変数 location)に設定しています。

そしてその続きでは SKAction というクラスが新登場しています。これはSpriteKitにおいて、各ノードに様々なアクション(移動、回転、拡大縮小、アニメーションなどなど)を行わせるための仕組みとなります。

SKAction *action = [SKAction rotateByAngle:M_PI duration:1];
        
[sprite runAction:[SKAction repeatActionForever:action]];
        
[self addChild:sprite];

ここでは rotateByAngle: メソッドで「1秒間かけて、ノードを180°(πラジアン)回転する」というアクションを生成しています。メソッドの第1引数で角度を、第2引数(durationラベル)で一度のアクションにかける秒数を指定します。

そして、先程生成したスプライトに runAction: メソッドを用いてアクションを設定します。ここでは repeatActionForever: というクラスメソッドを介していますが、これによりアクションが何度も繰り返し行われる(ここでは回転し続ける)ようになります(runAction: の引数にアクションを直接指定した場合は、一度だけ行われます)。

あとは先程同様に、スプライトをシーンに追加するだけです。

最後に、update: メソッドです。

-(void)update:(CFTimeInterval)currentTime {
    /* Called before each frame is rendered */
}

 
これは、シーンの毎フレームごとに自動的に呼び出されるメソッドです。毎フレームごとに何らかの共通処理を行う必要などがある場合には、このメソッドに中身を追加します(テンプレートでは特に何もしていないため中身がカラとなっています)。

テンプレートの解読は以上となります。そこまで難解な点はなく、比較的中身が理解し易いテンプレートであると思います。
 

テンプレートを少し改造

テンプレートをただ解読するだけでは面白くないので、少しだけ改造して遊んでみます。MyScene.m の touchesBegan: メソッドの中身を少しだけ書き換えてみます。

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    /* Called when a touch begins */
    
    for (UITouch *touch in touches) {
        CGPoint location = [touch locationInNode:self];
        
        SKSpriteNode *sprite = [SKSpriteNode spriteNodeWithImageNamed:@"Spaceship"];
        
        sprite.position = location;
        
        SKAction *rotate = [SKAction rotateByAngle:M_PI * 2 duration:1];
        SKAction *fadeOut = [SKAction fadeOutWithDuration:1];
        SKAction *remove = [SKAction removeFromParent];

        SKAction *rotateAndfade = [SKAction group:@[rotate, fadeOut]];
        SKAction *sequence = [SKAction sequence:@[rotateAndfade, remove]];
        
        [sprite runAction:[SKAction repeatActionForever:sequence]];
        
        [self addChild:sprite];
    }
}

ここでは、 まず fadeOutWithDuration: メソッドで「指定した秒数でノードをフェードアウトさせる」というアクション、そして removeFromParent: メソッドで「親(この場合はシーン)からノードを除去する」というアクションを生成しています。

そして、その直後で group: メソッドを用いていますが、このメソッドは「引数(配列)として与えられたアクションをまとめて、同時に行わせる」というアクションを生成します。ここでは「1秒間かけて、ノードを回転させながらフェードアウトさせる」というアクション(変数 rotateAndFade)を生成していることになります。

続いて sequence: メソッドを用いていますが、このメソッドは「引数(配列)として与えられたアクションを順番に行わせる」というアクションを生成します。すなわち、最終的には「1秒間かけてノードを回転させながらフェードアウトさせた後に、そのノードをシーンから除去する」というアクション(変数 sequence)を生成していることになります。

これをビルドして実行してみると、画面をタップした位置に戦闘機が出現しますが、それらは回転しながらフェードアウトして最終的には消滅します。消滅時にはノードがシーンから除去されるので、画面下部に表示されるノード数はきちんと減少します。

f:id:fleron:20140121234541p:plain

group: メソッド、sequence: メソッドを用いて様々なアクションを組み合わせることで、かなり複雑な動きやアニメーションも、各ノードに設定出来るようになると思います。

SpriteKitに触れてみる(1)

「SpriteKit Game」テンプレートの追加

iOS 7以降向けのアプリ開発に対応したXcode 5から、「SpriteKit Game」なる新しいテンプレートが追加されました。
f:id:fleron:20140107230910p:plain

最近までスルーしてしまっていたのですが、調べてみると、SpriteKit はApple社純正の2Dゲーム開発用フレームワークであり、スプライト(キャラクタの画像など)を容易に画面表示させて、移動させたりアニメーションさせたりすることが出来るようです。更には、物理エンジンやパーティクルシステムまで搭載しているので、これ1つでかなり手の込んだアプリが作成出来そうです。

このようなフレームワークとしては、cocos2d などの強力な先発品が既にある(私も「門清ドリルi」の制作などに使用)のですが、サンプルソースなどを少し齧ってみた印象だと、SpriteKit の方が私のような趣味レベルのプログラマには取っ付き易そうな印象を受けました(特に物理エンジン関連)。例えば下図のように、箱が物理法則に従って落下し積み重なるようなアプリも比較的容易に実装することができ、応用次第で色々面白いモノが作れそうな感触です。
f:id:fleron:20140108004436p:plain

非常に興味を惹かれたので、しばらくは SpriteKit について勉強してみたいと思います。そして学んだことを本ブログにちょくちょくメモして行きたいと思います。

(つづく…)

自作アプリのApp Storeへのリリース手順メモ(3)

本記事は、自作アプリのApp Storeへのリリース手順メモ(2)の続きとなります。ここでは、iOS Dev Centerからプロビジョニングファイルを取得し、実際にアプリをXcode(本記事執筆時点のバージョンは5.0)からApp Storeへアップロードするまでの手順についてメモして行きます。ラストスパート!

※これは2013/10/20時点の情報であり、今後iOS Dev Centerの画面レイアウトの変更や、手順の変更などが生じる可能性もあります。

プロビジョニングファイルの取得

App IDの登録

アプリをApp Store経由で配布するためには、iOS Dev Centerからプロビジョニングファイルを取得して、Xcodeにインストールする必要があります。そこで、ここからは iOS Dev Center における作業となります。まずサインインをして「Certificates, Identifiers & Profiles」をクリックします。
f:id:fleron:20131020171616p:plain
 
ページが移動したら「iOS Apps」の欄にある項目のどれかをクリックします。
f:id:fleron:20131020225133p:plain
 
ページが移動したら、まずはアプリのApp IDを登録します。左側のメニューから「App IDs」を選択して、右上の方にある「+」ボタンをクリックします。
f:id:fleron:20131020203004p:plain
 
「Resister iOS App ID」というページに移動しますので、必要な項目を埋めていきます。
f:id:fleron:20131020215306p:plain

  • 「App ID Description」欄の「Name」には、分かりやすい名前を適当に入力します(コンマやピリオドなどの記号は使えないようです)。
  • 「App ID Prefix」欄には、最初から「Value」に変更不可能な値が表示されているはずです。
  • 「App ID Suffix」欄では「Explicit App ID」にデフォルトでチェックが入っているので、その下にある「Bundle ID」に、XcodeプロジェクトのInfo.pristの「Bundle identifier」の値と同じ文字列を入力します。
  • 「App Services」はひとまずデフォルトのままで問題はないようです。後で変更することも可能です。

入力が完了したら「Continue」ボタンをクリックします。これでApp IDの登録は完了です。
 

プロビジョニングプロファイルの生成

続いて、左側のメニューから「Distribution」を選択し、先程同様に右上の方にある「+」ボタンをクリックします。
f:id:fleron:20131020172403p:plain
 
下図のように「What type of provisioning〜」ページに移動しますので、「Distribution」欄の「App Store」にチェックを入れて「Continue」ボタンをクリックします。
f:id:fleron:20131020184756p:plain
 
下図のように「Select App. ID.」ページに移動しますので、先程登録したApp IDを選んで「Continue」ボタンをクリックします。次は「Select certificates.」ページに移動します。チェックの入ったプロファイル名が表示されるはずなので、そのまま「Continue」ボタンをクリックします。
f:id:fleron:20131020234435p:plain
 
下図のように「Name this profile〜」ページに移動します。この「Profile Name」がプロビジョニングプロファイルの名前となるので、分かりやすい名前を入力します(自分は前述の Bundle identifier と同じ名前にしてしまっています)。入力したら「Generate」ボタンをクリック。
f:id:fleron:20131020234452p:plain
 
プロビジョニングプロファイルが無事生成されたら、「Download」ボタンを押してそのファイルをダウンロードします。これで iOS Dev Center における作業はひとまず完了です。
f:id:fleron:20131023120900p:plain
 
ダウンロードが完了したら、ファイルをダブルクリックすると、Xcodeにプロビジョニングプロファイルがインストールされます。確認するためにはXcodeメニューの「Xcode」→「Preference」を選択します。Preferenceのウインドウが開きますので「Accounts」アイコンをクリックし、「Apple ID」の欄にある「View Details」ボタンをクリックします。Xcodeにインストールされているプロビジョニングプロファイルの一覧を見ることが出来ます。
 

アプリのアーカイブ生成および提出

ここからはXcode上での作業となります。アプリのプロジェクトを開いて、プロジェクト設定の「Info」を表示します。そして下図の赤枠で囲んだ「+」ボタンをクリックし、「Dupulicate "Release" Configuration」をクリックします。
f:id:fleron:20131020174037p:plain
 
「Configurations」に新しい項目(デフォルトの名前は「Release copy」となるはず)が作られます。ここでは名前を「Distribution」と変えてみます。
f:id:fleron:20131020174417p:plain
 
今度はプロジェクト設定の「Build Setting」を表示します。「Code Signing」欄の「Code Signing Identity」に先程作った Distribution が追加されているので、その値として「iPhone Distributon:〜」(証明書)を選択します(一度アプリのリリース経験があり、かつ、iOS Developer Programの更新をきちんと行っていれば、有効な証明書がXcodeにインストールされているはずです)。
f:id:fleron:20131020180412p:plain
 
Distribution のすぐ下にある「Any iOS SDK」の値に「iOS Distribution」を選択します。
f:id:fleron:20131020180429p:plain
 
今度は、Xcodeの左上に表示されているプロジェクト名(Run、Stopボタンの右側)をクリックします。ポップアップが表示されるので「Edit Scheme」をクリックします。
f:id:fleron:20131020182135p:plain
 
下図のような画面が表示されますので、左側のリストから「Archive」を選び、「Build Configuration」を先程作った Distribution に設定して、「OK」ボタンをクリックします。
f:id:fleron:20131020182150p:plain

あとは、Xcodeのメニューから「Product」→「Archive」を選択します(このとき、ビルドターゲットをiOSシミュレーターにしていると、「Archive」が選択出来ないようです。必ずビルドターゲットはiOS Deviceにしておく必要があるようです)。これにより、アプリのアーカイブが生成されます(アプリはこのアーカイブという形式でApp Storeにアップロードされます)

アーカイブの生成が完了するとオーガナイザが開きます。右側に「Validate」「Distribute」という2つのボタンがありますので、まずは「Validate」をクリックします。するとiTunes Connectのログイン画面が表示されるので、ユーザー名およびパスワードを適切に入力して「Next」をボタンをクリックします。続いて下図のような画面が表示されますので「Provisioning Profile」に先程インストールしたプロビジョニングプロファイルを設定し「Validate」ボタンをクリックします。
f:id:fleron:20131020214057p:plain
Validateとは要するに、アプリが正しくビルドされているか確認する手続きです。しばらく待ち時間があった後に、何の問題も無ければ以下のような文章が表示されるので、「Finish」ボタンをクリックします。

No issue were found in (アプリ名).
(アプリ名) has passed validation and may be submitted to the App Store.

 
オーガナイザの最初の画面に戻りますので、今度は「Distribute」ボタンをクリックします。すると以下のような画面が表示されるので、そのまま「Next」ボタンをクリックします。
f:id:fleron:20131020222829p:plain
 
続いて「Validate」のときと同様に iTunes Connectのログイン→「Provisioning Profile」の設定がありますので、同様に入力します。そして「Submit」ボタンをクリックすると、いよいよApp Storeへアプリのアップロードが始まります。しばらく待ち時間があった後に、無事にアップデートが完了すると以下のような文章が表示されるので、「Finish」ボタンをクリックします。非常に長かったですが、これで手続きはひとまず全て完了です。

No issue were found in (アプリ名).
(アプリ名) has passed validation and has been submitted to the App Store for further review.

 
試しに再度iTunes Connectで状態を確認してみると、statusが「Waiting For Review」に変わっています。あとはApple社による審査が無事通ることを祈りましょう(審査の状況や結果は、登録しておいたメールアドレスに送られてきます)。
f:id:fleron:20131020223916p:plain

自作アプリのApp Storeへのリリース手順メモ(2)

本記事は、自作アプリのApp Storeへのリリース手順メモ(1)の続きとなります。ここでは、主にアプリをリリースする際にiTunes Connect上で行う手続きについてメモして行きます。

※これは2013/10/20時点の情報であり、今後iTunes Connectの画面レイアウトの変更や、手順の変更などが生じる可能性もあります(2014/11/20追記:実際にこれらの大きな変更が起こりました。変更点についてまとめた記事を作成しました)。


 

契約、税金、銀行情報の登録(有料アプリ配布の場合)

有料のアプリを配布する場合には、あらかじめiTunes Connectで契約、税金、銀行の情報を登録しておく必要があります。その手順について、画像付きで非常に分かりやすく解説している記事がありましたので、こちらを参照(丸投げ)。
Smartphone-Zine iPhone/iPad/iOSアプリのリリース手順

なお、銀行情報を登録する際には「Zengin Code」なるものを入力するように言われますが、これは全銀コード(正式には統一金融機関コードと言うらしい)というもので、「4桁の数字(銀行コード)-3桁の数字(支店コード)」という形式で銀行名と支店名を指定出来ます。全銀コードはこちらの金融機関コード・銀行コード検索などで調べることが出来ます。例えば、自分は住信SBIネット銀行のレモン支店を登録しようと思ったので、「0038-104」と登録したらキチンと出てきました(……レモン支店ってすごい名前だけど、正式名称なんですよね(笑))。
 

アプリの登録申請に必要な情報の入力

以下では、iTunes Connect上でアプリの登録申請に必要な情報を入力し、アプリをアップロードする準備を整えるまでの手順についてメモしていきます。

まずiTunes connectにログインして「Manage Your Applications」をクリックします。
f:id:fleron:20131020105808p:plain
 
ページが移動したら、左上の方にある「Add New App」ボタンをクリックします。
f:id:fleron:20131020105822p:plain
 
「App Information」というページに移動しますので、必要な項目を埋めていきます。
f:id:fleron:20131020105903p:plain

  • 「Default Language」ではデフォルトの言語を入力します。これをEnglishにしておき、後で日本向けの設定を追加することにより、日本のApp Storeでは日本語でアプリの説明などが表示され、それ以外の国では英語で表示されるように出来ます(方法については後述。もちろん、説明文などは自力で日本語版と英語版の両方を書く必要があります)。デフォルトをJapaneseにするという選択肢もありますが、これだとどんな国のApp Storeでも日本語の説明が出てしまうようです。完全に日本語オンリーのアプリであり、日本以外はターゲットにしない!というのであればJapaneseでも良いと思いますが、可能な限りはEnglishにしておくのが望ましいと思います(もちろんアプリの側でも、ローカライズなどにより日本語/英語の両方に対応するのが望ましい)。

  • 「App Name」にはアプリの名前を入力します。

  • 「SKU Number」には適当な英数字の文字列を入力して構わないようですが、自分のリリースしたアプリ同士で被りがないようにする必要があるそうです。

  • 「Bundle ID」は選択可能なものを選びます(一度リリース経験があれば、選択可能なものがあるはず)

  • 「Bundle ID Suffix」には、アプリのXcodeプロジェクトのinfo.plistにある「Bundle Identifier」と同じ英数字の文字列を入力するようです。自分はID Suffixの方を先に決めて、その後にinfo.plistのIdentifierを同じ文字列に書き換えています(例えば「ドメイン名+アプリ名」みたいな文字列が良く用いられるそうです。chemicalfactory.appname など)。これで問題が無かったのですが、一応Identifierを書き換えた後でも、Xcodeでのアプリのビルドや実機への転送がキチンと通るかは確認した方が良いでしょう。正直、Bundle ID周りはまだ良く分かっていないことが多いので、要調査です。

全ての入力を終えたら、右下の「Continue」ボタンをクリックします。
 
「Select the availability〜」というページに移動します。ここではアプリのリリース予定日および価格を設定します。
f:id:fleron:20131020113708p:plain

  • 「Availability Date」ではアプリのリリース予定日を入力するのですが、この項目には注意が必要です。リリース予定日を入力しても、その日付通りにアプリがApp Storeに並ぶ、とは限りません。ご存知の通りiPhoneアプリは登録申請後にフリーパスでApp Storeに並ぶのではなく、Apple社による審査が入ります。審査にはそれなりの時間が掛かりますし、アプリに不備などがあるとリジェクトされて修正を要求されますので、更に時間が掛かります。そのため入力したリリース予定日よりも、審査に通過する日が後になってしまう可能性があり、更にこの場合はApp Storeの新着枠にアプリが表示されなくなってしまうようです。アプリが注目される機会を失うのは非常に痛いですね。そこで、リリース予定日は後で変更可能ですので、最初は余裕をもって1ヶ月ぐらい先の日付に設定しておき、審査が早めに通過したら、その日の少しだけ先の日付に調整し直すというテクニックがあるようです。
    • ※後からリリース予定日を変更するためには、まずiTunec Connectにログインして「Manage Your Apps」を選択します。ページが移動したら「iOS App Recent Activity」の欄から対象となるアプリを選択します(アイコンをクリック)。そして「App Information」欄の右側にある「Rights and Pricing」と書かれた青いボタンをクリックすれば編集ページに行けます。

  • 「Price Tier」ではアプリの価格を設定します。無料にする場合には「Free」を選べばOKです。

  • 有料にする場合には、どんな価格でも自由に設定出来るわけではなく、あらかじめ用意された価格リストから選択することになります。「View Pricing Matrix」というリンクをクリックすると表示される一覧表を参照して、設定したい価格(「Tier 1」など)を選択します。※基準はドルであるため、日本円による価格は為替次第でしばしば改定されるようです。最近まで1ドル=85円というレートが基準でしたが、2013/10/18に円安を反映して1ドル=100円というレートに改定され、日本版アプリは実質的な値上げとなりました。

  • 「Discount for Educational Institutions」は、教育機関向けにアプリ価格の割引を行うか(いわゆる学割?)の選択です。特に必要がなければチェックは外して良いと思います。

  • 「Custom B2B App」は、通常のApp Storeには非公開でアプリを配布するための法人向けサービスであるようです。これも個人作成のアプリであれば、特に必要はないはずなのでチェックは外しておいて良いでしょう。

全ての入力を終えたら、右下の「Continue」ボタンをクリックします。
 
アプリの説明を入力するページに移動します(長いですので、下のスクリーンショットに収まりきっていないですが)。必要な項目を埋めていくのですが、先程「Default Language」をEnglishにした場合には全て英語で入力する必要があります。
f:id:fleron:20131020124035p:plain

  • 「Version Information」欄
    • 「Version Number」にはアプリのバージョン数を入力します。初めて登録するアプリなら「1.0」とすれば良いと思います。
    • 「Copyright」はアプリの著作権所有者を入力します。私は自分のハンドルネーム(fleron)を入力しています。
    • 「Category」ではApp Storeでアプリが登録されるカテゴリ(「教育」とか「ゲーム」とか)を選択します。「Primary Category」と「Secondary Category (Optional)」の2つまで選択出来ますので、そのアプリに適していると思うカテゴリを選びます。
    • 「Rating」では様々な項目がありますが、要するに、アプリ中に子供にとって有害な描写(暴力表現、性的表現など)があるかというチェックです。心当たりが無ければ全て「None」を選択して構いません。
    • 「Made for Kids (Optional)」は、特に子供向けに開発されたアプリであることを明示するためのオプションです。特に必要がなければチェックは外しておいて良いでしょう。
    • 「Description」では、App Storeで表示されるアプリの説明文という、非常に重要な項目を記入します。もちろん「Default Language」がEnglishなら英語で書く必要があります。辞書を片手に頑張るなり、英語に詳しい知り合いに支援してもらうなりしましょう。英語版App Storeを見て、他アプリの説明文を文例として参考にするという手もあります(なお、日本のApp Store向けに日本語の説明を追加する方法は後述します)。
    • 「Keywords」では、App Storeで検索をかけたときに引っ掛かる対象となるキーワードを、コンマ(,)で区切って入力します(最大100字まで)。アプリが人々の目に触れる機会に直結する項目ですので、アプリと関連するキーワードは出来るだけ多く入力しておいた方が良いでしょう。ただし、あまりにも無関係なキーワードを並べていると判断されると、審査でリジェクトされる可能性もあるようなので要注意です。

  • 「Metadata」欄
    • 「Support URL」ではアプリのサポートサイトのURLを入力します。私は自分のサイト(factory)を入力しています。地味に必須項目であるようですので、自分のサイトなんて持っていない!という方は、自作アプリの情報サイトでも開設した方が良いかもしれません。
    • 「Marketing URL (Optional)」「Privacy Policy URL (Optional)」のように(Optional)が付いた項目は、必要がなければ空欄で構いません。

  • 「Contact Information」欄
    • 「App Review Contact Information」という項目は以前は無かった気がするのですが、いつの間にか新設されたようです。自分の名前、Eメールアドレス、電話番号を入力します。これらの情報はあくまでApple社とのやり取りに使われるものであり、公開はされないようです。なお電話番号についてはカントリーコードを付与して国際電話の番号として入力する必要があるようです。例えば 090-xxxx-xxxx という携帯の電話番号であれば、最初の 0 を取って +81 というコードを付与し、+81-90-xxxx-xxxx と入力すれば良いようです。
    • 「Review Notes (Optional)」「Demo Account Information (Optional)」は例によって、必要がなければ空欄で構いません。
    • App Store Contact Information」ではアプリ作者の情報を入力しますが、最初から必要な項目は埋まっているはずです(もし不足している項目があれば書き足します)。

  • EULA」欄
    • ここではEULA(End User License Agreement)、すなわちユーザーがアプリを使用する上で守るべき条件をまとめた契約書を独自に定義することが出来るそうです。何も設定しなければ、標準で用意されているものが適用される…らしいです。

  • 「Uploads」欄
    • 「Large App Icon」では、1024x1024サイズのアプリのアイコン画像を登録します(以前は512x512だったのですが、いつの間にか変更されたようです)。iTunesArtwork@2x 用にこのサイズの画像を既に作っているはずです。
    • 「3.5-Inch Retina Display Screenshots」では、用意しておいた3.5インチ画面(iPhone 4s以前)向けのスクリーンショット画像を登録します(最大5枚)。
    • 「4-Inch Retina Display Screenshots」では、同様に4インチ画面(iPhone 5以降)向けのスクリーンショット画像を登録します(最大5枚)
    • iPad Screenshots」では、iPad画面向けのスクリーンショット画像を登録します(最大5枚)。前述のように、例えiPhone上での動作のみを想定したアプリであっても、これらのスクリーンショットは必須となるようです。

全ての必要な項目に入力が終わったら、ページ下部にある「Save」ボタンをクリックします。入力に不備があると警告が表示されますので修正します。問題が無いようであれば、ひとまず情報の登録は完了です。
 
アプリの詳細情報のページへと移動します。ステータスが下図のように「Prepare for Upload」となっていればOKです。ページの各所にある「Edit」ボタンをクリックすると、これまでに設定した各項目を編集することが出来ます(出来ない項目もある)。
f:id:fleron:20131020152940p:plain
 
「Default Language」をEnglishにした場合でも、日本のApp Storeでは日本語の説明を表示させたい場合は、下図のように「Choose Another Language」からJapaneseを選択します(右側の小さい「Add」をクリックする必要があることに注意)。日本のApp Store向けのアプリ名、説明、キーワード、スクリーンショットなどを登録する画面が表示されますので、必要事項を記入して(ここはもちろん日本語でOK)「Save」ボタンをクリックすればOKです。
f:id:fleron:20131020160024p:plain
 
いよいよ、アプリをアップロードするための準備に入ります。「Ready to Upload Binary」をクリックします。
f:id:fleron:20131020164359p:plain
 
下図のようなページに移動しますので、「NO」にチェックを入れて「Save」ボタンをクリックします。続いて「You are now ready to upload your binary using Application Loader〜」という文章が書かれたページに移動しますので、内容を確認して「Continue」ボタンをクリックします。これでアップロードの準備が整った状態になります。
f:id:fleron:20131020164715p:plain
 

以下、自作アプリのApp Storeへのリリース手順メモ(3)に続きます。

自作アプリのApp Storeへのリリース手順メモ(1)

せっかくアプリを作成したら、当然App Storeへとリリースして色々な方々に使っていただきたい…のですが、App Storeへのリリース手順は正直かなり複雑で面倒です。過去には色々なサイトや書籍を参考にしながら、なんとかリリースをすることが出来たのですが、この際自分なりの手順メモを作っておこうと思いました(既に一度はリリース経験があり、デベロッパー情報の登録などは済んでいることを前提としたメモです)。

本記事(1)では、下準備的な部分(アイコンおよびスクリーンショットの用意)についてメモして行きます。

アイコンの準備

アプリをリリースする以上は、当然ちゃんとしたアイコンを用意するのはほぼ必須なのですが、結構これが面倒です。対象とする機種(iPhoneiPad)やiOSのバージョンに対応して、様々なサイズのアイコンを指定された名前で用意する必要があります(こちらのiOSアイコンファイル早見表というページなどを参考)。従来は、最初にベースとなる1024 x 1024 のアイコン(iTunesArtwork@2x 用)を作成し、それを縮小してそれぞれのサイズのアイコンを作る(使用ソフトは GIMP)という方法をとっていました。しかし、ベースとなる画像を1枚アップデートするだけで、自動的に必要な全てのアイコンを作成してくれるMakeappiconというサイトがあることを知りました。メールアドレスを入力することにより、全てのアイコンが入った zip ファイルを送ってくれたりするので非常に便利です。

Makeappicon が作ってくれたアイコンを、Xcodeのプロジェクトに追加し(iTunesArtwork および iTunesArtwork@2x からは拡張子を削除しておくこと)、アプリをビルドし直して、無事にアイコンが表示されればOK(もし表示されない場合は、Xcodeのメニューの"Product" → "Clean"を試したり、一度実機(またはシミュレータ)からアプリを削除してみると直るかも)。

アイコンの準備について追記(2014/07/07)

Xcode 5からは「Asset Catalog」という、画像を一括して管理することが可能な機能が追加されました。アイコンの追加設定もこれを用いて、従来よりも楽に行うことが出来るようになりました。

Xcode 5以降でプロジェクトを作成すると、デフォルトで「Images.xcassets」というファイルがプロジェクトに存在しているはずです(自分で追加することも可能です。新規ファイル追加のメニューで「Resource」の項目にあります)。これがAsset Catalogです。
f:id:fleron:20140707212750p:plain

デフォルトの Images.xcassets には「AppIcon」というリソースがあります(左下にある「+」ボタンから「New App Icon」を選択すれば、自分で追加することも出来ます)。ここにアイコンを追加します。

まずは対象とする機種やiOSのバージョンに応じて、右側のチェックボックスから必要となるアイコンサイズを選択します。なお「iOS icon is pre-rendered」という項目は、iOS 6 以前でアイコンに自動的に光沢効果を追加するか、という設定です。

あとは下図のように、各サイズのアイコン画像ファイルをドラッグ&ドロップで、適切な枠線に放り込んで行くだけです。「2x」と描かれた枠線には、Retinaディスプレイ向けに解像度2倍の画像を追加します(従来は「@2x」というキーワードを付与していたような画像です)。誤った画像を放り込んだ場合には、それをクリックしてdeleteキーで削除出来ます。
f:id:fleron:20140707212807p:plain

従来は、アイコン画像の名前は「icon-76@2x」などのように、解像度に応じた規定の名前を付けるルールがありました。しかしAsset Catarogでは、解像度さえ適切であれば、放り込む画像の名前は何でも良いようです。

また「iTunesArtwork」(@2xも)はプロジェクトに追加する必要はなく、アプリの申請時に、iTunes Connect上で登録すれば良いようです。

 

スクリーンショットの準備

アプリの登録申請を行う際には、アプリ画面のスクリーンショット(最大5枚)も必要となります。しかしこれがまた面倒であり、iPhone 4s以前の3.5インチ画面、iPhone 5以降の4インチ画面、そしてiPad画面、これら3種類のサイズ全てについてスクリーンショットを用意する必要があります(2013/10/20時点)。例えiPhone上での動作のみを想定したアプリであっても、iPad画面用スクリーンショットは申請の際に要求されてしまうようです。

それらの実機を全部なんて用意出来ない!と嘆きそうになりますが、実はXcode付属のiPhone(またはiPad)シミュレータでも申請に使えるスクリーンショットを撮ることが出来るので安心です。以下ではその方法についてメモします。

  • 実機の場合は、実は本体上部にあるスリープボタンとホームボタンを同時押し(スリープボタンを押しながら、ホームボタンを押しても良い)することで、任意の画面におけるスクリーンショットを撮ることが出来ます(iOS7では、カシャっとカメラ音が鳴ります)。普通にカメラで撮影した画像と全く同じように、スクリーンショットも「写真」アプリのカメラロールに保存されます。

スクリーンショットについては、それぞれのサイズについて1枚1枚地道に撮り集めるのが、結局は一番手っ取り早そうです。App Store上で最も分かりやすくアプリの中身を伝えるための手段ですので頑張りましょう。なお、ただのスクリーンショットだけではなく、説明文などを付け加えたりするのもアリのようです(もちろん手間はかかりますし、あまり付け加えすぎると審査でリジェクトされてしまいそうですが…)。

 
以下、自作アプリのApp Storeへのリリース手順メモ(2)に続きます。

Xcode 5(iOS 7)におけるUIActionSheet利用時の注意点

先日、ついにiOS 7がリリースされ、それと同時にiOS 7向けアプリ開発に対応したXcode 5がリリースされました。さっそくXcode 5を自機に導入し、旧バージョンで作成したあるプロジェクト(UIActionSheetを利用している)をビルドしてみたのですが、そのとき旧バージョンでは全く無かった警告メッセージが表示されることに気がつきました。

そのプロジェクトでは UIPickerView を載せた UIActionSheet を表示して、手軽にアプリ上で選択肢を入力出来るようにしております(※具体的な方法についてはこちらのリンク先などを参照)。このような UIActionSheet を生成する際には、以下のようにボタンが何も表示されないように設定をします。

//Xcode 5以前のバージョンでは、これでも問題はない。
[[UIActionSheet alloc] initWithTitle:nil delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles:nil];

Xcode 5以前のバージョンでは、この書き方で何の問題も起こりませんでした。しかしXcode 5でこの記述をすると、一応動作はしますが下記のような大量の警告メッセージが表示されます。

CGContextSetFillColorWithColor: invalid context 0x0. This is a serious error. This application, or a library it uses, is using an invalid context and is thereby contributing to an overall degradation of system stability and reliability. This notice is a courtesy: please fix this problem. It will become a fatal error in an upcoming update.
CGContextSetStrokeColorWithColor: invalid context 0x0. This is a serious error. This application, or a library it uses, is using an invalid context and is thereby contributing to an overall degradation of system stability and reliability. This notice is a courtesy: please fix this problem. It will become a fatal error in an upcoming update.
CGContextSaveGState: invalid context 0x0. This is a serious error. This application, or a library it uses, is using an invalid context and is thereby contributing to an overall degradation of system stability and reliability. This notice is a courtesy: please fix this problem. It will become a fatal error in an upcoming update.
CGContextSetFlatness: invalid context 0x0. This is a serious error. This application, or a library it uses, is using an invalid context and is thereby contributing to an overall degradation of system stability and reliability. This notice is a courtesy: please fix this problem. It will become a fatal error in an upcoming update.
CGContextAddPath: invalid context 0x0. This is a serious error. This application, or a library it uses, is using an invalid context and is thereby contributing to an overall degradation of system stability and reliability. This notice is a courtesy: please fix this problem. It will become a fatal error in an upcoming update.
CGContextDrawPath: invalid context 0x0. This is a serious error. This application, or a library it uses, is using an invalid context and is thereby contributing to an overall degradation of system stability and reliability. This notice is a courtesy: please fix this problem. It will become a fatal error in an upcoming update.
CGContextRestoreGState: invalid context 0x0. This is a serious error. This application, or a library it uses, is using an invalid context and is thereby contributing to an overall degradation of system stability and reliability. This notice is a courtesy: please fix this problem. It will become a fatal error in an upcoming update.

この警告メッセージが出る背景や詳細についてはまだ理解出来ておりませんが、色々調べた結果、ひとまずXcode 5では以下のように cancelButtonTitle: の引数を nil ではなく @"" に書き換えることで、この警告メッセージが表示されなくなるようです。

//Xcode 5ではこのように記述する(※Xcode 5以前のバージョンでは、余計なボタンが表示される恐れあり)
[[UIActionSheet alloc] initWithTitle:nil delegate:self cancelButtonTitle:@"" destructiveButtonTitle:nil otherButtonTitles:nil];

 
 

追記(2013/10/01)

iOS Dev Center から旧バージョンのXcodeをダウンロードすることができ、かつ最新バージョン(Xcode 5)と共存させることが可能であることが分かったので、Xcode 4.6.3を自機に再導入してみました。

そして Xcode 4.6.3 のプロジェクト上で UIActionSheet を、cancelButtonTitle: の引数を nil にする従来の方法で生成しビルドしてみたところ、iPhone 6.1シミュレータ上で実行する際には何の警告も出ませんでした。しかしiOS 7を導入した実機上で実行してみたところ、例の大量の警告が出てしまいました。

Xcode 5では警告が出るのではなくて、iOS 7の環境では警告が出るという理解の方が正しいようです。