Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

reusable multithread implementation in Sprite Kit

I am working on a Sprite Kit game and I need to do some multithreading to maintain the healthy fps.

On update I call a function to create a lot of UIBezierPaths and merge them using a C++ static library.

If I have more than 10 shapes the frame rate drops dramatically so I decided I give GCD a try and try to solve the issue with a separate thread.

I put this in didMoveToView:

queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);

and in the function that is being called on every frame, I call this:

dispatch_async(queue,^(void){[self heavyCalculationsFunc];});

For somebody who knows GCD well it might be obvious it creates a new thread on every frame, but for me it wasn't clear yet.

My question is, is there any way to re-use a thread that I want to call on update?

Thanks for your help in advance!

like image 968
Velykovits Avatar asked Feb 26 '15 17:02

Velykovits


1 Answers

If you have work that you need to do on every frame, and that needs to get done before the frame is rendered, multithreading probably won't help you, unless you're willing to put a lot of effort into it.

Maintaining a frame rate is all about time — not CPU resources, just wall time. To keep a 60fps framerate, you have 16.67 ms to do all your work in. (Actually, less than that, because SpriteKit and OpenGL need some of that time to render the results of your work.) This is a synchronous problem — you have work, you have a specific amount of time to do it in, so the first step to improving performance is to do less work or do it more efficiently.

Multithreading, on the other hand, is generally for asynchronous problems — there's work you need to do, but it doesn't need to get done right now, so you can get on with the other things you need to do right now (like returning from your update method within 16 ms to keep your framerate up) and check back for the results of that work later (say, on a later frame).


There is a little bit of wiggle room between these two definitions, though: just about all modern iOS devices have multicore CPUs, so if you play your cards right you can fit a little bit of asynchronicity into your synchronous problem by parallelizing your workload. Getting this done, and doing it well, is no small feat — it's been the subject of serious research and investment by big game studios for years.

Take a look at the figure under "How a Scene Processes Frames of Animation" in the SpriteKit Programming Guide. That's your 16 ms clock. The light blue regions are slices of that 16 ms that Apple's SpriteKit (and OpenGL, and other system frameworks) code is responsible for. The other slices are yours. Let's unroll that diagram for a better look:

SpriteKit rendering loop, linear

If you do too much work in any of those slices, or make SpriteKit's workload too large, the whole thing gets bigger than 16 ms and your framerate drops.

The opportunity for threading is to get some work done on the other CPU during that same timeline. If SpriteKit's handling of actions, physics, and constraints doesn't depend on that work, you can do it in parallel with those things:

Game code in parallel to SK code

Or, if your work needs to happen before SpriteKit runs actions & physics, but you have other work you need to do in the update method, you can send some of the work off to another thread while doing the rest of your update work, then check for results while still in your update method:

Game code in parallel to itself


So how to accomplish these things? Here's one approach using dispatch groups and the assumption that actions/physics/constraints don't depend on your background work — it's totally off the top of my head, so it may not be the best. :)

// in setup
dispatch_queue_t workQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t resultCatchingGroup = dispatch_group_create();
id stuffThatGetsMadeInTheBackground;

- (void)update:(NSTimeInterval)currentTime {
    dispatch_group_async(group, queue, ^{
        // Do the background work 
        stuffThatGetsMadeInTheBackground = // ...
    });
    // Do anything else you need to before actions/physics/constraints
}

- (void)didFinishUpdate {
    // wait for results from the background work
    dispatch_group_wait(resultCatchingGroup, DISPATCH_TIME_FOREVER);

    // use those results
    [self doSomethingWith:stuffThatGetsMadeInTheBackground];
}

Of course, dispatch_group_wait will, as its name suggests, block execution to wait until your background work is done, so you still have that 16ms time constraint. If the foreground work (the rest of your update, plus SpriteKit's actions/physics/constraints work and any of your other work that gets done in response to those things) gets done before your background work does, you'll be waiting for it. And if the background work plus SpriteKit's rendering work (plus whatever you do in update before spawning the background work) takes longer than 16 ms, you'll still drop frames. So the trick to this is knowing your workload in enough detail to schedule it well.

like image 104
rickster Avatar answered Sep 28 '22 02:09

rickster