How to place a custom object (.scn) in ARKit on tap? - ios

I'm trying to place a custom object in ARKit when the user taps the screen. I figured out how to place an object by creating it in code but would like to place an object that has been imported into project (ex. ship.scn). Here is my code to place object in code and its working:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {return}
let results = sceneView.hitTest(touch.location(in: sceneView), types: [ARHitTestResult.ResultType.featurePoint])
guard let hitFeature = results.last else { return}
let hitTransform = SCNMatrix4.init(hitFeature.worldTransform)
let hitPosition = SCNVector3Make(hitTransform.m41, hitTransform.m42, hitTransform.m43)
createBall(hitPosition: hitPosition)
}
func createBall(hitPosition: SCNVector3) {
let newBall = SCNSphere(radius: 0.05)
let newBallNode = SCNNode(geometry: newBall)
newBallNode.position = hitPosition
self.sceneView.scene.rootNode.addChildNode(newBallNode)
}
I tried creating this function to place an imported .scn file but when I tap the screen nothing shows up:
func createCustomScene(objectScene: SCNScene?, hitPosition: SCNVector3) {
guard let scene = objectScene else {
print("Could not load scene")
return
}
let childNodes = scene.rootNode.childNodes
for childNode in childNodes {
sceneNode.addChildNode(childNode)
}
}
called like this: createCustomScene(objectScene: SCNScene(named: "art.scnassets/ship.scn"), hitPosition: hitPosition)
Does anyone have any clue why the .scn isn't showing up when the screen is tapped?

You should consider adding your childNode to the sceneView instead of adding it to your sceneNode.
for childNode in childNodes {
childNode.position = hitPosition
self.sceneView.scene.rootNode.addChildNode(childNode)
}

Related

How to get an AnchorEntity.position to align with a touch event

I tap the screen to fire a laser:
let tap = UITapGestureRecognizer(target: self, action: #selector(fireLaser(_:)))
arView.addGestureRecognizer(tap)
func fireLaser(_ recognizer: UITapGestureRecognizer) {
let anchor = ARAnchor(name: "LaserBeam", transform: arView.cameraTransform.matrix)
arView.session.add(anchor: anchor)
}
I want the laser to go to where I actually tapped and disappear from there. In touchesBegan I get the hitPosition and eventually set it to the AnchorEntity.position when I place the object like so
if let hitPosition = hitPosition {
anchorEntity.position = hitPosition
}
The problem is where I tap on the screen the laser goes to another position. For example if I tap on the bottom of the screen the laser is firing from the top of the screen and disappearing there, when it should occur from the bottom.
How can I get the laser to go to where I tap?
var hitPosition: SIMD3<Float>?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
guard let results = arView.hitTest(touch.location(in: arView), types: [ARHitTestResult.ResultType.featurePoint]) else { return }
guard let hitTest = results.last else { return }
let transform = hitTest.worldTransform
let hitPosition = SCNVector3(transform.columns.3.x, transform.columns.3.y, transform.columns.3.z)
let simd3Position = simd_float3(hitPosition)
self.hitPosition = simd3Position
}
func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
for anchor in anchors {
if let anchorName = anchor.name, anchorName == "LaserBeam" {
placeObject(named: anchorName, for: anchor)
}
}
}
func placeObject(named entityName: String, for anchor: ARAnchor) {
do {
let laserEntity = try ModelEntity.load(named: entityName)
let anchorEntity = AnchorEntity(anchor: anchor)
if let hitPosition = hitPosition {
anchorEntity.position = hitPosition
}
anchorEntity.addChild(laserEntity)
arView.scene.addAnchor(anchorEntity)
} catch let err as NSError {
print(err.debugDescription)
}
}
screenshot of me tapping the bottom of the screen but the laser firing and disappearing from the top of the screen.

Casting as a sksprite node

