적당한 고통은 희열이다

- 댄 브라운 '다빈치 코드' 중에서

Swift iOS 앱 개발/Swift 튜토리얼

6. Swift SceneKit Tutorial _3D 게임 만들기

hongssup_ 2020. 12. 24. 11:44
반응형

SceneKit Tutorial 1 - Getting Started

SceneKit Tutorial 2 - Nodes

SceneKit Tutorial 3 - Physics

SceneKit Tutorial 4 - Render Loop

SceneKit Tutorial 5 - Particle Systems

* 현재 진행중인 프로젝트에서는 Physics와 Particle은 필요하지 않아, 필자는 튜토리얼 1, 2, 4 만 참고를 하였다.

 

SceneKit : Apple의 내장 3D 게임 프레임 워크

Geometry Fighter 게임을 만들어보며 SceneKit를 활용한 3D 그래픽 제어를 공부해보자!

 

< 튜토리얼 개요 >

1. 초기화 작업

2. Nodes 노드란?

3. Geometry 개체 만들기

4. Render Loop

 

// How to position nodes in SceneKit

The SceneKit Coordinate System 좌표계

// How to actually display something onscreen

 

Adding ShapeTypes

게임에 사용될 다양한 모양들의 ShapeType을 *enum으로 정의해줄 새로운 Swift 파일 생성
*enum : 열거형 defines a set of related values 

// ShapeType.swift

import Foundation

enum ShapeType:Int {
  case box = 0
  case sphere
  case pyramid
  case torus
  case capsule
  case cylinder
  case cone
  case tube

  static func random() -> ShapeType {
    let maxValue = tube.rawValue
    let rand = arc4random_uniform(UInt32(maxValue+1))
    return ShapeType(rawValue: Int(rand))!
  }
}

enum 으로 다양한 모양들의 case를 열거하고 Int 값으로 받아온다.

ShapeType.random() 함수를 *static으로 생성하여 다양한 모양의 개체들을 랜덤하게 불러오는 메서드를 만들어준다.

*함수를 static으로 만들면 다른 클라스에서 참조 가능.

arc4random_uniform?

UInt32 : unsigned Int : 부호 없이 양수만 있는 정수 자료형

Int32 : signed Int

 

Adding a Geometry Node

GameViewController에 spawnShape() 함수를 만들어 앞서 만들어준 ShapeType의 물체들을 랜덤으로 불러와 생성해준다.

switch - case 구문 (if문 처럼 많이 쓰이는 조건문)을 이용하여 물체들을 선언, 랜덤으로 불러온 후

SCNNode 를 새로 만들어주고 scnScene의 자식노드로 지정하여 물체를 생성해준다.

 

Render Loop ?

개체를 여러개 생성하려면 render loop를 통해서 spawnShape() 메서드를 반복적으로 호출해주어야 한다.

9 steps of the render loop

60fps로 실행되는 게임에서는 이 모든 단계가 순서대로 초당 60회 실행됨

 

The Renderer Delegate ( render loop에서 개체 생성하는 시작점 선언)

GameViewController 클라스에 *extension으로 SCNSceneRendererDelegate 프로토콜을 추가해주고, renderer 프로토콜 속성을 통해 spawnShape()을 실행하여, render loop 속 SceneKit의 시작점을 만들어준다.

*extension 기능 중, 'Make an existing type conform to a protocol.' 기능

 

Spawn Timers

모든 기기에서 일관된 게임 환경을 구축하기 위해 ‘시간’을 활용해야 한다. 장치가 지원하는 프레임 속도에 관계없이 일정한 속도로 애니메이션을 구현할 수 있도록. 

updateAtTime 매개변수는 현재 시스템 시간(current system time)을 나타냄.

간단한 타이머를 사용하여 어떤 프로세서라도 처리할 수 있는 랜덤한 시간 간격으로 개체를 생성할 것.

 

Removing Child Nodes

spawnShape()은 계속해서 새로운 자식 노드를 scene에 추가하지만 화면에서 벗어난 후에도 제거되지 않는다. 최적의 성능과 프레임 속도로 실행하기 위해서는 이러한 눈에 띄지 않는 물체를 제거해주어야 한다. 어디서? Render loop 에서!
물체가 경계에 다다르면 scene에서 제거하도록. cleanScene() 메서드를 만들어주고
renderer 프로토콜에서 실행.

 

 

 

GameViewController.swift

import UIKit
import SceneKit

