Target Multiple Sprites Based on their Name? - swift

The problem with this code is that it's only targeting 1 SKNode named "coin", I want to target all SKNodes named "coin". I'm not sure how to do this...
Heres what I have:
func killCoins(){
let skcoin: SKNode = self.childNodeWithName("coin")!
// Looks for a SKNode named "coin".
let coinsprite: SKSpriteNode = (skcoin as? SKSpriteNode)!
// Converts the SKNode into a SKSpriteNode
coinsprite.removeFromParent()
// Kills Sprite
}

To remove multiple SKSpriteNodes, loop through all of the children and remove those SKSpriteNodes with the name "coin".
func killCoins() {
for child in self.children where child is SKSpriteNode {
if child.name == "coin" {
child.removeFromParent()
}
}
}

As an addition to vacawama's answer, you can use enumerateChildNodesWithName:usingBlock: for more advanced searches:
self.enumerateChildNodesWithName("coin") {
node, stop in
// do something with node or stop
}
From the docs:
This method enumerates the child array in order, searching for nodes
whose names match the search parameter. The block is called once for
each node that matches the name parameter.

Solution 1
You can look for all the children SKSpriteNode(s) having coin as name. The following code is totally Functiona Programming oriented
func killCoins() {
self
.children
.filter { $0 is SKSpriteNode && $0.name == "coin" }
.forEach { $0.removeFromParent() }
}
The time of this operation is O(n) where n is the total number of children of the current node/scene.
We can do better.
Solution 2
Just create a special node that will be the parent of all the coins
override func didMoveToView(view: SKView) {
let coins = SKNode()
coins.name = "coins"
self.addChild(coins)
let coin0 = SKSpriteNode(imageNamed: "coin")
coins.addChild(coin0)
let coin1 = SKSpriteNode(imageNamed: "coin")
coins.addChild(coin1)
}
Now the killCoins becomes
func killCoins() {
self.childNodeWithName("coins")?.removeAllChildren()
}
The time complexity of this operation is O(m) where m is the number of the coins.

Related

Detect touch on child node of object in SceneKit

