본문 바로가기
IT/Graphics

Metal 스터디 8주차 (23.02.02)

by YEON-DU 2023. 4. 16.
반응형

Interaction

게임의 실패 시나리오는 Player가 Paddle에서 공을 놓치고 공이 보드 뒤쪽이나 Paddle 뒤에서 튀는 경우이다. 또한 Player에게 3번의 기회를 준다.

먼저 lives 속성을 설정한다.

var lives = 3

bounceBall에서는 공이 테두리의 상단과 하단에 대해 튕기는지 확인한다.

 

func bounceBall() {
      ...
      if abs(ball.position.z) > gameArea.height / 2 {
          ballVelocity.z = -ballVelocity.z
          lives -= 1

          if lives < 0 {
              print("GAME OVER - YOU LOST")
          } else {
              print("Lives: ", lives)
          }
      }
            ...
}

볼 위치 Z를 확인한다. 볼의 위치가 실패 위치에 있으면 남은 생명 수를 줄인다.

생명이 0보다 적으면 게임에서 진 것이다.

 

// Ray Break에서 왼쪽 및 오른쪽 화살표 키에 대해
// 두 개의 속성을 생성하여 나중에 눌렸는지 여부를
// 확인할 수 있다.
var keyLeftDown = false
var keyRightDown = false

// Override Key Down and set these properties to true.
override func keyDown(key: Int, isARepeat: Bool) -> Bool {
    switch key {
        //the value for the left arrow
    case 123:
        keyLeftDown = true
        // the value for the right arrow
    case 124:
        keyRightDown = true
    default:
        return false
    }
    return true
}

// Do the same for Key Up.
override func keyUp(key: Int) -> Bool {
    switch key {
    case 123:
        keyLeftDown = false
    case 124:
        keyRightDown = false
    default:
        break
    }
    return true
}

// 매 프레임마다 화살표 키가 눌렸는지 확인하고
// 눌리면 Paddle 위치를 업데이트한다.
...

// In Update Scene, move the paddle left or right based on the pressed keyboard.
override func updateScene(deltaTime: Float) {
    ball.position += ballVelocity * deltaTime

    checkBricks()

    bounceBall()

        // We just have to constrain the paddle to stay within the boarder.
        // Before updating the paddle position, 
        // store the oldPosition just in case the new position is over the edge.
    let oldPaddlePosition = paddle.position.x

    if keyLeftDown {
        paddle.position.x -= Constants.paddleSpeed
    }
    if keyRightDown {
        paddle.position.x += Constants.paddleSpeed
    }

        // After the paddle position update, 
        // we check that the bounding box of the paddle isn't over the boarder.
    let paddleWidth = paddle.worldBoundingBox().width
    let halfBorderWidth = border.worldBoundingBox().width / 2

        // If the paddle is outside the boarder,
        // then we restore the paddles previous position.
    if abs(paddle.position.x) + (paddleWidth / 2) > halfBorderWidth {
        paddle.position.x = oldPaddlePosition
    }

        // Here we update each of the instances transform, so that the brick rotates.
    let transforms: [Transform] = bricks.transforms.map {
        var transform = $0
        transform.rotation.y += Float.pi / 4 * deltaTime
        return transform
    }
    bricks.transforms = transforms
}

 

Juice it up

 

게임 이벤트에 대한 피드백을 플레이어에게 제공하는 음향 효과가 있는 게임은 조용한 게임보다 더 재미있다.

또한 배경 음악은 경험과 즐거움 더한다.

App에 SoundController를 추가해본다. RayBreak뿐만 아니라 개발자가 작성하는 다른 게임에서도 이 SoundController를 사용할 수 있다.

 

//
//  SoundController.swift
//  MetalRenderer
//
//  Created by leesy on 2023/02/13.
//

import Foundation
import AVFoundation

// These are all held in the sounds group.

class SoundController {
    var backgroundMusicPlayer: AVAudioPlayer?
    var sounds: [String: AVAudioPlayer] = [:]

