Reality Composer - ar hittest not working? - swift

Alright, Im weeding thru the provided RealityComposer game at https://developer.apple.com/videos/play/wwdc2019/609/ and am trying to figure out how to have an entity move where the user taps.
I can reference my objects in my .rc scene like this:
struct ARViewContainer: UIViewRepresentable {
let arView = ARView(frame: .zero)
func makeUIView(context: Context) -> ARView {
// arView = ARView(frame: .zero)
// Load the "Box" scene from the "Experience" Reality File
let boxAnchor = try! Experience.loadBox()
//*Notifications
setupNotifications(anchor: boxAnchor)
//*Access vars
if boxAnchor.box1 != nil {
//print(box1.position.x)
/// Set local position
boxAnchor.box1!.position = [1.0, 0.0, 0.5]
/// Set world position
//boxAnchor.box1.setPosition([0.5, 0.2, 1.5], relativeTo: nil)
}
I know this involves some form arhitest, however with the following I get the error:
UIView has no member last?
func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
//1. Get The Current Touch Location
guard let currentTouchLocation = touches.first?.location(in: self.arView),
//2. Get The Tapped Node From An SCNHitTest
let hitTestResultNode = self.arView.hitTest(currentTouchLocation, with: nil).last?.node else { return } //error here
//3. Loop Through The ChildNodes
for node in hitTestResultNode.childNodes{
//4. If The Node Has A Name Then Print It Out
if let validName = node.name{
print("Node\(validName) Is A Child Node Of \(hitTestResultNode)")
}
}
}
Im pretty lost as to whether Im going about this at all correctly. Im referencing Detect touch on SCNNode in ARKit but this does not deal with RealityComposer.
How can I do this?

The simplest way to get an Entity to move via touch is to use the built in gestures provides by Apple; which you can read more about here: Apple Documentation
To enable your gesture of choice (in this case translation), first ensure that in RealityComposer that you set the partcipates value to true on every Entity you wish to interact with.
This then adds a Has Collision component to the Entitywhich is simply:
A component that gives an entity the ability to collide with other entities that also have collision components.
Using this you can install built in gestures to do the heavy lifting for you.
Assuming we have the default RealityKit example setup in Xcode, and we have selected participates for the box, its a simple as this to enable the user to pan it using the touch location:
class ViewController: UIViewController {
#IBOutlet var arView: ARView!
override func viewDidLoad() {
super.viewDidLoad()
//1. Load The Box Anchor
let boxAnchor = try! Experience.loadBox()
//2. Check The SteelBox Has A Collision Component & Add The Desired Gesture
if let hasCollision = boxAnchor.steelBox as? HasCollision {
arView.installGestures(.translation, for: hasCollision)
}
//Add The Box To The Scene Hierachy
arView.scene.anchors.append(boxAnchor)
}
}
Alternatively if you wanted to do some heavy lifting (who doesn't!) then you could do something like this by creating a global variable which will reference the Entity that you have selected (which in this case is called Steel Box):
//Variable To Store Our Currently Selected Entity
var currentEntity: Entity?
Then using touchesBegan and touchesMoved you can something like this:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
/* Get The Current Touch Location In The ARView
Perform A HitTest For The Nearest Entity
Checks That The Tapped Entity Is The Steel Box
Set It As The Current Entity
*/
guard let touchLocation = touches.first?.location(in: arView),
let tappedEntity = arView.hitTest(touchLocation, query: .nearest, mask: .default).first?.entity,
tappedEntity.name == "Steel Box" else {
return
}
currentEntity = tappedEntity
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
/* Get The Current Touch Location In The ARView
Perform A HitTest For An Existing Plane
Move The Current Entity To The New Transfortm
Set It As The Current Entity
*/
guard let touchLocation = touches.first?.location(in: arView),
let currentEntity = currentEntity else {
return
}
if let transform = arView.hitTest(touchLocation, types: .existingPlaneUsingExtent).first?.worldTransform {
currentEntity.move(to: transform, relativeTo: nil, duration: 0.1)
}
}
Hope it helps point the in the right direction.

Related

Select multiple nodes while dragging

I'm trying to make a game where the player at certain times draw a pattern on the screen. My solution for this is to add multiple nodes on the screen that are touchable via an extension of SKSpriteNode.
When the player touches a node, I want to call touchesmoved, and add all nodes touched to an array.
Then, when the player stops touching the screen, I want to match that array to another array, and something happens.
I've been playing around with the updates function and try to run a function each update loop, but it didn't work very well. I've also tried making the gameScene class a delegate of my touchableShapeNode Class, but I struggled to make it work.
class TouchableShapeNode: SKShapeNode {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if (name != nil) {
print("\(name ?? "node") touched")
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if (name != nil) {
print("\(name ?? "node") touched")
}
}
}
My problem is that the only node that gets selected right now is the first node I touch, not the ones the player's finger move over. Right now I'm just printing the name of the touched node.
I'm not exactly sure what you're after, but here's a little program that does the following:
Puts 15 red blocks on screen
As you drag around the screen, any nodes you touch are added to a set.
When you stop touching, all touched nodes have their colour changed to green.
When you start a new touch, the set of touched nodes is emptied and all nodes return to their starting colour (red).
To use, simply start a new, empty SpriteKit project and replace gameScene.swift with this code.
import SpriteKit
import UIKit
class GameScene: SKScene {
let shipSize = CGSize(width: 25, height: 25)
let normalColour = UIColor.red
let touchedColour = UIColor.green
var touchedNodes = Set<SKSpriteNode>()
override func didMove(to view: SKView) {
let sceneWidth = self.scene?.size.width
let sceneHeight = self.scene?.size.height
// Add 15 colour sprites to the screen in random places.
for _ in 1...15 {
let ship = SKSpriteNode(color: normalColour, size: shipSize)
ship.position.x = CGFloat.random(in: -sceneWidth!/2...sceneWidth!/2) * 0.7
ship.position.y = CGFloat.random(in: -sceneHeight!/2...sceneHeight!/2) * 0.7
ship.name = "ship"
addChild(ship)
}
}
// When the screen is toucheed, empty the 'touchedNodes' set and rest all nodes back to their normal colour.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
resetTouchedNodes()
}
// As the touch moves, if we touch a node then add it to our 'touchedNodes' set.
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
let location = touch!.location(in: self)
// If there is a node at the touch location, add it to our 'touchedSprites' set.
if let touchedNode = selectNodeForTouch(location) {
touchedNodes.insert(touchedNode)
}
}
// When the touch ends, make all nodes that were touched change colour.
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
for node in touchedNodes {
node.color = touchedColour
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
resetTouchedNodes()
}
// Return the first sprite where the user touched the screen, else nil
func selectNodeForTouch(_ touchLocation: CGPoint) -> SKSpriteNode? {
let nodes = self.nodes(at: touchLocation)
for node in nodes {
if node is SKSpriteNode {
return (node as! SKSpriteNode)
}
}
return nil
}
// Clear the touchedSprites set and return all nodes on screen to their normal colour
func resetTouchedNodes() {
touchedNodes.removeAll()
enumerateChildNodes(withName: "//ship") { node, _ in
let shipNode = node as! SKSpriteNode
shipNode.color = self.normalColour
}
}
}
You could amend this in various ways. For example, you could change the colour of the sprite immediately you touch it in touchesMoved etc.

