Registrarse

[pokeemerald] Animación Pokémon al "estilo Blanco y Negro" en pokeemerald

Thorec_A_C

Héroe de WaH

Continuación del post.

Como en la sección de Investigación no se puede publicar lo pongo por aquí, realmente el tutorial está casi completo a falta de un pequeño detalle y de los bugs que puedan surgir. Dejo el sistema corregido y actualizado.

Al lio, esto es una manera de implementar animaciones de los Pokémon al “estilo Blanco y Negro” en decomp, concretamente en pokeemerald. Voy a explicarlo lo mejor que pueda, aunque aviso que no tengo grandes conocimientos del código de decomp.

Esta implementación dista de mucho de ser profesional, salió un poco de ensayo y error, y presenta algunos comportamientos extraños.

Para empezar explicar que las animaciones de Esmeralda tienen 2 partes: una donde se carga los frames de los sprites y los repite x número de veces con x duración cada repetición; la otra que modifica los sprites dándoles efectos, un giro, estirarlos, etc.


Implementando la animación en los Front Sprites

Vamos a hacer que los frames se repitan indefinidamente para simular una animación continua como Blanco y Negro:

Vamos a: pokeemerald\src\data\pokemon_graphics\front_pic_anims.h

Aquí encontramos una función para cada uno de los Pokémon. La de Pikachu por ejemplo es:

Código:
static const union AnimCmd sAnim_PIKACHU_1[] =

{
    ANIMCMD_FRAME(0, 7),
    ANIMCMD_FRAME(1, 4),
    ANIMCMD_FRAME(0, 4),
    ANIMCMD_FRAME(1, 4),
    ANIMCMD_FRAME(0, 4),
    ANIMCMD_FRAME(1, 4),
    ANIMCMD_FRAME(0, 10),
    ANIMCMD_END,
};
Donde ANIMCMD_FRAME(X, Y) indica cada repetición del frame, la X indica el ID del frame y la Y al duración.

Modificamos la función por esto:

Código:
static const union AnimCmd sAnim_PIKACHU_1[] =

{
    ANIMCMD_FRAME(0, 15),
    ANIMCMD_FRAME(1, 15),
    ANIMCMD_FRAME(2, 15),
    ANIMCMD_FRAME(3, 15),
    ANIMCMD_LOOP(1),
    ANIMCMD_JUMP(0),
    ANIMCMD_END,
};
Añadiendo loop y jump la animación pasa a ejecutarse indefinidamente. Debemos añadir esto para todos los Pokémon que queramos tengan esta animación.

Hay un problema con esto y es que al no finalizar nunca la animación el combate no continua. Sin embargo quitando los efectos de las animaciones que mencione antes, lo soluciona. Aunque hay una forma de mantener lo efectos con el loop no lo voy a poner porque acaba dando artefactos visuales en los combates, según mi experiencia, además se ejecutan sólo una vez al principio y queda raro con la animación continua.

Para ello vamos a: pokeemerald\src\pokemon_animation.c

Y cogemos cualquiera de las funciones de efectos disponibles en Esmeralda, por ejemplo cojo la de Unown, que es static void Anim_ZigzagFast(struct Sprite *sprite) y sustituyo todo por:

Código:
static void Anim_ZigzagFast(struct Sprite *sprite)

{
    sprite->callback = SpriteCallbackDummy;
}
Ya tenemos un efecto anulado así que ahora vamos a: pokeemerald\src\pokemon.c

Y asignamos esa función a todos los Pokémon que tengan animación continua en static const u8 sMonFrontAnimIdsTable[NUM_SPECIES - 1] =

Por ejemplo Bulbasaur quedaría así: [SPECIES_PIKACHU - 1] = ANIM_ZIGZAG_FAST,

Con esto ya tendríamos implementado el sistema. Como he comentado habría que asignar "ANIM_ZIGZAG_FAST" a todos los Pokémon.

