FPS issues with adding a blur using SKEffectNode - swift

If there a better way of creating a blur effect? It seems like the way I am currently doing it creates FPS issues especially on older phones. It seems like the higher the blurAmount the lower the FPS. Could the blendMode be the reason here?
if effectsNode.parent == nil {
let filter = CIFilter(name: "CIGaussianBlur")
let blurAmount = 15.0
filter!.setValue(blurAmount, forKey: kCIInputRadiusKey)
effectsNode.filter = filter
effectsNode.blendMode = .add
sceneContent.removeFromParent()
effectsNode.addChild(sceneContent)
addChild(effectsNode)
}
When I pause my game, I call blurScreen() which does the following code above. However, it seems like my fps drops over time the longer the game is paused. I tried taking blurScreen() out and the FPS issues went away. How is the FPS dropping over time when blurScreen() is only called once?
EDIT:
func pauseGame() {
sceneContent.isPaused = true
intermission = true
physicsWorld.speed = 0
blurScreen()
}
Here is the code in touchesEnded()
// Tapped pause or pause menu options
if name == "pause" && touch.tapCount == 1 && pauseSprite.alpha == 1.0 && ((!sceneContent.isPaused && !GameData.shared.midNewDay) || (!sceneContent.isPaused && sceneElements[0].editingMode)) {
SKTAudio.sharedInstance.pauseBackgroundMusic()
SKTAudio.sharedInstance.playSoundEffect("Sounds/pause.wav")
pauseSprite.run(SKAction.sequence([SKAction.scale(to: 1.2, duration: 0.10), SKAction.scale(to: 1.0, duration: 0.10)])) { [unowned self] in
self.createPauseMenu()
self.pauseGame()
}
return
}
Update method
override func update(_ currentTime: TimeInterval) {
if GameData.shared.firstTimePlaying && GameData.shared.distanceMoved > 600 && !step1Complete {
tutorial2()
}
// Check for game over
if GameData.shared.hearts == 0 && !gameEnded {
gameOver()
}
// If we're in intermission, do nothing
if intermission || sceneContent.isPaused {
return
}
// some more stuff unrelated to pausing
}

You are running an effect node on the entire scene, that scene is going to be rendering that effect every frame which is going to put a lot of work on your system. If you do not have any animations going on behind it, I would recommend
converting your effect node to a sprite node by doing this
var spriteScene : SKSpriteNode!
func blurScreen() {
DispatchQueue.global(qos: .background).async {
[weak self] in
guard let strongSelf = self else { return }
let effectsNode = SKEffectNode()
let filter = CIFilter(name: "CIGaussianBlur")
let blurAmount = 10.0
filter!.setValue(blurAmount, forKey: kCIInputRadiusKey)
effectsNode.filter = filter
effectsNode.blendMode = .add
strongSelf.sceneContent.removeFromParent()
effectsNode.addChild(strongSelf.sceneContent)
let texture = self.view!.texture(from: effectsNode)
strongSelf.spriteScene = SKSpriteNode(texture: texture)
strongSelf.spriteScene.anchorPoint = CGPoint(x: 0.5, y: 0.5)
DispatchQueue.main.async {
strongSelf.sceneContent.removeFromParent()
strongSelf.addChild(strongSelf.spriteScene)
}
}
}

Related

Swift - SCNAnimationPlayer setting duration cancels out timeOffset

I have an animation that I'm trying to start & end at specific places. I can set the start by setting the animationPlayer.animation.timeOffset, I'm also trying to set the animation to end about 20s after the timeOffset & I can do that by setting animationPlayer.animation.duration.
The problem that I'm facing is that setting the duration cancels out the timeOffset. If I use just .timeOffset I can get the animation to start from any position but as soon as duration is set the animation will play from the beginning.
The intended result would be this: The animation starts at 25s (timeOffset) runs for 20s (duration) & then loops back to the timeOffset.
let rootNode = sceneView.rootNode
rootNode.enumerateChildNodes { child, _ in
guard let animationPlayer = child.animationPlayer(forKey: key) else { return }
animationPlayer.animation.timeOffset = 25
animationPlayer.animation.duration = 20
animationPlayer.animation.autoreverses = true
animationPlayer.animation.isRemovedOnCompletion = false
}
The best solution I have found is something like this:
let player = model.animationPlayer(forKey: "all")
let animation = player?.animation
func restartAnimation(atTimeOffset timeOffset: TimeInterval, duration: TimeInterval) {
animation?.timeOffset = timeOffset
if isWalking {
player?.play()
let uuid = isWalkingUUID
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
guard uuid == self.isWalkingUUID else { return }
player?.stop(withBlendOutDuration: 0.2)
restartAnimation(atTimeOffset: timeOffset, duration: duration)
}
} else {
player?.stop(withBlendOutDuration: 0.2)
}
}
restartAnimation(atTimeOffset: 33, duration: 0.6)

