1

I'm using SpriteKit for a 2D game and have an array of images for a simple animation.

  1. First, I'm creating an array with SKTextures to keep references to those images.
var hyperLeapArray: [SKTexture] = []
for i in 0 ..< 90 {
  hyperLeapArray.append(SKTexture(imageNamed: "hyper_leap_\(i)"))
}
  1. Then I create an animation action.
let animation = SKA.animate(with: hyperLeapArray, timePerFrame: 0.03)
  1. Finally, I run this action on my SKScene's node to play it on the screen.
self.animationHost.run(SKAction.repeatForever(animation)) // Plays the animaion on a loop.

The first time this is executed - FPS drops to ~20 and then smoothly stabilizes up to 60. To avoid this, I've decided to use SKTexture.preload method before playing the animation.

 Task.init {
    await SKTexture.preload(hyperLeapArray)
 }

And, on a simulator it worked fine, FPS are still dropping, but for acceptable > ~35 rate. But this solution caused another issue with memory on a real device. After investigating, I realized that calling SKTexture.preload - fills the memory up to ~1.5GB - which is enormous and keeps that memory in use before the animation is cashed. Original 90 images weight only 12Mb, I have no idea how this is growing to the enormous amount of space.

Space usage


What I've tried:

  1. To preload textures one by one, e.g.
   func preload(index: Int, array: [SKTexture]) {
     if (index < array.count) {
       array[index].preload {
         print("Preloaded \(index)")
         sleep(1)
         preload(index: index + 1, array: array)
       }
     } else {
       print("Done")
     }
   }
  1. To initialize UIImages first and then build SKTexutres(image: UIImage)
  2. Run animations in background without preloading for SpriteKit to cache that before actual use
  3. Used SKTextureAtlas - but it made it even worse, up to 4Gb of memory usage

But nothing helped so far.

Any ideas on

  1. Why does this happen?
  2. How to keep FPS on a high level at any time?
  3. Why SKTexture.preload reservice so much space in memory?

Here is full code snippet if you'd like to reproduce it locally:

final class TempScene: SKScene {
  private var animationHost: SKSpriteNode!
  private var hyperLeapAnimation: Animation!

  convenience init(test: Bool) {
    self.init(size: FULL_SCREEN_SIZE)
    let center = CGPoint(x: frame.width / 2, y: frame.height / 2)

    animationHost = SKSpriteNode(color: .clear, size: FULL_SCREEN_SIZE)
    animationHost.position = center
    animationHost.zPosition = 110
    addChild(animationHost)

    var hyperLeapTextures: [SKTexture] = []
    for i in 0 ..< 90 {
      // You can use any texture instead.
      hyperLeapTextures.append(SKTexture(imageNamed: "hyper_leap_\(i)"))
    }
    Task.init {
      await SKTexture.preload(hyperLeapTextures)
    }
    let animation = SKAction.animate(with: hyperLeapTextures, timePerFrame: 0.03)
    animationHost.run(SKAction.sequence([SKAction.wait(forDuration: 6),
                                         SKAction.repeatForever(animation)]))

  }
}
  • What are the images like (dimension, color depth, ...)? – bg2b Jul 07 '22 at 11:59
  • Kind: PNG image, Size: 117,239 bytes (119 KB on disk), Dimensions: 1536 × 2048, Color space: RGB, Alpha channel: NO, – Goga Tirkiya Jul 07 '22 at 18:43
  • So, I've tried a thousand things, and nothing is really working. According to my recent research and XCode Profiler(Allocation) is confirming next: 1 image when loaded weight 12Mb, 32x1536 × 2048 / 8 / 1024 / 1024 = 12Mb. And since I have about 100+ images- that gives us 1Gb+ in memory usage. I still can't believe in 2022 an official framework Apple can not handle simple animation with 100 frames, but anyways. Maybe you have some suggestions about what can I do here? Since these animations are a major part of the game and I can't just don't use them, optimizing the image does not help... – Goga Tirkiya Jul 07 '22 at 19:25
  • ...since image size does not really matter, matters resolution :( – Goga Tirkiya Jul 07 '22 at 19:26
  • 1
    As you've seen, once you unpack a texture for use any sort of compression that might have helped keep the file size down is out the window. Doing better will require a different approach. E.g., have a common background image layered with some animated parts, or fewer images that are blended dynamically, or maybe a background movie would give some inter-frame savings with hardward-assisted decoding? Looking back at your memory profile picture though, if the animation is playing the whole time then I don't understand the drop at the end. – bg2b Jul 08 '22 at 08:37
  • The drop at the end happens only on a simulator run on my MacBook, maybe it manages to cache images somehow even though the animation is still playing. On a real device - memory does not free up, it constantly stays and frees only when I remove the animation. It is still weird optimization from the SpriteKit, since even when we have 100 images - in the animation we display only one at a time, yes, we switch them really quick, but still - it is one image per 0.03, why can't we load to the memory only actual image that is displayed or at least free images that already were displayed :( – Goga Tirkiya Jul 08 '22 at 11:59
  • The memory use on a real device without preloading once it stabilizes is also around the same 1GB? – bg2b Jul 08 '22 at 12:08
  • Yes, it starts with dropping FPS to ~20 and reserving 1GB ram, then FPS stabilizes to 60, but the ram stays the same. – Goga Tirkiya Jul 08 '22 at 13:39
  • My only other suggestion would be to try smaller groups of textures. E.g., have two arrays of ten textures each. Preload array 1 with images 1-10 and start the animation. Meanwhile start preloading array 2 with images 11-20. When the animation with array 1 completes, animate with array 2; simultaneously reset array 1 to images 21-30 and preload. When the animation with 2 finishes, switch back to array 1. As long as you have no other references to the textures, SpriteKit should drop them from the cache. If the animation is slow enough, the preloading may be able to stay ahead. – bg2b Jul 08 '22 at 21:03
  • The extreme case would be to have two textures. Display one, preload the second with the next image in sequence, swap to display the second texture, and simultaneously start preloading the first texture again. You have to be sure not to keep around any references to the textures that you want SpriteKit to unload. – bg2b Jul 08 '22 at 21:07

1 Answers1

1

After investigating, I've realized that the reason was that no matter how much a picture file size is, the main thing that matters is picture resolution and in my case, a single image memory size was 32x1536×2048 / 8 / 1024 / 1024 = 12Mb. (Where 1536×2048 is the actual picture size). And I had at least 100 frames.

Unfortunately, it seems like there is no way to avoid memory overflow error in such cases :/

Frame animation should be used for small images, but for big ones the only acceptable way is to replace frame animation with a video file using SKVideoNode.