๐ŸŒ KO

๐Ÿ‘พ SpriteKit

โญ Difficulty: โญโญโญ โฑ๏ธ Est. Time: 2-3h ๐Ÿ“‚ Graphics & Media

2D ๊ฒŒ์ž„๊ณผ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋งŒ๋“ค๊ธฐ

iOS 7+Metal ์ตœ์ ํ™”

โœจ SpriteKit is?

SpriteKit is Apple's 2D game engine, providing sprite animation, physics simulation, particle effects and more. Metal-accelerated for high performance, with easy SwiftUI integration.

๐Ÿ’ก Key Features: Sprite Animation ยท 2D Physics Engine ยท Particle System ยท Tile Maps ยท Audio ยท Action System ยท Scene Transitions ยท Game Controller Support

๐ŸŽฏ 1. Basic Scene Setup

๊ฒŒ์ž„ ์”ฌ์„ ์ƒ์„ฑํ•˜๊ณ  ์Šคํ”„๋ผ์ดํŠธ is added.

GameScene.swift โ€” ๊ธฐ๋ณธ ์”ฌ
import SpriteKit

class GameScene: SKScene {
    var player: SKSpriteNode!

    override func didMove(to view: SKView) {
        // ์”ฌ ๋ฐฐ๊ฒฝ์ƒ‰
        backgroundColor = .init(white: 0.2, alpha: 1.0)

        // ํ”Œ๋ ˆ์ด์–ด ์ƒ์„ฑ
        setupPlayer()

        // ๋ฌผ๋ฆฌ ์›”๋“œ ์„ค์ •
        physicsWorld.gravity = CGVector(dx: 0, dy: -9.8)
        physicsWorld.contactDelegate = self
    }

    func setupPlayer() {
        // ์ƒ‰์ƒ ์Šคํ”„๋ผ์ดํŠธ
        player = SKSpriteNode(color: .blue, size: CGSize(width: 50, height: 50))
        player.position = CGPoint(x: frame.midX, y: frame.midY)

        // ๋˜๋Š” ์ด๋ฏธ์ง€ ์Šคํ”„๋ผ์ดํŠธ
        // player = SKSpriteNode(imageNamed: "player")

        // z-์œ„์น˜ (๋ ˆ์ด์–ด ์ˆœ์„œ)
        player.zPosition = 10

        addChild(player)
    }

    // ๋งค ํ”„๋ ˆ์ž„ ์—…๋ฐ์ดํŠธ
    override func update(_ currentTime: TimeInterval) {
        // ๊ฒŒ์ž„ ๋กœ์ง
    }
}

๐ŸŽฌ 2. ์•ก์…˜ (Actions)

์Šคํ”„๋ผ์ดํŠธ์— ์›€์ง์ž„๊ณผ ํšจ๊ณผ is added.

Actions.swift โ€” ์•ก์…˜ ์‹œ์Šคํ…œ
import SpriteKit

extension GameScene {
    // 1. ์ด๋™ ์•ก์…˜
    func movePlayerTo(_ position: CGPoint) {
        let moveAction = SKAction.move(to: position, duration: 1.0)
        player.run(moveAction)
    }

    // 2. ํšŒ์ „ ์•ก์…˜
    func rotatePlayer() {
        let rotateAction = SKAction.rotate(byAngle: .CGFloat.pi * 2, duration: 1.0)
        let repeatAction = SKAction.repeatForever(rotateAction)
        player.run(repeatAction)
    }

    // 3. ์Šค์ผ€์ผ ์•ก์…˜
    func pulseEffect(on node: SKNode) {
        let scaleUp = SKAction.scale(to: 1.2, duration: 0.5)
        let scaleDown = SKAction.scale(to: 1.0, duration: 0.5)
        let sequence = SKAction.sequence([scaleUp, scaleDown])
        let repeatAction = SKAction.repeatForever(sequence)
        node.run(repeatAction)
    }

    // 4. ํŽ˜์ด๋“œ ํšจ๊ณผ
    func fadeInOut(node: SKNode) {
        let fadeOut = SKAction.fadeOut(withDuration: 1.0)
        let fadeIn = SKAction.fadeIn(withDuration: 1.0)
        let sequence = SKAction.sequence([fadeOut, fadeIn])
        node.run(SKAction.repeatForever(sequence))
    }

    // 5. ๋ณตํ•ฉ ์•ก์…˜ (๊ทธ๋ฃน)
    func complexAnimation(node: SKNode) {
        let move = SKAction.moveBy(x: 100, y: 0, duration: 2.0)
        let rotate = SKAction.rotate(byAngle: .CGFloat.pi, duration: 2.0)
        let scale = SKAction.scale(to: 1.5, duration: 2.0)

        // ๋™์‹œ์— ์‹คํ–‰
        let group = SKAction.group([move, rotate, scale])
        node.run(group)
    }

