Xcode
iOS
SpriteKit
Swift

【iOS】【swift】SpriteKitによるAngry Birdsのようなゲームを作る。

More than 1 year has passed since last update.

Xcode5 + ObjC版を作成し、以下記事を投稿しました。
iOS SpriteKitによるAngry Birdsのようなゲームを作る。

http://qiita.com/kitanoow/items/b60e793d0b78fd861a61

今回は、Xcode6 + swift版です。

横画面対応

http://qiita.com/kitanoow/items/4be69cf705c6aef3a6c6
こちらを参考にしてください。

nodeの配置

今回は、力を加える(飛ばす)ボールと、障害物を用意します。
ボールについては、はじめから表示させておき、
障害物については、画面外に配置させ、あえてスクロールが必要な状態にしたいと
思います。

また、今回はボールに対して、センタリング処理を実現する予定ですので、
以下のようにworld/cameraの配置も行います。

world/camera については以下に記述しています。

GameScene.swift
    override func didMoveToView(view: SKView) {
        /* Setup your scene here */

        var size:CGSize = view.frame.size

        self.backgroundColor = UIColor(red:0.15,green:0.15,blue:0.3,alpha:1.0)
        self.anchorPoint = CGPointMake(0.5, 0.5)
        myWorld.name = "world"
        self.addChild(myWorld)

        var camera:SKNode = SKNode()
        camera.name = "camera";
        myWorld.addChild(camera)

GameScene.swift
    override func didSimulatePhysics(){
        var camera:SKNode = self.childNodeWithName("//camera")!
        self.centerOnNode(camera)

    }
    func centerOnNode(node:SKNode) {
        if let scene:SKScene = node.scene
        {
            var cameraPositionInScene:CGPoint = scene.convertPoint(node.position, fromNode: node.parent!)
            node.parent!.position = CGPointMake(node.parent!.position.x - cameraPositionInScene.x,                                       node.parent!.position.y - cameraPositionInScene.y);
        }
    }

ボール・地面・障害物の配置

それぞれdidMoveToViewにて、

GameScene.swift
        //地面
        var ground:SKSpriteNode = SKSpriteNode(color: SKColor.brownColor(),
            size:CGSizeMake(size.width*10,size.height))
        ground.position = CGPointMake(0, -ground.size.height + 30);
        ground.physicsBody = SKPhysicsBody(rectangleOfSize: ground.size)
        ground.physicsBody!.dynamic = false;
        myWorld.addChild(ground)

        //ボール
        ball = SKSpriteNode(imageNamed:"ball.png")
        ball.position = CGPointMake(0, -20);
        ball.name = "ball";
        ball.physicsBody = SKPhysicsBody(circleOfRadius:ball.size.width/2)
        ball.physicsBody!.dynamic = false;
        myWorld.addChild(ball)

なお、ball.pngは
ball.png
とします。

ついでといってはアレですが、今回利用するメンバ変数やenum値については
先に書いておきます。

GameScene.swift
enum GameStatus:Int{
    case kDragNone=0,  //初期値
    kDragStart, //Drag開始
    kDragEnd   //Drag終了
}

GameScene.swift

class GameScene: SKScene,SKPhysicsContactDelegate {
    var ball:SKSpriteNode!;
    var target:SKSpriteNode!;
    var gameStatus:Int = 0;
    var startPos:CGPoint!;
    var sprtical_flg:Bool = false;
    var myWorld:SKNode = SKNode()


障害物についてはdidMoveToViewにて以下を追記します。

GameScene.swift
        //障害物
        var start_x:Int = 500;
        var sprite:SKSpriteNode = SKSpriteNode(color: SKColor.greenColor(), size: CGSizeMake(15,80))
        sprite.physicsBody = SKPhysicsBody(rectangleOfSize: sprite.size)
        sprite.physicsBody!.categoryBitMask = 0x1 << 1;
        sprite.position = CGPointMake(CGFloat(start_x + 100),-90);
        myWorld.addChild(sprite)

        sprite = SKSpriteNode(color: SKColor.greenColor(), size: CGSizeMake(15,80))
        sprite.physicsBody = SKPhysicsBody(rectangleOfSize: sprite.size)
        sprite.physicsBody!.categoryBitMask = 0x1 << 1;
        sprite.position = CGPointMake(CGFloat(start_x + 200),-90);
        myWorld.addChild(sprite)

        sprite = SKSpriteNode(color: SKColor.greenColor(), size: CGSizeMake(150,15))
        sprite.physicsBody = SKPhysicsBody(rectangleOfSize: sprite.size)
        sprite.physicsBody!.categoryBitMask = 0x1 << 1;
        sprite.position = CGPointMake(CGFloat(start_x + 150),-50);
        myWorld.addChild(sprite)

        sprite = SKSpriteNode(color: SKColor.greenColor(), size: CGSizeMake(15,50))
        sprite.physicsBody = SKPhysicsBody(rectangleOfSize: sprite.size)
        sprite.physicsBody!.categoryBitMask = 0x1 << 0;
        sprite.position = CGPointMake(CGFloat(start_x + 120),-20);
        myWorld.addChild(sprite)

        sprite = SKSpriteNode(color: SKColor.greenColor(), size: CGSizeMake(15,50))
        sprite.physicsBody = SKPhysicsBody(rectangleOfSize: sprite.size)
        sprite.physicsBody!.categoryBitMask = 0x1 << 0;
        sprite.position = CGPointMake(CGFloat(start_x + 180),-20);
        myWorld.addChild(sprite)

        sprite = SKSpriteNode(color: SKColor.greenColor(), size: CGSizeMake(100,15))
        sprite.physicsBody = SKPhysicsBody(rectangleOfSize: sprite.size)
        sprite.position = CGPointMake(CGFloat(start_x + 150),0);
        sprite.physicsBody!.categoryBitMask = 0x1 << 0;
        myWorld.addChild(sprite)

        target = SKSpriteNode(color: SKColor.redColor(), size: CGSizeMake(15,15))
        target.physicsBody = SKPhysicsBody(rectangleOfSize: target.size)
        target.physicsBody!.contactTestBitMask = 0x1 << 0;
        target.position = CGPointMake(CGFloat(start_x + 150),-35);
        myWorld.addChild(target)

また、衝突を検出するために、didMoveToViewに

GameScene.swift
        self.physicsWorld.contactDelegate = self;

も追記します。

障害物については以下のような形を考えています。位置については数値決め打ちで申し訳ないですが。。
赤色が衝突対象物です。
スクリーンショット 2014-01-20 0.31.14.png

また、今回の衝突については
contact.png
上記のようなピンクで囲った箇所が赤色のNodeにぶつかったら
その箇所にパーティクルを表示させたいと思いますので、
ピンクで囲ったNodeとにtargetのNode(赤色)ついては、
categoryBitMaskを 1(0x1<<0)を
それ以外については
contactTestBitMaskを 2(0x1<<1)を指定しています。
衝突とmask値の関係については以下にまとめています。ざっくりですが。

【Xcode5】Xcodeの使い方 SpriteKit 編 Vol11 〜 ノード(剛体)の衝突とBitMaskについて〜

タッチを検出し、nodeを移動させる

行うこととしましては

  • タッチ開始時に、そこに、ballノードがあれば、その座標の保持とステータス変更。
  • タッチ移動時には、ballの位置をそのタッチの位置に合わせて変更。
  • タッチ終了時には、力を加えるということと、ズームアウトなどのアニメーションを実行します。
GameScene.swift

    override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
        /* Called when a touch begins */

        for touch: AnyObject in touches {

            let location = touch.locationInNode(self)

            var node:SKNode! = self.nodeAtPoint(location);
            if(node != nil){
                if(node.name=="ball"){
                    gameStatus = GameStatus.kDragStart.rawValue;
                    startPos = location;
                }
            }
        }
    }

    override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
        if(gameStatus == GameStatus.kDragStart.rawValue ){
            var touch:UITouch = touches.anyObject() as UITouch;
            var touchPos:CGPoint = touch.locationInNode(self) ;
            ball.position = touchPos;
        }


    }
    override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
        if(gameStatus == GameStatus.kDragStart.rawValue  ){
            gameStatus = GameStatus.kDragEnd.rawValue ;

            var touch:UITouch = touches.anyObject() as UITouch;
            var endPos:CGPoint = touch.locationInNode(self) ;
            //x,yの移動距離を算出
            var diff:CGPoint = CGPointMake(startPos.x - endPos.x, startPos.y - endPos.y);
            ball.physicsBody!.dynamic = true;
            //yを少し大きく
            ball.physicsBody!.applyForce(CGVectorMake(diff.x * 20 , diff.y * 50))

            var scaleOut:SKAction = SKAction.scaleTo(0.5,duration:0.2);
            var moveUp:SKAction   = SKAction.moveByX(0,y:-100,duration:0.2);
            var scale1:SKAction   = SKAction.group([scaleOut,moveUp]);
            var delay:SKAction    = SKAction.waitForDuration(1.0);
            var scaleIn:SKAction  = SKAction.scaleTo(1,duration:1.0);
            var moveDown:SKAction = SKAction.moveByX(0,y:100,duration:1.0);
            var scale2:SKAction   = SKAction.group([scaleIn,moveDown])
            var moveSequence:SKAction = SKAction.sequence([scale1, delay,scale2]);
            myWorld.runAction(moveSequence)

        }
    }


