7 mar 2014

Tutorial: Diseña niveles fácil y rápido en Unity 3D

Cuando se empieza a desarrollar un videojuego nos encontramos con que hay que crear un sistema para poder diseñar todas las fases para que las personas encargadas de crear niveles disponga de las herramientas necesarias para realizar dicha tarea.

Este tutorial está orientado a quien pretenda desarrollar un videojuego cuyos escenarios estén divididos en cuadrículas 2D y en poco tiempo. Es necesario tener conocimientos básicos del entorno de Unity 3D y de programación C#, aunque es fácilmente aplicable a otros entornos de desarrollo y lenguajes. Puede ser válido sólo en ciertos tipos de juegos como los del género de plataformas, videojuegos tipo arcade, puzzles, para los mapas de un juego de rol, y sobre todo para el juego que fue diseñado:Save The Orcs(antes Orcs Must Survive) de Echoboom Apps, un videojuego del sub-género Tower Defence donde las batallas transcurren en un tablero.

Existen muchísimas formas de enfocar ésta parte del desarrollo, desde utilizar aplicaciones externas (como TuDee, un editor de mapas) hasta programar un lector de ficheros (p.ej. XML) que interprete los datos de entrada y los transforme en secuencias de juego. La mejor elección dependerá del tipo de juego, de la cantidad de niveles, del tiempo disponible, etc.

El sistema que se propone sirve para guardar la información del escenario en imágenes, ya sean baldosas, objetos de juego o enemigos, pudiendo diseñar las fases con Paint, Gimp, Photoshop, etc.

El ejemplo que vamos a seguir básicamente genera los enemigos a partir de una imagen como un organillo emite las notas musicales de una canción a partir de las púas del cilindro.



Lo primero es definir el tamaño de las imágenes y cómo se va interpretar. Imagina que estás haciendo un juego en el que los enemigos van entrado por el lado derecho de un tablero de 6 filas de alto, en intervalos de tiempo fijos, 5 segundos por ejemplo. Por lo tanto las dimensiones de cada imagen son seis píxeles de alto por la duración de la fase entre cinco:    (Duración / 5) px.  x  6 px.

Por otro lado establecer una paleta de colores que se corresponda con tu lista de enemigos. Ésta paleta debe definir exactamente cada color, ya que el generador distinguirá tonos, brillos, etc...

En tu proyecto tienes que tener una carpeta donde situar las imágenes de fase, y deberás elegir la manera en la que el controlador de juego enviará al generador de enemigos las imágenes. Puedes tener un array de texturas para enlazar las imágenes desde el Inspector de Unity 3D, o si quieres leer las imágenes por código, sitúa la carpeta dentro de una nombrada Resources (ver doc).



Ten en cuenta que para poder leer las imágenes se empleará la clase Texture2D, por lo que todas las imágenes necesitarán las siguientes opciones de importación para ser leídas posteriormente (ver doc):

Texture type: Advanced
Non Power of 2: None
Read / Write Enabled: Active
Import type = Default
Alpha from Gray Scale: Not active
Bypass sRGB Sampling: Not active
Generate Mip maps: Not active

Wrap Mode: Repeat
Filter Mode:
Point
Default:       
    Max size: 1024     (por ejemplo)
    Format: RGB 24 bit






Ya es hora de empezar a escribir código. En los ejemplos se emplea C#, pero es fácilmente portable a UnityScript y Boo. Comenzamos con una estructura de datos que relacione cada prefabricado de Unity con su color, haciéndola serializable para poder rellenar los datos en el Inspector. Por ejemplo:

/// <summary>
/// Estructura de datos para los enemigos
/// </summary>
[System.Serializable]
public class Enemy {
   // Referencia al prefabricado de Unity
   public GameObject prefab;
   // Color indicativo del enemigo
   public Color color;
}


NOTA: Al final del tutorial hay un enlace al proyecto, con todos los códigos fuentes comentados

Crea un script que se va a encargar de controlar el tiempo y generar los enemigos, aquí llamado “EnemiesGenerator.cs”, con las siguientes características:
   Que tenga acceso a la imagen de fase, por ejemplo una variable pública de tipo Texture2D (public Texture2D levelImage) que asignemos posteriormente en el Inspector.
   Un listado o array de Enemigos(Prefabricado, Color) como es el caso. Este listado puede interesar rellenarlo en otro script / componente diferente, ya que es interesante que ésta información esté en un único lugar en todo el juego.
   Variables para controlar el tiempo (timeInterval, timer), el número de enemigos y un GameObject que albergará todos los enemigos creados (enemiesGroup).

