PokeHacking

Join our Discord community

Cámaras dinámicas en Cuarta Generación: cómo implementarlas y cómo usarlas

Mikelan98, Trifindo

Compatible con:


Introducción

Los videojuegos de Pokémon en Nintendo DS están hechos en 3D, en tanto que utilizan modelos 3D para representar los mapas por los que movemos al jugador. Sin embargo, la quinta generación se lleva el mérito de ser la que más aprovecha las tres dimensiones en cuanto a jugabilidad; esto se debe a la perspectiva mucho más profunda, a unos modelos más voluminosos y, sobre todo, a jugar con la cámara en multitud de escenarios.


Panorámica del mirador de Ciudad Engobe en Pokémon Blanco 2 y Negro 2.

Prácticamente podemos decir, salvo algunas excepciones, que el 3D está bastante desaprovechado en los videojuegos de cuarta generación, aunque está ahí y todavía puede usarse (a diferencia de otras plataformas en las que utilizar 3D implicaría cambiar completamente el motor del videojuego). Los modelos y elementos de los mapas dependen completamente del estilo personal de cada desarrollador, mientras que la perspectiva de cámara se encuentra codificada en el Overlay 1 y puede modificarse con relativa facilidad. Pero, ¿qué podemos hacer para mover la cámara en nuestros mapas? Existen muchas formas de implementar algo así en la ROM, pero en cualquier caso necesitamos incorporar código ASM desde cero.


Investigación

El primer paso para investigar cómo podemos mover la cámara del juego en los mapas es buscar en la memoria RAM los valores que definen los parámetros de la cámara. Para encontrarlos, podemos aprovechar la utilidad de Cheats de DeSmuME y realizar una búsqueda comparativa de valores en la RAM (por ejemplo, moviendo horizontalmente al personaje y buscando valores mayores o menores).


Reporte de la ventana Cheats de DeSmuME. Los 3 últimos valores pertenecen a la caja de cámara.

Si lo hacemos correctamente, DeSmuME nos reportará varias coincidencias: tres de ellas pertenecerán a una región de memoria que denominaremos “caja de cámara” y que guarda todos los valores necesarios que definen la cámara del juego.


Caja de cámara en la memoria de Pokémon HeartGold.

En la caja de cámara podemos encontrar la posición de la cámara, la posición del target (hacia dónde apunta la cámara) y el vector de cámara. Todos estos parámetros son suficientes para generar cualquier movimiento de cámara, si bien también podemos modificar otros valores con efectos distintos. Las propiedades de cada parámetro son las mismas en Pokémon Platino y en Pokémon HeartGold y SoulSilver.

ID Parameter Increasing effect Decreasing effect
00
04
08
0C
10
14 Camera X Counterclockwise rotation around the player vertical axis Clockwise rotation around the player vertical axis
18 Camera Y Zoom out and a more cenital camera Zoom in and a less cenital camera
1C Camera Z Zoom out and a less cenital camera Zoom in and a more cenital camera
20 Target X Counterclockwise rotation around the vertical axis camera Clockwise rotation around the vertical axis camera
24 Target Y Move up the camera Move down the camera
28 Target Z Rotate the camera down towards Rotate the camera up towards
2C Camera Up Vector X Counterclockwise rotation of the screen Clockwise rotation of the screen
30 Camera Up Vector Y
34 Camera Up Vector Z
38
3C
40
44
48 Target Previous X Retarded movement of the camera to the right Retarded movement of the camera to the left
4C Target Previous Y Retarded movement of the camera upwards Retarded movement of the camera downwards
50 Target Previous Z Retarded movement of the camera to the front Retarded movement of the camera to the back

Ahora bien, no es trivial crear funciones para modificar estos parámetros. Hay que tener en cuenta que estos valores tienen que ser modificados gradualmente, frame a frame, y que no basta con modificarlos reemplazándolos directamente por un valor (lo que haría que la cámara se teletransportase a otro punto). Hay que tener en cuenta esto a la hora de diseñar todas las subrutinas, lo que nos obliga a encontrar alguna subrutina a la que enganchar nuestro código y que se ejecute, como mínimo, una vez cada frame.

