https://developer.apple.com/documentation/spritekit/skphysicsbody/
SpriteKit
의 핵심 개념이라고 볼 수 있는 node
. 노드는 UI 컴포넌트(시각적 요소) 뿐만 아니라 사운드(청각적 요소)까지 구현할 수 있는 한 단위다. 여기에 물리를 부여할 수 있는 방법에 대해 정리해보려 한다.
노드에 물리를 부여하는 객체. SKNode
에 physicsBody
프로퍼티를 통해 물리를 부여할 수 있다. scene
이 새로운 프레임을 처리할 때 물리 계산을 수행하는데 여기에는 중력, 마찰, 충돌 작용이 포함된다. 또한 개발자가 직접 힘이나 충격을 적용할 수도 있다. 씬이 물리 계산을 완료하면 계산 결과에 따라 노드의 위치와 회전 상태가 갱신된다.
SpriteKit
은 두 가지 종류의 물리 바디를 지원한다. 볼륨 기반 바디(volume-based-bodies
)와 에지 기반 바디(edge-based-bodies
)인데, 볼륨 기반은 질량과 부피를 표현하며 에지 기반은 경계선을 표현할 때 사용한다.
물리 바디는 여러가지 종류의 생성자를 통해 부여할 수 있다. 우선 볼륨 기반 바디부터 살펴보자면
node.physicsBody = SKPhysicsBody(circleOfRadius: 30)
node.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 100, height: 50))
CGPath
타입)으로 부여할 수도 있다. (이 때, path
는 닫힌 상태여야 한다)let path = CGMutablePath()
// ... //
path.closeSubpath()
node.physicsBody = SKPhysicsBody(polygonFrom: path)
SKTexture
타입)를 사용해 물리 바디를 생성할 수도 있다. let texture = SKTexture(imageNamed: "rock")
node.physicsBody = SKPhysicsBody(texture: texture, size: node.size)
물리적 실체(질량, 부피) 없이 충돌 경계를 정의하기 위한 에지 기반 바디 생성자들도 있다. 에지 기반 물리 바디는 중력이나 힘을 무시한다.
scene.physicsBody = SKPhysicsBody(edgeLoopFrom: scene.frame) // 화면 경계 설정
init(edgeLoopFrom: CGRect)
: 주어진 사각형 경계를 따라 루프 형태(닫힌 경로)로 바디 생성
init(edgeLoopFrom: CGPath)
: 닫힌 경로를 따라 루프 형태의 바디 생성
init(edgeChainFrom: CGPath)
: 열린 경로를 따라 체인 형태의 바디 생성
init(edgeFrom CGPoint, to: CGPoint)
: 두 점 사이를 잇는 직선을 생성
CGPath
에 입힐 수 있는 물리 바디의 종류는 여러가지가 있지만 이미지와 같이 영역의 차이가 있고 volume
기반과 edge
기반이라는 차이가 있다.
항목 | polygonFrom | edgeLoopFrom | edgeChainFrom |
---|---|---|---|
경로 | 단힌 경로 | 닫힌 경로 | 열린 경로 |
충돌 범위 | 내부 전체 | 테두리 선만 | 선 위만 |
물리 영향 | 충격, 힘, 마찰 등 | 적용되지 않음 | 적용되지 않음 |
실체/움직임 | 실체(부피)가 있으며 움직일 수 있음 | 실체 없이 선만 고정되어 움직이지 않음 | 실체 없이 선만 고정되어 움직이지 않음 |
주 사용처 | 플레이어, 적, 블록 등 오브젝트 | 게임 씬 테두리, 투명한 벽, 맵 외곽 등 경계선 | 장애물 경계, 트랙, 땅 모양 등 배경 |
physicsBody
에 속성값을 부여하여 원하는 만큼 섬세하게 물리를 구현할 수 있다.
프로퍼티 | 설명 |
---|---|
isDynamic | true이면 물리 시뮬레이션의 영향을 받고, false이면 움직이지 않음 |
affectedByGravity | 중력 영향을 받을지 여부 |
allowsRotation | 회전을 허용할지 여부 |
mass | 질량 (기본값 1.0) |
friction | 마찰력 (0.0 ~ 1.0) |
restitution | 반발력 (튕기는 정도, 0.0 ~ 1.0) |
linearDamping | 직선 운동 감쇠 |
angularDamping | 회전 운동 감쇠 |
velocity | 속도 |
angularVelocity | 회전 속도 |
SKPhysicsBody
에는 3가지 비트마스크 프로퍼티가 있다. 비트마스크는 충돌 관련 설정을 담당한다. 데이터 타입은 UInt32
로, 물리 바디의 고유 식별에 용이하다.
categoryBitMask
: 객체가 속한 카테고리 정의. 객체의 정체성을 나타내는 식별자 역할을 한다.collisionBitMask
: 객체가 충돌 인식할 대상. 해당 객체의 categoryBitMask
값을 할당해주면 된다.contactTestBitMask
: 객체가 접촉 시 감지할 대상. 해당 객체의 categoryBitMask
값을 할당해주면 된다.여기서 collisionBitMask
와 contactTestBitMask
의 차이를 풀어서 설명하자면 전자의 경우 지정한 객체와 물리적으로 충돌을 인식하게 되고(서로 겹치지 않고 튕겨져 나감) 후자는 객체끼리 만나도 collisionBitMask
가 지정되어 있지 않으면 충돌하지 않고 겹쳐지며, 서로 만났다는 사실만 감지할 수 있다.
카테고리를 정의하는 방법은 physicsBody.categoryBitMask = 0
처럼 직접 할당하는 방법도 있지만, struct
또는 enum
으로 정의한 뒤 사용하면 더 좋다.
struct PhysicsCategory {
static let none: UInt32 = 0 // 인식, 감지할 대상이 없을 경우 할당할 값
static let ball: UInt32 = 0x1 << 0
static let path: UInt32 = 0x1 << 1
static let goal: UInt32 = 0x1 << 2
}
enum PhysicsCategory: UInt32 {
case none = 0
case ball = 0x1 << 0
case path = 0x1 << 1
case goal = 0x1 << 2
}
나는 카테고리라는 점에서 enum
을 사용하는 것이 더 일리있지 않나 생각했는데, PhysicsCategory.ball.rawValue
로 접근하느냐 PhysicsCategory.ball
로 접근하느냐의 편의성 면에서 struct
를 사용하는 경우가 많아보였다.
ballNode = SKShapeNode(circleOfRadius: 20)
// ... //
ballNode.physicsBody = SKPhysicsBody(circleOfRadius: 20) // 노드 모양 그대로 물리 부여
ballNode.physicsBody?.categoryBitMask = PhysicsCategory.ball
ballNode.physicsBody?.collisionBitMask = PhysicsCategory.path
ballNode.physicsBody?.contactTestBitMask = PhysicsCategory.goal
공이 길을 따라 움직여 도착점으로 가도록 하는 게임을 만든다고 가정했을 때, 공 노드에 물리를 정의하는 예시 코드다. 공 노드의 모양(20 radiue
의 원)와 똑같은 형태의 물리를 부여한 뒤 ball
카테고리로 정의하였다. 그리고 길 노드(path
)와 닿았을 경우 물리적 충돌이 일어나도록 하였으며 도착점(goal
)에 도달할 경우 감지할 수 있도록 설정했다. 이렇게 되면 개발자는 SKPhysicsContactDelegate
를 통해 공이 도착점에 도달한 시점을 알 수 있다. (물리 엔진은 SKScene
의 physicsWorld
에 의해 전역적으로 동작하며, SKPhysicsContactDelegate
통해 접촉 이벤트를 받을 수 있음)
class GameScene: SKScene, SKPhysicsContactDelegate {
// ... //
override func didMove(to view: SKView) {
// ... //
physicsWorld.contactDelegate = self
}
func didBegin(_ contact: SKPhysicsContact) {
// ~ contact 매개변수 활용하여 원하는 동작 구현 ~ //
}
}