Basically this question but for SceneKit.
I have a parent node with a few smaller nodes inside it, the parent node at a later point becomes transparent, (parent's diffuse material opacity is set to 0) after that i would like to get the node that was tapped inside the object, how should i go about doing that? The default hit testing returns the parent node, and since there are quiet a few smaller nodes inside the object, i need the precise one that was tapped.
To fix this issue I recommend to read next topic from Apple:
https://developer.apple.com/documentation/scenekit/scnhittestoption
General idea:
func registerGestureRecognizer() {
let tap = UITapGestureRecognizer(target: self, action: #selector(search))
self.sceneView.addGestureRecognizer(tap)
}
#objc func search(sender: UITapGestureRecognizer) {
let sceneView = sender.view as! ARSCNView
let location = sender.location(in: sceneView)
let results = sceneView.hitTest(location, options: [SCNHitTestOption.searchMode : 1])
guard sender.state == .ended else { return }
for result in results.filter( { $0.node.name == "Your node name" }) {
// do manipulations
}
}
Hope it helps!
Best regards.

remove nodes from scene after use ARSCNView

I am making an app using ARKit to measure between two points. The goal is to be able to measure length, store that value, then measure width and store.
The problem I am having is disposing of the nodes after I get the measurement.
Steps so far:
1) added a button with a restartFunction. this worked to reset the measurements but did not remove the spheres from scene, and also made getting the next measurement clunky.
2) set a limit on > 2 nodes. This functionaly works best. But again the spheres just stay floating in the scene.
Here is a screen shot of the best result I have had.
#objc func handleTap(sender: UITapGestureRecognizer) {
let tapLocation = sender.location(in: sceneView)
let hitTestResults = sceneView.hitTest(tapLocation, types: .featurePoint)
if let result = hitTestResults.first {
let position = SCNVector3.positionFrom(matrix: result.worldTransform)
let sphere = SphereNode(position: position)
sceneView.scene.rootNode.addChildNode(sphere)
let tail = nodes.last
nodes.append(sphere)
if tail != nil {
let distance = tail!.position.distance(to: sphere.position)
infoLabel.text = String(format: "Size: %.2f inches", distance)
if nodes.count > 2 {
nodes.removeAll()
}
} else {
nodes.append(sphere)
}
}
}
I am new to Swift (coding in general) and most of my code has come from piecing together tutorials.
I think the issue here is that your are not actually removing the SCNNodes you have added to hierarchy.
Although you are removing the nodes from what I assume is an array of SCNNodes by calling: nodes.removeAll(), you first need to actually remove them from the scene hierarchy.
So what you need to do is call the following function on any node you wish to remove:
removeFromParentNode()
Which simply:
Removes the node from its parent’s array of child nodes.
As such you would do something like this which first removes the nodes from the hierarchy, and then removes them from the array:
for nodeAdded in nodesArray{
nodeAdded.removeFromParentNode()
}
nodesArray.removeAll()
So based on the code provided you could do the following:
if nodes.count > 2 {
for nodeAdded in nodes{
nodeAdded.removeFromParentNode()
}
nodes.removeAll()
}
For future reference, if you want to remove all SCNNodes from you hierarchy you can also call:
self.augmentedRealityView.scene.rootNode.enumerateChildNodes { (existingNode, _) in
existingNode.removeFromParentNode()
}
Whereby self.augmentedRealityView refers to the variable:
var augmentedRealityView: ARSCNView!
Here is a very basic working example based on (and modified from) the code you have provided:
/// Places A Marker Node At The Desired Tap Point
///
/// - Parameter sender: UITapGestureRecognizer
#objc func handleTap(_ sender: UITapGestureRecognizer) {
//1. Get The Current Tap Location
let currentTapLocation = sender.location(in: sceneView)
//2. Check We Have Hit A Feature Point
guard let hitTestResult = self.augmentedRealityView.hitTest(currentTapLocation, types: .featurePoint).first else { return }
//3. Get The World Position From The HitTest Result
let worldPosition = positionFromMatrix(hitTestResult.worldTransform)
//4. Create A Marker Node
createSphereNodeAt(worldPosition)
//5. If We Have Two Nodes Then Measure The Distance
if let distance = distanceBetweenNodes(){
print("Distance == \(distance)")
}
}
/// Creates A Marker Node
///
/// - Parameter position: SCNVector3
func createSphereNodeAt(_ position: SCNVector3){
//1. If We Have More Than 2 Nodes Remove Them All From The Array & Hierachy
if nodes.count >= 2{
nodes.forEach { (nodeToRemove) in
nodeToRemove.removeFromParentNode()
}
nodes.removeAll()
}
//2. Create A Marker Node With An SCNSphereGeometry & Add It To The Scene
let markerNode = SCNNode()
let markerGeometry = SCNSphere(radius: 0.01)
markerGeometry.firstMaterial?.diffuse.contents = UIColor.cyan
markerNode.geometry = markerGeometry
markerNode.position = position
sceneView.scene.rootNode.addChildNode(markerNode)
//3. Add It To The Nodes Array
nodes.append(markerNode)
}
/// Converts A matrix_float4x4 To An SCNVector3
///
/// - Parameter matrix: matrix_float4x4
/// - Returns: SCNVector3
func positionFromMatrix(_ matrix: matrix_float4x4) -> SCNVector3{
return SCNVector3(matrix.columns.3.x, matrix.columns.3.y, matrix.columns.3.z)
}
/// Calculates The Distance Between 2 Nodes
///
/// - Returns: Float?
func distanceBetweenNodes() -> Float? {
guard let firstNode = nodes.first, let endNode = nodes.last else { return nil }
let startPoint = GLKVector3Make(firstNode.position.x, firstNode.position.y, firstNode.position.z)
let endPoint = GLKVector3Make(endNode.position.x, endNode.position.y, endNode.position.z)
let distance = GLKVector3Distance(startPoint, endPoint)
return distance
}
For an example of a measuringApp which might help your development you can look here: ARKit Measuring Example
Hope it helps...
This looks like a logic issue. You're assigning nodes.last to tail just before checking if tail is not nil. So it will never be != nil so you'll never execute the nodes.append(sphere) in the else.
I agree with #dfd. Set a breakpoint to make sure the code is being executed before continuing.

How to remove a SKSpriteNode correctly

I'm doing a small game in Swift 3 and SpriteKit. I want to do a collision with my character and a special object that increases my score in 1, but for some reason, when I detect the collision, the score increases in 2 or 3.
I'm removing from parent my SpriteKitNode but it seems that it doesn't work.
Here's my code:
func checkCollisionsObject(){
enumerateChildNodes(withName: "objeto") {node, _ in
let objeto = node as! SKSpriteNode
if objeto.frame.intersects(self.personaje.frame){
objeto.removeFromParent()
self.actualizarPoints()
//self.labelNivel.text = "Level: \(self.nivel)"
}
}
}
func actualizarPoints() {
self.pointsCounter += 1
points.text = "Points: \(pointsCounter)"
}
The problem is that the collision detection is happening at 60fps (pretty fast). So in that time multiple collision detections are occurring. You are just handling the first one.
I usually like to have a property on the object that I can trigger so that I know whether or not the object has collided, and set it so that it doesn't detect anymore collisions.
In your case the object is just a SKSpriteNode so you would have to set the property in userData or make the object a custom object and have the property in the custom object class
func checkCollisionsObject(){
enumerateChildNodes(withName: "objeto") {node, _ in
let objeto = node as! CustomObject
if objeto.frame.intersects(self.personaje.frame) && objeto.hasCollided == false {
objeto.hasCollided = true
objeto.removeFromParent()
self.actualizarPoints()
}
}
}
func actualizarPoints() {
self.pointsCounter += 1
points.text = "Points: \(pointsCounter)"
}

