Crea tus Juegos con 8BPCursos

8BP: Programación con lógicas masivas

5
(5)

¡Hola amigos de 8 Bits de Poder! Hoy venimos con un artículo importante, y ya os avanzo que algo extenso, puesto que trataremos el control de las lógicas masivas.

Uno de nuestros principales objetivos en la creación de un juego, para un microordenador de 8 Bits, es poder controlar de forma masiva los movimientos de los sprites, sin por ello sobrecargar los procesos en la CPU de nuestro Amstrad CPC.

No solo debemos cumplir el objetivo de hacer un bonito juego, sino que además, este debe ser jugable. Para ello debemos tener muy en cuenta cuales son las capacidades de gestión de nuestro ordenador, además del funcionamiento del intérprete Basic de Locomotive Software.

Descubre 8BP

El intérprete BASIC es muy pesado en ejecución, debido a que no solo ejecuta cada comando, sino que analiza el número de línea, realiza un análisis sintáctico del comando introducido, valida su existencia, el número y tipo de parámetros, que sus valores se encuentren en rangos válidos (por ejemplo, PEN 40 es ilegal) y muchas más cosas.

Es el análisis sintáctico y de semántica de cada comando lo que realmente pesa, y no tanto su ejecución. El caso de los comandos RSX no es una excepción. El intérprete BASIC comprueba su sintaxis, y eso pesa mucho, a pesar de que sean rutinas escritas en ensamblador, ya que antes de invocarlas, el intérprete BASIC ya ha hecho muchas cosas.

Medición de la velocidad de los comandos

Por consiguiente, hay que ahorrar ejecuciones de comandos, programando con astucia para que la lógica del programa pase por el menor número de instrucciones posibles, aunque ello a veces implique escribir más. Una práctica indispensable es: usar instrucciones que muevan o afecten a un grupo de sprites, tales como COLSPALL, AUTOALL o MOVERALL, evitando el uso de bucles con instrucciones que afectan a un solo sprite.

logicas masivas
Usa lógicas masivas con los sprites

Un factor decisivo a la hora de invocar un comando, es el paso de parámetros. Cuantos más parámetros tiene, más costoso es su interpretación por parte del BASIC, incluso aunque sea una rutina escrita en ensamblador que se invoque por CALL, pues el comando CALL sigue siendo BASIC, y antes de acceder a la rutina escrita en ensamblador, se analiza el número y el tipo de los parámetros, irremediablemente.

Lógicas masivas en Amstrad CPC

Para evaluar el coste de ejecución de un comando, puedes usar el siguiente programa:

1 call &6b78
10 MEMORY 23999
11 DEFINT a-z
12 c%=0: a=2
30 FOR i=0 TO 31:|SETUPSP,i,0,0:NEXT:'reset
31 iteraciones=1000
40 a!= TIME
50 FOR i=1 TO iteraciones
60 <aqui pones un commando, por ejemplo, PRINT “A”>
70 NEXT
80 b!=TIME
90 PRINT (b!-a!): rem lo que tarda en unidades de tiempo cpc. (1/300 segundos)
100 c!=((b!-a!)*1/300)/iteraciones: rem c! = lo que tarda cada iteracion en segundos
120 d!=(1/50)/c!
130 PRINT "puedes ejecutar ",d!, "comandos por barrido (1/50 seg)"
140 PRINT "el comando tarda ";(c!*1000 -0.47);"milisegundos"     

También te servirá para evaluar el rendimiento de nuevas funciones en ensamblador que incorpores a la librería 8BP, si deseas hacerlo.

Nota para los expertos en lenguaje ensamblador: debéis tener en cuenta que si pretendéis medir el tiempo de ejecución de una rutina que internamente desactiva las interrupciones (usa las instrucciones DI, EI) el tiempo que transcurre durante la desactivación no es medible con este programa BASIC. Los comandos de 8BP no desactivan las interrupciones, y son todos medibles.

Vamos a ver a continuación el resultado del rendimiento de algunos comandos (medidos con el programa anterior). Hay que decir que, es más rápido ejecutar una llamada directa a la dirección de memoria (un CALL &XXXX) que invocar el comando RSX correspondiente.

