I have an SKNode which contains 2 sub nodes (a sprite node w/an image and an SKLabelNode above the image which prints the number of available units).
Previously, after each time the USER 'spent' a unit the counters would update by removing the previous unit and adding a new one.
That was working, but obviously quite inefficient. Now I'm trying to grab the existing node and update its text like so...
let newNode = createUnitCounter(type: type, nation: nation, count: countStr)
if let existingNode = iterator.filter({ $0.name == newNode.name }).first?.children.filter({ $0 is SKLabelNode }).first as? SKLabelNode {
existingNode.text = countStr
} else {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.addChild(newNode)
}
}
Using breakpoints I can see that the node should be updating (it's text shows the proper values when I type po existingNode.text in the console), but the screen never shows the update. Other nodes I've refactored are updating on the screen, but not this one.
How do tell the node to redraw (e.g. something like existingNode.setNeedsDisplay())?
Here's how I create the node originally in createUnitCounter:type:nation:count ...
func createUnitCounter(type: UnitType, nation: Nation, count: String) -> SKNode {
let node = SKNode()
node.name = type.prefix + ElId.Nodes.counter
let image = createCounterImage(mode: .offense, type: type, nation: nation)
assignCounterPosition(to: image, of: type)
node.addChild(image)
let label = createCounterLabel(text: count, position: nil, type: type)
label.verticalAlignmentMode = .bottom
assignCounterPosition(to: label, of: type)
node.addChild(label)
return node
}
func createCounterLabel(text: String, position: CGPoint?, type: UnitType?) -> SKLabelNode {
let node = SKLabelNode(fontNamed: Assets.font)
node.text = text
node.zPosition = Position.zUIElementH
if type == selectedUnitType { node.fontColor = Colors.red }
if let p = position { node.position = p }
return node
}
func createCounterImage(mode: GameMode, type: UnitType = .infantry, nation: Nation = .usa) -> SKSpriteNode {
...
}
So I isolated the issue to the fact that it was chaining the filters through an iterator which broke the node's reference. When I switched the iterator back out to the children array, everything was working. However that was going to create issues, so I found a similar method and for some reason I was able to keep the iterator and the references survived when accessing the same node independent of the iterator and then accessing that...
let newNode = createUnitCounter(type: type, nation: nation, count: countStr)
if let existingNode = children.makeIterator().filter({ $0.name == newNode.name }).first {
for child in existingNode.children {
if let label = child as? SKLabelNode { label.text = countStr }
}
} else {
addChild(newNode)
}
After what I've read in the documentation and on the internet a SCNBillboardConstraint would rotate a node to always face the pointOfView node - in the case of ARKit, the user's camera.
The thing is, when I add a SCNBillboardConstraint to a child node, it dissapears. The nodes are just some SCNTexts added as a subchild of a more complex model.
The hierarchy looks something like this: RootNode - > Text node (two of them).
Just after I added the root node to the scene's root node, I add this constraint in the following way:
updateQueue.async {
self.sceneView.scene.rootNode.addChildNode(virtualObject)
self.sceneView.addOrUpdateAnchor(for: virtualObject)
self.addBillboardContraintsToText(object: virtualObject)
}
func addBillboardContraintsToText(object: VirtualObject) {
guard let storeNode = object.childNodes.first else {
return
}
for node in storeNode.childNodes {
if let geometry = node.geometry, geometry.isKind(of: SCNText.self) {
let billboard = SCNBillboardConstraint()
node.constraints = [billboard]
}
}
}
The text nodes have their position set properly relative to their root node, so there's no problem with that. When I add a SCNLookAtConstraint though, it works just fine.
node.pivot = SCNMatrix4Rotate(node.pivot, Float.pi, 0, 1, 0)
let lookAt = SCNLookAtConstraint(target: sceneView.pointOfView)
lookAt.isGimbalLockEnabled = true
node.constraints = [lookAt]
Any ideas why the SCNBillboardConstraint might not work? Am I doing something wrong?
This Code (with apples CupScn) works just fine for me:
cupNode.position = SCNVector3(0.5,0,-0.5)
guard let virtualObjectScene = SCNScene(named: "cup.scn", inDirectory: "Models.scnassets/cup") else {
return
}
let wrapperNode = SCNNode()
for child in virtualObjectScene.rootNode.childNodes {
child.geometry?.firstMaterial?.lightingModel = .physicallyBased
wrapperNode.addChildNode(child)
}
cupNode.addChildNode(wrapperNode)
scene.rootNode.addChildNode(cupNode)
let billboardConstraint = SCNBillboardConstraint()
billboardConstraint.freeAxes = SCNBillboardAxis.Y
cupNode.constraints = [billboardConstraint]
So basically I am looking to choose one of the 4 different coloured balls at random to come into the scene which each have an animation, physics properties and movement & spacing that I have already coded. I am not sure exactly how to make the array then choose at random from the array of the 4 coloured balls so that I have one ball chosen at random to come into the scene.
To make it more clear what I'm asking here's some code (I only use two balls in this code so you don't have to read as much):
var moveandremove = SKAction() < this is in my ballScene.swift
The spawn runBlock is inside didMovetoView
let spawn = SKAction.runBlock({
() in
self.allballs()
})
let delay = SKAction.waitForDuration(2.0)
let SpawnDelay = SKAction.sequence([spawn, delay])
let spawndelayforever = SKAction.repeatActionForever(SpawnDelay)
self.runAction(spawndelayforever)
let distance = CGFloat(brnball.frame.width * 20 + brnball.frame.width)
let moveball = SKAction.moveByX(-distance, y: 0, duration: NSTimeInterval(0.003 * distance))
let removeball = SKAction.removeFromParent()
moveandremove = SKAction.sequence([moveball])
}
func allballs() {
TextureAtlasblk = SKTextureAtlas(named: "blkball")
for i in 1...TextureAtlasblk.textureNames.count{
var Name = "blkball_\(i)"
blkarray.append(SKTexture(imageNamed: Name))
}
blkball = SKSpriteNode(imageNamed: "blkball_1")
blkball.position = CGPoint(x: CGRectGetMidX(self.frame) + 100, y: CGRectGetMidY(self.frame))
blkball.zPosition = 7
blkball.setScale(0.1)
self.addChild(blkball)
blkball.runAction(SKAction.repeatActionForever(SKAction.animateWithTextures(blkarray, timePerFrame: 0.2)))
//brownball
TextureAtlasbrn = SKTextureAtlas(named: "brnball")
for i in 1...TextureAtlasbrn.textureNames.count{
var Name = "brnball_\(i)"
brnarray.append(SKTexture(imageNamed: Name))
}
brnball = SKSpriteNode(imageNamed: "brnball_1")
brnball.position = CGPoint(x: CGRectGetMidX(self.frame) + 50, y: CGRectGetMidY(self.frame))
brnball.zPosition = 7
brnball.setScale(0.1)
self.addChild(brnball)
brnball.runAction(SKAction.repeatActionForever(SKAction.animateWithTextures(brnarray, timePerFrame: 0.2)))
Here is my terrible starting point at trying to make an array to choose from each ball (this is inside my allballs() function):
var ballarray: NSMutableArray = [blkball, brnball, yelball, bluball]
runAction(moveandremove)
I am new to swift and pretty hopeless, would be awesome if someone could help me out :)
Thanks
It's hard for me to find the array that you're talking about in your code. But nevertheless, I can still show you how.
Let's say we have an [Int]:
let ints = [10, 50, 95, 48, 77]
And we want to get a randomly chosen element of that array.
As you may already know, you use the subscript operator with the index of the element to access an element in the array, e.g. ints[2] returns 95. So if you give a random index to the subscript, a random item in the array will be returned!
Let's see how can we generate a random number.
The arc4random_uniform function returns a uniformly distributed random number between 0 and one less the parameter. Note that this function takes a UInt32 as a parameter and the return value is of the same type. So you need to do some casting:
let randomNumber = Int(arc4random_uniform(UInt32(ints.count)))
With randomNumber, we can access a random element in the array:
let randomItem = ints[randomNumber]
Try to apply this technique to your situation.
Here's a generic method to do this as well:
func randomItemInArray<T> (array: [T]) -> T? {
if array.isEmpty {
return nil
}
let randomNumber = Int(arc4random_uniform(UInt32(array.count)))
return array[randomNumber]
}
Note that if the array passed in is empty, it returns nil.
You could make and extension for Array that returns a random element.
extension Array {
func randomElement() -> Element {
let i = Int(arc4random_uniform(UInt32(count - 1)))
return self[i]
}
}
You could take that a step further and allow a function to be applied directly to a random element.
mutating func randomElement(perform: (Element) -> Element) {
let i = Int(arc4random_uniform(UInt32(count - 1)))
self[i] = perform(self[i])
}
You can use this function when using an array of reference types.
func randomElement(perform: (Element) -> ()) {
let i = Int(arc4random_uniform(UInt32(count - 1)))
perform(self[i])
}
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
}
}
}
I have a SpriteKit game I am building and I am loading a level from a multidimensional array. The loadlevel function works the first time. It does fail if I do a println of the physicsBody above the physicsBody assignment (after the initialization of the physicBody). When I remove all the tiles with removeChildrenInArray the secondtime I run load level it throws an error saying fatal error: unexpectedly found nil while unwrapping an Optional and it points to line right below the println below. And the println indicates that it is the physicsBody that is nil. In my mind there is no reason a freshly initialize PhysicsBody should ever be nil. The println prints physicsBody nil. I have no idea why the physicsBody would be nil. I am just trying to reset the level by removing all block nodes and adding new ones in the original place according to the level map.
func loadLevel() {
var levels = Levels().data
var frameSize = view.frame.size
var thisLevel = levels[currentLevel]
println("running load level")
for (rowIndex,row) in enumerate(thisLevel) {
for (colIndex,col) in enumerate(row) {
if col == 4 {
continue
}
println("COL: \(col)")
var tile = SKSpriteNode(texture: SKTexture(imageNamed: "brick_\(tileMap[col])"))
tile.name = "tile_\(rowIndex)_\(colIndex)"
tile.position.y = frameSize.height - (tile.size.height * CGFloat(rowIndex)) - (tile.size.height/2)
tile.position.x = tile.size.width * CGFloat(colIndex) + (tile.size.width/2)
var physicsBody = SKPhysicsBody(rectangleOfSize: tile.size)
tile.physicsBody = physicsBody
tile.physicsBody.affectedByGravity = false
tile.physicsBody.categoryBitMask = ColliderType.Block.toRaw()
tile.physicsBody.contactTestBitMask = ColliderType.Ball.toRaw()
tile.physicsBody.collisionBitMask = ColliderType.Ball.toRaw()
scene.addChild(tile)
tileCount++
}
}
}
Here is my ColliderType
enum ColliderType:UInt32 {
case Paddle = 1
case Block = 2
case Wall = 3
case Ball = 4
}
This is my reset function contents:
func reset() {
tileCount = 0
var removeTiles = [SKSpriteNode]()
// remove all the tiles
for child in scene.children {
var a_tile = child as SKSpriteNode
if a_tile.name.hasPrefix("tile_") {
a_tile.removeFromParent()
a_tile.name = ""
removeTiles.append(a_tile)
}
}
removeTiles.removeAll(keepCapacity: false)
ball!.position = CGPoint(x: 200, y: 200)
ballVel = CGPoint(x: 0, y: -5)
currentLevel++
loadLevel()
lost = false
won = false
}
Here is my Level structs
struct Tile {
let map = ["blue","green","purple","red"]
}
struct Levels {
let data = [
[
[4,4,0,4,0,4,0,4,0,4,0,4,0,0,4,4],
[4,4,1,4,1,4,1,4,1,4,1,4,1,1,4,4],
[4,4,2,4,2,4,2,4,2,4,2,4,2,2,4,4],
[4,4,3,4,3,4,3,4,3,4,3,4,3,3,4,4]
],
[
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[2,2,2,2,2,2,2,4,2,2,2,2,2,2,2,2],
[3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3]
]
]
}
If this is a bug in Swift I am trying to figure out a way around so I can just make this work.
Looks like SKPhysicsBody is instantiated with an empty size. Try to create the physics body object with an explicit size, like so:
var physicsBody = SKPhysicsBody(rectangleOfSize: CGSizeMake(100, 100))
Alternatively, you can set the size directly on the SKSpriteNode or use one of its constructors that take a CGSize construct like initWithTexture:color:size: