Troubles to detect if a CGPoint is inside a square (diamond-shape) - ios

I have 2 SKSpriteNode:
a simple square (A)
the same square with a rotation (-45°) (B)
I need to check, at any time, if the center of another SKSpriteNode (a ball) is inside one of these squares.
The ball and the squares have the same parent (the main scene).
override func update(_ currentTime: TimeInterval) {
let spriteArray = self.nodes(at: ball.position)
let arr = spriteArray.filter {$0.name == "square"}
for square in arr {
print(square.letter)
if(square.contains(self.puck.position)) {
print("INSIDE")
}
}
}
With the simple square (A), my code works correctly. The data are right. I know, at any time, if the CGPoint center is inside or outside the square.
But with the square with the rotation (B), the data aren't as desired. The CGPoint is detected inside as soon as it's in the square which the diamond-shape is contained.
The SKSpriteNode squares are created via the level editor.
How can I do to have the correct result for the diamond-shape?
EDIT 1
Using
view.showsPhysics = true
I can see the bounds of all the SKSpriteNode with physicsBody. The bounds of my diamond-square is the diamond-square and not the grey square area.
square.frame.size -> return the grey area
square.size -> return the diamond-square
In the Apple documentation, func nodes(at p: CGPoint) -> [SKNode], the method is about node and not frame, so why it doesn't work?

There are many ways to do it, usually I like to work with paths so , if you have a perfect diamond as you describe I would like to offer a different way from the comments, you could create a path that match perfectly to your diamond with UIBezierPath because it have the method containsPoint:
let f = square.frame
var diamondPath = UIBezierPath.init()
diamondPath.moveToPoint(CGPointMake(f.size.width-f.origin.x,f.origin.y))
diamondPath.addLineToPoint(CGPointMake(f.origin.x,f.size.height-f.origin.y))
diamondPath.addLineToPoint(CGPointMake(f.size.width-f.origin.x,f.size.height))
diamondPath.addLineToPoint(CGPointMake(f.size.width,f.size.height-f.origin.y))
diamondPath.closePath()
if diamondPath.containsPoint(<#T##point: CGPoint##CGPoint#>) {
// point is inside diamond
}

Related

Align 3D object parallel to vertical plane detected by estametedVerticalPlane

I have this book, but I'm currently remixing the furniture app from the video tutorial that was free on AR/VR week.
I would like to have a 3D wall canvas aligned with the wall/vertical plane detected.
This is proving to be harder than I thought. Positioning isn't an issue. Much like the furniture placement app you can just get the column3 of the hittest.worldtransform and provide the new geometry this vector3 for position.
But I do not know what I have to do to get my 3D object rotated to face forward on the aligned detected plane. As I have a canvas object, the photo is on one side of the canvas. On placement, the photo is ALWAYS facing away.
I thought about applying a arbitrary rotation to the canvas to face forward but that then was only correct if I was looking north and place a canvas on a wall to my right.
I'v tried quite a few solutions on line all but one always use .existingPlaneUsingExtent. for vertical plane detections. This allows for you to get the ARPlaneAnchor from the
hittest.anchor? as ARPlaneAnchor.
If you try this when using .estimatedVerticalPlane the anchor? is nil
I also didn't continue down this route as my horizontal 3D objects started getting placed in the air. This maybe down to a control flow logic but I am ignoring it until the vertical canvas placement is working.
My current train of thought is to get the front vector of the canvas and rotate it towards the front facing vector of the vertical plane detected UIImage or the hittest point.
How would I get a forward vector from a 3D point. OR get the front vector from the grid image, that is a UIImage that is placed as an overlay when ARKit detects a vertical wall?
Here is an example. The canvas is showing the back of the canvas and is not parallel with the detected vertical plane that is the column. But there is a "Place Poster Here" grid which is what I want the canvas to align with and I'm able to see the photo.
Things I have tried.
using .estimatedVerticalPlane
ARKit estimatedVerticalPlane hit test get plane rotation
I don't know how to correctly apply this matrix and eular angle results from the SO answer.
my add picture function.
func addPicture(hitTestResult: ARHitTestResult) {
// I would like to convert estimate hitTest to a anchorpoint
// it is easier to rotate a node to a anchorpoint over calculating eularAngles
// we have all detected anchors in the _Renderer SCNNode. however there are
// Get the current furniture item, correct its position if necessary,
// and add it to the scene.
let picture = pictureSettings.currentPicturePiece()
//look for the vertical node geometry in verticalAnchors
if let hitPlaneAnchor = hitTestResult.anchor as? ARPlaneAnchor {
if let anchoredNode = verticalAnchors[hitPlaneAnchor]{
//code removed as a .estimatedVerticalPlane hittestResult doesn't get here
}
}else{
// Transform hitresult to world coords
let worldTransform = hitTestResult.worldTransform
let anchoredNodeOrientation = worldTransform.eulerAngles
picture.rotation.y =
-.pi * anchoredNodeOrientation.y
//set the transform matirs
let positionMatris = worldTransform.columns.3
let position = SCNVector3 (
positionMatris.x,
positionMatris.y,
positionMatris.z
)
picture.position = position + pictureSettings.currentPictureOffset();
}
//parented to rootNode of the scene
sceneView.scene.rootNode.addChildNode(picture)
}
Thanks for any help available.
Edited:
I have notice the 'handness' or the 3D model isn't correct/ is opposite?
Positive Z is pointing to the Left and Positive X is facing the camera for what I would expects is the front of the model. Is this a issue?
You should try to avoid adding node directly into the scene using world coordinates. Rather you should notify the ARSession of an area of interest by adding an ARAnchor then use the session callback to vend an SCNNode for the added anchor.
For example your hit test might look something like:
#objc func tapped(_ sender: UITapGestureRecognizer) {
let location = sender.location(in: sender.view)
guard let hitTestResult = sceneView.hitTest(location, types: [.existingPlaneUsingGeometry, .estimatedVerticalPlane]).first,
let planeAnchor = hitTestResult.anchor as? ARPlaneAnchor,
planeAnchor.alignment == .vertical else { return }
let anchor = ARAnchor(transform: hitTestResult.worldTransform)
sceneView.session.add(anchor: anchor)
}
Here a tap gesture recognized is used to detect taps within an ARSCNView. When a tap is detected a hit test is performed looking for existing and estimated planes. If the plane is vertical, we add an ARAnchor is added with the worldTransform of the hit test result, and we add that anchor to the ARSession. This will register that point as an area of interest for the ARSession, so we'll receive better tracking and less drift after our content is added there.
Next, we need to vend our SCNNode for the newly added ARAnchor. For example
func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
if anchor is ARPlaneAnchor {
let anchorNode = SCNNode()
anchorNode.name = "anchor"
return anchorNode
} else {
let plane = SCNPlane(width: 0.67, height: 1.0)
plane.firstMaterial?.diffuse.contents = UIImage(named: "monaLisa")
let planeNode = SCNNode(geometry: plane)
planeNode.eulerAngles = SCNVector3(CGFloat.pi * -0.5, 0.0, 0.0)
let node = SCNNode()
node.addChildNode(planeNode)
return node
}
}
Here we're first checking if the anchor is an ARPlaneAnchor. If it is, we vend an empty node for debugging purposes. If it is not, then it is an anchor that was added as the result of a hit test. So we create a geometry and node for the most recent tap. Because it is a vertical plane and our content is lying flat need to rotate it about the x axis. So we adjust it's eulerAngles to have it be upright. If we were to return planeNode directly adjustment to eulerAngles would be removed so we add it as a child node of an empty node and return it.
Should result in something like the following.