より上空にあがるようにするために、y向きに少し力を大きめにしています。
また、アニメーションについては

  • ズームアウトと上への移動
  • 待機
  • ズームインと下への移動 をsequenceで実行しています。

SKActionの、groupとsequenceの違いについては
こちらにまとめています。
【Xcode5】Xcodeの使い方 SpriteKit 編 Vol3 〜 SKActionの使い方その1 〜

ただ、このままではボールが画面外に出た場合に追跡できないため、
ballが中心より右側に移動した場合は、カメラの位置も、ballの位置になるように
変更したいと思います。ただし今回は、ballのxだけを引きづくようにします。

GameScene.swift
    override func didSimulatePhysics(){
        var camera:SKNode = self.childNodeWithName("//camera")!
        if gameStatus == GameStatus.kDragEnd.rawValue && ball.position.x > 0 {
            camera.position = CGPointMake(ball.position.x, camera.position.y);
        }
        self.centerOnNode(camera)
    }

パーティクルの実行について

衝突が合った場合は

GameScene.swift
//衝突開始時
    func didBeginContact(contact:SKPhysicsContact){

        if !sprtical_flg {
            sprtical_flg = true;
            var path:String = NSBundle.mainBundle().pathForResource("MyParticle", ofType: "sks")!
            var spark:SKEmitterNode = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as SKEmitterNode!
            println(contact.contactPoint)
            spark.numParticlesToEmit = 50;
            spark.particlePosition = target.position;
            myWorld.addChild(spark)
            ball.removeFromParent();
            target.removeFromParent();
        }

    }

衝突が合った箇所にパーティクルを表示させ
赤色のNode(target)を非表示にしています。

パーティクルの利用については

【Xcode5】Xcodeの使い方 SpriteKit 編 Vol3 〜 SKActionの使い方その1 〜

ひとまず以上です。

細かい部分は微妙な所や決め打ちな所があって恐縮ですが、
何かの参考になれば幸いでございます。

あと以下にソースをアップしておりますので、よろしければ
AngryBirdsLikeGameBySwift