Creación de mundos isométricos: una guía para desarrolladores de juegos, continuación
() translation by (you can also view the original English article)
En este tutorial, desarrollaremos el primer original Creando Mundos Isométricos, y aprenderemos sobre la implementación de recolecciones, fichas de activación, intercambio de niveles, búsqueda y seguimiento de rutas, desplazamiento de niveles, altura isométrica y proyectiles isométricos.
1. Recolecciones
Las recolecciones son elementos que se pueden recoger dentro del nivel, normalmente simplemente caminando sobre ellos, por ejemplo, monedas, gemas, efectivo y munición.
Los datos de recolección también se pueden acomodar directamente en nuestros datos de nivel de la siguiente manera:
1 |
[[1,1,1,1,1,1], |
2 |
[1,0,0,0,0,1], |
3 |
[1,0,8,0,0,1], |
4 |
[1,0,0,8,0,1], |
5 |
[1,0,0,0,0,1], |
6 |
[1,1,1,1,1,1]] |
En este
nivel de datos, usamos 8
para denotar un arranque (1
y 0
representan
paredes y baldosas transitables, respectivamente, como antes).
Es
importante entender que 8
en realidad denota dos fichas, no solo una:
significa que primero tenemos que colocar una loseta para pasto y luego
colocar una camioneta en la parte superior. Esto significa que cada
recolección siempre estará en un mosaico de césped. Si
queremos que esté sobre una losa de ladrillo transitable, entonces
necesitaremos otra loseta denotada por otro número, digamos 9
, que
represente "recogido en la losa de ladrillo".
El
arte isométrico típico tendrá múltiples losetas transitables,
supongamos que tenemos 30. El enfoque anterior significa que si tenemos N
pastillas, necesitaremos baldosas (N * 30
) además de las 30 fichas
originales, ya que cada baldosa deberá tener una versión con pastillas y
una sin. Esto no es muy eficiente; en su lugar, debemos tratar de crear
dinámicamente estas combinaciones.
Para hacer esto, podemos usar otra matriz con los datos de recolección por sí solos, y usar esto para colocar mosaicos de recolección encima de los datos de diseño de niveles:
1 |
// Level layout data
|
2 |
[[1,1,1,1,1,1], |
3 |
[1,0,0,0,0,1], |
4 |
[1,0,0,0,0,1], |
5 |
[1,0,0,0,0,1], |
6 |
[1,0,0,0,0,1], |
7 |
[1,1,1,1,1,1]] |
...más:
1 |
// Pickup layout data
|
2 |
[[0,0,0,0,0,0], |
3 |
[0,0,0,0,0,0], |
4 |
[0,0,8,0,0,0], |
5 |
[0,0,0,8,0,0], |
6 |
[0,0,0,0,0,0], |
7 |
[0,0,0,0,0,0]] |
...resultados en:



Este enfoque asegura que solo necesitamos los 30 mosaicos originales además de N fichas de selección, ya que podemos crear cualquier combinación combinando ambas piezas de arte al representar el nivel.
Recogiendo recogidas
La detección de las recolecciones se realiza de la misma manera que la detección de las fichas de colisión, pero después de mover el personaje.
1 |
tile coordinate= getTileCoordinates (isoTo2D (Iso point) , tile height); |
2 |
if(isPickup(tile coordinate)) { |
3 |
pickupItem(tile coordinate); |
4 |
}
|
En
la función isPickup(tile coordinate)
, verificamos si el valor
del conjunto de datos de captación en la coordenada dada es un mosaico
de captación o no. El número en la matriz de captación en esa coordenada de mosaico indica el tipo de captación.
Comprobamos las colisiones antes de mover el personaje, pero luego buscamos recogidas, porque en el caso de las colisiones el personaje no debería ocupar el lugar si ya está ocupado por la ficha de colisión, pero en el caso de las recolecciones, el personaje puede moverse libremente sobre él.
Otra
cosa a tener en cuenta es que los datos de colisión generalmente nunca
cambian, pero los datos de recolección cambian cada vez que recogemos un
artículo. (Esto generalmente implica cambiar el valor en la matriz de recolección de, digamos, 8
a 0
.)
Esto
genera un problema: ¿qué sucede cuando tenemos que reiniciar el nivel
y, por lo tanto, restablecer todas las recolecciones a sus posiciones
originales? No
tenemos la información para hacer esto, ya que la matriz de recolección
se ha cambiado a medida que el jugador recogió los artículos. La
solución es usar una matriz duplicada para recolecciones mientras está
en juego y mantener intacta la matriz de recolección original; por
ejemplo, usamos pickupsArray []
y pickupsLive[]
, clonamos la última de
la anterior al inicio del nivel, y solo cambiar pickupsLive[]
durante el juego.
Deberías notar que buscamos
recolecciones cada vez que el personaje está en ese mosaico. Esto
puede ocurrir varias veces en un segundo (solo verificamos cuando el
usuario se mueve, pero podemos dar vueltas y vueltas dentro de un
mosaico) pero la lógica anterior no fallará; ya que establecemos los
datos de la matriz de recolección en 0
la
primera vez que detectamos un recogido, todas las verificaciones
siguientes isPickup(tile)
devolverán resultados falsos false
para
esa ficha.
2. Trigger Tiles
Como su nombre indica, las casillas de activación causan que algo suceda cuando el jugador las pisa o presiona una tecla al pisarlas. Pueden teletransportar al jugador a una ubicación diferente, abrir una puerta o engendrar un enemigo, por poner algunos ejemplos. En cierto sentido, las pastillas son solo una forma especial de fichas de activación: cuando el jugador pisa una ficha que contiene una moneda, la moneda desaparece y el contador de monedas aumenta.
Veamos cómo podemos implementar una puerta que lleva al jugador a un nivel diferente. La ficha al lado de la puerta será una ficha de activación; cuando el jugador presione la barra espaciadora, pasarán al siguiente nivel.



