Registrarse

[Decomp-GBA] [Avanzado] ¿Qué son y como funcionan las Tasks y los Callbacks?

Kaktus

Miembro insignia
Miembro insignia
¡¡Buenas!!

Imagino que si habéis trasteado lo suficiente con Decompilación y el código C, os habréis encontrado (sobretodo en interfaces) con las tasks y los callbacks. Quería hacer este post para que todos los que ya sabéis programar y os sentís bastante a gusto entre las líneas del código, os acerquéis un poco más al funcionamiento interno del juego.

La pregunta a qué son las tasks y los callbacks se responde sabiendo como funcionan, así que, vamos directos al grano.

Todos sabemos que el funcionamiento de cualquier pantalla se base en los frames, ¿cierto? Es decir, que la consola o el hardware donde sea que estemos jugando, a nivel interno, lo que hace (entre muchas otras cosas) es que cada x fracción de tiempo, calcula un nuevo frame (imagen del juego, en este caso) para mostrarnos, y lo hace de forma tan rápida, que nos da esa sensación de movimiento.

Bien, pues partiendo de esa base, cuando nosotros escribimos código en cualquier archivo .c del juego, por lo general todo eso que hemos escrito, se ejecuta antes de cargar el siguiente frame. Entonces, si nosotros escribimos en una misma función un código que hiciera algo así
(es un código de ejemplo, me lo acabo de inventar)
C:
static void LoadNewText(const u8* text)
{
    CleanPastText(); // Si había texto cargado, lo borra
    LoadNewTextInBuffer(text); // Deja el nuevo texto listo para mostrar
    PrintBufferedText();// Dibuja el texto en pantalla
}
Lo que podríamos pensar intuitivamente que está haciendo, es que en cada frame, hace una cosa, es decir, que en el primer frame borraría el texto, en el segundo dejaría preparado el nuevo texto, y que ya en el tercero, lo mostraría, ¿no?. Pues en realidad no, se hace todo justo antes de cargar el siguiente frame, por lo que automáticamente, pasaremos de ver un texto, a ver el otro (en el caso hipotético de que no hubieran animaciones de carga de texto ni mierdas de esas).

Hmm... Pero es que da la casualidad de que yo quería cargar 4 imágenes de 256 colores de forma consecutiva para que hiciera un efecto de movimiento, si todo se ejecuta en un mismo frame, ¿como voy a lograr mostrar esas 4 imágenes en frames separados? :/

¡¡Bieen!! Ya hemos llegado donde queríamos. Para eso podemos recurrir a las tasks y los callbacks. Básicamente, siempre y cuando estén activos, ejecutarán el código de su función cada frame, y si ya tenéis experiencia con programación, ya habréis pillado cómo cargar esa animación de 4 frames, pero si no, tranquilos, yo os lo explico.

Antes de seguir, remarcar de forma más concreta los usos que debéis darle a cada cosa (según lo que he podido observar en el código del juego, mi humilde opinión y experiencia). Los callbacks, están destinados a ser llamados en cada frame de forma indefinida, o hasta que se asigne un callback nuevo que lo reemplace. Se suelen usar cuando quieres que tu código se ejecute hasta que se cumpla una condición o pase algo concreto. Por poner un ejemplo práctico, nosotros cuando estamos caminando por los mapas, y pulsamos una tecla para movernos, o pulsamos A para hablar con un NPC o coger un objeto, o incluso mantener B para correr, lo que estamos haciendo es pasar esa información al callback. Existe un callback para el overworld, encargado de ser ejecutado en cada frame, para detectar que teclas estamos pulsando, y para realizar una acción u otra, dependiendo de esas teclas pulsadas. Cuando pulsamos start y ese callback lo detecta, dejamos de estar usando el callback del overworld, para pasar a estar usando el callback del menú start. Y esto es lógico, porque mientras estemos en el menú de start, no queremos por ejemplo que al pulsar el bottón de arriba el personaje se mueva hacia arriba, si no que queremos que la flechita del menú suba hacia arriba. Si pulsamos la B, de nuevo, el callback volverá a ser el del overworld, y así podría seguir poniéndoos ejemplos, pero creo que ya lo habéis entendido.

