How to make touch.locationInNode() recognize the difference between a node and its child? - swift

I started out by declaring two SKSpriteNodes, handle and blade, and adding handle as a child of self, and blade as a child of handle
var handle = SKSpriteNode(imageNamed: "Handle.png")
var blade = SKSpriteNode(imageNamed: "Blade.png")
override func didMoveToView(view: SKView) {
handle.position = CGPointMake(self.size.width / 2, self.size.height / 14)
blade.position = CGPointMake(0, 124)
self.addChild(Handle)
Handle.addChild(Blade)
}
When I click on the handle, it prints to the console "Handle was clicked", however when I click on the Blade, it also prints "Handle was clicked". It is clearly recognizing that the blade is a child of handle, but how can I make it so when I click on blade, it prints "Blade was clicked"?
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
for touch in (touches as! Set<UITouch>) {
let location = touch.locationInNode(self)
if (Handle.containsPoint(location)){
NSLog("Handle was clicked")
}
else if (Blade.containsPoint(location)){
NSLog("Blade was clicked")
}
}
}

Determining whether the user touched the sword's handle or the blade is fairly straightforward with some caveats. The following assumes that 1. the sword image is facing to the right when zRotation = 0, 2. the anchorPoint of the sword is (0, 0.5), and 3. the sword (blade and handle) is a single sprite node. When you add a sprite to another sprite, the size of the parent's frame expands to include the child node. That's why your test of Handle.containsPoint is true no matter where you click on the sword.
The figure below shows a sword sprite with a dark gray handle (on the left) and lighter gray blade. The black rectangle surrounding the sword represents the sprite's frame and the circle represents the location of the user's touch. The length of the line labeled a is the distance from the touch point to the bottom of the sword. We can test this distance to see if the user touched the handle (if a <= handleLength) or the blade (if a > handleLength). When zRotation = 0, a = x so the test is x <= handleLength, where the bottom of the sword is x = 0.
In the below figure, the sword is rotated by 90 degree (i.e., zRotation = M_PI_2). Similarly, if a <= handleLength, the user touched the handle, else the user touched the blade. The only difference is a is now the y value instead of x due to the sword's rotation. In both cases, the frame's bounding box can be used, as is, to detect if the user touched the sword.
When the sprite is rotated by 45 degree, however, its frame automatically expands to enclose the sprite as shown by the black rectangle in the figure below. Consequently, when the user touches anywhere in the rectangle, the test if sprite.frame.contains(location) will be true. This may result in the user picking up the sword when the location of the touch is relatively far from the sword (i.e., when the distance b is large). If we want the maximum touch distance to be the same across all rotation angles, additional testing is required.
The good news is Sprite Kit provides a way to convert from one coordinate system to another. In this case, we need to convert from scene coordinates to the sword coordinates. This greatly simplifies the problem because it also rotates the point to the new coordinate system. After converting from scene to sword coordinates, the converted touch location's x and y values are the same as the distances a and b over all rotation angles! Now that we know a and b, we can determine how close the touch was to the sword and whether the user touched the handle or the blade.
From the above, we can implement the following code:
let location = touch.locationInNode(self)
// Check if the user touched inside of the sword's frame (see Figure 1-3)
if (sword.frame.contains(location)) {
// Convert the touch location from scene to sword coordinates
let point = sword.convertPoint(location, fromNode: self)
// Check if the user touched any part of the sword. Note that a = point.x and b = point.y
if (fabs(point.y) < sword.size.height/2 + touchTolerance) {
// Check if the user touched the handle
if (point.x <= handleLength) {
println("touched handle")
}
else {
println("touched blade")
}
}
}

