How to detect a node is in a range when touch a button? - sprite-kit

Look at the image, The red rect is move from top to bottom. I want to know when I touched the blue button, if the red rect is in the gray area. And when the rect is move over the greay area I want to know if the button is touched?
At first I tried determine the position of the red rect when I touched button, but if there are many rect, how to detect each one?
And also think if collision detection can do this, but I have no ideal.
So please help.Thanks.
The example image

Lets assume that your red rect, the blue button and gray area are each a SKNode or childs of his, e.g: SKSpriteNode or a SKShapenode.
You can check the collision at any time in the update function.
override func update(_ currentTime: TimeInterval)
{
// Called before each frame is rendered
if red_rect.intersects(blue_button) && red_rect.intersects(gray_area)
{
// at this point the red_rect touched the blue_button
// and the red_rect is over the gray_area
// *your code here*
}
}
You can find more information about the intersect(_:) function here.

Related

Changing the anchor point of a button and then rotating the button by it's anchor

I'm trying to rotate my button by it's anchor point which is meant to be it's frame's right bottom corner. The rotation that I want is 60 degrees to the right.
So basically I want a button which rotates 60 degrees to right by it's new anchor point which is button frame's right bottom instead of it's center.
Outlet name of my button is: LoginScreenSelectionButton
So the code that I use is
override func viewDidLoad() {
super.viewDidLoad()
let anchorPoint = CGPoint(x: LoginScreenSelectionButton.frame.width, y: LoginScreenSelectionButton.frame.height)
LoginScreenSelectionButton.layer.anchorPoint = anchorPoint // setting new anchor
LoginScreenSelectionButton.transform = CGAffineTransform(rotationAngle: CGFloat(60 * Double.pi/180))
}
When I use this code my button just disappears. I suppose that it rotates with a weird anchor point. Am I setting the anchor points wrong or rotation code is wrong?
I am adding the desired pre-rotation and post-rotation positions as well.
Initial position angle of the triangle Button
The exact rotation that I want to have after the rotation
Thank you.

Making the frame of a button triangle instead of rectangle in Swift

