ゲームアプリの簡単な骨組み的なモノ(SpriteKit)
(※2020/01/12追記:本記事はObjective-Cを用いた、やや古い記事となります。同構成のアプリをSwiftで作成するための最新記事も公開しています)
chemicalfactory.hatenablog.com
SpriteKitではある画面(シーン)から別の画面へと、アニメーション付きで移動するような処理を、容易に実装可能な仕組みが備わっています。本記事ではそれらを活用して、下図のようにゲームアプリの簡単な骨組み的なモノを作成してみます。
このアプリでは最初タイトル画面が表示され、「情報」をタップすると説明画面に移動します(アプリの説明などを書くことを想定)。また「スタート」をタップするとゲーム画面に移動します。ゲーム画面では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 テンプレートを選んでプロジェクトを作成してみます。
作成されたプロジェクトをビルドして実行してみると、最初は下図左のように、深青色の背景に「Hello, World!」という語句がラベルにより表示されています。そして画面をタップすると、下図右のように戦闘機(?)の画像(スプライト)がその位置に出現して一定速度で回転を続けます。そして画面下部にはノード(文字ラベルやスプライトなど、SpriteKitで描画される要素の総体)の数、およびFPSが表示されています(下図はシミュレータからのキャプチャなのでFPSの値が低いですが、iPhone 5sの実機では、もっと多数のスプライトを表示させてもFPS 60でスムーズに動作します)。
このテンプレートの中身を読み解いてみます。
(参考:SpriteKit の公式リファレンス)
SpriteKit Game テンプレートの中身の解読
プロジェクトの設定周りについて
まずテンプレートで生成されたプロジェクトを眺めてみると、「SpriteKit.framework」というフレームワークがリンクされています。名前の通りSpriteKitを利用時に必要なフレームワークです。例えばテンプレートを用いずに作成した他プロジェクトでSpriteKitを利用したい場合は、これをリンクする必要があるようです。
またstoryboardを見ると、View のクラスとして「SKView」が設定されています。SKView は UIView の派生クラスであり、SpriteKit を用いたコンテンツを表示するための部品です。
プロジェクトの設定周りで、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)を生成していることになります。
これをビルドして実行してみると、画面をタップした位置に戦闘機が出現しますが、それらは回転しながらフェードアウトして最終的には消滅します。消滅時にはノードがシーンから除去されるので、画面下部に表示されるノード数はきちんと減少します。
group: メソッド、sequence: メソッドを用いて様々なアクションを組み合わせることで、かなり複雑な動きやアニメーションも、各ノードに設定出来るようになると思います。
SpriteKitに触れてみる(1)
「SpriteKit Game」テンプレートの追加
iOS 7以降向けのアプリ開発に対応したXcode 5から、「SpriteKit Game」なる新しいテンプレートが追加されました。
最近までスルーしてしまっていたのですが、調べてみると、SpriteKit はApple社純正の2Dゲーム開発用フレームワークであり、スプライト(キャラクタの画像など)を容易に画面表示させて、移動させたりアニメーションさせたりすることが出来るようです。更には、物理エンジンやパーティクルシステムまで搭載しているので、これ1つでかなり手の込んだアプリが作成出来そうです。
このようなフレームワークとしては、cocos2d などの強力な先発品が既にある(私も「門清ドリルi」の制作などに使用)のですが、サンプルソースなどを少し齧ってみた印象だと、SpriteKit の方が私のような趣味レベルのプログラマには取っ付き易そうな印象を受けました(特に物理エンジン関連)。例えば下図のように、箱が物理法則に従って落下し積み重なるようなアプリも比較的容易に実装することができ、応用次第で色々面白いモノが作れそうな感触です。
非常に興味を惹かれたので、しばらくは 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」をクリックします。
ページが移動したら「iOS Apps」の欄にある項目のどれかをクリックします。
ページが移動したら、まずはアプリのApp IDを登録します。左側のメニューから「App IDs」を選択して、右上の方にある「+」ボタンをクリックします。
「Resister iOS App ID」というページに移動しますので、必要な項目を埋めていきます。
- 「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」を選択し、先程同様に右上の方にある「+」ボタンをクリックします。
下図のように「What type of provisioning〜」ページに移動しますので、「Distribution」欄の「App Store」にチェックを入れて「Continue」ボタンをクリックします。
下図のように「Select App. ID.」ページに移動しますので、先程登録したApp IDを選んで「Continue」ボタンをクリックします。次は「Select certificates.」ページに移動します。チェックの入ったプロファイル名が表示されるはずなので、そのまま「Continue」ボタンをクリックします。
下図のように「Name this profile〜」ページに移動します。この「Profile Name」がプロビジョニングプロファイルの名前となるので、分かりやすい名前を入力します(自分は前述の Bundle identifier と同じ名前にしてしまっています)。入力したら「Generate」ボタンをクリック。
プロビジョニングプロファイルが無事生成されたら、「Download」ボタンを押してそのファイルをダウンロードします。これで iOS Dev Center における作業はひとまず完了です。
ダウンロードが完了したら、ファイルをダブルクリックすると、Xcodeにプロビジョニングプロファイルがインストールされます。確認するためにはXcodeメニューの「Xcode」→「Preference」を選択します。Preferenceのウインドウが開きますので「Accounts」アイコンをクリックし、「Apple ID」の欄にある「View Details」ボタンをクリックします。Xcodeにインストールされているプロビジョニングプロファイルの一覧を見ることが出来ます。
アプリのアーカイブ生成および提出
ここからはXcode上での作業となります。アプリのプロジェクトを開いて、プロジェクト設定の「Info」を表示します。そして下図の赤枠で囲んだ「+」ボタンをクリックし、「Dupulicate "Release" Configuration」をクリックします。
「Configurations」に新しい項目(デフォルトの名前は「Release copy」となるはず)が作られます。ここでは名前を「Distribution」と変えてみます。
今度はプロジェクト設定の「Build Setting」を表示します。「Code Signing」欄の「Code Signing Identity」に先程作った Distribution が追加されているので、その値として「iPhone Distributon:〜」(証明書)を選択します(一度アプリのリリース経験があり、かつ、iOS Developer Programの更新をきちんと行っていれば、有効な証明書がXcodeにインストールされているはずです)。
Distribution のすぐ下にある「Any iOS SDK」の値に「iOS Distribution」を選択します。
今度は、Xcodeの左上に表示されているプロジェクト名(Run、Stopボタンの右側)をクリックします。ポップアップが表示されるので「Edit Scheme」をクリックします。
下図のような画面が表示されますので、左側のリストから「Archive」を選び、「Build Configuration」を先程作った Distribution に設定して、「OK」ボタンをクリックします。
あとは、Xcodeのメニューから「Product」→「Archive」を選択します(このとき、ビルドターゲットをiOSシミュレーターにしていると、「Archive」が選択出来ないようです。必ずビルドターゲットはiOS Deviceにしておく必要があるようです)。これにより、アプリのアーカイブが生成されます(アプリはこのアーカイブという形式でApp Storeにアップロードされます)
アーカイブの生成が完了するとオーガナイザが開きます。右側に「Validate」「Distribute」という2つのボタンがありますので、まずは「Validate」をクリックします。するとiTunes Connectのログイン画面が表示されるので、ユーザー名およびパスワードを適切に入力して「Next」をボタンをクリックします。続いて下図のような画面が表示されますので「Provisioning Profile」に先程インストールしたプロビジョニングプロファイルを設定し「Validate」ボタンをクリックします。
Validateとは要するに、アプリが正しくビルドされているか確認する手続きです。しばらく待ち時間があった後に、何の問題も無ければ以下のような文章が表示されるので、「Finish」ボタンをクリックします。
No issue were found in (アプリ名).
(アプリ名) has passed validation and may be submitted to the App Store.
オーガナイザの最初の画面に戻りますので、今度は「Distribute」ボタンをクリックします。すると以下のような画面が表示されるので、そのまま「Next」ボタンをクリックします。
続いて「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社による審査が無事通ることを祈りましょう(審査の状況や結果は、登録しておいたメールアドレスに送られてきます)。
自作アプリの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」をクリックします。
ページが移動したら、左上の方にある「Add New App」ボタンをクリックします。
「App Information」というページに移動しますので、必要な項目を埋めていきます。
- 「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〜」というページに移動します。ここではアプリのリリース予定日および価格を設定します。
- 「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にした場合には全て英語で入力する必要があります。
- 「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」ボタンをクリックすると、これまでに設定した各項目を編集することが出来ます(出来ない項目もある)。
「Default Language」をEnglishにした場合でも、日本のApp Storeでは日本語の説明を表示させたい場合は、下図のように「Choose Another Language」からJapaneseを選択します(右側の小さい「Add」をクリックする必要があることに注意)。日本のApp Store向けのアプリ名、説明、キーワード、スクリーンショットなどを登録する画面が表示されますので、必要事項を記入して(ここはもちろん日本語でOK)「Save」ボタンをクリックすればOKです。
いよいよ、アプリをアップロードするための準備に入ります。「Ready to Upload Binary」をクリックします。
下図のようなページに移動しますので、「NO」にチェックを入れて「Save」ボタンをクリックします。続いて「You are now ready to upload your binary using Application Loader〜」という文章が書かれたページに移動しますので、内容を確認して「Continue」ボタンをクリックします。これでアップロードの準備が整った状態になります。
以下、自作アプリのApp Storeへのリリース手順メモ(3)に続きます。
自作アプリのApp Storeへのリリース手順メモ(1)
せっかくアプリを作成したら、当然App Storeへとリリースして色々な方々に使っていただきたい…のですが、App Storeへのリリース手順は正直かなり複雑で面倒です。過去には色々なサイトや書籍を参考にしながら、なんとかリリースをすることが出来たのですが、この際自分なりの手順メモを作っておこうと思いました(既に一度はリリース経験があり、デベロッパー情報の登録などは済んでいることを前提としたメモです)。
本記事(1)では、下準備的な部分(アイコンおよびスクリーンショットの用意)についてメモして行きます。
アイコンの準備
アプリをリリースする以上は、当然ちゃんとしたアイコンを用意するのはほぼ必須なのですが、結構これが面倒です。対象とする機種(iPhone、iPad)や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です。
デフォルトの Images.xcassets には「AppIcon」というリソースがあります(左下にある「+」ボタンから「New App Icon」を選択すれば、自分で追加することも出来ます)。ここにアイコンを追加します。
まずは対象とする機種やiOSのバージョンに応じて、右側のチェックボックスから必要となるアイコンサイズを選択します。なお「iOS icon is pre-rendered」という項目は、iOS 6 以前でアイコンに自動的に光沢効果を追加するか、という設定です。
あとは下図のように、各サイズのアイコン画像ファイルをドラッグ&ドロップで、適切な枠線に放り込んで行くだけです。「2x」と描かれた枠線には、Retinaディスプレイ向けに解像度2倍の画像を追加します(従来は「@2x」というキーワードを付与していたような画像です)。誤った画像を放り込んだ場合には、それをクリックしてdeleteキーで削除出来ます。
従来は、アイコン画像の名前は「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では、カシャっとカメラ音が鳴ります)。普通にカメラで撮影した画像と全く同じように、スクリーンショットも「写真」アプリのカメラロールに保存されます。
- iPhone(またはiPad)シミュレータ(本記事執筆時点のバージョンは7.0)の場合は、メニューの「ファイル」→「スクリーンショットを保存」を選ぶと、デスクトップにスクリーンショットが保存されます。
スクリーンショットについては、それぞれのサイズについて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を導入した実機上で実行してみたところ、例の大量の警告が出てしまいました。
OpenGL ES2.0でポイントスプライト(2)
※OpenGLでポイントスプライト(1)の続きとなります。
サンプルプログラムの要点
ここでは前回の記事に掲載した、OpenGL ES2.0でポイントスプライトを利用するサンプルプログラムの要点についてメモしていきます。
GLKitを用いたテクスチャの読み込み
ポイントスプライトを利用するためには、当然ポイントに貼付けるためのテクスチャを用意する必要があります。しかし素のOpenGL ES2.0には、汎用的な画像ファイル(JPEGとかPNGとか)をテクスチャとして読み込む仕組みは用意されていません。そのため、本来なら自前でメソッドを実装したりしないといけないのですが、幸いなことにGLKitには画像ファイルのパスを指定するだけで、自動的にテクスチャを生成してくれる GLKTextureLoader が用意されています。以下に使い方のサンプルコードを示します(前回のサンプルプログラムより抜粋)。
//GLKBaseEffectを使用する準備。初期化のとき(setupGL メソッドの冒頭など)で行っておけば良い。 self.effect = [[GLKBaseEffect alloc] init]; //テクスチャのロード。以下の手続きで画像をテクスチャとして読み込み、テクスチャユニット0にバインドすることが出来る。 //「particle.png」というPNG画像を読み込む例 NSString* filePath = [[NSBundle mainBundle] pathForResource:@"particle" ofType:@"png"]; GLKTextureInfo *texInfo = [GLKTextureLoader textureWithContentsOfFile:filePath options:nil error:nil]; if (texInfo) { //確認用。このログ表示は削除しても構わない。 NSLog(@"Texture loaded successfully. name = %d size = (%d x %d)", texInfo.name, texInfo.width, texInfo.height); } self.effect.texture2d0.name = texInfo.name;
頂点シェーダにおけるポイントサイズの設定
サンプルプログラムにおける頂点シェーダの全コードを以下に再掲します。
//Shader.vsh attribute vec4 position; attribute vec4 color; varying vec4 vcolor; uniform mat4 modelViewProjectionMatrix; void main() { vcolor = color; gl_Position = modelViewProjectionMatrix * position; //ポイントのサイズをここで設定する gl_PointSize = 20.0; }
頂点シェーダのコードは非常にシンプルです。attribute変数 color に入力された各頂点の色は、そのまま(varying変数を介して)フラグメントシェーダに渡しています。attribute変数 position に入力された各頂点の位置は、渡された変換行列(モデルビュー行列と投影行列を乗算したもの)によって座標変換されます。
そして、組み込みの特殊変数である gl_PointSize に値を設定することにより、ポイントサイズをピクセル単位で設定することが出来ます。昔のOpenGLではシェーダを用いずとも "glPointSize(20.0f);" といった関数によりポイントサイズを変更出来たようですが、OpenGL ES2.0ではこのような関数は(私が試した限りでは)使えないようです。
またgl_PointSize に定数の値を設定すると、当然全てのポイントは同じサイズで表示されますが、場合によってはポイントに遠近感を付けて表示したい場合もあります。すなわち、視点に近いポイントほど大きく、遠いポイントほど小さく描画されるようにしたいわけです。これは以下のように、ある定数を gl_Position の w 要素で割った値を gl_PointSize に設定することにより容易に実現出来ます。
//gl_Positionには、既に(座標変換された)ポイントの位置が格納されているとする gl_PointSize = 200.0 / gl_Position.w;
詳しい原理については(私自身が理解しきれていないので)省きますが、視点の位置に対して、各ポイントの位置が離れていればいるほど w 要素の値は大きくなります。すなわち上記の方法によって、視点からの距離に反比例してポイントサイズの大きさが変わるようになり、以下の画像のように遠近感を付けることが出来ます。
フラグメントシェーダによるアルファテスト
サンプルプログラムにおけるフラグメントシェーダの全コードを以下に再掲します。
//Shader.fsh precision mediump float; uniform sampler2D s_texture; varying vec4 vcolor; void main() { //サンプラで取り込んだテクスチャを、変数"baseColor"に格納 vec4 baseColor = texture2D(s_texture, gl_PointCoord); //アルファ値が0.5未満である場合はフラグメントを破棄(アルファテスト) if(baseColor.a < 0.5){ discard; } else{ //元々のテクスチャの色に、各ポイントに設定された色を付けて出力する gl_FragColor = baseColor * vec4(vcolor); } }
フラグメントシェーダのコードも非常にシンプルであり、基本的にはサンプラによって取り込んだテクスチャの色に、頂点シェーダから(varying変数を介して)受け取った各頂点の色を付けて出力しているだけです。
そして、テクスチャを格納する変数 baseColor の a 要素には、テクスチャ画像のアルファ値が入っています。この値が0.5未満である場合には discard キーワードを呼び出しています。discard を呼び出すと、そのフラグメントは破棄されるため、アルファ値が0.5未満の領域を切り抜く(あるいは透明にする)ことが出来ます。
昔のOpenGLではシェーダを用いずとも、glAlphaFunc(GL_GREATER, 0.5); や glEnable(GL_ALPHA_TEST); といった関数によってアルファテストの機能を利用することが出来たのですが、例によってOpenGL ES2.0ではこれらはサポートされなくなったようです。
OpenGL ES2.0でポイントスプライト(1)
OpenGLでは、GL_POINTS で描かれる点(ポイント)の1つ1つにテクスチャを貼付ける「ポイントスプライト」という機能をサポートしています。これらのポイントは、そのサイズを拡大すると実は正方形として描かれており、その全面にテクスチャを貼付けることが出来ます。この正方形は常に視点に対して垂直な方向に表示される(あくまで点である以上、視点の方向によって見え方が変わらない)ため、ビルボードの代わりとして用いることが出来ます。
またアルファブレンドを有効にした状態で、様々な色を付けた多数の粒子(パーティクル)をポイントスプライトにより描画すると、非常に奇麗で幻想的なグラフィックを作ることが出来ます。更にパーティクルシステム(参考:Wikipedia)をポイントスプライトを用いて実装すると、炎、爆発、流水、霧といった事象を効率的に、かつ奇麗に表現することが出来るようです。
そこで、OpenGL ES2.0で実際にポイントスプライトを用いてグラフィックを描画する方法を模索しました。参考資料が少なく、かなりの試行錯誤と挫折を重ねましたが、最近になってようやくその方法が分かってきました。それについてメモしていきます。
シェーダ利用の必要性について
私は最初、シェーダなどを使わずにGLKitの範疇だけでポイントスプライトを使用することが出来ないか、色々調べて試行錯誤してみました。しかし私が調べた限りでは、GLKitの範疇だけでポイントサイズを変更したり、ポイントへテクスチャを貼付けたりする方法を見つけることが出来ませんでした。OpenGL ES2.0でポイントスプライトを実現するためには、(部分的にでも)シェーダを利用することが避けられないようです。そこで本記事では、GLKitとシェーダを併用する形での方法についてメモします。
簡単なサンプル
ひとまず、ポイントスプライトを利用した簡単なサンプルプログラムを作成しましたので、ソースとその実行手順を示します。なお、こちらのサンプルを作成するにあたっては、「OpenGL ES2.0 プログラミングガイド」という書籍をかなり参考にしています。
- 次に、こちらのリンク先にある「particle.png」という画像(アルファチャンネル付きのPNG画像として作成した、ポイント用のテクスチャ)を適当な場所に保存します。ファイル名は「particle.png」という名前のままにしておいてください。
- そして、保存した「particle.png」をプロジェクトに追加します。Xcodeのメニューから「File」→「Add Files to "(プロジェクト名)"」と選択するとファイルを読み込むためのダイアログが開きますので、先程保存した「particle.png」を探して選択し、読み込んでください。
- 続いて、ViewController.m の中身を、以下のコードに書き換えます(ViewController.h の書き換えは不要)。このコードは「OpenGL Game」テンプレートで自動生成されるサンプルを土台としており、前述のようにGLKitとシェーダを併用しています(シェーダのコンパイルやロード関連の方法については、土台となったサンプルの方法をほぼそのまま流用しています)。
//ViewController.m #import "ViewController.h" #define BUFFER_OFFSET(i) ((char *)NULL + (i)) #define VERTEX_NUM 100 #define RADIUS 3.0f //各シェーダのuniform変数の位置を格納する配列。 enum { UNIFORM_MODELVIEWPROJECTION_MATRIX, UNIFORM_S_TEXTURE, NUM_UNIFORMS }; GLint uniforms[NUM_UNIFORMS]; //ポイントの座標および色の格納用配列 GLfloat vertex[VERTEX_NUM * 6]; @interface ViewController () { GLuint _program; GLuint _vertexArray; GLuint _vertexBuffer; GLKMatrix4 _modelViewProjectionMatrix; GLKMatrix4 modelViewMatrix; GLfloat _rotation; } @property (strong, nonatomic) EAGLContext *context; @property (strong, nonatomic) GLKBaseEffect *effect; - (void)setupGL; - (void)tearDownGL; - (BOOL)loadShaders; - (BOOL)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file; - (BOOL)linkProgram:(GLuint)prog; - (BOOL)validateProgram:(GLuint)prog; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; if (!self.context) { NSLog(@"Failed to create ES context"); } GLKView *view = (GLKView *)self.view; view.context = self.context; view.drawableDepthFormat = GLKViewDrawableDepthFormat24; [self setupGL]; } - (void)dealloc { [self tearDownGL]; if ([EAGLContext currentContext] == self.context) { [EAGLContext setCurrentContext:nil]; } } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; if ([self isViewLoaded] && ([[self view] window] == nil)) { self.view = nil; [self tearDownGL]; if ([EAGLContext currentContext] == self.context) { [EAGLContext setCurrentContext:nil]; } self.context = nil; } // Dispose of any resources that can be recreated. } - (void)setupGL { [EAGLContext setCurrentContext:self.context]; [self loadShaders]; self.effect = [[GLKBaseEffect alloc] init]; self.effect.light0.enabled = GL_TRUE; self.effect.light0.diffuseColor = GLKVector4Make(1.0f, 0.4f, 0.4f, 1.0f); //glEnable(GL_DEPTH_TEST); デプステストはオフにしておく。 //テクスチャのロード。これだけで画像をテクスチャとして読み込み、テクスチャユニット0にバインドすることが出来る。便利! NSString* filePath = [[NSBundle mainBundle] pathForResource:@"particle" ofType:@"png"]; GLKTextureInfo *texInfo = [GLKTextureLoader textureWithContentsOfFile:filePath options:nil error:nil]; if (texInfo) { NSLog(@"Texture loaded successfully. name = %d size = (%d x %d)", texInfo.name, texInfo.width, texInfo.height); } self.effect.texture2d0.name = texInfo.name; //vertex[i]〜vertex[i+3]に、各ポイントの座標(半径"RADIUS"の球上のランダムな位置)を設定 //vertex[i+4]〜vertex[i+6]に、各ポイントの色(RGB各成分に0.5f〜1.0fの間のランダムな値)を設定 for(int i = 0; i < VERTEX_NUM * 6; i+=6){ float theta = GLKMathDegreesToRadians(arc4random()%360); float phi = GLKMathDegreesToRadians(arc4random()%360); vertex[i] = RADIUS * sin(theta) * cos(phi); vertex[i + 1] = RADIUS * sin(theta) * sin(phi); vertex[i + 2] = RADIUS * cos(theta); vertex[i + 3] = ((arc4random() % 10000) / 20000.0f) + 0.5f; vertex[i + 4] = ((arc4random() % 10000) / 20000.0f) + 0.5f; vertex[i + 5] = ((arc4random() % 10000) / 20000.0f) + 0.5f; } glGenVertexArraysOES(1, &_vertexArray); glBindVertexArrayOES(_vertexArray); glGenBuffers(1, &_vertexBuffer); glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer); glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer); glBufferData(GL_ARRAY_BUFFER, sizeof(vertex), vertex, GL_STATIC_DRAW); //各ポイントの座標、色を頂点配列に格納 glEnableVertexAttribArray(GLKVertexAttribPosition); glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(float)*6, BUFFER_OFFSET(0)); glEnableVertexAttribArray(GLKVertexAttribColor); glVertexAttribPointer(GLKVertexAttribColor, 3, GL_FLOAT, GL_FALSE, sizeof(float)*6, BUFFER_OFFSET(sizeof(float)*3)); glBindVertexArrayOES(0); } - (void)tearDownGL { [EAGLContext setCurrentContext:self.context]; glDeleteBuffers(1, &_vertexBuffer); glDeleteVertexArraysOES(1, &_vertexArray); self.effect = nil; if (_program) { glDeleteProgram(_program); _program = 0; } } #pragma mark - GLKView and GLKViewController delegate methods - (void)update { float aspect = fabsf(self.view.bounds.size.width / self.view.bounds.size.height); GLKMatrix4 projectionMatrix = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(65.0f), aspect, 0.1f, 300.0f); self.effect.transform.projectionMatrix = projectionMatrix; modelViewMatrix = GLKMatrix4MakeTranslation(0.0f, 0.0f, -10.0f); modelViewMatrix = GLKMatrix4Rotate(modelViewMatrix, _rotation, 1.0f, 1.0f, 1.0f); _modelViewProjectionMatrix = GLKMatrix4Multiply(projectionMatrix, modelViewMatrix); _rotation += self.timeSinceLastUpdate * 0.2f; } - (void)glkView:(GLKView *)view drawInRect:(CGRect)rect { glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); //ブレンディングの設定(デプステストはオフにしておく) glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE); //シェーダをロードして、利用可能にする [self loadShaders]; glUseProgram(_program); //変数"_modelViewProjectionMatrix"を、頂点シェーダのuniform変数"modelViewProjectionMatrix"(mat4型)に渡す //(loadShadersメソッドの中で、"uniforms[UNIFORM_MODELVIEWPROJECTION_MATRIX]"に、uniform変数の場所を格納) glUniformMatrix4fv(uniforms[UNIFORM_MODELVIEWPROJECTION_MATRIX], 1, 0, _modelViewProjectionMatrix.m); //フラグメントシェーダでテクスチャを取り込むためのサンプラ。テクスチャユニット0に設定 glUniform1i(uniforms[UNIFORM_S_TEXTURE], 0); //ポイントの描画 glBindVertexArrayOES(_vertexArray); glDrawArrays(GL_POINTS, 0, VERTEX_NUM); glBindVertexArrayOES(0); } //以下、シェーダのロード関連のメソッド群 #pragma mark - OpenGL ES 2 shader compilation - (BOOL)loadShaders { GLuint vertShader, fragShader; NSString *vertShaderPathname, *fragShaderPathname; // Create shader program. _program = glCreateProgram(); // Create and compile vertex shader. vertShaderPathname = [[NSBundle mainBundle] pathForResource:@"Shader" ofType:@"vsh"]; if (![self compileShader:&vertShader type:GL_VERTEX_SHADER file:vertShaderPathname]) { NSLog(@"Failed to compile vertex shader"); return NO; } // Create and compile fragment shader. fragShaderPathname = [[NSBundle mainBundle] pathForResource:@"Shader" ofType:@"fsh"]; if (![self compileShader:&fragShader type:GL_FRAGMENT_SHADER file:fragShaderPathname]) { NSLog(@"Failed to compile fragment shader"); return NO; } // Attach vertex shader to program. glAttachShader(_program, vertShader); // Attach fragment shader to program. glAttachShader(_program, fragShader); // Bind attribute locations. // This needs to be done prior to linking. glBindAttribLocation(_program, GLKVertexAttribPosition, "position"); //頂点配列に格納した各ポイントの色を、頂点シェーダのattribute変数"color"に入力 glBindAttribLocation(_program, GLKVertexAttribColor, "color"); // Link program. if (![self linkProgram:_program]) { NSLog(@"Failed to link program: %d", _program); if (vertShader) { glDeleteShader(vertShader); vertShader = 0; } if (fragShader) { glDeleteShader(fragShader); fragShader = 0; } if (_program) { glDeleteProgram(_program); _program = 0; } return NO; } //頂点シェーダのuniform変数"modelViewProjectionMatrix"(mat4型)の場所を取得する。 uniforms[UNIFORM_MODELVIEWPROJECTION_MATRIX] = glGetUniformLocation(_program, "modelViewProjectionMatrix"); //フラグメントシェーダのuniform変数"s_texture"(テクスチャのサンプラ)の場所を取得する。 uniforms[UNIFORM_S_TEXTURE] = glGetUniformLocation(_program, "s_texture"); // Release vertex and fragment shaders. if (vertShader) { glDetachShader(_program, vertShader); glDeleteShader(vertShader); } if (fragShader) { glDetachShader(_program, fragShader); glDeleteShader(fragShader); } return YES; } - (BOOL)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file { GLint status; const GLchar *source; source = (GLchar *)[[NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil] UTF8String]; if (!source) { NSLog(@"Failed to load vertex shader"); return NO; } *shader = glCreateShader(type); glShaderSource(*shader, 1, &source, NULL); glCompileShader(*shader); #if defined(DEBUG) GLint logLength; glGetShaderiv(*shader, GL_INFO_LOG_LENGTH, &logLength); if (logLength > 0) { GLchar *log = (GLchar *)malloc(logLength); glGetShaderInfoLog(*shader, logLength, &logLength, log); NSLog(@"Shader compile log:\n%s", log); free(log); } #endif glGetShaderiv(*shader, GL_COMPILE_STATUS, &status); if (status == 0) { glDeleteShader(*shader); return NO; } return YES; } - (BOOL)linkProgram:(GLuint)prog { GLint status; glLinkProgram(prog); #if defined(DEBUG) GLint logLength; glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &logLength); if (logLength > 0) { GLchar *log = (GLchar *)malloc(logLength); glGetProgramInfoLog(prog, logLength, &logLength, log); NSLog(@"Program link log:\n%s", log); free(log); } #endif glGetProgramiv(prog, GL_LINK_STATUS, &status); if (status == 0) { return NO; } return YES; } - (BOOL)validateProgram:(GLuint)prog { GLint logLength, status; glValidateProgram(prog); glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &logLength); if (logLength > 0) { GLchar *log = (GLchar *)malloc(logLength); glGetProgramInfoLog(prog, logLength, &logLength, log); NSLog(@"Program validate log:\n%s", log); free(log); } glGetProgramiv(prog, GL_VALIDATE_STATUS, &status); if (status == 0) { return NO; } return YES; } @end
- 更に今回はシェーダを利用するため、シェーダのコードについても書き換える必要があります。まず頂点シェーダ(Shader.vsh)の中身を以下のコードに書き換えます。
//Shader.vsh attribute vec4 position; attribute vec4 color; varying vec4 vcolor; uniform mat4 modelViewProjectionMatrix; void main() { vcolor = color; gl_Position = modelViewProjectionMatrix * position; //ポイントのサイズをここで設定する gl_PointSize = 20.0; }
- 続いて、フラグメントシェーダ(Shader.fsh)の中身を以下のコードに書き換えます。
//Shader.fsh precision mediump float; uniform sampler2D s_texture; varying vec4 vcolor; void main() { //サンプラで取り込んだテクスチャを、変数"baseColor"に格納 vec4 baseColor = texture2D(s_texture, gl_PointCoord); //アルファ値が0.5未満である場合はフラグメントを破棄(アルファテスト) if(baseColor.a < 0.5){ discard; } else{ //元々のテクスチャの色に、各ポイントに設定された色を付けて出力する gl_FragColor = baseColor * vec4(vcolor); } }
- これを実行すると、以下のように球状に配置されたいくつものカラフルな点が描画されます。これらの点の一つ一つが、テクスチャ(particle.png)を貼付けられたポイントであり、各点に(頂点属性として)設定された色に応じた塗り分けがされて表示されます。またアルファブレンドを有効にしてあるため、各ポイントの色はブレンディングされて表示されます。
このサンプルプログラムのソースの詳細については、次回以降の記事にメモしていく予定です。