miércoles, 8 de julio de 2020

Desarrollo de Street Fighter 2 Champion Edition para C64. Parte 3: los sprites

Ahora sí, entramos en el desarrollo del juego puramente dicho. Street Fighter 2 es un juego de lucha uno contra uno que en el apartado de los sprites cuenta con varias peculiaridades: luchadores muy grandes, gran variedad de movimientos especiales o movimientos sobre el suelo muy suaves. A priori, plasmar todo esto en una máquina con las limitaciones del C64 se antoja imposible, pero sacrificando algunos aspectos se puede conseguir algo decente. Eso creo, vamos.

Al abordar los sprites de los luchadores estudié cómo los habían gestionado otros juegos similares para C64. Para poder explicarlo mejor he utilizado C64 Debugger, una potente herramienta que permite depurar la ejecución de un programa, así como ver el contenido de la memoria, los sprites, los bitmaps, etc.

Street Fighter 2 (1992, US Gold)
La conversión oficial utiliza hasta 4 sprites multicolor para cada luchador, 128 por luchador en total, que ocupan 8KB de memoria.
Hoja de sprites de Ryu/Ken en Street Fighter 2 de US Gold.

Para que se vean por pantalla utiliza un doble buffer en el que se copian los sprites de la postura actual. El doble buffer permite evitar glitches, ya que puede suceder que se muestren sprites a medida que se actualizan. Con el doble buffer los punteros de los sprites apuntan siempre al buffer de sprites que ya ha sido copiado anteriormente, y se pueden copiar unos sprites mientras se muestran otros. Las diferentes alturas de cada luchador no se han respetado y por ejemplo Zangief es más alto que Chun Li.
Punteros a sprites en Street Fighter 2 de US Gold, se observa que en cada frame los punteros apuntan a direcciones diferentes de memoria (buffers).


Street Fighter versión EEUU (1988, Pacific Dataworks International)
La versión americana del videojuego de Capcom utiliza hasta 4 sprites por luchador pero en modo overlay, es decir, superpone sprites en alta resolución sobre sprites en baja resolución, lo que permite tener unos personajes coloridos y muy definidos, aunque de tamaño pequeño. El jugador 1, que siempre lleva a Ryu, tiene 136 sprites que ocupan 8,5KB de memoria. El jugador 2, por ejemplo Retsu, tiene 90 sprites que ocupan 5760 bytes.
Hoja de sprites de Ryu en Street Fighter versión EEUU.


En este caso también se usa el doble buffer explicado antes. Los luchadores tienen alturas similares, ya que son de un máximo de 2 sprites de alto.
Punteros a sprites en Street Fighter versión EEUU, en rojo los buffers de sprites.

Barbarian (1987, Palace Software)
Este caso es bastante peculiar: los personajes se dibujan directamente sobre el fondo bitmap y solamente se utilizan sprites para el chaleco del jugador 2, las manchas de sangre, la parte superior de los luchadores en algunos movimientos. Este sistema, aunque permite luchadores grandes (60 píxeles de altura en reposo, casi 3 sprites de alto), está implementado de forma que los movimientos son carácter a carácter, de 8 en 8 píxels, lo que provoca que los movimientos sean algo menos fluidos que en juegos que usan sprites y de la sensación de framerate bajo al no tener la misma fluidez. A pesar de esto, el juego es espectacular.
En los cuadros rojos están indicados los sprites de Barbarian. El resto de gráficos son bitmaps o caracteres.

IK + (1987, System 3)
El clásico de System 3 combina métodos vistos en los juegos anteriores: utiliza 12 sprites para 2 de los 3 luchadores, y el tercero lo dibuja en el bitmap como en el Barbarian. Para mostrar los sprites de los luchadores utiliza multiplexación, con la ventaja de que esos sprites siempre están alineados de 3 en 3, como se ve en la captura. Al no moverse los sprites verticalmente se evitan parpadeos. Todo esto proporciona una acción más fluida que en Barbarian y 3 personajes de gran tamaño en pantalla.
IK+, en rojo están marcados los sprites.


No he encontrado las hojas de sprites en memoria, así que imagino que estarán comprimidos. Al tener los 3 luchadores el mismo aspecto excepto por el color, con un bloque de sprites de un luchador se pueden dibujar los 3 personajes en pantalla, lo cual permite ahorrar mucha memoria.