Adding multiple sprite nodes to the scene using single function/method in swift

Let's say I have 10 nodes, where all of the nodes are the dots image, which are node1 thru node10. I create node1 as the following:
func createNode1() -> SKNode {
let spriteNode = SKNode()
spriteNode.position = CGPointMake(CGRectGetMidX(self.frame)/1.35, CGRectGetMidY(self.frame)/1.32)
let sprite = SKSpriteNode(imageNamed: "dot_1")
sprite.zPosition = 3.0
sprite.name = "A1_Dot"
spriteNode.addChild(sprite)
return spriteNode
}
I create the rest of nodes by creating 9 more functions, where next one would be as func createNode2etc, all the way up to 10 functions, where the only difference between them is node's name and its location. Basically each node has different location in the scene and of course different image name. Is there a way to load of the 10 nodes to the scene at once and manipulate node's locations at the time of use.? I'm looking for a way to load all 10 nodes to scene using a single function or method and assign node's positions within this same function. Thanks.
You need to use a loop to iterate through an array of positions, and move your code that adds the node to the scene into the loop:
let positions = [CGPointMake(CGRectGetMidX(self.frame)/1.35, CGRectGetMidY(self.frame)/1.32), ... /*add your 9 other positions here*/]
positions.enumerate().forEach { (index, point) in
let spriteNode = SKNode()
spriteNode.position = point
let sprite = SKSpriteNode(imageNamed: "dot_\(index + 1)")
sprite.zPosition = 3.0
sprite.name = "A\(index + 1)_Dot"
spriteNode.addChild(sprite)
// Add spriteNode to the scene here
}
You can either use a loop as Jugale suggested or you could just pass the values you want into the method
For example
func createNode1(imageNamed imageNamed: String, name: String, pos: CGPoint) -> SKNode {
let spriteNode = SKNode()
spriteNode.position = pos
let sprite = SKSpriteNode(imageNamed: imageNamed)
sprite.zPosition = 3.0
sprite.name = name
spriteNode.addChild(sprite)
return spriteNode
}
And now in your scene you can add the nodes like so
let node1Pos = ...
node1 = createNode1(imageNamed: "...", name: "A1_Dot", pos: node1Pos)
let node2Pos = ...
node2 = createNode1(imageNamed: "...", name: "A1_Dot", pos: node2Pos)
I am saying ImageNamed twice in the create Node function because when you pass stuff into functions Swift by default does not require the first description to be typed when calling the method. (see below)
So if would say imageNamed only once than you would call it like so.
node1 = createNode1("...", pos: node1Pos)
Also your creating node function could be simplified, unless you specifically want to return a SKNode in the method.
func createNode1(imageNamed imageNamed: String, name: String, pos: CGPoint) -> SKSpriteNode {
let sprite = SKSpriteNode(imageNamed: imageNamed)
sprite.position = pos
sprite.zPosition = 3.0
sprite.name = name
addChild(sprite)
return sprite
}
Either way is acceptable solution. Here are full details. In baseScene we create function createNodes and call that function in didMoveToView where nodeA1 is given position and added to the scene as shown below.
override func didMoveToView(view: SKView) {
let nodeA1Pos = CGPointMake(CGRectGetMidX(self.frame)/1.5, CGRectGetMidY(self.frame)/2.0)
nodeA1 = createNodes(imageNamed: "dot_1", name: "A1_Dot", pos: nodeA1Pos)
addChild(nodeA1)
}
Then in Level1Scene which is subclass of baseScene we just give a new position to nodeA1 which will override position originally set in baseScene:
override func didMoveToView(view: SKView) {
super.didMoveToView(view)
nodeA1.position = CGPointMake(CGRectGetMidX(self.frame)/1.3, CGRectGetMidY(self.frame)/0.67)
}
This way of subclassing saves a lot of time and code as only one function is used to generate all common sprite nodes.
All thanks to crashoverride777 !!!!

How do you change the color of all of the nodes of a specific name in SpriteKit with Swift

I'm trying to change the color of every single instance of a node in SpriteKit. When I try to put it in the update method, it only changes one and not all of them.
override func update(currentTime: CFTimeInterval) {
if changeColor {
self.enumerateChildNodesWithName("blocks", usingBlock: { node, stop in
block?.color = UIColor.orangeColor()
})
}
}
I think you should change the color of the node, not of some block variable. You will retrieve all SKNodes with that name. But those nodes don't have a color property. Only some subclasses, for example the SKSpriteNode have a color property. You therefore have to add some additional logic to change the color. For example you could try to convert the node to SKSpriteNode and only then change the color:
self.enumerateChildNodesWithName("blocks", usingBlock: { node, stop in
if let sprite = node as? SKSpriteNode {
sprite.color = UIColor.orangeColor()
}
})
As #appzYourLife correctly mentioned that code can be simplified / swiftyfied to
self.enumerateChildNodesWithName("blocks") { node, stop in
if let sprite = node as? SKSpriteNode {
sprite.color = .orangeColor()
}
}