SceneKit – Display multiple scenes in same SCNView - ios

Is there a way I can display multiple scenes in the same SCNView at the same time? So let's say I have a ball in ball.dae and a pyramid in pyramid.dae. So these are separate scenes and I would like to show both at the same time.
Currently I only have one and I create it like this:
let scene = SCNScene(named: "world1.dae")!
If I just create another one below then it just displays the second one... :/
Thanks in advance!

Ok I already figured it out. The thing is to not add another scene but nodes from the 2nd scene to the first like this:
let scene = SCNScene(named: "world1.dae")!
let subScene = SCNScene(named: "pyramid.dae")!
let pyramid = subScene.rootNode.childNodeWithName("pyramid", recursively: true)!
scene.rootNode.addChildNode(pyramid)

I have made two SCNNode extension functions that clone nodes
from another scene:
extension SCNNode
{
func addNodeClonesWithNames(#fromScene: SCNScene, nodeNames: [String] )
{
for nodename in nodeNames
{
self.addClonedChildNode( fromScene.rootNode.childNodeWithName(nodename, recursively: true)! )
}
}
func addClonedChildNode(node: SCNNode)
{
self.addChildNode(node.clone() as! SCNNode)
}
}
in your view controller, loading two scenes here:
let scnMain = SCNScene(named: "world1.dae")!
let scnAssets = SCNScene(named: "props.dae")!
// then add some nodes from the assets scene to the main scene's
// root node by name:
scnMain.rootNode.addNodeClonesWithNames(fromScene: scnAssets,
nodeNames: ["btnStartEngine","btnStop","btnReverse","btnEject"])
//
Of course, this works not only on a root node, but on all SCNNodes.
I use cloning here so that the original stays intact for multiple usage.
I hope this is useful. So glad this is Swift.. Regards. Ted

In the latest version of Xcode 6.2 with iOS 8.2, I had to slightly change the code
func getNodeFromScene(colladaFile: String) -> SCNNode {
var subScene = SCNScene(named: "art.scnassets/\(colladaFile).dae")
let geometryNode = subScene?.rootNode.childNodeWithName(colladaFile, recursively: true)!
return geometryNode!
}
subScene to subScene?

Related

Is it safe (or good programming style) to initialize a lazy var with nil vars?

Let me explain a little better what I mean since it's kinda tricky to understand.
I'm creating a prototype for a videogame. Every level inherits the main rules from a SKScene called SceneLogic:
class SceneLogic: SKScene, SKPhysicsContactDelegate {
// Set up the physics, the contacts, touches and so on...
}
class Level1: SceneLogic { }
class Level2: SceneLogic { }
Every level has its own .sks file which specifies the different icon to show in the HUD. In order to create a kind of "game engine" I thought to init every kind of graphics inside the SceneLogic class by lazy var and ignore them if the current level doesn't need it.
Let me explain with an example
class SceneLogic: SKScene, SKPhysicsContactDelegate {
// Text, available only for level 1
private lazy var textTopHUD = childNode(withName: "textTop") as! SKLabelNode
// Icon, available only for levels 3,4,5
private lazy var iconBottomHUD = childNode(withName: "iconBottom") as! SKSpriteNode
// Icon, available only for level 2
private lazy var iconLeftHUD = childNode(withName: "iconLeft") as! SKSpriteNode
func initGame(level: Int) {
switch mode {
case 1: // Level 1
textTopHUD.text = "Level 1"
case 2: // Level 2
iconLeftHUD.position = ....
}
}
}
The fact is: for level 1, iconBottomHUD is nil, for level 2 textTopHUD is nil... but the app doesn't crash since the var is lazy and it won't be called for some levels.
My question is: is it a good programming style? Is it safe to use lazy in this way?
The thing about lazy properties is that it defers the execution of the initialization code. So in your case it doesn't really matter since they are actually nil. So you defer the init of nothing basically. If i were you i would basically either make it as a computed propert as such:
private lazy var iconBottomHUD: SKSpriteNode = {
guard let node = childNode(withName: "iconBottom") as? SKSpriteNode else {
fatalError()
}
return node
}
Or make it as #JavierRivarola mentioned, make it protocol based.

Updating Labels on a second View Controller

I have several different ViewControllers set up in my project. The first one acts as a 'landing' page and and on pressing a button the view is directed to a following ViewController.
There are various buttons and labels on there that I want to use to provide information and run a method. The buttons all work ok, but I have followed every tutorial I can to get the labels to update based on a method, and I can't seem to get them to do so.
I know the methods are called correctly as I have put print statements in there to check.
Basic idea is, program plays a series of beeps separated by a delay (bleep_time), this changes each level (bleep_level) and there are several steps (bleep_step) in each level. Ive simplified the arrays containing this data to save space.
I have successfully created a number of tutorial projects and the labels all update correctly, but they only use one ViewController.
Is the issue I am facing due to using 2 ViewControllers?
ViewController 1
import UIKit
class Bleep_Test_Menu: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
View Controller 2
import UIKit
import AVFoundation
class Bleep_Test_Level_1: UIViewController {
// Global Variables
var audioPlayer: AVAudioPlayer?
// Bleep test delays
var bleep_time = [1,2,3,4]
// Levels of bleep test
var bleep_level = [1,2,3,4]
// Level Label
#IBOutlet weak var bleepTestLevel1Level: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
// Function to run repeated bleeps
#IBAction func bleepTestLevel1Start(_ sender: Any) {
var i = 0
tracker = false
let length = bleep_time.count
while i < length {
var bleeplevel = bleep_level[i]
bleepTestLevel1Level.text = "\(bleeplevel)"
playSaveSound()
sleep(UInt32(bleep_time[i]))
i += 1
}
}
// Function to play sounds
func playSaveSound(){
let path = Bundle.main.path(forResource: "Sound1.wav", ofType: nil)!
let url = URL(fileURLWithPath: path)
do {
//create your audioPlayer in your parent class as a property
audioPlayer = try AVAudioPlayer(contentsOf: url)
audioPlayer?.play()
} catch {
print("couldn't load the file")
}
}
}
The only error I keep getting in the console is this;
"Tests[53616:891478] - changing property contentsGravity in transform-only layer, will have no effect"
But I can't seem to find anything that relates to my issue.