Dispatch queue to animate SCNNode in ARKit

I'm facing an issue when trying to periodically animate my nodes on an ARSession. I'm fetching data from Internet every 5 seconds and then with that data I update this nodes (shrink or enlarge).
My code looks something like this:
Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { timer in
fetchDataFromServer() {
let fetchedData = $0
DispatchQueue.main.async {
node1.update(fetchedData)
node2.update(fetchedData)
node3.update(fetchedData)
}
if stopCondition { timer.invalidate() }
}
}
Problem is that when calling the updates I'm seeing a glitch in which the camera seems to freeze for a fraction of second and I see the following message in the console: [Technique] World tracking performance is being affected by resource constraints [1]
Update happens correctly, but the UX is really clumpsy if every 5 seconds I get these "short freezes"
I've tried creating a concurrent queue too:
let animationQueue = DispatchQueue(label: "animationQueue", attributes: DispatchQueue.Attributes.concurrent)
and call animationQueue.async instead of main queue but problem persists.
I'd appreciate any suggestions.
EDIT: Each of the subnodes on it's update method looks like this
private func growingGeometryAnimation(newHeight height: Float) -> CAAnimation{
// Change height
let grow = CABasicAnimation(keyPath: "geometry.height")
grow.toValue = height
grow.fromValue = prevValue
// .... and the position
let move = CABasicAnimation(keyPath: "position.y")
let newPosition = getNewPosition(height: height)
move.toValue = newPosition.y + (yOffset ?? 0)
let growGroup = CAAnimationGroup()
growGroup.animations = [grow, move]
growGroup.duration = 0.5
growGroup.beginTime = CACurrentMediaTime()
growGroup.timingFunction = CAMediaTimingFunction(
name: kCAMediaTimingFunctionEaseInEaseOut)
growGroup.fillMode = kCAFillModeForwards
growGroup.isRemovedOnCompletion = false
growGroup.delegate = self
return growGroup
}
self.addAnimation(growingGeometryAnimation(newHeight: self.value), forKey: "bar_grow_animation")
To make any updates to the scene use SCNTransaction, it makes sure all of the changes are made on the appropriate thread.
Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { timer in
fetchDataFromServer() {
let fetchedData = $0
SCNTransaction.begin()
node1.update(fetchedData)
node2.update(fetchedData)
node3.update(fetchedData)
SCNTransaction.commit()
if stopCondition { timer.invalidate() }
}
}

How to get current texture from current frame of animation in SKAction?

I'm trying to animate something that spins / left to right, but whenever I call
spinLeft() or spinRight() then the animation always starts from frame 0.
In other words, I want to be able to say spin something 4 out of 10 frames, stop, then
spin in the opposite direction, FROM frame 4. Right now, it resets to frame 0.
var textures = [SKTexture]() // Loaded with 10 images later on.
var sprite = SKSpriteNode()
func spinLeft() {
let action = SKAction.repeatForever(.animate(with: textures, timePerFrame: 0.1))
sprite.run(action)
}
func spinRight() {
let action = SKAction.repeatForever(.animate(with: textures, timePerFrame: 0.1)).reversed()
sprite.run(action)
}
You could do this (Syntax may be a little off, but you get the point):
The key here is the .index(of: ... ) which will get you the index.
func spinUp() {
let index = textures.index(of: sprite.texture)
if index == textures.count - 1 {
sprite.texture = textures[0]
}
else {
sprite.texture = textures[index + 1]
}
}
func spinDown() {
let index = textures.index(of: sprite.texture)
if index == 0 {
sprite.texture = textures[textures.count - 1]
}
else {
sprite.texture = textures[index - 1]
}
}
func changeImage(_ isUp: Bool, _ amountOfTime: CGFloat) {
let wait = SKAction.wait(duration: amountOfTime)
if isUp {
run(wait) {
self.imageUp()
}
}
else {
run(wait) {
self.imageDown()
}
}
}
If you use something like a swipe gesture recognizer, you can use it's direction to set the isUp Bool value and the velocity of that swipe for the amountOfTime for the changeImage function.
The changeImage will only change the image once, so you will need to handle this somewhere else, or create another function if you want it to continuously spin or die off eventually.
Hope this helps!

