しめ鯖日記

swift, iPhoneアプリ開発, ruby on rails等のTipsや入門記事書いてます

GameplayKitの「Entities and Components」を試してみる

GameplayKitの「Entities and Components」を試してみました。

Entities and Componentsとは

Entities and Componentsとは、コンポーネント指向プログラミングをサポートするものです。
コンポーネント指向プログラミングとは、そのオブジェクトの性質を継承などではcomponentの追加を使って表すものです。

例えばタップ時にジャンプするオブジェクトにはジャンプコンポーネントを追加を、ランダムに移動するオブジェクトにはランダム移動コンポーネントの追加をすると言った具合です。
そうすることで、簡単に1つのオブジェクトが複数の性質を持たせる事ができます。

Entities and Componentsの実装

早速実装してみます。
今回はNodeを2つ作って、片方は横移動+タップで回転、もう片方は縦移動+タップで回転するようにします。

まずは画面にNodeを2つ表示します。

import SpriteKit
import GameplayKit

class GameScene: SKScene {
    let node1 = SKSpriteNode(color: UIColor(red: 0.1, green: 0.8, blue: 0.8, alpha: 1), size: CGSize(width: 100, height: 100))
    let node2 = SKSpriteNode(color: UIColor(red: 0.8, green: 0.8, blue: 0.1, alpha: 1), size: CGSize(width: 100, height: 100))
    
    override func didMove(to view: SKView) {
        node1.position.x = -100
        node2.position.x =  100
        
        addChild(node1)
        addChild(node2)
    }
}

f:id:llcc:20170322232927p:plain

次にnode1は右方向、node2は上方向に動くようにします。
まずは上方向に移動する性質を表すHorizontalComponentクラスと横方向に移動する性質を表すVerticalComponentクラスを作ります。

class HorizontalComponent: GKComponent {
    let node: SKNode
    
    init(node: SKNode) {
        self.node = node
        
        super.init()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func update(deltaTime seconds: TimeInterval) {
        node.position.x += 1
    }
}

class VerticalComponent: GKComponent {
    let node: SKNode
    
    init(node: SKNode) {
        self.node = node
        
        super.init()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func update(deltaTime seconds: TimeInterval) {
        node.position.y += 1
    }
}

続けて今作ったクラスをnode1、node2に適用します。

import SpriteKit
import GameplayKit

class GameScene: SKScene {
    let horizontalComponentSystem = GKComponentSystem(componentClass: HorizontalComponent.self)
    let verticalComponentSystem = GKComponentSystem(componentClass: VerticalComponent.self)
    var lastTime: TimeInterval = 0
    
    let node1 = SKSpriteNode(color: UIColor(red: 0.1, green: 0.8, blue: 0.8, alpha: 1), size: CGSize(width: 100, height: 100))
    let node2 = SKSpriteNode(color: UIColor(red: 0.8, green: 0.8, blue: 0.1, alpha: 1), size: CGSize(width: 100, height: 100))
    let entity1 = GKEntity()
    let entity2 = GKEntity()
    
    override func didMove(to view: SKView) {
        node1.position.x = -100
        node2.position.x =  100
        
        addChild(node1)
        addChild(node2)
        
        let horizontalComponent = HorizontalComponent(node: node1)
        entity1.addComponent(horizontalComponent)
        horizontalComponentSystem.addComponent(horizontalComponent)
        
        let verticalComponent = VerticalComponent(node: node2)
        entity2.addComponent(verticalComponent)
        verticalComponentSystem.addComponent(verticalComponent)
    }
    
    override func update(_ currentTime: TimeInterval) {
        if lastTime == 0 {
            lastTime = currentTime
        }
        let deltaTime = currentTime - lastTime
        horizontalComponentSystem.update(deltaTime: deltaTime)
        verticalComponentSystem.update(deltaTime: deltaTime)
        lastTime = currentTime
    }
}

これでnode1が右方向、node2が上方向に移動するようになりました。

f:id:llcc:20170322235811p:plain

entity1とentity2は下のupdateメソッドのように、片方のコンポーネントから別のコンポーネントのメソッドを呼び出す時に便利です。

class HorizontalComponent: GKComponent {
    let node: SKNode
    
    init(node: SKNode) {
        self.node = node
        
        super.init()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func update(deltaTime seconds: TimeInterval) {
        node.position.x += 1
        entity?.components.flatMap { $0 as? VerticalComponent }.forEach { $0.update(deltaTime: seconds) }
    }
}

最後に、それぞれのnodeに「画面タップで回転」という性質を追加します。
「画面タップで回転」という性質を表すRotateComponentクラスを下のように作成します。

class RotateComponent: GKComponent {
    let node: SKNode
    
    init(node: SKNode) {
        self.node = node
        
        super.init()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func rotate() {
        node.run(SKAction.rotate(byAngle: 2, duration: 1))
    }
}

今作ったcomponentをnode1とnode2に反映します。

import SpriteKit
import GameplayKit

class GameScene: SKScene {
    let horizontalComponentSystem = GKComponentSystem(componentClass: HorizontalComponent.self)
    let verticalComponentSystem = GKComponentSystem(componentClass: VerticalComponent.self)
    let rotateComponentSystem = GKComponentSystem(componentClass: RotateComponent.self)
    var lastTime: TimeInterval = 0
    
    let node1 = SKSpriteNode(color: UIColor(red: 0.1, green: 0.8, blue: 0.8, alpha: 1), size: CGSize(width: 100, height: 100))
    let node2 = SKSpriteNode(color: UIColor(red: 0.8, green: 0.8, blue: 0.1, alpha: 1), size: CGSize(width: 100, height: 100))
    let entity1 = GKEntity()
    let entity2 = GKEntity()
    
    override func didMove(to view: SKView) {
        node1.position.x = -100
        node2.position.x =  100
        
        addChild(node1)
        addChild(node2)
        
        let horizontalComponent = HorizontalComponent(node: node1)
        entity1.addComponent(horizontalComponent)
        horizontalComponentSystem.addComponent(horizontalComponent)
        
        let verticalComponent = VerticalComponent(node: node2)
        entity2.addComponent(verticalComponent)
        verticalComponentSystem.addComponent(verticalComponent)
        
        // ここから今回追加
        let rotateComponent1 = RotateComponent(node: node1)
        let rotateComponent2 = RotateComponent(node: node2)
        entity1.addComponent(rotateComponent1)
        entity2.addComponent(rotateComponent2)
        rotateComponentSystem.addComponent(rotateComponent1)
        rotateComponentSystem.addComponent(rotateComponent2)
    }
    
    override func update(_ currentTime: TimeInterval) {
        if lastTime == 0 {
            lastTime = currentTime
        }
        let deltaTime = currentTime - lastTime
        horizontalComponentSystem.update(deltaTime: deltaTime)
        verticalComponentSystem.update(deltaTime: deltaTime)
        lastTime = currentTime
    }
    
    // ここから今回追加
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        rotateComponentSystem.components.flatMap { $0 as? RotateComponent }.forEach {
            $0.rotate()
        }
    }
}

これで画面タップでnodeが回転するようになりました。

f:id:llcc:20170323001027p:plain