はじめに
iOSのScenekKitで作ったゲームをリリースしたので、メモとして残しておきます。
どんなゲーム?
一言で言うと動画のジグソーパズル。
ピースが下部に配置されているので、それを画面上部のエリアにドラッグし、上下左右を合わせて正しい形にセットするジグソーパズルっぽいゲーム。
(要は昔ファミコンであったきね子というゲーム)
アーキテクチャ(仕組み)
・動画部分を何で作るか
静止画のパズルであれば、画像を分割すればいいだけ。
動くパズルということで、ここを何で作るかが一番の課題。
movやmpegなどのムービーファイルを用いる案は、実装は簡単そうだけど、動画自体をオリジナルで作るのは大変。どこかから用意する必要がある。
リアルタイムに描画して作成したほうがいいと思ったので、3Dも可能な
SceneKitを採用。ただしこちらは実装が多分難しそう。
かつSceneKitは結構マイナーな技術なのでWEBに情報はあまりなさそう。
ただ現在はAIがあるので、聞きながら作れば、情報がないのはあまり関係ない。
実装ポイント
SceneKitのViewは分割できるか?が一番のポイント
解決方法として、
SceneKit の View を CADisplayLink でミラーリング し、それを分割して各 View に適用する方法を採用。下記にざっくり概要を解説。
- SCNView のミラーリング
SceneKit の View (SCNView) を 定期的にキャプチャ し、それを画像として利用するために CADisplayLink を使用します。
var _displayLink: CADisplayLink!
func startMirroring() {
_displayLink = CADisplayLink(target: self, selector: #selector(updateMirrorView))
_displayLink.preferredFramesPerSecond = 30 // 30fps
_displayLink.add(to: .current, forMode: .common)
}
func stopMirroring() {
_displayLink.invalidate()
_displayLink = nil
}
✔ CADisplayLink を利用することで、一定間隔で SceneKit の描画をキャプチャ可能
✔ フレームレートを 30fps に設定し、負荷を抑える
- SceneKit の描画を画像として取得
UIGraphicsImageRenderer を使用し、現在の SceneKit の描画内容を画像 (UIImage) として取得 します。
@objc func updateMirrorView() {
let renderer = UIGraphicsImageRenderer(size: scnView.bounds.size)
let image = renderer.image { _ in
scnView.drawHierarchy(in: scnView.bounds, afterScreenUpdates: false)
}
✔ drawHierarchy(in:afterScreenUpdates:) を使うことで、現在の SceneKit の描画をそのままキャプチャ
✔ リアルタイムの表示内容を取得可能
- 画像の分割
キャプチャした UIImage を グリッド状に分割し、各セルの View に適用 します。
var croppedImages: [UIImage] = [] // 分割後の画像を保持する配列
let scale = Int(UIScreen.main.scale) // デバイスのスケールを考慮
let cropWidth = Int(scnView.bounds.width) / Board.shared.colCount * scale
let cropHeight = Int(scnView.bounds.height) / Board.shared.rowCount * scale
for y in 0..<Board.shared.rowCount {
for x in 0..<Board.shared.colCount {
let cropRect = CGRect(x: x * cropWidth, y: y * cropHeight, width: cropWidth, height: cropHeight)
guard let cgImage = image.cgImage?.cropping(to: cropRect) else {
continue
}
let croppedImage = UIImage(cgImage: cgImage)
✔ cgImage?.cropping(to:) を使って画像を切り出し
✔ デバイスの scale を考慮し、高品質な分割を実現
- 画像のリサイズ
分割した画像を、各 View のサイズに合わせてリサイズします。
let newSize = CGSize(width: Board.shared.pieceWidth, height: Board.shared.pieceHeight)
UIGraphicsBeginImageContextWithOptions(newSize, false, 0.0)
croppedImage.draw(in: CGRect(origin: .zero, size: newSize))
let resizedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
croppedImages.append(resizedImage!)
}
}
}
✔ UIGraphicsBeginImageContextWithOptions を使って適切なサイズにリサイズ
✔ 高解像度デバイス(Retinaディスプレイ)でも美しく表示
ステージ追加は楽
SceneKitで作ったことによって、気分転換がてらに3Dの勉強しつつステージを追加してアップデートできるので、なかなかいい仕組みで作れたと思います。(現在7ステージ)
AppStoreリンク