Personaje en entorno isométrico utilizando as3isolib

danii . miércoles 6 de junio de 2012. a las 15:46

Sra. Bombilla Entorno isométrico as3isolib as3

No hace mucho publicamos en este blog un artículo sobre creación de entornos isométricos en flash ActionScript 3. Puesto que utilizamos ese post como introducción a la librería as3isolib, nos limitábamos a crear una rejilla para que sirviera de «escenario isométrico» y a añadir objetos en este escenario, en nuestro caso, sillas de heladería a las que dotamos además de cierta interactividad, pudiéndolas rotar. Os recomendamos echarle un ojo a dicho post ya que es una introducción al uso de as3isolib y muchas de las técnicas que vamos a utilizar se explican ahí más detalladamente.

as3isolib

Para este ejemplo, vamos a dar una vuelta de tuerca más y vamos a programar un personaje de videojuego que el usuario (o jugador) va a poder mover libremente por un entorno isométrico con obstáculos, y todo esto en menos de 500 líneas de código!

Las principales dificultades a las que nos enfrentamos en este ejemplo son :

  • creación del personaje 3D isométrico y que responda adecuadamente a los eventos de teclado
  • detección de colisiones entre el personaje y los obstáculos

Ejemplo en funcionamiento: Utiliza las flechas del teclado para mover a la Sra. Bombilla por la rejilla isométrica

Get Adobe Flash player

Creación del personaje isométrico

Puesto que nuestro personaje debe moverse en un entorno en perspectiva isométrica vamos a necesitar crear varias vistas y programar un sistema para que el personaje cambie a la vista adecuada en función de en qué dirección se esté moviendo, a menos que nuestro personaje se vaya a mover solamente en una única dirección y sentido (booooring!).

En este ejemplo hemos utilizado a la Sra. Bombilla de OCB Blackthinking (cariñosamente apodada la niña en fase de desarrollo) como personaje en perspectiva isométrica, ya que para este proyecto fue necesario crear muchas (muchísimas!) animaciones en varias perspectivas y tamaños. Para cubrir todas las posibilidades, necesitamos 8 vistas diferentes, 4 para las direcciones básicas (arriba, abajo, derecha, izquierda) y otras 4 para las direcciones «diagonales» (arriba+derecha, arriba+izquierda, abajo+derecha, abajo+izquierda).


Las distintas perspectivas de la niña, en cada una de las 8 posibles direcciones, tanto parada como andando

Cada una de estas vistas es una animación compuesta por varios fotogramas que se repite en bucle y que muestran a la niña andando o parada para cada una de las ocho posibles direcciones antes mencionadas. Os hemos proporcionado con las animaciones de la niña junto con el código fuente. Cada una de estas animaciones está compilada como clase de ActionScript 3 para poder acceder a ellas dinámicamente.

Como en el ejemplo simple de la silla, nos vamos a programar una clase que herede de IsoSprite (la clase base de los objetos gráficos en as3isolib) y que sea esta misma clase internamente la que se encargue de adoptar la vista adecuada en función de la dirección en la que la queremos mover (o hacia dónde queremos que mire).


	public class SimpleIsoActor extends IsoSprite
	{
		public var direction:String;
		public var walking:Boolean;
		
		public function SimpleIsoActor()
		{
			super();
			
			direction = "Front";
			walking=false;
			sprites = [FrontStand];
			
			setSize(25,25,120);
			
			isAnimated=true;
			
		}

		public function walkDirection(newDirection:String,mX:int,mY:int):void
		{
			moveBy(mX,mY,0);
			
			if(newDirection=="")
				newDirection=direction;
			
			if(walking && (newDirection == direction))
				return;
			
			walking=true;
			
			direction = newDirection;
			
			var classWalk:Object = getDefinitionByName(direction+"Walk");
			sprites = [classWalk];
				
		}
		
		public function standDirection(newDirection:String):void
		{
			if(newDirection=="")
				newDirection=direction;
			
			if((!walking) && (newDirection == direction))
				return;
			
			walking=false;
			
			direction = newDirection;
			
			var classStand:Object = getDefinitionByName(direction+"Stand");
			sprites = [classStand];
		}
	}//end class

Eventos de teclado

Por supuesto, todo videojuego (hasta un ejemplo tan simple como éste) precisa de cierta interacción con el usuario. En este ejemplo hemos optado por manejar al personaje mediante el uso de las teclas de flecha del teclado. Por supuesto, para que el personaje responda a eventos de teclado de as3, hemos de añadir los correspondientes event listeners:


		//función que añade el personaje y sus eventos
		//a la escena isométrica
		private function addPerson():void
		{
			//creamos personaje
                        //la clase SimpleIsoActor extiende de IsoSprite
			character = new SimpleIsoActor();
			
			//posición inicial
			character.moveTo(140, 140, 0);
			
			//añadimos a la escena isométrica
			scene.addChild(character);
			
			//añadimos listeners de teclado
			stage.addEventListener(KeyboardEvent.KEY_DOWN,keydown_handler);
			stage.addEventListener(KeyboardEvent.KEY_UP,keyup_handler);
		}
		//...

Una dificultad al elegir el teclado como mecanismo de input es que por supuesto tenemos que tener en cuenta que el usuario puede querer pulsar más de una tecla a la vez para producir un movimiento diagonal. Para detectar esto (que no es automático en as3) utilizaremos una técnica basada en controlar en todo momento mediante el uso de variables booleanas si una tecla está o no presionada:

		//variables que controlan 
		//si el usuario mantiene presionada una tecla
		private var left_key_down:Boolean=false;
		private var right_key_down:Boolean=false;
		private var up_key_down:Boolean=false;
		private var down_key_down:Boolean=false;
		
		//event listener que se encarga de actualizar las
		//variables de control de teclado (KEYDOWN)
		private function keydown_handler(e:KeyboardEvent):void
		{
			switch(e.keyCode)
			{
				case Keyboard.LEFT:
					left_key_down=true;
					break;
				
				case Keyboard.RIGHT:
					right_key_down=true;
					break;
				
				case Keyboard.UP:
					up_key_down=true;
					break;
				
				case Keyboard.DOWN:
					down_key_down=true;
					break;
			}
			
		}
		
		//event listener que se encarga de actualizar las
		//variables de control de teclado (KEYDOWN)
		private function keyup_handler(e:KeyboardEvent):void
		{
			switch(e.keyCode)
			{
				case Keyboard.LEFT:
					left_key_down=false;
					break;
				
				case Keyboard.RIGHT:
					right_key_down=false;
					break;
				
				case Keyboard.UP:
					up_key_down=false;
					break;
				
				case Keyboard.DOWN:
					down_key_down=false;
					break;
			}
		}

Una vez ese problema está resuelto, tan sólo debemos en nuestro bucle principal (que será un ENTER_FRAME) decidir en qué dirección nos debemos mover en función de qué teclas están presionadas. Un problema que tiene la perspectiva isométrica es que hay que realizar una conversión poco intuitiva de las direcciones, de traslaciones en un plano normal a un plano isométrico. Es decir, en isométrico un movimiento en negativo en el eje Y resulta en un movimiento aparente diagonal al representarlo en una pantalla bidimiensional, no en «arriba» como cabría esperar. Hemos resuelto este problema mediante el uso de unas variables que almacenan el movimiento en cada eje, en función de las teclas presionadas:

		//variables globales necesarias
		private var moveX:int;
		private var moveY:int;
		private var sumXY:int;
		private var direction:String;
		private var do_move:Boolean;
		
		//MAIN LOOP
		private function enterFrame_handler(e:Event):void
		{
			sumXY=moveY=moveX=0;
			direction="";
			do_move=false;
			
			//conversión de teclas a traslaciones isométricas
			if(up_key_down)
			{
				moveX-=5;
				moveY-=5;	
			}
			
			if(down_key_down)
			{
				moveX+=5;
				moveY+=5;
			}
			
			if(left_key_down)
			{	
				moveX-=5;
				moveY+=5;
			}
			
			if(right_key_down)
			{
				moveX+=5;
				moveY-=5;
			}
			
			//ajustamos velocidad, entre 5 y -5
			moveX = Math.max(Math.min(moveX,5),-5);
			moveY = Math.max(Math.min(moveY,5),-5);
			
			//comproblamos que no se produce colisión en el movimiento
			do_move = canMove();
			
			//"truco" para hacer un switch de 2 variables 😉
			sumXY = moveX*10 + moveY;
			switch(sumXY)
			{
				case 0:
					direction="";
					break;
				
				case 5://x:0,y:+5
					direction = "LeftFront";
					break;
				
				case -5://x:0,y:-5
					direction = "RightBack";
					break;
				case -55: //x:-5, y:-5
					direction = "Back";		
					break;
				
				case -50://x:-5, y:0
					direction = "LeftBack";
					break;
				
				case -45: //x:-5, y:+5
					direction = "Left";		
					break;
				
				case 55: //x:+5, y:+5
					direction = "Front";		
					break;
				
				case 50: //x:+5, y:-5
					direction = "RightFront";
					break;
				
				case 45: //x:+5, y:-5
					direction = "Right";		
					break;
				
			}
			
			if(do_move)
			{
				character.walkDirection(direction,moveX,moveY);
				
			}else
			{
				character.standDirection(direction);
			}
			
			//renderizamos escena
			scene.render();
		}
		

Detección de colisiones: isoBounds.intersects

Cualquiera que haya programado un videojuego en ActionScript 3 (o en cualquier otro lenguaje de programación) sabrá que una de las partes críticas a implementar es la detección de colisiones. Ésto es debido a que esta parte del código debe comprobar que todos y cada uno de los elementos en la pantalla no estén colisionando (y si lo están, lanzar los comportamientos adecuados), resultando a menudo en costosos bucles anidados que pueden consumir gran parte de los recursos disponibles. Existen muchas diferentes formas de implementar la detección de colisiones en ActionScript 3, desde la más clásica hitTest y sus múltiples variantes a otras más esotéricas como Pixel-Perfect collision (detección de colisiones pixel a pixel) utilizando propiedades de los Bitmaps. Es fundamental para el buen desarrollo del videojuego el elegir el método más adecuado, que dependerá de las características del proyecto y las de los elementos que pueden colisionar.

