A Day In The Life

とあるプログラマの備忘録

SpriteKitではじめる2Dゲームプログラミング

先日こちらの記事にも書きましたが「Hedgehog Drive」というゲームをリリースしました。このアプリのゲーム部分は SpriteKit を使って実装しました。初めて SpriteKit を使ったので備忘録的にこのフレームワークの使い方をまとめてみました。

2D ゲームで使用される基本用語

SpriteKit の説明の前に 2D ゲーム開発でよく使われる用語とその説明をします。SpriteKit でも以下で説明する用語が使われていますので覚えておくと良いと思います。

  • ゲームループ
    ゲームではキャラクタや背景などが常に動いています。このような常に変化する状態を実現するために、ゲームの状態をプログラムで処理する必要があります。これを処理するための仕組みがゲームループ(またはメインループ)です。ゲームループの処理は常に一定時間ごとに呼び出されます
  • FPS
    Frames Per Second の略。1秒間に何回フレームが処理されるかを表す単位のことです。ゲールループはFPSごとに呼び出されます。
  • シーン(Scene)
    ゲームの場面のことです。RPG のマップ、町の中、バトル、ムービーなどゲームの見せ方が変わる単位です。ストーリーボードで使われるシーンと考え方は同じです
  • トランジション
    シーンの切り替え時のアニメーションのこと
  • スプライト(Sprite)
    シーンの上で動き回るオブジェクトや障害物のことです。スーパーマリオのマリオやクリボー、土管やハテナボックスがスプライトです
  • パーティクル(Particle)
    iOS ではエミッター(Emitter)とも言われています。シューティングゲームで敵を撃破したときの爆発とか、飛行機やロケットのエンジンから噴射される炎なんかを指します。スプライトが爆発する時の演出に使ったりスプライトの動きをよりリアルに見せる為に使うことが多いです
  • アクション(Action)
    スプライトの動き、アニメーションのこと
  • テクスチャ(Texture)
    スプライトの見た目や質感を表現するための模様やパターン、画像のこと

シーンとトランジションの関係を図にすると以下のようになります。
シーンとトランジション
シーンとスプライト、アクションの関係を図にすると以下のようになります。
スプライトとアクション

SpriteKit クラス構成

SpriteKit ではシーン、スプライト、パーティクルがツリーで構成されています。それぞれシーンが SKScene クラス、スプライトが SKSpriteNode クラス、パーティクルが SKEmitterNode クラスとなっています。またツリーを構成する要素のことをノードと呼んでいます。ノードにあたるクラスが SKNode クラスで、SKScene、SKSpriteNode、SKEmitterNode クラスは SKNode クラスを継承しています。
また SpriteKit では UIView オブジェクト上にゲーム画面を配置できるように SKView というクラスを提供しています。SKView オブジェクト上にシーンオブジェクトを配置します。
以下は SpriteKit から提供されている主なクラスの関係を図にしたものです。
SpriteKitクラス図

ゲームプログラム作成の流れ

ゲームプログラム作成の流れは大まかに以下の順番で行います。

  • シーンの配置と表示
  • スプライトの配置
  • スプライトに動きや音をつける

シーンの配置と表示

シーンをビューに配置して表示するには SKView と SKScene クラスを使います。SKView はビューとシーンの橋渡しのためのクラスです。シーンの配置と表示は以下のように UIViewController の viewDidAppear: メソッドで行います。