Los tasks, son algo más genérico, como su nombre indica, son tareas, por tanto, las usaremos cuando queramos que realicen una serie de acciones concretas separadas en varios frames, al margen del código del callback que esté activo.

A continuación os voy a poner un ejemplo muy práctico para terminar de asentar los conocimientos que os acabo de explicar.

Imaginad que estamos creando un minijuego. El minijuego consiste en que en X frame aleatorio, aparece en pantalla la imagen de un botón, y nosotros tenemos cierta cantidad de frames (que al final, se puede traducir en tiempo) para pulsar ese mismo botón que ha salido en la imagen. Si lo hacemos a tiempo, ganamos el juego, si no lo hacemos a tiempo, vuelve a salir otro botón, y así hasta que lo pulsemos a tiempo. Además, el gráfico del botón se queda cargado hasta que se acabe el tiempo, y si lo pulsamos a tiempo, debe animar esa imagen con 3 frames, como si el botón estuviera siendo pulsado, para que el jugador sepa que lo ha pulsado a tiempo.

Pues para ello, en primer lugar, tendríamos que tener un callback en el que siempre ejecute un código que sea algo así como -> Genera un número aleatorio del 1 al 60, y dentro de él, metemos una condición que sea algo así Si el número es 1, cambia el callback, a otro que esté todo el rato detectando si ha presionado la tecla correspondiente, y ahora, imaginad que el jugador presiona el botón a tiempo, y hay que hacer la animación de 3 frames. Pues muy fácil, desde el callback donde hemos detectado que se ha pulsado, crearíamos una tarea (task) encargada de cargar cada frame de forma separada, y ya tendríamos nuestro minijuego listo.

Finalmente, he de poneros ejemplos reales, porque está muy guay saberse la teoría, pero si no sabéis como se aplica, estamos jodidos. Lo que voy a explicar ahora, aplica tanto para callback como para task, sólo que el callback suele estar más destinado a ejecutar siempre el mismo código sea el frame que sea, y en la task, normalmente se ejecutan cosas distintas dependiendo del frame.

Si queremos separar una acción en varios frames, lo único que necesitamos es tener un contador que se encuentre en una variable global, es decir, que no tenemos que crear el contador dentro de la propia función, porque si no, cada vez que se ejecuta la task, el contador empezaría de nuevo, y no tendría sentido.

Por lo general, el juego usa gMain.state como contador para los callbacks, que es como un contador global (sólo hay uno, porque se supone que sólo debe haber un callback ejecutándose al mismo tiempo). Y para las tasks, tenemos un contador propio para cada una de ellas, siendo este (teniendo que definirlo previamente en el .c si no lo está) task.tTimer. Con estos dos, podremos trabajar con esa lógica, a continuación os dejo un código inventado de ejemplo.

C:
/*
Suponemos que antes de crear la tarea,
habéis puesto a 0 el taskId.tTimer
*/
static void AnimButton(u8 taskId)
{
    switch(taskId.tTimer)
    {
        case 0:
            ShowSprite(sFrame0);
            taskId.tTimer++;
            break;
        case 1:
            ShowSprite(sFrame1);
            taskId.tTimer++;
            break;
        case 2:
            ShowSprite(sFrame2);
            taskId.tTimer++;
            break;
        case 3:
            DestroyTask(taskId);
            break;
    }
}
Y como veis, con esto ya habríamos logrado nuestro objetivo (recordad que una vez la task haya cumplido su cometido, debéis destruirla con DestroyTask(indiceDeLaTask); para evitar que se siga llamando en cada frame aunque no haga nada).

Bueno, os habéis ganado un descanso de puta madre leyéndoos este post, id a descansar un rato anda :p
 

Driox24

Usuario de platino
Muy buen post este que acabas de hacer, ya que explicas con muy buenos ejemplos unos de los elementos más confusos a la hora de intentar programar algo en gba. Y no es que sean elementos que se usan poco, es que encima se usan muchisimo y siempre son necesarios para una cosa u otra.
Además, con esto la gente se animará más a intentar hacer interfaces menús y nuevas mecánicas, ya que al principio puede verse muy imponente el código, pero una vez vas entendiendo todo, es un gustazo poder llevar tus ideas a una rom original de gba sin tener que saber ASM, que es bastante más complicado, y el cuál antes era la única opción para hacer todo esto jajajaj
 
Arriba