Inconsistent contact detection in Swift 3 using SpriteKit

I'm having an issue with contact detection in Swift 3 using SpriteKit. The contact detection is working...sometimes. It seems purely random as to when it fires and when it doesn't.
I have a yellow "bullet" that moves up on the screen to hit a red sprite named targetSprite. The desired behavior is to have the bullet removed when it hits the target, but sometimes it just passes through underneath.
I've found many questions about contact detection not working at all, but I haven't found any dealing with inconsistent detection.
What can I do to fix this?
Here's the code:
import SpriteKit
import GameplayKit
enum PhysicsCategory:UInt32 {
case bullet = 1
case sprite1 = 2
case targetSprite = 4
// each new value should double the previous
}
class GameScene: SKScene, SKPhysicsContactDelegate {
// Create sprites
let sprite1 = SKSpriteNode(color: SKColor.blue, size: CGSize(width:100,height:100))
let targetSprite = SKSpriteNode(color: SKColor.red, size: CGSize(width:100,height:100))
let bullet = SKSpriteNode(color: SKColor.yellow, size: CGSize(width: 20, height: 20))
// show the bullet?
var isShowingBullet = true
// Timers
//var timer:Timer? = nil
var fireBulletTimer:Timer? = nil
// set up bullet removal:
var bulletShouldBeRemoved = false
let bulletMask = PhysicsCategory.bullet.rawValue
override func didMove(to view: SKView) {
// Physics
targetSprite.physicsBody = SKPhysicsBody(rectangleOf: targetSprite.centerRect.size)
targetSprite.physicsBody?.affectedByGravity = false
bullet.physicsBody = SKPhysicsBody(rectangleOf: bullet.centerRect.size)
bullet.physicsBody?.affectedByGravity = false
// Contact Detection:
targetSprite.physicsBody?.categoryBitMask = PhysicsCategory.targetSprite.rawValue
targetSprite.physicsBody?.contactTestBitMask =
//PhysicsCategory.sprite1.rawValue |
PhysicsCategory.bullet.rawValue
targetSprite.physicsBody?.collisionBitMask = 0 // no collision detection
// bullet physics
bullet.physicsBody?.categoryBitMask = PhysicsCategory.bullet.rawValue
bullet.physicsBody?.contactTestBitMask =
PhysicsCategory.targetSprite.rawValue
bullet.physicsBody?.collisionBitMask = 0 // no collision detection
// execute once:
fireBulletTimer = Timer.scheduledTimer(timeInterval: 1,
target: self,
selector: #selector(self.fireBullet),
userInfo: nil,
repeats: false)
// Add sprites to the scene:
self.addChild(sprite1)
self.addChild(bullet)
self.addChild(targetSprite)
// Positioning
targetSprite.position = CGPoint(x:0, y:300)
// Note: bullet and sprite1 are at 0,0 by default
// Delegate
self.physicsWorld.contactDelegate = self
}
func didBegin(_ contact: SKPhysicsContact) {
print("didBegin(contact:))")
//let firstBody:SKPhysicsBody
// let otherBody:SKPhysicsBody
// Use 'bitwise and' to see if both bits are 1:
if contact.bodyA.categoryBitMask & bulletMask > 0 {
//firstBody = contact.bodyA
//otherBody = contact.bodyB
print("if contact.bodyA....")
bulletShouldBeRemoved = true
}
else {
//firstBody = contact.bodyB
//otherBody = contact.bodyA
print("else - if not contacted?")
}
/*
// Find the type of contact:
switch otherBody.categoryBitMask {
case PhysicsCategory.targetSprite.rawValue: print(" targetSprite hit")
case PhysicsCategory.sprite1.rawValue: print(" sprite1 hit")
case PhysicsCategory.bullet.rawValue: print(" bullet hit")
default: print(" Contact with no game logic")
}
*/
} // end didBegin()
func didEnd(_ contact: SKPhysicsContact) {
print("didEnd()")
}
func fireBullet() {
let fireBulletAction = SKAction.move(to: CGPoint(x:0,y:500), duration: 1)
bullet.run(fireBulletAction)
}
func showBullet() {
// Toggle to display or not, every 1 second:
if isShowingBullet == true {
// remove (hide) it:
bullet.removeFromParent()
// set up the toggle for the next call:
isShowingBullet = false
// debug:
print("if")
}
else {
// show it again:
self.addChild(bullet)
// set up the toggle for the next call:
isShowingBullet = true
// debug:
print("else")
}
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
if bulletShouldBeRemoved {
bullet.removeFromParent()
}
}
}
Sorry for the inconsistent indentation, I can't seem to find an easy way to do this...
EDIT:
I have found that using 'frame' instead of 'centerRect' makes the collision area the size of the sprite. For example:
targetSprite.physicsBody = SKPhysicsBody(rectangleOf: targetSprite.centerRect.size)
should be:
targetSprite.physicsBody = SKPhysicsBody(rectangleOf: targetSprite.frame.size)
First advice - Do not use NSTimer (aka Timer) in SpriteKit. It is not paired with a game loop and can cause different issues in a different situations. Read more here ( answer posted by LearnCocos2D)
So, do this:
let wait = SKAction.wait(forDuration: 1)
run(wait, completion: {
[unowned self] in
self.fireBullet()
})
What I have noticed is that if I run your code in Simulator, I get the behaviour you have described. didBegin(contact:) is being fired randomly. Still, this is not happening on a device for me, and device testing is what matters.
Now, when I have removed Timer and did the same thing with SKAction(s) everything worked, means contact were detected every time.
Have you tried adding
.physicsBody?.isDynamic = true
.physicsBody?.usesPreciseCollisionDetrction =true
SpriteKit physics engine will calculate collision correctly if you do following:
1) set "usesPreciseCollisionDetection" property to true for bullet's physics body. This will change collision detection algorithm for this body. You can found more information about this property here, chapter "Working with Collisions and Contacts".
2) move your bullet using applyImpulse or applyForce methods. Collision detection will not woking correctly if you move body by changing it's position manually. You can find more information here, chapter "Making Physics Bodies Move".