Alternative to animation(forKey:) (now deprecated)?

I am working with iOS 11 (for ARKit) and while many point to a sample app for SceneKit from Apple with a Fox, I am having problem with the extension it uses in that sample project (file) to add animations:
extension CAAnimation {
class func animationWithSceneNamed(_ name: String) -> CAAnimation? {
var animation: CAAnimation?
if let scene = SCNScene(named: name) {
scene.rootNode.enumerateChildNodes({ (child, stop) in
if child.animationKeys.count > 0 {
animation = child.animation(forKey: child.animationKeys.first!)
stop.initialize(to: true)
}
})
}
return animation
}
}
It seems that this extension is very handy but I am not sure how to migrate this now that it is deprecated? Is it built into SceneKit by default now?
The documentation didn't really show much info on why it was deprecated or where to go from here.
Thanks
TL;DR: examples of how to use new APIs can be found in Apple's sample game (search for SCNAnimationPlayer)
Even though animation(forKey:) and its sister methods that work with CAAnimation have been deprecated in iOS11, you can continue using them – everything will work.
But if you want to use new APIs and don't care about backwards compatibility (which you wouldn't need in the case of ARKit anyway, because it's only available since iOS11), read on.
The newly introduced SCNAnimationPlayer provides a more convenient API compared to its predecessors. It is now easier to work with animations in real time.
This video from WWDC2017 would be a good starting point to learn about it.
As a quick summary: SCNAnimationPlayer allows you to change animation's speed on the fly. It provides a more intuitive interface for animation playback using methods such as play() and stop() compared to adding and removing CAAnimations.
You also can blend two animations together which, for example, can be used to make smooth transitions between them.
You can find examples of how to use all of this in the Fox 2 game by Apple.
Here's the extension you've posted adapted to use SCNAnimationPlayer (which you can find in the Character class in the Fox 2 sample project):
extension SCNAnimationPlayer {
class func loadAnimation(fromSceneNamed sceneName: String) -> SCNAnimationPlayer {
let scene = SCNScene( named: sceneName )!
// find top level animation
var animationPlayer: SCNAnimationPlayer! = nil
scene.rootNode.enumerateChildNodes { (child, stop) in
if !child.animationKeys.isEmpty {
animationPlayer = child.animationPlayer(forKey: child.animationKeys[0])
stop.pointee = true
}
}
return animationPlayer
}
}
You can use it as follows:
Load the animation and add it to the corresponding node
let jumpAnimation = SCNAnimationPlayer.loadAnimation(fromSceneNamed: "jump.scn")
jumpAnimation.stop() // stop it for now so that we can use it later when it's appropriate
model.addAnimationPlayer(jumpAnimation, forKey: "jump")
Use it!
model.animationPlayer(forKey: "jump")?.play()
Lësha Turkowski's answer without force unwraps.
extension SCNAnimationPlayer {
class func loadAnimationPlayer(from sceneName: String) -> SCNAnimationPlayer? {
var animationPlayer: SCNAnimationPlayer?
if let scene = SCNScene(named: sceneName) {
scene.rootNode.enumerateChildNodes { (child, stop) in
if !child.animationKeys.isEmpty {
animationPlayer = child.animationPlayer(forKey: child.animationKeys[0])
stop.pointee = true
}
}
}
return animationPlayer
}
}
Here's an example of SwiftUI and SceneKit
import SwiftUI
import SceneKit
struct ScenekitView : UIViewRepresentable {
#Binding var isPlayingAnimation: Bool
let scene = SCNScene(named: "art.scnassets/TestScene.scn")!
func makeUIView(context: Context) -> SCNView {
// create and add a camera to the scene
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)
let scnView = SCNView()
return scnView
}
func updateUIView(_ scnView: SCNView, context: Context) {
scnView.scene = scene
// allows the user to manipulate the camera
scnView.allowsCameraControl = true
controlAnimation(isAnimating: isPlayingAnimation, nodeName: "TestNode", animationName: "TestAnimationName")
}
func controlAnimation(isAnimating: Bool, nodeName: String, animationName: String) {
guard let node = scene.rootNode.childNode(withName: nodeName, recursively: true) else { return }
guard let animationPlayer: SCNAnimationPlayer = node.animationPlayer(forKey: animationName) else { return }
if isAnimating {
print("Play Animation")
animationPlayer.play()
} else {
print("Stop Animation")
animationPlayer.stop()
}
}
}
struct DogAnimation_Previews: PreviewProvider {
static var previews: some View {
ScenekitView(isPlayingAnimation: .constant(true))
}
}
A 2023 example.
I load typical animations like this:
func simpleLoadAnim(filename: String) -> SCNAnimationPlayer {
let s = SCNScene(named: filename)!
let n = s.rootNode.childNodes.filter({!$0.animationKeys.isEmpty}).first!
return n.animationPlayer(forKey: n.animationKeys.first!)!
}
So,
laugh = simpleLoadAnim(filename: "animeLaugh") // animeLaugh.dae
giggle = simpleLoadAnim(filename: "animeGiggle")
You then, step one, have to add them to the character:
sally.addAnimationPlayer(laugh, forKey: "laugh")
sally.addAnimationPlayer(giggle, forKey: "giggle")
very typically you would have only one going at a time. So set the weights, step two.
laugh.blendFactor = 1
giggle.blendFactor = 0
to play or stop an SCNAnimationPlayer it's just step three
laugh.play()
giggle.stop()
Almost certainly, you will have (100s of lines) of your own code to blend between animations (which might take only a short time, 0.1 secs, or may take a second or so). To do so, you would use SCNAction.customAction.
If you prefer you can access the animation, on, the character (sally) using the keys. But really the "whole point" is you can just start, stop, etc etc the SCNAnimationPlayer.
You will also have lots of code to set up the SCNAnimationPlayer how you like (speed, looping, mirrored, etc etc etc etc etc etc etc etc)
You will need THIS very critical answer to get collada files (must be separate character / anim files) working properly https://stackoverflow.com/a/75093081/294884
Once you have the various SCNAnimationPlayer animations working properly, it is quite easy to use, run, blend etc animes.
The essential sequence is
each anime must be in its own .dae file
load each anime files in to a SCNAnimationPlayer
"add" all the animes to the character in question
program the blends
then simply play() or stop() the actual SCNAnimationPlayer items (don't bother using the keys on the character, it's a bit pointless)

