SKSpriteNode losing velocity upon collision - ios

I am making an iOS app and running into some problems with physics. As you can tell by the .GIF below, when I rotate the hexagon and the ball hits the rectangle at an angle, it loses some of its velocity and doesn't bounce as high. This is because of the reason shared here (basically because I am constraining the balls horizontal position, it's only using the vertical velocity when hitting an angle, thus losing speed).
I cannot for the life of me figure out a solution to fix this problem. Does anybody have any ideas??
Code for Ball node:
func createBallNode(ballColor: String) -> SKSpriteNode {
let ball = SKSpriteNode(imageNamed: ballColor)
ball.position = CGPoint(x: CGRectGetMidX(frame), y: CGRectGetMidY(frame)+30)
ball.zPosition = 1
ball.physicsBody = SKPhysicsBody(circleOfRadius: ball.size.width/2)
ball.physicsBody?.affectedByGravity = true
ball.physicsBody?.restitution = 1
ball.physicsBody?.linearDamping = 0
ball.physicsBody?.friction = 0
ball.physicsBody?.categoryBitMask = ColliderType.Ball.rawValue
ball.physicsBody?.contactTestBitMask = ColliderType.Rect.rawValue
ball.physicsBody?.collisionBitMask = ColliderType.Rect.rawValue
let centerX = ball.position.x
let range = SKRange(lowerLimit: centerX, upperLimit: centerX)
let constraint = SKConstraint.positionX(range)
ball.constraints = [constraint]
return ball
}

Yes, the problem is probably causes by the ball hitting the hexagon when it is not perfectly "aligned". In this case the ball loses vertical speed in favour of the horizontal axis.
Since you want a "discrete logic" I believe in this scenario your should avoid physics (at least for the bouncing part). It would be much easier repeating an SKAction that moves the ball vertically.
Example
I prepared a simple example
class GameScene: SKScene {
override func didMove(to view: SKView) {
super.didMove(to: view)
let ball = createBallNode()
self.addChild(ball)
}
func createBallNode() -> SKSpriteNode {
let ball = SKSpriteNode(imageNamed: "ball")
ball.position = CGPoint(x: frame.midX, y: frame.minY + ball.frame.height / 2)
let goUp = SKAction.move(by: CGVector(dx: 0, dy: 600), duration: 1)
goUp.timingMode = .easeOut
let goDown = SKAction.move(by: CGVector(dx: 0, dy: -600), duration: 1)
goDown.timingMode = .easeIn
let goUpAndDown = SKAction.sequence([goUp, goDown])
let forever = SKAction.repeatForever(goUpAndDown)
ball.run(forever)
return ball
}
}
Update
If you need to perform a check every time the ball touches the base of the Hexagon you can use this code
class GameScene: SKScene {
override func didMove(to view: SKView) {
super.didMove(to: view)
let ball = createBallNode()
self.addChild(ball)
}
func createBallNode() -> SKSpriteNode {
let ball = SKSpriteNode(imageNamed: "ball")
ball.position = CGPoint(x: frame.midX, y: frame.minY + ball.frame.height / 2)
let goUp = SKAction.move(by: CGVector(dx: 0, dy: 600), duration: 1)
goUp.timingMode = .easeOut
let goDown = SKAction.move(by: CGVector(dx: 0, dy: -600), duration: 1)
goDown.timingMode = .easeIn
let check = SKAction.customAction(withDuration: 0) { (node, elapsedTime) in
self.ballTouchesBase()
}
let goUpAndDown = SKAction.sequence([goUp, goDown, check])
let forever = SKAction.repeatForever(goUpAndDown)
ball.run(forever)
return ball
}
private func ballTouchesBase() {
print("The ball touched the base")
}
}
As you can see now the method ballTouchesBase is called every time the ball is a te lower y coordinate. This is the right place to add a check for the color of your hexagon.

Related

How do I applyImpulse at a certain angle in SpriteKit using Swift 5?

