← Nesim Apps

Game feel no Jetpack Compose sem motor de jogo

Por Nesim Razon, desenvolvedor Android independente · 2026-11-12

#Jetpack Compose#Design de jogos#Android

O Cubbles é um quebra-cabeça de encaixar blocos com carinhas simpáticas, e quase todo o seu charme é resposta ao jogador: uma peça que assenta com um quique, uma linha que derrete em onda, um som e uma vibração que crescem com a sua sequência. Nada disso precisa de um motor de jogo. Este texto mostra como se faz com Jetpack Compose puro, quais são os números, onde estão os atalhos e como outros dois jogos nossos resolvem o mesmo problema de outro jeito.

Por que um quebra-cabeça não precisa de motor

Um tabuleiro de 8 por 8, três peças numa bandeja e um gesto de arrastar não precisam de física, de grafo de cena nem de um framework de laço de jogo. Precisam de um relógio e de uma forma de desenhar. O Compose oferece as duas coisas: um Canvas para pintar e uma chamada por quadro de onde ler o tempo. Tudo o que vem a seguir se apoia nessas duas peças.

Um Canvas, um relógio

A tela do jogo desenha o tabuleiro, a bandeja, os efeitos e os textos flutuantes num único Canvas. Uma corrotina iniciada com um efeito lançado roda para sempre: espera o próximo quadro com withFrameNanos, ignora o tempo de quadro recebido, grava o tempo de atividade atual em milissegundos numa variável de estado chamada now e remove de uma lista os efeitos que terminaram.

O detalhe importante é onde now é lido. Ele é lido dentro da lambda de desenho do Canvas, então cada quadro invalida apenas o desenho e não a composição da tela inteira. O texto da pontuação e os botões em volta do tabuleiro não são recompostos sessenta vezes por segundo. O custo desse desenho é claro: o laço nunca para, mesmo sem efeito em andamento, porque os olhos que piscam e as peças da bandeja que balançam suavemente também precisam de um relógio.

Efeitos são dados, não animações

Em vez de um Animatable por bloco, o jogo mantém uma lista de pequenos objetos de efeito. Existe uma classe selada com instante de início e duração, e subclasses para o quique ao colocar, a limpeza de linha, o texto flutuante, o tremor de tela, a rajada de partículas, o clarão de cor e a luz que varre. Eles são criados no instante em que você solta a peça e acrescentados à lista.

O código de desenho transforma cada um num valor de progresso entre zero e um, subtraindo o início de now e dividindo pela duração, e deriva tudo desse número. Não há corrotina por efeito, nada a cancelar e nada que possa sair de sincronia com o tabuleiro.

Squash, stretch e a onda

Uma peça colocada quica por 480 milissegundos. Sua escala é um mais 0,28 vezes um seno sobre a primeira parte da animação, multiplicado por um menos o progresso, de modo que ela incha e assenta com uma amplitude que vai se apagando. Para ser exato, é um inchaço amortecido e não um squash and stretch que preserva o volume: os blocos são quadrados arredondados escalados de modo uniforme, o que é barato e se lê bem no tamanho de um celular.

A personalidade está na animação de limpeza. Cada célula limpa espera 28 milissegundos por passo de distância Manhattan a partir do centro da peça que você colocou, então a limpeza se espalha para fora como uma ondulação e diz ao seu olho onde começou. Depois a célula incha até 1,4 vez o seu tamanho nos primeiros 30 por cento de 380 milissegundos, encolhe até sumir, mistura-se ao branco, perde opacidade e usa uma carinha feliz até 70 por cento do caminho. Uma única função smoothstep, três t ao quadrado menos dois t ao cubo, é a única curva de suavização usada em todo o arquivo.

Rostos são só alguns arcos

Cada bloco tem um rosto desenhado com círculos, linhas e arcos, com um humor escolhido a cada quadro: normal, feliz, empolgado ou triste. Piscar não precisa de estado. Os olhos se fecham quando o tempo atual mais o índice da célula vezes 911, tomado módulo 3700 milissegundos, é menor que 130, então cada bloco pisca no seu próprio ritmo. Os olhos também se deslocam quatro por cento do tamanho do bloco em direção ao dedo enquanto você arrasta, um detalhe pequeno que faz o tabuleiro parecer vivo.

Partículas sem sistema de partículas

Aqui uma partícula é um objeto imutável com posição inicial, velocidade, tamanho, cor e giro. Nada é atualizado por quadro. O código de desenho calcula a posição em forma fechada a partir do tempo decorrido: início mais velocidade vezes t, com um termo de gravidade de meio g vezes t ao quadrado no eixo vertical, em que g vale dezesseis alturas de célula por segundo ao quadrado. Não há reserva de objetos nem estado por partícula para manter.

As quantidades são limitadas por projeto. Cada célula limpa emite 2 mais o nível de empolgação, no máximo 4, partículas, em que a empolgação é linhas limpas mais a sequência menos um. Um tabuleiro tem 64 células, então uma única rajada não passa de 384 partículas. Uma limpeza perfeita acrescenta 90 peças de confete e um novo recorde acrescenta 70. Uma rajada vive 1,1 segundo, encolhe até 40 por cento do tamanho e some.

Texto flutuante que continua legível