La primera opción para integrar los movimientos de cámara en el mapa es utilizar triggers y scripts para llamar a las subrutinas que van a mover las cámaras. Aunque este sistema tiene la ventaja de que no hay que añadir nada que no exista ya (excepto las subrutinas para modificar los valores de la cámara), presenta varios inconvenientes. Por un lado, cada vez que entramos a un trigger, el juego se congela durante unos pocos frames, lo que puede resultar visualmente incómodo. Así mismo, se involucran demasiados elementos en la ROM (mapas, eventos y scripts).


Movimiento de cámara ejecutado por trigger en Pokémon Light Platinum DS. Se pueden apreciar pequeños freezes al inicio del movimiento.

Cualquier otra opción implicaría, a priori, diseñar una nueva especie de formato para determinar los movimientos de cámara. Lo más lógico es que este formato estuviera incluido dentro los archivos de mapas, junto con los archivos de colisiones, los archivos de placas de sonido o los archivos de terreno. Aunque es una opción mucho más complicada que utilizar triggers, una vez estén programadas las subrutinas necesarias será mucho más fácil diseñar los movimientos de cámara para cada mapa.

Si queremos seguir esta última estrategia, es necesario conocer muy bien cómo se cargan los datos de los mapas en la RAM. Cada una de las secciones se carga en direcciones de memoria distintas, es decir, el archivo del mapa no se mantiene en su conjunto en la RAM. Si buscamos secuencias de bytes del archivo de mapa, lo primero que podemos ver en la RAM es el archivo de colisiones, que siempre tiene un tamaño fijo de 0x800 bytes (2048 bytes, correspondientes a 32 tiles x 32 tiles x 2 capas de permisos). Modificando el valor en el header del mapa correspondiente al tamaño del archivo de colisiones no se modifica el tamaño de esta sección, por lo que podemos concluir que este valor es ignorado por el juego, y a las colisiones se les asigna siempre un tamaño fijo de 0x800 bytes. Detrás del archivo vienen escritos distintos punteros, a direcciones de memoria que contienen otras secciones del mapa.


Punteros en la memoria RAM hacia cada una de las secciones de un archivo de mapa. La sección de colisiones está señalada en azul.

Uno de ellos es el puntero al archivo BDHC, al final del cual vamos a incorporar nuestro archivo de cámaras (se podría incorporar también en los archivos de placas de sonido, pero entonces habría que buscar una estrategia distinta para Pokémon Platino, que no posee esta sección, y no habría compatibilidad entre ambos). Nuestro formato, por tanto, debe diseñarse como un archivo de placas. Insertar nuestro archivo de cámaras como una sección independiente del archivo de mapa sería extremadamente difícil y obligaría a modificar el código que lee todas las demás secciones (debido, sobre todo, a cómo se distribuyen los punteros de los que hemos hablado antes en la memoria).

Para comprobar la posibilidad de añadir datos al final de los archivos BDHC, y de que estos datos luego se puedan encontrar en la memoria RAM junto al archivo de terreno, se probó a modificar un archivo de mapa y unirle una secuencia aleatoria de bytes al final de este (es decir, al final de la sección del BDHC). Sin embargo, en el dump de memoria del juego no se encontró la secuencia de bytes, por lo que estos datos no estaban siendo cargados a la RAM. Esto se debe a que el tamaño de BDHC que va a ser copiado en la RAM está definido por el header del archivo de mapas y el header del propio BDHC. Es necesario incrementar el tamaño del BDHC, en el header del mapa, tantos bytes como tenga el archivo de cámaras que estemos uniendo detrás del BDHC. Igualmente, en el header del BDHC hay que incrementar el tamaño de la Parte U en [bytes del archivo de cámaras / 2], de modo que nuestro archivo de cámara quede incorporado en la Parte U del BDHC. En este caso sí que podemos ver los datos en el dump de la memoria RAM.


