[SpriteKit] SKPhysicsBody

Emily·2025년 7월 7일
1
post-thumbnail
https://developer.apple.com/documentation/spritekit/skphysicsbody/

SpriteKit의 핵심 개념이라고 볼 수 있는 node. 노드는 UI 컴포넌트(시각적 요소) 뿐만 아니라 사운드(청각적 요소)까지 구현할 수 있는 한 단위다. 여기에 물리를 부여할 수 있는 방법에 대해 정리해보려 한다.

SKPhysicsBody

노드에 물리를 부여하는 객체. SKNodephysicsBody 프로퍼티를 통해 물리를 부여할 수 있다. 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에 입힐 수 있는 물리 바디 차이점

CGPath에 입힐 수 있는 물리 바디의 종류는 여러가지가 있지만 이미지와 같이 영역의 차이가 있고 volume 기반과 edge 기반이라는 차이가 있다.

항목polygonFromedgeLoopFromedgeChainFrom
경로단힌 경로닫힌 경로열린 경로
충돌 범위내부 전체테두리 선만선 위만
물리 영향충격, 힘, 마찰 등적용되지 않음적용되지 않음
실체/움직임실체(부피)가 있으며 움직일 수 있음실체 없이 선만 고정되어 움직이지 않음실체 없이 선만 고정되어 움직이지 않음
주 사용처플레이어, 적, 블록 등 오브젝트게임 씬 테두리, 투명한 벽, 맵 외곽 등 경계선장애물 경계, 트랙, 땅 모양 등 배경

주요 프로퍼티

physicsBody에 속성값을 부여하여 원하는 만큼 섬세하게 물리를 구현할 수 있다.

프로퍼티설명
isDynamictrue이면 물리 시뮬레이션의 영향을 받고, false이면 움직이지 않음
affectedByGravity중력 영향을 받을지 여부
allowsRotation회전을 허용할지 여부
mass질량 (기본값 1.0)
friction마찰력 (0.0 ~ 1.0)
restitution반발력 (튕기는 정도, 0.0 ~ 1.0)
linearDamping직선 운동 감쇠
angularDamping회전 운동 감쇠
velocity속도
angularVelocity회전 속도

BitMask

SKPhysicsBody에는 3가지 비트마스크 프로퍼티가 있다. 비트마스크는 충돌 관련 설정을 담당한다. 데이터 타입은 UInt32로, 물리 바디의 고유 식별에 용이하다.

  1. categoryBitMask : 객체가 속한 카테고리 정의. 객체의 정체성을 나타내는 식별자 역할을 한다.
  2. collisionBitMask : 객체가 충돌 인식할 대상. 해당 객체의 categoryBitMask 값을 할당해주면 된다.
  3. contactTestBitMask : 객체가 접촉 시 감지할 대상. 해당 객체의 categoryBitMask 값을 할당해주면 된다.

여기서 collisionBitMaskcontactTestBitMask의 차이를 풀어서 설명하자면 전자의 경우 지정한 객체와 물리적으로 충돌을 인식하게 되고(서로 겹치지 않고 튕겨져 나감) 후자는 객체끼리 만나도 collisionBitMask가 지정되어 있지 않으면 충돌하지 않고 겹쳐지며, 서로 만났다는 사실만 감지할 수 있다.

01) 카테고리 정의

카테고리를 정의하는 방법은 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를 사용하는 경우가 많아보였다.

02) 노드에 physicsBody 설정

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를 통해 공이 도착점에 도달한 시점을 알 수 있다. (물리 엔진은 SKScenephysicsWorld에 의해 전역적으로 동작하며, SKPhysicsContactDelegate 통해 접촉 이벤트를 받을 수 있음)

class GameScene: SKScene, SKPhysicsContactDelegate {
	// ... //
    
    override func didMove(to view: SKView) {
    	// ... //
        
        physicsWorld.contactDelegate = self
    }
    
    func didBegin(_ contact: SKPhysicsContact) {
    	// ~ contact 매개변수 활용하여 원하는 동작 구현 ~ //
    }
}
profile
iOS Junior Developer

0개의 댓글