Coroutines

Coroutines are a special kind of function introduced in Python 3.5 that can be temporarily suspended, pending the completion of an asynchronous operation, to be resumed after this operation is complete. Panda3D’s task system has full support for Python’s coroutines.

This feature can be hard to understand at first, but it is tremendously useful and powerful, since it makes it easy to write lag-free applications. Heavy operations that would otherwise cause the application to lag or hang can be performed in the background without adding significant complexity to the code.

To turn a regular function into a coroutine, it is marked with the async keyword. The await keyword can then be used within the function to pause it while some asynchronous operation runs in the background. In the meantime, other parts of the application can continue to run, eliminating any lag that may otherwise manifest itself.

To understand how async functions run, you must understand that you cannot simply invoke an async function as though it were a regular function. Some process needs to be in charge of the lifetime of a coroutine, resuming it whenever necessary. In regular Python, this is the asyncio event loop, but Panda3D already has the task manager to schedule the execution of functions, which (unlike asyncio) is thread-safe, and integrates cleanly with the rest of Panda3D.

Let’s take this Python function as an example of a regular, synchronous function that generates undesirable lag. It counts down a given number of seconds and then prints “Launch!” to the console.

import time


def launchRocket(countdown):
    print("Beginning countdown…")

    while countdown > 0:
       print(countdown)

       # Suspend the application for a second
       time.sleep(1.0)
       countdown -= 1

    print("Launch!")

launchRocket(countdown=3)

The problem with the above code is that time.sleep() will block the main thread while it is waiting, meaning that other tasks (including Panda3D’s rendering loop) will not get a chance to run in the meantime. The entire application will appear to have frozen until the countdown is complete!

It is certainly possible to use multiple tasks with delays in order to solve this problem. However, this will quickly make the code a lot more complex, with multiple functions and state variables that need to be stored somewhere. Instead, let us see how we can turn this into a coroutine with minimal modifications:

from direct.task.Task import Task


async def launchRocket(countdown):
    print("Beginning countdown…")

    while countdown > 0:
       print(countdown)

       # Suspend the task for a second
       await Task.pause(1.0)
       countdown -= 1

    print("Launch!")

taskMgr.add(launchRocket(countdown=3))

The moment we use await in the above code, the function is paused until the given operation completes. We use Task.pause(1.0) here, which creates a task that simply finishes after 1 second. In the meantime, other tasks can continue to run, including Panda3D’s render loop, so the lag is eliminated.

Coroutine Tasks

Please note that even though the coroutine is added to the task manager, it is not the same thing as a task, since it is invoked only once and it does not receive the task argument. We can in fact create a recurring task that is also a coroutine by simply prepending the async keyword to a regular task, as demonstrated by this pseudo-code:

from direct.task.Task import Task


async def damageTask(task):
    if player just collided with invincibility item:
        # Suspend damage task until invincibility is no longer active
        await Task.pause(10.0)

    return task.cont

# Note the lack of parentheses here!
taskMgr.add(damageTask)

This behaves identically to a regular task, except that it permits use of the await keyword.

Awaitables

In the examples so far have only used Task.pause(), but there are in fact many things that can be used as our argument to await:

  • All Intervals. This is very useful for transitions or cutscenes, where it is desirable to disable user input, await a sequence of intervals, and then re-enable user input when they are done. With coroutines, this can all happen in a single function.

  • All Tasks. When awaiting a task, it is automatically scheduled with the task manager (on the current task chain), if not already.

  • Any AsyncFuture object. Such an object is returned by various Panda3D operations that take a long time to complete.

  • Any Python object that implements a suitable __await__ method.

Some examples of operations that satisfy one or more of the above conditions:

  • Model load operations, see Asynchronous Loading.

  • messenger.future('event'), to suspend the coroutine until an event is fired from outside the coroutine.

  • tex.prepare(), to wait for a texture to finish uploading to the graphics card. The returned value is the prepared TextureContext object.

Experimental feature

As of Panda3D 1.10, this is still an experimental feature, and some behavior may change in future versions. The upcoming version of Panda3D, 1.11, will improve support for cancellation of futures in particular.