SpriteKitでAngryBird風のゲームを作るチュートリアル

SpriteKitだと物理エンジンを使ったゲームがcocos2dと比べて非常に簡単に作れるっぽかったので、AngryBird風のゲームを途中まで作ってみました。

別の人がQiitaで画像/動画付きのチュートリアルを上げてるので、文字ばっかなのが苦手な人はこちらをどうぞ。iOS SpriteKitによるAngry Birdsのようなゲームを作る。 - Qiita

SpriteKitとは

iOS7からSDKに含まれるようになった、Apple製の2Dゲームフレームワークです。物理エンジンも標準搭載されています。

iOSの2Dゲームフレームワークだと、cocos2dが有名だと思うけど、今から学習するのであれば、SpriteKit一択だと思います。

KoboldTouchやcocos2dで有名なSteffen Itterheimさんがそんな感じの事を言ってます。

cocos2dとの違い

しっかりと触ったわけでは無いけど、使い方はほとんどcocos2dです。ある機能を提供するクラス名が変わってたりして、最初は戸惑うかもしれないけど、SpriteKit自体はクラスがそんなに多くないので、最初にどんなクラスがあるかを把握してしまえば、すんなりと使えると思います。

Sprite Kit Framework Reference

cocos2dとの大きな違いはあまりないけど、列挙すると、以下のようになります。

propertyやメソッド名がUIKitとだいたい同じような命名なため、UIKitを使ったアプリ開発に慣れている人でも戸惑わずに開発できると思います。

cocos2dで物理エンジンを使用したゲームを作ろうとすると、Box2DかChipmunk(C or C++)の二択で、少しつらい感じでした。cocos2dでBox2dでworld作ってbody作って毎フレームworld->step呼んで物理演算の結果をCCNodeに伝えて〜みたいな感じだったのが、SpriteKitだと、SKNode.physicsBodyにSKPhysicsBodyインスタンスをセットして、任意のSKnode(or SKScene)にaddChildするだけで後は勝手にやってくれます。

ただ、最低限しか実装されていない印象があるので、物理演算が重要なアプリとかはSpriteKitと任意の物理エンジンを組み合わせたほうがいい気がしました。privateなHeaderを見る感じ、内部的にはbox2dを使ってるっぽいです。

iOS7-Runtime-Headers/PrivateFrameworks/PhysicsKit.framework/PKPhysicsJointRevolute.h at master · JaviSoto/iOS7-Runtime-Headers · GitHub

その他もろもろは、上に張ったWhy Apple Created Sprite Kit And What It Means For Cocos2Dを読めば、cocos2dとSpriteKitどちらを採用すべきか?というのが判断しやすいと思います。

マルチプラットフォームとか3Dとかを考えると別の選択肢があるけど、iOSの2DゲームだったらSpriteKit一択という感じっぽい。


AngryBird風ゲームを作る

ここから本題。実際に作りながら書いてるので、説明が色々と抜けてたりするかもしれません。

説明とかいらん!って人とかはgithubにproject上げてるので、それを見るといいかも。

あとは、commitログ順に進んでくと、だいたい作業順になるので、参考にどうぞ。

gin0606/AngryBirdClone · GitHub

Projectを作る

Xcodeを開いて新しいProjectを作成します。iOS->ApplicationにあるSpriteKit Gameを選択します。

プロジェクト名は好きに付けましょう(僕はAngryBirdCloneにしました)。その他項目も適当に付けましょう。Class PrefixABにするとこの後の説明が読みやすいかもしれません。

とりあえず動かしてみる

Projectを作った状態でアプリを実行すると、Sampleアプリみたいなのが起動します。

画面をタッチすると宇宙船が出てきてくるくる回ります。

サンプルコードを消す

ABMySceneにあるコードをひと通り消しましょう。

こんな感じ

#import "ABMyScene.h"

@implementation ABMyScene

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

    }
    return self;
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

}

@end

説明しやすくするために、殆どのコードをABMySceneに追加するので、設計とかは参考にしないでください。

横画面にする

projectの設定のGeneral -> Deployment Info -> Device OrientationLandscape LeftLandscape Rightにして、ABViewController-viewDidLoadを書き換えます。

// 24行目あたり
SKScene * scene = [ABMyScene sceneWithSize:skView.bounds.size];
scene.scaleMode = SKSceneScaleModeResizeFill; // SKSceneScaleModeAspectFill -> SKSceneScaleModeResizeFill