Para cambiar los niveles, todo lo que tenemos que hacer es intercambiar la matriz de datos del nivel actual con la del nuevo nivel, y establecer la nueva posición y dirección del elemento héroe.
Supongamos que hay dos niveles con puertas para permitir el paso entre ellos. Como la casilla de tierra junto a la puerta será la casilla de activación en ambos niveles, podemos usar esto como la nueva posición para el personaje cuando aparecen en el nivel.
La
lógica de implementación aquí es la misma que para las recolecciones, y
nuevamente usamos una matriz para almacenar los valores de activación. Esto
es ineficiente y debe considerar otras estructuras de datos para este
propósito, pero vamos a mantener esto simple por el bien del tutorial.
Deje que las nuevas matrices de niveles sean las siguientes (7
denota
una puerta):
1 |
// Level 1
|
2 |
[[1,1,1,1,1,1], |
3 |
[1,0,0,0,0,1], |
4 |
[1,0,0,0,0,7], |
5 |
[1,0,0,0,0,1], |
6 |
[1,0,0,0,1,1], |
7 |
[1,1,1,1,1,1]]; |
8 |
|
9 |
// Level 2
|
10 |
[[1,1,1,1,1,1], |
11 |
[1,0,0,0,1,1], |
12 |
[7,0,0,0,0,1], |
13 |
[1,0,0,0,0,1], |
14 |
[1,1,0,0,0,1], |
15 |
[1,1,1,1,1,1]]; |
Deje que los niveles tengan algunas pastillas, como se detalla en las matrices de recolección siguientes:
1 |
[[0,0,0,0,0,0], |
2 |
[0,0,0,0,0,0], |
3 |
[0,0,8,0,0,0], |
4 |
[0,0,0,8,0,0], |
5 |
[0,0,0,0,0,0], |
6 |
[0,0,0,0,0,0]]; |
7 |
|
8 |
[[0,0,0,0,0,0], |
9 |
[0,0,0,0,0,0], |
10 |
[0,0,0,0,0,0], |
11 |
[0,0,0,0,8,0], |
12 |
[0,0,8,0,0,0], |
13 |
[0,0,0,0,0,0]]; |
Deje que las matrices de fichas de activación correspondientes para cada nivel sean las siguientes:
1 |
[[0,0,0,0,0,0], |
2 |
[0,0,0,0,0,0], |
3 |
[0,0,0,0,2,0], |
4 |
[0,0,0,0,0,0], |
5 |
[0,0,0,0,0,0], |
6 |
[0,0,0,0,0,0]]; |
7 |
|
8 |
[[0,0,0,0,0,0], |
9 |
[0,0,0,0,0,0], |
10 |
[0,1,0,0,0,0], |
11 |
[0,0,0,0,0,0], |
12 |
[0,0,0,0,0,0], |
13 |
[0,0,0,0,0,0]]; |
Los valores (1
y 2
) indican el nivel que se cargará cuando el jugador presiona Espacio.
Aquí está el código que se ejecuta cuando el jugador golpea esa tecla:
1 |
// Pseudocode
|
2 |
tile coordinate = getTileCoordinates(isoTo2D(Iso point), tile height); |
3 |
if (isTrigger(tile coordinate)) { |
4 |
doRelevantAction(tile coordinate); |
5 |
}
|
La función isTrigger ()
comprueba si el valor del conjunto de datos de activación en la coordenada dada es mayor que cero. Si es así, nuestro código pasa ese valor a doRelevantAction()
, que decide a qué función llamar después. Para
nuestros propósitos, usaremos la regla simple de que si el valor se
encuentra entre 1
y 10
, es una puerta, por lo que se llamará a esta
función:
1 |
function swapLevel(level) { |
2 |
// swap level arrays, pickup arrays, and trigger arrays with new level's data
|
3 |
// move player's starting position to tile next to door that leads to previous level
|
4 |
// set character's direction
|
5 |
}
|
Dado que el
valor de la tesela en la matriz de disparadores también denota el nivel
que debe cargarse, simplemente podemos pasarlo a swapLevel()
. Esto implica, a su vez, que nuestro juego tiene diez niveles.
Aquí hay una demostración funcional. Intenta recoger objetos caminando sobre ellos e intercambiando niveles de pie junto a las puertas y golpeando Espacio.
He hecho que el disparador se active cuando se libera Espacio; si solo escuchamos presionar la tecla, terminamos en un bucle en el que intercambiamos los niveles siempre que la tecla se mantenga presionada, ya que el carácter siempre aparece en el nuevo nivel encima de una ficha de activación.
Aquí está el código completo (en AS3):
1 |
import flash.display.Sprite; |
2 |
import com.csharks.juwalbose.IsoHelper; |
3 |
import flash.display.MovieClip; |
4 |
import flash.geom.Point; |
5 |
import flash.filters.GlowFilter; |
6 |
import flash.events.Event; |
7 |
import com.senocular.utils.KeyObject; |
8 |
import flash.ui.Keyboard; |
9 |
import flash.display.Bitmap; |
10 |
import flash.display.BitmapData; |
11 |
import flash.geom.Matrix; |
12 |
import flash.geom.Rectangle; |
13 |
import flash.events.KeyboardEvent; |
14 |
|
15 |
var level1Data=[[1,1,1,1,1,1], |
16 |
[1,0,0,0,0,1], |
17 |
[1,0,0,0,0,7], |
18 |
[1,0,0,0,0,1], |
19 |
[1,0,0,0,1,1], |
20 |
[1,1,1,1,1,1]]; |
21 |
|
22 |
var pickup1Array=[[0,0,0,0,0,0], |
23 |
[0,0,0,0,0,0], |
24 |
[0,0,8,0,0,0], |
25 |
[0,0,0,8,0,0], |
26 |
[0,0,0,0,0,0], |
27 |
[0,0,0,0,0,0]]; |
28 |
|
29 |
var level2Data=[[1,1,1,1,1,1], |
30 |
[1,0,0,0,1,1], |
31 |
[7,0,0,0,0,1], |
32 |
[1,0,0,0,0,1], |
33 |
[1,1,0,0,0,1], |
34 |
[1,1,1,1,1,1]]; |
35 |
|
36 |
var pickup2Array=[[0,0,0,0,0,0], |
37 |
[0,0,0,0,0,0], |
38 |
[0,0,0,0,0,0], |
39 |
[0,0,0,0,8,0], |
40 |
[0,0,8,0,0,0], |
41 |
[0,0,0,0,0,0]]; |
42 |
|
43 |
var trigger1Array=[[0,0,0,0,0,0], |
44 |
[0,0,0,0,0,0], |
45 |
[0,0,0,0,2,0], |
46 |
[0,0,0,0,0,0], |
47 |
[0,0,0,0,0,0], |
48 |
[0,0,0,0,0,0]]; |
49 |
|
50 |
var trigger2Array=[[0,0,0,0,0,0], |
51 |
[0,0,0,0,0,0], |
52 |
[0,1,0,0,0,0], |
53 |
[0,0,0,0,0,0], |
54 |
[0,0,0,0,0,0], |
55 |
[0,0,0,0,0,0]]; |
56 |
|
57 |
|
58 |
var levelData:Array=new Array(); |
59 |
var pickupArray:Array=new Array(); |
60 |
var triggerArray:Array=new Array(); |
61 |
var currentLevel:uint=1; |
62 |
var newLevel:uint=1; |
63 |
|
64 |
var tileWidth:uint = 50; |
65 |
var borderOffsetY:uint = 70; |
66 |
var borderOffsetX:uint = 275; |
67 |
|
68 |
var facing:String = "south"; |
69 |
var currentFacing:String = "south"; |
70 |
var hero:MovieClip=new herotile(); |
71 |
hero.clip.gotoAndStop(facing); |
72 |
var heroPointer:Sprite; |
73 |
var key:KeyObject = new KeyObject(stage);//Senocular KeyObject Class |
74 |
var heroHalfSize:uint=20; |
75 |
var pickupCount:uint=0; |
76 |
|
77 |
//the tiles
|
78 |
var grassTile:MovieClip=new TileMc(); |
79 |
grassTile.gotoAndStop(1); |
80 |
var wallTile:MovieClip=new TileMc(); |
81 |
wallTile.gotoAndStop(2); |
82 |
var pickupTile:MovieClip=new TileMc(); |
83 |
pickupTile.gotoAndStop(3); |
84 |
var doorTile:MovieClip=new TileMc(); |
85 |
doorTile.gotoAndStop(4); |
86 |
|
87 |
//the canvas
|
88 |
var bg:Bitmap = new Bitmap(new BitmapData(650,450)); |
89 |
addChild(bg); |
90 |
var rect:Rectangle=bg.bitmapData.rect; |
91 |
|
92 |
//to handle depth
|
93 |
var overlayContainer:Sprite=new Sprite(); |
94 |
addChild(overlayContainer); |
95 |
|
96 |
//to handle direction movement
|
97 |
var dX:Number = 0; |
98 |
var dY:Number = 0; |
99 |
var idle:Boolean = true; |
100 |
var speed:uint = 5; |
101 |
var heroCartPos:Point=new Point(); |
102 |
var heroTile:Point=new Point(); |
103 |
|
104 |
var spaceKeyUp:Boolean=false; |
105 |
//initial hero position
|
106 |
var spawnPt:Point=new Point(1,3); |
107 |
|
108 |
//add items to start level, add game loop
|
109 |
function createLevel() |
110 |
{
|
111 |
if(currentLevel==1){ |
112 |
levelData=level1Data; |
113 |
pickupArray=pickup1Array; |
114 |
triggerArray=trigger1Array; |
115 |
}else{ |
116 |
levelData=level2Data; |
117 |
pickupArray=pickup2Array; |
118 |
triggerArray=trigger2Array; |
119 |
}
|
120 |
levelData[spawnPt.x][spawnPt.y]=2; |
121 |
|
122 |
var tileType:uint; |
123 |
for (var i:uint=0; i<levelData.length; i++) |
124 |
{
|
125 |
for (var j:uint=0; j<levelData[0].length; j++) |
126 |
{
|
127 |
tileType = levelData[i][j]; |
128 |
placeTile(tileType,i,j); |
129 |
if (tileType == 2) |
130 |
{
|
131 |
//trace(i,j);
|
132 |
levelData[i][j] = 0; |
133 |
}
|
134 |
}
|
135 |
}
|
136 |
|
137 |
depthSort(); |
138 |
addEventListener(Event.ENTER_FRAME,loop); |
139 |
}
|
140 |
|
141 |
//place the tile based on coordinates
|
142 |
function placeTile(id:uint,i:uint,j:uint) |
143 |
{
|
144 |
var pos:Point=new Point(); |
145 |
if (id == 2) |
146 |
{
|
147 |
|
148 |
id = 0; |
149 |
pos.x = j * tileWidth; |
150 |
pos.y = i * tileWidth; |
151 |
pos = IsoHelper.twoDToIso(pos); |
152 |
hero.x = borderOffsetX + pos.x; |
153 |
hero.y = borderOffsetY + pos.y; |
154 |
//overlayContainer.addChild(hero);
|
155 |
heroCartPos.x = j * tileWidth; |
156 |
heroCartPos.y = i * tileWidth; |
157 |
heroTile.x=j; |
158 |
heroTile.y=i; |
159 |
heroPointer=new herodot(); |
160 |
heroPointer.x=heroCartPos.x; |
161 |
heroPointer.y=heroCartPos.y; |
162 |
|
163 |
}
|
164 |
|
165 |
}
|
166 |
|
167 |
//the game loop
|
168 |
function loop(e:Event) |
169 |
{
|
170 |
|
171 |
if (key.isDown(Keyboard.UP)) |
172 |
{
|
173 |
dY = -1; |
174 |
}
|
175 |
else if (key.isDown(Keyboard.DOWN)) |
176 |
{
|
177 |
dY = 1; |
178 |
}
|
179 |
else
|
180 |
{
|
181 |
dY = 0; |
182 |
}
|
183 |
if (key.isDown(Keyboard.RIGHT)) |
184 |
{
|
185 |
dX = 1; |
186 |
if (dY == 0) |
187 |
{
|
188 |
facing = "east"; |
189 |
}
|
190 |
else if (dY==1) |
191 |
{
|
192 |
facing = "southeast"; |
193 |
dX = dY=0.5; |
194 |
}
|
195 |
else
|
196 |
{
|
197 |
facing = "northeast"; |
198 |
dX=0.5; |
199 |
dY=-0.5; |
200 |
}
|
201 |
}
|
202 |
else if (key.isDown(Keyboard.LEFT)) |
203 |
{
|
204 |
dX = -1; |
205 |
if (dY == 0) |
206 |
{
|
207 |
facing = "west"; |
208 |
}
|
209 |
else if (dY==1) |
210 |
{
|
211 |
facing = "southwest"; |
212 |
dY=0.5; |
213 |
dX=-0.5; |
214 |
}
|
215 |
else
|
216 |
{
|
217 |
facing = "northwest"; |
218 |
dX = dY=-0.5; |
219 |
}
|
220 |
}
|
221 |
else
|
222 |
{
|
223 |
dX = 0; |
224 |
if (dY == 0) |
225 |
{
|
226 |
//facing="west";
|
227 |
}
|
228 |
else if (dY==1) |
229 |
{
|
230 |
facing = "south"; |
231 |
}
|
232 |
else
|
233 |
{
|
234 |
facing = "north"; |
235 |
}
|
236 |
}
|
237 |
if (dY == 0 && dX == 0) |
238 |
{
|
239 |
hero.clip.gotoAndStop(facing); |
240 |
idle = true; |
241 |
}
|
242 |
else if (idle||currentFacing!=facing) |
243 |
{
|
244 |
idle = false; |
245 |
currentFacing = facing; |
246 |
hero.clip.gotoAndPlay(facing); |
247 |
}
|
248 |
if (! idle && isWalkable()) |
249 |
{
|
250 |
heroCartPos.x += speed * dX; |
251 |
heroCartPos.y += speed * dY; |
252 |
heroPointer.x=heroCartPos.x; |
253 |
heroPointer.y=heroCartPos.y; |
254 |
|
255 |
var newPos:Point = IsoHelper.twoDToIso(heroCartPos); |
256 |
//collision check
|
257 |
hero.x = borderOffsetX + newPos.x; |
258 |
hero.y = borderOffsetY + newPos.y; |
259 |
heroTile=IsoHelper.getTileCoordinates(heroCartPos,tileWidth); |
260 |
|
261 |
if(isPickup(heroTile)){ |
262 |
pickupItem(heroTile); |
263 |
}
|
264 |
depthSort(); |
265 |
//trace(heroTile);
|
266 |
}
|
267 |
tileTxt.text="Level "+ currentLevel.toString()+" - x: "+heroTile.x +" & y: "+heroTile.y; |
268 |
pickupTxt.text="Pickups Collected: "+pickupCount.toString(); |
269 |
|
270 |
if (spaceKeyUp) |
271 |
{
|
272 |
spaceKeyUp=false; |
273 |
if(isTrigger(heroTile)){ |
274 |
doRelevantAction(heroTile); |
275 |
}
|
276 |
}
|
277 |
}
|
278 |
function doRelevantAction(tilePt:Point):void{ |
279 |
newLevel=triggerArray[tilePt.y][tilePt.x]; |
280 |
|
281 |
if(newLevel==1){ |
282 |
spawnPt=getSpawn(trigger1Array,currentLevel); |
283 |
}else{ |
284 |
spawnPt=getSpawn(trigger2Array,currentLevel); |
285 |
}
|
286 |
|
287 |
swapLevel(newLevel); |
288 |
}
|
289 |
function getSpawn(ar:Array,index:uint):Point{ |
290 |
var tilePt:Point=new Point(); |
291 |
for (var i:uint=0; i<ar.length; i++) |
292 |
{
|
293 |
for (var j:uint=0; j<ar[0].length; j++) |
294 |
{
|
295 |
if(ar[i][j]==index){ |
296 |
tilePt.x=i; |
297 |
tilePt.y=j; |
298 |
break; |
299 |
}
|
300 |
}
|
301 |
}
|
302 |
return tilePt; |
303 |
}
|
304 |
function swapLevel(level:uint):void{ |
305 |
removeEventListener(Event.ENTER_FRAME,loop); |
306 |
currentLevel=level; |
307 |
//trace("load",level);
|
308 |
createLevel(); |
309 |
}
|
310 |
function isPickup(tilePt:Point):Boolean{ |
311 |
return(pickupArray[tilePt.y][tilePt.x]==8); |
312 |
}
|
313 |
function isTrigger(tilePt:Point):Boolean{ |
314 |
return(triggerArray[tilePt.y][tilePt.x]>0); |
315 |
}
|
316 |
function pickupItem(tilePt:Point):void{ |
317 |
pickupCount++; |
318 |
pickupArray[tilePt.y][tilePt.x]=0; |
319 |
}
|
320 |
//check for collision tile
|
321 |
function isWalkable():Boolean{ |
322 |
var able:Boolean=true; |
323 |
var newPos:Point =new Point(); |
324 |
newPos.x=heroCartPos.x + (speed * dX); |
325 |
newPos.y=heroCartPos.y + (speed * dY); |
326 |
switch (facing){ |
327 |
case "north": |
328 |
newPos.y-=heroHalfSize; |
329 |
break; |
330 |
case "south": |
331 |
newPos.y+=heroHalfSize; |
332 |
break; |
333 |
case "east": |
334 |
newPos.x+=heroHalfSize; |
335 |
break; |
336 |
case "west": |
337 |
newPos.x-=heroHalfSize; |
338 |
break; |
339 |
case "northeast": |
340 |
newPos.y-=heroHalfSize; |
341 |
newPos.x+=heroHalfSize; |
342 |
break; |
343 |
case "southeast": |
344 |
newPos.y+=heroHalfSize; |
345 |
newPos.x+=heroHalfSize; |
346 |
break; |
347 |
case "northwest": |
348 |
newPos.y-=heroHalfSize; |
349 |
newPos.x-=heroHalfSize; |
350 |
break; |
351 |
case "southwest": |
352 |
newPos.y+=heroHalfSize; |
353 |
newPos.x-=heroHalfSize; |
354 |
break; |
355 |
}
|
356 |
newPos=IsoHelper.getTileCoordinates(newPos,tileWidth); |
357 |
if(levelData[newPos.y][newPos.x]>=1){ |
358 |
able=false; |
359 |
}else{ |
360 |
//trace("new",newPos);
|
361 |
}
|
362 |
return able; |
363 |
}
|
364 |
|
365 |
//sort depth & draw to canvas
|
366 |
function depthSort() |
367 |
{
|
368 |
bg.bitmapData.lock(); |
369 |
bg.bitmapData.fillRect(rect,0xffffff); |
370 |
var tileType:uint; |
371 |
var mat:Matrix=new Matrix(); |
372 |
var pos:Point=new Point(); |
373 |
for (var i:uint=0; i<levelData.length; i++) |
374 |
{
|
375 |
for (var j:uint=0; j<levelData[0].length; j++) |
376 |
{
|
377 |
tileType = levelData[i][j]; |
378 |
//placeTile(tileType,i,j);
|
379 |
|
380 |
pos.x = j * tileWidth; |
381 |
pos.y = i * tileWidth; |
382 |
pos = IsoHelper.twoDToIso(pos); |
383 |
mat.tx = borderOffsetX + pos.x; |
384 |
mat.ty = borderOffsetY + pos.y; |
385 |
if(tileType==0){ |
386 |
bg.bitmapData.draw(grassTile,mat); |
387 |
}else if(tileType==1){ |
388 |
bg.bitmapData.draw(wallTile,mat); |
389 |
}else if(tileType==7){ |
390 |
bg.bitmapData.draw(doorTile,mat); |
391 |
}
|
392 |
if(pickupArray[i][j]==8){ |
393 |
bg.bitmapData.draw(pickupTile,mat); |
394 |
}
|
395 |
if(heroTile.x==j&&heroTile.y==i){ |
396 |
mat.tx=hero.x; |
397 |
mat.ty=hero.y; |
398 |
bg.bitmapData.draw(hero,mat); |
399 |
}
|
400 |
|
401 |
}
|
402 |
}
|
403 |
bg.bitmapData.unlock(); |
404 |
//add character rectangle
|
405 |
}
|
406 |
function handleKeyUp(e:KeyboardEvent):void{//listen for space key release |
407 |
if(e.charCode==Keyboard.SPACE){ |
408 |
spaceKeyUp=true; |
409 |
}
|
410 |
}
|
411 |
stage.addEventListener(KeyboardEvent.KEY_UP,handleKeyUp); |
412 |
createLevel(); |
3. Búsqueda de ruta
La búsqueda de ruta y seguimiento de ruta es un proceso bastante complicado. Hay varios enfoques que usan diferentes algoritmos para encontrar la ruta entre dos puntos, pero nuestros datos de nivel son una matriz 2D. Las cosas son más fáciles de lo que podrían ser: tenemos nodos bien definidos y únicos que el jugador puede ocupar y podemos verificar fácilmente si son caminables
Una visión detallada de los algoritmos de ruta está fuera del alcance de este artículo, pero intentaré explicar la forma más común de hacerlo funcionar: el algoritmo de ruta más corta, cuyos algoritmos A * y Dijkstra son implementaciones famosas.
Nuestro objetivo es encontrar nodos conectando un nodo inicial y un nodo final. Desde el nodo de inicio visitamos los ocho nodos vecinos y los marcamos como visitados; este proceso central se repite para cada nodo recientemente visitado, recursivamente. Cada hilo rastrea los nodos visitados. Al saltar a los nodos vecinos, los nodos que ya han sido visitados se omiten (la recursión se detiene); de lo contrario, el proceso continúa hasta que llegamos al nodo final, donde la recursión finaliza y la ruta completa seguida se devuelve como una matriz de nodos. A veces, el nodo final nunca se alcanza, en cuyo caso falla la búsqueda de ruta. Por lo general, terminamos encontrando múltiples rutas entre los dos nodos, en cuyo caso tomamos el que tiene el menor número de nodos.
Hay muchas soluciones estándar disponibles para encontrar rutas basadas en matrices 2D, por lo que evitaremos reinventar esa rueda. Usemos esta solución AS3 (le recomiendo que revise esta gran demostración explicativa)).
La solución devuelve una matriz con puntos que forman la ruta desde el punto de inicio hasta el punto final:
1 |
path = PathFinder.go(start x, start y, end x, end y, levelData); |
Seguimiento de Camino
Una vez que tenemos la ruta como una matriz de nodos, tenemos que hacer que el personaje la siga.
Digamos que queremos hacer que el personaje camine hacia un azulejo en el que hacemos clic. Primero debemos buscar una ruta entre el nodo que el personaje ocupa actualmente y el nodo donde hicimos clic. Si se encuentra una ruta exitosa, entonces tenemos que mover el carácter al primer nodo en la matriz de nodos configurando es como el destino. Una vez que llegamos al nodo de destino, verificamos dónde hay más nodos en la matriz de nodos y, si es así, configuramos el próximo nodo como destino, y así sucesivamente hasta llegar al nodo final.
También cambiaremos la dirección del jugador según el nodo actual y el nuevo nodo de destino cada vez que lleguemos a un nodo. Entre los nodos, simplemente caminamos en la dirección requerida hasta que llegamos al nodo de destino. Esta es una IA muy simple.
Mira este ejemplo de trabajo:
Aquí está la fuente completa:
1 |
import flash.display.Sprite; |
2 |
import com.csharks.juwalbose.IsoHelper; |
3 |
import flash.display.MovieClip; |
4 |
import flash.geom.Point; |
5 |
import flash.filters.GlowFilter; |
6 |
import flash.events.Event; |
7 |
import com.senocular.utils.KeyObject; |
8 |
import flash.ui.Keyboard; |
9 |
import flash.display.Bitmap; |
10 |
import flash.display.BitmapData; |
11 |
import flash.geom.Matrix; |
12 |
import flash.geom.Rectangle; |
13 |
import flash.events.MouseEvent; |
14 |
|
15 |
import libs.PathFinder.*; |
16 |
|
17 |
var levelData=[[1,1,1,1,1,1,1,1], |
18 |
[1,0,0,0,0,2,0,1], |
19 |
[1,0,0,1,0,0,0,1], |
20 |
[1,1,0,0,0,0,0,1], |
21 |
[1,0,0,0,0,1,1,1], |
22 |
[1,0,0,1,0,0,0,1], |
23 |
[1,0,0,1,0,0,0,1], |
24 |
[1,1,1,1,1,1,1,1]]; |
25 |
|
26 |
var tileWidth:uint = 50; |
27 |
var borderOffsetY:uint = 70; |
28 |
var borderOffsetX:uint = 275; |
29 |
|
30 |
var facing:String = "south"; |
31 |
var currentFacing:String = "south"; |
32 |
var hero:MovieClip=new herotile(); |
33 |
hero.clip.gotoAndStop(facing); |
34 |
var heroPointer:Sprite; |
35 |
var heroHalfSize:uint=20; |
36 |
|
37 |
//the tiles
|
38 |
var grassTile:MovieClip=new TileMc(); |
39 |
grassTile.gotoAndStop(1); |
40 |
var wallTile:MovieClip=new TileMc(); |
41 |
wallTile.gotoAndStop(2); |
42 |
|
43 |
//the canvas
|
44 |
var bg:Bitmap = new Bitmap(new BitmapData(650,450)); |
45 |
addChild(bg); |
46 |
var rect:Rectangle=bg.bitmapData.rect; |
47 |
|
48 |
//to handle depth
|
49 |
var overlayContainer:Sprite=new Sprite(); |
50 |
addChild(overlayContainer); |
51 |
|
52 |
//to handle direction movement
|
53 |
var dX:Number = 0; |
54 |
var dY:Number = 0; |
55 |
var idle:Boolean = true; |
56 |
var speed:uint = 5; |
57 |
var heroCartPos:Point=new Point(); |
58 |
var heroTile:Point=new Point(); |
59 |
|
60 |
var path:Array=new Array(); |
61 |
var destination:Point=new Point(); |
62 |
var stepsTillTurn:uint=5; |
63 |
var stepsTaken:uint; |
64 |
|
65 |
var glowFilter:GlowFilter=new GlowFilter(0x00ffff); |
66 |
|
67 |
//add items to start level, add game loop
|
68 |
function createLevel() |
69 |
{
|
70 |
var tileType:uint; |
71 |
for (var i:uint=0; i<levelData.length; i++) |
72 |
{
|
73 |
for (var j:uint=0; j<levelData[0].length; j++) |
74 |
{
|
75 |
tileType = levelData[i][j]; |
76 |
placeTile(tileType,i,j); |
77 |
if (tileType == 2) |
78 |
{
|
79 |
levelData[i][j] = 0; |
80 |
}
|
81 |
}
|
82 |
}
|
83 |
overlayContainer.addChild(heroPointer); |
84 |
overlayContainer.alpha=0.9; |
85 |
overlayContainer.scaleX=overlayContainer.scaleY=0.3; |
86 |
overlayContainer.y=320; |
87 |
overlayContainer.x=10; |
88 |
depthSort(); |
89 |
|
90 |
addEventListener(Event.ENTER_FRAME,loop); |
91 |
}
|
92 |
|
93 |
//place the tile based on coordinates
|
94 |
function placeTile(id:uint,i:uint,j:uint) |
95 |
{
|
96 |
var pos:Point=new Point(); |
97 |
if (id == 2) |
98 |
{
|
99 |
|
100 |
id = 0; |
101 |
pos.x = j * tileWidth +tileWidth/2; |
102 |
pos.y = i * tileWidth +tileWidth/2; |
103 |
pos = IsoHelper.twoDToIso(pos); |
104 |
hero.x = borderOffsetX + pos.x; |
105 |
hero.y = borderOffsetY + pos.y; |
106 |
//overlayContainer.addChild(hero);
|
107 |
heroCartPos.x = j * tileWidth +tileWidth/2; |
108 |
heroCartPos.y = i * tileWidth +tileWidth/2; |
109 |
|
110 |
heroTile.x=j; |
111 |
heroTile.y=i; |
112 |
heroPointer=new herodot(); |
113 |
heroPointer.x=heroCartPos.x; |
114 |
heroPointer.y=heroCartPos.y; |
115 |
|
116 |
}
|
117 |
var tile:MovieClip=new cartTile(); |
118 |
tile.gotoAndStop(id+1); |
119 |
tile.x = j * tileWidth; |
120 |
tile.y = i * tileWidth; |
121 |
tile.name="tile_"+j.toString()+"_"+i.toString(); |
122 |
overlayContainer.addChild(tile); |
123 |
}
|
124 |
|
125 |
//the game loop
|
126 |
function loop(e:Event) |
127 |
{
|
128 |
aiWalk(); |
129 |
|
130 |
if (dY == 0 && dX == 0) |
131 |
{
|
132 |
hero.clip.gotoAndStop(facing); |
133 |
idle = true; |
134 |
}
|
135 |
else if (idle||currentFacing!=facing) |
136 |
{
|
137 |
//trace(idle,"facing ",currentFacing,facing);
|
138 |
idle = false; |
139 |
|
140 |
currentFacing = facing; |
141 |
hero.clip.gotoAndPlay(facing); |
142 |
}
|
143 |
if (! idle ) |
144 |
{
|
145 |
heroCartPos.x += speed * dX; |
146 |
heroCartPos.y += speed * dY; |
147 |
heroPointer.x=heroCartPos.x; |
148 |
heroPointer.y=heroCartPos.y; |
149 |
|
150 |
var newPos:Point = IsoHelper.twoDToIso(heroCartPos); |
151 |
//collision check
|
152 |
hero.x = borderOffsetX + newPos.x; |
153 |
hero.y = borderOffsetY + newPos.y; |
154 |
heroTile=IsoHelper.getTileCoordinates(heroCartPos,tileWidth); |
155 |
depthSort(); |
156 |
//trace(heroTile);
|
157 |
}
|
158 |
tileTxt.text="Hero is on x: "+heroTile.x +" & y: "+heroTile.y; |
159 |
}
|
160 |
function aiWalk():void{ |
161 |
if(path.length==0){//path has ended |
162 |
dX=dY=0; |
163 |
return; |
164 |
}
|
165 |
if(heroTile.equals(destination)){//reached current destination, set new, change direction |
166 |
//wait till we are few steps into the tile before we turn
|
167 |
stepsTaken++; |
168 |
if(stepsTaken<stepsTillTurn){ |
169 |
return; |
170 |
}
|
171 |
//place the hero at tile middle before turn
|
172 |
var pos:Point=new Point(); |
173 |
pos.x = heroTile.x * tileWidth +tileWidth/2; |
174 |
pos.y = heroTile.y * tileWidth +tileWidth/2; |
175 |
pos = IsoHelper.twoDToIso(pos); |
176 |
hero.x = borderOffsetX + pos.x; |
177 |
hero.y = borderOffsetY + pos.y; |
178 |
heroCartPos.x = heroTile.x * tileWidth +tileWidth/2; |
179 |
heroCartPos.y = heroTile.y * tileWidth +tileWidth/2; |
180 |
heroPointer.x=heroCartPos.x; |
181 |
heroPointer.y=heroCartPos.y; |
182 |
depthSort(); |
183 |
|
184 |
//new point, turn, find dX,dY
|
185 |
stepsTaken=0; |
186 |
destination=path.pop(); |
187 |
if(heroTile.x<destination.x){ |
188 |
dX = 1; |
189 |
}else if(heroTile.x>destination.x){ |
190 |
dX = -1; |
191 |
}else { |
192 |
dX=0; |
193 |
}
|
194 |
if(heroTile.y<destination.y){ |
195 |
dY = 1; |
196 |
}else if(heroTile.y>destination.y){ |
197 |
dY = -1; |
198 |
}else { |
199 |
dY=0; |
200 |
}
|
201 |
if(heroTile.x==destination.x){//top or bottom |
202 |
dX=0; |
203 |
}else if(heroTile.y==destination.y){//left or right |
204 |
dY=0; |
205 |
}
|
206 |
|
207 |
if (dX == 1) |
208 |
{
|
209 |
|
210 |
if (dY == 0) |
211 |
{
|
212 |
facing = "east"; |
213 |
}
|
214 |
else if (dY==1) |
215 |
{
|
216 |
facing = "southeast"; |
217 |
dX = dY=0.5; |
218 |
}
|
219 |
else
|
220 |
{
|
221 |
facing = "northeast"; |
222 |
dX=0.5; |
223 |
dY=-0.5; |
224 |
}
|
225 |
}
|
226 |
else if (dX==-1) |
227 |
{
|
228 |
|
229 |
if (dY == 0) |
230 |
{
|
231 |
facing = "west"; |
232 |
}
|
233 |
else if (dY==1) |
234 |
{
|
235 |
facing = "southwest"; |
236 |
dY=0.5; |
237 |
dX=-0.5; |
238 |
}
|
239 |
else
|
240 |
{
|
241 |
facing = "northwest"; |
242 |
dX = dY=-0.5; |
243 |
}
|
244 |
}
|
245 |
else
|
246 |
{
|
247 |
|
248 |
if (dY == 0) |
249 |
{
|
250 |
facing=currentFacing; |
251 |
}
|
252 |
else if (dY==1) |
253 |
{
|
254 |
facing = "south"; |
255 |
}
|
256 |
else
|
257 |
{
|
258 |
facing = "north"; |
259 |
}
|
260 |
}
|
261 |
}
|
262 |
|
263 |
|
264 |
|
265 |
}
|
266 |
//sort depth & draw to canvas
|
267 |
function depthSort() |
268 |
{
|
269 |
bg.bitmapData.lock(); |
270 |
bg.bitmapData.fillRect(rect,0xffffff); |
271 |
var tileType:uint; |
272 |
var mat:Matrix=new Matrix(); |
273 |
var pos:Point=new Point(); |
274 |
for (var i:uint=0; i<levelData.length; i++) |
275 |
{
|
276 |
for (var j:uint=0; j<levelData[0].length; j++) |
277 |
{
|
278 |
tileType = levelData[i][j]; |
279 |
//placeTile(tileType,i,j);
|
280 |
|
281 |
pos.x = j * tileWidth; |
282 |
pos.y = i * tileWidth; |
283 |
pos = IsoHelper.twoDToIso(pos); |
284 |
mat.tx = borderOffsetX + pos.x; |
285 |
mat.ty = borderOffsetY + pos.y; |
286 |
if(tileType==0){ |
287 |
bg.bitmapData.draw(grassTile,mat); |
288 |
}else{ |
289 |
bg.bitmapData.draw(wallTile,mat); |
290 |
}
|
291 |
if(heroTile.x==j&&heroTile.y==i){ |
292 |
mat.tx=hero.x; |
293 |
mat.ty=hero.y; |
294 |
bg.bitmapData.draw(hero,mat); |
295 |
}
|
296 |
|
297 |
}
|
298 |
}
|
299 |
bg.bitmapData.unlock(); |
300 |
//add character rectangle
|
301 |
}
|
302 |
function handleMouseClick(e:MouseEvent):void{ |
303 |
path.splice(0,path.length); |
304 |
var clickPt:Point=new Point(); |
305 |
clickPt.x=e.stageX-borderOffsetX; |
306 |
clickPt.y=e.stageY-borderOffsetY; |
307 |
clickPt=IsoHelper.isoTo2D(clickPt); |
308 |
clickPt.x-=tileWidth/2; |
309 |
clickPt.y+=tileWidth/2; |
310 |
clickPt=IsoHelper.getTileCoordinates(clickPt,tileWidth); |
311 |
if(clickPt.x<0||clickPt.y<0||clickPt.x>levelData.length-1||clickPt.x>levelData[0].length-1){ |
312 |
//trace("invalid");
|
313 |
//we have clicked outside
|
314 |
return; |
315 |
}
|
316 |
if(levelData[clickPt.y][clickPt.x]==1){ |
317 |
//trace("wall");
|
318 |
//we clicked on a wall
|
319 |
return; |
320 |
}
|
321 |
trace("find ",heroTile, clickPt); |
322 |
destination=heroTile; |
323 |
path= PathFinder.go(heroTile.x, heroTile.y, clickPt.x, clickPt.y, levelData); |
324 |
path.reverse(); |
325 |
path.push(clickPt); |
326 |
path.reverse(); |
327 |
paintPath(); |
328 |
}
|
329 |
function paintPath():void{//show hihglighted path in minimap |
330 |
var sp:Sprite; |
331 |
for (var i:uint=0; i<levelData.length; i++) |
332 |
{
|
333 |
for (var j:uint=0; j<levelData[0].length; j++) |
334 |
{
|
335 |
sp=overlayContainer.getChildByName("tile_"+j.toString()+"_"+i.toString()) as Sprite; |
336 |
sp.filters=[]; |
337 |
}
|
338 |
}
|
339 |
for(i=0;i<path.length;i++){ |
340 |
sp=overlayContainer.getChildByName("tile_"+(path[i].x).toString()+"_"+(path[i].y).toString()) as Sprite; |
341 |
sp.filters=[glowFilter]; |
342 |
overlayContainer.setChildIndex(sp,overlayContainer.numChildren-1); |
343 |
}
|
344 |
overlayContainer.setChildIndex(heroPointer,overlayContainer.numChildren-1); |
345 |
}
|
346 |
stage.addEventListener(MouseEvent.CLICK, handleMouseClick); |
347 |
createLevel(); |
Puede haber notado que eliminé la lógica de verificación de colisión; ya no es necesario ya que no podemos mover nuestro personaje manualmente usando el teclado. Sin embargo, debemos filtrar puntos de clics válidos para determinar si hemos hecho clic dentro del área transitable, en lugar de un mosaico de pared u otro mosaico que no se pueda recorrer.
Otro punto interesante para codificar la IA: no queremos que el personaje mire hacia la próxima casilla en la matriz de nodos tan pronto como llegue al actual, ya que un giro inmediato da como resultado que nuestro personaje camine sobre los bordes de azulejos. En su lugar, debemos esperar hasta que el personaje esté unos pocos pasos dentro del mosaico antes de buscar el próximo destino. También es mejor colocar manualmente al héroe en el centro de la casilla actual justo antes de que giremos, para que todo se sienta perfecto.
Además, si exploras la demostración anterior, puedes notar que nuestra lógica de sorteo se interrumpe cuando el héroe se mueve diagonalmente cerca de una ficha de pared. Este es un caso extremo en el que, para un marco, nuestro héroe parece estar dentro del mosaico de la pared. Esto sucede porque hemos desactivado el control de colisión. Una solución consiste en utilizar un algoritmo de identificación de ruta que ignora las soluciones diagonales. (Casi todos los algoritmos de búsqueda de rutas tienen opciones para habilitar o deshabilitar las soluciones de recorridos diagonales).
4. Proyectiles
Un proyectil es algo que se mueve en una dirección particular con una velocidad particular, como una bala, un hechizo mágico, una pelota, etc.
Todo sobre el proyectil es el mismo que el personaje héroe, aparte de la altura: en lugar de rodar por el suelo, los proyectiles a menudo flotan por encima a una cierta altura. Una bala viajará por encima del nivel de la cintura del personaje, e incluso una pelota puede necesitar rebotar.
Una cosa interesante a tener en cuenta es que la altura isométrica es la misma que la altura en una vista lateral 2D. No hay complicadas conversiones involucradas. Si una bola está a 10 píxeles del suelo en coordenadas cartesianas, está a 10 píxeles del suelo en coordenadas isométricas. (En nuestro caso, el eje relevante es el eje y).
Tratemos de implementar una pelota rebotando en nuestra pradera amurallada. Ignoraremos los efectos de amortiguación (y así haremos que los rebotes continúen interminablemente), y para darle un toque de realismo agregaremos una sombra a la pelota. Movemos la sombra tal como movemos el personaje héroe (es decir, sin usar un valor de altura), pero para la pelota debemos agregar el valor de altura al valor Y isométrico. El valor de altura cambiará de cuadro a cuadro dependiendo de la gravedad, y una vez que la pelota toque el suelo, voltearemos la velocidad actual a lo largo del eje y.
Antes de abordar el rebote en un
sistema isométrico, veremos cómo podemos implementarlo en un sistema
cartesiano 2D. Vamos a representar la altura de la pelota con un zValue
variable. Imagine que, para empezar, la bola tiene diez píxeles de
altura, por
lo que zValue = 10
. Usaremos dos variables más: incrementValue
, que
comienza en 0
, y gravity
, que tiene un valor de 1
.
Cada cuadro,
agregamos incrementValue
to zValue
, y restamos gravity
de
incrementValue
. Cuando zValue
llega a 0
, significa que la pelota ha
alcanzado el suelo; en este punto, volteamos el signo de incrementValue
multiplicándolo por -1
, convirtiéndolo en un número positivo. Esto
significa que la bola se moverá hacia arriba desde el siguiente cuadro,
rebotando.
Así es como se ve en el código:
1 |
zValue=10; |
2 |
gravity=1; |
3 |
incrementValue=0; |
4 |
|
5 |
gameLoop(){ |
6 |
incrementValue-=gravity; |
7 |
zValue-=incrementValue; |
8 |
if(zValue<=0){ |
9 |
zValue=0; |
10 |
incrementValue*=-1; |
11 |
}
|
12 |
}
|
De hecho, vamos a utilizar una versión ligeramente modificada de eso:
1 |
zValue=10; |
2 |
gravity=1; |
3 |
incrementValue=0; |
4 |
incrementReset=-12 |
5 |
|
6 |
gameLoop(){ |
7 |
incrementValue-=gravity; |
8 |
zValue-=incrementValue; |
9 |
if(zValue<=0){ |
10 |
zValue=0; |
11 |
incrementValue=incrementReset; |
12 |
}
|
13 |
}
|
Esto elimina el efecto de amortiguación y permite que la pelota rebote para siempre.
Aplicando esto a nuestra pelota, obtenemos la demostración a continuación:
Aquí está el código AS3 completo:
1 |
import flash.display.Sprite; |
2 |
import com.csharks.juwalbose.IsoHelper; |
3 |
import flash.display.MovieClip; |
4 |
import flash.geom.Point; |
5 |
import flash.events.Event; |
6 |
import flash.display.Bitmap; |
7 |
import flash.display.BitmapData; |
8 |
import flash.geom.Matrix; |
9 |
import flash.geom.Rectangle; |
10 |
import com.senocular.utils.KeyObject; |
11 |
|
12 |
var levelData=[[1,1,1,1,1,1], |
13 |
[1,0,0,0,0,1], |
14 |
[1,0,0,0,0,1], |
15 |
[1,0,0,2,0,1], |
16 |
[1,0,0,0,0,1], |
17 |
[1,1,1,1,1,1]]; |
18 |
|
19 |
var tileWidth:uint = 50; |
20 |
var borderOffsetY:uint = 70; |
21 |
var borderOffsetX:uint = 275; |
22 |
|
23 |
var key:KeyObject = new KeyObject(stage); |
24 |
|
25 |
var facing:String = "south"; |
26 |
var currentFacing:String = "south"; |
27 |
var heroHalfSize:uint = 20; |
28 |
|
29 |
//the tiles
|
30 |
var grassTile:MovieClip=new TileMc(); |
31 |
grassTile.gotoAndStop(1); |
32 |
var wallTile:MovieClip=new TileMc(); |
33 |
wallTile.gotoAndStop(2); |
34 |
var pickupTile:MovieClip=new TileMc(); |
35 |
pickupTile.gotoAndStop(3); |
36 |
var ball:Sprite=new Ball(); |
37 |
var shadow_S:Sprite=new Shadow(); |
38 |
|
39 |
//the canvas
|
40 |
var bg:Bitmap = new Bitmap(new BitmapData(650,450)); |
41 |
addChild(bg); |
42 |
var rect:Rectangle = bg.bitmapData.rect; |
43 |
|
44 |
//to handle depth
|
45 |
var heroPointer:Sprite; |
46 |
var overlayContainer:Sprite=new Sprite(); |
47 |
addChild(overlayContainer); |
48 |
|
49 |
//to handle direction movement
|
50 |
var dX:Number = 0; |
51 |
var dY:Number = 0; |
52 |
var speed:uint = 5; |
53 |
var ballCartPos:Point=new Point(); |
54 |
var ballTile:Point=new Point(); |
55 |
|
56 |
var zValue:int = 50; |
57 |
var gravity:int = -1; |
58 |
var incrementValue:Number = 0; |
59 |
|
60 |
//add items to start level, add game loop
|
61 |
function createLevel() |
62 |
{
|
63 |
var tileType:uint; |
64 |
for (var i:uint=0; i<levelData.length; i++) |
65 |
{
|
66 |
for (var j:uint=0; j<levelData[0].length; j++) |
67 |
{
|
68 |
tileType = levelData[i][j]; |
69 |
placeTile(tileType,i,j); |
70 |
if (tileType == 2) |
71 |
{
|
72 |
levelData[i][j] = 0; |
73 |
}
|
74 |
}
|
75 |
}
|
76 |
overlayContainer.addChild(heroPointer); |
77 |
overlayContainer.alpha=0.5; |
78 |
overlayContainer.scaleX=overlayContainer.scaleY=0.5; |
79 |
overlayContainer.y=290; |
80 |
overlayContainer.x=10; |
81 |
|
82 |
depthSort(); |
83 |
addEventListener(Event.ENTER_FRAME,loop); |
84 |
}
|
85 |
|
86 |
//place the tile based on coordinates
|
87 |
function placeTile(id:uint,i:uint,j:uint) |
88 |
{
|
89 |
var pos:Point=new Point(); |
90 |
if (id == 2) |
91 |
{
|
92 |
|
93 |
id = 0; |
94 |
pos.x = j * tileWidth; |
95 |
pos.y = i * tileWidth; |
96 |
pos = IsoHelper.twoDToIso(pos); |
97 |
|
98 |
ball.x = borderOffsetX + pos.x; |
99 |
ball.y = borderOffsetY + pos.y; |
100 |
|
101 |
ballCartPos.x = j * tileWidth; |
102 |
ballCartPos.y = i * tileWidth; |
103 |
ballTile.x = j; |
104 |
ballTile.y = i; |
105 |
|
106 |
heroPointer=new herodot(); |
107 |
heroPointer.x=ballCartPos.x; |
108 |
heroPointer.y=ballCartPos.y; |
109 |
|
110 |
}
|
111 |
var tile:MovieClip=new cartTile(); |
112 |
tile.gotoAndStop(id+1); |
113 |
tile.x = j * tileWidth; |
114 |
tile.y = i * tileWidth; |
115 |
overlayContainer.addChild(tile); |
116 |
|
117 |
}
|
118 |
|
119 |
//the game loop
|
120 |
function loop(e:Event) |
121 |
{
|
122 |
incrementValue -= gravity; |
123 |
zValue -= incrementValue; |
124 |
if (zValue <= 0) |
125 |
{
|
126 |
zValue = 0; |
127 |
incrementValue = -12; |
128 |
}
|
129 |
|
130 |
|
131 |
if (key.isDown(Keyboard.UP)) |
132 |
{
|
133 |
dY = -1; |
134 |
}
|
135 |
else if (key.isDown(Keyboard.DOWN)) |
136 |
{
|
137 |
dY = 1; |
138 |
}
|
139 |
else
|
140 |
{
|
141 |
dY = 0; |
142 |
}
|
143 |
if (key.isDown(Keyboard.RIGHT)) |
144 |
{
|
145 |
dX = 1; |
146 |
if (dY == 0) |
147 |
{
|
148 |
facing = "east"; |
149 |
}
|
150 |
else if (dY==1) |
151 |
{
|
152 |
facing = "southeast"; |
153 |
dX = dY = 0.5; |
154 |
}
|
155 |
else
|
156 |
{
|
157 |
facing = "northeast"; |
158 |
dX = 0.5; |
159 |
dY = -0.5; |
160 |
}
|
161 |
}
|
162 |
else if (key.isDown(Keyboard.LEFT)) |
163 |
{
|
164 |
dX = -1; |
165 |
if (dY == 0) |
166 |
{
|
167 |
facing = "west"; |
168 |
}
|
169 |
else if (dY==1) |
170 |
{
|
171 |
facing = "southwest"; |
172 |
dY = 0.5; |
173 |
dX = -0.5; |
174 |
}
|
175 |
else
|
176 |
{
|
177 |
facing = "northwest"; |
178 |
dX = dY = -0.5; |
179 |
}
|
180 |
}
|
181 |
else
|
182 |
{
|
183 |
dX = 0; |
184 |
if (dY == 0) |
185 |
{
|
186 |
//facing="west";
|
187 |
}
|
188 |
else if (dY==1) |
189 |
{
|
190 |
facing = "south"; |
191 |
}
|
192 |
else
|
193 |
{
|
194 |
facing = "north"; |
195 |
}
|
196 |
}
|
197 |
|
198 |
|
199 |
if (isWalkable()) |
200 |
{
|
201 |
ballCartPos.x += speed * dX; |
202 |
ballCartPos.y += speed * dY; |
203 |
|
204 |
heroPointer.x=ballCartPos.x; |
205 |
heroPointer.y=ballCartPos.y; |
206 |
|
207 |
|
208 |
var newPos:Point = IsoHelper.twoDToIso(ballCartPos); |
209 |
//collision check
|
210 |
ball.x = borderOffsetX + newPos.x; |
211 |
ball.y = borderOffsetY + newPos.y; |
212 |
ballTile = IsoHelper.getTileCoordinates(ballCartPos,tileWidth); |
213 |
|
214 |
}
|
215 |
depthSort(); |
216 |
|
217 |
}
|
218 |
|
219 |
//check for collision tile
|
220 |
function isWalkable():Boolean |
221 |
{
|
222 |
var able:Boolean = true; |
223 |
var newPos:Point =new Point(); |
224 |
newPos.x=ballCartPos.x + (speed * dX); |
225 |
newPos.y=ballCartPos.y + (speed * dY); |
226 |
switch (facing) |
227 |
{
|
228 |
case "north" : |
229 |
newPos.y -= heroHalfSize; |
230 |
break; |
231 |
case "south" : |
232 |
newPos.y += heroHalfSize; |
233 |
break; |
234 |
case "east" : |
235 |
newPos.x += heroHalfSize; |
236 |
break; |
237 |
case "west" : |
238 |
newPos.x -= heroHalfSize; |
239 |
break; |
240 |
case "northeast" : |
241 |
newPos.y -= heroHalfSize; |
242 |
newPos.x += heroHalfSize; |
243 |
break; |
244 |
case "southeast" : |
245 |
newPos.y += heroHalfSize; |
246 |
newPos.x += heroHalfSize; |
247 |
break; |
248 |
case "northwest" : |
249 |
newPos.y -= heroHalfSize; |
250 |
newPos.x -= heroHalfSize; |
251 |
break; |
252 |
case "southwest" : |
253 |
newPos.y += heroHalfSize; |
254 |
newPos.x -= heroHalfSize; |
255 |
break; |
256 |
}
|
257 |
newPos = IsoHelper.getTileCoordinates(newPos,tileWidth); |
258 |
if (levelData[newPos.y][newPos.x] == 1) |
259 |
{
|
260 |
able = false; |
261 |
}
|
262 |
else
|
263 |
{
|
264 |
//trace("new",newPos);
|
265 |
}
|
266 |
return able; |
267 |
}
|
268 |
|
269 |
//sort depth & draw to canvas
|
270 |
function depthSort() |
271 |
{
|
272 |
bg.bitmapData.lock(); |
273 |
bg.bitmapData.fillRect(rect,0xffffff); |
274 |
var tileType:uint; |
275 |
var mat:Matrix=new Matrix(); |
276 |
var pos:Point=new Point(); |
277 |
for (var i:uint=0; i<levelData.length; i++) |
278 |
{
|
279 |
for (var j:uint=0; j<levelData[0].length; j++) |
280 |
{
|
281 |
tileType = levelData[i][j]; |
282 |
//placeTile(tileType,i,j);
|
283 |
|
284 |
pos.x = j * tileWidth; |
285 |
pos.y = i * tileWidth; |
286 |
pos = IsoHelper.twoDToIso(pos); |
287 |
mat.tx = borderOffsetX + pos.x; |
288 |
mat.ty = borderOffsetY + pos.y; |
289 |
if (tileType == 0) |
290 |
{
|
291 |
bg.bitmapData.draw(grassTile,mat); |
292 |
}
|
293 |
else
|
294 |
{
|
295 |
bg.bitmapData.draw(wallTile,mat); |
296 |
}
|
297 |
|
298 |
if (ballTile.x == j && ballTile.y == i) |
299 |
{
|
300 |
mat.tx = ball.x;//+ tileWidth; |
301 |
mat.ty = ball.y;// +tileWidth/2; |
302 |
bg.bitmapData.draw(shadow_S,mat); |
303 |
mat.ty = mat.ty - zValue - heroHalfSize; |
304 |
bg.bitmapData.draw(ball,mat); |
305 |
}
|
306 |
|
307 |
}
|
308 |
}
|
309 |
bg.bitmapData.unlock(); |
310 |
|
311 |
}
|
312 |
createLevel(); |
Comprenda que el papel desempeñado por la sombra es muy importante y contribuye al realismo de esta ilusión. En el ejemplo anterior, he agregado la mitad de la altura de la bola a la posición y de la bola, de modo que rebota en la posición correcta con respecto a la sombra.
Además, tenga en cuenta que ahora estamos utilizando las dos coordenadas de la pantalla (xey) para representar tres dimensiones en coordenadas isométricas: el eje y en las coordenadas de la pantalla es también el eje z en las coordenadas isométricas. ¡Esto puede ser confuso!
5. Desplazamiento isométrico
Cuando el área del nivel es mucho más grande que el área de la pantalla visible, tendremos que hacer que se desplace.
El área de pantalla visible se puede considerar como un rectángulo más pequeño dentro del rectángulo más grande del área de nivel completa. Desplazarse es, esencialmente, simplemente mover el rectángulo interno dentro del más grande:



