0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SceneKitで作ったパズルゲームをリリースした

Posted at

はじめに

iOSのScenekKitで作ったゲームをリリースしたので、メモとして残しておきます。

どんなゲーム?

一言で言うと動画のジグソーパズル。
ピースが下部に配置されているので、それを画面上部のエリアにドラッグし、上下左右を合わせて正しい形にセットするジグソーパズルっぽいゲーム。
(要は昔ファミコンであったきね子というゲーム)

Simulator Screenshot - iPad Air 13-inch (M2) - 2024-09-06 at 17.05.53.png
Simulator Screenshot - iPad Air 13-inch (M2) - 2024-09-01 at 13.45.28.png

アーキテクチャ(仕組み)

・動画部分を何で作るか
静止画のパズルであれば、画像を分割すればいいだけ。
動くパズルということで、ここを何で作るかが一番の課題。

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ステージ)
Simulator Screenshot - iPhone 16 Pro - 2025-02-15 at 16.14.37.png

AppStoreリンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?