Esta é a última parte da minha série de 4 partes, onde aprendo como construir um jogo de plataforma simples com o motor Flame. Já sabemos como adicionar um personagem de jogador animado que chamei de The Boy, como criar um nível de jogo rolável usando o editor Tiled e como adicionar gravidade e pular com a ajuda da detecção de colisão (partes 1 , 2 , 3 ).
Nesta parte, vamos adicionar moedas, que podem ser coletadas pelo personagem, HUD para mostrar quantas moedas o jogador tem, e uma tela de vitória, que mostraremos quando todas as moedas forem coletadas.
Precisamos dizer ao jogo onde gerar moedas (ou qualquer outro objeto do jogo). Como você deve ter adivinhado, vamos usar o editor Tiled para adicionar outra camada de objetos, de maneira semelhante à que adicionamos Plataformas, mas com duas diferenças:
Com as plataformas, inserimos imagens sprite nos dados de nível. Este método não é muito adequado para moedas, porque queremos remover o objeto assim que o jogador o coletar. Então vamos apenas adicionar pontos de spawn, para saber onde as moedas devem aparecer no jogo, e a renderização será feita usando os componentes do Flame.
Para as plataformas, usamos retângulos de qualquer tamanho, mas para as moedas, vamos adicionar pontos de spawn do tamanho de 1 ladrilho. No entanto, se você quiser adicionar uma linha de moedas uma após a outra (oi Mario), poderá fazê-lo facilmente modificando o código do jogo e considerando o tamanho de um ponto de geração ao adicionar objetos de moeda. Mas, para os propósitos desta série, assumimos que os pontos de geração de moedas são 1x1.
Abra nosso nível no editor Tiled e crie uma nova camada de objeto chamada Coins. Em seguida, usando a ferramenta Retangular, adicione vários pontos de spawn no mapa para adicionarmos componentes de moeda usando o mecanismo de jogo. Meu nível agora está assim:
Devo acrescentar que nosso jogo é bastante simples e sabemos que esses retângulos vazios se transformarão em moedas durante a execução do jogo. Mas se quisermos adicionar mais tipos de objetos, será difícil distingui-los. Felizmente, o Tiled tem uma ferramenta para isso, chamada “Insert Tile”, que pode adicionar dicas visuais para cada objeto, mas essas imagens não serão renderizadas no jogo.
Tudo bem, salve o nível e retorne ao IDE. Vamos adicionar nossa classe Coin
à pasta /objects/
.
class Coin extends SpriteAnimationComponent with HasGameRef<PlatformerGame> { late final SpriteAnimation spinAnimation; late final SpriteAnimation collectAnimation; Coin(Vector2 position) : super(position: position, size: Vector2.all(48)); @override Future<void> onLoad() async { spinAnimation = SpriteAnimation.fromFrameData( game.images.fromCache(Assets.COIN), SpriteAnimationData.sequenced( amount: 4, textureSize: Vector2.all(16), stepTime: 0.12, ), ); collectAnimation = SpriteAnimation.fromFrameData( game.images.fromCache(Assets.COIN), SpriteAnimationData.range( start: 4, end: 7, amount: 8, textureSize: Vector2.all(16), stepTimes: List.filled(4, 0.12), loop: false ), ); animation = spinAnimation; final hitbox = RectangleHitbox() ..collisionType = CollisionType.passive; add(hitbox); return super.onLoad(); } }
Temos 2 animações diferentes para girar e coletar e um RectangleHitbox
, para verificar colisões com o jogador posteriormente.
Em seguida, retorne ao game.dart
e modifique o método spawnObjects
para gerar nossas moedas:
final coins = tileMap.getLayer<ObjectGroup>("Coins"); for (final coin in coins!.objects) { add(Coin(Vector2(coin.x, coin.y))); }
Execute o jogo e veja as moedas adicionadas:
Volte para coin.dart
e adicione o método collect
:
void collect() { animation = collectAnimation; collectAnimation.onComplete = () => { removeFromParent() }; }
Quando esse método for chamado, vamos mudar a animação giratória para a de coleta e, quando terminar, vamos remover esse componente do jogo.
Em seguida, vá para a classe theboy.dart
e sobrescreva o método onCollisionStart
:
@override void onCollisionStart(Set<Vector2> intersectionPoints, PositionComponent other) { if (other is Coin) { other.collect(); } super.onCollisionStart(intersectionPoints, other); }
A razão pela qual usamos onCollisionStart
em vez de onCollision
é que queremos que o retorno de chamada de colisão seja acionado apenas uma vez.
As moedas agora estão desaparecendo em colisão com o menino. Vamos adicionar a interface do usuário para rastrear o número de moedas coletadas.
HUD, ou heads-up display, é simplesmente uma barra de status que exibe qualquer informação sobre o jogo: pontos de vida, munição, etc. Vamos exibir um ícone de moeda para cada moeda coletada.
Para simplificar, vou armazenar o número de moedas em uma variável, mas para interfaces mais complexas, considere usar um pacote flame_bloc , que permite atualizar e observar o estado do jogo de maneira conveniente.
Adicione uma nova classe que conterá a lógica do HUD: lib/hud.dart
class Hud extends PositionComponent with HasGameRef<PlatformerGame> { Hud() { positionType = PositionType.viewport; } void onCoinsNumberUpdated(int total) { final coin = SpriteComponent.fromImage( game.images.fromCache(Assets.HUD), position: Vector2((50 * total).toDouble(), 50), size: Vector2.all(48)); add(coin); } }
Duas coisas interessantes aqui:
positionType
como PositionType.viewport
para fixar nosso HUD no canto da tela. Se não fizermos isso, devido ao movimento da câmera, o HUD estará se movendo com o nível.onCoinsNumberUpdated
será chamado toda vez que o jogador coletar a moeda. Ele usa o parâmetro total
para calcular o deslocamento do próximo ícone de moeda e, em seguida, adiciona um novo sprite de moeda à posição calculada.
Em seguida, retorne ao arquivo game.dart
e adicione novas variáveis de classe:
int _coins = 0; // Keeps track of collected coins late final Hud hud; // Reference to the HUD, to update it when the player collects a coin
Em seguida, adicione o componente Hud
na parte inferior do método onLoad
:
hud = Hud(); add(hud);
E adicione um novo método:
void onCoinCollected() { _coins++; hud.onCoinsNumberUpdated(_coins); }
Por fim, chame-o do método collect
de Coin
:
void collect() { game.onCoinCollected(); animation = collectAnimation; collectAnimation.onComplete = () => { removeFromParent() }; }
Brilhante, nosso HUD agora mostra quantas moedas coletamos!
A última coisa que quero adicionar é a tela de vitória, que será exibida assim que o jogador coletar todas as moedas.
Adicione uma nova const à classe PlatformerGame
:
late int _totalCoins;
E atribua a ele o número de moedas que temos no nível. Adicione esta linha ao final do método spawnObjects
:
_totalCoins = coins.objects.length;
E adicione isso ao final do método onCoinCollected
.
Observe que pode ser necessário adicionar import 'package:flutter/material.dart';
manualmente.
if (_coins == _totalCoins) { final text = TextComponent( text: 'U WIN!', textRenderer: TextPaint( style: TextStyle( fontSize: 200, fontWeight: FontWeight.bold, color: Colors.white, ), ), anchor: Anchor.center, position: camera.viewport.effectiveSize / 2, )..positionType = PositionType.viewport; add(text); Future.delayed(Duration(milliseconds: 200), () => { pauseEngine() }); }
Aqui, verifico se o contador de moedas é igual ao número de moedas no nível e, se for, adiciono um rótulo de vitória no topo da tela do jogo. Em seguida, pause o jogo para interromper o movimento do jogador. Também adicionei um atraso de 200 ms, para que o rótulo seja renderizado antes da pausa.
Deve ficar assim:
E esse é o jogo! Claro, não parece um jogo acabado agora, mas com tudo o que expliquei, deve ser bastante fácil adicionar mais níveis, inimigos ou outros itens colecionáveis.
Esta parte conclui a série.
O mecanismo Flame tem muito mais a oferecer que eu não mencionei, incluindo o mecanismo de física Forge2D, partículas, efeitos, menus de jogo, áudio etc. ter uma compreensão de como construir jogos mais complexos.
O Flame é uma ferramenta poderosa, mas fácil de usar e aprender. É modular, o que permite trazer outras coisas legais como Box2D e é mantido ativamente. Mas uma das maiores vantagens do Flame é que ele é construído sobre o Flutter, o que significa que oferece suporte multiplataforma com um pouco de trabalho adicional. No entanto, ser uma extensão do Flutter significa que todos os problemas do Flutter também persistem no Flame. Por exemplo, o bug de antialiasing do Flutter está aberto há vários anos sem resolução, e você também pode notá-lo no jogo que construímos. Mas, no geral, é uma ótima ferramenta para criar jogos que vale a pena experimentar.
O código completo deste tutorial você encontra no meu github
No final de cada parte, adicionarei uma lista de criadores e recursos incríveis com os quais aprendi.