spritekit について
SpriteKit を使った 2D ゲームやインタラクティブなアニメーション開発を支援する Swift スキルです。シーンとノード、アニメーション、物理演算、入力、カメラ、パーティクル、SwiftUI 連携を解説します。
主な機能
- 01シーンのライフサイクルとノード階層
- 02スプライトのアクションとアニメーション
- 032D 物理演算と接触検出
- 04タッチ処理とヒットテスト
- 05カメラとパーティクル効果
- 06SwiftUI SpriteView との連携
活用シーン
- 012D ゲームシーンの作成
- 02衝突と接触イベントの実装
- 03アニメーション効果とカメラ移動の追加
- 04SwiftUI への SpriteKit ゲームの埋め込み
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill spritekitインストール後、Claude Code で/spritekitを入力して読み込まれたか確認します。
同梱ファイルevals/evals.jsonreferences/spritekit-patterns.md
詳細
SpriteKit
SpriteKit と Swift 6.3 を使用して、iOS 26 以降向けの 2D ゲームやインタラクティブなアニメーションを構築します。 シーンのライフサイクル、ノード階層、アクション、物理演算、パーティクル、 カメラ、タッチ処理、SwiftUI との統合について説明します。
目次
- シーンのセットアップ
- ノードとスプライト
- アクションとアニメーション
- 物理演算
- タッチ処理
- カメラ
- パーティクルエフェクト
- SwiftUI との統合
- よくある間違い
- レビュー用チェックリスト
- 参考資料
シーンのセットアップ
SpriteKit は SKView を通じてコンテンツをレンダリングします。SKView は SKScene を表示します。SKScene はツリーのルートノードであり、フレームワークが各フレームでアニメーションとレンダリングを行います。
シーンの作成
SKScene をサブクラス化し、ライフサイクルメソッドをオーバーライドします。デフォルトでは、座標系の原点は左下にあります。
import SpriteKit
final class GameScene: SKScene {
override func didMove(to view: SKView) {
backgroundColor = .darkGray
physicsWorld.contactDelegate = self
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
setupNodes()
}
override func update(_ currentTime: TimeInterval) {
// Called once per frame before actions are evaluated.
}
}
シーンの表示(UIKit)
guard let skView = view as? SKView else { return }
skView.ignoresSiblingOrder = true
let scene = GameScene(size: skView.bounds.size)
scene.scaleMode = .resizeFill
skView.presentScene(scene)
スケールモード
シーンをビューサイズの変更(回転やマルチタスキングなど)に合わせて調整する場合は、.resizeFill を使用します。デザインを固定したゲームシーンには .aspectFill を使用します。.aspectFit はレターボックス表示になり、.fill は引き伸ばされて歪む場合があります。
フレームサイクル
各フレームは次の順序で処理されます。
update(_:)-- ゲームロジック- アクションの評価
didEvaluateActions()-- アクション後の処理- 物理演算のシミュレーション
didSimulatePhysics()-- 物理演算後の調整- 制約の適用
didApplyConstraints()didFinishUpdate()-- レンダリング前の最終調整
処理が必要なコールバックのみをオーバーライドします。
ノードとスプライト
ビジュアルを持たない SKNode は、透明なコンテナやレイアウトグループとして使用します。
子ノードは、親の位置、スケール、回転、アルファ値、速度を継承します。
SKSpriteNode は主要なビジュアルノードです。
一般的なノードの種類
| クラス | 用途 |
|---|---|
SKSpriteNode | テクスチャ画像または単色 |
SKLabelNode | テキストのレンダリング |
SKShapeNode | ベクターパス(描画呼び出しごとの負荷が高い) |
SKEmitterNode | パーティクルエフェクト |
SKCameraNode | ビューポートの制御 |
SKTileMapNode | グリッドベースのタイル |
SKAudioNode | 位置オーディオ |
SKCropNode / SKEffectNode | マスキング / CIFilter |
SK3DNode | 埋め込み SceneKit コンテンツ |
スプライトの作成
let player = SKSpriteNode(imageNamed: "hero")
player.position = CGPoint(x: frame.midX, y: frame.midY)
player.name = "player"
addChild(player)
描画順序
パフォーマンスを向上させるには、SKView で ignoresSiblingOrder = true を設定します。SpriteKit はその後、zPosition を使って描画順序を決定します。設定しない場合、ノードはツリー順に描画されます。
background.zPosition = -1
player.zPosition = 0
foregroundUI.zPosition = 10
名前の設定と検索
インスタンス変数を使わずにノードを検索できるよう、name を設定します。childNode(withName:)、enumerateChildNodes(withName:using:)、または subscript を使用します。パターンでは、// はツリー全体を検索し、* は任意の文字列に一致し、.. は親を指します。
player.name = "player"
if let found = childNode(withName: "player") as? SKSpriteNode { /* ... */ }
アクションとアニメーション
SKAction オブジェクトは、時間の経過とともにノードに適用される変更を定義します。アクションは不変で、再利用できます。node.run(_:) で実行します。
基本的なアクション
let moveUp = SKAction.moveBy(x: 0, y: 100, duration: 0.5)
let grow = SKAction.scale(to: 1.5, duration: 0.3)
let spin = SKAction.rotate(byAngle: .pi * 2, duration: 1.0)
let fadeOut = SKAction.fadeOut(withDuration: 0.3)
let remove = SKAction.removeFromParent()
アクションの組み合わせ
// Sequential: run one after another
let dropAndRemove = SKAction.sequence([
SKAction.moveBy(x: 0, y: -500, duration: 1.0),
SKAction.removeFromParent()
])
// Parallel: run simultaneously
let scaleAndFade = SKAction.group([
SKAction.scale(to: 0.0, duration: 0.3),
SKAction.fadeOut(withDuration: 0.3)
])
// Repeat
let pulse = SKAction.repeatForever(
SKAction.sequence([
SKAction.scale(to: 1.2, duration: 0.5),
SKAction.scale(to: 1.0, duration: 0.5)
])
)
テクスチャアニメーション
let walkFrames = (1...8).map { SKTexture(imageNamed: "walk_\($0)") }
let walkAction = SKAction.animate(with: walkFrames, timePerFrame: 0.1)
player.run(SKAction.repeatForever(walkAction))
timingMode(.linear、.easeIn、.easeOut、.easeInEaseOut)で速度曲線を制御します。後からアクセスできるように、アクションにキーを割り当てます。
let easeIn = SKAction.moveTo(x: 300, duration: 1.0)
easeIn.timingMode = .easeInEaseOut
player.run(pulse, withKey: "pulse")
player.removeAction(forKey: "pulse") // stop later
物理演算
SpriteKit には、組み込みの 2D 物理エンジンがあります。シーンの physicsWorld が重力と衝突検出を管理します。
物理ボディの追加
// Circle body
player.physicsBody = SKPhysicsBody(circleOfRadius: player.size.width / 2)
player.physicsBody?.restitution = 0.3
// Static rectangle
ground.physicsBody = SKPhysicsBody(rectangleOf: ground.size)
ground.physicsBody?.isDynamic = false
// Texture-based body for irregular shapes
player.physicsBody = SKPhysicsBody(texture: player.texture!, size: player.size)
カテゴリマスクと接触マスク
ビットマスクを使って、衝突と接触コールバックを制御します。
struct PhysicsCategory {
static let player: UInt32 = 0b0001
static let enemy: UInt32 = 0b0010
static let ground: UInt32 = 0b0100
}
player.physicsBody?.categoryBitMask = PhysicsCategory.player
player.physicsBody?.contactTestBitMask = PhysicsCategory.enemy
player.physicsBody?.collisionBitMask = PhysicsCategory.ground
categoryBitMask はボディを識別します。collisionBitMask は物理的な応答(跳ね返り)を制御します。contactTestBitMask は didBegin / didEnd をトリガーします。
接触の検出
SKPhysicsContactDelegate を実装し、didMove(to:) で physicsWorld.contactDelegate = self を設定します。
extension GameScene: SKPhysicsContactDelegate {
func didBegin(_ contact: SKPhysicsContact) {
let mask = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
if mask == PhysicsCategory.player | PhysicsCategory.enemy {
queuePlayerHit()
}
}
}
接触コールバックは、物理演算のシミュレーション中に実行されます。queuePlayerHit() ではフラグを設定するかイベントを追加し、ノード、ボディ、ワールドの変更は update(_:) で適用します。
力とインパルス
player.physicsBody?.applyForce(CGVector(dx: 0, dy: 50)) // continuous
player.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 200)) // instant
player.physicsBody?.applyAngularImpulse(0.5) // spin
ジャンプや飛び道具の発射には .applyImpulse を使用します。physicsWorld.gravity = CGVector(dx: 0, dy: -9.8) で重力を設定し、各ボディの重力は affectedByGravity で設定します。
タッチ処理
SKScene は UIResponder を継承しています。シーンで touchesBegan、touchesMoved、touchesEnded をオーバーライドします。ヒットテストには nodes(at:) を使用します。
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
let tappedNodes = nodes(at: location)
if tappedNodes.contains(where: { $0.name == "playButton" }) {
startGame()
}
}
ノード単位でタッチを処理するには、ノードをサブクラス化して isUserInteractionEnabled = true に設定します。すると、そのノードがシーンではなく、タッチを直接受け取ります。
カメラ
SKCameraNode はシーンの表示範囲を制御します。カメラを子として追加し、scene.camera に割り当てます。
let cameraNode = SKCameraNode()
addChild(cameraNode)
camera = cameraNode
cameraNode.position = CGPoint(x: frame.midX, y: frame.midY)
キャラクターを追従する
didSimulatePhysics() でカメラの位置を更新するか、制約を使用します。
override func didSimulatePhysics() {
cameraNode.position = player.position
}
// Constrain camera to world bounds
let xRange = SKRange(lowerLimit: frame.midX, upperLimit: worldWidth - frame.midX)
let yRange = SKRange(lowerLimit: frame.midY, upperLimit: worldHeight - frame.midY)
cameraNode.constraints = [SKConstraint.positionX(xRange, y: yRange)]
カメラのズームと HUD
カメラノードを逆方向にスケーリングします。setScale(0.5) で 2 倍にズームインし、setScale(2.0) で 2 倍にズームアウトします。カメラの子として追加したノードは画面上の位置が固定されます(HUD 要素)。
let scoreLabel = SKLabelNode(text: "Score: 0")
scoreLabel.position = CGPoint(x: 0, y: frame.height / 2 - 40)
scoreLabel.fontName = "AvenirNext-Bold"
scoreLabel.fontSize = 24
cameraNode.addChild(scoreLabel)
パーティクルエフェクト
SKEmitterNode はパーティクルエフェクトを生成します。Xcode の SpriteKit Particle File エディタ(.sks)でエミッターをデザインするか、コードで設定します。
// Load from file
guard let emitter = SKEmitterNode(fileNamed: "Fire") else { return }
emitter.position = CGPoint(x: frame.midX, y: 100)
addChild(emitter)
単発エミッター
有限のエフェクトには numParticlesToEmit を設定し、完了後に削除します。
func spawnExplosion(at position: CGPoint) {
guard let explosion = SKEmitterNode(fileNamed: "Explosion") else { return }
explosion.position = position
explosion.numParticlesToEmit = 100
addChild(explosion)
let wait = SKAction.wait(forDuration: TimeInterval(explosion.particleLifetime))
explosion.run(SKAction.sequence([wait, .removeFromParent()]))
}
エミッターが移動してもパーティクルがワールド座標系にとどまるよう、targetNode にシーンを設定します。emitter.targetNode = self のように指定します。
SwiftUI との統合
SpriteView は SpriteKit のシーンを SwiftUI に埋め込みます。
import SwiftUI
import SpriteKit
struct GameView: View {
@State private var scene: GameScene = {
let s = GameScene()
s.size = CGSize(width: 390, height: 844)
s.scaleMode = .resizeFill
return s
}()
var body: some View {
SpriteView(scene: scene)
.ignoresSafeArea()
}
}
SpriteView のオプション
透明な背景には options: [.allowsTransparency]、画面外のノードをカリングするには .shouldCullNonVisibleNodes、zPosition に基づく描画順序には .ignoresSiblingOrder を渡します。開発中は debugOptions: [.showsFPS, .showsNodeCount] を使用します。
SwiftUI とシーン間の通信
共有の @Observable オブジェクトを介してデータを渡します。ビューの再描画時にシーンが再作成されないよう、シーンを @State に保存します。
@Observable final class GameState {
var score = 0
var isPaused = false
}
struct GameContainerView: View {
@State private var gameState = GameState()
@State private var scene = GameScene()
var body: some View {
SpriteView(scene: scene, isPaused: gameState.isPaused)
.onAppear { scene.gameState = gameState }
}
}
よくある間違い
SwiftUI の再描画のたびに新しいシーンを作成する
// DON'T: Scene is recreated on every body evaluation
var body: some View {
SpriteView(scene: GameScene(size: CGSize(width: 390, height: 844)))
}
// DO: Create once and reuse
@State private var scene = GameScene(size: CGSize(width: 390, height: 844))
var body: some View {
SpriteView(scene: scene)
}
すでに親を持つ子ノードを追加する
ノードが持てる親は 1 つだけです。まず現在の親から削除するか、別のインスタンスを作成します。すでに親を持つノードを追加するとクラッシュします。
contactTestBitMask を設定し忘れる
// DON'T: Bodies collide but didBegin is never called
player.physicsBody?.categoryBitMask = PhysicsCategory.player
enemy.physicsBody?.categoryBitMask = PhysicsCategory.enemy
// DO: Set contactTestBitMask to receive contact callbacks
player.physicsBody?.contactTestBitMask = PhysicsCategory.enemy
パフォーマンスが重要な描画に SKShapeNode を使用する
SKShapeNode はインスタンスごとに個別の描画呼び出しを行います。繰り返し使う要素にはテクスチャを使った SKSpriteNode を優先し、描画をバッチ化できるようにします。
画面外に出たノードを削除しない
// DON'T
enemy.run(SKAction.moveBy(x: -800, y: 0, duration: 3.0))
addChild(enemy)
// DO: Remove after leaving the visible area
enemy.run(SKAction.sequence([
SKAction.moveBy(x: -800, y: 0, duration: 3.0),
SKAction.removeFromParent()
]))
addChild(enemy)
physicsWorld.contactDelegate の設定が遅すぎる
physicsWorld.contactDelegate = self は didMove(to:) で設定します。update(_:) 内や遅延後に設定しないでください。
レビューチェックリスト
- シーンサブクラスでは、
initではなくセットアップ用にdidMove(to:)をオーバーライドしている - ゲームのデザインに適した
scaleModeを選択している - パフォーマンス向上のため、
SKViewでignoresSiblingOrderをtrueに設定している -
ignoresSiblingOrderが有効な場合、zPositionを一貫して使用している -
didMove(to:)で物理のcontactDelegateを設定している - カテゴリ、衝突、接触のビットマスクを正しく設定している
-
didBegin/didEndコールバックが必要なペアにcontactTestBitMaskを設定している - 接触コールバックでは、物理ワールドを直接変更せず、変更をキューに追加している
- 静的ボディに
isDynamic = falseを設定している - パフォーマンスが重要な箇所では
SKShapeNodeを避け、SKSpriteNodeを優先している - ノードを画面外に移動するアクションのシーケンスに
.removeFromParent()を含めている - 単発エミッターはパーティクルの寿命が切れた後に自身を削除している
- パーティクルをワールド座標系にとどめる場合、エミッターの
targetNodeを設定している - SwiftUI で
SpriteViewと併用する際、シーンを@Stateに保存している - 関連するスプライトにテクスチャアトラスを使用して、描画呼び出しを減らしている
- フレームレートに依存しない移動のため、
update(_:)でデルタタイムを使用している - 別の場所に再追加する前に、ノードを親から削除している
参考資料
- タイルマップ、テクスチャアトラス、シェーダー、シーン遷移、ゲームループのパターン、オーディオ、SceneKit の埋め込みについては、references/spritekit-patterns.md を参照してください。
- SpriteKit のドキュメント
- SKScene
- SKSpriteNode
- SKAction
- SKPhysicsBody
- SKEmitterNode
- SKCameraNode
- SpriteView
- SKTileMapNode