En el caso de mi juego, inicialmente hice pruebas con sprites en alta resolución, pero lo descarté pronto por varios motivos:
  • Los luchadores aunque detallados, se veían muy pequeños, más o menos del tamaño de los del Street Figher USA.
  • Todos los personajes tendrían una altura similar, y así no podría respetar las proporciones entre luchadores, algo imprescindible.
  • Para implementar luchadores más altos tendría que utilizar más memoria por luchador y un multiplexador, y lo primero habría hecho más difícil programar la versión disco.
  • La implementación de los movimientos de miembros estirados de Dhalsim se complicaría considerablemente y se necesitaría usar multiplexación para un resultado óptimo.
Prueba de Ken en modo overlay.


Al final me decanté por una solución similar a la del Street Fighter 2 de US Gold: usar 4 sprites multicolor por luchador sin multiplexación pero respetando las dimensiones y proporciones de cada personaje. La "hoja de sprites" de un luchador contiene 128 sprites que ocupa 8KB de memoria. Así, los sprites de los 2 luchadores ocupan 16KB de memoria. En el VBANK (banco de memoria de vídeo) reservo un espacio de 512 bytes para copiar los 8 sprites de ambos luchadores. Con este uso de la memoria me queda espacio para el código, el bitmap del escenario, música y fx, y puedo hacer versiones en disco y cartucho.
Hoja de sprites de Ken (versión anterior).


Como curiosidad, inicialmente implementé un doble buffer como en otros títulos, pero al final he dejado de usarlo porque no lo necesito.

En el siguiente artículo os explicaré la conversión de los sprites originales y cómo el juego gestiona el pintado de los mismos. Si tenéis otro punto de vista sobre lo que he explicado aquí o si queréis comentar cualquier cosa sobre esto, me encantaría leeros en los comentarios.


Saludos, Paco.