SpriteKit: how to smoothly animate SKCameraNode while tracking node but only after node moves Y pixels?

This question and others discuss how to track a node in SpriteKit using a SKCameraNode.
However, our needs vary.
Other solutions, such as updating the camera's position in update(_ currentTime: CFTimeInterval) of the SKScene, do not work because we only want to adjust the camera position after the node has moved Y pixels down the screen.
In other words, if the node moves 10 pixels up, the camera should remain still. If the node moves left or right, the camera should remain still.
We tried animating the camera's position over time instead of instantly, but running a SKAction against the camera inside of update(_ currentTime: CFTimeInterval) fails to do anything.
I just quickly made this. I believe this is what you are looking for?
(the actual animation is smooth, just i had to compress the GIF)
This is update Code:
-(void)update:(CFTimeInterval)currentTime {
/* Called before each frame is rendered */
SKShapeNode *ball = (SKShapeNode*)[self childNodeWithName:#"ball"];
if (ball.position.y>100) camera.position = ball.position;
if (fabs(ball.position.x-newLoc.x)>10) {
// move x
ball.position = CGPointMake(ball.position.x+stepX, ball.position.y);
}
if (fabs(ball.position.y-newLoc.y)>10) {
// move y
ball.position = CGPointMake(ball.position.x, ball.position.y+stepY);
}
}
I would not put this in the update code, try to keep your update section clutter free, remember you only have 16ms to work with.
Instead create a sub class for your character node, and override the position property. What we are basically saying is if your camera is 10 pixels away from your character, move towards your character. We use a key on our action so that we do not get multiple actions stacking up and a timing mode to allow for the camera to smoothly move to your point, instead of being instant.
class MyCharacter : SKSpriteNode
{
override var position : CGPoint
{
didSet
{
if let scene = self.scene, let camera = scene.camera,(abs(position.y - camera.position.y) > 10)
{
let move = SKAction.move(to: position, duration:0.1)
move.timingMode = .easeInEaseOut
camera.run(move,withKey:"moving")
}
}
}
}
Edit: #Epsilon reminded me that SKActions and SKPhysics access the variable directly instead of going through the stored property, so this will not work. In this case, do it at the didFinishUpdate method:
override func didFinishUpdate()
{
//character should be a known property to the class, calling find everytime is too slow
if let character = self.character, let camera = self.camera,(abs(character.position.y - camera.position.y) > 10)
{
let move = SKAction.move(to: character.position, duration:0.1)
move.timingMode = .easeInEaseOut
camera.run(move,withKey:"moving")
}
}

How to rotate an SKShapeNode around its own axis?

I want to rotate an SKShapeNode around its axis (a task I assumed would be easy to do in a framework like SpriteKit). The shape rotates, but around an axis that seems to be off screen...
Any ideas on what's going wrong here?
import SpriteKit
class Square: NSObject {
// this gets created and initialized later...
var shape: SKShapeNode? = nil
//…
func animateMe(){
let action = SKAction.rotateByAngle(CGFloat(M_PI), duration:1)
self.shape.runAction(action, completion: { [unowned self] () -> Void in
self.shape.removeFromParent()
})
}
}
Rotation happens around the node's position. So rather than giving the shape node a path with an x/y offset to position your shape, you should change the node's position, and give it a path which is centered around (0,0) so that it draws the shape near its position (because the path is drawn relative to the position as well).
See this answer as well.

How to simulate a world larger than the screen in SpriteKit?

I'm looking for the proper SpriteKit way to handle something of a scrollable world. Consider the following image:
In this contrived example, the world boundary is the dashed line and the blue dot can move anywhere within these boundaries. However, at any given point, a portion of this world can exist off-screen as indicated by the image. I would like to know how I can move the blue dot anywhere around the "world" while keeping the camera stationary on the blue dot.
This is Adventure, a sprite kit game by apple to demonstrate the point I made below. Read through the docs, they explain everything
Theres a good answer to this that I can't find at the moment. The basic idea is this:
Add a 'world' node to your scene. You can give it a width/height that is larger than the screen size.
When you 'move' the character around (or blue dot), you actually move your world node instead, but in the opposite direction, and that gives the impression that you're moving.
This way the screen is always centered on the blue dot, yet the world around you moves
below is an example from when I was experimenting a while ago:
override func didMoveToView(view: SKView) {
self.anchorPoint = CGPointMake(0.5, 0.5)
//self.size = CGSizeMake(600, 600)
// Add world
world = SKShapeNode(rectOfSize: CGSize(width: 500, height: 500))
world.fillColor = SKColor.whiteColor()
world.position = CGPoint(x: size.width * 0.5, y: size.height * 0.5)
world.physicsBody?.usesPreciseCollisionDetection = true
self.addChild(world)
}
override func update(currentTime: CFTimeInterval) {
world.position.x = -player.position.x
world.position.y = -player.position.y
}
override func didSimulatePhysics() {
self.centerOnNode(self.camera)
}
func centerOnNode(node: SKNode) {
if let parent = node.parent {
let nodePositionInScene: CGPoint = node.scene!.convertPoint(node.position, fromNode: parent)
parent.position = CGPoint(
x: parent.position.x - nodePositionInScene.x,
y: parent.position.y - nodePositionInScene.y)
}}
If you create a "camera" node which you add to your "world" node, a couple of simple functions (above) allow you to "follow" this camera node as it travels through the world, though actually you are moving the world around similar to Abdul Ahmad's answer.
This method allows you to use SpriteKit functionality on the camera. You can apply physics to it, run actions on it, put constraints on it, allowing effects like:
camera shaking (an action),
collision (a physics body, or matching the position of another node with a physics body),
a lagging follow (place a constraint on the camera that keeps it a certain distance from a character, for example)
The constraint especially adds a nice touch to a moving world as it allows the "main character" to move around freely somewhat while only moving the world when close to the edges of the screen.

Move a specific node in SceneKit using touch

I have a scene with multiple nodes. I want to select a node by tapping it (if I tap nothing I want nothing to happen) and make him follow my finger only on XY axis (I know position on Z axis). Is there any method that converts location in view to SceneKit coords?
After few researches I found this and it's exactly what I want, but I don't get the code. Can somebody explain me or help me figure how can I solve my problem?
https://www.youtube.com/watch?v=xn9Bt2PFp0g
func CGPointToSCNVector3(view: SCNView, depth: Float, point: CGPoint) -> SCNVector3 {
let projectedOrigin = view.projectPoint(SCNVector3Make(0, 0, depth))
let locationWithz = SCNVector3Make(Float(point.x), Float(point.y), projectedOrigin.z)
return view.unprojectPoint(locationWithz)
}
Looks like was pretty simple, I've made a function that gets 3 parameters. View is the SCNView where scene is attached to, depth is the z value of node, and point is a CGPoint that represents projection of 3D scene on screen.
This is Alec Firtulescu's answer as an extension for watchOS (convert it to iOS by changing WKInterfaceSCNScene to SCNView):
extension CGPoint {
func scnVector3Value(view: WKInterfaceSCNScene, depth: Float) -> SCNVector3 {
let projectedOrigin = view.projectPoint(SCNVector3(0, 0, depth))
return view.unprojectPoint(SCNVector3(Float(x), Float(y), projectedOrigin.z))
}
}

Resources