I am trying to throw a ball in an iOS game in the direction of touchEnded method. I have searched a number of threads but some are old and for the others I could not figure out the issue.
What I'm trying to do is as follows:
1- Add A ball as SKShapeNode - Set affectedByGravity to false.
2- On touchesEnded - I get the location CGPoint (somehow I get the y position reversed - I correct it).
3- I calculate the angle bearing using atan().
4- I calculate the CGVector dx and dy impulses based on the sin() and cos() of the angle.
5- I applyImpulse to the ball based on the CGVector above.
My issue is that the ball is not shot exactly to the touchesEnded direction. If the touchesEnded direction is vertically below the ball, then it works. The further away from vertical, the larger the deviation.
To help troubleshoot and visualize the issue, I added a line and end blue ball in the direction that the ball should follow. The red ball should ideally collide with the blue ball.
However, the real direction I get is as per the green ball added. The green ball should ideally be on the line.
I was trying to do exactly like what was done here but I don't get the results:
SpriteKit physics applyImpulse in direction
Appreciate your help.
'''
import SpriteKit
class GameScene: SKScene {
var sprite = SKShapeNode()
var radius: CGFloat = 10
var ballSpawnPosition: CGPoint = CGPoint(x: 0, y: 0)
override func didMove(to view: SKView) {
self.physicsBody = SKPhysicsBody(edgeLoopFrom: self.frame)
ballSpawnPosition = CGPoint(x: frame.midX, y: frame.maxY - 50)
spawnBall()
}
func spawnBall() {
sprite = SKShapeNode(circleOfRadius: radius)
sprite.physicsBody = SKPhysicsBody(circleOfRadius: radius)
sprite.physicsBody?.collisionBitMask = 0x1
sprite.fillColor = .red
sprite.position = ballSpawnPosition
addChild(sprite)
sprite.physicsBody?.affectedByGravity = false
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let position = touch.location(in: view)
let blueTestTouchPointBall = SKShapeNode(circleOfRadius: radius)
blueTestTouchPointBall.physicsBody = SKPhysicsBody(circleOfRadius: radius)
blueTestTouchPointBall.physicsBody?.categoryBitMask = 0x1
blueTestTouchPointBall.fillColor = .blue
let y = frame.size.height - position.y
blueTestTouchPointBall.position = CGPoint(x: position.x, y: y)
blueTestTouchPointBall.physicsBody?.affectedByGravity = false
addChild(blueTestTouchPointBall)
var path = CGMutablePath()
path.move(to: ballSpawnPosition)
path.addLine(to: CGPoint(x: position.x, y: y))
path.closeSubpath()
let line = SKShapeNode()
line.path = path
line.strokeColor = .white
line.lineWidth = 2
addChild(line)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first { //should not work if dragged.
let position = touch.location(in: view)
let dx = position.x - ballSpawnPosition.x
let dy = frame.size.height - abs( position.y - ballSpawnPosition.y)
let ang = atan(abs(dx)/abs(dy))
//let tang = sqrt(dx*dx + dy*dy)
let impulse: CGFloat = 15.0
let x_impulse = impulse * sin(ang) * dx/abs(dx)
let y_impulse = -1.0 * impulse * cos(ang)
// let x_impulse = impulse * dx/abs(dx) * dx/tang
// let y_impulse = impulse * -1 * dy/tang
sprite.physicsBody?.applyImpulse(CGVector(dx: x_impulse , dy: y_impulse ))
let greenTestBall = SKShapeNode(circleOfRadius: radius/2)
greenTestBall.fillColor = .green
greenTestBall.position = CGPoint(x: ballSpawnPosition.x + 300 * sin(ang) * dx/abs(dx), y: ballSpawnPosition.y - 300 * cos(ang) )
addChild(greenTestBall)
}
}
}
'''
You are mixing points in different coordinate systems. The location of the UITouch should be converted to the node in which the ball resides (typically the scene, but it could be another node) using the location(in:) method of UITouch and passing in the SKNode. See here for more on coordinate conversions in SpriteKit. Since the ball is a child of the scene, change
let position = touch.location(in: view)
to
let position = touch.location(in: self) // i.e. the `GameScene`
The angle calculation also needs to be adjusted supposing that you are using the ball spawn position as the reference point and the take the angle with respect to the vertical. Change
let dy = frame.size.height - abs(position.y - ballSpawnPosition.y)
to
let dy = abs(position.y - ballSpawnPosition.y)
Changing the above creates what I believe you are looking for. Note that momentum is additive: applying an impulse right while the ball is moving left will not necessarily cause the ball to start moving left (unless the impulse is great enough)

