Swift SpriteKit allow user to choose between player nodes - swift

In my game, the user controls a ship which they move around.
How should I allow the user to select from a range of nodes?
For example, offering a red/blue/green ship which they can choose from or even unlock when their score = X.
Here is my code for the current player node:
let shipTexture = SKTexture(imageNamed: "ship1.png")
ship = SKSpriteNode(texture: shipTexture)
ship.position = CGPoint(x: self.frame.midX, y: -self.frame.height / 3)
ship.zPosition = 3
ship.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 30, height: 100))
ship.physicsBody!.isDynamic = false
ship.run(makeShipAnimate)
ship.physicsBody!.contactTestBitMask = ColliderType.ship.rawValue
ship.physicsBody!.categoryBitMask = ColliderType.ship.rawValue
ship.physicsBody!.collisionBitMask = ColliderType.ship.rawValue
ship.physicsBody?.affectedByGravity = false
self.addChild(ship)
Is there a way to allow the user to pick between "ship1.png" or "ship2.png" for example?

There are many ways to do this. Something like this should get you started:
import SpriteKit
class GameScene: SKScene {
var shipName = ""//create a var to hold the chosen ship
Then create some buttons or a way for the user to choose which ship and create a function to handle the selection by passing in an Int which corresponds to a particular color:
func chooseShip(ship: Int) {
switch ship {
case 0:
shipName = "blueShip.png"
break
case 1:
shipName = "greenShip.png"
break
default:
shipName = "greenShip.png"
}
}
So if the blue ship is chosen for example, that button would call the above function and pass in the selection:
chooseShip(ship: 0)
After the user has chosen you can load the image you want:
let shipTexture = SKTexture(imageNamed: shipName)
etc...

Related

SpriteKit Refer to a Node that's Outside of a Function

I'm currently working on a game in SpriteKit that uses a functions to spawn a spike and I call that function every few seconds to spawn another spike. Here is that function
func summonSpike() {
let spike = SKSpriteNode(imageNamed: "Spike")
let randomizer = Int.random(in: 1..<2)
// 1 = Left
// 2 = Right
if randomizer == 1 {
spike.position = CGPoint(x: -70, y: 300)
spike.zRotation = -1.5708
} else {
spike.position = CGPoint(x: 70, y: 300)
spike.zRotation = 1.5708
}
spike.name = "spike"
spike.size = CGSize(width: 30, height: 60)
spike.physicsBody = SKPhysicsBody(texture: SKTexture(imageNamed: "Spike"), size: CGSize(width: 30, height: 60))
spike.physicsBody?.affectedByGravity = false
spike.physicsBody?.allowsRotation = false
if gameStarted == true && gameEnded == false {
addChild(spike)
}
}
I need to refer to each spike node that exists outside of the function. What's the best way to do this?, I tried putting the let spike = SKSpriteNode(imageNamed: "Spike") outside of the function but then it doesn't let me spawn multiple spikes. How do I refer to a node outside of the function?
there are different ways to do this. as Whirlwind mentioned, you can save your sprites in an array. however i would recommend simply relying on the node structure itself as your storage. once you call addChild(node) you've effectively saved that node for later. here are two ways to access those nodes again, first by filtering on the node's class, and second by using the node's assigned name.
//get all child SKSpriteNodes by class
let nodes_by_class = self.children.compactMap({ $0 as? SKSpriteNode })
//get all children with a particular name
let nodes_by_name = self.children.filter({ $0.name == "spike" })
you can also use childNode(withName:) to grab a single child based on name

I want to reduce the code in methods that are basically identical and pass in the parameters but don't know where to begin?

I have a spriteKit project where I have many characters across several scenes.
As a beginner I just built each one individually for each scene - which makes for a ton of extra code.
I know I could clean this up with a "Build character class" or something like that...
I am just not sure where to begin.
Here is code from two of the characters in one scene...but imagine 5-10 characters per scene?
Also is there a way a property list could be useful for storing these type of properties?
//BEAR
func buildBear() {
let bearAnimatedAtlas = SKTextureAtlas(named: "Bear")
var bearFrames: [SKTexture] = []
let numImages = bearAnimatedAtlas.textureNames.count
for i in 1...numImages {
let bearTextureName = "bear\(i)"
bearFrames.append(bearAnimatedAtlas.textureNamed(bearTextureName))
}
animatedBear = bearFrames
let firstFrameTexture = animatedBear[0]
bear = SKSpriteNode(texture: firstFrameTexture)
bear.size.height = 370
bear.size.width = 370
bear.position = CGPoint(x: 295, y: 25)
bear.zPosition = 1
bear.name = "bear"
isUserInteractionEnabled = true
addChild(bear)
}
//CAT
func buildCat() {
let catAnimatedAtlas = SKTextureAtlas(named: "Cat")
var catFrames: [SKTexture] = []
let numImages = catAnimatedAtlas.textureNames.count
for i in 1...numImages {
let catTextureName = "cat\(i)"
catFrames.append(catAnimatedAtlas.textureNamed(catTextureName))
}
animatedCat = catFrames
let firstFrameTexture = animatedCat[0]
cat = SKSpriteNode(texture: firstFrameTexture)
cat.size.height = 240
cat.size.width = 240
cat.position = CGPoint(x: 134, y: -38)
cat.zPosition = 2
cat.name = "cat"
isUserInteractionEnabled = true
addChild(cat)
}
How could I clean up something like this - I need different position/size per scene but I imagine I could just override that per scene?
I know I know how to do this! - just not where to start?
Gimme a nudge please!
One of the confusing things about the existence of so many languages is that they each have their own jargon, and their own conventions. The root of your problem, however, has nothing to do with Swift or Sprite Kit. When I read your question, I see code that could use some Abstract Data Types. In Java, you would create an Interface, in C++ you would create a "pure virtual" class. Well a rose by any other name still gets the job done. I recommend creating a Protocol, perhaps called Spritable, to define the types of objects that you intend to build into sprites. It would probably be as simple as this:
protocol Spritable {
var species: String { get }
var height: Int { get }
var width: Int { get }
}
The only other thing that differs between your two functions appears to be the starting position. Since this is not inherent in the meaning of a Spritable object, I would package that data separately. A tuple should do the job. With these revisions, your two functions can be merged into one:
func buildSprite(of creature: Spritable, at position: (x: Int, y: Int, z: Int)) {
let spriteAnimatedAtlas = SKTextureAtlas(named: creature.species)
var spriteFrames: [SKTexture] = []
let numImages = spriteAnimatedAtlas.textureNames.count
for i in 1...numImages {
let spriteTextureName = "\(creature.species.lowercased())\(i)"
spriteFrames.append(spriteAnimatedAtlas.textureNamed(spriteTextureName))
}
animatedSprite = spriteFrames
let firstFrameTexture = animatedSprite[0]
sprite = SKSpriteNode(texture: firstFrameTexture)
sprite.size.height = creature.height
sprite.size.width = creature.width
sprite.position = CGPoint(x: position.x, y: position.y)
sprite.zPosition = position.z
sprite.name = creature.species
isUserInteractionEnabled = true
addChild(sprite)
}
To build a bear, aside from a workshop, you will need to define a struct that implements Spritable:
struct Bear: Spritable {
var species: String { return "Bear" }
var height: Int
var width: Int
init(height: Int, width: Int) {
self.height = height
self.width = width
}
}
Then here would be your function call:
buildSprite(of: Bear(height: 370, width: 370), at: (295, 25, 1))
This is a pretty simple example, and could be solved in a few simpler ways than this. However, I find that the larger a project gets, the greater the benefits of organizing code around Abstract Data Types become, so it's worth taking that approach even in a simple case like this.
I ended up not using a protocol on this.
I simply built a method to construct the sprites - similar to what #TallChuck suggested.
func buildCharacter(name:String, height: CGFloat, width: CGFloat, position: CGPoint, zPosition: CGFloat) {
let animatedAtlas = SKTextureAtlas(named: name)
var animationFrames: [SKTexture] = []
let numImages = animatedAtlas.textureNames.count
for i in 1...numImages {
let textureName = "\(name)\(i)"
animationFrames.append(animatedAtlas.textureNamed(textureName))
}
animatedCharacter = animationFrames
let firstFrameTexture = animatedCharacter[0]
builtCharacter = SKSpriteNode(texture: firstFrameTexture)
builtCharacter.size.height = height
builtCharacter.size.width = width
builtCharacter.position = position
builtCharacter.zPosition = zPosition
builtCharacter.name = name
isUserInteractionEnabled = true
addChild(builtCharacter)
}
It works perfect for building and adding to the scenes - had some issues accessing the nodes names for touch detection but got it sorted. Now trying to figure out how to call actions on the nodes - its all different than the normal way. So I will prolly ask that question next. But overall reduced a ton of repeated code! Thanks for the help.
None of this has to be done in code. With Sprite Kit, you create your bear and your cat via the sprite kit editor, and then you load in the sks file by using the constructor that loads by filename.
This is a similar behavior to how game objects work in unity.

Spawn SKSpriteNode objects at random times

I am making a game where I would like to spawn obstacles at random times. The obstacles are going to make contact with the player.
Here the code that I have:
var obstacle : SKSpriteNode?
func createObstacles() {
let obstaclesArray = ["obstacle_1", "obstacle_2", "obstacle_3", "obstacle_4", "obstacle_5"]
let randomObs = obstaclesArray.randomElement()
let selectedTexture = SKTexture(imageNamed: randomObs!)
obstacle = SKSpriteNode(imageNamed: randomObs!)
obstacle?.physicsBody = SKPhysicsBody(texture: selectedTexture, size: selectedTexture.size())
obstacle?.position = CGPoint(x: scene!.size.width/2 + selectedTexture.size().width, y: -120)
obstacle?.zPosition = -15
if let obs = obstacle?.physicsBody {
obs.affectedByGravity = true
obs.allowsRotation = false
obs.isDynamic = true
obs.restitution = 0
obs.categoryBitMask = obstacleCategory
obs.collisionBitMask = floorCategory
obs.contactTestBitMask = heroCategory
}
addChild(obstacle!)
}
func spawnObstacles() {
let wait = SKAction.wait(forDuration: 1, withRange: 0.4)
let spawn = SKAction.run {
createObstacles()
}
let moveLeft = SKAction.moveBy(x: -scene!.size.width - obstacle.size.width - 10, y: 0, duration: 2)
let sequence = SKAction.sequence([wait, spawn, moveLeft, SKAction.removeFromParent()])
self.run(SKAction.repeatForever(sequence))
}
I read some a similar questions but the response is my same code but it is not working.
Spawning a Spritekit node at a random time
I also tried other way:
var randDelay = Double.random(in: 0.7 ..< 1.4)
DispatchQueue.main.asyncAfter(deadline: randDelay, execute: {
if self.canCreateObstacle == true {
self.spawnObstacle()
}})
But it is not working and everytime I restart the game it seems like the function is being called two times, if I restart the game a third time it is called 3 times and so on.
Anyone with a good and clean solution to spawn objects at random times?
Do not use DispatchQueue Set up a repeating sequential action using wait(forDuration:withRange:) like you previously had.
https://developer.apple.com/documentation/spritekit/skaction/1417760-wait
First, create a generic node used to spawn obstacles, then attach this generic node to the scene.
Finally assign the repeating sequential action to this node.
Boom, you are done.
The reason why you want to assign it to a random node is because you want to be able to give your game the opportunity to stop generating obstacles, plus you alter the speed property to make the node generate nodes faster or slower.
You also want to detach the spawning/waiting from the moving/destroying, because as of right now, your code is confused. You are saying move the scene left for 2 seconds, then wait a random amount of time to spawn the next enemy, but I think you are trying to just spawn enemies on a time interval and move the enemy to the left.
Your scene code should look something like this
class GameScene : SKScene{
let obstacleGenerator = SKNode()
func didMove(to: view){
let wait = SKAction.wait(forDuration: 1, withRange: 0.4)
let spawn = SKAction.run({createObstacles()})
let sequence = SKAction.sequence([wait, spawn])
obstacleGenerator.run(SKAction.repeatForever(sequence))
addChild(obstacleGenerator)
}
func createObstacles() {
let obstaclesArray = ["obstacle_1", "obstacle_2", "obstacle_3", "obstacle_4", "obstacle_5"]
let randomObs = obstaclesArray.randomElement()
let selectedTexture = SKTexture(imageNamed: randomObs!)
obstacle = SKSpriteNode(imageNamed: randomObs!)
obstacle.position = CGPoint(x: scene!.size.width/2 + selectedTexture.size().width, y: -120)
obstacle.zPosition = -15
let body = SKPhysicsBody(texture: selectedTexture, size: selectedTexture.size())
body.affectedByGravity = true
body.allowsRotation = false
body.isDynamic = true
body.restitution = 0
body.categoryBitMask = obstacleCategory
body.collisionBitMask = floorCategory
body.contactTestBitMask = heroCategory
obstacle.physicsBody = body
addChild(obstacle!)
let moveLeft = SKAction.moveBy(x: -scene!.size.width - obstacle.size.width - 10, y: 0, duration: 2)
let seq = SKAction.sequence([moveLeft,SKAction.removeFromParent()])
obstacle.run(seq)
}
}
Now as for your spawning increasing with each reset, you never post how you are resetting. I am going to assume you never remove the previous action upon reset, and this is why your rate increases. It is always better to just create a new GameScene instance when doing a reset.

Object positioning is off

Hi, I’m trying to get an object (in this case a green frog) to spawn in line with the player sprite (the red frog) on a platform which is as wide as the scene and what i mean by this, is getting the object to spawn so that when the player advances it doesn’t overlap the object. (The picture shows how the green frog is between two red frogs and not in line with one of the red frogs)
My code for positioning of the objects is as follows
obstacle.position = CGPointMake(-(backgroundSprite.size.width / 2) + CGFloat(randomX) + (spacing * CGFloat(i)), 0)
this currently spawns it on the left side the screen half off the scene. the background sprite is what the object is being added to which is defined like so:
let theSize:CGSize = CGSizeMake(levelUnitWidth, levelUnitHeight)
let tex:SKTexture = SKTexture(imageNamed: imageName)
backgroundSprite = SKSpriteNode(texture: tex, color: SKColor.blackColor(), size: theSize)
random x is also what spawns them randomly on the x axis of the background sprite (which I have also tried adjusting with no luck)
let randomX = arc4random_uniform( UInt32 (backgroundSprite.size.height) )
lastly spacing is the distance between the objects in the same level unit.
let spacing:CGFloat = 250
I have tried implementing the player sprites width as a reference and is hasn’t worked. Can some please tell me what i’m doing wrong here.
Here is the full code if you need to look at it all:
if (theType == LevelType.road) {
for (var i = 0; i < Int(numberOfObjectsInLevel); i++) {
let obstacle:Object = Object()
obstacle.theType = LevelType.road
obstacle.createObject()
addChild(obstacle)
let spacing:CGFloat = 250
obstacle.position = CGPointMake((backgroundSprite.size.width / 4) + CGFloat(randomX) + (spacing * CGFloat(i)), 0)
}
EDIT:
I have tried implementing that code you made in your edit post with code I had already and this is what I got.
if (theType == LevelType.road) {
let xAxisSpawnLocations: [CGFloat] = {
var spawnLocations:[CGFloat] = []
//Create 5 possible spawn locations
let numberOfNodes = 5
for i in 0...numberOfNodes - 1 {
/*
Spacing between nodes will change if:
1) number of nodes is changed,
2) screen width is changed,
3) node's size is changed.
*/
var xPosition = (frame.maxX - thePlayer.size.width) / CGFloat((numberOfNodes - 1)) * CGFloat(i)
//add a half of a player's width because node's anchor point is (0.5, 0.5) by default
xPosition += thePlayer.size.width/2.0
spawnLocations.append( xPosition )
}
return spawnLocations
}()
print(xAxisSpawnLocations)
let yAxisSpawnLocations: [CGFloat] = [0]
let obstacle:Object = Object()
obstacle.theType = LevelType.road
obstacle.createObject()
addChild(obstacle)
let randx = xAxisSpawnLocations[Int(arc4random_uniform(UInt32(xAxisSpawnLocations.count)))]
obstacle.position = CGPoint(x: randx, y: yAxisSpawnLocations[0] )
obstacle.zPosition = 200
}
EDIT 2:
so implemented the code again this time the right way and I got this:
So the player still isn't in line with the objects and for some reason it only spawns on the right side of the screen. I think it is because I have a worldNode that holds everything.
the worldNode holds the player which has a starting point of (0,0) in the worldNode and it also holds the level units which holds the objects. the camera position is centered on the player node I'm not sure if this is the problem but i'll provide the code below so you can have a look at it.
let startingPosition:CGPoint = CGPointMake(0, 0)
The woldNode Code:
let worldNode:SKNode = SKNode()
//creates the world node point to be in the middle of the screen
self.anchorPoint = CGPointMake(0.5, 0.5)
addChild(worldNode)
//adds the player as a child node to the world node
worldNode.addChild(thePlayer)
thePlayer.position = startingPosition
thePlayer.zPosition = 500
The camera positioning code:
override func didSimulatePhysics() {
self.centerOnNode(thePlayer)
}
//centers the camera on the node world.
func centerOnNode(node:SKNode) {
let cameraPositionInScene:CGPoint = self.convertPoint(node.position, fromNode: worldNode)
worldNode.position = CGPoint(x: worldNode.position.x , y:worldNode.position.y - cameraPositionInScene.y )
}
I pretty sure this is my problem but tell me what you think.
Like I said in comments, the key is to predefine coordinates for x (and y) axis and spawn nodes based on that. First, let's define a player inside your GameScene class:
let player = SKSpriteNode(color: .redColor(), size: CGSize(width: 50, height: 50))
Now, predefine spawn locations (for both x and y axis):
let xAxisSpawnLocations: [CGFloat] = [50.0, 125.0, 200.0, 275.0, 350.0, 425.0]
let yAxisSpawnLocations: [CGFloat] = [50.0, 125.0, 200.0, 275.0]
Now when we know possible positions, let position our player first and add it to the scene:
player.position = CGPoint(x: xAxisSpawnLocations[0], y: yAxisSpawnLocations[0])
player.zPosition = 10
addChild(player)
You could create those positions based on player's width and height and screen's size, but because of simplicity, I've hardcoded everything.
So, lets fill one row, right above the player with green frogs:
for xLocation in xAxisSpawnLocations {
let greenFrog = SKSpriteNode(color: .greenColor(), size: player.size)
greenFrog.position = CGPoint(x: xLocation, y: yAxisSpawnLocations[1])
addChild(greenFrog)
}
The result would be something like this:
Or, for example, move the player by one place to the right, and make a column of green frogs right above him:
player.position = CGPoint(x: xAxisSpawnLocations[1], y: yAxisSpawnLocations[0])
for yLocation in yAxisSpawnLocations {
let greenFrog = SKSpriteNode(color: .greenColor(), size: player.size)
greenFrog.position = CGPoint(x: xAxisSpawnLocations[1], y: yLocation)
addChild(greenFrog)
}
And it should look like this:
EDIT:
Based on your comments, this is how you could distribute nodes across the screen based on number of nodes, screen width and node's size:
let xAxisSpawnLocations: [CGFloat] = {
var spawnLocations:[CGFloat] = []
//Create 5 possible spawn locations
let numberOfNodes = 5
for i in 0...numberOfNodes - 1 {
/*
Spacing between nodes will change if:
1) number of nodes is changed,
2) screen width is changed,
3) node's size is changed.
*/
var xPosition = (frame.maxX - player.size.width) / CGFloat((numberOfNodes - 1)) * CGFloat(i)
//add a half of a player's width because node's anchor point is (0.5, 0.5) by default
xPosition += player.size.width/2.0
spawnLocations.append( xPosition )
}
return spawnLocations
}()
print(xAxisSpawnLocations)
You should handle what is happening when too much nodes are added, or if nodes are too big, but this can give you a basic idea how to distribute nodes along x axis and preserve the same distance between them.

Custom Class in Swift using SKSpritenodes/Collision Detection

I am currently working on a game where enemies get spawned from the left of the screen and move right. I want to give these enemies their own attributes (health, strength, etc). So I am working on creating a Basic_fighter class. I also have a user sniper scope that the user uses to hit the enemies. The problem I have is how to access the enemies attributes in the DidBeginContact Function since the function only returns two nodes, and not the class information. I will put my code below
Basic_Fighter_Class
import Foundation
import SpriteKit
class Basic_Fighter {
var health = Int()
var type = SKSpriteNode()
init(sk:SKSpriteNode){
self.type = sk
self.health = 3
}
}
func spawn_enemies(){
let enemynode = SKSpriteNode(imageNamed: "Shooter")
enemynode.size = CGSize(width: 100, height: 40)
enemynode.position = CGPoint(x: self.size.width / 2, y: self.size.height / 2)
enemynode.physicsBody = SKPhysicsBody(rectangleOfSize: enemynode.size)
enemynode.physicsBody?.affectedByGravity = false
enemynode.physicsBody?.categoryBitMask = BodyType.enemy
enemynode.physicsBody?.contactTestBitMask = BodyType.bullet
let enemy = Basic_Fighter(sk: enemynode)
addChild(enemynode)
}
I am able to detect the contact made between the user scope and the enemy in the DidBeginContact function, but I do not know how to access the information of the enemy, such as its health.
I think you can access it with enemynode.health, I'm not sure though.