ABViewControllerのself.view.frameが縦画面の状態になってて、色々とズレます。以降には支障は無いので放置するけど、ちゃんと横画面にする方法が分かったら修正します。

物理エンジンを体験してみる

このまま実行しても何も起こらないので、とりあえず-initWithSize:物理エンジンの効果が分かるコードを追加します。

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

SKLabelNode *helloPhysicsWorld = [SKLabelNode labelNodeWithFontNamed:@"HelveticaNeue"];
helloPhysicsWorld.text = @"Hello physics world!";
helloPhysicsWorld.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMaxY(self.frame));
helloPhysicsWorld.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:helloPhysicsWorld.frame.size];
[self addChild:helloPhysicsWorld];

実行すると画面の真ん中に表示されるHello SpriteKit!と、落ちていくHello physics world!という文字が見えたと思います。

helloPhysicsWorld.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:helloPhysicsWorld.frame.size];

この一行だけで物理演算の対象に出来ました。便利!

地面を作る

helloPhysicsWorldが落ちていって可哀想なので地面を作ります。 ground.pngを作成&追加して -initWithSize:に下のコードを追加します。

SKSpriteNode *ground = [SKSpriteNode spriteNodeWithImageNamed:@"ground.png"];
ground.position = CGPointMake(CGRectGetMidX(self.frame), 0);
ground.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:ground.frame.size];
ground.physicsBody.dynamic = NO;
[self addChild:ground];

実行すると、helloPhysicsWorldがgroundにぶつかって画面外に出なくなりました。

鳥を飛ばす

鳥を追加する

その前に、そろそろhelloPhysicsWorldが邪魔になってくるので、helloPhysicsWorld関連の5行を消します。

bird.pngを作成&追加して -initWithSize:に下のコードを追加します。

self.bird = [SKSpriteNode spriteNodeWithImageNamed:@"bird.png"];
self.bird.position = CGPointMake(50, 50);
self.bird.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:self.bird.frame.size];
self.bird.physicsBody.dynamic = NO;
[self addChild:self.bird];

dynamic == NOなphysicsBodyは、重力の影響を受けなくなってその場に固定されたような感じになります。

鳥を引っ張る

まずは、birdをpropertyにして他のメソッドからも参照できるようにします。

@interface ABMyScene ()
@property(nonatomic, strong) SKSpriteNode *bird;
@end

-initWithSize:でbirdを使ってるところも適宜self.birdに書き換えます。

-touchesBegan:withEvent:, -touchesMoved:withEvent:, -touchesEnded:withEvent:をオーバーライドして下記のような感じにします。

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    CGPoint touchPos = [touch locationInNode:self];

    if ([self.bird containsPoint:touchPos]) {
        self.bird.physicsBody.dynamic = YES;

        // タッチ地点とbirdをくっつける
        self.mouseNode = [SKNode node];
        self.mouseNode.position = touchPos;
        self.mouseNode.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:CGSizeMake(1, 1)];
        self.mouseNode.physicsBody.dynamic = NO;
        [self addChild:self.mouseNode];

        SKPhysicsJointFixed *fixed = [SKPhysicsJointFixed jointWithBodyA:self.bird.physicsBody bodyB:self.mouseNode.physicsBody anchor:self.mouseNode.position];
        [self.physicsWorld addJoint:fixed];
    }
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    CGPoint touchPos = [touch locationInNode:self];
    self.mouseNode.position = touchPos;
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    [self.mouseNode.physicsBody.joints enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        [self.physicsWorld removeJoint:obj];
    }];
    [self.mouseNode removeFromParent];
}

実行すると、鳥が指についてきて、指を話すと落ちます。

鳥を飛ばす

-touchesEnded:withEvent:の最後に下記のコードを追加します。

UITouch *touch = [touches anyObject];
CGPoint touchPos = [touch locationInNode:self];
// 発射角度計算
CGPoint p = CGPointMake(50 - touchPos.x, 50 - touchPos.y);
float radians = atan2f(p.y, p.x);
CGPoint angle = CGPointMake(cosf(radians), sinf(radians));

[self.bird.physicsBody applyForce:CGVectorMake(angle.x * 5000, angle.y * 5000)];

これで指を離すと鳥が飛ぶようになりました。

もうちょいちゃんと実装する場合

