I've got a really simple problem, but I've not been able to find a solution...how do I get the screen coordinates of a SKSpriteNode, given that its position property is CGPoint.zero but that its a number of children down from the SKScene and each parent will have applied some translation?
you'll want to use convert(_ point: CGPoint, to node: SKNode) and convert(_ point: CGPoint, from node: SKNode)
you'll probably have to use both
example
let nestedSpritePos = convert(convert(nestedSpritePos.position, from: nestedSpritePos.parent!), to: self)
assuming self is the scene. You may have to play around with those but they definitely are what you are looking for.
Related
I’m trying to make my AR experience more user friendly.
I have this SCNnode (objectNodeToPlace) that I want to place over the node created/update by the renderers whenever an imageReference is detected by the camera:
class ViewController: UIViewController, ARSCNViewDelegate {
var objectNodeToPlace: SCNNode
...
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
guard let imageAnchor = anchor as? ARImageAnchor else { return }
placeObject(object: objectNodeToPlace, at: node, ...)
}
}
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
guard let imageAnchor = anchor as? ARImageAnchor else { return }
placeObject(object: objectNodeToPlace, at: node, ...)
}
}
My placeObject function is pretty simple, I’m just changing the orientation of the “objectNodeToPlace”:
private func placeObject(object objectNodeToPlace: SCNNode, at node: SCNNode, ...) {
...
node.addChildNode(objectNodeToPlace)
}
Everything works well, my object is always placed on the latest detected imageReference.
But when my object is place onto an image and a new one is detected, the object jumps to the newest, it’s not great, very jittery / jerky.
My goal is to make this transition smoother.
What I currently have
Here’s what it looks like rigth now:
What I have now
In red, the imageReference.
What I want
Here’s what I would like:
What I would like
What I’ve found yet
I’ve found this package, SceneKit Bezier Animations, to animate the movement between 2 points but I look a little bit overkill for want I want.
I also read this topic, SceneKit Rotate and Animate a SCNNode, one response suggest to use CABasicAnimation and another one suggest SCNAction.
I feel like SCNAction is the best way to go for the quick, not that precise animation that I want, I’m not sure of what I’m doing so I will be happy to hear from more experienced developers.
Edit
I've found what I'm looking for on the Apple documentation Animating SceneKit Content
It's called Implicit animation, and it should work with only one line of code that determines the animation duration of my changes :
SCNTransaction.animationDuration = 1.0
I tried that line just before I change the euler angle (who is "animatable") of my node in my place()function:
SCNTransaction.animationDuration = 1.0
objectNodeToPlace.eulerAngles.x = radian
but that didn't worked. I'm pretty sure that I'm just missing a simple thing but I can't find examples online, even in the documentation of SCNTransaction.
Does someone have an idea ?
Axel,
SCNTransaction needs a minimum of three statements.
SCNTransaction.start()
SCNTransaction.animationDuration = 1.0
//commands
SCNTransaction.commit()
I written a dozen articles on SceneKit recently you'll find here.
I have a SpriteKit based ARKit application that shows SKSpriteNodes at various anchor points in the AR space. I am providing the node for each anchor using the ARSKViewDelegate method
func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode?
That works when I first add the anchor, but at point in the runtime I would like to switch the nodes for some anchors to different nodes. How do I force the ARSKView to "refresh" and call that node for anchor method again?
Use view(_:didUpdate:for:) instance method.
This method tells the delegate that a SpriteKit node's properties have been updated to match the current state of its corresponding anchor.
optional func view(_ view: ARSKView,
didUpdate node: SKNode,
for anchor: ARAnchor)
And don't forget about delegate allowing you to use a mentioned method:
weak var delegate: ARSKViewDelegate? { get set }
The ARFaceTrackingConfiguration of ARKit places ARFaceAnchor with information about the position and orientation of the face onto the scene. Among others, this anchor has the lookAtPoint property that I'm interested in. I know that this vector is relative to the face. How can I draw a point on the screen for this position, meaning how can I translate this point's coordinates?
.lookAtPoint instance property is for direction's estimation only.
Apple documentation says: .lookAtPoint is a position in face coordinate space that is estimating only the gaze of face's direction. It's a vector of three scalar values, and it's just gettable, not settable:
var lookAtPoint: SIMD3<Float> { get }
In other words, this is the resulting vector from the product of two quantities – .rightEyeTransform and .leftEyeTransform instance properties (which also are just gettable):
var rightEyeTransform: simd_float4x4 { get }
var leftEyeTransform: simd_float4x4 { get }
Here's an imaginary situation on how you could use this instance property:
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
if let faceAnchor = anchor as? ARFaceAnchor,
let faceGeometry = node.geometry as? ARSCNFaceGeometry {
if (faceAnchor.lookAtPoint.x >= 0) { // Looking (+X)
faceGeometry.firstMaterial?.diffuse.contents = UIImage(named: "redTexture.png")
} else { // Looking (-X)
faceGeometry.firstMaterial?.diffuse.contents = UIImage(named: "cyanTexture.png")
}
faceGeometry.update(from: faceAnchor.geometry)
facialExrpession(anchor: faceAnchor)
DispatchQueue.main.async {
self.label.text = self.textBoard
}
}
}
And here's an image showing axis directions for ARFaceTrackingConfiguration():
Answering your question:
I could say that you can't manage this point's coordinates directly because it's gettable-only property (and there is just XYZ orientation, not XYZ translation).
So if you need both – translation and rotation – use .rightEyeTransform and .lefttEyeTransform instance properties instead.
There are two projection methods:
FIRST. In SceneKit/ARKit you need to take the following instance method for projecting a point onto 2D view (for sceneView instance):
func projectPoint(_ point: SCNVector3) -> SCNVector3
or:
let sceneView = ARSCNView()
sceneView.projectPoint(myPoint)
SECOND. In ARKit you need to take the following instance method for projecting a point onto 2D view (for arCamera instance):
func projectPoint(_ point: simd_float3,
orientation: UIInterfaceOrientation,
viewportSize: CGSize) -> CGPoint
or:
let camera = ARCamera()
camera.projectPoint(myPoint, orientation: myOrientation, viewportSize: vpSize)
This method helps you project a point from the 3D world coordinate system of the scene to the 2D pixel coordinate system of the renderer.
There's also the opposite method (for unprojecting a point):
func unprojectPoint(_ point: SCNVector3) -> SCNVector3
...and ARKit's opposite method (for unprojecting a point):
#nonobjc func unprojectPoint(_ point: CGPoint,
ontoPlane planeTransform: simd_float4x4,
orientation: UIInterfaceOrientation,
viewportSize: CGSize) -> simd_float3?
I have an app thats spawn ball on the screen every 1 second. now, I want the user to touch those balls what make them disappear (removeFromParent()). as I understand I have to set the touch function via touchesBegan and I do so, here is my code:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch: AnyObject in touches{
let positionOfTouch = touch.location(in: self)
enumerateChildNodes(withName: "BALL") { (node: SKNode, nil) in
if positionOfTouch == node.position {
print("just touched the ball")
}
else{
print("error")
}
}
}
the problem is that when I touch the screen/ ball the console print error instead of just touched the ball, which mean that my code doesn't work. moreover, the console print the error message as the number of the balls in my view. i don't relay understand what I am doing wrong and how to really set this function.
here is my createBall function which implement from my BallNode class (type SKShapeNode):
func createBall(){
let ball = BallNode(radius: 65)
print(ball.Name)
print(ball._subName!)
ball.position.y = ((frame.size.height) - 200)
let ballXPosition = arc4random_uniform(UInt32(frame.size.width)) // set the ball a randon position from the top of the screen
ball.position.x = CGFloat(ballXPosition)
ball.physicsBody?.categoryBitMask = PhysicsCategory.ball // ball's category bitMask
ball.physicsBody?.collisionBitMask = PhysicsCategory.ball // prevent objects from intersecting
ball.physicsBody?.contactTestBitMask = PhysicsCategory.topBorder // when need to know if two objects touch each other
addChild(ball)
}
can you help me with that? because I am quit new for swift I also would like to get some explanation about this touch detection (and touches in general - the apple doc is poor).
every time you touch the screen you are cycling through all balls to see if you're touching one of them. if you have 50 balls on the screen it goes through them all to see if you are touching 1. that's not an efficient way of figuring out if you are touching 1.
There are many ways you can do this but what I would do is handle the touches inside of the Ball class. That way you don't have to figure out if you are touching a ball and which one it might be.
Explanation of protocol (to the best of my ability) this may seem a little much right now, but the faster you learn and understand protocols that better off you will be (IMO).
In this example we will use a protocol to setup a delegate of the
BallNode class. A protocol is a set user defined "rules" that must be
followed by any class that you designate compliant to that protocol.
In my example I state that for a class to be compliant to the
BallNodeDelegate protocol it must contain the didClick func. When you
add the BallNodeDelegate after GameScene you are stating that this
class will be compliant to that protocol. So if in GameScene you did
not have the didClick func it will cause an error. All this is put in
place so that you have an easy way to communicate between your
BallNode instances and your GameScene class (without having to pass
around references to your GameScene to each BallNode). Each BallNode
then has a delegate (GameScene) which you can pass back the
information to.
inside your BallNode class make sure you have isUserInteraction = true
outside of your BallNode class create a protocol that will send the touch info back to the GameScene
protocol BallNodeDelegate: class {
func didClick(ball: BallNode)
}
create a delegate variable in your BallNode class
weak var delegate: BallNodeDelegate!
move the touches began to you BallNode class
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.delegate?.didClick(ball: self)
}
in GameScene add the compliance to the BallNode protocol
class GameScene: SKScene, BallNodeDelegate
in GameScene when you create a Ball make sure you set it's delegate
let ball = BallNode()
ball.delegate = self
in GameScene add the nest. func to handle the clicks
func didClick(ball: BallNode) {
print("clicked ball")
}
You are comparing the exact touch point with the exact position of the node, which are very unlikely to ever be the same.
if positionOfTouch == node.position {
Instead, you'll need to test to see if the user's touch is close enough to the position of the ball.
One option is to use SKNode's contains function, which will handle this for you.
if node.contains(positionOfTouch) {
Side note: You'll probably want to use SKSpriteNode instead of SKShapeNode, as SKShapeNode has poor performance in SpriteKit.
Take a look at nodes(at:CGPoint) defined at SKNode to retrieve a list of the nodes at the touched position. You'll need to convert in between view coordinates and scene coordinates, though, using convertPoint(fromView). Documentation here and here.
I am building a game in which the player drags a piece around the gameboard. I wish to know what are all of the nodes underneath that piece, and I am getting odd results. Here is the touchesMoves func:
override func touchesMoved(touches: NSSet!, withEvent event: UIEvent!) {
let touch = touches.anyObject() as UITouch
let location = touch.locationInNode(self.parent)
self.position = location
println("checker x: \(location.x)")
println("node at point: \(self.nodeAtPoint(location))")
println("nodes at point: \(self.nodesAtPoint(location))")
}
The sprite moves around the board just fine, but what is reported as the nodeAtPoint is always the sprite being moved around (which kind of makes sense but is not useful. Any oddly, the nodesAtPoint is always reported as an empty array! How is this possible? What should I be doing differently?
Update: This continues to be a struggle. I want to keep the touch methods in the node itself, and not the scene. The most recent version of the touchesMoved is the following:
override func touchesMoved(touches: NSSet!, withEvent event: UIEvent!) {
// Turn off the touched bool:
touched = false
let touch = touches.anyObject() as UITouch
let location = touch.locationInNode(self.parent)
let loc = touch.locationInView(scene.view)
let loc2 = touch.locationInNode(scene)
self.position = location
println("checker: \(loc2.x), \(loc2.y)")
println("node at point: \(self.nodeAtPoint(loc2).name)")
println("nodes at point: \(self.nodesAtPoint(loc2))")
}
The nodesAtPoint array continues to be empty with one really odd exception. When hovering near the center of the scene, I get this:
nodes at point: [ name:'(null)' accumulatedFrame:{{-30, -19.80000114440918}, {60, 39.600002288818359}}]
There is not shape node there that I am aware of! Why am I not detecting the nodes I pass over?
I discovered the answer to this. Essentially I was trying to detect nodesAtPoint on self, which in this case was the node being moved around the screen. Once I changed that to self.scene, the nodesAtPoint array populated as expected. So, to be clear:
println("nodes at point: \(self.nodesAtPoint(loc2))")
Needed to be changed to this:
println("nodes at point: \(self.scene.nodesAtPoint(loc2))")
If self is SKScene, try to change
let location = touch.locationInNode(self.parent)
to
let location = touch.locationInNode(self)
because SKScene's parent is nil