SpriteKit Fast Movement Frame Rate Drop - ios

I've been working on a game involving fast movement in SpriteKit. I've tested and profiled the game removing multiple sections of the game and haven't been able to hold a steady 60 fps. The frame rate drop causes node movement to stutter. I decided to set up a simple program to test. With only 4 nodes on screen moving from left to right there is still a drop in frames running on iPhone 7 Plus. Is there any way to optimize fast movement in SpriteKit to hold a steady frame rate?
import SpriteKit
import GameplayKit
class GameScene: SKScene {
var ship = SKSpriteNode()
var ship2 = SKSpriteNode()
var ship3 = SKSpriteNode()
var ship4 = SKSpriteNode()
override func didMove(to view: SKView) {
ship.texture = SKTexture(imageNamed: "Spaceship")
ship.position = CGPoint(x: -300, y: 0)
ship.size = CGSize(width: 50, height: 50)
ship.setScale(1)
ship.physicsBody = SKPhysicsBody(rectangleOf: ship.size)
ship.physicsBody?.affectedByGravity = false
ship.zPosition = 2
self.addChild(ship)
ship2.texture = SKTexture(imageNamed: "Spaceship")
ship2.position = CGPoint(x: -200, y: -50)
ship2.size = CGSize(width: 50, height: 50)
ship2.setScale(1)
ship2.physicsBody = SKPhysicsBody(rectangleOf: ship.size)
ship2.physicsBody?.affectedByGravity = false
ship2.zPosition = 2
self.addChild(ship2)
ship3.texture = SKTexture(imageNamed: "Spaceship")
ship3.position = CGPoint(x: -100, y: 50)
ship3.size = CGSize(width: 50, height: 50)
ship3.setScale(1)
ship3.physicsBody = SKPhysicsBody(rectangleOf: ship.size)
ship3.physicsBody?.affectedByGravity = false
ship3.zPosition = 2
self.addChild(ship3)
ship4.texture = SKTexture(imageNamed: "Spaceship")
ship4.position = CGPoint(x: -0, y: 100)
ship4.size = CGSize(width: 50, height: 50)
ship4.setScale(1)
ship4.physicsBody = SKPhysicsBody(rectangleOf: ship.size)
ship4.physicsBody?.affectedByGravity = false
ship4.zPosition = 2
self.addChild(ship4)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
ship.position.x += 15
ship2.position.x += 15
ship3.position.x += 15
ship4.position.x += 15
if ship.position.x > 800 {
ship.position.x = -950
} else if ship2.position.x > 800 {
ship2.position.x = -900
} else if ship3.position.x > 800 {
ship3.position.x = -850
} else if ship4.position.x > 800 {
ship4.position.x = -800
}
}
}
Edit: When profiling the test program above the frame rate drops to 59 fps.

Related

why when ever I try to tap on my SKSpriteNode it is not interactive