@implementation MyViewController {

- (void)viewDidAppear:(BOOL)animated
{
  [super viewDidAppear:animated];
    
  // SKViewオブジェクトの生成と追加
  SKView *skView = [[SKView alloc] initWithFrame:self.view.frame];
  skView.showsFPS = YES; // FPSの表示(デバッグ用設定)
  skView.showsNodeCount = YES; // 配置されているノードの数を表示(デバッグ用設定)
  [self.view addSubview:skView];
    
  // SKSceneオブジェクトの生成と配置
  SKScene *scene = [MyScene sceneWithSize:skView.bounds.size];
  scene.scaleMode = SKSceneScaleModeAspectFill;
  scene.userInteractionEnabled = YES;
    
  // シーンの表示
  [skView presentScene:scene];
}

@end

はじめに SKView オブジェクトを生成してビューに追加します。その後、SKScene オブジェクトを生成して SKView オブジェクトの presentScene: メソッドを使って表示します。なお SKView オブジェクトの生成と追加はストーリーボード上で行うことも出来ます。

トランジションを使ったシーンの切り替え

SKTransition クラスを使うとシーンの切り替えにちょっとした演出を加えることが出来ます。
以下は UIViewController のタッチイベントでシーンを切り替えるプログラムの例です。

@implementation MyViewController

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
  // シーンオブジェクトの生成
  SKScene *scene = [[SKScene alloc] initWithSize:self.stageView.frame.size];
    
  // トランジションオブジェクトの生成(ドアオープン)
 SKTransition *transition = [SKTransition doorsOpenHorizontalWithDuration:2];
    
  // トランジションをつかったシーンの切り替え
  [self.stageView presentScene:scene transition:transition];
}

@end

トランジションはここで紹介したドアオープン以外にもいろいろあります。

SKScene クラスの拡張

シーンはゲームの場面ごとに分かれていて、各場面ごとに各種イベント処理やスプライトの配置(後ほど説明します)をする必要があります。従って各場面ごとに SKScene クラスを継承してシーンを作成する必要があります。

@interface MyScene : SKScene

@end
タッチイベントの受け取り

SKScene オブジェクトは以下のようにタッチ系イベントを受け取ることが出来ます。

@implementation MyScene {

// Touch Began イベント
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
  // シーン上の位置を取得する
  UITouch *touch = [touches anyObject];
  CGPoint location = [touch locationInNode:self]; 
  NSLog(@"%@", NSStringFromCGPoint(location));
}
// Touch Moved イベント
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
}
// Touch Ended イベント
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
}

@end

基本 UIView オブジェクトでタッチイベントを受け取る方法と同じです。UITouch オブジェクトの locationInNode: メソッドを使うとシーン上のどの位置がタッチされたかがわかります。

ゲームループイベントの受け取り

また SKScene オブジェクトでは以下のようにゲームループイベントを受け取ることが出来ます。