Por suerte para nosotros, la librería isométrica as3isolib nos proporciona el método perfecto para la detección de colisiones de elementos isométricos: isoBounds.intersects. Todos los elementos isométricos que estén colocados sobre la escena tienen una propiedad isoBounds que implementa la interfaz IBounds y contiene los «boundaries» (es decir, los límites del espacio ocupado) de dicho objeto isométrico.
isoBounds.intersects por tanto detecta una colisión entre dos objetos isométricos si estos «boundaries» o límites se solapan.

De esta manera, antes de mover al personaje 5 pixels a la derecha, voy a comprobar si este movimiento provoca alguna colisión, para ello utilizo la siguiente técnica:

               
		//comprobar preventivamente si un movimiento de x,y pixels
		//provocaría una colisión con el obstáculo o
		private function checkIntersects(x:int,y:int,o:IsoSprite):Boolean
		{
			//muevo "virtualmente" el personaje 
			//(sin actualizar su posición gráficamente)
			character.moveBy(x,y,0);
			//compruebo si choca
			var intersects:Boolean = character.isoBounds.intersects(o.isoBounds);
			//lo devuelvo a su sitio
			character.moveBy(-x,-y,0);
			
			return intersects;
		}

Esquiva como un ninja!

Al detectar una colisión lo más sencillo (y lo primero que se nos ocurre sin duda) es «anular» el movimiento que provocaba dicha colisión. Sin embargo, al probarlo nos pareció que parar de golpe al personaje resultaba demasiado brusco y la experiencia de usuario (la «jugabilidad» de este ejemplo, podríamos decir) no era demasiado buena. Por tanto, y tras ponernos durante un rato el gorro de pensar, nos decidimos por el siguiente comportamiento:

  • En caso de colisión, el personaje intentará tomar una «alternativa» al movimiento, sin cambiar radicalmente de dirección. Por ejemplo, si me estoy moviendo en dirección derecha y se produce una colisión, en vez de parar al personaje voy a comprobar si en las direcciones diagonales derecha (arriba-derecha o abajo-derecha) también se produce dicha colisión. Tan sólo si hay colisiones en las 3 direcciones posibles el personaje se detendrá.

Finalmente la función que se encarga de detectar colisiones para que también nos proporcione esta «alternativa» nos quedó de la siguiente manera:

		
		//comprobamos si el movimiento es válido
		private function canMove():Boolean
		{
			var choca:Boolean;
			
			//loop de obstáculos en el stage
			for each(var o:IsoSprite in obstacles)
			{
				//si ya no me estoy moviendo, salgo del loop
				if( (moveX == 0) && (moveY == 0) )
					break;

				//choco con el obstáculo?			
				choca = checkIntersects(moveX,moveY,o);
				
				//si choca, voy a intentar "esquivar el obstáculo"
				if(choca)
				{
					//intento esquivarlo por el eje Y
					choca = checkIntersects(0,moveY,o);
					
					//si ya no choco, me quedo con este movimiento
					if(!choca)
					{
						moveX=0;
						break;
					}
					
					//intento esquivarlo por el eje X
					choca = checkIntersects(moveX,0,o);
					
					//si ya no choco, me quedo con este movimiento
					if(!choca)
					{
						moveY=0;
						break;
					}
					
					//si sigo aquí, es que me he quedado sin movimientos
					//no puedo seguir moviendo
					return false;
				}
				
			}
			
			//me podré mover si aún me queda algún moviemento
			//en el eje X o Y
			return ((moveX != 0) || (moveY != 0));
			
		}

¡Y con ésto terminamos con todo el código de este ejemplo! Por supuesto, para un proyecto de videojuego real habría que tener en cuenta muchas otras cosas (como por ejemplo que el personaje hiciera de hecho algo además de moverse por ahí :P) pero partiendo de una base tan simple como ésta (tan sólo 2 clases y no más de 500 líneas de código), nos parece que este ejemplo es una buen punto de partida para aprender a utilizar un poco más la librería as3isolib y una introducción a las dificultades típicas del desarrollo de videojuegos.

Etiquetas: , , , , ,

6 Comentarios
» Feed RSS de los Comentarios

  1. Marcos dice:

    Como siempre, un post más que interesante.
    Que nivelazo tiene este blog!! 🙂

  2. danii dice:

    Gracias Marcos! 🙂 Son comentarios así los que nos animan a seguir dándole caña a este blog, y más viniendo de cracks como tú!

  3. giancarlo dice:

    Interesante y completo tutorial muy util sigue con ello enorabuena!

  4. Maximiliano dice:

    Muchisimas Gracias! Me sirvió completamente . Al fin algo en español que es de calidad.

  5. Juanan dice:

    El mejor tuto que he visto de este tema tras intensas búsquedas a lo largo y ancho de Google! Enhorabuena y muchísimas gracias.

  6. Laura dice:

    Excelente tutorial, lo mejor y casi unico que he encontrado en este tema. ¡Felicitaciones!!

Enviar comentario