Sensación de juego en Jetpack Compose sin motor de juego
Por Nesim Razon, desarrollador Android independiente · 2026-11-12
Cubbles es un puzle de colocar bloques con caras simpáticas, y casi todo su encanto es respuesta al jugador: una pieza que se asienta con un rebote, una fila que se deshace en ola, un sonido y una vibración que crecen con tu racha. Nada de eso necesita un motor de juego. Esta entrada explica cómo se hace con Jetpack Compose a secas, qué números se usan, dónde están los atajos y cómo otros dos juegos nuestros resuelven el mismo problema de otra manera.
Por qué un puzle no necesita motor
Un tablero de 8 por 8, tres piezas en una bandeja y un gesto de arrastre no necesitan física, un grafo de escena ni un marco de bucle de juego. Necesitan un reloj y una forma de dibujar. Compose da las dos cosas: un Canvas donde pintar y una llamada por fotograma de la que leer el tiempo. Todo lo que sigue se apoya en esas dos piezas.
Un Canvas, un reloj
La pantalla de juego dibuja el tablero, la bandeja, los efectos y los textos flotantes en un único Canvas. Una corrutina lanzada con un efecto lanzado da vueltas para siempre: espera el siguiente fotograma con withFrameNanos, ignora el tiempo de fotograma que recibe, escribe el tiempo de actividad actual en milisegundos en una variable de estado llamada now y quita de una lista los efectos terminados.
Lo importante es dónde se lee now. Se lee dentro de la lambda de dibujo del Canvas, así que cada fotograma invalida solo el dibujo y no la composición de toda la pantalla. El texto de la puntuación y los botones alrededor del tablero no se recomponen sesenta veces por segundo. El coste de este diseño es evidente: el bucle no se detiene nunca, ni siquiera sin efectos activos, porque los ojos que parpadean y las piezas de la bandeja que se mecen suavemente también necesitan un reloj.
Los efectos son datos, no animaciones
En lugar de un Animatable por bloque, el juego mantiene una lista de pequeños objetos de efecto. Hay una clase sellada con un instante de inicio y una duración, y subclases para el rebote al colocar, la limpieza de línea, el texto flotante, la sacudida de pantalla, la ráfaga de partículas, el destello de color y la luz que barre. Se crean en el instante en que sueltas una pieza y se añaden a la lista.
El código de dibujo convierte cada uno en un valor de progreso entre cero y uno, restando el inicio a now y dividiendo por la duración, y deriva todo de ese número. No hay una corrutina por efecto, nada que cancelar y nada que pueda desincronizarse del tablero.
Squash, stretch y la ola
Una pieza colocada rebota durante 480 milisegundos. Su escala es uno más 0,28 por un seno sobre la primera parte de la animación, multiplicado por uno menos el progreso, de modo que se hincha y se asienta con una amplitud que se apaga.
La personalidad está en la animación de limpieza. Cada celda limpiada espera 28 milisegundos por cada paso de distancia Manhattan desde el centro de la pieza que colocaste, así que la limpieza se extiende hacia fuera como una onda y le dice a tu ojo dónde empezó. Después la celda se hincha hasta 1,4 veces su tamaño durante el primer 30 por ciento de 380 milisegundos, se encoge hasta desaparecer, se mezcla hacia el blanco, pierde opacidad y lleva una cara feliz hasta el 70 por ciento del recorrido. Una sola función smoothstep, tres t al cuadrado menos dos t al cubo, es la única curva de suavizado de todo el archivo.
Las caras son unos pocos arcos
Cada bloque tiene una cara dibujada con círculos, líneas y arcos, con un estado de ánimo elegido en cada fotograma: normal, feliz, emocionado o triste. Parpadear no necesita estado. Los ojos se cierran cuando el tiempo actual más el índice de la celda por 911, tomado módulo 3700 milisegundos, es menor que 130, así que cada bloque parpadea según su propio calendario. Además los ojos se desplazan un cuatro por ciento del tamaño del bloque hacia el dedo mientras arrastras, un detalle pequeño que hace que el tablero parezca vivo.
Partículas sin sistema de partículas
Aquí una partícula es un objeto inmutable con posición inicial, velocidad, tamaño, color y giro. No se actualiza nada por fotograma. El código de dibujo calcula la posición en forma cerrada a partir del tiempo transcurrido: inicio más velocidad por t, con un término de gravedad de un medio de g por t al cuadrado en el eje vertical, donde g son dieciséis alturas de celda por segundo al cuadrado. No hay reserva de objetos ni estado por partícula que mantener.
Las cantidades están acotadas por diseño. Cada celda limpiada emite 2 más el nivel de emoción, con un máximo de 4, partículas, donde la emoción es líneas limpiadas más la racha menos uno. Un tablero tiene 64 celdas, así que una sola ráfaga no puede pasar de 384 partículas. Una limpieza perfecta añade 90 piezas de confeti y un récord nuevo añade 70. Una ráfaga vive 1,1 segundos, se encoge hasta el 40 por ciento de su tamaño y se desvanece.
Texto flotante que sigue siendo legible
Los textos flotantes, como la puntuación, la racha y las palabras de elogio, suben con el mismo smoothstep, aparecen con un rápido sobrepaso y permanecen 1,9 segundos. Las palabras se dibujan con un contorno oscuro hecho de ocho copias desplazadas, porque el texto blanco sobre un fondo claro de azulejos de baño no se lee sin él. Aquí también vivió un error real: en pantallas muy pequeñas una palabra de elogio larga era más ancha que el espacio disponible, y una llamada de limitación con el rango invertido hacía fallar la aplicación. La solución fue una pequeña función auxiliar que nunca invierte el rango y reducir el texto hasta que cabe.
Sonido sin archivos de sonido
No hay recursos de audio. En el primer uso, un sonido se sintetiza en una matriz corta de muestras de 16 bits a 22.050 Hz en mono: un seno a la frecuencia de la nota más un segundo armónico al 20 por ciento, multiplicado por una caída exponencial, con una rampa de ataque de 60 muestras, unos tres milisegundos, para evitar el chasquido que produce empezar a plena amplitud. Las matrices se guardan en caché por nombre; cada reproducción crea un AudioTrack en modo estático con esa matriz, marcado como uso de juego, y lo libera desde un marcador de posición al terminar.
Vibración que de verdad se siente
La clase de vibración maneja el vibrador directamente. Un comentario en el código explica el motivo: la llamada háptica estándar de las vistas depende del ajuste de respuesta táctil del usuario y apenas se notaba en muchos móviles en la primera versión. En su lugar el juego construye sus propias formas de onda, con control de amplitud donde el hardware lo declara y solo con tiempos donde no, y envuelve la llamada para que un dispositivo sin vibrador nunca provoque un fallo.
La regla que mantiene la honestidad
Añadir respuesta al jugador es barato y es fácil pasarse, así que las notas del proyecto fijan una regla: ningún efecto por el efecto. El puzle debe seguir siendo legible y un mal momento nunca debe sentirse como un castigo. En el código se ve así. Antes de soltar aparecen un fantasma de la pieza y un resaltado de las filas que limpiaría. La pieza arrastrada se levanta 1,6 celdas sobre el dedo para que la mano no la tape. Las bandejas se generan de modo que siempre quede una jugada legal. Y la pantalla de fin de partida solo dice lo cerca que estuviste de tu mejor puntuación cuando estás dentro del 60 por ciento de ella; si no, solo muestra el récord, de modo que una mala partida no va seguida de una regañina.
Una carencia, dicha sin rodeos: Cubbles todavía no tiene un interruptor para reducir el movimiento. Nuestro juego de bingo sí, y muestra el patrón. El ajuste se expone mediante un composition local, y el confeti y las animaciones en bucle de la pantalla de inicio lo consultan y se retiran.
Las mismas ideas en otros dos juegos
Cuando la pantalla se compone de widgets en lugar de un solo Canvas, encaja mejor la API de animación estándar. En el juego de bingo, la bola extraída aparece con un Animatable y un muelle rebotón, escalando de 0,2 a 1 mientras gira. La marca sobre un cartón de bingo usa una llamada animate-as-state con un muelle de amortiguación 0,42 para asentarse con un temblor. Un toque rechazado incrementa una clave entera, y un efecto lanzado sobre esa clave fija un valor de sacudida en uno y lo devuelve a cero con un muelle, leído dentro de una graphics layer para que nada se recomponga.
En un puzle espacial nuestro, el bucle de fotogramas acerca cada puerta, en cada fotograma, un 38 por ciento fijo de la distancia restante hacia su celda de destino. Es el suavizado más simple posible y se siente bien, con la salvedad de que la velocidad depende de la tasa de fotogramas. Una regla práctica de los tres: la API de animación para widgets y dibujo derivado del tiempo para escenas libres.
Lo que conviene llevarse
Guarda los efectos como pequeños objetos de datos con inicio y duración, deriva todo del progreso, lee tu reloj dentro de la lambda de dibujo, pon un techo a cada efecto y deja que las reglas del juego vayan por delante de la animación. Se obtiene una cantidad sorprendente de vida con unos cientos de líneas de Canvas y una función seno. Si quieres ver estos números en acción, Cubbles es el juego para el que se ajustaron.
Preguntas frecuentes
¿Se puede lograr sensación de juego en Jetpack Compose sin motor de juego?
Sí, en puzles y juegos casuales. Un Canvas, un bucle de fotogramas con withFrameNanos y dibujo derivado del tiempo cubren el squash, las partículas y la sacudida.
¿Cómo se evita recomponer toda la pantalla en cada fotograma?
Leyendo el estado del reloj solo dentro de la lambda de dibujo del Canvas. Así cada fotograma invalida el dibujo y no la composición.
¿Cómo se hacen los sonidos sin archivos de audio?
Se sintetizan una vez en matrices cortas de muestras, se guardan en caché y se reproducen con un AudioTrack en modo estático que se libera al terminar.
¿Por qué se maneja el vibrador directamente?
La llamada háptica estándar depende del ajuste de respuesta táctil del usuario y apenas se sentía en muchos móviles, así que el juego construye sus propias formas de onda.
¿Más respuesta al jugador siempre mejora un juego?
No. Los efectos no deben tapar el puzle ni castigar al jugador, por eso cada efecto aquí tiene un límite y la pantalla final evita mensajes desalentadores.
Más del blog
Cómo garantizamos que un Mahjong Solitario tenga solución
Historia del Mahjong Solitario y su diferencia con el Mahjong- Cómo dar el mismo puzle diario a todos sin un servidor
- Cómo se generan y verifican cientos de niveles de puzle
- Tres en raya: historia de un juego resuelto y sus variantes
- Magnitud e intensidad de un terremoto: cuál es la diferencia