How to generate a random SKSpriteNode based on four possible values - ios

To generate an image I have written the following function. However, I want that function to able to generate one of four possible images, instead of the same image every time. Right now it generates the "Top Side" image every time, but I want it to randomly choose one of the four images I have to generate.
func spawnBrick() {
let Brick = SKSpriteNode(imageNamed: "Top Side")
Brick.size = CGSize(width: 130, height: 100)
Brick.position = CGPoint(x: frame.midX, y: frame.maxY - Brick.size.width)
addChild(Brick)
}
enum brickType: UInt {
case brickTop = 1
case brickLeft = 2
case brickRight = 3
case brickBottom = 4
}
let brickTop = SKSpriteNode(imageNamed: "Top Side")
let brickLeft = SKSpriteNode(imageNamed: "Left Side")
let brickRight = SKSpriteNode(imageNamed: "Right Side")
let brickBottom = SKSpriteNode(imageNamed: "Bottom Side")

Swift array has a method called randomElement that will give you an optional element from the array (not its optional because the array could be empty)
https://developer.apple.com/documentation/swift/array/2994747-randomelement
func spawnBrick() {
let Brick = SKSpriteNode(imageNamed: ["Top Side","Left Side","Right Side","Bottom Side"].randomElement()!)
Brick.size = CGSize(width: 130, height: 100)
Brick.position = CGPoint(x: frame.midX, y: frame.maxY - Brick.size.width)
addChild(Brick)
}

Related

How to add to a variable in SpriteKit

Relatively new to swift so apologies if I'm being stupid.
I'm using Xcode 13.3 beta 3
I have some code which spawns in a sprite named Monster and moves it from the right of the screen to the left at random speeds and at a random y location.
When it moves off screen it removes from parent and transitions to a loose screen.
what I want it to do when it leaves the screen is for it to add to a variable I've defined at the top of the class:
var pass = 0
here's the code for the monsters:
func addMonster() {
let monster = SKSpriteNode(imageNamed: "monster")
monster.physicsBody = SKPhysicsBody(rectangleOf: monster.size)
monster.physicsBody?.isDynamic = true
monster.physicsBody?.categoryBitMask = PhysicsCatagory.monster
monster.physicsBody?.contactTestBitMask = PhysicsCatagory.projectile
monster.physicsBody?.collisionBitMask = PhysicsCatagory.none
let actualY = random(min: monster.size.height/2, max: size.height - monster.size.height/2)
monster.position = CGPoint(x: size.width + monster.size.width/2, y: actualY)
addChild(monster)
let actualDuration = random(min: CGFloat(1.65), max: CGFloat(5.5))
let actionMove = SKAction.move(to: CGPoint(x: -monster.size.width/2, y: actualY), duration: TimeInterval(actualDuration))
let actionMoveDone = SKAction.removeFromParent()
let loseAction = SKAction.run() { [weak self] in
guard let `self` = self else {return}
let end = SKTransition.crossFade(withDuration: 0.5)
let gameOverScene = GameOverScene(size: self.size, won: false)
self.view?.presentScene(gameOverScene, transition: end)
}
monster.run(SKAction.sequence([actionMove,loseAction, actionMoveDone]))
}
what I want is the loose action to just add +1 to the pass variable then have an if statement for when pass is greater than 3 to run the code that goes to the loose screen.
Hope that all makes sense
any help would be much appreciated!

double spawning (& overlapping) enemies on Spritekit

