Continuously rendering new child node objects on SceneKit scene in Swift - ios

I got a question about a Scene Kit project I'm doing. So I have this scene where I randomly spawn cubes and add them to my scene root node, the thing is that they are rendered at the beginning, but quickly they stop being rendered, so I don't see these new objects being spawned, I do see them again when I tap on the screen (so when I make an action on the scene or whatever) OR sometimes some of them are randomly rendered and I don't know why
I have tried setting rendersContinuously to true but this does not change anything.
Here is the cube spawning thread :
DispatchQueue.global(qos: .userInitiated).async {
while true {
self.spawnShape()
sleep(1)
}
}
Here is how I add them to the child nodes :
let geometryNode = SCNNode(geometry: geometry)
geometryNode.simdWorldPosition = simd_float3(Float.random(in: -10..<10), 2, shipLocation+Float.random(in: 20..<30))
self.mainView.scene!.rootNode.addChildNode(geometryNode)
And here's the endless action applied to the camera and my main node :
ship.runAction(SCNAction.repeatForever(SCNAction.moveBy(x: 0, y: 0, z: 30, duration: 1)))
camera.runAction(SCNAction.repeatForever(SCNAction.moveBy(x: 0, y: 0, z: 30, duration: 1)))
The added geometryNode cubes stop being rendered unless I tap on screen
How can I force the rendering of these new child node objects on the scene even when I don't touch the screen?
Thank you
EDIT asked by ZAY:
Here is the beginning of my code basically:
override func viewDidLoad() {
super.viewDidLoad()
guard let scene = SCNScene(named: "art.scnassets/ship.scn")
else { fatalError("Unable to load scene file.") }
let scnView = self.view as! SCNView
self.mainView = scnView
self.mainView.rendersContinuously = true
self.ship = scene.rootNode.childNode(withName: "ship", recursively: true)!
self.camera = scene.rootNode.childNode(withName: "camera", recursively: true)!
self.ship.renderingOrder = 1
self.ship.simdWorldPosition = simd_float3(0, 0, 0)
self.camera.simdWorldPosition = simd_float3(0, 15, -35)
self.mainView.scene = scene
DispatchQueue.global(qos: .userInitiated).async {
while true {
self.spawnShape()
sleep(1)
}
}
ship.runAction(SCNAction.repeatForever(SCNAction.moveBy(x: 0, y: 0, z: 30, duration: 1)))
camera.runAction(SCNAction.repeatForever(SCNAction.moveBy(x: 0, y: 0, z: 30, duration: 1)))
let tap = UILongPressGestureRecognizer(target: self, action: #selector(tapHandler))
tap.minimumPressDuration = 0
self.mainView.addGestureRecognizer(tap)
}
When I tap on screen, this code is called:
func tapHandler(gesture: UITapGestureRecognizer) {
let p = gesture.location(in: self.mainView)
let turnDuration: Double = 0.3
print("x: \(p.x) y: \(p.y)")
if gesture.state == .began {
if p.x >= self.screenSize.width / 2 {
self.delta = 0.5
}
else {
self.delta = -0.5
}
self.ship.runAction(SCNAction.rotateBy(x: 0, y: 0, z: self.delta, duration: turnDuration))
return
}
if gesture.state == .changed {
return
}
self.ship.runAction(SCNAction.rotateBy(x: 0, y: 0, z: -self.delta, duration: turnDuration))
}
Basically it rotates my ship to right or left depending on which side of screen I tap, and it displays the coordinates where I tapped in the console. When I release, the ship goes back to the initial position/rotation.

DispatchQueue.global(qos: .userInitiated).async {
while true {
self.spawnShape()
sleep(1)
}
}
Aren't you blocking that shared queue by doing that? Use a Timer if you want to run something periodically.

Related

How to add to a variable in SpriteKit

Relatively new to swift so apologies if I'm being stupid.
I'm using Xcode 13.3 beta 3
I have some code which spawns in a sprite named Monster and moves it from the right of the screen to the left at random speeds and at a random y location.
When it moves off screen it removes from parent and transitions to a loose screen.
what I want it to do when it leaves the screen is for it to add to a variable I've defined at the top of the class:
var pass = 0
here's the code for the monsters:
func addMonster() {
let monster = SKSpriteNode(imageNamed: "monster")
monster.physicsBody = SKPhysicsBody(rectangleOf: monster.size)
monster.physicsBody?.isDynamic = true
monster.physicsBody?.categoryBitMask = PhysicsCatagory.monster
monster.physicsBody?.contactTestBitMask = PhysicsCatagory.projectile
monster.physicsBody?.collisionBitMask = PhysicsCatagory.none
let actualY = random(min: monster.size.height/2, max: size.height - monster.size.height/2)
monster.position = CGPoint(x: size.width + monster.size.width/2, y: actualY)
addChild(monster)
let actualDuration = random(min: CGFloat(1.65), max: CGFloat(5.5))
let actionMove = SKAction.move(to: CGPoint(x: -monster.size.width/2, y: actualY), duration: TimeInterval(actualDuration))
let actionMoveDone = SKAction.removeFromParent()
let loseAction = SKAction.run() { [weak self] in
guard let `self` = self else {return}
let end = SKTransition.crossFade(withDuration: 0.5)
let gameOverScene = GameOverScene(size: self.size, won: false)
self.view?.presentScene(gameOverScene, transition: end)
}
monster.run(SKAction.sequence([actionMove,loseAction, actionMoveDone]))
}
what I want is the loose action to just add +1 to the pass variable then have an if statement for when pass is greater than 3 to run the code that goes to the loose screen.
Hope that all makes sense
any help would be much appreciated!

How to spawn images forever? Sprite-Kit

Im trying to create a dodgeball feature in my game and I need to repeatedly spawn a dodgeball and the current code I used (shown below) results in Thread 1: Exception: "Attemped to add a SKNode which already has a parent: name:'(null)' texture:[ 'dodgeball5' (500 x 500)] position:{-140.00019836425781, -55.124687194824219} scale:{1.00, 1.00} size:{30, 30} anchor:{0.5, 0.5} rotation:0.00". What am I doing incorrect?
class ClassicLevelScene: SKScene {
// Right Pointing Cannons
var rightPointingCannon: [SKReferenceNode] = []
// Dodgeball 5 Constants
var dodgeball5 = SKSpriteNode(imageNamed: "dodgeball5")
// Dodgeball SKActions
var dodgeballRepeat = SKAction()
let dodgeballMoveLeft = SKAction.moveBy(x: -400, y: 0, duration: 0.5)
let dodgeballMoveRight = SKAction.moveBy(x: 400, y: 0, duration: 0.5)
let dodgeballMoveDone = SKAction.removeFromParent()
let dodgeballWait = SKAction.wait(forDuration: 1)
var dodgeballLeftSequence = SKAction()
var dodgeballRightSequence = SKAction()
override func didMove(to view: SKView) {
// Cannon Setup
for child in self.children {
if child.name == "rightPointingCannon" {
if let child = child as? SKReferenceNode {
rightPointingCannon.append(child)
dodgeball5.position = child.position
run(SKAction.repeatForever(
SKAction.sequence([
SKAction.run(spawnDodgeball),
SKAction.wait(forDuration: 1.0)
])
))
}
}
}
// Dodgeball Right Sequence
dodgeballRightSequence = SKAction.sequence([dodgeballMoveRight, dodgeballMoveDone, dodgeballWait])
}
func spawnDodgeball() {
dodgeball5.zPosition = -1
dodgeball5.size = CGSize(width: 30, height: 30)
addChild(dodgeball5)
dodgeball5.run(dodgeballRightSequence)
}
}
Assuming single ball and ball can go off screen:
I would actually recommend against cloning the sprite.
Simply reuse it.
Once it's off the screen, you can reset it's position and speed and display it again. No need to create lots of objects, if you only need one.
One SKNode only can have 1 parent at time.
You need to create a new SKSpriteNode or clone the dodgeball5 before to add again on spawnDodgeball()
You can use
addChild(dodgeball5.copy())
instead of
addChild(dodgeball5)
But be make sure dodgeball5 have no parent before.
Your code seems to be unnecessarily complex.
all you need to do is copy() you ball and add it to your scene
Also all of your SKActions don't need to be declared at a class level
And it's not clear why you are creating an array of cannons when only one is instantiated
class ClassicLevelScene: SKScene {
// Dodgeball 5 Constants
var dodgeball5 = SKSpriteNode(imageNamed: "dodgeball5")
override func didMove(to view: SKView) {
setupCannonAndBall()
let dodgeballMoveRight = SKAction.moveBy(x: 400, y: 0, duration: 0.5)
let dodgeballMoveDone = SKAction.removeFromParent()
let dodgeballWait = SKAction.wait(forDuration: 1)
dodgeballRightSequence = SKAction.sequence([dodgeballMoveRight, dodgeballMoveDone, dodgeballWait])
//setup sequences ^ before spawning ball
let spawn = SKAction.run(spawnDodgeball)
let wait = SKAction.wait(forDuration: 1.0)
run(SKAction.repeatForever(SKAction.sequence([spawn, wait])))
}
func setupCannonAndBall() {
if let cannonRef = SKReferenceNode(fileNamed: "rightPointingCannon") {
dodgeball5.position = cannonRef.position
dodgeball5.isHidden = true
dodgeball5.size = CGSize(width: 30, height: 30)
dodgeball5.zPosition = -1
}
}
func spawnDodgeball() {
let dodgeball = dodgeball.copy()
dodgeball.isHidden = false
addChild(dodgeball)
dodgeball.run(dodgeballRightSequence)
}
}

How to play a video using on Camera (SCNNode) Image Tracking using Swift

I am new to ARKit . I am using Image Tracking to detect the image and dislpay the content beside that Image ..
On the left side of the image I am displaying some information of the image and on the right side I am showing the Web View .
I just want to display some video over the image (top).
Can you guys please help me to display a video over the image (top). I have attached the code for the Info and webview similary i want display to the video
func displayDetailView(on rootNode: SCNNode, xOffset: CGFloat) {
let detailPlane = SCNPlane(width: xOffset, height: xOffset * 1.4)
detailPlane.cornerRadius = 0.25
let detailNode = SCNNode(geometry: detailPlane)
detailNode.geometry?.firstMaterial?.diffuse.contents = SKScene(fileNamed: "DetailScene")
// Due to the origin of the iOS coordinate system, SCNMaterial's content appears upside down, so flip the y-axis.
detailNode.geometry?.firstMaterial?.diffuse.contentsTransform = SCNMatrix4Translate(SCNMatrix4MakeScale(1, -1, 1), 0, 1, 0)
detailNode.position.z -= 0.5
detailNode.opacity = 0
rootNode.addChildNode(detailNode)
detailNode.runAction(.sequence([
.wait(duration: 1.0),
.fadeOpacity(to: 1.0, duration: 1.5),
.moveBy(x: xOffset * -1.1, y: 0, z: -0.05, duration: 1.5),
.moveBy(x: 0, y: 0, z: -0.05, duration: 0.2)
])
)
}
func displayWebView(on rootNode: SCNNode, xOffset: CGFloat) {
// Xcode yells at us about the deprecation of UIWebView in iOS 12.0, but there is currently
// a bug that does now allow us to use a WKWebView as a texture for our webViewNode
// Note that UIWebViews should only be instantiated on the main thread!
DispatchQueue.main.async {
let request = URLRequest(url: URL(string: "https://www.youtube.com/watch?v=QvzVCOiC-qs")!)
let webView = UIWebView(frame: CGRect(x: 0, y: 0, width: 400, height: 672))
webView.loadRequest(request)
let webViewPlane = SCNPlane(width: xOffset, height: xOffset * 1.4)
webViewPlane.cornerRadius = 0.25
let webViewNode = SCNNode(geometry: webViewPlane)
webViewNode.geometry?.firstMaterial?.diffuse.contents = webView
webViewNode.position.z -= 0.5
webViewNode.opacity = 0
rootNode.addChildNode(webViewNode)
webViewNode.runAction(.sequence([
.wait(duration: 3.0),
.fadeOpacity(to: 1.0, duration: 1.5),
.moveBy(x: xOffset * 1.1, y: 0, z: -0.05, duration: 1.5),
.moveBy(x: 0, y: 0, z: -0.05, duration: 0.2)
])
)
}
}
I had called this methods in the below function ..
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
guard let imageAnchor = anchor as? ARImageAnchor else { return }
updateQueue.async {
let physicalWidth = imageAnchor.referenceImage.physicalSize.width
let physicalHeight = imageAnchor.referenceImage.physicalSize.height
// Create a plane geometry to visualize the initial position of the detected image
let mainPlane = SCNPlane(width: physicalWidth, height: physicalHeight)
mainPlane.firstMaterial?.colorBufferWriteMask = .alpha
// Create a SceneKit root node with the plane geometry to attach to the scene graph
// This node will hold the virtual UI in place
let mainNode = SCNNode(geometry: mainPlane)
mainNode.eulerAngles.x = -.pi / 2
mainNode.renderingOrder = -1
mainNode.opacity = 1
// Add the plane visualization to the scene
node.addChildNode(mainNode)
// Perform a quick animation to visualize the plane on which the image was detected.
// We want to let our users know that the app is responding to the tracked image.
self.highlightDetection(on: mainNode, width: physicalWidth, height: physicalHeight, completionHandler: {
// Introduce virtual content
self.displayDetailView(on: mainNode, xOffset: physicalWidth)
// Animate the WebView to the right
self.displayWebView(on: mainNode, xOffset: physicalWidth)
})
}
}
Any help is Appreciated ..
The effect you are trying to achieve is to play a video on a SCNNode. To do this, in your renderer func, you need to create an AVPlayer and use it to create a SKVideoNode. Then you need to create an SKScene, add the SKVideoNode. Next set the texture of your plane to that SKScene.
so this in the context of your code above:
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
guard let imageAnchor = anchor as? ARImageAnchor else { return }
updateQueue.async {
let physicalWidth = imageAnchor.referenceImage.physicalSize.width
let physicalHeight = imageAnchor.referenceImage.physicalSize.height
// Create a plane geometry to visualize the initial position of the detected image
let mainPlane = SCNPlane(width: physicalWidth, height: physicalHeight)
mainPlane.firstMaterial?.colorBufferWriteMask = .alpha
// Create a SceneKit root node with the plane geometry to attach to the scene graph
// This node will hold the virtual UI in place
let mainNode = SCNNode(geometry: mainPlane)
mainNode.eulerAngles.x = -.pi / 2
mainNode.renderingOrder = -1
mainNode.opacity = 1
// Add the plane visualization to the scene
node.addChildNode(mainNode)
// Perform a quick animation to visualize the plane on which the image was detected.
// We want to let our users know that the app is responding to the tracked image.
self.highlightDetection(on: mainNode, width: physicalWidth, height: physicalHeight, completionHandler: {
// Introduce virtual content
self.displayDetailView(on: mainNode, xOffset: physicalWidth)
// Animate the WebView to the right
self.displayWebView(on: mainNode, xOffset: physicalWidth)
// setup AV player & create SKVideoNode from avPlayer
let videoURL = URL(fileURLWithPath: Bundle.main.path(forResource: videoAssetName, ofType: videoAssetExtension)!)
let player = AVPlayer(url: videoURL)
player.actionAtItemEnd = .none
videoPlayerNode = SKVideoNode(avPlayer: player)
// setup SKScene to hold the SKVideoNode
let skSceneSize = CGSize(width: physicalWidth, height: physicalHeight)
let skScene = SKScene(size: skSceneSize)
skScene.addChild(videoPlayerNode)
videoPlayerNode.position = CGPoint(x: skScene.size.width/2, y: skScene.size.height/2)
videoPlayerNode.size = skScene.size
// Set the SKScene as the texture for the main plane
mainPlane.firstMaterial?.diffuse.contents = skScene
mainPlane.firstMaterial?.isDoubleSided = true
// setup node to auto remove itself upon completion
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: nil, using: { (_) in
DispatchQueue.main.async {
if self.debug { NSLog("video completed") }
// do something when the video ends
}
})
// play the video node
videoPlayerNode.play()
})
}
}
For 2022 ..
This is now (conceptually) simple,
guard let url = URL(string: " ... ") else { return }
let vid = AVPlayer(url: url)
.. some node .. .geometry?.firstMaterial?.diffuse.contents = vid
vid.play()
It's that easy.
You'll spend a lot of time monkeying with the mesh, buffers etc to get a good result, depending on what you're doing.

