Last active
August 28, 2025 02:44
-
-
Save AchrafKassioui/cecc4f84fffa9340f27125e56ee4df35 to your computer and use it in GitHub Desktop.
Code sample for manual camera setup in SpriteKit.
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
| /** | |
| Code sample for manual camera setup in SpriteKit. | |
| Achraf Kassioui | |
| Created 27 August 2025 | |
| Updated 28 August 2025 | |
| */ | |
| import SpriteKit | |
| import SwiftUI | |
| // MARK: Live Preview | |
| struct ManualCameraView: View { | |
| var body: some View { | |
| SpriteView( | |
| scene: ManualCameraScene() | |
| ) | |
| .ignoresSafeArea() | |
| } | |
| } | |
| #Preview { | |
| ManualCameraView() | |
| } | |
| // MARK: Layers | |
| enum SceneLayer: CGFloat, Codable, CaseIterable { | |
| case base = 0 | |
| case content = 10_000 | |
| case ui = 20_000 | |
| var isScalable: Bool { | |
| return [.content].contains(self) | |
| } | |
| } | |
| // MARK: Scene | |
| class ManualCameraScene: SKScene { | |
| var parentNodes: [SceneLayer: SKNode] = [:] | |
| let cameraNode = SKNode() | |
| // MARK: didMove | |
| override func didMove(to view: SKView) { | |
| size = view.bounds.size | |
| scaleMode = .resizeFill | |
| backgroundColor = .darkGray | |
| anchorPoint = CGPoint(x: 0.5, y: 0.5) | |
| createLayers() | |
| createCamera() | |
| createContent(view: view) | |
| animateCamera() | |
| } | |
| // MARK: Layer Nodes | |
| func createLayers() { | |
| for layer in SceneLayer.allCases { | |
| let node = SKNode() | |
| node.name = "\(layer) layer" | |
| addChild(node) | |
| node.zPosition = layer.rawValue | |
| parentNodes[layer] = node | |
| } | |
| } | |
| func parentNode(forLayer layer: SceneLayer) -> SKNode { | |
| guard let node = parentNodes[layer] else { | |
| print("Layer node for \(layer) not found. Returning the scene node instead.") | |
| return self | |
| } | |
| return node | |
| } | |
| // MARK: Camera | |
| func createCamera() { | |
| parentNode(forLayer: .base).addChild(cameraNode) | |
| } | |
| /// A sample animation that controls the camera node. | |
| /// We could use anything to control the camera, including physics. | |
| /// If the camera node is controlled with physics, make sure to reset its scale to 1 in update, | |
| /// then set it back to its scale value after the physics step. | |
| func animateCamera() { | |
| let action = SKAction.sequence([ | |
| .group([ | |
| .scale(to: 0.25, duration: 1), | |
| .move(to: CGPoint(x: 0, y: 0), duration: 1), | |
| ]), | |
| .wait(forDuration: 0.5), | |
| .group([ | |
| .scale(to: 1.5, duration: 1), | |
| .move(to: CGPoint(x: 0, y: 0), duration: 1), | |
| ]), | |
| .wait(forDuration: 0.5) | |
| ]) | |
| action.timingMode = .easeInEaseOut | |
| cameraNode.run(SKAction.repeatForever(action)) | |
| } | |
| // MARK: Content | |
| func createContent(view: SKView) { | |
| let UILabel = SKLabelNode(text: "UI Layer") | |
| UILabel.fontName = "Menlo-Bold" | |
| UILabel.fontSize = 24 | |
| UILabel.position = CGPoint(x: 0 , y: -view.bounds.height/2 + 100) | |
| parentNode(forLayer: .ui).addChild(UILabel) | |
| let contentSquare = SKShapeNode(rectOf: CGSize(width: 200, height: 100), cornerRadius: 4) | |
| contentSquare.fillColor = .systemYellow | |
| contentSquare.lineWidth = 3 | |
| contentSquare.strokeColor = .black | |
| parentNode(forLayer: .content).addChild(contentSquare) | |
| let contentLabel = SKLabelNode(text: "Scalable Layer") | |
| contentLabel.verticalAlignmentMode = .center | |
| contentLabel.fontName = "Menlo-Bold" | |
| contentLabel.fontSize = 16 | |
| contentLabel.fontColor = .black | |
| contentSquare.addChild(contentLabel) | |
| } | |
| // MARK: Loop | |
| override func update(_ currentTime: TimeInterval) { | |
| /// All nodes that scale and move with camera are reset, | |
| /// so physics and any other processing happen in scene coordinates. | |
| for layer in SceneLayer.allCases where layer.isScalable { | |
| parentNode(forLayer: layer).position = .zero | |
| parentNode(forLayer: layer).setScale(1) | |
| parentNode(forLayer: layer).zRotation = 0 | |
| } | |
| /// If the camera is controlled with physics, store its scale, and set its scale to 1 for now. | |
| /// ... run frame logic | |
| } | |
| override func didFinishUpdate() { | |
| /// Restore camera scale if needed. | |
| /// Apply the camera transforms to all scalable layers. | |
| for layer in SceneLayer.allCases where layer.isScalable { | |
| parentNode(forLayer: layer).position = cameraNode.position | |
| parentNode(forLayer: layer).scaleAsPoint = cameraNode.scaleAsPoint | |
| parentNode(forLayer: layer).zRotation = cameraNode.zRotation | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment