Este tutorial consta de las siguientes partes:
0.0 Antes de empezar
1.0 ¿Que es un Tile Based Game?
2.0 El Mapa
2.1 Dibujando el mapa
2.2 Eligiendo el sprite para dibujar el bloque
2.3 Clipping
2.4 Parallax Scrolling
3.0 Game Loop
4.0 Conclusión
4.1 Mpdv
5.0 Contactando al autor
0. Antes de empezar
Quiero aclarar que no soy un programador profesional (y mucho menos escritor), de modo que la presente información puede contener (he tratado de evitarlos) errores.
En este tutorial vamos a aprender a programar juegos Tile Based en Visual Basic, se puede bajar el código de ejemplo (y en el que me voy a basar) haciendo clic AQUÍ, este código no es un tile based game completo, debido a que esta es la parte I del tutorial, de todas formas, vamos a apuntar a terminar haciendo algo como el Visual Mario (que lo podes conseguir en www.canalprogramadores.com.ar)
El código de ejemplo usa DirectDraw de DirectX 7.0 para manejo de gráficos, te recomiendo que consigas mi tutorial sobre DirectDraw en Visual Basic si no lo sabes usar. Pueden escribirme y yo se los enviare con gusto ;)
1.0.
Que es un Tile Based Game?
La traducción literal queda muy mal, de modo que la mejor forma de explicar que son estos juegos, es hacer referencia a alguno conocido, estos pueden ser el Mario Bros, Giana Sisters,
Duke Nukem I y II, y Visual Mario :p ... suficientes ejemplos.. si aun no saben que es, no entiendo porque están leyendo esto.. de todas formas, aquí hay un snapshot del Visual Mario.
Existen 2 tipos de Tile Based Games , los que tienen este tipo de perspectiva (que por lo general se ven de costado), y los Isometric Tile Based Games, que se ven desde una perspectiva isométrica, un ejemplo de estos pueden ser los clásicos Head Over Hells, Batman, Killer Tomatoes y Gun Fright de la querida MSX, y Ultima 8, Relentless (L.B.A), Dungeon Hack, The Summoning, y Veil of Darkness de PC.
mmm.. lamento desilusionarlos, pero no tocaremos el tema de la detección de colisión en este tutorial, lo siento..
2.
El mapa
Si se presta un poco de atención (no mucha, resulta realmente obvio), se puede notar que el mapa esta compuesto por pequeños bloquecitos cuadrados uno detrás de otro. Bien, necesitamos conseguir un método de mantener ese mapa en memoria y manejarlo a la hora de mostrarlo, o lo que sea que le vayamos a hacer.
La
forma mas fácil (o bueno.. como se me ocurrió..) es usar un array de strings
para guardar el mismo, como seria este array? Muy fácil, a
continuación se puede ver el mapa que usamos en el demo en su formato array.
Mapa(0) = "T EF T"
Mapa(1) = "T NL"
Mapa(2) = "T EF EF EF
T"
Mapa(3) = "T EF
T"
Mapa(4) = "T
NO NL"
Mapa(5) = "T VWX VWX NLU
T"
Mapa(6) = "T YZa YZa B NLLU T"
Mapa(7) = "T
GHI c NMO
c ADC NLLLLO NL"
Mapa(8) =
"TMMMMMO NMMMMMMMO
GHINMMMMMMMMMMMMMLLLLLLO T"
Mapa(9) =
"TLLLLLU TLLLLLLLLO NMMLLLLLLLLLLLLLLLLLLLLLO T"
Mapa(10) =
"TLLLLLRPPPPPPQLLLLLLLLLO
NMMO
NMMMMLLLLLLLLLLLLLLLLLLLLLLLLLO GHI T"
Mapa(11) =
"TLLLLLeSSSSSSdLLLLLLLLLU
NLLLRPPPPPPPPPQLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLMMMMML"
Mapa(12) =
"TLLLLLeSSSSSSdLLLLLLL
TLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL"
Mapa(13) =
"TLLLLLeSSSSSSdLLLLLLLPPPPPPQLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL"
Mapa(14) =
"TLLLLLeSSSSSSdLLLLLLLSSSSSSdLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL"
Mapa(15) =
"TLLLLLeSSSSSSdLLLLLLLSSSSSSdLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL"
Bien, cada carácter del array, es un bloque en el mapa, así, dependiendo del carácter que sea, depende el bloque que es, y se usara una imagen diferente para representarlo. Por el momento, tene en cuenta que un carácter vacío (“ “), es un espacio libre en el mapa.
2.1.
Dibujando el mapa
Genial, ahora, la gran pregunta será: ¿como dibujo el mapa?
Tenemos que ir barriendo el array, para detectar que caracteres hay, e ir dibujando según corresponda, para leer un carácter en la posición X e Y del mapa, usaremos simplemente nuestra querida función MID$ de la siguiente manera.
Dim CH as String *1
CH = MID$(Mapa(Y),X,1)
Entonces,
para dibujar el mapa, barreremos los caracteres que podemos ver (no tiene
sentido barrer y dibujar aquellos que están fuera del rango de visión, seria
perder recursos en algo que no veremos), esto es fácil, mira este código:
For Y = 0 To 14
For X = 0 + ColMap To 20 + ColMap
Bloq = Mid$(Mapa(Y), X, 1)
If Bloq <> " " Then
posX = ((X) * 16) - (ColMap * 16)
posY = (Y * 16)
GraficarBloque(PosX, PosY, Bloque)
End If
Next
Next
Donde:
FilMap y ColMap son las coordenadas del mapa a partir de las cuales empezamos a leer el array,
Serian algo así como la posición de la cámara en X e Y
GraficarBloque es una función inventada ahora para que el
código se entienda mejor, la misma graficaría al bloque del tipo Bloque, en la posición PosX,
PosY.
Notar que los bloques tienen 16x16 píxeles de tamaño.
Ahora, la pregunta que podrían hacer es la siguiente:
“cuando calculas PosX, porque le restas (ColMap * 16)?”
Mira
esto, suponete que esas restas no estarían, y la cámara esta en ColMap=40,
Entonces cuando graficamos en bloque situado en X=5 e Y=7 (ver el For), lo cual corresponderia realmente al bloque X=ColMap+5 e Y=7 :
posX = (X) * 16)
posY = (Y) * 16)
Que valor tiene PosX y PosY ahora? Serian...
PosX = (ColMap+5) *16 = 720
PosY = Y *16 = 112
Estaríamos graficando el bloque en la posición (720,112) -en píxeles- en una resolución de 320x240!!.. evidentemente no veríamos nada, de todas formas, eso se aplica solo a la coordenada X, ya que en las Y solo tenemos la cantidad de bloques necesaria para llenar la pantalla y nada mas (no haremos Scrolling vertical para no complicar mas esto, pero de todas formas es igual que el horizontal)
La posición donde deberíamos graficar seria en realidad de
X=5*16=80
Y=7*16=122
Fijate que ese es el resultado que obtenes con la resta. En resumen, podríamos decir que la misma hace que los bloques que veamos, o lo que tenemos que ver, se centren en la pantalla.
2.2. Eligiendo el sprite para dibujar el bloque
Antes una aclaración, en el archivo sprites.bmp, están todos los sprites, formando una grilla,
Ese BMP lo cargamos en una superficie DIRECTDRAWSURFACE7 (una vez mas, busca mi tutorial sobre DirectDraw en VB), y vamos sacando las imágenes de ahí usando un tipo RECT para seleccionar el sprite, el RECT es cuadrado (de 16x16), y lo que haremos es ir desplazándolo por la grilla, para sacar el sprite que queramos.
Dijimos que dependiendo del carácter que haya en el array, será el grafico que tendrá el bloque, entonces, tendremos que variar la posición del RECT de origen, como dijimos antes, para sacar la imagen correspondiente. A continuación se ve el archivo sprites.bmp
Vamos a ver como sacar el sprite que necesitamos con el RECT en caso de que no lo entiendas bien, suponete que queremos la imagen de los ladrillitos naranjas, (Col. 10, Fil. 2), entonces el RECT tendría que tener sus miembros con los siguientes valores:
Dim rSprite As RECT
rSprite.Top = 1 + (17 * (2 - 1))
rSprite.Bottom = rSprite.Top + 16
rSprite.Left = 1 + (17 * (10 - 1))
rSprite.Right = rSprite.Left + 16
La formula se complica porque tenemos que saltearnos la línea divisoria entre los sprites, sin ella seria mucho mas fácil, pero un poco mas difícil para el grafista, de modo que, mejor suframos un poco nosotros y dejemos en paz al Gfx Man. :p
Ahora podemos hacer una formula general para extraer sprites de la grilla:
rSprite.Top = 1 + (17 * (Fila - 1))
rSprite.Left = 1 + (17 * (Columna - 1))
rSprite.Bottom = 16 + rSprite.Top
rSprite.Right = 16 + rSprite.Left
(odio la forma en la que el Editor de Visual te acomoda las cosas)
Bien, volviendo a lo de elegir el sprite según el carácter, lo que tenemos que hacer es modificar el valor de Columna y Fila segur el carácter, y después modificar los valores del RECT.
Aquí hay una porción del código que hace esto en nuestro demo:
Bloq = Mid$(Mapa(Y), X, 1)
If Bloq <> " " Then
If Bloq = "A" Then Fila = 2: Columna = 1
If Bloq = "B" Then Fila = 2: Columna = 2
If Bloq = "C" Then Fila = 2: Columna = 3
If Bloq = "D" Then Fila = 2: Columna = 4
If Bloq = "E" Then Fila = 2: Columna = 5
If Bloq = "F" Then Fila = 2: Columna = 6
If Bloq = "G" Then Fila = 2: Columna = 7
If Bloq = "H" Then Fila = 2: Columna = 8
If Bloq = "I" Then Fila = 2: Columna = 9
If Bloq = "J" Then Fila = 2: Columna = 10
If Bloq = "K" Then Fila = 2: Columna = 11
If Bloq = "L" Then Fila = 3: Columna = 1
If Bloq = "M" Then Fila = 3: Columna = 2
If Bloq = "N" Then Fila = 3: Columna = 3
If Bloq = "O" Then Fila = 3: Columna = 4
..
..
(y asi la cantidad de
veces necesarias)
End If
Admito que es un poquito mediocre, pero.. buee.. :p
Listo, ahora lo único que nos queda por hacer es ver como
graficamos el bloque en la pantalla (o mejor dicho en el back buffer), esto lo haremos con un simple llamado a BltFast
BackBuffer.BltFast posX, posY, Sprite, rSprite, DDBLTFAST_SRCCOLORKEY Or DDBLTFAST_WAIT
Donde:
posX y posY son las
coordenadas en la pantalla
Sprite es el DIRECTDRAWSURFACE7 donde esta
la grilla de sprites (archivo sprites.bmp)
rSprite es el RECT fuente con que elegimos
el Sprite que queremos
DDBLTFAST_WAIT y DDBLTFAST_SRCCOLORKEY son flags para BltFast.
2.3. Clipping
Aun nos queda algo por hacer: el clipping
Bien, hay varias formas de hacer esto, podríamos recortar el RECT destino, para que no se vaya fuera de los limites de la superficie, pero, como no estamos usando Blt, sino BltFast, no podemos hacerlo (BltFast no toma un RECT de destino para saber donde graficar, sino que toma un X e Y , y copia el RECT fuente exactamente como es).
Entonces, lo que debemos hacer es tomar un RECT mas pequeño, de modo que cuando lo copiemos a la pantalla no salga fuera de esta.
Voy
a darte un ejemplo grafico de nuestro problema, y la solución, de modo que se
entienda un poco mejor.
Aquí, queremos pegar un bloque en el borde derecho de la
superficie, pero el sprite sale fuera de los limites de la misma. Fijate que cuando
tomamos todo el sprite con el RECT fuente y lo pegamos, el DirectDraw no lo dibuja
porque sale fuera.
Cuando tomamos una porción, en cambio, nada queda fuera de
la superficie destino, y todo sale como esperábamos.
Entonces, cada vez que armamos un RECT para tomar un Sprite,
tenemos que ver si el mismo, por la posición en la que ira pegado, va a salir
de la pantalla, y si así es, debemos cortarlo para impedir que esto pase,
obviamente esto dependerá de la resolución de video actual, que es la que le dará
las dimensiones al back buffer, a lo largo de todo este tutorial y los
siguientes (y probablemente en todos los que pueda llegar a escribir basados en
Visual Basic), la resolución será de 320x240 (esto es por tres
razones: soy un nostálgico y creo que todos los juegos deberían seguir siendo
en Modo
X, la mediocridad de Visual Basic
hace que en otra resolución vaya lento, y por ultimo, todos sabemos que
arriba de 640x480, todas las resoluciones apestan).
Las únicas coordenadas del RECT que tendremos que
modificar serán las de la derecha e izquierda, debido a que solo haremos Scrolling
horizontal (de todos modos, el clipping vertical es exactamente
igual al horizontal)
'/////////////////// clipping izquierdo /////////////////
If posX <= 0 Then
rSprite.Left = rSprite.Left + (Abs(posX))
posX = 0
End If
'/////////////////// clipping derecho /////////////////
If pos1X >= 319 Then
rSprite.Right = rSprite.Right - (pos1X - 319)
End If
Donde:
pos1X es la coordenada X de la
Derecha en
la pantalla
posX es la coordenada X de la
Izquierda en la pantalla
rSprite es el RECT fuente, que
indica que sprite sacaremos de la grilla
La explicación de este código es sencilla (iba a escribir
“...y se la dejo al lector”, pero odio cuando alguien hace eso :p)
Lo que hacemos es lo siguiente, doy el ejemplo con el
borde derecho ya que para el izquierdo es igual: Si el borde derecho del sprite
sale fuera de la superficie destino (el back buffer) en n
píxeles, entonces al RECT fuente, le sacamos n píxeles del lado
derecho. Como dije, para el lado izquierdo es igual, solo que después de
sacarle n píxeles del lado izquierdo, seteamos la coordenada de
graficación del bloque en 0, así el mismo se graficara en el borde izquierdo, y no fuera de la
pantalla. (Si aun no entendiste bien esto, te recomiendo dibujar en una hoja de
papel un rectángulo grande [que seria el back buffer] y uno pequeño [que seria
el sprite] saliendo fuera del mismo, y fijate que modificaciones deberías
hacerle para que no salga fuera del rectángulo grande)
2.4.Parallax Scrolling
Sin duda la parte mas divertida para programar, el Scroll
Parallax (me encanta como suena, creo que me voy a cambiar el nick).
No se de donde viene el nombre de Parallax, de
todas formas, todos lo relacionamos con un paneo suave de cámara, o un Smooth
Scroll, es lo mismo.
El Scroll que tenemos hasta ahora es muy brusco, ya
que la cámara avanza de a bloques (que serian 16 píxeles (recordar ColMap), lo cual es
extremadamente poco atractivo, tenemos que hacer que el Scroll sea de a 1 píxel, para lograr un paneo
suave y agradable, para esto, debemos modificar la sección de código donde
dibujamos el mapa. Para que no tengas que volver atrás, aquí esta nuestro
código actualmente:
For Y = 0 To 14
For X = 0 + ColMap To 20 + ColMap
Bloq = Mid$(Mapa(Y), X, 1)
If Bloq <> " " Then
posX = ((X) * 16) - (ColMap * 16)
posY = (Y * 16)
GraficarBloque (PosX, PosY, Bloque)
End If
Next
Next
Otra vez, GraficarBloque es una función inventada ahora solo para simplificar el código,
la línea resaltada es a la cual tenemos que prestarle atención, debemos
agregarle algo que nos permita hacer que el mapa (entero) se desplace
suavemente, esto es, si señores (y señoritas), una variable cualquiera que
luego veremos como manejamos.
Así, esa línea resaltada será reaplazada por esta:
posX = ((X) * 16) - (ColMap * 16) – MapDspCol
Que es lo mismo que la anterior, solo que le restamos MapDspCol (MapaDesplazamientoColumna)
Ahora, lo único que tenemos que hacer es manejar MapDspCol y ColMap de manera que tengamos un buen Smooth Scroll.
Recordemos que hacia ColMap y que función tiene ahora
MapDspCol:
ColMap es la posición de la cámara,
en bloques, o sea que si incrementamos (o decrementamos) su valor, tenemos un Scroll
de a 16 píxeles (que es el tamaño de los bloques).
MapDspCol nos proporciona un
desplazamiento del mapa en la
pantalla, si modificamos este valor, vamos a ver que todo el mapa se mueve
de a píxeles (o bueno, en la cantidad que hayamos aumentado la variable), pero,
no tiene la misma función que ColMap, MapDspCol puede ser visto como el control de posición horizontal
del mapa en la pantalla (algo así como el potenciómetro del monitor, pero del
mapa).
Bien, ahora tenemos que combinar estas dos variables para
tener un Smooth Scroll, esto es sencillo, ahora que entendimos que hace cada
variable.
Vamos a analizar que pasaría si vamos a la derecha, voy a
ir tirando algunas posibilidades, creo que así se va a entender, imagina que
estoy al lado tuyo diciéndote esto:
- ¿qué pasa si aumento ColMap?
- El mapa se mueve de a 16 píxeles hacia la izquierda,
teniendo un Scroll brusco.
- ¿Y si uso MapDspCol para
desplazar el mapa de a píxeles hasta llegar a los 16, y después incremento ColMap?
- En los primeros 16 píxeles funcionaria, pero después el
mapa se empezaría a ir a la izquierda de la pantalla cada vez mas y dejaríamos
de verlo,
-¿Y si aumento MapDspCol y cuando llega a los 16 píxeles aumento ColMap y vuelvo a poner MapDspCol en 0?
- Bien ahí, es así como funciona.
Tal vez con una explicación grafica se entendería mejor,
lo mejor puede ser jugar con MapDspCol y ColMap en el código fuente de demo, anulando MapDspCol y viendo que pasa,
por ejemplo.
Bien, y ahora la implementacion del Parallax Scroll:
son dos piezas de código, una para cuando voy a la izquierda, y otra cuando voy
a la derecha.
'//////////// Muevo la "camara" a la derecha
/////////////////
If ColMap <= 58 Then
MapDspCol = MapDspCol + 1
If MapDspCol > 15 Then
MapDspCol = 0
ColMap = ColMap + 1
End If
End If
'/////////////////////////////////////////////////////////////
'//////////// Muevo la "camara" a la izquierda
///////////////
If ColMap > 1 Then
MapDspCol = MapDspCol - 1
If MapDspCol < 0 Then
MapDspCol = 15
ColMap = ColMap - 1
End If
End If
'////////////////////////////////////////////////////////////
Las líneas en verde son para impedir que no nos vayamos fuera del mapa.
3.0 Game Loop
Bien, creo que hemos terminado de implementar todos los features
del demo, ahora, falta algo.
Tenemos que ver el orden en que viene todo implementado,
el pseudo-código del game loop del demo se encuentra a
continuación.
GAME LOOP
{
.Dibujo el fondo
BARRIDO DEL ARRAY
{
.Me fijo que tipo de
bloque es
.Armo el RECT para
elegir el sprite
.Clipping
.Dibujo el bloque
}
.Movimiento de la
cámara
}
4.0 Conclusión
Hemos aprendido a manejar, dibujar, y desplazar un mapa
para un tile based game, este tipo de manejos de mapa puede ser usado
para una amplia gama de juegos, de todas formas, para algo mas sofisticado,
podría convenir usar una lista enlazada, por ejemplo (con lo cual nos
tendríamos que trasladar a otro lenguaje, C++ preferentemente), pero de todas
formas, podemos aprender mucho con este tipo de técnicas. Además, con Visual
Basic no vamos a apuntar a hacer algo extremadamente sofisticado.
4.1 mpdv
Personalmente siempre me gustaron los tutoriales,
encontrar algo que necesitaba y aprender de ahí, de todas formas, siempre lo he
hecho, y recomiendo, antes de buscar un tutorial*, intentar pensar, e idear un
método propio, se aprende mucho experimentando técnicas propias, sobre todo
cuando es 2D, en 3D puede complicarse un poco mas. Además, me ha pasado, y nada
se compara a hacer algo con un método, y después ver que un programador en la
otra punta del mundo, implemento algo de forma similar.
*Solo se aplica
a lo que son técnicas, puede resultar imposible aprender DirectX sin
documentación, por ejemplo.
5.0. Contactando al autor.
Pueden contactarme con ICQ o MSN a las direcciones que aparecen debajo, para realizar cualquier tipo de preguntas, comentarios, sugerencias, criticas, o simplemente hablar, me gusta conocer gente nueva.
ICQ# 115506483
Pueden también escribirme a mi dirección de e-mail (la
misma de MSN), o encontrarme en el canal #programadores de irc.ciudad.com.ar (un abrazo a todos mis amigos allí!)
GreetingZ a todo el equipo de HammerHood Development Team
Espero
que esto les haya sido de ayuda, y ansío escuchar noticias de ustedes,
cuídense, suerte.
Assembly [Rodrigo Pelorosso]
8/7/2002
Bs.As. Argentina