chapter.01에서 사용했던 상자와 먼지털이 이미지를 고양이 얼굴과 손가락 이미지로 대체했다. SpriteKit
관련 포트폴리오를 고양이 시리즈로 유지하고 싶어서다.
상자 위에 쌓인 먼지 털기 대신 고양이 얼굴에 묻은 먼지 털기가 되었다.
원래 만들었던 먼지 털기의 완성 화면은 이렇게 생겼다. 먼지털이를 터치 드래그하여 문지르면 먼지털이 노드에 닿은 먼지 노드가 사라지도록 구현했다. 이때는 먼지 이미지 종류가 여러개여서 먼지 별 크기 지정이 필요했고 먼지 생성 로직에 무작위 이미지 추출이 포함되었지만 고양이 얼굴에 묻은 먼지는 이미지 종류가 한가지라 그런 내용들이 다 삭제되었다.
random
값을 뽑았다.let xRange: ClosedRange<CGFloat> = 60...(size.width - 60) // 화면 양쪽 끝에서 각각 60 만큼 떨어진 범위
let yOffset = (size.height - catFaceImage.size.width) / 2 // 고양이 얼굴 y 좌표 계산
let yRange: ClosedRange<CGFloat> = (yOffset + 40)...(yOffset + catFaceImage.size.height - 40) // 투명 공백 고려한 y 범위
// x, y 범위에서 각각 무작위 좌표 추출
let x = CGFloat.random(in: xRange)
let y = CGFloat.random(in: yRange)
이름(name)
을 지정한 뒤 scene
에 추가 → 50개let node = SKSpriteNode(imageNamed: Assets.dust.rawValue)
node.size = CGSize(width: 40, height: 40)
node.zPosition = 2
node.position = CGPoint(x: x, y: y)
node.name = "dust"
addChild(node)
private func addDusts() {
for _ in 1...50 {
addDustImage()
}
}
위치 랜덤이다보니 이미지가 조금 겹치거나 얼굴 밖으로 삐져나오기도 했다
private func cleanDusts() {
// 손 이미지 노드에서 손가락 끝부분 point
let x = handImage.position.x - handImage.size.width / 2
let y = handImage.position.y + handImage.size.height / 2
let point = CGPoint(x: x, y: y)
// 손가락과 같은 위치에 있는 먼지 노드에 대해 action 시퀀스 적용
for node in nodes(at: point) where node.name == "dust" {
let moveAction = SKAction.move(to: point, duration: 0.01) // 닿자마자 사라지니까 좀 투박해서 짧게 비벼지는 느낌 추가
let fadeAction = SKAction.fadeAlpha(to: 0, duration: 0.7) // 먼지 노드 fade out
let removeAction = SKAction.removeFromParent() // scene에서 노드 제거
let sequenceAction = SKAction.sequence([moveAction, fadeAction, removeAction])
node.run(sequenceAction)
}
}
touchesMoved
에 적용override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
if ishandImageTouched {
let moveAction = SKAction.move(to: location, duration: 0.07)
handImage.run(moveAction)
// 먼지 제거 메소드 호출
cleanDusts()
}
}
손가락 끝에 닿은 먼지 노드가 fade out 되는 모습
restart
버튼이 생기도록 구현class GameScene: SKScene {
// ... //
// 재시작 버튼(이미지 노드)
private let restartImage = SKSpriteNode(imageNamed: Assets.restart.rawValue)
// ... //
// 재시작 버튼 크기, 위치 지정 메소드
private func setupRestartButton() {
restartImage.size = CGSize(width: 40, height: 40)
restartImage.position = CGPoint(x: 40, y: size.height - 135)
}
// 먼지 노드가 다 사라진 시점에 addChild
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
// ... //
if scene?.children.filter({ $0.name == "dust" }).count == 0 {
addChild(restartImage)
}
}
}
restart
버튼이 눌렸을 때 : 1. 손 이미지 노드 기본 위치로 이동 2. 고양이 얼굴 이미지 랜덤 리셋 3. 먼지 노드 50개 무작위로 다시 생성 4. restart
버튼 제거// 고양이 종류 랜덤 추출하여 노드 texture replace
private func resetCatFace() {
let imageName = Cat.allCases.randomElement()?.rawValue ?? Cat.cheese.rawValue
let texture = SKTexture(imageNamed: imageName)
catFaceImage.texture = texture
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
// ... //
if restartImage.contains(location) {
setHandPosition() // 1. 손 이미지 노드 기본 위치로 이동
resetCatFace() // 2. 고양이 얼굴 이미지 랜덤 리셋
addDusts() // 3. 먼지 노드 50개 무작위로 다시 생성
restartImage.removeFromParent() // 4. 재시작 버튼 제거
}
}
손으로 고양이 얼굴을 문지르는 동안 고양이가 그르릉 거리는 소리가 나오게 하고, 모든 먼지가 제거되면 야옹! 소리를 재생하려고 한다.
autoplayLooped
를 true
로 해준다. 최초로 손 이미지 노드 터치 시 addChild
를 해준 뒤 이후에는 음소거 on/off
만 하고 노드를 재사용한다.❗️트러블슈팅 : 처음에는 문지르기 시작하면 오디오 노드를
addChild
, 손을 떼면removeFromParent
를 반복하도록 했었는데, 매우 빠르게 연속적으로 탭 했을 때 타이밍 충돌로 인한 에러를 겪었다. 이를 방지하기 위해 볼륨을 조절하는 방식으로 바꾸었다.
class GameScene: SKScene {
// ... //
private let purringSound: SKAudioNode = {
let node = SKAudioNode(fileNamed: Assets.purringSound)
node.autoplayLooped = true
node.isPositional = true
return node
}()
// ... //
private func playPurringSound() {
if purringSound.parent == nil { // 최초 터치 시 addChild
addChild(purringSound)
}
// 오디오 노드의 볼륨을 1.0으로 올리는 SKAction 적용
let changeVolumeAction = SKAction.changeVolume(to: 1.0, duration: 0.1)
purringSound.run(changeVolumeAction)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// ... //
if ishandImageTouched {
playPurringSound()
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
ishandImageTouched = false
// 오디오 노드의 볼륨을 0.0으로 내리는 SKAction 적용
let fadeOutAction = SKAction.changeVolume(to: 0, duration: 0.3)
purringSound.run(fadeOutAction)
}
}
scene
에서 제거해준다.class GameScene: SKScene {
// 야옹 소리가 한번 재생되고 나서 게임을 재시작 하기 전까지 터치를 뗄 때마다 야옹 소리가 재생되는 것 방지
private var hasClearSoundPlayed: Bool = false
private func playClearSound() {
// 플래그 변수 설정
hasClearSoundPlayed = true
// 그르릉 소리 볼륨 0으로 줄이는 action
let fadeOutAction = SKAction.changeVolume(to: 0.0, duration: 0.5)
// action이 작동된 후에 야옹 소리 노드 생성 및 재생
purringSound.run(fadeOutAction) { [weak self] in
// 오디오 노드 생성 및 scene에 추가
let clearSound = SKAudioNode(fileNamed: Assets.meowSound)
clearSound.autoplayLooped = false
self?.addChild(clearSound)
// 액션 시퀀스
// 1. 음원 자체 볼륨이 커서 미리 줄여주기
// 2. 야옹 소리 재생
// 3. 재생 시간(2초) 만큼 대기
// 4. 오디오 노드 scene에서 제거
let setVolumeAction = SKAction.changeVolume(to: 0.3, duration: 0.0)
let playAction = SKAction.play()
// wait 2 seconds(play time) before removing node from parent
let waitAction = SKAction.wait(forDuration: 2.0)
let removeAction = SKAction.removeFromParent()
let sequence = SKAction.sequence([setVolumeAction, playAction, waitAction, removeAction])
clearSound.run(sequence)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
if ishandImageTouched {
let moveAction = SKAction.move(to: location, duration: 0.07)
handImage.run(moveAction)
cleanDusts()
// 먼지 노드가 0이 되고, 플래그 변수가 default 상태면 야옹 소리 재생 메소드 호출
if scene?.children.filter({ $0.name == "dust" }).count == 0 && !hasClearSoundPlayed {
playClearSound()
addChild(restartImage)
}
}
}
// 플래그 변수 원점 : 게임 재시작 버튼 눌렀을 때
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
// ... //
if restartImage.contains(location) {
hasClearSoundPlayed = false
// ... //
}
}
}
시연 영상에서 효과음과 함께 동작을 확인할 수 있다.
이 프로젝트를 통해서는 touchesMoved
와 노드의 position
을 활용하여 문지르는 대로 이미지 노드를 이동시키는 동작을 구현해볼 수 있었고, 터치에 따른 오디오 노드 재생을 구현하는 과정에서 트러블슈팅을 경험하며 경우에 따라 노드를 재사용하는 것이 적절한지 추가/제거가 적절한지 판단하는 기준을 배웠다.
반성합니다. 저도 제 스위프트 책 위에 있는 먼지 털러 갑니다