AngryBirdみたいに鳥が発射台から一定以上離れないようにしたい時は、下記のコードみたいにするといいと思います。

// ファイルの先頭あたりに`#define kShotPos CGPointMake(50, 50)`を追加する
CGPoint shotPoint = kShotPos;
CGPoint point = CGPointMake(touchPos.x - shotPoint.x, touchPos.y - shotPoint.y);
float diff = sqrtf(point.x * point.x + point.y * point.y);
if (diff > 30.f) {
    float radians = atan2f(point.y, point.x);

    CGPoint p = CGPointMake(shotPoint.x + 30.f, shotPoint.y);
    CGPoint r = CGPointMake(p.x - shotPoint.x, p.y - shotPoint.y);
    float cosa = cosf(radians);
    float sina = sinf(radians);
    float t = r.x;
    r.x = t * cosa - r.y * sina + shotPoint.x;
    r.y = t * sina + r.y * cosa + shotPoint.y;

    self.mouseNode.position = r;
} else {
    self.mouseNode.position = touchPos;
}

cocos2dやKoboldKit使ってると下記みたいな感じになります。 KoboldKit/KoboldKit/KoboldKitExternal/External/CGPointExtension/CGPointExtension.m at master · KoboldKit/KoboldKit · GitHub

float diff = ccpDistance(touchPos, shotPoint);
if (diff > 30.f) {
    float radians = ccpToAngle(ccpSub(touchPos, shotPoint));

    CGPoint p = CGPointMake(shotPoint.x + 30.f, shotPoint.y);
    p = ccpRotateByAngle(p, shotPoint, radians);

    self.mouseNode.position = p;
} else {
    self.mouseNode.position = touchPos;
}

障害物を作る

これで最後です。鳥が狙いを定める障害物を作ります。

障害物の表示

target.png-initWithSize:に下記コードを追加する。

SKSpriteNode *target = [SKSpriteNode spriteNodeWithImageNamed:@"target.png"];
target.position = CGPointMake(CGRectGetMaxX(self.frame), CGRectGetMidY(self.frame));
target.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:target.frame.size];
[self addChild:target];

衝突判定の準備

ABMySceneにSKPhysicsContactDelegateを設定します。interfaceが↓こんな感じになる。

@interface ABMyScene () <SKPhysicsContactDelegate>
@property(nonatomic, strong) SKSpriteNode *bird;
@property(nonatomic, strong) SKNode *mouseNode;
@end

-initWithSize:に下記コードを追加します。

self.physicsWorld.contactDelegate = self;

鳥と障害物に名前を付けます。-initWithSize:に下記コードを追加。

self.bird.name = @"bird";
target.name = @"target";

さらに下記コードを追加

self.bird.physicsBody.contactTestBitMask = 1;
target.physicsBody.contactTestBitMask = 1;

BitMask設定しないと、SKPhysicsContactDelegateのメソッドが呼ばれないので結構ハマりどころだと思う。

衝突判定

SKPhysicsContactDelegateのメソッドを実装します。

SKPhysicsContactDelegateには-didBeginContact:-didEndContact:があって、今回はBeginを使います。

以下のように- (void)didEndContact:を実装。

- (void)didBeginContact:(SKPhysicsContact *)contact {
    if ([contact.bodyA.node.name isEqualToString:@"bird"]
            && [contact.bodyB.node.name isEqualToString:@"target"]) {
        [contact.bodyB.node removeFromParent];
    }
}

これで鳥がTargetにぶつかるとTargetが消えます!

やったね!

終わり!!!!

さいごに

進むに連れて説明が適当になっていったけど、全体的にそんなに難しいことやってないので、その他のゲームエンジンとかUIKit使ったことある人なら簡単にSpriteKit入門できると思います。

SpriteKit自体は簡単なんだけど、やっぱり物理演算を使う場合はSpriteKitについてくるやつじゃなくて、別個でbox2dとか導入したほうがいい気がした。

box2d触ったの結構前なので、色々と曖昧なんだけど、Jointの種類がいろいろあって良かった。cocos2dのテストprojectにbox2dのひと通りの機能見られるのもあって、それを参考にして実装してけばなんとかなった。

SpriteKit良いんだけど、生で触るのは若干微妙な感じするので、ガッツリ使うならKoboldKit使ったほうがいいと思う。

最後に一応もっかいgithubのURL

gin0606/AngryBirdClone · GitHub

参考