Sección del BDHC en la memoria, junto a un marcador que nos indica que es posible añadir datos al final de la sección. El BDHC está señalado en azul.

Es necesario indicarle al juego dónde comienza nuestro archivo de cámaras dinámicas (a partir de ahora llamado DCAM) dentro del archivo BDHC. Así mismo, debemos indicarle el tamaño de la sección DCAM para incrementar ese valor al tamaño de la parte U y que así se pueda leer junto a esta parte (ya que incrementar manualmente el tamaño de la parte U en el header del BDHC es muy tedioso y puede acabar provocando complicaciones). Para ello utilizaremos los 4 bytes de la firma del archivo BDHC, es decir, los 4 primeros bytes del archivo que contienen la cadena de texto "BDHC". Los dos primeros bytes serán reservados para el tamaño del DCAM, mientras que los dos siguientes serán para la posición relativa del DCAM dentro del contenido del archivo BDHC. Esto nos limita a que el tamaño del BDHC no pueda superar los 65535 bytes (64 KB), aunque es una limitación hipotética, ya que los archivos de terreno más complejos rara vez llegan a los 3 KB. La sección DCAM tampoco podría superar los 64 KB, aunque rara vez va a sobrepasar 1 KB. También hay que tener en cuenta que, en la RAM, al archivo BDHC se le elimina el header, por lo que la posición del archivo DCAM dentro del BDHC hay que contarla a partir del primer byte de la Parte P (justo después del header).


Header de un archivo de terreno en Platino, HeartGold y SoulSilver. Los bytes que vamos a utilizar están señalados en azul.

Con un diseño de los archivos establecido, solo queda encontrar una subrutina original en la ROM que cumpla con los siguientes requisitos:

Como requisito previo, también se buscó que la subrutina original trabajase con el puntero de los datos del mapa, de modo que pudieran aprovecharse para nuestra subrutina. Sin embargo, más tarde se descubrió un método para obtener el puntero desde cero en cualquier subrutina a partir de la estructura FIELDSYS_WORK, por lo que ya no fue necesario.

Lo ideal y lógico sería utilizar la propia subrutina de lectura de los BDHCs para usar un branch link (BL) a nuestro código de lectura de los DCAM. Sin embargo, si utilizamos un depurador (como IDA Pro) y colocamos breakpoints en esta subrutina, vemos que no cumple con algunos requisitos. La subrutina que lee el archivo de colisiones sí cumple con todos los requisitos en HeartGold y SoulSilver, así que inicialmente se enlazó el código con ella. Concretamente, en el offset 0x020547F4 (IPKS) en un BL que fue modificado para que apuntase hacia otro sitio donde se encontraba nuestro código. Sin embargo, cuando se probó en Pokémon Platino, la subrutina no se ejecutaba a cada frame, haciéndola inservible.

Finalmente, se escogió la subrutina que se encarga de actualizar la posición de la cámara a cada momento en función de la posición del jugador: una parte de esta función sólo se ejecuta en el overworld, cumpliendo con todos los requisitos propuestos. Sin embargo, al ser una subrutina tan distante a las que inicialmente se plantearon (la de lectura de BDHC y la de lectura de colisiones) el divmap termina siendo impredecible a la hora de ejecutar nuestra subrutina y debe ser recalculado de nuevo. El divmap es un número que identifica en qué mapa estamos, dentro de los 4 posibles mapas que se cargan a la vez en la memoria del juego. Por tanto, solo puede tomar 4 posibles valores distintos (de 0 a 3), tal y como se ve en el esquema siguiente.


Esquema de la asignación de divmap para los 4 mapas cargados en cada momento.

El glitch del tweaking en la Cuarta Generación (DP/Pt/HGSS) consiste precisamente en combinar movimientos caóticos lo suficientemente rápidos como para que el divmap no pueda actualizarse de forma correcta y se lean los datos correspondientes a otros mapas.


Tweaking glitch en la Cuarta Generación.