En la siguiente tabla, obviamente, cuanto menor sea el resultado (expresado en milisegundos), más rápido es el comando. La tabla que aquí se presenta, debes tenerla en todo momento presente y tomar tus decisiones de programación, en base a ella. Es una tabla con medidas de comandos BASIC y comandos 8BP.

ComandoMilisegundos
PRINT “A”3.63
Lentísimo. Ni se te ocurra usarlo, salvo, puntualmente, para cambiar el número de vidas, pero no imprimas puntuación en un juego por cada enemigo que mates.
LOCATE 1,1 PRINT puntos24.87
Colocar el cursor de texto con LOCATE e imprimir el valor de una variable “puntos” es muy costoso. Si actualizas puntos, hazlo sólo de vez en cuando y no en cada ciclo de juego.
C$=str$(puntos) |PRINTAT,0, y, x, @c$10.00
Imprimir los puntos usando PRINTAT es mucho mas eficiente que usar PRINT, pero aun así es costoso. Usa PRINTAT con moderación.
REM hola0.20
Los comentarios consumen.
‘ hola0.25
Ahorras 2 bytes de memoria. ¡Pero es más lento
GOTO 600.19
¡Muy rápido! Más rápido incluso que REM. Usa este comando sin piedad. ¡Úsalo!
A = 3
A = B
A = miarray(x)
A = miarray(x,y)
0.55
0.72
1.33
1.84
Una simple asignación cuesta. Todo cuesta, cada instrucción debe ser pensada. Asignar el valor de una variable a otra es más costoso que asignar un valor. Y asignar el valor de un array es todavía más costoso, porque acceder al array, cuesta. Y si el array es bidimensional, todavía cuesta más.
|LOCATESP,i,10,20
|LOCATESP,i,y,x
CALL &XXXX,i,x,y
2.8
3.22
1.81
Si no usas coordenadas negativas, es mejor usar el comando BASIC POKE para establecer coordenadas. Si las coordenadas son variables, entonces tarda más. El equivalente CALL es mucho más rápido.
|MOVER,31,1,1
CALL &XXXX,31,1,1
3.23
1.77
Es algo lento y por ello debes usarlo con moderación. El equivalente CALL es mucho más rápido.
POKE &XXXX, valor0.71
¡Muy rápido! Úsalo para actualizar las coordenadas de los sprites (si son positivas). POKE no acepta números negativos, pero puedes usar la formula 255+x+1 si quieres meter un numero negativo. Por ejemplo, para meter un -4 debes meter 255-4+1=252. Otra forma sencilla de meter positivos y negativos es usar POKE dirección, x and 255.
POKE dir,dato0.85
Muy rápido, teniendo en cuenta que además debe traducir la variable “dir”.
|POKE,&xxxx,valor2.50
Permite números negativos y si sólo actualizas una coordenada (X o Y). Es mejor que LOCATESP.
X=PEEK(&xxxx)0.93
¡Muy rápido! Según el tipo de videojuego, puede ser una alternativa a COLSP, mirando el color de una dirección de memoria de pantalla. En el apéndice sobre la memoria de vídeo, te explico como hacerlo.
X=INKEY(27)1.12
Muy rápido. Apto para videojuegos, aunque debes usarlo inteligentemente, como se recomienda en este libro.
IF x>50 THEN x=01.42
Cada IF pesa. Hay que tratar de ahorrarlos, porque una lógica de juego va a tener muchos.
IF A=valor THEN GOTO 100
vs
IF A=valor THEN 100
1.24
vs
1.18
Ambas sentencias son equivalentes, pero la segunda tarda menos.
IF inkey(27)=0 then x=51.75
Aceptable. Es más rápido que hacer b=INKEY(27) y después el IF…THEN
10 If inkey(27) then 30
20 x=5
30 …instrucciones
1.0
Una forma mucho más eficiente de hacer lo mismo.
IF x>0 then
vs
IF x then
1.30
vs
0.80
En BASIC es posible ahorrar 0.50ms teniendo en cuenta que cualquier valor distinto de cero significa TRUE. Si queremos controlar un valor concreto haremos: 10 IF x-20 THEN 30 20 …cosas a hacer si x=20 30 … El uso de esta técnica es muy recomendable en la lectura de teclado.
A=A+1: IF A>4 then A=0
vs
A=A MOD 3 +1
2.60
vs
1.70
Este es un ejemplo clarísimo de como debemos programar. Es mucho mejor usar la segunda opción. Por otro lado, el uso de MOD hay que hacerlo con cautela. Si hacemos: A=(A+1) MOD 3 nos cuesta 2 ms, ya que los paréntesis son muy costosos y sin embargo, conseguimos lo mismo. Hay una forma mejor de hacerlo, con el operador binario AND.
A=1+A AND 7
A=20 +A MOD 7
A=21 + (A and 7)
1.60
1.88
1.95
Esto te permite hacer cambiar una variable cíclicamente entre N valores, de modo que te sirve para elegir un sprite ID para tu nuevo disparo, o para un enemigo que entra en pantalla. Es mejor usar AND que MOD, ya que AND es una operación binaria rápida, y MOD implica una división, muy costosa para nuestro querido microprocesador Z80. Sin embargo, si necesitamos usar sprites ID que no comiencen en 1, entonces necesitaremos paréntesis y la ventaja de velocidad del AND se pierde.
:0.05
No ahorra mucho, pero es más rápido usar “:” en lugar de un nuevo número de línea, y si aplicas esto muchas veces acabas ahorrando de forma significativa. Dos instrucciones en dos líneas gastan 0.03ms más que si ambas están en la misma línea separadas por “:”.
|PRINTSP,0,10,105.10
Un solo sprite de 14 x 24 (7 bytes x 24 líneas). Ojo, si vas a imprimir varios, compensa mucho más imprimir todos los sprites de golpe con PRINTSPALL.
CALL &xxxx,0,10,103.50
Equivalente a PRINTSP. Así es más rápido, aunque menos legible.
|PRINTSPALL
(32 sprites 8×16 de mode 0, es decir 4 bytes x 16 lineas)
57.00
Esto son unos 17 fps a plena carga de sprites. Lo que tarda es T = 3.25 + N x 1.7, es decir, 1.7 ms por sprite y un coste fijo de 3 ms. Este coste fijo, es el coste del análisis sintáctico de BASIC, sumado al de recorrer la tabla de sprites buscando cuales hay que imprimir. Si se omiten los parámetros (es posible y se tomarían los valores de la última invocación), se ahorran 0.6ms en la parte fija, es decir, T = 2.6 + N x 1.1. Si la impresión es con sobre escritura y/o flipeada, es más costosa. A continuación, se muestran los costes relativos de cada tipo de impresión:
Impresión normal: 100%
Impresión con sobre escritura: 164%
Impresión flipeada: 179%
Impresión flipeada con sobre escritura: 220%
|PRINTSPALL,N,0,0
(ningún sprite activo)
N=0
N=10
N=31
2.60
4.30
5.90
Coste de ordenar los sprites. Cuando N=0, no habiendo ningún sprite que imprimir, la función debe recorrer la tabla de sprites de forma secuencial. Pero recorrerla de forma ordenada es más costoso, tal como evidencia el tiempo consumido al aumentar N. La diferencia de tiempos (5.9 -2.6 =2.5ms) es lo que cuesta ordenar todos los sprites.
|COLAY,@x%,0
vs
|COLAY
3.00
vs
2.40
Usar solo con el personaje, no con los enemigos o el juego irá lento. Si el personaje mide múltiplos de 8, es más rápido. En este ejemplo era de 14×24 y lógicamente, 14 no es múltiplo de 8. Cuanto mayor es el sprite, más tarda. ¡Si invocas el comando sin parámetros es mucho mas rápido! (Ahorras 0.6 ms).
|COLAY
vs
CALL &XXXX
2.40
vs
2.00
Usar CALL como siempre es más rápido, pero menos legible.
GOSUB / RETURN0.56
Aceptablemente rápido. La medida la he hecho con una rutina que solo hace return.
|SETUPSP, id, param, valor2.70
Aceptable, aunque POKE es mucho mejor para ciertos parámetros. Hay parámetros que se pueden establecer con POKE, como el estado, pero otros no, como una ruta. Consulta la guía de referencia.
FOR / NEXT0.60
Lo puedes usar para recorrer varios enemigos, y que cada uno se mueva de acuerdo a una misma regla. Debes valorar si puedes usar AUTOALL o MOVEALL para tus propósitos, ya que con un solo comando moverías a todos los que quieras, lo cual es mucho mejor que un bucle.
|COLSP,0, @c%5.50
Tarda lo mismo, con independencia del número de sprites activos. Esta rutina la tendrás que invocar en cada ciclo de la lógica de tu juego, de modo que son casi 5ms que obligatoriamente tienes que destinar a esto. Si tienes una nave o personaje, y varios disparos, es mucho más eficiente que invoques a COLSPALL, en lugar de invocar varias veces a COLSP.
|ANIMALL3.50
Es costoso, pero hay una forma de invocarlo conjuntamente al invocar |PRINTSPALL, mediante un parámetro que hace que se invoque a esta función, antes de imprimir los sprites. Ello permite ahorrar la capa del BASIC, es decir, lo que consume enviar el comando, que es >1ms. Por ello podemos decir que este comando consumirá normalmente algo menos de 2ms.
|AUTOALL2.76
No es costosa y puede mover a la vez los 32 sprites.
|MOVERALL,1,13.40
No es muy costosa y puede mover a la vez los 32 sprites.
SOUND10.00
El comando sound es “bloqueante”, en cuanto se llena el buffer de 5 notas. Esto significa que tu lógica de BASIC no debe encadenar más de 5 comandos SOUND, o se parará hasta que alguna nota termine. Si decides usarlo, debe ser con sumo cuidado, ya que consume mucho tiempo en su ejecución (10 ms es muchísimo).
IF a>1 AND a>2 THEN a=2
vs
IF a>1 THEN IF a>2 THEN a=2
2.52
vs
2.39
Una sencilla forma de ahorrar 0.13 ms. En cada cosa que programes, ten en cuenta estos detalles: cada ahorro es importante.
A=RND*104.20
La función RND de BASIC es muy costosa. Puedes usarla, pero no en cada ciclo de juego, sino solo eventualmente, por ejemplo, cuando aparezca un nuevo enemigo o cosas así. Otra solución sencilla es almacenar 10 números aleatorios en un array, y utilizarlos en lugar de invocar a RND.
BORDER <x>0.75
Bastante rápida. Útil para usarla en combinación con algún tipo de colisión de sprites, reforzando el efecto explosivo.
IF a AND 7 then 30
vs
IF A MOD 8 then 30
1.19
vs
1.29
He puesto el tiempo de ejecución cuando se cumple la condición. Ambos casos son bastante rápidos.
8BP: Programación con lógicas masivas 1
Lógicas masivas, el tiempo importa

