SpriteAnimationTicker en Flame para controlar una animación en Flutter

Un AnimationTicker es una técnica utilizada en varias bibliotecas de animación y permiten controlar una animación; en el caso de Flame, también permiten escuchar los estados de la animación; por ejemplo, cuando se completa la animación:

animationTicker.onComplete = () {
    ***
  }
};
Cuando se ejecuta un frame de la animación:
animationTicker.onFrame = (index) {
  ***
  }
};

Todo esto se hace mediante la clase de SpriteAnimationTicker de Flame; para el juego que estamos implementando, es necesario conocer cuando acaba la animación de "muriendo" para reiniciar la partida; para esto, podemos emplear cualquiera de los listeners mostrados anteriormente.

Primero, necesitamos inicializar el ticker:

lib/components/player_component.dart

class PlayerComponent extends Character {
  ***
  late SpriteAnimationTicker deadAnimationTicker;

  @override
  void onLoad() async {
    ***
    deadAnimationTicker = deadAnimation.createTicker();
  }

  @override
  void update(double dt) {
    ***
    deadAnimationTicker.update(dt);
    super.update(dt);
  }
}

Con la función de createTicker() creamos un ticker (SpriteAnimationTicker) sobre la animación que vamos a controlador; en el caso del juego que estamos implementando, nos interesa es detectar cuando termina la animación de muriendo, que se va a ejecutar únicamente cuando el player se queda sin vidas y al terminar la animación, se reinicia el nivel. La razón de que es inicializado la propiedad de deadAnimationTicker en el onLoad() y no cuando se emplee la animación de deadAnimation (al quedarse el player sin vidas) es que es necesario actualizar el ticker en la función de upload() según el ciclo de vida del ticker:

deadAnimationTicker.update(dt)

Con el ticket, se crea un listener para detectar cuando termina la animación de ejecutarse; para ello, podemos ejecutar el listener de onComplete():

deadAnimationTicker.onComplete = () {
  // TODO
};

O el de onFrame(), que se ejecuta por cada Frame, pero, preguntando si el frame actual es el último:

deadAnimationTicker.onFrame = (index) {
  if (deadAnimationTicker.isLastFrame) {
    // TODO
  }
};

Finalmente, el código completo queda como:

lib/components/player_component.dart

class PlayerComponent extends Character {
  void reset({bool dead = false}) async {
    game.overlays.remove('Statistics');
    game.overlays.add('Statistics');
    velocity = Vector2.all(0);
    game.paused = false;
    blockPlayer = true;
    inviciblePlayer = true;
    movementType = MovementType.idle;
    if (dead) {
      animation = deadAnimation;

      deadAnimationTicker = deadAnimation.createTicker();
      deadAnimationTicker.onFrame = (index) {
        // print("-----" + index.toString());
        if (deadAnimationTicker.isLastFrame) {
          animation = idleAnimation;
          position =
              Vector2(spriteSheetWidth / 4, mapSize.y - spriteSheetHeight);
        }
      };

      deadAnimationTicker.onComplete = () {
        if (animation == deadAnimation) {
          animation = idleAnimation;
          position =
              Vector2(spriteSheetWidth / 4, mapSize.y - spriteSheetHeight);
        }
      };
    } else {
      animation = idleAnimation;
      position = Vector2(spriteSheetWidth / 4, mapSize.y - spriteSheetHeight);
      size = Vector2(spriteSheetWidth / 4, spriteSheetHeight / 4);
    }
    game.colisionMeteors = 0;
    game.addConsumibles();

    //position = Vector2(spriteSheetWidth / 4, 0);
  }
}

Este material forma parte de mi curso y libro completo sobre el desarrollo de juegos en 2D con Flutter y Flame.

Transcripción del vídeo