SCNParticleSystem not adding to SCNNode on touchesbegan

I am placing multiple SCNNodes in my view on load of my application.
On touchesbegan I am removing whatever node is tapped on.
All of this works so far so I know my code is working however just adding a SCNParticleSystem is giving me issues.
I have put two stars (**) by the lines that are not working
// On tap
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Register tap
let touch = touches.first!
// Get location
let location = touch.location(in: sceneView)
// Create a hit
let hitList = sceneView.hitTest(location, options: nil)
if let hitObject = hitList.first {
// Get node from hit
let node = hitObject.node
if node.name == target {
score += 3
playAudio(fileName: "two")
**let explosion = SCNParticleSystem(named: "stars.scnp", inDirectory: nil)
**node.addParticleSystem(explosion!)
node.removeFromParentNode()
// Async call
DispatchQueue.main.async {
node.removeFromParentNode()
self.scoreLabel.text = String(self.score)
}
}
}
}
How do I attach the particle to the node?
If you want to see the explosion and remove the node, just set a wait timer, for example:
let explosion = SCNParticleSystem(named: "stars.scnp", inDirectory: nil)
node.addParticleSystem(explosion!)
let waitAction = SCNAction.wait(duration: 3)
node.runAction(waitAction, completionHandler: {
self.node.removeFromParentNode()
self.scoreLabel.text = String(self.score)
})
You can post the wait action on any node, so if you have a central node in the scene, it will work with that as well