11 comentarios:

  1. Muy interesante Paco. Os pongo algún ejemplo más.

    Samurai Warrior (Usagi Yojimbo) tiene un engine formidable con 4 personajes en pantalla muy grandes, y sin glitches. Son 2 sprites de ancho por 3 de alto, todos contiguos. Es decir, 24 sprites apelotonados en las mismas scanlines lo cual es un auténtico desafío para el multiplexador. Curiosamente lo hicieron usando los 4 timers hardware (uno por personaje) en lugar de por raster interrupts. Los sprites están comprimidos con un esquema RLC sencillito. No tiene double buffer, no sé muy bien cómo se aseguraba de que no se note (¿cómo lo has hecho tú?)

    La técnica usada en este juego es el llamado "Sprite Interfacing". Quieres pintar el sprite superior (torso) usando un sprite hardware, y el sprite superior (piernas) multiplexando el mismo sprite. Para ello necesitas una interrupción muy precisa, y dependiendo de si hay otros sprites a esa altura o de si te encuentras en una badline, los glitches están servidos. Esto es lo que les pasaba precisamente a los personajes en Double Dragon 1, y para disimular pusieron la famosa "línea transparente" entre torso y piernas. La técnica de interfacing consiste en crear dinámicamente un sprite intermedio, que tiene la mitad superior copiada del sprite torso, y la mitad inferior copiada de las piernas. De este modo, la transición vertical entre A y B no es necesario que se realice en una línea, sino que tienes 10.5 líneas de margen. Se pasa A -> A/B -> B sin que sea visible el cambio a lo largo de los 42 pixels de alto.

    Está técnica se usa también al menos en Double Dragon 3 y Altered Beast. El beneficio es que consigues meter más personajes a lo ancho.

    En el caso de #Mazinger64, se combina el Sprite Interfacing con varias técnicas más. Los pesonajes verticales usan interfacing (un único sprite), y además pueden tener sprites supletorios con cualquier offset X e Y con respecto al principal. Cualquier sprite puede ser hires, X expanded o Y expanded, y se puede elegir la prioridad entre los mismos. Los sprites se copian en tiempo real (en este caso desde ROM, en la demo .prg era desde RAM) haciendo "flipping" en tiempo real. DE este modo sólo es necesario definirlos hacia la derecha, y si el sprite está mirando a la izquierda la rutina de copia los voltea. Para los sprites "humanoides", se copia A, B y se genera A/B en la misma rutina (tanto a derecha como izquierda).

    ResponderEliminar
    Respuestas
    1. Yep, gracias... ¿Carlos? ;-)
      Lo del Samurai Warrior lo leí hace un tiempo y me dejó alucinado, implementar algo así en aquella época me parece muy meritorio. En mi caso no uso multiplexación, son 4 sprites por luchador, 8 en total. Usar más de 4 sprites por luchador implicaría dedicarles más memoria y tener que multiplexarlos, con el riesgo de parpadeos que supone y un incremento de ciclos, de los que no voy muy sobrado. El "flipping" también lo hago en tiempo real. En su día implementé las sombras como sprites y sí hacía multiplexación, pero no quedaba bien y lo quité.

      Eliminar
  2. Super interesante Paco, gracias por compartir estas cosas.

    Y tambien lo comentado por Zub!

    ResponderEliminar
  3. Si no necesitas multiplexar, te quitas un montón de esfuerzo CPU y tienes más FPS.. Muchas veces el éxito con estos entornos tan limitantes consiste en saber en qué charcos no meterse!

    De todas formas, lo de las sombras creo que no debería ser problema. Si te he entendido bien, cada personaje son 4 sprites (2x2) y hay espacio de sobra entre los 2 sprites superiores y la sombra (que está a una altura Y fija en el suelo). Te bastaría con poner una IRQ fija después de que se hayan pintado los sprites superiores pero antes del suelo (fácil porque tienes margen de sobra). Actualizas la Y de los sprites, color (negro) y forma (sombra).

    En cuanto se ha acabado de pintar los sprites superiores ya puedes poner la IRQ, no va a afectar que esté pintando las piernas porque son otro sprite diferente.

    ResponderEliminar
    Respuestas
    1. El tema es que si uso multiplexación para los luchadores introduzco dos problemas más. Por un lado he de implementar un multiplexador, algo que puede ser complejo y que consume más ciclos por frame. Por otro lado, se tendrían que copiar más sprites en cada frame, y actualmente copiando los 3-4 de un jugador (alternos, es decir, en un frame los del P1 y en el siguiente los del P2) me sobran pocos ciclos por frame. A pesar de las copias alternas de sprites, el juego ahora se mueve a 50 fps y es realmente rápido (a veces demasiado). He visto versiones para otras máquinas con luchadores grandotes y scroll, y visualmente están muy bien, pero el framerate es menor y afecta a la sensación de fluidez.

      Sobre las sombras, en su día implementé algo como lo que explicas, pero algo no debí hacer bien porque el resultado no quedaba perfecto y tenía muchos glitches. Lo revisaré de nuevo, pero más adelante.

      Eliminar
  4. Efectivamente está perfecto sin multiplexar.

    Lo de las sombras cuando quieras le echamos un ojo y lo dejamos niquelado!

    ResponderEliminar
  5. Me parece muy enriquecedor todo lo que comentas (algo extensible a los comentarios).
    De buenas a primeras, de esta lectura saco en claro que tengo que dedicarle algo más de tiempo al C64 Debugger y que tengo que mirar los temas de la multiplexación.
    Cuando le pegue otro par de lecutas adicionales una vez que haya hecho los deberes seguro que saco mucho más.

    ResponderEliminar
    Respuestas
    1. Gracias Javier, los comentarios de Carlos, el programador del Mazinger 64, son interesantísimos, como siempre. El tema de la multiplexación es muy interesante y complejo, yo he hecho cositas muy sencillas y no me he metido a fondo, pero hay un mundo de algoritmos de ordenación, IRQs, etc que un día de estos también me tocará explorar.

      Eliminar
  6. Lo de la multiplexación es un tema interesante, al menos por lo "exótico" que resulta si vienes de programar en otros entornos. Pero como os decía, la clave es acertar con qué técnicas aplicas para el juego que quieres.. No meterse en jardines... Ya os contaré en otro blog las movidas en Mazinger, no desvirtuemos el hilo.

    El C64Debugger me ha resultado útil para ver cosas gráficas muy concretas frame a frame, pero para debugging normal y corriente lo más útil para mí es el monitor de VICE.

    ResponderEliminar