Why do I need to adjust after convert point? - swift

My scene is setup scene > world: sknode > camera: sknode > ship: SKSpriteNode
I want the ship to point to the location of the touch:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touchLocation = touch.locationInNode(self)
let location = cameraNode.convertPoint(touchLocation, toNode: world)
moveCameraNode(CGPointsToVector(location, b: cameraNode.position))
let shipTouchPosition = scene?.convertPoint(touchLocation, fromNode: world)
let shipPosition = scene?.convertPoint(ship.position, fromNode: world)
let angle = atan2(shipTouchPosition!.y - shipPosition!.y, shipTouchPosition!.x - shipPosition!.x)
let magicNumber = CGFloat(M_PI_2)
ship.runAction(SKAction.rotateToAngle(angle - magicNumber, duration: 0.1, shortestUnitArc: true))
}
override func didFinishUpdate() {
centerOnNode(cameraNode)
}
func centerOnNode(node: SKNode) {
let cameraPositionInScene = node.scene?.convertPoint(node.position, fromNode: node.parent!)
node.parent!.position = CGPointsSubtract(node.parent!.position, b: cameraPositionInScene!)
}
So, it now works ok but I'm not sure why I need the magicNumber.
EDIT: Thanks to #mundi for catching a stupid error on my part which was obscuring my real question of what I'm doing wrong with convertPoint that I have to adjust it afterwards.

Angles are calculated in radians. 360 degrees are 2 times the constant PI, or 2 * M_PI. In SpriteKit, angle calculations start from center right (equivalent to 3 o'clock), corresponding to 90 degrees or one half PI, M_PI_2.
80 read as radians evaluates to 1.46018 which is very close to M_PI_2 or 1.57079. This is why this number appears to "magically" fix this discrepancy, even though it is slightly off. The true "magic" number is CGFloat(M_PI_2).

Related

How to move an SKNode?

I'm writing a Pong-like game using SpriteKit and Swift 5 for learning purpose. And I'm trying to made the player's paddle (an SKNode rectangle) move across the screen in the x coordinate but couldn't.
I've tried this simple solution:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches {
let location = t.location(in: self)
playerPaddle.position.x = location.x
}
It works but seems like the paddle only moves on clicks (touches), but not drags.
Also I've tried using the UIPanGestureRecognizer, but I don't really understand how to call it, here is the code which I copied from the official Apple Developer's doc and changed a little to match my situation:
#IBAction func panPiece(_ gestureRecognizer : UIPanGestureRecognizer) {
guard gestureRecognizer.view != nil else {return}
let piece = gestureRecognizer.view!
let translation = gestureRecognizer.translation(in: piece.superview)
if gestureRecognizer.state == .began {
self.initialPos = piece.center
}
else if gestureRecognizer.state != .changed {
let newPos = CGPoint(x: initialPos.x + translation.x, y: initialPos.y)
piece.center = newPos
}
}
Your touchesMoved solution works for me, so I'd assume that something else is amiss. Regardless, I can offer an alternative:
I also built Pong to learn Swift and SpriteKit, and I implemented the paddle movement using SKActions. Any time a touch begins, cancel the paddle's current actions and begin moving to the new x location.
If you want to keep it simple to start, then you can have a constant move time, like 0.5 seconds. Otherwise, you can calculate the time to move by getting the absolute distance between the paddle and touch location and dividing by some constant. Changing the constant will change the speed at which the paddle moves.
Here's an example with the constant move time:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if playerPaddle.hasActions() { playerPaddle.removeAllActions() }
for touch in touches {
let newX = touch.location(in: self).x
let oldX = playerPaddle.position.x
playerPaddle.run(.moveBy(x: newX - oldX, y: 0, duration: 0.5))
}
}

How to control sprite using swiping gestures so sprite can move along x axis

