Create a moving Button for a Game in Swift - swift

I have an assignment where I need to create a game with SpriteKit.
It should have a button moving randomly within a view and if I click on it I get points.
The issue is that I have no idea how to create a button in SpriteKit.
Do I need to do a workaround by using a SKSpriteNode? But how would I make it look like a standard button? Or can I actually create a button somehow for that?

SpriteKit has no built-in SKButton class. but we can build some basic functionality. as sangony said you need three parts: draw the graphics (I'm using SKShapeNode for simplicity but you could use SKSpriteNode); move the node; add picking functionality. here is some code to illustrate.
Draw
add a SKShapeNode or SKSpriteNode to your SKNode class
shape = SKShapeNode(circleOfRadius: 40)
shape.fillColor = .green
addChild(shape)
Move
SKAction is very useful. here is an example that moves to a random position, then recursively calls itself. stop/go is regulated by a boolean flag.
func movement() {
print("movement")
let DURATION:CGFloat = 2.0
let random_x = CGFloat.random(in: -200...200)
let random_y = CGFloat.random(in: -200...200)
let random_point = CGPoint(x: random_x, y: random_y)
let move = SKAction.move(to: random_point, duration: DURATION)
move.timingMode = .easeInEaseOut
let wait = SKAction.wait(forDuration: DURATION)
let parallel = SKAction.group([move,wait])
let recursion = SKAction.run {
if self.isInMotion { self.movement() }
}
let serial = SKAction.sequence([parallel, recursion])
self.run(serial)
}
Pick
Testing for user clicks in a scene is called picking. I use a PickableNode protocol which helps filter out which SKNodes you want to be clickable.
extension GameScene {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches{
//call `pick` on any `PickableNode` that exists at touch location
let location = touch.location(in: self)
let _ = self.nodes(at: location).map { ($0 as? PickableNode)?.pick() }
}
}
}
Here is the whole completed class
protocol PickableNode {
func pick()
}
class Button: SKNode, PickableNode {
let shape:SKShapeNode
var isInMotion:Bool = true
override init() {
shape = SKShapeNode(circleOfRadius: 40)
shape.fillColor = .green
super.init()
addChild(shape)
movement()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func movement() {
print("movement")
let DURATION:CGFloat = 2.0
let random_x = CGFloat.random(in: -200...200)
let random_y = CGFloat.random(in: -200...200)
let random_point = CGPoint(x: random_x, y: random_y)
let move = SKAction.move(to: random_point, duration: DURATION)
move.timingMode = .easeInEaseOut
let wait = SKAction.wait(forDuration: DURATION)
let parallel = SKAction.group([move,wait])
let recursion = SKAction.run {
if self.isInMotion { self.movement() }
}
let serial = SKAction.sequence([parallel, recursion])
self.run(serial)
}
func pick() {
print("i got picked")
if !isInMotion {
isInMotion = true
shape.fillColor = .green
movement()
} else {
isInMotion = false
shape.fillColor = .red
self.removeAllActions()
}
}
}

Related

spawning random enemies with an array

I am currently making a game where I need random enemies from my array, to spawn in a random location on repeat. This code seems to work okay other than the fact that it can only rotate through each Enemy once. It comes up with an error saying "Attemped to add a SKNode which already has a parent". Any help? Here is my current code:
func random() -> CGFloat {
return CGFloat(Float(arc4random()) / 0xFFFFFFFF)
}
func random(min: CGFloat, max: CGFloat) -> CGFloat {
return random() * (max - min) + min
}
func spawnEnemy() {
let EnemyArray = [Enemy1, Enemy2, Enemy3, Enemy4, Enemy5, Enemy6]
let randomElement = EnemyArray.randomElement()!
self.addChild(randomElement)
var moveEnemy = SKAction.moveTo(y: -800, duration: 4.0)
let deleteEnemy = SKAction.removeFromParent()
let EnemySequence = SKAction.sequence([moveEnemy, deleteEnemy])
randomElement.run(EnemySequence)
}
func runEnemy() {
run(SKAction.repeatForever(SKAction.sequence([SKAction.run(spawnEnemy), SKAction.wait(forDuration: 2.0)])))
}
as jnpdx suggested, you should spawn new instances of your Enemy class rather than starting with an array of them. you can introduce randomness inside the Enemy class -- for example a random start position or a random color. i would also put your movement and removeFromParent code inside the class as well. You didn't post your Enemy code, but it might look something like this
class Enemy:SKNode {
var shape:SKShapeNode?
override init() {
super.init()
//how ever you want to graphically represent your enemy... using a SKShapeNode for demo
shape = SKShapeNode(ellipseOf: CGSize(width: 20, height: 40))
shape?.fillColor = .blue
addChild(shape ?? SKNode())
//randomize starting x position
position.x = CGFloat.random(in: -200...200)
position.y = 200
move()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//move and remove this node using SKAction
func move() {
let move = SKAction.moveTo(y: -200, duration: 4.0)
let delete = SKAction.removeFromParent()
let sequence = SKAction.sequence([move, delete])
self.run(sequence)
}
}
then you would simply activate your spawn point from didMove(to view: SKView) like this
override func didMove(to view: SKView) {
runSpawnPoint()
}
func runSpawnPoint() {
run(SKAction.repeatForever(SKAction.sequence([SKAction.run(spawnEnemy), SKAction.wait(forDuration: 2.0)])))
}
func spawnEnemy() {
let enemy = Enemy() //a brand new Enemy object each time
addChild(enemy)
}
optional: save your spawned Enemy objects in an array if you want to access them later. alternately you can simply query self.children from your SKScene since they're all stored there as well. in which case you don't need an additional array for storage.
I have found an answer. So originally my problem was trying to spawn multiple, different-looking enemies, at random. I realized that I could solve the same issue by changing the texture of the Enemy, instead of creating many different Enemy Nodes. In order to spawn enemies at random with an array of textures, it would look something like this:
var enemy1 = SKTexture(imageNamed: "Enemy1")
var enemy2 = SKTexture(imageNamed: "Enemy2")
var enemy3 = SKTexture(imageNamed: "Enemy3")
var enemy4 = SKTexture(imageNamed: "Enemy4")
var enemy5 = SKTexture(imageNamed: "Enemy5")
var enemy6 = SKTexture(imageNamed: "Enemy6")
let EnemyArray = [Enemy1, Enemy2, Enemy3, Enemy4, Enemy5, Enemy6]
let randomElement = EnemyArray.randomElement()!
let enemy = SKSpriteNode(imageNamed: "")
enemy.name = "Enemy"
enemy.texture = randomElement
enemy.size = CGSize(width: 30, height: 30)
enemy.zPosition = 2
self.addChild(enemy)
var moveEnemy = SKAction.moveTo(y: -800, duration: 4.0)
let deleteEnemy = SKAction.removeFromParent()
let EnemySequence = SKAction.sequence([moveEnemy, deleteEnemy])
enemy.run(EnemySequence)
}
func runEnemy() {
run(SKAction.repeatForever(SKAction.sequence([SKAction.run(spawnEnemy), SKAction.wait(forDuration: 2.0)])))
}
Thanks everyone for the help

enumerateChildNodes not finding child nodes in sprite kit - Swift 4

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 :]

