Xcode 10、SceneKit の Cross-platform Game テンプレートを自力でつくる
Xcode 9 までは Cross-platform Game テンプレートは SpriteKit しかなかったのだが、Xcode 10 から SceneKit と Metal が増えた。
今回はこちらがどのような構造になっているのかというのと、このテンプレートのつくり方を探ってみる。
特に何かなければ、自力でつくらずに Cross-platform Game テンプレートをつくり、watchOS などいらないものを消していったほうが楽かと思われる。
Command + Shift + N で新規プロジェクトを作成し、Cross-platform タブの Cross-platform Game テンプレートを選択して Game technology を SceneKit にするだけ。
構造
1つのプロジェクトファイルを起点に iOS、iOS のエクステンションとなる watchOS、macOS、tvOS がターゲットとして設定されており、共有するアセットやコードを1つにまとめている。
また、watchOS では Notification や Complication のアイコンなどが設定されていないので必要であれば設定する必要がある。
テンプレートであるため、プロジェクトファイルに対してターゲットが付加されているが、 Cocoa Pods のようにワークスペースをつくり(Command + Control + N)に iOS + watchOS、macOS、tvOS のプロジェクトを設定してもよいかもしれない。
共有するデータを誤って消したり外れたりする場合さえ注意すれば、こちらの管理の方が楽そうではある。
(各プラットフォームプロジェクト単位で他の人が触れるため)
プロジェクトをつくる流れ
- iOS のプロジェクトをつくる(どのプロジェクトでも構わないがここでは iOS)
- ターゲットから macOS、tvOS、watchOS のプロジェクトをつくる
- アセットをひとつにまとめる
ひとまず、全てのプラットフォームで動くプロジェクトをつくる
新規プロジェクトで iOS タブの Game テンプレートを選択しプロジェクトを作成。
Xcode のアプリケーションメニュー「File」>「New」>「Target」から、 macOS のタブを選択し、Game のテンプレートを選択して保存する。
同じように、Target から tvOS と watchOS の Game のテンプレートを選択して保存を繰り返す。
あとは、各プラットフォームのスキーマを変更してビルドするだけ。
シーンファイルや画像、アイコンなどのリソースを1つにまとめる
流れ
- 新規でグループを作成する
- シーンファイルを含む art.scnassets フォルダをコピーする
- scnassets のターゲットを変更する
- xcassets で各プラットフォームのアイコンを設定する
- xcassets のターゲットを変更する
- 各プラットフォームでアイコンや起動画像の参照先を変更する
- 各プラットフォームの scnassets や xcassets を削除する
シーンファイルが格納されている scnassets を全てのプラットフォームで使用できるように設定する
まず、共有するため適当に新規でグループを作成。ここでは「Shared」として作成した。
そして、iOS から art.scnassets のフォルダを移動させる。
そのままだと、Targets Membership で iOS しかこのリソースを読み込むことができないので、macOS、tvOS、watchOS の Extension にチェックを入れる。
(watchOS のアプリ側ではなく Extension にチェックを入れているのは、Extension がコードやリソースを持つため)
必要であれば、アイコン画像と起動画面を共有する
アイコン画像と起動画面はサイズが異なり基本的には共有できないため、 個人的には各プラットフォームのフォルダにアイコン画像等があった方が良い気はするので必要なければ読み飛ばしてかまわない。
設定してみる
新規で xcassets を作成するか、各プラットフォームから移動させる。
新規した場合 xcassets を開き左側で右クリックか下のプラスボタンクリックから「App Icons & Launch Images」から以下の Image Set を設定する
- New iOS App Icon
- New macOS App Icon
- New tvOS App Icon and Top Shelf Image
- New tvOS Launch Image
- New watchOS App Icon
設定が完了したらプロジェクトの「General」>「TARGETS」から各プラットフォームの Image Set を関連づける。
watchOS の Extension 側ではなくアプリの方を開く。
OS | 設定箇所 | 上で作成したリスト番号で設定する |
---|---|---|
iOS | App Icons & Launch Images > App Icons Source | 1 |
macOS | App Icons > Source | 2 |
tvOS | App Icons & Launch Images > App Icons Source | 3 |
tvOS | App Icons & Launch Images > Launch Images Source | 4 |
watchOS | App Icons > App Icons Source | 5 |
xcassets は作成したプラットフォームしか読み込まれないので、 Targets Membership で、macOS、tvOS、watchOS にチェックを入れる。
汎用的なコードを一つにまとめる
Cross-platform Game テンプレートでは共有するフォルダに GameController.swift というファイルがある。
GameController クラスは NSObject の派生クラスで、SCNSceneRendererDelegate のプロトコルが設定されており、このクラスのイニシャライズで SCNSceneRenderer(SCNView) が参照渡しされる。
参照している SCNView に SCNScene と SCNSceneRenderer の delegate と関数を設定。
そして、タッチされた際にシーンのジオメトリのマテリアルの色を変える関数を設定している。
先頭に watchOS の場合「import WatchKit」を使用する分岐と macOS の場合に NSColor を使用する SCNColor という typealias が設定されている。
なぜこのようなつくりになっているのか
SCNView を NSObject のクラスに参照させ、このクラスでシーンの処理をしている。
元となる ViewController 側では SCNView の初期設定と View へのタッチやクリックイベント、ゲームコントローラーなどを設定するだけで表示部分や何らかの操作からの表示変更は各プラットフォーム共通して使用することができる。
厳密には watchOS では荷が重い処理があったりするので分岐や調整は必要。
また、A8 より前と A9 からでは SceneKit / Metal で使える機能が増えているためそこら辺も注意。
GameController.swift のコード
import SceneKit #if os(watchOS) import WatchKit #endif #if os(macOS) typealias SCNColor = NSColor #else typealias SCNColor = UIColor #endif class GameController: NSObject, SCNSceneRendererDelegate { let scene: SCNScene let sceneRenderer: SCNSceneRenderer init(sceneRenderer renderer: SCNSceneRenderer) { sceneRenderer = renderer scene = SCNScene(named: "Art.scnassets/ship.scn")! super.init() sceneRenderer.delegate = self if let ship = scene.rootNode.childNode(withName: "ship", recursively: true) { ship.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 2, z: 0, duration: 1))) } sceneRenderer.scene = scene } func highlightNodes(atPoint point: CGPoint) { let hitResults = self.sceneRenderer.hitTest(point, options: [:]) for result in hitResults { // get its material guard let material = result.node.geometry?.firstMaterial else { return } // highlight it SCNTransaction.begin() SCNTransaction.animationDuration = 0.5 // on completion - unhighlight SCNTransaction.completionBlock = { SCNTransaction.begin() SCNTransaction.animationDuration = 0.5 material.emission.contents = SCNColor.black SCNTransaction.commit() } material.emission.contents = SCNColor.red SCNTransaction.commit() } } func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) { // Called before each frame is rendered } }
ViewController 側のコード
iOS と tvOS はほぼ同じ、macOS は UIColor > NSColor、タッチイベントをクリックイベントに変える。
watchOS は Storyboard で Tap Gesture Recognizer が設定されている。
(テンプレートでは @IBAction が設定されていないので注意)
iOS / tvOS (GameController.swift)
var gameView: SCNView { return self.view as! SCNView } var gameController: GameController! override func viewDidLoad() { super.viewDidLoad() self.gameController = GameController(sceneRenderer: gameView) // Allow the user to manipulate the camera self.gameView.allowsCameraControl = true // Show statistics such as fps and timing information self.gameView.showsStatistics = true // Configure the view self.gameView.backgroundColor = UIColor.black // Add a tap gesture recognizer let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:))) var gestureRecognizers = gameView.gestureRecognizers ?? [] gestureRecognizers.insert(tapGesture, at: 0) self.gameView.gestureRecognizers = gestureRecognizers } @objc func handleTap(_ gestureRecognizer: UIGestureRecognizer) { // Highlight the tapped nodes let p = gestureRecognizer.location(in: gameView) gameController.highlightNodes(atPoint: p) }
macOS (GameController.swift)
var gameView: SCNView { return self.view as! SCNView } var gameController: GameController! override func viewDidLoad() { super.viewDidLoad() self.gameController = GameController(sceneRenderer: gameView) // Allow the user to manipulate the camera self.gameView.allowsCameraControl = true // Show statistics such as fps and timing information self.gameView.showsStatistics = true // Configure the view self.gameView.backgroundColor = NSColor.black // Add a click gesture recognizer let clickGesture = NSClickGestureRecognizer(target: self, action: #selector(handleClick(_:))) var gestureRecognizers = gameView.gestureRecognizers gestureRecognizers.insert(clickGesture, at: 0) self.gameView.gestureRecognizers = gestureRecognizers } @objc func handleClick(_ gestureRecognizer: NSGestureRecognizer) { // Highlight the clicked nodes let p = gestureRecognizer.location(in: gameView) gameController.highlightNodes(atPoint: p) }
watchOS (InterfaceController.swift)
@IBOutlet var scnInterface: WKInterfaceSCNScene! var gameController: GameController! override func awake(withContext context: Any?) { super.awake(withContext: context) gameController = GameController(sceneRenderer: scnInterface) } @IBAction func handleTap(_ gestureRecognizer: WKTapGestureRecognizer) { // Highlight the tapped nodes let p = gestureRecognizer.locationInObject() gameController.highlightNodes(atPoint: p) }
まとめ
自力でつくる必要はないのだが、全てのプラットフォームでの SceneKit アプリの作成と各プラットフォームでの差があまりないことが分かったと思われる。
ゲーム系アプリとなるとフルスクリーン表示となり、 操作系以外はわりと iOS と tvOS の違いは少ないため移植は簡単かつ、tvOS のアプリ自体が少ないため狙いどころではある。