I'm having trouble with an endless run game, mainly with the spawning on the two kind of the enemies that I created, sometimes happens that two enemies spawns when we call the function "startDifficultyTimer".
Basically when i call that function the game spawn 2 nodes in the same row so the player is forced to lose, I want to avoid that but I don't know how, I tried almost everything,I've tried to remove createsequentialenemy from didmove but the problem still persist, I think (as newbie) that the problem is in startDifficultTimer function because when the value reach the limit the problem vanish
I've posted the code below, sorry for the probably huge mistake but we're new in swift development game, an huge thank you all!
func createEnemy() {
let enemy: Enemy
let duration: CGFloat
switch Int(arc4random() % 100) {
case 0...70:
enemy = Enemy.createEnemy()
duration = CGFloat(Float(arc4random()%1)) + durationV
enemy.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 55, height: 37))
let enemyf = enemy.frame
let fixedx = frame.width + enemy.frame.width/2.0
let positions = [ CGPoint(x: fixedx, y: 383), CGPoint(x: fixedx, y: 447), CGPoint(x: fixedx, y: 511)]
let position = positions[Int(arc4random_uniform(UInt32(positions.count)))]
enemy.position = position
case 71...100:
enemy = Enemy.createEnemyMedium()
duration = CGFloat(Float(arc4random()%1)) + durationV
enemy.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 40, height: 70))
let enemyf = enemy.frame
let fixedx = frame.width + enemy.frame.width/2.0
let positions = [ CGPoint(x: fixedx, y: 415), CGPoint(x: fixedx, y: 479)]
let position = positions[Int(arc4random_uniform(UInt32(positions.count)))]
enemy.position = position
default:
enemy = Enemy.createEnemy()
//type = .small
duration = CGFloat(Float(arc4random()%1)) + durationV
let enemyf = enemy.frame
let fixedx = frame.width + enemy.frame.width/2.0
let positions = [ CGPoint(x: fixedx, y: 383), CGPoint(x: fixedx, y: 447), CGPoint(x: fixedx, y: 511)]
let position = positions[Int(arc4random_uniform(UInt32(positions.count)))]
let texture = SKTexture(imageNamed: "dronea1")
enemy.position = position
}
enemy.physicsBody!.isDynamic = false
enemy.physicsBody!.categoryBitMask = PhysicsCategory.Enemy
addChild(enemy)
let moveTo = SKAction.moveTo(x: 0.0, duration: TimeInterval(duration))
enemy.run(.repeatForever(.sequence([moveTo, .removeFromParent()])))
}
func createSequentialEnemies() {
// remove previous action if running. This way you can adjust the spawn duration property and call this method again and it will cancel previous action.
removeAction(forKey: spawnKey)
let spawnAction = SKAction.run(createEnemy)
let spawnDelay = SKAction.wait(forDuration: spawnDuration)
let spawnSequence = SKAction.sequence([spawnAction, spawnDelay])
run(SKAction.repeatForever(spawnSequence), withKey: spawnKey)later
}
func startDifficultyTimer() {
let difficultyTimerKey = "DifficultyTimerKey"
let action1 = SKAction.wait(forDuration: 1)
let action2 = SKAction.run { [unowned self] in
guard self.spawnDuration > 0.5 else { // set a min limit
self.removeAction(forKey: difficultyTimerKey) // if min duration has been reached than you might as well stop running this timer.
return
}
self.spawnDuration -= 0.5 // reduce by half a second
self.createSequentialEnemies() // spawn enemies again
}
let sequence = SKAction.sequence([action1, action2])
run(SKAction.repeatForever(sequence), withKey: difficultyTimerKey)
}
I think that the issue is that you are saying "stop this action" when the timer changes. But that statement doesn't know if an enemy has just been created or is just about to be created. Wherever it is in its loop you are stopping it and starting the loop over. So if it had just created an enemy and you stop the loop and start it over by generating a new enemy faster you will get two enemies in a row.
A way around this could be to run your sequence in reverse. Pause and then generate your enemy.
let spawnSequence = SKAction.sequence([spawnDelay, spawnAction])
you might get a slightly longer gap between enemies but you won't get two that span on top of each other.
Otherwise you could track the spawn time each time the last enemy is spawned and minus it from the next spawn time to put a custom wait action in between.

How to call a UITextField inside of SpriteKit?

