Last active
December 30, 2025 05:24
-
-
Save AchrafKassioui/8b9ce3e0044639154c01bf9a579faa5f to your computer and use it in GitHub Desktop.
SpriteKit Physics Determinism
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
| /** | |
| # SpriteKit Physics Determinism | |
| Is SpriteKit's physics engine deterministic? | |
| In some cases, the physics-based behavior is quite repeatable. | |
| In other cases, the simulation yields a different outcome every run. | |
| This is a setup to explore such behaviors. | |
| Press one of the buttons to play a sequence. | |
| Explore further by modifying the methods called by the buttons. | |
| ## Links | |
| - https://stackoverflow.com/questions/38995939/spritekit-physics-giving-different-results-each-time | |
| - https://medium.com/element84/comparing-sprite-kit-physics-to-direct-box2d-5955b6653f71 | |
| Achraf Kassioui | |
| Created 29 Dec 2025 | |
| Updated 30 Dec 2025 | |
| */ | |
| import SpriteKit | |
| import SwiftUI | |
| // MARK: View | |
| struct SKDeterminismView: View { | |
| let scene = SKDeterminismScene() | |
| var body: some View { | |
| ZStack { | |
| SpriteView( | |
| scene: scene, | |
| preferredFramesPerSecond: 120, | |
| debugOptions: [.showsFPS, .showsNodeCount] | |
| ) | |
| .ignoresSafeArea() | |
| VStack { | |
| Spacer() | |
| HStack { | |
| Button("Bouncing Balls") { | |
| scene.dropBouncingBalls() | |
| } | |
| Button("Sequence of Balls") { | |
| scene.playSequenceOfBalls() | |
| } | |
| } | |
| .buttonStyle(.borderedProminent) | |
| } | |
| } | |
| .background(.black) | |
| } | |
| } | |
| #Preview { | |
| SKDeterminismView() | |
| } | |
| // MARK: Scene | |
| class SKDeterminismScene: SKScene { | |
| let ballname: String = "ball" | |
| // MARK: didMove | |
| override func didMove(to view: SKView) { | |
| view.contentMode = .center | |
| size = view.bounds.size | |
| scaleMode = .resizeFill | |
| backgroundColor = .darkGray | |
| anchorPoint = CGPoint(x: 0.5, y: 0.5) | |
| createGround() | |
| } | |
| // MARK: Cleanup | |
| override func willMove(from view: SKView) { | |
| removeAllActions() | |
| removeAllChildren() | |
| } | |
| private func reset() { | |
| removeAllActions() | |
| enumerateChildNodes(withName: ballname, using: { ball, _ in | |
| /// Remove the line below to keep the created balls | |
| /// Collision count will increase, and impredictable behavior will likely happen | |
| ball.removeFromParent() | |
| }) | |
| } | |
| // MARK: API | |
| /// This sequence seems remarkably stable run after run | |
| func playSequenceOfBalls() { | |
| reset() | |
| let waitDuration: TimeInterval = 0.3 | |
| let sequence1 = SKAction.sequence([ | |
| .wait(forDuration: waitDuration), | |
| .run { | |
| self.createCircle() | |
| }, | |
| ]) | |
| sequence1.timingMode = .linear | |
| run(SKAction.repeat(sequence1, count: 10)) | |
| } | |
| /// This sequence doesn't yield the same outcome every run | |
| func dropBouncingBalls() { | |
| reset() | |
| let sequence2 = SKAction.sequence([ | |
| .wait(forDuration: 0.1), | |
| .run { [weak self] in | |
| self?.createBouncingBalls() | |
| } | |
| ]) | |
| sequence2.timingMode = .linear | |
| run(SKAction.repeat(sequence2, count: 1)) | |
| } | |
| // MARK: Node Creation | |
| private func createCircle() { | |
| let circle = SKShapeNode(circleOfRadius: 18) | |
| circle.name = ballname | |
| circle.fillColor = .systemOrange | |
| circle.lineWidth = 0 | |
| circle.physicsBody = SKPhysicsBody(circleOfRadius: 18) | |
| circle.physicsBody?.restitution = 0.4 | |
| circle.physicsBody?.linearDamping = 0 | |
| circle.position = CGPoint(x: -10, y: 200) | |
| addChild(circle) | |
| } | |
| private func createBouncingBalls() { | |
| let circleCount = 5 | |
| let spacing: CGFloat = 60 | |
| let totalWidth = CGFloat(circleCount - 1) * spacing | |
| let startX = -totalWidth / 2 | |
| for i in 0..<circleCount { | |
| let circle = SKShapeNode(circleOfRadius: 18) | |
| circle.name = ballname | |
| circle.fillColor = .systemOrange | |
| circle.lineWidth = 0 | |
| circle.physicsBody = SKPhysicsBody(circleOfRadius: 18) | |
| circle.physicsBody?.restitution = 1 | |
| //circle.physicsBody?.linearDamping = 0 /// Toggle this line | |
| let x = startX + CGFloat(i) * spacing | |
| circle.position = CGPoint(x: x, y: 150) | |
| addChild(circle) | |
| } | |
| } | |
| private func createGround() { | |
| let groundWidth: CGFloat = 300 | |
| let ground = SKSpriteNode(color: .gray, size: CGSize(width: groundWidth, height: 10)) | |
| ground.physicsBody = SKPhysicsBody(rectangleOf: ground.size) | |
| ground.physicsBody?.isDynamic = false | |
| ground.position = CGPoint(x: 0, y: -300) | |
| addChild(ground) | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment