Creando mundos isométricos: una guía para desarrolladores de juegos
() translation by (you can also view the original English article)
En este tutorial, le daré una descripción general amplia de lo que necesita saber para crear mundos isométricos. Aprenderá qué es la proyección isométrica y cómo representar los niveles isométricos como matrices en 2D. Formularemos relaciones entre la vista y la lógica, de modo que podamos manipular fácilmente los objetos en la pantalla y manejar la detección de colisiones basada en mosaicos. También veremos la clasificación de profundidad y la animación de personajes.
¿Quieres más consejos para crear mundos isométricos? Consulte la publicación de seguimiento, Creando mundos isometricos: Una guía para los desarrolladores de juegos, Continuado, y el libro de Juwal, Starling Game Development Essentials.
1. El mundo isométrico
La vista isométrica es un método de visualización que se usa para crear una ilusión de 3D para un juego que, de otro modo, sería 2D, a veces denominado pseudo 3D o 2.5D. Estas imágenes (tomadas de Diablo 2 y Age of Empires) ilustran lo que quiero decir:






Implementar una vista isométrica se puede hacer de muchas maneras, pero en aras de la simplicidad me centraré en un enfoque basado en mosaicos, que es el método más eficiente y ampliamente utilizado. He superpuesto cada captura de pantalla anterior con una cuadrícula de diamantes que muestra cómo el terreno se divide en mosaicos.
2. Juegos basados en azulejos
En el enfoque basado en mosaicos, cada elemento visual se divide en piezas más pequeñas, llamadas mosaicos, de un tamaño estándar. Estas fichas se organizarán para formar el mundo del juego de acuerdo con los datos de nivel predeterminados, generalmente una matriz 2D.
Por ejemplo, consideremos una vista 2D estándar de arriba hacia abajo con dos mosaicos, una baldosa de hierba y una baldosa de pared, como se muestra aquí:

Estos mosaicos son del mismo tamaño que el otro y son cuadrados, por lo que la altura del azulejo y el ancho del mosaico son los mismos.
Para un nivel con pastizales cerrados por todos lados por muros, la matriz 2D de los datos de nivel se verá así:
1 |
[[1,1,1,1,1,1], |
2 |
[1,0,0,0,0,1], |
3 |
[1,0,0,0,0,1], |
4 |
[1,0,0,0,0,1], |
5 |
[1,0,0,0,0,1], |
6 |
[1,1,1,1,1,1]] |
Aquí, 0
denota una baldosa de hierba y 1
denota una baldosa de pared. Organizar las fichas de acuerdo con los datos de nivel producirá la imagen de nivel inferior:

Podemos mejorar esto agregando azulejos de esquina y azulejos verticales y horizontales separados, que requieren cinco azulejos adicionales:
1 |
[[3,1,1,1,1,4], |
2 |
[2,0,0,0,0,2], |
3 |
[2,0,0,0,0,2], |
4 |
[2,0,0,0,0,2], |
5 |
[2,0,0,0,0,2], |
6 |
[6,1,1,1,1,5]] |

Espero que el concepto del enfoque basado en mosaico sea ahora claro. Esta es una implementación de grilla 2D directa, que podríamos codificar de la siguiente manera:
1 |
for (i, loop through rows) |
2 |
for (j, loop through columns) |
3 |
x = j * tile width |
4 |
y = i * tile height |
5 |
tileType = levelData[i][j] |
6 |
placetile(tileType, x, y) |
Aquí suponemos que el ancho del azulejo y la altura del azulejo son iguales (y lo mismo para todos los azulejos), y coinciden con las dimensiones de las imágenes del azulejo. Por lo tanto, el ancho del azulejo y la altura del azulejo para este ejemplo son ambos de 50px, que conforman el tamaño de nivel total de 300x300px, es decir, seis filas y seis columnas de losetas que miden 50x50px cada una.
En un enfoque normal basado en mosaicos, implementamos una vista descendente o una vista lateral; para una vista isométrica necesitamos implementar la proyección isométrica.
3. Proyección isométrica
La mejor explicación técnica de lo que significa "proyección isométrica", hasta donde yo sé, es de este artículo de Clint Bellanger:
Inclinamos nuestra cámara a lo largo de dos ejes (balancee la cámara 45 grados hacia un lado, luego 30 grados hacia abajo). Esto crea una rejilla en forma de diamante (rombo) donde los espacios de la rejilla son dos veces más anchos que altos. Este estilo fue popularizado por juegos de estrategia y juegos de rol de acción. Si miramos un cubo en esta vista, son visibles tres lados (lado superior y dos lados enfrentados).
Aunque suena un poco complicado, implementar esta visión es sencillo. Lo que debemos comprender es la relación entre el espacio 2D y el espacio isométrico, es decir, la relación entre los datos de nivel y la vista; la transformación de coordenadas "cartesianas" de arriba hacia abajo en coordenadas isométricas.



