Swinging Beam that Doesn't Stop Swinging in Swift SpriteKit - swift

I have a ios 9 spritekit game. I am adding a i-beam or a wrecking ball that should swing from a jointed rope like a pendulum. My game requires gravity and I want the beam to react to gravity and sprites that jump on the beam or hit the beam from below. When I use the following code, the beam eventually slows down and comes to rest without any interaction with other sprites. The top node in the jointed node is fixed (i.e., modeling attachment to a crane or building) I start the beam swinging by applying an impulse on the bottom node in the jointed node. I have set the friction, linear dampening, and angular dampening to 0.
What I need the beam to do prior to interacting with any sprites is to swing back and forth where the maximum height on the left swing and right swing is nearly the same throughout time. I want the beam or wrecking ball to act like its swinging from a frictionless pivot. The beam or ball doesn't go in a full circle, so I cannot use a constant angular velocity.
I tried something similar to:
Constant speed orbit around point with SKNode but neither the linear nor angular velocity is constant after the initial impulse as the beam or ball will arc up, slow down, stop at the top of the arc, and then circle back in the other direction.
let (actionSceneObjectNode, jointArray) = ActionSceneObject.createActionJointedSceneObjectAtPosition(CGPoint(x: positionX, y: positionY), ofType: actionSceneObject.type!, withImage: actionSceneObject.imageName, withActionDirection: actionSceneObject.imageDirection)
foregroundNode.addChild(actionSceneObjectNode)
if let bottomNode = actionSceneObjectNode.childNodeWithName("bottomObject") {
bottomNode.physicsBody?.applyImpulse(CGVector(dx: 50000.0, dy: 0))
}
// add the joints
for joint in jointArray {
self.physicsWorld.addJoint(joint)
}
Function
class func createActionJointedSceneObjectAtPosition(position: CGPoint, ofType type: ActionSceneObjectType, withImage imageName: String, withActionDirection actionDirection: DirectionValue) -> (ActionSceneObjectNode, [SKPhysicsJoint]) {
let node = ActionSceneObjectNode()
node.position = position
node.name = SceneObjectType.Action.rawValue
node.actionType = type
node.actionDirection = actionDirection
var jointArray = [SKPhysicsJoint]()
var sprite: SKSpriteNode
////////
// adapted from https://stackoverflow.com/questions/20811931/how-to-create-a-rope-in-spritekit
let countJointElements:Int = 3
let texture = SKTexture(imageNamed: "Rope.png")
//let textureSize = CGSize(width: texture.size().width*SceneObjectSizeScale.ActionSceneObject, height: texture.size().height*SceneObjectSizeScale.ActionSceneObject)
let topAnchor = SKSpriteNode(texture: texture, size: texture.size())
topAnchor.name = "topAnchor"
//topAnchor.position = CGPointMake(position.x, position.y) // the node holds the joint start position
topAnchor.physicsBody = SKPhysicsBody(rectangleOfSize: texture.size())
topAnchor.physicsBody?.categoryBitMask = PhysicsCategoryBitmask.None
topAnchor.physicsBody?.affectedByGravity = false
topAnchor.physicsBody?.friction = 0.0
topAnchor.physicsBody?.restitution = 1.0
topAnchor.physicsBody?.linearDamping = 0.0
topAnchor.physicsBody?.angularDamping = 0.0
topAnchor.physicsBody?.mass = 10.0
node.addChild(topAnchor)
// by default, the joints build top to bottom
for index in 0 ..< countJointElements {
let item = SKSpriteNode(texture: texture, size: texture.size())
item.name = "ropeitem_" + String(index)
item.position = CGPointMake(0, 0 - CGFloat(index+1) * item.size.height)
item.physicsBody = SKPhysicsBody(rectangleOfSize: texture.size())
item.physicsBody?.categoryBitMask = PhysicsCategoryBitmask.None
item.physicsBody?.affectedByGravity = true
item.physicsBody?.friction = 0.0
item.physicsBody?.restitution = 1.0
item.physicsBody?.linearDamping = 0.0
item.physicsBody?.angularDamping = 0.0
item.physicsBody?.mass = 10.0
node.addChild(item)
var bodyA = SKPhysicsBody()
if (index == 0)
{
bodyA = topAnchor.physicsBody!;
}
else
{
let nameString = "ropeitem_" + String(index - 1)
let nodeItem = node.childNodeWithName(nameString) as! SKSpriteNode
bodyA = nodeItem.physicsBody!
}
// needs to in terms of the physics world - the item position in the node is already negative
let joint = SKPhysicsJointPin.jointWithBodyA(bodyA, bodyB: item.physicsBody!, anchor: CGPointMake(position.x, position.y + item.position.y + item.size.height/2))
jointArray.append(joint)
}
let nameString = NSString(format: "ropeitem_%d", countJointElements - 1)
let lastLinkItem = node.childNodeWithName(nameString as String)
let bottomObject = SKSpriteNode(imageNamed: "I-Beam.png")
bottomObject.name = "bottomObject"
bottomObject.setScale(SceneObjectSizeScale.Platform)
bottomObject.position = CGPointMake(0, 0 + lastLinkItem!.position.y - lastLinkItem!.frame.size.height/2.0 - bottomObject.frame.size.height/2.0)
bottomObject.physicsBody = SKPhysicsBody(rectangleOfSize: bottomObject.size)
bottomObject.physicsBody?.categoryBitMask = PhysicsCategoryBitmask.Platform
bottomObject.physicsBody?.affectedByGravity = true
bottomObject.physicsBody?.friction = 0.0
//bottomObject.physicsBody?.restitution = 1.0
bottomObject.physicsBody?.linearDamping = 0.0
bottomObject.physicsBody?.angularDamping = 0.0
bottomObject.physicsBody?.mass = 500.0
node.addChild(bottomObject)
let jointLast = SKPhysicsJointFixed.jointWithBodyA(lastLinkItem!.physicsBody!, bodyB: bottomObject.physicsBody!, anchor: CGPointMake(position.x, position.y + bottomObject.position.y + bottomObject.frame.size.height/2.0))
jointArray.append(jointLast)
///////
///////
sprite = SKSpriteNode(imageNamed: imageName)
//sprite.setScale(SceneObjectSizeScale.ActionSceneObject)
node.sprite = sprite
//node.addChild(sprite)
node.physicsBody = SKPhysicsBody(texture: texture, size: sprite.size)
node.physicsBody!.categoryBitMask = PhysicsCategoryBitmask.None
switch actionDirection {
case .Left:
node.sprite.zRotation = CGFloat(M_PI_2)
case .Right:
node.sprite.zRotation = CGFloat(-M_PI_2)
case .Down:
node.sprite.zRotation = CGFloat(M_PI)
case .Up, .Unknown:
break
}
node.physicsBody?.dynamic = true
node.physicsBody?.restitution = 0.0 // bounciness
node.physicsBody?.categoryBitMask = PhysicsCategoryBitmask.ActionSceneObject
node.physicsBody?.collisionBitMask = 0
return (node, jointArray)
}