Recomendaciones importantes sobre el uso de comandos Basic en 8BP

  • Usar DEFINT A-Z al principio del programa. El rendimiento mejorará muchísimo, esto es casi obligatorio. Este comando borra las variables que existiesen antes, y obliga a que todas las nuevas variables sean enteros a menos que se indique lo contrario con modificadores como “$” o “!” (Consulta la guía de referencia de programación BASIC de Amstrad). Ojo, en cuanto uses DEFINT, si quieres asociar un número mayor que 32768, tendrás que hacerlo en hexadecimal.
  • Si puedes, evita pasar por un IF insertando un GOTO, siempre será preferible.
  • Cuando te falte velocidad, y necesites un poquito más de rapidez, utiliza CALL <dirección> en lugar de RSX. En caso de hacer esto, has de pasar los parámetros que contengan números negativos en hexadecimal.
  • No sincronices el comando PRINTSPALL con el barrido de pantalla a menos que tu juego funcione muy rápido. Sincronizar puede reducir tus FPS. En general, con que consigas 12 FPS, tu juego será “jugable”.
  • Elimina espacios en blanco. Cada espacio en blanco en tu listado BASIC consume 0.01ms en ejecución.
  • Una vez que hayas invocado con parámetros al comando STARS, o al comando PRINTSPALL, o a COLAY, o a otros comandos de 8BP, las siguientes veces no lo invoques con parámetros. La librería 8BP tiene “memoria” y usará los últimos parámetros que usaste. Esto ahorra milisegundos al atravesar la capa de análisis sintáctico del intérprete BASIC.
  • Ten siempre en cuenta que, una expresión diferente de cero es TRUE. Esto te permitirá ahorrar 0.5ms en cada IF y lo puedes usar en la lectura de teclado y en el control de variables.