I am trying to have my SKSpriteNode named "slots" be tapped on when ever a user is trying to hit a target in the middle of the sprite. But whenever I Tapp on the slot in does not blink or do anything really.
I was trying to have it blink and print("Tapped on shotSlotNode") before I'll go on to continue creating this game but im still struck on this part of the game.
var slots = [shotSlot]()
var targets = [shotSlot]()
var gameScore: SKLabelNode!
var slotsRed = [targetSlotRed]()
var shotSlotNode: shotSlot? // Define a property for shotSlotNode
override func didMove(to view: SKView) {
let backGround = SKSpriteNode(imageNamed: "background")
backGround.position = CGPoint(x: 512, y: 384)
backGround.blendMode = .replace
backGround.zPosition = -1
// line 20 makes sure things go on top of the background view.
backGround.scale(to: CGSizeMake(1024, 768))
// I used line 21 to strech the image out to fix my scrrens size because I know I set my screen size to 1024 in the GameScene UI
addChild(backGround)
gameScore = SKLabelNode(fontNamed: "Chalkduster")
gameScore.text = "Score: 0"
gameScore.position = CGPoint(x: 480, y: 70)
addChild(gameScore)
self.view?.isMultipleTouchEnabled = true
for i in 0 ..< 3 { createSlot(at: CGPoint(x: 120 + (i * 370), y: 560)) }
for i in 0 ..< 3 { createSlot(at: CGPoint(x: 120 + (i * 370), y: 370)) }
for i in 0 ..< 3 { createSlot(at: CGPoint(x: 120 + (i * 370), y: 200)) }
// this code breaks my slots into 3 rows and 3 cloumms of the shotSlots.
for i in 0 ..< 1 { createGreenTarget(at: CGPoint(x: 120 + (i * 370), y: 560)) }
for i in 0 ..< 1 { createRedTarget(at: CGPoint(x: 860 + (i * 370), y: 200)) }
}
func slotTapped(_ slot: shotSlot) {
// Make the slot sprite blink
print("Tapped on slot")
let blinkOut = SKAction.fadeAlpha(to: 0.2, duration: 0.15)
let blinkIn = SKAction.fadeAlpha(to: 1, duration: 0.15)
let blink = SKAction.sequence([blinkOut, blinkIn])
let blinkForever = SKAction.repeatForever(blink)
slot.sprite.run(blinkForever)
print("Started blinking")
// Handle the slot tap logic
if slot === shotSlotNode {
print("Tapped on shot slot node")
}
}
// now we need to make this greenTarget slide.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
for slot in slots {
if slot.contains(location) {
// The touch is inside the slot node
print("Tapped on shot slot")
slotTapped(slot)
// You can also check if the tapped slot is your `shotSlotNode`
if slot === shotSlotNode {
print("Tapped on shot slot node")
}
}
}
}
}
func createSlot(at position: CGPoint) {
let slot = shotSlot()
slot.configure(at: position)
slot.zPosition = 1
slot.isUserInteractionEnabled = true
addChild(slot)
targets.append(slot) // Add the slot to the targets array
if position == CGPoint(x: 512, y: 100) {
slot.sprite.name = "shotSlotNode"
slot.isUserInteractionEnabled = true
self.shotSlotNode = slot
print("Set shotSlotNode to sprite with name \(slot.sprite.name)")
}
}
class shotSlot: SKNode {
let sprite = SKSpriteNode(imageNamed: "slots")
func configure(at position: CGPoint) {
self.position = position
sprite.name = "shotSlotNode"
// sprite.position = CGPoint(x: frame.midX, y: frame.midY)
sprite.scale(to: CGSizeMake(220, 140))
sprite.isUserInteractionEnabled = true
sprite.zPosition = 13
addChild(sprite)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
if let node = self.childNode(withName: "shotSlotNode") {
let convertedLocation = node.convert(location, from: self)
if node.contains(convertedLocation) {
print("Tapped on shotSlotNode")
}
}
}
}
}

SpriteKit keep moving player in current direction while falling after touchesEnded