Related

Adding physics bodies to all tiles on an SKTileMapNode

I'm trying to make a simple game in the latest version of Xcode. I use a TileMap that is pretty simple. I'm trying to figure out how to add a physics body to each and every tile. I'm stuck at getting the position of each tile and adding an SKNode there. Any way at getting the position of each tile would be extremely helpful. All the code below is in didMove(to: view)
let tileSize = grassTileMap.tileSize
let halfWidth = CGFloat(grassTileMap.numberOfColumns) / 2.0 * tileSize.width
let halfHeight = CGFloat(grassTileMap.numberOfRows) / 2.0 * tileSize.height
for node in self.children {
if node.name == "grassTileMap" {
grassTileMap = node as! SKTileMapNode
}
for col in 0..<grassTileMap.numberOfColumns {
for row in 0..<grassTileMap.numberOfRows {
let tileDef = grassTileMap.tileDefinition(atColumn: col, row: row)
if tileDef == nil {
print("no tile here")
} else {
let hitboxTileNode = SKSpriteNode(color: UIColor.clear, size: CGSize(width: 35.5, height: 35.5))
// I need to set the position of each tile to hitBoxTileNode here
hitboxTileNode.anchorPoint = CGPoint(x: 0, y: 0)
hitboxTileNode.physicsBody = SKPhysicsBody(edgeLoopFrom: hitboxTileNode.frame)
hitboxTileNode.physicsBody?.affectedByGravity = false
hitboxTileNode.physicsBody?.isDynamic = false
hitboxTileNode.physicsBody?.pinned = false
hitboxTileNode.physicsBody?.restitution = 0
hitboxTileNode.physicsBody?.friction = 0
addChild(hitboxTileNode)
}
}
}
}