This should work without changing too much of your existing code...
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
for touch in (touches as! Set<UITouch>) {
let locationInScene = touch.locationInNode(self)
let locationInHandel = touch.locationInNode(Handle)
if (Blade.containsPoint(locationInHandel)){
NSLog("Blade was clicked")
}
else if (Handle.containsPoint(locationInScene)){
NSLog("Handle was clicked")
}
}
}
Note you are checking for blade first then you check for handle. Also note you have to convert the touchpoint to give you a point from within handle.
With that being said this will work on a small scale, but you may want to look at creating a subclass for SKSpriteNode called Handle or Sword (this is why you don't use first caps for variable names they are normally only use first caps for classes), set it to userInteractionEnabled and then override touchesBegan in that subclass and see if it is touching the blade and if not you know it touched the handle.
Hopefully that helped and made sense.

Related

Swift-Setting a physics body velocity by angle

I was wondering if it was at all possible to make an SKNode move forward in a particular direction, but with only one factor. I'm aware of both applying an impulse and setting the velocity of a physics body, but they're both determined by two factors; dx and dy. I also know of rotating to an angle with SKActions. But is it possible to make an object simply "move forward" once it has been set on an angle? Or set its velocity with just one factor?
Thanks in advance.
Yes, is the answer to your question.
What I think you're looking for = THRUST... right?
What you want is for the "ship" to be able to rotate in any direction, and the thrust to be applied correctly, out of the "arse" of the ship, moving it forward, in ship terms.
This is absolutely possible, but does require a little "dummy" trick.
But I'm confusing you.
The local space of a SKPhysicsBody is relative to its parent. I presume.
And there's the speculative part. I'm guessing. I haven't tried this.
But... most physicsBodys are the child of an SKNode that's parented to the scene.
If you parent your ship to a dummy node for the purposes of rotation, and then rotate the dummy node, you should be able to make your spaceship fly in circles without ever changing the thrust vector, by simply rotating the dummy node.
Theoretically.
Something like this horrible pseudo code might help to start... maybe.
let dummy = SKNode()
let ship = SKSPriteNode()
dummy.addchild(ship)
ship.Physicsbody(add how you want here...)
ship.PhysicsBody.applyForce (vector that's only X, for example)
rotate dummy with action over time...
Sure I think what you're talking about is something like this:
Now let's say you have an SKSpriteNode that is called player who eventually has a physicsBody setup.
var player: SKSpriteNode!
You can just set the dx property of their velocity, so lets say you wanted to move them horizontally towards the location where the user tapped on the right hand side of the screen. If you then detect the position of the touch with touchesBegan(_:)
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
// Replace with name of node to detect touch
let touchLocation = touch.location(in: <NAME_OF_NODE_PROPERTY>)
// Verify it's in front of the player and not behind
if (touchLocation.x - playerPosition.x) > 0 {
movePlayerVertically(toward: touchLocation)
}
}
func movePlayerVertically(toward location: CGPoint) {
let dx:CGFloat = location.x - player.position.x
player.physicsBody!.velocity.dx = dx
}
EDIT: -
Since you said you just want to be able to move your player horizontally without knowing the destination, you could do something like this, this is just moving the player forward on the x-axis by 50pts every second, and will repeat it forever. Obviously you would want to tweak it to your liking.
let move = SKAction.moveBy(x: 50, y: 0, duration: 1)
let repeatAction = SKAction.repeatForever(move)
player.run(repeatAction)

Swift Spritekit. How am I able to lock a node in a particular point?

I am trying to figure out how to lock a SKSpriteNode in a particular y point. When my finger touches on the screen, the node displayed on the point where I touched. This is because inside TouchesBegan method, I wrote lines of code which are:
if isMovable == false {
isMovable = true
for touch in touches {
let location = touch.locationInNode(self)
ship.position = location
}
}
And then, I wrote these lines of code instead in order to get only the x point:
for touch in touches {
let location = touch.locationInNode(self)
ship.position = CGPointMake(location, self.frame.height/2 - 200)
}
However, the result I got was that location is not usable there because it is CGPoint, not CGFloat. I tried to cast it as CGFloat(location), but futile. Is there any way to lock a node in a y point?
The desirable function I want is when I touch anywhere on the screen, a y point is locked, meaning that a node is only movable horizontally, but not vertically movable.
Since my English is not good enough, I may lack missing some explanation that enables you guys to understand my problem more clearly. I appreciate if you understand me, and give me the solution! Thanks in advance.
Your location variable is of type CGPoint which is :
A structure that contains a point in a two-dimensional coordinate system.
Apple Documentation
So you could use that same variable location to get the xposition and lock the yto whatever point you choose:
for touch in touches {
let location = touch.locationInNode(self)
ship.position = CGPointMake(location.x, self.frame.height/2 - 200)
}

Trying to get Physics Shape That Matches the Graphical Representation

what I'm trying to achieve is that the Node would have the same shape as PhysicsBody/texture (fire has a complicated shape), and then I'm trying to make only fireImage touchable. So far when I'm touching outside of the fireImage on the screen and it still makes a sound. It seems that I'm touching the squared Node, but I want to touch only the sprite/texture.
Would appreciate your help.
The code is below:
import SpriteKit
import AVFoundation
private var backgroundMusicPlayer: AVAudioPlayer!
class GameScene2: SKScene {
var wait1 = SKAction.waitForDuration(1)
override func didMoveToView(view: SKView) {
setUpScenery()
}
private func setUpScenery() {
//I'm creating a Fire Object here and trying to set its Node a physicsBody/texture shape:
let fireLayerTexture = SKTexture(imageNamed: fireImage)
let fireLayer = SKSpriteNode(texture: fireLayerTexture)
fireLayer.anchorPoint = CGPointMake(1, 0)
fireLayer.position = CGPointMake(size.width, 0)
fireLayer.zPosition = Layer.Z4st
var firedown = SKAction.moveToY(-200, duration: 0)
var fireup1 = SKAction.moveToY(10, duration: 0.8)
var fireup2 = SKAction.moveToY(0, duration: 0.2)
fireLayer.physicsBody = SKPhysicsBody(texture: fireLayerTexture, size: fireLayer.texture!.size())
fireLayer.physicsBody!.affectedByGravity = false
fireLayer.name = "fireNode"
fireLayer.runAction(SKAction.sequence([firedown, wait1, fireup1, fireup2]))
addChild(fireLayer)
}
//Here, I'm calling a Node I want to touch. I assume that it has a shape of a PhysicsBody/texture:
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let touch = touches.anyObject() as UITouch
let touchLocation = touch.locationInNode(self)
let node: SKNode = nodeAtPoint(touchLocation)
if node.name == "fireNode" {
var playguitar = SKAction.playSoundFileNamed("fire.wav", waitForCompletion: true)
node.runAction(playguitar)
}
}
}
}
Physics bodies and their shapes are for physics — that is, for simulating things like collisions between sprites in your scene.
Touch handling (i.e. nodeAtPoint) doesn't go through physics. That's probably the way it should be — if you had a sprite with a very small collision surface, you might not necessarily want it to be difficult to touch, and you also might want to be able to touch nodes that don't have physics. So your physics body doesn't affect the behavior of nodeAtPoint.
An API that lets you define a hit-testing area for a node — that's independent of the node's contents or physics — might not be a bad idea, but such a thing doesn't currently exist in SpriteKit. They do take feature requests, though.
In the meantime, fine-grained hit testing is something you'd have to do yourself. There are at least a couple of ways to do it:
If you can define the touch area as a path (CGPath or UIBezierPath/NSBezierPath), like you would for creating an SKShapeNode or a physics body using bodyWithPolygonFromPath:, you can hit-test against the path. See CGPathContainsPoint / containsPoint: / containsPoint: for the kind of path you're dealing with (and be sure to convert to the right local node coordinate space first).
If you want to hit-test against individual pixels... that'd be really slow, probably, but in theory the new SKMutableTexture class in iOS 8 / OX X v10.10 could help you. There's no saying you have to use the modifyPixelDataWithBlock: callback to actually change the texture data... you could use that once in setup to get your own copy of the texture data, then write your own hit testing code to decide what RGBA values count as a "hit".