public class EnemiesGenerator : MonoBehaviour {

   public const int NUMBER_OF_ROWS = 6;

   public Enemy[] enemies;

   public Texture2D levelImage;

   public bool isCompleted;

   public float timeInterval = 3;

   public float timer;       

   [HideInInspector]
   public int numberOfEnemies;

   private int stageColumn = 0;

   GameObject enemiesGroup;

   (…)
}


Como de costumbre, en el método Start(), inicializa las variables necesarias:

void Start ()
{
   isCompleted = false;
   enemiesGroup = GameObject.Find("Enemies group");
   numberOfEnemies = 0;
}


En el método Update()  sólo hay que controlar el llamar a la lectura de la imagen de fase cada cierto tiempo, en nuestro caso en el intervalo de tiempo indicado en   timeInterval, y controlar que no se haya llegado a la última columna de la imagen:

void Update ()
{
   if(timer > 0){
      timer -= Time.deltaTime;
   }
   if(timer <= 0)
   {
      timer = timeInterval;
      if (stageColumn < levelImage.width)
         ReadLevelImage(stageColumn);
      else if (stageColumn > levelImage.width)
         isCompleted = true;
      stageColumn++;
   }
}


Creamos un método para leer una columna de la imagen de fase, que recorra la columna pixel a pixel y que se dé cuenta de cuando un pixel es distinto de blanco para buscar el prefabricado del enemigo correspondiente que habrá que instanciar:

void ReadLevelImage(int column) {

   for (int row = 0; row < NUMBER_OF_ROWS; row++)
   {
      int rowInTexture = NUMBER_OF_ROWS - row - 1;
      Color colorPixel = levelImage.GetPixel(column, rowInTexture);

      // Si no es un pixel blanco busca el enemigo a crear
      if ( !colorPixel.Equals( new Color(1,1,1) ) )
      {
         GameObject prefab = EnemyWithColor(colorPixel);
         CreateEnemy(prefab, row);
      }
   }
}


Nos puede venir muy bien una función que busque en la lista de enemigos cual es el que corresponde al color del píxel leído:


GameObject EnemyWithColor(Color color) {

   foreach (Enemy enemy in enemies) {
      // Es mejor comparar canal por canal para evitar problemas con el alpha
      // enemy.color.Equals(color)   en principio no funcionará si el alpha es distinto
      if (enemy.color.r == color.r &&
         enemy.color.g == color.g &&
         enemy.color.b == color.b) {

         return enemy.prefab;
      }
   }
   Debug.Log("No se ha encontrado enemigo con color " + color.ToString());
   return null;
}


Y por último un método que instancie el enemigo encontrado en la posición correcta: 

void CreateEnemy(GameObject enemyType, int fila) {

   if (enemyType == null)
      return;

   numberOfEnemies++;
   Vector3 position = new Vector3(10, fila - 3, 0);
   // Aquí instanciamos el enemigo y lo preparamos para su uso.
   GameObject newEnemy = (GameObject) Instantiate(enemyType, position, new Quaternion(0,0,0,0));
   newEnemy.transform.parent = enemiesGroup.transform;
   newEnemy.name = enemyType.name + " " +numberOfEnemies.ToString();
   newEnemy.transform.Rotate(new Vector3(0,0,90));

   Debug.Log("Creado enemigo \"" + newEnemy.name + "\" en la fila " + fila.ToString());           
}


En la escena de Unity 3D, añadimos el script creado a un GameObject, y en las propiedades rellenamos los datos, vinculamos prefabricados y colores, ajustamos el intervalo de tiempo, asignamos la imagen de fase, etc.


Puedes descarte este proyecto de ejemplo con todos los comentarios del código fuente desde aquí, o bien verlo en funcionamiento si dispones del plugin Unity Web Player.

¿Os ha servido de ayuda el tutorial?  Por favor dejad vuestros comentarios y os responderé en cuanto pueda.

2 comentarios:

  1. Gracias por la explicación de esa forma de hacerlo! :)

    ResponderEliminar
  2. Se agradece mucho, encontrar este tipo de info, incluso a pesar de que han pasado más de dos años desde que lo publicases. Gracias :)

    ResponderEliminar

Athagon
Servicios
Compañia
Prensa
Blog
Trabajo
Contacto
Politica de privacidad
Productos
Save Your Pets
Siguenos
twiter facebook
google youtube
linkedin vimeo
En resumen
Somos una compañia independiente centrada en el desarrollo de videojuegos y aplicaciones