SpriteKit Inelastic Collision Reducing Velocity

I'm building a pong/breaker game with a ball and non-static blocks. I'd like the ball to never stop moving, but whenever it hits a block it loses velocity.
I have the restitusion = 1 for all sprites involved, I've tried setting the mass equal to each other and the density and the friction = 0. But, the ball still loses velocity on a bounce.
When the ball hits a block I'm removing it in the didBegin(contact:) function. I've also tried delaying the removal and it didn't help.
I'd like for the ball to have a constant velocity, but still be able to interact with the blocks as later I'd like to add blocks that can be hit without immediately being broken. So, the blocks can't be static but the ball needs to have a constant velocity.
My code for creating the ball node:
func ballNode(_ position: CGPoint?) -> SKSpriteNode {
let node = SKSpriteNode()
node.position = position == nil ? CGPoint(x: size.width/2, y: 100) : position!
node.size = CGSize(width: 17, height: 17)
//background
let background = SKShapeNode(circleOfRadius: 8.5)
background.fillColor = UIColor.white
node.addChild(background)
//physics
node.physicsBody = SKPhysicsBody(circleOfRadius: 8.5)
node.physicsBody?.allowsRotation = true
node.physicsBody?.friction = 0
node.physicsBody?.restitution = 1
node.physicsBody?.linearDamping = 0
node.physicsBody?.angularDamping = 0
node.physicsBody?.categoryBitMask = BallCategory
node.physicsBody?.contactTestBitMask = AddBlockBorderCategory | PaddleCategory
node.physicsBody?.collisionBitMask = PaddleCategory | BlockCategory | BorderCategory
return node
}
My code for creating the block node:
func createBlockNode() -> BlockNode {
let width = (size.width-CGFloat(6*layout[0].count))/CGFloat(layout[0].count)
let height = width*0.5
let nodeSize = CGSize(width: width, height: height)
let node = BlockNode(texture: nil, size: nodeSize)
let background = SKShapeNode(rectOf: nodeSize)
background.fillColor = .darkGray
background.strokeColor = .lightGray
//physics
node.physicsBody = SKPhysicsBody(rectangleOf: nodeSize)
node.physicsBody?.restitution = 1
node.physicsBody?.allowsRotation = true
node.physicsBody?.friction = 0
node.physicsBody?.categoryBitMask = BlockCategory
node.physicsBody?.contactTestBitMask = BallCategory
node.addChild(background)
return node
}
And a screen recording:
screen recording of the ball losing velocity
I'm starting the ball using this:
ball!.physicsBody?.applyForce(CGVector(dx: 0, dy: 50))

How do you pass through a Spritekit joint anchor?