Como no podemos utilizar el divmap que lee por defecto la rutina de colisiones, tenemos que diseñar nosotros una subrutina que calcule el divmap correcto en el que deberíamos estar, en función de la posición del jugador. Haciendo esto podemos leer correctamente el archivo de cámaras correspondiente al mapa en donde se encuentra el jugador, de entre los 4 mapas cargados en la RAM en cada momento.

En el apartado visual, hay que tener en cuenta que, al modificar la cámara, las normales de los modelos 3D de los mapas van a interaccionar de forma distinta con la cámara. Debido a esto, es común ver cómo algunos polígonos, e incluso sprites, se oscurecen o aclaran un poco conforme se mueve la cámara. De momento no hay forma de solucionar esto, ya que sería extremadamente complejo modificar las normales de un modelo 3D en tiempo real.


Capturas del mismo mapa con distintos ángulos de cámara. En A se puede ver el mapa con el ángulo de cámara por defecto. En B los colores de los polígonos se han oscurecido ligeramente, mientras que en C son los sprites los que se han oscurecido.


Tutorial

Para incorporar esta nueva funcionalidad a la ROM, es necesario expandir el ARM9, tal y como es explica en este tutorial.

El formato final que se ha diseñado para los archivos DCAM se detalla a continuación. Existen dos tipos de placas: aquellas que efectúan una animación de cámara definida en un intervalo de tiempo, y aquellas que modifican la cámara en función de la posición del jugador dentro de la placa. Este último tipo se divide a su vez en dependencia horizontal o vertical.


Esquema general del diseño final de un archivo DCAM.

Lo primero que debe hacer nuestra subrutina, como ya hemos mencionado, es obtener el divmap correcto para que se pueda leer el archivo DCAM correcto correspondiente al mapa en el que estemos. Para ello nos basta con conocer la posición del jugador cada vez que ejecutemos la rutina, aunque han sido necesarios algunos arreglos de código en los casos en los que el jugador se encuentra en el centro de los mapas (ya que el mapa actual cambia de divmap en esos tiles).

Lo siguiente es leer correctamente el archivo DCAM, a partir de los headers que hemos modificado, e incrementar o disminuir los valores de la caja de cámara. En las placas que tienen un movimiento de cámara que depende de la posición, la gradualidad del movimiento de cámara la tenemos de la gradualidad del movimiento del personaje moviéndose por la placa. En las placas que tienen una animación independiente de la posición del jugador, por el contrario, la gradualidad hay que obtenerla limitando el incremento de los valores de la caja de cámara a cada frame. Podemos observar fácilmente con IDA Pro que nuestra subrutina se ejecuta más de una vez por frame, por lo que necesitamos una subrutina auxiliar que actúe como una función booleana, y que nos indique si ha trascurrido 1 frame desde la última vez que se ejecutó.

Así mismo, la subrutina necesita de un pequeño espacio en la memoria RAM para almacenar algunos parámetros (incrementos aplicados a cada valor de la caja de cámara, ángulos aplicados con anterioridad, valores originales de la caja de cámara…). Este espacio se encuentra al final de la subrutina, como un espacio relleno de 00. Esta región de la subrutina no debe alterarse ni ocuparse con otros valores, pues resultaría en un comportamiento inpredecible por parte de la cámara.

A continuación se puede consultar la tabla que indica qué reemplazos hay que efectuar y dónde. Debido a su tamaño, la subrutina de lectura de DCAM se encuentra como un archivo a parte y no como texto.

