Is Many Tasks Too Many?

I’m currently working on a “sprite” class, intended to somewhat mirror the “Actor” class: it plays, loops, etc.

As part of that, naturally, it has updating to do–and this is where I’m uncertain.

For the most part, I tend to design features to work from a central “update task”, which calls “update” methods on any relevant objects, which in turn call “update” methods on any relevant sub-objects, and so on.

But of course, Actor doesn’t work that way: it updates silently in the background somewhere.

And it’s tempting to do the same for this “sprite” class: to have the sprite’s updating be something that the developer doesn’t have to worry about.

The obvious thought for implementing this, then, is to have each sprite-object create a task to be run each frame, which performs its update-logic.

But… that seems like an awful lot of tasks, potentially! Is that… wise…?

Alternatively, I suppose that I could have a manager class that runs a single “update” task, and which then iterates over a list of all sprites. But that seems like it could be a long list, and such a system feels fragile in having to keep in sync which sprites are still valid…

So, does anyone have any thoughts on this…?

Turning back to Actor, I… don’t see where it’s calling its update function. Does anyone know where this happens?

I think the answer is simple, it launches the C++ shell as a loop when you call the play() method.

Hmm… I may be missing it, but I don’t see there any indication of whether it’s handled by individual tasks, or a single central (presumably C++) loop, or what…

In fact, the class that handles animation on the C++ side is based on the frame rate. That is, it relies on ClockObject.
https://docs.panda3d.org/1.10/python/reference/panda3d.core.AnimInterface

But that could be the case in either a central update or an individual-task scenario…

Looking at the file to which you link, and others associated with it… It looks like the updating of an animation is handled by the “CycleDataWriter” class? And it looks like a new “CycleDataWriter” is spawned for each call to “play” or “loop”.

I’m getting the impression that these are all handled in a single “update”–the “pipeline” that CycleDataWriter objects get put into.

But without more digging I’m not confident of that, and I don’t have the energy to want to do it right now.

And in any case, as you point out, what Actor does appears to be happening on the C++ side, while I’m working on the Python side, and so its logic may not be all that useful in guiding my decision here…

So, let’s return to the original matter:

Is it a bad idea to have lots and lots of tasks (e.g. one per enemy or environmental animation)?

Conversely, as I said, I’m hesitant to make a list of sprites, as I don’t know how many I’m going to have, and it seems likely that keeping such a construct in sync with which sprites are still valid could be rather fragile…

In fact, a task is a function that is executed every frame, I don’t see anything excessive in this. With the reverse logic, you must provide each class with an update function and call it yourself. Under such circumstances, one is identical to the other.

Hmm… That’s somewhat true–although with a task there may be additional overhead (at the least due to spawning more objects; I don’t know whether there’s other overhead).

The only thing that can be done is a global list of sprites that should be updated. Next, in the cycle of one function or task call, iterate through this list. Accordingly, you can make the list dynamic, this will reduce the cost of processor time for the function.

That is one of the options that I mentioned, indeed.

The trick, as I was thinking before, is in keeping it synced as sprites are culled or invalidated in some way…

However, it occurs to me that I might be able to update the list from various relevant overloads in the sprite class–aside from giving it a custom “destroy” method, it inherits from NodePath, so I can potentially overload things like “hide” and “show”…

Ah, I must think on this!

Well, thank you for your help! :slight_smile:

Returning to how the animation is coming, I came to this class.

Everything indicates that this is a separate thread.

1 Like

Pipeline and CycleDataWriter are related to the multi-threaded pipeline. They manage multiple sets of data that the different stages of the pipeline operate on simultaneously. They are not related to update logic.

Actor updates are handled in the Character class, in C++, in the cull_callback method. This is called automatically when the object is culled, but you can call it yourself manually if you want (only the first call to update() in a frame will do anything).

There is a slight bit of overhead in creating a task, but not much. Why not try it? Estimate how many sprites you will have, and spawn that many tasks, and measure the overhead. If it’s not acceptable, use something else.

1 Like

Ah, thank you for the clarity, and for the advice! :slight_smile:

Hmm… I might just do that!

I was thinking that the overhead–even if slight–is surely not less than the overhead for the “call an update method” approach.

But it occurs to me that, between that approach (in my current conception) involving a call to “isinstance” (to check whether the model is a sprite), and tasks presumably running on the C++ side, it might actually still be faster to use a task.

So, I’ll probably do a quick stress-test and see which performs the better!

All right, I performed some quick tests, and thought that you might be interested to know the results!

Now, the tests were fairly simple, but they did compare a scenario in which the sprites each spawned their own tasks against a scenario in which the sprites were updated from a single central task.

Note too that the sprites were rendered on-screen in the tests, and so time for rendering, etc. was presumably a part of the frame-time observed in the frame-rate meter.

For reference, there were I believe 625 sprites on-screen for the test. (A grid of 25 x 25.)

So, when using a single, central task, I observed a frame-time of ~18ms.

And when using a task per sprite, I observed a frame-time of… ~19-20ms.

So… Tasks actually do perform pretty well! :slight_smile:

Now, there are still some caveats with this approach:

For one, cleaning up the task does mean that the developer has to remember to call a cleanup function when done with the sprite.

And for another, my game has some logic that detaches ceases updating regions that are not intended to be visible to the player. That would then call for some additional logic if I wanted to also stop the tasks. (But I think that I know where to do that, if I do.)

Still, I think that I’m going to take the “Task” approach to this, given the results that I’m seeing! :slight_smile:

3 Likes