As the question states, I am attempting to call on a UITextField inside of SpriteKit to accept a user input for a name. I've looked at many posts on here but none of them seem to apply to the issue I am facing. The first thing I am doing is declaring the UITextField as a class variable, so I can remove it from view in a separate function later by doing the following:
let nameEntry = UITextField(frame: CGRect(origin: CGPoint(x: 800, y: 875), size: CGSize(width: 600, height: 200)))
then I go on to add the UITextField by stating:
self.view?.addSubview(nameEntry)
However, the text box does not show up in my scene. I've looked at it in hierarchy view and it is simply not there. I am fairly inexperienced at using SceneKit so I'm curious to see what I'm doing wrong. Thanks in advance for anyone who attempts to help!
for a better look at how I am going about this:
class Tutorial : SKScene{
let nameEntry = UITextField(frame: CGRect(origin: CGPoint(x: 800, y: 875), size: CGSize(width: 600, height: 200)))
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
//insert many lines of code here
self.view?.addSubview(nameEntry)
}
}
EDIT: Here's all of the code leading up to this point.
class Tutorial : SKScene{
var chatBoxInt : Int = 0
let chatBoxLabel : SKLabelNode = SKLabelNode(text: "")
let arrow = SKSpriteNode(imageNamed: "Arrow")
let HappinessIcon = SKSpriteNode(imageNamed: "Happiness Icon")
let IntelligenceIcon = SKSpriteNode(imageNamed: "IntelligenceIcon")
let HealthIcon = SKSpriteNode(imageNamed: "HealthIcon")
let chatBox = SKSpriteNode(imageNamed: "ChatBox")
let maleButtonBackground = SKSpriteNode(imageNamed: "ButtonBackground")
let femaleButtonBackground = SKSpriteNode(imageNamed: "ButtonBackground")
let selectedGenderButtonBackground = SKSpriteNode(imageNamed: "ButtonBackgroundSelected")
let femaleLabel = SKLabelNode(text: "Female")
let maleLabel = SKLabelNode(text: "Male")
let genderLabel = SKLabelNode(text: "Gender:")
let nameLabel = SKLabelNode(text: "Name:")
let nameEntry = UITextField(frame: CGRect(origin: CGPoint(x: 800, y: 875), size: CGSize(width: 600, height: 200)))
enum genders {
case Male
case Female
}
var genderSelected : genders = .Male
override func didMove(to view: SKView) {
let background = SKSpriteNode(imageNamed: "Background")
background.size = CGSize(width: self.size.width, height: self.size.height)
background.position = CGPoint(x: self.size.width/2, y: self.size.height/2)
background.zPosition = 0
scene?.addChild(background)
Timer.scheduledTimer(timeInterval: 1.5, target: self, selector: #selector(beginTutorial), userInfo: nil, repeats: false)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch : AnyObject in touches{
let pointOfTouch = touch.location(in: self)
if(maleButtonBackground.contains(pointOfTouch) || femaleButtonBackground.contains(pointOfTouch)){
if(maleButtonBackground.contains(pointOfTouch)){
swapGenderButton(ImageToSwap: maleButtonBackground)
genderSelected = .Male
}
if(femaleButtonBackground.contains(pointOfTouch)){
swapGenderButton(ImageToSwap: femaleButtonBackground)
genderSelected = .Female
}
}else{
chatBoxInt += 1
let fadeInAnimation = SKAction.fadeIn(withDuration: 1)
let fadeOutAnimation = SKAction.fadeOut(withDuration: 1)
let removeAction = SKAction.removeFromParent()
let fadeOutSequence = SKAction.sequence([fadeOutAnimation, removeAction])
switch chatBoxInt {
case 1:
ChangeText(LabelText: "Best Life is the place for you to make the life you've always dreamed of!")
case 2:
ChangeText(LabelText: "In Best Life you can be who you want to be, do what you want to do... as long as you don't die.")
case 3:
ChangeText(LabelText: "The goal of Best Life is simple, make the best life possible for yourself before your time runs out.")
case 4:
ChangeText(LabelText: "Let's go over some of the basics of best life:")
case 5:
ChangeText(LabelText: "When you get into your new life, you'll have a HUD at the bottom of your screen at all times. This HUD will act as your guide as you go throughout Best Life. Let's get familiar with the icons and what they mean.")
case 6:
ChangeText(LabelText: "This is your happiness indicator, it will show you how much you are enjoying your life, try and keep this high at all times... too little happiness can result in your alter ego committing suicide.")
HappinessIcon.position = CGPoint(x: self.size.width/2, y: (self.size.height/2) - 100)
HappinessIcon.size = CGSize(width: 200, height: 200)
HappinessIcon.zPosition = 2
HappinessIcon.alpha = 0
self.addChild(HappinessIcon)
HappinessIcon.run(fadeInAnimation)
case 7:
HappinessIcon.run(fadeOutSequence)
ChangeText(LabelText: "This is your intelligence indicator... it shows... well.... intelligence... what else would it do...? Too little of this and you may not get good jobs; leading to lower income.")
IntelligenceIcon.position = CGPoint(x: self.size.width/2, y: (self.size.height/2) - 100)
IntelligenceIcon.size = CGSize(width: 200, height: 200)
IntelligenceIcon.zPosition = 2
IntelligenceIcon.alpha = 0
self.addChild(IntelligenceIcon)
IntelligenceIcon.run(fadeInAnimation)
case 8:
IntelligenceIcon.run(fadeOutSequence)
ChangeText(LabelText: "This is your health icon. It shows your current overall health level. If this gets too low it can contribute to life threatning illnesses which can ultimately lead to your demise.")
HealthIcon.position = CGPoint(x: self.size.width/2, y: (self.size.height/2) - 100)
HealthIcon.size = CGSize(width: 200, height: 200)
HealthIcon.zPosition = 2
HealthIcon.alpha = 0
self.addChild(HealthIcon)
HealthIcon.run(fadeInAnimation)
case 9:
HealthIcon.run(fadeOutSequence)
ChangeText(LabelText: "The last thing that you need to know is that one day in your world will pass one year in your alter ego's life. Make sure you are making the most out of every day to make the best life possible for your alter ego!")
case 10:
ChangeText(LabelText: "Alright, that pretty much sums it up. The world is yours for the taking, go seize it! Have fun in your new Best Life!")
case 11:
ChangeText(LabelText: "First, we'll need to we'll need to set up your new alter ego. Some of these options can only be accessed by purchasing them, but since this is your first go around I'll cover this one for you.")
chatBoxLabel.run(SKAction.moveTo(y: 1750, duration: 1))
let changeSize = SKAction.scale(to: CGSize(width: 800, height: 1600), duration: 1)
chatBox.run(changeSize)
arrow.run(fadeOutSequence)
genderLabel.position = CGPoint(x: 500, y: 1300)
genderLabel.fontColor = .black
genderLabel.zPosition = 2
genderLabel.fontSize = 45
genderLabel.alpha = 0
self.addChild(genderLabel)
genderLabel.run(fadeInAnimation)
maleButtonBackground.position = CGPoint(x: 575, y: 1175)
maleButtonBackground.zPosition = 2
maleButtonBackground.setScale(0.3)
maleButtonBackground.alpha = 0
self.addChild(maleButtonBackground)
maleButtonBackground.run(fadeInAnimation)
femaleButtonBackground.position = CGPoint(x: 955, y: 1175)
femaleButtonBackground.zPosition = 2
femaleButtonBackground.setScale(0.3)
femaleButtonBackground.alpha = 0
self.addChild(femaleButtonBackground)
femaleButtonBackground.run(fadeInAnimation)
maleLabel.fontSize = 45
maleLabel.fontColor = .white
maleLabel.zPosition = 4
maleLabel.alpha = 0
maleLabel.position = CGPoint(x: 575, y: 1162)
self.addChild(maleLabel)
maleLabel.run(fadeInAnimation)
femaleLabel.fontSize = 45
femaleLabel.fontColor = .white
femaleLabel.zPosition = 4
femaleLabel.alpha = 0
femaleLabel.position = CGPoint(x: 955, y: 1162)
self.addChild(femaleLabel)
femaleLabel.run(fadeInAnimation)
nameLabel.position = CGPoint(x: 500, y: 1000)
nameLabel.fontSize = 45
nameLabel.fontColor = .black
nameLabel.alpha = 0
nameLabel.zPosition = 3
self.addChild(nameLabel)
nameLabel.run(fadeInAnimation)
nameEntry.backgroundColor = .white
self.view?.addSubview(nameEntry)
default:
return
}
}
}
}
SpriteKit's SKSKScene exists inside of UIKit's SKView. ie your whole Spritekit scene is basically the backing layer for a single view in your app. If you want to do things in the UIKit layer it makes more sense to do them at the level of your view controller and delegate the responsibility back. So, for instance if you want a textField you can set it up in interface builder and make it hidden, then show it only when your scene requests it.
If you don't want to do it this way, you can still reach up into your view as you are doing and add subviews from your SKScene (its bad OOP to reach up into and alter your parent, but nothing in UIKit prevents you from doing it). Most likely the issue you are having is that the coordinate systems in SpriteKit and UIKit are different and they are actually upside down. So you cannot just use your SKScene coordinates for UIViews. You need to convert. SKView has a family of methods that will do the math for you: SKView.convert(:to:) SkView.convert(:from:). Note that that only gets you as far as your SKView. You then need to convert to your viewController.view coordinates using UIKit's convert methods.
I think its far less confusing if you leave the UIKit stuff in the UIKit layer and just have the viewController and scene pass the relevant information back and forth.
EDIT: here is an example
import PlaygroundSupport
import SpriteKit
class GameScene: SKScene {
private lazy var textField: UITextField = {
let textField = UITextField()
textField.frame.size = CGSize(width: 100, height: 30)
textField.backgroundColor = .cyan
return textField
}()
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let view = view,
let point = touches.first?.location(in: self) else {
return
}
if textField.superview != nil {
textField.removeFromSuperview()
}
textField.center = view.convert(point, from: self)
view.addSubview(textField)
textField.becomeFirstResponder()
}
}
let sceneView = SKView(frame: CGRect(x:0 , y:0, width: 640, height: 480))
if let scene = GameScene(fileNamed: "GameScene") {
scene.scaleMode = .aspectFill
sceneView.presentScene(scene)
}
PlaygroundSupport.PlaygroundPage.current.liveView = sceneView