Cant drag ARKit SCNNode?

Ok, like everyone else I am having trouble dragging/translating an SCNNode in ARKit/world space. Ive looked at Dragging SCNNode in ARKit Using SceneKit and all the popular questions, as well as the code from Apple https://github.com/gao0122/ARKit-Example-by-Apple/blob/master/ARKitExample/VirtualObject.swift
Ive tried to simplify as much as possible and just did what I would in a normal scene kit game - http://dayoftheindie.com/tutorials/3d-games-graphics/t-scenekit-3d-picking-dragging/
I can get the tapped object and store the current finger pos no problem with:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let results = gameView.hitTest(touch.location(in: gameView), types: [ARHitTestResult.ResultType.featurePoint])
//TAP Test
let hits = gameView.hitTest(touch.location(in: gameView), options: nil)
currentTapPos = getARPos(hitFeature: hitFeature) //TAP POSITION
if let tappedNode = hits.first?.node {
My issue is, however, doing this in update - there is no animation. The object just appears wherever I tap, and overall not working -
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let results = gameView.hitTest(touch.location(in: gameView), types: [ARHitTestResult.ResultType.featurePoint])
guard let hitFeature = results.last else { return }
testNode.position = currentTapPos
I convert to SCNVector3 with this func:
func getARPos(hitFeature: ARHitTestResult)->SCNVector3
{
let hitTransform = SCNMatrix4.init(hitFeature.worldTransform)
let hitPosition = SCNVector3Make(hitTransform.m41,
hitTransform.m42,
hitTransform.m43)
return hitPosition
}
I have tried:
-translate by vector
-pan gesture (this screwed up other functions)
-SCNAction.moveTo
What can I do here? Whats wrong?
I agree with #Rickster that the Apple Code provides a much more robust example, however, I have this and it seems to work smoothly (much more so when you are using PlaneDetection (since feature points are much more sporadic):
I don't make use of touchesBegan but simply do the work in touchesMoved instead:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
//1. Get The Current Touch Point
guard let currentTouchPoint = touches.first?.location(in: self.augmentedRealityView),
//2. Get The Next Feature Point Etc
let hitTest = augmentedRealityView.hitTest(currentTouchPoint, types: .existingPlane).first else { return }
//3. Convert To World Coordinates
let worldTransform = hitTest.worldTransform
//4. Set The New Position
let newPosition = SCNVector3(worldTransform.columns.3.x, worldTransform.columns.3.y, worldTransform.columns.3.z)
//5. Apply To The Node
nodeToDrag.simdPosition = float3(newPosition.x, newPosition.y, newPosition.z)
}
I might be going about this completely the wrong way (and if I am I would love to get feedback).
Anyway, I hope it might help... You can see a quick video of it in action here: Dragging SCNNode

Nodes not moving

