How to attach a struct or class object to a SCNNode - ios

I'm trying to create a scene where different users can display different cars. When the user first signs into the app they get uid, username etc. When they choose a car the car model has the type of car and a user object that identifies them.
I can create a car and implement the hitTest to find which car was tapped. What I can't figure out is how do I assign a data model to the car that was tapped so that I know who's who?
The only property I found on the node so far is the node.name, I can attach the userId to it and just go through an array of users to find out who's who but that doesn;t seem practical if I have a lot of users. I also need that property when removing a node:
sceneView.session.pause()
sceneView.scene.rootNode.enumerateChildNodes { (node, _) in
// this would actually remove all the ferraries on the scene but this is just an example
if node.name == "ferrari" {
node.removeFromParentNode()
}
}
How can I attach a data model to a SCNNode?
class User {
var userId: String?
var username: String?
}
class RaceCar {
var user: User?
var type: String?
}
// user creates an account and picks a ferrari
let raceCar = RaceCar()
let type = raceCar.type!
switch type {
case "ferrari":
createFerrariAndAddItToScene(raceCar)
case "bmw":
...etc
}
func createFerrariAndAddItToScene(_ raceCar: RaceCar) {
let image = art.scnassests/"\(raceCar.typ!e).jpg"
let material = SCNMaterial()
material.diffuse.contents = image
let plane = SCNPlane(width: 0.1, height: 0.1)
plane.materials = [material]
let node = SCNNode()
node.name = raceCar.type!
node.geometry = plane
node.position = SCNVector(0.0, 0.0, -0.2)
sceneView.scene.rootNode.addChildNode(node)
}
#objc func nodeWasTapped(_ sender: UITapGestureRecognizer) {
guard let sceneView = sender.view as? ARSCNView else { return }
let touchLocation = sender.location(in: sceneView)
let hitResults = sceneView.hitTest(touchLocation, options: [:])
if !hitResults.isEmpty {
guard let hitResult = hitResults.first else { return }
let node = hitResult.node
print(node.name) // I need to get some info from the dataModel here like node.raceCar.user.username or node.raceCar.user.userId
}
}

