[SpriteKit] 먼지 털기 게임 chapter.02

Emily·2025년 8월 27일
1

DustSweepGame

목록 보기
2/2

chapter.01에서 사용했던 상자와 먼지털이 이미지를 고양이 얼굴과 손가락 이미지로 대체했다. SpriteKit 관련 포트폴리오를 고양이 시리즈로 유지하고 싶어서다.

상자 위에 쌓인 먼지 털기 대신 고양이 얼굴에 묻은 먼지 털기가 되었다.

원래 만들었던 먼지 털기의 완성 화면은 이렇게 생겼다. 먼지털이를 터치 드래그하여 문지르면 먼지털이 노드에 닿은 먼지 노드가 사라지도록 구현했다. 이때는 먼지 이미지 종류가 여러개여서 먼지 별 크기 지정이 필요했고 먼지 생성 로직에 무작위 이미지 추출이 포함되었지만 고양이 얼굴에 묻은 먼지는 이미지 종류가 한가지라 그런 내용들이 다 삭제되었다.

01) 먼지 노드 추가

  • 노드 위치 무작위 지정 : 최대한 고양이 얼굴 위에 먼지가 생기도록 고양이 얼굴 이미지 노드의 위치를 기준으로 x, y 범위를 잡고 그 중에서 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()
    }
}

위치 랜덤이다보니 이미지가 조금 겹치거나 얼굴 밖으로 삐져나오기도 했다

02) 먼지 제거 효과

  • 손가락 부분에 닿은 노드가 사라지는 효과 구현
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 되는 모습

03) 재시작 기능

  • 먼지가 모두 제거 되었을 때 : 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. 재시작 버튼 제거
    }
}

04) 효과음 추가

손으로 고양이 얼굴을 문지르는 동안 고양이가 그르릉 거리는 소리가 나오게 하고, 모든 먼지가 제거되면 야옹! 소리를 재생하려고 한다.

  • 그르릉 소리 : 문지르는 동안 계속 재생되어야 하기 때문에 autoplayLoopedtrue로 해준다. 최초로 손 이미지 노드 터치 시 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)
    }
}
  • 야옹 소리 : 먼지 노드의 개수가 0이 되는 시점에 그르릉 소리를 끈 뒤 한 번 재생한다. 그르릉 소리는 손을 댔다 뗐다 반복하는 동안 노드 추가/삭제를 반복하는 것이 오히려 에러를 유발할 수 있지만 야옹 소리는 짧게 한 번 쓰고 말기 때문에 재생이 끝나면 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을 활용하여 문지르는 대로 이미지 노드를 이동시키는 동작을 구현해볼 수 있었고, 터치에 따른 오디오 노드 재생을 구현하는 과정에서 트러블슈팅을 경험하며 경우에 따라 노드를 재사용하는 것이 적절한지 추가/제거가 적절한지 판단하는 기준을 배웠다.

profile
iOS Junior Developer

2개의 댓글

comment-user-thumbnail
5일 전

반성합니다. 저도 제 스위프트 책 위에 있는 먼지 털러 갑니다

1개의 답글