viernes, 27 de marzo de 2026

FFMPEG: Añadiendo pulsaciones del mando a tus gameplays en emulador

Algunos emuladores permiten grabar tus gameplays/speedruns de forma que las pulsaciones del mando se graban y luego se puede volver a reproducir ese mismo gameplay, pudiendo incluso exportar el gameplay a video.

Pues hay gente a la que le gusta ver las pulsaciones del mando a la vez que mira el gameplay, y no hay muchas herramientas para ello. Aquí os enseñare como se puede hacer tal cosa tan solo usando el codificador de video FFMPEG, nada de programas externos. Así es, solamente FFMPEG y sus filtros. Para este ejemplo usaré el emulador de NES Nestopia.

Este emulador en concreto guarda las grabaciones (comprimidas con ZLIB) en bloques de 8 bytes por frame donde cada byte representa uno de los 8 botones del mando de la NES. 0x40 cuando no esta pulsado, 0x41 cuando esta pulsado.
Muy bien, ¿y que hacemos con este archivo en FFMPEG? Pues resulta que podemos abrirlo como rawvideo. Le decimos a FFMPEG que tiene una resolucion de 8x1, que esta en escala de grises (1 byte por pixel), y le adaptamos el brillo y contraste para que se note la diferencia de 0x40 y 0x41.
Ahora si ponemos el video .AVI exportado y el rawvideo del mando uno encima del otro, nos quedaría algo así:

ffmpeg -f rawvideo -pixel_format gray -video_size 8x1 -framerate 60 -i input.dat -i v.avi -filter_complex [0]eq=brightness=1:contrast=6.1:gamma=10,scale=256:-1:flags=neighbor[k];[1][k]vstack v.mp4

¡Nada mal de momento! La sincronización de las pulsaciones en los pixeles de abajo es perfecta, pero ahora nos faltaria visualizarlo como un mando de NES. Para ello lo primero es dibujarnos nuestro mando de NES:

Ahora lo que haremos mediante el filtro geq es mapear las posiciones de los pixeles para que iluminen los botones correspondientes del mando que hemos dibujado. No hace falta complicarnos mucho con la forma exacta de los botones, luego haremos que iluminen solo las zonas grises con blend.
Ahora el resultado es el siguiente:



ffmpeg -f rawvideo -pixel_format gray -video_size 8x1 -framerate 60 -i input.dat -i controller.png -i v.avi -filter_complex [0]scale=256:128:flags=neighbor,eq=brightness=1:contrast=6.1:gamma=10,geq=g='if(gt(X,193),p(0,0),if(gt(X,163),p(32,0),if(gt(X,120),p(96,0),if(gt(X,90),p(64,0),if(gt(X,62),p(224,0),if(gt(X,48),if(gt(Y,77),p(160,0),if(lt(Y,64),p(128,0))),p(192,0)))))))':r=0:b=0[k];[k][1]blend=all_mode=hardlight[p];[2][p]vstack v.mp4
También podemos hacer que el mando aparezca sobre la imagen, e incluso que tenga un componente alpha. El resultado sería el siguiente:
ffmpeg -f rawvideo -pixel_format gray -video_size 8x1 -framerate 60 -i input.dat -loop 1 -i controller.png -i v.avi -filter_complex [0]scale=256:128:flags=neighbor,eq=brightness=1:contrast=6.1:gamma=10,geq=g='if(gt(X,193),p(0,0),if(gt(X,163),p(32,0),if(gt(X,120),p(96,0),if(gt(X,90),p(64,0),if(gt(X,62),p(224,0),if(gt(X,48),if(gt(Y,77),p(160,0),if(lt(Y,64),p(128,0))),p(192,0)))))))':r=0:b=0[k];[k][1]blend=all_mode=hardlight[p];[1]alphaextract[a];[p][a]alphamerge,scale=128:-1:flags=neighbor[pa];[2][pa]overlay=main_w-overlay_w:main_h-overlay_h:shortest=1 v.mp4
¿Que os ha parecido? ¿Que otras ideas teneis para implementar en FFMPEG?

jueves, 19 de marzo de 2026

Head On / Crash: Predecir los movimientos de la CPU

 Si eres viejo, como yo, es posible que recuerdes el juego Arcade "Head On" (Gremlin) o "Crash" (Exidy) de 1979. Básicamente los dos son una copia el uno del otro. Es un juego simple y adictivo, y que sirvió de inspiración para Pac-Man unos años más adelante, es el primero de la historia con el objetivo de recoger todos los "puntitos". En él llevas un cochecito que va en sentido anti-horario en un circuito de 4 carriles, mientras que otro cochecito va en dirección contraria en sentido horario intentando chocarse contigo, y si lo consigue: GAME OVER