How to request data from another scene in SpriteKit

I have 2 SKScenes. The first scene has 2 buttons: a male player button and a female player button. The second scene has 2 hidden images: a male player image and a female player images.
If male player is selected in Scene1, I want to make the male player image visible in Scene2, but I'm having trouble passing the data between SKScenes. I'm using Sprite Kit and programming in Swift. Please advise.
Here is what I have tried so far:
// Scene1:
import SpriteKit
class Scene1: SKScene {
var malePlayer = SKSpriteNode()
var femalePlayer = SKSpriteNode()
var maleSelect = false
var femaleSelect = false
// in touches began..
if self.nodeAtPoint(location) == self.malePlayer {
maleSelect = true
// then i present scene2
} else if self.nodeAtPoint(location) == self.femalePlayer {
femaleSelect = true
//then i present scene2
// in scene 2:
class Scene2: SKScene {
var playerSelected: Scene1()
// did move to view
var boy = SKSpriteNode()
var girl = SKSpriteNode()
if self.playerSelected.maleSelect == true {
self.addChild(boy)
} else if self.playerSelected.femaleSelect == true {
self.addChild(girl)
}
There are several ways to do this. Here is one example that uses NSUserDefaults. One reason that makes this a good option is that the selection will persist even after the app is closed and can be reloaded when the app is next opened.
To save the selection, use the following code:
if self.nodeAtPoint(location) == self.malePlayer {
//save the selection in NSUserDefaults
NSUserDefaults.standardUserDefaults().setBool(true, forKey:"maleSelected")
//present next scene
} else if self.nodeAtPoint(location) == self.femalePlayer {
//save the selection
NSUserDefaults.standardUserDefaults().setBool(false, forKey:"maleSelected")
//present next scene
}
The value is now saved in local storage. When you need to access the selection in your other scenes, grab the value from NSUserDefaults:
So Scene2 might looking something like this:
class Scene2: SKScene {
var playerSelected: Scene1()
// did move to view
var boy = SKSpriteNode()
var girl = SKSpriteNode()
//grab the selection value
let maleSelected = NSUserDefaults.standardUserDefaults().boolForKey("maleSelected")
if maleSelected == true {
self.addChild(boy)
} else {
self.addChild(girl)
}
}
In scene 2 the code:
var playerSelected: Scene1()
Creates a new instance of the scene 1 class, it does not provide a link back to your original scene 1. To access scene 1's properties you need to pass a reference to it from scene 2.

How do you rename the default scene in the iOS SpriteKit template?

I'm very new to both Swift and iOS development.
I have been working on a game based off Apple's SpriteKit (with GameplayKit integration) template.
All was working fine until I renamed GameScene.swift and GameScene.sks to MapMainScene.swift and MapMainScene.sks. I made sure to change this code in GameViewController.swift:
override func viewDidLoad() {
super.viewDidLoad()
// Load 'GameScene.sks' as a GKScene. This provides gameplay related content
// including entities and graphs.
if let scene = GKScene(fileNamed: "GameScene") {
// Get the SKScene from the loaded GKScene
if let sceneNode = scene.rootNode as! GameScene? {
// Copy gameplay related content over to the scene
sceneNode.entities = scene.entities
sceneNode.graphs = scene.graphs
// Set the scale mode to scale to fit the window
sceneNode.scaleMode = .aspectFill
// Present the scene
if let view = self.view as! SKView? {
view.presentScene(sceneNode)
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
}
}
}
}
to:
override func viewDidLoad() {
super.viewDidLoad()
// Load 'MainMapScene.sks' as a GKScene. This provides gameplay related content
// including entities and graphs.
if let scene = GKScene(fileNamed: "MainMapScene") {
// Get the SKScene from the loaded GKScene
if let sceneNode = scene.rootNode as! MainMapScene? {
// Copy gameplay related content over to the scene
sceneNode.entities = scene.entities
sceneNode.graphs = scene.graphs
// Set the scale mode to scale to fit the window
sceneNode.scaleMode = .aspectFill
// Present the scene
if let view = self.view as! SKView? {
view.presentScene(sceneNode)
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
}
}
}
}
I also changed the following in Gamescene.swift:
class GameScene: SKScene {
// Some code
}
to this in MainMapScene.swift:
class MainMapScene: SKScene {
// Some code
}
but the app crashes upon launch. The output of the console says:
2016-07-21 16:29:35.593592 MyGame[10656:1944648] [User Defaults] CFPrefsPlistSource<0x6080000e5e00> (Domain: kCFPreferencesAnyApplication, User: kCFPreferencesCurrentUser, ByHost: No, Container: (null)) is waiting for writes to complete so it can determine if new data is available
2016-07-21 16:29:35.603729 MyGame[10656:1944648] [FenceWorkspace] creating a CA fence port (5d13)
2016-07-21 16:29:35.638 MyGame[10656:1944648] *** Terminating app due to uncaught exception 'NSInvalidUnarchiveOperationException', reason: '*** -[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (GameScene) for key (root); the class may be defined in source code or a library that is not linked'
*** First throw call stack:
So it looks like there is still a reference to GameScene somewhere in my project but I have done a project-wide search and there is no GameScene anywhere to be found.
I'm sure this is something really straightforwards. Do I need to change an outlet or something in the storyboard editor?? I can reproduce this problem by trying to change the name of GameScene in a new project too so I know it's not something specific to my project.
GameScene is actually referenced in one more place, and the error coming from NSKeyedArchiver should clue you in as to where: it's encoded in the .sks file you're loading. You can find it in the "Custom Class" tab of the .sks file in Xcode's editor.
This setting tells NSKeyedUnarchiver what class the encoded scene should be when you load the .sks file. This should be the last remaining reference to GameScene, so this should complete your renaming!

Resources