How to pause an SKSpriteNode, Swift

I created this game using sprite kit. During the game sprite nodes are moving. When the "Game Over" Label pops up, I would like the monster sprite node to stop moving or pause, but the rest of the scene to still move on. I know where to put the code, I just don't know how to write it. This is my monster code.
func addMonster() {
// Create sprite
let monster = SKSpriteNode(imageNamed: "box")
monster.setScale(0.6)
monster.physicsBody = SKPhysicsBody(rectangleOfSize: monster.size)
monster.physicsBody?.dynamic = true
monster.physicsBody?.categoryBitMask = UInt32(monsterCategory)
monster.physicsBody?.contactTestBitMask = UInt32(laserCategory)
monster.physicsBody?.collisionBitMask = 0
monster.name = "box"
var random : CGFloat = CGFloat(arc4random_uniform(320))
monster.position = CGPointMake(random, self.frame.size.height + 20)
self.addChild(monster)
}
EDIT
override func update(currentTime: CFTimeInterval) {
if isStarted == true {
if currentTime - self.lastMonsterAdded > 1 {
self.lastMonsterAdded = currentTime + 3.0
self.addMonster()
}
self.moveObstacle()
} else {
}
self.moveBackground()
if isGameOver == true {
}
}
If I understand your question correctly, you want to pause a single (or small number of) nodes. In that case...
monster.paused = true
Is probably what you want.