        // We'll create a class method to preload a sound effect.
    static func preloadSoundEffect(_ filename: String) -> AVAudioPlayer? {
                // First, check that the URL of the file exists, 
                // and then preload the sound effect into its own player.
        guard let url = Bundle.main.url(forResource: filename,
                                        withExtension: nil) else {
            return nil
        }
        do {
            let player = try AVAudioPlayer(contentsOf: url)
            player.prepareToPlay()
            return player
        } catch {
            print(error.localizedDescription)
            return nil
        }
    }

        // We'll create a new method to load an array of sound effects.
    func load(soundNames: [String]) {
                // Let's load up all the sound effects.
        for name in soundNames {
            let sound = SoundController.preloadSoundEffect(name)
            sounds[name] = sound
        }
    }


        // We need now is a method to play the sound effect.
    func playEffect(name: String) {
        sounds[name]?.play()
    }

        // We only have one file for the background music,
        // so that's a bit less complicated.
        // We'll call a method at the start of the scene,
        // and the file will load and immediately play.
    func playBackgroundMusic(_ filename: String) {
        backgroundMusicPlayer = SoundController.preloadSoundEffect(filename)
        backgroundMusicPlayer?.numberOfLoops = -1
        backgroundMusicPlayer?.play()
    }


        // We also need a stop method, and that's the sound controller complete.
    func stopBackgroundMusic() {
        backgroundMusicPlayer?.stop()
    }
}

 

Game Over

우리 게임은 Player에게 승패를 알려주는 GameOver Scene이 필요하다.

또한 Player가 게임을 다시 시작할 수 있도록 만든다.

Protocol을 사용하여 장면 간 전환 기능을 추가한다. Base Scene에 Delegate 속성을 추가하고 View Control을 이 Delegate로 설정하고 ViewController의 Transition 메서드에서 장면 전환을 제어한다.

 

Scene.swift

// First open Scene.swift and create a new protocol.

// We inherit the protocol from any object because we'll make a delegate property
// in Scene and make it weak so it doesn't retain it's delegate.
protocol SceneDelegate: AnyObject {
        // The protocol will hold a transition method 
    func transition(to scene: Scene)
}

class Scene {
        ...
        // and Scene needs to have a delegate property.
        // Delegate properties should generally be declared as weak, 
        // to prevent retain cycles.
    weak var sceneDelegate: SceneDelegate?
        ...
}

 

ViewController.swift

// In ViewController, and an extension that conforms to SceneDelegate.
extension ViewController: SceneDelegate {
        // In transition, set the new ScenesDelegate
        // to be the ViewController and replace the scene in Renderer.
    func transition(to scene: Scene) {
        scene.sceneDelegate = self
        self.scene = scene
        renderer?.scene = scene
    }
}

 

Raybreak.swift

func gameOver(win: Bool) {
        // Create the new scene and mark it as won or lost.

        // Let's play the win or lose sound effect and stop the background music.
        soundController.stopBackgroundMusic()

    let sound = win ? Sounds.win : Sounds.lose
    soundController.playEffect(name: sound)

    let gameOver = GameOver(sceneSize: sceneSize)
    gameOver.win = win

        // We don't want to see the ball still bouncing, 
        // so we'll set the velocity to zero and remove the node.
    ballVelocity = SIMD3<Float>(repeating: 0)
    ball.position = SIMD3<Float>(repeating: 0)
    remove(node: ball)

        // To let the sound play before the transition happens, 
        // we need a slight delay in transitioning to the GameOver
        // scene after the end of the game.
        // So we can wrap the transition with a one second delay.
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        self.sceneDelegate?.transition(to: gameOver)
    }
}

 

Player가 이기면 You Win! Model을 보여주고 Player가 지면 You Lose Model을 보여준다.

Model을 가장 잘 볼 수 있도록 Camera를 장면에 다시 배치한다.

GameOver Scene에 추가할 Model은 Game Model > Game Over에 위치해 있다.