I have tree that is built out of a series of joints. The anchor is the base/stump of the tree. My hero is currently not able to walk through the anchor. Setting collisionBitMask = 0 isn't working for the anchor but that approach does work for the individual joint segments.
So, essentially I just want to avoid this collision. Here is the code:
//code for anchor
let chunkHolder = SKSpriteNode(imageNamed: ImageName.ChunkHolder)
chunkHolder.position = anchorPoint
chunkHolder.physicsBody = SKPhysicsBody(circleOfRadius: chunkHolder.size.width / 2)
chunkHolder.physicsBody?.isDynamic = false
chunkHolder.physicsBody?.categoryBitMask = PhysicsCategory.chunkAnchor.rawValue
chunkHolder.physicsBody?.collisionBitMask = 0
addChild(chunkHolder)
//individual tree segements where hero correctly passes through:
for i in 0..<length {
let treeSegment = SKSpriteNode(imageNamed: ImageName.ChunkTexture)
let offset = treeSegment.size.height * CGFloat(i + 1)
treeSegment.position = CGPoint(x: anchorPoint.x, y: anchorPoint.y - offset)
treeSegment.name = "tree" + String(i)
treeSegments.append(treeSegment)
addChild(treeSegment)
treeSegment.physicsBody = SKPhysicsBody(rectangleOf: treeSegment.size)
treeSegment.physicsBody?.collisionBitMask = 0
}
//joints
for i in 1..<length {
let nodeA = treeSegments[i - 1]
let nodeB = treeSegments[i]
let joint = SKPhysicsJointPin.joint(withBodyA: nodeA.physicsBody!, bodyB: nodeB.physicsBody!, anchor: CGPoint(x: nodeA.frame.midX, y: nodeA.frame.minY))
scene.physicsWorld.add(joint)
}
//for reference here is Hero's physics body:
self.physicsBody?.categoryBitMask = PhysicsCategory.hero.rawValue
self.physicsBody?.contactTestBitMask = PhysicsCategory.ground.rawValue
Also setting hero's collisionBitMask to 0 will not currently help because then hero will also fall through the ground / floor.
This was resolved by also setting the categoryBitMask to 0.
chunkHolder.physicsBody?.categoryBitMask = 0
chunkHolder.physicsBody?.collisionBitMask = 0

SCNCamera limit arcball rotation