(No estamos considerando una técnica basada en mosaicos hexagonales, que es otra forma de implementar mundos isométricos).
Colocando azulejos isométricos
Permítanme intentar simplificar la relación entre los datos de nivel almacenados como una matriz 2D y la vista isométrica, es decir, cómo transformamos las coordenadas cartesianas en coordenadas isométricas.
Trataremos de crear la vista isométrica para nuestros datos de nivel de pastizales cerrados:
1 |
[[1,1,1,1,1,1], |
2 |
[1,0,0,0,0,1], |
3 |
[1,0,0,0,0,1], |
4 |
[1,0,0,0,0,1], |
5 |
[1,0,0,0,0,1], |
6 |
[1,1,1,1,1,1]] |
En este
escenario, podemos determinar un área transitable comprobando si el
elemento de la matriz es 0
en esa coordenada, lo que indica hierba. La
implementación de vista 2D del nivel anterior fue una iteración directa
con dos bucles, colocando mosaicos cuadrados compensando cada uno con
la altura del azulejo fijo y los valores de ancho del azulejo.
1 |
for (i, loop through rows) |
2 |
for (j, loop through columns) |
3 |
x = j * tile width |
4 |
y = i * tile height |
5 |
tileType = levelData[i][j] |
6 |
placetile(tileType, x, y) |
Para la vista isométrica, el código sigue siendo el mismo, pero la función placeTile()
cambia.
Para una vista isométrica, necesitamos calcular las coordenadas isométricas correspondientes dentro de los bucles.
Las
ecuaciones para hacer esto son las siguientes, donde isoX
y isoY
representan las coordenadas isométricas x e y, y cartX
y cartY
representan
las coordenadas cartesianas x e y:
1 |
//Cartesian to isometric:
|
2 |
|
3 |
isoX = cartX - cartY; |
4 |
isoY = (cartX + cartY) / 2; |
1 |
//Isometric to Cartesian:
|
2 |
|
3 |
cartX = (2 * isoY + isoX) / 2; |
4 |
cartY = (2 * isoY - isoX) / 2; |
Estas funciones muestran cómo se puede convertir de un sistema a otro:
1 |
function isoTo2D(pt:Point):Point{ |
2 |
var tempPt:Point = new Point(0, 0); |
3 |
tempPt.x = (2 * pt.y + pt.x) / 2; |
4 |
tempPt.y = (2 * pt.y - pt.x) / 2; |
5 |
return(tempPt); |
6 |
}
|
1 |
function twoDToIso(pt:Point):Point{ |
2 |
var tempPt:Point = new Point(0,0); |
3 |
tempPt.x = pt.x - pt.y; |
4 |
tempPt.y = (pt.x + pt.y) / 2; |
5 |
return(tempPt); |
6 |
}
|
El pseudocódigo para el ciclo se ve así:
1 |
for(i, loop through rows) |
2 |
for(j, loop through columns) |
3 |
x = j * tile width |
4 |
y = i * tile height |
5 |
tileType = levelData[i][j] |
6 |
placetile(tileType, twoDToIso(new Point(x, y))) |