Mala opciónBuena opción (ahorras 0.5ms)
IF x<>0 THEN <instrucciones>IF x THEN <instrucciones>
IF x=20 THEN…10 If x-20 THEN 30 20 <instrucciones> 30
IF INKEY(34)=0 THEN <instrucciones>10 IF INKEY(34) THEN 30 20 <instrucciones> 30
Lógicas masivas, buenas y malas opciones
  • En juegos de naves, donde no uses sobre escritura, procura que tu nave sea el sprite 31, de este modo pasará por “encima” de los sprites que simulan ser el fondo, pues tu nave se imprimirá después.
  • Prueba versiones alternativas de una misma operación. Por ejemplo:
A=A+1:IF A>4 THEN A=0 : REM esto consume 2.6ms
A=A MOD 3 +1 : REM esto consume 1.84 ms
A=1 + A AND 3 : REM esto consume 1.6 ms
Lógicas masivas, prueba distintas alternativas
  • Evita el uso de coordenadas negativas. Ello te permitirá usar POKE para actualizar la posición de tu personaje. El comando POKE (el de BASIC) es muy veloz, pero solo soporta números positivos, al igual que PEEK. En caso de usar coordenada negativas, usa |POKE y |PEEK (comandos de 8BP). Reserva el uso de |LOCATESP para cuando vayas a modificar ambas coordenadas a la vez, y puedan ser positivas y/o negativas. Recuerda también que, un POKE de un valor x negativo se puede hacer usando POKE dirección, 255+x+1. En caso de que quieras usar coordenadas negativas para que se vea como poco a poco los enemigos entran a la pantalla, por el lateral izquierdo (que se perciba el clipping), puedes evitar las coordenadas negativas usando un SETLIMITS y de esa manera producir el mismo efecto con coordenadas que comienzan en cero, y una pantalla de juego ligeramente más pequeña.
  • Si necesitas comprobar algo, no lo hagas en todos los ciclos de juego. A lo mejor basta que compruebes ese «algo» cada 2 o 3 ciclos, sin ser necesario que lo compruebes en cada ciclo. Para poder elegir cuando ejecutar algo, haz uso de la «aritmética modular». En BASIC dispones de la instrucción MOD, que es una excelente herramienta. Por ejemplo, para ejecutar una de cada 5 veces puedes hacer : IF ciclo MOD 5 = 0 THEN… aunque es mejor que uses operaciones AND que operaciones MOD.
  • Haz uso de las «secuencias de muerte». Ello te permitirá ahorrar instrucciones para comprobar si un sprite, que está explotando, ha llegado a su último fotograma de animación para desactivarlo.
  • La sobre escritura es costosa: si puedes hacer tu juego sin sobre escritura, ahorrarás milisegundos y ganaras colorido. Úsala cuando la necesites, pero no sin motivo.
  • Las macro secuencias de animación te ahorran líneas de BASIC, ya que no necesitas chequear la dirección de movimiento del sprite. Úsalas siempre que puedas.
logicas masivas
Lógicas masivas

En el próximo artículo de 8 Bits de Poder, hablaremos de como gobernar las distintas pantallas en la memoria de nuestro ordenador, haciendo uso de «lógicas masivas» o «massive logics». Quizás, a alguno le suene un poco esto 🙂

Toda la información de 8 Bits de Poder la puedes encontrar en el repositorio Github de Jjaranda.

¿De cuánta utilidad te ha parecido este contenido?

¡Haz clic en una estrella para puntuar!

Etiquetas

Un comentario

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Publicaciones relacionadas

Botón volver arriba
Cerrar
Cerrar
Configuración de Cookie Box