SwiftUI で SceneKit と SpriteKit 同時に使う

iOS
UIViewController 時代

SwiftUI が出る前の時代、SceneKit と SpriteKit を同時に使うために、

SCNView の overlaySKScene に SKScene をセットしていました。

SwiftUI 時代

時は、iOS 13, SwiftUI の時代、世の民は UIViewController などという古の様式を捨て去った。

さらに iOS 14 から以下のように UIViewRepresentable などを使わなくても直接、SKScene, SCNScene を SwiftUI の View として読み込めるようになった。

SpriteView – A SwiftUI view that renders a SpriteKit scene.
https://developer.apple.com/documentation/spritekit/spriteview

SceneView – A SwiftUI view for displaying 3D SceneKit content.
https://developer.apple.com/documentation/scenekit/sceneview

これ、使ってみるとわかるんですが、SKView と SCNView が不要になります。

overlaySKScene も不要です。なぜなら SpriteView と SceneView を ZStack で重ねればいいから。

いやはや、これで SpriteKit と SceneKit のコードが分離されて、わかりやすくなるような、ならないような。

少なくとも SKView, SKScene, SCNView, SCNScene の4つが、SKScene, SCNScene の2つだけに集中すればよいので、理解しやすくなると思います!

これから本格的に実装していくうちに何か不便な点が発生するかもしれませんし、しないかもしれません。

とりあえず、UIViewRepresentable を使わない、最低限の SpriteKit + SceneKit on Pure SwiftUI のコードを以下に載せます。

これを contentView.swift にコピペするだけで実行できると思います。

ポイントは GameObject という NSObject のインスタンスを作り、そこの中で SKScene と SCNScene を生成しています。

この GameObject を介することにより、SceneKit を SpriteKit の連携もしやすくなります。

import SwiftUI
import SceneKit
import SpriteKit

struct ContentView: View {
    @StateObject var gameObject = GameObject()
    
    var body: some View {
        ZStack() {
            SceneView(scene: gameObject.scnScene,
                      options: [.allowsCameraControl],
                      delegate: gameObject)
            
            Button(action: {
                gameObject.showsStatistics = true
                gameObject.debugOptions = [.renderAsWireframe,
                                            .showBoundingBoxes]
            }) {
                Text("Debug").padding(.top, 300)
            }
            SpriteView(scene: gameObject.skScene,
                       options: [.allowsTransparency])
        }
    }
}

class GameObject: NSObject, SCNSceneRendererDelegate, ObservableObject {
    var showsStatistics: Bool = false
    var debugOptions: SCNDebugOptions = []
    
    var scnScene: SCNScene = {
        // create a new scene
        //    let scene = SCNScene(named: "art.scnassets/ship.scn")!
        let scene = GameScene()
        return scene
    }()
    
    var skScene: SKScene = {
        let scene = SKScene()
        scene.backgroundColor = .clear

        return scene
    }()
    
    func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {        
        renderer.showsStatistics = self.showsStatistics
        renderer.debugOptions = self.debugOptions
    }
}

class GameScene: SCNScene {
    override init() {
        super.init()
        
        self.background.contents = UIColor.black

        let cg = SCNCapsule(capRadius: 2.0,
                            height: 6.0)
        cg.firstMaterial?.diffuse.contents = UIColor.red
        cg.firstMaterial?.specular.contents = UIColor.white
        let cn = SCNNode(geometry: cg)
        cn.position = SCNVector3(x: 0.0,
                                 y: 50.0,
                                 z: 20.0)
        cn.rotation = SCNVector4(x: 0,
                                 y: 0,
                                 z: 1,
                                 w: Float.pi / 6.0)
        cn.physicsBody = SCNPhysicsBody(type: .dynamic,
                                        shape: nil)
        self.rootNode.addChildNode(cn)

        let cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        cameraNode.position = SCNVector3(0, 20, 100)
        self.rootNode.addChildNode(cameraNode)

        let omniLight = SCNNode()
        omniLight.light = SCNLight()
        omniLight.light?.type = .omni
        omniLight.position = SCNVector3(10, 10, 50)
        self.rootNode.addChildNode(omniLight)

        let floorGeo = SCNFloor()
        let floorNode = SCNNode(geometry: floorGeo)
        floorGeo.firstMaterial?.diffuse.contents = UIColor.green
        floorNode.position.y = -1
        floorNode.physicsBody = SCNPhysicsBody(type: .static,
                                               shape: nil)
        self.rootNode.addChildNode(floorNode)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

それではよい SwiftUI ライフを!

SwiftUIを学ぶのにおすすめの本

SwiftUI 徹底入門

SwiftUIではじめるiPhoneアプリプログラミング入門

iOS/macOS UIフレームワーク SwiftUIプログラミング

コメント

タイトルとURLをコピーしました