At the moment I have a game in which the sprite can be controlled and moved along the x axis (with a fixed y position) using the accelerometer of the device. I wish to change this so the user can control the sprite by dragging on the screen like in popular games such as snake vs.block.
I've already tried using the touches moved method which gives the correct effect, although the sprite first moves to the location of the users touch which I don't want.
Below is the environment I've been using to experiment, the touches moved gives the correct type of control I want although I can't seem to figure out how to stop the sprite first moving to the location of the touch before the swipe/drag
import SpriteKit
import GameplayKit
var player = SKSpriteNode()
var playerColor = UIColor.orange
var background = UIColor.black
var playerSize = CGSize(width: 50, height: 50)
var touchLocation = CGPoint()
var shape = CGPoint()
class GameScene: SKScene {
override func didMove(to view: SKView) {
self.backgroundColor = background
spawnPlayer()
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches{
let touchLocation = touch.location(in: self)
player.position.x = touchLocation.x
}
}
func spawnPlayer(){
player = SKSpriteNode(color: playerColor, size: playerSize)
player.position = CGPoint(x:50, y: 500)
self.addChild(player)
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
}
In summary, I'm looking for the same method of controlling a sprite as in snakes vs blocks
Hey so you have the right idea, you need to store a previous touchesMovedPoint. Then use it to determine how far your finger has moved each time touchesMoved has been called. Then add to the player's current position by that quantity.
var lastTouchLoc:CGPoint = CGPoint()
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches{
let touchLocation = touch.location(in: self)
let dx = touchLocation.x - lastTouchLoc.x
player.position.x += dx // Change the players current position by the distance between the previous touchesMovedPoint and the current one.
lastTouchLoc.x = touchLocation.x // Update last touch location
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
// When releasing the screen from point "E" we don't want dx being calculated from point "E".
// We want dx to calculate from the most recent touchDown point
let initialTouchPoint = touch.location(in: self)
lastTouchLoc.x = initialTouchPoint.x
}
}
You want to use SKAction.move to perform what you want.
func distance(from lhs: CGPoint, to rhs: CGPoint) -> CGFloat {
return hypot(lhs.x.distance(to: rhs.x), lhs.y.distance(to: rhs.y))
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches{
let touchLocation = touch.location(in: self)
let speed = 100/1 //This means 100 points per second
let duration = distance(player.position,touchLocation) / speed //Currently travels 100 points per second, adjust if needed
player.run(SKAction.move(to:touchLocation, duration:duration),forKey:"move")
}
}
What this does is it will move your "snake" to the new location at a certain duration. To determine duration, you need to figure out how fast you want your "snake" to travel. I currently have it set up to where it should take 1 second to move 100 points, but you may want to adjust that as needed.

How can I limit the range of motion on a custom UIControl Knob?

I'm following the tutorial here, which demonstrates how to implement a custom UIControl. However, I'd like to stop the custom control from spinning endlessly. I've been able to hinder the ability to spin the knob but have been unable to create a definite stopping point.
My Attempt
In the code below I'm using the authors angle variable which ranges from 0 to 360. The logic is a bit wonky, but I'm
override func continueTrackingWithTouch(touch: UITouch, withEvent event: UIEvent?) -> Bool {
super.continueTrackingWithTouch(touch, withEvent: event)
if (angle <= 1) {
angle = 2
return false
} else if angle >= 356 {
angle = 355
return false
} else {
let lastPoint = touch.locationInView(self)
self.moveHandle(lastPoint)
self.sendActionsForControlEvents(UIControlEvents.ValueChanged)
return true
}
}
view code on github
note: I've tried all of the obvious operators and logic. I feel like this is the wrong approach entirely.
Result
Basically, the controls motion will stop at 1, but only if I'm moving slowly. If I quickly drag the knob, it'll spin right past the 1, allowing the control to spin endlessly.
Question
How can I properly limit the UIControls range of motion from 1 to 355?
I've put together a simple working project that you can download and test.
project files
Change continueTrackingWithTouch to:
// Part of UIControl, used to track user input
override func continueTrackingWithTouch(touch: UITouch, withEvent event: UIEvent?) -> Bool {
let lastPoint = touch.locationInView(self)
self.moveHandle(lastPoint)
return super.continueTrackingWithTouch(touch, withEvent: event)
}
Change moveHandle to:
func moveHandle(lastPoint:CGPoint){
let threshholdAngle = 180
let centerPoint:CGPoint = CGPointMake(self.frame.size.width / 2, self.frame.size.height / 2)
let currentAngle:Double = AngleFromNorth(centerPoint, p2: lastPoint, flipped: false)
let angleInt = Int(floor(currentAngle))
let newAngle = Int(360 - angleInt)
if abs(newAngle - angle) > threshholdAngle {
return
}
//Store the new angle
angle = newAngle
sendActionsForControlEvents(UIControlEvents.ValueChanged)
//Update the textfield
//textField!.text = "\(angle)"
//Redraw
setNeedsDisplay()
}

An efficient way to find all SKNodes within a certain distance from a given point?