(Swift 3)
import SpriteKit
import GameplayKit
class GameScene: SKScene {
var object = SKSpriteNode()
override func didMove(to view: SKView) {
object = self.childNode(withName: "dot") as! SKSpriteNode
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
object.run(SKAction.move(to: location, duration: 0.3))
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
object.run(SKAction.move(to: location, duration: 0.3))
}
}
override func update(_ currentTime: TimeInterval) {
}
}
This code is designed to allow me to drag a "dot" around the scene, however... I just cant! It won't budge! I've looked online, but I can't find anything. I tried pasting this code into a fresh project, and it worked there, but even deleting and replacing my "dot" node doesn't seem to help on my main project.
So... any ideas?
Confused has already said everything you must know about your issue.
I try to add details that help you with the code. First of all, you should use optional binding when you search a node with self.childNode(withName so if this node does not exist your project does not crash.
Then, as explained by Confused, you should add controls before you call your actions to be sure that your action is not already running.
I make an example here below:
import SpriteKit
class GameScene: SKScene {
var object = SKSpriteNode.init(color: .red, size: CGSize(width:100,height:100))
override func didMove(to view: SKView) {
if let sprite = self.childNode(withName: "//dot") as? SKSpriteNode {
object = sprite
} else { //dot not exist so I draw a red square..
addChild(object)
}
object.position = CGPoint(x:self.frame.midX,y:self.frame.midY)
}
func makeMoves(_ destination: CGPoint) {
if object.action(forKey: "objectMove") == nil {
object.run(SKAction.move(to: destination, duration: 0.3),withKey:"objectMove")
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
makeMoves(location)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
makeMoves(location)
}
}
}
Output:
The fact it works in a new, barebones project, and not in your fuller project indicates the problem is in the fuller project. Without a glimpse at that, it's pretty hard to tell what's actually going wrong.
Peculiar, to me, is the use of the SKAction to do the move in touchesMoved(). If you think about this in terms of how SKActions works, you're continually adding new move actions, at whatever rate the touchscreen is picking up changes in position. This is (almost always) much faster than 0.3 seconds. More like 0.016 seconds. So this should be causing all manner of problems, in its own right.
Further, you need something in the GameScene's touchesMoved() to ascertain what's being touched, and then, based on touching the dot, move it, specifically. This, I assume, is your "object".
A simpler way of doing this might be to use touchesMoved in a subclass of SKSpriteNode that represents and creates your dot instance.

Moving multiple nodes at once from touch with SpriteKit

I have a game that has 3 SKSpriteNodes that the user can move around using the touchesBegan and touchesMoved. However, when the users moves nodeA and passes another node called nodeB, nodeB follows nodeA and so on.
I created an array of SKSpriteNodes and used a for-loop to make life easier.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let nodes = [nodeA, nodeB, nodeC]
for touch in touches {
let location = touch.location(in: self)
for node in nodes {
if node!.contains(location) {
node!.position = location
}
}
}
}
Everything is working except when nodeA is moving and cross paths with nodeB, nodeB follows nodeA.
How can I make it so that when the user is moving nodeA and nodeA passes through nodeB that nodeB would not follow nodeA.
Instead of doing slow searches, have a special node variable for your touched node
class GameScene
{
var touchedNodeHolder : SKNode?
override func touchesBegan(.....)
{
for touch in touches {
guard touchNodeHandler != nil else {return} //let's not allow other touches to interfere
let pointOfTouch = touch.location(in: self)
touchedNodeHolder = nodeAtPoint(pointOfTouch)
}
}
override func touchesMoved(.....)
{
for touch in touches {
let pointOfTouch = touch.location(in: self)
touchedNodeHolder?.position = pointOfTouch
}
}
override func touchesEnded(.....)
{
for touch in touches {
touchedNodeHolder = nil
}
}
}
After some experimenting, I found a way to go about this problem.
Instead of selecting the node by the position of the touch, I found that selecting the node by its name property did the trick.
The code shown is implemented inside the touchesMoved and touchesBegan methods
//name of the nodes
let nodeNames = ["playO", "playOO", "playOOO", "playX", "playXX", "playXXX"]
//the actual nodes
let player = [playerO, playerOO, playerOOO, playerX, playerXX, playerXXX]
for touch in touches {
let pointOfTouch = touch.location(in: self)
let tappedNode = atPoint(pointOfTouch)
let nameOfTappedNode = tappedNode.name
for i in 0..<nodeNames.count {
if nameOfTappedNode == nodeNames[i] {
z += 1
player[i]!.zPosition = CGFloat(z)
print(player[i]!.zPosition)
player[i]!.position = pointOfTouch
}
}
}