Tras montar mi kit de desarrollo cruzado con el Visual Code, vcc y vasm como puse aquí, me puse manos a la obra. Pues bien, abro este hilo con la intención de compartir con vosotros lo que vaya haciendo y pedir consejo e ideas. Sobre todo esto último. Tengo la impresión de que, a veces, estoy matando moscas a cañonazos, además de que llegará un momento en que necesite optimizar y empezarán mis dudas con el uso del blitter.
Os aviso. Esto es un

He tenido que sustituir en el código todos las sentencias if por si o me daba error al postear

A ver, sin entrar en rollos, sabéis ya lo que son los juegos con perspectiva isométrica: básicamente un truco para hacer un falso 3D. En plan filmation como Knight Lore. Y si no, a la Wikipedia.
Primero las malas noticias. No hacemos 3D real (es decir no hay proyecciones ni escalados), así que no hay un método general que funcione bien. Es necesario imponer restricciones. La primera que vamos a poner es que todos los bloques sean del mismo tamaño.
Voy a mantenerlo todo lo más sencillo posible, así que queremos usar bloques como éste (con su máscara):
Lo he hecho de 32 pixels de ancho y de 8 pixels de alto (me gustan bajitos, pero eso es cosa mía). Como véis le he puesto una máscara, porque vamos a pintarlo como un BOB usando el blitter tal y como dice Mlake en su curso. De momento estoy usando 4 bitplanes y el modo entrelazado.
Para hacerlo todo muy simple, supongamos que queremos renderizar algo como esto:
Veis que es una especie de suelo de 10x10 bloques (ocupa 320 pixels de ancho). Supongamos que el mapa es una matriz de 10x10 que contiene un 1 donde queremos poner un bloque y un 0 donde haya un hueco. Ahora son todo 1s. Supongamos que tenemos ya el código que usa el blitter para pintar un BOB en unas coordenadas de pantalla (x,y). Le pasamos el código del bob (un 1, por ejemplo) y la rutina sabe cuál es el gráfico y su máscara.
Para determinar la posición en pantalla de un bloque en unas coordenadas de mapa (i,j) podemos usar una función como ésta:
Código: Seleccionar todo
void isoToScreen(int i, int j, int *x, int *y){
*x = (j - i) + (160-ancho_tile/2);
*y = (i + j) / 2 + (32+ alto_tile);
}
Así, sin optimizar ni nada. 160 es justo la mitad de la pantalla, 32 es la fila donde empezamos a pintar (podía ser otro valor), ancho_tile vale también 32 y alto_tile es justo la mitad del ancho (cosas de la perspectiva isométrica), es decir 16. Pero no le hagáis caso. El problema no es ahora mismo dónde va cada bloque, sino en qué orden los tenemos que ir pintando. Hay varias alternativas, pero la idea es ir de la esquina de arriba hacia abajo por filas. Algo así:
Os pongo el código C que implementa este bucle de renderizado. Se basa en que pintamos 19 filas de bloques. Empezamos en mitad de la pantalla pintando un bloque. Bajamos el alto de un bloque, nos desplazamos la mitad del ancho a la derecha y pintamos dos. Después repetimos y pintamos 3 y así sucesivamente hasta llegar a la mitad (la fila 10), donde pintas 10 bloque y a partir de ahí todo al revés.
Código: Seleccionar todo
void RenderRoom(){
int xx, xd, yd;
int l, k, length, jo;
int i, j;
int tile_code;
xx = 160 - (ancho_tile / 2);
xd = xx;
yd = 32 + alto_tile;
l = 0;
k = 0;
length = 0;
jo = 0;
for (i = 0; i <= 18; i++){
for (j = jo; j <= length; j++){
// Get the tile to draw
k = j;
l = length + jo - j;
tile_code = map[k][l];
si (tile_code)
drawBlock(tile_code-1, xx, yd);
xx += ancho_tile;
}
si (i < 9){
length++;
xd -= (ancho_tile / 2);
}
else{
jo++;
xd += (ancho_tile / 2);
}
xx = xd;
yd += (alto_tile / 2);
}
}
Hala, ya está. Además vamos calculando las posiciones (x,y) en pantalla, así que no necesitamos tirar de la función isoToScreen (ya os dije que, por ahora, ni caso). Por supuesto esto está sin optimizar. Se podría precalcular todo, almacenar los bloques ordenados así y con sus coordenadas en pantalla para posteriores renderizados y esas cosas. Tampoco estamos renderizando una escena que se salga por los límites de la pantalla ni nada. Pero por ahora es suficiente.
Pero así sólo tenemos el suelo, sin nada encima. El siguiente paso es poder añadir bloques sobre estos, es decir, más niveles. Esto es relativamente fácil. Basta con que nuestro mapa sea no solo una matriz de 10x10, sino que tenga una dimensión más. Podemos, entonces poner un bucle externo e ir pintando por niveles: primero el más bajo, luego el siguiente y así. Como las coordenadas y de pantalla crecen hacia abajo, para pintar un nivel encima de otro, hay que restar el alto de cada bloque (en este caso 8 pixels) multiplicado por el nivel que sea. El bucle queda:
Código: Seleccionar todo
void RenderRoom(){
int xx, xd, yd;
int l, k, length, jo, h;
int i, j;
int tile_code;
// Go through all layers
for (h = 0; h < num_layers; h++){
xx = 160 - (ancho_tile / 2);
xd = xx;
yd = 32 + alto_tile;
l = 0;
k = 0;
length = 0;
jo = 0;
for (i = 0; i <= 18; i++){
for (j = jo; j <= length; j++){
// Get the tile to draw
k = j;
l = length + jo - j;
tile_code = map[h][k][l];
si (tile_code){
drawBlock(tile_code-1, xx, yd-h*layer_heigth);
}
xx += ancho_tile;
}
si (i < 9){
length++;
xd -= (ancho_tile / 2);
}
else{
jo++;
xd += (ancho_tile / 2);
}
xx = xd;
yd += (alto_tile / 2);
}
}
}
De nuevo se podría optimizar, por supuesto. Os aseguro que esto funciona, pero sólo acabamos de empezar. Si os fijáis todos los bloques están como "atados" a posiciones fijas en una rejilla. Esto funciona bien para un escenario (imaginad el terreno y las construcciones en el juego Populous), pero fijo que vamos a querer montar sobre ese escenario personajes y objetos que no tengan esta restricción, y que puedan moverse suavemente pasando por posiciones intermedias y, por tanto, "pisar" más de una baldosa del suelo. Normalmente estos bloques, a los que llamaré "libres", tienen coordenadas en el mapa isométrico más "finas" o con más granularidad. Como si cada posición del mapa estuviese subdividida, en este caso, en 16 píxeles.
¿Y cómo, o mejor dicho cuándo pintamos estos bloques libres? Ahí está el primer problema. Necesitamos tener un punto de referencia (ancla o punto de anclaje) que nos represente su posición. Podríamos pensar que, una vez determinado éste, basta pintarlos cuando toque, o sea, cuando pintemos en el bucle interno esas coordenadas. Por ejemplo:
Código: Seleccionar todo
for (i = 0; i <= 18; i++){
for (j = jo; j <= length; j++){
// Get the tile to draw
k = j;
l = length + jo - j;
tile_code = map3[h][k][l];
si (tile_code){
drawBlock(tile_code-1, xx, yd-h*layer_heigth);
}
// Now plot the sprite
si( ( sprite_k >>3 == h) && ( (sprite_i) >> 4 == l) && ( (sprite_j) >> 4 == k) )
drawBlock(0, sprite_x, sprite_y - sprite_k);
xx += ancho_tile;
}
Es decir, si tenemos un bloque libre (lo he llamado sprite) en esta altura y cuya "ancla" está sobre la posición que estamos pintando (por eso se divide su coordenada "fina" por 16), lo dibujamos.
Pero esto no funciona. Para ilustrarlo pongamos que usamos como "ancla" una de las esquinas derechas del bloque (da igual la delantera o la trasera para este ejemplo). La situación que tendríamos sería:
Como véis, el bloque se ha pintado después del de su izquierda, que debería de ocultarlo parcialmente, y tenemos un error de renderizado. Ah, vale, es que hemos elegido mal el punto de anclaje... pongamos, entonces, una esquina de la izquierda. Ese error desaparece, pero tenemos este otro:
Ups. Pues tampoco funciona. Ahora se ha pintado antes del de su derecha, generando otro error ¿y cómo lo solucionamos?
En realidad deberíamos haberlo pintado entre la segunda y la tercera filas. Y ¿cómo se hace eso? pues con cuidado. Fijaos en esta imagen, que nos dice cuándo pintar:
Es entre la fila 2 y la 3 (o 1 y 2, si empezáis a contar en 0), o sea que nuestro bucle puede pasar a:
Código: Seleccionar todo
void RenderRoom()
{
int xx, xd, yd;
int l, k, length, jo, h;
int i, j;
int tile_code;
// Go though all layers
for (h = 0; h < num_layers; h++){
xx = 160 - (ancho_tile / 2);
xd = xx;
yd = 32 + alto_tile;
l = 0;
k = 0;
length = 0;
jo = 0;
for (i = 0; i <= 18; i++){
for (j = jo; j <= length; j++){
// Get the tile to draw
k = j;
l = length + jo - j;
tile_code = map[h][k][l];
si (tile_code)
drawBlock(tile_code-1, xx, yd-h*layer_heigth);
xx += ancho_tile;
}
// Now plot the sprite
si ( ((sprite_k >> 3)==h) && ( ( (sprite_i >> 4) + (sprite_j >> 4) ) == (l+k) ) )
drawBlock(0, sprite_x, sprite_y - sprite_k);
si (i < 9){
length++;
xd -= (ancho_tile / 2);
}
else{
jo++;
xd += (ancho_tile / 2);
}
xx = xd;
yd += (alto_tile / 2);
}
}
}
O sea, acabamos de pintar una fila y miramos si nuestro bloque libre cumple que:
- Está en ese nivel (su coordenada fina de altura entre 8 coincide con la capa que estamos pintando) -> (sprite_k >> 3)==h
- Le corresponde esta fila a su "ancla" (la suma de sus coordenadas finas isométricas dividida por 16 coincide con la de la fila que estamos pintando) -> ( (sprite_i >> 4) + (sprite_j >> 4) ) == (l+k) )
Nadie en su sano juicio lo implementaría así. Lo suyo es poner una lista enlazada de bloques libres por cada línea y capa (por ejemplo) que se actualiza cuando estos bloques se mueven, pero para ilustrar lo que hay que hacer vale.
¿O sea que ya está?
Bueno, pues casi, pero no del todo. Este método tiene un problema: los bloques tienen una altura limitada. Si queremos que uno sea un panel o una pared de 32 o 40 píxeles de altura, o bien lo dividimos en bloques, o aparecerán errores de renderizado. Me temo que igual que si nuestro bloque "libre" se mueve verticalmente. Esto, en ocasiones, es una limitación muy fuerte. Nos vendría bien poder pintar una pared alta de piedra y poner a media altura una antorcha en algunos casos, o una ventana. Ahorra memoria, y mejora la eficiencia (hay menos operaciones de blitter).
Pero para ello hay que usar otro método de dibujado, lo que se llama "rendering en columnas o apilado", donde pintamos un bloque y todos los que están encima. Pero en ese caso todo el trabajo se viene abajo porque ocurre esto:
¿Véis como al pintar el suelo se superpone al bloque que, en teoría, está sobre él? Podéis hacer las variaciones que queráis, pero me parece que esto no tiene solución fácil. ¡Si alguien la conoce que me la diga!
Cuando desarrollé Space:1999 me encontré con el mismo problema y tuve que buscarme la vida para usar las máscaras de cada objeto para generar una máscara adicional con la que evitar que se pintase sobre ellos en estas circunstancias. Se trata de generar una máscara que indique, para los siguientes blits, dónde no deben estropear el fondo.
No es sencillo, pero no se me ocurre otro método. De hecho ya ni me acuerdo cómo lo tenía hecho, pero involucra varios blits de más.
Si alguien tiene una solución simple... yo no la he encontrado por ningún sitio.