2

I'm new to SpriteKit and I'm trying to simulate a population of 1000 moving individuals each being represented by a simple circle. I have an awful frame rate (around 6/7 fps) so I reduced to 100 and reached 35 fps...

My nodes are the simplest in the world, I'm using the entity component system from GameplayKit, but nothing fancy.

So what can I do to improve and reach 60 fps?

First my shape entity, later in the process the color and filling will change for some individuals, but here there's only a simple circle node:

class ShapeComponent : GKComponent {
    let node = SKShapeNode(ellipseOf: CGSize(width: 10, height: 10))

    override func didAddToEntity() {
        node.entity = entity
    }

    override func willRemoveFromEntity() {
        node.entity = nil
    }
}

Then my movement component, having just a speed and an angle. I have even stored sine and cosine to limit computation:

class MovementComponent : GKComponent {
    var speed : CGFloat = 25
    var direction : CGFloat = .random(in: 0...(2 * .pi)) {
        didSet { updateSinCos() }
    }
    var sinDir : CGFloat = 0
    var cosDir : CGFloat = 0

    override init() {
        super.init()

        updateSinCos()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    var shapeComponent: ShapeComponent {
        guard let shapeComponent = entity?.component(ofType: ShapeComponent.self)
            else { fatalError("A MovementComponent's entity must have a ShapeComponent") }

        return shapeComponent
    }

    func updateSinCos() {
        sinDir = sin(direction)
        cosDir = cos(direction)
    }

    override func update(deltaTime: TimeInterval) {
        super.update(deltaTime: deltaTime)

        // Declare local versions of computed properties so we don't compute them multiple times.
        let delta = speed * CGFloat(deltaTime)
        let vector = CGVector(dx: delta * sinDir, dy: delta * cosDir)

        let node = shapeComponent.node
        let currentPosition = node.position

        node.position = CGPoint(x: currentPosition.x + vector.dx, y: currentPosition.y + vector.dy)
    }
}

And at last my entity, with movement and shape components:

class IndividualEntity : GKEntity {

    var shapeComponent: ShapeComponent {
        guard let shapeComponent = component(ofType: ShapeComponent.self)
            else { fatalError("A IndividualEntity must have a ShapeComponent") }

        return shapeComponent
    }

    var movementComponent: MovementComponent {
        guard let movementComponent = component(ofType: MovementComponent.self)
            else { fatalError("A IndividualEntity must have a MovementComponent") }

        return movementComponent
    }

    override init() {
        super.init()

        addComponent(ShapeComponent())
        addComponent(MovementComponent())
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

In my scene:

func setUpScene() {
    for _ in 1...100 {
        let position = CGPoint(x: .random(in: 0..<size.width) - 0.5*size.width, y: .random(in: 0..<size.height) - 0.5*size.height)
        addIndividual(at: position)
    }
}

var componentSystem = GKComponentSystem(componentClass: MovementComponent.self)
var individuals = [IndividualEntity]()

func addIndividual(at pos: CGPoint) {
    let individual = IndividualEntity()
    individual.shapeComponent.node.position = pos
    addChild(individual.shapeComponent.node)
    componentSystem.addComponent(foundIn: individual)

    individuals.append(individual)
}

var lastUpdateTimeInterval: TimeInterval = 0
let maximumUpdateDeltaTime: TimeInterval = 1.0 / 60.0

override func update(_ currentTime: TimeInterval) {
    guard view != nil else { return }

    var deltaTime = currentTime - lastUpdateTimeInterval        
    deltaTime = deltaTime > maximumUpdateDeltaTime ? maximumUpdateDeltaTime : deltaTime

    lastUpdateTimeInterval = currentTime

    componentSystem.update(deltaTime: deltaTime)
}

So if someone can help me with this problem... I just want those circles to wander, and later respond to collision by changing direction and color.

Thanks in advance.

Zaphod
  • 6,758
  • 3
  • 40
  • 60
  • Every time we talk about performance it is important to mention where it is running. MacBook? Simulator? AppleTV? – Maetschl Mar 29 '20 at 14:47
  • 1
    Generally it's much more efficient to use SKSpriteNode instead of SKShapeNode. If you don't want to use an image asset for the circles,, you can make a shape node, render it to a texture using the view's texture(from:) method, and then make a bunch of sprite nodes using that texture. – bg2b Mar 29 '20 at 17:17
  • Also, turn on the view's showsDrawCount and make sure that the number of draw passes is staying under control. Set ignoresSiblingOrder if you haven't – bg2b Mar 29 '20 at 17:20
  • @Maetschl it is to run either on iOS device or on macOS, and only if it’s possible on tvOS. @ bg2b ok I’ll try with a texture, I have already disabling sibling order. – Zaphod Mar 29 '20 at 19:37
  • I tried several things to try to improve performance and effectively as it says @bg2b SKSpriteNode performs best. – Maetschl Mar 29 '20 at 19:42
  • Thank you very much, indeed with sprite node, I've reached 60 fps without any problem! – Zaphod Mar 29 '20 at 20:02

0 Answers0