Saving and outputting a high score

Following a tutorial and have the following code displaying the current coin count. How do I save a new high score so I can output for display?
I can work out how/where to output/display it - just need to work out how to save it first.
import SpriteKit
class HUD: SKNode {
var textureAtlas = SKTextureAtlas(named:"HUD")
var coinAtlas = SKTextureAtlas(named: "Environment")
// An array to keep track of the hearts:
var heartNodes:[SKSpriteNode] = []
// An SKLableNode to print the coin score:
let coinCountText = SKLabelNode(text: "000000")
let restartButton = SKSpriteNode()
let menuButton = SKSpriteNode()
func createHudNodes(screenSize:CGSize) {
let cameraOrigin = CGPoint(x: screenSize.width / 2,
y: screenSize.height / 2)
// Create the coin counter:
// First, create and position a bronze coin icon:
let coinIcon = SKSpriteNode(texture: coinAtlas.textureNamed("coin-bronze"))
// Size and position the coin icon:
let coinPosition = CGPoint(x: -cameraOrigin.x + 23, y: cameraOrigin.y - 23)
coinIcon.size = CGSize(width: 26, height: 26)
coinIcon.position = coinPosition
// Configure the coin text label:
coinCountText.fontName = "AvenirNext-HeavyItalic"
let coinTextPosition = CGPoint(x: -cameraOrigin.x + 41, y: coinPosition.y)
coinCountText.position = coinTextPosition
// These two properties allow you to align the
// text relative to the SKLabelNode's position:
coinCountText.horizontalAlignmentMode = SKLabelHorizontalAlignmentMode.left
coinCountText.verticalAlignmentMode = SKLabelVerticalAlignmentMode.center
// Add the text label and coin icon to the HUD:
self.addChild(coinCountText)
self.addChild(coinIcon)
}
func setCoinCountDisplay(newCoinCount:Int) {
// We can use the NSNumberFormatter class to pad
// leading 0's onto the coin count:
let formatter = NumberFormatter()
let number = NSNumber(value: newCoinCount)
formatter.minimumIntegerDigits = 6
if let coinStr = formatter.string(from: number) {
// Update the label node with the new count:
coinCountText.text = coinStr
}
}
}
You could use the help of UserDefaults for storing small amount of data.
UserDefaults.standard.set(00, forKey: "HighScore")