If I want to know which node or nodes cover certain point of the scene I can do either scene.nodeAtPoint(point) or scene.nodesAtPoint(point). Is there an efficient way to do something similar with a circle? That is to be able to find all nodes that have their positions within certain distance from a given point.
I need this to be able to handle user taps better. The scene is rather scarce (not much user-active sprites on it), so there is no need to demand the user to be very precise with tapping. I want to use some sort a tolerance around the coordinates of the tap and detect the node that is just close enough to tap location.
The best idea that I have so far is to do nodeAtPoint() several times: at the location of the tap, and then at various offsets from it (e.g. up, up-right, rigth, ..., up-left).
Distance
First of all we need an extension to find the distance between 2 CGPoint(s)
extension CGPoint {
func distance(point: CGPoint) -> CGFloat {
return CGFloat(hypotf(Float(point.x - self.x), Float(point.y - self.y)))
}
}
The closest child
Now we can add an extension to SKScene to find the child closest to a given point and within a maxDistance.
extension SKScene {
func closestChild(point: CGPoint, maxDistance: CGFloat) -> SKNode? {
return self
.children
.filter { $0.position.distance(point) <= maxDistance }
.minElement { $0.0.position.distance(point) < $0.1.position.distance(point) }
}
}
Time
The computational complexity of the extension above is O(n) where n is the number of direct children of the scene.
To determine if one or more nodes in the scene are within a fixed distance of a touch event, 1) compute the distance between the touch and each node and 2) compare if that distance is less than or equal to a constant. Here's an example of how to do that:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if let touch = touches.first {
let location = touch.locationInNode(self)
let nodes = nodesNearPoint(self, point:location, maxDistance: 30)
if nodes.count > 0 {
print("node is near touch point")
}
}
}
// Returns an array of all SKNode objects in the subtree that near the specified point.
// If no nodes are near the point, an empty array is returned.
func nodesNearPoint(container:SKNode, point:CGPoint, maxDistance:CGFloat) -> [SKNode] {
var array = [SKNode]()
for node in container.children {
// Only test sprite nodes (optional)
if node is SKSpriteNode {
let dx = point.x - node.position.x
let dy = point.y - node.position.y
let distance = sqrt(dx*dx + dy*dy)
if (distance <= maxDistance) {
array.append(node)
}
}
}
return array
}
Thanks to all the answerers, this is what I came up with in the end:
extension SKNode {
private func squareDistanceTo(point: CGPoint) -> CGFloat {
let dx = point.x - position.x
let dy = point.y - position.y
return (dx * dx + dy * dy)
}
func nearestNode(point: CGPoint) -> SKNode {
return children.minElement() {
$0.squareDistanceTo(point) < $1.squareDistanceTo(point)
} ?? self
}
func nodesInVicinity(point: CGPoint, tolerance: CGFloat) -> [SKNode] {
let squareTolerance = tolerance * tolerance
return children.filter() {
$0.squareDistanceTo(point) <= squareTolerance
}
}
}
Above extends SKNode with functionality:
nearestNode to find a child node nearest to the specific point, or give back the parent if there's no child nodes.
nodesInVicinity to get an array of nodes that have their positions within certain distance from a given point.
After some test I came to the conclusion that even nodeAtPoint has O(n) complexity, hence no hope for quick wins.
One way I've handled this before is to create a circle (using a png of a circle), setting the size to the radius I'm interested in and then seeing what nodes fall in that radius.
This is the basic idea assuming otherNodes is an array of the nodes I'm interested in checking:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let radius = 500
let circle = SKSpriteNode(texture: SKTexture(imageNamed: "circle.png"), color: UIColor.clearColor(), size: CGSize(width: radius, height: radius))
circle.position = touches.first!.locationInNode(self)
for node in otherNodes {
if circle.containsPoint(node.position) {
//The center of this node falls in the radius
}
}
}
EDIT:
After looking at an old solution where I used this, I noticed that containsPoint is actually looking to see if the point is within the node's bounding box, so having a png of a circle is actually irrelevant and consequently, matches might fall outside of the radius of the circle, but still within its bounding box.

Co-ordinate of touch location is zero

I am struggling to get the coordinates based on touch.
For simplicity I have created a new Project using swift and a sprite kit game - the default spinning spaceship one.
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
/* Called when a touch begins */
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
let sprite = SKSpriteNode(imageNamed: "Spaceship")
sprite.xScale = 0.5
sprite.yScale = 0.5
sprite.position = location
**NSLog("position x %f and y %f", location.x, location.y)**
let action = SKAction.rotateByAngle(CGFloat(M_PI), duration: 1)
sprite.runAction(SKAction.repeatActionForever(action))
self.addChild(sprite)
}
}
All I added to the project was the NSLog line to output the coordinates of the touch. But all I get in the output is 0 for x and y.
Anyone have any idea?
Convert whatever the location object is returning for variables x and y to a Float if you want to print the coordinates using NSLog with the %f formatter.
NSLog("position x %f and y %f", Float(location.x), Float(location.y))
If you aren't dead set on using NSLog, you can also do what Okapi suggested in his comment and use println. Both mechanisms work.
println("x:\(location.x) y:\(location.y)")