I have a button which has a triangle shaped image in it. Which looks like below.
This button also has a triangle clickable area. In order to do that I've created a UIButton class as below.
Button's clickable area is triangle but the frame of the button is still rectangle. How can I make the frame of the button a triangle? If there is a such way, I won't be using bezierpath to draw a clickable area because the frame will already be a triangle.
Thanks.
class triangleShapeButton: UIButton {
private let triangle = UIBezierPath()
override func draw(_ rect: CGRect) {
super.draw(rect)
triangle.move(to: CGPoint(x: rect.width, y: rect.height))
triangle.addLine(to: CGPoint(x:rect.width/2, y: 0))
triangle.addLine(to: CGPoint(x: 0, y:rect.height))
triangle.close()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// This is for allow touches only in shape area.
if triangle.contains(touches.first?.location(in: self) ?? .zero) {
super.touchesBegan(touches, with: event)
}
}
EDIT: What I want to create is a circular shaped button set as below. And while doing that, the rectangular frames of some of the triangle buttons overlaps the next triangle button. This causes to block the clickable area of some of the triangle buttons. I've tried to change the layer hierarchy but at least 1 of the buttons still overlaps the other one. How can I solve that issue if I can not change the frame?
Thanks a lot
Here's what I would suggest:
Draw your shapes using CAShapeLayers. If you ultimately want to fill those shapes with images, look at setting up layers where the contents of the layer is a CGImage. Add another CAShapeLayer to draw the border of the shape. Then apply a CAShapeLayer as a mask to crop the image to your triangle/circle wedge shape.
Now, for figuring out what the user tapped:
Build an array of your shape paths, and their frame rectangles. When you get a tap in your view, first loop through the array of rectangles and find all the rectangles that contain the tap point. (There may be more than one.)
Then loop through the smaller array where the tap was contained in the rectangle, and use UIBezierPath.contains(point:) or CGPath.contains(_:using:transform:) to figure out which of your shapes actually contains the tap point.
By first filtering your array to only those shapes who's frames contain the tap point, you use a very fast test to reduce the number of shapes you have to check using the much slower path contains() method.
I have added some code in your existing code and you can try it. It might help you.
I recommend you to ask for rectangular shape image.
class TriangleShapeButton: UIButton {
let triangle = UIBezierPath()
let triangleLayer = CAShapeLayer()
override func draw(_ rect: CGRect) {
super.draw(rect)
triangle.move(to: CGPoint(x: rect.width, y: rect.height))
triangle.addLine(to: CGPoint(x:rect.width/2, y: 0))
triangle.addLine(to: CGPoint(x: 0, y:rect.height))
triangleLayer.path = triangle.cgPath
triangleLayer.fillColor = UIColor.red.cgColor
self.layer.addSublayer(triangleLayer)
triangle.close()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// This is for allow touches only in shape area.
if triangle.contains(touches.first?.location(in: self) ?? .zero) {
super.touchesBegan(touches, with: event)
}
}
}
Your original approach of changing the shape of a UIView is not possible. Every UIView is a rectangle at the low-level, so adjacent triangles as in your case will necessarily overlap.
But there is a different approach:
You can create a single class for the entire circular view. Inside this class, you modify the drawRect function in such a way, that the individual triangles (or circle segments) are drawn adjacent to each other.
Inside this class you can then override the touchesBegan method. There you can check for the touch position using touches.first?.location(in: self). This will give you a CGPoint. Then you can manually check in which triangle the touch occurred and perform actions accordingly.

Get position of an UIView in an UIImageView

I have the following:
(1) Is a Giphy. I want to be able to move the giphy to the trash. I get the coordinates of the giphy with:
print("position of finger in moving giphy")
let position = gesture.location(in: imageViewImageTaken)
I struggle to get the position of my "trash" icon. This icon is a subview of my UIImageView, which is a photo.
I tried to get the position of the "trash" icon with:
let positionTrash = imageViewBin.convert(imageViewBin.bounds, to: imageViewImageTaken)
But when I move my finger to the trash icon and get the coordinates of (1) it does not match.
How can I correctly receive the position of (2)?
Since frame is the x, y, width, height of a view in its superview's coordinate system, you can use frame to get the x, y, width, height of the trash icon in the image view's coordinate system. There is no need to use convert:
let trashFrame = imageViewBin.frame
You seem to have another misunderstanding about how to check whether the finger is touching the trash. To do this, you need to check whether a CGRect (trashFrame) contains a CGPoint (position). This can be done using CGRect.contains:
if trashFrame.contains(position) {
print("The finger is on the trash icon!")
}
Alternatively, get the location of the finger and check if it is in the bounds of the trash:
let position = gesture.location(in: imageViewBin)
if imageViewBin.bounds.contains(position) {
}

Increase Touchable Area of an SKSpriteNode

I know a similar question has been asked before about this topic but here is my problem. I am using the following simple code to touch and SKNode.
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first as UITouch!
let location = touch.locationInNode(self)
let node = self.nodeAtPoint(location)
I then remove the touches node with the following:
node.removeFromParent()
Now, my problem is that the image of the node I am touching is small. Therefore it is very hard to accurately register a touch. I do not want to make the image bigger. This wouldn't be a problem but the nature of my game is such that I:
1) Cannot use another invisible SKSpriteNode ontop of my image SKSpriteNode to increase the registered touch area because each image has a unique name so I need that specific node to be touched and removed.
2) I cannot give the invisible SKSpriteNode the same name as the image SKSpriteNode because then only the invisible SKSpriteNode would be removed on touching.
3) I have tried to load a new SKSpriteNode into the original image SKSpriteNode to increase the touchable area, but once again, on touching, only the invisible SKSpriteNode gets removed, not the image SKSpriteNode which I want.
So, how can I increase the touchable area? Is there a way to maybe:
1) Remove all nodes associated with the node that is touched? This would then work if I put an invisible SKSpriteNode inside the original node to increase the touchable area. If they had the same name. But I have already tried "node.removeAllChildren()"......didn't work.
2) Detect a touch and the closest possible image near that touch is the image that the code in TouchesBegan gets use on? This would also work for my situation.
Any other suggestions? Cheers :)
-------------------------------UPDATE---------------------------
The following code below works! If I touch the invisible SKNode, then both the invisible and Original SKNodes disappear. However, for whatever reason, the original SKNode always appears infront of the invisible SKNode. I cannot fix this?
let original = SKSpriteNode(imageNamed: "original")
original.size = CGSize(width: 50, height: 50)
original.zPosition = 0
let invisible = SKSpriteNode(color: UIColor.redColor(), size: CGSizeMake(70, 70))
invisible.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
invisible.alpha = 1.0
invisible.zPosition = 1
invisible.name = original.name
invisible.addChild(original)
self.addChild(invisible)
For zPosition order, put this inside GameViewController
//Put this line
skView.ignoresSiblingOrder = true
//before this
skView.presentScene(scene)
In you code you have two nodes:
original
invisible
Who is the parent, who is the child?
According to your question "If I touch the invisible SKNode, then both the invisible and original SKNodes disappear..however, for whatever reason, the original SKNode always appears infront of the invisible SKNode" seems that you want invisible inside original, in other words original should be the parent and invisible should be the child.
But your code says the exact opposite:
invisible.addChild(original)
self.addChild(invisible)
So , we make an example:
Suppose that your original node is green, this how appear your elements follow your code: invisible node (red) do an addChild to the original (green) , and then you add to self the invisible. Invisible is parent, original is child.
Finally, if you touch the red rectangle to remove it, both red and childs (green) will be removed, if you touch the green rectangle to remove it, only the green rectangle disappear.
Hope you can help you to understand what happen to your code.

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

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.