You can subclass SCNNode and add any data model you need to it:
class CarNode: SCNNode {
var raceCar: RaceCar?
init(raceCar: RaceCar, geometry: SCNGeometry) {
super.init()
self.geometry = geometry
self.raceCar = raceCar
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
To create new node simply use this new class:
func createFerrariAndAddItToScene(_ raceCar: RaceCar) {
...
let node = CarNode(raceCar: raceCar, geometry: plane)
...
}
To access your data model you can do like so:
#objc func nodeWasTapped(_ sender: UITapGestureRecognizer) {
...
if let node = hitResult.node as? CarNode {
print(node.raceCar.user.username)
}
}

I had some luck using setValue:ForKey:
node.setValue(tile, forKey: "tile")
...
if let tile = node.value(forKey: "tile") as? Tile {
...
}

Related

ARKit: Tracking VisonCoreML detected object

I'm new to iOS and I am currently refactoring a code I got from a tutorial on VisionCoreML and ARKit that adds a node to the detected object.
currently, if the I move the object the node does not move and follow the object. I can see from Apple's sample code for Recognizing Objects in Live Capture they use layers and repositions this each time Vision detects the object at a new position which is what I was hoping to replicate with an ARObject.
Is there a way I can achieve this with ARKit?
Any help around this would be greatly appreciated.
Thanks.
EDIT: Working code with solution
#IBOutlet var sceneView: ARSCNView!
private var viewportSize: CGSize!
private var previousAnchor: ARAnchor?
private var trackingNode: SCNNode!
lazy var objectDetectionRequest: VNCoreMLRequest = {
do {
let model = try VNCoreMLModel(for: yolov5s(configuration: MLModelConfiguration()).model)
let request = VNCoreMLRequest(model: model) { [weak self] request, error in
self?.processDetections(for: request, error: error)
}
return request
} catch {
fatalError("Failed to load Vision ML model.")
}
}()
func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) {
guard let capturedImage = sceneView.session.currentFrame?.capturedImage
else { return }
let imageRequestHandler = VNImageRequestHandler(cvPixelBuffer: capturedImage, orientation: .leftMirrored, options: [:])
do {
try imageRequestHandler.perform([objectDetectionRequest])
} catch {
print("Failed to perform image request.")
}
}
func processDetections(for request: VNRequest, error: Error?) {
guard error == nil else {
print("Object detection error: \(error!.localizedDescription)")
return
}
guard let results = request.results else { return }
for observation in results where observation is VNRecognizedObjectObservation {
let objectObservation = observation as! VNRecognizedObjectObservation
let topLabelObservation = objectObservation.labels.first
print(topLabelObservation!.identifier + " " + "\(Int(topLabelObservation!.confidence * 100))%")
guard recognisedObject(topLabelObservation!.identifier) && topLabelObservation!.confidence > 0.9
else { continue }
let rect = VNImageRectForNormalizedRect(
objectObservation.boundingBox,
Int(self.sceneView.bounds.width),
Int(self.sceneView.bounds.height))
let midPoint = CGPoint(x: rect.midX, y: rect.midY)
let raycastQuery = self.sceneView.raycastQuery(from: midPoint,
allowing: .estimatedPlane,
alignment: .any)
let raycastArray = self.sceneView.session.raycast(raycastQuery!)
guard let raycastResult = raycastArray.first else { return }
let position = SCNVector3(raycastResult.worldTransform.columns.3.x,
raycastResult.worldTransform.columns.3.y,
raycastResult.worldTransform.columns.3.z)
if let _ = trackingNode {
trackingNode!.worldPosition = position
} else {
trackingNode = createNode()
trackingNode!.worldPosition = position
self.sceneView.scene.rootNode.addChildNode(trackingNode!)
}
}
}
private func recognisedObject(_ identifier: String) -> Bool {
return identifier == "remote" || identifier == "mouse"
}
private func createNode() -> SCNNode {
let sphereNode = SCNNode(geometry: SCNSphere(radius: 0.01))
sphereNode.geometry?.firstMaterial?.diffuse.contents = UIColor.purple
return sphereNode
}
private func loadSession() {
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = []
sceneView.session.run(configuration)
}
override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = self
viewportSize = sceneView.frame.size
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadSession()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
sceneView.session.pause()
}
To be honest, the technologies you're using here cannot do that out of the box. YOLO (and any other object detection model you swapped out for it) have no built in concept of tracking the same object in a video. They look for objects in a 2D bitmap, and return 2D bounding boxes for them. As either the camera or object moves, and you pass in the next capturedImage buffer, it will give you a new bounding box in the correct position, but it has no way of knowing whether or not it's the same instance of the object detected in a previous frame.
To make this work, you'll need to do some post processing of those Vision results to determine whether or not it's the same object, and if so, manually move the anchor/mesh to match the new position. If you're confident there should only be one object in view at any given time, then it's pretty straightforward. If there will be multiple objects, you're venturing into complex (but still achievable) territory.
You could try to incorporate Vision Tracking, which might work though would depend on the nature and behavior of the tracked object.
Also, sceneView.hitTest() is deprecated. You should probably port that over to use ARSession.raycast()

ARKit : Handle tap to show / hide a node

