Hello reader,
I am facing an issue that I am not able to resolve. I have been able to create a demo project that demonstrates the issue, which I hope enables you to have a look as well and hopefully find a way to resolve it.
What is the issue:
I am using SKTileMapNode in order to draw tile maps. Instead of using the tilesets as you can use from within the Xcode editor, I prefer to do it all programmatically using tilesheets (for a plethora of reasons that I will leave out of this equation).
This is the code of the gameScene:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
private let tileSize = CGSize(width: 32, height: 32)
override func didMove(to view: SKView) {
super.didMove(to: view)
let tileSet = createTileSet()
let tileMap = SKTileMapNode(tileSet: tileSet,
columns: 100,
rows: 100,
tileSize: tileSize)
for column in 0..<tileMap.numberOfColumns {
for row in 0..<tileMap.numberOfRows {
guard let tileGroup = tileSet.tileGroups.randomElement() else {
fatalError()
}
tileMap.setTileGroup(tileGroup, forColumn: column, row: row)
}
}
addChild(tileMap)
}
private func createTileSet() -> SKTileSet {
let tileSheetTexture = SKTexture(imageNamed: "terrain")
var tileGroups = [SKTileGroup]()
let relativeTileSize = CGSize(width: tileSize.width/tileSheetTexture.size().width,
height: tileSize.height/tileSheetTexture.size().height)
for idx in 0...2 {
for jdx in 0...2 {
let tileTexture = SKTexture(rect: .init(x: CGFloat(idx) * relativeTileSize.width,
y: CGFloat(jdx) * relativeTileSize.height,
width: relativeTileSize.width,
height: relativeTileSize.height),
in: tileSheetTexture)
let tileDefinition = SKTileDefinition(texture: tileTexture,
size: tileSize)
let tileGroup = SKTileGroup(tileDefinition: tileDefinition)
tileGroups.append(tileGroup)
}
}
let tileSet = SKTileSet(tileGroups: tileGroups)
return tileSet
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
presentSceneAgain()
}
func presentSceneAgain() {
if let frame = view?.frame {
view?.presentScene(GameScene(size: frame.size),
transition: .doorsCloseHorizontal(withDuration: 1.0))
}
}
}
This demo project create a tilemapnode of 100 X 100 tiles. Then, it fills these 10.000 tiles with a random tile from the tilesheet named "terrain.png". This tile sheet contains many tiles, but I only take the 9 tiles (3 X 3) from the lower left corner as a random tile option.
Thus, the 10.000 tiles get filled with one of these 9 tiles. So it doesnt look pretty or anything, but that isnt the purpose.
Now, to create these 9 tile textures, I use the SKTexture(rectIn:) method on the source texture being "terrain.png".
I think the code is quite clear in itself, but so far the explanation. When you run it, you should see the map being rendered.
When you tap the scene, the scene will present a new instance of the scene. Not more than that.
Now, when you do this, have a look at the RAM usage of the app. You will see it steadily increases over time, each time you click the scene and a new scene is presented.
I looked deeper into what is happening, and what I see in the memory graph, is that for every present of the scene that is done, there are 3 SKTexture instances being created that are never released. The first time the scene is rendered, there 11 SKTexture instances allocated (I dont know why there are 11 though. I would expect 10: the source texture and the 9 tile textures).
But then as mentioned, after a tap and a new present, I get 14 SKTexture, of which 3 are zombies, see image leak_1.
Moreover, Xcode reports multiple additional leaks from Jet and Metal allocations, see image leak_all.
As far as I know, the code presented is not retaining any references that it should not, and I suspect this leaks are happening somewhere inside SpriteKit. But I am not able to find exactly where, or how to resolve it.
I hope someone can help with this issue.
Post
Replies
Boosts
Views
Activity
Hi there,
With a couple of other developers we have been busy with migrating our SpriteKit games and frameworks to Swift 6.
There is one issue we are unable to resolve, and this involves the interaction between SpriteKit and GameplayKit.
There is a very small demo repo created that clearly demonstrates the issue. It can be found here:
https://github.com/AchrafKassioui/GameplayKitExplorer/blob/main/GameplayKitExplorer/Basic.swift
The relevant code also pasted here:
import SwiftUI
import SpriteKit
struct BasicView: View {
var body: some View {
SpriteView(scene: BasicScene())
.ignoresSafeArea()
}
}
#Preview {
BasicView()
}
class BasicScene: SKScene {
override func didMove(to view: SKView) {
size = view.bounds.size
anchorPoint = CGPoint(x: 0.5, y: 0.5)
backgroundColor = .gray
view.isMultipleTouchEnabled = true
let entity = BasicEntity(color: .systemYellow, size: CGSize(width: 100, height: 100))
if let renderComponent = entity.component(ofType: BasicRenderComponent.self) {
addChild(renderComponent.sprite)
}
}
}
@MainActor
class BasicEntity: GKEntity {
init(color: SKColor, size: CGSize) {
super.init()
let renderComponent = BasicRenderComponent(color: color, size: size)
addComponent(renderComponent)
let animationComponent = BasicAnimationComponent()
addComponent(animationComponent)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
@MainActor
class BasicRenderComponent: GKComponent {
let sprite: SKSpriteNode
init(color: SKColor, size: CGSize) {
self.sprite = SKSpriteNode(texture: nil, color: color, size: size)
super.init()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class BasicAnimationComponent: GKComponent {
let action1 = SKAction.scale(to: 1.3, duration: 0.07)
let action2 = SKAction.scale(to: 1, duration: 0.15)
override init() {
super.init()
}
override func didAddToEntity() {
if let renderComponent = entity?.component(ofType: BasicRenderComponent.self) {
renderComponent.sprite.run(SKAction.repeatForever(SKAction.sequence([action1, action2])))
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
As SKNode is designed to run on the MainActor, the BasicRenderComponent is attributed with MainActor as well. This is needed as this GKComponent is dedicated to encapsulate the node that is rendered to the scene.
There is also a BasicAnimationComponent, this GKComponent is responsible for animating the rendered node.
Obviously, this is just an example, but when using GameplayKit in combination with SpriteKit it is very common that a GKComponent instance manipulates an SKNode referenced from another GKComponent instance, often done via open func update(deltaTime seconds: TimeInterval) or as in this example, inside didAddToEntity.
Now, the problem is that in the above example (but the same goes foupdate(deltaTime seconds: TimeInterval) the methoddidAddToEntity is not isolated to the MainActor, as GKComponent is not either.
This leads to the error Call to main actor-isolated instance method 'run' in a synchronous nonisolated context, as indeed the compiler can not infer that didAddToEntity is isolated to the MainActor.
Marking BasicAnimationComponent as @MainActor does not help, as this isolation is not propogated back to the superclass inherited methods.
In fact, we tried a plethora of other options, but none resolved this issue.
How should we proceed with this? As of now, this is really holding us back migrating to Swift 6. Hope someone is able to help out here!
Hi there!
I am using SpriteKit for most of my game development, and while I am usually able to achieve what I need using the framework, I find my self a bit stuck with the usage of SKEmitterNode.
I am developing a pixel-art style game, which among others drills down to the fact that whatever is rendered to the scene, should not be anti-aliased. This because that will remove the pixelated effect.
For textures, one can use the filteringMode property and set it to .nearest to achieve this.
However, with an SKEmitterNode, setting this property of the emitted particles using particleTexture has no effect. As a result, all emitted particles are antialiased, which breaks the pixel art look of the game.
Looking at the documentation, I guess particles are rendered in a different way compared to normal SKSpriteNode, hence this property has no effect.
Specifically this sentence A particle’s texture is always stretched uniformly. draws me to that conclusion.
I have been trying to workaround this, but I find no working solution to the problem. Rolling my own emitter system will be a daunting task, that I prefer not to engage into.
I hope someone here might have a suggestion how to work around this. If not, I would be interested to learn how I could request this as a feature to Apple (as I dont think this is necessarily a bug).
Hope you can help, Sander