GameOver.swift를 열고 Message Model에 대한 속성을 추가한다.

 

GameOver.swift

import Foundation

class GameOver: Scene {
    var messageModel: Model?

        I'm making it optional, as I don't know which model to load until win has been set.
    var win = false {
        didSet {
                        // In wins didset closure, replace the print command
                        // with loading the correct model.
            if win {
                messageModel = Model(name: "youwin")
            } else {
                messageModel = Model(name: "youlose")
            }
                        // And then add it to the scene.
            add(node: messageModel!)
        }
    }

    override func click(location: SIMD2<Float>) {
        let scene = RayBreak(sceneSize: sceneSize)
        sceneDelegate?.transition(to: scene)
    }

        // In setupScene, set the camera back from the model
        // and widen the field of view.
    override func setupScene() {
        camera.distance = 15
        camera.fov = radians(fromDegrees: 100)
    }

        // To rotate the model, override updatescene and add a litte bit
        // to the rotation of the model every frame.
    override func updateScene(deltaTime: Float) {
        messageModel?.rotation.y += .pi / 4 * deltaTime
    }
}

 

Raybreak에서 lives를 0으로 변경하고 Click하면 게임이 다시 시작되지만, Player가 준비가 되기 전에 시작된다는 문제가 있다. 시작할 준비가 될 때까지 게임이 시작되지 않도록 bool을 추가한다.

 

var startGame = false

 

게임이 시작되지 않은 경우 updateScene을 종료한다.

 

override func updateScene(deltaTime: Float) {
        // nothing will be updated.
        // Start the game whenever you press a key.
    if !startGame {
        return
    }
        ..
}

 

keyDown 시 게임을 시작한다.

 

override func keyDown(key: Int, isARepeat: Bool) -> Bool {
        ..
    startGame = true
    return true
}

 

Conclusion

자신만의 게임 엔진을 코딩하는 핵심은 규칙을 만드는 것임을 기억해라.

디자인 결정을 내리는 과정을 통해 얻은 Metal 지식으로 자신만의 게임과 3D 시각화를 만드는 방법을 잘 알고 있을 것이다.

3D Model을 로드하고 GPU Pipeline으로 보내 2D로 Rendering하는 것으로 시작했다. 그런 다음 몇 가지 수학 함수를 사용하여 Model을 3D 공간에 배치했다.

 

파편화에 뛰어들어 Model에 조명을 비추는 방법을 배운 다음 Material과 Texture로 원하는 대로 색상을 지정하는 방법을 배웠다. 버퍼의 내부를 검사하고 GPU에서 무슨 일이 일어나고 있는지 정확히 볼 수 있도록 GPU debuggur의 중요성에 대해 배웠다.

 

마지막으로 우리는 게임 엔진을 사용하여 사운드와 음악, Game Over Scene으로 완성된 게임을 작성했다. 여전히 Metal과 Computer Graphics에 대해 배워야 할 것이 많다. Metal은 방대하고 복잡한 API이며 기본 사항만 다루었다.

하지만 이제 실험을 할 수 있을 만큼 충분히 알아야 한다.

Metal로 더 깊이 들어가 애니메이션이나 Tesselation을 발견하거나 역추적을 수행하기 위해 Compute Shader를 사용하려는 경우 Metal by Tutorials라는 책이 있다.

 

Apple은 지난 몇 년 동안 Metal 및 모델 레이아웃의 복잡성에 대한 비디오를 제작하는 데 정말 잘 해왔다. 기본 사항을 모르면 이해하기 어려울 수 있다.

반응형

'IT > Graphics' 카테고리의 다른 글

Metal 스터디 7주차 (23.01.26)  (0) 2023.02.11
Metal 스터디 6주차 (23.01.19)  (0) 2023.02.06
Metal 스터디 5주차 (23.01.12)  (0) 2023.01.15
Metal 스터디 4주차 (23.01.06)  (0) 2023.01.15
Metal 스터디 3주차 (22.12.22)  (0) 2022.12.28

댓글