工場裏のアーカイブス

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

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