내가 고양이 퍼즐 게임을 만들며 SpriteKit
을 공부하게 된 이유가 인턴으로 근무하게 된 기업에서 이 프레임워크를 활용한 작업을 요구했기 때문인데, 그 작업 중 하나인 먼지 털기 화면을 구현하는 과정을 기록으로 남겨보려 한다. (참고로 인턴 근무 동안의 작업물을 포트폴리오에 활용할 수 있다는 내용이 계약서에 적혀있다)
우선 화면의 디자인 설계나 리소스 없이 "먼지 쌓인 상자를 문질러서 닦는 화면"이라는 텍스트 한줄의 요구사항 만으로 작업에 착수해야 했기 때문에 (그렇다고 내가 그림을 그릴 줄 알거나 디자인 툴을 다룰 줄 아는 것도 아니어서) 구글링 해서 구할 수 있는 무료 이미지 리소스들로 화면을 구성하느라 디자인이 투박한 점을 언급하고 시작하겠다.
지난 프로젝트(고양이 퍼즐 게임)에서는 단일 탭 동작에 대한 반응만 구현했다면, 이번에는 문지르는 터치(드래그)에 따른 반응을 구현하는 것이 주요사항이다.
나무바닥 배경, 종이상자, 먼지털이 이미지를 구해다가 SKSpriteNode
로 scene
에 추가하였다. 개념과 방법에 대한 내용은 고양이 퍼즐 게임 시리즈에서 이미 다뤘으므로 간단하게 짚고 넘어가겠다.
class GameScene: SKScene {
// 노드 생성
private let backgroundImage = SKSpriteNode(imageNamed: Assets.woodenfloor.rawValue)
private let boxImage = SKSpriteNode(imageNamed: Assets.box.rawValue)
private let dusterImage = SKSpriteNode(imageNamed: Assets.duster.rawValue)
override func didMove(to view: SKView) {
self.size = view.bounds.size
addBackgroundImage()
addBoxImage()
addDusterImage()
}
// 배경 이미지 : 전체 화면을 덮고, 모든 노드들보다 아래에 있도록 지정
private func addBackgroundImage() {
backgroundImage.size = size
backgroundImage.anchorPoint = .zero
backgroundImage.zPosition = -1
addChild(backgroundImage)
}
// 상자 이미지 : 정사각형 모양으로, leading과 trailing에서 각각 25 만큼 떨어지도록 지정
private func addBoxImage() {
let width = size.width - 50
boxImage.size = CGSize(width: width, height: width)
boxImage.position = CGPoint(x: size.width / 2, y: size.height / 2)
boxImage.zPosition = 1
addChild(boxImage)
}
// 먼지털이 이미지 : 상자 너비의 1/4 너비를 갖고, 화면의 우측 상단에 위치하도록 지정.
private func addDusterImage() {
let width = boxImage.size.width / 4
let height = width * 1.3
dusterImage.size = CGSize(width: width, height: height)
dusterImage.anchorPoint = CGPoint(x: 0.5, y: 0.2)
dusterImage.position = CGPoint(x: size.width - (width / 2) - 25, y: size.height - (height / 2) - 90)
dusterImage.zPosition = 3
addChild(dusterImage)
}
}
zPosition
은 위에서 내려다 봤을 때 기준으로 위에 있느냐, 밑에 있느냐를 지정하는 건데 당연히 배경을 제일 아래 두었고, 그 다음이 상자다. 먼지털이 노드의 경우 나중에 추가될 먼지 노드들보다 위에 있어야 하므로 상자의 z 값보다 2를 더 높게 지정했다.
또한 먼지털이의 anchorPoint
를 주목해야 하는데, x 좌표는 기본값 그대로 가운데 위치하되 y 좌표는 이미지의 하단 20% 지점에 지정했다. 이유는 사용자가 화면을 문지를 때 먼지털이로 상자를 문지르는 듯한 느낌을 주기 위해서는 손가락이 닿는 지점을 따라서 움직이는 부분이 먼지털이 이미지의 깃털 부분이어야 자연스러울 거 같기 때문이다.
이미지 노드가 모두 배치된 모습
이미지 출처
나무바닥 - https://images.app.goo.gl/4rsXeHtxPJK3ijQS7
택배상자 - https://www.vecteezy.com/vector-art/6771731-top-view-closed-cardboard-box-with-shipment-label-isolated-on-white-background-vector-illustration
먼지털이 - https://www.pngegg.com/en/png-eroop
고양이 퍼즐 게임에서는 탭 제스처(터치)에 따른 동작을 위해 touchesBegan
메소드를 활용했다면, 이번에는 touchesMoved
메소드를 쓸 것이다. 사용자가 화면을 터치한 채로 손가락을 움직일 때마다 호출된다.
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
print(location)
}
터치가 된 지점을 출력해보겠다.
이렇게 계속 전달되는 터치 위치값을 활용하여 먼지털이를 사용자의 손가락 위치에 따라 움직이도록 해보겠다.
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
dusterImage.position = location
}
하지만 이렇게 간단할 리가 없다. 여기서 문제점은 사용자가 멀쩡하게 먼지털이를 터치했을 때만 손가락을 움직여주면 좋겠지만, 그렇지 않은 경우도 있을 거라는 점이다. 먼지털이가 없는 지점에서 터치를 시작하여 드래그하는 순간 먼지털이 위치가 갑자기 바뀌기 때문에 부자연스러워보인다.
이 문제를 해결하기 위해서는 사용자의 터치 시작지점이 먼지털이 부분이여야만 노드가 드래그를 따라 이동하는 것으로 조건을 걸어야 한다.
private var isDusterTouched: Bool = false
플래그 변수를 선언하여 조건에 맞을 경우 true
를 할당한 뒤 실행되도록 할 것이다.
// 손가락이 화면에 닿는 순간 호출되는 메소드
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
// xRange : 먼지털이 이미지 노드의 leading ~ trailing 거리
// yRange : 먼지털이 이미지 노드의 하단 부분(깃털 부분)을 대략적으로 계산하여 지정
let xRange: ClosedRange<CGFloat> = dusterImage.position.x - dusterImage.size.width / 2 ... dusterImage.position.x + dusterImage.size.width / 2
let yRange: ClosedRange<CGFloat> = dusterImage.position.y - dusterImage.size.height * 0.2 ... dusterImage.position.y + dusterImage.size.height * 0.3
// 터치 지점이 range에 속할 경우 isDusterTouched에 true 할당
isDusterTouched = xRange.contains(location.x) && yRange.contains(location.y)
}
// 손가락을 화면에 드래그하는 동안 호출되는 메소드
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
// 플래그 변수가 true일 때만 먼지털이 노드를 움직여준다
if isDusterTouched {
dusterImage.position = location
}
}
// 손가락이 화면에서 떨어지는 순간 호출되는 메소드
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
// 플래스 변수를 원점으로 돌려준다
isDusterTouched = false
}
먼지털이를 만져야만 따라 움직이는 모습