티스토리 뷰
요즘 iOS 게임 앱을 만들고싶은 생각이 많이 들어요! Unity 같은 게임엔진을 따로 공부하지 않아도 스위프트에서 제공해주는 여러 Kit이 존재하더라구요! 갑자기 할 수 있겠다는 생각과 의욕이 생겨서 GameplayKit부터 공부해볼게요. 순조롭게 공부를 마치고 제 이름으로된 게임을 하나 출시할 수 있었으면 좋겠네요!!
GameplayKit은 기초적인 툴을 제공하고 게임설계를 위한 기술을 제공하는 객체지향 프레임워크입니다.
GameplayKit은 기능적이고 재사용할 수있는 구조로 게임을 디자인하는 도구뿐만 아니라 캐릭터 이동이나 적대행동(AI 적 행동?)같은 게임 기능을 설계하거나 강화하는 기술을 포함합니다.
GameplayKit은 게임디자인과 개발측면의 많은부분을 다룹니다.
GameplayKit 기능으로 게임을 구축하는 것을 보여주는 튜토리얼과 함께 GameplayKit으로 활용할 수 있는 게임 디자인 패턴에 대한 더 깊은 논의는 GameplayKit 프로그래밍 가이드를 참조하십시오.
구성요소가 엄청 많은데.. 일단 파악하려면 튜토리얼 해보는게 가장 좋을 것 같아요 여러가지 예제 코드들이 개발문서에 링크되어 있는데 첫 번째 먼저 볼게요!
Boxes: GameplayKit Entity-Component Basics
Boxes: GameplayKit Entity-Component Basics Last Revision:Build Requirements:Xcode 8.0, OS X 10.11 SDK, iOS 9.0 SDK, tvOS 9.0 SDKRuntime Requirements:OS X 10.11, iOS 9.0, tvOS 9.0, and later Important: This document is no longer being updated. For the late
developer.apple.com
엔티티 및 구성 요소 (Entities and Components) 에 대한 예제 코드라고 하네요.
이 예제는 GameplayKit의 엔티티 구성 요소 기능을 사용하여 모듈식의 확장 가능한 게임 아키텍처를 만드는 방법을 보여줍니다.
그것은 `GKEntity`, `GKComponent` 및 `GKComponentSystem` 클래스를 다룹니다.
여기서는 이 3가지라도 잘 익혀간다면 아주 성공한 튜토리얼인가봐요. 흥미로워요..
Project를 생성할때 Game으로 생성하면 좀 더 쉽게 만들 수 있지만 저는 처음이니만큼 그냥 App파일로 만들어볼게요!
이 튜토리얼은 3D 공간에서 다양한 색상의 박스들이 화면을 터치할때마다 통통 튀는 동작을 해주고 있어요!
그러니만큼 3D를 지원하는 Scene Kit도 쓰이고 2D를 지원하는 SpriteKit도 같이 쓰이네요.. 이것들도 공부해야하는데 갈길이 멉니다!
아무튼 3D환경을 지원하고자 main 스토리 보드로 가서 ViewController의 Identity Inspector영역에서 rootView를 SCNView(3DView)로 변경해줍니다!

그리고 ViewController에 오셔서 rootView 인스턴스를 가져와 SCNView(3DView)로 캐스팅 해주고 색상을 변경한 뒤 빌드해봅시다!
(ViewController의 view의 리턴은 UIView타입이지만 SCNView는 UIView의 하위클래스이기 때문에 캐스팅이 가능합니다.)
(참고 저는 임의로 ViewController를 GameViewController로 바꿨어요!)
import UIKit
import SceneKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
guard let scnView = self.view as? SCNView else { fatalError("error") }
scnView.backgroundColor = .lightGray
}
}

