CRASH: Cómo programar un juego sencillo en el QL paso a paso

Aunque hace mucho tiempo que no le dedico un rato al QL, sigo con interés las noticias del mundo retro y este blog, QBlog, que tan bien cuida Afx. Me gustan los artículos que leo aquí porque consiguen despertar mi curiosidad, y son, generalmente, eminentemente prácticos.

En una de estas noches en las que me atacó el insomnio, estuve revisando QForum, el foro de QL en castellano, y me encontré con algunos posts antiguos muy interesantes, que creo que merece la pena traer aquí para que lleguen a más gente por este medio. Este juego que explico a continuación es un ejemplo de ello.

crash

Lo que se expone en este artículo es aprovechable también para programar juegos en el BASIC de otros sistemas, así que espero que este contenido sirva más allá del QL.

El juego propuesto se llama CRASH, y nació como ejemplo para mostrar cómo detectar colisiones básicas en SuperBASIC, el BASIC del QL, a raíz de un comentario de Radastán en QBlog. Puedes ir al hilo original haciendo clic aquí.

En el juego se exponen nociones de uso de matrices, de creación de ventanas, scroll y uso de canales.

El artículo es un poco largo, así que respirad hondo. Allá vamos.

Te será muy útil la guía del usuario de SuperBASIC para resolver tus dudas sobre las explicaciones, concretamente la Guía de referencia de palabras clave (Descargar en PDF)

El juego

El juego consiste en pintar una carretera en la que habrá un coche que conduciremos arriba, abajo, izquierda o derecha, con las teclas del cursor, mientras adelantamos coches que iremos encontrando en la carretera sin chocar. El juego acaba cuando te estrellas con otro coche.

Para detectar las distintas posiciones de los coches usaremos caracteres y una matriz DIM como explico luego.

Pongamos que tienes una zona de la carretera de 11 caracteres de ancho por 20 de alto que están definidas en la matriz carretera$(10,19). Esta sería la estructura del programa:

defines micoche=19
defines tope=0
Pintas tu coche en la posición carretera$(6,micoche)
inicias bucle
  borras micoche
  subes el coche una posición: micoche=micoche-1
  si micoche=-1 entonces micoche=19
  cuando detectas colisión, sales del bucle
  pintas micoche
  subes el tope de la carretera tope=tope-1
  si tope=-1 then tope=19
  borras la linea de la carretera: 
    - carretera$(tope)=FILL$(" ",11) 
    - por si hubiese otro coche pintado antes
  Si toca poner un nuevo coche en la carrera:
    pintas un nuevo coche en la pos. (RND(0 to 10),tope)
terminas bucle

Si no has entendido el contenido del bucle no te preocupes, luego lo explico con más detalle.

Caracteres como sprites

Vamos a jugar con los caracteres en vez de utilizar gráficos de píxeles. Simplificamos así la complejidad del juego evitando las rutinas de definición, pintura y colisión de sprites.

En el QL no hay un sistema de atributos por un lado y una pantalla por otro como en el Spectrum. Además, los caracteres no están sujetos a una rejilla de n caracteres de ancho por n de alto, pues podemos escribir un carácter en cualquier ventana, y una ventana podemos ubicarla en cualquier posición x,y (medida en píxeles) de la pantalla, pero es que además, incluso dentro de una ventana, un carácter puede imprimirse en cualquier coordenada de pixels con el comando CURSOR.

Si lo que queremos pues es leer una posición determinada en una rejilla de caracteres figurada, y conocer si allí hay algo, tenemos que establecer una referencia con la pantalla usando una matriz (DIM) de 2 dimensiones.

El listado

Este es el código del juego que iré desgranando a continuación:

100 REMark crash_bas - 2009 - badaman
110 MODE 8: CLS: CLS #0
120 REMark cambia "an" = ancho pista
130 al=9: an=11: REMark vals. 2 a 28
140 REMark cambia "nv" = dificultad
150 nv=3: REMark 1 dific. a 5 facil
160 h=INT(an/2): ch=h+1: v=al: cv=v
170 coche$=CHR$(174): otro$=CHR$(183)
180 lin$=FILL$(" ",an): tp=0: sw=0
190 DIM camino$(al,an): ca=5
200 FOR i=0 TO al: camino$(i)=lin$
210 OPEN #ca,scr_448x200a32x16
220 PAPER #ca,4: CLS#ca: INK#ca,1
230 WINDOW #ca,an*16,(al+1)*20,32+(448-(an*16))/2,16+(200-((al+1)*20))/2
240 PAPER #ca,6: CLS#ca: CSIZE#ca,3,1
250 REPeat carrera
260   ov=v:oh=h
270   IF KEYROW(1)=4 AND v>1 AND cv>1
280     IF camino$(cv-1,ch)<>" " THEN sw=1: ELSE cv=cv-1: v=v-1
290   END IF
300   IF KEYROW(1)=128 AND v<al THEN cv=cv+1: v=v+1
310   IF KEYROW(1)=2  AND ch>1  THEN ch=ch-1: h=h-1
320   IF KEYROW(1)=16 AND ch<an THEN ch=ch+1: h=h+1
330   cv=cv-1: IF cv<0 THEN cv=al
340   AT #ca,ov,oh: PRINT #ca," "
350   SCROLL #ca,20
360   AT #ca,v,h: PRINT #ca,coche$
370   IF camino$(cv,ch)<>" " OR sw THEN EXIT carrera
380   REMark PAUSE 5: REMark quita REM para GoldCar
390   tp=tp-1: IF tp<0 THEN tp=al
400   camino$(tp)=lin$
410   n=RND(0 TO nv)
420   IF NOT n
430     n=RND(1 TO an)
440     AT #ca,0,n-1: PRINT #ca,otro$
450     camino$(tp,n)=otro$
460   END IF
470 END REPeat carrera
480 AT #ca,v,h: PRINT #ca,"*";
490 AT #ca,0,0: PRINT #ca,"Crash!"
500 CSIZE #ca,0,0: CLOSE #ca

Copia y pega el código en un fichero con el nombre crash_bas para ejecutarlo en tu emulador de QL favorito con LRUN crash_bas.

Podemos cambiar el número de coches (1 a 5) que llenan la pista y el alto (1 a 9) y ancho (2 a 28) de la carretera (aunque una carretera de menos de 5×6 caracteres ya es un poco angustioen el modosa). Y todo ello en 40 líneas de código.

Si tenemos GoldCard o un emulador muy rápido, podemos quitar el REMark inicial de la línea 380 para ralentizar un poco el juego.

Truco por Radastán: Prueba a poner REMark delante de la línea 340 😉

Variables y estructura

Veamos la lista de variables empleadas y su uso en el juego:

al=alto de la carretera en caracteres
an=ancho de la carretera en caracteres
nv=nivel de dificultad
h=posición horizontal de nuestro coche en la pantalla en chr.
v=posición vertical de nuestro coche en la pantalla en chr.
ch=pos. horizontal de nuestro coche en la matriz de la carretera
cv=pos. vertical de nuestro coche ne la matriz de la carretera
coche$=letra que define el dibujo de nuestro coche
otro$=letra que define el dibujo de los otros coches
lin$=cadena de caracteres llena de un número de espacios igual al ancho de la carretera
camino$=matriz para guardar los otros coches que aparecen en la carretera
tp=puntero que indica la posición dentro de camino$ donde empieza la carretera
sw=variable swich que sirve para saber si se debe abandonar el juego cuando su valor es 1
ca=número del canal que vamos a abrir para pintar la carretera
ov=valor anterior de v
oh=valor anterior de h
n=variable para saber si debemos pintar un nuevo coche en la carretera o no dependiendo de nv y del azar.

Estructura

La estructura del programa  en líneas es la siguiente:

100-190 Definimos las variables
200 llenamos de espacios la matriz carretera$
210-240 abrimos el canal y pintamos la pantalla
250 abrimos el bucle del juego
260 guardamos los valores de la posición actual del coche en la pantalla en ov,oh
270-320 comprobamos si hemos pulsado una tecla del cursor y actualizamos las variables oportunas
330 actualizamos el puntero de la posición de nuestro coche en la matriz subiendo una posición
340-360 borramos nuestro coche de la pantalla, hacemos scroll vertical y pintamos de nuevo el coche
370 Si hay algo en esa posición, hemos chocado y salimos del bucle del juego
380 Sin REMark el juego se ralentiza
390 actualizamos el puntero del tope o donde empieza la pantalla
400 borramos la fila dela carretera por si hubiese algún coche ya pintado en esa fila
410 elegimos un valor al azar entre 0 y el nivel de juego
420 Si sale 0 entonces pintamos un coche adversario
430 elegimos aleatoriamente la posición del coche adversario a lo ancho de la carretera
440 lo pintamos en la pantalla
450 lo reflejamos en la matriz de la carretera
460 coche adversario pintado
470 vuelta a empezar hasta que salgamos del bucle
480-490, si hemos salido del bucle es que nos hemos chocado, y  pintamos el choque en pantalla
500 cerramos el canal

En detalle

Veamos ahora la programación en si.

180 lin$=FILL$(" ",an): tp=0: sw=0
190 DIM camino$(al,an): ca=5
200 FOR i=0 TO al: camino$(i)=lin$

En la línea 180 llenamos de espacios la variable lin$, y en la 190 definimos la matriz camino$.

Los parámetros de camino$ son alto y ancho. aunque en realidad los valores al y an corresponden a alto y ancho +1.

Cuando definimos esta matriz lo que estamos diciendo en SuperBASIC es que tenemos una lista ‘al’ de cadenas de ‘an’ caracteres de ancho. Dado que una cadena de cualquier longitud numera sus caracteres del 1 a n, siendo n la longitud total de la cadena, no podemos usar el valor 0 de ‘an’.

En la línea 200 lo que hacemos es pues asignar a cada cadena de la lista camino$(0), camino$(1)… la cadena de caracteres lin$ para limpiar toda la matriz con espacios.

En la línea:

400   camino$(tp)=lin$

lo hacemos de nuevo.

En otros BASICs tendríamos que hacer dos bucles anidados, uno para poner el valor ‘al’ y otro para poner el valor ‘an’.

En estas líneas vemos como el valor de pantalla y el valor de la matriz varían en 1:

430     n=RND(1 TO an)
440     AT #ca,0,n-1: PRINT #ca,otro$
450     camino$(tp,n)=otro$

Ya que si pintamos caracteres en la pantalla, estos pueden pintarse en las posiciones n,0, no así en la matriz camino$(n,0) como hemos visto.

Vamos con más cosas.

210 OPEN #ca,scr_448x200a32x16
220 PAPER #ca,4: CLS#ca: INK#ca,1
230 WINDOW #ca,an*16,(al+1)*20,32+(448-(an*16))/2,16+(200-((al+1)*20))/2
240 PAPER #ca,6: CLS#ca: CSIZE#ca,3,1

en la línea 210 abrimos un nuevo canal.

Por cierto, hemos usado una variable para el canal ‘ca’ para que, en cualquier momento podamos cambiar el número de canal fácilmente por si ya existiese ese canal abierto por otro programa en ejecución. Es una buena costumbre hacer siempre esto, de igual forma que es conveniente siempre definir una variable de tipo cadena para guardar la información relativa al dispositivo de almacenamiento que queremos usar, por ejemplo ‘mdv1_’, ‘flp1_’, ‘ram1_’, ‘win1_’… Luego podemos usar este dato dentro del programa. Por ejemplo:

100 disp$="flp1_"
110 DIR disp$

