Sempre quis fazer videogames. Meu primeiro aplicativo Android que me ajudou a conseguir meu primeiro emprego foi um jogo simples, feito com visualizações Android. Depois disso, houve muitas tentativas de criar um jogo mais elaborado usando um motor de jogo, mas todas falharam por falta de tempo ou pela complexidade de um framework. Mas quando ouvi pela primeira vez sobre o Flame engine, baseado no Flutter, fui imediatamente atraído por sua simplicidade e suporte multiplataforma, então decidi tentar criar um jogo com ele. Eu queria começar com algo simples, mas desafiador, para ter uma ideia do motor. Esta série de artigos é minha jornada para aprender Flame (e Flutter) e construir um jogo de plataforma básico. Vou tentar torná-lo bem detalhado, então deve ser útil para quem está apenas mergulhando no Flame ou no desenvolvedor de jogos em geral. Ao longo de 4 artigos, vou construir um jogo de rolagem lateral 2D, que inclui: Um personagem que pode correr e pular Uma câmera que segue o jogador Mapa de nível de rolagem, com solo e plataformas Fundo de paralaxe Moedas que o jogador pode coletar e HUD que exibe o número de moedas tela de vitória Na primeira parte, vamos criar um novo projeto Flame, carregar todos os recursos, adicionar um personagem do jogador e ensiná-lo a correr. Configuração do projeto Primeiro, vamos criar um novo projeto. O tutorial oficial faz um ótimo trabalho ao descrever todas as etapas para fazer isso, então apenas siga-o. do jogo Bare Flame Uma coisa a acrescentar: ao configurar o arquivo , você pode atualizar as versões das bibliotecas para as últimas disponíveis ou deixá-las como estão, porque o sinal de circunflexo (^) antes de uma versão garantirá que seu aplicativo use as versões não -versão de quebra. ( ) pubspec.yaml sintaxe de acento circunflexo Se você seguiu todas as etapas, seu arquivo deve ficar assim: main.dart import 'package:flame/game.dart'; import 'package:flutter/widgets.dart'; void main() { final game = FlameGame(); runApp(GameWidget(game: game)); } Ativos Antes de continuarmos, precisamos preparar os recursos que serão usados no jogo. Ativos são imagens, animações, sons, etc. Para os propósitos desta série, usaremos apenas imagens que também são chamadas de sprites no desenvolvimento de jogos. A maneira mais simples de construir um nível de plataforma é usar mapas de blocos e sprites de blocos. Significa que o nível é basicamente uma grade, onde cada célula indica qual objeto/chão/plataforma ela representa. Mais tarde, quando o jogo está rodando, as informações de cada célula são mapeadas para o sprite do tile correspondente. Os gráficos dos jogos construídos com esta técnica podem ser muito elaborados ou muito simples. Por exemplo, em Super Mario bros, você vê que muitos elementos se repetem. Isso porque, para cada ladrilho de chão na grade do jogo, existe apenas uma imagem de chão que o representa. Seguiremos a mesma abordagem e prepararemos uma única imagem para cada objeto estático que tivermos. Também queremos que alguns dos objetos, como o personagem do jogador e as moedas, sejam animados. A animação geralmente é armazenada como uma série de imagens estáticas, cada uma representando um único quadro. Quando a animação está sendo reproduzida, os quadros vão um após o outro, criando a ilusão do objeto em movimento. Agora, a questão mais importante é onde obter os ativos. Claro, você mesmo pode desenhá-los ou encomendá-los a um artista. Além disso, existem muitos artistas incríveis que contribuíram com ativos de jogos para código aberto. Estarei usando da . o pacote Arcade Platformer Assets GrafxKid Normalmente, os recursos de imagem vêm em duas formas: folhas de sprite e sprites individuais. O primeiro é uma imagem grande, contendo todos os recursos do jogo em um. Em seguida, os desenvolvedores de jogos especificam a posição exata do sprite necessário e o mecanismo de jogo o corta da folha. Para este jogo, usarei sprites únicos (exceto animações, é mais fácil mantê-los como uma imagem) porque não preciso de todos os ativos fornecidos na folha de sprite. Esteja você mesmo criando sprites ou obtendo-os de um artista, pode ser necessário cortá-los para torná-los mais adequados para o mecanismo de jogo. Você pode usar ferramentas criadas especificamente para esse fim (como ou qualquer editor gráfico. Eu usei o Adobe Photoshop, porque nessa folha de sprites os sprites tem espaço desigual entre eles, o que dificultou a extração de imagens por ferramentas automáticas, então tive que fazer manualmente. o empacotador de texturas) Você também pode querer aumentar o tamanho dos ativos, mas se não for uma imagem vetorial, o sprite resultante pode ficar embaçado. Uma solução alternativa que descobri que funciona muito bem para pixel art é usar o método de redimensionamento no Photoshop (ou Interpolação definida como Nenhum no Gimp). Mas se seu ativo for mais detalhado, provavelmente não funcionará. Nearest Neighbour (hard edges) Com as explicações prontas, baixe ou prepare o seu próprio e adicione-os à pasta do seu projeto. os recursos que preparei assets/images Sempre que adicionar novos recursos, você precisa registrá-los no arquivo como este: pubspec.yaml flutter: assets: - assets/images/ E a dica para o futuro: se você estiver atualizando ativos já cadastrados, você precisa reiniciar o jogo para ver as alterações. Agora vamos carregar os ativos no jogo. Gosto de ter todos os nomes de recursos em um só lugar, o que funciona muito bem para um jogo pequeno, pois é mais fácil acompanhar tudo e modificar, se necessário. Então, vamos criar um novo arquivo no diretório : lib assets.dart const String THE_BOY = "theboy.png"; const String GROUND = "ground.png"; const String PLATFORM = "platform.png"; const String MIST = "mist.png"; const String CLOUDS = "clouds.png"; const String HILLS = "hills.png"; const String COIN = "coin.png"; const String HUD = "hud.png"; const List<String> SPRITES = [THE_BOY, GROUND, PLATFORM, MIST, CLOUDS, HILLS, COIN, HUD]; E então crie outro arquivo, que conterá toda a lógica do jogo no futuro: game.dart import 'package:flame/game.dart'; import 'assets.dart' as Assets; class PlatformerGame extends FlameGame { @override Future<void> onLoad() async { await images.loadAll(Assets.SPRITES); } } é a classe principal que representa nosso jogo, ela estende , a classe de jogo base usada no motor Flame. O que, por sua vez, estende - o bloco de construção básico do Flame. Tudo em seu jogo, incluindo imagens, interface ou efeitos são Componentes. Cada possui um método assíncrono , que é chamado na inicialização do componente. Normalmente, toda a lógica de configuração do componente vai para lá. PlatformerGame FlameGame Component Component onLoad Por fim, importamos nosso arquivo que criamos anteriormente e adicionamos para declarar explicitamente de onde vêm nossas constantes de assets. E usei o método para carregar todos os recursos listados na lista para o cache de imagens do jogo. assets.dart as Assets images.loadAll SPRITES Então, precisamos criar nosso novo a partir de . Modifique o arquivo da seguinte maneira: PlatformerGame main.dart import 'package:flame/game.dart'; import 'package:flutter/widgets.dart'; import 'game.dart'; void main() { runApp( const GameWidget<PlatformerGame>.controlled( gameFactory: PlatformerGame.new, ), ); } Toda a preparação é feita e a parte divertida começa. Adicionando personagem do jogador Crie uma nova pasta e um novo arquivo dentro dela. Este será o componente que representa o personagem do jogador: The Boy. lib/actors/ theboy.dart import '../game.dart'; import '../assets.dart' as Assets; import 'package:flame/components.dart'; class TheBoy extends SpriteAnimationComponent with HasGameRef<PlatformerGame> { TheBoy({ required super.position, // Position on the screen }) : super( size: Vector2.all(48), // Size of the component anchor: Anchor.bottomCenter // ); @override Future<void> onLoad() async { animation = SpriteAnimation.fromFrameData( game.images.fromCache(Assets.THE_BOY), SpriteAnimationData.sequenced( amount: 1, // For now we only need idle animation, so we load only 1 frame textureSize: Vector2.all(20), // Size of a single sprite in the sprite sheet stepTime: 0.12, // Time between frames, since it's a single frame not that important ), ); } } A classe estende que é um componente usado para sprites animados e possui um mixin que nos permite referenciar o objeto do jogo para carregar imagens do cache do jogo ou obter variáveis globais posteriormente. SpriteAnimationComponent HasGameRef Em nosso método , criamos uma nova a partir da folha de sprite que declaramos no arquivo . onLoad SpriteAnimation THE_BOY assets.dart Agora vamos adicionar nosso jogador ao jogo! Retorne ao arquivo e adicione o seguinte ao final do método : game.dart onLoad final theBoy = TheBoy(position: Vector2(size.x / 2, size.y / 2)); add(theBoy); Se você executar o jogo agora, poderemos conhecer o Garoto! movimento do jogador Primeiro, precisamos adicionar a capacidade de controlar The Boy a partir do teclado. Vamos adicionar o mixin ao arquivo . HasKeyboardHandlerComponents game.dart class PlatformerGame extends FlameGame with HasKeyboardHandlerComponents Em seguida, vamos retornar ao mixin e : theboy.dart KeyboardHandler class TheBoy extends SpriteAnimationComponent with KeyboardHandler, HasGameRef<PlatformerGame> Em seguida, adicione algumas novas variáveis de classe ao componente : TheBoy final double _moveSpeed = 300; // Max player's move speed int _horizontalDirection = 0; // Current direction the player is facing final Vector2 _velocity = Vector2.zero(); // Current player's speed Por fim, vamos sobrescrever o método que permite ouvir as entradas do teclado: onKeyEvent @override bool onKeyEvent(RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed) { _horizontalDirection = 0; _horizontalDirection += (keysPressed.contains(LogicalKeyboardKey.keyA) || keysPressed.contains(LogicalKeyboardKey.arrowLeft)) ? -1 : 0; _horizontalDirection += (keysPressed.contains(LogicalKeyboardKey.keyD) || keysPressed.contains(LogicalKeyboardKey.arrowRight)) ? 1 : 0; return true; } Agora é igual a 1 se o jogador se mover para a direita, -1 se o jogador se mover para a esquerda e 0 se o jogador não se mover. No entanto, ainda não podemos vê-lo na tela, porque a posição do jogador ainda não foi alterada. Vamos corrigir isso adicionando o método . _horizontalDirection update Agora preciso explicar o que é o loop do jogo. Basicamente, significa que o jogo está sendo executado em um loop infinito. A cada iteração, o estado atual é renderizado no método e então um novo estado é calculado no método . O parâmetro na assinatura do método é o tempo em milissegundos desde a última atualização de estado. Com isso em mente, adicione o seguinte ao : render Component's update dt theboy.dart @override void update(double dt) { super.update(dt); _velocity.x = _horizontalDirection * _moveSpeed; position += _velocity * dt; } Para cada ciclo de loop do jogo, atualizamos a velocidade horizontal, usando a direção atual e a velocidade máxima. Em seguida, alteramos a posição do sprite com o valor atualizado multiplicado por . dt Por que precisamos da última parte? Bem, se você atualizar a posição apenas com velocidade, o sprite voará para o espaço. Mas podemos apenas usar o menor valor de velocidade, você pode perguntar? Podemos, mas a maneira como o jogador se move será diferente com diferentes taxas de quadros por segundo (FPS). O número de quadros (ou loops de jogo) por segundo depende do desempenho do jogo e do hardware em que ele é executado. Quanto melhor o desempenho do dispositivo, maior o FPS e mais rápido o jogador se move. Para evitar isso, fazemos a velocidade depender do tempo passado desde o último quadro. Dessa forma, o sprite se moverá de maneira semelhante em qualquer FPS. Ok, se executarmos o jogo agora, veremos isso: Incrível, agora vamos fazer o menino virar quando for para a esquerda. Adicione isso ao final do método : update if ((_horizontalDirection < 0 && scale.x > 0) || (_horizontalDirection > 0 && scale.x < 0)) { flipHorizontally(); } Lógica bastante fácil: verificamos se a direção atual (a seta que o usuário está pressionando) é diferente da direção do sprite e, em seguida, invertemos o sprite ao longo do eixo horizontal. Agora vamos também adicionar animação em execução. Primeiro defina duas novas variáveis de classe: late final SpriteAnimation _runAnimation; late final SpriteAnimation _idleAnimation; Em seguida, atualize assim: onLoad @override Future<void> onLoad() async { _idleAnimation = SpriteAnimation.fromFrameData( game.images.fromCache(Assets.THE_BOY), SpriteAnimationData.sequenced( amount: 1, textureSize: Vector2.all(20), stepTime: 0.12, ), ); _runAnimation = SpriteAnimation.fromFrameData( game.images.fromCache(Assets.THE_BOY), SpriteAnimationData.sequenced( amount: 4, textureSize: Vector2.all(20), stepTime: 0.12, ), ); animation = _idleAnimation; } Aqui extraímos a animação inativa adicionada anteriormente à variável de classe e definimos uma nova variável de animação de execução. Em seguida, vamos adicionar um novo método : updateAnimation void updateAnimation() { if (_horizontalDirection == 0) { animation = _idleAnimation; } else { animation = _runAnimation; } } E, finalmente, invoque esse método na parte inferior do método e execute o jogo. update Conclusão É isso para a primeira parte. Aprendemos como configurar um jogo Flame, onde encontrar recursos, como carregá-los em seu jogo e como criar um personagem animado incrível e movê-lo com base nas entradas do teclado. O código desta parte pode ser encontrado . no meu github No próximo artigo, abordarei como criar um nível de jogo usando Tiled, como controlar a câmera Flame e adicionar um plano de fundo paralaxe. Fique atento! Recursos No final de cada parte, adicionarei uma lista de criadores e recursos incríveis com os quais aprendi. Ativos do jogo de plataforma GrafxKid's Arcade https://opengameart.org/content/arcade-platformer-assets Série de desenvolvimento de jogos DevKage Flame: https://youtu.be/mSPalRqZQS8 Canal Craig Oda https://youtu.be/hwQpBuZoV9s Tutorial do jogo Ember Quest https://github.com/flame-engine/flame/blob/main/doc/tutorials/platformer/platformer.md Documentação do Flame Engine https://docs.flame-engine.org/1.6.0/flame/flame.html