Si has llegado hasta este punto ya has visto clases estas clases sección tras sección para completar el primer proyecto como tal o la primera aplicación que hacemos realmente que es el del Dinosaurio felicidades ahora sí como te comenté inicialmente antes abarcarte en todo esto te recomendaba que ampliaras Play en su versión 1.7.3 que era la última en la cual yo utilicé para crear esa aplicación pero a partir de esa versión como ocurre con prácticamente todas las versiones de flame ocurrieron unos ligeros cambios y por lo tanto tenemos que adaptar el juego para que funcione con la última versión de flame y más que esto para que tú también tengas las referencias actuales de cómo funciona básicamente los componentes que empleamos es una versión actual que a la fecha es la 1.8.1 de flame Así que, son básicamente dos cambios vamos a tratar el primero que sería con las animaciones automóviles anteriormente para que nosotros podamos definir un callback por ejemplo para indicar cuando una animación era completada simplemente tenemos que indicar aquí la animación por ejemplo en animación Dead o cualquiera de estos que son los que tenemos aquí definidos colocamos aquí por ejemplo esta la de Animation punto on compute Y por qué no salía básicamente cosa que ya no sale tal cual puedes ver en este caso en este proyecto ya yo estoy empleando la última versión de Flame que a la fecha que sería como te indicaba la 1.8.1 Entonces cómo es ahora la cuestión Bueno ahora tenemos que emplear un ticket un Tinker básicamente esto viene siendo un componente que va a permitir gestionar la animación es decir ya las animaciones No la vamos a manipular de manera directa sino para poder cambiar la misma tenemos que ampliar otra clase un auxiliar que en este caso sería un ticket es básicamente eso así que bueno básicamente bueno por aquí tenemos varias tenemos varios calmas que podemos emplear el dedo complete son los que me aparecieron más interesantes es decir esto se ejecuta cuando se complete la animación Y ésta Se ejecuta básicamente frame por frame así que bueno en este caso te estoy mostrando aquí en los fragmentos de mi libro para que se entienda un poco mejor la idea por aquí tenemos básicamente el Tinker que nos interesa es definirlo para cuando la animación de muerte se ejecuta ya que cuando esta animación termina de ejecutarse es cuando nosotros hacemos el reset del juego es decir terminar la animación:

class PlayerComponent extends Character {
  void reset({bool dead = false}) async {
    game.overlays.remove('Statistics');
    game.overlays.add('Statistics');
    velocity = Vector2.all(0);
    game.paused = false;
    blockPlayer = true;
    inviciblePlayer = true;
    movementType = MovementType.idle;
    if (dead) {
      animation = deadAnimation;

      deadAnimationTicker = deadAnimation.createTicker();
      deadAnimationTicker.onFrame = (index) {
        // print("-----" + index.toString());
        if (deadAnimationTicker.isLastFrame) {
          animation = idleAnimation;
          position =
              Vector2(spriteSheetWidth / 4, mapSize.y - spriteSheetHeight);
        }
      };

      deadAnimationTicker.onComplete = () {
        if (animation == deadAnimation) {
          animation = idleAnimation;
          position =
              Vector2(spriteSheetWidth / 4, mapSize.y - spriteSheetHeight);
        }
      };
    } else {
      animation = idleAnimation;
      position = Vector2(spriteSheetWidth / 4, mapSize.y - spriteSheetHeight);
      size = Vector2(spriteSheetWidth / 4, spriteSheetHeight / 4);
    }
    game.colisionMeteors = 0;
    game.addConsumibles();

    //position = Vector2(spriteSheetWidth / 4, 0);
  }
}

Colocamos al Player al inicio y refrescamos los overlajes y también sus propiedades asociadas Entonces esta es la propiedad que tenemos que emplear tal cual puedes ver aquí en el load estamos inicializando un objeto de tipo Tinker que era lo que te comentaba antes pero eso aquí ahora tenemos una función que se llama como creaticker que la podemos ejecutar directamente de la animación la animación que por supuesto aquí en este punto se encuentra inicializada ya a partir de aquí tenemos que implementarlo en un state, es decir cada vez que se actualiza el juego básicamente También tenemos que actualizar el Tinker Y esto es para que el Tinker se mantenga vivo básicamente pero la parte interesante es que como te comentaba ahora la lógica que estábamos utilizando antes Esto sí te lo voy a mostrar aquí en el en el proyecto mejor voy a buscarlo acá justamente por acá Bueno aquí está un poquito todavía desordenado el código pero aún así se puede entender ya que hay unas cositas que te quiero mostrar aquí como puedes ver cuando muere Esta es la misma función la de receta le pasamos la bandera de muerte Entonces se va a ejecutar esto que nuevamente este pedazo o esta sección es la que se ejecuta cuando queremos reiniciar el juego por muerte de Player entonces aquí podemos hacerlo básicamente de dos formas la más compleja entre comillas sería frame por frame detectando el último que sería justamente esto Fíjate que aquí ya tenemos todo el control es decir esto se va a ejecutar justamente en base a esta condición cuando se ejecute el último frame por lo tanto sería cuando ya fue completada viene siendo un equivalente entonces cuando ya ocurrió la animación de ver Animation entonces ejecutamos todo esto que es para reposicionar el Player:

      deadAnimationTicker.onFrame = (index) {
        // print("-----" + index.toString());
        if (deadAnimationTicker.isLastFrame) {
          animation = idleAnimation;
          position =
              Vector2(spriteSheetWidth / 4, mapSize.y - spriteSheetHeight);
        }
      };