fading video over swift spritkit scene

I am porting a showroom application from as3/starling to a native swift iPad app.
I have 2 questions:
How can I fade a video over my spritekit content (from alpha 0 to 1).
How to control the iPad volume with an individual UI element without showing the iOS onscreen-volume notification graphic.
My first answer answers your initial question, and this answer is tailored to the subsequent questions that were posted in the comments. I feel that both answers are useful to the community, instead of combining into just one which I think would be confusing.
Here I implement a volume slider and a scrubbing slider that controls the VideoNode's AVPlayer object.. you get the full power of the AVPlayer but with the convenience of the SKVideoNode. Also, the volume for playing video is controlled without having to adjust the system volume and subsequent gray sound box pop-up:
import SpriteKit
import MediaPlayer
class GameScene: SKScene {
let seekSlider = UISlider(frame: CGRect(x: 0, y: 0, width: 200, height: 15))
let volSlider = UISlider(frame: CGRect(x: 0, y: 0, width: 200, height: 15))
// Because it takes time for us to load the video, we don't know it's duration,
// hence we don't know what the `.maximumValue` should be for the seekSlider
let defaultMax = Float(0.123456789)
var player: AVPlayer!
var videoNode: SKVideoNode!
func seekSliderChangedValue(sender: UISlider) {
// Wait for file to finish loading so we can get accurate end duration:
if sender.maximumValue == defaultMax {
if CMTimeGetSeconds(player.currentItem!.duration).isNaN { return }
else { sender.maximumValue = Float(CMTimeGetSeconds(player.currentItem!.duration)) }
}
let value = CMTimeMake(Int64(seekSlider.value), 1)
player.seek(to: value)
player.play()
}
func volSliderChangedValue(sender: UISlider) {
player.volume = sender.value
}
override func didMove(to view: SKView) {
removeAllChildren() // Delete this in your actual project.
// Add some labels:
let seekLabel = SKLabelNode(text: "Seek")
seekLabel.setScale(3)
seekLabel.verticalAlignmentMode = .center
seekLabel.position = CGPoint(x: frame.minX + seekLabel.frame.width/2,
y: frame.maxY - seekLabel.frame.height)
let volLabel = SKLabelNode(text: "Volume")
volLabel.setScale(3)
volLabel.verticalAlignmentMode = .center
volLabel.position = CGPoint(x: frame.minX + volLabel.frame.width/2,
y: frame.minY + volLabel.frame.height + volSlider.frame.height)
// Make player and node:
let url = Bundle.main.url(forResource: "sample", withExtension: "mp4")!
player = AVPlayer(url: url)
videoNode = SKVideoNode(avPlayer: player!)
//Configure seek slider:
seekSlider.addTarget(self, action: #selector(seekSliderChangedValue), for: UIControlEvents.valueChanged)
seekSlider.maximumValue = defaultMax
let seekOrigin = convertPoint(toView: CGPoint(x: seekLabel.frame.minX, y: seekLabel.frame.minY))
seekSlider.frame = CGRect(origin: seekOrigin, size: CGSize(width: 200, height: 15))
//Configure vol slider:
volSlider.addTarget(self, action: #selector(volSliderChangedValue), for: UIControlEvents.valueChanged)
volSlider.value = 1
let volOrigin = convertPoint(toView: CGPoint(x: volLabel.frame.minX, y: volLabel.frame.minY))
volSlider.frame = CGRect(origin: volOrigin, size: CGSize(width: 200, height: 15))
//Scene stuff:
view.addSubview(seekSlider)
view.addSubview(volSlider)
addChild(seekLabel)
addChild(volLabel)
addChild(videoNode)
// Start video and animation:
videoNode.alpha = 0
videoNode.play()
videoNode.run(.fadeIn(withDuration: 5))
}
}

Resources