Como ejemplo, veamos cómo una posición 2D típica se convierte a una posición isométrica:
1 |
2D point = [100, 100]; |
2 |
// twoDToIso(2D point) will be calculated as below |
3 |
isoX = 100 - 100; // = 0 |
4 |
isoY = (100 + 100) / 2; // = 100 |
5 |
Iso point == [0, 100]; |
Del mismo modo, una entrada de [0, 0]
dará como resultado [0, 0]
, y [10, 5]
dará [5, 7.5]
.
El método anterior nos permite crear una correlación directa entre los datos de nivel 2D y las coordenadas isométricas. Podemos encontrar las coordenadas del mosaico en los datos de nivel de sus coordenadas cartesianas usando esta función:
1 |
function getTileCoordinates(pt:Point, tileHeight:Number):Point{ |
2 |
var tempPt:Point = new Point(0, 0); |
3 |
tempPt.x = Math.floor(pt.x / tileHeight); |
4 |
tempPt.y = Math.floor(pt.y / tileHeight); |
5 |
return(tempPt); |
6 |
}
|
(Aquí, suponemos esencialmente que la altura del azulejo y el ancho del azulejo son iguales, como en la mayoría de los casos).
Por lo tanto, a partir de un par de coordenadas de pantalla (isométricas), podemos encontrar las coordenadas del mosaico llamando:
1 |
getTileCoordinates(isoTo2D(screen point), tile height); |
Este punto de pantalla podría ser, por ejemplo, una posición de clic del mouse o una posición de pick-up.
Consejo: Otro método de colocación es el modelo Zigzag, que adopta un enfoque completamente diferente.
Moviéndose en coordenadas isométricas
El
movimiento es muy fácil: manipula los datos de su mundo de juego en
coordenadas cartesianas y simplemente usa las funciones anteriores para
actualizarlo en la pantalla. Por ejemplo, si desea
mover un carácter hacia adelante en la dirección y positiva,
simplemente puede incrementar su propiedad y
y luego convertir su
posición a coordenadas isométricas:
1 |
y = y + speed; |
2 |
placetile(twoDToIso(new Point(x, y))) |
Clasificación de profundidad
Además de la ubicación normal, tendremos que ocuparnos de la clasificación en profundidad para dibujar el mundo isométrico. Esto asegura que los elementos más cercanos al jugador se dibujen encima de los elementos más alejados.
El método más simple de clasificación en profundidad es simplemente usar el valor cartesiano de coordenada y, como se menciona en este consejo rápido: cuanto más arriba de la pantalla esté el objeto, más temprano se dibujará. Esto funciona bien siempre que no tengamos sprites que ocupen más de un espacio de teselas.
La forma más eficiente de clasificar en profundidad para los mundos isométricos es dividir todos los mosaicos en dimensiones estándar de un solo mosaico y no permitir imágenes más grandes. Por ejemplo, aquí hay una ficha que no encaja en el tamaño de tesela estándar: vea cómo podemos dividirla en varias teselas, cada una de las cuales se adapta a las dimensiones de la tesela:



4. Creando el Arte
El arte isométrico puede ser pixel art, pero no tiene que serlo. Al tratar con el arte de píxeles isométricos, la guía de RhysD te dice casi todo lo que necesitas saber. Algunas teorías se pueden encontrar en Wikipedia también.
Al crear arte isométrico, las reglas generales son
- Comience con una cuadrícula isométrica en blanco y adhiérase a la precisión perfecta de píxeles.
- Intenta dividir el arte en imágenes de baldosas isométricas individuales.
- Intente asegurarse de que cada baldosa sea accesible o no. Será complicado si tenemos que acomodar una sola loseta que contenga tanto áreas transitables como no transitables.
- La mayoría de las fichas necesitarán mosaico sin fisuras en una o más direcciones.
- Las sombras pueden ser difíciles de implementar, a menos que usemos un enfoque por capas donde dibujamos sombras en la capa del suelo y luego dibujamos el héroe (o árboles u otros objetos) en la capa superior. Si el enfoque que utilizas no es de varias capas, asegúrate de que las sombras caigan al frente para que no se caigan, por ejemplo, el héroe cuando se para detrás de un árbol.
- En caso de que necesite usar una imagen de mosaico más grande que el tamaño de mosaico isométrico estándar, intente utilizar una dimensión que sea un múltiplo del tamaño del mosaico iso. Es mejor tener un enfoque en capas en tales casos, donde podemos dividir el arte en diferentes piezas en función de su altura. Por ejemplo, un árbol se puede dividir en tres partes: la raíz, el tronco y el follaje. Esto hace que sea más fácil ordenar las profundidades ya que podemos dibujar las piezas en las capas correspondientes que corresponden con sus alturas.
Los mosaicos isométricos que son más grandes que las dimensiones de un solo mosaico crearán problemas con la clasificación de profundidad. Algunos de los problemas se discuten en estos enlaces:
5. Personajes isométricos
Implementar caracteres en vista isométrica no es complicado, ya que puede sonar. El arte de los personajes debe ser creado de acuerdo con ciertos estándares. Primero necesitaremos fijar cuántas direcciones de movimiento están permitidas en nuestro juego; por lo general, los juegos proporcionarán movimiento en cuatro direcciones u movimiento en ocho direcciones.



