Sprite-Kit: moving an SKSpriteNode - ios

i know that it's not that hard to move an object, but mine is different, i tried various ways none of them worked..
What i want to achieve?
If the user taps the left side of the screen, the ball would go left
If the user taps the right side of the screen, the ball would go right
So i went straight to the touchesBegan and wrote the following:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
ball?.removeAllActions()
for touch in touches {
let moveBY = SKAction.moveTo(x: touch.location(in: view).x, duration: 1.0)
self.ball?.run(moveBY)
}
}
i tried four different ways, i couldn't get over it, also for your reference, here's a photo of my ball's
info:

In order to move a node in a direction you want, you first have to know three things
the anchorPoint of the node you want to move
the anchorPoint of the parent node of the node you want to move
the position of node you want to move
The anchorPoint property of the nodes store its values in normalised way from (0, 0) to (1, 1). Defaults to (0.5, 0.5) for SPSpriteNode
The position property of a node store its position as (x, y) coordinates in a ortogonal coordinate system in which the point (0, 0) is positioned where the anchor point of the PARENT is.
So your scene's anchorPoint is (0.5, 0.5) meaning if you place a direct child in it at coordinates (0, 0), the child will be positioned so that its anchorPoint is located at (0, 0) which will always match its parent's anchorPoint.
When you are using those overloads touchesBegan, touchesMoved, etc... inside SKScene or SKNodes as a general, if you want to get the position of touch represented in the coordinate system in which all direct children of scene(sknode) are situated, you should do something like
class GameScene: SKScene {
override func didMove(to view: SKView) {
super.didMove(to: view)
let rect = SKSpriteNode(color: .green, size: CGSize(width: 100, height: 100))
addChild(rect)
rect.name = "rect"
// don't pay attention to these two i need them because i dont have .sks file
size = CGSize(width: 1334, height: 750)
anchorPoint = CGPoint(x: 0.5, y: 0.5)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let moveAction = SKAction.move(to: touch.location(in: self), duration: 1)
childNode(withName: "rect")?.run(moveAction)
}
}
hope it was helpful, other problems that you may have is that your ball is not direct child of scene, and you get its coordinates wrong

Related

SKNode follow another SKNode within SKConstraint bounds