Aquí en este video podeis ver el gameplay de la versión GameBoy, que es la que voy a desensamblar en esta ocasión.
Y pensareis... Pues el cochecito de la CPU intentará moverse siempre al mismo carril donde estemos nosotros para chocar. ¡Que fácil! ... Pues no. El cochecito de la CPU a veces hará cosas raras como moverse 2 carriles cuando solo estamos a 1 de distancia. ¡Incluso se puede dejar el juego en bucle infinito sin que choque nunca!
Más raro aun, en algunas ocasiones incluso se puede ver al cochecito de la CPU sin cambiar de carril pasando totalmente de nosotros. WTF?

Pues bien, llegó la hora de desensamblar, y por suerte el algoritmo que dicta al cochecito si debe girar o no esta contenida en una única función en 0x3efc que se llama cada frame
Si reemplazamos la llamada por NOPs, el cochecito de la CPU no girará nunca
Os voy a ahorrar leer ensamblador, así que os pondré las direcciones de memoria con las variables del juego que nos importan, y a continuación una traducción a lenguaje C:


0xC024 -> CPU_Y (Posicion Y del cochecito de la CPU)
0xC025 -> CPU_X (Posicion X del cochecito de la CPU)
0xC132 -> dir (Dirección del cochecito de la CPU 0-3. 0=IZQUIERDA,...)
0xC127 -> frame_giro (contador de frames que llevamos sin girar el cochecito de la CPU)
0xC138 -> velocidad (velocidad del cochecito. Incrementa al recolectar puntitos)
0xC13A -> flag_mov (¡este es importante! su valor {0x10, 0x20, 0x40, 0x80} cambia cada vez que los cochecitos del jugador, o de la CPU salen de una intersección entrando en los carriles, y cuando el cochecito de la CPU cambia de dirección "dir". Indica hacia donde debe moverse el cochecito de la CPU para girar y acercarse al cochecito del jugador. Si están en el mismo carril, valdrá 0x00)

if (velocidad==1) {
  frame_giro++;
  if (frame_giro!=4) return;
  frame_giro=0;
} else {
  frame_giro++;
  if (frame_giro!=2) return;
  frame_giro=0;
}

switch (dir) {
  case IZQUIERDA:
    if (CPU_X<0x48 || CPU_X>0x60) return;
    if (flagmov & 0x40) {
      CPU_Y=max(CPU_Y-1, 0X6F);
    } else if (flagmov & 0x80) {
      CPU_Y=min(CPU_Y+1, 0X93);
    }
    return;
......................................

Como veis el código es muy muy simple. Se divide en 2 bloques: el primero limita el giro a cada 2/4 frames dependiendo de la velocidad que tenga el cochecito de la CPU, y el segundo simplemente lo mueve en la dirección indicada por flagmov cuando se encuentre en una intersección sin salirnos de los limites. Realmente nunca controla si quiere moverse 1 o 2 carriles. Entonces... ¿que pasa? Centremonos en el primer bloque:
  • En velocidad 1 se desplaza lateralmente cada 4 frames, y así le da tiempo a desplazarse 2 carriles, aunque el jugador este solo a 1 de distancia.
  • En velocidad 2 se desplaza lateralmente cada 2 frames, y así también le da tiempo a desplazarse 2 carriles, aunque el jugador este solo a 1 de distancia.
  • En velocidad 3 y en adelante, se desplaza lateralmente cada 2 frames y solo le da tiempo a desplazarse 1 carril, aunque el jugador este a 2 o más de distancia.
Resumiendo, el coche de la CPU saltará siempre o 1 o 2 carriles dependiendo únicamente de la velocidad que tenga, moviendose siempre en dirección hacia el carril del jugador (esta dirección cambia cuando los coches salen de las intersecciones y entran de nuevo en los carriles)

Pero entonces... ¿Que ocurria cuando el cochecito de la CPU no cambiaba de carril y nos ignoraba?
Si veis el GIF detenidamente, veréis que justo antes de entrar al cruce, cambiaba de velocidad (1 -> 2)
⚠ ¡Alerta, bug! ⚠ si el valor de 0xC127 -> frame_giro en ese momento de cambiar velocidad vale 2 o 3, se seguirá incrementando sin control sin que llegue a valer de nuevo 2, y por lo tanto sin ejecutar ninguna lógica de giro durante los siguientes 254 frames, que es cuando el numero de 8bit hará overflow.

¿Que os ha parecido? Ahora que sabéis la lógica sigue el cochecito de la CPU, ya nunca veréis el juego de la misma forma. ¿preguntas? ¿dudas?...