Con esto ya tendríamos implementado el sistema. Pero con estas modificaciones se producen los siguientes comportamientos extraños:
  1. Cuando accedemos al menú del equipo Pokémon o a la mochila en un combate, al volver a la escena de combate la animación no se ejecuta.
  2. Al cambiar de Pokémon en un combate con un entrenador la animación del segundo Pokémon y los sucesivos no se inicia.
  3. Cuando se usa Sustituto y se rompe, se deja de ejecutar la animación.
  4. Cuando Ditto se transforma deja de ejecutar la animación.
  5. Cuando Castform cambia de forma, la nueva forma no ejecuta la animación.
  6. Los cambios de stats y los efectos de algunos ataques como Fortaleza o Maldición dan errores gráficos.
Para solucionar punto 1:

En pokeemerald\src\reshow_battle_screen.c en la función static void CreateBattlerSprite(u8 battler) añadimos:

Código:
else
        StartSpriteAnim(&gSprites[gBattlerSpriteIds[battler]], 1);
Después de la línea de código que hace referencia a Castform de tal forma que quede así:

Código:
StartSpriteAnim(&gSprites[gBattlerSpriteIds[battler]], gBattleMonForms[battler]);
    if (gBattleSpritesDataPtr->battlerData[battler].transformSpecies == SPECIES_CASTFORM)
         gSprites[gBattlerSpriteIds[battler]].anims = gMonFrontAnimsPtrTable[SPECIES_CASTFORM];
    else
         StartSpriteAnim(&gSprites[gBattlerSpriteIds[battler]], 1);”

Para solucionar punto 2:

En battle_controller_opponent.c modificamos en la función static void CreateBattlerSprite(u8 battler):

Código:
StartSpriteAnim(&gSprites[gBattlerSpriteIds[gActiveBattler]], 0);
por

Código:
StartSpriteAnim(&gSprites[gBattlerSpriteIds[gActiveBattler]], 1);
Como veis solo es cambiar “0” por “1”. Entiendo que abría que cambiar también lo mismo en todas las funciones static void CreateBattlerSprite(u8 battler) de los demás battle_controller_X. Pero aun no he llegado a un punto en el juego donde se usen.

Para solucionar punto 3:

En pokeemerald\src\battle_gfx_sfx_util.c modificamos en la función void LoadBattleMonGfxAndAnimate(u8 battlerId, bool8 loadMonSprite, u8 spriteId) añadimos StartSpriteAnim(&gSprites[spriteId], 1) al final:

Código:
void LoadBattleMonGfxAndAnimate(u8 battlerId, bool8 loadMonSprite, u8 spriteId)
{
    BattleLoadSubstituteOrMonSpriteGfx(battlerId, loadMonSprite);
    StartSpriteAnim(&gSprites[spriteId], gBattleMonForms[battlerId]);

    if (!loadMonSprite)
        gSprites[spriteId].y = GetSubstituteSpriteDefault_Y(battlerId);
    else
        gSprites[spriteId].y = GetBattlerSpriteDefault_Y(battlerId);
        StartSpriteAnim(&gSprites[spriteId], 1);
}

Para solucionar punto 4:

En pokeemerald\src\battle_anim_effects_3.c en la función void AnimTask_TransformMon(u8 taskId) en el caso 4 añadimos StartSpriteAnim(&gSprites[gBattlerSpriteIds[gBattleAnimAttacker]], 1) al final:

Código:
    case 4:
        SetGpuReg(REG_OFFSET_MOSAIC, 0);
        if (GetBattlerSpriteBGPriorityRank(gBattleAnimAttacker) == 1)
            SetAnimBgAttribute(1, BG_ANIM_MOSAIC, 0);
        else
            SetAnimBgAttribute(2, BG_ANIM_MOSAIC, 0);

        if (!IsContest())
        {
            if (GetBattlerSide(gBattleAnimAttacker) == B_SIDE_OPPONENT)
            {
                if (gTasks[taskId].data[10] == 0)
                    SetBattlerShadowSpriteCallback(gBattleAnimAttacker, gBattleSpritesDataPtr->battlerData[gBattleAnimAttacker].transformSpecies);
            }
        }

        DestroyAnimVisualTask(taskId);
        StartSpriteAnim(&gSprites[gBattlerSpriteIds[gBattleAnimAttacker]], 1);
        break;

