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
}
}
}
Related
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.
What I have been trying to do is create a "Joy stick" that moves a player around. Here is what I have so far:
import UIKit
import SpriteKit
import SceneKit
class GameViewController: UIViewController, SCNSceneRendererDelegate {
var isTracking = false
var firstTrackingLocation = CGPoint.zero
var trackingVelocity = CGPoint.zero
var trackingDistance : CGFloat = 0.0
var previousTime : NSTimeInterval = 0.0
override func viewDidLoad() {
super.viewDidLoad()
let scene = SCNScene(named: "art.scnassets/level.scn")!
let scnView = self.view as! SCNView
scnView.delegate = self
scnView.scene = scene
scnView.showsStatistics = true
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if isTracking == false {
for touch in touches {
isTracking = true
let location = touch.locationInView(self.view)
firstTrackingLocation = location
}
}
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
if isTracking {
trackingVelocity = touches.first!.locationInView(self.view)
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
isTracking = false
trackingVelocity = CGPoint.zero
}
func renderer(renderer: SCNSceneRenderer, updateAtTime time: NSTimeInterval) {
if isTracking == true {
let scnView = self.view as! SCNView
let character = scnView.scene!.rootNode.childNodeWithName("person", recursively: true)
let deltaTime = time - previousTime
let pointsPerSecond: CGFloat = 1.0 * CGFloat(deltaTime)
var xResult:CGFloat = 0.0
var yResult:CGFloat = 0.0
let point = firstTrackingLocation
let endPoint = trackingVelocity
let direction = CGPoint(x: endPoint.x - point.x, y: endPoint.y - point.y)
if direction.x > direction.y {
let movePerSecond = pointsPerSecond/direction.x
xResult = direction.x*movePerSecond
yResult = direction.y*movePerSecond
} else {
let movePerSecond = pointsPerSecond/direction.y
xResult = direction.x*movePerSecond
yResult = direction.y*movePerSecond
}
character!.position = SCNVector3(CGFloat(character!.position.x) + (xResult), CGFloat(character!.position.y), CGFloat(character!.position.z) + (yResult))
let camera = scnView.scene?.rootNode.childNodeWithName("camera", recursively: true)
camera?.position = SCNVector3(CGFloat(camera!.position.x) + (xResult), CGFloat(camera!.position.y), CGFloat(camera!.position.z) + (yResult))
}
previousTime = time
}
override func shouldAutorotate() -> Bool {
return true
}
override func prefersStatusBarHidden() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.Landscape
}
}
Now this works except if you drag your finger to the other side of the phone the character moves 10 times faster then it would if you barely moved your finger. So what I would like to have, is a Joy stick that moves the character the same speed if you drag a little bit or to the other side of the screen. And I would also like if you changed direction at the other side of the screen that the character would move the other way. So, my guess is that there needs to be a lastPoint saved then when the touchesMoved gets called that somehow we calculate a direction from lastPoint to the currentPoint and then move the character in renderer. I understand that most of this code is probably rubbish, but thanks in advance.
Your joystick should be a value from 0 to 1, you need to determine the radius of your joystick, then calculate distance of (point touched to center of control) and the angle of the control with arc tan.
Now we need to ensure we never go past maxRadius, so if our distance is > maxRadius, we just set it to max radius, then we divide this value by our maxRadius to get out distance ratio.
Then we just take the cos and sin of our angle, and multiply it by our distance ratio, and get the x and y ratio values. (Should be between 0 and 1)
Finally, take this x and y value, and multiply it to the speed at which your object should be moving at.
let maxRadius = 100 //Your allowable radius
let xDist = (p2.x - p1.x)
let yDist = (p2.y - p1.y)
let distance = (sqrt((xDist * xDist) + (yDist * yDist))
let angle = atan2(yDist , xDist )
let controlDistanceRatio = (distance > maxRadius) ? 1 : distance / maxRadius
let controllerX = cos(angle) * controlDistanceRatio
let controllerY = sin(angle) * controlDistanceRatio
This seems like a good case to use a custom UIGestureRecognizer. See Apple API Reference.
In this particular case you would create a continuous gesture. The resultant CGVector would be calculated from the center (origin) point of your joystick on screen. The recognizer would fail if the joystick node isn't selected and end if unselected (deselected). The resultant CGVector will be updated while the gesture's state is moved.
Now the tricky part to figure out would be moving the node image in such a way that allows the user to have the feeling of a joystick. For this you may need to update the node texture and make slight adjustments to the node position to give the appearance of moving a node around.
See if this helps you: Single Rotation Gesture Recognizer
Let me know if this points you in the right direction.
Your stated problem is the character moves at a variable rate depending on the how far from the start point the user drags their finger. The crucial point in the code seems to be this
let direction = CGPoint(x: endPoint.x - point.x, y: endPoint.y - point.y)
The difference between endPoint and point is variable and so you are getting a variable magnitude in your direction. To simplify, you could just put in a constant value like
let direction = CGPoint(x: 10, y: 10)
That gets the character moving at a constant speed when the user presses the joystick, but the character is always moving the same direction.
So somehow you've got to bracket in the variable directional values. The first thing that comes to mind is using min and max
let direction = CGPoint(x: endPoint.x - point.x, y: endPoint.y - point.y)
direction.x = min(direction.x, 10)
direction.x = max(direction.x, -10)
direction.y = min(direction.y, 10)
direction.y = max(direction.y, -10)
That seems like it would keep the magnitude of the direction values between -10 and 10. That doesn't make the speed completely constant, and it allows faster travel along diagonal lines than travel parallel to the x or y axis, but maybe it is closer to what you want.
Im looking to create a swift function to return the degrees (Float) of the users touch on an image (SKSpritenode). In touchesBegan, I know how to detect the x & y positions of my image. The idea is to create a function that takes in these positions and returns the degrees.
Amended - The following code now works:
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
/* Setup your scene here */
self.anchorPoint = CGPointMake(0.5, 0.5)
myNode.position = CGPointMake(0, -myNode.frame.height / 2)
self.addChild(myNode)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Called when a touch begins */
for touch in touches {
let location = touch.locationInNode(self)
if myNode.containsPoint(location) {
print("tapped!")
let origin = myNode.position
let touch = touch.locationInNode(myNode.parent!)
let diffX = touch.x - origin.x
let diffY = touch.y - origin.y
let radians = atan2(diffY, diffX)
let degrees = radians * CGFloat(180 / M_PI)
print("degrees = \(degrees)")
}
}
}
You need to compare the user's touch position to an origin point, which might be the centre of your sprite node for example. Here's some code to get you started:
let origin = CGPoint(x: 0, y: 0)
let touch = CGPoint(x: 100, y: 100)
let diffX = touch.x - origin.x
let diffY = touch.y - origin.y
let radians = atan2(diffY, diffX)
let degrees = radians * CGFloat(180 / M_PI)
That last value – degrees – is the one you want if you want to show users information. If you want to do more calculations, you should probably stick with radians.
I am trying to make a simple bubble tapped game. I have a created a SKSpriteKid node in my code. The problem is I want to sprite to disappear when user tap on it. I can't seem to removeparent() the node. How do I tackle this?
I have created a sprite with bubble1 as the name.
func bubblePressed(bubble1: SKSpriteNode) {
print("Tapped")
bubble1.removeFromParent()
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch: AnyObject in touches {
let touchLocation = touch.locationInNode(self)
let touchedNode = self.nodeAtPoint(touchLocation)
if (touchedNode.name == "bubble1") {
touchedNode.removeFromParent()
print("hit")
}
}
}
//the node's creation.
func bubblesinView() {
//create the sprite
let bubble1 = SKSpriteNode(imageNamed: "bubble_green.png")
//spawn in the X axis min is size, max is the total height minus size bubble
let width_bubble_1 = random(min: bubble1.size.width/2, max: size.width - bubble1.size.height/2)
//y: size.height * X, X is 0 at bottom page to max
//spawn position, let x be random
bubble1.position = CGPoint(x: width_bubble_1, y: size.height)
// Add the monster to the scene
addChild(bubble1)
// Determine speed of the monster from start to end
let bubblespeed = random(min: CGFloat(1.1), max: CGFloat(4.0))
//this tell the bubble to move to the given position, changeble x must be a random value
let moveAction = SKAction.moveTo(CGPoint(x: width_bubble_1, y: size.height * 0.1), duration: NSTimeInterval(bubblespeed))
let actionMoveDone = SKAction.removeFromParent()
bubble1.runAction(SKAction.sequence([moveAction, actionMoveDone]))
}
I am really trying to make this work. Thanks in advance!
If you want to use the name property of the node, you should set it to something. The runtime cannot figure out the variable name as the name. So in bubblesInView function after
let bubble1 = SKSpriteNode(imageNamed: "bubble_green.png")
you should do
bubble1.name = "bubble1"
You just forgot to name the node :
//create the sprite
let bubble1 = SKSpriteNode(imageNamed: "bubble_green.png")
bubble1.name = "bubble1"
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
}
}
}