Skip to content

Overview∶ The Task System

redsquidz edited this page Feb 6, 2024 · 3 revisions

(Article originally by tustin2121)

The Task System

As discussed in The Game Loop, the main callbacks usually don't have any game logic in them. This is because the Gen 3 engine uses the Task System to perform logic operations during a given screen. Let's go over how these tasks work, using the Cable Car cutscene as an example.

During initialization of the cutscene, you'll see the following lines:

SetMainCallback2(CB2_CableCar);
CreateTask(Task_CableCar, 0);
if (!GOING_DOWN)
    sCableCar->bgTaskId = CreateTask(Task_AnimateBgGoingUp, 1);
else
    sCableCar->bgTaskId = CreateTask(Task_AnimateBgGoingDown, 1);

After setting the main callback (which is what will be calling RunTasks() for us), the game creates two tasks. The first task is assigned the function Task_CableCar, and runs at priority 0 (it will be run first). The second task is the function which animates the cable car, either going up or down. This second task is created with priority 1, which means it will be run after Task_CableCar every frame.

Note that the CreateTask function returns a Task ID, which is a simple byte. This ID can be used later to index the gTasks array to get the information about our current task. In the instance of the second task being created, we save off this id for later use. This ID is technically where in the gTasks array the task was created, but since the Task list is a linked list, where it is in the array has no bearing on in what order the task will be run in. (The order of the tasks is already determined by the priority passed to CreateTask.) Treat this ID like a pointer, in that it should never be modified and only should be stored and used to access information. (A u8 just happens to be a lot smaller than a full u32 pointer, so it's more convenient to store for us.)

Using the Task ID, we can access data in our new task:

struct Task
{
    TaskFunc func;
    bool8 isActive;
    u8 prev;
    u8 next;
    u8 priority;
    s16 data[NUM_TASK_DATA];
};

For our uses, we only need to use func and data; the rest is bookkeeping for the task system that we shouldn't touch.

The func pointer points to the function callback we passed to CreateTask. That function must be of the signature void Task_FunctionName(u8 taskId). In the pokeemerald repo, every function that is used as a task callback is named Task_XXX, so it should be easy for you to see which are task callbacks. When this function is called, it is passed the taskId, which will be the same thing that CreateTask returned. So we don't have to store our own task ID. We can store other tasks' IDs, however, as I'll discuss later.

The data array is an array of 16 u16, in which we can put any data we want. This data is for our own uses. Around the pokeemerald codebase, you'll often see these data fields referenced like so:

#define tTimer   data[0]
#define tState   data[1]

static void Task_ExampleTask(u8 taskId)
{
    // Access the task directly
    gTasks[taskId]->tTimer++; // This is actually gTasks[taskId]->data[0]++;
    
    // Or you can make a pointer to the task data
    s16 *data = gTasks[taskId].data;
    tState = 0; // This is actually using the pointer: data[1] = 0;
}

#undef tState
#undef tTimer

You'll find around the codebase both strategies being used interchangeably. The function in func is called once every frame, and the data put into data can be used to keep state between frames. You can make one of the data fields a state and make the function a large switch statement based on state, or you can assign other functions to the func field, and the function assigned will run next frame:

static void Task_WaitForFade(u8 taskId)
{
    if (!gPaletteFade.active)
    {
        // This block runs the frame after the palette has finished fading in or out
        gTasks[taskId].func = Task_HandlePlayerInput; // Change which function callback is run
    }
}

static void Task_HandlePlayerInput(u8 taskId)
{
    // This task will run with the same task ID and data as above
    // Any data carries over from the above function
}

If you want an example of this being used a lot, check out the credits.c. You'll also see in that file using the data fields to store the task IDs of the other tasks it creates.

gTasks[taskId].tTaskId_ShowMons = CreateTask(Task_ShowMons, 0); // Create a new task, assign the task id to a data field
gTasks[gTasks[taskId].tTaskId_ShowMons].tState = 1; // Set one of the data fields of the new task
gTasks[gTasks[taskId].tTaskId_ShowMons].tMainTaskId = taskId; // Assign a "pointer" back to our task, so it can access our fields

Finally, once a task has completed, it can delete itself with DestroyTask:

DestroyTask(taskId);
DestroyTask(sCableCar->bgTaskId);
SetMainCallback2(CB2_EndCableCar);

This will set the task to inactive and it will no longer run on subsequent frames. Make sure you do this when you clean up a screen or interaction; alternately ResetTasks will destroy all tasks, and you'll find that it's run usually when cleaning up a scene, just to make sure not to leave tasks accidentally lying around, active.

Miscellaneous

Some other functions you may find useful built into the task system are:

void SetTaskFuncWithFollowupFunc(u8 taskId, TaskFunc func, TaskFunc followupFunc)

This function will take your task and assign func to the func field of the task, and store the followupFunc in the last two slots of your data array. And this means a task can call the follow function without needing to know about who called it.

void SwitchTaskToFollowupFunc(u8 taskId)

This function call will assign the previously stored followupFunc and assign it to the task function. These two functions seem to be used most often in link functionality.

void SetWordTaskArg(u8 taskId, u8 index, u32 value)

This function will help you store values like the 32-bit pointers to functions in your 16-bit-per-entry data array.

u32 GetWordTaskArg(u8 taskId, u8 index)

And this will help you get that data back.

Clone this wiki locally