Os textos flutuantes, como a pontuação, a sequência e as palavras de elogio, sobem com o mesmo smoothstep, surgem com um rápido excesso e ficam 1,9 segundo. As palavras são desenhadas com um contorno escuro feito de oito cópias deslocadas, porque texto branco sobre um fundo claro de azulejo de banheiro não se lê sem ele. Aqui também morou um erro real: em telas muito pequenas uma palavra de elogio longa era mais larga que o espaço disponível, e uma chamada de limitação com o intervalo invertido derrubava o aplicativo. A correção foi uma pequena função auxiliar que nunca inverte o intervalo, mais reduzir o texto até caber.

Som sem arquivos de som

Não há recursos de áudio. No primeiro uso, um som é sintetizado numa matriz curta de amostras de 16 bits a 22.050 Hz em mono: um seno na frequência da nota mais um segundo harmônico a 20 por cento, multiplicado por um decaimento exponencial, com uma rampa de ataque de 60 amostras, cerca de três milissegundos, para evitar o estalo de começar em amplitude total. As matrizes ficam em cache por nome; cada reprodução cria um AudioTrack em modo estático com essa matriz, marcado como uso de jogo, e o libera a partir de um marcador de posição quando termina.

Vibração que dá para sentir de verdade

A classe de vibração controla o vibrador diretamente. Um comentário no código explica o motivo: a chamada háptica padrão das views depende da configuração de resposta ao toque do usuário e quase não era percebida em muitos celulares na primeira versão. Em vez disso o jogo monta suas próprias formas de onda, com controle de amplitude quando o hardware informa e só com tempos quando não, e envolve a chamada para que um aparelho sem vibrador nunca cause uma falha.

A regra que mantém a honestidade

Adicionar resposta ao jogador é barato e é fácil exagerar, por isso as notas do projeto fixam uma regra: nenhum efeito por si só. O quebra-cabeça tem de continuar legível e um mau momento nunca deve parecer um castigo. No código isso aparece assim. Antes de soltar, surgem um fantasma da peça e um realce das linhas que ela limparia. A peça arrastada fica 1,6 célula acima do dedo para que a mão não a esconda. As bandejas são geradas de modo que sempre exista uma jogada legal. E a tela de fim de jogo só diz o quão perto você ficou da sua melhor pontuação quando está dentro de 60 por cento dela; caso contrário, mostra apenas o recorde, para que uma partida ruim não termine em bronca.

Uma lacuna, dita sem rodeios: o Cubbles ainda não tem uma chave para reduzir o movimento. Nosso jogo de bingo tem, e mostra o padrão. A configuração é exposta por um composition local, e o confete e as animações em laço da tela inicial a consultam e se retiram.

As mesmas ideias em outros dois jogos

Quando a tela é feita de widgets em vez de um único Canvas, a API de animação padrão se encaixa melhor. No jogo de bingo, a bola sorteada surge com um Animatable e uma mola saltitante, escalando de 0,2 a 1 enquanto gira. A marca na cartela usa uma chamada animate-as-state com uma mola de amortecimento 0,42 para assentar com um tremor. Um toque rejeitado incrementa uma chave inteira, e um efeito lançado sobre essa chave fixa um valor de tremor em um e o devolve a zero com uma mola, lido dentro de uma graphics layer para que nada seja recomposto.

Num quebra-cabeça espacial nosso, o laço de quadros aproxima cada portal, a cada quadro, de uma fração fixa de 38 por cento da distância restante até a célula de destino. É a suavização mais simples possível e dá uma boa sensação, com a ressalva de que a velocidade depende da taxa de quadros. Uma regra prática dos três: API de animação para widgets e desenho derivado do tempo para cenas livres.

O que levar daqui

Guarde os efeitos como pequenos objetos de dados com início e duração, derive tudo do progresso, leia o relógio dentro da lambda de desenho, ponha um teto em cada efeito e deixe as regras do jogo andarem na frente da animação. Sai uma quantidade surpreendente de vida de algumas centenas de linhas de Canvas e uma função seno. Se quiser ver esses números em ação, o Cubbles é o jogo para o qual eles foram ajustados.

Perguntas frequentes

Dá para criar game feel no Jetpack Compose sem motor de jogo?

Sim, em quebra-cabeças e jogos casuais. Um Canvas, um laço de quadros com withFrameNanos e desenho derivado do tempo cobrem squash, partículas e tremor.

Como evitar recompor a tela inteira a cada quadro?

Lendo o estado do relógio apenas dentro da lambda de desenho do Canvas. Assim cada quadro invalida o desenho e não a composição.

Como os sons são feitos sem arquivos de áudio?

São sintetizados uma vez em matrizes curtas de amostras, guardados em cache e tocados com um AudioTrack em modo estático liberado ao fim da reprodução.

Por que controlar o vibrador diretamente?

A chamada háptica padrão depende da configuração de resposta ao toque do usuário e quase não era sentida em muitos celulares, então o jogo monta suas próprias formas de onda.

Mais resposta ao jogador sempre melhora um jogo?

Não. Os efeitos não devem esconder o quebra-cabeça nem punir o jogador, por isso cada efeito aqui tem um limite e a tela final evita mensagens desanimadoras.

Mais do blog