Upload
others
View
3
Download
0
Embed Size (px)
Citation preview
1
Grado en Ingeniería Multimedia
Trabajo Fin de Grado
Autor:
Alejandro Roca Vande Sype
Tutor/es:
Francisco José Gallego Durán
Septiembre 2019
2
3
1. Justificación
Desde el momento que entré en la carrera de Ingeniería Multimedia, sabía desde un principio
cual era mi objetivo: dedicarme en un futuro al desarrollo de videojuegos de manera
profesional. Es por ello que, en el último curso de este grado, realicé el itinerario de Creación
y Entretenimiento Digital el cual consistía en realizar un videojuego durante todo el año
junto con otros compañeros de la carrera.
Dentro de este itinerario, se encontraba la asignatura de Videojuegos I donde los primeros
meses consistía en realizar un videojuego en ensamblador para el ordenador Amstrad CPC.
Fue en esta asignatura, donde empecé a aprender a programar en ensamblador y a
comprender lo que consistía realizar un videojuego con las limitaciones que presentaba este
ordenador de 8 bits: espacio de memoria limitado, uso de un lenguaje de programación que
no proporciona ni ayudas de gestión de memoria ni ninguna función previamente integrada
que realice operaciones o tareas de una manera más sencilla o de forma automática.
Según iba aprendiendo a programar este lenguaje de bajo nivel, me iba dando cuenta de lo
útil que era, ya que es el programador el cual tiene el control en todo momento de todo
aquello que está realizando, ya que debajo de este nivel de programación no hay nada más
que el código máquina en el que el ordenador interpreta las instrucciones que nosotros
escribamos.
Todo esto te permite saber gestionar mucho mejor el espacio de memoria del cual se dispone
y a optimizar de una manera más efectiva los programas y/o funciones que se realizan. Ya
que, en caso de no conseguir optimizar lo máximo posible, con los pocos recursos y espacio
de memoria que por lo general disponen estos ordenadores y consolas antiguas, se
convertiría en algo bastante complejo el proceso de conseguir hacer un videojuego jugable.
Por otra parte, aprender ensamblador me permitió entender mejor el funcionamiento de
algunos conceptos de más alto nivel como son los punteros y así como, muchos otros
aspectos relacionados con el almacenamiento de datos, por lo que lo considero algo esencial
para poder programar de manera mucho más eficiente en lenguajes de más alto nivel como
podría ser C o C++.
4
Índice
1. Justificación ...................................................................................................................... 3
Índice de Figuras ........................................................................................................................ 7
2. Introducción ................................................................................................................... 10
3. Marco Teórico ................................................................................................................. 12
3.1. Análisis de Videojuegos previos similares ……………………………………………………………..12
4. Objetivos .......................................................................................................................... 26
5. Metodología .................................................................................................................... 28
6. Creación de una ROM básica .................................................................................... 30
6.1. Introducción ………………………………………………………………………………………………………….. 30
6.2. Bancos y ranuras de la ROM …………………………………………………………………………………. 31
6.3. Validación de la ROM …………………………………………………………………………………………… 33
6.4. Gestión de interrupciones ………………………………………………………………………………………. 35
6.5. Inicialización de la pila y el VDP …………………………………………………………………………… 36
6.6. Liberación de la VRAM ………………………………………………………………………………………… 39
7. Diseño del videojuego …………………………………………………………………………………………… 42
7.1. Descripción general ……………………………………………………………………………………………….. 42
7.2. Historia ………………………………………………………………………………………………………………….. 43
7.3. Género y Audiencia ……………………………………………………………………………………………….. 43
7.4. Ámbito …………………………………………………………………………………………………………………… 43
7.5. Jugabilidad …………………………………………………………………………………………………………….. 44
7.6. Mecánicas ………………………………………………………………………………………………………………. 44
7.7. Enemigos ……………………………………………………………………………………………………………….. 48
7.8. Niveles …………………………………………………………………………………………………………………… 50
7.9. Controles ……………………………………………………………………………………………………………….. 52
8. Desarrollo del videojuego …………………………………………………………………………………….. 53
8.1. Prototipo Mágica ……………………………………………………………………………………………………. 53
8.1.1. Código básico para la ROM ……………………………………………………………………………….. 55
8.1.2. Dibujado de un sprite y un fondo por pantalla …………………………………………………….. 58
8.1.3. Lectura de la entrada del jugador por teclado ………………………………………………………. 66
8.1.4. Organización del proyecto y makefile …………………………………………………………………. 70
5
8.1.5. Dibujado de un sprite y fondo creados desde cero ....................................................... 72
8.1.6. Generalización de las funciones de dibujado ............................................................... 77
8.1.7. Disparo del jugador ...................................................................................................... 80
8.1.8. Colisiones entre sprites ................................................................................................. 86
8.2. Replanificación................................................................................................................. 89
8.3. Demo jugable ................................................................................................................... 91
8.3.1. Colisiones con el mapa ................................................................................................. 91
8.3.2. Gravedad ...................................................................................................................... 95
8.3.3. Control del tiempo mediante VSYNC .......................................................................... 97
8.3.4. Inicialización de los datos .......................................................................................... 100
8.3.5. Cambio de mapas ....................................................................................................... 104
8.3.6. Muerte del jugador ..................................................................................................... 107
8.3.7. IA primer tipo de enemigos ........................................................................................ 109
8.3.8. Recibir daño ............................................................................................................... 117
8.4. Mecánicas extra .............................................................................................................. 118
8.4.1. IA segundo tipo de enemigos ..................................................................................... 118
8.4.2. Puzles ......................................................................................................................... 124
8.4.3. Contador de tiempo .................................................................................................... 126
8.4.4. Menú .......................................................................................................................... 132
8.4.5. Animaciones ............................................................................................................... 134
8.5. Contenido ....................................................................................................................... 136
8.5.1. Búsqueda de materiales .............................................................................................. 137
8.5.2. Diseño de niveles ....................................................................................................... 139
8.5.3. Diseño de la pantalla de muerte y menú ..................................................................... 139
8.6. Reducción del espacio ocupado en ROM ....................................................................... 141
8.6.1. Precisión subpíxel ...................................................................................................... 142
8.6.2. Codificador de mapas ................................................................................................. 144
8.6.3. Aumento del espacio disponible en ROM .................................................................. 148
8.7. Implementación del final del juego ................................................................................ 150
8.7.1. Creación de los niveles finales ................................................................................... 151
8.7.2. Creación de la pantalla de fin del juego ..................................................................... 154
8.8. Mejoras y arreglos .......................................................................................................... 156
8.8.1. Efecto de parpadeo de sprites ..................................................................................... 156
8.8.2. Música ........................................................................................................................ 159
6
9. Conclusiones................................................................................................................. 161
10. Bibliografía y referencias ......................................................................................... 163
11. Glosario .......................................................................................................................... 167
12. Anexo 1. Detalles técnicos de la Sega Master System .................................. 169
12.1. Información general ………………………………………………………………………………………… 169
12.2. CPU ………………………………………………………………………………………………………………… 171
12.3. Mapa de memoria ……………………………………………………………………………………………. 173
12.4. Sistema básico de entrada-salida (BIOS) …………………………………………………………. 175
12.5. Video Display Processor (VDP) ………………………………………………………………………. 177
7
Índice de Figuras
Figura 1. Portada del videojuego Alien 3. ....................................................................................... 12
Figura 2. Alien 3 para la SMS. ........................................................................................................ 14
Figura 3. Portada de Ninja Gaiden. ................................................................................................. 16
Figura 4. Ninja Gaiden para la SMS. .............................................................................................. 17
Figura 5. Portada de Alex Kidd in Shinobi World. ......................................................................... 18
Figura 6. Alex Kidd in Shinobi World para la SMS. ...................................................................... 19
Figura 7. Portada Master of Darkness. ............................................................................................ 20
Figura 8 Master of Darkness para la SMS. ..................................................................................... 21
Figura 9. Portada de Zillion. ........................................................................................................... 22
Figura 10. Zillion para la SMS. ....................................................................................................... 23
Figura 11. Portada de Operation C. ................................................................................................. 24
Figura 12. Operation C para la SMS. ................................................................................................ 25
Figura 13. Definición de ranuras. .................................................................................................... 31
Figura 14. Definición de bancos. .................................................................................................... 32
Figura 15. Definición de las ranuras y bancos que contendrá la ROM. .......................................... 33
Figura 16. Definición de la etiqueta SDSC. .................................................................................... 34
Figura 17. Definición de la dirección de memoria en la cual se escribirá el código. ...................... 35
Figura 18. Gestión de interrupciones en la SMS. ............................................................................ 35
Figura 19. Gestión del botón de pausa en la SMS. ......................................................................... 36
Figura 20. Valores iniciales para los registros del VDP. ................................................................ 37
Figura 21. Se envía los valores iniciales del VDP al puerto de control. ......................................... 38
Figura 22. Dirección de VRAM y orden a realizar. ........................................................................ 39
Figura 23. Vaciado de la VRAM. ................................................................................................... 40
Figura 24. Mensaje de ¡Hola Mundo! mostrado por pantalla. ........................................................ 40
Figura 25. Sprite del personaje principal. ....................................................................................... 46
Figura 26. Sprite de vida. ................................................................................................................ 47
Figura 27. Sprite de llave. ............................................................................................................... 47
Figura 28. Sprite del cristal. ............................................................................................................ 47
Figura 29. Sprite de puerta. ............................................................................................................. 48
Figura 30. Sprites del enemigo a melé. ........................................................................................... 49
Figura 31. Sprites del enemigo a distancia. ..................................................................................... 50
Figura 32. Bioma de zona cuevas. .................................................................................................. 50
Figura 33. Bioma de bosques. ......................................................................................................... 51
Figura 34. Niveles creados para el primer bioma. ........................................................................... 51
Figura 35. Niveles creados para el segundo bioma. ........................................................................ 52
Figura 36. Controles de Invasion para la SMS. ............................................................................... 52
Figura 37. Mágica para Amstrad CPC. ........................................................................................... 54
Figura 38. Racer para la SMS. ........................................................................................................ 55
Figura 39. Configuración de las ranuras y bancos para el prototipo. .............................................. 56
Figura 40. Inicialización de los registros del VDP para el prototipo. ............................................. 56
Figura 41. Direcciones de la paleta de colores (CRAM). ............................................................... 58
Figura 42. Cargado de la paleta de colores a los sprites. ................................................................ 59
Figura 43. Valores de la paleta de colores para un Sprite de Sonic usando BMP2TILE. ............... 60
Figura 44. Copia de los datos de los índices de tiles a la VRAM. .................................................. 61
8
Figura 45. Carga de un tilemap de un mapa en VRAM. ................................................................. 62
Figura 46. Buffer del Sprite Attribute Table. .................................................................................. 63
Figura 47. Representación del SAT de la SMS. .............................................................................. 63
Figura 48. Copia de los índices de tile del Sprite al buffer del SAT. .............................................. 64
Figura 49. Copia del buffer al SAT. ................................................................................................ 66
Figura 50. Sprite y fondo del tutorial dibujados por pantalla. ......................................................... 66
Figura 51. Zona de memoria para la gestión de interrupciones en SMS. ........................................ 67
Figura 52. Información sobre los bits correspondientes al Joystick P1. ......................................... 68
Figura 53. Control del movimiento hacia la derecha del jugador. .................................................. 69
Figura 54. Ejemplo de uso de la directiva”. section” de WLA DX. ............................................... 71
Figura 55. Makefile básico para poder generar un archivo ".sms". ................................................ 72
Figura 56. Tilemap creado con Tiled para representar un nivel de Mágica. ................................... 73
Figura 57. Editor de memoria del emulador MEKA. ...................................................................... 75
Figura 58. Dibujado de Sprites antes y después de resolver el problema con BMP2TILE. ........... 76
Figura 59. Zona de datos del Player mediante “enum”. .................................................................. 78
Figura 60. Función para actualizar la coordenada X del Sprite en el SAT utilizando IX. .............. 79
Figura 61. Función para actualizar la coordenada Y del Sprite en el SAT usando IX. ................... 80
Figura 62. Dibujado de 2 Sprites: Player y bala. ............................................................................. 81
Figura 63. Función de borrado de la bala. ....................................................................................... 83
Figura 64. Movimiento hacia la derecha del Sprite de la Bala........................................................ 84
Figura 65. Actualización de todos los aspectos del Sprite de la bala. ............................................. 85
Figura 66. Comprobación de si la 2º entidad está situada la izquierda de la 1º entidad o no.......... 88
Figura 67. Planificación abril (preproducción). .............................................................................. 90
Figura 68. Calendario Mayo (producción). ..................................................................................... 91
Figura 69. Zona donde se almacena la información relacionado con el tilemap. ........................... 92
Figura 70. División entre 8 del valor almacenado en el registro A. ................................................ 93
Figura 71. Acceso a la fila correspondiente donde se encuentra el tile a obtener del tilemap. ....... 94
Figura 72. Acceso a la columna del tile del tilemap. ...................................................................... 95
Figura 73. Tablas para implementar el salto del jugador. ............................................................... 96
Figura 74. Función para controlar la caída del personaje hasta colisionar con el mapa. ................ 97
Figura 75. Contador de frames y de tiempo mediante VSYNC. ..................................................... 98
Figura 76. Uso del tiempo para controlar la frecuencia de disparo del jugador. ............................. 99
Figura 77. Ejemplo de creación de un struct. ................................................................................ 101
Figura 78. Creación struct game_object player. ............................................................................ 101
Figura 79. Símbolos generados automáticamente con el struct del player. .................................. 102
Figura 80. Ejemplo copia datos con ldir. ...................................................................................... 103
Figura 81. Proceso de cambio de nivel del juego. ......................................................................... 105
Figura 82. Función para borrar los sprites de la pantalla. ............................................................. 106
Figura 83. Función para "matar" todas las entidades del nivel. .................................................... 106
Figura 84. Función para decrementar la vida del jugador una unidad. ......................................... 108
Figura 85. Comprobación del estado del enemigo. ....................................................................... 111
Figura 86. Función para comprobar si hay que eliminar al enemigo o no. ................................... 111
Figura 87. IA enemigos estado reposo. ......................................................................................... 113
Figura 88. Movimiento de los enemigos. ...................................................................................... 115
Figura 89. Update para más de un enemigo. ................................................................................. 116
9
Figura 90. Función para preparar la bala del enemigo para ser disparada. ................................... 121
Figura 91. Reseteo de la posición de la bala en función de la posición y dirección del enemigo. 122
Figura 92. Función para comprobar la colisión de la bala de los enemigos con el jugador. ......... 123
Figura 93. Función para efectuar el cambio de nivel con las puertas. .......................................... 126
Figura 94. Tilesheet de ejemplo que contiene los números a dibujar. .......................................... 127
Figura 95. Definición de una variable de más de dos bytes con WLADX. .................................. 128
Figura 96. Función para inicializar el reloj de tiempo al valor 300. ............................................. 129
Figura 97. Carga del tile correspondiente del sprite de los números en la posición correcta del
buffer. ............................................................................................................................................. 131
Figura 98. Dibujado del reloj de tiempo y el contador de vidas en Invasion. ............................... 132
Figura 99. Modificación del sprite a mostrar por pantalla mediante los índices de tile. ............... 135
Figura 100. Tilesheet con sprites para el protagonista. ................................................................. 138
Figura 101. Fondo para pantalla de muerte. .................................................................................. 139
Figura 102. Fondo para la pantalla del menú. ............................................................................... 140
Figura 103. Ejemplo estructura fichero que contiene los tiles de los mapas. ................................ 144
Figura 105. Sección encargada de gestionar lo que ocurre cuando se detecta un valor distinto. .. 146
Figura 106. Función para escribir en el fichero de salida. ............................................................ 147
Figura 107. Comparación fichero entrada/salida. Arriba: fichero sin comprimir. Abajo: fichero
comprimido. ................................................................................................................................... 147
Figura 108. Ejemplo de mapa con tiles simétricos creado con Tiled. ........................................... 150
Figura 109. Diseño del nivel 7 que contiene escaleras para dar al exterior. ................................. 151
Figura 110. Uso de la instrucción “jr c” y “jr nc” para hacer comprobaciones de mayor y menor de
un valor........................................................................................................................................... 152
Figura 111. Diseño del nivel final donde se encuentra el portal. .................................................. 153
Figura 112. Cambio de estado de mundo a Fin del Juego. ............................................................ 155
Figura 113. Pantalla de Fin del Juego. .......................................................................................... 155
Figura 114. Borrado de un sprite del SAT. ................................................................................... 157
Figura 115. Función para desplazar al enemigo cuando este recibe daño. ................................... 158
Figura 116. Una de las funciones incluidas en la librería PSGlib. ................................................ 159
Figura 117. Paso de información al puerto $bf del VDP. ............................................................. 180
10
2. Introducción
Desde la década de los 40, fecha en la cual se creó por primera vez lo que hoy en día se
considera como videojuego, el sector de los videojuegos ha ido creciendo en popularidad
poco a poco, hasta tal punto de que, a día de hoy, los videojuegos son considerados como
uno de los sectores que más beneficios generan, los cuales se ven incrementados año tras
año.
Actualmente, el sector de los videojuegos es mucho más variado y grande a como lo era hace
varias décadas. Hoy en día se puede encontrar multitud de lenguajes de programación, cada
uno con sus ventajas e inconvenientes, y así como, diferentes motores gráficos, consolas,
procesadores etc.
Sin embargo, es durante la década de 1980 cuando la fiebre de los videojuegos creció
considerablemente, en una etapa que se conoce como la edad de oro de los videojuegos.
Todo esto gracias al lanzamiento de juegos como Space Invaders o las máquinas
recreativas que se podían encontrar por cualquier sitio en aquellos tiempos, hicieron que los
ingresos generados por la industria crecieran considerablemente.
Durante esta época, crear un videojuego era un proceso bastante difícil y esto se debía
principalmente por dos motivos: En primer lugar, el acceso a la información no era tan
sencillo a como lo es hoy en día, ya que la principal fuente de información residía en los
libros y no se podía acceder de manera sencilla a Internet para obtener la información que se
necesitaba, además de que el Internet como hoy lo conocemos no existía por aquel entonces.
En segundo lugar, construir un videojuego requería de una habilidad elevada por parte del
programador ya que debía adaptarse a las limitaciones del ordenador o consola para la que
creaba el videojuego. La principal limitación a la que debían hacer frente residía en el
espacio de almacenamiento disponible en la máquina, el cual era bastante reducido y esto
requería que el programador tuviese que aprender a implementar técnicas que le permitiesen
reutilizar partes de código con el objetivo de ahorrar el máximo espacio posible.
Es por esta razón, por lo que he decidido realizar un videojuego para una consola antigua
ya que considero que crear un videojuego para una máquina con tales limitaciones me
permitirá mejorar mis habilidades como programador y así como aprender muchos otros
conceptos que hasta ahora desconocía o no manejaba del todo bien.
11
El tener tan poco espacio disponible me obliga a tener que aprender a crear un código que
sea reutilizable por diferentes funciones y que ocupe el menor espacio posible. Todo este
trabajo que debo realizar para crear un videojuego en tales condiciones, no tiene otro
resultado que el de perfeccionar y mejorar mis habilidades en lenguajes de programación
que se utilizan hoy en día, de tal manera que pueda conseguir realizar programas o
funciones que sean lo más óptimas posibles.
Por otro lado, también he decidido no utilizar un lenguaje de programación actual, como
podría ser C++, para crear el videojuego en cuestión. Considero mucho más rentable para
mí el utilizar lenguajes de programación de la época que son de más bajo nivel que los
actuales. Esto quiere decir que no habrá nada más por debajo (excepto el código máquina)
de este lenguaje y, por lo tanto, todo lo que realice estará totalmente controlado por mí y no
realizará nada de lo cual yo desconozca. Además, utilizar un lenguaje de bajo nivel me
permite entender de mejor manera conceptos actuales como pueden ser los punteros u otros.
También considero que me resultará mucho más beneficioso el aprender a manejarme con
lenguajes de bajo nivel en aspectos como a la hora de encontrar trabajo en el futuro en
cualquier industria de videojuegos, ya que estoy completamente seguro que tendrá mucho
más valor una persona que ha realizado un juego en lenguaje ensamblador que uno que lo
ha realizado, por ejemplo, en Unity con C++, ya que existirán una mayor cantidad de
personas que sepan trabajar con este último.
El lenguaje que utilizaré para crear el videojuego no es otro que el famoso Z80. Esto es
debido a que, durante esta etapa de los 80 fue también cuando se lanzó el Zilog Z80, un
microprocesador de 8 bits que se popularizó gracias a su uso en ordenadores como Spectrum,
Amstrad CPC u ordenadores de sistema MSX. Al ser el más famoso, es el microprocesador
que tendrán la mayoría de las máquinas de esa época y, por lo tanto, el lenguaje más
asequible a la hora de poder encontrar información con la que poder trabajar.
Finalmente, en cuanto a la consola u ordenador sobre la que crear el videojuego, me he
decantado por la Sega Master System, la cual, aunque no tuviese mucho éxito durante su
época, es una consola con la que aún no había trabajado y tras ver sus especificaciones
técnicas y el catálogo de juegos del cual dispone, decidí que el videojuego del proyecto sería
sobre esta consola de 8 bits.
12
3. Marco Teórico
En este apartado se va a realizar un pequeño estudio de algunos de los videojuegos antiguos
que han sido creados para la consola SMS o para cualquier otra con características técnicas
similares a esta. Concretamente, se tratarán aquellos videojuegos que pertenezcan al género
de plataforma/acción ya que es el tipo de género que tendrá el videojuego que se pretende
crear en este proyecto.
El objetivo de este análisis o estudio, es el de intentar averiguar qué mecanismos o técnicas
han sido utilizadas para conseguir los distintos aspectos jugables que contienen los
videojuegos que han sido estudiados, de tal manera que, se pueda estimar y decidir qué
características incluir y cuales no en el videojuego del proyecto. Todo esto basándose en
función del coste que suponga realizar dichas mecánicas y el tiempo del cual se dispone para
realizar el proyecto.
3.1. Análisis de Videojuegos previos similares
A continuación, se encuentran todos aquellos videojuegos que, tras realizar una pequeña
revisión de los mismos, se consideran de utilidad o del mismo parecido que el que se
pretende crear en este trabajo:
• Alien 3 – Master System
Juego de plataformas y acción de desplazamiento
lateral basado en la película de Alíen 3 de 1992. El
jugador controla a la protagonista de la película,
Ellen Ripley, mientras avanza por la colonia
Fiorina 161. Su objetivo es el de rescatar a unos
prisioneros que se encuentran en esta prisión
espacial.
Dentro del juego, el jugador puede realizar la
acción de saltar para poder avanzar por los niveles,
disparar para eliminar a los aliens con la
peculiaridad de que todas las armas disponibles
funcionarán si tienen munición, subir por escaleras,
abrir puertas etc.
Figura 1. Portada del videojuego Alien 3.
Fuente: www.gamefaqs.com
13
El motivo de elección de este juego es debido a que se pueden encontrar diversas
mecánicas que pueden servir de referencia para implementar en el videojuego del
proyecto. La idea es que el protagonista utilice un arma a distancia como herramienta
para eliminar a los enemigos, al igual que ocurre aquí en Alien 3.
Por esta razón, como ambos juegos utilizan un arma a distancia, se puede tomar como
referencia para determinar aspectos como la velocidad y dirección de disparo. En Alien
3, se puede realizar un disparo hacia cualquier dirección, incluyendo en diagonal, y,
además, la velocidad de disparo simula la de una ametralladora real, por lo que la
cadencia de disparo es bastante elevada. Esto no es algo que se desee integrar en el
proyecto, pero sirve para tomarse como referencia. Igualmente, el aspecto de la munición
obliga al jugador a no malgastar los disparos, lo cual es algo curioso que no se ha
observado en ningún de los otros juegos analizados. Sin embargo, este aspecto no se ha
llegado a considerar a introducir en el juego del proyecto, ya que se pretende que el
jugador no tenga que preocuparse de aspectos como la munición restante del arma.
Por otro lado, la mecánica de disparo a distancia, permite asimismo observar el tipo de
enemigos a los que puede hacer frente el jugador y la manera que este debe afrontarlos
para eliminarlos. No obstante, en Alien 3 no se ha llegado a observar una gran variedad
de enemigos, todos utilizan la misma mecánica de moverse a gran velocidad y reducir
esta cuando reciben un disparo del jugador.
Como se ha comentado previamente, en Alien 3 el jugador puede utilizar más de un
arma distinta, lo que permite cambiar la jugabilidad del juego en función del arma que
se esté manipulando. Sin embargo, no es un aspecto que se considere a introducir por el
motivo principal del tiempo disponible para el proyecto. Incluir varias armas resultaría
en crear un diseño de niveles y de enemigos mucho más complejo para que se pueda
observar así la diferencia real de utilizar un arma u otra y que no resulte en un uso
indiferente de cualquiera de ellas.
Finalmente, la mecánica más interesante a observar en Alien3, es el uso de granadas.
Una de las ideas principales del juego a realizar es que el jugador pueda manejar granadas
para poder eliminar a los enemigos. Esta mecánica permitiría añadir una mayor variedad
a la jugabilidad del videojuego y se considera mucho más simple a realizar que añadir
diversas armas donde cada uno de estas debería tener una implementación distinta lo que
deriva en mayor requerimiento de tiempo de desarrollo para realizarlas.
14
Figura 2. Alien 3 para la SMS.
Fuente: www.youtube.com/WorldofLongplays
Hasta este punto se encontrarían las mecánicas “clave” de Alien 3, es decir, aquellas que
son exclusivas de este juego y no se han llegado a encontrar en los otros analizados.
Además de estas, se encuentran mecánicas que, como se observará posteriormente, son
típicas de los videojuegos de plataformas de esa época. La única diferencia que se podrá
encontrar en estas mecánicas es la forma en la que estas se implementen, pero la idea es
la misma en todos.
Dentro de este apartado, se encuentran mecánicas tales como el reloj de tiempo, el cual
es un contador de tiempo que determina el tiempo restante que le queda al jugador para
terminar el nivel en cuestión. Al finalizar el nivel, se suman los puntos correspondientes
a la puntuación total en función del tiempo que le haya sobrado al jugador. Al comenzar
el siguiente nivel, se realiza un reinicio de dicho contador a su valor inicial.
El apartado del reloj, es un apartado que se va a implementar, pero la manera en la que
se va a realizar será diferente. Se encontrarán diferencias como que el contador mostrará
solo los segundos y que no se reiniciará al acabar el nivel. Sin embargo, el problema aquí
reside en averiguar cómo se realiza realmente esta mecánica.
15
En función de los conocimientos ya aprendidos sobre los aspectos técnicos de la consola,
se entiende que su funcionamiento reside en el cambio de los sprites que representan los
números, es decir, cada vez que transcurra un segundo se cambiará el sprite del número
que sea necesario al sprite que represente el siguiente número. Aunque por supuesto, está
implementación se basa en mi conocimiento y es probable que en Alien 3 se haya
realizado de una manera distinta, pero considero que la propuesta planteada puede ser
también válida a realizar.
También se puede encontrar el aspecto de la puntuación, que añade al videojuego una
mayor rejugabilidad. La tabla de puntuación brinda al jugador el interés de volver a jugar
al juego con el objetivo de mejorar la puntuación previamente obtenida en la partida
anterior. Esta mecánica se puede implementar de una manera similar que la del contador
de tiempo, con la diferencia de que se realizará el cambio de los sprites necesarios en
función de los puntos que se añadan lo cual complica levemente la implementación de
la mecánica ya que hay que realizar previamente la suma de los puntos obtenidos a la
puntuación total y después asignar los sprites correctos para que muestren el valor
resultante de la suma por pantalla.
Como última mecánica a examinar, se encuentra la de salto, una mecánica común en
cualquier videojuego de plataformas. Para esta mecánica, hay que fijarse en aspectos
como la velocidad del salto o la de caída, con el fin de obtener una representación lo más
idéntica a la real y que el jugador sienta que puede llegar a controlarlo de manera
correcta. Desconozco la manera de cómo se habrá implementado en Alien 3 o en
cualquiera de los otros videojuegos investigados, pero una forma podría ser mediante el
uso de una pequeña tabla de datos que almacene los valores a sumar o restar a la posición
Y del personaje en cuestión. De esta manera, lo único que habrá que realizar es recorrer
los valores e ir sumándolos a su posición hasta que se llegue al último valor.
16
• Ninja Gaiden – Master System
Ninja Gaiden es un videojuego de plataformas de
scroll lateral y de género de acción. Fue
desarrollado por SIMS y lanzado en 1992 para la
SMS. No es el primer juego de la saga y, por lo
tanto, contiene muchas mecánicas parecidas a sus
versiones predecesoras en NES.
Además, de las mecánicas comunes de juegos de
plataformas como saltar, atacar o trepar, también
se pueden utilizar habilidades especiales para
eliminar a los enemigos, lo cual hace que se
reduzca un contador y si este se encuentra a 0 no
puede volver a usar la habilidad hasta que se
vuelva a recargar con los objetos que encuentra
por el nivel.
El motivo principal por el cual se ha decidido incluir a Ninja Gaiden en los videojuegos
a analizar, es debido a su interfaz. Al contrario que en Alien 3, donde se implementa una
interfaz mucho más simple en la cual nunca se llegan a visualizar al mismo tiempo más
de dos elementos distintos de la misma, en Ninja Gaiden se utiliza una interfaz mucho
más compleja y variada, donde se puede encontrar información de todo tipo. Desde la
habilidad especial disponible en el momento hasta una barra de vida que se utiliza para
cada uno de los enfrentamientos con los jefes finales.
Figura 3. Portada de Ninja Gaiden.
Fuente:
www.wikipedia.org/wiki/Ninja_Gaiden_
(Master_System)
17
Figura 4. Ninja Gaiden para la SMS.
Fuente: www.youtube.com/WorldofLongplays
Esta interfaz, como se observará posteriormente, es una interfaz que se puede encontrar
en los juegos de plataformas de esa época. Sin embargo, este tipo de interfaz presenta
tanto ventajas como desventajas. Por un lado, es evidente que este tipo de interfaz
proporciona al jugador una mayor información sobre todo lo que ocurre en el juego. Esto
permite que su primera impresión ya sea positiva y que visualmente el juego resulte más
atractivo. Además, se proporciona un mayor “feedback” al jugador, ya que este recibe
mucha más información sobre lo que está ocurriendo dentro del juego.
Sin embargo, al tener una interfaz tan compleja y variada, esto deriva en que se tiene un
menor espacio disponible para el dibujado del resto de sprites que se vayan a utilizar en
el juego. Se desconoce cómo se ha implementado realmente está interfaz, pero se intuye
que todas aquellas partes que se modifican deben ser sprites y por lo tanto debe ocupar
espacio en el SAT (tabla de sprites). Esto deriva en un menor espacio disponible para
poder dibujar al mismo tiempo, por ejemplo, un mayor número de enemigos u objetos.
Se asume que las partes que no varíen, como las letras de “SCR” o “TIME” para el
contador de tiempo, vienen integradas conjuntamente con el dibujado del mapa, por lo
que no ocuparían espacio en el SAT sino en ROM.
18
Por estos motivos, se considerará utilizar este tipo de interfaz siempre que sea posible
manejar tal cantidad de sprites al mismo tiempo. En caso de que no sea posible, se optará
por una interfaz más sencilla que permita el correcto dibujado de todos los sprites que se
deseen.
• Alex Kidd in Shinobi World – Master System
Videojuego desarrollado por Sega y lanzado en
1990, en el cual el protagonista debe rescatar a su
novia de un malvado ninja. Para ello, el fantasma de
un antiguo ninja se fusiona con él otorgándole
poderes. Gracias a esto, el protagonista utiliza una
espada, la cual sirve no solo para matar a sus
enemigos, si no, también, para romper partes de
paredes y cofres. Dentro de estos cofres, se puede
encontrar desde vida extra hasta otra arma a utilizar
por el jugador.
El único aspecto atractivo de este videojuego y por
el cual se ha decidido analizar, es la mecánica de los
cofres. Como ya se ha explicado, mediante el arma
del protagonista se puede romper los cofres que
aperecen por los niveles para obtener diferentes ventajas. La idea es integrar este aspecto
para el videojuego del proyecto, utilizando esta mecánica de romper cofres para obtener
objetos que mejoren las habilidades del personaje o simplemente proporcionen salud.
Igualmente, se puede utilizar esta mecánica para que proporcione granadas en el caso de
que se haya terminado por integrar el aspecto de granadas.
Desde mi punto de vista, considero que es bastante sencilla de implementar la mecánica
de los cofres. Una forma de implementar podría ser la siguiente:
1. Se dibujan los cofres como sprites, no como parte del fondo de esta manera se
podrán borrar posteriormente.
Figura 5. Portada de Alex Kidd in
Shinobi World.
Fuente: www.wikipedia.org
/wiki/Alex_Kidd_in_Shinobi_World
19
2. Una vez dibujados, se implementaría una función que comprobase si el arma del
jugador colisiona con el sprite del cofre. Dicha función, solo se llamará cuando
el jugador realice la acción de atacar. En el caso del videojuego del proyecto, al
tratarse de un arma a distancia, se lanzaría la función cuando se realice el disparo
del arma.
3. Si resulta que colisionan ambos sprites, entonces el sprite del cofre dejaría de
dibujarse por pantalla y en su lugar se sustituiría por el sprite de la mejora que se
quiera proporcionar al jugador. Siempre teniendo en cuenta el espacio disponible
en el SAT y que no se solapen ninguno de los valores del SAT del nuevo sprite
aparecido con los valores de los sprites que ya estaban dibujados.
Por otro lado, al final de cada nivel surgen 2 puntuaciones: la puntuación total que lleva
conseguida el jugador en la partida y la puntuación más alta obtenida. La cuestión es
que, como se observará más adelante, hay más juegos que utilizan este sistema de 2
puntuaciones, pero en ningún momento se llega a entender cómo funciona realmente. La
cuestión reside en que no se cree posible que dicha puntuación se almacene una vez que
la consola es apagada. Se desconoce si es posible llegar a conseguir guardar datos del
jugador si la consola se desconecta. La única opción que se considera posible es que la
puntuación haga referencia a partidas anteriores siempre y cuando la consola no se haya
apagado, si no que al terminar el juego se haya reiniciado la partida.
Figura 6. Alex Kidd in Shinobi World para la SMS.
Fuente: www.youtube.com/WorldofLongplays
20
Finalmente, a diferencia a como ocurría en Ninja Gaiden, se puede observar una interfaz
muy simple, en la cual la única información que se proporciona al jugador es la salud
restante del personaje. En los momentos de enfrentamiento contra un jefe final de nivel,
también aparece la salud del mismo en la pantalla la cual utiliza el mismo formato de
dibujado que la del protagonista.
• Master of Darkness – Master System
Videojuego de plataformas desarrollado por SIMS y
lanzado por Sega en 1993. Es un juego muy parecido
al Castlevania de la NES, donde el jugador entra en el
rol de un psicólogo que trata de derrotar a Drácula, el
cual está detrás de numerosos asesinatos en Londres.
Como ya se ha comentado anteriormente, una de las
principales dudas relacionadas con el juego a realizar
para el proyecto reside en la manera a implementar el
sistema de armas y es por esa razón que varios de los
juegos analizados se deben principalmente por la
forma a que estos implementan este sistema. En el
caso de Master of Darkness, el protagonista jugable
tiene un arma principal, que es un arma a melee, y
un arma secundaria, que es un arma a distancia con
munición limitada.
Sin embargo, lo curioso de este sistema de armas reside en que, durante el transcurso del
juego, el jugador puede llegar a encontrar armas diferentes a utilizar, pero al recoger una
de esas armas esta se sustituye por la que el jugador tenía anteriormente equipada. Esto
implica que el jugador no puede llevar varias armas del mismo tipo, obligándole a elegir
en base de lo que le resulte más útil en cada momento.
Figura 7. Portada Master of Darkness.
Fuente: www.wikipedia.org
/wiki/Master_of_Darkness
21
Figura 8 Master of Darkness para la SMS.
Fuente: www.youtube.com/WorldofLongplays
Por otro lado, en este videojuego también se hace uso de bombas y se utilizan de una
manera muy parecida a cómo podrían usarse las granadas en el juego del proyecto.
Cuando se lanza una bomba en Master of Darkness, el Sprite ejecuta una especie de
parábola hasta caer al suelo o colisionar con un enemigo, en cuyo caso explota
inmediatamente eliminando al enemigo que este próximo.
Se desconoce actualmente de qué manera se habrá implementado ese movimiento
parabólico. Se intuye que para realizar este movimiento se utilizará una función que haga
mover el sprite de la bomba tanto en X como Y. La velocidad a la que se moverá el sprite
en el eje horizontal será siempre constante, sin embargo, en el eje Y será donde la
velocidad irá variando conforme transcurren los segundos, pasando de una velocidad que
aumentará progresivamente hasta que, trascurridos los segundos necesarios, empezará a
decrementar también progresivamente hasta colisionar con el enemigo o parte del mapa.
22
Otro de los aspectos a destacar en este videojuego de la SMS, reside en el apartado de la
IA. Si se observa de manera general el funcionamiento de los enemigos, se puede
observar que la mayoría no utiliza una Inteligencia Artificial muy compleja (o al menos
no lo aparenta). Por ejemplo, existe un tipo de enemigo que son los perros, estos siguen
un patrón muy sencillo: se desplazan de un punto a otro durante una cantidad de tiempo
y tras esto, cambian a un patrón de reposo, en el cual se quedan parados durante otro
intervalo de tiempo, el cual al terminar vuelven otra vez al patrón de movimiento y así
sucesivamente. Si colisionan con el jugador durante su movimiento este recibe daño.
También se encuentra otro ejemplo de enemigo que se desplazan de un punto a otro
repetidamente y de vez en cuando realizan un disparo hacia delante. Aparentemente, este
disparo parece aleatorio y no sigue ningún patrón, ya que, tras observarlos varias veces,
suelen disparar sin tener siquiera al jugador dentro de su campo de visión
También se encuentran enemigos fantasmas que se desplazan por el nivel sin colisionar
con el entorno y cuando detectan al jugador atacan desplazándose en una línea recta hacia
la dirección del jugador y si colisionan con él, este recibe daño.
Este IA que se utiliza en Master of Darkness es una IA que aparentemente no parece
demasiado compleja. Prácticamente todos los tipos enemigos se pueden implementar con
una simple máquina de estados la cual cambie permita cambiar el comportamiento del
enemigo cada vez que ocurra cierto aspecto o cada X segundos. Es por ello, por lo que
la IA de los enemigos del videojuego del proyecto se realice de esta forma.
• Zillion – Master System
Videojuego de plataformas desarrollado por Sega que
fue lanzado en 1987. Zillion está ambientado en un
mundo espacial donde existe una fuerza de
mantenimiento de la paz dentro del Sistema Planetario,
la cual desea destruir una base malvada y para ello el
personaje principal, denominado JJ, deberá infiltrarse
en dicha base y recuperar los cinco disquetes para
ingresar así la secuencia de autodestrucción en el
ordenador central de la base. Figura 9. Portada de Zillion.
Fuente: www.wikipedia.org
23
De todos los videojuegos analizados hasta el momento, Zillion es de los que posee una
de las mecánicas más simples, pero a la vez más divertidas y que ofrecen un
“gameplay” bastante entretenido. En primer lugar, la mecánica de combate, como acabo
de comentar es bastante simple y sencilla, pero sin embargo bastante divertida desde mi
punto de vista. El protagonista puede situarse de pie, agachado o tumbado, y en
cualquiera de las tres formas puede disparar su arma para elimnar a los enemigos o
destruir ciertos objetos del entorno. Además, estas tres posiciones permiten al jugador
esquivar los disparos de los enemigos, los cuales también pueden agacharse para disparar
e incluso disparar hacia arriba por si el jugador se encuentra situado por encima de ellos
lo que da mucho juego.
Figura 10. Zillion para la SMS.
Fuente: www.youtube.com/WorldofLongplays
Es probable que esta mecánica de combate se tome como referencia para ser integrar en
el videojuego del proyecto, ya que como ya se ha comentado varias veces, no se
considera muy compleja y, sin embargo, es bastante divertida.
24
Otro motivo por el cual Zillion es motivo de análisis en este proyecto, es debido a la
posibilidad de mejorar el arma principal. Al contrario que en juegos anteriores en los
que se podía utilizar más de un arma, aquí solo se dispone de una única arma a distancia,
la cual va siendo mejorada durante el trascurso del juego mediante accesorios que se
pueden encontrar por los niveles, pudiendo así mejorar aspectos como mayor daño o
mayor cadencia de disparo.
Al momento de observar esta mecánica, automáticamente se consideró bastante útil ya
que abre la posibilidad a añadir variedad al “gameplay” del juego y a la vez resulta mucho
más simple de implementar que, por ejemplo, añadir diversas armas que puedan ser
utilizadas. De esta manera, combinando mecánicas como la de los cofres que
proporcionan ayudas al romperlos, una posibilidad podría ser que solo haya un arma
disponible durante todo el juego y que los cofres pudiesen proporcionar mejoras
relacionadas con el arma. Mejoras tales como disparar más rápido o que las balas hagan
más daño, podrían ser alguno de los ejemplos. Además, con esta mecánica si se desea
añadir una mejora nueva al arma en algún momento dado será mucho más simple y
requerirá una menor cantidad de tiempo que el añadir una segunda arma utilizable.
• Operation C – Game Boy
Es un videojuego de Acción desarrollado por Konami
en 1991 para la Game Boy. En Operation C el jugador
toma el control de Bill Rizer, el cual debe destruir una
fuerza enemiga que almacena de manera secreta
alienígenas en su base.
Aunque se trate de un videojuego para una plataforma
distinta que la Sega Master System, el motivo único y
principal por el que está incluido en la lista reside en
su temática.
La idea para la temática del proyecto se basaba en una historia ambientada en la época
de la guerra de vietnam y que el protagonista fuese un soldado con características
parecidas al famoso personaje de Rambo. Características que se parecen mucho a las que
incluye el juego analizado en estos momentos y por el cual se toma como referencia para
coger ideas sobre diseño y sprites.
Figura 11. Portada de Operation C.
Fuente: www.wikipedia.org
/wiki/Operation_C_(video_game)
25
Figura 12. Operation C para la SMS.
Fuente: www.youtube.com/WorldofLongplays
26
4. Objetivos
El objetivo principal de este proyecto es el de realizar un videojuego para la consola de 8
bits, la Sega Master System. Además, el lenguaje de programación que se va a utilizar para
conseguir dicho objetivo es el que se utilizaba originalmente para la consola, el ensamblador
Z80.
Por otro lado, existen una serie de objetivos que también se pretenden lograr. En primer
lugar, se explicará el proceso de creación de una ROM básica, de tal manera que sirva
como base para poder crear cualquier videojuego para la SMS y que este sea total y
completamente funcional en una consola real y no solo en los emuladores.
En segundo lugar, como ya se puede observar en el apartado de marco teórico, se realizará
una investigación de videojuegos que se consideren similares con el que se pretende crear
en este proyecto. De ese modo, se analizarán todos aquellos videojuegos ya existentes para
la consola que puedan tener cierto parecido o que tengan mecánicas similares a las que tendrá
el videojuego del trabajo o que se consideren interesantes a incluir. Todo esto con el fin de
realizar una pequeña investigación que permita averiguar los métodos empleados en cada
uno de ellos para conseguir implementar todas aquellas mecánicas que se consideren
interesantes a integrar en el proyecto actual.
En tercer lugar, se explicará, con el mayor detalle posible, todo el proceso de desarrollo
que conlleva realizar un videojuego para la Master System. El objetivo es describir todo
lo que se ha ido efectuando durante el desarrollo y, así como, todos los posibles problemas
que hayan podido surgir y como se han solucionado en el caso de que se haya encontrado la
solución.
Finalmente, se escribirá un pequeño anexo al final de este documento donde se explicará
algunos de los aspectos técnicos de la consola Sega Master System. El objetivo de este
anexo es el de proporcionar mayor información sobre algunos aspectos técnicos de la consola
que se puedan comentar a lo largo de este documento y así facilitar el entendimiento tanto
como a mi persona como a la persona que lea el documento.
27
De manera resumida, todos los objetivos que se pretenden conseguir son los siguientes:
• Realización de un videojuego que funcione en Sega Master System.
• Mejorar los conocimientos de lenguaje ensamblador, concretamente, el lenguaje
que se utiliza para la CPU de la SMS, el Z80.
• Investigar sobre aspectos técnicos de videojuegos similares: Descubrir como
realizan ciertos aspectos y que técnicas utilizan en algunos videojuegos que tengan
características similares al videojuego del proyecto.
• Detallar el proceso de creación de una ROM básica para Sega Master System:
formato, cuestiones técnicas, herramientas disponibles, problemas encontrados etc.
• Pequeño anexo con detalles técnicos de la consola Sega Master System.
28
5. Metodología
El tipo de metodología que se ha aplicado a este proyecto está basado en las metodologías
de tipo ágil. Aunque por lo general el uso de este tipo de metodologías suele darse en
proyectos en grupo con el objetivo de trabajar de manera colectiva y obtener el mejor
resultado posible, es necesario aplicar un estilo de metodología ágil (Scrum, Cristal etc.)
ya que me enfrento a un proyecto sobre un tema del cual tengo poco conocimiento previo
del mismo al tratarse de una consola con la que nunca he trabajado anteriormente.
No se ha aplicado un estilo de metodología en concreto, sino que se ha tomado la idea por
la cual se fundamentan todas las metodologías ágiles existentes que, aunque diferentes entre
ellas, todas se basan en seguir un desarrollo iterativo durante el cual se van añadiendo
funcionalidades al producto o proyecto.
Esto quiere decir que el proyecto es dividido en diferentes etapas o iteraciones. Al finalizar
cada iteración se realiza una pequeña revisión de la misma con el objetivo de encontrar fallos
o bugs que hayan podido surgir. Sin embargo, al tratarse de un proyecto del cual se tiene
poco conocimiento previo, este está en constante modificación y lo que se realizó en la
primera etapa puede que no se mantenga igual en iteraciones siguientes debido a que cuanto
más tiempo transcurra trabajando en el proyecto más conocimientos se tendrán sobre el
mismo y se pueden detectar errores que inicialmente no se descubrieron.
Teniendo esto en mente, la planificación del proyecto se ha dividido en diferentes
iteraciones:
1. Fase de Investigación y pruebas: Durante esta etapa, se analizarán los aspectos
técnicos de la consola Master System con el objetivo de aprender sus limitaciones y
puntos fuertes para averiguar así que aspectos podrían ser posibles de implementar y
cuales no dentro del tiempo disponible. Además, durante esta fase también se
realizarán pequeñas pruebas sobre la consola y algún pequeño prototipo para ir
viendo cómo es trabajar con ella y los posibles problemas que se podrían encontrar.
29
2. Fase de preparación: Una vez terminado la primera fase de investigación, comienza
la fase de preparación de la idea del videojuego. Para ello, primero se hará un
pequeño análisis de los videojuegos creados para la misma plataforma durante la
época, con la intención de obtener ideas que puedan ser aplicadas al proyecto y
averiguar de qué manera podrían haber sido implementadas. Asimismo, una vez
analizados los videojuegos necesarios, se comenzará a realizar un pequeño GDD para
describir en qué consistirá el videojuego a crear y todo lo que se pretende
implementar en él.
3. Fase de desarrollo: Etapa de mayor duración durante la cual se realizará el
videojuego del proyecto mientras, a su vez, se va documentando todo lo realizado en
la memoria.
4. Fase de revisión y finalización: Etapa final que consistirá principalmente en
terminar todos los apartados de la memoria del trabajo que hayan quedado sin
finalizar y de realizar una revisión general del documento. Además, también servirá
para corregir fallos o bugs que puedan haber surgido en el videojuego.
Se trata de una planificación muy genérica, pero al tratarse de un proyecto el cual está en
constante evolución y modificación es posible que esta planificación se vea modificada
durante el transcurso del proyecto. Sin embargo, con esta pequeña planificación ya se puede
tener una idea de cómo se dividirá el trabajo del proyecto y permitirá avanzar de una manera
más gestionada y, por lo tanto, más rápida.
30
6. Creación de una ROM básica
6.1. Introducción
Si se quiere realizar un videojuego para la Sega Master System, primero es necesario crear
una ROM válida para que el juego pueda funcionar en una consola real. Esta explicación
toma como referencia el tutorial realizado por Maxim en SMSPOWER [23].
En esta sección, se va a explicar el proceso para crear una ROM básica para la SMS que
contenga las instrucciones necesarias para que la ROM funcione de manera correcta.
Algunas de estas instrucciones servirán para aspectos como la inicialización de los registros
del VDP, la configuración de los bancos y las ranuras, para vaciar la VRAM etc. De esta
manera, al finalizar esta sección, se tendrá una pequeña ROM que funcionará en una SMS
y, que, a su vez, servirá como base para poder crear futuros juegos.
Antes de comenzar a explicar el proceso, conviene indicar las herramientas que son
necesarias para poder crear la ROM:
• Un editor de texto para poder escribir todo el código necesario para crear la ROM.
El que se ha utilizado para esta explicación es Sublime Text 3.
• Un ensamblador que permita convertir el fichero escrito en lenguaje ensamblador a
código máquina para que este pueda ser entendido por la consola. El ensamblador
que se va a utilizar para realizar dicho cometido es WLA DX [12], debido a que este
software proporciona multitud de directivas útiles a la hora de programar que
facilitará el trabajo de creación del proyecto.
• Un emulador para poder probar que la ROM funcione correctamente. El emulador
que se ha utilizado para este ejemplo es MEKA [8], aunque también hay otros como
el Emukon que también puede servir.
Además de estas herramientas, en este mismo documento se encuentra disponible un
pequeño anexo con información extra sobre aspectos técnicos de la consola de Sega Master
System que sirven para completar toda información que no sea descrita en las siguientes
líneas. Asimismo, también hay disponible un pequeño glosario con algunos de los términos
que se puedan encontrar a lo largo del documento.
31
6.2. Bancos y ranuras de la ROM
Lo primero que se debe realizar, es indicar a la ROM como de grande va a ser, es decir, se
debe definir el número de bancos que se desea tener y el tamaño de cada uno de estos. Esto
es obligatorio debido a que es necesario saber con cuantos bancos se va a trabajar en la ROM
para obtener así el tamaño total del cartucho y, además, como se observará posteriormente,
esto permite también escribir el conjunto de instrucciones de código en distintas secciones
que pueden ser intercambiadas en todo momento.
Por otro lado, también es necesario definir el número de ranuras disponibles, es decir, hay
que indicar en cuantas ranuras o bloques va a ser dividido el espacio de direcciones del Z80.
La definición de estas ranuras es necesario debido a que es a donde se van a insertar los
bancos procedentes de la ROM del cartucho donde se tendrá escrito el código del juego.
Dicho todo esto, primero se va a definir el mapa de memoria. Para ello, se debe utilizar la
primera directiva disponible en WLA DX [12], denominada memorymap.
Figura 13. Definición de ranuras.
Fuente: WLA DX [12].
Si se observa la Figura 13, mediante slot se realiza la definición de una ranura y la dirección
de inicio desde donde esta comenzará. Pueden ser definidas hasta 256 ranuras (desde la
ranura 0 hasta la 256). Además, antes de definir la ranura, se especifica el tamaño de la
misma en bytes mediante slotsize.
32
Por otro lado, también existe la directiva defaultslot. Esta es utilizada para definir la ranura
que se quiera tener por defecto, de manera que, si se define un banco, como se observará
ahora, sin indicarle la ranura en la cual va a ser insertado, entonces será insertado en la ranura
que se haya indicado en defaultslot.
Sabiendo esto, se puede observar que en la Figura 13 se está estableciendo que la SMS
contenga 2 ranuras: La primera empezará en la dirección $0000 y tendrá un tamaño de 8 kb
(2000 bytes) y la segunda ranura empezará en $2000 y tendrá un tamaño de 24 kb (6000
bytes). Además, se establece la ranura 1 como la ranura por defecto.
Una vez definido las ranuras que se van a tener, hay que definir el mapa de bancos de ROM.
Para ello, se utiliza la directiva rombankmap, que permite describir los bancos que va a
contener la ROM de una manera parecida a como se realiza en memorymap.
Figura 14. Definición de bancos.
Fuente: WLA DX [12].
Como se puede observar en la Figura 14, dentro de la directiva de rombankmap, se utiliza
bankstotal para definir el número de bancos totales que va a contener el cartucho y,
mediante banksize, se establece el tamaño de cada uno de estos bancos indicados en la
directiva banks.
33
Una vez realizado todo esto, ya se tiene configurado el número de bancos que va a contener
la ROM y el número de ranuras que van a haber en el espacio de direcciones del Z80. En la
Figura 15, se observa una configuración mucho más simple a la que se ha visto
anteriormente, que servirá para realizar el ejemplo que se desea mostrar, donde solamente
se define una ranura y un banco, ambos de 32 kb de tamaño.
Figura 15. Definición de las ranuras y bancos que contendrá la ROM.
Fuente: Elaboración propia.
6.3. Validación de la ROM
Ahora se procede a configurar el arranque de la consola. Lo primero que se debe realizar
es validar la ROM, ya que la BIOS comprueba todas las ranuras disponibles de la consola
para comprobar si hay algún software valido a ejecutar y arranca lo primero que encuentre,
es decir, comprueba si hay algún videojuego válido a ejecutar.
La manera más rápida para poder realizar que la cabecera de la ROM sea válida y pueda
funcionar no solo en un emulador, sino también en una SMS real, es utilizar otra de las
directivas que proporciona WLA DX [12], la directiva sdsctag.
Esta directiva, es la que permite que el videojuego pueda funcionar en una consola real de
Sega Master System, ya que permite que la BIOS tome como válida la cabecera de la ROM.
Por otro lado, también permite añadir información adicional al programa creado, como su
versión, el nombre etc.
34
Figura 16. Definición de la etiqueta SDSC.
Fuente: Elaboración propia.
Mediante el simple proceso de incluir esta directiva, la cabecera de la ROM ya es válida.
Esto es gracias a que la directiva sdsctag define a su vez otra directiva, la smstag.
Esta última directiva, lo que realiza es forzar al ensamblador a escribir una etiqueta ROM
común en el fichero de ROM mediante la escritura del string “TMR SEGA” en los 8 primeros
bytes de la cabecera de la ROM. Asimismo, la smstag, también define otra directiva, la
directiva computesmschecksum, la cual escribe a su vez un checksum de ROM en la
dirección de memoria correspondiente.
Luego, mediante la simple inclusión de la directiva sdsctag se realizan una serie de
operaciones que permiten validar la ROM. Todo esto proceso, se encuentra explicado con
mayor detalle en la sección BIOS del anexo perteneciente a este documento.
Tras realizar la validación, se debe indicar en que zona va a comenzar a ejecutarse el código
del videojuego. Concretamente. hay que indicar el banco, la ranura y la dirección de memoria
correspondiente.
Esto puede ser realizado fácilmente con la directiva bank, mediante la cual indicamos el
banco donde escribiremos el código y la ranura donde va a ser insertado ese banco. Si solo
especificamos el banco, este se insertará en la ranura que hayamos indicado por defecto en
defaultslot.
35
Figura 17. Definición de la dirección de memoria en la cual se escribirá el código.
Fuente: Elaboración propia.
Siempre que se defina la directiva “.bank 0 slot 0”, seguidamente debe utilizarse la directiva
org u la orga. La directiva org, indica la dirección de inicio de memoria relativa al banco de
ROM que se haya indicado previamente en bank, la otra directiva, también proporciona la
dirección de inicio de memoria, pero de manera absoluta con respecto a toda la memoria. En
nuestro caso se utilizará org.
6.4. Gestión de interrupciones
A continuación, se deben gestionar las interrupciones, si se realiza hincapié en la Figura
18, se puede observar que se desactivan las interrupciones, esto se realiza con la intención
de que la ejecución del programa no salte a otras direcciones de memoria y ejecute otro
código distinto, además se define el modo 1 de interrupción del Z80, lo que hace que todas
las interrupciones salten a la dirección $0038 cuando estas se produzcan.
Figura 18. Gestión de interrupciones en la SMS.
Fuente: Elaboración propia.
36
Tras este código, se realiza un salto absoluto hacia la dirección donde se encuentra definida
la etiqueta main que es donde se encontraría el código principal de nuestro programa. Sin
embargo, antes de ir a esa dirección, se debe realizar una cosa antes: La gestión de la
interrupción de pausa de la consola, ya que es una interrupción no enmascarable [14] y no
puede ser ignorada por la CPU, por lo que debe ser tratada en la dirección $0066. En este
ejemplo, no se realizará ninguna acción cuando se pulse el botón de pausa, por lo que
simplemente se escribe la instrucción retn que se utiliza para volver a la dirección de
memoria donde se estaba antes de que saltase la interrupción.
Figura 19. Gestión del botón de pausa en la SMS.
Fuente: Elaboración propia.
6.5. Inicialización de la pila y el VDP
Llegados a este punto, ya se han realizado aspectos como la configuración del espacio de
ROM, la validación de la ROM o la gestión de aquellas interrupciones que puedan
producirse. Por lo que ya se han realizado todos los aspectos que son necesarios al principio
del código de la ROM.
Ahora, se va a empezar a trabajar con el programa principal, pero antes de realizar esto, es
necesario inicializar ciertas partes del sistema de la consola para que funcione correctamente.
En primer lugar, es necesario que la pila apunte a algún sitio de la memoria RAM y esto
debe hacerse antes de que ocurra cualquier interrupción, ya que estas harán uso de ella. Por
este motivo, es obligatorio realizar la inicialización del puntero a la pila lo más pronto posible
en el programa para evitar así problemas.
37
Sabiendo que la pila aumenta su espacio hacia direcciones inferiores de la RAM cada vez
que se apila un valor en ella, lo normal es inicializar el puntero a la dirección de memoria
RAM más alta disponible para evitar así que sobrescriba alguno de los datos que tengamos
escritos en la RAM. EN este ejemplo, no es estrictamente necesario realizar esto ya que la
pila no va a ser utilizada, pero conviene saberlo para evitar futuros problemas en otros
programas que se deseen realizar.
Dicho esto, se inicializa el puntero de la pila a la dirección $DFF0, que corresponde a casi
el final de la RAM disponible en nuestra consola y es la dirección recomendada por el
manual oficial de la Sega Master System [28].
Después de haber inicializado el puntero de la pila, hay que configurar el chip de gráficos de
la consola, denominado Video Display Processor. Lo que se va a realizar es la inicialización
de los registros que contiene el VDP. Para ello, se le pasará un bloque de datos que permite
inicializar cada uno de los bits de los registros con un valor determinado.
Figura 20. Valores iniciales para los registros del VDP.
Fuente: Elaboración propia.
De manera resumida, cada uno de los bits que contienen estos registros configuran ciertos
aspectos gráficos de la consola, como, por ejemplo, el color de fondo mediante el uso de los
colores de la paleta, activar o desactivar la visualización de pantalla, la dirección de memoria
donde se copiarán los tiles a usar en los sprites, si se quiere desactivar el desplazamiento en
algunas columnas de la pantalla etc.
38
Concretamente, los aspectos que se han configurado con estos valores iniciales han sido:
• Activación el modo específico de visualización que tiene el chip de VDP de la SMS
(El modo 4).
• Deshabilitación de la visualización de la pantalla.
• Establecer las direcciones base de memoria de la VRAM a su valor más
frecuentemente utilizado.
• Uso de los colores de la paleta para el fondo/borde de la pantalla.
• Etc.
Estos valores indicados en la figura 20, son valores que no necesariamente deber ser los
mismos siempre, cambiarán en función de cómo sea el videojuego a crear y los aspectos que
se deseen realizar, por lo que es importante conocer qué es lo que hace cada uno de los
registros del VDP [18][33]. De nuevo, para no extender la explicación, en el anexo se
encuentra disponible toda la información sobre lo que hace cada uno de los bits de estos
registros.
Una vez definido los valores iniciales de los registros del VDP, deben ser enviados al puerto
de control del mismo para poder establecer dichos valores y, para ello, se utiliza la
instrucción del Z80: otir, la cual permite mover un conjunto de bytes desde una dirección al
puerto que se desea (véase la figura 21).
Figura 21. Se envía los valores iniciales del VDP al puerto de control.
Fuente: Elaboración propia.
39
6.6. Liberación de la VRAM
Ahora que se han inicializado los registros del VDP a unos valores adecuados para la ROM
básica, hay que vaciar la memoria de video. Es necesario vaciar el contenido que pueda
contener la VRAM del VDP, ya que no se sabe qué es lo que puede contener y es preferible
vaciarla antes de que se realice cualquier acción con ella. De hecho, la primera vez que se
acceda a una consola real, la VRAM contendrá el logo oficial de Sega procedente de la
BIOS.
Para poder hacer esto, hay que enviarle la información necesaria al puerto de control del
VDP, es decir, hay que indicarle que queremos escribir en la VRAM y la dirección en la cual
se quiere escribir.
Figura 22. Dirección de VRAM y orden a realizar.
Fuente: Elaboración propia.
Una vez que se le ha indicado correctamente lo que se quiere hacer, hay que rellenar toda la
memoria de video con un valor (en el caso que repercute este ejemplo se utilizará el valor
$00 que hace referencia al color negro) y para ello, se necesita un contador que permita
recorrerla la zona de memoria entera. En la Figura 23, se observa el uso de un contador que
contiene el valor $4000 que corresponde a los 16 kb que tiene la memoria de video de la
SMS.
40
Figura 23. Vaciado de la VRAM.
Fuente: Elaboración propia.
Llegados a este punto, prácticamente ya está realizado todo lo necesario para poder obtener
una ROM básica. Lo único que faltaría sería activar la visualización de la pantalla para poder
observar algo por pantalla y esto se realiza pasando el valor $E4 al registro $01, tal cual
como se ha realizado anteriormente en la sección de inicialización del VDP. Al activar la
visualización de la pantalla debería verse aquello que hayamos implementando. En la figura
24, se puede ver mi versión realizada del “hola mundo” procedente del tutorial de Maxim en
SMSPOWER [23].
Figura 24. Mensaje de ¡Hola Mundo! mostrado por pantalla.
Fuente: How to program de Maxim [23].
41
Con esta base, y teniendo siempre en cuenta los valores de los registros del VDP que habrá
que modificarlos en función de las necesidades del proyecto, ya es posible construir
cualquier videojuego para la SMS y que funcione, tanto como en un emulador como en una
consola real.
42
7. Diseño del videojuego
En las siguientes líneas aparecerán imágenes relacionadas con el contenido gráfico utilizado
dentro del juego de Invasion realizado para este proyecto. Todo este contenido artístico no
está realizado por el autor de este proyecto y ha sido obtenido de diferentes sitios web
que ofrecen la posibilidad de usar gratuitamente materiales tales como: diseño de enemigos,
de armas, de objetos u otros [6][10][16].
Debido a esto, junto a la descripción de la imagen, se adjuntará el autor o autores de cada
uno de los materiales artísticos utilizados dentro del videojuego.
7.1. Descripción general
Invasion es un videojuego de plataformas para la Sega Master System. El jugador controla
a un soldado perteneciente a las fuerzas especiales de Estados Unidos a través de los bosques
de algún lugar de Vietnam, con el objetivo de destruir una nueva amenaza que acaba de
surgir. Toda la acción tiene lugar desde una perspectiva lateral y los niveles están conectados
mediante puertas que al abrirse transportan al jugador al siguiente mapa.
Mediante el D-Pad, el jugador puede mover al personaje principal a través de los niveles de
izquierda a derecha en función de la colocación del mismo. Del mismo modo, si el D-Pad se
encuentra colocado hacia arriba, el personaje efectuará un salto. Además, mediante el uso
de los dos botones disponibles de la consola, se puede efectuar un disparo para eliminar a
los enemigos e interaccionar con los elementos del nivel como pueden ser las puertas u otros.
El personaje controlable por el jugador dispone de un fusil de asalto. Este fusil, es la única
arma utilizable en el juego y se utiliza principalmente para eliminar a los enemigos que
puedan ir apareciendo durante el transcurso del juego.
Desde que comienza el juego hasta que finaliza hay un contador de tiempo que determina el
tiempo total disponible para finalizar el juego. Si el tiempo finaliza antes de llegar al final
del nivel se acaba la partida, independientemente del número de vidas restantes del jugador.
43
7.2. Historia
Corre el año 1966, se han detectado una serie de criaturas extrañas en los bosques de
Vietnam, las cuales están destruyendo todo a su paso, matando a civiles y cualquier otro tipo
de vida que se encuentren. Informes recientes de nuestros agentes asignados en la zona
indican que estas criaturas son inteligentes y están preparando una especie de defensa
alrededor de la zona para evitar que nadie entre o salga. Se prevé que la zona deba estar
fuertemente defendida por estas criaturas e incluso hay construidas estructuras desconocidas
cuya función se desconoce.
Tu eres Mark, un soldado perteneciente a las fuerzas especiales de Estados Unidos que ha
sido enviado para acabar con esta amenaza desconocida. La misión asignada a tu pelotón es
encontrar la fuente de origen de estos seres y destruirla. Para ello, deberás avanzar por los
bosques de Vietnam y enfrentarte a estas criaturas hasta encontrar la forma erradicarlas.
Durante la misión, tu pelotón ha sido emboscado y acribillado por estos seres inteligentes.
Por suerte o por desgracia, tu eres el único superviviente, pero has caído en una especie de
cuevas que se encontraban ocultas hasta ahora y estás fuertemente herido. Ahora debes
acabar a toda costa con la misión que empezaste con tu equipo para poder salvar a la
humanidad.
7.3. Género y Audiencia
Invasion es un juego de género de Acción/Aventura, ya que, por una parte, el jugador debe
utilizar su habilidad, reflejos y puntería para eliminar a los enemigos que van apareciendo
por los niveles del juego, y, por otro lado, en algunas zonas el jugador deberá resolver
pequeños puzles.
Por otro lado, Invasion está enfocado para todos los públicos y edades.
7.4. Ámbito
Invasión tiene lugar en los bosques de Vietnam. Los niveles que aparecerán principalmente
en el juego corresponderán a unas cuevas ocultas dentro de los bosques. En los niveles
finales del juego, el personaje Mark consigue salir a la superficie por lo que los niveles
corresponderán a una simple representación de estos bosques. Hay disponibles un total de 9
niveles jugables en Invasión
44
Solo habrá disponible un personaje controlable por el jugador, el soldado Mark. El cual
solo podrá utilizar una única arma, el fusil de asalto del ejército.
Como NPC’s únicamente se encontrarán dos tipos de enemigos a los cuales el jugador deberá
eliminar. Cada uno con su patrón de comportamiento distinto.
Finalmente, distribuidos por los niveles habrá un corazón que proporcionará vida al jugador
y una llave para poder abrir las puertas en caso de que sea necesario.
7.5. Jugabilidad
La forma para progresar a través de los diferentes niveles del juego es bastante sencilla.
Mediante la apertura de una puerta el jugador podrá pasar de un mapa a otro. Esta puerta
se podrá encontrar en prácticamente todos los niveles.
En ocasiones, para poder abrir esta puerta, el jugador requerirá de una llave que estará
colocada en algún lugar del mismo nivel. En los mapas que se requiera de una llave, la puerta
no se abrirá hasta que no se haya recogido.
Por otro lado, para poder avanzar por los mapas, el jugador deberá hacer uso del salto y
movimiento del personaje jugable para avanzar por las plataformas. Asimismo, en algunos
niveles aparecerán enemigos que se podrán eliminar mediante el arma principal del
personaje.
En el último nivel jugable de Invasion, aparecerá un portal el cual el jugador deberá destruir
para poder cumplir la misión de Mark. Para lograr este objetivo, el jugador deberá destruir
los 3 cristales que dan vida al portal. Para destruir los cristales valdrá con simplemente
dispararles una única vez.
Una vez destruidos todos y cada uno de los cristales el juego habrá finalizado, pudiendo
reiniciar la partida si el jugador lo desea.
7.6. Mecánicas
A continuación, se van a detallar todas las mecánicas disponibles en Invasion. En primer
lugar, se van a describir aquellas que están relacionadas con la interfaz del juego:
45
• Contador de Tiempo: Una vez comenzado el juego, en la interfaz aparecerá un
simple contador de tiempo que indicará el tiempo total restante para terminar el
juego. El tiempo total disponible para finalizar el juego son unos 150 segundos. Si el
contador de tiempo llega a 0 y el jugador no ha logrado alcanzar el último nivel y
destruir los cristales, aparecerá por pantalla un mensaje indicando al jugador que se
ha terminado el juego Dentro de esta pantalla, el jugador podrá reiniciar la partida
pulsando el botón 2 de la consola. Si se pulsa dicho botón, el contador vuelve a su
valor inicial.
• Contador de vidas: En la interfaz también hay disponible un contador de vidas que
indica el número de vidas restantes del jugador. El número de vidas disponibles
puede ser aumentado recogiendo los corazones distribuidos por los niveles. Si el
contador de vidas llega a 0 se termina el juego. El número de vidas inicial al
comenzar el juego es 1 para simbolizar que el personaje está herido. Acciones como
recibir daño o caer fuera del mapa restan una vida al contador.
A continuación, se van a describir las mecánicas relacionadas con el personaje controlable
por el jugador:
• Saltar: Permite al personaje elevarse una pequeña distancia hacia arriba. Esto
permite al jugador poder avanzar por las plataformas y obstáculos que se encuentran
distribuidos a lo largo de los niveles de juego.
• Disparar: Acción que permite el personaje disparar las balas del fusil con alcance
limitado. Solamente se puede efectuar un disparo hacia la derecha o hacia la
izquierda. Cada vez que se pulsa el botón de disparo se disparará una bala siempre y
cuando no se haya disparado una ya. En el caso de que ya se haya disparado una bala
y no haya transcurrido el tiempo requerido para disparar la siguiente bala (2
segundos) no se podrá efectuar otro disparo. Mediante las balas del arma se pueden
eliminar a los enemigos y los cristales del último nivel.
• Interaccionar con el entorno: Si el jugador se encuentra cerca de una puerta podrá
pulsar el botón 2 de la consola para abrirla, siempre y cuando esta no requiera de una
llave para poder abrirse. En uno de los niveles finales, se utilizará esta misma acción
para poder usar una escalera.
46
• Salud: Representa la vida restante del jugador. Este valor puede ser incrementado
mediante la recogida de corazones o decrementado por el daño de los enemigos, por
la caída fuera del mapa o por que el contador de tiempo haya llegado a 0. En este
último caso, se pierden todas las vidas dando resultado al final del juego.
• Invulnerabilidad: Cuando el jugador reciba daño de cualquier fuente, el sprite de
su personaje empezará a realizar un efecto de parpadeo. Mientras duré este efecto
significará que el jugador es invulnerable para los enemigos por lo que no recibirá
daño alguno mientras dure el efecto. El efecto de invulnerabilidad persiste hasta 4
segundos. La invulnerabilidad no impide que el jugador pueda perder una vida por
caída fuera del mapa.
Figura 25. Sprite del personaje principal.
Fuente: Blue Yeti Studios [4].
Ahora se van a describir todas las mecánicas relacionadas con el nivel, es decir, todas
aquellas que tienen que ver con la interactuación con el entorno, los elementos disponibles,
el progreso de las secciones y demás:
• Agua/precipicio: En alguno de los niveles del juego, se encontrarán plataformas
separadas por una pequeña distancia. Entre las plataformas puede encontrarse agua
o un precipicio, en ambos casos si el jugador no consigue alcanzar la plataforma
siguiente y cae fuera de la misma, perderá una vida. Si no le quedan vidas, perderá
la partida.
• Corazones de vidas: Al pasar por encima de estos elementos el jugador verá
incrementado en uno su valor de salud. No hay límite máximo de vidas que pueda
tener el jugador.
47
Figura 26. Sprite de vida.
Fuente: DontMind8.
• Llaves: En algunos mapas aparecerá una llave. En estos casos, será necesario
recogerla para poder abrir la puerta del nivel. Para recoger la llave solo hay que pasar
por encima de la misma y se recogerá automáticamente. Al hacer uso de la llave se
pierde la misma.
Figura 27. Sprite de llave.
Fuente: BizmasterStudios [34].
• Cristales: En el nivel final de Invasión, aparecerán un total de 3 cristales. Estos
cristales proporcionan la energía necesaria para que el portal de los enemigos se
mantenga encendido. Pueden ser destruidos mediante el impacto de una sola bala del
arma del protagonista. Al ser destruidos todos los cristales el portal se desactiva y se
termina el juego.
Figura 28. Sprite del cristal.
48
• Puertas: En la mayoría de los niveles aparecerá una puerta dibujada por pantalla.
Esta puerta será necesaria para poder avanzar al siguiente nivel. Para hacer uso de
una puerta será necesario que el jugador pulse el botón 2 de la SMS. Si no hay
ninguna llave en el mapa, la puerta se abrirá lo que resultará en un cambio de mapa.
Figura 29. Sprite de puerta.
Fuente: Coolphill.
7.7. Enemigos
En Invasión el jugador podrá encontrar 2 tipos de enemigos diferentes, cada uno con un
patrón de comportamiento distinto.
En primer lugar, se encuentran los enemigos a melé los cuales son caracterizados por hacer
daño al jugador cuando este colisiona con ellos. A continuación, se detallan las
características de los mismos:
• Salud: Son necesarios 3 disparos del jugador para acabar con los enemigos a melé.
• Movimiento: Realizan un pequeño recorrido de un punto A hasta otro punto B y
viceversa durante un corto periodo de tiempo. Cuando este tiempo finaliza, el
enemigo se queda detenido en un punto fijo hasta que transcurra otro corto periodo
de tiempo. Una vez finalizado este segundo tiempo, este vuelve a realizar el recorrido
entre los dos puntos asignados. Siempre realiza el mismo recorrido, aunque el punto
donde puede permanecer inmóvil puede variar algunas veces.
• Ataque: No realizan ningún tipo de ataque especial. El daño que pueden hacer el
jugador se basa en el contacto de estos enemigos con el jugador. Cuando el jugador
colisiona con alguno de estos enemigos, este ve decrementado en uno su contador de
vidas.
49
Figura 30. Sprites del enemigo a melé.
Fuente: Jesse M (@Jsf23Art).
En segundo lugar, se encuentran los enemigos a distancia. A continuación, se detallan las
características principales de los mismos:
• Salud: Son necesarios 2 disparos del jugador para eliminar a estos enemigos.
• Movimiento: Realizan un pequeño recorrido de un punto A hasta otro punto B y
viceversa durante un corto periodo de tiempo. Cuando este tiempo termina, el
enemigo se queda detenido en un punto fijo hasta que transcurra otro pequeño
tiempo. Una vez finalizado este segundo tiempo, este vuelve a realizar el recorrido
entre los dos puntos asignados. Siempre realiza el mismo recorrido, aunque el punto
donde puede permanecer inmóvil puede variar algunas veces.
• Ataque: Dentro del periodo de tiempo en el cual el enemigo permanece inmóvil en
una posición fija, es cuando este determina la posición del jugador para calcular la
dirección en la que disparar la bala, por lo que el disparo de la misma siempre irá en
la dirección en la que se encuentra el jugador. La bala del enemigo dispone de un
alcance limitado y puede colisionar con los elementos del entorno.
50
7.8. Niveles
Existen un total de 9 niveles jugables en Invasión. Cada uno de ellos tiene una distribución
única y el jugador deberá afrontarlo de una manera distinta.
En total hay disponibles 2 biomas distintos. El primer bioma estará basado en una temática
de cuevas, donde principalmente se observarán materiales de piedra o roca y pequeñas
estructuras como columnas. Además, en algunos niveles también se podrá encontrar
pequeñas zonas con agua.
Figura 32. Bioma de zona cuevas.
Fuente: Adam Saltsman (@FinjiCo).
Figura 31. Sprites del enemigo a distancia.
Autor: Warren Clark.
51
El segundo bioma disponible será un bosque donde principalmente se encontrarán materiales
formados por hierba, madera o tierra. Todas las plataformas que se puedan encontrar en este
bioma estarán formadas por los materiales mencionados anteriormente.
Figura 33. Bioma de bosques.
Fuente: Vnitti (@vnitti_art).
A continuación, se muestran algunos ejemplos de los niveles creados para Invasión a partir
de estos tiles mostrados anteriormente:
Figura 34. Niveles creados para el primer bioma.
Fuente: Elaboración propia.
52
Figura 35. Niveles creados para el segundo bioma.
Fuente: Elaboración propia.
7.9. Controles
En la siguiente figura se pueden observar los controles del juego Invasion para la Sega
Master System.
Figura 36. Controles de Invasion para la SMS.
Fuente: www.1001freedownloads.com por PanamaG.
53
8. Desarrollo del videojuego
El objetivo de este apartado, es describir, lo más detalladamente posible, todo el proceso que
he tenido que realizar para poder conseguir un videojuego jugable en la Master System.
Mi intención es conseguir que esta sección sirva como apoyo de ayuda a otras personas que
se encuentren en la misma situación que la mía y quieran o deban crear un videojuego para
esta consola de 8 bits.
De esta manera, describiré todos los pasos que he ido realizando y todos aquellos problemas
que me haya ido encontrando durante el desarrollo, así como, las soluciones (si las he
encontrado) de los mismos.
8.1. Prototipo Mágica
Antes de empezar realmente a crear el videojuego para este proyecto, primero pretendo
realizar un pequeño prototipo donde pueda probar algunos aspectos básicos, ya que es la
primera vez que realizo algo para la SMS y desconozco como se realizan muchos aspectos
como: cargar tilemaps, sprites, hacer animaciones, recoger la entrada del teclado por el
usuario, colisiones etc.
A todo esto, hay que sumarle que voy a utilizar programas y software que no he utilizado
nunca, como es el ensamblador WLA DX [12] y el emulador MEKA [8], por lo que este
primer prototipo me sirve también para poder probarlos y entenderlos mejor antes de
ponerme directamente con el videojuego de este proyecto.
El prototipo que voy a realizar está basado en el videojuego Mágica de Amstrad CPC
creado por Juan J. Martínez para el CPCRetrodev de 2015 [15]. La razón por lo que he
decido crear un prototipo basado en este videojuego es debido a que comparte diversas
similitudes con el juego de mi proyecto: Juego de plataformas con mecánicas básicas como,
por ejemplo, saltar, disparar, colisiones con el entorno etc. Todo esto son mecánicas simples
que mi videojuego va a requerir tarde o temprano, por lo que la mayor parte de lo que realice
en este prototipo es trabajo que ya tendré realizado para integrar el videojuego del proyecto.
54
Figura 37. Mágica para Amstrad CPC.
Fuente: Sitio web de Juan J. Martinez, creador de Mágica [15].
Como ya he comentado previamente, es la primera vez que realizo un videojuego para esta
consola y, por consiguiente, no tengo ni idea de cómo empezar a hacer algo jugable para la
SMS. Lo único que conozco, es crear una pequeña base para un juego, de manera que este
pueda funcionar sin problemas en una consola Master System real. Dicha base, no la voy a
explicar aquí, ya que esta explicación ya está realizada en el apartado de creación de una
ROM básica de este documento, donde se explican aspectos como: el proceso de inicializar
el arranque de la consola, inicializar los registros del VDP etc.
A partir de esta base que ya tengo creada, voy a empezar a escribir el código de mi prototipo.
Para ello, buscando por internet información sobre aspectos de la SMS, me he topado con
una página muy interesante, llamada smspower [9].
En esta página, he encontrado información muy útil que me ha ayudado a crear la base para
que mi programa pueda funcionar de manera correcta en la consola. Además, en una de sus
secciones, he encontrado a un usuario llamado Anders, el cual explica cómo crear un juego
para la Master System y realiza un ejemplo de un juego de carreras de coches [3].
Es a partir del tutorial de este usuario, de donde me he fijado mayormente para poder hacer
los primeros pasos de mi prototipo de Mágica y haré referencia a este tutorial varias veces a
lo largo de la explicación.
55
Figura 38. Racer para la SMS.
Fuente: Create a Racing Game [3].
8.1.1. Código básico para la ROM
Lo primero que vamos a hacer es crear un fichero llamado main, el cual más adelante lo
único que contendrá será el bucle principal del prototipo y las funciones de inicialización de
la consola. Sin embargo, por ahora, este fichero contendrá todo el código del prototipo hasta
que logre entender cómo funciona más o menos todo y lo divida en varios ficheros para que
todo sea mucho más fácil de entender.
La mayor parte de lo que hay realizado en el apartado de Creación de una ROM básica va a
servir para este prototipo, sin embargo, hay ciertos detalles que son necesarios modificar. En
primer lugar, la configuración de bancos y ranuras de la ROM no va a ser la misma que
la que se tenía anteriormente, por lo que hay que cambiarla de tal manera que en nuestro
mapa de memoria tengamos 2 ranuras de ROM y una de RAM, donde en esta última la
utilizaremos para almacenar variables y datos que vayan a cambiar durante la ejecución de
nuestro juego.
También especificamos 2 bancos en nuestro cartucho del mismo tamaño que las 2 ranuras
de ROM. Recordemos que los bancos nos sirven para que en un futuro podamos llegar a
tener más espacio en nuestra consola. Aunque es probable que en este prototipo no lleguemos
a llenar todo el espacio que tenemos disponible, viene bien realizar la declaración para ir
acostumbrándonos a hacer una correcta definición de nuestro espacio de memoria.
56
Figura 39. Configuración de las ranuras y bancos para el prototipo.
Fuente: Elaboración propia.
Esta configuración de bancos y ranuras es libre, es decir, se puede hacer como uno quiera y
no tiene que porque ser necesariamente de esta forma, sin embargo, con la finalidad de poder
seguir el tutorial de la mejor manera y no cometer errores, se va a utilizar la misma
configuración que se utiliza en el tutorial y, más adelante, se modificará si es necesario.
El segundo aspecto que hay que cambiar de nuestro código base son los valores de
inicialización del VDP. Al igual que antes, las valores que se van a poner por ahora son los
mismos que utiliza Anders en su tutorial [3], de esta manera evitamos así problemas futuros
relacionados con el malfuncionamiento del prototipo debido a que se han utilizado otros
valores distintos a los del tutorial. Cuando ya se hay aprendido como funciona todo, se
cambiarán estos valores para hacer más pruebas y ver que todo lo explicado en el tutorial se
entiende a la perfección.
Figura 40. Inicialización de los registros del VDP para el prototipo.
Fuente: Elaboración propia.
La función que realiza cada registro del VDP ya se encuentra explicado en el Anexo. Aquí
solo se van a describir los aspectos más importantes que se están especificando en cada uno
de los registros:
57
• Registro 0: Se activa el modo 4 de visualización. Este es un modo específico que
contiene el chip del VDP de la SMS y debe estar activado siempre según la
documentación oficial de la consola [28].
• Registro 1: Se desactiva la visualización de la pantalla, ya que esta será activada
justo después de haber inicializado todo lo necesario en la consola. Además, se activa
el VBlank, de tal manera que cada vez que se pinte un frame, se generará una
interrupción cuando se tengan las interrupciones habilitadas. Esto se utilizará como
contador básico de tiempo en la consola.
• Registro 2: Se establece la dirección donde se encontrará el nombre de la tabla en
VRAM. Se utiliza la dirección por defecto que recomienda la documentación oficial
[28], que corresponde a la dirección $3800.
• Registros 3 y 4: Estos 2 registros no tienen ningún uso en la consola. Se establecen
todos sus bits a 1 que es como se recomienda para un funcionamiento estándar.
• Registro 5: Se establecen todos los bits a 1 para que la dirección de la tabla que
contiene la información de los sprites dibujados en pantalla corresponda a la
dirección por defecto ($3F00).
• Registro 6: El bit más importante de este registro es el segundo, el cual, al estar
activado, indica que todos los tiles que se utilizan para los sprites empezarán a
almacenarse a partir de la dirección $2000.
• Registro 7: Se indica que quiere emplear el color 3 de la paleta para el borde de la
pantalla.
• Registros 8 y 9: Por el momento no se requiere de ningún valor de scroll horizontal
ni vertical, por lo que se le pasa el valor $00.
• Registro 10: Se desactiva el HBlank, de esta manera no se generan interrupciones
cada vez que se dibuja una línea por pantalla.
Con estos dos aspectos cambiados y el resto de código que ya se disponía, ya se puede
empezar a probar cosas y, lo primero que se va a realizar, es probar a pintar un fondo y un
Sprite por pantalla.
58
8.1.2. Dibujado de un sprite y un fondo por pantalla
Como primera prueba, lo que se va a hacer es cargar el Sprite y el fondo que se proporciona
en el tutorial [3] y, posteriormente, ya se probará a cargar un fondo y Sprite distinto para
corroborar que se ha entendido el procedimiento.
Antes de mostrar cualquier cosa por pantalla, primero hay que cargar en memoria todos los
“assets”, es decir, todos los recursos que se necesitan tanto para pintar el fondo como para
pintar los sprites. Estos recursos consisten en:
• La paleta de colores para indicar que colores van a ser utilizados. Hay 2 paletas, una
para el fondo y otra para los sprites.
• Los índices de tiles (denominados charcodes en inglés) tanto de los sprites como del
fondo.
• El tilemap para el fondo.
Lo primero que vamos a cargar va a ser la paleta de colores, para ello, lo que hay que hacer
es indicar al VDP lo que se quiere realizar. En este caso, lo que queremos es copiar datos a
la CRAM, que es una memoria interna de solo escritura que se utiliza para guardar los datos
de las paletas a usar. La siguiente imagen muestra cómo está distribuida esta memoria:
Figura 41. Direcciones de la paleta de colores (CRAM).
Fuente: Software Reference Manual for the Sega Mark III Console [28].
59
Como se puede observar en la imagen, los primeros 15 colores corresponderían a la primera
paleta (la del fondo) y los 15 colores siguientes, a la segunda paleta (la de los sprites).
Por lo tanto, si yo quisiese, por ejemplo, cargar la paleta de colores de los sprites, hay que
pasar el valor $C010 al puerto de control, donde los dos últimos valores hexadecimales
representan la dirección de la CRAM, que fijándonos en la figura 41 vemos que corresponde
a la primera posición de la segunda paleta de colores (Bank 2, Color 0) que es la de los
sprites. Si por el contrario quisiera cargar la paleta del fondo, los dos últimos valores
corresponderían a $00 (Bank 1, Color 0).
El porqué de los dos primeros valores hexadecimales se encuentra explicado en el Anexo de
este documento, concretamente en el apartado de programación del VDP. De manera
resumida, estos dos valores indican al VDP la orden a realizar, es decir, qué es lo que se
quiere hacer exactamente. Cuando se trabaja con CRAM, el valor a pasar por el puerto
siempre empezará por $C0 ya que la CRAM es más pequeña que la VRAM y no se
requieren tantos bits para representar la dirección a donde copiar los datos.
Una vez que se han enviado por el puerto de control la orden y dirección, ahora tenemos que
copiar los datos de la paleta a la CRAM, para ello, creamos 2 etiquetas que indican la
dirección de inicio y final de los datos de la paleta, de tal manera que, si hacemos una resta
de estas 2 etiquetas, obtenemos el tamaño que ocupan los datos de la paleta y podemos
cargarlos en el registro necesario.
Figura 42. Cargado de la paleta de colores a los sprites.
Fuente: Elaboración propia.
La función denominada “prepararVDP” lo único que realiza es enviar el valor del registro
HL al puerto $BF (puerto de control) y la función nombrada “cargarPaleta” utiliza la
instrucción “otir” para enviar los datos de la paleta al puerto de datos del VDP.
60
Ahora vamos a cargar los tiles del Sprite que queremos dibujar por pantalla. Para ello, al
igual que con la paleta, lo primero que hay que hacer es indicar la orden y dirección al VDP.
En este caso, cargo el valor $2000 a HL, ya que como se especifica en el registro $06 del
VDP, los tiles de los sprites se van a comenzar a copiar a partir de la dirección $2000. Si
correspondiesen a los tiles del fondo, el valor a pasar sería $0000.
Ahora tenemos que indicar donde tenemos almacenado la información sobre los tiles y aquí
es cuando entra en juego un programa que nos va a ayudar mucho en cuanto al tema de
dibujado de sprites y fondo, el BMP2TILE [22].
Este programa, que es utilizado por Anders en el tutorial, permite cargar una imagen
cualquiera (siempre que cumpla unos requisitos como que no tenga más de un número
determinado de colores o que dichos colores correspondan a los de la paleta de la SMS). Al
cargar esta imagen, que corresponderá o a un fondo o a un Sprite, el programa nos
proporcionará automáticamente los índices de tile, el tilemap y la paleta de colores, y,
además, nos permitirá exportar cada uno de estos datos obtenidos en ficheros independientes,
los cuales podemos incluir en el proyecto.
Figura 43. Valores de la paleta de colores para un Sprite de Sonic usando BMP2TILE.
Fuente: BMP2TILE [22].
61
Gracias a este programa, podemos realizar un “include” del fichero exportado por el
programa a partir del sprite o fondo cargado en él. En nuestro caso, corresponderá al fichero
que contiene la información de los índices de tile de un sprite.
Para acceder a esta información creamos una etiqueta justo antes del “include” del fichero
y, de esta manera, tendremos una etiqueta que indica la dirección de inicio donde se
encuentran todos los datos de los índices.
Ahora, lo que hay que realizar es una copia de todos estos datos a la memoria de video y
para ello, utilizaremos la forma que utiliza Anders en su tutorial: indicar el tamaño total de
todos los datos a copiar en el registro BC e ir copiando uno a uno estos datos al puerto $BE
hasta que el registro BC llegue a 0. En este caso en concreto, hay 16 tiles donde cada uno
ocupará unos 32 bytes en total.
Figura 44. Copia de los datos de los índices de tiles a la VRAM.
Fuente: Elaboración propia.
Llegados a este punto, ya se tendrían todos los recursos necesarios para poder pintar un sprite
por pantalla, sin embargo, para el fondo aun es necesario hacer una cosa más: Cargar el
tilemap, es decir, el fichero que indica donde se dibuja en pantalla cada tile y se obtiene de
la misma manera que los índices de tile, mediante BMP2TILE [22].
62
El procedimiento para copiar el tilemap del fondo es el mismo que se ha estado realizando
hasta ahora para los índices de tile: primero se indica la orden y dirección al VDP, después,
se copia al registro HL la dirección donde se encuentran todos los datos del tilemap y en el
registro BC, se almacena el tamaño que ocupan estos datos. Con estos datos, ya se puede
utilizar la función de “escribirVRAM” para realizar la copia de los datos.
Figura 45. Carga de un tilemap de un mapa en VRAM.
Fuente: Elaboración propia.
Si ahora ejecutamos el juego en el emulador, ya se podría distinguir el fondo dibujado por
pantalla, sin embargo, el sprite aún no se puede observar. Es en este momento, cuando me
doy cuenta de que dibujar un Sprite en SMS es más complicado de lo que pensaba, ya que
mientras que, con el fondo, una vez cargado la paleta, los tiles y el tilemap, este ya se dibuja
por pantalla, con el Sprite no sucede lo mismo.
Si se quiere dibujar el Sprite correctamente por pantalla, es necesario cargarlo en el
Sprite Attribute table (SAT), que corresponde a la tabla de la memoria de video donde se
almacena toda la información relacionada con los sprites, como puede ser su posición
vertical y horizontal que ocupa en la pantalla, y el índice de tile a dibujar.
Para poder cargar dicho Sprite en la tabla, primero lo vamos a almacenar en un buffer que
represente dicha tabla y, de esta manera, una vez tengamos todos los datos necesarios, solo
hay que efectuar una copia de todo lo que se encuentra en el buffer a la tabla de la VRAM.
63
Figura 46. Buffer del Sprite Attribute Table.
Fuente: Elaboración propia.
Una vez creado nuestro buffer, hay que comenzar a copiar los datos del sprite en él. Lo
primero que vamos a copiar son los índices de tile del Sprite en cuestión y, para ello,
necesitamos saber cuál va a ser la primera posición que van a ocupar en la tabla del SAT, la
cual recordemos que es aquella donde se va a almacenar toda la información relacionada con
los sprites que pintamos por pantalla.
Con la finalidad de lograr entender mejor el funcionamiento de esta tabla, en la figura 47
imagen se puede observar su estructura:
Figura 47. Representación del SAT de la SMS.
Fuente: Sega Master System VDP Documentation por Charles MacDonald [18].
64
El SAT ocupa un total de unos 256 bytes, los cuales se encuentran desde la dirección $3F00
de la memoria de video hasta la dirección $3FF0. Sin embargo, si nos fijamos en el buffer
que hemos creado anteriormente, podemos observar que está situado en la RAM,
concretamente desde la dirección $C000 hasta la $C0F0, por lo que a la hora de indicar la
posición inicial de la tabla para los datos de cada uno de los sprites, hay que tener en cuenta
la dirección del buffer y no la de la VRAM. Una vez que tengamos los datos en el buffer
ya efectuaremos la copia de los datos a la dirección donde se encuentra la tabla en memoria
de video.
De estos 256 bytes totales, existen 64 que son libres para usar. Ahora mismo, los que nos
interesan son los bytes que se encuentran entre las hpos (posiciones horizontales) del sprite,
que corresponden a los bytes para los índices de tile (en la figura 47 están representados con
la letra n).
Teniendo esta información en mente, indicamos la primera posición de la tabla donde se van
a empezar a escribir los charcodes y los empezamos a copiar, siempre teniendo en cuenta
que cada vez que se copie uno hay que avanzar 2 bytes para poder copiar el siguiente, ya que
recordemos que los charcodes se encuentran entre las hpos.
Figura 48. Copia de los índices de tile del Sprite al buffer del SAT.
Fuente: Elaboración propia.
Una vez tenemos los índices de tile del Sprite en el buffer, hay que pasar las posiciones
iniciales X e Y del Sprite.
65
Para poder hacer esto, primero hay que cargar estos valores iniciales en alguna variable. Yo
he creado dos variables denominadas “ObjX” y “ObjY” que cumplirán dicha función. Estos
valores hay que actualizarlos con las hpos y vpos de la tabla de sprites y, para ello, he creado
dos funciones: una para actualizar la X y otra para actualizar la Y del objeto.
Por ahora, no voy a proporcionar el código que permite realizar estos dos aspectos, ya que
son funciones copiadas del tutorial de Anders y considero que no son lo más óptimas
posibles. Más adelante, aportaré un código que realiza lo mismo, pero de manera más óptima
y generalizada. De momento, si necesitas ver el código puedes ir a la página del tutorial de
Anders en smspower [3]. Lo que si voy a hacer es explicar qué es lo que se está realizando
en estas 2 funciones.
En primer lugar, en la función de actualizar la coordenada Y del Sprite con la vpos del SAT,
se ha de tener en cuenta las dimensiones que ocupa el Sprite. En este caso en concreto, el
Sprite del coche de carreras es de 4 tiles de ancho y 4 tiles de alto, donde cada tile a su vez
es de 8x8 píxeles.
Sabiendo esto, nos fijamos en que, para poder dibujar una fila del sprite por pantalla,
internamente de esa fila, la coordenada Y no va a cambiar su valor hasta que no pasemos a
la subsiguiente fila del sprite, ya que todos los tiles de la misma fila van a tener la misma
altura y, por consiguiente, el mismo valor de coordenada Y.
Teniendo en cuenta de que el sprite es de 4 tiles de ancho, cargamos 4 valores de Y iguales,
y cuando lleguemos al final de la fila, para pasar a la siguiente fila, sumamos 8 (cada tile
ocupa 8x8) al primer valor de Y de la fila actual en la que nos encontramos y repetimos todo
este proceso hasta conseguir pintar las 4 filas que ocupa el sprite.
El proceso para el valor de la coordenada X del sprite es similar, con la única diferencia de
que cada vez que introducimos un valor de X a este hay que sumarle 8, ya que cada columna
del sprite va a estar 8 píxeles más adelante que la anterior y hay que repetir este proceso para
todas las filas y columnas que ocupe el sprite. Además, no hay que olvidarse de que hay que
avanzar de 2 en 2 en las direcciones del buffer ya que las hpos están entre los charcodes.
Ahora lo único que queda es copiar toda esa información que se encuentra en el buffer a la
dirección de VRAM de la SMS donde se encuentra realmente la tabla del SAT y lograr así
dibujar nuestro sprite por pantalla:
66
Figura 49. Copia del buffer al SAT.
Fuente: Elaboración propia.
Una vez copiado, ahora sí que se puede observar dibujado por pantalla el fondo y el Sprite
del tutorial:
Figura 50. Sprite y fondo del tutorial dibujados por pantalla.
Fuente: Emulador Meka [8].
8.1.3. Lectura de la entrada del jugador por teclado
Una vez que se ha conseguido dibujar por pantalla el sprite y el fondo, lo que nos interesa
ahora es que el jugador sea capaz de moverlo, pero, para ello, primero tenemos que
configurar el VBlanking o también denominado frame interrupt.
67
El frame interrupt consiste en una interrupción que se genera en la Master System cada vez
que se dibuja un frame por pantalla, es decir, cada vez que se alcanza la última fila de la
pantalla y se vuelve a comenzar por la primera. Este es el método más simple que tiene la
consola para contar el tiempo y sincronizar correctamente todo lo que se dibuja por pantalla.
Más adelante observaremos como utilizar esto para poder controlar el tiempo en nuestro
videojuego.
Hay que gestionar esta interrupción, pero, para ello, primero hay que activar las
interrupciones, ya que recordemos que las teníamos desactivadas. Una vez activadas, en la
dirección $0038, que corresponde a la dirección donde se gestionan las interrupciones en la
SMS, lo que hacemos es leer del puerto de control para poder obtener el estado de las flags
de la VRAM. Este byte lo guardamos en una variable que hemos creado previamente, ya que
más adelante comprobaremos que nos va a proporcionar información realmente útil.
Figura 51. Zona de memoria para la gestión de interrupciones en SMS.
Fuente: Elaboración propia.
Con las interrupciones habilitadas, podemos ahora hacer un pequeño bucle en el programa
principal que empiece siempre y cuando se genere una interrupción, y como solo tenemos
activado el frame interrupt, hasta que no se dibuje un frame no volverá a empezar el bucle
otra vez. Esto se realiza con la instrucción “halt” que suspende la CPU hasta que ocurra una
interrupción o se produzca un reset.
Con todo esto, ya se tiene lo mínimo necesario para poder empezar a mover el sprite. Lo que
vamos a hacer es simplemente moverlo de izquierda a derecha y, para conseguir esto, hay
que fijarse en el manual oficial de la SMS [28].
68
En este manual, se encuentra la información necesaria para poder implementar la recogida
de la información que pueda generar el jugador, es decir, los botones o teclas que este pulsa.
Este documento indica que existen múltiples puertos encargados de almacenar la
información introducida por el jugador, dependiendo del joystick o dispositivo que se esté
utilizando. Para el caso que nos repercute, es el puerto $DC el encargado de almacenar la
información relacionada con el input del usuario.
Por lo tanto, leemos de este puerto y nos fijamos en los bits que se encuentran a 0, ya que
son los que indican qué botón ha sido pulsado por el jugador. Concretamente, nos indica que
para poder mover a la izquierda o a la derecha, hay que fijarse en los bits 2 y 3
respectivamente (véase la figura 52).
Figura 52. Información sobre los bits correspondientes al Joystick P1.
Fuente: Manual de la Sega Mark III [28].
Sabiendo esta información, comprobamos si se quiere mover a la derecha o la izquierda y
movemos el sprite actualizando esta nueva posición obtenida en el buffer con las 2 funciones
que se han mencionado anteriormente.
69
Lo más probable es que queramos que nuestro sprite no traspase los límites de la pantalla de
la consola. Cuando se llegue a alguno de los límites de la pantalla, el sprite deberá dejar de
moverse para evitar así que aparezca por el extremo opuesto.
La implementación más óptima para lograr esto sería a través de las colisiones del mapa,
pero como aún no han sido realizadas, es necesario idear una alternativa más simple:
mediante el tamaño de la resolución de la pantalla.
Si queremos controlar el límite derecho, sabemos que la pantalla tiene unos 256 píxeles de
ancho, por lo tanto, le restamos a esa cantidad el ancho que ocupa el Sprite, que en nuestro
caso actual son 32 píxeles (8 pixeles x 4 tiles de ancho) y obtendríamos así la posición
máxima a la cual puede llegar el Sprite.
Otra alternativa sería almacenar el valor máximo de la resolución de la pantalla, que ya
hemos indicado que es 256 píxeles. Tras esto, se recoge la posición X del sprite y se le suma
su ancho, obtendremos así la posición exacta hasta la cual podrá moverse el sprite sin que se
salga de la pantalla. Bastara con hacer una simple comparación con el valor del borde
derecho de la pantalla para comprobar si se puede mover hacia esa dirección o no (véase
Figura 53).
Figura 53. Control del movimiento hacia la derecha del jugador.
Fuente: Elaboración propia.
Para, controlar el lado izquierdo de la pantalla el proceso es idéntico al del lado derecho,
pero con la diferencia de que el valor más pequeño que se podrá alcanzar es 0 y no será
necesario saber el ancho del sprite.
70
8.1.4. Organización del proyecto y makefile
Ahora que ya tenemos un mapa y fondo pintados (aunque no sean nuestros) y podemos
mover el sprite por la pantalla, vamos a organizar un poco nuestro código. Esto es necesario
debido a que en este momento todo lo que hemos realizado lo hemos hecho en un único
fichero main y aunque ahora mismo tampoco tengamos mucho código, conforme vayamos
realizando más mecánicas para nuestro videojuego veremos cómo nuestro código va
creciendo poco a poco, por lo que conviene reorganizar ya nuestro proyecto antes de que se
tengan demasiadas cosas y sea un proceso más complicado de realizar.
Por el momento, solo vamos a añadir un fichero más, que es donde se encontrará todo el
código relacionado con el jugador, es decir, el sprite protagonista del prototipo y, de esta
manera, en el main, solo se encontrará la gestión de arranque de la consola, la carga de los
recursos necesarios y el bucle principal del juego.
La mejor forma para lograr que nuestro proyecto funcione a través de diferentes ficheros, es
mediante la inclusión de una cabecera en todos los ficheros de nuestro proyecto. En esta
cabecera se encontrará todo lo relacionado con la validación de la cabecera de la ROM y la
organización de los bancos y ranuras que ya se había relacionado previamente. Esto se
realiza fácilmente mediante la etiqueta “include” seguido del fichero que contenga la
cabecera.
Además de incluir todo lo necesario para que nuestro proyecto pueda ser ensamblado y
linkado correctamente, también podemos incluir, como veremos posteriormente, nuestras
definiciones o estructuras que necesitemos para nuestro videojuego ya que solo ocuparán
espacio en la consola cuando sean utilizadas.
Esta cabecera es necesaria incluirla en todos los ficheros del proyecto, ya que, si no,
surgirán diferentes problemas: En primer lugar, si en un fichero no se indica la definición
del mapa de memoria con sus bancos y ranuras, se mostrará un error al ensamblar nuestro
proyecto. Por otro lado, si no se incluye la etiqueta “sdsctag” necesaria para poder validar la
ROM del juego, en aquellos ficheros que no contengan dicha etiqueta, saltará un error de
linkado indicando que dichos ficheros no pertenecen al mismo proyecto.
71
Asimismo, en todos nuestros ficheros se debe indicar en qué dirección se encuentra nuestro
código. Para ello, podemos utilizar una directiva del WLA DX, denominada “.section”.
Mediante esta directiva, si le pasamos el parámetro “FREE”, todo lo que este entre dicha
directiva y “.ends” se colocará dinámicamente en la ROM, es decir, en el primer sitio
disponible de la memoria dentro del banco que se haya indicado previamente. De esta
manera, no hay que preocuparse de estar viendo donde tener que colocar el código y, a su
vez, se aprovecha al máximo el espacio que tenemos disponible en la ROM de la consola.
Figura 54. Ejemplo de uso de la directiva”. section” de WLA DX.
Fuente: Elaboración propia.
Siguiendo estos pasos, podremos conseguir que nuestro proyecto sea construido mediante
diversos ficheros. Sin embargo, para evitar perder tiempo innecesario, debemos de hacer uso
de un pequeño makefile que nos evite tener que estar ensamblado y linkando uno a uno todos
los ficheros del proyecto.
El único problema que podemos encontrar es con el linkado. Mediante el ensamblador WLA
DX, el linkado debe realizarse con fichero denominado “linkfile” el cual contendrá todos los
ficheros con extensión “.o” que se hayan generado tras ensamblar y se vayan a linkar. Este
fichero no se puede generar de manera automatizada y debe actualizarse cada vez que
creemos un fichero nuevo en nuestro proyecto.
72
Figura 55. Makefile básico para poder generar un archivo ".sms".
Fuente: Elaboración propia.
Con este makefile, desde la terminal podremos utilizar el comando “make” para generar
nuestro ejecutable del prototipo o juego y el comando “make cleanall” para borrar
rápidamente todos los ficheros objeto y el fichero ejecutable que se haya generado mediante
“make”.
8.1.5. Dibujado de un sprite y fondo creados desde cero
Una vez que ya tenemos todo más o menos organizado, vamos a intentar cargar y dibujar un
fondo y sprite creado por nosotros. No vamos a hacer un sprite de cero ni vamos crear los
tiles nosotros, al menos no por ahora, lo que vamos a realizar es descargar algunos gratuitos
de internet.
Lo que se pretenden conseguir con todo esto es usar un fondo y sprite construido por nosotros
(o al menos parcialmente) y observar que funcione correctamente en la consola y verificar
que entendemos todo el proceso que conlleva realizar tal cosa.
73
Para ello, lo primero que hay que hacer, una vez descargado los tiles que nos interesan, es
cargarlo en Tiled [17]. Tiled es un programa que permite crear tilemaps y exportarlos para
poder integrarlos en nuestro juego, pero, en nuestro caso, lo único que nos interesa es
conseguir la imagen de dicho tilemap y utilizamos tiled porque se pueden cargar tilesets para
formar el tilemap de manera sencilla y rápida, además de que se puede definir el tamaño del
fondo y, así como, el de los tiles.
Figura 56. Tilemap creado con Tiled para representar un nivel de Mágica.
Fuente: Elaboración propia.
Una vez que hemos exportado la imagen del fondo a través del programa, lo que hay que
hacer es reducir su profundidad de color, ya que ahora mismo tiene una profundidad de color
de RGB y hay que convertirla a la de índice de paleta de 256 colores, si no, nos daremos
cuanta que al cargar la imagen en el BMP2TILE no funcionará ya que saltará un error de
formato, debido a que la SMS no soporta dichos colores. Esto se puede realizar con cualquier
programa de edición de imágenes, yo he usado GIMP [21].
Al mismo tiempo, hay que tener cuidado con los colores que se utilizan, ya que en la Master
System solo hay 64 colores disponibles y si ponemos un color que no esté disponible en la
consola tampoco funcionará.
74
Una vez creado el fondo correctamente, solo hay que cargarlo en la consola como hemos
hecho para el fondo del tutorial, la única diferencia es que habrá que cambiar los valores
para la paleta de colores, los tiles y el tilemap.
Sin embargo, el problema llega a la hora de dibujar un sprite distinto al del tutorial. Vamos
a ver el proceso para cargar un sprite y los problemas que nos encontraremos:
Para obtener el sprite y todos sus assets necesarios se realiza de manera parecida que para el
fondo. Primero, hay que descargar el tileset que contiene el sprite que se desea como, por
ejemplo, The Spriters Resource [10]. Esta página permite descargar y usar sprites siempre y
cuando no sea para uso comercial.
Yo utilizo esta ya que tiene una sección con sprites para la Master System que ya se
encuentran en el formato adecuado para la consola y como por ahora solo se están realizando
pruebas y no va a ser nada definitivo no hay problema con los derechos de autor ya que todos
los recursos que se pueden encontrar aquí provienen de videojuegos conocidos de la época
de la SMS, por lo que desconozco cuanto de fiable es el sitio y estos recursos no van a ser
utilizados para crear el videojuego del proyecto.
Dicho esto, se realiza la carga de dicho tileset en Tiled, se observa cuáles son las dimensiones
del sprite y se crea un mapa con estas dimensiones. Posteriormente, se debe exportar en
formato imagen, se carga en GIMP, se le pone un fondo transparente y se le reduce la
profundidad de color. Con todo esto, se carga en el BMP2TILE y se obtienen los includes
con la información de los tiles y la paleta.
A pesar de todo esto, cuando nos dispongamos a cargar el Sprite ya exportado en el proyecto
y les cambiamos los valores correspondientes para que pueda ser dibujado bien, nos daremos
cuenta de que el sprite no se dibuja correctamente y no sabremos el porqué, ya que se está
realizando lo mismo que con el Sprite del tutorial, pero simplemente cambiando los valores
de tamaño, de la paleta y de los índices de tile.
75
Aquí es cuando descubriremos una de las cosas buenas que tiene el emulador de MEKA: el
editor de memoria. Este editor, te permite ver el mapa de todas las memorias de las que
dispone la consola y podemos observar que valores se almacenan en cada posición de la
memoria. Además, da la posibilidad de modificar una posición de memoria de manera
sencilla. Gracias a esto, se puede observar la memoria de VRAM en la dirección $3F00, que
como ya sabemos es donde se encuentra el SAT con toda la información de los sprites y así
observar si algo no está funcionando bien.
Figura 57. Editor de memoria del emulador MEKA.
Fuente: Emulador Meka [8].
Observando este editor, cargamos el sprite del tutorial y luego cargamos el sprite que
hayamos descargado nosotros para comparar los valores que hay en uno y en otro dentro de
la tabla del SAT. Al hacer esto nos daremos cuenta de que cuando se carga el sprite del
tutorial todas las hpos tienen un valor de charcode entre ellos, mientras que cuando cargamos
nuestro Sprite, las últimas posiciones de hpos están vacías, es decir, con valor de 00.
Con el fin de asegurarse de que esto no es una casualidad y de que está relacionado, podemos
probar a bajar más sprites y cargarlos en la SMS para ver si ocurre lo mismo. Al realizar
esto, nos daremos cuenta de que en que todos los sprites que se dibujan mal pasa exactamente
lo mismo que el primer sprite que intentamos cargar: hay valores de hpos que no tienen
índices de tile entre ellos y, por lo tanto, cuando esto ocurre los tiles del sprite no se dibujan
en la posición correcta en pantalla.
76
El origen de este problema reside en el programa de BMP2TILE. En el primer momento,
pensé que no funcionaba correctamente y que genera los índices de tile de manera incorrecta.
Sin embargo, si echamos un vistazo más a fondo al programa y a su documentación oficial,
nos daremos cuenta de que dentro de la ventana relacionada con los tiles del sprite, hay una
pestaña llamada “remove duplicates” que por defecto aparece marcada. Esto quiere decir
que cada vez que esta pestaña esté marcada todos los tiles que se repitan, es decir, aquellos
que sean idénticos a otros que ya están, el programa no los pondrá en el fichero para así
ahorrar espacio en la consola.
Al desmarcar esta pestaña y cargar el nuevo fichero obtenido al proyecto, ya se dibuja
correctamente el Sprite y, además, verifico que todos los hpos tienen sus valores de
charcodes entre ellos.
Todo este tiempo que yo he “perdido”, ha sido básicamente por no haber mirado la
documentación justo cuando empecé a usar el programa, debido a que, en este, entre otras
cosas, se explica todo esto que acabo de comentar. Por lo que os recomiendo
encarecidamente que siempre que uséis un programa nuevo primero os leáis la
documentación que proporciona y así os evitáis este tipo de problemas como me ha ocurrido
a mí.
Figura 58. Dibujado de Sprites antes y después de resolver el problema con BMP2TILE.
Fuente: Elaboración propia.
77
8.1.6. Generalización de las funciones de dibujado
Una vez que resuelto el problema de dibujado de sprites, vamos a mejorar algunas de las
funciones que se han realizado para que puedan servir para dibujar un sprite de cualquier
tamaño.
El principal problema con las funciones que hemos creado es que solo funcionan para dibujar
un sprite de un tamaño y valores específicos. Esto quiere decir que si ahora, por ejemplo,
queremos dibujar un sprite distinto, con unos tamaños diferentes a los del sprite que tenemos
dibujado actualmente, las funciones de actualizar los valores de X e Y en el SAT ya no nos
van a servir.
Esto es un problema, ya que las funciones encargadas de dibujar un sprite por pantalla son
idénticas, solo cambian los valores de tamaño del sprite en cuestión a dibujar. La idea es
conseguir que estas funciones de dibujado sean generales y puedan ser usadas por cualquier
sprite que queramos cargar, ahorrando así espacio y tiempo. Vamos a ver como generalizar
las funciones de actualizar las posiciones X e Y del sprite con las hpos y vpos del SAT.
Lo primero que hay que hacer es crear una zona de datos dentro del fichero que contiene
todo lo relacionado con el personaje que controla el jugador. Esta zona, contendrá todos los
datos relacionados con el protagonista como pueden ser los valores de X e Y, el ancho del
sprite, el alto del sprite etc. Mediante esto, podemos hacer uso de la instrucción IX del Z80
para crear una función donde lo único que se haga es obtener un puntero que apunte al
principio de los datos del personaje.
Todo esto podemos hacerlo exactamente de la misma manera en cualquier otra identidad que
tenga nuestro juego, ya sea un enemigo o algún objeto. La única condición que hace falta
cumplir es que la zona de datos que va tener cada identidad sea idéntica en todas, es decir,
si en el player la primera posición de memoria de datos almacena al valor de la posición X,
entonces el primer valor de la zona de datos para los enemigos también tiene que coincidir
con la posición X, ya que, si no, a la hora de usar la instrucción IX y acceder a una posición
de memoria en la cual se supone que tiene que contener el valor de la posición X de la
identidad, pero, en realidad es, por ejemplo, el valor de la posición Y, no va a funcionar
correctamente ya que esteremos usando un valor que no es el correcto.
78
Figura 59. Zona de datos del Player mediante “enum”.
Fuente: Elaboración propia.
Inicialmente comencé a utilizar la etiqueta “enum” para poder definir las variables de
cualquier entidad. Sin embargo, más adelante me doy cuenta de que hay una directiva que
realiza la misma función que “enum”, pero de manera parecida a cómo funciona las
directivas “section” del ensamblador.
Esta directiva se denomina “ramsection” y permite definir una zona de RAM en nuestro
proyecto para poder definir variables. La ventaja que tiene frente a “enum” es que no hay
que preocuparse de dónde colocar las variables, ya que esta etiqueta coloca de manera
automática todo el código escrito dentro de ella a partir de la primera posición de memoria
RAM disponible. Luego, recomiendo que utilices esa directiva en el lugar del “enum”.
Para evitar problemas y facilitar la lectura del código del proyecto, se podrían crear unas
definiciones en la cabecera que indiquen la posición en la que se encuentran cada uno de los
datos de la identidad respecto a la dirección a la que apunta IX, es decir, representarían la
cantidad necesaria a sumar al registro IX para poder acceder a la dirección de memoria donde
se encuentre la variable que queramos utilizar. Por ejemplo, para la posición X del player,
que es la primera posición a la que va a apuntar IX, se podría crear una definición
denominada “spr_x” con el valor 0 indicando que está en la primera posición de memoria.
De esta manera, cuando sea necesario usar el valor X del sprite, se puede acceder a dicho
valor haciendo uso de la instrucción IX junto con el valor de la definición que se ha creado
para poder acceder a la posición correcta donde se encuentra el valor de X.
79
Además, si en algún momento se cambia el orden en el cual se encuentran los datos de
nuestra entidad, solo haría falta cambiar los valores asignados a las definiciones y no cambiar
el valor en cada uno de los sitios donde ha sido utilizado. Más adelante, observaremos que
esto puede ser automatizado más aun, pero por ahora utilizaremos esta forma.
La única condición necesaria para hacer uso de los registros índice como IX o IY, es que
deben apuntar a la dirección de memoria donde se encuentren los datos justo antes de hacer
uso de ellos. De lo contrario, estaríamos accediendo a valores que no corresponden a los que
se desean y comenzar a aparecer muchos errores.
La figura 60 hace referencia a la función encargada de actualizar la coordenada X del sprite
con el SAT, haciendo uso del registro índice IX.
Figura 60. Función para actualizar la coordenada X del Sprite en el SAT utilizando IX.
Fuente: Elaboración propia.
Como se puede observar, cada vez que necesitemos usar un dato de la identidad, se hace uso
del registro IX y la definición de la cabecera.
80
Figura 61. Función para actualizar la coordenada Y del Sprite en el SAT usando IX.
Fuente: Elaboración propia.
Las funciones correspondientes a las figuras 60 y 61, son las funciones explicadas en la
sección de dibujado de sprite que han sido generalizadas para que puedan funcionar con
cualquier sprite que queramos dibujar.
8.1.7. Disparo del jugador
Llegados a este punto, ya hemos aprendido, entre otras cosas, a pintar correctamente un
fondo y un sprite. Ahora, vamos a ver cómo realizar una sencilla mecánica de disparo que
permita al jugador disparar una bala cada vez que este pulse el botón de disparo.
Para conseguir esto, es necesario cargar 2 sprites distintos a la vez en la memoria de VRAM
y que estos se muestren correctamente por pantalla. El primer paso que hay que realizar es
averiguar el espacio que ocupan todos los tiles de cada uno de los 2 sprites, debido a que hay
que colocarlos en la misma zona de memoria (a partir de la dirección $2000) y hay que evitar
que se solapen los valores de cada uno entre ellos.
En mi caso, el primer sprite ya cargado corresponde al de Batman. Este está formado por
unos 15 tiles en total, donde cada uno de estos ocupa 32 bytes, lo que quiere decir que el
sprite tendrá un tamaño total de 480 bytes en tiles. Esta información la podemos extraer
directamente del fichero de tiles que obtenemos del BMP2TILE.
Teniendo en cuenta que las posiciones de memoria están en hexadecimal, hay que convertir
el valor obtenido en decimal de los bytes a hexadecimal. Por ejemplo, para mi sprite de
batman, se obtendría el valor $1E0 que corresponde a los 480 bytes de los 15 tiles.
81
Este valor obtenido, hay que sumarlo a la dirección $2000, ya que esta es la primera dirección
desde donde se va a empezar a cargar los tiles de los sprites. Obteniendo así el valor $21E0,
que corresponderá a la primera posición de memoria a partir de la cual se puede copiar los
tiles del segundo sprite que se quiere cargar, que, en este caso, será el sprite de la bala para
poder hacer el disparo.
Este proceso que acabo de explicar será el mismo para los tiles de todos los sprites que se
quiera cargar en un futuro. Simplemente hay que calcular los bytes que ocupa en total, pasar
el valor obtenido a hexadecimal y sumarlo a la dirección de memoria del último tile cargado
del sprite.
Tras haber cargado todos los tiles, haber indicado cuales son los índices de tile que van a
usar los 2 sprites y cargado las vpos, hpos y los charcodes en el buffer del SAT, ya se puede
visualizar por pantalla el dibujado de ambos sprites.
Figura 62. Dibujado de 2 Sprites: Player y bala.
Fuente: Elaboración propia.
82
El único problema que seguramente encontréis ahora mismo, son los colores de la paleta, ya
que, para los ambos sprites, se están utilizando los mismos colores, es decir, los colores del
primer sprite que cargamos que corresponden a los primeros valores de la paleta, mientras
que los colores de la bala no se utilizan, ya que estos se encuentran en las últimas posiciones
de la paleta de colores. Por el momento, desconozco la manera de arreglar esto, habría que
averiguar la forma para que cada sprite obtenga sus colores correspondientes. Por ahora,
vamos a dejarlo así y más adelante intentaremos resolver este problema.
Una vez que ya tenemos dibujado ambos sprites por pantalla, es hora de hacer que el sprite
de la bala actúe como tal. Lo primero que vamos a hacer es una función de borrado de
Sprite, que nos permita borrar el sprite de la bala cada vez que llamemos a la función
encargada de realizar este proceso. Esta primera versión de la función de borrado, lo único
que hace es meter el valor 0 en todas las posiciones correspondientes del buffer, es decir, en
las posiciones de vpos, hpos y charcodes del sprite de la bala. De esta manera, el sprite de la
bala dejará de dibujarse.
Para poder saber desde que posición empezar a borrar en el SAT, tenemos que definir en el
proyecto las posiciones iniciales de memoria donde van a empezar las vpos, las hpos y los
charcodes del sprite que queramos borrar. Además, también debemos tener almacenada la
información sobre el ancho y el alto del sprite, para que de esta forma podamos saber hasta
qué posición de memoria hay que copiar los ceros, gracias a que solo hay que hacer una
multiplicación entre ambos valores de ancho y alto.
El problema es que en Z80 no existe la multiplicación como tal, es decir, no existe una
instrucción que permita multiplicar dos registros (o al menos yo no la conozco). Sin
embargo, podemos simular una multiplicación. Para ello, lo que vamos a hacer es sumar
al registro A el valor del ancho del sprite tantas veces como filas tenga dicho sprite. Por
ejemplo, si mi sprite es de 4x3 (4 tiles de ancho y 3 tiles de alto), se suma el valor del ancho,
que es cuatro, un total de tres veces que corresponde a las tres filas de alto que ocupa dicho
sprite, obteniendo así un resultado de 12, que es el resultado de multiplicar 4x3 y que
coincide con la cantidad de ceros que hay que copiar para poder borrar todas las posiciones
verticales del sprite en el buffer (véase figura 63).
No obstante, para poder borrar las posiciones horizontales y los charcodes, hay que copiar
dos ceros por cada iteración que se haga, ya que, recordemos que las hpos y los charcodes
se encuentran juntos en la tabla del SAT.
83
Figura 63. Función de borrado de la bala.
Fuente: Elaboración propia.
Una vez tenemos la función para borrar el sprite de la bala, lo siguiente que vamos a hacer
es moverla. Para moverla es fácil, al igual que con el movimiento del player, comprobamos
de nuevo el byte que contiene la información sobre el puerto $DC, el cual a su vez contiene
la información sobre las teclas que está pulsando el jugador.
Volviendo a mirar la documentación oficial de la consola [28], nos fijamos que es el bit
cuatro el cual hay que comprobar para saber si se está pulsando el botón de disparo. Si este
bit está a cero, quiere decir que se ha pulsado el botón y cuando esto suceda, lo que haremos
será establecer a uno una variable que tendremos que crear, que en mi caso la he denominado
“disparando”. Esta permitirá saber si el jugador está pulsando o no el botón de disparo.
Aquí surge otro problema y es el problema del tiempo. ¿Cómo se podría hacer para controlar
el tiempo? Es decir, hacer que la bala se dibuje durante x segundos desde que el jugador
dispara y, pasados esos segundos, se deje de dibujar la bala. El caso es que en el Z80 yo no
conozco ninguna instrucción que permita controlar el tiempo y no se me ocurre por ahora
una forma de implementarlo.
84
Por ello, para este pequeño ejemplo que estamos haciendo vamos a hacer que la bala se
dibuje hasta que “colisione” con algún elemento, en este caso, será la pared derecha o
izquierda del mapa. Pongo “colisión” entre comillas ya que realmente no va a colisionar,
simplemente vamos a comprobar si ha llegado a una determinada posición o no. Más
adelante, realizaremos esto de una manera mucha mejor mediante la comprobación de
colisiones con el mapa.
Para implementar esto que acabo de comentar es muy sencillo: Lo único que hay que realizar
es una comprobación antes de mover la bala. Esta comprobación la utilizamos para saber si
la bala se encuentra ya o no en el límite derecho del mapa (o en el límite izquierdo si se está
moviendo a la izquierda) y como ya tenemos previamente definido el valor del límite derecho
e izquierdo de la pantalla, lo único que hay hacer es comprobar ese valor con el de la posición
del sprite. Si se cumple la condición no se mueve la bala y, en caso contrario, sí que se puede
mover.
Figura 64. Movimiento hacia la derecha del Sprite de la Bala.
Fuente: Elaboración propia.
De esta manera, cuando el jugador dispara, la bala se mueve hasta alcanzar uno de los dos
límites del mapa y ahí se queda parada. Además, si almacenamos la dirección a la que mira
el player podemos hacer que la bala se dirija en la dirección a la que mira el jugador. Lo
único que hay que hacer es crear una variable nueva en nuestro fichero de bala, la cual en
mi caso se llamará bala_dir.
85
El motivo de crear una variable nueva para la dirección y no usar la misma que tiene ya el
player, es debido a que la del player va a estar cambiando continuamente cada vez que el
jugador mueva el sprite de un lado a otro. Esto lo que originaría es que, si la bala está yendo
en una dirección y el jugador en ese momento mira para el lado contrario, la bala cambiará
su dirección a la misma vez que el player y esto es algo que no queremos que suceda.
Por lo tanto, para evitar esto, creamos una variable nueva la cual va actualizar su dirección
solamente cuando esta haya finalizado su recorrido, que, en este caso en concreto, será
cuando llegue a uno de los límites del mapa.
Asimismo, con otra variable llamada “bala_state”, podemos hacer que el jugador no pueda
volver a disparar hasta que el anterior disparo aun no haya finalizado, evitando así que el
jugador pueda disparar demasiado rápido. Esta variable, proporciona información sobre si
la bala existe o no. La bala existirá siempre que el jugador haya disparado y esta no haya
finalizado su recorrido, cuando lo finalice entonces será cuando la bala dejará de existir,
siempre y cuando el jugador no haya vuelto a presionar la tecla de disparo.
Figura 65. Actualización de todos los aspectos del Sprite de la bala.
Fuente: Elaboración propia.
Con todo esto, ya tendríamos más o menos lo que queríamos. Un personaje capaz de disparar
una bala en función de la dirección a la que este mirando y, además, de que se pueda borrar
dicha bala. Está claro que la forma que lo hemos hecho no es la más óptima posible y
seguramente hay formas mejores de hacerlo, pero de momento cumple su función.
86
8.1.8. Colisiones entre sprites
Lo que realmente quería implementar ahora son las colisiones con el mapa de fondo, pero
tras estar investigando un poco y no encontrar casi nada de información sobre cómo hacerlo,
además que yo tampoco tengo mucha idea de cómo hacerlo, he decidido que vamos primero
a realizar las colisiones entre sprites que también es algo necesario e importante y más
adelante averiguaremos como efectuar las otras colisiones.
Para poder detectar una colisión entre dos sprites hay que tener en cuenta sus posiciones X
e Y que ocupan en la pantalla, además del ancho y alto del sprite. Con estos 4 valores, hay
que hacer un total de 4 comprobaciones para poder determinar correctamente si se ha
producido una colisión o no entre las dos entidades que hayamos decidido.
¿Por qué 4 comprobaciones? Esto es debido a que hay que comprobar todos los casos
posibles que puedan ocurrir, es decir, hay que comprobar en todo momento si la 1º entidad
que pasamos a la función está situada arriba, debajo, a la izquierda o a la derecha de la 2º
entidad que pasamos también por parámetro a la función dedicada a detectar las colisiones.
Si se cumple que esa primera entidad está en alguna de estas 4 situaciones, entonces no ha
habido colisión entre ambos sprite. En caso contrario, querrá decir que ambos sprites se están
solapando y, por consiguiente, están colisionando. Sabiendo esto, vamos a ver cómo realizar
las primeras dos comprobaciones que corresponden al eje X.
Lo primero que necesitamos son los datos de las dos entidades y, para ello, vamos a hacer
uso de los dos registros índices que existen en el Z80: los registros IX e IY. Recordemos que
antes de hacer uso de cualquiera de los dos registros será necesario guardar en ellos la
dirección de memoria donde empiezan los datos de las entidades que se van a tratar.
Además, al utilizar estos 2 registros podemos hacer que la función nos sirva no solo para
comprobar la colisiones entre dos entidades del juego concretas, sino para todas las entidades
que vaya a tener nuestro juego, de tal manera que la función para detectar colisiones sea una
función general.
La única condición que hay que cumplir es que los datos de cada entidad estén todos en el
mismo orden, para que así a la hora de acceder a los datos coincida, por ejemplo, que la
posición de memoria donde está la información sobre el ancho del sprite sea la misma para
todas las entidades.
87
Una vez tenemos los dos registros índices apuntando a las dos entidades que queremos
detectar colisión, nos disponemos a hacer la primera comprobación. Vamos a comprobar si
la 2º entidad (la entidad que pasamos en el registro IY) se encuentra situada a la izquierda
de la 1º entidad (la que pasamos por el registro IX) que en este caso en concreto que estamos
efectuando coincidirá con la bala y el enemigo respectivamente.
Para poder ver si la bala esta encima del enemigo o no, hay que comprobar si la posición X
del enemigo es mayor o igual que la posición X correspondiente de la bala sumado al ancho
que ocupa dicho Sprite. Esto resumido sería la siguiente condición: if (ent2_x +
ent2_w <= ent1_x) donde ent1 y ent2 hacen referencia a la entidad que pasamos
por el registro IX (el enemigo) y a la entidad que pasamos por el registro IY (la bala)
respectivamente.
Si se cumple esta condición quiere decir entonces que la bala estará situada a la derecha del
sprite del enemigo y, por lo tanto, no hay colisión. Hay que tener en cuenta que ent2_w
hace referencia al ancho que ocupa el sprite en píxeles no en tiles. Esto lo indico ya que
anteriormente para dibujar los sprites se tomaba el tamaño que ocupaban los sprites en tiles
y no en pixeles, por lo que es necesario añadir dos datos más a cada entidad que indiquen el
alto y el ancho del sprite en píxeles. Esto se consigue simplemente multiplicando el número
de tiles que ocupa por ocho, ya que cada tile en la SMS (si no se activa la visualización de
sprites de 8x16) ocupa 8x8 píxeles.
Dicho esto, y volviendo a la condición anterior para ver si la bala está a la izquierda o no, se
puede ver que es una simple inecuación y como tal podemos despejar la variable ent1_x
hacia la izquierda para tener todas las variables en un lado y que a la derecha se quede un
cero. El resultado tras despejar sería el siguiente: ent2_x + ent2_w – ent1_x <=
0. Luego, si el resultado es menor o igual que cero se cumplirá la condición de que la bala
está a la derecha del enemigo, si no, se estará produciendo una colisión.
88
Figura 66. Comprobación de si la 2º entidad está situada la izquierda de la 1º entidad o no.
Fuente: Elaboración propia.
A partir de aquí, las tres comprobaciones restantes siguen la misma estructura: cargamos los
valores necesarios en los registros, realizamos las operaciones necesarias de la condición
correspondiente y, finalmente, ponemos los dos saltos condicionales para que salten a la
dirección correspondiente en caso de que no haya colisión y que de esta manera no siga
comprobando el resto de condiciones.
Voy a poner simplemente el código de las condiciones que faltan para poder detectar la
colisión correctamente en todas las direcciones:
• Comprobación en X. ¿2º entidad a la derecha de la 1º? → if (ent1_x +
ent1_w <= ent2_x). Que despejándolo daría lugar a ent1_x + ent1_w
– ent2_x <= 0.
• Comprobación en Y. ¿2º entidad debajo de la 1º? → if (ent1_y + ent1_h
<= ent2_y). Que despejándolo daría lugar a ent1_y + ent1_h – ent2_y
<= 0.
• Comprobación en Y. ¿2º entidad encima de la 1º? → if (ent2_y + ent2_h
<= ent1_y). Que despejándolo daría lugar a ent2_y + ent2_h – ent1_y
<= 0.
89
Para poder verificar que se están haciendo bien las colisiones lo que podemos hacer es
cargar un valor aleatorio en una dirección de memoria cualquiera cada vez que se produce
una colisión. De esta manera, podemos ver de manera exacta si se produce una colisión
cuando ambas entidades se solapan.
Hay que tener cuidado a la hora de crear los sprites, ya que yo he tenido un problema a la
hora de comprobar si las colisiones funcionaban correctamente. El caso es que al estar
verificando el funcionamiento de la función me di cuenta que las colisiones no eran exactas
e indicaba que se producía colisión cuando no se estaban solapando los sprites.
Tras estar investigando el problema, me di cuenta que lo que pasaba era que al coger los
sprites del tilesheet no los cogí exactos y el fondo del sprite sobresalía por los lados del
mismo. Como yo después lo que hice fue convertir el fondo a transparente mediante GIMP,
a la hora de dibujar el Sprite por pantalla en la SMS, no se ve el fondo ni dichos bordes, pero
el tamaño del sprite sí que incluye los bordes por lo que la colisión se estaba haciendo
teniendo en cuenta esos bordes. Por lo tanto, para poder comprobar que se están haciendo
bien las colisiones he tenido que dibujar los sprites con el fondo original sin convertirlo a
transparente.
8.2. Replanificación
Llegados a este punto me encuentro con un problema bastante grave: el tiempo. Cuando me
encuentro escribiendo estas líneas es principios de abril y solo me quedan unos dos meses
para la entrega del proyecto en junio, lo cual es poco tiempo ya que no tengo realizado nada
jugable.
Está claro que siempre puedo atrasar la entrega y realizarla en septiembre, pero al menos
quiero intentar llegar a esta primera deadline y si no lo consigo que al menos no sea a causa
de que no lo he intentado.
Con esto claro, lo primero que tengo que hacer es una replanificación del proyecto, es
decir, debo planificar todo este tiempo que me queda para ver qué aspectos voy a
implementar del juego y cuáles no, pero siempre con la idea de intentar entregar un producto,
es decir, algo acabado y jugable.
90
Para ello, mi intención es que lo primero que debo hacer es una pequeña demo jugable de
mi videojuego la cual contenga todas las mecánicas básicas que va a tener, es decir, todo lo
que sí o sí debe estar en mi juego para que este sea algo jugable y acabado, aunque este sea
muy simple.
Tras esto, debo plantearme que más mecánicas podría introducir en lo que me resta de tiempo
para hacer que mi videojuego sea más divertido, pero siempre en mente de que debo entregar
un producto.
Sabiendo esto, he realizado un pequeño calendario donde he escrito todo lo que quiero hacer
dentro del tiempo que tengo (véase figuras 67 y 68). Un pequeño inciso es que el tiempo el
cual dispongo para terminar el juego no es hasta junio, si no hasta el 8 de mayo. Ya que
después de este día tendré que acabar otros aspectos como la memoria del proyecto o
preparar la entrega final.
Figura 67. Planificación abril (preproducción).
Fuente: Elaboración propia.
91
Figura 68. Calendario Mayo (producción).
Fuente: Elaboración propia.
Como se puede observar, he realizado una planificación de las tres primeras semanas, en las
cuales pretendo obtener todas las mecánicas que deseo. Esto se encontraría dentro del
apartado de preproducción y, tras este, comenzaría el de producción en el cual ya no se
deberían hacer más mecánicas si no que se realizará todo lo relacionado con el contenido del
juego: niveles, sprites, música etc.
8.3. Demo jugable
Tras tener claro más o menos qué es lo que se va a realizar en las próximas semanas, me
dispongo a hacer el primer paso: intentar conseguir una demo jugable de mi juego con todas
las mecánicas básicas del mismo. Esto significa que en esta demo deben estar todas las
mecánicas que deben de estar si o si en el juego para que este pueda ser algo jugable.
Como siempre, intentaré redactar todo el proceso para que otras personas que lo lean les
pueda servir de ayuda y no cometan los mismos errores que yo.
8.3.1. Colisiones con el mapa
Lo primero que he decidido que voy a realizar son las colisiones con el mapa. Esta mecánica
es algo que pospuse anteriormente debido a que no conocía la manera de realizar su
implementación. Sin embargo, ahora no queda otra que encontrar la forma de realizarla, así
que vamos a ello.
Para realizar este tipo de colisiones tenemos que acceder a la zona de memoria donde se
guarda toda la información relacionada con nuestro nivel. Esta información nosotros ya
sabemos dónde se encuentra ya que la hemos utilizado para poder pintar el nivel por pantalla.
92
Si hacemos memoria, recordaremos que utilizábamos una etiqueta para saber en la dirección
en la que se encontraba el fichero con la información de nuestro mapa, de tal manera que
simplemente llamando a dicha etiqueta ya estaríamos apuntando a la primera posición del
mapa.
Sin embargo, no hay que confundir la etiqueta que almacena los tiles con la del tilemap. A
nosotros nos interesa la que almacena el tilemap, ya que es la que contiene los índices de tile
que se van a dibujar en cada posición de la pantalla, mientras que en la etiqueta de tiles se
encontrarán todos los tiles a utilizar en el mapa.
Figura 69. Zona donde se almacena la información relacionado con el tilemap.
Fuente: Elaboración propia.
Ya sabemos dónde se encuentra la información que necesitamos, ahora tenemos que
averiguar cómo acceder a ella de manera correcta.
El problema que tenemos ahora mismo es que averiguar la manera para acceder al tile exacto
donde se encuentra el sprite el cual queremos detectar colisiones con el mapa. Una forma
para poder obtener esta posición podría ser mediante la división de la posición de nuestro
sprite en pantalla entre ocho. ¿Por qué entre ocho? Bien esto es debido a que cada tile
dibujado por pantalla tiene un tamaño de 8x8 pixeles, por lo que si queremos obtener el tile
exacto donde se encuentra el sprite bastaría con dividir tanto su posición en X como en Y
entre ocho.
93
No obstante, aquí surge otro problema ¿Cómo hacemos una división en Z80? Ya que, al
igual que con la multiplicación, no hay una instrucción que realice la división tal cual. A
pesar de ello, hay una forma muy sencilla de conseguir la división de un numero por otro en
base binaria.
Si queremos dividir un número binario esto se consigue sencillamente desplazando el byte
un bit a la derecha. Por ejemplo, supongamos que tenemos el valor 1002 (410) y queremos
dividirlo entre dos (21). Simplemente desplazando un bit a la derecha el divisor, ya
obtendríamos el resultado de dividir este entre 2, que en nuestro ejemplo daría 102 (210).
Si queremos hacer la división por otro número, por ejemplo, entre ocho como es en nuestro
caso para obtener el tile del mapa. Bastaría con desplazar tres bits a la derecha el divisor, ya
que ocho es equivalente a 23, es la potencia lo que nos indica el número de bits a desplazar
a la derecha.
Luego si queremos implementar esta operación en Z80, hay una instrucción que hace
exactamente esta operación que acabo de explicar, la instrucción srl. Esta instrucción lo que
va a realizar es desplazar un bit a la derecha el byte del registro que indiquemos,
consiguiendo así la división entre dos del resultado que tengamos almacenado en dicho
registro. Por lo tanto, si queremos dividirlo entre ocho, llamamos tres veces a la función de
srl.
Figura 70. División entre 8 del valor almacenado en el registro A.
Fuente: Elaboración propia.
De esta manera, aplicamos esta división a las posiciones X e Y donde se encuentra nuestro
sprite y así obtenemos el tile exacto donde se encuentra. Obtenido estos valores, los
almacenamos en un registro de 16 bits cualquiera para poder utilizarlos más adelante, ya que
ahora hay que acceder a dicho tile en la zona donde tenemos almacenado nuestro tilemap.
94
El primero paso es acceder a la fila donde se encuentra el tile. Fijándonos en nuestro fichero
donde almacenos el tilemap, podemos ver que cada fila del mismo ocupa un total de 64
bytes, ya que hay 32 valores, pero cada uno de ellos ocupa dos bytes. Sabiendo esto, nosotros
tenemos la información de la posición Y, es decir, la fila donde está el tile, por lo que
simplemente hay que ir sumando desde la primera dirección donde empieza el mapa hasta
la fila que tengamos que llegar, teniendo en cuenta que para pasar de una fila a otra hay que
sumar el tamaño que ocupa que son 64 bytes.
Figura 71. Acceso a la fila correspondiente donde se encuentra el tile a obtener del tilemap.
Fuente: Elaboración propia.
Llegados a este punto, ya estaríamos en la fila correspondiente del tilemap, ahora hay que
acceder a la columna exacta. Para ello, podemos seguir un procedimiento parecido al que
hemos realizado para las filas, con la diferencia que no hay que no sumar 64 bytes para cada
columna, sino 2 bytes.
95
Figura 72. Acceso a la columna del tile del tilemap.
Fuente: Elaboración propia.
Ahora ya tendríamos en el registro A nuestro valor del índice de tile en el que se encuentra
nuestro sprite. Ahora lo que debemos hacer es comprobar si ese valor es un valor de colisión
o no. Esta información se puede extraer simplemente observando la imagen de nuestro mapa
y la dirección de memoria donde lo almacenamos. Podremos darnos cuenta así de cuál es el
valor del índice de tile que representa el suelo, por ejemplo, o cualquier otra estructura de
nuestro mapa por el cual nuestro sprite debe colisionar en pantalla.
El aspecto más importante a conseguir en esta función es el de hacer que se convierta en una
función automatizada. De tal manera, que no tengamos que crear va una función distinta por
cada mapa que tengamos.
Es por eso, que debemos tener toda la información de cada uno de nuestros mapas
almacenada. Información como la dirección donde empieza el tilemap o los tiles, cuanto
ocupa cada fila del tilemap etc. De esta manera, simplemente tenemos que llamar a nuestro
registro índice IX o IY para que apunte a la zona donde tenemos almacenado toda esta
información.
8.3.2. Gravedad
Ya tenemos nuestras colisiones del mapa funcionando. Puede ser que no sean las mejores
colisiones del mundo, pero cumplen su función. En nuestra mano está ahora en darles uso
para que parezca que nuestro personaje se mueva realísticamente por el entorno del nivel.
Para ello, una cosa que podemos hacer es mejorar es el salto del sprite.
96
Si hacemos memoria, recordamos que la forma que hacíamos el salto del jugador era a través
de una tabla que recorríamos, la cual almacenaba valores que representaba la cantidad que
el sprite subía o bajaba en el salto. El problema es que si queremos que el salto parezca un
salto de verdad no podemos definirle nosotros los valores de bajada, ya que siempre va a
bajar la misma cantidad y puede ser que en nuestro mapa haya una caída más grande o más
pequeña. Por lo que, una solución a este problema es dejar que el valor de caída lo defina
nuestro mapa.
El proceso de subida del salto será el mismo, ya que nosotros definiremos cuanto queremos
que salte y a que velocidad. Sin embargo, ahora la tabla terminará justo cuando alcancemos
el valor más alto de subida y, a partir de ahí, utilizaremos otra tabla para representar la
velocidad a la que queremos que caiga el jugador.
Figura 73. Tablas para implementar el salto del jugador.
Fuente: Elaboración propia.
Serán las colisiones del mapa las que nos indicarán hasta cuando tiene que bajar el jugador,
de tal manera que recorreremos la tabla de caída hasta que se colisione con un elemento del
mapa. Para intentar simular que la caída parezca una caída de verdad, lo que hacemos es
recorrer la tabla desde el principio hasta llegar al último valor de la misma. Si al llegar a este
último valor, el jugador aún sigue cayendo entonces se sumará solo el último valor de la
tabla hasta terminar la caída.
97
Figura 74. Función para controlar la caída del personaje hasta colisionar con el mapa.
Fuente: Elaboración propia.
8.3.3. Control del tiempo mediante VSYNC
Una de los aspectos más transcendentales que hay que implementar en el juego es el tiempo.
Esto es muy importante ya que con él podemos controlar prácticamente todo en nuestro
juego: el tiempo del nivel, el tiempo que lleva la bala disparada, la IA de los enemigos etc.
Por ello, vamos a averiguar cómo podríamos hacer para poder guardar el tiempo que pasa
en nuestro videojuego.
Si nos vamos al apartado de los registros del VDP, podemos ver que en el bit 5 del registro
$01 se utiliza para habilitar/deshabilitar el VSYNC o, también conocido como frame
interrupt.
98
El frame interrupt es una interrupción que se genera cada vez que se dibuja un frame, es
decir, cada vez que pintamos todas las filas de la pantalla y se vuelve a empezar desde la
primera fila. De esta manera, si habilitamos el VSYNC, cada vez que se pinte un frame y
tenemos las interrupciones habilitadas, la ejecución de nuestro juego saltará a la dirección
$0038, que recordemos es la dirección donde se gestionan las interrupciones en la Sega
Master Sytem.
Por consiguiente, si nosotros tenemos un registro del número de frames que se han pintado
hasta el momento, podemos hacer que cada vez que el juego pinte 60 frames se incremente
el tiempo en un segundo. Este valor de tiempo lo almacenamos en otra variable y así tenemos
almacenado los segundos que han transcurrido desde que se ejecutó el juego.
Figura 75. Contador de frames y de tiempo mediante VSYNC.
Fuente: Elaboración propia.
Con nuestro contador de tiempo aparentemente funcionando podemos hacer alguna prueba
para ver si realmente funciona como debería. Vamos a realizar, por ejemplo, el control del
disparo del jugador.
Vamos a hacer que el jugador solo pueda disparar cada x segundos, de esta manera evitamos
que el jugador pueda realizar más disparos de la cuenta. Para ello, tenemos que tener un
registro del tiempo que ha pasado desde que se ha disparado la última vez y aquí es donde
tenemos que hacer uso de nuestro contador de tiempo.
99
Cuando el jugador pulse el botón de disparo, tenemos que registrar en otra variable el
segundo exacto cuando se realiza dicho disparo, para ello, lo que hacemos es recoger el valor
de nuestra variable tiempo, que contiene los segundos que han pasado y copiamos su valor
en otra variable llamada tiempoDisparo por ejemplo, la cual almacenará el segundo de
cuando se ha disparado. Esto solo lo haremos si dicha variable de tiempoDisparo estaba a
cero, ya que quiere decir que aún no se había efectuado el disparo anteriormente y es la
primera vez que se dispara.
Sin embargo, si no estaba a cero hay que comprobar si ha pasado el tiempo suficiente para
que el jugador pueda efectuar otro disparo. ¿Cómo hacemos esto? Pues simplemente
restando el valor del tiempoDisparo con el del tiempo del juego. Esto nos dará un valor que
representa el tiempo que ha transcurrido desde que se disparó la bala. Si este valor es mayor
que el que nosotros habíamos indicado, querrá decir que ya se puede disparar otra bala, si
no, entonces aún no se puede disparar otra vez.
Para poder hacer una comparación de mayor o igual en Z80, esto se puede conseguir
mediante la instrucción “jr nc”. Esta instrucción efectuara un salto a una dirección de
memoria siempre y cuando en la última operación no se haya producido acarreo, es decir,
no se requiere de un bit más para poder representar el valor. Lo que es equivalente a hacer
una comparación de mayor o igual en el Z80.
Figura 76. Uso del tiempo para controlar la frecuencia de disparo del jugador.
Fuente: Elaboración propia.
100
8.3.4. Inicialización de los datos
Lo siguiente que vamos a ver es el proceso de cambiar de nivel dentro del juego, sin
embargo, para poder realizar esto tenemos que tener implementado antes otro aspecto muy
importante y necesario para poder hacer de manera correcta el cambio de niveles, la
inicialización de nuestros datos.
Cada vez que arranquemos nuestro videojuego, este debe saber de alguna forma que datos
son los que necesita inicialmente para mostrar lo que tenga que mostrar, es decir, si nuestro
juego al arrancar muestra el mapa que corresponde al nivel 1 y este a su vez, contiene 2
enemigos, una puerta o lo que sea que necesite. Es por ello, por lo que necesitamos tener
almacenados nuestros datos en algún sitio para que cuando arranquemos el juego, lo primero
que se haga sea acceder a estos datos para saber qué es lo que se va a dibujar y donde.
Esta inicialización no solo es necesaria al arrancar el juego, también la necesitaremos a la
hora de cambiar de nivel, para saber que mapa hay que dibujar, que variables inicializar etc.
Para lograr esto, vamos a hacer uso de las estructuras (structs) que proporciona el
ensamblador WLA DX.
Con el objetivo de tener todo nuestro proyecto de manera más o menos organizada,
crearemos un fichero nuevo que hará de zona de almacenamiento de datos. De esta forma,
en este fichero guardaremos todos los datos que necesitemos en cualquier momento de
nuestro juego, ya sea para inicializar, para guardar información de los sprites o cualquier
otro aspecto relacionado con datos de nuestro juego.
Esta zona de datos, se encontrará en nuestra ROM del juego ya que son datos que no van a
cambiar en ningún momento de la ejecución de nuestro juego y que solo los utilizaremos
para inicializar los diferentes aspectos del mismo.
Antes de inicializar nada, tenemos que crear nuestras zonas de RAM donde vamos a copiar
todos esos datos que requerimos. Para ello, primero usaremos los structs, que permiten crear
estructuras de datos.
101
Figura 77. Ejemplo de creación de un struct.
Fuente: Elaboración propia.
Como vemos en la figura anterior, de esta manera crearíamos una estructura de datos. Sin
embargo, al hacer esto no estamos ocupando espacio aun, es decir, este código no va a ocupar
espacio en nuestra ROM, ya que funciona igual que las definiciones, hasta que no llamemos
a la estructura no se va generar el código en la consola. Si queremos crear nuestra estructura
de datos, tenemos que utilizar “instanceof”.
Mediante esta directiva, primero escribimos el nombre con el cual queremos llamar a la
estructura, tras esto, escribimos “instanceof”, el nombre de la estructura que queremos
utilizar y el número de estructuras a crear.
Veámoslo mejor con un ejemplo. Basándonos en la figura anterior, observamos que hemos
creado un struct llamado game_objects, el cual nos permitirá tener los datos de todas nuestras
entidades del juego. Enemigos, vidas, balas cualquier entidad que sea necesaria dibujarla por
pantalla o realizar modificaciones de su posición o de cualquier otro aspecto, tendremos que
crear un struct de ella.
Figura 78. Creación struct game_object player.
Fuente: Elaboración propia.
102
Con el ejemplo que se muestra en la figura previa, estaríamos creando una zona de datos
exclusiva para el player, es decir, es como si estuviésemos reservando memoria para todos
los datos de nuestro jugador. Datos que corresponderán a los que hay indicados en el struct
que hemos hecho referencia que en este caso es el de game_objects.
Pero la principal ventaja que proporcionan los structs es que podemos acceder a ellos de
manera bastante sencilla utilizando los registros índices ya que, como se observa en la
siguiente figura, el ensamblador nos genera automáticamente etiquetas con el nombre de
la estructura que hemos creado y cada una de las variables del mismo.
Figura 79. Símbolos generados automáticamente con el struct del player.
Fuente: Elaboración propia.
Gracias a esto, si queremos acceder en cualquier momento al valor de la posición X del
jugador, esta información la obtenemos con la etiqueta “player.x”. Pero, no solo eso, si
además tenemos varios game_objects y queremos hacer una función que sirva para todas las
entidades de nuestro juego, como nuestras funciones de copiar datos al buffer o de dibujado
de sprites, podemos apuntar con IX al principio de la estructura de nuestra entidad y, cuando
queramos acceder a cualquier dato del mismo, simplemente con hacer
(IX+game_object.estado) ya estamos automáticamente apuntando a la zona del struct que
almacena los datos de la variable que queremos obtener y esto nos sirve para todas las
entidades que utilicen la misma estructura, ya que la disposición de los datos siempre va a
ser de la misma manera.
Ahora que ya sabemos cómo crear nuestras zonas de datos para cada una de nuestras
entidades del juego, lo único que nos queda es inicializar estos datos a sus valores iniciales.
103
Hay diferentes maneras que podemos implementar esto, pero la manera más sencilla es
utilizando la instrucción del Z80 denominada “ldir”. Mediante esta instrucción podemos
realizar la copia de la cantidad de datos que nosotros queramos, desde una zona de memoria
a otra.
Para poder utilizar esta instrucción, primero en el registro HL, tenemos que almacenar la
dirección de memoria donde se encuentran los datos que queremos copiar. Como hemos
visto antes, hemos creado una zona exclusiva para almacenar todos nuestros datos, por lo
que simplemente tenemos que crear una etiqueta dentro de esta zona, que almacene la
dirección donde comienzan los datos que queremos duplicar. Una cosa que hay que tener en
cuenta, es que estos datos deben estar escritos de la misma forma que lo está el struct a donde
pretendemos copiar los datos, ya que, en caso contrario, almacenaremos los datos en las
variables incorrectas.
En segundo lugar, en DE, necesitamos tener la dirección a donde queremos copiar esos datos
a los que apuntamos en HL. En nuestro caso, almacenaremos en DE la etiqueta player, que
contiene la dirección donde empieza la zona de datos del jugador. Recordemos, que esta
zona de datos se encuentra en RAM, ya que van a ser modificados durante la ejecución del
juego.
Finalmente, tenemos que tener en el registro BC, el valor que indique la cantidad de datos
que queremos copiar. Este registro se utilizará como contador para saber cuántas posiciones
de memoria se tienen que avanzar tanto en HL como en DE. Siguiente el ejemplo de las
figuras anteriores, queremos copiar 17 bytes en total.
Figura 80. Ejemplo copia datos con ldir.
Fuente: Elaboración propia.
104
8.3.5. Cambio de mapas
El proceso de cambio de nivel, una vez que ya sabemos cómo inicializar nuestros datos, es
bastante sencillo. El único problema que puede llegar a ocurrir sea que con tantos datos nos
hagamos un lio y no sepamos donde están cada uno de ellos, o copiemos los datos que no
son etc. Es muy importante tener bien organizado nuestro proyecto para que en todo
momento lo tengamos bien estructurado y diferenciado en sus diferentes partes, para evitar
futuras confusiones.
Es por eso, por lo que considero necesario crear un fichero aparte que se encargue única y
exclusivamente de almacenar todos nuestros datos que necesitemos inicialmente, tanto a la
hora arrancar la consola como al hacer el cambio de nivel. De esta manera, evitamos lo
sobrecarga de código en los otros ficheros de nuestro proyecto.
Sabiendo esto, la mecánica de cambio de nivel, implica saber que datos vamos a necesitar
para el siguiente nivel. Estos datos pueden ser desde el mapa que queremos dibujar hasta el
número de entidades que van a haber o en qué posición del SAT se va a dibujar cada sprite.
La mayoría de estos datos ya los tenemos en nuestra estructura de game_objects, donde entre
otras cosas almacenamos la información relacionada con la dirección a donde copiar las
posiciones verticales y horizontales del sprite del game_object.
Sin embargo, no tenemos información sobre el nuevo mapa que hay que dibujar, el valor del
tile que indica las colisiones con el mapa o el número de cada tipo de entidades que van a
haber en el nivel. ¿La solución? Crear otra estructura para almacenar los datos de los
mapas.
Esta estructura almacenará toda la información que he comentado anteriormente. Además,
crearemos una zona extra de datos, para poder almacenar aspectos como el número de
enemigos o el de cualquier otra entidad. El motivo de esto es debido a que, aunque ya
tengamos en el struct del mapa los datos que indican el número de entidades de cada tipo
que van a haber, tener los datos en variables aparte del struct facilita su acceso, que, en caso
contrario, deberíamos hacerlo mediante los registros índice para apuntar al mapa que
corresponda al del nivel que nos encontramos y a su vez acceder a los datos de este para
saber la cantidad de entidades.
105
Si todo esto, en vez de hacerlo repetidamente cada vez que necesitemos acceder a los datos,
lo realizamos solo una vez justo cuando cambiamos de nivel, nos ahorraremos mucho tiempo
de ejecución y de espacio.
Con todo esto, la única información que nos falta es saber cuándo hay que hacer el cambio
de nivel y que nivel hay que cargar. Para esto, simplemente creamos dos variables, una que
almacene el nivel en el que estamos y otra que almacene dos valores (0 o 1) cuando está en
un valor quiere decir que no queremos cambiar de nivel, si está en el otro queremos cambiar.
Ambas variables se actualizarán al terminar el proceso de cambio de nivel.
Figura 81. Proceso de cambio de nivel del juego.
Fuente: Elaboración propia.
Ahora, cada vez que queramos cambiar de nivel, simplemente hay que cambiar ambas
variables cuando lo necesitemos a su valor correspondiente y cuando entremos en la función
de la figura previa, se hará el cambio automáticamente.
Dentro de cada nivel, debemos tener todos los datos que requiere este: dirección a la zona
de datos del mapa, inicialización de todos los game_objects que hay en el nivel, así como
del número de entidades de cada tipo etc.
Por otro lado, antes de hacer el cambio, siempre que no sea el primer nivel, debemos borrar
toda la información del nivel anterior. Esto es información relacionada con el mapa a dibujar,
borrar todos los sprites del SAT para evitar que se queden dibujados y desactivar todas las
entidades para que no se sigan haciendo actualizaciones de los mismos.
106
Para borrar el mapa del nivel anterior, simplemente cargamos el tilemap del nuevo mapa y
ya se haría el borrado del mismo. Para los sprites del SAT, bastaría con llenar de ceros toda
la tabla que almacena la información de los mismos y como sabemos cuánto ocupa gracias
a nuestro buffer del mismo, este proceso es bastante sencillo.
Figura 82. Función para borrar los sprites de la pantalla.
Fuente: Elaboración propia.
Finalmente, lo único que falta es “matar” a las entidades para que no continúen haciendo
cosas. Como ya tenemos la información de cuantas entidades hay de cada tipo en el nivel,
simplemente hay que hacer un bucle que recorra todas las entidades y ponga su estado a 0.
Figura 83. Función para "matar" todas las entidades del nivel.
Fuente: Elaboración propia.
107
Pero, para que esto funcione, hay que hacer que todas nuestras entidades solo funcionen si
su estado está activo, es decir, está a 1. Si no, aunque hagamos esto seguirán actualizándose
en los siguientes niveles.
Por último, a la hora de cargar los mapas, el programa BMP2TILE tiene una característica
muy útil que nos ahorra bastante trabajo y es el poder indicar el valor del primer índice de
tile del mapa que vamos a exportar. De esta manera, los valores del tilemap del mapa se
actualizarán automáticamente en función del valor del primer índice de tile y podremos
colocar los tiles deñ mapa en memoria justo después de donde acaba el mapa anterior, ya
que el tilemap ya cogerá automáticamente los tiles que le corresponden.
También es recomendable hacer esta carga de los tiles justo al arrancar la SMS y no cuando
se hace el cambio de nivel, ya que es preferible que ese tiempo que va a durar la carga de los
tiles de los mapas sea al arrancar la consola y no durante la ejecución del juego.
8.3.6. Muerte del jugador
Con el cambio de nivel aparentemente funcionando es hora de implementar la mecánica de
muerte del jugador. Esta mecánica requiere de dos aspectos: primero necesitamos saber la
vida restante del jugador para saber si este ha muerto o no y, por otro lado, si el jugador
muere, es necesario reiniciar el juego en caso de que este lo desee.
El primer punto es bastante sencillo de implementar, ya que todos nuestros game_objects
van a tener una variable que servirá para controlar el número de vidas restantes que le
quedan, cuanto este valor esté a cero, entonces es cuando la entidad muere.
Para restar una vida, basta con acceder a la variable de vida del struct de game_objects y
decrementar dicho valor en uno. Si después de decrementar, el valor es igual a cero, entonces
se ha muerto la entidad.
108
Figura 84. Función para decrementar la vida del jugador una unidad.
Fuente: Elaboración propia.
La variable mundo_state es una variable que veremos en profundidad más adelante, pero
básicamente se utiliza para controlar los distintos estados en los que se encuentra el juego.
En este caso, cuando el jugador muere, cambiamos al estado de muerte y, por lo tanto, la
ejecución de nuestro programa saltará a la dirección encargada de gestionar esta muerte.
La gestión de la muerte consiste en un “reset” del juego (reinicio en español). Esto quiere
decir que tenemos que hacer que el juego vuelva a estar como cuando arrancamos la consola.
Para ello, una vez que la ejecución del juego salta a la zona de reinicio, hay que hacer
diferentes cosas:
• Vaciar el SAT: Tenemos que vaciar nuestra tabla de sprites para evitar que estos se
queden dibujados en pantalla en caso de que no hayan sido borrados en el nivel donde
se produjo la muerte. Nosotros sabemos cuánto ocupa dicha tabla gracias al buffer
del SAT que construimos anteriormente, por lo que lo único que hay que realizar es
rellenar de ceros toda esta zona de memoria hasta que ya no queden posiciones
restantes.
109
• Inicialización de datos: Al igual que al arrancar la consola, tenemos que volver a
cargar todos los datos iniciales que requiere el primer nivel. Como estos datos los
tenemos guardados y ya sabemos cómo realizar una copia rápida y sencilla mediante
la instrucción “ldir”, hacemos la copia de todos estos datos a sus direcciones
correspondientes. Además, como esta copia se va a realizar de forma idéntica tanto
para arrancar la consola como para reiniciar el juego, es conveniente hacer una
función que sirva para ambos casos y así nos ahorramos espacio en memoria. No
olvidemos que estos datos también incluyen aquellos que hacen referencia al nivel,
como la variable que indica el nivel en el que se encuentra el jugador y la que indica
que queremos hacer un cambio de nivel. Como ya implementamos el cambio de nivel
anteriormente, cuanto se termine la función de reinicio, se llamará a la de comprobar
el nivel y está se encargará de borrar el mapa anterior, cargar el siguiente tilemap,
controlar el número de entidades etc.
• Cargar sprite del jugador y copia al SAT: Lo último que tenemos que implementar
para poder conseguir hacer el reset de manera correcta, es cargar el sprite del jugador
al buffer, ya que es el único sprite que sabemos que si o si hay que dibujar en pantalla
después de reiniciar. Podríamos dibujar otros sprites que sabemos que también están
en nuestro nivel, pero si más adelante los modificamos, nos tocará cambiar también
aquí todos los sprites que no dibujamos, mientras que el jugador sabemos que
siempre se va a dibujar. Tras cargar el sprite, solo nos queda llamar a la función que
copia el buffer al SAT y ya tendríamos nuestra función de reiniciar el juego. Lo único
necesario es llamar a la función de cambio de nivel, para que se gestione los aspectos
restantes como el borrado del mapa y carga del siguiente.
8.3.7. IA primer tipo de enemigos
Una de las últimas mecánicas que voy implementar para que el videojuego sea un juego
jugable, podría ser la de los enemigos. En cualquier juego de plataformas es necesario que
haya algún tipo de enemigo o IA con la cual jugar contra ella o con ella. En este caso, vamos
a ver cómo realizar la IA de un enemigo muy simple, de tal manera que podamos matarlo y
viceversa.
110
Para implementar la IA de este enemigo y la de los siguientes, vamos a utilizar una técnica
conocida como máquina de estados. La máquina de estados, de manera resumida, es una
de las metodologías que se utiliza para implementar la inteligencia artificial de los
videojuegos, donde se parte de un estado o comportamiento, mientras el cual esté activo, la
IA del enemigo o entidad correspondiente realizará la acción o acciones que se hayan
indicado dentro de este estado. Esto se realizará hasta que se cambie de estado a otro, donde
se realizará otras acciones distintas hasta que vuelva a cambiar al estado siguiente o vuelva
al anterior.
El motivo de usar está técnica y no otras es debido a que es una técnica bastante sencilla de
implementar y teniendo en cuenta que debemos realizarla en Z80, creo que es la más
conveniente y aparte de que no se va a implementar una IA demasiado compleja que requiera
utilizar otra técnica distinta a esta.
Vamos a tomar como referencia a uno de los enemigos que aparecen en el videojuego Master
of Darkness para la Sega Master System, que son los perros. Este tipo de enemigos, a simple
vista se puede apreciar que dispone de dos estados de IA diferentes: un estado de reposo, por
así llamarlo, en el cual la entidad se encuentra quieta en el lugar y, por otro lado, está el
estado de ataque, donde el perro, cuando ve al jugador, comienza a correr de un lado a otro
durante un tiempo determinado, donde incluso puede llegar a saltar plataformas. Si durante
este proceso se cruza con el jugador, este recibe daño.
Basándonos en este tipo de enemigo, vamos a crear nosotros uno bastante parecido, pero
más simple. Nuestro enemigo también va a disponer de dos comportamientos: el de reposo
y el de ataque. Durante el primer estado, la IA lo que va realizar es quedarse quieta durante
los segundos que le indiquemos. Tras transcurrir los segundos necesarios, esta cambiará al
comportamiento de ataque, durante el cual se moverá de una posición a otra hasta que, de
nuevo, se agote el tiempo disponible para el comportamiento. Lo que pretendemos conseguir
aquí es que el jugador se dé cuenta de que una vez que transcurra un breve lapso de tiempo,
el enemigo dejo de correr por lo que es más fácil eliminarlo sin recibir daño que cuando este
se mueve. Pero ahí es donde el jugador tiene que decidir si perder el tiempo en esperar a que
acabe o jugársela a matarlo antes con el riesgo de perder una vida o varias.
Para conseguir esto, tenemos que implementar una función que se encargue de gestionar
todo lo relacionado con el enemigo mientras este esté vivo y que cuando muera, se deje de
controlar y se borre del juego.
111
Para lo primero, podemos realizarlo de forma sencilla, ya que como sabemos, tenemos una
variable que nos indica el estado del enemigo (si está vivo o no) por lo que cuando esta esté
a cero, no haremos ninguna de las funciones de la entidad.
Figura 85. Comprobación del estado del enemigo.
Fuente: Elaboración propia.
Evidentemente, como podemos ver en la figura previa, necesitamos llamar antes a una
función encargada de controlar el estado del enemigo y borrarlo en caso de que este muera
ya sea por la causa que sea.
Figura 86. Función para comprobar si hay que eliminar al enemigo o no.
Fuente: Elaboración propia.
112
Para comprobar si el enemigo hay que borrarlo o no, podemos hacerlo con nuestra variable
de colisión de los game_objects. Cuando esta variable esté activa, querrá decir que ha
colisionado con alguna otra entidad del juego. Como ya sabemos, en la variable se almacena
el identificador de la entidad con la que colisiona, por lo que comprobamos dicho valor para
comprobar si es el de la entidad de bala o no.
Nosotros comprobamos si es de la entidad de la bala porque es lo único (por ahora) por lo
que podría morir el enemigo, pero aquí podríamos poner cualquier otro identificador por el
que nosotros queramos que el enemigo muera. En función del identificador que sea, si al
colisionar y llamar a la función comprobar vida de los enemigos esta devuelve cero, querrá
decir que ha muerto y hay que borrarlo del mapa.
En caso contrario de que la entidad haya muerto, entonces tenemos que comprobar el estado
de la IA en la que se encuentra, en el estado reposo o de ataque, para ello podemos utilizar
una variable para controlar este aspecto. En función del valor que sea, cero o uno, la entidad
tendrá un comportamiento u otro.
Comenzaremos primero con el estado de reposo, como ya hemos visto, necesitamos
controlar el tiempo que lleva el enemigo en cada uno de los estados. El proceso para calcular
el tiempo transcurrido siempre va ser el mismo: comprobamos si la variable encargada de
almacenar el tiempo es distinto de cero o no, ya que si está a cero quiere decir que el estado
aún no ha sido activado y por lo tanto, necesitaremos registrar el segundo exacto en el que
se entra a la función, que como ya hemos hecho otras veces, esta información la recogemos
de la variable tiempo que recordemos que se incrementa en uno cada vez que se dibujan
sesenta frames. Si, por el contrario, el valor de tiempo del estado es distinto de cero, entonces
el estado ya ha comenzado y debemos comprobar cuanto tiempo ha transcurrido con la
instrucción “jr c” que como sabemos era el equivalente a hacer una comprobación de mayor
o igual en el Z80.
Como en el estado de reposo lo único que hace el enemigo es quedarse quieto hasta que pase
el tiempo necesario, cuando el tiempo haya acabado cambiamos el estado de la IA al
siguiente valor, para que así la siguiente vez que se entre al update del enemigo se haga el
siguiente comportamiento.
113
Figura 87. IA enemigos estado reposo.
Fuente: Elaboración propia.
Para el estado de movimiento la primera parte va ser idéntica a la del estado de reposo, con
la diferencia de que habrá que utilizar otra variable distinta para controlar el tiempo de este
estado. Mientras que se esté en este estado, el enemigo tendrá que moverse entre las distintas
posiciones que nosotros le indiquemos, sin embargo aquí puede surgir un problema y es el
de que cuando el enemigo entre en este estado siempre debe coger la misma posición como
margen de distancia, de tal manera que, independientemente de donde se quede al acabar el
estado de movimiento, siempre recorrerá la misma distancia y no se desplace por el nivel de
manera incontrolada.
¿Cómo hacemos esto? Muy sencillo, al inicializar los datos del enemigo, podemos utilizar
una dirección de memoria para guardar la posición de inicio del enemigo, la cual
corresponderá a la posición donde aparece el mismo al cargar el nivel y así siempre tendrá
esta posición como referencia y sabremos que no ocurrirá nada raro ya que la hemos definido
nosotros y la tenemos controlada.
114
Además de esta variable, necesitamos información sobre la cantidad de desplazamiento, es
decir, cuánto se va a poder mover desde esa posición inicial. Podemos definir un valor que
represente esta distancia de desplazamiento del enemigo tanto para la izquierda como para
la derecha y de esta manera ya tendríamos una manera de hacer que el enemigo se mueva
entre dos posiciones distintas y siempre la misma cantidad.
Tras leer esto, puede que nos hayamos dado cuenta de que necesitamos unas cuentas
variables extras para gestionar la IA de nuestro enemigo. Podríamos simplemente añadir
estas variables a nuestra estructura de game_objects y así tener toda la información en una
única estructura, sin embargo, no sería eficiente, ya que estas variables solo nos van a servir
para los enemigos y para el resto de entidades no. Por lo que estaríamos ocupando espacio
de memoria RAM que no vamos a usar nunca. Si que es cierto que obtendríamos la ventaja
de poder acceder a esta información con solo un registro índice, pero sigue sin ser suficiente
como para salga rentable utilizar este método.
La forma más eficiente sería crear una nueva estructura encargada de almacenar esa
información extra que requieren los enemigos como puede ser el tiempo de cada
comportamiento o la posición de inicio.
Con toda la información ya almacenada, ya podemos usarla para hacer el movimiento del
enemigo. Esto se hará igual que mover cualquier otra entidad, con la diferencia de que se
podrá mover a la derecha o a la izquierda siempre que no haya superado el límite máximo
de distancia a recorrer que le hayamos indicado.
115
Figura 88. Movimiento de los enemigos.
Fuente: Elaboración propia.
Con todo esto, lo único que faltará sería llamar a la función para que dibuje el sprite del
enemigo siempre que este esté vivo y ya tendríamos a nuestro enemigo simple funcionando.
Lo normal sería que quisiésemos pintar más de un enemigo a la vez, por lo que tenemos que
hacer que el update de los enemigos se haga para todos los enemigos que hayan en el nivel.
Como habrás podido notar, en los ejemplos que he compartido se hace uso de los registros
índices para acceder a los datos de los enemigos y como ya deberías saber esto permite que
la función sea general y se pueda usar para todos los enemigos y no solo uno.
Pero, para poder usar los datos de cada enemigo, debemos estar apuntando previamente con
cada registro índice a la dirección de memoria correcta que almacena la información y
además debemos hacer esto de manera automatizada, es decir, no deberíamos tener que
indicarle nosotros manualmente la dirección de cada una de las entidades, teniendo en
cuenta también que el número de enemigos variará en función del nivel.
116
A pesar de todo, hacer que todo el update de los enemigos funcione de manera automatizada
no es tan complicado, aunque sí que puede llegar a dar problemas si nos liamos con los datos
y sus tamaños.
La información del número de enemigos que hay en el nivel ya sabemos que la tenemos
almacenada, por lo tanto, este valor lo usaremos para saber cuántas veces tenemos que hacer
el bucle o si no tenemos que hacerlo en caso de que no haya enemigos. En caso afirmativo,
lo siguiente que tenemos que hacer es apuntar con los dos registros IX e IY al principio de
los datos del enemigo actual que se esté actualizando. Para ello, podemos apuntar a los datos
del primer enemigo y a partir de ahí avanzar hasta los datos del enemigo correspondiente a
actualizar. Pero para poder hacer esto necesitamos saber en todo momento el tamaño de
datos que tiene cada enemigo y gracias a las structs del WLA DX, está información se guarda
automáticamente en una variable del tipo “sizeof” por lo que cada vez que se haga una
iteración del bucle, al final sumamos al registro índice el tamaño de los datos del enemigo y
de esta manera al empezar el bucle de nuevo ya estamos apuntando a los datos correctos del
mismo.
Figura 89. Update para más de un enemigo.
Fuente: Elaboración propia.
117
Hay que tener cuidado también con no perder los valores de los registros durante el bucle,
como por ejemplo el valor del contador del bucle. Este valor lo estamos almacenando en el
registro B y lo más probable es que sea modificado a lo largo de la iteración del bucle por lo
que para evitar problemas posteriores, apilamos el registro BC en la pila, ya que para hacer
un push a la pila se tiene que hacer con registros de 16 bits y cuando necesitemos recuperar
el valor del contador, que será antes de acabar la iteración, hacemos pop para desapilar de la
pila.
8.3.8. Recibir daño
Ya tenemos un enemigo muy simple funcionando, por lo que el último paso para conseguir
tener algo con lo que poder jugar es conseguir que ese enemigo nos haga daño y así poder
morir. La manera de comprobar cuando hay que restar una vida al jugador ya lo tenemos
implementado en la función de comprobar colisión, la cual solo hay que pasarle en los
registros IX e IY, los datos de las dos entidades a colisionar, que en este caso en concreto
serán el jugador y el enemigo.
No obstante, hay que hacer una comprobación de colisión con todos los enemigos que haya
en el nivel y no solo con uno, pero esto ya sabemos que podemos controlarlo con la variable
de número de enemigos del nivel y el tamaño de datos de cada uno como ya hemos realizado
anteriormente en el update de los mismos, por lo que el proceso es el mismo pero llamando
a la función de comprobar colisión y tras este comprobar si el id devuelto en colisión coincide
con el de los enemigos.
La cuestión que quiero explicar reside en cuando el jugador recibe daño por parte de
cualquier fuente, ya sea por un enemigo, por una bala, por una trampa o cualquier otra cosa.
Aquí debemos implementar una mecánica que seguramente todos habremos visto en muchos
juegos, el de la invulnerabilidad.
A lo que me refiero es al lapso de tiempo durante el cual el jugador no puede recibir daño
una vez que este ya ha recibido daño. De esta manera, cuando el jugador reciba daño tendrán
que pasar unos segundos hasta que pueda volver a recibir daño, de esta manera evitamos así
que el jugador pierda demasiadas vidas de golpe y que disponga de un tiempo para
recuperarse.
118
La implementación es sencilla, de hecho, es igual que cualquier otra función de comprobar
el tiempo transcurrido, ya que la manera a implementar esta mecánica es mediante el control
del tiempo con una variable. Si acabamos de recibir daño, es decir, la variable estaba a cero,
entonces registramos el segundo cuando recibimos daño y le restamos una vida al jugador.
En caso contrario, se comprueba cuanto tiempo ha pasado y si no ha pasado el tiempo
necesario no se resta una vida.
8.4. Mecánicas extra
Hasta este punto ya tendría lo que sería una pequeña demo del juego funcional con todas las
mecánicas básicas del mismo, es decir, todas aquellas que son si o si necesarias para que se
pueda jugar. Estas mecánicas consisten en las siguientes:
• Mecánicas del jugador: salto, disparo, movimiento, caída y recibir daño.
• Colisiones entre sprites.
• Colisiones con el mapa.
• Cambio de nivel.
• IA de un enemigo.
• Reinicio del juego.
• Control de tiempo.
A partir de ahora, se van a implementar todas las mecánicas extras que sean posibles dentro
del tiempo disponible para ello, que harán que el juego sea más divertido, pero que no son
estrictamente necesarias para que el juego funcione de tal manera que si hay alguna que por
cualquier razón no se puede integrar en el proyecto este no se vea afectado.
8.4.1. IA segundo tipo de enemigos
Teniendo solo un tipo de enemigo en el juego seguramente hará que la gente que lo juegue
se acabe cansando de enfrentarse siempre a lo mismo. Es por eso, por lo que considero que
la primera mecánica a añadir sea un segundo tipo de enemigo diferente y de esta manera,
añadir una mayor variedad al videojuego. Me gustaría añadir un par más de tipos enemigos
distintos, pero seguramente el tiempo no me lo permita. Además de que hay unas cuantas
más mecánicas que deseo implementar también.
119
Al igual que el primer enemigo, nos vamos a fijar de nuevo en uno de los enemigos que
aparecen en Master of Darkness. Este enemigo, al igual que el perro, se desplaza
continuamente dentro de una zona previamente indicada. Esto correspondería a su primer
estado de comportamiento, el cual, al finalizar, cambia a su siguiente comportamiento donde
se quedará quieto durante unos segundos. Durante este tiempo, la entidad lo que realiza es
cambiar consecutivamente su sprite de derecha a izquierda para dar así la impresión de que
el enemigo está mirando de un lado a otro, tras pasar los segundos necesarios, disparará hacia
una dirección que intuyo que corresponderá a la dirección donde se encuentra el jugador.
Como vemos, la IA es un poco más compleja pero tampoco en exceso, el mayor problema
que podemos llegar a tener es gestionar el comportamiento de las balas que dispara cada
enemigo, porque es posible que deseemos tener este tipo de enemigo repetido más de una
vez en el nivel y tenemos que hacer que la bala sea independiente de cada enemigo. En
cuanto a las animaciones del mismo, esto es algo que ya veremos más adelante cuando nos
encontramos en el apartado de añadir contenido al juego.
Para poder tener claramente diferenciados los dos tipos de enemigos que hay en el juego, se
utilizará el término de “básicos” para hacer referencia al primer tipo de enemigo que se
implementó anteriormente y el término “avanzado” para el siguiente tipo de enemigo que se
va a implementar a continuación.
La estructura para crear los enemigos avanzados va ser prácticamente la misma que hemos
implementado para los básicos. Por lo tanto, tendremos que crear una función que recorra
todos los enemigos avanzados que existen en el nivel y apuntar con los dos registros índices
a las estructuras de datos de cada uno de ellos. Esto es exactamente igual que la función de
actualizar los enemigos básicos, simplemente hay que cambiar la función a la que se llama
para hacer todo lo relacionado con ellos. Se podría haber implementado una función general
que sirva tanto para un enemigo como otro, pero no dispongo del tiempo para realizar tal
tarea actualmente.
De nuevo, dentro del enemigo avanzado, seguirá siendo una estructura bastante parecida a
la de los básicos. Primero comprobamos si la entidad esta viva, para ello podemos utilizar la
misma función que usamos para los básicos, la denominada “checkEstadoEnemigo” que
comprobaba si había que borrar al enemigo pasado en el puntero IX en función del valor de
su variable de su colisión y como se utilizan los registros índices, podemos usar la función
de manera general para todos los tipos de enemigos.
120
Por otro lado, tendremos también una variable para controlar el comportamiento de la
entidad, al igual que los básicos, en función de su valor hará un estado u otro. El primer
estado, el de movimiento, utilizaremos las mismas funciones que el primer tipo de enemigo
ya que va a hacer lo mismo, pero a una velocidad de movimiento reducida que el primero.
Por eso, es necesario añadir una variable extra dentro de nuestra estructura de variables extra
de los enemigos, la cual nos proporcionará información sobre la velocidad a la queremos
que vaya el enemigo. Con esto, solo tenemos que llamar a la función que controla el estado
de movimiento de la entidad y ya tendríamos este primer comportamiento funcionando.
Hay que tener cuidado cuando modificamos cualquiera de las estructuras de datos que
tenemos, ya que si añadimos o quitamos un dato su tamaño cambiará y por lo tanto, a la hora
de copiar todos los datos iniciales que tenemos en ROM al struct, tenemos que cambiar el
tamaño que se pasa al registro BC que requiera la instrucción “ldir”. Este es un problema
que he me ha surgido varias veces durante el desarrollo del proyecto, ya que muchas veces
olvidaba donde tenía que realizar las modificaciones correspondientes y el juego empezaba
a fallar ya que se asignaban valores erróneos a las variables de los structs.
Para evitar estos problemas recomiendo utilizar constantes, que en Z80 sería definiciones,
que representen el tamaño total a copiar a nuestra RAM. De tal manera que, si hacemos una
modificación, solo tenemos que cambiar el valor aquí y no en todos los sitios donde lo
utilicemos.
Siguiendo con el tema de los enemigos avanzados, solo nos quedaría implementar el estado
de ataque del mismo. Al igual que siempre, el tiempo lo controlamos de la misma forma que
hemos realizado hasta ahora. Dentro del comportamiento de ataque, el enemigo lo que va a
realizar es comprobar a que dirección disparar, para ello, durante los segundos que dure este
estado, el enemigo alternará entre los dos sprites que disponga para observar hacia una
dirección y a la otra. Esto se implementará más adelante cuando se estén realizando las
animaciones. Por ahora, solo es necesario saber que durante el comportamiento se alternarán
los sprites.
121
Cuando finalice el estado, se comprobará la posición X del jugador respecto al enemigo
avanzado, de tal manera, que, si esta posición es mayor, se realizará un disparo hacia la
derecha y, en caso contrario, hacia el lado izquierdo. Sin embargo, para poder efectuar el
disparo, primero hay que indicar la bala que se va a utilizar, ya que tendremos que tener
varias entidades de bala para poder conseguir que todas ellas se puedan ver a la vez por
pantalla y funcionen de manera independiente.
Por lo tanto, justo cuando se acabe el comportamiento, necesitaremos llamar a una función
que nos prepare la entidad de la bala que se va a disparar.
Figura 90. Función para preparar la bala del enemigo para ser disparada.
Fuente: Elaboración propia.
Como se puede ver en la figura, básicamente lo que estamos haciendo es acceder a las dos
zonas de datos que necesita la bala que va a ser disparada. Para ello, previamente deberemos
haber creado las balas que nosotros sepamos que vaya a haber. En mi caso, he creado un
máximo de tres entidades de bala, ya que nunca se va a dar el caso en el que se vayan a
dibujar más de tres por pantalla.
122
Tras apuntar a los datos de la bala, hacemos una simple función de reset de la misma que lo
que realiza es ponerla justo en la posición exacta donde se encuentre el enemigo, más o
menos a como se realizó para el disparo del jugador. Además, será dentro de esta función
donde comprobaremos la posición del jugador para saber dónde colocar la bala justo antes
de ser disparada.
Figura 91. Reseteo de la posición de la bala en función de la posición y dirección del enemigo.
Fuente: Elaboración propia.
Ahora llega la parte de controlar el movimiento de la bala. El enemigo ha terminado su
estado de ataque, ha efectuado su disparo y ha vuelto a su comportamiento anterior. Ahora
hay que mover la bala disparada y comprobar si colisiona en algún momento con alguna
entidad o con algún elemento del mapa.
Este control de movimiento no es necesario que lo explique ya que el proceso es
prácticamente a como se realiza con las balas del jugador. Indicamos el tiempo que queremos
que se mueva y cuando este acabe o colisione se llama a la función de borrado de sprites.
123
Sin embargo, hay que tener en cuenta que cuando el jugador muere, la bala se va a borrar
también, pero, cuando el enemigo muera no queremos que esto ocurra. Por lo tanto, tenemos
que seguir llamando a la función encargada de controlar el movimiento de la bala, aunque
el enemigo avanzado haya muerto.
Además de esto, tenemos que hacer que la bala haga daño al jugador cuando esta colisione
con él. Para ello, creamos una función que solo se llamará si hay enemigos avanzados en el
nivel actual, ya que son los únicos capaces de efectuar un disparo por ahora.
Figura 92. Función para comprobar la colisión de la bala de los enemigos con el jugador.
Fuente: Elaboración propia.
Como curiosidad, cuando he implementado está función y la he probado, he observado que
las balas de los enemigos no se borraban, sino que se quedaban inmóviles justo donde
colisionaban o justo cuando había acabado el tiempo de disparo. Sin embargo, me he dado
cuenta que la bala del jugador si se borraba, además lo hacía justamente cuando tenía que
borrarse cualquiera de las otras balas que había en el nivel.
124
Esto por su puesto no era ninguna casualidad, por lo que tras pasar un buen rato depurando
para buscar el error me di cuenta que este residía en la llamada de las etiquetas de salto. Es
decir, dentro de la función de controlar el movimiento de la bala, justo en la condición de
salto para comprobar si hay que borrar la bala o no por si ha colisionado, llamaba a una
etiqueta que no lo había puesto como local y que, casualmente, recibía el mismo nombre que
otra etiqueta colocada en la función de borrado de la bala del jugador.
Como utilizaba la instrucción “jp” para hacer la condición de salto y no “jr” podía saltar
hasta esa dirección, ya que en la SMS todo el código está escrito de manera continua y no
importa que nos encontremos en diferentes ficheros, podemos acceder a todo lo que se
encuentre en ficheros distintos, todo lo contrario, a como ocurre en Amstrad CPC que es el
primer computador donde empecé a trabajar con Z80.
8.4.2. Puzles
Tras implementar el segundo tipo de enemigos, voy a realizar la implementación de unos
puzles muy sencillos para los niveles del juego. La idea es dar un poco más de juego a los
niveles y que no se resuman simplemente a matar a los enemigos y avanzar. Les he dado el
nombre de puzles, pero realmente no lo son, solo es una forma diferente de avanzar por el
nivel.
Mi intención era la de implementar estos “puzles” mediante unas palancas y una puerta, de
tal manera que el jugador tuviese que colocar la combinación correcta de las palancas para
abrir la puerta. Sin embargo, tras estar buscando sprites gratuitos de palancas, no he
encontrado ninguno que me convenciese realmente y los que si lo hacían eran demasiado
grandes y al reducir su tamaño se perdía calidad. Teniendo en cuenta de que no me sobra el
tiempo, he tenido que buscar una alternativa.
He decidido sustituir las palancas por llaves, algo más simple pero que al fin y al cabo cumple
con mi objetivo de dar algo más de complejidad a los niveles. La idea a implementar es
bastante sencilla, si en el nivel se encuentra alguna llave, la puerta no se abrirá a no ser que
se haya recogido la llave, en caso contrario, si el jugador se encuentra dónde está la puerta y
pulsa la tecla “alt” se cambiará el nivel. Vamos a ver como implementar todo esto.
125
Lo primero que tenemos que hacer es crear el objeto de puertas, es decir, crear una instancia
nueva de game_objects para las puertas. La razón de esto es porque de las puertas vamos a
necesitar bastantes datos que se encuentran en el struct de game_objects, aunque haya unos
pocos que no sean necesarios. Realmente, siempre que necesitemos que una entidad se dibuje
por pantalla, vamos a necesitar que esta sea una instancia de game_objects. Por lo tanto, para
las llaves también tendremos que hacer una instancia de esta estructura.
Una vez creado el objeto puerta, al igual que con todos los game_objects, haremos una
función para actualizar todo lo relacionado con la puerta, que corresponderá a dibujarla y
comprobar si el jugador puede cambiar de nivel.
Necesitaremos pues otra función encargada de comprobar si hay que cambiar de nivel o no.
Para ello, utilizamos la función de checkColision para comprobar la colisión entre el jugador
y la puerta. Si se produce colisión, entonces habrá que comprobar si hay llaves en el nivel.
Si no hay llaves entonces salimos de la función, ya no hay que realizar nada más. En caso
contrario, debemos comprobar si el jugador lleva la llave encima. Para ello, podemos crear
una variable extra para el jugador que se utilice para almacenar el valor que indica si se ha
código la llave o no (cero o uno).
En caso de que la lleve encima, entonces se comprueba si se pulsa la tecla correspondiente
y si esto ocurre, realizamos todo lo necesario para el cambio de nivel.
126
Figura 93. Función para efectuar el cambio de nivel con las puertas.
Fuente: Elaboración propia.
8.4.3. Contador de tiempo
La siguiente mecánica que me gustaría realizar es una que podemos observar en
prácticamente todos los juegos clásicos de plataformas de la Sega Master System, el
contador de tiempo por nivel. Básicamente, esto es simplemente un reloj que indica el tiempo
que queda para terminar el nivel correspondiente, si su tiempo se acaba entonces el juego
termina y se tiene que empezar de nuevo. Vamos a ver cómo realizar la implementación de
este reloj en el videojuego.
A simple vista, puede parecer que realizar esta mecánica puede ser complicado, de hecho,
yo lo pensaba justo cuando empecé a investigar para poder realizarla. Sin embargo,
descubriremos que no es tan complicado como parece y que realmente su implementación
es bastante parecida a como se deberán hacer las animaciones del juego.
127
Lo primero que hay que saber es que los números que se pueden ver por la pantalla de la
consola, los que representan el valor del tiempo, son un sprite y como cualquier sprite
necesitamos cargar previamente los tiles del mismo para posteriormente dibujar los valores
por pantalla.
Figura 94. Tilesheet de ejemplo que contiene los números a dibujar.
Fuente: Blackwolfdave.
En la 94 tendríamos un ejemplo sobre cómo sería el tilesheet del cual partiríamos para
obtener los tiles. El creador del tilesheet es Blackwolfdave y lo he obtenido de la página
OpenGameArt [16].
Como vemos, la imagen solo contiene los números a utilizar para representar el reloj de
tiempo. La idea sería conseguir tener una imagen con solo los números que necesitemos, que
en nuestro caso serían los valores del cero al nueve. Por lo tanto, mediante cualquier
programa de edición de imagen, realizamos los ajustes necesarios para conseguir tener los
números juntos en una misma fila y, una vez hecho esto, los cargamos al programa de
BMP2TILE para obtener los tiles correspondientes.
Con nuestros tiles cargados, tendremos que crear una entidad de game_objects para el reloj
de tiempo. Dentro de los datos de esta entidad, en la variable que representa el ancho del
sprite a dibujar, debemos indicar la cantidad de números que queremos mostrar por pantalla,
en nuestro caso, queremos dibujar un valor de tres dígitos, por lo que aquí ponemos el valor
tres.
128
¡Cuidado! Esto solo funcionará si los valores de nuestros números tienen un ancho de ocho
píxeles, es decir, un tile. Si por el contrario fuesen más grandes, el valor de ancho del sprite
a dibujar será distinto, por lo que el valor variará en función del ancho de cada uno de los
números de la imagen de la cual hemos partido. Por ejemplo, si en nuestra imagen los valores
ocupan dieciséis píxeles, entonces en el ancho del sprite que tendremos que poner será seis,
ya que cada valor numérico ocupará un total de dos tiles.
El valor del índice de tile del sprite es el que va a representar cada uno de los números
de la imagen que nosotros cargamos previamente, por lo que si queremos cambiar uno de
los valores del número para, por ejemplo, hacer que el reloj de tiempo vaya decrementándose
de uno en uno como si fuese un reloj de verdad, tendremos que cambiar solamente los
charcodes del sprite ya que este no se va a mover su posición en ningún momento por lo que
no es necesario estar actualizando sus posiciones X e Y en el buffer.
La cuestión es que, si la imagen la hemos construido bien y tenemos los valores ordenados
en una fila de menor a mayor, eso querrá decir que el primer índice de tile representará el
número cero, el segundo el número uno y así hasta el último valor, siempre teniendo en
cuenta que el primer índice de tile dependerá de la posición donde hayamos cargado los tiles
en memoria.
Como vamos a tener que estar cambiando estos valores continuamente de manera
automática, lo lógico sería que tuviésemos almacenado en una variable el número que
queremos representar en todo momento y así saber que índice de tile hay que cargar en cada
posición de los charcodes del buffer. La variable en cuestión será una variable que ocupará
tantos bytes como números a dibujar. En nuestro caso serán tres bytes ya que el número a
representar contendrá tres dígitos en total.
Figura 95. Definición de una variable de más de dos bytes con WLADX.
Fuente: Elaboración propia.
129
Con esta variable creada, creamos una función que sirva para inicializar dicha variable al
valor que nosotros queramos, en nuestro caso será 300. Tras inicializar, llamaremos a una
función que servirá para dibujar el sprite con el tamaño que habremos indicado, pero aun sin
haber indicado los tiles que queremos utilizar, es decir, los números que queremos que se
visualicen.
Figura 96. Función para inicializar el reloj de tiempo al valor 300.
Fuente: Elaboración propia.
Ahora tenemos que realizar una función que con a partir de ese valor que estamos
almacenado en la variable de tres bytes, conseguir representar un contador de tiempo que
vaya desde el valor 300 hasta el 000.
Para implementar el decremento del reloj esto lo podemos hacer de manera sencilla mediante
nuestro contador de tiempo del juego, que recordemos que cuenta un segundo cada vez que
se dibujan 60 frames. Por lo que cada vez que pase un segundo decrementamos en uno la
variable “contador_tiempo” para así que el reloj disminuya como si fuese uno real.
A continuación, debemos implementar la función que se encargue de cambiar los valores de
índices de tile a sus valores correspondientes en función del valor que haya en nuestra
variable de 3 bytes. Para ello, tendremos que hacer un bucle, mediante el cual, cada vez que
se entre en el mismo, se comprueba el valor de la variable que contiene los 3 valores del
contador de tiempo, que según en que iteracción del bucle nos encontremos, corresponderá
a las centenas, decenas o unidades del número.
130
Si este valor es 0, ya tendremos en HL nuestra dirección a los números de los tiles que
representan cada número a dibujar, como es el 0 que hay que dibujar y el primer tile ya
representa este valor, no hay que hacer nada más para obtener el valor a dibujar. Si fuese
distinto a 0, se realiza otro bucle que accede hasta la dirección de memoria correspondiente
para obtener el tile correcto que representa dicho número. Por ejemplo, si fuese el número
9, se avanza 9 posiciones de memoria desde la primera dirección a la que apuntaba la etiqueta
con la información de todos los charcodes.
Tras esto, independientemente del valor a representar, ya tenemos en HL el número del tile
a dibujar, por lo que solo hay que cargarlo en la posición correcta del buffer para
posteriormente copiarlo al SAT. Para ello, como hemos creado el tiempo como un
game_object, tendremos almacenado la información de la posición donde hay que copiar el
valor en el buffer en nuestra variable “cc”.
Al realizar la copia, se avanzan dos posiciones en la memoria para apuntar a dirección del
buffer del siguiente índice de tile a copiar y se almacenan en los datos del game_object del
tiempo, de tal manera que la siguiente vez que se entre al bucle ya tendremos la dirección de
a donde hay que copiar el tile del sprite (véase figura 97).
131
Figura 97. Carga del tile correspondiente del sprite de los números en la posición correcta del buffer.
Fuente: Elaboración propia.
Con todo esto, llamamos todo el rato a la función que comprueba si ha pasado un segundo o
no. Si ha transcurrido ese segundo, entonces decrementamos el reloj. Al final de esta función
siempre se llamará a la función encargada de copiar los tiles correspondientes a la posición
del buffer correcta.
En la siguiente figura, podemos ver como se dibujaría el reloj de tiempo en la esquina
derecha de la pantalla y, en la esquina izquierda, el contador de vidas del jugador, que se
implementaría de manera parecida al tiempo, simplemente cambiando la cantidad de
números a dibujar y decrementado este valor cuando el jugador reciba daño.
132
Figura 98. Dibujado del reloj de tiempo y el contador de vidas en Invasion.
Fuente: Elaboración propia.
8.4.4. Menú
Anteriormente, en el apartado de control de caída del jugador, se hizo mención a una variable
llamada “mundo_state”. Esta variable se describió brevemente por encima como una
variable utilizada para controlar en todo momento el estado del juego, es decir, si se estaba
jugando, el jugador había muerto o si estamos en el menú. Vamos a ver exactamente cómo
funciona esta variable y cómo podemos hacer para controlar de manera muy simple en qué
estado del juego se encuentra el jugador en todo momento.
133
Considero que es necesario saber durante toda la ejecución del juego en qué estado del juego
se encuentra el jugador, de esta manera, podemos desplazar la ejecución del código a la zona
que sea necesaria donde se realizará todo lo relacionado con el estado correspondiente. Por
ejemplo, para el estado de Menú, que es el que nos concierne ahora mismo, como es un
estado al cual solo se accederá al arrancar la consola, tendremos que gestionar la
inicialización del VDP, cargar el tilemap del fondo que hayamos creado para el menú y
habilitar la visualización de pantalla. Además, si en un futuro quisiéramos realizar más cosas
dentro de este estado, como que se realice algún efecto de animación, o que hayan varias
opciones en el menú, todo esto tendremos que implementarlo en esta zona.
Por todo esto, es favorable que tengamos nuestro código claramente diferenciado para saber
que parte corresponde al menú, a la muerte o a la ejecución del juego. Para ello, creamos
una variable que controle todo esto, de tal manera que, en función del valor que almacene
en el momento de entrar a la función controlar el estado del juego, el código saltará a una
zona o otra. Esta variable la llamaremos “mundo_state”.
Como queremos que haya tres estados del juego (muerte, juego y menú) almacenará hasta 3
valores distintos. Asignaremos el valor 1 al estado de juego y colocaremos la función de
comprobar el valor de esta variable justo al principio de nuestro bucle principal del
videojuego, de tal manera que sea siempre lo primero que se compruebe. Si almacena el
valor 1, entonces saldremos de la función y se continuará con la siguiente función del bucle
principal, ya que esto significará que estamos en el estado del juego y por lo tanto la
ejecución del código debe seguir como siempre.
Si en caso contrario, no se almacena el valor 1 y almacena cualquiera de los otros dos valores
(0 o 2) querrá decir que estaremos o en el menú o en la pantalla de muerte respectivamente.
En ambos casos, saltaremos a la zona de código encargada de gestionar cada estado y, dentro
de ella, primero hará lo que tenga que hacer para preparar dicho estado.
En el caso del menú corresponderá a inicializar el VDP, cargar el tilemap del fondo del menú
y habilitar la pantalla. Tras esto, se pausará el juego mediante la función de pausa que
implementamos anteriormente, que recordemos que lo que realizaba esta pausa era que la
ejecución del juego se quedaba en un bucle infinito recogiendo la entrada por teclado del
jugador hasta que este pulsase la tecla “alt”.
134
En el caso de la muerte, deberá primero cargar el fondo de pantalla de muerte, pausar el
juego y, finalmente, después de que el jugador pulse la tecla “alt”, llamar a la función de
reinicio del juego que recordemos que también la hemos implementado anteriormente.
En ambos estados, al finalizar, se cambia la variable “mundo_state” al valor 1 para que se
empiece la ejecución de la pantalla de juego.
8.4.5. Animaciones
Como tampoco me queda mucho tiempo para poder entregar el proyecto, debo ir terminando
ya la parte de mecánicas y pasar a la sección de contenido. Por lo tanto, la última mecánica
que voy a implementar es la relacionada con las animaciones de los personajes del juego, ya
sea el protagonista o los enemigos. La idea es averiguar la forma de implementar estas
animaciones y no meter todas las animaciones que vaya a tener el videojuego, ya que eso se
hará en la sección de contenido.
El primer problema que nos vamos a encontrar será a la hora de cargar los tiles de cada una
de las animaciones, ya que lo lógico sería que, si ya tenemos cargado en memoria los tiles
del sprite de un enemigo mirando a la derecha quieto, por ejemplo, no deberíamos que tener
que cargar los tiles de lo mismo, pero mirando para el lado contrario ya que estaríamos
ocupando espacio innecesario que se podría utilizar para cargar cualquier otro sprite distinto.
La mecánica de poder girar de un lado a otro un sprite debería poder realizarse durante la
ejecución del juego, sin embargo, hasta este momento no he conseguido averiguar la forma
de hacerlo.
La única información que he encontrado relacionada con este aspecto, es la posibilidad de
voltear los tiles del tilemap que sean simétricos para así ahorrar espacio y tener mayor
variedad en los gráficos de los mapas. Esto se controla con los bits 0 y 1 del segundo byte
de cada una de las posiciones del tilemap. De hecho, he comprobado que esto se realiza de
manera automática, de tal manera que si al crear nuestro mapa, dibujamos un tile idéntico a
otro ya dibujado, pero girado hacia cualquier dirección, la SMS detectará de manera
automática que es el mismo tile y modificará los bits correspondientes.
A pesar de todo, este no funciona de manera igual para los tiles de los sprites y desconozco
por ahora como implementarlo. Por lo que no queda otra opción que cargar todos los tiles
necesarios sean simétricos a otros o no.
135
Las animaciones funcionarán de manera parecida a cómo funciona el reloj de tiempo que
implementamos anteriormente. Eso quiere decir que para poder cambiar de un sprite a otro
lo único que habría que hacer es modificar los índices de tile que se están cargando al SAT
a los correspondientes con el sprite que queramos que se modifique.
La idea es que todos los sprites que correspondan a una misma entidad del juego tengan el
mismo tamaño, si no, además de cambiar los charcodes tendremos que volver a cargar las
posiciones verticales y horizontales del mismo y así como, modificar su ancho o alto. En mi
caso en concreto, el sprite tiene el mismo tamaño para todas las animaciones, por lo que solo
es necesario modificar los índices de tile.
Lo que tenemos que saber es cuando queremos que se cambien estos valores, por ejemplo,
si queremos hacer que el sprite del jugador se cambie para mirar hacia la derecha o a la
izquierda en función de a la dirección que se esté moviendo, lo lógico sería que realicemos
el cambio justo cuando entremos a la función de actualizar todo lo relacionado con el
jugador.
Figura 99. Modificación del sprite a mostrar por pantalla mediante los índices de tile.
Fuente: Elaboración propia.
Al realizar este cambio justo al entrar en la función del jugador, lo que conseguimos es que
dicho cambio se mantenga en caso de que no hayamos cargado posteriormente cualquier
otro sprite relacionado con el jugador. Lo que quiero decir es que, si por ejemplo el jugador
salta, tendremos que cargar el sprite de salto y como este sprite se cargará después del sprite
de estar quieto, entonces el que se verá por pantalla será este último que hemos cargado. Si
no se carga ningún otro sprite, sabemos que siempre se estará cargando el de mirar a la
izquierda o a la derecha.
136
El verdadero problema llega con las animaciones que deben estar actualizándose
continuamente durante un tiempo determinado, como, por ejemplo, la animación de correr.
La cosa es que no consigo hacer que estas animaciones se cambien a una velocidad correcta
para que la animación parezca real.
Primero he intentado hacer el cambio de sprites mediante una variable que modificaba su
valor cada vez que se cargaba un sprite u otro, pero esto hace que las animaciones cambien
cada ciclo y va demasiado rápido. La segunda prueba ha sido utilizando el contador de
frames, intentando asi que el sprite de animación cambiase justo cuando se han dibujado 30
frames, pero ocurría que las animaciones se dibujasen demasiado lentas y si incrementado
el valor de frames para cambiar se hacían demasiado rápidas.
La cuestión es que tengo que averiguar la forma para poder hacer cuentas con decimales, es
decir, hasta ahora siempre utilizo números enteros para todo: los incrementos para controlar
la velocidad de movimiento son con números enteros al igual que el contador de tiempo y
muchos otros más, pero el caso es que no sé cómo hacer para que en Z80 se utilicen números
decimales en vez de los enteros y, desgraciadamente, no tengo tiempo suficiente para
averiguar la forma para implementarlo. Debo intentar entregar algo jugable, aunque sea muy
simple para esta primera entrega del proyecto por lo que debo descartar animaciones como
las de correr, tanto como para el jugador como para los enemigos.
8.5. Contenido
Dado que dispongo de un tiempo no mucho mayor de una semana para poder terminar el
juego, tengo que ir empezando ya a introducir contenido al mismo. A partir de este punto no
voy a implementar más mecánicas, todo lo que voy a hacer es buscar contenido como sprites
para las entidades de mi juego (personaje, enemigos, llaves etc) y así como, tiles para crear
mapas. Además de esto, intentaré crear una pantalla de muerte y de menú para poder
dibujarlos en sus correspondientes estados.
137
8.5.1. Búsqueda de materiales
Lo primero que voy a hacer es buscar sprites gratuitos por internet para poder usarlos en mi
proyecto. Para ello, he buscado en páginas web como OpenGameArt o Ithc.io. En ambas
podemos encontrar todo tipo de recursos para usar en nuestros proyectos, lo único que hay
que fijarse es si el autor del material a descargar nos permite usar su contenido para
proyectos no comerciales o personales y si es necesario mencionarle o no. Por mi parte,
todo material que utilice que no sea propio, mencionaré a su autor.
Como no soy un buen diseñador de sprites y no se hacer diseños de personajes guais para mi
personaje, además de que no dispongo el tiempo para hacerlo, me encuentro un problema
que me pasará con el resto de sprites que debo buscar y es que el tamaño de estos, por lo
general, suele ser mucho mayor que lo que yo necesito, ya que estos son creados para
programas modernos de hoy en día, como por ejemplo Unity, y tienen un tamaño mucho
mayor de los 32x32 píxeles como máximo que yo podría aceptar.
El motivo por el que no puedo aceptar dimensiones de sprites superiores a 32x32 pixeles es,
principalmente, por el SAT. Esta tabla, que es la encargada de mostrar los sprites por
pantalla, solo tiene espacio para 256 bytes, de los cuales, 40 son para las posiciones verticales
y 80 para los índices de tiles y posiciones horizontales, los 64 bytes restante son libres.
Por lo tanto, si cargo sprites demasiado grandes, al final no voy a tener capacidad para poder
dibujar muchos sprites por pantalla. Por ejemplo, digamos que cargo un sprite con las
dimensiones indicadas anteriormente, 32x32. Esto querría decir que dicho sprite estaría
formado de 4 filas por 4 columnas, lo que daría un total de 16 bytes para la zona de posiciones
verticales del SAT y un total de 32 bytes para la zona de posiciones horizontales y charcodes.
Esto querría decir que, si quisiese cargar más sprites del mismo tamaño, solo podría tener
espacio para un sprite más, lo cual sería un poco pobre.
Es por eso por lo que he intentado reducir de manera manual el tamaño de algunos de estos
sprites que buscaba, pero al reducirlos se perdía mucha calidad hasta tal punto de verse
demasiado píxelados. He buscado también programas que hagan una reducción de estos,
pero prácticamente ninguno conseguía hacer una reducción sin perder calidad. El único que
más o menos permite aumentar o reducir el tamaño de imágenes sin perder mucha calidad
es uno llamado “resizemypicture.com”, el cual lo he utilizado un par de veces para hacer
algunas modificaciones de algunos assets.
138
Por si no fuera suficiente, con el personaje del jugador tengo un problema adicional y es que
deseo que este sprite contenga un arma en la mano para poder simular que esta la está
disparando, por lo que es más complicado de encontrar un sprite que cumple con lo que
deseo y además tenga el tamaño justo.
El caso es que, tras buscar durante un buen tiempo, he encontrado un sprite para el jugador
que me podría servir, el cual se muestra en la siguiente figura y su autor es Blue Yeti Studios.
También he buscado sprites para el resto de entidades del juego. En el apartado de Diseño
del juego de este mismo documento se pueden encontrar los diseños definitivos para cada
una de estas entidades.
Figura 100. Tilesheet con sprites para el protagonista.
Fuente: Blue Yeti Studios [4].
Otro de los problemas que me encuentro es el de los colores. Hace tiempo, para hacer
pruebas de las mecánicas utilice un par de sprites y tiles descargados de la página
“thespriteresource” la cual contiene un montón de materiales para SMS y, por lo tanto, ya
están preparados para ser directamente cargados a la consola. Aun así, en el editor de
imágenes que yo utilizo, creé una paleta de colores basada en la paleta de la SMS donde
estaban todos los colores que se podían utilizar en la consola, la cual contenía un total de 64
colores.
La idea era utilizar dicha paleta para convertir el sprite o el mapa, ya preparado para ser
cargado en BMP2TILE, a una imagen indexada que contenga solo los colores posibles de la
consola. Sin embargo, al hacer dicha conversión se utilizaban más colores de los permitidos,
ya que en la consola solo pueden mostrarse un total de 16 colores por pantalla.
139
Por lo tanto, he tenido que crear una paleta nueva que solo contiene 16 colores en total, todos
ellos sacados de la paleta anterior de 64. He intentado que dichos colores sean los que más
se parecen al sprite original y así obtener más o menos un aspecto parecido.
8.5.2. Diseño de niveles
Para hacer el diseño de los mapas he utilizado el programa Tiled. La razón de ello es debido
a que Tiled permite crear mapas de un tamaño determinado, por lo que se pueden crear mapas
que tengan justo el tamaño que tiene la pantalla de la Master System. Además, se puede
ajustar el tamaño que tendrá cada uno de los tiles del mapa a 8x8 píxeles y, así como, cargar
todos los assets que necesito directamente al programa y dividirlos en rejillas de 8x8 pixeles
para poder obtener así los tiles a dibujar en el mapa.
Hay que tener cuidado a la hora de diseñar el mapa, ya que cuantos más tiles distintos haya
dibujados en el mismo, mayor será el espacio que este ocupe en la consola. Una vez
terminado su diseño, lo podemos exportar directamente a imagen. Esto será necesario, ya
que antes de poder cargarlos a la consola, debemos realizar el mismo proceso que con los
sprites, el de reducir sus colores, por lo que creamos otra paleta de colores para el mapa y se
convierte a imagen indexada.
8.5.3. Diseño de la pantalla de muerte y menú
Necesito hacer unos diseños de fondos, aunque sean muy simple, para el menú y la pantalla
de muerte, de tal manera que cuando dentro del juego se salte a uno de estos estados, se
dibuje dicho fondo sobre el mapa hasta que se vuelva al estado de juego. Para ello, he creado
unos diseños muy simples con GIMP.
Figura 101. Fondo para pantalla de muerte.
Fuente: Elaboración propia.
140
Figura 102. Fondo para la pantalla del menú.
Fuente: Elaboración propia.
Sin embargo, al cargar estos fondos al BMP2TILE me doy cuenta de que el tamaño que
ocupan es demasiado excesivo, llegando a ocupar hasta 30 tiles cada uno, lo cual no es
normal. La razón es el ancho de cada uno de los textos dibujados, seguramente ocuparán
unas dimensiones demasiado grandes y para cada letra se utilizan hasta 10 tiles. Es por eso
por lo que es mejor para la próxima vez hacer los diseños en el Tiled donde se sabe
exactamente el tamaño que ocupa cada tile.
Al cargar los dos fondos, me surge un problema y es por el cual no voy a poder entregar el
proyecto a tiempo para la primera entrega. Este problema es el del espacio, me doy cuenta
que no tengo sitio para cargar los dos fondos. Esto es un cúmulo de cosas mal gestionadas
como cargar sprites idénticos, pero simplemente rotados al lado contrario o aspectos como
el espacio que ocupan estos últimos fondos creados por GIMP [21].
La cuestión es que, en muchos de los mapas, hay tiles que se repiten los mimos valores
muchas veces e incluso hay ocasiones en los que hay filas donde solo hay ceros y esto ocupa
un espacio innecesario. Por lo que debo averiguar la forma de comprimir el espacio que
ocupan dichos mapas mediante alguna aplicación creada por mí que lea dichos ficheros y
reduzca los bytes a cargar.
También debo averiguar la forma para solo cargar una única vez un sprite que es simétrico
y luego este girarlo durante la ejecución del videojuego, en vez de cargar el mismo sprite
varias veces. Con todo esto, yo creo que debería bastar para poder obtener el espacio
necesario para poder cargar un total de 10 mapas para mi juego.
141
8.6. Reducción del espacio ocupado en ROM
Debido a los últimos problemas que me he encontrado, no me va ser posible entregar el
proyecto en junio ya que no dispongo del tiempo suficiente para resolver estos problemas y
entregar un producto acabado.
Por lo tanto, en el tiempo que me queda hasta la entrega de septiembre, debo intentar resolver
estos problemas, los cuales están principalmente relacionados con el espacio disponible
en la consola, concretamente con el espacio ROM.
Existe un fichero de extensión “.sym” el cual se genera con el WLA DX al escribir “-S” a la
hora de realizar el linkado del proyecto. Este fichero, además de indicar en que posición se
encuentra cada etiqueta del programa, cosa que es bastante útil para depurar en caso de
producirse algún error, también nos permite saber el espacio que ocupa cada etiqueta o
definición del mismo. Los valores escritos están en hexadecimal, pero representan el espacio
que ocupan en bytes.
Tabla 1. Etiquetas/funciones que mayor espacio ocupan en el proyecto.
Definición/Etiqueta Tamaño que ocupa (Bytes)
Tiles de los sprites 10.016 (entre 64 y 384 bytes por sprite)
Tilemaps de los mapas 6.144 (1.536 bytes x 4 mapas)
Tiles del menú/muerte 4.736 (2.368 bytes para cada uno)
Tiles del mapa 1 800
Tiles del mapa2 576
Función "cargaAssetsSprite" 345
Buffer del SAT 256
Struct de Game_Objects 187 (17 bytes por game_object)
Struct de levels 16 bytes (8 bytes por nivel)
Struct de variables enemigos 16 bytes (8 bytes por enemigo)
142
Observando la tabla anterior, se puede apreciar que evidentemente lo que más espacio ocupa
actualmente en el proyecto son los tilemaps de los mapas, es decir, los valores que indican
que tiles se van a dibujar en que posición de pantalla y aquellas características extras que
necesiten como la dirección del tile, si estarán encima de los sprites etc.
Aun así, los mapas no es lo que más espacio ocupa, si no los tiles de los sprites. Esto se debe
básicamente a que cometo el error de cargar 2 veces el “mismo” sprite pero girado hacia al
lado contrario al cual había cargado el primero. Esto está realizado de manera equivocada,
ya que lo lógico sería carga los tiles del sprite hacia una dirección y luego girarlo hacia la
dirección contraria cuando sea necesario durante la ejecución del juego.
Si consiguiese realizar esto, podría llegar a reducir el espacio que ocupan los tiles de los
sprites hasta la mitad, lo que supondría un espacio total aproximado de 5.000 bytes. Sin
embargo, a día de hoy, desconozco la manera a implementar este aspecto por lo que debo
investigar sobre ello.
Por otro lado, están los mapas del menú y la muerte, que no son mas que los fondos que
cargo cuando el juego se encuentra en el estado de menú, justo al arrancar la consola, o en
el estado de muerte, el cual ocurre cuando el jugador pierde todas las vidas. Estos dos mapas,
ocupan un espacio excesivamente grande si se compara con el que ocupan los mapas 1 y 2
del juego. El motivo de esto es debido a que ambos fondos los realicé con un programa de
edición de imágenes en un intento de conseguir entregar el proyecto en la convocatoria de
junio y por lo tanto, el dibujado del fondo se realizó sin tener en cuenta el espacio que ocupa
cada carácter dibujado, dando lugar a una cantidad excesiva de tiles para poder representar
ambos mapas.
8.6.1. Precisión subpíxel
Antes de intentar solucionar el problema de espacio, voy a explicar un concepto que
investigué en su momento y el cual pretendía integrar en el videojuego. La razón por la que
voy a explicarlo es porque he dedicado tiempo a intentar aprender cómo funciona y a como
integrarlo en mi proyecto, sin embargo, finalmente decidí que me conllevaría demasiado
tiempo y tampoco es un aspecto estrictamente necesario para que el juego funcione, por lo
que decidí apartarlo y si más adelante dispongo del tiempo necesario para implementarlo, lo
haré. Aun así, voy a intentar explicar todo lo que he aprendido por si a alguien le resulta útil.
143
El concepto que voy a explicar es el de la coma fija o el de cómo obtener precisión
subpíxel en consolas de 8 bits. Para explicar este concepto me he basado en el video de
coma fija de Francisco Gallego en su canal de YouTube, Profesor Retroman.
El problema que intento resolver en este apartado es el de utilizar valores decimales en vez
de enteros en mis operaciones. Como hemos podido ver a lo largo del proyecto, en casos
como el movimiento de las entidades, siempre se utilizan valores enteros, esto quiere decir
que lo más lento que puedo hacer que un enemigo se mueva es de pixel en pixel. Pero, ¿qué
pasa si yo quisiera que la velocidad de movimiento fuese 0,75 en vez de 2? ¿Cómo
conseguiría esto? La solución es la coma fija.
Si tuviésemos un número flotante como puede ser el 20’345 por ejemplo. Si a este valor lo
multiplicásemos por un factor escala como puede ser el valor 1000, obtendríamos el valor
20345. De esta manera, a la hora de operar, nosotros sabemos que ese valor realmente
representa un valor decimal y el cual se obtiene dividiendo por su factor escala, por lo que
hacemos dicha división a la hora de operar y obtendríamos el valor decimal.
El problema, sin embargo, es que en ordenadores o consolas de 8 bits utilizar un factor de
escala de valor 1000 puede resultar muy costoso para la máquina en cuestión, es por eso por
lo que es mejor usar un valor de 256. Como sabemos, 256 es la cantidad que puede
representar un byte y, además, al ser potencia de 2 permite efectuar operaciones de
multiplicación y división mediante un simple desplazamiento de bits en el byte.
Con un valor como el 20’34 por ejemplo, podemos utilizar 2 bytes para almacenar el número:
un byte para la parte entera y otro para la decimal. Hay que tener en cuenta que, en la parte
decimal, el valor será representado como 34/256, ya que 256 es el factor escala y el máximo
valor a poder representar en un byte.
De tal manera que, en operaciones sencillas de suma o resta, si queremos sumar dos valores
cualesquiera como pueden ser 50’32 y 75’180. Esta suma se representaría como (30 +
32/256) + (75 + 180/256), lo que daría lugar a 125 + 212/256. En este caso en cuestión, en
la parte decimal no se ha producido acarreo y no hay que hacer nada más. Si se diese el caso
en el que valor de la parte decimal supera los 256 que puede representar un byte, su sumaría
dicho acarreo a la parte entera.
144
8.6.2. Codificador de mapas
Lo primero que vamos a intentar reducir es el espacio que ocupan los tiles de los mapas,
para ello, vamos a implementar un codificador muy simple para los mismos, de tal manera
que estos vean reducido el tamaño que ocupan.
Para ello, vamos a hacer un pequeño programa en C++, el cual nos permita leer un fichero,
que será el que contendrá los tiles de los mapas, y a partir de este, generar otro que ocupe
menos espacio. Posteriormente, dentro del código del juego, habrá que implementar la
manera de leer este nuevo fichero generado para poder copiar todos los tiles correctamente
a su zona de memoria correspondiente.
Una vez abierto el fichero que contiene los valores de los tiles del mapa mediante la
instrucción “ifstream”, ya lo tenemos preparado para poder leerlo. Esta lectura se puede
hacer o bien línea a línea o bien carácter a carácter. El problema que tiene ambos métodos
es que la lectura se realiza para todo el fichero, es decir, todo lo que haya escrito en el mismo.
Si observamos la estructura del fichero que contiene los datos de los mapas, se puede ver
que contiene comentarios para indicar que tile es cada uno, por lo tanto, estos comentarios
se van a coger también en la lectura (véase figura 103).
Figura 103. Ejemplo estructura fichero que contiene los tiles de los mapas.
Fuente: Elaboración propia.
Sabiendo esto, lo más fácil es realizar dicha lectura mediante strings, es decir, línea a línea,
ya que lo único que habrá que tener cuidado es de no leer las líneas que contengan
comentarios, lo cual se resuelve fácilmente mediante una simple implementación mediante
la cual evitemos que se lean las líneas impares que son las que contendrán estos comentarios.
145
La idea es conseguir primero que se realice una codificación simple que vaya comprobando
valor a valor si se repiten y cuántas veces se produce dicha repetición. Para esto, hay que
guardarse los dos valores que se comparan en dos arrays distintos. Esto es debido ya que,
aunque la lectura del fichero sea línea a línea, para luego acceder al contenido del string, hay
que hacerlo carácter a carácter, por lo que hay que guardar el valor del tile en un array de
caracteres de 2 posiciones.
Una vez guardados ambos valores, se comparan para comprobar si son iguales. Si lo son,
entonces se incrementa en 1 un contador que usaremos para contar el número de veces que
se repiten los valores y avanzaremos hasta el siguiente valor del fichero.
Este nuevo valor, se almacenará en el segundo array de caracteres, el primero seguirá
conteniendo el mismo valor que antes para poder comparar su similitud. Todo este proceso
se repetirá hasta que se encuentre un valor distinto en el segundo array de caracteres o se
haya llegado al final de la línea.
En el primer caso, cuando se encuentra el valor distinto, se comprueba el contador de
repeticiones para comprobar si el valor del primer array se ha repetido más de una vez o no.
En caso negativo, esto significará que dicho valor no ha se ha repetido y por lo tanto debemos
escribirlo en el fichero de salida tal cual.
En caso contrario, si el valor se ha repetido más de una vez, se escribe en el fichero de salida
el número de veces que se ha repetido y el valor en cuestión que se repite. Dentro de este
apartado también se comprueba si el valor posterior al repetido corresponde al último valor
de la línea leída. Si esto se cumple, entonces se escribirá también dicho valor tal cual, en el
fichero, ya que ya no habrá más comparaciones en esa línea.
Para todos los casos en el que él se haya encontrado un valor distinto, se llamará a una
función llamada “escribirFichero” la cual será la que se encargue de escribir en el fichero de
salida el resultado. A esta función se le pasan los siguientes parámetros: el fichero a escribir,
un entero para indicar la operación a realizar (escribir un valor único o repetido), el número
de veces que se repite el valor y el valor repetido.
Tras escribir en el fichero, se reinicia el contador de “almacenados” para poder coger de
nuevo otros dos valores a comparar y el contador de repeticiones. Además, se vuelve una
posición hacia atrás para coger el valor distinto del segundo array como primer valor a
comparar con los siguientes valores.
146
Figura 104. Sección encargada de gestionar lo que ocurre cuando se detecta un valor distinto.
Fuente: Elaboración propia.
Dentro de la función “escribirFichero”, lo primero que se realiza es una comprobación de un
booleano, el cual servirá para saber si lo que se va a escribir en el fichero es el principio de
una nueva línea o la continuación de una ya empezada. Esto es necesario para saber si hay
que escribir el “.db” al principio de la línea o no.
Independientemente de que sea principio de línea o no, se comprueba el entero denominado
“caso” que determina que se va escribir en el fichero: un valor único sin repeticiones o un
valor que se repetirá tantas veces como indique el entero “rept”.
147
Figura 105. Función para escribir en el fichero de salida.
Fuente: Elaboración propia.
Con este pequeño código, ya tendríamos un simple compresor que se encargar de hacer una
codificación simple de valores individuales. El resultado obtenido se puede observar en la
siguiente figura.
Figura 106. Comparación fichero entrada/salida. Arriba: fichero sin comprimir. Abajo: fichero comprimido.
Fuente: Elaboración propia.
148
Tras hacer este simple codificador, nos podemos dar cuenta de que realmente no nos va a
servir. Si nos fijamos en la figura 107, podemos ver que hay un valor que se utiliza para
saber cuántas veces se repite el siguiente valor posterior a este, pero ¿Qué pasa con los
valores que no se repiten? ¿Cómo implementamos un código dentro de la SMS que sea capaz
de leer este fichero y averiguar cuando se trata de una valor único o repetido?
La solución es bastante sencilla, al igual que con los valores repetidos, podemos poner
previamente antes del valor que no se repite, un número que indique que este solo se repite
una única vez. De esta manera, al leer en la SMS, nosotros ya sabemos que primero se
indicará el número de veces que se repite y después vendrá el valor repetido.
Sin embargo, al hacer esto nos encontramos otro problema, en aquellos casos donde hayan
índices de tile que no contengan ningún valor repetido, el codificador en vez de comprimir
lo que hará será aumentar el tamaño de esa línea, ya que estamos incluyendo un byte más
indicar que no se repite y si no hay ningún valor repetido en esa línea, esto dará lugar a un
índice de tile que puede llegar a ocupar hasta 64 bytes, lo que es el doble de lo que ocupaba
antes y esto no es algo que queramos que ocurra.
La solución a todo esto es algo un poco más complejo, ya que habría que realizar un
codificador mucho menos simple que el realizado anteriormente, que se encargue de hacer
comprobaciones de parejas, de tríos y de cuartetos de valores. De esta forma, se reduciría
bastante la probabilidad de que salga un índice de tile que no contenga valores repetidos y
si ocurriese, habríamos reducido bastante el espacio ocupado que, aunque se ocupe el doble
de bytes en una línea, seguiría saliendo rentable.
Pero para esto habría que dedicar bastantes horas para poder conseguir un codificador de tal
estilo y esto algo de lo cual no dispongo, por lo que no me queda otra que descartar esta
opción.
8.6.3. Aumento del espacio disponible en ROM
Debido a que el codificador no ha resultado ser útil para reducir el espacio que ocupan los
mapas del juego, hay que averiguar otra manera que si nos permita ahorrar espacio. La
primera opción que puede surgir para conseguir este propósito, volviendo a mirar la tabla
que resume las funciones/etiquetas que mayor espacio ocupan, sería la de reducir el número
de tiles sprites que se cargan a la consola, ya que estos son los que más ocupan en ROM.
149
Si hacemos memoria, recordamos que el principal problema con los sprites residía en que se
cargaba dos veces en memoria ROM el mismo sprite pero girado hacia el lado contrario al
que apuntaba el primero de ellos. Esto no debería realizarse así, debería haber una forma
dentro de la consola que permita al programador solamente cargar uno de los dos sprites
simétricos que tiene y posteriormente, girarlo durante la ejecución de la consola mediante
código.
Sin embargo, tras estar investigando un buen rato, llegué a la conclusión de que esto no va a
ser posible. En múltiples foros sobre programación de consolas de antiguas como smspower
o nesdev, hay usuarios que formulan la misma cuestión que yo me planteo actualmente y las
respuestas a estos hilos inciden en que la SMS no permite girar los tiles de los sprites, sin
embargo, si permite realizar este giro en los tiles del fondo.
Por lo tanto, no es posible ahorrar espacio en los tiles de sprites pero si en los del fondo, por
lo tanto, vamos a aprovechar al máximo el aspecto de girar los tiles del fondo para conseguir
así que los mapas ocupen menos espacio en ROM.
Para ello, hay que realizar un mapa que no utilice muchos tiles diferentes para que así el
BMP2TILE nos cargue el menor número de índice de tile posible, ya que este elimina
aquellos tiles que sean repetidos, es decir, iguales. Para conseguir esto, debemos intentar
usar, siempre que se pueda, el mismo tile, pero girado, ya sea horizontalmente o
verticalmente.
La mejor manera para construir un mapa simétrico es utilizar un editor de mapas. El editor
que yo utilizo es el Tiled, el cual nos permite cargar imágenes que contengan los tiles que
queramos utilizar en nuestro mapa. Además, permite indicar el tamaño de los tiles de la
imagen, es decir, si cada tile va a ser de 8x8 píxeles o superior, lo cual es perfecto para
asegurarnos que cada tile tiene el tamaño adecuado para cargar en la consola.
Por otro lado, permite también girar los tiles en cualquier dirección mediante simplemente
pulsar una tecla, permitiéndonos así conseguir usar el mismo tile varias veces en nuestro
mapa si tener que usar otro tile distinto. Recordemos que cada tile se representa en la consola
como 32 bytes, por lo que cuantos menos tiles usemos menos espacio ocupará el mapa.
150
Figura 107. Ejemplo de mapa con tiles simétricos creado con Tiled.
Fuente: Elaboración propia.
En la figura anterior se puede observar un ejemplo claro de un mapa que contiene tiles
simétricos utilizados para varios aspectos. Si nos fijamos podemos ver que la pared de la
izquierda del todo es igual que la de la derecha, pero girada hacia el lado contrario. Las
columnas, por otro lado, también hacen uso de estas dos paredes, que juntándolas dan el
aspecto de una especie de columna. Como vemos, simplemente jugando con este aspecto de
girar los tiles hemos podido crear paredes y columnas solo usando 2 tiles.
Mediante el uso de esta simple técnica de girar tiles, he conseguido que un mapa que antes
requería un total de 24 tiles para ser dibujado en la consola, ahora ocupe solamente 14. Esto
supone un ahorro de unos 320 bytes en total.
Ahora solo habría que aplicar esto técnica a todos los mapas que se construyan para lograr
así algo más de espacio sobrante en la ROM de la SMS.
8.7. Implementación del final del juego
Ahora que ya se ha conseguido obtener más espacio libre en la ROM, ya es posible
implementar los niveles finales del juego que permitan dar al videojuego una sensación de
final. Como la historia del mismo no es un punto que se haya enfatizado demasiado para este
proyecto, la sensación de que estamos siguiendo una historia no está del todo lograda y por
lo cual, conseguir representar un final que transmita al jugador una sensación de que está
siguiendo una historia de principio a fin es un proceso complicado y más teniendo en cuenta
que me encuentro en la etapa final del proyecto.
151
Sin embargo, voy a intentar que los niveles finales del juego sean algo distintos a los
anteriores y que concuerden un poco con la pequeña historia que decidí construir allá cuando
empecé con el proyecto. En el apartado de diseño del juego se encuentra disponible una
pequeña explicación sobre el contexto en el cual trata el videojuego Invasion.
8.7.1. Creación de los niveles finales
Fijándose en los niveles anteriores, nos podemos dar cuenta de que todos están formados por
el mismo bioma: materiales de piedra o roca. Esto es debido a que se pretende dar la
sensación al jugador de que está en una cueva. El motivo de que el diseño esté relacionado
con cuevas es debido a que Mark, el personaje protagonista, cae a una cueva por un agujero
al sufrir una emboscada nada más empezar el juego y es en este lugar donde transcurrirá la
mayor parte de Invasión.
Mi intención ahora es la de hacer que los últimos 2 o 3 niveles sean con un bioma distinto.
Mi idea es que, en un punto determinado del juego, concretamente en el nivel 7, el personaje
encuentre una escapatoria de las cuevas y salga al exterior, a los bosques de Vietnam.
Figura 108. Diseño del nivel 7 que contiene escaleras para dar al exterior.
Fuente: Elaboración propia.
La mecánica de uso de la escalera es bastante simple, como la escalera es un elemento del
fondo y no es un sprite independiente, no es posible detectar colisión entre el personaje y la
escalera de la misma forma a como se estaba realizando hasta ahora. Entonces, para poder
saber que el jugador se encuentra en la zona correcta para usar la escalera, se utiliza
simplemente su posición.
152
La posición de la escalera es fija y se conoce en todo momento, por lo que lo único que hay
que realizar es una comprobación de la posición del jugador. Se crea así un pequeño rango
de espacio tanto en X como en Y que cubra la zona donde se encuentra la escalera y un poco
más de ella para que no sea muy exacto. Para poder realizar esto necesitamos hacer
comprobaciones de mayor o menor que en Z80. Esto, como ya se ha comentado
anteriormente, se realiza mediante la comprobación del flag de acarreo. En función de si se
ha producido o no acarreo en la operación anterior se determina si el valor almacenado en el
registro A es mayor o menor que el que se encuentra en la instrucción del CP.
Si se juntan las comprobaciones necesarias para comprobar tanto la posición X como en Y,
se consigue así saber cuándo la posición del jugador está dentro de un rango de espacio
determinado.
Figura 109. Uso de la instrucción “jr c” y “jr nc” para hacer comprobaciones de mayor y menor de un valor.
Fuente: Elaboración propia.
Gracias a esta salida de la zona de cuevas, el uso de un bioma distinto en los próximos niveles
está algo más justificado y pilla con menor sorpresa al jugador.
No obstante, con un cambio de bioma no es suficiente para evitar transmitir una sensación
de monotonía, ya que si las mecánicas siguen siendo las mismas que las de todo el juego no
va a importar ese cambio de diseño. Por lo tanto, he decidido añadir una mecánica nueva
dentro del nivel final.
153
La idea es que en el nivel final el jugador debe hacer destruir el portal del cual es donde están
saliendo todos los enemigos. Para lograr esto, el jugador deberá destruir una especie de
cristales los cuales estarán distribuidos a lo largo del nivel. Al destruir todos los cristales, el
portal se destruiría y tras esto, el juego habría finalizado.
Figura 110. Diseño del nivel final donde se encuentra el portal.
Fuente: Elaboración propia.
Para implementar esta mecánica de destrucción de cristales, en primer lugar, lo
recomendable es crear un fichero nuevo donde se encuentren todas las funciones encargadas
de gestionar todo lo relacionado con los cristales (dibujado, borrado, movimiento etc) al
igual que hacíamos con cualquier otra identidad del juego.
Para dibujar los cristales dentro del nivel seguimos el mismo proceso que hemos seguido
para dibujar cualquier sprite: se cargan sus tiles en la RAM, se especifica el tamaño del
sprite, se crea el espacio necesario en memoria para poder almacenar cada uno de los cristales
mediante instanceof y finalmente se utilizan las funciones de dibujado para copiar las
posiciones verticales, horizontales y charcodes en el SAT.
Una vez dibujados, queremos que estos sean borrados cuando la bala del jugador colisione
con ellos. Para ello, utilizaremos la misma estructura que se utilizaba para comprobar cuando
las balas colisionaban con los enemigos. La diferencia es que está función solo se llamará
cuando estemos en el nivel final y eso se determina fácilmente con una variable que lleve el
registro del nivel en el que se encuentra el jugador.
154
Podríamos intentar hacer una función general que pueda ser usada siempre que se quiera
comprobar cuando la bala colisiona con alguna entidad y evitar hacer funciones idénticas,
pero como voy falto de tiempo y tengo el espacio necesario para realizar todo lo que quiero,
no voy a hacer tal cosa, aunque lo más eficiente sería hacerlo.
Como digo, dicha función es idéntica a otra que ya se ha realizado anteriormente cuando se
estaba implementando a los enemigos por lo que no voy a poner una captura del código para
los cristales ya que en ese apartado se puede encontrar la misma función.
Dicho esto, ya se comprueba cuando la bala colisiona con un cristal, por lo que, si hacemos
memoria a lo que hacía la función, recordaremos que almacenaba en la variable colisión de
las dos entidades colisionadas el ID de la entidad con la que ha colisionado, por lo que solo
es necesario comprobar cuando esta variable de los cristales sea distinta de 0 y cuando eso
ocurra borrarlos de la pantalla. Además, cada vez que se borre un cristal se registra el número
de los mismos que han sido destruidos en una variable y cuando este valor alcance el total
de cristales que hayan dibujados, que en el caso de mi proyecto corresponde al valor 3,
entonces se terminará el juego.
8.7.2. Creación de la pantalla de fin del juego
Con la destrucción de todos los cristales el juego debería pasar a una pantalla nueva donde
se le indique al jugador que se ha terminado. Para ello, se puede utilizar una de las funciones
ya implementadas anteriormente.
Si recordamos, ya disponíamos de una función que se encargaba de gestionar los estados del
juego, es decir, se encargaba de controlar si el jugador se encontraba en el menú, jugando o
muerto, de tal manera que en función del estado del mundo se ejecutaban unas funciones u
otras. La idea para mostrar la pantalla de fin del juego es exactamente la misma: añadir un
cuarto estado de mundo que corresponda a la de la pantalla de fin del juego, cuando se
destruyan todos los cristales es cuando se pasará a este estado.
155
Figura 111. Cambio de estado de mundo a Fin del Juego.
Fuente: Elaboración propia.
Mediante el código de la figura anterior, automáticamente saltaremos a la función encargada
de cambiar el estado de mundo. Para la pantalla de fin del juego, al igual que los otros
estados, se quedará el juego en “pausa” hasta que se pulse la tecla ALT que reiniciará el
juego de nuevo. Es aquí donde habrá que cambiar el dibujado del fondo de pantalla por uno
que indique al jugador que ha terminado el juego. Esto se implementará de la misma manera
que se ha realizado para la pantalla de muerte o la de menú.
Figura 112. Pantalla de Fin del Juego.
Fuente: Elaboración propia.
Es una pantalla bastante simple pero que cumple la función de indicar al jugador que ha
finalizado el juego. Se podría hacer mucho más compleja pero la falta de tiempo y de espacio
en ROM impide actualmente que se pueda conseguir tal cosa.
156
Con todo esto realizado, ya se tendría un videojuego en Sega Master System el cual se puede
jugar de principio a fin. Quizás no se pueda jugar de la mejor manera, pero es ahora en el
poco tiempo restante en donde debo intentar corregir todos los “bugs” posibles y pulir todas
las mecánicas para que el juego de una sensación de acabado y de producto.
8.8. Mejoras y arreglos
Con todas las mecánicas ya implementadas y realizado prácticamente todo lo que se
pretendía hacer dentro del tiempo disponible, es tiempo ahora de pulir todas aquellas
mecánicas implementadas y corregir todos los fallos posibles.
Esta etapa final del proyecto, por lo tanto, servirá para intentar mejorar todo aquello que no
haya quedado de la mejor manera o no de la sensación de acabado. Además, se intentará
arreglar todos los fallos que hayan surgido durante el proceso de creación del juego.
8.8.1. Efecto de parpadeo de sprites
Uno de los objetivos principales a lograr en un videojuego, es transmitir al jugador lo que se
conoce como “feedback”, es decir, conseguir que el jugador tenga la información suficiente
para que sepa todo lo que está ocurriendo y no se encuentre perdido durante su partida.
La primera forma de conseguir esto, es mediante el efecto de parpadeo de sprites.
Actualmente, cuando el jugador recibe daño de cualquier fuente, la única información que
este recibe reside en el contador de vidas y esto no es suficiente puesto que el jugador, en
algún momento, puede pensar que no ha recibido daño alguno cuando en realidad si que lo
ha recibido. Esto puede derivar en un enfado del jugador y en un pensamiento de que el
juego no está bien realizado.
Para dar una mayor información al jugador en estos casos, se puede conseguir mediante un
simple parpadeo del sprite del protagonista, es decir, un continuo borrado y dibujado durante
el tiempo que dure la invencibilidad del jugador tras recibir daño. Esto, además de indicar
de mejor manera al jugador de que ha recibido daño, también sirve para que este sepa cuando
acaba la etapa de invencibilidad.
Para implementar esta mecánica bastará simplemente con implementar una función que se
encargue de borrar del SAT (es decir poner un 0) todos los valores del sprite del personaje
controlable por el jugador.
157
Figura 113. Borrado de un sprite del SAT.
Fuente: Elaboración propia.
Esto resultará en un borrado del sprite de la pantalla del juego. A continuación, en la
siguiente iteración del bucle principal del juego, se ejecutará la función de dibujado normal
del sprite, dando así lugar al efecto de parpadeo que se intentaba conseguir.
Sin embargo, este efecto, aunque sirve para lo que se pretendía, no es lo que se deseaba
conseguir exactamente. Anteriormente, recordamos que uno de los aspectos que se intentó
integrar al juego es la implementación de la coma fija, lo que hubiese permitido la utilización
de valores decimales para todos los cálculos que lo necesitasen.
No obstante, no se logró integrar este aspecto al proyecto por lo que muchos otros aspectos
del mismo se ven afectados por ello. Uno de esos aspectos es el que se trata en este apartado,
al no poder controlar con mucha precisión el tiempo que debe transcurrir entre que se dibuja
o se borra el sprite, solo es posible hacerlo con un transcurso de tiempo de 1 segundo, ya que
un valor superior no lograría el efecto que se desea al efectuarse un parpadeo demasiado
lento.
158
Aun con tiempo de 1 segundo entre ambas funciones se observa un parpadeo lento para mi
gusto. Debido a esto, he decidido que se llame a una función distinta cada vez que se realiza
una iteración en el bucle y para controlar cuando se ejecuta cada una, lo realizo mediante
una variable que cambia de valor cuando se ejecuta la función de borrado o dibujado.
Una vez implementado el efecto de parpadeo, se podría intentar aplicar este mismo efecto a
los enemigos para transmitir mayor información al jugador. Sin embargo, tras pensarlo
durante un breve tiempo, llegué a la conclusión de que quedaría mejor representar cuando
los enemigos reciben daño mediante un pequeño desplazamiento de los mismos.
La idea es que cuando los enemigos reciban la bala del jugador, estos se desplazarán una
pequeña distancia hacia la dirección contraria de donde provenía la bala. Se consigue
representar así de una manera sencilla y simple, el efecto de que el enemigo retrocede por el
impacto de la bala.
Hay diferentes maneras a implementar esta mecánica, la manera que he decidido realizar es
mediante una simple función que se ejecuta justo cuando el enemigo recibe daño. Dicha
función accederá a una variable que contendrá la información relacionado con la posición
del jugador en el eje X justo cuando este realizó el disparo, de tal manera que se compara
este valor con la del enemigo que recibe el impacto, en función de si es mayor o menor, el
desplazamiento será a la izquierda o a la derecha.
Figura 114. Función para desplazar al enemigo cuando este recibe daño.
Fuente: Elaboración propia.
159
Hay que tener cuidado con no utilizar la posición del jugador justo en el momento cuando el
enemigo recibe el impacto, ya que esto puede llegar a dar problemas como que el
desplazamiento no se realice hacia la dirección correcta, debido a que el jugador puede haber
cambiado de posición mientras la bala llegaba a impactar al enemigo.
8.8.2. Música
La música era uno de los aspectos más importantes que más deseaba introducir en el
proyecto, sin embargo, al no haber podido dedicarle el tiempo suficiente para investigar
sobre ella, finalmente no se ha podido integrar en el juego. No obstante, durante el proceso
de investigación he descubierto algunos detalles interesantes que pueden ser útiles para
poder lograr en un futuro implementar esta mecánica. Es por ello, por lo que a continuación
se procede a explicar todo lo que se ha aprendido.
La idea principal reside en simplemente introducir una música para que suene de fondo
durante todo el transcurso del juego. No se considera introducir efectos de sonido debido a
la falta de tiempo y a que hay otros aspectos a implementar aún.
Existe una librería de sonido llamada PSGLib, la cual pertenece al usuario Sverx [32].
Esta librería incluye una serie de funciones que permiten gestionar todo lo relacionado con
la música: Reproducir una canción, pausarla etc.
Figura 115. Una de las funciones incluidas en la librería PSGlib.
Fuente: Librería PSG [32].
160
Para utilizar esta librería basta con simplemente incluir el fichero que la contiene en nuestro
proyecto con la etiqueta “include”.
Una vez integrada en el proyecto, para poder utilizar las funciones de la librería y que
funcione todo correctamente, es necesario previamente que la canción este convertida al
formato correcto para que pueda ser reproducido por el chip de sonido de la SMS. Para ello,
en primer lugar, se utilizará uno de los tres programas disponibles (hay más disponibles,
pero son los que recomienda el autor de la librería) Mod2Psg, DefleMask o VGM Music
Maker. Estos tres programas realizan la misma función, crear nuestra canción de 8 bits y
exportarla al formato de VGM.
Tras exportar la canción, es necesario que esta sea optimizada y convertida al formato PSG
utilizando la herramienta de vgm2psg que forma parte de la librería. Desconozco
exactamente cómo se realiza este proceso de creación de una canción propia, ya que no he
tenido posibilidad de probarlo al no haber podido reproducir siquiera la canción que se utiliza
en el tutorial de smspower.
La idea es que, una vez se tenga la canción en el formato correcto, la incluyamos en nuestro
proyecto de la misma manera que se incluye cualquier otro recurso que queramos usar como
los sprites o los mapas. Una vez incluida, primero habrá que inicializar la librería mediante
la llamada a la función de “PSGInit” la cual se llamará solo una única vez y se recomienda
que esta llamada sea antes de habilitar las interrupciones de la consola.
Tras esto, copiamos la dirección donde se encuentra nuestra canción almacenada en el
registro “HL” y se llama a la función de “PSGPlay” para reproducir la canción. Finalmente,
solo quedará llamar a la función “PSGFrame” de manera constante. Una forma de realizar
esto, es mediante la llamada de una interrupción constante, es decir, que se haga siempre y
siempre tras el mismo transcurso de tiempo. Un ejemplo puede ser la interrupción VBlank
que ya está implementada en el proyecto, por lo que solo hay que llamar a la función justo
después de que salte esta interrupción.
A pesar de todo esto, no he conseguido hacer que se reproduzca la canción de fondo y eso
que, aparentemente, he realizado todos los pasos que indica Sverx en su repositorio. Sin
embargo, algo estoy realizando mal o hay algún paso que no he llegado a entender del todo
bien, pero no dispongo del tiempo para poder investigar más a fondo este aspecto por lo que
no queda más remedio que descartar la música.
161
9. Conclusiones
Tras casi un año de trabajo, el proyecto que empecé por octubre/noviembre ha sido
finalizado. Todos los objetivos propuestos han sido realizados y se ha conseguido crear un
videojuego que funcione de manera correcta en una consola de la Sega Master System.
En líneas generales me encuentro satisfecho con el resultado del proyecto y el trabajo
realizado a lo largo de este último año. Aunque el videojuego construido no sea el mejor
juego creado para esta consola, considero que el resultado obtenido, aunque siempre puede
ser mejor, es aceptable teniendo en cuenta que era la primera vez que trataba con esta consola
y no tenía ningún conocimiento previo sobre aspectos técnicos de la misma.
Se han conseguido realizar mecánicas que antes de empezar el proyecto no consideraba
capaces de realizar: reloj de tiempo, efecto de parpadeo de sprites, colisiones con el entorno
etc. Además, a pesar de que ya conocía el lenguaje ensamblador Z80 gracias a proyectos
previos realizados, he aprendido nuevos conceptos e instrucciones que anteriormente
desconocía y que me han ayudado a crear un mejor producto.
Por otro lado, he intentado explicar en este documento, de la mejor forma posible, todo lo
que me ha conllevado realizar este videojuego en la SMS: desde problemas encontrados y
sus soluciones hasta descripciones detallas con imágenes de las mecánicas implementadas
por mí. El motivo de esto era para que cualquier otra persona que en un futuro quiera o deba
realizar un videojuego para esta plataforma pueda encontrar en este documento una
pequeña ayuda para realizar tal tarea y que no cometa los mismos errores que he yo he
cometido (o que encuentre aquí la solución de los mismos). Considero que este objetivo se
ha logrado y espero que pueda servir de ayuda a muchas otras personas.
También he creado un pequeño anexo en este documento donde describo algunos de los
aspectos técnicos de la consola SMS que he ido aprendido a lo largo de mi pequeña
investigación. La idea principal de esto era para poder acceder a dicha fuente de información
en cualquier momento que lo necesitase durante el desarrollo del proyecto, ya sea por dudas
o simplemente para reafirmar mis conocimientos aprendidos. Sin embargo, este simple
anexo puede también ser usado por otras personas si así lo desean, ya que la mayor parte de
la información que se puede encontrar por Internet sobre la Sega Master System se encuentra
en inglés y aquí se puede encontrar, aunque de manera mucho más resumida, en español.
162
A pesar de todo, la razón principal por la cual me encuentro satisfecho con el videojuego
realizado es debido a que todo lo aprendido y realizado me ayuda bastante a la hora de
futuros proyectos, ya que mi idea es volver a realizar otro juego para la SMS que por
seguro será mucho mejor que el realizado aquí. Principalmente porque no volveré a cometer
los mismos errores y, además, ahora dispongo de un conocimiento mucho mayor sobre cómo
funciona la consola por lo que podré implementar mecánicas que no he conseguido realizar
en Invasion (scroll lateral, música, efectos de sonido etc.).
163
10. Bibliografía y referencias
[1] Akuma, C. Z80 Assembly programming for the Sega Master System. Sitio web:
http://www.chibiakumas.com/z80/MasterSystemGameGear.php (accedido el
05/09/19).
[2] Alonso, J. (2003). El microprocesador Z80. Sitio web: http://curso-
cm.speccy.org/fr_cap3.html (accedido el 05/09/19).
[3] Anders. (2015). Create a Racing Game. Sitio web:
http://www.smspower.org/Articles/CreateARacingGame (accedido el 05/09/19).
[4] Blue Yeti Studios. (2017-2018). Sitio web: https://opengameart.org/users/blue-
yeti-studios (accedido el 05/09/19).
[5] Boland, S. (2013). Sega Console Programming. Sitio web:
http://steveproxna.blogspot.com/2013/09/sega-console-programming.html
(accedido el 05/09/19).
[6] Corcoran, L. (2013). Itch.io Game Assets. Sitio web: https://itch.io/game-assets
(accedido el 05/09/19).
[7] Context. (2005). Resize my picture. Sitio web: http://www.resizemypicture.com/
(accedido el 05/09/19).
[8] Cornut, O., y otros. (2005). Meka Emulator. De Emutopia. Sitio web:
https://www.emutopia.com/index.php/emulators/item/298-sega-sg-1000-sc-
3000/239-meka (accedido el 05/09/19).
[9] Cornut, O., Maxim. (1997). SMS POWER. Sitio web: http://www.smspower.org/
(accedido el 05/09/19).
[10] Dazz, Petie. (2003-2019). The spriters Resource. Sitio web:
https://www.spriters-resource.com/ (accedido el 05/09/19).
[11] García, S., Américo, F. (2002). Registros de la CPU. Sitio web:
http://www.portalhuarpe.com/Medhime20/Sitios%20con%20Medhime/Computa
ci%C3%B3n/COMPUTACION/Menu/Modulo%205/5-6.htm (accedido el
05/09/19).
[12] Helín, V. (2019). Wla-dx 9.9 documentation. Recuperado de:
http://www.villehelin.com/wla-README.html (accedido el 05/09/19).
[13] Index Register. (2019). Wikipedia: La enciclopedia libre. Sitio web:
https://en.wikipedia.org/wiki/Index_register (accedido el 05/09/19).
164
[14] Interrupt. (2019). Wikipedia: La enciclopedia libre. Sitio web:
https://en.wikipedia.org/wiki/Interrupt (accedido el 05/09/19).
[15] Juan, M. (2015). Magica. Sitio web: https://www.usebox.net/jjm/magica/
(accedido el 05/09/19).
[16] Kelsey, B. (2009). Opengameart. Sitio web: https://opengameart.org/
(accedido el 05/09/19).
[17] Lindeijer, T. (2008-2017). Tiled Map Editor. Sitio web:
https://www.mapeditor.org/ (accedido el 05/09/19).
[18] MacDonald, C. (2000-2002). Sega Master System VDP Documentation.
Sitio web : http://www.smspower.org/uploads/Development/msvdp-
20021112.txt?sid=4dce9e3d17f7f1819f69c80237b9839a (accedido el
05/09/19).
[19] Macdonald, C. (2000-2002). SMS/GG Hardware Notes. Sitio web:
http://www.smspower.org/uploads/Development/smstech-
20021112.txt?sid=4dce9e3d17f7f1819f69c80237b9839a (accedido el
05/09/19).
[20] Mappers. (1997). SMS POWER Development. Sitio web:
http://www.smspower.org/Development/Mappers?from=Development.Mapper.
(accedido el 05/09/19).
[21] Mattis, P., Kimball, S. (1995). GIMP. Sitio web: http://www.gimp.org.es/
(accedido el 05/09/19).
[22] Maxim. (2005). BMP2TILE. Sitio web:
http://www.smspower.org/maxim/Software/BMP2Tile/ (accedido el 05/09/19).
[23] Maxim. (2010). SMSPOWER how to program. Sitio web:
http://www.smspower.org/maxim/HowToProgram/Index (accedido el 05/09/19).
[24] Memory Mapping. (2018). Tutorials Point. Sitio web:
https://www.youtube.com/watch?v=jkT9Bgz8PAg (accedido el 05/09/19).
[25] Memory Mapping 8-bit. Digital-circuitry. Sitio web: http://www.digital-
circuitry.com/8-bit_Memory_Mapping.htm (accedido el 05/09/19).
[26] Overflow Flag. (2019). Wikipedia: La enciclopedia libre. Sitio web:
https://en.wikipedia.org/wiki/Overflow_flag (accedido el 05/09/19).
[27] Scherrer, T. (2019). Z80 CPU Architecture. Sitio web:
http://www.z80.info/z80arki.htm (accedido el 05/09/19).
165
[28] De Sega Games Company Limited. (1985). Software Reference Manual for
the SEGA Mark III Console. Recuperado de:
https://segaretro.org/images/d/d6/SoftwareReferenceManualForSegaMarkIIIEU
.pdf (accedido el 05/09/19).
[29] Sega. (2019). Wikipedia: La enciclopedia libre. Sitio web:
https://es.wikipedia.org/wiki/Sega (accedido el 05/09/19).
[30] Sega Master System Technical specifications. Sega Retro. Sitio web:
https://segaretro.org/Sega_Master_System/Technical_specifications#
(accedido el 05/09/19).
[31] SMS Programming. Sega8Bit. Sitio web:
http://www.smstributes.co.uk/view_page.asp?articleid=207 (accedido el
05/09/19).
[32] Sverx. (2014). Librería de sonido PSG. Sitio web:
https://github.com/sverx/PSGlib (accedido el 05/09/19).
[33] Talbot, R. (1998). Sega Master System Technical information. Sitio web:
http://www.smspower.org/uploads/Development/richard.txt?sid=4dce9e3d17f7f
1819f69c80237b9839a (accedido el 05/09/19).
[34] Wesker. (2010). 25 años de la Sega Master System. Sitio web:
https://www.segasaturno.com/portal/25-anos-de-sega-mark-iii-master-system-
vf7-vt5114-vp39370.html (accedido el 05/09/19).
[35] Williams, M. Sitio web: https://opengameart.org/users/bizmasterstudios.
[36] Zilog Z80. (2019). Wikipedia: La enciclopedia libre. Sitio web:
https://en.wikipedia.org/wiki/Zilog_Z80 (accedido el 05/09/19).
Recursos utilizados
1. Clrhome. (2012). Z80 Table. De The ORG Project. Sitio web:
http://clrhome.org/table/.
2. Cornut, O., Maxim. (1997). SMS POWER Documents. Sitio web:
http://www.smspower.org/Development/Documents.
3. Luis, J. (2003). Glosario Terminología Informática. De Creative Commons BY
NC SA. Sitio web: http://www.tugurium.com/gti/index.php.
166
4. SUMMON PRESS, S.L. Calculadora Hexadecimal. De SUMMON PRESS, S.L.
Sitio web: https://es.calcuworld.com/calculadoras-matematicas/calculadora-
hexadecimal/.
167
11. Glosario
A continuación, se procede a describir los diferentes términos que se pueden encontrar a lo
largo de este documento y cuyo significado puede no haberse comprendido:
• Banco de memoria: Área de ROM o RAM que ha sido mapeada.
• Buffer: Espacio de memoria donde se almacenan datos de forma temporal, con
el objetivo de evitar que el programa o recurso que requiera de esos datos, se
quede sin datos durante una transferencia (de entrada, o de salida) de datos
irregular o por la velocidad del proceso.
• Charcode: Índices de tile de los sprites.
• CRAM: Colour RAM. Corresponde a la memoria interna del VDP y sirve para
definir la paleta de colores en la Master System. Ocupa 32 bytes y es de solo
escritura.
• Firmware: De manera general, el firmware es un programa informático
encargado de establecer toda la lógica de bajo nivel del dispositivo.
Concretamente, existe el programa BIOS que se encarga, entre otras cosas, de
gestionar el arranque de la consola y prepararla para ser utilizada.
• GDD: Game Design Document. Documento que describe detalladamente todos
los aspectos que contienen un videojuego (diseño, mecánicas, jugabilidad,
historia, género etc.). Este documento está en continua modificación durante el
desarrollo del videojuego y su versión final no coincidirá con la inicialmente
creada.
• IA: Inteligencia Artificial.
• Mapper: Hardware encargado de realizar el mapping [20].
• Mapping: El “mapeo”, es la transformación de datos de un formato a otro.
Cuando se utilice este término, se estará haciendo referencia al acto de llevar los
datos que se encuentran en el espacio de direcciones del chip a un rango del
espacio de direcciones del Z80. Esto permite aumentar el espacio de memoria
disponible en la consola [24][25].
• Ranura de memoria (slot): Área del espacio de direcciones del Z80 dentro del
cual varias zonas de ROM o RAM pueden ser mapeadas.
• ROM: Read Only Memory. Memoria de solo lectura que se utiliza
principalmente para almacenar datos.
168
• SAT: Sprite Attribute Table. Zona de datos situada en la dirección de memoria
$3F00 de la SMS. Contiene toda la información referente a los sprites que se
están dibujando por pantalla.
• SMS: Sega Master System, consola de 8 bits sobre la cual se creará el videojuego
de este proyecto.
• VDP: Video Display Processor. Chip de gráficos de la consola SMS [18].
• VRAM: Memoria de video.
Además de todos estos términos, a lo largo del documento puede aparecer escrito el símbolo
‘$’ para denotar aquellos números que están escritos en formato hexadecimal.
169
12. Anexo 1. Detalles técnicos de la Sega Master System
12.1. Información general
La Sega Master System es una consola de videojuegos de 8 bits que utiliza como soporte un
cartucho ROM y una tarjeta Sega Card. Originalmente, fue lanzada en Japón en 1985 con el
nombre de Sega Mark III, siendo la segunda consola de sobremesa de Sega, tras la SG-
1000 (acrónimo de Sega Game 1000) que fue la primera que fabricaron y que tuvo muy
poco éxito en los mercados europeos y asiáticos [28][29][33].
En los años posteriores, sería rediseñada y renombrada a Master System para poder ser
lanzada internacionalmente a zonas como Norte América (1986), Europa (1987) y Brasil
(1989).
La Master System, fue lanzada en competición con la Nintendo Entertainment System
(NES), lo que produjo que tuviese poco éxito en los mercados de Japón y Norte América,
aunque tuvo mucho mayor éxito en Europa y Brasil.
Antes de comenzar a detallar de manera más específica los aspectos más importantes de la
SMS, primero se van a explicar de manera resumida para obtener así una idea general del
hardware que forma parte de la consola:
Tabla 2. Aspectos técnicos generales de la SMS.
SEGA MASTER SYSTEM
CPU Zilog Z80A (NEC D780C), 8 bits
Velocidad de Reloj 3,58 MHz
ROM 8 kb
RAM 8 kb
VRAM 16 kb
Gráficos TMS9918
Resolución 256 x 192 píxeles
Paleta de colores 64 colores (32 simultáneos)
Sonido SN76489 PSG chip (4 canales)
Dimensiones 36.2 x 17 x 7 cm
170
Como información adicional, cabe destacar que las versiones japonesas integraban como
chip de sonido un Yamaha YM2413. Además, la SMS contiene 2 ranuras para la
introducción de juegos: Una para los cartuchos Mega y otra para las tarjetas SEGA CARD
que es una herencia de la Mark II.
Por otro lado, para la visualización de la pantalla se utiliza el TMS9918, que es un
controlador de visualización de pantalla (VDC – Video Display Controller), que contiene 3
bits mediante los cuales se pueden elegir diferentes modos de visualización para la consola,
llamados M1, M2 y M3. Sin embargo, solo hay 4 combinaciones documentadas:
• Modo 0 - Gráficos I
• Modo 1 – Texto
• Modo 2 – Gráficos II
• Modo 3 - Multicolor
Sin embargo, el VDP de la Master System añadió otro bit de modo de selección que
habilitaba el modo 4, el cual es específico de la consola SMS. Por lo tanto, en la SMS hay
muchos más modos de visualización gracias a que en lugar de los 3 bits iniciales que poseía
el VDC, el VDP contiene 4 bits.
A continuación, se procede a describir con mayor profundidad diferentes aspectos
pertenecientes a la consola de videojuegos SMS. Los aspectos que se van a tratar serán los
siguientes:
• CPU Zilog Z80A
• Mapa de memoria
• Sistema Básico de entrada-salida (BIOS)
• Video Display Processor (VDP)
171
12.2. CPU
La CPU principal de la SMS es una Zilog Z80A, la cual es una variante del microprocesador
de 8 bits Z80. Esta versión, fue la más utilizada de todas y funcionaba a una frecuencia de
reloj de 3,58 MHz [2][27][30][36].
El Z80 fue fundado principalmente por el físico Federico Faggin, que por aquel entonces
trabajaba en Intel como diseñador jefe del Intel 8080, entre otros, hasta que dejó su puesto a
finales de 1974 para fundar Zilog y trabajar en el diseño de Z80.
El Z80 fue diseñado con el objetivo de que su conjunto de instrucciones fuese binariamente
compatible con el Intel 8080, de manera que, la mayor parte del código del 8080 pudiese ser
ejecutado sin modificarse en la nueva CPU Z80.
El Z80 ofrecía diferentes mejoras respecto al 8080, entre ellas, se destacan las siguientes:
• Un mejor conjunto de instrucciones incluyendo el direccionamiento de un solo bit,
rotaciones en memoria y en más registros a parte del registro acumulador. Además,
incorporaba dentro del registro de flags un bit para indicar cuando en una operación
se ha producido overflow, es decir, indica que el complemento a 2 del resultado no
cabe en el número de bits utilizado para la operación.
• Nuevos registros índice IX e IY, así como las instrucciones necesarias para
manejarlos.
• Un mejor sistema de interrupción debido a que se incluían dos bancos separados
de registros que podían ser cambiados de manera rápida y acelerar así la respuesta a
las interrupciones.
• Un menor requerimiento de hardware tanto para la fuente de alimentación que solo
requería una alimentación única de 5 voltios, como para la generación de la señal de
reloj.
• Una función especial de reset que permitía limpiar solamente el registro Program
Counter (PC).
Debido a estas mejoras que ofrecía, el Z80 eliminó rápidamente al Intel 8080 del mercado,
convirtiéndose así en uno de los procesadores de 8 bits más populares de los 80.
172
Registros de la CPU
Los registros que contiene la CPU son memorias de alta velocidad y con poca capacidad que
vienen integrados en el microprocesador. Estos registros, permiten controlar las
instrucciones que se estén ejecutando, guardar datos temporalmente, manejar el
direccionamiento de la memoria etc [11].
Los registros pueden almacenar por sí solos hasta 1 byte (8 bits) de información. Este espacio
puede ser incrementado si se juntan dos registros, dando así lugar a un registro de 16 bits de
longitud. Esto permite, por ejemplo, almacenar una dirección de memoria que ocupe 2 bytes.
A continuación, se van a ver los registros que tienen una función específica dentro del
microprocesador:
• Program Counter (PC): Permite almacenar la dirección de memoria (16 bits)
de la siguiente instrucción que se va a ejecutar en el programa. Conjuntamente,
el PC es incrementado automáticamente una vez que su contenido es enviado a
través del bus de direcciones.
• Stack Pointer (SP): Este registro es un puntero a la pila, es decir, almacena la
dirección de memoria de la parte superior actual de la pila, que podrá estar
localizada en cualquier parte de la memoria RAM.
• Registros Índice (IX, IY): Registros de 16 bits que suelen ser utilizados para
apuntar a una región de memoria cualquiera donde los datos van a estar
almacenados o ser recuperados. Permiten acceder a tablas y estructuras de
manera sencilla, ya que sirven como índices dentro de estas estructuras. Las
instrucciones que utilizan estos registros presentan el inconveniente de que son
más lentas que el resto de registros, debido a que ocupan 2 bytes [13].
• Acumulador (A): Permite almacenar el resultado de una operación
aritmética/lógica de 8 bits. El registro A es el único que se utiliza para la mayoría
de instrucciones importantes y es el registro del cual parten y llegan la mayoría
de instrucciones.
• Flag (F): Es un registro de 8 bits, donde cada uno de estos bits se modifica cada
vez que se ejecuta una instrucción aritmética. Por ejemplo, el segundo bit se
utiliza para comprobar si la última operación aritmética ha resultado en cero o
no. Si ha dado cero, el bit se establecerá a 1. En caso contrario, a 0 [26].
173
Se procede ahora a ver los registros que tienen un propósito general:
• Registros BC y DE: También pueden ser utilizados individualmente como
registros de 8 bits. Se suelen utilizar mayormente como contadores o para
almacenar resultados.
• Registro HL: Suelen ser utilizados de manera conjunta en un amplio
conjunto de instrucciones para direccionamiento indirecto. Esto quiere decir
que el registro puede ser utilizado para almacenar un valor (que suele ser una
dirección de memoria) y posteriormente usar dicho valor como una dirección.
Conjunto de instrucciones
De manera genérica, las instrucciones del Z80 se agrupan en las siguientes categorías:
• Operaciones aritméticas/lógicas de 8 bits: ADD, SUB, XOR, OR, CP etc.
• Operaciones aritméticas de 16 bits: INC, DEC, ADD etc.
• Cargas de 8 bits: LD
• Cargas de 16 bits: LD, PUSH, POP
• Saltos: JP, JR, CALL, RET etc.
• Control de CPU: HALT, WAIT, RESET, INT etc.
12.3. Mapa de memoria
Como ya se ha indicado anteriormente, la unidad base de SMS contiene 8 kb de ROM y 8
kb de RAM. La ROM, encargada de almacenar el programa principal del juego, se conecta
a la unidad utilizando una de las dos ranuras que tiene disponible la consola.
El Z80A, es capaz de direccionar hasta un total de 65.536 posiciones de memoria distintas,
gracias al tamaño de su bus de direcciones que es de 16 bits. Este espacio, es el que abarca
desde la dirección de memoria $0000 hasta la $FFFF. Este espacio de direccionamiento es
mucho más simple que el de otras consolas de videojuegos como puede ser la Game Boy. A
continuación, se muestra el mapa de memoria de la Sega Master System [19][33]:
174
Tabla 3. Mapa de memoria de la SMS.
Dirección Descripción
$FFFC-$FFFF Registros de Mapeo
$E000-$FFFC 8k, espejo de la RAM en $C000-$DFFF
$C000-$DFFF 8k de RAM
$8000 - $BFFF 16k Slot 2 de la ROM ó Slot 0 de la RAM
$4000 - $7FFF 16k, Slot 1 de la ROM
$0400 - $3FFF 15k, Slot 0 de la ROM
$0000 - $03FF Primeros 1k del Slot 0 de la ROM
Tomando como referencia la figura anterior, se pueden destacar los siguientes aspectos:
• Desde la dirección $0000 hasta la $BFFF es la región del cartucho, es decir, viene
definida por el cartucho que este actualmente activo. En la mayoría de los casos,
corresponderá a la ROM o al cartucho RAM.
• Debido a que la memoria del cartucho que contiene el juego puede exceder de los 64
kb y el Z80A no puede manejar más de esos 64 kb, es necesario un sistema de gestión
de memoria en la SMS como son los “mappers” [20].
Un mapper no es más que un chip situado dentro de los cartuchos y que permite el
uso de ROM’s más grandes. Para ello, hacen uso de lo que se conoce como bancos
de memoria. Cuando se necesita más memoria, se produce un intercambio de uno
de esos bancos que se están usando por otro que esté disponible, de esta manera, se
consigue así aumentar las capacidades de la consola y obtener más espacio.
En la tabla anterior, se muestra el mapeo de la ROM asumiendo el mapper que
proporciona Sega. Como se puede observar, esto permite definir 4 ranuras (slots)
en el mapa de memoria. Cada banco de 16 kb de ROM, puede ser mapeado a
cualquiera de las 3 primeras ranuras disponibles. Cuando se mapea en la ranura 0,
los primeros 1kb no son afectados para poder preservar los vectores de interrupción
y permitir así que las interrupciones puedan ser controladas de manera correcta, solo
se mapean los 15 kb restantes. Adicionalmente, en la última ranura (slot 2) puede
ocurrir que sea mapeada también, uno de los dos bancos de 16 kb de RAM, dejando
de ser así una zona de solo lectura.
175
El hardware del mapper es controlado por los registros que se encuentran en el
rango de direcciones $FFFC - $FFFF. A continuación, se procede a realizar una
visión general de los mismos:
Tabla 4. $FFFC: Control mapping de la RAM.
Bit Función
7 Habilitar "escritura ROM"
6 - 5 No usado
4 Habilitar RAM ($C000-$FFFF)
3 Habilitar RAM ($8000-$BFFF)
2 Selección del banco de RAM
1 - 0 Cambio de RAM
Si el bit 2 está a 0 entonces se selecciona el primer banco del cartucho de la RAM, si
está a 1, se selecciona el segundo banco.
Tabla 5. $FFFD - $FFFF: Control del mapping de la ROM.
Dirección Selección del slot de ROM
$FFFD Ranura 0 ($0000-$3FFF)
$FFFE Ranura 1 ($4000-$7FFF)
$FFFF Ranura 2 ($8000-$BFFF)
12.4. Sistema básico de entrada-salida (BIOS)
La BIOS o el sistema básico de entrada-salida (Basic Input-Output System en inglés) define
la interfaz de firmware cuyo propósito principal es el de activar una máquina (la Sega Master
System en este caso) desde su encendido y preparar el entorno para cargar el sistema
operativo o gestor de arranque en la memoria RAM. Es lo primero que se ejecuta cuando
iniciamos la consola [19].
Las consolas SMS incluyen una simple BIOS que es la que se encarga de: gestionar el
proceso de arranque de la consola, mostrar por pantalla la animación de logo de inicio de
Sega y de mostrar instrucciones o errores por pantalla. Además, en las SMS no japonesas, la
BIOS requiere que la ROM insertada contenga una cabecera válida para poder ejecutar el
software, si no, no funcionará el juego.
176
Cabecera de la ROM
Para poder entender mejor cómo se comporta la BIOS en la SMS, primero hay que conocer
el contenido de la cabecera de la ROM. Esta tiene un tamaño total de 16 bytes y se puede
encontrar en las direcciones $7FF0, $3FF0 y $1FF0, aunque solo la primera de estas es la
que se suele utilizar. Dentro de esos 16 bytes de la cabecera se encuentra:
• TMR SEGA ($7FF0, 8 bytes): Los primeros 8 bytes de la cabecera contienen el
texto ASCII “TMR SEGA” y son los bytes que requiere la BIOS para verificar que
la cabecera de la ROM sea válida.
• Espacio reservado ($7FF8, 2 bytes).
• Checksum ($7FFA, 2 bytes): Estos 2 bytes almacenan el valor de Checksum. Este
valor es una medida de seguridad con el objetivo de prevenir a los piratas de
modificar la imagen de ROM. Este valor es comparado con el calculado por las
rutinas de la BIOS para verificar que los datos del cartucho sean válidos.
• Código del producto ($7FFC, 2.5 bytes): Contiene el código del producto.
• Código de región/versión ($7FFE, 1 byte): Los 4 bits más inferiores contienen la
versión del producto. Los otros 4 bits, proporcionan información sobre la región y
sistema del cartucho insertado. Si representan el valor $4 (0100), hace referencia a
las SMS Export, es decir, a las versiones no japonesas.
Comportamiento de la BIOS
Una vez conocido el contenido de la cabecera de la ROM, se procede ahora a explicar todo
lo que realiza la BIOS de la SMS, desde que se enciende la consola hasta que se ejecuta el
videojuego.
Lo primero que se realiza cuando se enciende una SMS es comprobar si en algunas de las
ranuras disponibles de la consola (la tarjeta, el cartucho o la expansión) hay algo adjunto en
ellas y arranca la primera que esté disponible. Esto se realiza mediante el puerto $3E (que es
el puerto por el que se accede al hardware encargado de gestionar el espacio de memoria
disponible) con el objetivo de mapear diferentes ranuras en memoria y comprobar ahí si se
ha encontrado algún dato válido en alguna de las ranuras.
Si en alguna de estas ranuras se encuentran datos válidos, entonces lo siguiente que realiza
la BIOS es mostrar por pantalla la animación de inicio del logo de SEGA.
177
Tras esto, comprueba todas las ranuras una detrás de otra. Para cada ranura, 16 bytes son
copiados a la RAM de la consola para comprobar si contienen una cabecera de ROM válida,
es decir, comprueba si los primeros 8 bytes de la cabecera coinciden con el texto ACII “TMR
SEGA” y esto lo hace desde las direcciones de memoria $7FF0, $3FF0 y $1FF0
respectivamente. Si coinciden, entonces continua con más pruebas. En caso de que no
encuentre una cabecera en ninguna de las 3 localizaciones especificadas, se asume que la
ranura está vacía y por lo tanto no se ejecutará nada.
A continuación, lo que comprueba es la zona referente a la región. Si los 4 bits más altos del
último byte de la cabecera de la ROM no coinciden con el valor $4, el cartucho es rechazado
y se dibuja por pantalla un mensaje de error de software.
Finalmente, la última comprobación que realiza la BIOS es el checksum. Las rutinas de la
BIOS calculan el checksum sumando cada palabra desde la dirección $200 hasta el final de
la ROM. Si el valor calculado no coincide con el que hay situado en la cabecera de la ROM,
vuelve a mostrar por pantalla un mensaje de error de Software. Si el valor coincide, entonces
se han pasado todas las pruebas de la BIOS y se procede a ejecutar el videojuego.
12.5. Video Display Processor (VDP)
El VDP es un chip de gráficos que se puede encontrar en el interior de las videoconsolas de
Sega, donde en cada una de estas, hay diferentes versiones de este chip. El VDP contiene en
su interior una serie de registros y algo de RAM, la cual es controlada por los puertos $be y
$bf, que son utilizados para controlar los datos y para el control del VDP, respectivamente
[18].
La resolución de la pantalla del VDP es de 256 píxeles horizontales (ancho) y 192 píxeles
verticales (alto), donde cada pixel puede ser mostrado como uno de 16 colores, estos
seleccionados a partir de una paleta formada por un total de 64 colores distintos.
El fondo de la pantalla está compuesto por tiles de 8x8 píxeles, por lo que esto da lugar a
un total de 768 tiles visibles en pantalla (32 horizontales x 24 verticales). Además, hay 4
filas adicionales debajo de las 24 filas verticales visibles para poder realizar el conocido
efecto de “scroll” con la información que se encuentre abajo. De esta forma, la información
ubicada en la parte inferior pasa a ser mostrada en la parte superior de la pantalla.
178
Hay dedicados un total de 16 kb de RAM para el sistema de video. Esta RAM recibe el
nombre de VRAM. Para poder escribir o leer de la Video RAM se hace a través de los
registros del VDP.
Organización de la VRAM
Estos 16kb pertenecientes a la memoria de vídeo, están divididos en 3 secciones:
• Un mapa de pantalla de bytes (1792 bytes): Determina la colocación de los tiles
sobre la pantalla de fondo de 32x24 cuadrículas, es decir, define las posiciones en
pantalla de los 896 tiles, de los cuales solo 768 son visibles.
• El Sprite Attribute Table (SAT) (256 bytes): Esta tabla establece las coordenadas
X-Y y número de tiles de hasta 64 objetos movibles o sprites.
• Generador de carácter de bytes (14.336 bytes): Estos son los tiles de 8x8 píxeles
para el fondo y/o los tiles de 8x8 píxeles (o 8x16) de los sprites.
La colocación de cada uno de las 3 partes de la VRAM es controlada por los registros del
VDP como se verá más adelante.
Programación del VDP
Para poder trabajar con el VDP se realiza el envío de una secuencia de 2 bytes al puerto de
control. Esta secuencia se utiliza para definir un desplazamiento en la VRAM o CRAM para
la posterior I/O (entrada/salida) del puerto de datos y también, para escribir en los registros
internos del VDP [18][33].
Con el fin de que el VDP pueda saber si recibe el primer o segundo byte, tiene un flag que
se establece después de que el primer byte sea enviado, y se vacía cuando el segundo byte
es escrito.
Este flag también es vaciado cuando se lee del puerto de control, o cuando se lee o se escribe
del puerto de datos. Esto principalmente se realiza para inicializar el flag a 0 después de que
este haya sido modificado impredeciblemente.
179
Para poder saber a qué parte acceder del VDP, este tiene 2 componentes que permiten
indicarlo: El registro de dirección (address register) y el registro de código (code register).
El primero, es de 14 bits y define la dirección en VRAM para lecturas y escrituras, y la
dirección en CRAM para escrituras. El code register, es de 2 bits y selecciona la operación
a realizar.
Cuando se escribe el primer byte, los 8 bits más bajos de los 14 totales disponibles en el
address register, son actualizados. Cuando el segundo byte es escrito, los 6 bits restantes del
address register y los 2 bits del code register son actualizados.
Hay 4 operaciones posibles a realizar: escribir en VRAM, leer de VRAM, escribir en CRAM
y escribir en un registro del VDP. La siguiente tabla resume que operación se realiza en
función del valor del code register.
Tabla 6. Operaciones disponibles del VDP.
Valor del code register Operación que se realiza
00
Se lee un byte de VRAM desde la dirección
definida por el address register y es almacenado
en el buffer de lectura. El address register es
incrementado en 1. Lo que se escriba en el puerto
de datos va a la VRAM.
01
Lo que hay escrito en el puerto de datos ($be) se
dirige a la VRAM, es decir, se escribe en
VRAM.
02 Escribir en un registro del VDP. Solo hay 10
registros disponibles en total.
03
Lo que hay escrito en el puerto de datos ($be) se
dirige a la CRAM, es decir, se escribe en la
CRAM.
180
Cuando se accede a la CRAM, los bits superiores del address register son ignorados ya que
la CRAM es más pequeña que los 16kb de la VRAM. Esto quiere decir que, si se desea
escribir en alguna dirección de la CRAM, el valor hexadecimal que se pase al registro HL
siempre empezará por $C0, ya que, los dos bits más significativos serán 11 y el resto serán
todos 0.
Vamos a observar exactamente cómo funciona todo este proceso de trabajar con el VDP con
un ejemplo. Imaginemos que tenemos el siguiente código:
Figura 116. Paso de información al puerto $bf del VDP.
Fuente: Elaboración propia.
Teniendo en cuenta el código anterior, digamos que en el registro hl, cargamos el valor
$4000. Al ejecutar el código anterior, lo que se haría sería primero enviar al puerto el valor
$00 (00000000) de $4000. Mediante este primer byte que enviamos (y los 6 bits del siguiente
byte que enviaremos a continuación) indicamos la dirección del VRAM (el address register).
Una vez enviado el primer byte al puerto $bf, enviamos el segundo. En h, se encontrará el
valor $40 (01000000) y corresponderá al segundo byte a enviar. De este, los 6 primeros bits
menos significativos hacen también referencia, junto a los bits del byte anterior, a la
dirección del VRAM. Los 2 bits restantes indican la operación a realizar (code register).
Por lo tanto, si nos fijamos en el valor que hemos pasado. Vemos que hemos pasado como
dirección de VRAM el valor 0000000000000 y como operación a realizar el valor 01, que
fijándonos en la figura 13-5, sabemos que corresponde a escribir en VRAM.
Escribir en un registro del VDP
Si se desea escribir en un registro del VDP, se tienen que enviar 2 bytes exactamente de la
misma forma a como se explica en la siguiente tabla.
181
Tabla 7. Formato para poder escribir en un registro del VDP.
En el primer byte, sus 8 dígitos se utilizarán para establecer los datos del registro, es decir,
para especificar que bits del registro son los que se desean activar (1) o desactivar (0). En el
segundo byte que se envía, sus 4 primeros bits menos significativos indicarán el número de
registro al cual se quieren enviar los datos del primer byte, mientras que, en los 4 bits
restantes, los 2 primeros son ignorados y los 2 bits más significativos contendrán los valores
1 y 0.
Registros del VDP
Los registros del VDP permiten configurar los aspectos gráficos de la consola. Existen en
total unos 10 registros en el VDP, cada uno con su función específica. A continuación, se
van a ver uno por uno, que aspectos de la consola pueden ser configurados y/o modificados
en cada uno de ellos:
• Registro $00 – Modo de Control Nº 1
Los bits de este registro, cuando son activados afectan a diversos aspectos de la
visualización de la pantalla. Al ser activados, cada uno tiene las siguientes funciones:
Bit 7: Las 8 columnas de más a la derecha (24-31) de la pantalla no se ven afectadas
por el desplazamiento vertical (vertical scrolling).
Bit 6: Las dos filas superiores (0-1) no se ven afectadas por el desplazamiento
horizontal (horizontal scrolling).
Bit 5: No se visualiza la columna más a la izquierda de la pantalla.
Bit 4: Habilita la interrupción de línea (line interrupt).
Bit 3: Desplaza todos los sprites de la izquierda un carácter (8 pixeles) permitiendo
que sean dibujados correctamente cuando su lado izquierdo está a la izquierda de la
pantalla.
MSB LSB
2º byte 1 0 ? ? R03 R02 R01 R00
1º byte D07 D06 D05 D04 D03 D02 D01 D00
Rxx: Numero de registro del VDP
Dxx: Datos del registro del VDP
?: Bits ignorados
182
Bit 2: Activa el Modo 4 de visualización de pantalla que es una visualización
específica del chip VDP de la SMS. Siguiendo la documentación oficial de Sega este
bit siempre debería estar activado para evitar problemas.
Bit 1: Habilita la altura extra de la pantalla. Siguiendo la documentación oficial de
Sega este bit siempre debería estar activado.
Bit 0: Desactiva la sincronización de la pantalla, la visualización es monocroma. Si
está a 0, la visualización es normal. Siguiendo la documentación oficial de Sega este
bit siempre debería estar a 0.
• Registro $01 – Modo de Control Nº 2
Más bits encargados de controlar la visualización de pantalla:
Bit 7: No se usa. Siguiendo la documentación oficial de Sega este bit siempre debería
estar activado.
Bit 6: Habilita la visualización de la pantalla. Si esta desactivado no se ve ninguna
imagen por pantalla.
Bit 5: Habilita la generación de interrupción del VSync (frame interrupt). La mayoría
de los juegos lo utilizan para controlar sus tiempos más básicos.
Bit 4: Extiende la pantalla 4 filas si el bit 1 del registro $00 está activado. Siempre
desactivado según lo documentación oficial de Sega.
Bit 3: Extiende la pantalla 8 filas si el bit 1 del registro $00 está activado. Siempre
desactivado según lo documentación oficial de Sega.
Bit 2: No se usa. Siguiendo la documentación oficial de Sega este bit siempre debería
estar desactivado.
Bit 1: Al activarse los sprites pasan a ser de 8x16. Si está a 0 se usan los sprites por
defecto (8x8).
Bit 0: Todos los pixeles de los sprites son duplicados en tamaño, obteniendo así
sprites con zoom.
• Registro $02 – Dirección base del nombre de la tabla de VRAM
Establece la dirección base del nombre de la tabla en VRAM. Este nombre tiene una
longitud de $700 bytes. Formato del registro:
183
Tabla 8. Formato del registro $02.
bit 7 6 5 4 3 2 1 0
x x x x d d d M
| | |
Direcc. 13 12 11 10
VRAM d d d 0
La d representa los bits que contienen la dirección base del nombre de la tabla en VRAM.
La x representa los bits que no se usan y la M, los bits de máscara. En la mayoría de los
casos, todos los bits del registro estan establecidos con el valor 1 para otorgar al nombre de
la tabla de VRAM una dirección base de $3800.
• Registro $03 – Dirección base del color de la tabla de VRAM
Este registro, es un registro del chip original del cual deriva el VDP de la SMS.
Debido a ello, sus bits no tienen ningún efecto en la SMS. Aun así, todos sus bits
deberían estar a 1 para un funcionamiento normal.
• Registro $04 – Dirección base del generador de patrones
Al igual que el registro $03, este registro deriva del chip original de VDP. Ninguno
de sus bits tiene algún efecto en la SMS. Aun así, por lo menos los 3 bits menos
significativos deberían estar activados para un funcionamiento normal.
• Registro $05 – Dirección base de la tabla con información de los sprites (Sprite
Attribute Table en inglés)
Se utiliza para establecer la dirección base de la tabla con información de los sprites
en VRAM. Concretamente, contiene información de las coordenadas X-Y y número
de tiles de hasta 64 objetos movibles o sprites. Formato del registro:
Tabla 9. Formato del registro $05.
bit 7 6 5 4 3 2 1 0
x d d d d d d M
| | | | | |
Direcc. 13 12 11 10 9 8 7 …….
VRAM d d d d d d 0 …….
La d representa los bits que constituyen la dirección base de la tabla de VRAM, que
serían los bits 13, 12, 11, 10, 9, 8 y 7. La M representa los bits de la máscara y la x,
los bits que no se usan.
184
En la mayoría de los casos, todos los bits pueden estar a 1 para dar a la tabla de
VRAM una dirección base de $3F00, que corresponde a los últimos 256 bytes de la
memoria de video.
• Registro $06 – Dirección base para la definición de los tiles
Este registro establece la dirección base de las definiciones de los tiles que se usan
para los sprites. Este registro es necesario debido a que solo se usa valor de 8 bits
para almacenar el número del tile de cada uno de los sprites en la tabla con la
información de sprites. Formato del registro:
Tabla 10. Formato del registro $06.
bit 7 6 5 4 3 2 1 0
x x x x x d M M
Si d=0, todos los sprites usan los tiles definidos desde la dirección $0000, (tiles 0-
255), si no, si d=1, todos los sprites usan los tiles definidos desde la dirección $2000
(tiles 256-511). Los 2 bits menos significativos, actúan como una máscara AND
sobre los bits 8 y 6 del índice del tile. La x representan los bits que no se usan.
• Registro $07 – Color de fondo/borde
En este registro, sus 4 bits menos significativos se utilizan para seleccionar el número
del color (de 0 a 15) de la paleta de sprites (2º paleta) para usarlo en el borde de la
pantalla. El resto de bits no tienen ningún efecto.
• Registro $08 – Desplazamiento en X del fondo
Está formado por 8 bits que definen el valor de scroll horizontal. Los 5 bits superiores
indican la columna de inicio y los 3 bits restantes, define el valor del scroll (fine
scroll value en inglés).
En cada scanline (línea de exploración horizontal) el VDP tiene un contador de
columnas que va desde 0 hasta 31. De tal manera que, el pixel exacto en el cual, el
VDP va a empezar a renderizar las columnas, es desplazado hacia la derecha en
función del valor de scroll definido en los 3 bits menos significativos del registro
actual.
Por ejemplo, si el valor del scroll fuese 7 (los 3 bits a 1), entonces las 31 columnas
serían totalmente visibles y el primer pixel de la columna 32 es mostrado en el borde
derecho de la pantalla. Después de cada columna es dibujada, el valor de la columna
inicial es incrementado en 1. Un valor de scroll $00 no produce ningún scroll.
185
Si el bit 6 del registro $00 estuviese activo, el scroll horizontal será arreglado a 0 para
las scanlines de 0 a 15. Este se suele utilizar para crear una barra de estado fija en la
parte superior de la pantalla para juegos de scroll horizontal.
• Registro $09 – Desplazamiento en Y del fondo
Mismo formato que el registro anterior, pero en este registro sus bits definen el valor
de scroll vertical. Sus 5 bits superiores controlan la fila de inicio y los 3 bits más
bajos, el valor de scroll.
En el modo de visualización normal de 192 líneas, el nombre de la tabla tiene un
tamaño de 32x28, por lo que el registro de scroll vertical se ajusta cuando supera 223.
El valor de scroll vertical no se puede cambiar hasta que no haya terminado el periodo
actual de visualización, todos los cambios que se realicen durante este proceso serán
almacenados en una localización temporal y serán utilizados cuando acabe el periodo
de visualización.
• Registro $0A – Contador de líneas
El haz de la televisión barre las líneas horizontales de la pantalla, que reciben el
nombre de raster lines. Este barrido, lo realiza de izquierda a derecha, moviéndose
desde la parte superior de la pantalla hasta la parte inferior. En total hay 192 raster
lines que corresponden a los 192 píxeles del alto de la pantalla.
Al final de cada raster line, se produce un pequeño intervalo en el que el barrido se
apaga hasta que retrocede desde el lado derecho de la pantalla hasta el lado izquierdo
del siguiente raster line y este intervalo, es lo que se conoce como HBlank.
Los 8 bits del registro $0A sirven para definir cada cuantas líneas (raster lines) se
desea que se produzca una interrupción, la conocida como HBlank interrupt o line
interrupt. De esta manera, si se pasa el valor $00 a este registro, se genera una
interrupción por cada línea horizontal, si se pasa el valor $01 se producirá cada 2
líneas horizontales etc. Si se pasa el valor $FF se desactiva la generación de
interrupciones, es decir, no se produce la line interrupt.