How to add new SKSprideNodes off the screen and sync their animation with existing objects?

I am trying to animate multiple SKSpriteNode's of the same type using single SKAction.
What I would like to do, is to display four SKSpriteNode's of the same type on the screen.
Then animate them all when the button is pressed and move them to the left.
As soon as the first SKSpriteNode would leave the screen I want to add another SKSpriteNode off the screen position on the right side and add him to this SKAction loop.
Image below shows in detail what I am after.
So far I was able to display four SKSpriteNode's of the same type on the screen and add them to the array.
var squareBox = SKSpriteNode()
func addSquares(size:CGSize){
for var i = 0; i < 4; i++ {
// Create a new sprite node from an image
squareBox = SKSpriteNode(imageNamed: "noBox")
// Square pysics settings
squareBox.physicsBody = SKPhysicsBody(rectangleOfSize: self.squareBox.frame.size)
squareBox.physicsBody!.dynamic = false
squareBox.physicsBody!.categoryBitMask = squareBoxCategory
squareBox.physicsBody!.contactTestBitMask = leftEdgeCategory | edgeCategory
// SquareBox positioning on X
var xPos = size.width/5 + squareBox.size.height/2
var xPosInt = Int(xPos) * (i + 1)
xPos = CGFloat(xPosInt)
var yPos = size.height/2 + (squareBox.size.height/2)
squareBox.position = CGPointMake(xPos - squareBox.size.height/2, yPos)
self.addChild(squareBox)
squareArray.append(squareBox)
}
}
I am not sure if that is correct way to animate four SKSpriteNode's of the same type, but when the button is pressed they all seem to move to the left as desired.
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
self.nodeAtPoint(location)
if self.nodeAtPoint(location) == self.button {
let move = SKAction.moveByX(-size.width/5 - squareBox.size.height/2, y: 0, duration: 1)
print("button clicked!")
for box in squareArray {
box.runAction(move)
}
}
}
}
I created an invisible 1px line on the left edge of the screen to define when the first SKSpriteNode leaves the screen.
func addLeftEdge(size:CGSize){
let leftEdge = SKNode()
leftEdge.physicsBody = SKPhysicsBody(edgeFromPoint: CGPointMake(1, 1), toPoint: CGPointMake(1, size.height))
leftEdge.physicsBody!.categoryBitMask = leftEdgeCategory
self.addChild(leftEdge)
}
The problem with this approach is that SKSpriteNode's in the array when I animate them don't respond on didBeginContact when they touch the 1px line.
func didBeginContact(contact: SKPhysicsContact) {
println("contact!")
}
In the end I want to be able to apply the Force to all SKSpriteNode's on the screen for a few seconds to move them to the left and descend the force speed over the time until they all stop. My idea was to create four objects of the same type and add them to the screen + add them to the array var squareArray = [SKSpriteNode]().
Then when the first SKSpriteNode leaves the screen I would remove it from array and add it again at the end and position it off the screen on the right so it will be moved in seamless animation.
I have a feeling that my entire approach is wrong.
Please advise what is the best approach to animate multiple SKSpriteNode's to achieve my goal as described above. Am I on the right track ?
Please help, thank you.
In order to detect contact, one of the SKSpriteNodes must have physicsBody.Dynamic= YES;
Since you want neither to be dynamic, there is a quick fix I have found:
leftEdge.physicsBody.Dynamic=YES; //This allows it to enter onContact
leftEdge.physicsBody.collisionBitMask=0; //Disables edge from being moved by collision
leftEdge.physicsBody.AffectedByGravity=NO; //Prevents any in game gravity from moving the edge
This allows you have an edge that is dynamic and can trigger onContact yet the edge will still act as if it is not dynamic.
I think this solution isn't very clean and requires a lot more code than what is necessary to get this effect. In the update() function since there are only 4 or 5 boxes why don't you simply loop through the objects and check if any of their frame's minX's are less than 0 and if they are reset them to the starting point which I'm assuming is (view.frame.width + box.size.width) because that's completely offscreen. That way there's no array manipulation in that you don't have to remove and append objects to the array. Also you don't need to attach a physicsBody where none is required. And you don't need the leftEdge