Volviendo a lo que estábamos, hay que saber que cuando abrimos un canal de tipo ‘scr_’ (pantalla), o ‘con_’ (consola) no podemos usar variables para definir sus parámetros. Así pues, si queremos abrir un canal en pantalla con valores que estén almacenados en variables, debemos primero abrir el canal con unos parámetros fijos, y luego cambiar los valores con el comando WINDOW, como hago en la línea 230.

Podemos también abrir el canal sin pasar parámetros:

OPEN #ca,scr_

En este caso, el sistema toma los parámetros por defecto: 448x180a32x16

O bien pasarle los valores de el ancho y alto de la pantalla completa en pixels y luego usar CLS para borrar toda la pantalla:

OPEN #ca,scr_512x256a0x0: CLS #ca

De cualquier forma, luego deberemos usar WINDOW para definir el tamaño de ventana con el valor de las variables que definimos.

El valor que uso por defecto es el del canal 1 y canal 2 cuando arrancamos el ordenador en el modo 8 (modo 256, modo TV).

CSIZE es un comando que nos permite usar un tamaño de letra mayor en pantalla. El valor CSIZE #ca,3,1 indica un carácter el doble de alto y de ancho que uno normal.

En la línea:

500 CSIZE #ca,0,0: CLOSE #ca

Devolvemos el tamaño de caracteres a su valor original (aunque no es necesario en el juego, ya que a continuación…) y cerramos el canal definitivamente.

La línea:

350   SCROLL #ca,20

Es la que más ‘juego’ nos dá, pues nos permite mover el contenido de la ventana o canal #ca hacia abajo. En otros BASICs tendríamos que mover una por una las posiciones de los coches contrarios o crear una rutina de scroll en C/M.

El valor 20 corresponde a la altura de los caracteres en pixel cuando el valor de CSIZE es 3.1 De esta forma hacemos un scroll sincronizado con los caracteres.

Detección de colisiones

Partimos de la definición de matriz camino$(al,an) y los datos de partida relacionados.

Pongamos que al=6 (0 a 5) y an=7. Al iniciar el juego, los valores de las variables serán los siguientes:

  1234567
0 ...E... <- tp
1 .......
2 .......
3 .......
4 .......
5 ...C... <- cv
     ^
    ch

Recordemos que tp indica la posición mas alta de la carretera, y es la línea donde aparecen nuevos coches en la carretera. por su parte, cv indica la posición vertical de nuestro coche en relación con la matriz (no con la pantalla). La variable ch nos indicaría la posición de nuestro coche horizontalmente.

Pintamos de espacios en blanco (línea 400) y en la posición (0,4) pintamos un coche enemigo (E).

Si no movemos las flechas del cursor, a cada ciclo restamos 1 a tp y a cv (líneas 330 y 390) y comprobamos que los valores sean siempre mayores o iguales a 0. Si llegamos a 0 en cualquiera de los dos punteros lo que hacemos es asignar al puntero el valor ‘al’.

Restando 1 a ambos punteros lo que hacemos es el efecto de recorrer hacia arriba la matriz como si fuese una cinta continua en vez de mover los mismos valores de la matriz hacia abajo.

Veamos lo que pasa a la siguiente vuelta:

  1234567
0 ...E...
1 .......
2 .......
3 .......
4 ...C... <- cv
5 .E..... <- tp

Vemos que el coche enemigo no se ha movido, pero ahora el puntero que indica donde está nuestro coche ha subido una posición acercándose a la línea del coche enemigo.

Por su parte, el puntero tp, al tener un valor menor que 0, ha pasado a apuntar a la posición 5 de la matriz. Y en esa línea ha pintado también un coche enemigo.

Si repetimos el proceso varias veces, veremos que tp siempre estará, como mínimo, una fila por detrás de cv, aun cuando movamos nuestro coche hacia abajo.

Veamos que pasa tras 3 vueltas más:

  1234567
0 ...E...
1 ...C... <- cv
2 .....E. <- tp
3 .......
4 ......E
5 .E.....

Los punteros han ido subiendo por la matriz, y en las líneas apuntadas por tp se han puesto coches enemigos al azar.

