Sprite kit을 이용해 단순한 게임을 만들어 보자
일단 기본적으로 알아야 하는 사항은 다음과 같다.
뷰 계층 구조
- ViewController
- 가장 상위 레벨의 컨트롤러
- 화면에 표시되는 SKView를 포함한다.
- SKView
- SpriteKit의 전용 뷰 객체
- SkScene을 표시
- SKScene
- SKView에 표시되는 장면 객체
- 게임 로직과 게임 오브젝트를 포함한다.
- SKSpriteNode
- SKScene에 포함된 개별 오브젝트 노드
- 이미지, 물리 엔진, 애니메이션을 적용하는 게 가능하다.
SpriteNode
https://developer.apple.com/documentation/spritekit/skspritenode
- 이미지나 단색으로 초기화가 가능한 화면상의 그래픽 요소다.
- 위치를 잡아주고 scene에 addChild를 통해 scene위에 보여주는 것이 가능하다.
- view의 addSubView와 비슷하다고 생각하면 된다.
- removeFromParent를 통해 제거가 가능하다.
SKAction
SKAction | Apple Developer Documentation
- Scene의 노드에서 실행되는 애니메이션
- 액션을 통해서 노드를 처리하거나 변경한다고 생각하면 된다.
DIdMove(to view: SkView)
didMove(to:) | Apple Developer Documentation
- scene이 처음 로드 되거나 다른 장면으로 전환될 때 호출되는 메서드
- 내부 구현을 통해 scene의 콘텐츠를 만들 수 있다
SKPhysicsContactDelegate
SKPhysicsContactDelegate | Apple Developer Documentation
- 물체끼리 접촉을 할때 반응을 할 수 있도록 한다.
간단한 게임 만들어 보기
SpriteKit Tutorial for Beginners
- 코데코의 튜토리얼을 통해 진행한다.
일단 첫 번째로 GameScene이라는 SKScene파일을 만들고 ViewController 에서 해당 GameScene을 불러온다.
import UIKit
import SpriteKit
final class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let scene = GameScene(size: view.bounds.size)
let skView = view as! SKView
skView.showsFPS = true
skView.showsNodeCount = true
skView.ignoresSiblingOrder = true
scene.scaleMode = .resizeFill
skView.presentScene(scene)
}
}
- showFPS는 현재 현재 프레임을 보여준다
- showNodeCount는 SpriteNode의 숫자
- ignoresSiblingOrder
- https://developer.apple.com/documentation/spritekit/skview/1520215-ignoressiblingorder
- 기본적으로 Sprite Kit은 노드의 추가 순서에 따라 렌더링 순서가 결정된다.
- 나중에 추가된 노드가 전에 추가된 노드를 가리게 된다.
- 하지만 ignoresSiblingOrder를 true로 변경하면 노드의 추가 순서와 상관없이 zPostion의 값에 따라 렌더링 순서가 결정
- 순서가 무시되면 SpriteKit은 렌더링 성능을 향상시키기 위해 추가 최적화를 적용한다.
GameScene부분
final class GameScene: SKScene {
let player = SKSpriteNode(imageNamed: "player")
var monstersDestroyed = 0
override func didMove(to view: SKView) {
super.didMove(to: view)
// BGM
let backgroundMusic = SKAudioNode(fileNamed: "background-music-aac.caf")
backgroundMusic.autoplayLooped = true
addChild(backgroundMusic)
// 중력은 없지만 물체끼리 접촉에 대한 반응은 한다
physicsWorld.gravity = .zero
physicsWorld.contactDelegate = self
backgroundColor = SKColor.white
player.position = CGPoint(x: size.width * 0.1,
y: size.height * 0.5)
addChild(player)
run(SKAction.repeatForever(
SKAction.sequence([
SKAction.run(addMonster),
SKAction.wait(forDuration: 1.0)
])
))
}
}
- scene이 전환되면서 addMonster라는 Action을 1초 간격으로 계속 실행을 하게 된다.
- addMonster를 살펴보자
extension GameScene {
func random() -> CGFloat {
return CGFloat.random(in: 0...4294967296) / 4294967296
}
func random(min: CGFloat, max: CGFloat) -> CGFloat {
return random() * (max - min) + min
}
func addMonster() {
// spriteNode 생성
let monster = SKSpriteNode(imageNamed: "monster")
// 어디에 생성 될 것인지 y포지션을 결정
let actualY = random(min: monster.size.height/2, max: size.height - monster.size.height/2)
// 화면 밖에 몬스터 생성을 해준다.
monster.position = CGPoint(x: size.width + monster.size.width/2, y: actualY)
// 몬스터의 물리값 부여
// 1.히트 박스 부여
monster.physicsBody = SKPhysicsBody(rectangleOf: monster.size)
//2. 물리엔진이 제어를 하는것이 아닌 액션으로 제어가 된다.
monster.physicsBody?.isDynamic = true
//3. 몬스터의 카테고리
monster.physicsBody?.categoryBitMask = PhysicsCategory.monster
//4. 어떤 카테고리랑 만났을때 반응을 하는 것인가
monster.physicsBody?.contactTestBitMask = PhysicsCategory.projectile
//5. 충돌 비트 마스크로 물리 엔진이 접촉 처리하는 것
// 벽에 닿으면 팅기는 것처럼
monster.physicsBody?.collisionBitMask = PhysicsCategory.none
// scene에 추가하는 작업
addChild(monster)
// 몬스터의 속도 계산
let actualDuration = random(min: CGFloat(2.0), max: CGFloat(4.0))
// 몬스터의 움직임
let actionMove = SKAction.move(to: CGPoint(x: -monster.size.width/2,
y: actualY),
duration: TimeInterval(actualDuration))
let actionMoveDone = SKAction.removeFromParent()
let loseAction = SKAction.run() { [weak self] in
guard let self = self else { return }
let reveal = SKTransition.flipHorizontal(withDuration: 0.5)
let gameOverScene = GameOverScene(size: self.size, won: false)
self.view?.presentScene(gameOverScene, transition: reveal)
}
monster.run(SKAction.sequence([actionMove, loseAction, actionMoveDone]))
}
}
- 여기서 주의 깊게 볼것은 몬스터하는 Node를 추가한 뒤 필요 없어지는 경우 제거를 해주는 것이다.
- 터치이벤트와 관련된 로직
extension GameScene {
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
// 1. 터치 이벤트를 받아오고 터치의 위치를 추적한다.
guard let touch = touches.first else {
return
}
let touchLocation = touch.location(in: self)
//수리검 날릴때 사운드
run(SKAction.playSoundFileNamed("pew-pew-lei.caf", waitForCompletion: false))
// 2. 수리검을 플레이어의 위치에 생성을 해준다.
let projectile = SKSpriteNode(imageNamed: "projectile")
projectile.position = player.position
// 카테고리 비트 마스크를 이용해서 물체에 대한 인식을 해준다.
projectile.physicsBody = SKPhysicsBody(circleOfRadius: projectile.size.width/2)
projectile.physicsBody?.isDynamic = true
projectile.physicsBody?.categoryBitMask = PhysicsCategory.projectile
projectile.physicsBody?.contactTestBitMask = PhysicsCategory.monster
projectile.physicsBody?.collisionBitMask = PhysicsCategory.none
// 정밀하게 물체 탐지
projectile.physicsBody?.usesPreciseCollisionDetection = true
//3. 터치 지점과 수리검의 위치 차이를 계산
let offset = touchLocation - projectile.position
// 4. 뒤로 터치를 했다면 바로 return
if offset.x < 0 { return }
// 5. 수리검을 추가
addChild(projectile)
//6. 방향을 알려주는 벡터 , 단위 벡터
let direction = offset.normalized()
// 7.화면 밖까지 나가기 위해서
// direction이 단위 벡터이기 때문에 1000을 곱한다.
let shootAmount = direction * 1000
// 8. 수리검의 위치에서 목적지 까지 더하게 된다.
// 좌표 시스템이 좌상단이 0,0 으로 그려지기 때문에
let realDest = shootAmount + projectile.position
//9. 액션 생성 2초동안 해당 위치로 이동하고 제거를 하는것
let actionMove = SKAction.move(to: realDest, duration: 2.0)
let actionMoveDone = SKAction.removeFromParent()
projectile.run(SKAction.sequence([actionMove, actionMoveDone]))
}
func projectileDidCollideWithMonster(projectile: SKSpriteNode, monster: SKSpriteNode) {
monstersDestroyed += 1
// 이기면 이겼다는 Secene을 띄우는 것
if monstersDestroyed > 30 {
let reveal = SKTransition.flipHorizontal(withDuration: 0.5)
let gameOverScene = GameOverScene(size: self.size, won: true)
view?.presentScene(gameOverScene, transition: reveal)
}
projectile.removeFromParent()
monster.removeFromParent()
}
}
- 물리 반응과 관련된 부분
extension GameScene: SKPhysicsContactDelegate {
func didBegin(_ contact: SKPhysicsContact) {
// 1. 충돌한 물체가 어떤 것인지 판단을 하게 하는 것
var firstBody: SKPhysicsBody
var secondBody: SKPhysicsBody
if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
firstBody = contact.bodyA
secondBody = contact.bodyB
} else {
firstBody = contact.bodyB
secondBody = contact.bodyA
}
// 2. 물체가 서로 몬스터와 수리검이 맞으면 제거하는 것
if ((firstBody.categoryBitMask & PhysicsCategory.monster != 0) &&
(secondBody.categoryBitMask & PhysicsCategory.projectile != 0)) {
if let monster = firstBody.node as? SKSpriteNode,
let projectile = secondBody.node as? SKSpriteNode {
projectileDidCollideWithMonster(projectile: projectile, monster: monster)
}
}
}
}
- CGPoint 계산을 위해 연산자 오버로딩을 사용
extension CGPoint {
func length() -> CGFloat {
return sqrt(x*x + y*y)
}
// 단위 벡터로 만들기 위한 방법
func normalized() -> CGPoint {
return self / length()
}
static func +(left: CGPoint, right: CGPoint) -> CGPoint {
return CGPoint(x: left.x + right.x, y: left.y + right.y)
}
static func -(left: CGPoint, right: CGPoint) -> CGPoint {
return CGPoint(x: left.x - right.x, y: left.y - right.y)
}
static func *(point: CGPoint, scalar: CGFloat) -> CGPoint {
return CGPoint(x: point.x * scalar, y: point.y * scalar)
}
static func /(point: CGPoint, scalar: CGFloat) -> CGPoint {
return CGPoint(x: point.x / scalar, y: point.y / scalar)
}
}
- 물체의 종류 카테고리라고 생각하면 된다
struct PhysicsCategory {
static let none : UInt32 = 0
static let all : UInt32 = UInt32.max
static let monster : UInt32 = 0b1 // 1
static let projectile: UInt32 = 0b10 // 2
}
- 게임오버 장면
import SpriteKit
final class GameOverScene: SKScene {
init(size: CGSize, won:Bool) {
super.init(size: size)
backgroundColor = SKColor.white
// 2
let message = won ? "You Won!" : "You Lose :["
// 3
let label = SKLabelNode(fontNamed: "Chalkduster")
label.text = message
label.fontSize = 40
label.fontColor = SKColor.black
label.position = CGPoint(x: size.width/2, y: size.height/2)
addChild(label)
// 4
run(SKAction.sequence([
SKAction.wait(forDuration: 3.0),
SKAction.run() { [weak self] in
// 5
guard let self = self else { return }
let reveal = SKTransition.flipHorizontal(withDuration: 0.5)
let scene = GameScene(size: size)
self.view?.presentScene(scene, transition:reveal)
}
]))
}
// 6
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
'SpriteKit' 카테고리의 다른 글
SpriteKit을 이용한 수박게임 만들기 (0) | 2024.06.30 |
---|---|
SpriteKit을 이용한 "벽돌 깨기" 만들기 (0) | 2024.06.23 |