I keep getting a SIGBRT error on this line of code:
let spriteTapped : SKSpriteNode = (nodeTapped as? SKSpriteNode)!
I am using it to check if my sprite is tapped and which one is tapped, I get this error whenever anything other than a SKSpriteNode is tapped, any ideas on how I might fix this?
If nodeTapped is not a SKSpriteNode then the conditional cast
nodeTapped as? SKSpriteNode
evaluates to nil, and the forced unwrapping with ! crashes.
Better use optional binding:
if let spriteTapped = nodeTapped as? SKSpriteNode {
// ... do something with spriteTapped ...
} else {
// tapped node is not a SKSpriteNode
}
or
guard let spriteTapped = nodeTapped as? SKSpriteNode else {
return // tapped node is not a SKSpriteNode
}
// ... do something with spriteTapped ...
If you have an array of nodes then
for case let spriteTapped as SKSpriteNode in nodesTapped {
// ... do something with spriteTapped ...
}
can be used to iterate over all sprite nodes in the list (and
silently ignore other nodes).
Answer by #Martin R is totally correct.
Let me add more details about detecting which nodes or sprites have been tapped.
If you add this to your scene
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
let tappedNodes = nodes(at: location)
let tappedSprites = tappedNodes.compactMap { $0 as? SKSpriteNode }
}
you can easily get the list of all the nodes (tappedNodes) and the sprites (tappedSprites) having the same coordinates of your touch.

ARKit : Handle tap to show / hide a node