SceneKit: slow loading of UIView after contact between two nodes

I just found a really odd behavior with Scene Kit. I created a clean project to see if this would happen, and it did. Consider the following GameViewController class:
import UIKit
import QuartzCore
import SceneKit
class GameViewController: UIViewController, SCNPhysicsContactDelegate {
func physicsWorld(world: SCNPhysicsWorld, didBeginContact contact: SCNPhysicsContact) {
let label = UILabel(frame: CGRectMake(0, 0, 100, 100))
label.textColor = .whiteColor()
label.text = "Test"
self.view.addSubview(label)
}
override func viewDidLoad() {
// Controller
super.viewDidLoad()
// Scene
let scene = SCNScene()
scene.physicsWorld.contactDelegate = self
// Scene View
let scnView = self.view as! SCNView
scnView.scene = scene
// Camera
let camera = SCNCamera()
camera.usesOrthographicProjection = true
camera.orthographicScale = 8
// Camera Node
let cameraNode = SCNNode()
cameraNode.camera = camera
cameraNode.position = SCNVector3(x: -3, y: 7.5, z: 10)
cameraNode.eulerAngles = SCNVector3(x: -0.5, y: -0.5, z: 0)
scene.rootNode.addChildNode(cameraNode)
// Light
let light = SCNLight()
light.type = SCNLightTypeDirectional
// Light Node
let lightNode = SCNNode()
lightNode.light = light
lightNode.eulerAngles = SCNVector3Make(-1, -0.5, 0)
scene.rootNode.addChildNode(lightNode)
// Box Shape
let geometry = SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0)
geometry.materials.first?.diffuse.contents = UIColor(red: 255, green: 255, blue: 0, alpha: 1)
// Upper Box
let box = SCNNode(geometry: geometry)
box.physicsBody = SCNPhysicsBody(type: .Dynamic, shape: nil)
box.physicsBody?.categoryBitMask = 1
box.physicsBody?.contactTestBitMask = 2
scene.rootNode.addChildNode(box)
// Bottom Box
let box2 = SCNNode(geometry: geometry)
box2.position.y -= 5
box2.physicsBody = SCNPhysicsBody(type: .Static, shape: nil)
box2.physicsBody?.categoryBitMask = 2
box2.physicsBody?.contactTestBitMask = 1
box2.physicsBody?.affectedByGravity = false
scene.rootNode.addChildNode(box2)
}
}
What happens here is, we create a SCNScene, then we add the camera/lightning and create two boxes that will collide. We also set the controller as a delegate for the physics contact delegate.
But the important code is at the top, when the contact between the two objects occurs. We create an UILabel and position it at the top left of the screen. But here is the problem: It takes around seven seconds for the label to display. And it seems to always be this time. This seems rather odd, but you can try it yourself: just create a SceneKit project (using Swift), and replace the controller code with the one I provided.
Additional note: if you add print("a") in the physicsWorld() function, it will run immediately, so the code is being run at the right time, but for some reason the UILabel isn't displaying right away.
At first I thought your scene wasn't being rendered after the contact; hence not displaying the label, so put a scnView.playing = true line in. However, this resulted in the label never being displayed.
Next thought was that you're trying to do something on a thread you shouldn't be. Modifying UIKit views should really only be done on the main thread, and it's not clear what thread calls the SCNPhysicsContactDelegate functions. Given it's relatively easy to ensure it does run on the main thread...
func physicsWorld(world: SCNPhysicsWorld, didBeginContact contact: SCNPhysicsContact) {
dispatch_async(dispatch_get_main_queue()) {
let label = UILabel(frame: CGRectMake(0, 0, 100, 100))
label.textColor = .whiteColor()
label.text = "Test"
self.view.addSubview(label)
}
}
This fixes the delay for me, and also works with scnView.playing = true.