Para el punto 5 tengo pendiente de solucionarlo pero tampoco afecta en exceso ya que es un caso muy concreto, sólo afecta a un Pokemon, y no produce ningún bug.

Para el punto 6, lo tengo pendiente también, para los efectos visuales creo que la clave está en hacer que la animación pare mientras se realiza el efecto, en muchos ataques ya lo hace, se puede ver si implementáis esto, para unas décimas de segundo mientras se hace la animación del ataque. Habría que añadir esto a los ataques que dan error como Fortaleza o Maldición. Con respecto a los cambios de estadísticas entiendo que habría que mirar en pokeemerald\src\battle_anim_utility_funcs.c pero no estoy seguro. Otra opción es directamente quitar la máscara de efectos en estas situaciones.


Implementando la animación en los Back Sprites
Respecto a los Back Sprites vamos a hacer que los sprites y animaciones se comporten igual que los de los Front Sprites:

Para los frames de los sprites y sus repeticiones vamos a pokeemerald\src\pokemon.c y habría que modificar la función void SetMultiuseSpriteTemplateToPokemon(u16 speciesTag, u8 battlerPosition) de la siguiente manera:

Código:
void SetMultiuseSpriteTemplateToPokemon(u16 speciesTag, u8 battlerPosition)
{
    if (gMonSpritesGfxPtr != NULL)
        gMultiuseSpriteTemplate = gMonSpritesGfxPtr->templates[battlerPosition];
    else if (sMonSpritesGfxManagers[MON_SPR_GFX_MANAGER_A])
        gMultiuseSpriteTemplate = sMonSpritesGfxManagers[MON_SPR_GFX_MANAGER_A]->templates[battlerPosition];
    else if (sMonSpritesGfxManagers[MON_SPR_GFX_MANAGER_B])
        gMultiuseSpriteTemplate = sMonSpritesGfxManagers[MON_SPR_GFX_MANAGER_B]->templates[battlerPosition];
    else
        gMultiuseSpriteTemplate = gBattlerSpriteTemplates[battlerPosition];

    gMultiuseSpriteTemplate.paletteTag = speciesTag;
    if (speciesTag > SPECIES_SHINY_TAG)
        gMultiuseSpriteTemplate.anims = gMonFrontAnimsPtrTable[speciesTag - SPECIES_SHINY_TAG];
    else
        gMultiuseSpriteTemplate.anims = gMonFrontAnimsPtrTable[speciesTag];
}
Para la animación del sprite de esmeralda, que hemos desactivado previamente en los Front Sprites, vamos a pokeemerald\src\pokeball.c y modificamos la función static void SpriteCB_ReleaseMonFromBall(struct Sprite *sprite) de la siguiente manera (se modifica al final a partir de StartSpriteAffineAnim):

