I am looking for some advice when it comes to saving a score in my app - I currently have a coin count working in which through my HUD class when I collide through Coins the value increases accordingly. My next step is trying to initiate a high-score that initiates, after some research I think the topic I am looking into is based around NSUserDefaults..
The current code :
import SpriteKit
class HUD: SKNode {
//An SKLabelNode to print the coin score:
let coinCountText = SKLabelNode(text: "000000")
func createHUDNodes(){
// Configure the coin text label:
coinCountText.fontName = "STHupo-Heavy-Italic"
let coinTextPosition = CGPoint(x: -cameraOrigin.x + 770, y: coinPosition.y)
coinCountText.position = coinTextPosition
coinCountText.horizontalAlignmentMode = SKLabelHorizontalAlignmentMode.left
coinCountText.verticalAlignmentMode = SKLabelVerticalAlignmentMode.center
// Add the text label and coin icon to the HUD:
self.addChild(coinCountText)
}
func setCoinCountDisplay(newCoinCount:Int) {
let formatter = NumberFormatter()
let number = NSNumber(value: newCoinCount)
formatter.minimumIntegerDigits = 6
if let coinStr =
formatter.string(from: number) {
// Update the label node with the new count:
coinCountText.text = coinStr
}
}
Working in the HUD class I think naturally I would need to create another 'SKlabelNode'for the highscore node which I can duplicate from the code that works above.. my problem is how can I implement a highscore feature in the app, as when closed and re-opening the application it will remember the highscore..
I assume the following code would need to be added but I do not understand how it ties together in the class.
var currentHighScore =
NSUserDefaults.standardUserDefaults().integerForKey("coin_highscore")
var highScoreLabel = SKLabelNode(fontNamed:"STHupo-Heavy-Italic")
highScoreLabel.text = ""
highScoreLabel.fontSize = 45
highScoreLabel.position = CGPoint(x: viewSize.width * 0.5, y: viewSize.height * 0.30)
self.addChild(highScoreLabel)
if (score > currentHighScore) {
NSUserDefaults.standardUserDefaults().setInteger(score, forKey: "player_highscore")
NSUserDefaults.standardUserDefaults().synchronize()
highScoreLabel.text = "New High Score: \(score) !"
} else {
highScoreLabel.text = "Better Luck Next Time: \(score)"
}
}
Any advice on how I could integrate this code would be much appreciated.
, AJ
Revised :
else if nodeTouched.name == "returnToMenu"{
// Transition to the main menu scene
self.view?.presentScene(MenuScene(size: self.size),transition.crossFade(withDuration: 0.6))
let currentHighScore = UserDefaults.standard.integer(forKey: "Player_highscore")
let highScoreLabel = SKLabelNode(fontNamed:"STHupo-Heavy-Italic")
highScoreLabel.text = "Value: \ (String(describing: coin.value))"
highScoreLabel.text = ""
highScoreLabel.fontSize = 45
highScoreLabel.zPosition = 100
highScoreLabel.position = CGPoint(x: 50, y: 50)
self.addChild(highScoreLabel)
if (coin.value > currentHighScore) {
UserDefaults.standard.set(coin.value, forKey: "Player_highscore")
UserDefaults.standard.synchronize()
highScoreLabel.text = "New High Value: \(String(describing: coin.value)) !"
} else {
highScoreLabel.text = "Better Luck Next Time: \(String(describing: coin.value))"
}
}
}
}
I am assuming it's the referencing to my original HUD that is causing the problem? In my coin class I declared the coin.value = 5, each time the coin is collected by coinCountDisplay increases accordingly. However still does not show - :/
Add a property to your view controller with both get and set and use it in all the places. The get method should get the value from UserDefaults and the set should update the value in UserDefaults.
var coinHighscore: Int {
get {
UserDefaults.standard.integer(forKey: "coin_highscore")
}
set {
UserDefaults.standard.set(newValue, forKey: "coin_highscore")
}
}
Extending on Frankenstein's answer, IF you want to use Swift 5 you can use Property Wrappers to avoid writing get and set every time:
#propertyWrapper struct UserDefaultsBacked<Value> {
let key: String
let defaultValue: Value
var storage: UserDefaults = .standard
var wrappedValue: Value {
get {
let value = storage.value(forKey: key) as? Value
return value ?? defaultValue
}
set {
storage.setValue(newValue, forKey: key)
}
}
}
And use it like this:
#UserDefaultsBacked(key: "coin_highscore", defaultValue: 0)
var coinHighscore: Int
I am creating an iOS AR app that sets text in a specific location and leaves it there for others to view. Is there a better way to implement it than what I am doing?
Currently, I have it set so that the text is saved to Firebase and loads it by setting the nodes relative to the camera’s position. I’m wondering if there is a way to save ARAnchors in a fashion similar to what I am doing but is that possible?
My current function for saving the text to the location via a user tapping the screen:
/*
* Variables for saving the user touch
*/
var touchX : Float = 0.0
var touchY : Float = 0.0
var touchZ : Float = 0.0
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// will be used for getting the text
let textNode = SCNNode()
var writing = SCNText()
// gets the user’s touch upon tapping the screen
guard let touch = touches.first else {return}
let result = sceneView.hitTest(touch.location(in: sceneView), types: [ARHitTestResult.ResultType.featurePoint])
guard let hitResult = result.last else {return}
let hitTransform = SCNMatrix4.init(hitResult.worldTransform)
let hitVector = SCNVector3Make(hitTransform.m41, hitTransform.m42, hitTransform.m43)
// saves X, Y, and Z coordinates of touch relative to the camera
touchX = hitTransform.m41
touchY = hitTransform.m42
touchZ = hitTransform.m43
// Was thinking of adding the ability to change colors. Probably can skip next seven lines
var colorArray = [UIColor]()
colorArray.append(UIColor.red)
writing = SCNText(string: input.text, extrusionDepth: 1)
material.diffuse.contents = colorArray[0]
writing.materials = [material]
// modifies the node’s position and size
textNode.scale = SCNVector3(0.01, 0.01, 0.01)
textNode.geometry = writing
textNode.position = hitVector
sceneView.scene.rootNode.addChildNode(textNode)
// last few lines save the info to Firebase
let values = ["X" : touchX, "Y" : touchY, "Z" : touchZ, "Text" : input.text!] as [String : Any]
let childKey = reference.child("Test").childByAutoId().key
if input.text != nil && input.text != "" {
let child = reference.child("Test").child(childKey!)
child.updateChildValues(values)
} else {
let child = reference.child("Test").child(childKey!)
child.updateChildValues(values)
} // if
} // override func
/*
* Similar to the previous function but used in next function
*/
func placeNode(x: Float, y: Float, z: Float, text: String) -> Void {
let textNode = SCNNode()
var writing = SCNText()
let hitVector = SCNVector3Make(x, y, z)
touchX = x
touchY = y
touchZ = z
var colorArray = [UIColor]()
colorArray.append(UIColor.red)
writing = SCNText(string: text, extrusionDepth: 1)
material.diffuse.contents = colorArray[0]
writing.materials = [material]
textNode.scale = SCNVector3(0.01, 0.01, 0.01)
textNode.geometry = writing
textNode.position = hitVector
sceneView.scene.rootNode.addChildNode(textNode)
} // func
/*
* This next function is used in my viewDidLoad to load the data
*/
func handleData() {
reference.child("Test").observeSingleEvent(of: .value, with: { (snapshot) in
if let result = snapshot.children.allObjects as? [DataSnapshot] {
for child in result {
let xCoord = Float(truncating: child.childSnapshot(forPath: "X").value as! NSNumber)
let yCoord = Float(truncating: child.childSnapshot(forPath: "Y").value as! NSNumber)
let zCoord = Float(truncating: child.childSnapshot(forPath: "Z").value as! NSNumber)
let inscription = child.childSnapshot(forPath: "Text").value
self.placeNode(x: xCoord , y: yCoord , z: zCoord , text: inscription as! String)
} // for
} // if
}) // reference
} // func
I have looked into a few things such as ARCore but that looks like it uses Objective-C. I’ve made this app in Swift and I am not sure if I can incorporate ARCore with how I have implemented my current application.
Do I just need to get over it and learn Objective-C? Can I still work with what I have?
I think that ARCore anchors are only available for 24 hours, so that could be a problem.
You probably need to use ARKit2.0's ARWorldMap and save it as data on firebase for others to see the text in the same place, otherwise you are assuming in your code that future users will start their AR session in the exact same position and direction as the person who left the text. You probably need to use core location first to see where in the world the user is.
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 tried using the following to get all nodes of a certain class:
let enemies = children.flatMap { $0 as? SomeEnemyClass }
The result is an empty array. I would like to understand why I'm not getting any results. enumerateChildNodesWithName works, but it doesn't list the parent/child relationships which I thought might shed some light. Thanks!
Here is the code: There is a simple class for the enemy, a level manager that replaces nodes in the scene editor with enemy objects, and GameScene.swift. Everything gets assigned to a world node.
Enemy Class
class Enemy: SKSpriteNode {
func spawn(parentNode:SKNode, position: CGPoint, size: CGSize = CGSize(width: 50, height: 50)) {
parentNode.addChild(self)
self.size = size
self.position = position
self.name = "enemy"
self.physicsBody = SKPhysicsBody(circleOfRadius: size.width / 2)
self.physicsBody?.affectedByGravity = false
self.physicsBody?.categoryBitMask = PhysicsCategory.enemy.rawValue
self.physicsBody?.contactTestBitMask = PhysicsCategory.hero.rawValue
self.physicsBody?.collisionBitMask = 0
}
}
Level Manager
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 "enemy":
let enemy = Enemy()
enemy.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)
levelManager.addLevelsToWorld(self.world)
levelManager.levels[0].position = CGPoint(x:0, y: 0)
map and flatMap transform one array into another, what you are really trying to do is just filtering your child nodes for enemies. Use filter instead.
let enemines = children.filter { $0 is SomeEnemyClass }
That said, I don't see any reason why your sample code would not work.
Here is an example of this code working.
This code will only work if you have real classes derived from SKNode. It is much more common to use node name strings to identify types of nodes. This would work if you had a constant string name for your enemies.
let enemies = children.filter { $0.name == "SomeEnemy" }
You said
let enemies = children.flatMap { $0 as? SomeEnemyClass }
The result is an empty array. I would like to understand why I'm not getting any results. enumerateChildNodesWithName works
First of all this code
let enemies = children.flatMap { $0 as? Enemy }
is correct and it does returns an array of Enemy. Specifically all objects inside children that are Enemy or subclasses of Enemy.
Look
Why is not working for you?
You said that enumerateChildNodesWithName si working for you. This makes me think that you don't have Enemy objects into children. You just have SKNode(s) you assigned the name enemy.
What I learned was that the node tree is really a tree and you have to start looking on the right branch to get results. The answer to my question is:
let enemies = childNodeWithName("//level")!.children.flatMap { ($0 as? SomeEnemyClass) }
or
let enemies = childNodeWithName("//level")!.children.filter { $0 is SomeEnemyClass }
This is not an answer to my question, but it works too...
self.enumerateChildNodesWithName("//*") {
node, stop in
if (node.name == "enemy") {
print(node)
}
}
Thank you to everyone who offered their input. It got me pointed in the right direction.
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: