Obligatory: first time writing an app, relevant code is below. My code runs mostly as I want it to, but does not achieve my goal of dynamic object tracking.
I'm using Swift and Scenekit to build a simple puzzle game, similar to a 3d-version of candy crush.
I have a class Cube that extends SCNNode. On initialization, this class will randomly draw a 5x5 cube of SCNBoxes with each box being red, green, or blue (all 6 sides of a box are 1 color).
The goal of the game is to get the highest score by removing "chains" of SCNBoxes of like-colors. When a chain is removed, cubes should recognize gravity and drop to fill in the voids created by the removed-chain. This is where I need to dynamically track position. As the cubes fall into the gaps, their neighbors change.
My approach: build a struct CubeDetails that has properties var color: String and var location: SCNVector3. Next, build a dictionary masterCubeDict = [SCNNode: CubeDetails] that has all of the cubes of 1 color (the color is provided by a hittestresult).
Every time a user taps a cube, grab its color, refresh the masterCubeDict, and then use math on the SCNVector3 position to determine which cubes are neighbors.
I think my algorithm for finding 'cube neighbors' using math on the scnvector3 is where I'm off. There must be a better way for scenekit nodes to identity/find each other, right?
Also -- I would like the physics of the cubes to let them fall and have no bounce/sliding at all. They should only move straight up/down. Collisions should never happen. I thought I implemented that properly through friction, restituion, and mass of the cubes but I'm not getting the outcome I want.
class Cube
import SceneKit
class Cube : SCNNode {
let cubeWidth:Float = 0.95
let spaceBetweenCubes:Float = 0.05
var cubecolor:UIColor = UIColor.black
var masterCubeDict: [SCNNode: CubeDetails] = [:]
struct CubeDetails {
var color:String
var position:SCNVector3
}
override init() {
super.init()
let cubeOffsetDistance = self.cubeOffsetDistance()
var cubeColorString: String = ""
var xPos:Float = -cubeOffsetDistance
var yPos:Float = -cubeOffsetDistance
var zPos:Float = -cubeOffsetDistance
let xFloor:Float = -1.5
let yFloor:Float = -1.5
let zFloor:Float = -1.5
let floorGeo = SCNBox(width: 20, height: 0, length: 20, chamferRadius: 0)
let floor = SCNNode(geometry: floorGeo)
floor.position = SCNVector3(x: xFloor, y: yFloor, z: zFloor)
floor.name = "floor"
floor.opacity = 0.0
floor.physicsBody = SCNPhysicsBody(type: .kinematic, shape: nil)
floor.physicsBody?.collisionBitMask = 1
floor.physicsBody?.friction = 1.0
self.addChildNode(floor)
for _ in 0..<5 {
for _ in 0..<5 {
for _ in 0..<5 {
let cubeGeometry = SCNBox(width: CGFloat(cubeWidth), height: CGFloat(cubeWidth), length: CGFloat(cubeWidth), chamferRadius: 0)
let material = SCNMaterial()
material.diffuse.contents = randomColor()
//unwrap material (type any) and cast to uicolor for switch
if let unwrapColor: UIColor = material.diffuse.contents as? UIColor {
switch unwrapColor {
case UIColor.red:
cubeColorString = "red"
case UIColor.green:
cubeColorString = "green"
case UIColor.blue:
cubeColorString = "blue"
default:
cubeColorString = "black"
}
} else { print("Error unwrapping color") }
cubeGeometry.materials = [material, material, material, material, material, material]
let cube = SCNNode(geometry: cubeGeometry)
cube.name = cubeColorString
cube.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
cube.physicsBody?.restitution = 0.0
cube.physicsBody?.isAffectedByGravity = true
cube.physicsBody?.mass = 25.0
cube.physicsBody?.friction = 1.0
cube.physicsBody?.collisionBitMask = 1
cube.position = SCNVector3(x: xPos, y: yPos, z: zPos)
let details = CubeDetails(color: cubeColorString, position: cube.position)
//add cube details to the master dict
masterCubeDict[cube] = details
//print(masterCubeDict)
xPos += cubeWidth + spaceBetweenCubes
self.addChildNode(cube)
}
xPos = -cubeOffsetDistance
yPos += cubeWidth + spaceBetweenCubes
}
xPos = -cubeOffsetDistance
yPos = -cubeOffsetDistance
zPos += cubeWidth + spaceBetweenCubes
}
}
private func cubeOffsetDistance()->Float {
return (cubeWidth + spaceBetweenCubes) / 2
}
private func randomColor() -> UIColor{
var tmpColor: UIColor
let num = Int.random(in:0...2)
switch num {
case 0:
tmpColor = UIColor.red
case 1:
tmpColor = UIColor.blue
case 2:
tmpColor = UIColor.green
default:
tmpColor = UIColor.black
}
return tmpColor
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
GameViewController
import UIKit
import QuartzCore
import SceneKit
var myMasterCubeDict: [SCNNode: Cube.CubeDetails] = [:]
class GameViewController: UIViewController {
let gameCube = Cube()
override func viewDidLoad() {
super.viewDidLoad()
// create a new scene
// let scene = SCNScene(named: "art.scnassets/ship.scn")!
let scene = SCNScene()
// create and add a camera to the scene
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)
// place the camera
cameraNode.position = SCNVector3(x: 2, y: 0, z: 20)
// create and add a light to the scene
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light!.type = .omni
lightNode.position = SCNVector3(x: 0, y: 10, z: 10)
scene.rootNode.addChildNode(lightNode)
// create and add an ambient light to the scene
let ambientLightNode = SCNNode()
ambientLightNode.light = SCNLight()
ambientLightNode.light!.type = .ambient
ambientLightNode.light!.color = UIColor.darkGray
scene.rootNode.addChildNode(ambientLightNode)
// init cube
myMasterCubeDict = gameCube.masterCubeDict
scene.rootNode.addChildNode(gameCube)
// retrieve the SCNView
let scnView = self.view as! SCNView
// set the scene to the view
scnView.scene = scene
// allows the user to manipulate the camera
scnView.allowsCameraControl = true
// show statistics such as fps and timing information
scnView.showsStatistics = true
// configure the view
scnView.backgroundColor = UIColor.black
// add a tap gesture recognizer
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
scnView.addGestureRecognizer(tapGesture)
}
#objc
func handleTap(_ gestureRecognize: UIGestureRecognizer) {
// retrieve the SCNView
let scnView = self.view as! SCNView
// check what nodes are tapped
let p = gestureRecognize.location(in: scnView)
let hitResults = scnView.hitTest(p, options: [:])
// check that we clicked on at least one object
if hitResults.count > 0 {
// retrieved the first clicked object
let result = hitResults[0]
//get dict of same-color node
var dictOfSameColor = findAndReturnChain(boi: result.node)
// print(dictOfSameColor)
var finalNodes: [SCNNode] = [result.node]
var resFlag = 1
repeat {
var xSame: Bool = false
var ySame: Bool = false
var zSame: Bool = false
resFlag = 0
for node in finalNodes {
// var nodeX = node.position.x
for (key, value) in dictOfSameColor {
if(abs(node.position.x - value.position.x) < 0.7) {
xSame = true
}
if(abs(node.position.y - value.position.y) < 0.7) {
ySame = true
}
if(abs(node.position.z - value.position.z) < 0.7) {
zSame = true
}
//print("X-val: \(xDif) \nY-val: \(yDif) \nZ-val: \(zDif) \nColor: \(key.name) \n\n\n\n")
if (xSame && ySame ) {
if !(zSame) {
if (abs((node.position.z-value.position.z)) < 2) {
finalNodes.append(key)
dictOfSameColor.removeValue(forKey: key)
resFlag = 1
}
}
}
if (xSame && zSame) {
if !(ySame) {
if (abs((node.position.y-value.position.y)) < 2) {
finalNodes.append(key)
dictOfSameColor.removeValue(forKey: key)
resFlag = 1
}
}
}
if (ySame && zSame) {
if !(xSame) {
if (abs((node.position.x-value.position.x)) < 2) {
finalNodes.append(key)
dictOfSameColor.removeValue(forKey: key)
resFlag = 1
}
}
}
xSame = false
ySame = false
zSame = false
}
}
//print(finalNodes)
} while resFlag == 1
//print(finalNodes)
for node in finalNodes {
if node.name != "floor" {
node.removeFromParentNode()
}
}
//IMPLEMENT: Reset dicts to current state of the cube
myMasterCubeDict = updateMasterCubeDict(cube: gameCube)
dictOfSameColor.removeAll()
}
}
func findAndReturnChain(boi: SCNNode) -> [SCNNode:Cube.CubeDetails] {
var ret: [SCNNode:Cube.CubeDetails] = [:]
//find cubes with the same color
for (key, value) in myMasterCubeDict {
if value.color == boi.name {
ret[key] = value
}
}
return ret
}
func updateMasterCubeDict(cube: Cube) -> [SCNNode:Cube.CubeDetails] {
myMasterCubeDict.removeAll()
var newNode: SCNNode = SCNNode()
var newDetails = Cube.CubeDetails(color: "", position: SCNVector3Zero)
cube.enumerateChildNodes { (cube, stop) in
newNode = cube
if let newName = cube.name {
newDetails.color = newName
}
newDetails.position = cube.position
myMasterCubeDict[newNode] = newDetails
}
return myMasterCubeDict
}
override var shouldAutorotate: Bool {
return true
}
override var prefersStatusBarHidden: Bool {
return true
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.userInterfaceIdiom == .phone {
return .allButUpsideDown
} else {
return .all
}
}
}
I did a game somewhat like this. You could probably get the math to work, but the way I did it was to map out each node and have an array containing its adjacent nodes. Doing it this way, I'm sure that when I remove a node and loop through its adjacent[array] nodes, then I got the right ones.
I don't subclass SCNNodes - some do, but I create the class I want that contains info about my node - I add the node to Scenekit there, that's separates the actual node from other work I may want to do with the class. Some nodes have a lot of detail that I may want to manage separately (multiple particle systems, movements, etc). Then I just keep my classes of nodes in an array and each class has direct access to it's own node.
Sorry - I don't know about the bounce, there are a lot of choices with the physics engine.
Related
I describe as StackOverflow standards the following issue.
Summarize the problem
I have issue about colliding two nodes. One is composed by a crowd and each people is a single item of my crowd defined in the same way (included in a while just to be clear and using index "i" and "j" to create the row of the crowd). I wanted to make disappear once arrive a node (as civilian) to the bottom and going to the top and the crowd remove the civilian spawned along the path. Actually I have this thing and the func tells me that the colliding happens but it didn't 'cause nothing happens and the civilian node captured by crowd it didn't disappear or removed with the removefromparent(). I've got no error messages with my compiler, it works for him. My scope is : detecting node civilian during the path by the crowd and remove this one from the path.
What I've tried
I tried many things to fix this. The first thing is following a lot of tutorials about how Collision Masks etc.. work. I know what they do. But what I've tried it was to make a invisible line for the last line of crowd of people just to see if the problem is the crowd itself and making that if the civilian node arrives to collide the invisible line is like he was in contact with the crowd but it didin't this effect. I followed a lot of tutorial such as HackingWithswift, Youtube tutorials but the procedure for me it's clear but nothing happens (sorry for being repetitive).
Show code
My problem is about this GameScene.sks because it it just one file with all the functions.
import SpriteKit
import GameplayKit
enum CategoryMask: UInt32 {
case civilian_value = 1
case crowd_value = 2
case background_value = 0
}
enum GameState {
case ready
case playing
case dead
}
var gameState = GameState.ready {
didSet {
print(gameState)
}
}
class GameScene: SKScene, SKPhysicsContactDelegate {
let player = SKSpriteNode(imageNamed: "player1")
let textureA = SKTexture(imageNamed: "player1")
let textureB = SKTexture(imageNamed: "player2")
let pause = SKSpriteNode(imageNamed: "pause-button")
let resume = SKSpriteNode(imageNamed: "pause-button")
var civilian = SKSpriteNode()
let pauseLayer = SKNode()
let gameLayer = SKNode()
weak var sceneDelegate: GameSceneDelegate?
//main
override func didMove(to view: SKView) {
self.anchorPoint = CGPoint(x: 0.5, y: 0.5)
self.physicsWorld.gravity = CGVector(dx: 0, dy: 0)
self.physicsWorld.contactDelegate = self
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
//func for dynamic background
moveBackground(image: ["background1", "background2", "background3", "background1"], x: 0, z: -3, duration: 5, size: CGSize(width: 0.5, height: 1.0))
character(player: player)
run(SKAction.repeatForever(
SKAction.sequence([
SKAction.run(civilians),
SKAction.wait(forDuration: 3.0)])))
run(SKAction.run(crowdSpawn))
pause.name="pause"
pause.position = CGPoint(x: frame.minX/1.3, y: frame.minY/1.15)
pause.size=CGSize(width: 0.1, height: 0.1)
pause.zPosition = 4
addChild(pause)
if self.scene?.isPaused == true {
resume.name="resume"
resume.position = CGPoint(x: frame.minX/1.5, y: frame.minY/1.15)
resume.size=CGSize(width: 0.1, height: 0.1)
resume.zPosition = 12
addChild(resume)
}
}
func pauseGame() {
sceneDelegate?.gameWasPaused()
let barr = SKSpriteNode()
let barrbehind = SKSpriteNode()
let buttonresume = SKSpriteNode(imageNamed: "back")
barrbehind.name = "barrbehind"
barrbehind.zPosition = 9
barrbehind.color = SKColor.black
barrbehind.size = CGSize(width: frame.width, height: frame.height)
barrbehind.alpha = 0.5
self.addChild(barradietro)
barr.name = "bar"
barr.size = CGSize(width: 0.4, height: 0.5)
barr.color = SKColor.white
barr.zPosition = 10
self.addChild(barr)
buttonresume.name = "resume"
buttonresume.zPosition = 11
buttonresume.color = SKColor.black
buttonresume.size = CGSize(width: 0.1, height: 0.1)
buttonresume.alpha = 0.5
self.addChild(buttonresume)
self.scene?.isPaused = true
}
//random func (it helps for generate randomly civilians along the path
func random() -> CGFloat {
return CGFloat(Float(arc4random()) / 0xFFFFFFFF)
}
func random(min: CGFloat, max: CGFloat) -> CGFloat {
return random() * (max - min) + min
}
//func to define civilians
func civilians() {
let civilian = SKSpriteNode(imageNamed: "PV")
civilian.name = "civilian"
//posiziono il civile
civilian.position = CGPoint(x: frame.size.width/8.0 * random(min: -1.5, max: 1.5), y: -frame.size.height * 0.45)
civilian.physicsBody = SKPhysicsBody(rectangleOf: civilian.size)
civilian.zPosition = 3
civilian.physicsBody?.categoryBitMask = CategoryMask.civilian_value.rawValue
civilian.physicsBody?.collisionBitMask = CategoryMask.crowd_value.rawValue
civilian.physicsBody?.contactTestBitMask = CategoryMask.crowd_value.rawValue
civilian.physicsBody?.isDynamic = true
//civilian size
civilian.size=CGSize(width: 0.2, height: 0.2)
//civilian movement
civilian.run(
SKAction.moveBy(x: 0.0, y: frame.size.height + civilian.size.height,duration: TimeInterval(1.77)))
addChild(civilian)
}
//func for the main character
func character(player: SKSpriteNode){
player.position = CGPoint(x: 0, y: 0)
player.size = CGSize(width: 0.2, height: 0.2)
let animation = SKAction.animate(with: [textureB,textureA], timePerFrame:0.2)
player.position = CGPoint(x: frame.midX, y: frame.midY)
addChild(player)
player.run(SKAction.repeatForever(animation))
}
//func for generate the crowd
func crowdSpawn(){
var i = 0.0
var j = 0.25
var crowdRaw : Bool = true
while crowdRaw {
if i <= 1 {
let crowd = SKSpriteNode(imageNamed: "player1")
crowd.name = "crowd"
//posiziono il civile
crowd.size=CGSize(width: 0.15, height: 0.15)
crowd.position = CGPoint(x: -frame.size.width / 3.6 + CGFloat(i)/2 * crowd.size.width , y: frame.size.height / 2 + (CGFloat(j)*2) * -crowd.size.height)
crowd.zPosition = 3
let animation = SKAction.animate(with: [textureB,textureA], timePerFrame:0.25)
crowd.run(SKAction.repeatForever(animation))
crowd.run(SKAction.moveBy(x: frame.size.width / 16.0 + CGFloat(i) * crowd.size.width, y: 0, duration: 0))
let infectedCollision = SKSpriteNode(color: UIColor.red,
size: CGSize(width: 1, height: 0.1))
infectedCollision.physicsBody = SKPhysicsBody(rectangleOf: infectedCollision.size)
infectedCollision.physicsBody?.categoryBitMask = CategoryMask.crowd_value.rawValue
//collisionBitMask : qui la linea della folla non può collidere con il civilian
infectedCollision.physicsBody?.collisionBitMask = CategoryMask.civilian_value.rawValue
infectedCollision.physicsBody?.contactTestBitMask = CategoryMask.civilian_value.rawValue
infectedCollision.physicsBody?.isDynamic = true
infectedCollision.name = "infectedCollision"
infectedCollision.position = crowd.position
addChild(crowd)
addChild(infectedCollision)
i += 0.25
} else {
j += 0.25
i = 0.0
}
if j == 1 {
crowdRaw = false
}
}
}
func didBegin(_ contact: SKPhysicsContact) {
if contact.bodyA.node?.position == contact.bodyB.node?.position {
let actionMoveDone = SKAction.removeFromParent()
civilian.run(SKAction.sequence([actionMoveDone]))
}
}
//func about the touches
func touchDown(atPoint pos : CGPoint) {
let action = SKAction.move(to: pos, duration: 1.0)
// playerSprite is a SpriteKit sprite node.
player.run(action)
}
//func about the touches
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
switch gameState {
case .ready:
gameState = .playing
case .playing:
for t in touches {
let location = t.location(in: self)
player.position.x = location.x/2
for node in self.nodes(at: location){
if node.name == "civilian" {
let explode = SKAction.colorize(with: UIColor.systemBlue,colorBlendFactor: 5.0, duration: 2)
let vanish = SKAction.fadeOut(withDuration: 2.0)
node.run(explode , completion: {
node.run(vanish) {
node.removeFromParent()
}
})
}else if node.name == "pause" {
pauseGame()
}else if node.name == "resume" {
self.scene?.isPaused = false
}
}
}
case .dead:
print("dead")
}
}
//function to have different backgrounds in scrolling (3 backgrounds in a loop)
func moveBackground(image: [String], x: CGFloat, z:CGFloat, duration: Double, size: CGSize) {
for i in 0...3 {
let background = SKSpriteNode(imageNamed: image[i])
background.position = CGPoint(x: x, y: size.height * CGFloat(i))
background.size = size
background.zPosition = z
let move = SKAction.moveBy(x: 0, y: -background.size.height*3, duration: 0)
let back = SKAction.moveBy(x: 0, y: background.size.height*3, duration: duration)
let sequence = SKAction.sequence([move,back])
let repeatAction = SKAction.repeatForever(sequence)
addChild(background)
background.run(repeatAction)
}
}
}
Ok, it was fun to recall how all this SpriteKit stuff works :D
First problem you have is node/sprite creation. The solution could be some kind of Factory pattern with more or less abstraction. GameScene doesn't have to know how nodes are initialized/configured. Scene could know only which type of nodes exist, and thats enough to get them ready for use.
//MARK: - Factory
protocol AbstractFactory {
func getNode()-> SKNode
func getNodeConfig()->SpriteConfig
}
class CivilianFactory : AbstractFactory {
// Local Constants
private struct K {
static let size = CGSize(width: 32, height: 32)
static let name = "civilian"
static let color = UIColor.yellow
}
// Here we get Civilian sprite config
func getNodeConfig() -> SpriteConfig {
let physics = SpritePhysicsConfig(categoryMask: Collider.civilian, contactMask: Collider.player | Collider.wall, collisionMask: Collider.none)
return SpriteConfig(name: K.name, size: K.size, color: K.color, physics: physics)
}
func getNode() -> SKNode {
let config = getNodeConfig()
let sprite = SKSpriteNode(color: config.color, size: config.size)
sprite.color = config.color
sprite.name = config.name
sprite.zPosition = 1
if let physics = config.physics {
sprite.physicsBody = SKPhysicsBody(rectangleOf: config.size)
sprite.physicsBody?.isDynamic = physics.isDynamic
sprite.physicsBody?.affectedByGravity = physics.isAffectedByGravity
sprite.physicsBody?.categoryBitMask = physics.categoryMask
sprite.physicsBody?.contactTestBitMask = physics.contactMask
sprite.physicsBody?.collisionBitMask = physics.collisionMask
}
}
return sprite
}
}
Same as this, You will make other "factories" as needed (just copy the factory and change visual/physics data setup). For this example I will make PlayerFactory.
and with next method I will create my nodes:
private func getNode(factory:AbstractFactory)->SKNode{
return factory.getNode()
}
and then just use it like this:
let node = getNode(factory: self.civiliansFactory) // or self.whateverFactory
Here you just provide a factory you want (can be anything that conforms to AbstractFactory), and in return, You get a desired node (You can return here anything that is SKNode). This way, we have hid initialization process, dependencies etc. from outside world (GameScene), and put everything in one place.
So, quite flexible, plus removes a bunch of repeating code from your scene.
And here are config structs for sprites creation:
//MARK: - Sprite Config
struct SpriteConfig {
let name:String
let size:CGSize
let color:UIColor
let physics:SpritePhysicsConfig? // lets make this optional
}
struct SpritePhysicsConfig {
let categoryMask: UInt32
let contactMask: UInt32
let collisionMask:UInt32
let isDynamic:Bool
let isAffectedByGravity:Bool
init(categoryMask:UInt32, contactMask:UInt32, collisionMask:UInt32, isDynamic:Bool = true, isAffectedByGravity:Bool = false){
self.categoryMask = categoryMask
self.contactMask = contactMask
self.collisionMask = collisionMask
self.isDynamic = isDynamic
self.isAffectedByGravity = isAffectedByGravity
}
}
Now some useful extensions that I needed:
//MARK: - Extensions
//Extension borrowed from here : https://stackoverflow.com/a/37760551
extension CGRect {
func randomPoint(x:CGFloat? = nil, y:CGFloat? = nil) -> CGPoint {
let origin = self.origin
return CGPoint(x: x == nil ? CGFloat(arc4random_uniform(UInt32(self.width))) + origin.x : x!,
y: y == nil ? CGFloat(arc4random_uniform(UInt32(self.height))) + origin.y : y!)
}
}
//Extension borrowed from here: https://stackoverflow.com/a/33292919
extension CGPoint {
func distance(point: CGPoint) -> CGFloat {
return abs(CGFloat(hypotf(Float(point.x - x), Float(point.y - y))))
}
}
And the GameScene:
//MARK: - Game Scene
class GameScene: SKScene {
//MARK: - Local Constants
// It's always good to have some kind of local constants per file, so that you have all variables in one place when it comes to changing/tuning
private struct K {
struct Actions {
static let civilianSpawningKey = "civilian.spawning"
static let playerMovingKey = "player.moving"
static let spawningDuration:TimeInterval = 0.7
static let spawningRange = 0.2
static let fadeOutDuration:TimeInterval = 0.35
}
struct General {
static let playerSpeed:CGFloat = 350
}
}
//MARK: - Private Properties
private var player:SKSpriteNode?
// Just in case, nodes are removed after physics simulation is done (in didSimulatePhysics which is called in each frame)
// Frame-Cycle Events : https://developer.apple.com/documentation/spritekit/skscene/responding_to_frame-cycle_events
private var trash:[SKNode] = []
private let civilianFactory = CivilianFactory()
private let playerFactory = PlayerFactory()
//MARK: - Scene lifecycle
override func sceneDidLoad() {
physicsWorld.contactDelegate = self
spawnCivilians()
}
//MARK: - Creating & Spawning sprites
private func getNode(factory:AbstractFactory)->SKNode{
return factory.getNode()
}
private func spawnCivilian(at position: CGPoint){
let node = getNode(factory: civilianFactory)
node.position = position
addChild(node)
}
private func spawnPlayer(at position: CGPoint){
// If its a first time, create player and leave it there
guard let `player` = player else {
let node = getNode(factory: playerFactory)
node.position = position
self.player = (node as? SKSpriteNode)
addChild(node)
return
}
// If player exists, move it around
let distance = player.position.distance(point: position)
let speed = K.General.playerSpeed
// To maintain same moving speed, cause if we use constant here, sprite would move faster or slower based on a given distance
let duration = distance / speed
let move = SKAction.move(to: position, duration:duration)
// This is a good way to check if some action is running
if player.action(forKey: K.Actions.playerMovingKey) != nil {
player.removeAction(forKey: K.Actions.playerMovingKey)
}
player.run(move, withKey: K.Actions.playerMovingKey)
}
private func spawnCivilians(){
let wait = SKAction .wait(forDuration: K.Actions.spawningDuration, withRange: K.Actions.spawningRange)
let spawn = SKAction.run({[weak self] in
guard let `self` = self else {return}
self.spawnCivilian(at: self.frame.randomPoint())
})
let spawning = SKAction.sequence([wait,spawn])
self.run(SKAction.repeatForever(spawning), withKey:K.Actions.civilianSpawningKey)
}
//MARK: - Touches Handling
func touchDown(atPoint pos : CGPoint) {
spawnPlayer(at: pos)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchDown(atPoint: t.location(in: self)) }
}
}
So I pretty much commented everything. Here, you :
Start spawning civilians infinitely, immediately after the scene is loaded
On touch you add player to the scene
On every next touch player travels to the touch location (by the same speed)
And contacts:
//MARK: - Physics
struct Collider{
static let player : UInt32 = 0x1 << 0
static let civilian : UInt32 = 0x1 << 1
static let wall : UInt32 = 0x1 << 2
static let none : UInt32 = 0x0
}
extension GameScene: SKPhysicsContactDelegate{
//MARK: - Removing Sprites
override func didSimulatePhysics() {
for node in trash {
// first remove node from parent (with fadeOut)
node.run(SKAction.sequence([SKAction.fadeOut(withDuration: K.Actions.fadeOutDuration), SKAction.removeFromParent()]))
}
trash.removeAll() // then empty the trash
}
//MARK: Removing
func didBegin(_ contact: SKPhysicsContact) {
guard let nodeA = contact.bodyA.node, let nodeB = contact.bodyB.node else {
//Silliness like removing a node from a node tree before physics simulation is done will trigger this error
fatalError("Physics body without its node detected!")
}
let mask = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
switch mask {
// Contact between player and civilian detected
case Collider.player | Collider.civilian:
if let civilian = (contact.bodyA.categoryBitMask == Collider.civilian ? nodeA : nodeB) as? SKSpriteNode
{
trash.append(civilian)
}
default:
break
}
}
}
I guess those contacts and node removal were your problem. The point is that nodes that have physics body, are safer to remove from a node tree when didSimulatePhysics method is finished. There is a link in comments that explains what happens each frame, but the bottom point is, that physics engine retains physics body cause simulation is not finished, but the node is removed and that often end up in some unexpected results.
So to try how this work, you just copy / paste it in your GameScene. Here is how it looks:
You can see how nodes are really removed by observing nodes count label. (to enable these labels, you just go (in your view controller class) with (self.view as? SKView)?.showsNodeCount = true, showsFPS, showsPhysics etc).
I am trying to make an iOS app using ARKit. So far I have used the "placing objects" example by Apple and augmented it so it has my own geometry. This part all works great.
I have several different objects that can be placed on the ground. To explain the objects in simple terms, they are boxes with doors on the front of them.
The problem I have is that I now want to add gestures to the app so when the door is tapped it rotates open. And then when it is tapped again the door closes.
I have looked for some tutorial on how to do this but couldn't find anything. Can someone explain to me how to do this or point me to a tutorial showing how to achieve this interactivity.
Thank you! :)
Below is a basic swift Playground which creates a door at loading. By tapping on the door you can rotate open, tapping again will close the door. I’ve broken the code up into different functions so you can see how the door opens & then closes.
import ARKit
import SceneKit
import PlaygroundSupport
class ViewController: NSObject {
var doorNode: SCNNode!
var doorisOpen: Bool!
var sceneView: ARSCNView
init(sceneView: ARSCNView) {
self.sceneView = sceneView
super.init()
self.setupWorldTracking()
self.sceneView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(ViewController.handleTap(_:))))
// place door
self.sceneView.scene.rootNode.addChildNode(createDoor(position: SCNVector3(0,0,-1)))
}
private func setupWorldTracking() {
if ARWorldTrackingConfiguration.isSupported {
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = .horizontal
configuration.isLightEstimationEnabled = true
self.sceneView.session.run(configuration, options: [])
}
}
#objc func handleTap(_ gesture: UITapGestureRecognizer) {
let results = self.sceneView.hitTest(gesture.location(in: gesture.view), types: ARHitTestResult.ResultType.featurePoint)
guard let result: ARHitTestResult = results.first else {
return
}
let tappedNode = self.sceneView.hitTest(gesture.location(in: gesture.view), options: [:])
if !tappedNode.isEmpty {
let node = tappedNode[0].node
if doorisOpen == true {
// rotate door
closeDoor()
} else {
// rotate door
openDoor()
}
} else {
return
}
}
func createDoor(position: SCNVector3) -> SCNNode {
let door = SCNBox(width: 0.3, height: 0.7, length: 0.025, chamferRadius: 0)
doorNode = SCNNode(geometry: door)
door.firstMaterial?.locksAmbientWithDiffuse = true
door.firstMaterial?.diffuse.contents = UIColor.brown
// place door
doorNode.position = position
// Pivot door from the end
endPivot(for: doorNode)
return doorNode
}
func openDoor() {
let rotate = SCNAction.rotateBy(x: 0, y: CGFloat(degToRadians(degrees: 90)), z: 0, duration: 1)
doorNode.runAction(rotate)
doorisOpen = true
}
func closeDoor() {
let rotate = SCNAction.rotateBy(x: 0, y: CGFloat(degToRadians(degrees: -90)), z: 0, duration: 1)
doorNode.runAction(rotate)
doorisOpen = false
}
func endPivot(for node: SCNNode) {
var min = SCNVector3Zero
var max = SCNVector3Zero
node.__getBoundingBoxMin(&min, max: &max)
node.pivot = SCNMatrix4MakeTranslation(min.x, 0, 0)
}
func degToRadians(degrees:Double) -> Double
{
return degrees * (M_PI / 180);
}
}
let sceneView = ARSCNView()
let viewController = ViewController(sceneView: sceneView)
sceneView.autoenablesDefaultLighting = true
PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.liveView = viewController.sceneView
I have a node object in 3d view and i need to drag that object,
So far i have tried from here : Placing, Dragging and Removing SCNNodes in ARKit
and converted in swift
#objc func handleDragGesture(_ gestureRecognizer: UIGestureRecognizer) {
let tapPoint = gestureRecognizer.location(in: self.sceneView)
switch gestureRecognizer.state {
case .began:
print("Object began to move")
let hitResults = self.sceneView.hitTest(tapPoint, options: nil)
if hitResults.isEmpty { return }
let hitResult = hitResults.first
if let node = hitResult?.node.parent?.parent?.parent {
self.photoNode = node
}
case .changed:
print("Moving object position changed")
if let _ = self.photoNode {
let hitResults = self.sceneView.hitTest(tapPoint, types: .featurePoint)
let hitResult = hitResults.last
if let transform = hitResult?.worldTransform {
let matrix = SCNMatrix4FromMat4(transform)
let vector = SCNVector3Make(matrix.m41, matrix.m42, matrix.m43)
self.photoNode?.position = vector
}
}
case .ended:
print("Done moving object")
default:
break
}
}
but it is not working properly. what is the correct way to do?
You can do this using panGestureRecongniser... see basic swift Playground code for handling a SCNNode.
import UIKit
import ARKit
import SceneKit
import PlaygroundSupport
public var textNode : SCNNode?
// Main ARKIT ViewController
class ViewController : UIViewController, ARSCNViewDelegate, ARSessionDelegate {
var textNode: SCNNode!
var counter = 0
#IBOutlet var sceneView: ARSCNView!
override func viewDidLoad() {
super.viewDidLoad()
// set the views delegate
sceneView.delegate = self as! ARSCNViewDelegate
// show statistics such as fps and timing information
sceneView.showsStatistics = true
// Create a new scene
sceneView.scene.rootNode
// Add ligthing
sceneView.autoenablesDefaultLighting = true
let text = SCNText(string: "Drag Me with Pan Gesture!", extrusionDepth: 1)
// create material
let material = SCNMaterial()
material.diffuse.contents = UIColor.green
text.materials = [material]
//Create Node object
textNode = SCNNode()
textNode.name = "textNode"
textNode.scale = SCNVector3(x:0.004,y:0.004,z:0.004)
textNode.geometry = text
textNode.position = SCNVector3(x: 0, y:0.02, z: -1)
// add new node to root node
self.sceneView.scene.rootNode.addChildNode(textNode)
// Add pan gesture for dragging the textNode about
sceneView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(panGesture(_:))))
}
override func loadView() {
sceneView = ARSCNView(frame:CGRect(x: 0.0, y: 0.0, width: 500.0, height: 600.0))
// Set the view's delegate
sceneView.delegate = self
let config = ARWorldTrackingConfiguration()
config.planeDetection = .horizontal
// Now we'll get messages when planes were detected...
sceneView.session.delegate = self
self.view = sceneView
sceneView.session.run(config)
}
#objc func panGesture(_ gesture: UIPanGestureRecognizer) {
gesture.minimumNumberOfTouches = 1
let results = self.sceneView.hitTest(gesture.location(in: gesture.view), types: ARHitTestResult.ResultType.featurePoint)
guard let result: ARHitTestResult = results.first else {
return
}
let position = SCNVector3Make(result.worldTransform.columns.3.x, result.worldTransform.columns.3.y, result.worldTransform.columns.3.z)
textNode.position = position
}
}
PlaygroundPage.current.liveView = ViewController()
PlaygroundPage.current.needsIndefiniteExecution = true
EDIT:
The above drag function only worked if you had 1 object in the view, so it was not really necessary to hit the node to start dragging. It will just drag from where ever you tapped on the screen. If you have multiple objects in the view, and you want to drag nodes independently. You could change the panGesture function to the following, detect each node tapped first:
// drags nodes independently
#objc func panGesture(_ gesture: UIPanGestureRecognizer) {
gesture.minimumNumberOfTouches = 1
let results = self.sceneView.hitTest(gesture.location(in: gesture.view), types: ARHitTestResult.ResultType.featurePoint)
guard let result: ARHitTestResult = results.first else {
return
}
let hits = self.sceneView.hitTest(gesture.location(in: gesture.view), options: nil)
if let tappedNode = hits.first?.node {
let position = SCNVector3Make(result.worldTransform.columns.3.x, result.worldTransform.columns.3.y, result.worldTransform.columns.3.z)
tappedNode.position = position
}
}
REF: https://stackoverflow.com/a/48220751/5589073
This code works for me
private func drag(sender: UIPanGestureRecognizer) {
switch sender.state {
case .began:
let location = sender.location(in: self.sceneView)
guard let hitNodeResult = self.sceneView.hitTest(location,
options: nil).first else { return }
self.PCoordx = hitNodeResult.worldCoordinates.x
self.PCoordy = hitNodeResult.worldCoordinates.y
self.PCoordz = hitNodeResult.worldCoordinates.z
case .changed:
// when you start to pan in screen with your finger
// hittest gives new coordinates of touched location in sceneView
// coord-pcoord gives distance to move or distance paned in sceneview
let hitNode = sceneView.hitTest(sender.location(in: sceneView), options: nil)
if let coordx = hitNode.first?.worldCoordinates.x,
let coordy = hitNode.first?.worldCoordinates.y,
let coordz = hitNode.first?.worldCoordinates.z {
let action = SCNAction.moveBy(x: CGFloat(coordx - self.PCoordx),
y: CGFloat(coordy - self.PCoordy),
z: CGFloat(coordz - self.PCoordz),
duration: 0.0)
self.photoNode.runAction(action)
self.PCoordx = coordx
self.PCoordy = coordy
self.PCoordz = coordz
}
sender.setTranslation(CGPoint.zero, in: self.sceneView)
case .ended:
self.PCoordx = 0.0
self.PCoordy = 0.0
self.PCoordz = 0.0
default:
break
}
}
I'm dealing with children nodes with pivot and position that have been altered. I found a lot of SCNNode transformation topics, but it seems none of them represent my situation.
I have six balls : (can't post more than 2 links, image is at i.stack.imgur.com/v3Lc4.png )
And I select the top four of them, adjust the pivot, adjust the position (to counter the pivot translation effect), and rotate. This is the code I use :
//core code
let fourBalls = SCNNode()
for i in 1...4
{
let ball = scene.rootNode.childNode(withName: "b" + String(i), recursively: false)!
ball.removeFromParentNode()
fourBalls.addChildNode(ball)
}
scene.rootNode.addChildNode(fourBalls)
//adjust the pivot of the fourBalls node
fourBalls.pivot = SCNMatrix4MakeTranslation(-1.5, 0.5, -5)
//fix the position
fourBalls.position = SCNVector3Make(-1.5, 0.5, -5)
//rotate
let action = SCNAction.rotateBy(x: 0, y: 0, z: CGFloat(M_PI_2), duration: 2)
fourBalls.run(action)
It did the job well :
Now, I need to release back the fourBalls child nodes into the rootNode, I use this code which I put as completion block :
//core problem
//how to release the node with the transform?
for node in fourBalls.childNodes
{ node.transform = node.worldTransform
node.removeFromParentNode()
self.scene.rootNode.addChildNode(node)
}
And here comes the problem, I released them wrongly :
So my question is, how to release the children nodes to the rootNode with correct pivot, position, and transform properties?
Here is my full GameViewController.swift for you who want to try :
import SceneKit
class GameViewController: UIViewController {
let scene = SCNScene()
override func viewDidLoad() {
super.viewDidLoad()
let ball1 = SCNSphere(radius: 0.4)
let ball2 = SCNSphere(radius: 0.4)
let ball3 = SCNSphere(radius: 0.4)
let ball4 = SCNSphere(radius: 0.4)
let ball5 = SCNSphere(radius: 0.4)
let ball6 = SCNSphere(radius: 0.4)
ball1.firstMaterial?.diffuse.contents = UIColor.purple()
ball2.firstMaterial?.diffuse.contents = UIColor.white()
ball3.firstMaterial?.diffuse.contents = UIColor.cyan()
ball4.firstMaterial?.diffuse.contents = UIColor.green()
ball5.firstMaterial?.diffuse.contents = UIColor.black()
ball6.firstMaterial?.diffuse.contents = UIColor.blue()
let B1 = SCNNode(geometry: ball1)
B1.position = SCNVector3(x:-2,y:1,z:-5)
scene.rootNode.addChildNode(B1)
B1.name = "b1"
let B2 = SCNNode(geometry: ball2)
B2.position = SCNVector3(x:-1,y:1,z:-5)
scene.rootNode.addChildNode(B2)
B2.name = "b2"
let B3 = SCNNode(geometry: ball3)
B3.position = SCNVector3(x:-2,y:0,z:-5)
scene.rootNode.addChildNode(B3)
B3.name = "b3"
let B4 = SCNNode(geometry: ball4)
B4.position = SCNVector3(x:-1,y:0,z:-5)
scene.rootNode.addChildNode(B4)
B4.name = "b4"
let B5 = SCNNode(geometry: ball5)
B5.position = SCNVector3(x:-2,y:-1,z:-5)
scene.rootNode.addChildNode(B5)
B5.name = "b5"
let B6 = SCNNode(geometry: ball6)
B6.position = SCNVector3(x:-1,y:-1,z:-5)
scene.rootNode.addChildNode(B6)
B6.name = "b6"
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3Make(-1.5,0,2)
scene.rootNode.addChildNode(cameraNode)
// create and add an ambient light to the scene
let ambientLightNode = SCNNode()
ambientLightNode.light = SCNLight()
ambientLightNode.light!.type = SCNLightTypeAmbient
ambientLightNode.light!.color = UIColor.yellow()
scene.rootNode.addChildNode(ambientLightNode)
let scnView = self.view as! SCNView
scnView.scene = scene
scnView.allowsCameraControl = false
scnView.backgroundColor = UIColor.orange()
//core code
let fourBalls = SCNNode()
for i in 1...4
{
let ball = scene.rootNode.childNode(withName: "b" + String(i), recursively: false)!
ball.removeFromParentNode()
fourBalls.addChildNode(ball)
}
scene.rootNode.addChildNode(fourBalls)
//adjust the pivot of the fourBalls node
fourBalls.pivot = SCNMatrix4MakeTranslation(-1.5, 0.5, -5)
//fix the position
fourBalls.position = SCNVector3Make(-1.5, 0.5, -5)
//rotate
let action = SCNAction.rotateBy(x: 0, y: 0, z: CGFloat(M_PI_2), duration: 2)
fourBalls.run(action, completionHandler:
{
//core problem
for node in fourBalls.childNodes
{
node.transform = node.worldTransform
node.removeFromParentNode()
self.scene.rootNode.addChildNode(node)
}
})
}
override func shouldAutorotate() -> Bool {
return true
}
override func prefersStatusBarHidden() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
if UIDevice.current().userInterfaceIdiom == .phone {
return .allButUpsideDown
} else {
return .all
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
}
SCNActions update the presentation tree directly, not the model tree, this is probably best explained in the 2014 WWDC video (skip to 16:38). What this means is that throughout the animation the current transform for each of the animating nodes is only available from the presentationNode. Your code works if we get the transform from here instead.
Apologies for the Swift 2 backport...
//rotate
let action = SCNAction.rotateByX(0, y: 0, z: CGFloat(M_PI_2), duration: 2)
fourBalls.runAction(action, completionHandler: {
//core problem
for node in fourBalls.childNodes
{
node.transform = node.presentationNode.worldTransform
node.removeFromParentNode()
self.scene.rootNode.addChildNode(node)
}
})
TBH I was expecting node.worldTransform == node.presentationNode.worldTransform when it got to the completion handler.
I’m new to programming and I recently did a tutorial that I found online to make a endless frogger game. The tutorial person didn’t show me how to do the score label and I have tried endlessly to find a video tutorial that will show me how to position a score label in the top left corner. I have tried using this code:
label.horizontalAlignmentMode = .Left
label.position = CGPoint(x:0.0, y:self.size.height)
but I have had no luck, It does not show up in the top left corner. So i tried another way to get it positioned in the corner and I played around with the values and ended up with this
scoreLabel.position = CGPointMake(frame.size.width / -2.231, frame.size.height / 2.29)
which positions it perfectly in the corner of the screen on the simulator, but not for all the devices. I have a seperate swift file for my score label with is here:
import Foundation
import SpriteKit
import UIKit
class Score: SKLabelNode {
var number = 0
init (num: Int) {
super.init()
fontColor = UIColor.whiteColor()
fontName = "Helvetica"
fontSize = 150.0
number = num
text = "\(num)"
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func addOneToScore() {
number++
text = "\(number)"
}
}
I have a world node that has a player inside it and the camera position is focused on the player. I have provided my game Scene class for you to have a look at. All I want is to be able to position a score label in the top left or top right corner of the screen for all devices.
import SpriteKit
enum BodyType:UInt32 {
case player = 1
case roadObject = 2
case waterObject = 4
case water = 8
case road = 16
}
enum LevelType:UInt32 {
case road, water
}
class GameScene: SKScene, SKPhysicsContactDelegate {
//recongises swipe and tap gestures
let TapUpRec = UITapGestureRecognizer()
let swipeRightRec = UISwipeGestureRecognizer()
let swipeLeftRec = UISwipeGestureRecognizer()
let swipeDownRec = UISwipeGestureRecognizer()
//defines the attributes for level units.
var levelUnitCounter:CGFloat = 1 // Not sure, i think it starts the frog further up.
var levelUnitWidth:CGFloat = 0 //will be screenwidth
var levelUnitHeight:CGFloat = 50 // changes the height of the level units
var initialUnits:Int = 10 // tells how many level units will be spawned as you climb.
//defines the world node for the scene & defines the player image in a constant.
var screenWidth:CGFloat = 0
var screenHeight:CGFloat = 0
let worldNode:SKNode = SKNode()
let thePlayer:Player = Player(imageNamed: "Frog")
var increment:CGFloat = 0
// variables with boolean values to check if the player is on certain types of levels or objects.
var onLilyPad:Bool = false
var onWater:Bool = false
var onRoad:Bool = false
//same variable that checks if the player is dead.
var isDead:Bool = false
//variable that links with the swift file called object and (maybe passes it through :( )
var waterObject:Object?
//creates a constant that will be the starting point of the player
let startingPosition:CGPoint = CGPointMake(0, 0)
var direction:CGFloat = 1
// var nodeToMove:Object?
// var moveInProgress:Bool = false
override func didMoveToView(view: SKView) {
/* Setup your scene here */
// Defines the view as the target defines the direction and calls the function (in red)
swipeRightRec.addTarget(self, action:"swipedRight")
swipeRightRec.direction = .Right
self.view!.addGestureRecognizer(swipeRightRec)
swipeLeftRec.addTarget(self, action: "swipedLeft")
swipeLeftRec.direction = .Left
self.view!.addGestureRecognizer(swipeLeftRec)
TapUpRec.addTarget(self, action: "tapUp")
self.view!.addGestureRecognizer(TapUpRec)
swipeDownRec.addTarget(self, action: "swipedDown")
swipeDownRec.direction = .Down
self.view!.addGestureRecognizer(swipeDownRec)
//makes the background colour black and defines the screenWidth variable as sk view boundry
self.backgroundColor = SKColor.greenColor()
screenWidth = self.view!.bounds.width
screenHeight = self.view!.bounds.height
//makes the world able to have objects that can collide (I Think)
physicsWorld.contactDelegate = self
//physicsWorld.gravity = CGVector(dx:0.3, dy:0.0)
//creates the world node point to be in the middle of the screen
self.anchorPoint = CGPointMake(0.5, 0.5)
addChild(worldNode)
let scoreLabel = Score(num: 0)
scoreLabel.horizontalAlignmentMode = .Left
scoreLabel.position = CGPoint(x:0.0, y:self.size.height)
addChild(scoreLabel)
//scoreLabel.position = CGPointMake(frame.size.width / -2.231, frame.size.height / 2.29)
// let highscoreLabel = Score(num: 0)
//adds a child node to the world node (so the player is playing within the world node)
worldNode.addChild(thePlayer)
thePlayer.position = startingPosition
thePlayer.zPosition = 500 // zPosition is the order the player will be displayed.
addLevelUnits() //runs the func that addds levelUnits.
}
func addScoreLabels(){
}
// this function along with the next three run the swipe and tap gesture actions.
func swipedRight(){
let amountToMove:CGFloat = levelUnitHeight
let move:SKAction = SKAction.moveByX(amountToMove, y: 0, duration: 0.1)
thePlayer.runAction(move) // links the action with the players
}
func swipedLeft(){
let amountToMove:CGFloat = levelUnitHeight
let move:SKAction = SKAction.moveByX(-amountToMove, y: 0, duration: 0.1)
thePlayer.runAction(move)
}
func swipedDown(){
let amountToMove:CGFloat = levelUnitHeight
let move:SKAction = SKAction.moveByX(0, y: -amountToMove, duration: 0.1)
thePlayer.runAction(move)
// clearNodes()
}
func tapUp(){
let amountToMove:CGFloat = levelUnitHeight
let move:SKAction = SKAction.moveByX(0, y: amountToMove, duration: 0.1)
thePlayer.runAction(move)
clearNodes()
}
func resetLevel(){
//searches the world node for child nodes that have the name "levelUnit"
worldNode.enumerateChildNodesWithName("levelUnit") {
node, stop in
//removes all the child nodes from the parent.
node.removeFromParent()
}
levelUnitCounter = 1
addLevelUnits()
}
func addLevelUnits() {
for (var i = 0; i < initialUnits; i++ ) {
createLevelUnit()
}
}
func createLevelUnit() {
if (direction == 1) {
direction = -1
} else {
direction = 1
}
print(direction )
let levelUnit:LevelUnit = LevelUnit()
worldNode.addChild(levelUnit)
levelUnit.zPosition = -1
levelUnit.levelUnitWidth = screenWidth
levelUnit.levelUnitHeight = levelUnitHeight
levelUnit.direction = direction
levelUnit.setUpLevel()
levelUnit.position = CGPointMake( 0 , levelUnitCounter * levelUnitHeight) // counts the level unit and multiplies it so t goes above the last level unit.
levelUnitCounter++ //constantly makes the level units appear.
}
func clearNodes(){
var nodeCount:Int = 0
worldNode.enumerateChildNodesWithName("levelUnit") {
node, stop in
let nodeLocation:CGPoint = self.convertPoint(node.position, fromNode: self.worldNode) //converts cordinates of level units with the world node.
if ( nodeLocation.x < -(self.screenWidth / 2) - self.levelUnitWidth ) { // checks to see if the node is off the screen.
node.removeFromParent()
print("levelUnit was removed", terminator: "")
} else {
nodeCount++
}
}
print( "levelUnits in the scene is \(nodeCount)")
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
worldNode.enumerateChildNodesWithName("levelUnit"){
node, stop in
let levelUnit:LevelUnit = node as! LevelUnit //cast as an actual LevelUnit class
levelUnit.enumerateChildNodesWithName("obstacle"){
node, stop in
let obstacle:Object = node as! Object //cast as an actual Object class
obstacle.update()
let obstacleLocation:CGPoint = self.convertPoint(obstacle.position, fromNode: levelUnit)
let buffer:CGFloat = 150
if (obstacleLocation.x < -(self.screenWidth / 2) - buffer) { //changes the speed of object when it reaches middle of the screen.
levelUnit.changeSpeed()
obstacle.position = CGPointMake(obstacle.position.x + (self.screenWidth + (buffer * 2)) , obstacle.position.y)
} else if (obstacleLocation.x > (self.screenWidth / 2) + buffer ) { //changes the speed of the object again.
levelUnit.changeSpeed()
obstacle.position = CGPointMake(obstacle.position.x - (self.screenWidth + (buffer * 2)) , obstacle.position.y)
}
}
}
// creates new level units if always centering horizontally
let nextTier:CGFloat = (levelUnitCounter * levelUnitHeight) - (CGFloat(initialUnits) * levelUnitHeight)
if (thePlayer.position.y > nextTier) {
createLevelUnit()
}
//deal with the players location....
let playerLocation:CGPoint = self.convertPoint(thePlayer.position, fromNode: worldNode)
var repositionPlayer:Bool = false
if ( playerLocation.x < -(screenWidth / 2)) {
repositionPlayer = true
} else if ( playerLocation.x > (screenWidth / 2)) {
repositionPlayer = true
} else if ( playerLocation.y < 0) {
repositionPlayer = true
} else if ( playerLocation.y > screenHeight) {
repositionPlayer = true
}
if (repositionPlayer == true) {
/* great code for reference later */
killPlayer()
thePlayer.physicsBody?.velocity = CGVector(dx: 0, dy: 0)
}
}
// this function centers the world node on teh player.
override func didSimulatePhysics() {
self.centerOnNode(thePlayer)
if (onLilyPad == true) {
thePlayer.position = CGPointMake(thePlayer.position.x + waterObject!.xAmount , thePlayer.position.y)
}
}
//centers the camera on the node world.
func centerOnNode(node:SKNode) {
let cameraPositionInScene:CGPoint = self.convertPoint(node.position, fromNode: worldNode)
worldNode.position = CGPoint(x: worldNode.position.x , y:worldNode.position.y - cameraPositionInScene.y )
}
func didBeginContact(contact: SKPhysicsContact) {
// Defines the contact between objects.
/// lily pad
if (contact.bodyA.categoryBitMask == BodyType.player.rawValue && contact.bodyB.categoryBitMask == BodyType.waterObject.rawValue ) {
waterObject = contact.bodyB.node!.parent as? Object
onLilyPad = true
self.removeActionForKey("checkOnLilyPad")
let waterObjectLocation:CGPoint = self.convertPointToView(waterObject!.position)
thePlayer.position = self.convertPointFromView(waterObjectLocation)
} else if (contact.bodyA.categoryBitMask == BodyType.waterObject.rawValue && contact.bodyB.categoryBitMask == BodyType.player.rawValue ) {
waterObject = contact.bodyA.node!.parent as? Object
onLilyPad = true
self.removeActionForKey("checkOnLilyPad")
let waterObjectLocation:CGPoint = self.convertPointToView(waterObject!.position)
thePlayer.position = self.convertPointFromView(waterObjectLocation)
}
//// check on water
if (contact.bodyA.categoryBitMask == BodyType.player.rawValue && contact.bodyB.categoryBitMask == BodyType.water.rawValue ) {
onRoad = false
onWater = true
waitAndThenCheckOnLilyPad()
} else if (contact.bodyA.categoryBitMask == BodyType.water.rawValue && contact.bodyB.categoryBitMask == BodyType.player.rawValue ) {
onRoad = false
onWater = true
waitAndThenCheckOnLilyPad()
}
//// cars
if (contact.bodyA.categoryBitMask == BodyType.player.rawValue && contact.bodyB.categoryBitMask == BodyType.roadObject.rawValue ) {
killPlayer()
} else if (contact.bodyA.categoryBitMask == BodyType.roadObject.rawValue && contact.bodyB.categoryBitMask == BodyType.player.rawValue ) {
killPlayer()
}
//// check on road
if (contact.bodyA.categoryBitMask == BodyType.player.rawValue && contact.bodyB.categoryBitMask == BodyType.road.rawValue ) {
onRoad = true
onWater = false
onLilyPad = false
} else if (contact.bodyA.categoryBitMask == BodyType.road.rawValue && contact.bodyB.categoryBitMask == BodyType.player.rawValue ) {
onRoad = true
onWater = false
onLilyPad = false
}
}
func waitAndThenCheckOnLilyPad() {
let wait:SKAction = SKAction.waitForDuration(0.1)
let check:SKAction = SKAction.runBlock(checkIfOnLilyPad)
let seq:SKAction = SKAction.sequence([wait, check])
self.runAction(seq, withKey:"checkOnLilyPad")
}
func checkIfOnLilyPad() {
if ( onRoad == false) {
if ( onWater == true && onLilyPad == false) {
killPlayer()
} else if ( onWater == true && onLilyPad == true) {
print("safely on water")
// maybe play sound here
}
}
}
func didEndContact(contact: SKPhysicsContact) {
let contactMask = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
switch (contactMask) {
case BodyType.waterObject.rawValue | BodyType.player.rawValue:
onLilyPad = false
waterObject = nil
onWater = true
waitAndThenCheckOnLilyPad()
default:
return
}
}
func killPlayer() {
if ( isDead == false) {
isDead = true
let fadeOut:SKAction = SKAction.fadeAlphaTo(0, duration: 0.2)
let move:SKAction = SKAction.moveTo(startingPosition, duration: 0.2)
let block:SKAction = SKAction.runBlock(revivePlayer)
let seq:SKAction = SKAction.sequence([fadeOut, move, block])
thePlayer.runAction(seq)
}
}
func revivePlayer() {
isDead = false
onRoad = false
onWater = false
onLilyPad = false
let fadeOut:SKAction = SKAction.fadeAlphaTo(0, duration: 0.2)
let block:SKAction = SKAction.runBlock(resetLevel)
let fadeIn:SKAction = SKAction.fadeAlphaTo(1, duration: 0.2)
let seq:SKAction = SKAction.sequence([fadeOut, block, fadeIn])
worldNode.runAction(seq)
let wait:SKAction = SKAction.waitForDuration(1)
let fadeIn2:SKAction = SKAction.fadeAlphaTo(1, duration: 0.2)
let seq2:SKAction = SKAction.sequence([wait , fadeIn2])
thePlayer.runAction(seq2)
}
}
If other solutions don't work, you could try something like this:
import UIKit
var screenSize = UIScreen.mainscreen().bounds
var screenWidth = screenSize.width
var screenHeight = screenSize.height
This will get the dimensions of the screen and store them in the screenHeight and screenWidth variables.
Then when you call scoreLabel.position you could say
scoreLabel.position = CGPoint(x: screenWidth / 10, y: screenHeight / 15)
Or something like that but experiment with the math until it is in the correct position.
If this doesn't work you may also need to declare the size or scoreLabel. Let me know if it works.
Swift 4 and Xcode 9 code:
let screenSize = UIScreen.main.bounds
let screenWidth = screenSize.width
let screenHeight = screenSize.height
self.menuButton.position = CGPoint(x: screenWidth / 10, y: screenHeight / 15)
I think you are on the right track with:
let scoreLabel = Score(num: 0)
scoreLabel.horizontalAlignmentMode = .Left
scoreLabel.position = CGPoint(x:0.0, y:self.size.height)
addChild(scoreLabel)
But I think you also need to add
scoreLabel.verticalAlignmentMode = .Top
as the default is.Baseline which would cause it draw most off the screen.