Podemos intuir que pasará en la siguiente vuelta, cuando cv=0. Nos encontramos entonces con que en la posición ch hay un coche enemigo. En ese momento hemos chocado (línea 370).

De no haber habido ahí un coche, seguiríamos adelante volviendo a la primera posición dibujada, donde tp=0 y cv=5.

Como hemos ido borrando las líneas donde apunta tp, el circuito se va renovando a cada vuelta.

En todos estos casos, en la pantalla han pasado solo tres cosas: hemos borrado nuestro coche, hemos hecho scroll, y hemos pintado de nuevo nuestro coche en la misma posición.

Moviendo nuestro coche en todas direcciones

La cosa no es más complicada si añadimos movimiento al coche.

Si desplazamos el coche a izquierda o derecha, la cosa no se ve muy afectada. lo único que cambia es el valor de ch. pero si movemos el coche arriba o abajo la cosa es distinta.

Si movemos el coche hacia abajo comprobaremos nos ponemos como tope el final de la carretera, lo que en la matriz va a coincidir con la posición tp-1 siempre que tp>0. Vamos, que se cumple eso de que tp siempre estará por debajo de nuestro coche.

Si movemos el coche hacia arriba nos separamos de tp, pero no lo superaremos por arriba.

Veamos mejor un ejemplo. si usamos la tecla de subir para mover el coche 1 posición hacia arriba.

Partimos de la siguiente posición:

  1234567
0 ...E...
1 .......
2 .......
3 .......
4 ...C... <- cv
5 .E..... <- tp

Y pulsamos la tecla subir (recordemos que el bucle resta 1 a tp y a cv)

  1234567
0 ...E...
1 .......
2 ...C... <- cv
3 .......
4 ....... <- tp
5 .E.....

la linea apuntada por cv esta a 2 líneas de la apuntada por tp.

cv podrá seguir separándose de tp cada vez que pulsemos la tecla arriba, pero nunca superará a tp. Y no es que tp esté entonces por encima de cv, es que cv le lleva una vuelta de ventaja.

Si usamos la tecla abajo después del ejemplo anterior, volveremos a acortar la distancia entre cv y tp:

  1234567
0 ...E...
1 .......
2 ...C... <- cv
3 ....... <- tp
4 .......
5 .E.....

Parece que cv no se ha movido, pero en realidad cv debería estar en la posición 1 debido a que a cada ciclo, como hemos dicho, restamos 1 a cv y a tp.

En los dos últimos ejemplos no han aparecido nuevos coches enemigos. Es cosa del azar.

Y como se verá todo esto en la pantalla pues solo haremos esto a cada vuelta: borrar nuestro coche, hacer scroll y pintar de nuevo el coche en la posición v,h.

Sin el comando SCROLL, como decía más arriba, tendríamos que repintar también cada uno de los coches enemigos o hacernos una rutina de scroll en C/M. Así es más sencillo (gráficamente hablando claro).

Y eso es todo

…o no. Porque esto puede ser el comienzo. ¿Para que puede servir todo lo explicado respecto a las matrices y al scroll? Pues para despertar tu imaginación. Por ejemplo, con algo más de trabajo en el código, podemos convertir nuestro coche en una nave espacial que debe disparar a otras naves que encuentre de frente en un scroll vertical, o bien dibujar un super héroe que se desplace en un scroll lateral a izquierda y derecha de la pantalla volando sobre los tejados de una ciudad y sorteando a los malos…

Espero que os animéis a poner vuestras preguntas y a crear vuestros propios juegos para este ordenador retro que también sirve para jugar y aprender.

Anuncios
2 comentarios
  1. afx dijo:

    ¡Fantástico!

    Este es el tipo de post que me encanta. Ideal para una tarde entretenida con algo de cacharreo-retro, lectura y programación.

    Badaman, esa es una buena idea, ir rescatando algunos viejos hilos de qforum y darles forma de post aquí en QBlog.

  2. badaman dijo:

    Sí, hay que repetir la experiencia. A ver si más gente se anima a trastear.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s