잘 적용 되었네요! 위 작업이 귀찮으시면 그냥 SCNView()로 생성하시고 rootView에 서브뷰로 추가해주셔도 됩니다.
이제 이 SCNView에 Scene을 올리고 Scene에 객체들을 추가해주는 작업들을 해주어야하는데 그러려면 객체를 먼저 만들어야겠죠!
이번 튜토리얼에 핵심 GKEntity`, `GKComponent` 및 `GKComponentSystem 친구들을 만들러 가봅시다!
우선 scn파일을 먼저 만들어 둡니다! File>New>File>Scene Kit Scene File 클릭하고 이름은 GameScene으로 해줄게요
그러면 3D작업을 해줄 수 있을것 같은 3차원 공간이 나와요! 이따가 여기에 박스같은 객체 노드들을 만들어줄거에요!
이제 진짜 저 위에 3가지 친구들을 만들어 보아요.
우선 만들건 Component인데요 뭔지부터 알아봐야겠죠?
CKComponent
Entity-Component 아키텍처로 게임을 빌드할 때 엔티티에 특정 게임플레이 기능을 추가하는 객체를 만들기 위한 추상적인 슈퍼클래스라고 합니다.
이 아키텍처에서 엔티티는 게임과 관련된 객체이며,
컴포넌트는 일반적인 방식으로 엔티티의 행동의 특정 측면을 처리하는 객체입니다.
컴포넌트의 기능 범위가 제한되어 있어 다양한 종류의 엔티티에 동일한 컴포넌트 클래스를 재사용할 수 있습니다.
이 아키텍처에서 엔티티는 게임과 관련된 객체이며, 구성 요소는 일반적인 방식으로 엔티티의 행동의 특정 측면을 처리하는 객체이다. 컴포넌트의 기능 범위가 제한되어 있기 때문에, 다양한 종류의 엔티티에 대해 동일한 구성 요소 클래스를 재사용할 수 있습니다.
컴포넌트를 재사용 하려면 재사용할 컴포넌트를 서브클래싱하여 구현한 후
재사용할 GKEntity 객체를 만들고 addComponent(_:) 메서드를 사용하여 커스텀 컴포넌트 클래스의 인스턴스를 첨부하여 게임 엔티티를 구축합니다.
런타임에 컴포넌트 기반 게임은 update(_:) (SpriteKit) 또는 renderer(_:updateAtTime:) (SceneKit) 또는 사용자 지정 렌더링 엔진의 CADisplayLink (iOS) 또는 CVDisplayLink (macOS) 타이머와 같은 업데이트/렌더링 루프 메서드에서 각 컴포넌트로 주기적인 논리를 파견해야 합니다. GameplayKit은 업데이트를 발송하기 위한 두 가지 메커니즘을 제공합니다:
- Per-entity : 각 엔티티의 update(deltaTime:) 메소드를 호출하면 각 컴포넌트의 update(deltaTime:) 메소드로 전달됩니다. 이 옵션은 소수의 엔티티와 컴포넌트가 있는 게임에서 빠르게 구현될 수 있습니다.
- ㄹ : GKComponentSystem 객체를 사용하여 특정 컴포넌트 클래스의 모든 인스턴스를 처리하십시오. GKComponentSystem의 update(deltaTime:) 메서드를 호출하면, 관리하는 모든 컴포넌트 객체의 update(deltaTime:) 메서드로 전달됩니다. GKComponentSystem은 게임의 엔티티/컴포넌트 계층에 대한 지식이 필요하지 않기 때문에, 이 옵션은 복잡한 개체 그래프가 있는 게임에서 잘 작동합니다.
엔티티가 객체고 컴포넌트는 엔티티의 특정 행동을 나타내는거구나.. 반복되는 루프 메소드에서 각 컴포넌트는 업데이트 또는 렌더링을 해주어야하는구나 까지 집고 컴포넌트를 만들러 가볼게요.
Geometry Component만들기
Geometry 기하학입니다! 고등 수학 벡터에서 많이 들었었죠..
공간에 있는 도형의 성질, 즉 대상들의 치수, 모양, 상대적 위치 등을 연구하는 수학의 한 분야이다.
라고 하네요. 무언가를 움직이거나 모양을 바꾸거나 하는 특정 동작을 구현하기 위해 GKComponent를 서브클래싱합니다.
여기서는 박스 엔티티가 점프를 띄는 동작을 구현하기 위해 GeometryComponent를 만들어줍니다.
GeometryComponent.swift 파일을 만들구요.
class GeometryComponent: GKComponent {
// MARK: Properties
// 중력 노드
let geometryNode: SCNNode
// MARK: Init
init(geometryNode: SCNNode) {
self.geometryNode = geometryNode
super.init()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Methods
/// 상자 노드에 상향 임펄스를 적용해서 점프를 발생시키는 함수입니다.
func applyImpulse(_ vector: SCNVector3) {
geometryNode.physicsBody?.applyForce(vector, asImpulse: true)
}
}
위 코드를 넣어주면됩니다. 주석과 같이 벡터를 넘겨주면 그 x,y,z축으로 임펄스를 주어 점프할 수 있게 해줍니다.
PlayerControlComponent 만들기
import GameplayKit
import SceneKit
class PlayerControlComponent: GKComponent {
// MARK: Properties
var geometryComponent: GeometryComponent? {
return entity?.component(ofType: GeometryComponent.self)
}
// MARK: Methods
// 객체에 y축으로 2만큼 임펄스를 전달해 점프동작을 만드는 함수
func jump() {
let jumpVector = SCNVector3(x: 0, y: 2, z: 0)
geometryComponent?.applyImpulse(jumpVector)
}
}
위에서와 같이 PlayerControlComponent.swift를 만들구요.
y를 2만큼 임펄스를 줘서 점프 동작을 구현해낼 수 있어요
ParticleComponent 만들기
이거는 뭐지 뭐지 하다가 보니까 박스 엔티티가 반짝반짝하는 특수효과를 주는 컴포넌트를 만드는 예제코드인 것 같아요.
뭔가 되게 많아 보이지만 주석을 열심히 읽고 한국어로 써놨거든요!! 이거만 보셔도 충분히 이해가실 것 같아서 다른 설명은 생략합니다!
import SpriteKit
import SceneKit
import GameplayKit
class ParticleComponent: GKComponent {
// MARK: Propertie
/// 엔티티의 모양,위치 등을 변경하는 컴포넌트
var geometryComponent: GeometryComponent? {
return entity?.component(ofType: GeometryComponent.self)
}
/// particle effec를 가지는지 bool 여부
var boxHasParticleEffect = false
/// particle effect를 생성하고 관리하는 객체
let particleEmitter: SCNParticleSystem
/// 객체 주변에 빛나게 해주는 객체
let boxLight = SCNLight()
/// 밝기 변경 연산 프로퍼티
var lightBrightness: CGFloat = 1 {
didSet {
boxLight.color = SKColor(white: lightBrightness, alpha: 1)
}
}
/// 난수를 발생시키는 객체입니다. 빛의 밝기를 랜덤하게 만드는데 사용합니다.
let randomSource = GKRandomSource()
/// 밝기가 목표 밝기보다 낮으면 증가된 밝기를 반환하고 그렇지 않으면 감소된 밝기를 반환합니다. 이 작업을 통해 자연스러운 반짝임을 나타낼 수 있습니다.
var nextLightBrightness: CGFloat {
let delta: CGFloat = lightBrightness < targetLightBrightness ? 0.025 : -0.025
return lightBrightness + delta
}
/// 목표 밝기입니다.
var targetLightBrightness: CGFloat = 0.5
// MARK: Initialization
init(particleName: String) {
/// particle생성자
particleEmitter = SCNParticleSystem(named: particleName, inDirectory: "/")!
super.init()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Methods
override func update(deltaTime _: TimeInterval) {
//상자 geometry 컴포넌트가 생성되었지만 아직 particle 효과나 조명 효과가 부착되지 않은경우 부착합니다.
updateGeometryComponent()
// 프레임마다 새로운 목표 밝기를 생성합니다.
targetLightBrightness = nextTargetLightBrightness()
// 프레임마다 조명 밝기를 업데이트하여 조명이 깜박거립니다.
lightBrightness = nextLightBrightness
}
/// 상자 geometry 컴포넌트가 생성되었지만 아직 particle 효과나 조명 효과가 부착되지 않은경우 부착합니다.
func updateGeometryComponent() {
if let geometryComponent = geometryComponent, !boxHasParticleEffect {
geometryComponent.geometryNode.addParticleSystem(particleEmitter)
geometryComponent.geometryNode.light = boxLight
}
// 부착 여부 갱신
boxHasParticleEffect = geometryComponent != nil
}
// MARK: Light Flickering Algorithm
// 조명의 목표 밝기를 무작위로 조정합니다.
func nextTargetLightBrightness() -> CGFloat {
// 목표 밝기가 랜덤하게 증가하거나 감소합니다.
let increaseLightTargetBrightness = randomSource.nextBool() // false true가 랜덤으로
let delta: CGFloat = increaseLightTargetBrightness ? 0.2 : -0.2 // 증가 or 감소
// 다음 목표값 조정
let newTargetLightBrightness = targetLightBrightness + delta
// 밝기가 아무리 작아져도 0 아무리 커져도 1로 제한
let clampedLightBrightness = (newTargetLightBrightness...newTargetLightBrightness).clamped(to: (0...1)).lowerBound
return clampedLightBrightness
}
}
이제 이 컴포넌트를 어떻게 관리할지 엔티티를 어떻게 Scene에 렌더링 해야하는지 해봅시다!
Game 클래스를 만들고 NSObject객체를 상속받고 SCNSceneRenderDelegate를 채택해주세요.
SCNSceneRenderDelegate는 Scene을 렌더링하는데 필요한 메소드들을 위임하고있습니다.
SCNScene(named: ) 생성자를 통해 아까 만든 GameScene.scn Scene의 인스턴스를 받아옵니다.
let scene = SCNScene(named: "GameScene.scn")!
import SceneKit
import GameplayKit
class Game: NSObject, SCNSceneRendererDelegate {
// MARK: Properties
/// Scene이에요~!
let scene = SCNScene(named: "GameScene.scn")!
/// 모든 player control components를 관리합니다. 이 컴포넌트시스템을 통해 모든 컴포넌트에 접근할 수 있어요.
let playerControlComponentSystem = GKComponentSystem(componentClass: PlayerControlComponent.self)
/// 모든 particle components를 관리합니다. 이 컴포넌트시스템을 통해 모든 컴포넌트에 접근할 수 있어요.
let particleComponentSystem = GKComponentSystem(componentClass: ParticleComponent.self)
/// box 엔티티를 담을 배열이에요.
var boxEntities = [GKEntity]()
/// 컴포넌트들 업데이트 해줘야한다했죠! 업데이트 시간을 추적하는 변수입니다.
var previousUpdateTime: TimeInterval = 0
// MARK: Initialization
override init() {
super.init()
setUpEntities() // Scene에 엔티티 셋팅
addComponentsToComponentSystems() ///ComponentSystem에서 모든 component들을 관리할 수 있게 추가해주는 함수
}
/// Scene에 엔티티를 설정합니다. 4개는 팩토리 메소드를 만들어 생성해보구요. 보라색박스 하나는 직접 해볼게요.
func setUpEntities() {
// 네개 색상의 박스를 만들건데 makeBoxEntity 팩토리메소드 구현을 어떻게 하는지 먼저 보러 내려갔다옵시다!
let redBoxEntity = makeBoxEntity(forNodeWithName: "redBox")
let yellowBoxEntity = makeBoxEntity(forNodeWithName: "yellowBox", withParticleComponentNamed: "Fire")
let greenBoxEntity = makeBoxEntity(forNodeWithName: "greenBox", wantsPlayerControlComponent: true)
let blueBoxEntity = makeBoxEntity(forNodeWithName: "blueBox", wantsPlayerControlComponent: true, withParticleComponentNamed: "Sparkle")
// 이 보라색 박스는 튜토리얼에서 직접 만들어보라는 Todo를 줬네요.
let purpleBoxEntity = GKEntity()
let purpleBoxNode = scene.rootNode.childNode(withName: "purpleBox", recursively: false)
// Create the purple box's geometry component, and add it to the entity.
let geometryComponent = GeometryComponent(geometryNode: purpleBoxNode!)
purpleBoxEntity.addComponent(geometryComponent)
/*
이 아래 부분에 보라색 박스 엔티티에 particle효과를 붙이거나 사용자가 컨트롤할지 여부를 코드로 작성해보라고 하네요.
*/
boxEntities = [
redBoxEntity,
yellowBoxEntity,
greenBoxEntity,
blueBoxEntity,
purpleBoxEntity
]
}
/// Per-component 방식으로 업데이트 메소드를 전송하는거 위에서 배웠죠! ComponentSystem을 이용해 특정 컴포넌트를 다 같이 관리하는것! 그걸 위해서 system에 컴포넌트들을 추가해주는 작업입니다.
func addComponentsToComponentSystems() {
for box in boxEntities {
particleComponentSystem.addComponent(foundIn: box)
playerControlComponentSystem.addComponent(foundIn: box)
}
}
// MARK: Methods
func jumpBoxes() {
//컨트롤 여부 true로 설정해둔 엔티티들의 점프를 발생시킵니다.
for case let component as PlayerControlComponent in playerControlComponentSystem.components {
component.jump()
}
}
/// 모든 프레임을 업데이트하는 델리게이트 메소드
func renderer(_: SCNSceneRenderer, updateAtTime time: TimeInterval) {
// 이전 업데이트 후 흐른 시간
let timeSincePreviousUpdate = time - previousUpdateTime
// particle component system을 업데이트시킴
particleComponentSystem.update(deltaTime: timeSincePreviousUpdate)
// 이전 업데이트 시간을 업데이트해 이후 계산을 정확히 유지시킴
previousUpdateTime = time
}
// MARK: Box Factory Method
func makeBoxEntity(forNodeWithName name: String, wantsPlayerControlComponent: Bool = false, withParticleComponentNamed particleComponentName: String? = nil) -> GKEntity {
// 일단 엔티티를 생성해줍니다.
let box = GKEntity()
// 위에서 GameScene.snc 가져왔었죠? 거기 존재하는 identity name과 일치해야합니다 거기서 노드를 가져와서 엔티티로 만드는거같아요!
guard let boxNode = scene.rootNode.childNode(withName: name, recursively: false) else {
fatalError("Making box with name \(name) failed because the GameScene scene file contains no nodes with that name.")
}
// 아까 component에서 component재사용하는 방법이였죠? 서브클래싱하고 addComponent를 통해 엔티티에 추가해주라고한 부분 이렇게 사용하는 겁니다! 이로써 박스 엔티티가 이동할 수 있게 되겠어요!
let geometryComponent = GeometryComponent(geometryNode: boxNode)
box.addComponent(geometryComponent)
// particle이 존재하면 엔티티에 추가해주구요!
if let particleComponentName = particleComponentName {
let particleComponent = ParticleComponent(particleName: particleComponentName)
box.addComponent(particleComponent)
}
// 사용자가컨트롤하기 원하면 추가해줍니다.
if wantsPlayerControlComponent {
let playerControlComponent = PlayerControlComponent()
box.addComponent(playerControlComponent)
}
return box
}
}
이제 GameScene.scn에 들어가셔서 노드들을 추가해줘야합니다. 이 때 박스 노드들의 Identity Name은 위에 SetUpEntity에서 작성했던 이름과 같아야 코드에서 노드를 불러올 수 있습니다.!!
그리고 Particle 이미지도 다운 받아서 넣어주세요!

그리고 진짜 마지막으로!! ViewController에 돌아가서 이 작업만 하면 끝!!
import UIKit
import SceneKit
class GameViewController: UIViewController {
let game = Game()
override func viewDidLoad() {
super.viewDidLoad()
guard let scnView = self.view as? SCNView else { fatalError("error") }
scnView.backgroundColor = .lightGray
scnView.scene = game.scene
scnView.delegate = game
let tap = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
scnView.addGestureRecognizer(tap)
}
@objc private func handleTap(_: UITapGestureRecognizer) {
game.jumpBoxes()
}
}

잘 동작하시나요!!?
GameScene.scn에서 node만드실때 박스 노드들은 오른쪽 physics body부분에서 Type Dynamic으로 바꿔주셔야 합니다.
physics body 기본값이 nil인데 이는 기하학적으로 관여하지않는다? 이런 의미라 동작을 안해요!
아직 부족한 부분이 많지만 재밌어서 꾸준히 부족한 내용 채워 넣어볼게요!! 다음편도 계속됩니다!
'Swift' 카테고리의 다른 글
[Swift] TaskGroup이란? (0) | 2024.04.11 |
---|---|
[Xcode] 생산성을 높이는 효율적인 단축키 모음 (1) | 2024.03.31 |
[Swift] TextField ClearButton Image 커스텀하기 (0) | 2023.06.29 |
[Swift] SpriteKit Particle effect다루기[1] (0) | 2023.05.25 |
[Swift] AssociatedType이란? typealias란? (0) | 2023.03.29 |