    // 6. ์• ๋‹ˆ๋ฉ”์ด์…˜ ์Šคํ”„๋ผ์ดํŠธ
    func setupAnimatedSprite() {
        // ํ…์Šค์ฒ˜ ๋ฐฐ์—ด
        let textures = [
            SKTexture(imageNamed: "frame1"),
            SKTexture(imageNamed: "frame2"),
            SKTexture(imageNamed: "frame3")
        ]

        // ์• ๋‹ˆ๋ฉ”์ด์…˜ ์•ก์…˜
        let animation = SKAction.animate(with: textures, timePerFrame: 0.1)
        let repeatAnimation = SKAction.repeatForever(animation)

        let sprite = SKSpriteNode(texture: textures.first)
        sprite.run(repeatAnimation)
        addChild(sprite)
    }

    // 7. ์™„๋ฃŒ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ์žˆ๋Š” ์•ก์…˜
    func moveWithCompletion() {
        let move = SKAction.moveTo(y: 100, duration: 1.0)
        player.run(move) {
            print("์ด๋™ ์™„๋ฃŒ!")
        }
    }
}

โš™๏ธ 3. ๋ฌผ๋ฆฌ ์—”์ง„

์ถฉ๋Œ ๊ฐ์ง€์™€ ๋ฌผ๋ฆฌ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ implementation.

Physics.swift โ€” Physics Simulation
import SpriteKit

// ๋ฌผ๋ฆฌ ์นดํ…Œ๊ณ ๋ฆฌ
struct PhysicsCategory {
    static let none: UInt32 = 0
    static let player: UInt32 = 0b1      // 1
    static let enemy: UInt32 = 0b10       // 2
    static let wall: UInt32 = 0b100       // 4
}

extension GameScene {
    // ๋ฌผ๋ฆฌ ๋ฐ”๋”” ์„ค์ •
    func setupPhysics() {
        // ํ”Œ๋ ˆ์ด์–ด ๋ฌผ๋ฆฌ ๋ฐ”๋””
        player.physicsBody = SKPhysicsBody(rectangleOf: player.size)
        player.physicsBody?.isDynamic = true
        player.physicsBody?.categoryBitMask = PhysicsCategory.player
        player.physicsBody?.contactTestBitMask = PhysicsCategory.enemy
        player.physicsBody?.collisionBitMask = PhysicsCategory.wall

        // ์งˆ๋Ÿ‰๊ณผ ๋งˆ์ฐฐ
        player.physicsBody?.mass = 1.0
        player.physicsBody?.friction = 0.2
        player.physicsBody?.restitution = 0.5 // ํƒ„์„ฑ
    }

    // ๋ฒฝ ์ƒ์„ฑ
    func createWall(at position: CGPoint, size: CGSize) {
        let wall = SKSpriteNode(color: .gray, size: size)
        wall.position = position

        // ์ •์  ๋ฌผ๋ฆฌ ๋ฐ”๋”” (์›€์ง์ด์ง€ ์•Š์Œ)
        wall.physicsBody = SKPhysicsBody(rectangleOf: size)
        wall.physicsBody?.isDynamic = false
        wall.physicsBody?.categoryBitMask = PhysicsCategory.wall

        addChild(wall)
    }

    // ํž˜ ์ ์šฉ
    func jumpPlayer() {
        let impulse = CGVector(dx: 0, dy: 500)
        player.physicsBody?.applyImpulse(impulse)
    }

    // ์†๋„ ์„ค์ •
    func movePlayerRight() {
        player.physicsBody?.velocity = CGVector(dx: 200, dy: 0)
    }
}

// ์ถฉ๋Œ ๊ฐ์ง€ ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ
extension GameScene: SKPhysicsContactDelegate {
    func didBegin(_ contact: SKPhysicsContact) {
        let collision = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask

        if collision == PhysicsCategory.player | PhysicsCategory.enemy {
            print("ํ”Œ๋ ˆ์ด์–ด์™€ ์  ์ถฉ๋Œ!")
            handlePlayerEnemyCollision(contact)
        }
    }

    func handlePlayerEnemyCollision(_ contact: SKPhysicsContact) {
        // ์ถฉ๋Œ ์ฒ˜๋ฆฌ (๊ฒŒ์ž„ ์˜ค๋ฒ„, ์ ์ˆ˜ ๊ฐ์†Œ ๋“ฑ)
    }
}

โœจ 4. ํŒŒํ‹ฐํด ์‹œ์Šคํ…œ

๋ถˆ, ์—ฐ๊ธฐ, ํญ๋ฐœ ๋“ฑ์˜ ํŒŒํ‹ฐํด ํšจ๊ณผ๋ฅผ ๋งŒ๋“ญ.

Particles.swift โ€” ํŒŒํ‹ฐํด ํšจ๊ณผ
import SpriteKit

extension GameScene {
    // 1. ํŒŒํ‹ฐํด ์ด๋ฏธํ„ฐ ๋กœ๋“œ (.sks ํŒŒ์ผ)
    func addFireEffect(at position: CGPoint) {
        if let fire = SKEmitterNode(fileNamed: "Fire") {
            fire.position = position
            addChild(fire)
        }
    }