I'm making my own Mario Bros. replica for the first level to learn how to make games with iOS, with my own assets. So far I've managed to place three SKSpriteNodes for the controls (left, right, up), and my player node can move in those three directions, but if I make my player jump while running in either direction, as soon as I remove my finger from the "left control", the player loses all its momentum and falls right there (as if it hit a wall) instead of following the parabola.
I don't know what might be needed in this case to be an MRE, so this is basically the whole thing that can reproduce the issue, along with some attempts I've made to make it work.
Basically I tried to apply an impulse / set the velocity / change the position directly and this last one was the one with better results (yet it still makes the player node to fall as soon as I remove the finger from the direction controls).
Here's a video demonstrating the issue.
This is the GameScene
import SpriteKit
import GameplayKit
class GameScene: SKScene {
private var player = SKSpriteNode()
private var bg = SKSpriteNode()
private var leftArrow = SKSpriteNode()
private var rightArrow = SKSpriteNode()
private var upArrow = SKSpriteNode()
private var floor = [SKSpriteNode]()
private var isLeftTouched = false
private var isRightTouched = false
private var selectedNodes: [UITouch:SKSpriteNode] = [:]
override func didMove(to view: SKView) {
addBackground()
addFloor()
addPlayer(xOffset: 0, yOffset: 0)
addControls()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
//player.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 50))
let touch = touches.first! as UITouch
let positionInScene = touch.location(in: self)
let touchedNode = self.atPoint(positionInScene)
for touch in touches {
let location = touch.location(in:self)
if let node = self.atPoint(location) as? SKSpriteNode {
if let name = touchedNode.name {
selectedNodes[touch] = node
if name == "up" {
player.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 60))
} else if name == "left" {
isLeftTouched = true
} else if name == "right" {
isRightTouched = true
}
}
}
}
if let name = touchedNode.name {
if name == "up" {
player.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 60))
} else if name == "left" {
isLeftTouched = true
} else if name == "right" {
isRightTouched = true
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
//let direction = ((touches.first?.location(in: self).x)! < (touches.first?.previousLocation(in: self).x)!) ? Direction.LEFT : Direction.RIGHT
//runIn(direction: direction)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
if selectedNodes[touch] != nil {
if selectedNodes[touch]?.name == "left" {
isLeftTouched = false
} else if selectedNodes[touch]?.name == "right" {
isRightTouched = false
}
selectedNodes[touch] = nil
}
}
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
if isLeftTouched {
runIn(direction: Direction.LEFT)
}
if isRightTouched {
runIn(direction: Direction.RIGHT)
}
}
// MARK: INTERACTION METHODS
func runIn(direction: Direction) {
let x = player.position.x + (direction == Direction.RIGHT ? 5 : -5)
let position = CGPoint(x: x, y: player.position.y)
if position.x >= self.frame.maxX || position.x <= self.frame.minX {
return
}
player.position = position
//player.physicsBody?.velocity = CGVector(dx: direction == Direction.RIGHT ? 50 : -50, dy: 0)
//player.physicsBody?.applyImpulse(CGVector(dx: direction == Direction.RIGHT ? 5 : -5 , dy: 0))
}
// MARK: UI METHODS
func addBackground() {
let bgTexture = SKTexture(imageNamed: "bg")
bg = SKSpriteNode(texture: bgTexture)
bg.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
bg.size.height = self.frame.height
bg.zPosition = -10
self.addChild(bg)
}
func addPlayer(xOffset: CGFloat, yOffset: CGFloat) {
let playerTexture = SKTexture(imageNamed: "player")
player = SKSpriteNode(texture: playerTexture)
//let xPos = calculateXOffset(for: player, from: self.frame.midX, offset: xOffset)
//let yPos = calculateXOffset(for: player, from: self.frame.midY, offset: yOffset)
player.position = CGPoint(x: self.frame.midX,
y: self.frame.midY)
player.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: player.frame.width, height: player.frame.height))
player.physicsBody?.isDynamic = true
self.addChild(player)
}
func addFloor() {
let blockTexture = SKTexture(imageNamed: "block")
for i in 0 ... (Int) (self.frame.width / blockTexture.size().width) {
let blockNode = SKSpriteNode(texture: blockTexture)
blockNode.position = CGPoint(x: self.frame.minX + (blockNode.frame.width * CGFloat(i)),
y: self.frame.minY + blockNode.frame.height / 2)
blockNode.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: blockNode.frame.width, height: blockNode.frame.height))
blockNode.physicsBody?.isDynamic = false
floor.append(blockNode)
self.addChild(blockNode)
}
}
func addControls() {
addLeftArrow()
addRightArrow()
addUpArrow()
}
func addLeftArrow() {
let leftTexture = SKTexture(imageNamed: "left")
leftArrow = SKSpriteNode(texture: leftTexture)
leftArrow.name = "left"
leftArrow.position = CGPoint(x: calculateXOffset(for: leftArrow, from: self.frame.minX, offset: 50),
y: calculateXOffset(for: leftArrow, from: self.frame.minY, offset: 50))
self.addChild(leftArrow)
}
func addRightArrow() {
let rightTexture = SKTexture(imageNamed: "right")
rightArrow = SKSpriteNode(texture: rightTexture)
rightArrow.name = "right"
rightArrow.position = CGPoint(x: calculateXOffset(for: rightArrow, from: self.frame.minX, offset: 150),
y: calculateXOffset(for: rightArrow, from: self.frame.minY, offset: 50))
self.addChild(rightArrow)
}
func addUpArrow() {
let upTexture = SKTexture(imageNamed: "up")
upArrow = SKSpriteNode(texture: upTexture)
upArrow.name = "up"
upArrow.position = CGPoint(x: calculateXOffset(for: upArrow, from: self.frame.maxX, offset: -(125 + upTexture.size().width)),
y: calculateXOffset(for: upArrow, from: self.frame.minY, offset: 50))
self.addChild(upArrow)
}
// MARK: UTILITY FUNCTIONS
func calculateXOffset(for asset: SKSpriteNode, from coord: CGFloat, offset: CGFloat) -> CGFloat {
let width = asset.frame.width
return coord + offset + width;
}
func calculateYOffset(for asset: SKSpriteNode, from coord: CGFloat, offset: CGFloat) -> CGFloat {
let height = asset.frame.height
return coord + offset + height;
}
}
My Direction enum:
enum Direction {
case LEFT
case RIGHT
case UP
case DOWN
}
And the only change I made in GameViewController was this:
scene.scaleMode = .resizeFill
My GameScene.sks is 926 x 428, only supporting landscape. I also set the LaunchScreen to Main due to a bug in Xcode 12: Background is not filling the whole view SpriteKit
And these are all my assets:
Edit
I tried applying an impulse in my runIn method like this:
player.physicsBody?.applyImpulse(CGVector(dx: direction == Direction.RIGHT ? 2 : -2 , dy: 0))
This makes the player node move in the parabola but now from time to time it gets stuck and the only way to make it move is to make it jump until it happens again.
Here's a video demonstrating the issue again.
If I try to set the velocity instead, then I'm not able to jump while moving and it seems to glide when jumping and moving after.
I ended up following #JohnL suggestion in the comments above, to use an impulse as well to move my player node:
player.physicsBody?.applyImpulse(CGVector(dx: direction == Direction.RIGHT ? 2 : -2 , dy: 0))
The issue where the player node was stuck while moving was removed when changing the floor for a single asset rather than multiple blocks one next to each other.

