SO I created a game where you tap balls to make them jump up. You get a point for every tap. Why do my sprites start flashing when I reach 10 points. I'm thinking it has something to do with the update(timeInterval) method. It may be a bug or bad coding.
import SpriteKit
class GameScene: SKScene {
let backgroundNode = SKSpriteNode(imageNamed: "redbackground")
override func didMoveToView(view: SKView) {
backgroundNode.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
self.addChild(backgroundNode)
//=======Ball 1=======//
let ball = Ball()
ball.position = CGPoint(x:self.frame.midX, y:440)
addChild(ball)
ball.physicsBody = SKPhysicsBody(circleOfRadius: 90)
ball.physicsBody?.dynamic = true
ball.physicsBody?.allowsRotation = false
ball.physicsBody?.friction = 0
ball.physicsBody?.angularDamping = 0
ball.physicsBody?.linearDamping = 0
ball.physicsBody?.usesPreciseCollisionDetection = true
ball.physicsBody!.categoryBitMask = 0
}
class Ball: SKSpriteNode {
init() {
let texture = SKTexture(imageNamed: "Ball")
super.init(texture: texture, color: .clearColor(), size: texture.size())
userInteractionEnabled = true
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let scene = self.scene as! GameScene
scene.score += 1
physicsBody?.velocity = CGVectorMake(0, 100)
physicsBody?.applyImpulse(CGVectorMake(0, 900))
}
override func update(currentTime: CFTimeInterval) {
if (score >= 10){
self.backgroundNode.texture = SKTexture(imageNamed: "orangebackground")
}
if (score >= 20){
self.backgroundNode.texture = SKTexture(imageNamed: "yellowbackground")
}
if (score >= 30) {
self.backgroundNode.texture = SKTexture(imageNamed: "greenbackground")
}
}
See this to see what I mean.(Click or tap link)
This shows the sprites flashing at 10 points
thanks in advance, Jordan
Instead of changing the background image in update, you can add a property observer to score that changes the background only when the score reaches specific values. For example:
let backgroundNames = ["orangebackground","yellowbackground","greenbackground"]
// The didSet observer is called when the score is updated
var score:Int = 0 {
didSet {
// Check if score is divisible by 10
if score % 10 == 0 {
// Determine the array index
let index = score / 10
// Wrap to avoid index out of range
let name = backgroundNames[index % backgroundNames.count]
print (name)
}
}
}
That said, the flashing could be caused by the order in which the nodes are rendered. From Apple's documentation,
...with [ignoresSiblingOrder = true], you cannot predict the
rendering order for nodes that share the same height. The rendering
order may change each time a new frame is rendered.
You can resolve this by setting the zPosition of the background to a negative value, for example
self.backgroundNode.zPosition = -100
so the background is always drawn before the game nodes.
It is flashing because of this
override func update(currentTime: CFTimeInterval) {
if (score >= 10){
self.backgroundNode.texture = SKTexture(imageNamed: "orangebackground")
}
if (score >= 20){
self.backgroundNode.texture = SKTexture(imageNamed: "yellowbackground")
}
if (score >= 30) {
self.backgroundNode.texture = SKTexture(imageNamed: "greenbackground")
}
}
Infact inside the update method, when the score become >= 10, you are updating the texture EVERY FRAME.
This is absolutely wrong.
If you want to update the texture just do it when you need to change it (e.g. when the score goes from 9 to 10, or from 19 to 20 or from 29 to 30.).
But there's no point in updating the texture when it stays at 10 of when it goes from 10 to 11 since you will update it with another texture which is the same.
How can you do it?
Just create a shouldUpdateTexture: Bool = false property, then each time you update the score, if the score goes from 9 to 10 or 19 to 20 or 29 to 30 then turn shouldUpdateTexture to true.
Finally inside the update method check if shouldUpdateTexture is true, in this case set it back to false and update the texture.
Full code
class GameScene: SKScene {
private var score = 0 {
didSet {
shouldUpdateTexture = oldValue < 10 && score >= 10 || oldValue < 20 && score >= 20 || oldValue < 30 && score >= 30
}
}
private var shouldUpdateTexture = false
private var backgroundNode = SKSpriteNode(imageNamed: "defaultbackground")
override func update(currentTime: CFTimeInterval) {
if shouldUpdateTexture {
shouldUpdateTexture = false
switch score {
case 10...19: backgroundNode.texture = SKTexture(imageNamed: "orangebackground")
case 20...29: backgroundNode.texture = SKTexture(imageNamed: "yellowbackground")
case 30...Int.max: backgroundNode.texture = SKTexture(imageNamed: "yellowbackground")
default: break
}
}
}
}
That's it.
Update
As #Knight0fDragon pointed out, it would probably be faster moving the code I put into the update method into the didSet.
Pros: this way you don't need to perform a boolean equality check every frame
Cons: if you change the score value multiple times within the same frame you will perform multiple loads of the texture.
Just an alternative that allows for multiple changes in score to happen, without flooding the update function with unnecessary code
class GameScene: SKScene {
private var score = 0 {
didSet {
///if a change in the 10s happen, then update texture
if(score/10 > oldValue/10)
{
switch score{
case 10...19: backgroundNode.texture = SKTexture(imageNamed: "orangebackground")
case 20...29: backgroundNode.texture = SKTexture(imageNamed: "yellowbackground")
case 30...Int.max: backgroundNode.texture = SKTexture(imageNamed: "greenbackground")
default: break
}
}
}
}
private var shouldUpdateTexture = false
private var backgroundNode = SKSpriteNode(imageNamed: "defaultbackground")
}
Then for optimization: (Not really needed, but may help with how you think about code)
class GameScene: SKScene {
let bgTextures = [SKTexture(imageNamed: "orangebackground"),SKTexture(imageNamed: "yellowbackground"),SKTexture(imageNamed: "greenbackground")]
private var score = 0 {
didSet {
///if a change in the 10s happen, then update texture
let bgIndex = score / 10
if((bgIndex > oldValue/10) && score < bgTextures.count * 10)
{
backgroundNode.texture = bgTextures[bgIndex - 1]
}
else
{
backgroundNode.texture = bgTextures[bgTextures.count - 1]
}
}
}
private var shouldUpdateTexture = false
private var backgroundNode = SKSpriteNode(imageNamed: "defaultbackground")
}
Related
apple documentation gives a physics bodies velocity as in meters per second.
If this is true is there a relationship between pixels and meters? and if there is, what is it?
or is this an error in the documentation and its really pixels per second?
I wrote a small program to try to work this out and got an answer of 135 points to the metre.
https://stackoverflow.com/a/37523818/1430420
Edit: Here is the program for anyone to check to see if my maths is off
//
// GameScene.swift
// gravityTest
//
// Created by Steve Ives on 30/05/2016.
// Copyright (c) 2016 Steve Ives. All rights reserved.
//
import SpriteKit
class GameScene: SKScene {
// var elapsedTime: CFTimeInterval = 0.0
var timeOfLastUpdate: CFTimeInterval = 0.0
var updateCount = 0
let sprite = SKSpriteNode(imageNamed:"Spaceship")
override func didMove(to view: SKView) {
/* Setup your scene here */
// physicsWorld.gravity = CGVectorMake(-9.8, 0)
let location = CGPoint(x: (scene?.frame.midX)!, y: (scene?.frame.midY)!)
sprite.xScale = 0.5
sprite.yScale = 0.5
sprite.physicsBody = SKPhysicsBody(rectangleOf: sprite.size)
sprite.physicsBody?.isDynamic = true
sprite.position = location
self.addChild(sprite)
}
override func update(_ currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
updateCount += 1
if (currentTime - timeOfLastUpdate) < 1 {return}
// if (timeOfLastUpdate - currentTime > 1.0) {
print("\(updateCount) - Time : \(currentTime) \(currentTime-timeOfLastUpdate) Location is : \(sprite.position) Velocity is : \(sprite.physicsBody!.velocity)")
// }
timeOfLastUpdate = currentTime
}
}
I used playground to calculate my distance. I am getting around 150
We know distance is d = (gt^2)/2 where g is gravity and t is time.
d = (9.8m/t^2)(t^2)/2
d = (9.8m)/2
d = 4.9m
In 1 second, we would travel 4.9m
In our case, we travelled 765.9298706054688 points.
765.9298706054688/4.9 gets us around 156.312218491.
Ideally, we would have travelled 735 points, not sure what in my code could cause a 30 point difference, I suspect it has to do with SKAction
//: A SpriteKit based Playground
import PlaygroundSupport
import SpriteKit
class GameScene: SKScene {
private var testNode = SKSpriteNode(color: .red, size: CGSize(width: 20, height: 20))
private var flag = false
private var time : CGFloat = 0
private var beginTest = false
override func didMove(to view: SKView) {
}
#objc static override var supportsSecureCoding: Bool {
// SKNode conforms to NSSecureCoding, so any subclass going
// through the decoding process must support secure coding
get {
return true
}
}
func touchDown(atPoint pos : CGPoint) {
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
testNode = SKSpriteNode(color: .red, size: CGSize(width: 20, height: 20))
let physicsBody = SKPhysicsBody(rectangleOf: testNode.size)
physicsBody.angularDamping = 0
physicsBody.linearDamping = 0
physicsBody.friction = 0
testNode.physicsBody = physicsBody
let action = SKAction.customAction(withDuration: 2)
{
node,elapsedTime in
if elapsedTime >= 1 && !self.flag && self.time == 0{
self.flag = true
self.time = elapsedTime
}
}
testNode.run(action)
beginTest = true
}
override func update(_ currentTime: TimeInterval) {
if beginTest{
beginTest = false
addChild(testNode)
}
}
override func didFinishUpdate() {
}
override func didApplyConstraints() {
if flag{
let position = CGPoint(x:testNode.position.x,y:-testNode.position.y)
print("Node traveled \(position.y) points over \(time) seconds with a velocity of \(testNode.physicsBody!.velocity)")
let pointsPerMeter = -testNode.physicsBody!.velocity.dy/9.8
print("Node traveled \(pointsPerMeter) points per meter over \(time) seconds with a velocity of \(testNode.physicsBody!.velocity)")
flag = false
testNode.removeFromParent()
time = 0
}
}
}
// Load the SKScene from 'GameScene.sks'
let sceneView = SKView(frame: CGRect(x:0 , y:0, width: 640, height: 480))
if let scene = GameScene(fileNamed: "GameScene") {
// Set the scale mode to scale to fit the window
scene.scaleMode = .aspectFill
// Present the scene
sceneView.presentScene(scene)
}
PlaygroundSupport.PlaygroundPage.current.liveView = sceneView
Node traveled 765.9298706054688 points over 1.0 seconds with a velocity of (0.0, -1494.4998779296875)
Node traveled 152.49998754384566 points per meter over 1.0 seconds with a velocity of (0.0, -1494.4998779296875)
An answer to anyone and myself
There is nothing to really define a pixel within apples SKSpriteKit, this is a misnomer in my question. A point on the other hand is well defined.
Everybody probably already knows this but if you attach a SKPhysicsBody to a Node and assign the following properties:
seg.physicsBody?.isDynamic = true
seg.physicsBody?.affectedByGravity = false
seg.physicsBody?.allowsRotation = false
seg.physicsBody?.usesPreciseCollisionDetection = true
seg.physicsBody?.mass = 0
seg.physicsBody?.restitution = 1
seg.physicsBody?.linearDamping = 0
Then applying a velocity dy component of 200 results in the object moving at 200 points per second. This says to me that with all external forces remove Sprite kits physics world translates to 1m/s = 1point/s.
I am in developing an application using Xcode and currently I am facing a issue regarding SKSpriteNodes. When there are more than a single SKSpriteNode and when the node is touched, the touched node is not removed but the other not touched is removed. I have also noticed that when there are multiple nodes on the screen only the latest node coming from the top of the screen is removed whilst others are still moving down, although they are being touched. Can someone help identify why this has occurred and the methods of preventing such mistakes please?
For reference, I am have included the class in which the bug is in.
import SpriteKit
import GameplayKit
class GameScene: SKScene {
private var gameCounter: Int = 0
private var currentLevel: Int = 0
private var debug: SKLabelNode?
private var increasedTouchArea:SKSpriteNode?
private var generatedNode:SKSpriteNode?
private var moveAndRemove:SKAction?
private var moveNode:SKAction?
// Here we set initial values of counter and level. Debug label is created here as well.
override func didMove(to view: SKView) {
print(action(forKey: "counting") == nil)
gameCounter = 0
currentLevel = 1
//backgroundColor = SKColor.gray
debug = SKLabelNode(fontNamed: "ArialMT")
debug?.fontColor = SKColor.red
debug?.fontSize = 30.0
debug?.position = CGPoint(x: frame.midX, y: frame.midY)
debug?.text = "Counter : [ \(gameCounter) ], Level [ \(currentLevel) ]"
if let aDebug = debug {
addChild(aDebug)
}
}
//Method to start a timer. SKAction is used here to track a time passed and to maintain current level
func startTimer() {
print("Timer Started...")
weak var weakSelf: GameScene? = self
//make a weak reference to scene to avoid retain cycle
let block = SKAction.run({
weakSelf?.gameCounter = (weakSelf?.gameCounter ?? 0) + 1
//Maintaining level
if (weakSelf?.gameCounter ?? 0) < 5 {
//level 1
weakSelf?.currentLevel = 1
} else if (weakSelf?.gameCounter ?? 0) >= 5 && (weakSelf?.gameCounter ?? 0) < 10 {
//level 2
weakSelf?.currentLevel = 2
} else {
//level 3
weakSelf?.currentLevel = 3
}
weakSelf?.debug?.text = "Counter : [ \(Int(weakSelf?.gameCounter ?? 0)) ], Level [ \(Int(weakSelf?.currentLevel ?? 0)) ]"
})
run(SKAction.repeatForever(SKAction.sequence([SKAction.wait(forDuration: 1), block])), withKey: "counting")
}
//Method for stopping timer and reseting everything to default state.
func stopTimer() {
print("Timer Stopped...")
if action(forKey: "counting") != nil {
removeAction(forKey: "counting")
}
gameCounter = Int(0.0)
currentLevel = 1
debug?.text = "Counter : [ \(gameCounter) ], Level [ \(currentLevel) ]"
}
//Get current speed based on time passed (based on counter variable)
func getCurrentSpeed() -> CGFloat {
if gameCounter < 30 {
//level 1
return 1.0
} else if gameCounter >= 31 && gameCounter < 49 {
//level 2
return 2.0
} else {
//level 3
return 3.0
}
}
//Method which stop generating stones, called in touchesBegan
func stopGeneratingStones() {
print("STOPPED GENERATING STONES...")
if action(forKey: "spawning") != nil {
removeAction(forKey: "spawning")
}
}
func randomFloatBetween(_ smallNumber: CGFloat, and bigNumber: CGFloat) -> CGFloat {
let diff: CGFloat = bigNumber - smallNumber
return CGFloat(arc4random() % (UInt32(RAND_MAX) + 1)) / CGFloat(RAND_MAX) * diff + smallNumber
}
//Method for generating stones, you run this method when you want to start spawning nodes (eg. didMoveToView or when some button is clicked)
func generateStones() {
print("Generating Stones...")
let delay = SKAction.wait(forDuration: 1, withRange: 0.5) //Change forDuration: delay decreases as game progresses.
//randomizing delay time
weak var weakSelf: GameScene? = self
//make a weak reference to scene to avoid retain cycle
let block = SKAction.run({
let stone: SKSpriteNode? = weakSelf?.spawnNodes(withSpeed: weakSelf?.getCurrentSpeed() ?? 0.0)
stone?.zPosition = 20
if let aStone = stone {
weakSelf?.addChild(aStone)
}
})
run(SKAction.repeatForever(SKAction.sequence([delay, block])), withKey: "spawning")
}
func spawnNodes(withSpeed stoneSpeed: CGFloat) -> SKSpriteNode? {
let nodeSize = CGSize(width: 60, height: 60) //size of shape.
let initalNodePosition = CGPoint(x: randomFloatBetween(0.0, and: (self.view?.bounds.size.width)!) - 110, y: frame.maxY)
generatedNode = SKSpriteNode(color: SKColor.green, size: nodeSize)
generatedNode?.position = initalNodePosition
moveNode = SKAction.moveBy(x: 0, y: self.view!.scene!.frame.minY + self.view!.scene!.frame.minY , duration: 5.25)
generatedNode?.isUserInteractionEnabled = false //Allows users to touch shape.
increasedTouchArea = SKSpriteNode(color: UIColor.clear, size: CGSize(width: nodeSize.width * 1.35, height: nodeSize.height * 1.35))
increasedTouchArea?.name = "generatedNode"
increasedTouchArea?.isUserInteractionEnabled = false
generatedNode?.addChild(increasedTouchArea!)
moveNode?.speed = stoneSpeed
moveAndRemove = SKAction.sequence([moveNode!, SKAction.removeFromParent()])
generatedNode?.run(moveAndRemove!, withKey: "moving")
generatedNode?.name = "generatedNode"
return generatedNode
}
func deleteNode(){
moveAndRemove = SKAction.sequence([SKAction.removeFromParent()])
generatedNode?.run(moveAndRemove!, withKey: "moving")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent? {
for touch in touches {
let locationTocuhed = touch.location(in: self)
let touchedNode : SKNode = self.atPoint(locationTocuhed)
if touchedNode.name == generatedNode?.name {
print("Node touched")
deleteNode()
}
}
if action(forKey: "counting") == nil {
print("Game started...")
startTimer()
generateStones()
} else {
print("Game paused...")
}
}
}
Your method deleteNode() runs the deletion animation on the node pointed to by generatedNode, not the node last touched:
func deleteNode(){
moveAndRemove = SKAction.sequence([SKAction.removeFromParent()])
generatedNode?.run(moveAndRemove!, withKey: "moving")
}
If you want to delete the node last touched, pass a reference to this node to the method deleteNode and then run your deletion animation in that, not generatedNode.
So far, I almost finished my game. I have a ball that falls due to gravity and the person taps it to make it bounce. Every time the sprite is tapped, +1 is added to the score. There are 3 balls in total that show up when the view loads.I want each ball to show up at a certain point mark(one ball shows up when the person reaches 10 points, another when the player reaches 20).
I tried putting this in update(currentTime: CFTimeInterval), but I end up with this:
(This is the secondball coming in at 10 points.)
Seems like there are an infinite number of balls until the person gets to 21 points which stops the never-ending cascade. If you tap the cascade though, it does pick a ball and makes it jump out which is sorta what I wanted.
This is GameScene.swift (excluding the update(CFTimeInterval) function)
import SpriteKit
class GameScene: SKScene {
var ball: Ball!
var secondball: Ball!
override func didMoveToView(view: SKView) {
//=======Ball 1=======//
let ball = Ball()
ball.position = CGPoint(x:self.frame.midX, y:440)
addChild(ball)
ball.physicsBody = SKPhysicsBody(circleOfRadius: 90)
ball.physicsBody?.dynamic = true
ball.physicsBody?.allowsRotation = false
ball.physicsBody?.friction = 0
ball.physicsBody?.angularDamping = 0
ball.physicsBody?.linearDamping = 0
ball.physicsBody?.usesPreciseCollisionDetection = true
ball.physicsBody!.categoryBitMask = 0
//======Ball 2======//
let secondball = Ball()
secondball.position = CGPoint(x:self.frame.midX * 1.65, y: 440)
addChild(secondball)
secondball.physicsBody = SKPhysicsBody(circleOfRadius: 90)
secondball.physicsBody?.dynamic = false
secondball.physicsBody?.allowsRotation = false
secondball.physicsBody?.friction = 0
secondball.physicsBody?.angularDamping = 0
secondball.physicsBody?.linearDamping = 0
secondball.physicsBody?.usesPreciseCollisionDetection = true
secondball.physicsBody!.categoryBitMask = 0
}
class Ball: SKSpriteNode {
init() {
let texture = SKTexture(imageNamed: "Ball")
super.init(texture: texture, color: .clearColor(), size: texture.size())
userInteractionEnabled = true
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let scene = self.scene as! GameScene
scene.score += 1
physicsBody?.velocity = CGVectorMake(0, 100)
physicsBody?.applyImpulse(CGVectorMake(0, 900))
}
}
override func update(currentTime: CFTimeInterval) {
if (score >= 10){
self.backgroundNode.texture = SKTexture(imageNamed: "orangebackground")
}
if (score >= 20){
self.backgroundNode.texture = SKTexture(imageNamed: "yellowbackground")
}
if (score >= 30) {
self.backgroundNode.texture = SKTexture(imageNamed: "greenbackground")
}
if (score >= 40){
self.backgroundNode.texture = SKTexture(imageNamed: "bluebackground")
}
if (score >= 50){
self.backgroundNode.texture = SKTexture(imageNamed: "darkbluebackground")
}
if (score >= 60){
self.backgroundNode.texture = SKTexture(imageNamed: "purplebackground")
}
if (score >= 70){
self.backgroundNode.texture = SKTexture(imageNamed: "brownbackground")
}
if (score >= 80) {
self.backgroundNode.texture = SKTexture(imageNamed: "maroonbackground")
}
if (score >= 90){
self.backgroundNode.texture = SKTexture(imageNamed: "tanbackground")
}
if (score >= 100){
self.backgroundNode.texture = SKTexture(imageNamed: "pinkbackground")
}
if (score >= 125) {
self.backgroundNode.texture = SKTexture(imageNamed: "bronzebackground")
}
if (score >= 150) {
self.backgroundNode.texture = SKTexture(imageNamed: "silverbackground")
}
if (score >= 175) {
self.backgroundNode.texture = SKTexture(imageNamed: "goldbackground")
}
if (score >= 200) {
self.backgroundNode.texture = SKTexture(imageNamed: "elitebackground")
}
}
}
The update method is called faster then the increased score. Therefore in every call of the update method a new ball is added.
Try something like this:
Move this line of code outside of the update method to make a global variable which has one ball instance:
let secondball = Ball()
Inside of update:
if (score == 10) && secondball.parent == nil {
You have choosed to control the touches part to the Ball class, this is possible but not convenient.
First of all give a name to your balls during the creation:
ball.name = "ball1"
self.addChild(ball)
...
secondball.name = "ball2"
secondball.alpha = 0.0 // hide your secondBall at start
self.addChild(secondball)
Then , move the touchesBegan from your Ball class, to your GameScene:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first
let positionInScene = touch!.locationInNode(self)
let touchedNode = self.nodeAtPoint(positionInScene)
if let name = touchedNode.name {
switch name {
case "ball1": // if you're touching ball1
self.score += 1
ball.physicsBody?.velocity = CGVectorMake(0, 100)
ball.physicsBody?.applyImpulse(CGVectorMake(0, 900))
if score == 10 {
ball.removeFromParent()
// or make some animation like:
//let fadeOut=SKAction.fadeOutWithDuration(1.0)
//ball.runAction(fadeOut,completion: {
// self.ball.removeFromParent()
//})
secondball.alpha = 1
// or make some animation like:
//let fadeIn=SKAction.fadeInWithDuration(0.5)
//secondball.runAction(fadeIn,completion: {
// self.secondball.physicsBody?.dynamic = true
//})
secondball.physicsBody?.dynamic = true
}
case "ball2": // if you're touching ball2
self.score += 1
secondball.physicsBody?.velocity = CGVectorMake(0, 100) // here you can decrease parameters to more difficult
secondball.physicsBody?.applyImpulse(CGVectorMake(0, 900))
if score == 20 {
// do the same thing maked for ball to show other ball
}
case "ball3": // if you're touching ball3
self.score += 1
secondball.physicsBody?.velocity = CGVectorMake(0, 100) // here you can decrease parameters to more difficult
secondball.physicsBody?.applyImpulse(CGVectorMake(0, 900))
if score == 21 {
// do other stuff here
}
default:
break
}
}
}
So I want a game made, this was striped from the Xcode SpriteKit sampler, pretty simple. It will evolve greatly as I get this key issue out of the way. It has a player, Wall's, and a door. Nodes are assigned, player works fine. Wall's attempted for children in self, but crashes with my comments removed. I have a guess as multiple nodes of same name? But the door, when assigned node, for some reason no matter what slowly falls, with no gravity ticked and no gravity coded.
Those are lesser concerns. I come to you today to pick at why my collisions might not be activating my collision argument functions, to enter the house.
Yes I am aware it says contact mapped to the event. It suits my theory I am pretty sure.
//
// GameScene.swift
// Sandbox
//
// Created by M on 7/1/16.
// Copyright © 2016 M. All rights reserved.
//
import SpriteKit
import GameplayKit
class GameScene: SKScene, SKPhysicsContactDelegate {
var entities = [GKEntity]()
var graphs = [GKGraph]()
private var lastUpdateTime : TimeInterval = 0
private var label : SKLabelNode?
var playerNode : SKSpriteNode?
var wallNode : SKSpriteNode?
var doorNode : SKSpriteNode?
private var spinnyNode : SKShapeNode?
var furnishing : SKSpriteNode?
var playerCategory = 0x1 << 0
var wallCategory = 0x1 << 1
var doorCategory = 0x1 << 2
var pathCategory = 0x1 << 3
func nextRoom() {
let sceneNode = SKScene(fileNamed: "MyScene")
sceneNode?.scaleMode = .aspectFill
// Present the scene
if let view = self.view {
view.presentScene(sceneNode)
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
}
}
func loadRoom() {
let furnishing = SKSpriteNode(color: #colorLiteral(red: 0.2464724183, green: 0.05352632701, blue: 0.03394328058, alpha: 1), size:CGSize(width:25, height:25))
doorNode?.addChild(furnishing)
}
func enterHouse() {
let newWindow = CGSize(width: 500, height: 500)
doorNode?.scale(to: newWindow)
loadRoom()
}
func exitHouse(){
let oldWindow = CGSize(width: 100, height: 100)
doorNode?.scale(to: oldWindow)
}
override func sceneDidLoad() {
self.lastUpdateTime = 0
physicsWorld.contactDelegate = self
// Get nodes from scene and store for use later
self.playerNode = self.childNode(withName: "//player") as? SKSpriteNode
playerNode?.physicsBody = SKPhysicsBody(rectangleOf: (playerNode?.frame.size)!)
playerNode?.physicsBody?.isDynamic = true
playerNode?.physicsBody?.affectedByGravity = false
playerNode?.physicsBody?.categoryBitMask = UInt32(playerCategory)
playerNode?.physicsBody?.collisionBitMask = UInt32(wallCategory)
playerNode?.physicsBody?.contactTestBitMask = UInt32(doorCategory)
for child in self.children {
/*if child.name == "wall" {
if let child = child as? SKSpriteNode {
wallNode?.physicsBody = SKPhysicsBody(rectangleOf: (wallNode?.frame.size)!)
wallNode?.physicsBody?.isDynamic = false
wallNode?.physicsBody?.categoryBitMask = UInt32(wallCategory)
wallNode?.physicsBody?.collisionBitMask = UInt32(playerCategory)
self.addChild(child)
}
}*/
}
self.doorNode = self.childNode(withName: "door") as? SKSpriteNode
doorNode?.physicsBody?.affectedByGravity = false
doorNode?.physicsBody?.isDynamic = false
doorNode?.physicsBody = SKPhysicsBody(rectangleOf: (doorNode?.frame.size)!)
doorNode?.physicsBody?.categoryBitMask = UInt32(doorCategory)
doorNode?.physicsBody?.contactTestBitMask = UInt32(playerCategory)
}
func touchDown(atPoint pos : CGPoint) {
let fromX = playerNode?.position.x
let fromY = playerNode?.position.y
let toX = pos.x
let toY = pos.y
let resultX = toX - (fromX)!
let resultY = toY - (fromY)!
let newX = (playerNode?.position.x)! + resultX / 10
let newY = (playerNode?.position.y)! + resultY / 10
playerNode?.position.x = newX
playerNode?.position.y = newY
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchDown(atPoint: t.location(in: self)) }
}
func didBeginContact(contact: SKPhysicsContact) {
//this gets called automatically when two objects begin contact with each other
// 1. Create local variables for two physics bodies
var firstBody: SKPhysicsBody
var secondBody: SKPhysicsBody
// 2. Assign the two physics bodies so that the one with the lower category is always stored in firstBody
if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
firstBody = contact.bodyA
secondBody = contact.bodyB
} else {
firstBody = contact.bodyB
secondBody = contact.bodyA
}
if secondBody.categoryBitMask == UInt32(doorCategory){
enterHouse()
}
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
// Initialize _lastUpdateTime if it has not already been
if (self.lastUpdateTime == 0) {
self.lastUpdateTime = currentTime
}
// Calculate time since last update
let dt = currentTime - self.lastUpdateTime
// Update entities
for entity in self.entities {
entity.update(withDeltaTime: dt)
}
self.lastUpdateTime = currentTime
}
}
I presume this line is causing the crash
wallNode?.physicsBody = SKPhysicsBody(rectangleOf: (wallNode?.frame.size)!)
as you are force unwrapping (!) the wallNode size however looking at your code you never assign it to anything like so
wallNode = self.childNode(withName: "wallNode") as? SKSpriteNode
or in the for loop.
Try this code in your for in loop that should avoid crashes and assigns your wall node.
for child in self.children where child.name == "wall" {
if let child = child as? SKSpriteNode {
wallNode = child // Try this
if let wallNode = wallNode { // safely unwrap wall node to avoid crashes
wallNode.physicsBody = SKPhysicsBody(rectangleOf: (wallNode.frame.size))
wallNode.physicsBody?.isDynamic = false
wallNode.physicsBody?.categoryBitMask = UInt32(wallCategory)
wallNode.physicsBody?.collisionBitMask = UInt32(playerCategory)
self.addChild(wallNode) // add wall node here instead if you are using your wallNode property
}
}
}
Hope this helps
So I created this game where you have to shoot at objects. Now, I have an imageset that replicates an object exploding. I would like to call those images to appear in a sequence so it looks like an explosion after the projectile hits the object. Obviously the images have to be called at the exact location of where the projectile hits the object. Does anyone have any idea on how to make this happen? Here is some code.
func projectileDidCollideWithMonster(projectile:SKSpriteNode, monster:SKSpriteNode) {
projectile.removeFromParent()
monster.removeFromParent()
playerScore = playerScore + 1
playerScoreUpdate()
if (playerScore > 100) {
let reveal = SKTransition.crossFadeWithDuration(0.3)
let gameOverScene = GameOverScene(size: self.size, won: true)
self.view?.presentScene(gameOverScene, transition: reveal)
}
}
func didBeginContact(contact: SKPhysicsContact) {
var firstBody: SKPhysicsBody
var secondBody: SKPhysicsBody
if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
firstBody = contact.bodyA
secondBody = contact.bodyB
} else {
firstBody = contact.bodyB
secondBody = contact.bodyA
}
if (firstBody.categoryBitMask & UInt32(laserCategory)) != 0 && (secondBody.categoryBitMask & UInt32(monsterCategory)) != 0 {
projectileDidCollideWithMonster(firstBody.node as SKSpriteNode, monster: secondBody.node as SKSpriteNode)
}
if playerScore > highScore() {
saveHighScore(playerScore)
println("New Highscore = " + highScore().description)
highScoreLabel.text = "Best score: \(highScore().description)"
} else {
println("HighScore = " + highScore().description ) // "HighScore = 100"
}
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
let node = self.nodeAtPoint(location)
if node.name == "muteSound" {
if musicIsPlaying == true {
backgroundMusicPlayer.stop()
musicIsPlaying = false
} else if musicIsPlaying == false {
backgroundMusicPlayer.play()
musicIsPlaying = true
}
} else {
let touch = touches.anyObject() as UITouch
let touchLocation = touch.locationInNode(self)
let projectile = SKSpriteNode(imageNamed: "boom")
projectile.setScale(0.6)
projectile.position = player.position
projectile.physicsBody = SKPhysicsBody(circleOfRadius: projectile.size.width/2)
projectile.physicsBody?.dynamic = true
projectile.physicsBody?.categoryBitMask = UInt32(laserCategory)
projectile.physicsBody?.contactTestBitMask = UInt32(monsterCategory)
projectile.physicsBody?.collisionBitMask = 0
projectile.physicsBody?.usesPreciseCollisionDetection = true
// 3 - Determine offset of location to projectile
let offset = touchLocation - projectile.position
// 4 - Bail out if you are shooting down or backwards
if (offset.y < 0) { return }
// 5 - OK to add now - you've double checked position
addChild(projectile)
// 6 - Get the direction of where to shoot
let direction = offset.normalized()
// 7 - Make it shoot far enough to be guaranteed off screen
let shootAmount = direction * 1000
// 8 - Add the shoot amount to the current position
let realDest = shootAmount + projectile.position
// 9 - Create the actions
let actionMove = SKAction.moveTo(realDest, duration: 2.0)
let actionMoveDone = SKAction.removeFromParent()
if !isStarted {
start()
}else{
projectile.runAction(SKAction.sequence([actionMove, actionMoveDone]))
}
}
}
}
func addMonster() {
let monster = SKSpriteNode(imageNamed: "box")
monster.setScale(0.6)
monster.physicsBody = SKPhysicsBody(rectangleOfSize: monster.size)
monster.physicsBody?.dynamic = true
monster.physicsBody?.categoryBitMask = UInt32(monsterCategory)
monster.physicsBody?.contactTestBitMask = UInt32(laserCategory)
monster.physicsBody?.collisionBitMask = 0
monster.name = "box"
var random : CGFloat = CGFloat(arc4random_uniform(320))
monster.position = CGPointMake(random, self.frame.size.height + 10)
self.addChild(monster)
}
For you explosion you could create an SKSpriteNode that play the frames you mentioned:
1. You're going to need the images as an array of SKTextures. You said you've got you images in an image set so the easiest thing to do may be to create an array using a for loop, for example:
// I don't know how many images you've got, so I'll use 10.
var textures: [SKTexture] = []
for i in 0..<10 {
let imageName = "explosion\(i)"
textures.append(SKTexture(imageNamed: imageName))
}
Alternatively, which is what I would recommend, is to create a Texture Atlas of your images. (For more information on texture atlases see here) To create an atlas, make a folder with the extension .atlas and adding all your explosion images to it. (Then add this to your project). Here's an extension I wrote to get your sprites out of a texture atlas, ready for animation:
extension SKTextureAtlas {
func textureArray() -> [SKTexture] {
var textureNames = self.textureNames as! [String]
// They need to be sorted because there's not guarantee the
// textures will be in the correct order.
textureNames.sort { $0 < $1 }
return textureNames.map { SKTexture(imageNamed: $0) }
}
}
And here's how to use it:
let atlas = SKTextureAtlas(named: "MyAtlas")
let textures = atlas.textureArray()
2. Now you've got your textures you need to create an SKSpriteNode and animate it:
let explosion = SKSpriteNode(texture: textures[0])
let timePerFrame = // this is specific to your animation.
let animationAction = SKAction.animateWithTextures(textures, timePerFrame: timePerFrame)
explosion.runAction(animationAction)
3. Add the sprite to your scene and position it correctly. To add it in the correct place you could use the contactPoint variable on SKPhysicsContact, after checking it was the projectile hitting an object.
func didBeginContact(contact: SKPhysicsContact) {
// Other stuff...
explosion.position = contact.contactPoint
self.addChild(explosion)
}
Hope that helps!