Y por aquí abajo tenemos la parte de limpiar las conexiones y también aquí agregar todos los consumibles básicamente el reset igualito como lo teníamos anteriormente aquí puedes ver el juego aquí tenemos el dinosaurio voy a dejar que me choquen Recuerda que al principio tenemos un tiempo de ser invulnerable bueno aquí ya agarré unas comidas Dios mío aquí me chocó uno no quiero agarrar el escudo que me chocó otro y vamos a ver que aparezca otro por aquí no hay no para que agarre el escudo ahí murió tal cual puedes ver y ser el recálculo todo Tal cual puedes ver es decir está funcionando correctamente toda esta lógica Aquí también puedes ver depresión Fíjate que aquí aparecen las Bueno Este era de otra impresión que hice cuando puse el vídeo que justamente cuando definimos el Animation creamos el ticker nuevamente Y esto es para reinicializarlo al menos Así es como me funciona actualmente ya que la documentación oficial de todo esto es bastante escasa y bastante difícil de seguir ahí la puedes revisar si tú gustas pero en fin aquí nuevamente como te indicaba la animación indicamos la de muerte porque es la que tiene que ocurrir y cuando termine cuando termine de ocurrir la muerte entonces reposicionamos nuestro carácter Y a partir de Aquí hacemos las operaciones típicas para reinicializar todo el nivel es decir colocar el nuevamente los consumibles y aquí las colisiones reinicializarlas también entonces Esto lo puedes hacer ya sea midiendo frame por frame aquí lo puedes imprimir y vas a ver que esto Se imprime que a la fecha Sería bueno para la animación que hicimos sería n 8 frames Esto lo puedes ver aquí en la inicialización por acá arriba son 8 de 0 a 8 Pero bueno Esto también lo puedes ejecutar cuando se complete aquí en la animación es básicamente lo mismo podemos argumentar esto otro punto importante es que ya no tenemos aquí el receta tampoco hace falta ejecutarlo Recuerda que anteriormente cuando nosotros saltábamos o el Player saltaba cuando estábamos aquí en el hongo o un grupo Bueno voy a buscar aquí por receta mejor receta nosotros teníamos que reiniciar la animación porque si no se ha quedado ahí en el último frame ya esto no es necesario en las últimas versiones de Play por lo tanto ya la puedes remover fíjate que yo aquí Salto y no lo estoy reiniciando ya lo hace automáticamente cosa que siempre tuvo que funcionar así pero en fin bueno volviendo aquí a la de un complete esto ya no haría falta como te comentaba aquí va a haber bueno Exactamente lo mismo que teníamos antes el de indel y reposicionarlo el de 6 no hace falta No sé porque lo tengo ahí podemos comentar esto un momentico reinicializamos todo y esperamos morir nuevamente voy a venirme para acá Aquí me chocó uno que me chocó el otro agarré ella el escudo ahí me chocó uno falta uno más Bueno tengo el escudo otra vez ahí está ahí murió mira que funciona de igual manera y por aquí puedes ver las impresiones de devote que dejé entonces puedes utilizar ya sea este esquema y preguntas por el último frame O si quieres hacer algo entre frames ahí lo puedes hacer o puedes emplear el que sería más lógico que sería el de un cumplido Pero eso viene siendo básicamente todo en resumen tenemos que crear ahora un ticker para controlar y saber el estado en todo momento de la animación en este caso para escuchar cuando se completa La animación de la muerte:

    if (dead) {
      animation = deadAnimation;

      deadAnimationTicker = deadAnimation.createTicker();
      deadAnimationTicker.onFrame = (index) {
        // print("-----" + index.toString());
        if (deadAnimationTicker.isLastFrame) {
          animation = idleAnimation;
          position =
              Vector2(spriteSheetWidth / 4, mapSize.y - spriteSheetHeight);
        }
      };

      deadAnimationTicker.onComplete = () {
        if (animation == deadAnimation) {
          animation = idleAnimation;
          position =
              Vector2(spriteSheetWidth / 4, mapSize.y - spriteSheetHeight);
        }
      };

Y poder ejecutar la misma lógica que teníamos antes de reposicionar nuestro Player por lo demás Todo queda exactamente Igual también recuerda otro cambio lo que se refiere con las animaciones es que ya no hace falta hacerle el reset de las mismas y automáticamente cuando se ejecuta la misma ya se reinicia automáticamente Así que nuevamente no hay necesidad de hacer el reset de manera manual y eso serían todos los cambios que tienes que hacer Así que Recuerda que este cambio lo tienes también el repositorio también Crea una etiqueta asociada para justamente esta versión ya que seguramente en versiones futuras de flame van a seguir cambiando cosas así que bueno Esto es todo por los momentos.

- Andrés Cruz

In english

Andrés Cruz

Desarrollo con Laravel, Django, Flask, CodeIgniter, HTML5, CSS3, MySQL, JavaScript, Vue, Android, iOS, Flutter

Andrés Cruz En Udemy

Acepto recibir anuncios de interes sobre este Blog.