Rotate a sprite along an axis

In SpriteKit I need to rotate a sprite along an axis (e.g. the one that passes through the center of the sprite) just like a wheel to be spinned by the user.
I tried to use applyTorque function (to apply a force that is only angular and not linear), but I cannot handle the different forces caused by different movements on the screen (longer the touch on the screen, stronger the force to apply).
Can someone help me to understand how to deal with this problem?
Here is an answer that spins a ball according to how fast you swipe left / right:
class GameScene: SKScene {
let speedLabel = SKLabelNode(text: "Speed: 0")
let wheel = SKShapeNode(circleOfRadius: 25)
var recognizer: UIPanGestureRecognizer!
func pan(recognizer: UIPanGestureRecognizer) {
let velocity = recognizer.velocity(in: view!).x
// Play with this value until if feels right to you.
let adjustor = CGFloat(60)
let speed = velocity / adjustor
wheel.physicsBody!.angularVelocity = -speed
}
// Scene setup:
override func didMove(to view: SKView) {
removeAllChildren()
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
wheel.fillColor = .blue
wheel.physicsBody = SKPhysicsBody(circleOfRadius: 25)
wheel.physicsBody!.affectedByGravity = false
let wheelDot = SKSpriteNode(color: .gray, size: CGSize(width: 5, height:5))
wheel.addChild(wheelDot)
wheelDot.position.y += 20
wheel.setScale(3)
speedLabel.setScale(3)
speedLabel.position.y = (frame.maxY - speedLabel.frame.size.height / 2) - 45
recognizer = UIPanGestureRecognizer(target: self, action: #selector(pan))
view.addGestureRecognizer(recognizer)
addChild(wheel)
addChild(speedLabel)
}
override func didSimulatePhysics() {
speedLabel.text = "Speed: \(abs(Int(wheel.physicsBody!.angularVelocity)))"
}
}
Here is a basic example of spinning a wheel clockwise or counterclockwise depending on if you press on left / right side of screen. Hold to increase speed:
class GameScene : SKScene {
enum Direction { case left, right }
var directionToMove: Direction?
let wheel = SKShapeNode(circleOfRadius: 25)
let speedLabel = SKLabelNode(text: "Speed: 0")
override func didMove(to view: SKView) {
// Scene setup:
anchorPoint = CGPoint(x: 0.5, y: 0.5)
removeAllChildren()
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
wheel.fillColor = .blue
wheel.physicsBody = SKPhysicsBody(circleOfRadius: 25)
wheel.physicsBody!.affectedByGravity = false
let wheelDot = SKSpriteNode(color: .gray, size: CGSize(width: 5, height:5))
wheel.addChild(wheelDot)
wheelDot.position.y += 20
wheel.setScale(3)
speedLabel.setScale(3)
speedLabel.position.y = (frame.maxY - speedLabel.frame.size.height / 2) - 45
addChild(wheel)
addChild(speedLabel)
}
// Change this to touchesBegan for iOS:
override func mouseDown(with event: NSEvent) {
// Change this to touches.first!.location(in: self) for iOS.
let location = event.location(in: self)
// Determine if touch left or right side:
if location.x > 0 {
directionToMove = .right
}
else if location.x < 0 {
directionToMove = .left
}
}
override func mouseUp(with event: NSEvent) {
// Stop applying gas:
directionToMove = nil
print("lol")
}
override func update(_ currentTime: TimeInterval) {
// This is how much speed we gain each frame:
let torque = CGFloat(0.01)
guard let direction = directionToMove else { return }
// Apply torque in the proper direction
switch direction {
case .left:
wheel.physicsBody!.applyTorque(torque)
case .right:
wheel.physicsBody!.applyTorque(-torque)
}
}
override func didSimulatePhysics() {
// Speedometer:
speedLabel.text = "Speed: \(abs(Int(wheel.physicsBody!.angularVelocity)))"
}
}

How to make shape not to go to GameOver? -SpriteKit

I am creating a game where there is a square shape and every time the player taps on the square, it goes to GameOver scene. All I want to do is when the square shape is tapped, it will be allotted different position of the screen to be tapped on the squares.
Here is my code:
let touch:UITouch = touches.first!
let positionInScene = touch.location(in: self)
let touchedNode = self.atPoint(positionInScene)
if let name = touchedNode.name
{
//The first ball that shows up
if name == "startball"
{
print("Touched", terminator: "")
addBall(ballSize)
self.addChild(score)
}
else if name == "shape"{
scoreCount += 1
addBall(ballSize)
audioPlayer.play()
}
}
else {
let scene = GameOver(size: self.size)
scene.setMyScore(0)
let skView = self.view! as SKView
skView.ignoresSiblingOrder = true
scene.scaleMode = .resizeFill
scene.size = skView.bounds.size
scene.setMessage("You Lost!")
scene.setEndGameMode(va)
gameTimer.invalidate()
shownTimer.invalidate()
print(" timers invalidated ", terminator: "")
ran = true
skView.presentScene(scene, transition: SKTransition.crossFade(withDuration: 0.25))
}
if firstTouch {
shownTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(GameScene.decTimer), userInfo: nil, repeats: true)
gameTimer = Timer.scheduledTimer(timeInterval: TIME_INCREMENT, target:self, selector: Selector("endGame"), userInfo: nil, repeats: false)
firstTouch = false
}
if touchCount > 5 {
for touch: AnyObject in touches {
let skView = self.view! as SKView
skView.ignoresSiblingOrder = true
var scene: Congratz!
scene = Congratz(size: skView.bounds.size)
scene.scaleMode = .aspectFill
skView.presentScene(scene, transition: SKTransition.doorsOpenHorizontal(withDuration: 1.0))
}
}
touchCount += 1
}
override func update(_ currentTime: TimeInterval) {
super.update(currentTime)
}
func endGame(){
shownTimer.invalidate()
gameTimer.invalidate()
let scene = GameOver(size: self.size)
scene.setMyScore(scoreCount)
if let skView = self.view {
skView.ignoresSiblingOrder = false
scene.scaleMode = .resizeFill
scene.size = skView.bounds.size
scene.setMessage("Times up")
skView.presentScene(scene, transition: SKTransition.crossFade(withDuration: 0.25))
}
}
override func addBall(_ size: Int) {
// Add the ball
let currentBall = SKShapeNode(circleOfRadius: CGFloat(size))
let viewMidX = view!.bounds.midX
let viewMidY = view!.bounds.midY
currentBall.fillColor = pickColor()
currentBall.position = randomBallPosition()
if scoreCount != 0{
if scoreCount == 1{
self.addChild(score)
self.addChild(timeLeftLabel)
self.childNode(withName: "welcome")?.removeFromParent()
}
self.childNode(withName: "ball")?.run(getSmaller)
self.childNode(withName: "ball")?.removeFromParent()
}
currentBall.name = "ball"
self.addChild(currentBall)
}
func addSquare(_ size: Int) {
// Add the square
let shape = SKShapeNode(rectOf: CGSize(width:CGFloat(size), height:CGFloat(size)))
shape.path = UIBezierPath(roundedRect: CGRect(x: 64, y: 64, width: 160, height: 160), cornerRadius: 50).cgPath
shape.fillColor = pickColor()
shape.position = randomBallPosition()
shape.name = "shape"
self.addChild(shape)
}
func randomBallPosition() -> CGPoint {
let xPosition = CGFloat(arc4random_uniform(UInt32((view?.bounds.maxX)! + 1)))
let yPosition = CGFloat(arc4random_uniform(UInt32((view?.bounds.maxY)! + 1)))
return CGPoint(x: xPosition, y: yPosition)
}
What I would suggest would be to make something like an enum for the different shapes, so you can keep track of what shape you're using.
enum GameShape: Int {
case circle = 0
case square = 1
}
Then create a GameShape property at the top of your GameScene:
var currentShape: GameShape = .circle
Then you could create some sort of updateShape method, which you could call in your touchesBegan method instead of just addBall
func updateShape(shapeSize: CGSize) {
switch currentShape {
case .circle:
addCircle(shapeSize)
case .square:
addSquare(shapeSize)
default:
break
}
// However you want to setup the condition for changing shape
if (condition) {
currentShape = .square
}
}
func addBall(_ size: CGSize) {
// Add the ball
}
func addSquare(_ size: CGSize) {
// Add the square
}
Now in your touchesBegan method, instead of calling addBall(size, you could call updateShape:
override func touchesBegan(_ touches: Set<UITouch>!, with event: UIEvent?) {
// Setup your code for detecting position for shape origin
updateShape(shapeSize)
}
EDIT - Your code is a mess. You really should take the time to make sure it's properly formatted when you submit it, otherwise it's really hard to help you. Indentation helps to see where a closure begins and ends. From what I can tell, it looks like you have two or more functions nested within your addBall method. This is not good. I tried my best to clean it up for you. You'll still need to write the code to make the shape a square, but I've lead you in the right direction to start to make that happen:
func addBall(_ size: CGSize) {
// Add the ball
let currentBall = SKShapeNode(circleOfRadius: CGFloat(size))
let viewMidX = view!.bounds.midX
let viewMidY = view!.bounds.midY
currentBall.fillColor = pickColor()
shape.path = UIBezierPath(roundedRect: CGRect(x: 64, y: 64, width: 160, height: 160), cornerRadius: 50).cgPath
shape.fillColor = pickColor()
currentBall.position = randomBallPosition()
shape.position = randomBallPosition()
self.addChild(shape)
if scoreCount != 0{
if scoreCount == 1{
self.addChild(score)
self.addChild(timeLeftLabel)
self.childNode(withName: "welcome")?.removeFromParent()
}
self.childNode(withName: "ball")?.run(getSmaller)
self.childNode(withName: "ball")?.removeFromParent()
}
currentBall.name = "ball"
shape.name = "ball"
self.addChild(currentBall)
}
func addSquare(_ size: CGSize) {
// Add the square
}
func randomBallPosition() -> CGPoint {
let xPosition = CGFloat(arc4random_uniform(UInt32((view?.bounds.maxX)! + 1)))
let yPosition = CGFloat(arc4random_uniform(UInt32((view?.bounds.maxY)! + 1)))
return CGPoint(x: xPosition, y: yPosition)
}
Now the above code assumes you have some property named shape because from what I could tell you never explicitly instantiated that, but you begin manipulating it.

Swift: Attemped to add a SKNode which already has a parent:

I know why I'm getting that error, but I can't figure out a way around it. I'm trying to have objects come appear and then be removed and the player should try to tap them before they're removed, but everytime the next node is about to appear it crashes. If i declare it inside its func then it all comes out but I can't tap on it...
Code:
let step = SKSpriteNode()
override func didMoveToView(view: SKView) {
physicsWorld.contactDelegate = self
backgroundColor = UIColor.feelFreeToColor()
self.color = self.randomNumbersInt(3)
self.showBars()
self.showScore()
let spawn = SKAction.runBlock {
//self.color = self.randomNumbersInt(3)
self.showSteps()
}
let delay = SKAction.waitForDuration(1.5)
let spawnDelay = SKAction.sequence([spawn , delay])
let spawnDelayForever = SKAction.repeatActionForever(spawnDelay)
self.runAction(spawnDelayForever)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch in touches {
var location = touch.locationInNode(self)
if self.nodeAtPoint(location) == step {
self.score += 1
}
}
}
func showSteps() {
let createSteps = SKAction.moveByX(0, y: -self.frame.height - 30 , duration: 10)
let removeSteps = SKAction.removeFromParent()
step.color = colors[randomNumbersInt(3)]!
step.size = CGSize(width: 275, height: 30)
step.position = CGPoint(x: self.frame.width * 0.5, y: self.frame.height * 0.75)
step.physicsBody = SKPhysicsBody(rectangleOfSize: step.size)
step.physicsBody?.categoryBitMask = Config.PhysicBodyType.Steps.rawValue
step.physicsBody?.affectedByGravity = false
step.physicsBody?.dynamic = false
step.zPosition = 1
step.runAction(SKAction.repeatActionForever(SKAction.sequence([createSteps, removeSteps])))
addChild(step)
}
In your showSteps function, declare the step node inside it, not at the top of your code, and also give it a name:
func showSteps() {
let step = SKSpriteNode()
...
step.name = "step"
step.color = colors[randomNumbersInt(3)]!
// etc
}
In your touchesBegan method, you have this if statement:
if self.nodeAtPoint(location) == step {
self.score += 1
}
You want to remove that node that you have hit, but now you can just check the name property like so:
if self.nodeAtPoint(location)?.name == "step" {
self.nodeAtPoint(location)?.removeFromParent()
self.score += 1
}
Please note that I am not super fluent in Swift, but I think you will probably need the ? in your if statement, as it might not exist (such as if you didn't tap on a specific node). Somebody more familiar with Swift is free to correct me.