I have touchesBegan discover and then place a node when a user taps the screen.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touchLocation = touches.first?.location(in: sceneView)
{
let hitTestResults = sceneView.hitTest(touchLocation, types: .featurePoint)
if let hitResult = hitTestResults.first
{
addDot(at: hitResult)
}
}
}
func addDot(at hitResult : ARHitTestResult)
{
let imagePlane = SCNPlane(width: 0.1, height: 0.1)
imagePlane.firstMaterial?.diffuse.contents = UIImage(named: "helmet_R.png")
let planeNode = SCNNode(geometry: imagePlane)
planeNode.position = SCNVector3(hitResult.worldTransform.columns.3.x, hitResult.worldTransform.columns.3.y, hitResult.worldTransform.columns.3.z)
planeNode.constraints = [SCNBillboardConstraint()]
sceneView.scene.rootNode.addChildNode(planeNode)
However, I'm confused as to removing the last placed node in the view.
I attempted a simple
planeNode.removeFromParentNode()
but that did not achieve the results I was hoping for. Therefore, how would I achieve the desired result? I'm aware that removing the parent node is arbitrary, but i'm unsure how to call the last child node placed.
You should create a reference to the last added node in your class.
var lastNode: SCNNode?
Then, let's say for example you want to remove the last node everytime you add a new one. In this case, you would create and add a new one, remove the lastNode and assign your newly created node as the last added node. So in your case, in the method addDot:
sceneView.scene.rootNode.addChildNode(planeNode)
lastNode?.removeFromParentNode()
lastNode = planeNode
Related
I am using the following to to place an object on a plane. It works but the position of the anchor is always coming out to be 0,0,0 even though it is based on the raycast result.
let results = arView.raycast(from: tappedLocation,
allowing: .estimatedPlane,
alignment: .horizontal)
if let result = results.first {
print(result.worldTransform) // contains values
let anchor = AnchorEntity(raycastResult: result)
print(anchor.position) // why 0,0,0
}
Any ideas?
It seems that position becomes zero when anchor is nil (transform becomes identity matrix).
If we call raycast with .existingPlaneGeometry, anchor will not be nil.
AnchorEntity has world space position inside.
This is not included in transform.
If we want to know world space position, we can calculate it by the following code. (It seems that there is no property that can be obtained...)
let anchorWorldTransform = anchorEntity.convert(transform: .identity, to: nil)
Alternative approach
If .init(raycastResult:) doesn't work, use alternative approach with .init(world:) initializer.
fileprivate func raycaster() {
guard let query = arView.makeRaycastQuery(from: arView.center,
allowing: .existingPlaneInfinite,
alignment: .horizontal)
else { return }
if let result = arView.session.raycast(query).first {
let anchor = AnchorEntity(world: result.worldTransform)
anchor.addChild(self.yourModel)
arView.scene.anchors.append(anchor)
}
}
Then:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.raycaster()
}
Modifying your code
Also, you may modify your code adding these lines:
let anchor = AnchorEntity(raycastResult: result)
let transform = Transform(matrix: result.worldTransform)
anchor.transform = transform
I am trying to make one single, straight line follow my finger after I touch a certain sprite object. I have it working so far except instead of one single line being drawn, infinite lines are drawn following the touch...
My Code:
import SpriteKit
import GameplayKit
class WireDrawTest: SKScene{
var drawingLayer: CAShapeLayer!
var redBox = SKSpriteNode()
var redBoxPoint = CGPoint(x: 445, y: 800)
var redBoxTouched:Int = -1
var currentTouch = touchesMoved
func drawLine(from: CGPoint, to: CGPoint) {
let line = SKShapeNode()
let path = CGMutablePath()
path.addLines(between: [from, to])
line.path = path
line.strokeColor = UIColor.yellow
line.lineWidth = 13
addChild(line)
}
override func didMove(to view: SKView) {
redBox = self.childNode(withName: "redBox") as! SKSpriteNode
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print(redBoxTouched)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
if let location = touch?.location(in: self){
let nodesArray = self.nodes(at: location)
if nodesArray.first?.name == "redBox" {
if redBoxTouched == -1 {
redBoxTouched = 1
}
}
if redBoxTouched == 1 {
drawLine(from: redBoxPoint, to: location)
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
print(redBoxTouched)
if redBoxTouched == 1 {
redBoxTouched = -1
}
}
}
This is a screenshot of the current result:
screenshot
TIA!!
Every time you call drawLine, you're creating a new shape node and adding it to the scene. However, you're never removing any of these shape nodes, so the number of lines is increasing as you move your finger.
One fix is to have an optional shape node that remembers the previous line, if any. Then just remove and discard the previous shape node if there is one whenever the touch moves. And when the touch ends, remove the line.
Or you could create one shape node and add it to the scene when the touch begins, change its path as the touch moves, and remove it from the scene when the touch ends. If I recall correctly, the shape node makes a copy of the path internally, so just having a mutable path and changing the path won't update the shape node. But I believe you can make a new path and assign to the shape node's path property to update the node.
I figured out the solution to this with thanks to bg2b settings me on the right track...
First of all instead of 'let line = SKShapeNode' being contained inside of the drawWire function, it needs to be outside of it, in the main scene setup.
From there I just added 'line.removeFromParent' to touchesEnded. Now it works! I don't know why this works but it does! LOL!
I place an object on a wall, then try to recognize tap on it, but hit test returns 0 objects. When I change Z position of the object and place it a little bit closer to cam, it's recognized well, but this isn't a solution, because planes are always changing and it can cover the object in any moment. How can I made hitTest work correctly and recognize my nodes behind planes? Or, maybe, I use the wrong method?
fileprivate func addNode(atPoint point: CGPoint) {
let hits = sceneView.hitTest(point, types: .existingPlaneUsingExtent)
if hits.count > 0, let firstHit = hits.first, let originNode = originNode {
let node = originNode.clone()
sceneView.scene.rootNode.addChildNode(node)
node.position = SCNVector3Make(firstHit.worldTransform.columns.3.x, firstHit.worldTransform.columns.3.y, firstHit.worldTransform.columns.3.z)
let resize = simd_float4x4(SCNMatrix4MakeScale(0.2, 0.2, 0.2))
let rotation = simd_float4x4(SCNMatrix4MakeRotation(.pi / 2, -1, 0, 0))
let transform = simd_mul(firstHit.worldTransform, resize)
let finalTransform = simd_mul(transform, rotation)
node.simdTransform = finalTransform
addedNodes.insert(node)
}
}
func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {
print("Unable to identify touches on any plane. Ignoring interaction...")
return
}
let touchPoint = touch.location(in: sceneView)
let hits = sceneView.hitTest(touchPoint, options: [SCNHitTestOption.boundingBoxOnly: true])
let filtered = hits.filter({ addedNodes.contains($0.node) })
print("\(hits.count) vs \(filtered.count), \(hits.first?.node.name ?? "no name")")
if let node = filtered.first?.node {
node.removeFromParentNode()
addedNodes.remove(node)
return
}
addPictureToPlane(atPoint: touchPoint)
}
addedNodes - set with added objects. When I added translating transform with changing Z coordinate at least on 0.05 (close to the camera) detecting working good. At least before plane changing and moving ahead the node.
I believe what you need to do is change your SCNHitTestSearchModeparameter which allows you to set:
Possible values for the searchMode option used with hit-testing
methods.
static let searchMode: SCNHitTestOption
Whereby:
The value for this key is an NSNumber object containing the raw
integer value of an SCNHitTestSearchMode constant.
From the Apple Docs there are three possible options you can use here:
case all
The hit test should return all possible results, sorted from nearest
to farthest.
case any
The hit test should return only the first object found, regardless of
distance.
case closest
The hit test should return only the closes object found.
Based on your question therefore, you would likely need to to utilise the all case.
As such your hitTest function would probably need to look something like this (remembering that self.augmentedRealityView refers to an ARSCNView):
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
//1. Get The Current Touch Location
guard let currentTouchLocation = touches.first?.location(in: self.augmentedRealityView) else { return }
//2. Perform An SCNHitTest Setting The SearchMode To 1 (All) Which Returns A List Of Results Sorted From Nearest To Farthest
if #available(iOS 11.0, *) {
let hitTestResults = self.augmentedRealityView.hitTest(currentTouchLocation, options: [SCNHitTestOption.searchMode: 1])
//3. Loop Through The Results & Get The Nodes
for index in 0..<hitTestResults.count{
let node = hitTestResults[index]
print(node)
}
}
}
I am trying to develop a basic game and I have a scene with several child nodes added to the root node. Each node has one of two names, either friend or enemy.
If a user touches one of the enemy nodes I want to remove all child nodes that are named enemy.
I have tried several things, but can't seem to get anything working.
In my touchesBegan function:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first!
let location = touch.location(in: gameView)
let hitList = gameView.hitTest(location, options: nil)
if let hitObject = hitList.first {
let node = hitObject.node
//This doesn't work
gameScene.rootNode.childNodes(passingTest: { (node, UnsafeMutablePointer<ObjCBool>) -> Bool in
node.removeFromParentNode()
}
}
I have also tried to use gameScene.rootNode.enumerateChildNodes(withName:) but I can't get that working either.
What I can get working is if I do something like this in there instead:
if node.name == "enemy" {
node.removeFromParentNode()
}
However this will only remove the single node that was hit, not all of them. How can I get all of the child nodes with a certain name in Swift with Scene Kit?
Filter out the nodes with matching name and remove them from the parent node:
gameScene.rootNode.childNodes.filter({ $0.name == "Enemy" }).forEach({ $0.removeFromParentNode() })
I have several programmatically made SKSpriteNode's. Some of them I want to move around, some I want to be static (have a fixed position). When adding physics to the nodes (need that to be able to do collision detection, right?) and set physicsBodyXXXX.dynamic = false they stay in the same position when moving other object over them. That's fine!
But, I'm still able to grab the node I want to be statically positioned, and move them around. How can I mask out the node I don't want to move in touches function? Or is there another solution?
Tried to find a property like static which made the node's position fixed, but can't find it...
Here's my code for auto generating nodes (in override func didMoveToView(view: SKView):
for Character in englishWord{
// Make letters:
let letterToMove = SKSpriteNode(imageNamed: "\(Character)")
//then setting size and position
var physicsBodyLetterToMove = SKPhysicsBody(rectangleOfSize: letterToMove.size)
physicsBodyLetterToMove.affectedByGravity = false
physicsBodyLetterToMove.allowsRotation = false
physicsBodyLetterToMove.dynamic = false
letterToMove.physicsBody = physicsBodyLetterToMove
self.addChild(letterToMove)
// Make empty boxes for the letters:
let letterRecBox = SKSpriteNode(imageNamed: "EmptyBox")
//then setting size and position
var physicsBodyLetterRecBox = SKPhysicsBody(rectangleOfSize: letterRecBox.size)
physicsBodyLetterToMove.affectedByGravity = false
physicsBodyLetterRecBox.dynamic = false
letterRecBox.physicsBody = physicsBodyLetterRecBox
self.addChild(letterRecBox)
}
So the touches func's:
var selected: [UITouch: SKNode] = [:]
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
/* Called when a touch begins */
selected = [:]
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
selected[touch as UITouch] = nodeAtPoint(location)
}
}
override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
for (touch, node) in selected{
if !contains([self], node){
let action = SKAction.moveTo(location, duration: 0.1)
node.runAction(SKAction.repeatAction(action, count: 1))
}
}
}
}
Any idea?
Setting dynamic to false will make the node unaffected by physics. SKActions and touch events are not considered physics so they will still affect your nodes that are not dynamic.
You could do something like:
YourSpriteNode.name = #"staticNode"; //Right after you create it
Then alter your touch method:
for (touch, node) in selected{
if !contains([self], node){
if(![node.name isEqualToString:#"staticNode"])
{
let action = SKAction.moveTo(location, duration: 0.1)
node.runAction(SKAction.repeatAction(action, count: 1))
}
}
}
The newly introduced if statement will prevent any node named "staticNode" from getting moved due to your SKActions. Other nodes will move as expected.
Thanx, that worked.
Only, I had to write it like this:
letterRecBox.name = "staticNode"
...
if !(node.name == "staticNode"){
...then do something...
}