@implementation MyScene {

// ゲームループイベント
-(void)update:(CFTimeInterval)currentTime {
  // FPSごとに呼び出される
  NSLog(@"%g", currentTime);
}

@end

スプライトの移動やスプライト同士の衝突判定なんかをするときに使います。

スプライトの配置

シーンを表示しただけではなにも表示されませんので、シーンにスプライトを配置します。スプライトの配置には SKSprite クラスを使います。スプライトをシーン上に配置するとスプライトが画面に表示されます。スプライトの配置は以下のようにシーンのコンストラクタメソッドで行います。

@implementation MyScene

-(instancetype)initWithSize:(CGSize)size {
  if (self = [super initWithSize:size]) {
    // 画像名を指定してスプライトオブジェクトを生成する
    SKSprite *sprite = [[SKSprite alloc] initWithImageNamed:@"hedgehog"];
    // シーンの中央に設定
    sprite.position = CGPointMake(CGRectGetMidX(self.frame),
                                  CGRectGetMidY(self.frame));
    // スプライトに名前をつける
    sprite.name = @“hedgehog”;
    // スプライトを配置する
    [self addChild:sprite];
  }
}

@end

スプライトに名前をつけるとシーンオブジェクトの処理でスプライトを検索する時に役立ちます。

スプライトの検索

スプライトに対する処理は通常、シーンオブジェクトのタッチイベントかゲームループイベントで行います。各イベントでスプライトオブジェクトを参照する場合はシーン上のスプライトを検索する必要があります。
SKScne クラスの childNodeWithName: メソッドを使うとシーン上に配置されているスプライトを検索してオブジェクトを取得することが出来ます。

@implementation MyScene {

-(void)update:(CFTimeInterval)currentTime {
  // hedgehogという名前のスプライトを探してオブジェクトを取得する
  SKNode *sprite = [self childNodeWithName:@“hedgehog”];
}

@end

スプライトに動きや音をつける

SKAction クラスを使うとスプライトに簡単に動きや音をつけることが出来ます。スプライトをシーンに配置しただけではスプライトは動かずゲームらしくないのでスプライトにアクションをつけていきます。スプライトにアクションを実行させるには以下のようにシーンのイベントメソッドを実装します。

@implementation MyScene {

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
  // シーン上の位置を取得する
  UITouch *touch = [touches anyObject];
  CGPoint location = [touch locationInNode:self]; 
  // hedgehogスプライトの取得する
  SKNode *sprite = [self childNodeWithName:@“hedgehog”];
  // タッチした位置に移動するアクションの生成
  SKAction *move = [SKAction moveTo:location duration:1];
  // アクションを実行する
  [sprite runAction:move];
}

@end
複数のアクションを順番に実行する

シーケンスを使うと、複数のアニメーションを連続して実行することができます。シーケンスは SKAction クラスの sequence メソッドを使って作成することが出来ます。また作成したシーケンスオブジェクトは通常のアクションを実行するのと同じように runAction メソッドを使って実行することが出来ます。シーケンスを使ったアニメーションの実行では前のアクションが実行されるまで次のアクションは実行されません。
以下はスプライトの向きを変えてから移動するプログラムの例です。

@implementation MyScene {

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
  // シーン上の位置を取得する
  UITouch *touch = [touches anyObject];
  CGPoint location = [touch locationInNode:self];
  // hedgehogスプライトの取得する
  SKNode *sprite = [self childNodeWithName:@“hedgehog”];
  // タッチした位置に移動するアクションの生成
  SKAction *move = [SKAction moveTo:location duration:1];
  // 角度の計算
  CGFloat dx = location.x - self.position.x;
  CGFloat dy = location.y - self.position.y;
  CGFloat angle = atan2(dx, dy);
  // 角度を変えるアクションの生成
  SKAction *rotate = [SKAction rotateToAngle:-angle duration:1];
  // シーケンスの生成
  SKAction *seq = [SKAction sequence:@[rotate, move]];
  // アクションの実行(角度が変わってから移動する)
  [self runAction:seq];
}

@end
複数のアクションを同時に実行する

グループを使うと、複数のアニメーションを同時に実行することができます。グループは SKAction クラスの group メソッドを使って作成することが出来ます。また作成したグループオブジェクトは通常のアクションを実行するのと同じように runAction メソッドを使って実行することが出来ます。
以下はスプライトの向きを変えながら移動するプログラムの例です。

@implementation MyScene {

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
  // シーン上の位置を取得する
  UITouch *touch = [touches anyObject];
  CGPoint location = [touch locationInNode:self];
  // hedgehogスプライトの取得する
  SKNode *sprite = [self childNodeWithName:@“hedgehog”];
  // タッチした位置に移動するアクションの生成
  SKAction *move = [SKAction moveTo:location duration:1];
  // 角度の計算
  CGFloat dx = location.x - self.position.x;
  CGFloat dy = location.y - self.position.y;
  CGFloat angle = atan2(dx, dy);
  // 角度を変えるアクションの生成
  SKAction *rotate = [SKAction rotateToAngle:-angle duration:1];
  // グループの生成
  SKAction *group = [SKAction group:@[rotate, move]];
  // アクションの実行(角度が変わってから移動する)
  [self runAction:group];
}

@end
音の再生

アクションを使って音を再生することもできます。AVFundation フレームワークや AudioToolbox フレームワークを使って音を再生せるよりも簡単にできます。
以下はシーンでBGM音源を再生させるためのプログラム例です。

@implementation MyScene

-(instancetype)initWithSize:(CGSize)size {
  if (self = [super initWithSize:size]) {
    // 音声ファイルのパスを指定してアクションを生成
    SKAction *sound = [SKAction playSoundFileNamed:@“bgm.m4a" waitForCompletion:YES];
    // 音を再生する
    [self runAction:sound];
  }
}

@end