I have a scene setup with SCNCamera that rotates around an object.
What would be the best way to limit the extents of rotation the camera can achieve around the object?
Example: instead of being able to rotate around a whole sphere, how would I limit rotation to a single hemisphere?
My first attempt was to see if there was any clamps for .allowsCameraControl. Could not find anything.
I then tried adapting c# Unity : mouse orbit script, no luck.
Some pointers on how to approach or solve this would great.
Boilerplate Arcball thanks to this answer.
var lastWidthRatio: Float = 0
var lastHeightRatio: Float = 0
let camera = SCNCamera()
let cameraNode = SCNNode()
let cameraOrbit = SCNNode()
override func viewDidLoad() {
super.viewDidLoad()
// create a new scene
let scene = SCNScene(named: "art.scnassets/ship.scn")!
// create and add a camera to the scene
camera.usesOrthographicProjection = true
camera.orthographicScale = 9
camera.zNear = 0
camera.zFar = 100
cameraNode.position = SCNVector3(x: 0, y: 0, z: 50)
cameraNode.camera = camera
cameraOrbit.addChildNode(cameraNode)
scene.rootNode.addChildNode(cameraOrbit)
// retrieve the ship node
let ship = scene.rootNode.childNodeWithName("ship", recursively: true)!
// retrieve the SCNView
let scnView = self.view as! SCNView
// set the scene to the view
scnView.scene = scene
// add a tap gesture recognizer
let gesture = UIPanGestureRecognizer(target: self, action: "panDetected:");
scnView.addGestureRecognizer(gesture);
}
func panDetected(sender: UIPanGestureRecognizer) {
let translation = sender.translationInView(sender.view!)
let widthRatio = Float(translation.x) / Float(sender.view!.frame.size.width) + lastWidthRatio
let heightRatio = Float(translation.y) / Float(sender.view!.frame.size.height) + lastHeightRatio
self.cameraOrbit.eulerAngles.y = Float(-2 * M_PI) * widthRatio
self.cameraOrbit.eulerAngles.x = Float(-M_PI) * heightRatio
print(Float(-2 * M_PI) * widthRatio)
if (sender.state == .Ended) {
lastWidthRatio = widthRatio % 1
lastHeightRatio = heightRatio % 1
}
}
Maybe this could be useful for readers.
class GameViewController: UIViewController {
var cameraOrbit = SCNNode()
let cameraNode = SCNNode()
let camera = SCNCamera()
//HANDLE PAN CAMERA
var lastWidthRatio: Float = 0
var lastHeightRatio: Float = 0.2
var WidthRatio: Float = 0
var HeightRatio: Float = 0.2
var fingersNeededToPan = 1
var maxWidthRatioRight: Float = 0.2
var maxWidthRatioLeft: Float = -0.2
var maxHeightRatioXDown: Float = 0.02
var maxHeightRatioXUp: Float = 0.4
//HANDLE PINCH CAMERA
var pinchAttenuation = 20.0 //1.0: very fast ---- 100.0 very slow
var lastFingersNumber = 0
override func viewDidLoad() {
super.viewDidLoad()
// create a new scene
let scene = SCNScene(named: "art.scnassets/ship.scn")!
// create and add a light to the scene
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light!.type = SCNLightTypeOmni
lightNode.position = SCNVector3(x: 0, y: 10, z: 10)
scene.rootNode.addChildNode(lightNode)
// create and add an ambient light to the scene
let ambientLightNode = SCNNode()
ambientLightNode.light = SCNLight()
ambientLightNode.light!.type = SCNLightTypeAmbient
ambientLightNode.light!.color = UIColor.darkGrayColor()
scene.rootNode.addChildNode(ambientLightNode)
//Create a camera like Rickster said
camera.usesOrthographicProjection = true
camera.orthographicScale = 9
camera.zNear = 1
camera.zFar = 100
cameraNode.position = SCNVector3(x: 0, y: 0, z: 50)
cameraNode.camera = camera
cameraOrbit = SCNNode()
cameraOrbit.addChildNode(cameraNode)
scene.rootNode.addChildNode(cameraOrbit)
//initial camera setup
self.cameraOrbit.eulerAngles.y = Float(-2 * M_PI) * lastWidthRatio
self.cameraOrbit.eulerAngles.x = Float(-M_PI) * lastHeightRatio
// retrieve the SCNView
let scnView = self.view as! SCNView
// set the scene to the view
scnView.scene = scene
//allows the user to manipulate the camera
scnView.allowsCameraControl = false //not needed
// add a tap gesture recognizer
let panGesture = UIPanGestureRecognizer(target: self, action: "handlePan:")
scnView.addGestureRecognizer(panGesture)
// add a pinch gesture recognizer
let pinchGesture = UIPinchGestureRecognizer(target: self, action: "handlePinch:")
scnView.addGestureRecognizer(pinchGesture)
}
func handlePan(gestureRecognize: UIPanGestureRecognizer) {
let numberOfTouches = gestureRecognize.numberOfTouches()
let translation = gestureRecognize.translationInView(gestureRecognize.view!)
if (numberOfTouches==fingersNeededToPan) {
widthRatio = Float(translation.x) / Float(gestureRecognize.view!.frame.size.width) + lastWidthRatio
heightRatio = Float(translation.y) / Float(gestureRecognize.view!.frame.size.height) + lastHeightRatio
// HEIGHT constraints
if (heightRatio >= maxHeightRatioXUp ) {
heightRatio = maxHeightRatioXUp
}
if (heightRatio <= maxHeightRatioXDown ) {
heightRatio = maxHeightRatioXDown
}
// WIDTH constraints
if(widthRatio >= maxWidthRatioRight) {
widthRatio = maxWidthRatioRight
}
if(widthRatio <= maxWidthRatioLeft) {
widthRatio = maxWidthRatioLeft
}
self.cameraOrbit.eulerAngles.y = Float(-2 * M_PI) * widthRatio
self.cameraOrbit.eulerAngles.x = Float(-M_PI) * heightRatio
print("Height: \(round(heightRatio*100))")
print("Width: \(round(widthRatio*100))")
//for final check on fingers number
lastFingersNumber = fingersNeededToPan
}
lastFingersNumber = (numberOfTouches>0 ? numberOfTouches : lastFingersNumber)
if (gestureRecognize.state == .Ended && lastFingersNumber==fingersNeededToPan) {
lastWidthRatio = widthRatio
lastHeightRatio = heightRatio
print("Pan with \(lastFingersNumber) finger\(lastFingersNumber>1 ? "s" : "")")
}
}
func handlePinch(gestureRecognize: UIPinchGestureRecognizer) {
let pinchVelocity = Double.init(gestureRecognize.velocity)
//print("PinchVelocity \(pinchVelocity)")
camera.orthographicScale -= (pinchVelocity/pinchAttenuation)
if camera.orthographicScale <= 0.5 {
camera.orthographicScale = 0.5
}
if camera.orthographicScale >= 10.0 {
camera.orthographicScale = 10.0
}
}
It looks like you're almost there, using just the #Rickster code from
the answer you cited.
The change you could make would be in these lines:
self.cameraOrbit.eulerAngles.y = Float(-2 * M_PI) * widthRatio
self.cameraOrbit.eulerAngles.x = Float(-M_PI) * heightRatio
which implicitly allow pitch and yaw to cover the entire
sphere. That's where you can do your limiting. For instance,
instead of allowing the pitch (eulerAngles.x) to vary from 0
to -π, you could do
self.cameraOrbit.eulerAngles.x = Float(-M_PI_2) + Float(-M_PI_2) * heightRatio
to vary smoothly between -π/2 and -π, using full screen
vertical scrolling to cover that range. Or you could put
hard min/max limits/checks in those two lines to constrain
to a particular area of the globe.
(Edit to address the inertia comment)
For rotational damping, or inertia, I'd approach it by using the built in SceneKit Physics, and perhaps put the camera on an invisible (no geometry) SCNNode. That camera node becomes a gimbal, similar to the approach taken in this project: An interactive seven-foot globe created entirely in RubyMotion and SceneKit.
The virtual gimbal then gets an SCNPhysicsBody (you add that, it doesn't come with one by default) with some damping. Or perhaps you put the physics on your central object, and give that object some angularDamping.

Swift SpriteKit SKPhysicsJointPin

I'm trying to implement a rope in Swift SpriteKit and to add physics to it, the position is good for all, but they won't attach, when I hit play they all fall except the first one which is the "holder". Here is my code:
// create rope holder
let chainHolder = SKSpriteNode(imageNamed: "chainHolder")
chainHolder.position.y = self.frame.maxY - chainHolder.size.height
chainHolder.physicsBody = SKPhysicsBody(circleOfRadius: chainHolder.size.width / 2)
chainHolder.physicsBody?.dynamic = false
//chainHolder.physicsBody?.allowsRotation = true
chains.append(chainHolder)
addChild(chainHolder)
// add each of the rope parts
for i in 0...5 {
let chainRing = SKSpriteNode(imageNamed: "chainRing")
let offset = chainRing.size.height * CGFloat(i + 1)
chainRing.position = CGPointMake(chainHolder.position.x, chainHolder.position.y - offset)
chainRing.name = String(i)
chainRing.physicsBody = SKPhysicsBody(rectangleOfSize: chainRing.size)
//chainRing.physicsBody?.allowsRotation = true
chains.append(chainRing)
addChild(chainRing)
}
// set up joints between rope parts
for i in 1...5 {
var nodeA = chains[i - 1]
var nodeB = chains[i]
var joint = SKPhysicsJointPin.jointWithBodyA(nodeA.physicsBody, bodyB: nodeB.physicsBody,
anchor: CGPointMake(CGRectGetMidX(nodeA.frame), CGRectGetMinY(nodeA.frame)))
physicsWorld.addJoint(joint)
}
I found out that the problem was because I was setting the anchor point for the scene in (0.5, 0.5). If I leave it in (0, 0) everything is ok.