I am new to ARKit , and i am trying an example to create a SCNBox on tap location. What i am trying to do is on initial touch i create a box and on the second tap on the created box it should be removed from the scene. I am doing the hit test. but it keeps on adding the box. I know this is a simple task, but i am unable to do it
#objc func handleTap(sender: UITapGestureRecognizer) {
print("hande tapp")
guard let _ = sceneView.session.currentFrame
else { return }
guard let scnView = sceneView else { return }
let touchLocation = sender.location(in: scnView)
let hitTestResult = scnView.hitTest(touchLocation, types: [ .featurePoint])
guard let pointOfView = sceneView.pointOfView else {return}
print("point \(pointOfView.name)")
if hitTestResult.count > 0 {
print("Hit")
if let _ = pointOfView as? ARBox {
print("Box Available")
}
else {
print("Adding box")
let transform = hitTestResult.first?.worldTransform.columns.3
let xPosition = transform?.x
let yPosition = transform?.y
let zPosition = transform?.z
let position = SCNVector3(xPosition!,yPosition!,zPosition!)
basketCount = basketCount + 1
let newBasket = ARBox(position: position)
newBasket.name = "basket\(basketCount)"
self.sceneView.scene.rootNode.addChildNode(newBasket)
boxNodes.append(newBasket)
}
}
}
pointOfView of a sceneView, is the rootnode of your scene, which is one used to render your whole scene. For generic cases, it usually is an empty node with lights/ camera. I don't think you should cast it as ARBox/ or any type of SCNNode(s) for that matter.
What you probably can try is the logic below (hitResults are the results of your hitTest):
if hitResults.count > 0 {
if let node = hitResults.first?.node as SCNNode? (or ARBox) {
// node.removeFromParentNode()
// or make the node opaque if you don't want to remove
else {
// add node.

Using GKComponent to make reusable shields

I am trying to make a simple game: Space ship on the bottom of the screen shooting asteroids "falling" from the top of the screen.
I am learning ECS and GameplayKit, and have been trying to turn shields into a component. I've heavily relied on Apple's DemoBots sample app, and have lifted the PhysicsComponent, ColliderType, and ContactNotifiableType from the sample code.
A shield needs to render the assets assoicated with it (one for full shields and one for half shields), a different physics body from the ship because it's radius is noticeably larger than the ship, and to keep track of it's state. To do this I wrote:
final class ShieldComponent: GKComponent {
enum ShieldLevel: Int {
case full = 0, half, none
}
var currentShieldLevel: ShieldLevel = .full {
didSet {
switch currentShieldLevel {
case .full:
node.isHidden = false
node.texture = SKTexture(image: #imageLiteral(resourceName: "shield"))
case .half:
node.isHidden = false
node.texture = SKTexture(image: #imageLiteral(resourceName: "damagedShield"))
case .none:
node.isHidden = true
}
}
}
let node: SKSpriteNode
override init() {
node = SKSpriteNode(imageNamed: "shield")
super.init()
node.physicsBody = {
let physicsBody = SKPhysicsBody(circleOfRadius: node.frame.size.width / 2)
physicsBody.pinned = true
physicsBody.allowsRotation = false
physicsBody.affectedByGravity = false
ColliderType.definedCollisions[.shield] = [
.obstacle,
.powerUp
]
physicsBody.categoryBitMask = ColliderType.shield.rawValue
physicsBody.contactTestBitMask = ColliderType.obstacle.rawValue
physicsBody.collisionBitMask = ColliderType.obstacle.rawValue
return physicsBody
}()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func loseShields() {
if let newShieldLevel = ShieldLevel(rawValue: self.currentShieldLevel.rawValue + 1) {
self.currentShieldLevel = newShieldLevel
}
}
func restoreShields() {
self.currentShieldLevel = .full
}
}
And in my Ship initializer I do this:
let shieldComponent = ShieldComponent()
renderComponent.node.addChild(shieldComponent.node)
It would great if I could reuse the RenderComponent, and PhysicsComponent from DemoBots have I have with my ship and asteroid GKEntity subclasses, but components cannot have components. I had made ShieldComponent a ContactNotifiableType, but because the shield node does not actually belong to the ship entity.
I know I'm clearly coming at this wrong, and I'm at a loss of how to correct this. I'm hoping to get an example of how to make a shield component.
You must understand that components are meant to handle only one behaviour. so git rid of the physics code in your init() function and instead Build a Physics component similar to the one in DemoBots.
Tweak Your render Component to your liking. The problem with using DemoBots code is that its not perfectly Suited. So lets tweak it
class RenderComponent: GKComponent {
// MARK: Properties
// The `RenderComponent` vends a node allowing an entity to be rendered in a scene.
#objc let node = SKNode()
var sprite = SKSpriteNode
// init
init(imageNamed name: String) {
self.sprite = SKSpriteNode(imageNamed: name)
}
// MARK: GKComponent
override func didAddToEntity() {
node.entity = entity
}
override func willRemoveFromEntity() {
node.entity = nil
}
}
final class ShieldComponent: GKComponent {
var node : SKSpriteNode
//add reference to ship entity
weak var ship: Ship?
enum ShieldLevel: Int {
case full = 0, half, none
}
var currentShieldLevel: ShieldLevel = .full {
didSet {
switch currentShieldLevel {
case .full:
node.isHidden = false
node.texture = SKTexture(image: #imageLiteral(resourceName: "shield"))
case .half:
node.isHidden = false
node.texture = SKTexture(image: #imageLiteral(resourceName: "damagedShield"))
case .none:
node.isHidden = true
}
}
}
// Grab the visual component from the entity. Unwrap it with a Guard. If the Entity doesnt have the component you get an error.
var visualComponentRef : RenderComponent {
guard let renderComponent = ship?.component(ofType: RenderComponent.self) else {
fatalError("entity must have a render component")
}
}
override init(shipEntity ship: Ship) {
let visualComponent = RenderComponent(imageNamed: "imageName")
node = visualComponent.sprite
self.ship = ship
super.init()
// get rid of this. Use a Physics Component for this, Kep your components to one behaviour only. Make them as dumb as possible.
// node.physicsBody = {
// let physicsBody = SKPhysicsBody(circleOfRadius: node.frame.size.width / 2)
// physicsBody.pinned = true
// physicsBody.allowsRotation = false
// physicsBody.affectedByGravity = false
//
// ColliderType.definedCollisions[.shield] = [
// .obstacle,
// .powerUp
// ]
//
// physicsBody.categoryBitMask = ColliderType.shield.rawValue
// physicsBody.contactTestBitMask = ColliderType.obstacle.rawValue
// physicsBody.collisionBitMask = ColliderType.obstacle.rawValue
// return physicsBody
// }()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func loseShields() {
if let newShieldLevel = ShieldLevel(rawValue: self.currentShieldLevel.rawValue + 1) {
self.currentShieldLevel = newShieldLevel
}
}
func restoreShields() {
self.currentShieldLevel = .full
}
};
Make sure to look at how I changed my components interaction with the entity. You can create a reference Object to Ship Entity Directly. Or you can check weather or not the ShieldComponent has an entity with the entity? property. (beware. it is an optional, so unwrap it.
Once you have the Entity reference you can then search it for other Components and retrieve The using component(ofType:_) property.
eg ship?.component(ofType: RenderComponent.self)
Other than this, I think you have a decent shield component

SpriteKit Shop Scene in game

Any idea how I could implement a shop in my spriteKit game that users could buy different players with coins they have earned in game? any tutorials out there?
This is a multi-step project that took me about 500 loc (more without using .SKS) Here is the link to github finished project: https://github.com/fluidityt/ShopScene
Note, I am using a macOS SpriteKit project because it launches much faster on my computer. Simply change mouseDown() to touchesBegan() to get this to run on iOS.
First edit your GameScene.sks to look like this: (saves a bunch of time coding labels)
Make sure that you name everything EXACTLY as we need this to detect touch:
"entershop", "getcoins", "coinlabel", "levellabel"
This is the main "gameplay" scene and as you click coins++ you get levels and can move around. Clicking the shop will enter the shop.
Here is our GameScene.swift which matches this SKS:
import SpriteKit
class GameScene: SKScene {
let player = Player(costume: Costume.defaultCostume)
lazy var enterNode: SKLabelNode = { return (self.childNode(withName: "entershop") as! SKLabelNode) }()
lazy var coinNode: SKLabelNode = { return (self.childNode(withName: "getcoins" ) as! SKLabelNode) }()
lazy var coinLabel: SKLabelNode = { return (self.childNode(withName: "coinlabel") as! SKLabelNode) }()
lazy var levelLabel: SKLabelNode = { return (self.childNode(withName: "levellabel") as! SKLabelNode) }()
override func didMove(to view: SKView) {
player.name = "player"
if player.scene == nil { addChild(player) }
}
override func mouseDown(with event: NSEvent) {
let location = event.location(in: self)
if let name = atPoint(location).name {
switch name {
case "entershop": view!.presentScene(ShopScene(previousGameScene: self))
case "getcoins": player.getCoins(1)
default: ()
}
}
else {
player.run(.move(to: location, duration: 1))
}
}
override func update(_ currentTime: TimeInterval) {
func levelUp(_ level: Int) {
player.levelsCompleted = level
levelLabel.text = "Level: \(player.levelsCompleted)"
}
switch player.coins {
case 10: levelUp(2)
case 20: levelUp(3)
case 30: levelUp(4)
default: ()
}
}
};
Here you can see that we have a few other things going on not yet introduced: Player and Costume
Player is a spritenode subclass (it doubles as a data model and a UI element). Our player is just a colored square that gets moved around when you click the screen
The player wears something of Costume type, which is just a model that keeps track of data such as price, name, and the texture for the player to display.
Here is Costume.swift:
import SpriteKit
/// This is just a test method should be deleted when you have actual texture assets:
private func makeTestTexture() -> (SKTexture, SKTexture, SKTexture, SKTexture) {
func texit(_ sprite: SKSpriteNode) -> SKTexture { return SKView().texture(from: sprite)! }
let size = CGSize(width: 50, height: 50)
return (
texit(SKSpriteNode(color: .gray, size: size)),
texit(SKSpriteNode(color: .red, size: size)),
texit(SKSpriteNode(color: .blue, size: size)),
texit(SKSpriteNode(color: .green, size: size))
)
}
/// The items that are for sale in our shop:
struct Costume {
static var allCostumes: [Costume] = []
let name: String
let texture: SKTexture
let price: Int
init(name: String, texture: SKTexture, price: Int) { self.name = name; self.texture = texture; self.price = price
// This init simply adds all costumes to a master list for easy sorting later on.
Costume.allCostumes.append(self)
}
private static let (tex1, tex2, tex3, tex4) = makeTestTexture() // Just a test needed to be deleted when you have actual assets.
static let list = (
// Hard-code any new costumes you create here (this is a "master list" of costumes)
// (make sure all of your costumes have a unique name, or the program will not work properly)
gray: Costume(name: "Gray Shirt", texture: tex1 /*SKTexture(imageNamed: "grayshirt")*/, price: 0),
red: Costume(name: "Red Shirt", texture: tex2 /*SKTexture(imageNamed: "redshirt")*/, price: 5),
blue: Costume(name: "Blue Shirt", texture: tex3 /*SKTexture(imageNamed: "blueshirt")*/, price: 25),
green: Costume(name: "Green Shirt", texture: tex4 /*SKTexture(imageNamed: "greenshirt")*/, price: 50)
)
static let defaultCostume = list.gray
};
func == (lhs: Costume, rhs: Costume) -> Bool {
// The reason why you need unique names:
if lhs.name == rhs.name { return true }
else { return false }
}
The design of this struct is twofold.. first is to be a blueprint for a Costume object (which holds the name, price, and texture of a costume), and second it serves as a repository for all of your costumes via a hard-coded static master list property.
The function at the top makeTestTextures() is just an example for this project. I did this just so that way you can copy and paste instead of having to download image files to use.
Here is the Player.swift, which can wear the costumes in the list:
final class Player: SKSpriteNode {
var coins = 0
var costume: Costume
var levelsCompleted = 0
var ownedCostumes: [Costume] = [Costume.list.gray] // FIXME: This should be a Set, but too lazy to do Hashable.
init(costume: Costume) {
self.costume = costume
super.init(texture: costume.texture, color: .clear, size: costume.texture.size())
}
func getCoins(_ amount: Int) {
guard let scene = self.scene as? GameScene else { // This is very specific code just for this example.
fatalError("only call this func after scene has been set up")
}
coins += amount
scene.coinLabel.text = "Coins: \(coins)"
}
func loseCoins(_ amount: Int) {
guard let scene = self.scene as? GameScene else { // This is very specific code just for this example.
fatalError("only call this func after scene has been set up")
}
coins -= amount
scene.coinLabel.text = "Coins: \(coins)"
}
func hasCostume(_ costume: Costume) -> Bool {
if ownedCostumes.contains(where: {$0.name == costume.name}) { return true }
else { return false }
}
func getCostume(_ costume: Costume) {
if hasCostume(costume) { fatalError("trying to get costume already owned") }
else { ownedCostumes.append(costume) }
}
func wearCostume(_ costume: Costume) {
guard hasCostume(costume) else { fatalError("trying to wear a costume you don't own") }
self.costume = costume
self.texture = costume.texture
}
required init?(coder aDecoder: NSCoder) { fatalError() }
};
Player has a lot of functions, but they all could be handled elsewhere in the code. I just went for this design decision, but don't feel like you need to load up your classes with 2 line methods.
Now we are getting to the more nitty-gritty stuff, since we have set up our:
Base scene
Costume list
Player object
The last two things we really need are:
1. A shop model to keep track of inventory
2. A shop scene to display inventory, UI elements, and handle the logic of whether or not you can buy items
Here is Shop.swift:
/// Our model class to be used inside of our ShopScene:
final class Shop {
weak private(set) var scene: ShopScene! // The scene in which this shop will be called from.
var player: Player { return scene.player }
var availableCostumes: [Costume] = [Costume.list.red, Costume.list.blue] // (The green shirt wont become available until the player has cleared 2 levels).
// var soldCostumes: [Costume] = [Costume.defaultCostume] // Implement something with this if you want to exclude previously bought items from the store.
func canSellCostume(_ costume: Costume) -> Bool {
if player.coins < costume.price { return false }
else if player.hasCostume(costume) { return false }
else if player.costume == costume { return false }
else { return true }
}
/// Only call this after checking canBuyCostume(), or you likely will have errors:
func sellCostume(_ costume: Costume) {
player.loseCoins(costume.price)
player.getCostume(costume)
player.wearCostume(costume)
}
func newCostumeBecomesAvailable(_ costume: Costume) {
if availableCostumes.contains(where: {$0.name == costume.name}) /*|| soldCostumes.contains(costume)*/ {
fatalError("trying to add a costume that is already available (or sold!)")
}
else { availableCostumes.append(costume) }
}
init(shopScene: ShopScene) {
self.scene = shopScene
}
deinit { print("shop: if you don't see this message when exiting shop then you have a retain cycle") }
};
The idea was to have the fourth costume only be available at a certain level, but I've run out of time to implement this feature, but most of the supporting methods are there (you just need to implement the logic).
Also, Shop can pretty much just be a struct, but I feel that it's more flexible as a class for now.
Now, before jumping into ShopScene, our biggest file, let me tell you about a couple of design decisions.
First, I'm using node.name to handle touches / clicks. This lets me use the .SKS and the regular SKNode types quickly and easily. Normally, I like to subclass SKNodes and then override their own touchesBegan method to handle clicks. You can do it either way.
Now, in ShopScene you have buttons for "buy", "exit" which I have used as just regular SKLabelNodes; but for the actual nodes that display the costume, I have created a subclass called CostumeNode.
I made CostumeNode so that way it could handle nodes for displaying the costume's name, price, and doing some animations. CostumeNode is just a visual element (unlike Player).
Here is CostumeNode.swift:
/// Just a UI representation, does not manipulate any models.
final class CostumeNode: SKSpriteNode {
let costume: Costume
weak private(set) var player: Player!
private(set) var
backgroundNode = SKSpriteNode(),
nameNode = SKLabelNode(),
priceNode = SKLabelNode()
private func label(text: String, size: CGSize) -> SKLabelNode {
let label = SKLabelNode(text: text)
label.fontName = "Chalkduster"
// FIXME: deform label to fit size and offset
return label
}
init(costume: Costume, player: Player) {
func setupNodes(with size: CGSize) {
let circle = SKShapeNode(circleOfRadius: size.width)
circle.fillColor = .yellow
let bkg = SKSpriteNode(texture: SKView().texture(from: circle))
bkg.zPosition -= 1
let name = label(text: "\(costume.name)", size: size)
name.position.y = frame.maxY + name.frame.size.height
let price = label(text: "\(costume.price)", size: size)
price.position.y = frame.minY - price.frame.size.height
addChildrenBehind([bkg, name, price])
(backgroundNode, nameNode, priceNode) = (bkg, name, price)
}
self.player = player
self.costume = costume
let size = costume.texture.size()
super.init(texture: costume.texture, color: .clear, size: size)
name = costume.name // Name is needed for sorting and detecting touches.
setupNodes(with: size)
becomesUnselected()
}
private func setPriceText() { // Updates the color and text of price labels
func playerCanAfford() {
priceNode.text = "\(costume.price)"
priceNode.fontColor = .white
}
func playerCantAfford() {
priceNode.text = "\(costume.price)"
priceNode.fontColor = .red
}
func playerOwns() {
priceNode.text = ""
priceNode.fontColor = .white
}
if player.hasCostume(self.costume) { playerOwns() }
else if player.coins < self.costume.price { playerCantAfford() }
else if player.coins >= self.costume.price { playerCanAfford() }
else { fatalError() }
}
func becomesSelected() { // For animation / sound purposes (could also just be handled by the ShopScene).
backgroundNode.run(.fadeAlpha(to: 0.75, duration: 0.25))
setPriceText()
// insert sound if desired.
}
func becomesUnselected() {
backgroundNode.run(.fadeAlpha(to: 0, duration: 0.10))
setPriceText()
// insert sound if desired.
}
required init?(coder aDecoder: NSCoder) { fatalError() }
deinit { print("costumenode: if you don't see this then you have a retain cycle") }
};
Finally we have ShopScene, which is the behemoth file. It handles the data and logic for not only showing UI elements, but also for updating the Shop and Player models.
import SpriteKit
// Helpers:
extension SKNode {
func addChildren(_ nodes: [SKNode]) { for node in nodes { addChild(node) } }
func addChildrenBehind(_ nodes: [SKNode]) { for node in nodes {
node.zPosition -= 2
addChild(node)
}
}
}
func halfHeight(_ node: SKNode) -> CGFloat { return node.frame.size.height/2 }
func halfWidth (_ node: SKNode) -> CGFloat { return node.frame.size.width/2 }
// MARK: -
/// The scene in which we can interact with our shop and player:
class ShopScene: SKScene {
lazy private(set) var shop: Shop = { return Shop(shopScene: self) }()
let previousGameScene: GameScene
var player: Player { return self.previousGameScene.player } // The player is actually still in the other scene, not this one.
private var costumeNodes = [CostumeNode]() // All costume textures will be node-ified here.
lazy private(set) var selectedNode: CostumeNode? = {
return self.costumeNodes.first!
}()
private let
buyNode = SKLabelNode(fontNamed: "Chalkduster"),
coinNode = SKLabelNode(fontNamed: "Chalkduster"),
exitNode = SKLabelNode(fontNamed: "Chalkduster")
// MARK: - Node setup:
private func setUpNodes() {
buyNode.text = "Buy Costume"
buyNode.name = "buynode"
buyNode.position.y = frame.minY + halfHeight(buyNode)
coinNode.text = "Coins: \(player.coins)"
coinNode.name = "coinnode"
coinNode.position = CGPoint(x: frame.minX + halfWidth(coinNode), y: frame.minY + halfHeight(coinNode))
exitNode.text = "Leave Shop"
exitNode.name = "exitnode"
exitNode.position.y = frame.maxY - buyNode.frame.height
setupCostumeNodes: do {
guard Costume.allCostumes.count > 1 else {
fatalError("must have at least two costumes (for while loop)")
}
for costume in Costume.allCostumes {
costumeNodes.append(CostumeNode(costume: costume, player: player))
}
guard costumeNodes.count == Costume.allCostumes.count else {
fatalError("duplicate nodes found, or nodes are missing")
}
let offset = CGFloat(150)
func findStartingPosition(offset: CGFloat, yPos: CGFloat) -> CGPoint { // Find the correct position to have all costumes centered on screen.
let
count = CGFloat(costumeNodes.count),
totalOffsets = (count - 1) * offset,
textureWidth = Costume.list.gray.texture.size().width, // All textures must be same width for centering to work.
totalWidth = (textureWidth * count) + totalOffsets
let measurementNode = SKShapeNode(rectOf: CGSize(width: totalWidth, height: 0))
return CGPoint(x: measurementNode.frame.minX + textureWidth/2, y: yPos)
}
costumeNodes.first!.position = findStartingPosition(offset: offset, yPos: self.frame.midY)
var counter = 1
let finalIndex = costumeNodes.count - 1
// Place nodes from left to right:
while counter <= finalIndex {
let thisNode = costumeNodes[counter]
let prevNode = costumeNodes[counter - 1]
thisNode.position.x = prevNode.frame.maxX + halfWidth(thisNode) + offset
counter += 1
}
}
addChildren(costumeNodes)
addChildren([buyNode, coinNode, exitNode])
}
// MARK: - Init:
init(previousGameScene: GameScene) {
self.previousGameScene = previousGameScene
super.init(size: previousGameScene.size)
}
required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented")}
deinit { print("shopscene: if you don't see this message when exiting shop then you have a retain cycle") }
// MARK: - Game loop:
override func didMove(to view: SKView) {
anchorPoint = CGPoint(x: 0.5, y: 0.5)
setUpNodes()
select(costumeNodes.first!) // Default selection.
for node in costumeNodes {
if node.costume == player.costume { select(node) }
}
}
// MARK: - Touch / Click handling:
private func unselect(_ costumeNode: CostumeNode) {
selectedNode = nil
costumeNode.becomesUnselected()
}
private func select(_ costumeNode: CostumeNode) {
unselect(selectedNode!)
selectedNode = costumeNode
costumeNode.becomesSelected()
if player.hasCostume(costumeNode.costume) { // Wear selected costume if owned.
player.costume = costumeNode.costume
buyNode.text = "Bought Costume"
buyNode.alpha = 1
}
else if player.coins < costumeNode.costume.price { // Can't afford costume.
buyNode.text = "Buy Costume"
buyNode.alpha = 0.5
}
else { // Player can buy costume.
buyNode.text = "Buy Costume"
buyNode.alpha = 1
}
}
// I'm choosing to have the buttons activated by searching for name here. You can also
// subclass a node and have them do actions on their own when clicked.
override func mouseDown(with event: NSEvent) {
guard let selectedNode = selectedNode else { fatalError() }
let location = event.location(in: self)
let clickedNode = atPoint(location)
switch clickedNode {
// Clicked empty space:
case is ShopScene:
return
// Clicked Buy / Leave:
case is SKLabelNode:
if clickedNode.name == "exitnode" { view!.presentScene(previousGameScene) }
if clickedNode.name == "buynode" {
// guard let shop = shop else { fatalError("where did the shop go?") }
if shop.canSellCostume(selectedNode.costume) {
shop.sellCostume(selectedNode.costume)
coinNode.text = "Coins: \(player.coins)"
buyNode.text = "Bought"
}
}
// Clicked a costume:
case let clickedCostume as CostumeNode:
for node in costumeNodes {
if node.name == clickedCostume.name {
select(clickedCostume)
}
}
default: ()
}
}
};
There's a lot to digest here, but pretty much everything happens in mouseDown() (or touchesBegan for iOS). I had no need for update() or other every-frame methods.
So how did I make this? The first step was planning, and I knew there were several design decisions to make (which may not have been the best ones).
I knew that I needed a certain set of data for my player and shop inventory, and that those two things would also need UI elements.
I chose to combine the data + UI for Player by making it a Sprite subclass.
For the shop, I knew that the data and UI elements would be pretty intense, so I separated them (Shop.swift handling the inventory, Costume.swift being a blueprint, and CostumeNode.swift handling most of the UI)
Then, I needed to link the data to the UI elements, which meant that I needed a lot of logic, so I decided to make a whole new scene to handle logic pertaining just to entering and interacting with the shop (it handles some graphics stuff too).
This all works together like this:
Player has a costume and coins
GameScene is where you collect new coins (and levels)
ShopScene handles most of the logic for determining which UI elements to display, while CostumeNode has the functions for animating the UI.
ShopScene also provides the logic for updating the Player's texture (costume) and coins through Shop.
Shop just manages the player inventory, and has the data with which to populate more CostumeNodes
When you are done with the shop, your GameScene instance is immediately resumed where you left off prior to entering
So the question you may have is, "how do I use this in my game??"
Well, you aren't going to be able to just copy and paste it. A lot of refactoring will likely be needed. The takeaway here is to learn the basic system of the different types of data, logic, and actions that you will need to create, present, and interact with a shop.
Here is the github again:
https://github.com/fluidityt/ShopScene

SpriteKit reference nodes from level editor

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
}
}
}

Resources