class GameViewController: UIViewController {    
    var scnView: SCNView!
    var scnScene: SCNScene!
    var cameraNode: SCNNode!
    var spawnTime: TimeInterval = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupView()
        setupScene()
        setupCamera()
    }
    
    override var shouldAutorotate: Bool {
        return true
    }
    
    override var prefersStatusBarHidden: Bool {
        return true
    }
    
    func setupView() {
        scnView = self.view as! SCNView
        
        // 1 real-time statistics panel 활성화
        scnView.showsStatistics = true
        // 2 간단한 제스처 통해 활성 카메라 수동 제어
        scnView.allowsCameraControl = true
        // 3 새로 광원 추가할 필요없이 기본 조명 생성
        scnView.autoenablesDefaultLighting = true
        // GameViewController가 view의 대리자 역할을 할 것이라 선언.
        scnView.delegate = self
        // SceneKit view를 무한 재생 모드로.
        scnView.isPlaying = true
    }
    
    func setupScene() {
        scnScene = SCNScene()
        scnView.scene = scnScene
        scnScene.background.contents = "GeometryFighter.scnassets/Textures/Background_Diffuse.png"
    }
    
    func setupCamera() {
        // 1 빈 SCNNode 생성하여 cameraNode에 할당
        cameraNode = SCNNode()
        // 2 새 SCNCamera 오브젝트를 생성하여 cameraNode의 속성 camera에 할당
        cameraNode.camera = SCNCamera()
        // 3 카메라 위치 설정
        cameraNode.position = SCNVector3(x: 0, y: 5, z: 10)
        // 4 scene의 root node의 자식 노드로 cameraNode를 scene에 더해줌
        scnScene.rootNode.addChildNode(cameraNode)
    }
    
    // ShapeType.swift에 정의된 물체들을 무작위로 생성하는 메서드
    func spawnShape() {
        var geometry:SCNGeometry
        switch ShapeType.random() {
        case .box:
            geometry = SCNBox(width: 1.0, height: 1.0, length: 1.0, chamferRadius: 0.0)
        case .sphere:
            geometry = SCNSphere(radius: 0.5)
        case .pyramid:
            geometry = SCNPyramid(width: 1.0, height: 1.0, length: 1.0)
        case .torus:
            geometry = SCNTorus(ringRadius: 0.5, pipeRadius: 0.25)
        case .capsule:
            geometry = SCNCapsule(capRadius: 0.3, height: 2.5)
        case .cylinder:
            geometry = SCNCylinder(radius: 0.3, height: 2.5)
        case .cone:
            geometry = SCNCone(topRadius: 0.25, bottomRadius: 0.5, height: 1.0)
        case .tube:
            geometry = SCNTube(innerRadius: 0.25, outerRadius: 0.5, height: 1.0)
        }
        geometry.materials.first?.diffuse.contents = UIColor.random()
        
        let geometryNode = SCNNode(geometry: geometry)
        geometryNode.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
        
        let randomX = Float.random(min: -2, max: 2)
        let randomY = Float.random(min: 10, max: 18)
        let force = SCNVector3(x: randomX, y: randomY , z: 0)
        let position = SCNVector3(x: 0.05, y: 0.05, z: 0.05)
        geometryNode.physicsBody?.applyForce(force, at: position, asImpulse: true)
        scnScene.rootNode.addChildNode(geometryNode)
    }
    
    // 화면에서 벗어난 물체들을 제거해주는 메서드
    func cleanScene() {
        for node in scnScene.rootNode.childNodes {
            // presentationNode를 활용하여 애니메이션 실행 중인 개체의 실제 위치를 얻기.
            if node.presentation.position.y < -2 {
                node.removeFromParentNode()
            }
        }
    }
    
}

// SceneKit의 render loop 시작점 만들어주기
extension GameViewController: SCNSceneRendererDelegate {
    func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
        // 1. time(현재 시스템 시간)이 spawnTime보다 크면 새로운 모양 생성.
        if time > spawnTime {
            spawnShape()
            // 개체 생성 후 spawnTime 업데이트. 현재 시간 이후 0.2초 ~ 1.5초 사이에 다음 개체 생성
            spawnTime = time + TimeInterval(Float.random(min: 0.2, max: 1.5))
        }
        cleanScene()
    }
}

 

이번 튜토리얼에서는 geometry의 생성, 제거, 시간 제어 등을 학습하며 

랜덤한 시간간격으로 랜덤한 물체를 지속적으로 생성하는 3D 프로젝트를 구현해보았다.  

 

+ 화면을 터치 했을 때 물체가 생성되게 하려면 어떻게 할 수 있을까??

UITapGestureRecognizer 클라스를 활용하여 간단한 터치 이벤트를 추가로 구현해보았다. 

@objc func spawnShapebyTouch(_ sender: UITapGestureRecognizer) {
    if sender.state == .ended {
        spawnShape()
    }
}

@objc로 UITapGestureRecognizer 속성을 활용한 터치 이벤트 함수를 만들어준다. 

화면을 터치했을 때 새로운 물체가 생성되도록 하는 것이 목표이므로, 함수 안에 spawnShape()를 넣어준다.

그런 다음 setupView()에서 다음 코드를 추가하여 view에 .addGestureRecognizer()를 통해 함수를 작동시켜준다. 

let tap = UITapGestureRecognizer(target: self, action: #selector(self.spawnShapebyTouch(_:)))
scnView.addGestureRecognizer(tap)

 

 

 

 

< 용어 정리 >

● viewing frustum :

enum

 

728x90
반응형