I am attempting to simulate an eye with SpriteKit.
The pupil of the eye tracks the users's finger as it moves across the screen, but must stay within the bounds of the eye.
I have attempted to solve this unsuccessfully with the use of SKConstraint.
Edit
My thought was to apply SKConstraints against the pupil to restrict its bounds to the eye. Any touches (i.e. touchesMoved(), etc) will be applied to the pupil in the form of of SKAction.moveTo() and SpriteKit handles maintaining the pupil within the eye bound.
let touchPoint = CGPoint()
SKAction.moveTo( touchPoint, duration: 2)
The code for the video is available: https://gist.github.com/anonymous/f2356e07d1ac0e67c25b1940662d72cb
A picture is worth a thousand words...
Imagine the pupil is the small, white, filled circle. The blue box simulates a user moving their finger across the screen.
Ideally, the pupil follows the blue box around the screen and follows the path as defined by the yellow circle.
iOS 10 | Swift 3 | Xcode 8
Instead of constraining by distance, you can use an orientation constraint to rotate a node to face toward the touch location. By adding the "pupil" node with an x offset to the node being constrained, the pupil will move toward the touch point. Here's an example of how to do that:
let follow = SKSpriteNode(color:.blue, size:CGSize(width:10,height:10))
let pupil = SKShapeNode(circleOfRadius: 10)
let container = SKNode()
let maxDistance:CGFloat = 25
override func didMove(to view: SKView) {
addChild(container)
// Add the pupil at an offset
pupil.position = CGPoint(x: maxDistance, y: 0)
container.addChild(pupil)
// Node that shows the touch point
addChild(follow)
// Add an orientation constraint to the container
let range = SKRange(constantValue: 0)
let constraint = SKConstraint.orient(to: follow, offset: range)
container.constraints = [constraint]
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches {
let location = t.location(in: self)
follow.position = location
adjustPupil(location: location)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches {
let location = t.location(in: self)
follow.position = location
adjustPupil(location: location)
}
}
// Adjust the position of the pupil within the iris
func adjustPupil(location:CGPoint) {
let dx = location.x - container.position.x
let dy = location.y - container.position.y
let distance = sqrt(dx*dx + dy*dy)
let x = min(distance, maxDistance)
pupil.position = CGPoint(x:x, y:0)
}
I wanted to wait for a response before I posted an answer, but #0x141E and I debated about how constraints work, so here is the code. Use it to find out where you are going wrong.
import SpriteKit
class GameScene: SKScene {
static let pupilRadius : CGFloat = 30
static let eyeRadius : CGFloat = 100
let follow = SKSpriteNode(color:.blue, size:CGSize(width:10,height:10))
let pupil = SKShapeNode(circleOfRadius: pupilRadius)
override func didMove(to view: SKView) {
let eye = SKShapeNode(circleOfRadius: GameScene.eyeRadius)
eye.strokeColor = .white
eye.fillColor = .white
addChild(eye)
pupil.position = CGPoint(x: 0, y: 0)
pupil.fillColor = .blue
eye.addChild(pupil)
addChild(follow)
pupil.constraints = [SKConstraint.distance(SKRange(lowerLimit: 0, upperLimit: GameScene.eyeRadius-GameScene.pupilRadius), to: eye)]
}
func moveFollowerAndPupil(_ location:CGPoint){
follow.position = location
pupil.position = convert(location, to: pupil.parent!)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
touches.forEach({moveFollowerAndPupil($0.location(in: self))})
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
touches.forEach({moveFollowerAndPupil($0.location(in: self))})
}
}
As you can see, no use of square root on my end, hopefully Apple is smart enough to not use it either since it is not needed, meaning this should in theory be faster than manually doing the distance formula.

Follow a path inverse

I'm making a sprite to orbit a point and for that I made a path that is followed by that sprite:
let dx1 = base1!.position.x - self.frame.width/2
let dy1 = base1!.position.y - self.frame.height/2
let rad1 = atan2(dy1, dx1)
path1 = UIBezierPath(arcCenter: circle!.position, radius: (circle?.position.y)! - 191.39840698242188, startAngle: rad1, endAngle: rad1 + CGFloat(M_PI * 4), clockwise: true)
let follow1 = SKAction.followPath(path1.CGPath, asOffset: false, orientToPath: true, speed: 200)
base1?.runAction(SKAction.repeatActionForever(follow1))
That works and the sprite starts to orbit around that point. The thing is that when the user touches the screen I want that sprite to start rotating counterclockwise. For that I write the same code for rotating clockwise but editing the last line to:
base1?.runAction(SKAction.repeatActionForever(follow1).reversedAction())
But the problem is that although it rotates counterclockwise, the sprite image flips horizontal. What can I do to avoid that? Or is there any other way to orbit a sprite through a point?
A straightforward way to rotate a sprite about a point (i.e., orbit) is to create a container node, add a sprite to the node at an offset (relative to the container's center), and rotate the container node. Here's an example of how to do that:
class GameScene: SKScene {
let sprite = SKSpriteNode(imageNamed:"Spaceship")
let node = SKNode()
override func didMove(to view: SKView) {
sprite.xScale = 0.125
sprite.yScale = 0.125
sprite.position = CGPoint (x:100, y:0)
node.addChild(sprite)
addChild(node)
let action = SKAction.rotate(byAngle:CGFloat.pi, duration:5)
node.run(SKAction.repeatForever(action), withKey:"orbit")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let action = node.action(forKey: "orbit") {
// Reverse the rotation direction
node.removeAction(forKey:"orbit")
node.run(SKAction.repeatForever(action.reversed()),withKey:"orbit")
// Flip the sprite vertically
sprite.yScale = -sprite.yScale
}
}
}

SKFieldNode with circular region only affecting bodies when center is within region

Ok so from the SKFieldNode documentation, under the region property:
A field node applies its effect to all physics bodies that are partially or completely inside its region. The default value is a region of infinite size.
From this I understand that once a part of the physics body attached to a node enters the SKRegion on the field node ("partially or complete inside the region"), the field will start acting on the body.
So I started using the fields but came across the fact that they only start acting on the physics body when the body's center is within the region, not any part of the body.
Here is the code I wrote:
import SpriteKit
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
backgroundColor = SKColor.whiteColor()
self.physicsWorld.gravity = CGVector(dx: 0, dy: 0)
let field = SKFieldNode.springField()
field.position = view.center
let shape = SKShapeNode(circleOfRadius: size.height / 3)
shape.fillColor = SKColor.grayColor()
field.region = SKRegion(path: shape.path!)
field.addChild(shape)
addChild(field)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Called when a touch begins */
for touch in touches {
let location = touch.locationInNode(self)
let ball = SKShapeNode(circleOfRadius: size.height / 10)
ball.position = location
ball.strokeColor = SKColor.blackColor()
ball.physicsBody = SKPhysicsBody(circleOfRadius: size.height / 10)
let center = SKShapeNode(circleOfRadius: 1)
center.strokeColor = SKColor.blackColor()
ball.addChild(center)
self.addChild(ball)
ball.physicsBody!.applyImpulse(CGVectorMake(60, 60))
}
}
}
(I added the shape node to the field to see where it`s region ends, and added the center to the ball to make it clear when it runs that the field only acts once the center is in the region.)
When I run it the field only starts acting on the balls when their center is within the region. It seems to me this is not what should happen based on the documentation. I haven't changed any of the field node's properties before adding it, so I expected the standard behavior.
I don't know it I am misinterpreting the documentation or there is some other property I have to set on the field for it to act on any physics body that is partially within its region.
A walkaround to let ball be effected by field when they touched immediately is to enlarge the region (radius) of field. Something like:
field.region = SKRegion(radius: shapeRadius + ballRadius)

SKPhysicsBody scale with parent scaling

I have a node, A, in a parent node, B.
I'm using setScale on node B to give the illusion of zooming, but when I do that, node A and all of its siblings do not have the expected physics bodies.
Even though A appears smaller and smaller, its physics body stays the same size. This leads to wacky behavior.
How can I "zoom out" and still have sensible physics?
SKPhysicsBody cannot be scaled. As a hack you could destroy the current physics body and add a smaller one as you scale up or down but unfortunately this is not a very efficient solution.
The SKCameraNode (added in iOS9) sounds perfect for your scenario. As opposed changing the scale of every node in the scene, you would change the scale of only the camera node.
Firstly, you need to add an SKCameraNode to your scene's hierarchy (this could all be done in the Sprite Kit editor too).
override func didMoveToView(view: SKView) {
super.didMoveToView(view)
let camera = SKCameraNode()
camera.position = CGPoint(x: size.width / 2, y: size.height / 2)
self.addChild(camera)
self.camera = camera
}
Secondly, since SKCameraNode is a node you can apply SKActions to it. For a zoom out effect you could do:
guard let camera = camera else { return }
let scale = SKAction.scaleBy(2, duration: 5)
camera.runAction(scale)
For more information have a look at the WWDC 2015 talk: What's New In Sprite Kit.
A simple example that shows that a physics body scales/behaves appropriately when its parent node is scaled. Tap to add sprites to the scene.
class GameScene: SKScene {
var toggle = false
var node = SKNode()
override func didMoveToView(view: SKView) {
self.physicsBody = SKPhysicsBody(edgeLoopFromRect: view.frame)
scaleMode = .ResizeFill
view.showsPhysics = true
addChild(node)
let shrink = SKAction.scaleBy(0.25, duration: 5)
let grow = SKAction.scaleBy(4.0, duration: 5)
let action = SKAction.sequence([shrink,grow])
node.runAction(SKAction.repeatActionForever(action))
}
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
/* Called when a touch begins */
for touch in (touches as! Set<UITouch>) {
let location = touch.locationInNode(node)
let sprite = SKSpriteNode(imageNamed:"Spaceship")
if (toggle) {
sprite.physicsBody = SKPhysicsBody(circleOfRadius: sprite.size.width/2.0)
}
else {
sprite.physicsBody = SKPhysicsBody(rectangleOfSize: sprite.size)
}
sprite.xScale = 0.125
sprite.yScale = 0.125
sprite.position = location
node.addChild(sprite)
toggle = !toggle
}
}
}

Locking scene to a sprite node swift xcode

Basically, I want to move the character (player) and have the “camera” or scene follow them in the world. The problem is Apple/xcode does not have a built in camera node and if I try to move the scene, I get an error saying something to the effect of “SKScene cannot be moved.” I don’t want to just move the background around and lock the player in place because I want to be able to apply forces to the player node. I have gone to Apple’s website to the Advanced Scene Processing page and followed the example but it doesn’t work. I just need a nudge in the right direction. Thanks!
Code:
(I have deleted pieces of code that are irrelevant)
class PlayScene: SKScene, SKPhysicsContactDelegate {
let world = SKNode()
let player = SKShapeNode(circleOfRadius: 25)
var cannonRotation = 0.0
var fired = false
override func didMoveToView(view: SKView) {
physicsWorld.contactDelegate = self
self.anchorPoint = CGPoint(x: 0.5, y: 0.5)
self.addChild(world)
//Adds the background to the world
//Adds the elements to the scene
player.fillColor = UIColor.whiteColor()
player.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMinY(self.frame)+65+25)
player.zPosition = 2
player.physicsBody = SKPhysicsBody(circleOfRadius: 25)
player.physicsBody?.dynamic = true
player.physicsBody?.restitution = 0.9
player.physicsBody?.categoryBitMask = BodyType.player.rawValue
//Adds the physics properties to GameScene
let physicsGround = SKPhysicsBody(edgeFromPoint: CGPoint(x: CGRectGetMinX(self.frame), y: CGRectGetMinY(self.frame)+65), toPoint: CGPoint(x: CGRectGetMaxX(self.frame), y: CGRectGetMinY(self.frame)+65))
self.physicsBody = physicsGround
}
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
powerLevel()
totalPower = powerReached * playCannon.powerMultiplier
self.addChild(player)
player.physicsBody?.applyImpulse(CGVector(dx: CGFloat(Float(totalPower)*cosf(Float(cannonRotation))), dy: CGFloat(Float(totalPower)*sinf(Float(cannonRotation)))))
fired = true
}
Override didsimulatePhysics, And in that function call this
self.centerOnNode(player);
What that does, It calles a function we will later create and it sendes a parameter of an SKNode and in your case it is the player node. Next create a method called centerOnNede and add the following parameters
func centerOnNode(node: SKNode) {
}
In the centerOnNode function add this following line of code.
var cameraPosition: CGPoint = node.scene?.convertPoint(node.position, fromNode: node.parent!)
cameraPosition.x = 0
let posX = node.parent?.position.x
let posY = node.parent?.position.y
node.parent?.position = CGPointMake(posX! - cameraPosition.x, posY! - cameraPosition.y)
That will center the camera on to the node and update it's position. Code converted from here. How to make camera follow SKNode in Sprite Kit?
There is a detailed example of how to do this in the Apple Adventure reference game. It has both Swift and Objective-C examples.
In summary what they recommend is that you
Put all nodes on a background node (SKNode) container.
implement didSimulatePhysics and move the world in the opposite direction of the player. That will keep the player in the middle of the screen. Special logic will be needed if you want the camera to bump into the edge of your world.
A camera node would be very helpful, but has not been implemented into SpriteKit.
As of iOS 9 there is now an SKCameraNode that does all the heavy lifting for you.
Simply add these three lines of code in your didMoveToView function:
let cameraNode = SKCameraNode()
self.addChild(cameraNode)
self.camera = cameraNode
Then in your didFinishUpdate function just set the cameraNode's position to whatever SKSpriteNode you want it to follow.

Resources