I have been developing a simple game in Swift in order to increase my exposure towards the language syntax and concepts. I am currently facing a problem in which the touches are not detected in the game application. The application uses action for a key method to start the game but unfortunately, when I tap on the screen the SKSpriteNodes are not spawning from the top of the screen (at-least visually). I have entered a few print states to see if the code has reached certain methods but it looks like the cause of the problem is within the touch method as the print line is not executed at the touch method. Can someone help me identify what is my mistake and how to prevent this?
For reference purposes I have included the class:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
private var counter: Int = 0
private var level: Int = 0
private var debug: SKLabelNode?
// Here we set initial values of counter and level. Debug label is created here as well.
override func didMove(to view: SKView) {
counter = 0
level = 1
backgroundColor = SKColor.gray
debug = SKLabelNode(fontNamed: "ArialMT")
debug?.fontColor = SKColor.purple
debug?.fontSize = 30.0
debug?.position = CGPoint(x: frame.midX, y: frame.midY)
debug?.text = "Counter : [ \(counter) ], Level [ \(level) ]"
if let aDebug = debug {
addChild(aDebug)
}
print(action(forKey: "counting") == nil)
}
//Method to start a timer. SKAction is used here to track a time passed and to maintain the 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?.counter = (weakSelf?.counter ?? 0) + 1
//Maintaining level
if (weakSelf?.counter ?? 0) < 5 {
//level 1
weakSelf?.level = 1
} else if (weakSelf?.counter ?? 0) >= 5 && (weakSelf?.counter ?? 0) < 10 {
//level 2
weakSelf?.level = 2
} else {
//level 3
weakSelf?.level = 3
}
weakSelf?.debug?.text = "Counter : [ \(Int(weakSelf?.counter ?? 0)) ], Level [ \(Int(weakSelf?.level ?? 0)) ]"
})
run(SKAction.repeatForever(SKAction.sequence([SKAction.wait(forDuration: 1), block])), withKey: "counting")
}
//Method for stopping the timer and reset everything to the default state.
func stopTimer() {
print("TIMER STOPPED.")
if action(forKey: "counting") != nil {
removeAction(forKey: "counting")
}
counter = Int(0.0)
level = 1
debug?.text = "Counter : [ \(counter) ], Level [ \(level) ]"
}
//Get current speed based on time passed (based on counter variable)
func getCurrentSpeed() -> CGFloat {
if counter < 5 {
//level 1
return 1.0
} else if counter >= 5 && counter < 10 {
//level 2
return 2.0
} else {
//level 3
return 3.0
}
}
//Method which stops 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_uniform(UInt32(CGFloat(RAND_MAX) + 1 / CGFloat(RAND_MAX) * diff )))) + 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: 2, withRange: 0.5)
//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?.spawnStone(withSpeed: weakSelf?.getCurrentSpeed() ?? 0.0)
stone?.zPosition = 20
if let aStone = stone {
weakSelf?.addChild(aStone)
}
})
run(SKAction.repeatForever(SKAction.sequence([delay, block])), withKey: "spawning")
}
//Returns stone with moving action added. Inside, you set standard things, like size, texture, physics body, name and position of a stone
func spawnStone(withSpeed stoneSpeed: CGFloat) -> SKSpriteNode? {
print("SPAWNNING STONES...")
let stoneSize = CGSize(width: 30, height: 30) //size of shape.
//you can randomize size here
let stonePosition = CGPoint(x: randomFloatBetween(0.0, and: frame.size.width), y: frame.maxY) //initial position
//you can randomize position here
let stone = SKSpriteNode(color: SKColor.green, size: stoneSize) //setting size and color.
stone.name = "stone" //named shape so we can check collision.
//this helps if you want to enumerate all stones by name later on in your game
stone.position = stonePosition //set position.
let move = SKAction.moveBy(x: 0, y: -200, duration: 3.25)
//one way to change speed
move.speed = stoneSpeed
let moveAndRemove = SKAction.sequence([move, SKAction.removeFromParent()])
stone.run(moveAndRemove, withKey: "moving")
//access this key if you want to stop movement
return stone
}
func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
/* Called when a touch begins */
//just a simple way to start and stop a game
/**
TOUCH METHOD NOT WORKING.
*/
if action(forKey: "counting") == nil {
print("HERE")
startTimer()
generateStones()
} else {
print("OR HERE")
stopTimer()
stopGeneratingStones()
}
}
}
I have fixed the problem to my own question after some debugging. I have realised that the touchesBegan method does not use override the super, hence the application is mistaking it for a local method. To overcome this problem simply adding the override will fix problem.
change this:
func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
/* Called when a touch begins */
//just a simple way to start and stop a game
/**
TOUCH METHOD NOT WORKING.
*/
if action(forKey: "counting") == nil {
print("HERE")
startTimer()
generateStones()
} else {
print("OR HERE")
stopTimer()
stopGeneratingStones()
}
}
to this:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
/* Called when a touch begins */
//just a simple way to start and stop a game
/**
TOUCH METHOD NOT WORKING.
*/
if action(forKey: "counting") == nil {
print("HERE")
startTimer()
generateStones()
} else {
print("OR HERE")
stopTimer()
stopGeneratingStones()
}
}
Related
I want my character to jump whenever I press two buttons at the same time. I've already tried this:
if rightButton.contains(location) && leftButton.contains(location) {
character.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 50))
}
One approach would be:
In your functions that detects the interaction with the button prepare it with a boolean.
Then in your Update function, use a timer to add a range of time where we can say that both buttons are pressed at the same time (100 ms for example).
I'll let you here some pseudocode that I hope it helps.
func RightBtnClick()->Void{
rightBtnPressed = true
}
func LeftBtnClick()->Void{
leftBtnPressed = true
}
func Start()->Void{
rightBtnTimer = 0
leftBtnTimer = 0
}
func Update(deltatime ms:float)->Void{
if(rightBtnPressed){
rightBtnTimer += ms;
if(rightBtnTimer>100){
rightBtnTimer = 0
rightBtnPressed=false
}
}
if(leftBtnPressed){
leftBtnTimer += ms;
if(leftBtnTimer>100){
leftBtnTimer = 0
leftBtnPressed=false
}
}
// Lastly let's check if both are pressed.
if(leftBtnPressed && rightBtnPressed){
DoStuff()
}
}
First of all, make sure in GameViewController.swift you have multitouch enabled.
class GameViewController: UIViewController
{
override func viewDidLoad()
{
super.viewDidLoad()
// ...
if let view = self.view as! SKView?
{
// ...
view.isMultipleTouchEnabled = true
}
}
}
In GameScene give name to your buttons. On tap we will create a list of every node your fingers touched that has a name. If the list contains both right and left button, it means he pressed both at the same time.
class GameScene: SKScene
{
override func didMove(to view: SKView)
{
// add name to each button
left_button.name = "left_button"
right_button.name = "right_button"
}
func buttons_touched(_ touches: Set<UITouch>) -> [String]
{
// get the list of buttons we touched on each tap
var button_list : [String] = []
for touch in touches
{
let positionInScene = touch.location(in: self)
let touchedNode = self.nodes(at: positionInScene)
let buttons = touchedNode.compactMap { (node) -> String in
node.name ?? ""
}.filter({$0 != ""})
button_list += buttons
}
return button_list
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
{
let buttons_tapped = buttons_touched(touches)
if buttons_tapped.contains("right_button") && buttons_tapped.contains("left_button")
{
// jump code
}
}
}
You can simulate multitouch inside Simulator by holding Option button.
I've been following this solution (How can I increase and display the score every second?) to add scoring to my game based on how much time has passed. I have it working perfectly; the score stops when the player loses and it restarts back to 0 when the player restarts the game.
However, the "timer" begins automatically before the user taps to begin, and I'm trying to have the "timer" start when the player taps on the game to begin playing (the user first has to tap on the screen to start running, beginning the game).
In my didMove method, I have
scoreLabel = SKLabelNode(fontNamed: "Press Start K")
scoreLabel.text = "Score: 0"
scoreLabel.position = CGPoint(x: 150.0, y: 620.0)
scoreLabel.zPosition = GameConstants.ZPositions.hudZ
addChild(scoreLabel)
and in my override func update method, I have
if gameStateIsInGame {
if counter >= 10 {
score += 1
counter = 0
} else {
counter += 1
}
}
I figured that by adding the if gameStateIsInGame {} to the touchesBegan method, it would start when the user taps on the screen but that didn't work. I tried adding it under case.ready: and under case.ongoing: but neither worked.
This is what I have at the top of my touchesBegan method.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
switch gameState {
case .ready:
gameState = .ongoing
spawnObstacles()
case .ongoing:
touch = true
if !player.airborne {
jump()
startTimers()
}
}
}
Any ideas on how to fix this small issue? I can't seem to figure it out.
****EDIT****
Here's the updated override func update method. I got rid of if gameStateIsInGame {} and added the if counter >= 10 statement under if gameState.
override func update(_ currentTime: TimeInterval) {
if lastTime > 0 {
dt = currentTime - lastTime
} else {
dt = 0
}
lastTime = currentTime
if gameState == .ongoing {
worldLayer.update(dt)
backgroundLayer.update(dt)
backgroundGround.update(dt)
backgroundSunset.update(dt)
if counter >= 10 {
score += 1
counter = 0
} else {
counter += 1
}
}
}
Simplified Solution
Added the score counter under override func update.
override func update(_ currentTime: TimeInterval) {
if gameState == .ongoing {
if counter >= 10 {
score += 1
counter = 0
} else {
counter += 1
}
}
}
I am making a game with Sprite Kit where the user has tap balls that pass through the screen. The balls are spawned every 1 second. However, if two balls have spawned and the user taps the first ball only the second (and any that have spawned after that) will be removed/recorded and not the one the user actually tapped.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
let node = self.nodes(at: location).first
if node?.name == "BALL" {
currentScore += ballValue
player?.removeFromParent()
}
else {
gameOver()
}
}
}
override func didMove(to view: SKView) {
setupTracks()
createHUD()
self.run(SKAction.repeatForever(SKAction.sequence([SKAction.run {
self.createBall(forTrack: self.track)
}, SKAction.wait(forDuration: 2)])))
}
func createBall(forTrack track: Int) {
setLevel()
player?.name = "BALL"
player?.size = CGSize(width: 100, height: 100)
ballValue = 1
let ballPosition = trackArray?[track].position
player?.position = CGPoint(x: (ballPosition?.x)!, y: (ballPosition?.y)!)
player?.position.y = (ballPosition?.y)!
player?.zPosition = 1
if ballDirection == "right" {
player?.position.x = 0
moveRight()
}
else {
player?.position.x = (self.view?.frame.size.height)!
moveLeft()
}
}
I’m pretty sure it’s because you do:
player?.removeFromParent()
no matter which sprite is touched, but player is always the last sprite spawned. You’ve already assigned the node that was touched to node, so I think you need to do:
node.removeFromParent()
instead.
I am making a game where as the main sprite/player moves constantly, he/she needs to jump through barriers.
I need help with how to set a constant velocity for my moving sprite. When I try and do this in the SpriteKit update function, I can’t apply an impulse to jump whenever the user taps the screen.
Here is my code. I commented the places where I am having trouble:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Called when a touch begins */
if (gameStarted == false) {
gameStarted = true
mainSprite.physicsBody?.affectedByGravity = true
mainSprite.physicsBody?.allowsRotation = true
let spawn = SKAction.runBlock({
() in
self.createWalls()
})
let delay = SKAction.waitForDuration(1.5)
let spawnDelay = SKAction.sequence([spawn, delay])
let spawnDelayForever = SKAction.repeatActionForever(spawnDelay)
self.runAction(spawnDelayForever)
let distance = CGFloat(self.frame.height + wallPair.frame.height)
let movePipes = SKAction.moveByX(0, y: -distance - 50, duration: NSTimeInterval(0.009 * distance)) // Speed up pipes
let removePipes = SKAction.removeFromParent()
moveAndRemove = SKAction.sequence([movePipes, removePipes])
} else {
if died == true {
}
else {
mainSprite.physicsBody?.applyImpulse(CGVectorMake(0, 20)) // TRYING TO APPLY AN IMPULSE TO MY SPRITE SO IT CAN JUMP AS IT MOVES
}
}
for touch in touches {
let location = touch.locationInNode(self)
}
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
updateSpritePosition()
mainSprite.physicsBody?.velocity = CGVectorMake(400, 0) // SETS A CONSTANT VELOCITY, HOWEVER I CAN NOT APPLY AN IMPULSE.
}
The problem is that you're overwriting the velocity in the update method. So even though you added an impulse, it gets immediately overwritten by code in the update. Try overwriting just the dx part of the velocity.
override func update(currentTime: CFTimeInterval) {
updateSpritePosition()
mainSprite.physicsBody?.velocity = CGVectorMake(400, mainSprite.physicsBody?.velocity.dy)
}
The timeLabel should count down from 60 to 0 but I have yet to implement a duration. For instance timeLabel.text = String(i) //implement every 1 second So that it will resemble a real count down timer. How would I do that. The other issue is that the game won't start in the simulator when running this code. I get an error and I am redirected to the AppDelegate.swift file: class AppDelegate: UIResponder, UIApplicationDelegate { //error: Thread 1: signal SIGABRT
class GameScene: SKScene {
var timeLabel = SKLabelNode()
override func didMoveToView(view: SKView) {
for var i = 60; i > 0; i-- {
timeLabel.text = String(i)
timeLabel.position = CGPointMake(frame.midX, frame.midY)
timeLabel.fontColor = UIColor.blackColor()
timeLabel.fontSize = 70
timeLabel.fontName = "Helvetica"
self.addChild(timeLabel)
}
}
}
You can do this in a few ways, and here is an example on how to update label text (counter) using SKAction:
import SpriteKit
class GameScene: SKScene {
let timeLabel = SKLabelNode(fontNamed: "Geneva")
var counter = 60
override func didMoveToView(view: SKView) {
timeLabel.text = "60"
timeLabel.position = CGPointMake(frame.midX, frame.midY)
timeLabel.fontColor = UIColor.blackColor()
timeLabel.fontSize = 40
self.addChild(timeLabel)
}
func countdown(){
let updateCounter = SKAction.runBlock({
self.timeLabel.text = "\(self.counter--)"
if(self.counter == 0){
self.counter = 60
}
})
timeLabel.text = "60"
timeLabel.position = CGPointMake(frame.midX, frame.midY)
timeLabel.fontColor = UIColor.blackColor()
timeLabel.fontSize = 40
let countdown = SKAction.repeatActionForever(SKAction.sequence([SKAction.waitForDuration(1),updateCounter]))
//You can run an action with key. Later, if you want to stop the timer, are affect in any way on this action, you can access it by this key
timeLabel.runAction(countdown, withKey:"countdown")
}
func stop(){
if(timeLabel.actionForKey("countdown") != nil){
timeLabel.removeActionForKey("countdown")
}
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
if(timeLabel.actionForKey("countdown") == nil){
self.countdown()
}
}
}
What I am doing here is updating a label's text property each second. To achieve that, I've created a block of code which updates a counter variable. That block of code is called each second using the action sequence.
Note that your current code trying to add label in each loop. Node can have only one parent, and like the app will crash with following error message :
Attemped to add a SKNode which already has a parent
Also you are not running updating label's text property once in a second. You are executing the whole for loop at once (which is done in much less time then a second).