paint-brush
Ensinando seu personagem a correr em chamaspor@eugene-kleshnin
3,841 leituras
3,841 leituras

Ensinando seu personagem a correr em chamas

por Eugene Kleshnin11m2023/02/28
Read on Terminal Reader

Muito longo; Para ler

Esta série de artigos é minha jornada para aprender Flame (e Flutter) e construir um jogo de plataforma básico. Vou tentar torná-lo bastante detalhado, por isso deve ser útil para quem está apenas mergulhando no Flame ou no desenvolvedor de jogos em geral. Na primeira parte, vamos criar um novo projeto Flame, carregar todos os recursos, adicionar um personagem do jogador e ensiná-lo a correr.
featured image - Ensinando seu personagem a correr em chamas
Eugene Kleshnin HackerNoon profile picture
0-item

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


jogo completo


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 do jogo Bare Flame faz um ótimo trabalho ao descrever todas as etapas para fazer isso, então apenas siga-o.

Uma coisa a acrescentar: ao configurar o arquivo pubspec.yaml , 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. ( sintaxe de acento circunflexo )

Se você seguiu todas as etapas, seu arquivo main.dart deve ficar assim:

 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.


O nível é construído com ladrilhos repetidos


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 o pacote Arcade Platformer Assets da 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.



Um único sprite que representa o solo


Folha de sprite com 6 sprites para as animações do jogador


Executar animação


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 o empacotador de texturas) 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.


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 Nearest Neighbour (hard edges) no Photoshop (ou Interpolação definida como Nenhum no Gimp). Mas se seu ativo for mais detalhado, provavelmente não funcionará.


Com as explicações prontas, baixe os recursos que preparei ou prepare o seu próprio e adicione-os à pasta assets/images do seu projeto.


Sempre que adicionar novos recursos, você precisa registrá-los no arquivo pubspec.yaml como este:

 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); } }


PlatformerGame é a classe principal que representa nosso jogo, ela estende FlameGame , a classe de jogo base usada no motor Flame. O que, por sua vez, estende Component - o bloco de construção básico do Flame. Tudo em seu jogo, incluindo imagens, interface ou efeitos são Componentes. Cada Component possui um método assíncrono onLoad , que é chamado na inicialização do componente. Normalmente, toda a lógica de configuração do componente vai para lá.


Por fim, importamos nosso arquivo assets.dart que criamos anteriormente e adicionamos as Assets para declarar explicitamente de onde vêm nossas constantes de assets. E usei o método images.loadAll para carregar todos os recursos listados na lista SPRITES para o cache de imagens do jogo.


Então, precisamos criar nosso novo PlatformerGame a partir de main.dart . Modifique o arquivo da seguinte maneira:

 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 lib/actors/ e um novo arquivo theboy.dart dentro dela. Este será o componente que representa o personagem do jogador: The Boy.

 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 SpriteAnimationComponent que é um componente usado para sprites animados e possui um mixin HasGameRef que nos permite referenciar o objeto do jogo para carregar imagens do cache do jogo ou obter variáveis globais posteriormente.


Em nosso método onLoad , criamos uma nova SpriteAnimation a partir da folha de sprite THE_BOY que declaramos no arquivo assets.dart .


Agora vamos adicionar nosso jogador ao jogo! Retorne ao arquivo game.dart e adicione o seguinte ao final do método onLoad :

 final theBoy = TheBoy(position: Vector2(size.x / 2, size.y / 2)); add(theBoy);

Se você executar o jogo agora, poderemos conhecer o Garoto!


Conheça o Garoto

movimento do jogador

Primeiro, precisamos adicionar a capacidade de controlar The Boy a partir do teclado. Vamos adicionar o mixin HasKeyboardHandlerComponents ao arquivo game.dart .

 class PlatformerGame extends FlameGame with HasKeyboardHandlerComponents


Em seguida, vamos retornar ao mixin theboy.dart e 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 onKeyEvent que permite ouvir as entradas do teclado:

 @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 _horizontalDirection é 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 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 render Component's e então um novo estado é calculado no método update . O parâmetro dt na assinatura do método é o tempo em milissegundos desde a última atualização de estado. Com isso em mente, adicione o seguinte ao 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:


O menino muda de posição ao longo do eixo X


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 onLoad assim:

 @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 update e execute o jogo.

Executando a animação e invertendo o sprite


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.