Por lo general, cuando ocurre ese desplazamiento, la posición del jugador sigue siendo la misma con respecto al rectángulo de la pantalla, comúnmente en el centro de la pantalla. Todo lo que necesitamos, para implementar el desplazamiento, es seguir el punto de esquina del rectángulo interno:



Este punto de esquina, que está en coordenadas cartesianas (en la imagen solo podemos mostrar los valores isométricos), estará dentro de un mosaico en los datos de nivel. Para desplazarse, incrementamos la posición x e y del punto de esquina en coordenadas cartesianas. Ahora podemos convertir este punto en coordenadas isométricas y usarlo para dibujar la pantalla.
Los
valores recién convertidos, en el espacio isométrico, también deben ser
la esquina de nuestra pantalla, lo que significa que son los nuevos (0,
0)
. Por lo tanto, al analizar y dibujar los datos de
nivel, restamos este valor de la posición isométrica de cada mosaico y
solo lo dibujamos si la nueva posición del mosaico cae dentro de la
pantalla. Podemos expresar esto en pasos así:
- Actualice las coordenadas x e y del punto de esquina cartesiano.
- Convierta esto a espacio isométrico.
- Reste este valor de la posición de extracción isométrica de cada mosaico.
- Dibuje el mosaico solo si la nueva posición de extracción isométrica cae dentro de la pantalla.
Mira este ejemplo (usa las flechas para desplazarte):
Aquí está el código fuente completo de AS3:
1 |
import flash.display.Sprite; |
2 |
import com.csharks.juwalbose.IsoHelper; |
3 |
import flash.display.MovieClip; |
4 |
import flash.geom.Point; |
5 |
import flash.filters.GlowFilter; |
6 |
import flash.events.Event; |
7 |
import com.senocular.utils.KeyObject; |
8 |
import flash.ui.Keyboard; |
9 |
import flash.display.Bitmap; |
10 |
import flash.display.BitmapData; |
11 |
import flash.geom.Matrix; |
12 |
import flash.geom.Rectangle; |
13 |
|
14 |
var levelData=[[1,1,1,1,1,1,1,1,1,1,1,1], |
15 |
[1,0,0,0,0,0,0,0,0,1,0,1], |
16 |
[1,0,0,0,0,0,0,0,0,1,0,1], |
17 |
[1,0,0,1,0,0,0,0,0,0,0,1], |
18 |
[1,0,0,1,2,0,0,0,0,0,0,1], |
19 |
[1,0,0,1,0,0,0,0,0,0,0,1], |
20 |
[1,0,0,0,0,0,0,0,0,1,0,1], |
21 |
[1,0,0,0,0,0,0,0,0,0,0,1], |
22 |
[1,0,0,0,1,1,1,0,0,0,0,1], |
23 |
[1,0,0,0,0,0,0,0,0,0,0,1], |
24 |
[1,0,0,0,0,0,0,0,1,1,0,1], |
25 |
[1,1,0,0,0,0,0,0,0,0,0,1], |
26 |
[1,1,1,1,1,1,1,1,1,1,1,1]]; |
27 |
|
28 |
var tileWidth:uint = 50; |
29 |
var borderOffsetY:uint = 70; |
30 |
var borderOffsetX:uint = 275; |
31 |
|
32 |
var facing:String = "south"; |
33 |
var currentFacing:String = "south"; |
34 |
var hero:MovieClip=new herotile(); |
35 |
hero.clip.gotoAndStop(facing); |
36 |
var heroPointer:Sprite; |
37 |
var key:KeyObject = new KeyObject(stage);//Senocular KeyObject Class |
38 |
var heroHalfSize:uint=20; |
39 |
|
40 |
//the tiles
|
41 |
var grassTile:MovieClip=new TileMc(); |
42 |
grassTile.gotoAndStop(1); |
43 |
var wallTile:MovieClip=new TileMc(); |
44 |
wallTile.gotoAndStop(2); |
45 |
|
46 |
//the canvas
|
47 |
var bg:Bitmap = new Bitmap(new BitmapData(650,450)); |
48 |
addChild(bg); |
49 |
var rect:Rectangle=bg.bitmapData.rect; |
50 |
|
51 |
//to handle depth
|
52 |
var overlayContainer:Sprite=new Sprite(); |
53 |
addChild(overlayContainer); |
54 |
|
55 |
//to handle direction movement
|
56 |
var dX:Number = 0; |
57 |
var dY:Number = 0; |
58 |
var idle:Boolean = true; |
59 |
var speed:uint = 6; |
60 |
var heroCartPos:Point=new Point(); |
61 |
var heroTile:Point=new Point(); |
62 |
|
63 |
var cornerPoint:Point=new Point(); |
64 |
|
65 |
//add items to start level, add game loop
|
66 |
function createLevel() |
67 |
{
|
68 |
var tileType:uint; |
69 |
for (var i:uint=0; i<levelData.length; i++) |
70 |
{
|
71 |
for (var j:uint=0; j<levelData[0].length; j++) |
72 |
{
|
73 |
tileType = levelData[i][j]; |
74 |
placeTile(tileType,i,j); |
75 |
if (tileType == 2) |
76 |
{
|
77 |
levelData[i][j] = 0; |
78 |
}
|
79 |
}
|
80 |
}
|
81 |
overlayContainer.addChild(heroPointer); |
82 |
overlayContainer.alpha=0.5; |
83 |
overlayContainer.scaleX=overlayContainer.scaleY=0.2; |
84 |
overlayContainer.y=310; |
85 |
overlayContainer.x=10; |
86 |
depthSort(); |
87 |
addEventListener(Event.ENTER_FRAME,loop); |
88 |
}
|
89 |
|
90 |
//place the tile based on coordinates
|
91 |
function placeTile(id:uint,i:uint,j:uint) |
92 |
{
|
93 |
var pos:Point=new Point(); |
94 |
if (id == 2) |
95 |
{
|
96 |
|
97 |
id = 0; |
98 |
pos.x = j * tileWidth; |
99 |
pos.y = i * tileWidth; |
100 |
pos = IsoHelper.twoDToIso(pos); |
101 |
hero.x = borderOffsetX + pos.x; |
102 |
hero.y = borderOffsetY + pos.y; |
103 |
//overlayContainer.addChild(hero);
|
104 |
heroCartPos.x = j * tileWidth; |
105 |
heroCartPos.y = i * tileWidth; |
106 |
heroTile.x=j; |
107 |
heroTile.y=i; |
108 |
heroPointer=new herodot(); |
109 |
heroPointer.x=heroCartPos.x; |
110 |
heroPointer.y=heroCartPos.y; |
111 |
|
112 |
}
|
113 |
var tile:MovieClip=new cartTile(); |
114 |
tile.gotoAndStop(id+1); |
115 |
tile.x = j * tileWidth; |
116 |
tile.y = i * tileWidth; |
117 |
overlayContainer.addChild(tile); |
118 |
}
|
119 |
|
120 |
//the game loop
|
121 |
function loop(e:Event) |
122 |
{
|
123 |
if (key.isDown(Keyboard.UP)) |
124 |
{
|
125 |
dY = -1; |
126 |
}
|
127 |
else if (key.isDown(Keyboard.DOWN)) |
128 |
{
|
129 |
dY = 1; |
130 |
}
|
131 |
else
|
132 |
{
|
133 |
dY = 0; |
134 |
}
|
135 |
if (key.isDown(Keyboard.RIGHT)) |
136 |
{
|
137 |
dX = 1; |
138 |
if (dY == 0) |
139 |
{
|
140 |
facing = "east"; |
141 |
}
|
142 |
else if (dY==1) |
143 |
{
|
144 |
facing = "southeast"; |
145 |
dX = dY=0.5; |
146 |
}
|
147 |
else
|
148 |
{
|
149 |
facing = "northeast"; |
150 |
dX=0.5; |
151 |
dY=-0.5; |
152 |
}
|
153 |
}
|
154 |
else if (key.isDown(Keyboard.LEFT)) |
155 |
{
|
156 |
dX = -1; |
157 |
if (dY == 0) |
158 |
{
|
159 |
facing = "west"; |
160 |
}
|
161 |
else if (dY==1) |
162 |
{
|
163 |
facing = "southwest"; |
164 |
dY=0.5; |
165 |
dX=-0.5; |
166 |
}
|
167 |
else
|
168 |
{
|
169 |
facing = "northwest"; |
170 |
dX = dY=-0.5; |
171 |
}
|
172 |
}
|
173 |
else
|
174 |
{
|
175 |
dX = 0; |
176 |
if (dY == 0) |
177 |
{
|
178 |
//facing="west";
|
179 |
}
|
180 |
else if (dY==1) |
181 |
{
|
182 |
facing = "south"; |
183 |
}
|
184 |
else
|
185 |
{
|
186 |
facing = "north"; |
187 |
}
|
188 |
}
|
189 |
if (dY == 0 && dX == 0) |
190 |
{
|
191 |
hero.clip.gotoAndStop(facing); |
192 |
idle = true; |
193 |
}
|
194 |
else if (idle||currentFacing!=facing) |
195 |
{
|
196 |
idle = false; |
197 |
currentFacing = facing; |
198 |
hero.clip.gotoAndPlay(facing); |
199 |
}
|
200 |
if (! idle && isWalkable()) |
201 |
{
|
202 |
heroCartPos.x += speed * dX; |
203 |
heroCartPos.y += speed * dY; |
204 |
|
205 |
cornerPoint.x -= speed * dX; |
206 |
cornerPoint.y -= speed * dY; |
207 |
|
208 |
heroPointer.x=heroCartPos.x; |
209 |
heroPointer.y=heroCartPos.y; |
210 |
|
211 |
var newPos:Point = IsoHelper.twoDToIso(heroCartPos); |
212 |
heroTile=IsoHelper.getTileCoordinates(heroCartPos,tileWidth); |
213 |
depthSort(); |
214 |
//trace(heroTile);
|
215 |
}
|
216 |
tileTxt.text="Hero is on x: "+heroTile.x +" & y: "+heroTile.y; |
217 |
}
|
218 |
|
219 |
//check for collision tile
|
220 |
function isWalkable():Boolean{ |
221 |
var able:Boolean=true; |
222 |
var newPos:Point =new Point(); |
223 |
newPos.x=heroCartPos.x + (speed * dX); |
224 |
newPos.y=heroCartPos.y + (speed * dY); |
225 |
switch (facing){ |
226 |
case "north": |
227 |
newPos.y-=heroHalfSize; |
228 |
break; |
229 |
case "south": |
230 |
newPos.y+=heroHalfSize; |
231 |
break; |
232 |
case "east": |
233 |
newPos.x+=heroHalfSize; |
234 |
break; |
235 |
case "west": |
236 |
newPos.x-=heroHalfSize; |
237 |
break; |
238 |
case "northeast": |
239 |
newPos.y-=heroHalfSize; |
240 |
newPos.x+=heroHalfSize; |
241 |
break; |
242 |
case "southeast": |
243 |
newPos.y+=heroHalfSize; |
244 |
newPos.x+=heroHalfSize; |
245 |
break; |
246 |
case "northwest": |
247 |
newPos.y-=heroHalfSize; |
248 |
newPos.x-=heroHalfSize; |
249 |
break; |
250 |
case "southwest": |
251 |
newPos.y+=heroHalfSize; |
252 |
newPos.x-=heroHalfSize; |
253 |
break; |
254 |
}
|
255 |
newPos=IsoHelper.getTileCoordinates(newPos,tileWidth); |
256 |
if(levelData[newPos.y][newPos.x]==1){ |
257 |
able=false; |
258 |
}else{ |
259 |
//trace("new",newPos);
|
260 |
}
|
261 |
return able; |
262 |
}
|
263 |
|
264 |
//sort depth & draw to canvas
|
265 |
function depthSort() |
266 |
{
|
267 |
bg.bitmapData.lock(); |
268 |
bg.bitmapData.fillRect(rect,0xffffff); |
269 |
var tileType:uint; |
270 |
var mat:Matrix=new Matrix(); |
271 |
var pos:Point=new Point(); |
272 |
for (var i:uint=0; i<levelData.length; i++) |
273 |
{
|
274 |
for (var j:uint=0; j<levelData[0].length; j++) |
275 |
{
|
276 |
tileType = levelData[i][j]; |
277 |
|
278 |
//pos.x = j * tileWidth;
|
279 |
//pos.y = i * tileWidth;
|
280 |
|
281 |
pos.x = j * tileWidth+cornerPoint.x; |
282 |
pos.y = i * tileWidth+cornerPoint.y; |
283 |
|
284 |
pos = IsoHelper.twoDToIso(pos); |
285 |
mat.tx = borderOffsetX + pos.x; |
286 |
mat.ty = borderOffsetY + pos.y; |
287 |
if(tileType==0){ |
288 |
bg.bitmapData.draw(grassTile,mat); |
289 |
}else{ |
290 |
bg.bitmapData.draw(wallTile,mat); |
291 |
}
|
292 |
if(heroTile.x==j&&heroTile.y==i){ |
293 |
mat.tx=hero.x; |
294 |
mat.ty=hero.y; |
295 |
bg.bitmapData.draw(hero,mat); |
296 |
}
|
297 |
|
298 |
}
|
299 |
}
|
300 |
bg.bitmapData.unlock(); |
301 |
//add character rectangle
|
302 |
}
|
303 |
createLevel(); |
Tenga en cuenta que el punto de esquina se incrementa en la dirección opuesta a la actualización de la posición del héroe a medida que se mueve. Esto asegura que el héroe se quede donde está con respecto a la pantalla:
1 |
heroCartPos.x += speed * dX; |
2 |
heroCartPos.y += speed * dY; |
3 |
cornerPoint.x -= speed * dX; |
4 |
cornerPoint.y -= speed * dY; |
La lógica de dibujo solo cambia en dos líneas, donde determinamos las coordenadas cartesianas de cada mosaico. Acabamos de pasar el punto de esquina al punto original que realmente combina los puntos 1, 2 y 3 anteriores:
1 |
pos.x = j * tileWidth+cornerPoint.x; |
2 |
pos.y = i * tileWidth+ |