Código:
static void SpriteCB_ReleaseMonFromBall(struct Sprite *sprite)
{
    u8 battlerId = sprite->sBattler;
    u32 ballId;

    StartSpriteAnim(sprite, 1);
    ballId = ItemIdToBallId(GetBattlerPokeballItemId(battlerId));
    AnimateBallOpenParticles(sprite->x, sprite->y - 5, 1, 28, ballId);
    sprite->data[0] = LaunchBallFadeMonTask(TRUE, sprite->sBattler, 14, ballId);
    sprite->callback = HandleBallAnimEnd;

    if (gMain.inBattle)
    {
        struct Pokemon *mon, *illusionMon;
        s8 pan;
        u16 wantedCryCase;
        u8 taskId;

        if (GetBattlerSide(battlerId) != B_SIDE_PLAYER)
        {
            mon = &gEnemyParty[gBattlerPartyIndexes[battlerId]];
            pan = 25;
        }
        else
        {
            mon = &gPlayerParty[gBattlerPartyIndexes[battlerId]];
            pan = -25;
        }

        if ((battlerId == GetBattlerAtPosition(B_POSITION_PLAYER_LEFT) || battlerId == GetBattlerAtPosition(B_POSITION_OPPONENT_LEFT))
         && IsDoubleBattle() && gBattleSpritesDataPtr->animationData->introAnimActive)
        {
            if (gBattleTypeFlags & BATTLE_TYPE_MULTI && gBattleTypeFlags & BATTLE_TYPE_LINK)
            {
                if (IsBGMPlaying())
                    m4aMPlayStop(&gMPlayInfo_BGM);
            }
            else
            {
                m4aMPlayVolumeControl(&gMPlayInfo_BGM, TRACKS_ALL, 128);
            }
        }

        if (!IsDoubleBattle() || !gBattleSpritesDataPtr->animationData->introAnimActive)
            wantedCryCase = 0;
        else if (battlerId == GetBattlerAtPosition(B_POSITION_PLAYER_LEFT) || battlerId == GetBattlerAtPosition(B_POSITION_OPPONENT_LEFT))
            wantedCryCase = 1;
        else
            wantedCryCase = 2;

        gBattleSpritesDataPtr->healthBoxesData[battlerId].waitForCry = TRUE;

        taskId = CreateTask(Task_PlayCryWhenReleasedFromBall, 3);

        illusionMon = GetIllusionMonPtr(battlerId);
        if (illusionMon != NULL)
            gTasks[taskId].tCryTaskSpecies = GetMonData(illusionMon, MON_DATA_SPECIES);
        else
            gTasks[taskId].tCryTaskSpecies = GetMonData(mon, MON_DATA_SPECIES);

        gTasks[taskId].tCryTaskPan = pan;
        gTasks[taskId].tCryTaskWantedCry = wantedCryCase;
        gTasks[taskId].tCryTaskBattler = battlerId;
        gTasks[taskId].tCryTaskMonSpriteId = gBattlerSpriteIds[sprite->sBattler];
        gTasks[taskId].tCryTaskMonPtr1 = (u32)(mon) >> 16;
        gTasks[taskId].tCryTaskMonPtr2 = (u32)(mon);
        gTasks[taskId].tCryTaskState = 0;
    }

    StartSpriteAffineAnim(&gSprites[gBattlerSpriteIds[sprite->sBattler]], BATTLER_AFFINE_EMERGE);

    if (GetBattlerSide(sprite->sBattler) == B_SIDE_OPPONENT)
        gSprites[gBattlerSpriteIds[sprite->sBattler]].callback = SpriteCB_OpponentMonFromBall;
    else
        gSprites[gBattlerSpriteIds[sprite->sBattler]].callback = SpriteCB_OpponentMonFromBall;

    AnimateSprite(&gSprites[gBattlerSpriteIds[sprite->sBattler]]);
    gSprites[gBattlerSpriteIds[sprite->sBattler]].data[1] = 0x1000;
}

Conclusiones finales

No lo comenté en un inicio porque entiendo que al ver los frames en Esmeralda se da por sentado, pero por si acaso: la imagen con los frames tiene que ser en vertical, ancho 64 pixeles y el alto 64 x nº de frames. Para mi ejemplo estoy usando estas imágenes, front y back:


Las imágenes están en la carpeta pokeemerald\graphics\pokemon.

Por último comentar que si bien esto funciona con los sprites de Esmeralda, el resultado no es muy bueno ya que al usar sólo 2 frames las animaciones son bruscas. Sin embargo con 4 frames, como en el ejemplo, el resultado es bastante bueno. Importante, no usar más de 4 frames por Pokémon ya que según he leído supera lo que la memoria ram del juego en los combates puede soportar en combates dobles, aunque no estoy muy seguro de ello.

Otra cosa a tener en cuenta de esto es el espacio de la rom, ya que estamos metiendo el doble de frames por Pokémon. No creo que sea posible aplicarlo a los más de 1000 Pokémon que existen, si los metiéramos en la rom. Además que sería un trabajo titánico crear frames para cada Pokémon.

Y esto sería todo, animo al que esté interesado a implementarlo y testearlo, seguro que hay más errores que yo no he visto. Y si alguien descubre como arreglar el tema de las animaciones de cambios de características sería genial.
 
Última edición:

Thorec_A_C

Héroe de WaH
Actualizo el post principal añadiendo la implementación de la animación en los Back Sprites, asi como algunas modificaciones para que funcionen con algunas mecánicas del juego (Sustituto, Transformación...)
 
Arriba