Game Code Branch Offset Branch Code Expanded ARM9 file Dynamic Camera Subroutine Offset (relative to the expanded ARM9 file) Dynamic Camera Subroutine Code
CPUE 0x0202040C B9 F3 E2 F8 data/weather_sys/weather_sys_9.rlcn 0x000115B0 cpue_cam.bin
IPKE 0x02023174 B6 F3 2E FA a/0/2/8/0.rlcn 0x000115B0 ipke_cam.bin
IPGE 0x02023174 B6 F3 2E FA a/0/2/8/0.rlcn 0x000115B0 ipge_cam.bin
CPUS 0x0202047C B9 F3 AA F8 data/weather_sys/weather_sys_9.rlcn 0x000115B0 cpus_cam.bin
IPKS 0x02023174 B6 F3 2E FA a/0/2/8/0.rlcn 0x000115B0 ipks_cam.bin
IPGS 0x02023174 B6 F3 2E FA a/0/2/8/0.rlcn 0x000115B0 ipgs_cam.bin
IPKJ 0x02022E14 B6 F3 DE FB a/0/2/8/0.rlcn 0x000115B0 ipkj_cam.bin
IPGJ 0x02022E14 B6 F3 DE FB a/0/2/8/0.rlcn 0x000115B0 ipgj_cam.bin

Así mismo, es necesario modificar ligeramente las subrutinas de lectura de los archivos BDHC para que lean correctamente el segmento DCAM y lo incorporen a la memoria.

Game Code Overlay File First Offset (relative to overlay file) First Code Second Offset (relative to overlay file) Second Code
CPUE Overlay 5 0x0001E1B4 00 4B 18 47 41 9C 3D 02 0x0001E2CC 00 4B 18 47 01 9C 3D 02
IPKE Overlay 1 0x0001574C 00 4B 18 47 41 9C 3D 02 0x00015864 00 4B 18 47 01 9C 3D 02
IPGE Overlay 1 0x0001574C 00 4B 18 47 41 9C 3D 02 0x00015864 00 4B 18 47 01 9C 3D 02
CPUS Overlay 5 0x0001E1BC 00 4B 18 47 41 9C 3D 02 0x0001E2D4 00 4B 18 47 01 9C 3D 02
IPKS Overlay 1 0x0001574C 00 4B 18 47 41 9C 3D 02 0x00015864 00 4B 18 47 01 9C 3D 02
IPGS Overlay 1 0x0001574C 00 4B 18 47 41 9C 3D 02 0x00015864 00 4B 18 47 01 9C 3D 02
IPKJ Overlay 1 0x000155D4 00 4B 18 47 41 9C 3D 02 0x000156EC 00 4B 18 47 01 9C 3D 02
IPGJ Overlay 1 0x000155D4 00 4B 18 47 41 9C 3D 02 0x000156EC 00 4B 18 47 01 9C 3D 02

Los archivos BDHC que tienen incorporado su DCAM (denominados BDHCam) se pueden elaborar manualmente mediante edición hexadecimal, pero la herramienta PDSMS tiene soporte para generar estos archivos, de modo que se puede hacer de forma más fácil y con referencias visuales. Por otro lado, la importación de los BDHCam en los archivos de mapa se puede hacer con SDSME o DSPRE como si fuesen un archivo BDHC. No es necesario hacer ninguna otra modificación. Los archivos BDHCam son intercompatibles entre HeartGold/SoulSilver (IPKX/IPGX) y Platino (CPUX).

Si se opta por crear un BDHCam manualmente mediante edición hexadecimal, es recomendable que el DCAM quede alineado en 4 bytes, por lo que muchas veces es necesario un padding de 2 bytes (FF FF). Estos bytes también deben contabilizarse para incrementar el tamaño de la sección DCAM en el header del archivo.


Créditos requeridos

Si vas a utilizar esta investigación en tu proyecto, es necesario dar créditos a Mikelan98 y Trifindo. Puedes citar esta investigación copiando la siguiente referencia y colocándola en los créditos del post o página web de tu proyecto:

Mikelan98, Trifindo: Cámaras dinámicas (pokehacking.com/r/20110900)

Se prohibe la copia total o parcial de esta investigación en otros sitios sin el consentimiento previo de los autores.






© 2020 PokeHacking

Pokémon characters and images belong to The Pokémon Company International and Nintendo.
This website is in no way affiliated with or endorsed by Nintendo, Creatures, GAMEFREAK, The Pokémon Company or The Pokémon Company International.