I am new to ARKit , and i am trying an example to create a SCNBox on tap location. What i am trying to do is on initial touch i create a box and on the second tap on the created box it should be removed from the scene. I am doing the hit test. but it keeps on adding the box. I know this is a simple task, but i am unable to do it
#objc func handleTap(sender: UITapGestureRecognizer) {
print("hande tapp")
guard let _ = sceneView.session.currentFrame
else { return }
guard let scnView = sceneView else { return }
let touchLocation = sender.location(in: scnView)
let hitTestResult = scnView.hitTest(touchLocation, types: [ .featurePoint])
guard let pointOfView = sceneView.pointOfView else {return}
print("point \(pointOfView.name)")
if hitTestResult.count > 0 {
print("Hit")
if let _ = pointOfView as? ARBox {
print("Box Available")
}
else {
print("Adding box")
let transform = hitTestResult.first?.worldTransform.columns.3
let xPosition = transform?.x
let yPosition = transform?.y
let zPosition = transform?.z
let position = SCNVector3(xPosition!,yPosition!,zPosition!)
basketCount = basketCount + 1
let newBasket = ARBox(position: position)
newBasket.name = "basket\(basketCount)"
self.sceneView.scene.rootNode.addChildNode(newBasket)
boxNodes.append(newBasket)
}
}
}
pointOfView of a sceneView, is the rootnode of your scene, which is one used to render your whole scene. For generic cases, it usually is an empty node with lights/ camera. I don't think you should cast it as ARBox/ or any type of SCNNode(s) for that matter.
What you probably can try is the logic below (hitResults are the results of your hitTest):
if hitResults.count > 0 {
if let node = hitResults.first?.node as SCNNode? (or ARBox) {
// node.removeFromParentNode()
// or make the node opaque if you don't want to remove
else {
// add node.

SpriteKit reference nodes from level editor

I'm using the scene editor in SpriteKit to place color sprites and assign them textures using the Attributes Inspector. My problem is trying to figure out how to reference those sprites from my GameScene file. For example, I'd like to know when a sprite is a certain distance from my main character.
Edit - code added
I'm adding the code because for some reason, appzYourLife's answer worked great in a simple test project, but not in my code. I was able to use Ron Myschuk's answer which I also included in the code below for reference. (Though, as I look at it now I think the array of tuples was overkill on my part.) As you can see, I have a Satellite class with some simple animations. There's a LevelManager class that replaces the nodes from the scene editor with the correct objects. And finally, everything gets added to the world node in GameScene.swift.
Satellite Class
func spawn(parentNode:SKNode, position: CGPoint, size: CGSize = CGSize(width: 50, height: 50)) {
parentNode.addChild(self)
createAnimations()
self.size = size
self.position = position
self.name = "satellite"
self.runAction(satAnimation)
self.physicsBody = SKPhysicsBody(circleOfRadius: size.width / 2)
self.physicsBody?.affectedByGravity = false
self.physicsBody?.categoryBitMask = PhysicsCategory.satellite.rawValue
self.physicsBody?.contactTestBitMask = PhysicsCategory.laser.rawValue
self.physicsBody?.collisionBitMask = 0
}
func createAnimations() {
let flyFrames:[SKTexture] = [textureAtlas.textureNamed("sat1.png"),
textureAtlas.textureNamed("sat2.png")]
let flyAction = SKAction.animateWithTextures(flyFrames, timePerFrame: 0.14)
satAnimation = SKAction.repeatActionForever(flyAction)
let warningFrames:[SKTexture] = [textureAtlas.textureNamed("sat8.png"),
textureAtlas.textureNamed("sat1.png")]
let warningAction = SKAction.animateWithTextures(warningFrames, timePerFrame: 0.14)
warningAnimation = SKAction.repeatActionForever(warningAction)
}
func warning() {
self.runAction(warningAnimation)
}
Level Manager Class
import SpriteKit
class LevelManager
{
let levelNames:[String] = ["Level1"]
var levels:[SKNode] = []
init()
{
for levelFileName in levelNames {
let level = SKNode()
if let levelScene = SKScene(fileNamed: levelFileName) {
for node in levelScene.children {
switch node.name! {
case "satellite":
let satellite = Satellite()
satellite.spawn(level, position: node.position)
default: print("Name error: \(node.name)")
}
}
}
levels.append(level)
}
}
func addLevelsToWorld(world: SKNode)
{
for index in 0...levels.count - 1 {
levels[index].position = CGPoint(x: -2000, y: index * 1000)
world.addChild(levels[index])
}
}
}
GameScene.swift - didMoveToView
world = SKNode()
world.name = "world"
addChild(world)
physicsWorld.contactDelegate = self
levelManager.addLevelsToWorld(self.world)
levelManager.levels[0].position = CGPoint(x:0, y: 0)
//This does not find the satellite nodes
let satellites = children.flatMap { $0 as? Satellite }
//This does work
self.enumerateChildNodesWithName("//*") {
node, stop in
if (node.name == "satellite") {
self.satTuple.0 = node.position
self.satTuple.1 = (node as? SKSpriteNode)!
self.currentSatellite.append(self.satTuple)
}
}
The Obstacle class
First of all you should create an Obstacle class like this.
class Obstacle: SKSpriteNode { }
Now into the scene editor associate the Obstacle class to your obstacles images
The Player class
Do the same for Player, create a class
class Player: SKSpriteNode { }
and associate it to your player sprite.
Checking for collisions
Now into GameScene.swift change the updated method like this
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
let obstacles = children.flatMap { $0 as? Obstacle }
let player = childNodeWithName("player") as! Player
let obstacleNearSprite = obstacles.contains { (obstacle) -> Bool in
let distance = hypotf(Float(player.position.x) - Float(obstacle.position.x), Float(player.position.y) - Float(obstacle.position.y))
return distance < 100
}
if obstacleNearSprite {
print("Oh boy!")
}
}
What does it do?
The first line retrieves all your obstacles into the scene.
the second line retrieves the player (and does crash if it's not present).
Next it put into the obstacleNearSprite constant the true value if there is at least one Obstacle at no more then 100 points from Player.
And finally use the obstacleNearSprite to print something.
Optimizations
The updated method gets called 60 times per second. We put these 2 lines into it
let obstacles = children.flatMap { $0 as? Obstacle }
let player = childNodeWithName("player") as! Player
in order to retrieves the sprites we need. With the modern hardware it is not a problem but you should save references to Obstacle and Player instead then searching for them in every frame.
Build a nice game ;)
you will have to loop through the children of the scene and assign them to local objects to use in your code
assuming your objects in your SKS file were named Obstacle1, Obstacle2, Obstacle3
Once in local objects you can check and do whatever you want with them
let obstacle1 = SKSpriteNode()
let obstacle2 = SKSpriteNode()
let obstacle3 = SKSpriteNode()
let obstacle3Location = CGPointZero
func setUpScene() {
self.enumerateChildNodesWithName("//*") {
node, stop in
if (node.name == "Obstacle1") {
self.obstacle1 = node
}
else if (node.name == "Obstacle2") {
self.obstacle2 = node
}
else if (node.name == "Obstacle3") {
self.obstacle3Location = node.position
}
}
}

Different results between iOS 7.1 and 8 while using Swift

I am practicing swift by writing a simple game. However, I noticed something weird happening. I am getting different results when I run my app on iOS 7.1 compared to iOS 8. I am trying to downcast an SKNode to a custom class that I wrote called CircleNode, which inherits from SKSpriteNode. Here is the class:
import UIKit
import SpriteKit
class CircleNode: SKSpriteNode {
var _hasMoved = false
var _touchingCircles:NSMutableArray = NSMutableArray()
var _isTouchingObject = false
var _selectedForDeletion = false
var _isBeingTouched = false
var _xOffset:CGFloat = 0
var _yOffset:CGFloat = 0
func addTouchingCircle(touchingCircle:CircleNode) {
_touchingCircles.addObject(touchingCircle)
_isTouchingObject = true
}
func removeAllCircles(){
self._selectedForDeletion = true
for circ :CircleNode! in _touchingCircles {
if circ._selectedForDeletion == false {
circ.removeAllCircles()
}
}
self.removeFromParent()
}
}
The code that is resulting in weird results is :
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
/* Called when a touch begins */
for touch: AnyObject in touches {
let locationInScene = touch.locationInNode(self)
var selectedNode = self.nodeAtPoint(locationInScene)
let node = selectedNode as? CircleNode
if node {
println("NODE FOUND:circle")
} else {
println("NULL")
}
}
}
In iOS 8, when I click on one of the on screen circles, it prints "NODE FOUND:circle". However, when I do the same on iOS 7.1 it prints "NULL". I have been trying to figure out why this is happening for days, but I can't seem to figure it out. It seems like in iOS 8, var selectedNode = self.nodeAtPoint(locationInScene) actually returns my CircleNode, but in iOS 7.1, it doesn't recognize the node. Any help? Thanks in advance!
There seems to be an issue with the way the XCode templates have changed between XCode 6.0 and 6.1 that causes problems when working with SpriteKit scenes in Swift.
The problem is actually with the initialization of the SKScene from your controller, not the node itself: XCode 6.1 implements the 'unarchiveFromFile' extension method which doesn't appear to work well with Swift on iOS7.1
I got round this by using the following extension:
import UIKit
import SpriteKit
extension SKNode {
class func unarchiveFromFile<T: SKScene>(file : NSString) -> T {
let path = NSBundle.mainBundle().pathForResource(file, ofType: "sks")
var sceneData = NSData(contentsOfFile: path!, options: .DataReadingMappedIfSafe, error: nil)
var archiver = NSKeyedUnarchiver(forReadingWithData: sceneData!)
archiver.setClass(self.classForKeyedUnarchiver(), forClassName: "SKScene")
let scene = archiver.decodeObjectForKey(NSKeyedArchiveRootObjectKey) as T
archiver.finishDecoding()
return scene
}
class func safeInit<T: SKScene>(size: CGSize, _ skFile: NSString) -> T {
let currentVersion = UIDevice.currentDevice().systemVersion
let isIOS8 = currentVersion.compare("8.0", options: NSStringCompareOptions.NumericSearch) != NSComparisonResult.OrderedAscending
if isIOS8 {
let scene = T.unarchiveFromFile(skFile)
scene.size = size
return scene as T
} else {
return T(size: size)
}
}
}
And then calling this from your controller using the following code:
let skView = self.view as SKView
let scene = GameScene.safeInit(skView.bounds.size, "GameScene") as GameScene
That seems to work for me. It feels like a bit of a hack but I can't see any alternative option that actually works.

Resources