An SKSpriteNode not following another node properly

In my game scene I have a ball, a magnet and a platform called leveUnit as seen below
import SpriteKit
import GameplayKit
class GameScene: SKScene {
var ball = SKShapeNode()
var magnet = SKShapeNode()
let worldNode:SKNode = SKNode()
var levelUnit = SKSpriteNode()
override func didMove(to view: SKView) {
ball = SKShapeNode(circleOfRadius: 30)
ball.fillColor = .yellow
ball.position = CGPoint(x: 0, y: 200)
magnet = SKShapeNode(circleOfRadius: 30)
magnet.fillColor = .blue
magnet.position = CGPoint(x: 0, y: 0)
levelUnit = SKSpriteNode(imageNamed: "Wall")
levelUnit.position = CGPoint(x: 0, y: 250)
worldNode.addChild(levelUnit)
self.anchorPoint = CGPoint(x: 0.5, y: 0.5)
addChild(worldNode)
worldNode.addChild(magnet)
//addChild(ball)
levelUnit.addChild(ball)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch: UITouch = touches.first!
let location: CGPoint = touch.location(in: self)
let node: SKNode = self.atPoint(location)
if node == self {
magnet.position = location
}
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
let playerLocation:CGPoint = self.convert(magnet.position, from: worldNode)
let location = playerLocation
let nodeSpeed:CGFloat = 20.5
let node = ball
//Aim
let dx = location.x - (node.position.x)
let dy = location.y - (node.position.y)
let angle = atan2(dy, dx)
node.zRotation = angle
//Seek
let vx = cos(angle) * nodeSpeed
let vy = sin(angle) * nodeSpeed
node.position.x += vx
node.position.y += vy
}
}
My problem here is when I add the ball to the levelUnit instead of just to the view itself it doesn't seem to follow the magnet to its exact position it just seems to hover slightly above its position while when I have it added to the view itself it goes to the magnets exact location. I am using the solution from this question here to make the ball go to the magnet's location.
what am I doing wrong? and what is causing this to happen?

Rotate object under gravity force

I'm trying to create a scenario where there's a radial gravity field. In this scene, there's also an object built by two physics bodies with a different mass.
When I run this code, the radial gravity field is created correctly and the body goes to gravityCenter.
I'm expecting that the body rotates too because the head is heavier than the tail but this doesn't happend.
Why?
class GameScene: SKScene {
let object = SKSpriteNode(imageNamed: "myobj")
let myCategory : UInt32 = 0x1 << 0
override func didMove(to view: SKView) {
self.physicsWorld.gravity = CGVector(dx: 0, dy: 0)
let gravityCenter = SKFieldNode.radialGravityField()
gravityCenter.isEnabled = true
gravityCenter.position = CGPoint(x: size.width, y: size.height * 0.5)
gravityCenter.strength = 0.5
addChild(gravityCenter)
object.position = CGPoint(x: size.width * 0.1, y: size.height * 0.9)
object.scale(to: CGSize(width: 100, height: 25))
let head = SKPhysicsBody(circleOfRadius: object.size.width/5, center: CGPoint(x: object.size.width/2, y: 0))
let tail = SKPhysicsBody(circleOfRadius: object.size.width/50, center: CGPoint(x: -object.size.width/2, y: 0))
head.mass = 500
head.categoryBitMask = myCategory
head.allowsRotation = true
head.isDynamic = true
head.angularDamping = 0
head.affectedByGravity = true
tail.mass = 2
tail.categoryBitMask = myCategory
tail.allowsRotation = true
tail.isDynamic = true
tail.angularDamping = 0
tail.affectedByGravity = true
object.physicsBody = SKPhysicsBody(bodies: [head, tail])
object.physicsBody?.categoryBitMask = myCategory
object.physicsBody?.allowsRotation = true
object.physicsBody?.isDynamic = true
object.physicsBody?.angularDamping = 0
object.physicsBody?.affectedByGravity = true
addChild(object)
}
}
Well, from a physics standpoint SpriteKit is behaving correctly. If you think about it, more mass does mean more gravitational force, but it also means more inertia, which exactly cancels out the increased force. Perhaps introduce a little bit of linearDamping into the tail? That would get the body to rotate by making the head drag the tail a little bit.

