I am making a game in Xcode with Swift 4, SpriteKit. My crash detection is working, however when I try to make text appear onto the screen when the player crashes, the text doesn't appear. My game basically has the use control a rocket ship which must dodge meteors, the player also has the ability to fire bullets. I have detected when the rocket ship hits a meteor but cannot get text to appear onto the screen. I know for a fact that the crash detection is working, because whenever I add a print statement in the didBegin function, the print statement executes. So it must be that way im displaying the label, but im not sure what I'm doing wrong with the label.
Here's my code:
import SpriteKit
import GameplayKit
import UIKit
class GameScene: SKScene, SKPhysicsContactDelegate{
let player = SKSpriteNode(imageNamed: "spaceship")
let stars = SKSpriteNode(imageNamed: "stars")
let meteor = SKSpriteNode(imageNamed: "meteor")
override func didMove(to view: SKView) {
physicsWorld.contactDelegate = self
print(frame.size.width)
print(frame.size.height)
stars.position = CGPoint(x:0, y:0)
stars.zPosition = 1
player.position = CGPoint(x:0, y:-320)
player.zPosition = 4
player.physicsBody = SKPhysicsBody(circleOfRadius: player.size.width / 2)
player.physicsBody?.affectedByGravity = false
player.physicsBody?.isDynamic = false
player.physicsBody?.categoryBitMask = 2
player.physicsBody?.collisionBitMask = 1
player.physicsBody?.contactTestBitMask = 1
self.addChild(player)
self.addChild(stars)
self.addMeteor()
}
func didBegin(_ contact: SKPhysicsContact) {
gameOver()
}
func addMeteor() {
meteor.physicsBody = SKPhysicsBody(circleOfRadius: meteor.size.width / 2)
meteor.physicsBody?.affectedByGravity = false
meteor.setScale(0.50)
meteor.position = CGPoint(x:Int(arc4random()%300),y:Int(arc4random()%600))
//meteor.position = CGPoint(x:0 , y:0)
meteor.zPosition = 4
meteor.physicsBody?.categoryBitMask = 1
meteor.physicsBody?.collisionBitMask = 0
meteor.physicsBody?.contactTestBitMask = 2
self.addChild(meteor)
}
func fireBullet() {
let bullet = SKSpriteNode(imageNamed: "bullet")
bullet.position = player.position
bullet.setScale(0.5)
bullet.zPosition = 3
self.addChild(bullet)
let moveBullet = SKAction.moveTo(y: self.size.height + bullet.size.height, duration: 1)
let deleteBullet = SKAction.removeFromParent()
let bulletSequence = SKAction.sequence([moveBullet, deleteBullet])
bullet.run(bulletSequence)
}
func gameOver() {
print("Game Over!")
var gameOverLabel: SKLabelNode!
gameOverLabel = SKLabelNode(fontNamed: "Chalkduster")
gameOverLabel.text = "Game Over! You lost!"
gameOverLabel.horizontalAlignmentMode = .right
gameOverLabel.position = CGPoint(x: 0, y:0)
self.addChild(gameOverLabel)
print("Label added")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
fireBullet()
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch: AnyObject in touches {
let pointOfTouch = touch.location(in: self)
let previousPointOfTouch = touch.previousLocation(in: self)
let amountDragged = pointOfTouch.x - previousPointOfTouch.x
player.position.x += amountDragged
}
}
override func update(_ currentTime: TimeInterval) {
meteor.position.y -= 6
if meteor.position.y < player.position.y - 300{
meteor.removeFromParent()
addMeteor()
}
}
}
Not sure what I'm doing wrong? I call the gameOver() function in didBegin which is supposed to be executed every time collision is detected. But nothing happens? The meteor simply goes past the rocket ship. If someone could help me that'd be awesome!
As i see you are trying to center the label on the screen, you may try to add this to your code, on gameover func:
1.Try change the position to this
gameOverLabel.position = CGPoint(x: (self.scene!.frame.width / 2) - (self.scene!.frame.width * self.scene!.anchorPoint.x) / 2, y:(self.scene!.frame.height / 2) - (self.scene!.frame.height * self.scene!.anchorPoint.y))
2.I don't know if you have something that is over the label and affecting its view, so try to:
gameOverLabel.zPosition = 10 //A number higher than any other zPosition
3.The label may be too small...try:
gameOverLabel.fontSize = 30 //Adjust it to what you want
4.Just a tip:
move the:
var gameOverLabel: SKLabelNode!
to next line after the
let meteor = SKSpriteNode(imageNamed: "meteor")
This way you will have a universal label, and do not have to regenerate the label every time.
If you do this, just a final tip...I don't know if you have a func like gameRestart() or something like this...but make sure that when you restart the game, you remove the label:
gameOverLabel.removeFromParent()
If you don't do this, when the app try to add the label again it will crash!
Hope it helps!
You are not moving any of your objects as far as your physics in concerned. You need to apply forces instead of manually moving a position. Also your player dynamic is set to false, which means that he never moves, so contacts only happen on the meteor end.
Related
I am struggling with one issue. Global declaration of my sprite so that I can interact with it. In this game, I have created a local sprite called enemy featured below:
func spawnEnemy() {
let enemy = SKSpriteNode(imageNamed: "as")
let yPosition = CGFloat(frame.maxY - enemy.size.height)
let getXvalue = GKRandomDistribution(lowestValue: Int(frame.minX + enemy.size.width), highestValue: Int(frame.maxX - enemy.size.width))
let xPosition = CGFloat(getXvalue.nextInt())
enemy.position = CGPoint(x: xPosition, y: yPosition)
enemy.name = "asteroid"
enemy.zPosition = 100
addChild(enemy)
let animationDuration:TimeInterval = 6
var actionArray = [SKAction]()
actionArray.append(SKAction.move(to: CGPoint(x: xPosition, y: 0), duration: animationDuration))
actionArray.append(SKAction.self.removeFromParent())
enemy.run(SKAction.sequence(actionArray))
}
I want to tap the enemy to make it disappear from the screen. The variable is declared locally and not globally so the touchesBegan function does not "see" enemy. However, when I move the statement:
let enemy = SKSpriteNode(imageNamed: "as")
outside of local declaration and into global. It works until the code tries to spawn in another enemy and i get an error of "Tried to add an SKNode who already has a parent" This is the code I have running in my view did load:
run(SKAction.repeatForever(SKAction.sequence([SKAction.run{self.spawnEnemy()
}, SKAction.wait(forDuration: 1.0)])))
Every time it spawns a new enemy it crashes and says that the SKNode already has a parent which i understand. However, for my game to function I need the player to be able to touch the individual instance of that enemy and remove it. Hence my code for
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
if let location = touch?.location(in:self) {
let nodesArray = self.nodes(at:location)
if nodesArray.first?.name == "asteroid" {
print("Test")
enemy.removeFromParent()
print("Test Completed")
}
}
}
Now the error says unresolved use of "enemy" because the enemy is not global. I have been going back and forth on this issue for quite some time. If anyone has any potential solution or work around I would be very grateful, and thank you for your help.
Move your enemies to their own class and handle the touch for each of those enemies in their own class. This cleans up your GameScene and keeps your code more organized. You can now add as many instances of enemy as you want.
FYI not related to this question but somethings to consider after you get this working
when game over or level change or win make sure you have a clean up function to remove all enemies
you should strongly consider recycling your objects vs creating them on the fly...better performance
try to separate as much code to your objects class as possible
class enemy: SKSpriteNode {
init() {
super.init(texture: nil, color: .clear, size: CGSize.zero)
setup()
}
func setup() {
isUserInteractionEnabled = true
name = "asteroid"
zPosition = 100
let image = SKSpriteNode(imageNamed: "as")
imagine.zPosition = 1
addChild(image)
self.size = image.size
animate()
}
func animate() {
let animationDuration: TimeInterval = 6
let move = SKAction.move(to: CGPoint(x: xPosition, y: 0), duration: animationDuration)
let remover = SKAction.self.removeFromParent()
run(SKAction.sequence(move, remover))
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
removeFromParent()
}
}
class GameScene: SKScene {
override func didMove(to view: SKView) {
let sequence = SKAction.sequence([SKAction.run{ self.spawnEnemy()
}, SKAction.wait(forDuration: 1.0)])
run(SKAction.repeatForever(sequence))
}
func spawnEnemy() {
let enemy = Enemy()
let yPosition = CGFloat(frame.maxY - enemy.size.height)
let getXvalue = GKRandomDistribution(lowestValue: Int(frame.minX + enemy.size.width), highestValue: Int(frame.maxX - enemy.size.width))
let xPosition = CGFloat(getXvalue.nextInt())
enemy.position = CGPoint(x: xPosition, y: yPosition)
addChild(enemy)
}
}
I am trying to make a 'money' value show on 3 different scenes, and it is showing on the GameScene but not on the GameOverScene or the ShopScene.
this is the relevant code of the GameScene that is working:
func adjustMoney(by points: Int){
var money = UserDefaults().integer(forKey: "moneyscore")
money += points
MoneyLabel.text = "Money = " + UserDefaults().integer(forKey: "moneyscore").description
UserDefaults().set(money, forKey: "moneyscore")
}
func projectileDidCollideWithMonster(projectile: SKSpriteNode, monster: SKSpriteNode) {
print("Hit")
projectile.removeFromParent()
monster.removeFromParent()
monstersDestroyed += 1
adjustScore(by: 1)
adjustMoney(by: 2)
and this is the total code in the other scenes (there is just the ShopScene as it is the same in the other):
import Foundation
import SpriteKit
var welcomeLabel: SKLabelNode!
class ShopScene: SKScene {
var background = SKSpriteNode(imageNamed: "background")
override func didMove(to view: SKView) {
background.zPosition = -1
background.position = CGPoint(x: frame.size.width / 2, y: frame.size.height / 2)
addChild(background) welcomeLabel = SKLabelNode(fontNamed: "Chalkduster")
welcomeLabel.text = "Welcome to the shop!"
welcomeLabel.fontSize = 40
welcomeLabel.fontColor = SKColor.black
welcomeLabel.position = CGPoint(x: size.width/2, y: size.height/1.2)
addChild(welcomeLabel)
MoneyLabel = SKLabelNode(fontNamed: "Chalkduster")
MoneyLabel.text = "Money = \(money)"
MoneyLabel.fontSize = 20
MoneyLabel.fontColor = SKColor.black
MoneyLabel.position = CGPoint(x: size.width/6.2, y: size.height/1.35)
addChild(MoneyLabel)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
let gameScene = GameScene(size: self.size)
gameScene.scaleMode = .aspectFill
self.view?.presentScene(gameScene, transition: SKTransition.doorsCloseHorizontal(withDuration: 1.0))
}
}
However it only says money = 0
---- I'm not sure if it is of any relevance but before my 'projectile' and 'monster' first collide each game all my values of high score and score and money are 0 then they go back to their saved values, apart from score obviously that goes to 0. Score is the only value that isn't saved and that does move across to the GameOverScene.
You need to access UserDefaults().integer(forKey: "moneyscore") to use this in both the GameOverScene or the ShopScene class as you are updating userdefaults value(money).
This is actually for your specific case only. On the other hand, if you are interested to pass data in different classes(you can mention them as screens if they have views), you can follow different methodologies. You can follow this link for references.
I have a simple game where meteors are spawned and the player must dodge these meteors. I am trying to detect if the player (rocketship) has collided with the meteor (enemy) with PhysicsBody. However, nothing happens when they collide. The console should print "Crash detected"
Here's my code:
import SpriteKit
import GameplayKit
import UIKit
class GameScene: SKScene, SKPhysicsContactDelegate{
let player = SKSpriteNode(imageNamed: "spaceship")
let stars = SKSpriteNode(imageNamed: "stars")
let meteor = SKSpriteNode(imageNamed: "meteor")
override func didMove(to view: SKView) {
print(frame.size.width)
print(frame.size.height)
stars.position = CGPoint(x:0, y:0)
stars.zPosition = 1
player.physicsBody = SKPhysicsBody(circleOfRadius: player.size.width / 2)
player.position = CGPoint(x:0, y:-320)
player.zPosition = 4
player.physicsBody = SKPhysicsBody()
player.physicsBody?.affectedByGravity = false
player.physicsBody?.isDynamic = false
player.physicsBody?.categoryBitMask = 0
player.physicsBody?.collisionBitMask = 1
player.physicsBody?.contactTestBitMask = 1
self.addChild(player)
self.addChild(stars)
addMeteor()
}
func addMeteor() {
meteor.physicsBody = SKPhysicsBody(circleOfRadius: meteor.size.width / 2)
meteor.physicsBody?.affectedByGravity = false
meteor.setScale(0.50)
meteor.position = CGPoint(x:Int(arc4random()%300),y:Int(arc4random()%600))
//meteor.position = CGPoint(x:0 , y:0)
meteor.zPosition = 4
meteor.physicsBody?.categoryBitMask = 1
meteor.physicsBody?.collisionBitMask = 0
self.addChild(meteor)
}
func fireBullet() {
let bullet = SKSpriteNode(imageNamed: "bullet")
bullet.position = player.position
bullet.setScale(0.5)
bullet.zPosition = 3
self.addChild(bullet)
let moveBullet = SKAction.moveTo(y: self.size.height + bullet.size.height, duration: 1)
let deleteBullet = SKAction.removeFromParent()
let bulletSequence = SKAction.sequence([moveBullet, deleteBullet])
bullet.run(bulletSequence)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
fireBullet()
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch: AnyObject in touches {
let pointOfTouch = touch.location(in: self)
let previousPointOfTouch = touch.previousLocation(in: self)
let amountDragged = pointOfTouch.x - previousPointOfTouch.x
player.position.x += amountDragged
}
}
func didBegin(_ contact: SKPhysicsContact) {
print("Crash detected")
}
override func update(_ currentTime: TimeInterval) {
meteor.position.y -= 6
if meteor.position.y < player.position.y - 300{
meteor.removeFromParent()
addMeteor()
}
}
}
The program should detect collision and then execute the didBegin function (which prints "Crash detected" to the console). However, when I run the program, the console doesn't have anything printed to it. Anybody know what im doing wrong? Thanks!
in didMove method add this line of code physicsWorld.contactDelegate = self. you set playerCateoryBitmask to 0. that means nothing can hit the player. use player.physicsBody?.categoryBitMask = 2 and also set your meteor a contactTestBitMask like so: meteor.physicsBody?.contactTestBitMask
I am trying to create a SpriteKit game where a ball moves across the screen. When the ball leaves the screen I would like to remove it from the parent and switch to a different scene (GameOverScene).
I am using enumerateChildNodes however it doesn't as if that is working. I am not really sure what the problem is however I think it may have something to do with the parent/child relationship...
func createBall(forTrack track: Int) {
setupTracks()
player?.physicsBody?.linearDamping = 0
player = SKSpriteNode(imageNamed: "small")
player?.name = "BALL"
player?.size = CGSize(width: 100, height: 100)
ballValue = 1
randFloat = Float(arc4random()) / Float(UINT32_MAX)
if randFloat > 0.001 {
ballSpeed = randFloat / 50
}
else {
ballSpeed = randFloat / 50
}
let ballPosition = trackArray?[track].position
player?.position = CGPoint(x: (ballPosition?.x)!, y: (ballPosition?.y)!)
player?.position.y = (ballPosition?.y)!
if ballDirection == "right" {
player?.position.x = 0
moveRight()
}
else {
player?.position.x = (self.view?.frame.size.height)!
moveLeft(speed: ballSpeed)
}
self.addChild(player!)
self.enumerateChildNodes(withName: "BALL") { (node: SKNode, nil) in
if node.position.x < -100 || node.position.x > (self.size.width) + 100 {
print("balls Out")
node.removeFromParent()
let transition = SKTransition.fade(withDuration: 1)
self.gameScene = SKScene(fileNamed: "GameOverScene")
self.gameScene.scaleMode = .aspectFit
self.view?.presentScene(self.gameScene, transition: transition)
}
}
}
I call this function twice, first in override func didMove():
override func didMove(to view: SKView) {
createHUD()
createBall(forTrack: track)
}
And second in override func touchesBegan:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let location = touch.previousLocation(in: self)
let node = self.nodes(at: location).first
if node?.name == "BALL" {
currentScore += ballValue
player?.removeFromParent()
createBall(forTrack: track)
}
else {
let transition = SKTransition.fade(withDuration: 1)
gameScene = SKScene(fileNamed: "GameOverScene")
gameScene.scaleMode = .aspectFit
self.view?.presentScene(gameScene, transition: transition)
}
}
}
update:
The line self.enumerateChildNodes(withName: "BALL") { (node: SKNode, nil) in works so it is not a child parent relationship issue. The if statement is not working.
reading your code I think it's not about a solution, but a different approach. These suggestions will make your life easier:
Start with a smaller code, avoid or comment out all unneeded (like the if randFloat)
Force unwrap player = SKSpriteNode(imageNamed: "small")! with the ! because you actually want to crash if the initialization fails, then you can get rid of all ?
in touchesBegan, better use for touch in touches { as it is more simple to manage
let location = touch.previousLocation(in: self) you probably mean let location = touch.location(in: self)
When the ball leaves the screen I would like to remove it from the parent so this is something happening at some time in the game. You would like to call your self.enumerateChildNodes(withName: "BALL") { into the update call, not every time you touch the screen
If this is not enough, feel free to post the least amount of code to make a playground and let me test it for you :]
I created a project where I have a ball and when the view loads, it falls down, which is good. I'm trying to get the ball to jump back up and fall down again when the Spritenode is tapped.
--This Question was edited--
Originally, I was able to get it to work when sprite.userInteractionEnabled = false. I had to turn this statement true in order to get the score to change.
Now I can't get the balls to fall and be tapped to jump. When I turn ball.physicsBody?.dynamic = true, the balls will fall due to gravity. How do I tap the sprite itself and make it jump.
GameScene.swift (For those who want to try the code for themselves.)
import SpriteKit
class GameScene: SKScene {
var ball: Ball!
private var score = 0 {
didSet { scoreLabel.text = "\(score)" }
}
override func didMoveToView(view: SKView) {
let ball = Ball()
scoreLabel = SKLabelNode(fontNamed:"Geared-Slab")
scoreLabel.fontColor = UIColor.blackColor()
scoreLabel.position = CGPoint( x: self.frame.midX, y: 3 * self.frame.size.height / 4 )
scoreLabel.fontSize = 100.0
scoreLabel.zPosition = 100
scoreLabel.text = String(score)
self.addChild(scoreLabel)
ball.position = CGPoint(x:self.size.width / 2.0, y: 440)
addChild(ball)
ball.physicsBody = SKPhysicsBody(circleOfRadius: 120)
ball.physicsBody?.dynamic = true
ball.physicsBody?.allowsRotation = false
ball.physicsBody?.restitution = 3
ball.physicsBody?.friction = 0
ball.physicsBody?.angularDamping = 0
ball.physicsBody?.linearDamping = 0
ball.physicsBody?.usesPreciseCollisionDetection = true
}
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
}
Before, it was a SKNode being tapped using CGVectorMake(impulse, velocity) now, it's a SKSpriteNode, and I tried using SKAction, but it either does not work, or I'm putting it in the wrong place(touches begin).
I tested your code and it seems that using firstBall.userInteractionEnabled = true is the cause. Without it, it should work. I did some research (here for example), but can't figure out what's the reason of this behavior with userInteractionEnabled. Or for what reason do you use userInteractionEnabled?
Update due to update of question
First ball.physicsBody?.restitution = 3 defines the bounciness of the physics body. The default value is 0.2 and the property must be between 0.0 ans 1.0. So if you set it to 3.0 it will cause some unexpected effects. I just deleted it to use the default value of 0.2.
Second, to make the ball jump after tap and increase the score I put
physicsBody?.velocity = CGVectorMake(0, 600)
physicsBody?.applyImpulse(CGVectorMake(0, 1100))
in the touchesBegan method of the Ball class
Result