    // 2. ์ฝ”๋“œ๋กœ ํŒŒํ‹ฐํด ์ƒ์„ฑ
    func createExplosion(at position: CGPoint) {
        let emitter = SKEmitterNode()

        // ํŒŒํ‹ฐํด ํ…์Šค์ฒ˜
        emitter.particleTexture = SKTexture(imageNamed: "spark")

        // ์ƒ์„ฑ ์†๋„
        emitter.particleBirthRate = 100

        // ์ˆ˜๋ช…
        emitter.particleLifetime = 1.0

        // ์†๋„
        emitter.particleSpeed = 200
        emitter.particleSpeedRange = 50

        // ์ƒ‰์ƒ
        emitter.particleColor = .orange
        emitter.particleColorBlendFactor = 1.0

        // ํฌ๊ธฐ
        emitter.particleScale = 0.5
        emitter.particleScaleRange = 0.2
        emitter.particleScaleSpeed = -0.3

        // ์•ŒํŒŒ
        emitter.particleAlpha = 1.0
        emitter.particleAlphaSpeed = -1.0

        // ๊ฐ๋„
        emitter.emissionAngle = 0
        emitter.emissionAngleRange = .CGFloat.pi * 2

        emitter.position = position
        addChild(emitter)

        // 1์ดˆ ํ›„ ์ œ๊ฑฐ
        emitter.run(SKAction.sequence([
            .wait(forDuration: 1.0),
            .removeFromParent()
        ]))
    }

    // 3. ํŠธ๋ ˆ์ผ ํšจ๊ณผ
    func addTrailEffect(to node: SKNode) {
        if let trail = SKEmitterNode(fileNamed: "Trail") {
            trail.targetNode = self
            node.addChild(trail)
        }
    }
}

๐ŸŽฎ 5. ํ„ฐ์น˜ ๋ฐ ์ž…๋ ฅ ์ฒ˜๋ฆฌ

์‚ฌ์šฉ์ž ์ž…๋ ฅ handling.

Input.swift โ€” ์ž…๋ ฅ ์ฒ˜๋ฆฌ
import SpriteKit

extension GameScene {
    // ํ„ฐ์น˜ ์‹œ์ž‘
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }
        let location = touch.location(in: self)

        // ํ„ฐ์น˜ํ•œ ๋…ธ๋“œ ์ฐพ๊ธฐ
        let touchedNodes = nodes(at: location)

        for node in touchedNodes {
            if node.name == "button" {
                handleButtonTap()
            } else if node == player {
                jumpPlayer()
            }
        }

        // ๋˜๋Š” ํ”Œ๋ ˆ์ด์–ด๋ฅผ ํ„ฐ์น˜ ์œ„์น˜๋กœ ์ด๋™
        movePlayerTo(location)
    }

    // ํ„ฐ์น˜ ์ด๋™
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }
        let location = touch.location(in: self)
        player.position = location
    }

    // ํ„ฐ์น˜ ์ข…๋ฃŒ
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        // ํ„ฐ์น˜ ์ข…๋ฃŒ ์ฒ˜๋ฆฌ
    }

    func handleButtonTap() {
        print("๋ฒ„ํŠผ ํƒญ๋จ")
    }
}

๐Ÿ“ฑ SwiftUI Integration

GameView.swift โ€” SwiftUI Integration
import SwiftUI
import SpriteKit

struct GameView: View {
    @State private var score = 0
    @State private var isPaused = false

    var scene: SKScene {
        let scene = GameScene()
        scene.size = CGSize(width: 300, height: 400)
        scene.scaleMode = .aspectFill
        return scene
    }

    var body: some View {
        ZStack {
            // SpriteKit ๋ทฐ
            SpriteView(scene: scene)
                .ignoresSafeArea()

            // UI ์˜ค๋ฒ„๋ ˆ์ด
            VStack {
                // ์ ์ˆ˜
                Text("์ ์ˆ˜: \(score)")
                    .font(.title)
                    .foregroundStyle(.white)
                    .padding()

                Spacer()

                // ์ปจํŠธ๋กค
                HStack {
                    Button {
                        isPaused.toggle()
                        scene.isPaused = isPaused
                    } label: {
                        Image(systemName: isPaused ? "play.circle" : "pause.circle")
                            .font(.largeTitle)
                    }
                }
                .padding()
            }
        }
    }
}

๐Ÿ’ก HIG Guidelines

๐ŸŽฏ Practical Usage

๐Ÿ“š Learn More

โšก๏ธ Performance Tips: SKView.showsFPS์™€ showsNodeCount to monitor performance during development. Using texture atlases (.atlas) reduces draw calls for significant performance improvement.

๐Ÿ“Ž Apple Official Resources

๐Ÿ“˜ Documentation ๐ŸŽฌ WWDC Sessions