Ball dropping animation not working (Swift)

This code is supposed to drop a ball from the top of the screen to the bottom. And once it touches the bottom of the screen, it should appear back to the top of the screen. It doesn't relocate to the top and it stops moving. I want it to be a continuous loop that resets the ball.y position every time it touches the bottom.
import SpriteKit
class GameScene: SKScene {
let ball = SKShapeNode(circleOfRadius: 20)
let label = SKLabelNode(fontNamed: "Futura")
let movingObjects = SKSpriteNode()
override func didMoveToView(view: SKView) {
let sceneBody = SKPhysicsBody(edgeLoopFromRect: self.frame)
self.physicsBody = sceneBody
//Ball Transition
let ballTransition = SKAction.sequence([SKAction.fadeInWithDuration(1)])
ball.runAction(ballTransition)
//Ball function
ball.fillColor = SKColor.redColor()
ball.physicsBody = SKPhysicsBody(circleOfRadius: 25)
ball.physicsBody?.affectedByGravity = false
//Ball Movement
ball.position = CGPoint(x: self.frame.size.width/2, y: CGFloat(self.frame.size.height*1))
ballMovement()
movingObjects.addChild(ball)
self.addChild(label)
}
func ballMovement() {
let moveBall = SKAction.moveToY(self.frame.size.height*0, duration: 3)
let removeBall = SKAction.removeFromParent()
let moveAndRemove = SKAction.sequence([moveBall, removeBall])
ball.runAction(moveAndRemove)
//Label Sprite
label.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
label.fontColor = SKColor.redColor()
label.fontSize = 30
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
label.text = "\(ball.position.y)"
if ball.position.y < 26 {
ball.position = CGPoint(x: self.frame.size.width/2, y: CGFloat(self.frame.size.height*1))
}
}
}
removing the ball from its parent within the action and still acting on it elsewhere is going to become very fragile as your program gets more complex. Why not just do it all with the actions and let sprite kit worry about it ?
let moveBall = SKAction.moveToY(0, duration: 3)
let goBackUp = SKAction.moveToY(self.frame.size.height, duration:0)
let keepFalling = SKAction.sequence([moveBall, goBackUp])
ball.runAction(SKAction.repeatActionForever(keepFalling))

Why is my camera not centered?

I've noticed that the camera property my SKScene is nil. I remedied that by creating a SKCameraNode setting that equal to the camera property, setting its position and adding it to the scene. However, when I do this all of a sudden my objects are no longer in the center of the scene. They are all shifted in some way that does not make sense to me. What am I doing wrong?
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
/*
// This is the problem code
let cam = SKCameraNode()
cam.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
self.camera = cam
self.addChild(cam)
*/
let sprite = SKSpriteNode(imageNamed:"Spaceship")
sprite.xScale = 0.5
sprite.yScale = 0.5
sprite.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
self.addChild(sprite)
}
}
Object appears in middle of scene, as expected.
Now the object is shifted to the left. Why?
UPDATE
The following code is giving me the behavior I want. The key is to reposition the camera every time the size of the SKScene changes. Thanks for the suggestions, they got me on the right track.
class GameScene: SKScene {
override func didChangeSize(oldSize: CGSize) {
if let view = self.view{
camera?.position = convertPointFromView(view.center)
}
}
override func didMoveToView(view: SKView) {
let cam = SKCameraNode()
cam.position = CGPoint(x: frame.midX, y:frame.midY)
self.camera = cam
self.addChild(cam)
let sprite = SKSpriteNode(imageNamed:"Spaceship")
sprite.xScale = 0.5
sprite.yScale = 0.5
sprite.position = CGPoint(x: frame.midX, y: frame.midY)
self.addChild(sprite)
}
}

Resources