Drag UILabel from point A to point B

How can I go about dragging a UILabel from point A to point B on my iPhone application?
Basically, you would need to build the functionality yourself. You could do this by listening to touches in a superview that includes the full draggable area. The method hitTest:withEvent: can tell you if the touch down point is in the label to be dragged.
From there, you can override the touchesMoved:withEvent: method to update the position of the UILabel to keep it aligned with the finger. Updating the position of the label will automatically redraw it for you.
Here's a relevant question for iphone drag/drop.
Once you get the dragging working, I would also recommend paying attention to the location within the UILabel where the first touch was made, and handle the repositioning in such a way that the finger is always on that point within the UILabel. An easier-to-code but worse-looking version is to simply reposition, say, the upper-left corner of the label to be where the finger is, but this could make the label appear to jump when it first starts dragging.
It may sound intimidating, but it's really not that bad -- just a few lines of code in total. Unless you have views that might overlap. That's another few lines, depending on how you want to handle it.
// MARK: - Touch Functions
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
guard let location = touches.first?.preciseLocationInView(quoteView) else { return }
if quoteView.pointInside(location, withEvent: event) {
locationInView = location.y
isInButton = true
} else {
isInButton = false
}
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
if isInButton {
guard let totalYMovement = touches.first?.preciseLocationInView(self.view).y,
locationInView = self.locationInView else { return }
var yMovement = totalYMovement - quoteViewLocation
if locationInView >= (self.quoteView.frame.height / 2) {
// Works!
yMovement = yMovement - (locationInView - (self.quoteView.frame.height / 2))
} else {
yMovement = yMovement + ((self.quoteView.frame.height / 2) - locationInView)
}
self.quoteView.transform.ty = yMovement
}
}
I used the above code to translate a view along the y axis. The quoteViewLocation variable is a gettable CGPoint which returns
self.view.frame.height / 2
locationInView is a optional CGPoint that is set every time a tap is made inside of the view to be transformed is tapped.
You can place the button anywhere in your view using my code, just make sure your quoteViewLocation (Whatever you are going to call it) variable returns the CGPoint.y horizontally bisecting the view. This point must be the corresponding point of the superview. What I mean by this is if you have a child view with a height of 40 points and a super view with a height of 40 points and the child view is centered smack dab in the middle of the super view you may think the correct CGPoint to return is 20. Nope! While this is the correct center Y of the child view the center of the super view is 200. Return the CGPoint of the superview.
You have to do this to keep the transformation smooth.
If you want to move a view along the x axis it's the same idea.
If I missed anything or misspoke tear into me. Trying to learn here too.
Thanks