TouchesBegan does not work

In the last label lblTryAgain I want to return to another class named GameScene but the tap action does not enter to the function touchesBegan.
I am just following a tutorial to learn how to create games with SpriteKit if someone want to follow the tutorial or see the entire code is available in https://www.raywenderlich.com/87231/make-game-like-mega-jump-sprite-kit-swift-part-1
import SpriteKit
class EndGameScene: SKScene {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(size: CGSize) {
super.init(size: size)
// Stars
let star = SKSpriteNode(imageNamed: "Star")
star.position = CGPoint(x: 25, y: self.size.height-30)
addChild(star)
let lblStars = SKLabelNode(fontNamed: "ChalkboardSE-Bold")
lblStars.fontSize = 30
lblStars.fontColor = SKColor.white
lblStars.position = CGPoint(x: 50, y: self.size.height-40)
lblStars.horizontalAlignmentMode = SKLabelHorizontalAlignmentMode.left
lblStars.text = String(format: "X %d", GameState.sharedInstance.stars)
addChild(lblStars)
// Score
let lblScore = SKLabelNode(fontNamed: "ChalkboardSE-Bold")
lblScore.fontSize = 60
lblScore.fontColor = SKColor.white
lblScore.position = CGPoint(x: self.size.width / 2, y: 300)
lblScore.horizontalAlignmentMode = SKLabelHorizontalAlignmentMode.center
lblScore.text = String(format: "%d", GameState.sharedInstance.score)
addChild(lblScore)
// High Score
let lblHighScore = SKLabelNode(fontNamed: "ChalkboardSE-Bold")
lblHighScore.fontSize = 30
lblHighScore.fontColor = SKColor.cyan
lblHighScore.position = CGPoint(x: self.size.width / 2, y: 150)
lblHighScore.horizontalAlignmentMode = SKLabelHorizontalAlignmentMode.center
lblHighScore.text = String(format: "High Score: %d", GameState.sharedInstance.highScore)
addChild(lblHighScore)
// Try again
let lblTryAgain = SKLabelNode(fontNamed: "ChalkboardSE-Bold")
lblTryAgain.fontSize = 30
lblTryAgain.fontColor = SKColor.white
lblTryAgain.position = CGPoint(x: self.size.width / 2, y: 50)
lblTryAgain.horizontalAlignmentMode = SKLabelHorizontalAlignmentMode.center
lblTryAgain.text = "Tap To Try Again"
lblTryAgain.isUserInteractionEnabled = true
addChild(lblTryAgain)
}
func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
// Transition back to the Game
let reveal = SKTransition.fade(withDuration: 0.5)
let gameScene = GameScene(size: self.size)
self.view!.presentScene(gameScene, transition: reveal)
}
}
touchesBegan is a override func which only works in self.viewand it will not work in other UIs
try this
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("working")
}
make sure isUserInteractionEnabled is checked and the print statement may not work. add or update sceneDidLoad and print isUserInteractionEnabled

Is there a way that the for loop can always check a value of variable outside this loop in swift?

