-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Overview∶ The Task System
(Article originally by tustin2121)
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.
Some other functions you may find useful built into the task system are:
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.
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.
This function will help you store values like the 32-bit pointers to functions in your 16-bit-per-entry data array.
And this will help you get that data back.