Last active
May 29, 2024 02:03
-
-
Save AchrafKassioui/efd92aa28c46be10620dc0ab98c9284e to your computer and use it in GitHub Desktop.
A SpriteKit scene to investigate how physics joints work across various camera scales.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
# Physics Joints and Camera | |
A scene to investigate how physics joints work across various camera scales. | |
Achraf Kassioui | |
Created: 28 May 2024 | |
Updated: 29 May 2024 | |
*/ | |
import SwiftUI | |
import SpriteKit | |
struct JointsWithCameraView: View { | |
var scene = JointsWithCameraScene() | |
@State var showPhysics = false | |
var body: some View { | |
VStack(spacing: 0) { | |
SpriteView( | |
scene: scene, | |
options: [.ignoresSiblingOrder, .shouldCullNonVisibleNodes] | |
) | |
.ignoresSafeArea() | |
VStack { | |
menuBar() | |
} | |
} | |
.background(.black) | |
} | |
private func menuBar() -> some View { | |
HStack (spacing: 2) { | |
Spacer() | |
/// Zoom Button 1 | |
Button(action: { | |
scene.setCameraScale(2) | |
}, label: { | |
Text("50%") | |
}) | |
.buttonStyle(.borderedProminent) | |
/// Zoom Button 2 | |
Button(action: { | |
scene.setCameraScale(1) | |
}, label: { | |
Text("100%") | |
}) | |
.buttonStyle(.borderedProminent) | |
/// Zoom Button 3 | |
Button(action: { | |
scene.setCameraScale(0.5) | |
}, label: { | |
Text("200%") | |
}) | |
.buttonStyle(.borderedProminent) | |
Spacer() | |
/// Debug Button | |
Button(action: { | |
showPhysics.toggle() | |
scene.toggleDebugView(showPhysics) | |
}, label: { | |
Text(showPhysics ? "Hide Physics" : "Show Physics") | |
}) | |
.buttonStyle(.borderedProminent) | |
Spacer() | |
} | |
.padding([.top, .leading, .trailing], 10) | |
} | |
} | |
#Preview { | |
JointsWithCameraView() | |
} | |
class JointsWithCameraScene: SKScene { | |
override func didMove(to view: SKView) { | |
size = view.bounds.size | |
backgroundColor = .darkGray | |
scaleMode = .resizeFill | |
view.isMultipleTouchEnabled = true | |
createCamera() | |
createSceneLayers() | |
createZoomLabel(view: view, parent: uiLayer) | |
createPhysicalBoundaryForUIBodies(view: view, parent: uiLayer) | |
createObjectWithJoints(parent: uiLayer) | |
createSceneObjects(parent: objectsLayer) | |
} | |
// MARK: - Variables | |
let uiLayer = SKNode() | |
let objectsLayer = SKNode() | |
let zoomLabel = SKLabelNode() | |
// MARK: - Scene Setup | |
func createSceneLayers() { | |
guard let camera = self.camera else { | |
print("No camera in scene") | |
return | |
} | |
uiLayer.zPosition = 9999 | |
camera.addChild(uiLayer) | |
objectsLayer.zPosition = 1 | |
self.addChild(objectsLayer) | |
} | |
func toggleDebugView(_ show: Bool) { | |
guard let view = self.view else { return } | |
view.showsFPS = show | |
view.showsPhysics = show | |
view.showsNodeCount = show | |
view.showsDrawCount = show | |
view.showsFields = show | |
view.showsQuadCount = show | |
} | |
// MARK: - Camera | |
func createZoomLabel(view: SKView, parent: SKNode) { | |
zoomLabel.fontName = "Menlo-Bold" | |
zoomLabel.fontSize = 16 | |
zoomLabel.fontColor = .white | |
zoomLabel.horizontalAlignmentMode = .center | |
zoomLabel.verticalAlignmentMode = .center | |
zoomLabel.position.y = view.bounds.height/2 - zoomLabel.calculateAccumulatedFrame().height/2 - view.safeAreaInsets.top - 20 | |
parent.addChild(zoomLabel) | |
} | |
func updateZoomLabel() { | |
if let camera = self.camera { | |
let zoomPercentage = 100 / (camera.xScale) | |
zoomLabel.text = String(format: "Zoom: %.0f%%", zoomPercentage) | |
} | |
} | |
func setCameraScale(_ scale: CGFloat) { | |
if let camera = self.camera { | |
let zoomAction = SKAction.scale(to: scale, duration: 0.2) | |
zoomAction.timingMode = .easeInEaseOut | |
camera.run(zoomAction) | |
} | |
} | |
/** | |
# Uncomment this if you want to use InertialCamera | |
Inertial Camera allows camera control with gestures. | |
Requires the InertialCamera class to be added to the project. | |
*/ | |
/* | |
func createInertialCamera(scene: SKScene) { | |
let inertialCamera = InertialCamera(scene: scene) | |
inertialCamera.lockRotation = true | |
inertialCamera.minScale = 0.2 | |
inertialCamera.maxScale = 20 | |
scene.camera = inertialCamera | |
scene.addChild(inertialCamera) | |
} | |
*/ | |
func createCamera() { | |
let myCamera = SKCameraNode() | |
self.camera = myCamera | |
addChild(myCamera) | |
} | |
// MARK: - UI Physics | |
struct PhysicsBitMasks { | |
static let sceneBody: UInt32 = 0x1 << 0 | |
static let sceneField: UInt32 = 0x1 << 1 | |
static let sceneParticle: UInt32 = 0x1 << 2 | |
static let sceneParticleCollider: UInt32 = 0x1 << 3 | |
static let sceneBoundary: UInt32 = 0x1 << 4 | |
static let uiBody: UInt32 = 0x1 << 5 | |
static let uiField: UInt32 = 0x1 << 6 | |
static let uiParticle: UInt32 = 0x1 << 7 | |
static let uiBoundary: UInt32 = 0x1 << 8 | |
} | |
func createPhysicalBoundaryForUIBodies(view: SKView, parent: SKNode) { | |
let margin: CGFloat = 0 | |
let uiArea = CGRect( | |
x: -view.bounds.width/2 - margin/2, | |
y: -view.bounds.height/2 - margin/2, | |
width: view.bounds.width - view.safeAreaInsets.left - view.safeAreaInsets.right + margin, | |
height: view.bounds.height - view.safeAreaInsets.top - view.safeAreaInsets.bottom + margin | |
) | |
let uiFrame = SKShapeNode(rect: uiArea) | |
uiFrame.alpha = 0 | |
uiFrame.physicsBody = SKPhysicsBody(edgeLoopFrom: uiArea) | |
uiFrame.physicsBody?.categoryBitMask = PhysicsBitMasks.uiBoundary | |
uiFrame.physicsBody?.collisionBitMask = PhysicsBitMasks.uiBody | |
uiFrame.physicsBody?.fieldBitMask = 0 | |
uiFrame.physicsBody?.restitution = 0 | |
uiFrame.physicsBody?.friction = 0 | |
parent.addChild(uiFrame) | |
} | |
// MARK: - UI Objects | |
func createObjectWithJoints(parent: SKNode) { | |
/// The visible sprite | |
let rectangle = SKSpriteNode(color: .systemYellow, size: CGSize(width: 140, height: 60)) | |
rectangle.physicsBody = SKPhysicsBody(rectangleOf: rectangle.size) | |
rectangle.physicsBody?.categoryBitMask = PhysicsBitMasks.uiBody | |
rectangle.physicsBody?.collisionBitMask = PhysicsBitMasks.uiBoundary | PhysicsBitMasks.uiBody | |
rectangle.physicsBody?.fieldBitMask = PhysicsBitMasks.uiField | |
rectangle.physicsBody?.affectedByGravity = true | |
rectangle.physicsBody?.allowsRotation = true | |
rectangle.physicsBody?.linearDamping = 1 | |
rectangle.physicsBody?.charge = 1 | |
rectangle.position = CGPoint(x: 0, y: -200) | |
parent.addChild(rectangle) | |
/// The first anchor point | |
let anchorPoint1 = SKNode() | |
anchorPoint1.position = CGPoint(x: -50, y: 100) | |
anchorPoint1.physicsBody = SKPhysicsBody(circleOfRadius: 1) | |
anchorPoint1.physicsBody?.isDynamic = false | |
parent.addChild(anchorPoint1) | |
/// The second anchor point | |
let anchorPoint2 = SKNode() | |
anchorPoint2.position = CGPoint(x: 50, y: 200) | |
anchorPoint2.physicsBody = SKPhysicsBody(circleOfRadius: 1) | |
anchorPoint2.physicsBody?.isDynamic = false | |
parent.addChild(anchorPoint2) | |
/// Spring joints | |
let springJoint1 = SKPhysicsJointSpring.joint( | |
withBodyA: rectangle.physicsBody!, | |
bodyB: anchorPoint1.physicsBody!, | |
anchorA: rectangle.position, | |
anchorB: anchorPoint1.position | |
) | |
springJoint1.frequency = 1 | |
springJoint1.damping = 0 | |
let springJoint2 = SKPhysicsJointSpring.joint( | |
withBodyA: rectangle.physicsBody!, | |
bodyB: anchorPoint2.physicsBody!, | |
anchorA: rectangle.position, | |
anchorB: anchorPoint2.position | |
) | |
springJoint2.frequency = 1 | |
springJoint2.damping = 0 | |
/** | |
# Uncomment/comment this to toggle the Spring Joints | |
*/ | |
physicsWorld.add(springJoint1) | |
physicsWorld.add(springJoint2) | |
/// Pin joints | |
let pinJoint1 = SKPhysicsJointPin.joint( | |
withBodyA: rectangle.physicsBody!, | |
bodyB: anchorPoint1.physicsBody!, | |
anchor: anchorPoint1.position | |
) | |
pinJoint1.frictionTorque = 0 | |
pinJoint1.rotationSpeed = 0 | |
let pinJoint2 = SKPhysicsJointPin.joint( | |
withBodyA: rectangle.physicsBody!, | |
bodyB: anchorPoint2.physicsBody!, | |
anchor: anchorPoint2.position | |
) | |
pinJoint2.frictionTorque = 0 | |
pinJoint2.rotationSpeed = 0 | |
/** | |
# Uncomment/comment this to toggle the Pin Joints | |
*/ | |
//physicsWorld.add(pinJoint1) | |
//physicsWorld.add(pinJoint2) | |
} | |
// MARK: - Scene Objects | |
func createSceneObjects(parent: SKNode) { | |
let sprite0 = SKSpriteNode(color: SKColor(white: 0, alpha: 0.1), size: CGSize(width: 800, height: 800)) | |
parent.addChild(sprite0) | |
let sprite = SKSpriteNode(color: SKColor(white: 0, alpha: 0.2), size: CGSize(width: 400, height: 400)) | |
parent.addChild(sprite) | |
let sprite2 = SKSpriteNode(color: SKColor(white: 0, alpha: 0.3), size: CGSize(width: 100, height: 100)) | |
parent.addChild(sprite2) | |
} | |
// MARK: - Update | |
override func update(_ currentTime: TimeInterval) { | |
updateZoomLabel() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Here is a video of how spring joints attached to the camera behave when the camera is scaled:
SpriteKit.-.Joints.with.Camera.mp4