In SpriteKit, is it possible to add elements programmatically to the scene, so when .sks is opened, they display? - sprite-kit

In SpriteKit, is it possible to add elements programmatically to the scene, so when .sks is opened, they display? Currently I have my GameScene (which extends SKScene) class that opens up already added elements in the scene editor, from the .sks file, using the UnarchiveFromFile method pattern. This method looks like this:
#implementation SKScene (Unarchive)
+ (instancetype)unarchiveFromFile:(NSString *)file {
/* Retrieve scene file path from the application bundle */
NSString *nodePath = [[NSBundle mainBundle] pathForResource:file ofType:#"sks"];
/* Unarchive the file to an SKScene object */
NSData *data = [NSData dataWithContentsOfFile:nodePath
options:NSDataReadingMappedIfSafe
error:nil];
NSKeyedUnarchiver *arch = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
[arch setClass:self forClassName:#"SKScene"];
SKScene *scene = [arch decodeObjectForKey:NSKeyedArchiveRootObjectKey];
[arch finishDecoding];
return scene;
}
#end
How can I do the opposite thing from the unarchiveFromFile, ie. to go from the code and add sprites to the .sks file. After that, the unarchiveFromFile would be triggered and the rest goes as it is right now. Any suggestions guys? Thanks.

You don't want update your .sks file in code, even if you managed to work out how to do it. I think what you want to do is to add elements to the scene in didMoveTo(view:)
https://developer.apple.com/documentation/spritekit/skscene/1519607-didmove
didMove(to:) Called immediately after a scene is presented by a view.
Discussion This method is intended to be overridden in a subclass. You
can use this method to implement any custom behavior for your scene
when it is about to be presented by a view. For example, you might use0
this method to create the scene’s contents.
Here's an example to add a border:
override func didMove(to view: SKView) {
/* Setup your scene here */
// drawBorder()
let border = drawSKShapeNode([CGPoint.zero,
CGPoint(x: 0, y: frame.height),
CGPoint(x: frame.width, y: frame.height),
CGPoint(x: frame.width, y: 0)],
withWidth: 10,
inColour: SKColor.yellow,
called: "border")
border.zPosition = 200
addChild(border)
}
// Helper function to draw an SkShapeNode
func drawSKShapeNode(_ withPoints: [CGPoint], withWidth width: CGFloat, inColour colour: SKColor, called name: String) -> SKShapeNode {
var points = withPoints
let shape = SKShapeNode(points: &points, count: points.count)
shape.lineWidth = width
shape.strokeColor = colour
shape.name = name
return shape
}

I solved my actual problem! - by just putting in a graph on the scene and getting it in the code. It is a NavigationGraph, and works fine. The programatic adding however didn't work.

Related

Call a function from a different swift file

XCode Version 12.4 (12D4e)
The first SKScene class in Functions.swift included the buildBackground function which I want to call in different SKScene class.
class Functions: SKScene {
func buildBackground(backgroundPath: String){
let texture = SKTexture(imageNamed: backgroundPath)
let background = SKSpriteNode(texture: texture)
background.size = CGSize(width: 512, height: 512)
background.position = CGPoint(x: 0, y: 0)
background.name = "BACKGROUND"
addChild(background)
print("Background loaded")
}
}
I'm trying to call this function in the second SKScene class in the Room.swift file
I created an instance of Functions in Room and called my function just like that:
public class Room: SKScene {
let functions = Functions()
override public func didMove(to view: SKView) {
functions.buildBackground(backgroundPath: "roomBackground")
}
}
I got "Cannot find 'Functions' in scope" on the "let functions = Functions()" line. After running a build, I got my message "Background loaded", so I guess, the function was called, however, my wallpaper SKSpriteNode has not appeared in my scene. After I tried to move buildBackground method inside Room.swift class, it all works, but there are a lot of different scenes in my project (Room1, Room2, etc.) so I don't want to copy-paste this method each file.
since you are overriding a public function, you probably need to use self :
self.functions.buildBackground(...)

ARKit – How to display the feed from a virtual SCNCamera placed on SCNPlane?

I put some objects in AR space using ARKit and SceneKit. That works well. Now I'd like to add an additional camera (SCNCamera) that is placed elsewhere in the scene attached and positioned by a common SCNNode. It is oriented to show me the current scene from an other (fixed) perspective.
Now I'd like to show this additional SCNCamera feed on i.Ex. a SCNPlane (as the diffuse first material) - Like a TV screen. Of course I am aware that it will only display the SceneKit content which stays in the camera focus and not rest of the ARKit image (which is only possible by the main camera of course). A simple colored background then would be fine.
I have seen tutorials that describes, how to play a video file on a virtual display in ARSpace, but I need a realtime camera feed from my own current scene.
I defined this objects:
let camera = SCNCamera()
let cameraNode = SCNNode()
Then in viewDidLoad I do this:
camera.usesOrthographicProjection = true
camera.orthographicScale = 9
camera.zNear = 0
camera.zFar = 100
cameraNode.camera = camera
sceneView.scene.rootNode.addChildNode(cameraNode)
Then I call my setup function to place the virtual Display next to all my AR stuff, position the cameraNode as well (pointing in the direction where objects stay in the scene)
cameraNode.position = SCNVector3(initialStartPosition.x, initialStartPosition.y + 0.5, initialStartPosition.z)
let cameraPlane = SCNNode(geometry: SCNPlane(width: 0.5, height: 0.3))
cameraPlane.geometry?.firstMaterial?.diffuse.contents = cameraNode.camera
cameraPlane.position = SCNVector3(initialStartPosition.x - 1.0, initialStartPosition.y + 0.5, initialStartPosition.z)
sceneView.scene.rootNode.addChildNode(cameraPlane)
Everything compiles and loads... The display shows up at the given position, but it stays entirely gray. Nothing is displayed at all from the SCNCamera I put in the scene. Everything else in the AR scene works well, I just don't get any feed from that camera.
Hay anyone an approach to get this scenario working?
To even better visualize, I add some more print screens.
The following shows the Image trough the SCNCamera according to ARGeo's input. But it takes the whole screen, instead of displaying its contents on a SCNPlane, like I need.
The next Print screen actually shows the current ARView result as I got it using my posted code. As you can see, the gray Display-Plane remains gray - it shows nothing.
The last print screen is a photomontage, showing the expected result, as I'd like to get.
How could this be realized? Am I missing something fundamental here?
After some research and sleep, I came to the following, working solution (including some inexplainable obstacles):
Currently, the additional SCNCamera feed is not linked to a SCNMaterial on a SCNPlane, as it was the initial idea, but I will use an additional SCNView (for the moment)
In the definitions I add an other view like so:
let overlayView = SCNView() // (also tested with ARSCNView(), no difference)
let camera = SCNCamera()
let cameraNode = SCNNode()
then, in viewDidLoad, I setup the stuff like so...
camera.automaticallyAdjustsZRange = true
camera.usesOrthographicProjection = false
cameraNode.camera = camera
cameraNode.camera?.focalLength = 50
sceneView.scene.rootNode.addChildNode(cameraNode) // add the node to the default scene
overlayView.scene = scene // the same scene as sceneView
overlayView.allowsCameraControl = false
overlayView.isUserInteractionEnabled = false
overlayView.pointOfView = cameraNode // this links the new SCNView to the created SCNCamera
self.view.addSubview(overlayView) // don't forget to add as subview
// Size and place the view on the bottom
overlayView.frame = CGRect(x: 0, y: 0, width: self.view.bounds.width * 0.8, height: self.view.bounds.height * 0.25)
overlayView.center = CGPoint(x: self.view.bounds.width * 0.5, y: self.view.bounds.height - 175)
then, in some other function, I place the node containing the SCNCamera to my desired position and angle.
// (exemplary)
cameraNode.position = initialStartPosition + SCNVector3(x: -0.5, y: 0.5, z: -(Float(shiftCurrentDistance * 2.0 - 2.0)))
cameraNode.eulerAngles = SCNVector3(-15.0.degreesToRadians, -15.0.degreesToRadians, 0.0)
The result, is a kind of window (the new SCNView) at the bottom of the screen, displaying the same SceneKit content as in the main sceneView, viewed trough the perspective of the SCNCamera plus its node position, and that very nicely.
In a common iOS/Swift/ARKit project, this construct generates some side effects, that one may struggle into.
1) Mainly, the new SCNView shows SceneKit content from the desired perspective, but the background is always the actual physical camera feed. I could not figure out, how to make the background a static color, by still displaying all the SceneKit content. Changing the new scene's background property affects also the whole main scene, what is actually NOT desired.
2) It might sound confusing, but as soon as the following code get's included (which is essential to make it work):
overlayView.scene = scene
the animation speed of the entire scenes (both) DOUBLES! (Why?)
I got this corrected by adding/changing the following property, which restores the animation speed behavour almost like normal (default):
// add or change this in the scene setup
scene.physicsWorld.speed = 0.5
3) If there are actions like SCNAction.playAudio in the project, all the effects will no longer play - as long as I don't do this:
overlayView.scene = nil
Of course, the additional SCNView stops working but everything else gets gets back to its normal.
Use this code (as a starting point) to find out how to setup a virtual camera.
Just create a default ARKit project in Xcode and copy-paste my code:
import UIKit
import SceneKit
import ARKit
class ViewController: UIViewController, ARSCNViewDelegate {
#IBOutlet var sceneView: ARSCNView!
override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = self
sceneView.showsStatistics = true
let scene = SCNScene(named: "art.scnassets/ship.scn")!
sceneView.scene = scene
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(0, 0, 1)
cameraNode.camera?.focalLength = 70
cameraNode.camera?.categoryBitMask = 1
scene.rootNode.addChildNode(cameraNode)
sceneView.pointOfView = cameraNode
sceneView.allowsCameraControl = true
sceneView.backgroundColor = UIColor.darkGray
let plane = SCNNode(geometry: SCNPlane(width: 0.8, height: 0.45))
plane.position = SCNVector3(0, 0, -1.5)
// ASSIGN A VIDEO STREAM FROM SCENEKIT-RECORDER TO YOUR MATERIAL
plane.geometry?.materials.first?.diffuse.contents = capturedVideoFromSceneKitRecorder
scene.rootNode.addChildNode(plane)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let configuration = ARWorldTrackingConfiguration()
sceneView.session.run(configuration)
}
}
UPDATED:
Here's a SceneKit Recorder App that you can tailor to your needs (you don't need to write a video to disk, just use a CVPixelBuffer stream and assign it as a texture for a diffuse material).
Hope this helps.
I'm a little late to the party, but I've had a similar issue recently.
As far as I can tell, you cannot directly connect a camera to a node's material. You can, however, use a scene's layer as a texture for a node.
The code below is not verified, but should be more or less ok:
class MyViewController: UIViewController {
override func loadView() {
let projectedScene = createProjectedScene()
let receivingScene = createReceivingScene()
let projectionPlane = receivingScene.scene?.rootNode.childNode(withName: "ProjectionPlane", recursively: true)!
// Here's the important part:
// You can't directly connect a camera to a material's diffuse texture.
// But you can connect a scene's layer as a texture.
projectionPlane.geometry?.firstMaterial?.diffuse.contents = projectedScene.layer
projectedScene.layer.contentsScale = 1
// Note how we only need to connect the receiving view to the controller.
// The projected view is not directly connected as a subview,
// but updates in projectedScene will still be reflected in receivingScene.
self.view = receivingScene
}
func createProjectedScene() -> SCNView {
let view = SCNView()
// ... set up scene ...
return view
}
func createReceivingScene() -> SCNView {
let view = SCNView()
// ... set up scene ...
let projectionPlane = SCNNode(geometry: SCNPlane(width: 2, height: 2)
projectionPlane.name = "ProjectionPlane"
view.scene.rootNode.addChildNode(projectionPlane)
return view
}
}

Accessing functions of another class

So I have a class called GameScene which has a function called makeBackground. In another class called I created an instance of it (GameScene) and called makeBackground as follows in the didMoveToView of the new class:
var gameSceneInstance:GameScene = GameScene()
gameSceneInstance.makeBackground()
However when I run it, nothing happens!
This is the function makeBackground:
func makeBackground ()
{
let bgTxt = SKTexture(imageNamed: "bg.png")
let moveBg = SKAction.moveByX(0, y: -bgTxt.size().height, duration: 5)
let replaceBg = SKAction.moveByX(0, y: bgTxt.size().height, duration: 0)
let moveBgAnimation = SKAction.repeatActionForever(SKAction.sequence([moveBg, replaceBg]))
//-----background-----
for var i:CGFloat = 0 ; i<3; i++
{
bg = SKSpriteNode(texture: bgTxt)
bg.position = CGPoint(x: CGRectGetMidX(self.frame), y: bgTxt.size().height/2 + bgTxt.size().height * i)
bg.size.width = self.frame.width
bg.runAction(moveBgAnimation)
movingObjects.addChild(bg)
}
}
Do I have to copy and paste the makeBackground function into the new class to make it work?
Similarly, in the didMoveToView of the GameScene class, I have some sprites. Can I use them in this new class?
Thanks!
if you created an instance of the GameScene object and called the makeBackground function and nothing happened then there is something wrong with your code in the makeBackground function.
Also in another topic, instance objects which are created in a class are equeals of each other and cannot be used within each others methods unless they are specifically passed in. Perhaps you can make a method in your GameScen object with which you could pass these sprites in either as a list of sprites or as individual sprites

swift/spritekit: Is there a way to add a SKLabelNode to an SKView?

I'm trying to create a hud that sits at the top of the screen and doesn't move with the rest of the scene. I was able to get a static view up there with this:
class GameScene: SKScene, SKPhysicsContactDelegate {
override func didMoveToView(view: SKView) {
/* Setup your scene here */
let hud = SKView()
hud.alpha = 0.5
hud.backgroundColor = SKColor.greenColor()
hud.frame = CGRect(x: 0.5, y: 0.5, width: view.bounds.width, height: 50)
self.view?.addSubview(hud)
So with that, my player can walk around the scene, the camera follows him, but there is always a transparent view across the top which is what I think I want.
So next I tried adding things to the hud view at the top but I can't seem to figure it out. For example, if I create an SKLabel node I can't add it with hud.addChild because SKView doesn't have addChild. I tried doing hud.addSubview but a SKLabel isn't a UIView, which is what addSubview expects.
Does anyone know how I add things to the hud like labels?

Swift: Multiple scenes in Sprite Kit, touchesBegan from different SpriteNodes types on 2nd and 1st scene are called

I am facing a problem using multiple scenes in Sprite Kit (Swift). I’ve setup 3 scene (the game scene, preferences and a help scene). I’ve defined two different SpriteNode classes, called CardItem and MenuItem. While I am using CardItem just at the game scene, I uses MenuItem at all three scenes. So when I touch at the game scene (standard scene) the CardItem SpriteNode, just the touchesBegan function from that class (in game scene) is called. But when I jump into the preferences or help scene (from the game scene), and touch on a SpriteNode MenuItem not just their touchesBegan functions is called but also the touchesBegan from the CardItem type at the game scene!!! (which still is available in the back) is called.
My question is now, is this a feature or a bug in my app?
Here my GameScene Class with the scene initialization
class GameScene: SKScene, PreferencesSceneDelegate, HelpSceneDelegate {
var prefView: SKView?
var helpView: SKView?
var activeGame: Game?
var gamePrefs = gamePreferences()
var prefManager: PreferencesManager!
func prefSceneDidFinish(myScene: PreferencesScene, command: String) {
// Remove PreferencesScene from Screen
if (command == "Abbruch") {
myScene.view!.removeFromSuperview()
setButtonTexture(command)
} else if command == "Done" {
myScene.view!.removeFromSuperview()
activeGame!.setupNewGame(self, gamePrefs: self.gamePrefs, prefManager: self.prefManager, command: 1)
} else {
println("prefSceneDidFinish: command \(command) not specified")
}
}
func helpSceneDidFinish(myScene: HelpScene) {
// Remove HelpScene from Screen
myScene.view!.removeFromSuperview()
}
override func didMoveToView(view: SKView) {
let background = Card(cardType: .background)
background.cardID = CardName.background.rawValue
background.anchorPoint = CGPoint(x: 0.5, y: 1.0)
background.position = CGPoint(x: size.width/2, y: size.height)
background.zPosition = -2
background.userInteractionEnabled = false
self.addChild(background)
//
// Preferences Initialization
//
self.prefView = SKView(frame: CGRectMake(0, 0, self.frame.width, self.frame.height))
// Create and configure PreferencesScene
prefscene = PreferencesScene(size: view.bounds.size)
prefscene.scaleMode = .AspectFill
prefManager = PreferencesManager(prefs: gamePrefs)
tmpSeriesSelection = gamePrefs.inScope // Initialize after data load fromn file
self.prefView!.presentScene(prefscene)
prefscene!.thisDelegate = self
//
// Help Initialization
//
self.helpView = SKView(frame: CGRectMake(0, 0, self.frame.width, self.frame.height))
// Create and configure HelpScene
helpscene = HelpScene(size: view.bounds.size)
helpscene.scaleMode = .AspectFill
self.helpView!.presentScene(helpscene)
helpscene!.thisDelegate = self
var helpSceneBackground = SKSpriteNode(imageNamed: "HelpSceneBackground.png")
helpSceneBackground.anchorPoint = CGPoint(x: 0.5, y: 1.0)
helpSceneBackground.position = CGPoint(x: size.width/2, y: size.height)
helpSceneBackground.zPosition = 0
helpSceneBackground.userInteractionEnabled = false
helpscene.addChild(helpSceneBackground)
activeGame = Game(scene: self, gamePrefs: gamePrefs, prefManager: prefManager)
}
Here is the code snippet (MenuItem touchesBegan) where I call the scenes
case .help :
println("Pressed Help")
var transition = SKTransition.fadeWithDuration(Double(0.0))
self.scene!.view?.addSubview(gamescene.helpView!)
case .preferences :
…
var transition = SKTransition.fadeWithDuration(Double(0.0))
self.scene!.view?.addSubview(gamescene.prefView!)
case .ok :
println("OK Selektion")
…
gamescene.prefManager.save(gamescene.gamePrefs)
prefscene.thisDelegate!.prefSceneDidFinish(prefscene, command: "Done")
Thanks in advance for your help
You are bound to run into problems when your scenes create their own SKView-instances which presents scenes, which creates views, which creates scenes etc. So, skip your prefView and helpView properties and use the scene's view property to present your scenes one after another.
view?.presentScene(scoreScene)
If (for some reason) you need to retain(1) the different scene-instances handle this in your GameViewController instead.
(1) In most cases you probably shouldn't...