I am trying to create nodes through for loop in sprite kit but I want every time the "for loop" wants to create a new node to check if a value outside the loop if it is changed( depending on this value the node created).
what I want to do is in CreatSquares function
Sorry for my bad English
import SpriteKit
import GameplayKit
var Score = 0
class GameScene: SKScene {
let SquareSide = 100
var touchedNode = SKNode()
var touchLocation = CGPoint()
let ScoreLabel = SKLabelNode()
override func didMove(to view: SKView) {
CreatSquares()
}
func CreatSquares(){
for _ in 0...100{
/*===========================================================================================
(((((((( I want here while creating nodes if the score is equal (let's say) to 20 ->>>> change in SquareSide or change anything in square's properties ))))))))
The problem is that the 100 square created together
================================================================================================
*/
let square = SKShapeNode(rect: CGRect(x: Int(arc4random_uniform(UInt32(self.frame.width))), y: Int(arc4random_uniform(UInt32(self.frame.height))), width: SquareSide, height: SquareSide))
if Score <= 10{
square.fillColor = UIColor.orange}
else{
square.fillColor = UIColor.blue
}
square.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: square.frame.width, height: square.frame.height), center: CGPoint(x: square.position.x, y: square.position.y))
square.physicsBody?.affectedByGravity = false
square.physicsBody?.allowsRotation = false
square.physicsBody?.categoryBitMask = 0
square.name = "square"
self.addChild(square)
}
}
func CreatScoreLabel(){
let ScoreLabel = SKLabelNode()
ScoreLabel.text = "Your Score is:\(Score)"
ScoreLabel.position = CGPoint(x: self.frame.width/2, y: self.frame.height - 50)
ScoreLabel.color = UIColor.white
self.addChild(ScoreLabel)
}
func updatScore(){
ScoreLabel.text = "Your Score is:\(Score)"
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
touchLocation = touch.location(in: self)
touchedNode = self.atPoint(touchLocation)
if touchedNode.name == "square"{
Score += 1
touchedNode.removeFromParent()
}
}
}
override func update(_ currentTime: TimeInterval) {
updatScore()
}
}

Swift SpriteKit change background image score based

i am developing a game using Swift and SpriteKit. I want that the background changes at a certain score. Here is the code:
class GameScene: SKScene {
var bg = SKSpriteNode()
override func didMoveToView(view: SKView) {
makeBg()
}
func makeBg() {
let bgTexture = SKTexture(imageNamed: "img/bg.png")
let moveBg = SKAction.moveByX(-bgTexture.size().width, y: 0, duration: 9)
let replaceBg = SKAction.moveByX(bgTexture.size().width, y: 0, duration:0)
let animateBg = SKAction.repeatActionForever(SKAction.sequence([moveBg, replaceBg]))
for var i: CGFloat = 0; i<3; i++ {
let bg = SKSpriteNode(texture: bgTexture)
bg.name = "background"
bg.position = CGPoint(x: bgTexture.size().width/2 + bgTexture.size().width * i, y: CGRectGetMidY(self.frame))
bg.size.height = self.frame.height
bg.runAction(animateBg)
addChild(bg)
}
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if score == 0 {
bg.texture = SKTexture(imageNamed: "img/bg.png")
} else if score == 3 {
bg.texture = SKTexture(imageNamed: "img/bgOri.png")
}
}
But image doesn't change...where is the mistake?
So this is how you can change a texture on all the nodes created inside your for loop:
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
makeBg()
}
func makeBg() {
let bgTexture = SKTexture(imageNamed: "img/bg.png")
let moveBg = SKAction.moveByX(-bgTexture.size().width, y: 0, duration: 9)
let replaceBg = SKAction.moveByX(bgTexture.size().width, y: 0, duration:0)
let animateBg = SKAction.repeatActionForever(SKAction.sequence([moveBg, replaceBg]))
for var i: CGFloat = 0; i<3; i++ {
let bg = SKSpriteNode(texture: bgTexture)
bg.name = "background"
bg.position = CGPoint(x: bgTexture.size().width/2 + bgTexture.size().width * i, y: CGRectGetMidY(self.frame))
bg.size.height = self.frame.height
bg.runAction(animateBg)
addChild(bg)
}
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
enumerateChildNodesWithName("background", usingBlock: { node, stop in
//Make a check based on score and
(node as? SKSpriteNode)?.texture = //set the right texture
})
}
}
Note that you don't need bg property defined in the GameScene to accomplish this.

Resources