Para una vista de arriba hacia abajo, podríamos crear un conjunto de animaciones de personajes orientadas en una dirección, y simplemente rotarlas para todas las demás. Para el arte de los personajes isométricos, debemos volver a representar cada animación en cada una de las direcciones permitidas, por lo que para el movimiento a ocho bandas necesitamos crear ocho animaciones para cada acción. Para facilitar la comprensión, generalmente indicamos las direcciones como Norte, Noroeste, Oeste, Suroeste, Sur, Sureste, Este y Noreste, en sentido antihorario, en ese orden.



Colocamos personajes de la misma manera que colocamos los mosaicos. El movimiento de un personaje se logra calculando el movimiento en coordenadas cartesianas y luego convirtiendo a coordenadas isométricas. Supongamos que estamos usando el teclado para controlar el personaje.
Estableceremos dos variables, dX
y dY
, basadas en las teclas
direccionales presionadas. Por
defecto, estas variables serán 0
y se actualizarán según el cuadro
siguiente, donde U
, D
, R
y L
indican las teclas de flecha Arriba, Abajo,
Derecha e Izquierda, respectivamente. Un valor de 1
debajo de una tecla
representa esa tecla presionada; 0
implica que no se está presionando
la tecla.
1 |
Key Pos |
2 |
U D R L dX dY |
3 |
================ |
4 |
0 0 0 0 0 0 |
5 |
1 0 0 0 0 1 |
6 |
0 1 0 0 0 -1 |
7 |
0 0 1 0 1 0 |
8 |
0 0 0 1 -1 0 |
9 |
1 0 1 0 1 1 |
10 |
1 0 0 1 -1 1 |
11 |
0 1 1 0 1 -1 |
12 |
0 1 0 1 -1 -1 |
Ahora, usando los valores de dX
y dY
, podemos actualizar las coordenadas cartesianas de la siguiente manera:
1 |
newX = currentX + (dX * speed); |
2 |
newY = currentY + (dY * speed); |
Entonces dX
y dY
representan el cambio en las posiciones x e y del personaje, en función de las teclas presionadas.
Podemos calcular fácilmente las nuevas coordenadas isométricas, como ya hemos discutido:
1 |
Iso = twoDToIso(new Point(newX, newY)) |
Una vez que tenemos la nueva posición isométrica, necesitamos mover el personaje a esta posición. En
función de los valores que tenemos para dX
y dY
, podemos decidir a qué
dirección se enfrenta el personaje y usar el arte del personaje
correspondiente.
Detección de colisiones
La detección de colisión se realiza comprobando si la ficha en la nueva posición calculada es una ficha no accesible. Entonces, una vez que encontramos la nueva posición, no movemos el personaje inmediatamente, pero primero verificamos qué tesela ocupa ese espacio.
1 |
tile coordinate = getTileCoordinates(isoTo2D(iso point), tile height); |
2 |
if (isWalkable(tile coordinate)) { |
3 |
moveCharacter(); |
4 |
} else { |
5 |
//do nothing; |
6 |
} |
En
la función isWalkable()
, verificamos si el valor del conjunto de datos
de nivel en la coordenada dada es un mosaico con desplazamiento o no. Debemos tener cuidado de actualizar la dirección en la que se
encuentra el personaje, incluso si él no se mueve, como en el caso de
que golpee una ficha que no se puede mover.
Clasificación de profundidad con personajes
Considera un personaje y una ficha de árbol en el mundo isométrico.
Para comprender correctamente la clasificación por profundidad, debemos entender que siempre que las coordenadas xey del personaje sean menores que las del árbol, el árbol se solapa con el carácter. Siempre que las coordenadas xey del personaje sean mayores que las del árbol, el carácter se superpone al árbol.
Cuando tienen la misma coordenada x, decidimos basándonos únicamente en la coordenada y: la que tenga una coordenada y más alta se superpone a la otra. Cuando tienen la misma coordenada y, entonces decidimos basándonos solamente en la coordenada x: lo que tenga la coordenada x más alta se superpone a la otra.
Una
versión simplificada de esto es dibujar secuencialmente los niveles
comenzando desde el mosaico más alejado, es decir, tile[0][0]
,
luego dibujar todos los mosaicos en cada fila uno por uno. Si un
personaje ocupa una ficha, primero dibujamos la ficha de tierra y luego
renderizamos la ficha de personaje. Esto funcionará bien, porque el
personaje no puede ocupar un azulejo de la pared.
La clasificación de profundidad debe hacerse cada vez que una baldosa cambie de posición. Por ejemplo, tenemos que hacerlo cada vez que se muevan los personajes. Luego actualizamos la escena mostrada, después de realizar la clasificación de profundidad, para reflejar los cambios de profundidad.
6. ¡Pruebalo!
Ahora, aproveche sus nuevos conocimientos creando un prototipo funcional, con controles de teclado y clasificación de profundidad adecuada y detección de colisión. Aquí está mi demo:
Haga clic para dar el foco SWF, luego use las teclas de flecha. Haga clic aquí para ver la versión completa.
Puede encontrar útil esta clase de utilidad (la he escrito en AS3, pero debería poder entenderla en cualquier otro lenguaje de programación):
1 |
package com.csharks.juwalbose |
2 |
{
|
3 |
import flash.display.Sprite; |
4 |
import flash.geom.Point; |
5 |
|
6 |
public class IsoHelper |
7 |
{
|
8 |
/**
|
9 |
* convert an isometric point to 2D
|
10 |
* */
|
11 |
public static function isoTo2D(pt:Point):Point{ |
12 |
//gx=(2*isoy+isox)/2;
|
13 |
//gy=(2*isoy-isox)/2
|
14 |
var tempPt:Point=new Point(0,0); |
15 |
tempPt.x=(2*pt.y+pt.x)/2; |
16 |
tempPt.y=(2*pt.y-pt.x)/2; |
17 |
return(tempPt); |
18 |
}
|
19 |
/**
|
20 |
* convert a 2d point to isometric
|
21 |
* */
|
22 |
public static function twoDToIso(pt:Point):Point{ |
23 |
//gx=(isox-isoxy;
|
24 |
//gy=(isoy+isox)/2
|
25 |
var tempPt:Point=new Point(0,0); |
26 |
tempPt.x=pt.x-pt.y; |
27 |
tempPt.y=(pt.x+pt.y)/2; |
28 |
return(tempPt); |
29 |
}
|
30 |
|
31 |
/**
|
32 |
* convert a 2d point to specific tile row/column
|
33 |
* */
|
34 |
public static function getTileCoordinates(pt:Point, tileHeight:Number):Point{ |
35 |
var tempPt:Point=new Point(0,0); |
36 |
tempPt.x=Math.floor(pt.x/tileHeight); |
37 |
tempPt.y=Math.floor(pt.y/tileHeight); |
38 |
|
39 |
return(tempPt); |
40 |
}
|
41 |
|
42 |
/**
|
43 |
* convert specific tile row/column to 2d point
|
44 |
* */
|
45 |
public static function get2dFromTileCoordinates(pt:Point, tileHeight:Number):Point{ |
46 |
var tempPt:Point=new Point(0,0); |
47 |
tempPt.x=pt.x*tileHeight; |
48 |
tempPt.y=pt.y*tileHeight; |
49 |
|
50 |
return(tempPt); |
51 |
}
|
52 |
|
53 |
}
|
54 |
}
|
Si realmente te quedas atascado, aquí tienes el código completo de mi demo (en formato de código de línea de tiempo Flash y AS3):
1 |
// Uses senocular's KeyObject class
|
2 |
// http://www.senocular.com/flash/actionscript/?file=ActionScript_3.0/com/senocular/utils/KeyObject.as
|
3 |
|
4 |
import flash.display.Sprite; |
5 |
import com.csharks.juwalbose.IsoHelper; |
6 |
import flash.display.MovieClip; |
7 |
import flash.geom.Point; |
8 |
import flash.filters.GlowFilter; |
9 |
import flash.events.Event; |
10 |
import com.senocular.utils.KeyObject; |
11 |
import flash.ui.Keyboard; |
12 |
import flash.display.Bitmap; |
13 |
import flash.display.BitmapData; |
14 |
import flash.geom.Matrix; |
15 |
import flash.geom.Rectangle; |
16 |
|
17 |
var levelData=[[1,1,1,1,1,1], |
18 |
[1,0,0,2,0,1], |
19 |
[1,0,1,0,0,1], |
20 |
[1,0,0,0,0,1], |
21 |
[1,0,0,0,0,1], |
22 |
[1,1,1,1,1,1]]; |
23 |
|
24 |
var tileWidth:uint = 50; |
25 |
var borderOffsetY:uint = 70; |
26 |
var borderOffsetX:uint = 275; |
27 |
|
28 |
var facing:String = "south"; |
29 |
var currentFacing:String = "south"; |
30 |
var hero:MovieClip=new herotile(); |
31 |
hero.clip.gotoAndStop(facing); |
32 |
var heroPointer:Sprite; |
33 |
var key:KeyObject = new KeyObject(stage);//Senocular KeyObject Class |
34 |
var heroHalfSize:uint=20; |
35 |
|
36 |
//the tiles
|
37 |
var grassTile:MovieClip=new TileMc(); |
38 |
grassTile.gotoAndStop(1); |
39 |
var wallTile:MovieClip=new TileMc(); |
40 |
wallTile.gotoAndStop(2); |
41 |
|
42 |
//the canvas
|
43 |
var bg:Bitmap = new Bitmap(new BitmapData(650,450)); |
44 |
addChild(bg); |
45 |
var rect:Rectangle=bg.bitmapData.rect; |
46 |
|
47 |
//to handle depth
|
48 |
var overlayContainer:Sprite=new Sprite(); |
49 |
addChild(overlayContainer); |
50 |
|
51 |
//to handle direction movement
|
52 |
var dX:Number = 0; |
53 |
var dY:Number = 0; |
54 |
var idle:Boolean = true; |
55 |
var speed:uint = 5; |
56 |
var heroCartPos:Point=new Point(); |
57 |
var heroTile:Point=new Point(); |
58 |
|
59 |
//add items to start level, add game loop
|
60 |
function createLevel() |
61 |
{
|
62 |
var tileType:uint; |
63 |
for (var i:uint=0; i<levelData.length; i++) |
64 |
{
|
65 |
for (var j:uint=0; j<levelData[0].length; j++) |
66 |
{
|
67 |
tileType = levelData[i][j]; |
68 |
placeTile(tileType,i,j); |
69 |
if (tileType == 2) |
70 |
{
|
71 |
levelData[i][j] = 0; |
72 |
}
|
73 |
}
|
74 |
}
|
75 |
overlayContainer.addChild(heroPointer); |
76 |
overlayContainer.alpha=0.5; |
77 |
overlayContainer.scaleX=overlayContainer.scaleY=0.5; |
78 |
overlayContainer.y=290; |
79 |
overlayContainer.x=10; |
80 |
depthSort(); |
81 |
addEventListener(Event.ENTER_FRAME,loop); |
82 |
}
|
83 |
|
84 |
//place the tile based on coordinates
|
85 |
function placeTile(id:uint,i:uint,j:uint) |
86 |
{
|
87 |
var pos:Point=new Point(); |
88 |
if (id == 2) |
89 |
{
|
90 |
|
91 |
id = 0; |
92 |
pos.x = j * tileWidth; |
93 |
pos.y = i * tileWidth; |
94 |
pos = IsoHelper.twoDToIso(pos); |
95 |
hero.x = borderOffsetX + pos.x; |
96 |
hero.y = borderOffsetY + pos.y; |
97 |
//overlayContainer.addChild(hero);
|
98 |
heroCartPos.x = j * tileWidth; |
99 |
heroCartPos.y = i * tileWidth; |
100 |
heroTile.x=j; |
101 |
heroTile.y=i; |
102 |
heroPointer=new herodot(); |
103 |
heroPointer.x=heroCartPos.x; |
104 |
heroPointer.y=heroCartPos.y; |
105 |
|
106 |
}
|
107 |
var tile:MovieClip=new cartTile(); |
108 |
tile.gotoAndStop(id+1); |
109 |
tile.x = j * tileWidth; |
110 |
tile.y = i * tileWidth; |
111 |
overlayContainer.addChild(tile); |
112 |
}
|
113 |
|
114 |
//the game loop
|
115 |
function loop(e:Event) |
116 |
{
|
117 |
if (key.isDown(Keyboard.UP)) |
118 |
{
|
119 |
dY = -1; |
120 |
}
|
121 |
else if (key.isDown(Keyboard.DOWN)) |
122 |
{
|
123 |
dY = 1; |
124 |
}
|
125 |
else
|
126 |
{
|
127 |
dY = 0; |
128 |
}
|
129 |
if (key.isDown(Keyboard.RIGHT)) |
130 |
{
|
131 |
dX = 1; |
132 |
if (dY == 0) |
133 |
{
|
134 |
facing = "east"; |
135 |
}
|
136 |
else if (dY==1) |
137 |
{
|
138 |
facing = "southeast"; |
139 |
dX = dY=0.5; |
140 |
}
|
141 |
else
|
142 |
{
|
143 |
facing = "northeast"; |
144 |
dX=0.5; |
145 |
dY=-0.5; |
146 |
}
|
147 |
}
|
148 |
else if (key.isDown(Keyboard.LEFT)) |
149 |
{
|
150 |
dX = -1; |
151 |
if (dY == 0) |
152 |
{
|
153 |
facing = "west"; |
154 |
}
|
155 |
else if (dY==1) |
156 |
{
|
157 |
facing = "southwest"; |
158 |
dY=0.5; |
159 |
dX=-0.5; |
160 |
}
|
161 |
else
|
162 |
{
|
163 |
facing = "northwest"; |
164 |
dX = dY=-0.5; |
165 |
}
|
166 |
}
|
167 |
else
|
168 |
{
|
169 |
dX = 0; |
170 |
if (dY == 0) |
171 |
{
|
172 |
//facing="west";
|
173 |
}
|
174 |
else if (dY==1) |
175 |
{
|
176 |
facing = "south"; |
177 |
}
|
178 |
else
|
179 |
{
|
180 |
facing = "north"; |
181 |
}
|
182 |
}
|
183 |
if (dY == 0 && dX == 0) |
184 |
{
|
185 |
hero.clip.gotoAndStop(facing); |
186 |
idle = true; |
187 |
}
|
188 |
else if (idle||currentFacing!=facing) |
189 |
{
|
190 |
idle = false; |
191 |
currentFacing = facing; |
192 |
hero.clip.gotoAndPlay(facing); |
193 |
}
|
194 |
if (! idle && isWalkable()) |
195 |
{
|
196 |
heroCartPos.x += speed * dX; |
197 |
heroCartPos.y += speed * dY; |
198 |
heroPointer.x=heroCartPos.x; |
199 |
heroPointer.y=heroCartPos.y; |
200 |
|
201 |
var newPos:Point = IsoHelper.twoDToIso(heroCartPos); |
202 |
//collision check
|
203 |
hero.x = borderOffsetX + newPos.x; |
204 |
hero.y = borderOffsetY + newPos.y; |
205 |
heroTile=IsoHelper.getTileCoordinates(heroCartPos,tileWidth); |
206 |
depthSort(); |
207 |
//trace(heroTile);
|
208 |
}
|
209 |
tileTxt.text="Hero is on x: "+heroTile.x +" & y: "+heroTile.y; |
210 |
}
|
211 |
|
212 |
//check for collision tile
|
213 |
function isWalkable():Boolean{ |
214 |
var able:Boolean=true; |
215 |
var newPos:Point =new Point(); |
216 |
newPos.x=heroCartPos.x + (speed * dX); |
217 |
newPos.y=heroCartPos.y + (speed * dY); |
218 |
switch (facing){ |
219 |
case "north": |
220 |
newPos.y-=heroHalfSize; |
221 |
break; |
222 |
case "south": |
223 |
newPos.y+=heroHalfSize; |
224 |
break; |
225 |
case "east": |
226 |
newPos.x+=heroHalfSize; |
227 |
break; |
228 |
case "west": |
229 |
newPos.x-=heroHalfSize; |
230 |
break; |
231 |
case "northeast": |
232 |
newPos.y-=heroHalfSize; |
233 |
newPos.x+=heroHalfSize; |
234 |
break; |
235 |
case "southeast": |
236 |
newPos.y+=heroHalfSize; |
237 |
newPos.x+=heroHalfSize; |
238 |
break; |
239 |
case "northwest": |
240 |
newPos.y-=heroHalfSize; |
241 |
newPos.x-=heroHalfSize; |
242 |
break; |
243 |
case "southwest": |
244 |
newPos.y+=heroHalfSize; |
245 |
newPos.x-=heroHalfSize; |
246 |
break; |
247 |
}
|
248 |
newPos=IsoHelper.getTileCoordinates(newPos,tileWidth); |
249 |
if(levelData[newPos.y][newPos.x]==1){ |
250 |
able=false; |
251 |
}else{ |
252 |
//trace("new",newPos);
|
253 |
}
|
254 |
return able; |
255 |
}
|
256 |
|
257 |
//sort depth & draw to canvas
|
258 |
function depthSort() |
259 |
{
|
260 |
bg.bitmapData.lock(); |
261 |
bg.bitmapData.fillRect(rect,0xffffff); |
262 |
var tileType:uint; |
263 |
var mat:Matrix=new Matrix(); |
264 |
var pos:Point=new Point(); |
265 |
for (var i:uint=0; i<levelData.length; i++) |
266 |
{
|
267 |
for (var j:uint=0; j<levelData[0].length; j++) |
268 |
{
|
269 |
tileType = levelData[i][j]; |
270 |
//placeTile(tileType,i,j);
|
271 |
|
272 |
pos.x = j * tileWidth; |
273 |
pos.y = i * tileWidth; |
274 |
pos = IsoHelper.twoDToIso(pos); |
275 |
mat.tx = borderOffsetX + pos.x; |
276 |
mat.ty = borderOffsetY + pos.y; |
277 |
if(tileType==0){ |
278 |
bg.bitmapData.draw(grassTile,mat); |
279 |
}else{ |
280 |
bg.bitmapData.draw(wallTile,mat); |
281 |
}
|
282 |
if(heroTile.x==j&&heroTile.y==i){ |
283 |
mat.tx=hero.x; |
284 |
mat.ty=hero.y; |
285 |
bg.bitmapData.draw(hero,mat); |
286 |
}
|
287 |
|
288 |
}
|
289 |
}
|
290 |
bg.bitmapData.unlock(); |
291 |
//add character rectangle
|
292 |
}
|
293 |
createLevel(); |
Puntos de registro
Presta especial atención a los puntos de registro de las fichas y al héroe. (Los puntos de registro pueden considerarse como los puntos de origen para cada sprite en particular). Generalmente, estos no caerán dentro de la imagen, sino que serán la esquina superior izquierda del cuadro delimitador del sprite.
Tendremos que modificar nuestro código de dibujo para corregir los puntos de registro correctamente, principalmente para el héroe.
Detección de colisiones
Otro punto interesante a tener en cuenta es que calculamos la detección de colisión en función del punto donde se encuentra el héroe.
Pero el héroe tiene volumen y no puede representarse con precisión con un solo punto, por lo que debemos representar al héroe como un rectángulo y verificar las colisiones contra cada esquina de este rectángulo para que no haya solapamientos con otros mosaicos y, por lo tanto, no haya artefactos de profundidad.
Atajos
En la demostración, simplemente vuelvo a dibujar la escena de nuevo en cada fotograma según la nueva posición del héroe. Encontramos la ficha que ocupa el héroe y dibujamos al héroe sobre la ficha de tierra cuando los bucles de representación llegan a esas fichas.
Pero si miramos más de cerca, veremos que no hay necesidad de recorrer todas las fichas en este caso. Las fichas de hierba y las fichas de la pared superior e izquierda siempre se dibujan antes de que se dibuje el héroe, por lo que no es necesario que vuelvas a dibujarlas. Además, los azulejos de la pared inferior y derecha siempre están delante del héroe y, por lo tanto, se dibujan después de que se dibuja el héroe.
Esencialmente, entonces, solo tenemos que realizar una clasificación en profundidad entre la pared dentro del área activa y el héroe, es decir, dos fichas. Al darse cuenta de este tipo de accesos directos, podrá ahorrar mucho tiempo de procesamiento, lo que puede ser crucial para el rendimiento.
Conclusión
Por ahora, debe tener una gran base para construir sus propios juegos isométricos: puede renderizar el mundo y los objetos en él, representar datos de nivel en matrices 2D simples, convertir entre coordenadas cartesianas e isométricas, y tratar conceptos como la clasificación en profundidad y animación de personajes. ¡Disfruta creando mundos isométricos!