๐พ 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
- ํ๋ ์ ๋ ์ดํธ: 60 FPS ์ ์ง
- ํ ์ค์ฒ ์ํ๋ผ์ค: ํ ์ค์ฒ๋ฅผ ํ๋๋ก ํฉ์ณ ์ฑ๋ฅ ํฅ์
- ๋ ธ๋ ์ฌ์ฌ์ฉ: ์์ฃผ ์์ฑ/์ญ์ ๋๋ ๋ ธ๋๋ ํ ์ฌ์ฉ
- z-Position: ๋ ์ด์ด ์์ ๋ช ํํ ๊ด๋ฆฌ
- ๋ฌผ๋ฆฌ ์ต์ ํ: ๋ถํ์ํ ๋ฌผ๋ฆฌ ๋ฐ๋ ๋นํ์ฑํ
๐ฏ Practical Usage
- ์บ์ฃผ์ผ Games: ํผ์ฆ, ํ๋ซํฌ๋จธ, ์ํ ๊ฒ์
- Education Apps: ์ธํฐ๋ํฐ๋ธ ํ์ต ์ฝํ ์ธ
- ๋ฐ์ดํฐ ์๊ฐํ: ์ ๋๋ฉ์ด์ ์ฐจํธ์ ๊ทธ๋ํ
- ๋ฏธ๋ Games: ์ฑ ๋ด ๋ณด๋์ค ๊ฒ์
๐ Learn More
โก๏ธ Performance Tips:
SKView.showsFPS์ showsNodeCount to monitor performance during development. Using texture atlases (.atlas) reduces draw calls for significant performance improvement.