Preload Scenekit Scene

There seems to be a delay for when my SCNView is ready, and when it actually renders on my view - I always see a split second of white before the view appears. What can I do to prevent this?
func loadModels(frame:CGRect, type:ItemViewType, completion: (() -> Void)!) {
self.container = self.scene.rootNode.childNodeWithName("Container", recursively: true)!
self.item = self.container.childNodeWithName("Item", recursively: true)
self.pattern = self.item.childNodeWithName("TopLayer", recursively: true)
self.buttom = self.item.childNodeWithName("BottomRubber", recursively: true)!
self.shell = self.item.childNodeWithName("Shell", recursively: true)!
self.leftRimLit = self.scene.rootNode.childNodeWithName("leftRimLit", recursively: true)
self.specDirLit = self.scene.rootNode.childNodeWithName("specDirLit", recursively: true)
self.omni = self.scene.rootNode.childNodeWithName("omni", recursively: true)
self.scnView = SCNView(frame: frame)
self.scnView!.scene = self.scene
self.scnView!.autoenablesDefaultLighting = false
//default position
self.scnView?.pointOfView?.position = SCNVector3Make(-1.41011, -21.553, 3.34132)
self.scnView?.pointOfView?.eulerAngles = SCNVector3Make(1.58788, -0.0114007, -0.0574705)
self.scnView?.pointOfView?.scale = SCNVector3Make(1, 1, 1.5)
self.item.pivot = SCNMatrix4MakeTranslation(0, 0, 4)
self.item.position = SCNVector3(x: 0.387027979, y: 9.58867455, z: 3.71733069)
self.item.scale = SCNVector3(x: 1.0999999, y: 1.0999999, z: 1.10000002)
self.item.rotation = SCNVector4(x: 0.865282714, y: 0.455411941, z: 0.20948948, w: 2.20000005)
self.container.pivot = SCNMatrix4MakeTranslation(0, 0, 0)
self.setBackground(type)
self.setItemPosition(type)
completion()
}
All of the positioning a background is correct once it does finally appear.
self.SO3DItemView = SO3DItemModel(frame: self.view.frame)
self.SO3DItemView!.loadModels(self.view.frame, type: .Tada, completion:
{() -> Void in
self.view.insertSubview(self.SO3DItemView!.scnView!, belowSubview:self.overlay)
UIView.animateWithDuration(0.7, delay: 0.0, options: .CurveEaseIn, animations:
{() -> Void in
self.overlay.alpha = 0.0
}, completion:{ (finished: Bool) -> Void in
self.view.sendSubviewToBack(self.overlay)
})
Right now I have an overlay that fades out to mitigate some of this, but on slow devices (iPhone 5) I can still see the white before the scnView snaps in to complete.
The SCNSceneRenderer protocol (your SCNView) has a function prepareObjects:withCompletionHandler: that will accept a SCNScene. This will instruct SceneKit to perform any initialisation needed, otherwise it will wait for the scene to be added before transferring data to the GPU, etc.
FWIW I've settled on using background threads to get around this same issue. Simple example below...
let scene = SCNScene()
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
dispatch_async(dispatch_get_global_queue(priority, 0)) {
//lengthy scene setup process
let slowNode = SCNNode(geometry:SCNSphere(radius:1.0))
sleep(2)
dispatch_async(dispatch_get_main_queue())
{
scene.rootNode.addChildNode(slowNode)
}
}
let quickNode = SCNNode(geometry:SCNBox(width:1.0, height:1.0, length:1.0))
scene.rootNode.addChildNode(quickNode)
scnView.scene = scene
If you were to run this code, which I haven't, you should see the box appear immediately when the app launches. Two seconds later the sphere will be added to the scene.

Resources