PixelPerfect en Unity 2D

Si os poneis manos a la obra en haver un proyecto con estética de los años 80 del siglo pasado lo mas probable es que opteis por una de las 2 opciones, o modificais la escala de vuestros assets gráficos con un editor externo o trabajais con gráficos de la época (16x16 pixeles o 32 x 32 pixeles) y utilzais el propio Unity para escalar el gráfico al tamaño deseado.


Para ello es necesario preparar los gráficos para que no os queden distosionados al aplicar el algoritmo de antialiasing al escalar el gráfico desde su tamaño original (16x16 pixeles en este ejemplo) a la escala deseada en nuestro juego.


Cuando importamos una imagen Unity configura de manera predeterminada algunos filtros para suavizarla cuando la muestra en la pantalla. Esta puede ser una buena idea para otro tipo de juegos, pero no para los PixelArt.

Al importar los graficos del juego debemos configurar Unity para que el escalado será correcto debemos modificar el filter mode a Point (no filter).


y seguidamente el modo de compresion a None.


Tambien es necesario modificar el valor del Pixel per Unit (PPU). Puede encontrar esta configuración también en Sprite Mode del Sprite. Es 100 por defecto. Cámbielo a su tamaño de mosaico en píxeles (por ejemplo, en nuestro caso tenemos mosaicos de 16x16).

Ahora que hemos configurado nuestra PPU, necesitamos decirle a la cámara qué tan grande queremos que se base en esa PPU para que un píxel en pantalla corresponda con un píxel en nuestro juego.

Para obtener ese tamaño solo necesitamos hacer una fórmula simple:

Tamaño ortográfico de la cámara = resolución vertical / PPU / 2
Una vez que tenga ese tamaño, haga clic en el objeto Cámara principal y coloque ese valor en la configuración Tamaño:


En nuestro caso tenemos 180px como resolución vertical y 16px PPU así que:

180/16/2 = 5.625

Es importante mencionar que en nuestro caso renderizamos todo en una resolución pequeña y luego escalamos. No coloque la resolución vertical deseada (en nuestro caso 720p) sino la que está renderizando.

Finalmente para garantizar que el movimiento pixelado de Sprites sea coherente entre sí, configure Spans settings para establecer el ajuste adecuado para el proyecto. 

Abra la Configuración de ajuste (menú: Edición> Snap Settings...)
Para Mover X / Y / Z, establezca sus valores en 1 / PPU (en nuestro ejemplo 1/16 = 0.0625)
 

La configuración de ajuste no se aplica de forma retroactiva. Si hay GameObjects preexistentes en su escena, seleccione cada uno de ellos y haga clic en Ajustar todos los ejes para aplicar la configuración del Snap.

El mundo a través de 1bit

Una de las cosas que admiro es la capacidad que tienen algunos diseñadores y artistas para crear un mundo con personalidad a través de imagenes a 1bit.


Unos de los ejemplos más aclamados son juegos como Gato Roboto o 1-Bit Rogue. Juegos donde los gráficos a 1bit son parte de la identidad del propio diseño del juego.


Para todos aquellos que os gustaria provar de hacer un juego con este estilo tan especial me gustaria compartir los recursos que he encontrado que podrian ser de gran utilidad. Para ello la mejor página de recursos es Itch.io.

Aquí podeis ver un conjunto de recursos de 1bit que pueden ser interesantes:

1 Bit-Game Assets Pack



CanariPack 1BIT TopDown

 


CanariPack 1BIT TopDown1700+ 1Bit Game Assets & PixaTool


75 Free 48x48 1-Bit Fantasy Sprites


16x16 1-bit RPG Forest Tile Set
 

1-Bit RPG Tileset 32x32 Pixels



1-Bit Icons

1 bit Sci Fi Pixel Art Tileset (16x16)


16x16 1-bit Church Interior Tile Set

Gráficos en 8bits para juegos modernos en Photoshop

Uno de los proyectos que tengo en mente es hacer algun juego con tecnologia actual pero con un aspecto de 8bits. Uno de los aspectos que más se debe que cuidar son los gráficos ya que al trabajar con imagenes pixeladas podemos tener problemas cuando las manipulamos.


Despues de analizar algunos juegos de 8bits podemos observar que basicamente los gráficos utilizados tienen dimensiones pequeñas segun los estandares actuales. Durante esa época se utilizavan sprites base de 16x16 pixeles y 32x32 pixeles añadiendo varios para hacer imagenes más grandes. 
Yo personalmete trabajo con Adobe Photoshop y para cambiar las escalas de los sprites. Debemos tener en cuenta algunos trucos antes de utilizarlos en Unity como assets de un juego. Lo mas habitual para trabajar con Sprites 2D en un modo actual es o simlemente utilizar los sprites en el tamaño original y dejar que Unity se encargue del escalado utilzando el sistema de Pixel Perfect del motor a partir de la versión 2018.2.


O rescalar los gráficos manualmente utilizando un programa de edición de imagenes. Si necesitais hacer esto debeis optar por una de las dos  opciones que Adobe Photoshop nos ofrece para trabajar manteniendo la cualidad de los graficos en el escalado.

Con las parametros originales de Photoshop lo mas probable sea que al escalar los graficios nos pase una cosa como esta:


Para solucionarlo debemos ir a las Edicion > Preferencias > General y modificar las opciones de escalado a Nearest Neighbor (preserve hard edges) y a partir de aquí escalar las imagenes normalmente.


El segundo metodo consiste en tener en cuenta las propiedades de escalado cuando modificamos el tamaño de un sprite. Ahora, en Imagen > Tamaño de imagen puede cambiar el tamaño de la imagen. Esta opción también es accesible presionando Alt + Ctrl + I en el teclado.

Una vez que se abre la ventana Tamaño de imagen, puede cambiar el ancho y la altura del archivo. Asegúrese de que la opción Restringir proporciones esté marcada para que Photoshop pueda llenar automáticamente el ancho o la altura apropiados de la imagen cuando complete la otra información. Esto es opcional Sin embargo, se recomienda esta acción para que la imagen se escale correctamente.


Finalmente haga clic en el menú desplegable en la parte inferior de la ventana Tamaño de imagen y cambie la opción a Nearest Neighbor (preserve hard edges)

Con estas dos métodos prodrá obtener images a cualquier tamaño manteniendo el efecto de pixel perfect de sus imagenes.


Juegos, un mundo global en varios idiomas

En el juego que estamos desarrollando es primordial llegar al máximo público posible. Al ser un juego de estrategia es necesario explicar a través de un tutorial explicar las opciones que en cada momento tiene el jugador. Para ello es importante que le jugador pueda entender bien las instrucciones con lo cual llegar a través de un idioma conocido es importante.


En este proyecto utilizo el asset de Unity creado por Inter Illusion: I2 Localization. Este asset nos permite cambiar, entre muchas otras cosas, dinámicamente el idoma del proyecto y localizar el juego para cada jugador. L2 puede utilizar el traductor de Google para traducir el juego a varios idiomas de forma dinámica y gestionar varias fuentes para lenguas no latinas.  Todo ello a través de la interfaz integrada en el propio editor de Unity para máxima comodidad.



En nuestro caso, más allá de las herramientas elementales del producto queremos que en tiempo de ejecución todos los idiomas disponibles y que cree una scroll list dinámica a través de un elemento prefab. De este modo podremos añadir nuevos idiomas en el futuro sin tener que modificar una línea de código.


Para ello es necesario leer el número de idiomas disponibles y crear un elemento de la lista a través de un prefab para cada idioma. Al pulsar sobre el idioma llamaremos a la función que modifica el idioma actual.

 using System.Collections.Generic;  
 using UnityEngine;  
 using UnityEngine.UI;  
 public class Languages : MonoBehaviour  
 {  
   public GameObject langPrefab;  
   public GameObject itemsParent;  
   // Start is called before the first frame update  
   void Start()  
   {  
     I2.Loc.LocalizationManager.CurrentLanguageCode = GlobalInfo.language;  
     List<string> langs = new List<string>();  
     langs = I2.Loc.LocalizationManager.GetAllLanguagesCode();  
     List<string> langsName = new List<string>();  
     langsName = I2.Loc.LocalizationManager.GetAllLanguages();  
     for (int i = 0; i < langs.Count ; i++)  
     {  
       //Edit Prefab before Instantiate  
       Transform langTitle = langPrefab.GetComponentInChildren<Transform>().Find("Name");        
       langTitle.GetComponent<Text>().text = I2.Loc.LocalizationManager.GetTermTranslation("Language", true, 0, true, false, null, langsName[i]);  
       Transform langCode = langPrefab.GetComponentInChildren<Transform>().Find("Code");  
       langCode.GetComponent<Text>().text = langs[i].ToUpper();  
       Instantiate(langPrefab, new Vector3(0, 0, 0), Quaternion.identity, itemsParent.transform);  
     }      
   }  
 }  

Para ello es necesario leer el número de idiomas disponibles y crear un elemento de la lista a través de un prefab para cada idioma. Al pulsar sobre el idioma llamaremos a la función que modifica el idioma actual.


 using UnityEngine;  
 using UnityEngine.UI;  
 public class Language : MonoBehaviour  
 {  
   public Text langCode;  
   public void LoadNewLanguage()  
   {  
     I2.Loc.LocalizationManager.CurrentLanguageCode = langCode.text;  
     GlobalInfo.language = langCode.text;  
     PlayerInfo loadedData = DataSaver.loadData<PlayerInfo>(GlobalInfo.configFile, "txt");  
     loadedData.language = GlobalInfo.language;  
     DataSaver.saveData(loadedData, GlobalInfo.configFile, "txt");  
   }  
 }  

Después de seleccionar un nuevo idioma, es necesario actualizar la variable que define el idioma actual y guardar la información en el archivo de configuración para que la próxima partida cargué el idioma deseado automáticamente.

Attila. Condiciones de victoria y derrota

Una vez construida las mecánicas básicas del juego llego el momento de programar las condiciones de victoria y derrota. De hecho son aquellas condiciones que nos hacen progresar dentro del nivel de juego y avanzar si conseguimos superar el nivel y acceder al siguiente nivel.


Este enfoque necesita gestionar el movimiento del caballo en la escena del juego así como programar las mecánica del menú de niveles que hasta ahora era un composición de imágenes estáticas antes de entrar en el nivel de prueba.

Basándonos en la mecánica del juego:

  1. Recogemos las propiedades del ejército.
  2. El caballo de mueve a una casilla disponible.
  3. Destruimos la casilla de origen.
  4. Se produce un evento en función de la casilla destino.
  5. Actualizamos las propiedades del ejército.
  6. Mostramos las nuevas casillas disponibles.


Consideraciones:

  1. Si la casilla destino es un objetivo la descontamos.
  2. Si la casilla es final pero aún quedan casillas objetivo por destruir no podemos avanzar.
  3. Si no quedan casillas objetivo y estamos en una casilla final… ejecutamos el evento y si es superado terminados el nivel.

Sobre esta mecánica hay que tener en cuenta dos situaciones distintas. En cualquier casilla se debe poder ver cuál es el balance del movimiento del caballo sobre la casilla y como afecta a la composición del ejército. En las casillas donde se supone que hay una contienda entre romanos y hunos (pueblo, ciudad, objetivo, casilla de ejército) debemos mostrar, además, el resultado de la contienda.


Puede darse el caso que en cualquier batalla, si nuestros recursos no son suficientes, que seamos derrotados y debamos volver a iniciar el nivel desde la posición inicial. En caso de ganar la batalla, recibiremos la compensación de esa casilla y seguiremos avanzando.

Para gestionar estas condiciones trabajaremos en primer lugar con la clase MovePlayer donde dejamos preparada la función para gestionar los eventos. Ahora sólo es necesario distinguir en función del tipo la casilla destino del jugador y actuar en consecuencia. Si conseguimos terminar el nivel, será necesario, también, salvar la información en el archivo de configuración y avanzar de nivel.

 public void ProcessEvents()  
   {  
     //Update values  
     if (GameObject.Find(destination).GetComponent<GameCell>().objective == true)  
     {  
       Objective();  
     }  
     if (GameObject.Find(destination).GetComponent<GameCell>().final == true)  
     {  
       Final();  
     }  
     if ((GameObject.Find(destination).GetComponent<GameCell>().final == false) && (GameObject.Find(destination).GetComponent<GameCell>().objective == false))  
     {  
       int nType = GlobalInfo.gridStage[GameObject.Find(destination).GetComponent<GameCell>().num - 1].type;  
       // Is Town OR City OR Army  
       if (nType == 4 || nType == 5 || nType == 6 )  
       {  
         GameObject.Find("GameManager").GetComponent<GameManager>().ShowBattleResult();  
       }  
       Normal();  
     }  
     //Events  
     if (GlobalInfo.objectivesNum == 0)  
     {  
       if (GlobalInfo.finalNum == 0)  
       {  
         //Good job!  
         GlobalInfo.maxStageCompleted = GlobalInfo.actualStage;  
         PlayerInfo loadedData = DataSaver.loadData<PlayerInfo>(GlobalInfo.configFile, "txt");  
         loadedData.actualStage = GlobalInfo.actualStage;  
         loadedData.maxStageCompleted = GlobalInfo.maxStageCompleted;  
         loadedData.troops = GlobalInfo.troops;  
         loadedData.weapons = GlobalInfo.weapons;  
         loadedData.water = GlobalInfo.water;  
         loadedData.food = GlobalInfo.food;  
         loadedData.gold = GlobalInfo.gold;  
         loadedData.score = GlobalInfo.gold;  
         DataSaver.saveData(loadedData, GlobalInfo.configFile, "txt");  
         if (GlobalInfo.maxStageCompleted == GlobalInfo.maxStagesGame)  
         {  
           //Game Finish  
           SceneManager.LoadScene("Winner");  
         }  
         else  
         {  
           //Next stage  
           StartCoroutine(NextLevel());            
         }          
       }  
     }  
     if (GlobalInfo.movementsNum == 0)  
     {  
       //Game over;  
     }  
     ShowInfo();  
     GlobalInfo.isPlayerMoving = false;  
   }  
   public void Objective()  
   {  
     GlobalInfo.score = GlobalInfo.score + 100;  
     GameObject.Find(destination).GetComponent<GameCell>().UpdateValues();  
     GlobalInfo.objectivesNum--;  
     GameObject.Find("GameManager").GetComponent<GameManager>().ShowBattleResult();  
   }  
   public void Final()  
   {  
     GlobalInfo.score = GlobalInfo.score + 1000;  
     if (GlobalInfo.objectivesNum == 0)  
     {  
       GameObject.Find(destination).GetComponent<GameCell>().UpdateValues();  
       GlobalInfo.finalNum--;  
       GameObject.Find("GameManager").GetComponent<GameManager>().ShowBattleResult();  
     }      
   }  
   public void Normal()  
   {  
     GameObject.Find(destination).GetComponent<GameCell>().UpdateValues();  
     GlobalInfo.score = GlobalInfo.score + 10;  
   }  
   IEnumerator NextLevel()  
   {  
     yield return new WaitUntil(() => GlobalInfo.isShowingInfo == false);  
     GlobalInfo.actualStage++;  
     SceneManager.LoadScene("Attila");  
   }  

Así mismo debemos controlar el número de objetivos por conseguir antes de ejecutar el evento de la casilla final. Esto nos permite además incluir otra pieza en la estrategia del jugador. Mientras no ataquemos la casilla final la podemos utilizar como pivote para movernos entre otras casillas ya que esta no será destruida al pasar por ella. También las casillas de tipo montaña nos permites hacer esta función ya que tampoco serán destruidas al pasar por ellas. Estas características especiales nos permitirán modular la estrategia para resolver cada nivel y graduar y a tener más piezas para modelar la dificultad de los diferentes niveles del juego.


La segunda parte del proceso consiste en mostrar las cajas de dialogo para mostrar la información cuando es requerida o al entrar en una casilla que produce una batalla. Para ello se ha creado unas cajas de dialogo en el canvas para mostrar la información.

Hama Beads: Knight Lore

Una de las cosas que me gusta hacer para relajarme es jugar con Hama beads. Estas pequeñas piezas de colores de plástico que sobre un platilla podemos construir dibujos en modo Pixel Art. Es una buena manera de distraerse y crear composiciones con los diseños y patrones que podemos encontrar en Internet.



Sobre la temática de juegos hay muchos patrones pero de juegos clásicos y retro ya cuesta más de encontrar, así que os pongo aquí algunos diseños de juegos clásicos que he he hecho yo mismo.



Espero que os gusten y paséis un rato entretenido haciendo y compartiendo vuestras creaciones. 

CABALLERO: KNIGHT LORE (1984)

Attila. Mecánica de juego

Una vez terminado el editor de niveles, podemos crear algunos y gravarlos en la carpeta resources para que al instalar el juego en un dispositivo móvil podamos seguir accediendo a los archivos individualmente.


El siguiente paso, más allá de crear las escenas intermedias (menú principal, menú de etapas) que más adelante implementaremos nos permiten llegar a la escena del juego. En esta escena queremos mostrar el tablero en formato isométrico para poder jugar con el nivel previamente diseñado.


En concreto yo utilizo Isometric Builder para para implementar las escenas isométricas ya que me permite, más adelante, tratar cada casilla como un objetos independiente y en este caso añadir clases para cada casilla independiente.


El primer paso es diseñar el tablero máximo para distribuir los elementos de juego (tablero HUD, controles) en el espacio. En este caso yo he utilizado un gráfico isométrico genérico para implementar las casillas vacías.


A partir de aquí la implementación del juego consiste en la interrelación de las clases compuestas por el GameManager, el Player, y las casillas del tablero.



El GameManager se encarga de cargar el tablero a partir del archivo en la carpeta resources y generar el tablero en pantalla. Así mismo debemos calcular los movimientos disponibles después de mover el jugador así como procesar los eventos que se generen al mover el caballo. También es su función estar permanentemente a la escucha para saber cuándo el jugador ha pulsado alguna casilla de destino.


GameManager.cs
 using System.Collections;  
 using System.Collections.Generic;  
 using UnityEngine;  
 using UnityEngine.SceneManagement;  
 using UnityEngine.UI;  
 public class GameManager : MonoBehaviour  
 {  
   public Text troops;  
   public Text weapons;  
   public Text water;  
   public Text food;  
   public Text gold;  
   public Text stageName;  
   public Sprite tundra;  
   public Sprite desert;  
   public Sprite woods;  
   public Sprite town;  
   public Sprite city;  
   public Sprite army;  
   public Sprite crops;  
   public Sprite lake;  
   public Sprite river;  
   public Sprite mine;  
   public Sprite objective;  
   public Sprite mountains;  
   public Sprite empty;  
   public Sprite horse;  
   private int troopsO;  
   private int weaponsO;  
   private int waterO;  
   private int foodO;  
   private int goldO;  
   private int scoreO;  
   // Start is called before the first frame update  
   void Start()  
   {  
     SaveOriginals();  
     GlobalInfo.isPlaying = false;  
     GlobalInfo.stagesCount++;  
     SetEnviroment();  
     StartPlay();  
   }  
   public void SetEnviroment()  
   {  
     troops.text = GlobalInfo.troops.ToString("#,#");  
     weapons.text = GlobalInfo.weapons.ToString("#,#");  
     water.text = GlobalInfo.water.ToString("#,#");  
     food.text = GlobalInfo.food.ToString("#,#");  
     gold.text = GlobalInfo.gold.ToString("#,#");  
     Levels.LoadLevel(GlobalInfo.actualStage);  
     GlobalInfo.objectivesNum = 0;  
     GlobalInfo.finalNum = 0;  
     stageName.text = GlobalInfo.stageName;  
     PaintStage();  
     PaintInfo();  
   }  
   public void SaveOriginals()  
   {  
     //Save original values  
     troopsO = GlobalInfo.troops;  
     weaponsO = GlobalInfo.weapons;  
     waterO = GlobalInfo.water;  
     foodO = GlobalInfo.food;  
     goldO = GlobalInfo.gold;  
     scoreO = GlobalInfo.score;  
   }  
   public void RestoreOriginals()  
   {  
     //Restore original values  
     GlobalInfo.troops = troopsO;  
     GlobalInfo.weapons = weaponsO;  
     GlobalInfo.water = waterO;  
     GlobalInfo.food = foodO;  
     GlobalInfo.gold = goldO;  
     GlobalInfo.score = scoreO;  
   }   
   private void StartPlay()  
   {  
     GlobalInfo.isPlaying = true;  
   }  
   private void PaintStage()  
   {  
     int xx = 0;  
     int yy = 0;  
     for (int i = 1; i <= 64; i++)  
     {  
       GameObject cell = GameObject.Find("Cell" + i.ToString());  
       GameObject regular = GetChildWithName(cell, "Regualr_Collider_Union");  
       GameObject spriteCell = GetChildWithName(regular, "Iso2DObject_Union");  
       // GameCell Class info  
       cell.GetComponent<GameCell>().num = i;  
       cell.GetComponent<GameCell>().x = xx;  
       cell.GetComponent<GameCell>().y = yy;  
       xx++;  
       if (xx == 8)  
       {  
         xx = 0;  
         yy++;  
       }  
       //Sprite  
       if (GlobalInfo.gridStage[i - 1].type > 0)  
       {  
         spriteCell.GetComponent<SpriteRenderer>().sprite = TypeSprite(GlobalInfo.gridStage[i - 1].type);  
         if (GlobalInfo.gridStage[i - 1].isObjective)  
         {            
           GameObject ObjFlag = GeneralUtils.FindObject(cell, "Flag");  
           ObjFlag.SetActive(true);  
           cell.GetComponent<GameCell>().objective = true;  
           GlobalInfo.objectivesNum++;            
         }  
         if (GlobalInfo.gridStage[i - 1].isFinal)  
         {           
           GameObject FinFlag = GeneralUtils.FindObject(cell, "FlagF");  
           FinFlag.SetActive(true);  
           cell.GetComponent<GameCell>().final = true;  
           GlobalInfo.finalNum++;  
         }  
       } else  
       {  
         Destroy(cell);  
       }  
       //Player info  
       if (GlobalInfo.gridStage[i - 1].isStart == true)  
       {  
         GlobalInfo.playerPos = i;  
       }        
     }  
     //Set player  
     GameObject player = GameObject.Find("Player");  
     player.transform.position = GameObject.Find("Cell" + GlobalInfo.playerPos.ToString()).GetComponent<Transform>().position;  
     //Set player cell  
     GameObject cellStart = GameObject.Find("Cell" + GlobalInfo.playerPos.ToString());  
     GameObject regularStart = GetChildWithName(cellStart, "Regualr_Collider_Union");  
     GameObject spriteCellStart = GetChildWithName(regularStart, "Iso2DObject_Union");  
     spriteCellStart.GetComponent<SpriteRenderer>().sprite = horse;  
     Invoke("CalculateMovementsAvaliable", 0.5f);  
   }  
   private void CalculateMovementsAvaliable()  
   {  
     //Calculate movements avaliable  
     GameObject[] cells = GameObject.FindGameObjectsWithTag("GameCell");  
     foreach (GameObject cell in cells)  
     {  
       cell.GetComponent<GameCell>().CalculateMovements();  
     }  
     GameObject cellStart = GameObject.Find("Cell" + GlobalInfo.playerPos.ToString());  
     cellStart.GetComponent<GameCell>().SetMoveables();  
     cellStart.GetComponent<GameCell>().ShowMoveables();  
   }  
   private GameObject GetChildWithName(GameObject obj, string name)  
   {  
     Transform trans = obj.transform;  
     Transform childTrans = trans.Find(name);  
     if (childTrans != null)  
     {  
       return childTrans.gameObject;  
     }  
     else  
     {  
       return null;  
     }  
   }  
   public void PaintInfo()  
   {  
     GameObject.Find("Player").GetComponent<MovePlayer>().ShowInfo();  
   }  
   public Sprite TypeSprite(int num)  
   {  
     if (num == 1) { return tundra; }  
     if (num == 2) { return desert; }  
     if (num == 3) { return woods; }  
     if (num == 4) { return town; }  
     if (num == 5) { return city; }  
     if (num == 6) { return army; }  
     if (num == 7) { return crops; }  
     if (num == 8) { return lake; }  
     if (num == 9) { return river; }  
     if (num == 10) { return mine; }  
     if (num == 11) { return objective; }  
     if (num == 12) { return mountains; }  
     return empty;  
   }  
   public void ToStageMenu()  
   {  
     SceneManager.LoadScene("StageSelector");  
   }  
   public void RestartLevel()  
   {  
     RestoreOriginals();  
     SceneManager.LoadScene("Attila");  
   }  
   public void MoveHorse(string origen, string final)  
   {  
     GameObject.Find("Player").GetComponent<MovePlayer>().Move(final);  
   }  
   private bool DestionationAvaliable(GameObject dest)  
   {      
     return dest.GetComponent<GameCell>().moveable;  
   }  
   // Update is called once per frame  
   void Update()  
   {  
     if (Input.GetMouseButtonDown(0))  
     {  
       Ray screenRay = Camera.main.ScreenPointToRay(Input.mousePosition);  
       RaycastHit hit;  
       if (Physics.Raycast(screenRay, out hit))  
       {  
         if (hit.collider != null)  
         {  
           if (hit.collider.gameObject.name == "Regualr_Collider_Union"   
             && GlobalInfo.isPlaying == true   
             && GlobalInfo.isPlayerMoving == false)  
           {  
             if (DestionationAvaliable(GameObject.Find(hit.collider.gameObject.transform.parent.name)))  
             {  
               MoveHorse("Cell" + GlobalInfo.playerPos.ToString(), hit.collider.gameObject.transform.parent.name);  
             }              
           }            
         }          
       }  
     }  
   }  
 }  

Hay que tener en cuenta que el caballo tarda un tiempo en moverse por lo cual debemos gestionar ese tiempo de movimiento y actuar al llegar a la casilla destino y no dejar que se produzca ningún evento mientras el caballo está en movimiento. La clase MovePlayer se encarga de este trabajo, mover el caballo utilizando el NavMesh que hemos creado encima del tablero. También será la encargada de gestionar los eventos al mover el jugador e interaccionar con una casilla.

MovePlayer.cs
 using System.Collections;  
 using UnityEngine;  
 using UnityEngine.AI;  
 using UnityEngine.UI;  
 public class MovePlayer : MonoBehaviour  
 {  
   public Text objectivesText;  
   public Text finalText;  
   private NavMeshAgent agent;  
   private string destination;  
   public void Start()  
   {  
     destination = "";  
     GlobalInfo.isOldCellDestroyed = true;  
   }  
   public void Move(string mCell)  
   {  
     GameObject dest = GameObject.Find(mCell);  
     if (dest != null)  
     {  
       //Don't move at the same position  
       if ("Cell" + GlobalInfo.playerPos.ToString() != mCell)  
       {  
         NavMeshAgent agent = GetComponent<NavMeshAgent>();  
         agent.destination = dest.transform.position;  
         GlobalInfo.isPlayerMoving = true;  
         GlobalInfo.isOldCellDestroyed = false;  
         destination = mCell;  
         dest.GetComponent<GameCell>().SetMoveables();  
         GameObject origin = GameObject.Find("Cell" + GlobalInfo.playerPos.ToString());  
         origin.GetComponent<GameCell>().HideMoveables();  
         StartCoroutine(DestroyCell(origin));  
       }        
     }      
   }  
   IEnumerator DestroyCell(GameObject cellToDestroy)  
   {  
     yield return new WaitForSeconds(1f);      
     Destroy(cellToDestroy);  
     yield return new WaitForSeconds(0.5f);  
     CalculateNewMovements();  
     GlobalInfo.isOldCellDestroyed = true;  
   }  
   private void CalculateNewMovements()  
   {  
     GameObject[] cells = GameObject.FindGameObjectsWithTag("GameCell");  
     foreach (GameObject cell in cells)  
     {  
       cell.GetComponent<GameCell>().CalculateMovements();  
     }  
   }  
   private bool PathComplete()  
   {  
     NavMeshAgent mNavMeshAgent = GetComponent<NavMeshAgent>();  
     if (!mNavMeshAgent.pathPending)  
     {  
       if (mNavMeshAgent.remainingDistance <= mNavMeshAgent.stoppingDistance)  
       {  
         if (!mNavMeshAgent.hasPath || mNavMeshAgent.velocity.sqrMagnitude == 0f)  
         {  
           return true;  
         }  
         return false;  
       }  
       return false;  
     }  
     return false;  
   }  
   public void ProcessEvents()  
   {  
     //Update values  
     if (GameObject.Find(destination).GetComponent<GameCell>().objective == true)  
     {  
       GlobalInfo.objectivesNum--;  
     }  
     if (GameObject.Find(destination).GetComponent<GameCell>().final == true)  
     {  
       GlobalInfo.finalNum--;  
     }  
     if (GlobalInfo.objectivesNum == 0)  
     {  
       if (GlobalInfo.finalNum == 0)  
       {  
         //Good job!  
       }  
     }  
     if (GlobalInfo.movementsNum == 0)  
     {  
       //Game over;  
     }  
     ShowInfo();  
     GlobalInfo.isPlayerMoving = false;  
   }  
   public void ShowInfo()  
   {  
     objectivesText.text = GlobalInfo.objectivesNum.ToString();      
   }  
   private void Update()  
   {  
     if (GlobalInfo.isPlayerMoving == true)  
     {  
       if (PathComplete() && GlobalInfo.isOldCellDestroyed)  
       {          
         GameObject.Find(destination).GetComponent<GameCell>().ShowMoveables();  
         GlobalInfo.playerPos = GameObject.Find(destination).GetComponent<GameCell>().num;  
         //Set destionation cell  
         GameObject cellStart = GameObject.Find("Cell" + GlobalInfo.playerPos.ToString());  
         GameObject regularStart = GetChildWithName(cellStart, "Regualr_Collider_Union");  
         GameObject spriteCellStart = GetChildWithName(regularStart, "Iso2DObject_Union");  
         spriteCellStart.GetComponent<SpriteRenderer>().sprite = GameObject.Find("GameManager").GetComponent<GameManager>().horse;  
         ProcessEvents();          
       }        
     }  
   }  
   private GameObject GetChildWithName(GameObject obj, string name)  
   {  
     Transform trans = obj.transform;  
     Transform childTrans = trans.Find(name);  
     if (childTrans != null)  
     {  
       return childTrans.gameObject;  
     }  
     else  
     {  
       return null;  
     }  
   }  
 }  

Y finalmente tenemos la clase GameCell que se encarga de calcular las posibilidades de movimiento teniendo en cuenta el movimiento del caballo y las casillas que nos quedan disponibles después de cada tirada, mostrar u ocultar las posibilidades en pantalla.


GameCell.cs
 using System.Collections;  
 using System.Collections.Generic;  
 using UnityEngine;  
 using System;  
 public class GameCell : MonoBehaviour  
 {  
   public int x;  
   public int y;  
   public int num;  
   public bool moveable;  
   public bool final;  
   public bool objective;  
   public int[] moves = new int[8] ;  
   // Start is called before the first frame update  
   void Start()  
   {  
     ResetMovements();      
   }  
   private void ResetMovements()  
   {  
     moves[0] = 0;  
     moves[1] = 0;  
     moves[2] = 0;  
     moves[3] = 0;  
     moves[4] = 0;  
     moves[5] = 0;  
     moves[6] = 0;  
     moves[7] = 0;  
   }  
   public void CalculateMovements()  
   {  
     ResetMovements();  
     if ((x + 1 < 8) && (y - 2 >= 0))  
     {  
       moves[0] = FindGameCell(x+1,y-2);  
     }  
     if ((x + 2 < 8) && (y + 1 < 8))  
     {  
       moves[1] = FindGameCell(x + 2, y + 1);  
     }  
     if ((x + 2 < 8) && (y - 1 >= 0))  
     {  
       moves[2] = FindGameCell(x + 2, y - 1);  
     }  
     if ((x + 1 < 8) && (y + 2 < 8))  
     {  
       moves[3] = FindGameCell(x + 1, y + 2);  
     }  
     if ((x - 1 >= 0) && (y + 2 < 8))  
     {  
       moves[4] = FindGameCell(x - 1, y + 2);  
     }  
     if ((x - 2 >= 0) && (y + 1 < 8))  
     {  
       moves[5] = FindGameCell(x - 2, y + 1);  
     }  
     if ((x - 2 >= 0) && (y - 1 >= 0))  
     {  
       moves[6] = FindGameCell(x - 2, y - 1);  
     }  
     if ((x - 1 >= 0) && (y - 2 >= 0))  
     {  
       moves[7] = FindGameCell(x - 1, y - 2);  
     }  
   }    
   private int FindGameCell(int xx, int yy)  
   {  
     GameObject[] cells = GameObject.FindGameObjectsWithTag("GameCell");  
     foreach (GameObject cell in cells)  
     {  
       if (cell.GetComponent<GameCell>().x == xx && cell.GetComponent<GameCell>().y == yy)  
       {  
         return cell.GetComponent<GameCell>().num;  
       }  
     }  
     return 0;  
   }  
   public void SetMoveables()  
   {  
     GameObject[] cells = GameObject.FindGameObjectsWithTag("GameCell");  
     foreach (GameObject cell in cells)  
     {  
       cell.GetComponent<GameCell>().moveable = false;  
       int pos = Array.IndexOf(moves, cell.GetComponent<GameCell>().num);  
       if (pos > -1)  
       {  
         cell.GetComponent<GameCell>().moveable = true;          
       }  
     }  
   }  
   public void ShowMoveables()  
   {  
     GlobalInfo.movementsNum = 0;  
     for (int i = 0; i < 8; i++)  
     {  
       if (moves[i]>0)  
       {  
         GameObject cell = GameObject.Find("Cell" + moves[i].ToString());  
         GameObject selector = GeneralUtils.FindObject(cell, "Selector");  
         selector.SetActive(true);  
         GlobalInfo.movementsNum++;  
       }  
     }  
   }  
   public int GetMoveables()  
   {  
     int num = 0;  
     for (int i = 0; i < 8; i++)  
     {  
       if (moves[i] > 0)  
       {  
         num++;  
       }  
     }  
     return num;  
   }  
   public void HideMoveables()  
   {  
     for (int i = 0; i < 8; i++)  
     {  
       if (moves[i] > 0)  
       {  
         GameObject cell = GameObject.Find("Cell" + moves[i].ToString());  
         GameObject selector = GeneralUtils.FindObject(cell, "Selector");  
         selector.SetActive(false);  
       }  
     }  
   }  
 }  

Editor de niveles (Parte 3)

En esta tercera y última parte del editor de niveles he hecho la parte donde podemos editar las casillas, pintarlas, modificarlas y guardar el archivo en un archivo JSON y cargar los niveles en el editor para seguir trabajando.


El objetivo final de editor es poder generar archivos que son imprescindibles para mostrar el tablero en las diversas etapas del juego. Una vez los generemos con el editor de niveles podremos generar el tablero con su configuración en el núcleo del juego.



El principal problema que genera un editor de niveles en Unity, es que es necesario tener sincronizada la información entre la parte visual del editor y la estructura que guarda la información que en definitiva será la que a través de la rutina guardaremos en el fichero físico.

En concreto, en nuestro caso, tenemos un objeto en memoria que almacena la información parar todo el editor y un conjunto de celdas (cells) que contiene el componente para cada una de las casillas en pantalla con la información.


Entre el editor visual y la estructura de datos necesitamos rutinas de intercambio que nos permitan pasar de una estructura a la otra para mantener la coherencia de los datos.

Información de un nivel:
 [Serializable]  
 public class StageCell  
 {  
   public int x;  
   public int y;  
   public int listPos;  
   public int type;  
   public bool isFinal;  
   public bool isObjective;  
   public bool isStart;  
   public int water;  
   public int food;  
   public int troops;  
   public int weapons;  
   public int gold;    
 }  
 [Serializable]  
 public class Stage  
 {  
   public int version;  
   public int numStage;  
   public string nameStage;  
   public string fileName;  
   public List<StageCell> gridStage = new List<StageCell>();  
   public Stage()  
   {      
     DefaultCells cond = new DefaultCells();  
     version = cond.version;  
     numStage = 0;  
     nameStage = "";  
     fileName = "";      
     for (int i = 0; i < 64; i++)  
     {  
       StageCell defaultCell = new StageCell();  
       defaultCell.x = 0;  
       defaultCell.y = 0;  
       defaultCell.listPos = 0;  
       defaultCell.type = cond.type;  
       defaultCell.isFinal = false;  
       defaultCell.isObjective = false;  
       defaultCell.isStart = false;  
       defaultCell.water = cond.water;  
       defaultCell.food = cond.food;  
       defaultCell.troops = cond.troops;  
       defaultCell.weapons = cond.weapons;  
       defaultCell.gold = cond.gold;  
       gridStage.Add(defaultCell);  
     }  
   }  
 }  

Cell.cs (gestión de las celdas visuales):
   public void Start()  
   {  
     cellIcon = GameObject.Find("Tile");  
     type = GameObject.Find("Type").GetComponent<Text>();  
     x = GameObject.Find("XText").GetComponent<Text>();  
     y = GameObject.Find("YText").GetComponent<Text>();  
     troops = GameObject.Find("TroopsText").GetComponent<Text>();  
     weapons = GameObject.Find("WeaponsText").GetComponent<Text>();  
     water = GameObject.Find("WaterText").GetComponent<Text>();  
     food = GameObject.Find("FoodText").GetComponent<Text>();  
     gold = GameObject.Find("GoldText").GetComponent<Text>();  
   }  
   public void ImClicked()  
   {  
     if (GlobalInfo.isEditing)  
     {        
       SetInfo(GlobalInfo.paintCellType);  
     }  
     if (GlobalInfo.isPainting)  
     {        
       PaintAll(GlobalInfo.paintCellType);  
     }  
     GlobalInfo.editingCell = info.listPos;  
     ShowInfo();  
   }  
   public void ShowCell()  
   {  
     this.gameObject.GetComponent<SpriteRenderer>().sprite = GameObject.Find("EditorManager").GetComponent<EditorClickManager>().TypeSprite(info.type);  
     finalImage.SetActive(info.isFinal);  
     objectiveImage.SetActive(info.isObjective);  
     startImage.SetActive(info.isStart);  
   }  
   public void ShowInfo()  
   {  
     cellIcon.GetComponent<Image>().sprite = GameObject.Find("EditorManager").GetComponent<EditorClickManager>().TypeSprite(info.type);      
     type.text = TypeName(info.type);  
     x.text = info.x.ToString();  
     y.text = info.y.ToString();  
     troops.text = info.troops.ToString("#,#");  
     weapons.text = info.weapons.ToString("#,#");  
     water.text = info.water.ToString("#,#");  
     food.text = info.food.ToString("#,#");  
     gold.text = info.gold.ToString("#,#");  
     finalImage.SetActive(info.isFinal);         
     objectiveImage.SetActive(info.isObjective);      
     startImage.SetActive(info.isStart);  
     ShowBorder();  
   }  
   private void PaintAll(int num)  
   {  
     GameObject[] cells = GameObject.FindGameObjectsWithTag("EditorCell");  
     foreach (GameObject cell in cells)  
     {  
       cell.GetComponent<Cell>().SetInfo(GlobalInfo.paintCellType);        
     }  
   }  
   public void SetInfo(int num)  
   {  
     DefaultCells cond = new DefaultCells();  
     cond.SetValues(num);  
     this.gameObject.GetComponent<SpriteRenderer>().sprite = GameObject.Find("EditorManager").GetComponent<EditorClickManager>().TypeSprite(num);  
     info.type = num;  
     info.water = cond.water;  
     info.food = cond.food;  
     info.troops = cond.troops;  
     info.weapons = cond.weapons;  
     info.gold = cond.gold;  
     info.isFinal = cond.isFinal;  
     info.isObjective = cond.isObjective;  
     info.isStart = cond.isStart;  
     finalImage.SetActive(info.isFinal);  
     objectiveImage.SetActive(info.isObjective);  
     startImage.SetActive(info.isStart);  
   }  




Editor.cs (Gestión del los datos del nivel):
 public class Editor : MonoBehaviour  
 {  
   public GameObject saveBox;  
   public GameObject newBox;  
   public GameObject modifyBox;  
   public GameObject fileInfoItem;  
   public GameObject editTile;  
   private Stage editorStage;  
   private void ShowFiles(string filePath, string where)  
   {  
     DirectoryInfo dir = new DirectoryInfo(filePath);  
     FileInfo[] info = dir.GetFiles("*.json");  
     GameObject itemsParent = GameObject.Find(where);  
     foreach (Transform child in itemsParent.transform)  
     {  
       Destroy(child.gameObject);  
     }  
     //Find files  
     foreach (FileInfo f in info)  
     {  
       Debug.Log(f.ToString());  
       //Read File info  
       Stage stage = new Stage();  
       stage = DataSaver.loadData<Stage>(f.ToString(),"");  
       //Edit Prefab before Instantiate  
       Transform levelTitle = fileInfoItem.GetComponentInChildren<Transform>().Find("Name");  
       levelTitle.GetComponent<Text>().text = stage.nameStage;  
       Transform levelNum = fileInfoItem.GetComponentInChildren<Transform>().Find("Num");  
       levelNum.GetComponent<Text>().text = stage.numStage.ToString();  
       Transform levelFilename = fileInfoItem.GetComponentInChildren<Transform>().Find("File");  
       levelFilename.GetComponent<Text>().text = Path.GetFileName(f.ToString());  
       Instantiate(fileInfoItem, new Vector3(0, 0, 0), Quaternion.identity, itemsParent.transform);        
     }  
   }  
   public void OpenFile(string file, string name, string num)  
   {          
     editorStage = DataSaver.loadData<Stage>(file, "");  
     editorStage.nameStage = name;  
     editorStage.numStage = int.Parse(num);  
     GlobalInfo.editingCell = -1;  
     GameObject.Find("Grid").GetComponent<Grid>().Clean();  
     Invoke("UpdateGrid", 1.0f);      
     GameObject.Find("StageNumText").GetComponent<Text>().text = editorStage.numStage.ToString();  
   }  
   private void UpdateGrid()  
   {  
     GameObject.Find("Grid").GetComponent<Grid>().PaintCells();  
     GameObject[] cells = GameObject.FindGameObjectsWithTag("EditorCell");  
     int iAux = 0;  
     foreach (GameObject cell in cells)  
     {  
       cell.GetComponent<Cell>().info.x = editorStage.gridStage[iAux].x;  
       cell.GetComponent<Cell>().info.y = editorStage.gridStage[iAux].y;  
       cell.GetComponent<Cell>().info.listPos = editorStage.gridStage[iAux].listPos;  
       cell.GetComponent<Cell>().info.type = editorStage.gridStage[iAux].type;  
       cell.GetComponent<Cell>().info.isFinal = editorStage.gridStage[iAux].isFinal;  
       cell.GetComponent<Cell>().info.isObjective = editorStage.gridStage[iAux].isObjective;  
       cell.GetComponent<Cell>().info.isStart = editorStage.gridStage[iAux].isStart;  
       cell.GetComponent<Cell>().info.water = editorStage.gridStage[iAux].water;  
       cell.GetComponent<Cell>().info.food = editorStage.gridStage[iAux].food;  
       cell.GetComponent<Cell>().info.troops = editorStage.gridStage[iAux].troops;  
       cell.GetComponent<Cell>().info.weapons = editorStage.gridStage[iAux].weapons;  
       cell.GetComponent<Cell>().info.gold = editorStage.gridStage[iAux].gold;  
       cell.GetComponent<Cell>().ShowCell();  
       iAux++;  
     }  
   }  
   public void OpenNewPanel()  
   {  
     GlobalInfo.isModifying = true;  
     newBox.SetActive(true);  
     GameObject inputNumField = GameObject.Find("InputStageNum");  
     inputNumField.GetComponent<InputField>().text = "0";  
   }  
   public void NewStage()  
   {  
     GameObject inputNumField = GameObject.Find("InputStageNum");      
     if (inputNumField.GetComponent<InputField>().text != "0")  
     {  
       editorStage.numStage = int.Parse(inputNumField.GetComponent<InputField>().text);  
       GameObject.Find("Grid").GetComponent<Grid>().Clean();  
       Invoke("UpdateStage", 1.0f);  
       GameObject.Find("StageNumText").GetComponent<Text>().text = editorStage.numStage.ToString();  
       GlobalInfo.editingCell = -1;  
       CloseNewPanel();  
     }  
   }  
   private void UpdateStage()  
   {  
     GameObject.Find("Grid").GetComponent<Grid>().PaintCells();  
     GameObject[] cells = GameObject.FindGameObjectsWithTag("EditorCell");  
     int iAux = 0;  
     foreach (GameObject cell in cells)  
     {  
       editorStage.gridStage[iAux].x = cell.GetComponent<Cell>().info.x;  
       editorStage.gridStage[iAux].y = cell.GetComponent<Cell>().info.y;  
       editorStage.gridStage[iAux].listPos = cell.GetComponent<Cell>().info.listPos;  
       editorStage.gridStage[iAux].type = cell.GetComponent<Cell>().info.type;  
       editorStage.gridStage[iAux].isFinal = cell.GetComponent<Cell>().info.isFinal;  
       editorStage.gridStage[iAux].isObjective = cell.GetComponent<Cell>().info.isObjective;  
       editorStage.gridStage[iAux].isStart = cell.GetComponent<Cell>().info.isStart;  
       editorStage.gridStage[iAux].water = cell.GetComponent<Cell>().info.water;  
       editorStage.gridStage[iAux].food = cell.GetComponent<Cell>().info.food;  
       editorStage.gridStage[iAux].troops = cell.GetComponent<Cell>().info.troops;  
       editorStage.gridStage[iAux].weapons = cell.GetComponent<Cell>().info.weapons;  
       editorStage.gridStage[iAux].gold = cell.GetComponent<Cell>().info.gold;  
       iAux++;  
     }  
   }  
   public void CloseNewPanel()  
   {  
     newBox.SetActive(false);  
     GlobalInfo.isModifying = false;  
   }  
   public void OpenSavePanel()  
   {  
     if (editorStage.numStage > 0)  
     {  
       GlobalInfo.isModifying = true;  
       saveBox.SetActive(true);  
       GameObject inputNameField = GameObject.Find("InputStageName");  
       inputNameField.GetComponent<InputField>().text = editorStage.nameStage;  
       GameObject inputFileNameField = GameObject.Find("InputFileName");  
       inputFileNameField.GetComponent<InputField>().text = editorStage.fileName;  
     }      
   }  
   public void SaveFile()  
   {  
     GameObject inputNameField = GameObject.Find("InputStageName");      
     GameObject inputFileNameField = GameObject.Find("InputFileName");  
     if (inputFileNameField.GetComponent<InputField>().text !="")  
     {  
       editorStage.nameStage = inputNameField.GetComponent<InputField>().text;  
       editorStage.fileName = inputFileNameField.GetComponent<InputField>().text;        
       string fileName = Path.Combine(Application.persistentDataPath, "levels");  
       fileName = Path.Combine(fileName, editorStage.fileName + "." + "json");  
       UpdateEditorStage();  
       DataSaver.saveData(editorStage, fileName, "");  
       ShowFiles(Path.Combine(Application.persistentDataPath, "levels"), "ObjectGrid");  
       CloseSavePanel();  
     }      
   }  
   public void CloseSavePanel()  
   {  
     saveBox.SetActive(false);  
     GlobalInfo.isModifying = false;  
   }  
   public void OpenModifyPanel()  
   {      
     if (GlobalInfo.editingCell >= 0)  
     {  
       GameObject clickedCell = GameObject.Find("Cell" + (GlobalInfo.editingCell + 1).ToString() + "(Clone)");        
       if (clickedCell.GetComponent<Cell>().info.type > 0)  
       {  
         GlobalInfo.isModifying = true;  
         modifyBox.SetActive(true);          
         clickedCell.GetComponent<Cell>().ShowStaticBorder();  
         GameObject inputTroopsField = GameObject.Find("InputTroops");  
         GameObject inputWeaponsField = GameObject.Find("InputWeapons");  
         GameObject inputWaterField = GameObject.Find("InputWater");  
         GameObject inputFoodField = GameObject.Find("InputFood");  
         GameObject inputGoldField = GameObject.Find("InputGold");  
         inputTroopsField.GetComponent<InputField>().text = clickedCell.GetComponent<Cell>().info.troops.ToString();  
         inputWeaponsField.GetComponent<InputField>().text = clickedCell.GetComponent<Cell>().info.weapons.ToString();  
         inputWaterField.GetComponent<InputField>().text = clickedCell.GetComponent<Cell>().info.water.ToString();  
         inputFoodField.GetComponent<InputField>().text = clickedCell.GetComponent<Cell>().info.food.ToString();  
         inputGoldField.GetComponent<InputField>().text = clickedCell.GetComponent<Cell>().info.gold.ToString();  
         GameObject inputFinalField = GameObject.Find("isFinal");  
         GameObject inputObjectiveField = GameObject.Find("isObjective");  
         GameObject inputStartField = GameObject.Find("isStart");  
         inputFinalField.GetComponent<Toggle>().isOn = clickedCell.GetComponent<Cell>().info.isFinal;  
         inputObjectiveField.GetComponent<Toggle>().isOn = clickedCell.GetComponent<Cell>().info.isObjective;  
         inputStartField.GetComponent<Toggle>().isOn = clickedCell.GetComponent<Cell>().info.isStart;  
       }        
     }      
   }  
   public void Modify()  
   {  
     GameObject clickedCell = GameObject.Find("Cell" + (GlobalInfo.editingCell + 1).ToString() + "(Clone)");  
     GameObject inputTroopsField = GameObject.Find("InputTroops");  
     GameObject inputWeaponsField = GameObject.Find("InputWeapons");  
     GameObject inputWaterField = GameObject.Find("InputWater");  
     GameObject inputFoodField = GameObject.Find("InputFood");  
     GameObject inputGoldField = GameObject.Find("InputGold");  
     GameObject inputFinalField = GameObject.Find("isFinal");  
     GameObject inputObjectiveField = GameObject.Find("isObjective");  
     GameObject inputStartField = GameObject.Find("isStart");  
     clickedCell.GetComponent<Cell>().info.troops = int.Parse(inputTroopsField.GetComponent<InputField>().text);  
     clickedCell.GetComponent<Cell>().info.weapons = int.Parse(inputWeaponsField.GetComponent<InputField>().text);  
     clickedCell.GetComponent<Cell>().info.water = int.Parse(inputWaterField.GetComponent<InputField>().text);  
     clickedCell.GetComponent<Cell>().info.food = int.Parse(inputFoodField.GetComponent<InputField>().text);  
     clickedCell.GetComponent<Cell>().info.gold = int.Parse(inputGoldField.GetComponent<InputField>().text);  
     clickedCell.GetComponent<Cell>().info.isFinal = inputFinalField.GetComponent<Toggle>().isOn;  
     clickedCell.GetComponent<Cell>().info.isObjective = inputObjectiveField.GetComponent<Toggle>().isOn;  
     clickedCell.GetComponent<Cell>().info.isStart = inputStartField.GetComponent<Toggle>().isOn;  
     UpdateEditorStage();  
     clickedCell.GetComponent<Cell>().ShowInfo();  
     CloseModifyPanel();  
   }  
   public void CloseModifyPanel()  
   {  
     GameObject clickedCell = GameObject.Find("Cell" + (GlobalInfo.editingCell + 1).ToString() + "(Clone)");  
     clickedCell.GetComponent<Cell>().HideBorder();  
     modifyBox.SetActive(false);  
     GlobalInfo.isModifying = false;  
   }  
   private void UpdateEditorStage()  
   {  
     GameObject[] cells = GameObject.FindGameObjectsWithTag("EditorCell");  
     int iAux = 0;  
     foreach (GameObject cell in cells)  
     {        
       editorStage.gridStage[iAux].type = cell.GetComponent<Cell>().info.type;  
       editorStage.gridStage[iAux].isFinal = cell.GetComponent<Cell>().info.isFinal;  
       editorStage.gridStage[iAux].isObjective = cell.GetComponent<Cell>().info.isObjective;  
       editorStage.gridStage[iAux].isStart = cell.GetComponent<Cell>().info.isStart;  
       editorStage.gridStage[iAux].water = cell.GetComponent<Cell>().info.water;  
       editorStage.gridStage[iAux].food = cell.GetComponent<Cell>().info.food;  
       editorStage.gridStage[iAux].troops = cell.GetComponent<Cell>().info.troops;  
       editorStage.gridStage[iAux].weapons = cell.GetComponent<Cell>().info.weapons;  
       editorStage.gridStage[iAux].gold = cell.GetComponent<Cell>().info.gold;  
       iAux++;  
     }  
   }  
   // Start is called before the first frame update  
   void Start()  
   {  
     editorStage = new Stage();  
     string filePath = Path.Combine(Application.persistentDataPath, "levels");  
     ShowFiles(filePath, "ObjectGrid");  
     editorStage.nameStage = "";  
     editorStage.fileName = "";  
     editorStage.numStage = 0;  
     GlobalInfo.editingCell = -1;  
     DefaultCells cond = new DefaultCells();  
     editorStage.version = cond.version;  
   }  
 }  

Ahora que puedo gestionar las dos vertientes de la información, a través de la clase EditorClickManager, puedo gestionar la UI de usuario y atender cuando hacemos click sobre una celda en los diferentes modos de edición de la información.

 public class EditorClickManager : MonoBehaviour {  
   public Sprite edit;  
   public Sprite noEdit;  
   public Sprite paint;  
   public Sprite noPaint;  
   public GameObject editButton;  
   public GameObject editTile;  
   public GameObject paintButton;  
   public Sprite tundra;  
   public Sprite desert;  
   public Sprite wood;  
   public Sprite town;  
   public Sprite city;  
   public Sprite army;  
   public Sprite crop;  
   public Sprite lake;  
   public Sprite river;  
   public Sprite mine;  
   public Sprite objective;  
   public Sprite mountain;  
   public Sprite empty;  
   private bool paintFistTime;  
   public void Start()  
   {  
     GlobalInfo.isEditing = false;  
     GlobalInfo.isPainting = false;  
     paintFistTime = true;  
   }  
   public void onClickEditor()  
   {  
     GlobalInfo.isEditing = !(bool)GlobalInfo.isEditing;  
     if (GlobalInfo.isEditing == true)  
     {  
       GlobalInfo.isPainting = false;  
       paintButton.GetComponent<Image>().sprite = noPaint;  
       editButton.GetComponent<Image>().sprite = edit;  
       if (paintFistTime)  
       {  
         GlobalInfo.paintCellType = 0;          
       }  
       editTile.GetComponent<Image>().sprite = TypeSprite(GlobalInfo.paintCellType);  
       editTile.SetActive(true);  
     } else  
     {  
       editButton.GetComponent<Image>().sprite = noEdit;  
       editTile.SetActive(false);  
     }  
   }  
   public void onClickPainter()  
   {  
     GlobalInfo.isPainting = !(bool)GlobalInfo.isPainting;  
     if (GlobalInfo.isPainting == true)  
     {  
       GlobalInfo.isEditing = false;  
       editButton.GetComponent<Image>().sprite = noEdit;  
       paintButton.GetComponent<Image>().sprite = paint;  
       if (paintFistTime)  
       {  
         GlobalInfo.paintCellType = 0;  
       }  
       editTile.GetComponent<Image>().sprite = TypeSprite(GlobalInfo.paintCellType);  
       editTile.SetActive(true);  
     }  
     else  
     {  
       paintButton.GetComponent<Image>().sprite = noPaint;  
       editTile.SetActive(false);  
     }  
   }  
   public void onClickTundra()  
   {  
     if (GlobalInfo.isEditing || GlobalInfo.isPainting)  
     {  
       GlobalInfo.paintCellType = 1;  
       editTile.GetComponent<Image>().sprite = TypeSprite(GlobalInfo.paintCellType);  
     }      
   }  
   ...  
   public Sprite TypeSprite(int num)  
   {  
     if (num == 1) { return tundra; }  
     if (num == 2) { return desert; }  
     if (num == 3) { return wood; }  
     if (num == 4) { return town; }  
     if (num == 5) { return city; }  
     if (num == 6) { return army; }  
     if (num == 7) { return crop; }  
     if (num == 8) { return lake; }  
     if (num == 9) { return river; }  
     if (num == 10) { return mine; }  
     if (num == 11) { return objective; }  
     if (num == 12) { return mountain; }  
     return empty;  
   }  
   void Update()  
   {  
     if (Input.GetMouseButtonDown(0))  
     {  
       Vector3 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);  
       Vector2 mousePos2D = new Vector2(mousePos.x, mousePos.y);  
       RaycastHit2D hit = Physics2D.Raycast(mousePos2D, Vector2.zero);  
       if (hit.collider != null)  
       {  
         if (hit.collider.gameObject.tag == "EditorCell" && GlobalInfo.isModifying == false)  
         {  
           GameObject.Find(hit.collider.gameObject.name).GetComponent<Cell>().ImClicked();            
         }          
       }  
     }  
   }  
 }  

Editor de niveles (Parte 2)

En esta segunda parte del editor de niveles nos centraremos en la creación de las estructuras internas parar poder definir correctamente un nivel y la generación del archivo base a partir de un nivel con datos por defecto.


El objetivo es poder pulsar al botón de añadir nivel, que nos pregunte el número de etapa y genere la cuadrícula con una clase adjuntada en cada celda con la información interna que procesa, para que posteriormente poder editar las casillas y modificar los datos en esta estructura dinámicamente.


Una vez generado el nivel a través de la grid visual e interna en memoria, debemos poder salvar la información en un fichero para poder comprobar que estamos guardando correctamente la información y posteriormente mostrar y actualizar los ficheros encontrados en la carpeta de etapas a través de la lista que creamos en el primera parte del tutorial.



Clase Stage con el contructor y las variables de ficheros:
 using System.Collections.Generic;  
 using UnityEngine;  
 using System;  
 using System.Text;  
 [Serializable]  
 public class StageCell  
 {  
   public int x;  
   public int y;  
   public int listPos;  
   public int type;  
   public bool isFinal;  
   public bool isObjective;  
   public int water;  
   public int food;  
   public int troops;  
   public int weapons;  
   public int gold;  
 }  
 [Serializable]  
 public class Stage  
 {  
   public int version;  
   public int numStage;  
   public string nameStage;  
   public string fileName;  
   public List<StageCell> gridStage = new List<StageCell>();  
   public Stage()  
   {      
     DefaultCells cond = new DefaultCells();  
     version = cond.version;  
     numStage = 0;  
     nameStage = "";  
     fileName = "";      
     for (int i = 0; i < 64; i++)  
     {  
       StageCell defaultCell = new StageCell();  
       defaultCell.x = 0;  
       defaultCell.y = 0;  
       defaultCell.listPos = 0;  
       defaultCell.type = cond.type;  
       defaultCell.isFinal = false;  
       defaultCell.isObjective = false;  
       defaultCell.water = cond.water;  
       defaultCell.food = cond.food;  
       defaultCell.troops = cond.troops;  
       defaultCell.weapons = cond.weapons;  
       defaultCell.gold = cond.gold;  
       gridStage.Add(defaultCell);  
     }  
   }  
 }  
 public class Levels : MonoBehaviour  
 {  
   public static void LoadLevel(int levelNum)  
   {  
     //load info from levels folder      
     TextAsset jsonTextFile = Resources.Load<TextAsset>("Levels/Attila" + levelNum.ToString());  
     object resultValue = JsonUtility.FromJson<Stage>(Encoding.ASCII.GetString(jsonTextFile.bytes));  
     Stage loadlevel = (Stage)Convert.ChangeType(resultValue, typeof(Stage));  
     GlobalInfo.stageVersion = loadlevel.version;   
     GlobalInfo.actualStage = loadlevel.numStage;  
     GlobalInfo.gridStage = loadlevel.gridStage;  
   }  
 }  

Hay que tener en cuenta que el sistema de ficheros no funciona en sistemas Android, ya que el sistema de protección del sistema impide acceder libremente en todas las carpetas del SO. Así mismo el editor esta pensado parar ser ejecutado en ordenador, diseñar las etapas y pasar el contenido a la carpeta Resouces donde el juego accederá a los niveles.

Clase Editor:
 using System.Collections;  
 using System.Collections.Generic;  
 using UnityEngine;  
 using UnityEngine.UI;  
 using System.IO;  
 public class DefaultCells  
 {  
   public int version;  
   //Game  
   public int type;  
   public int water;  
   public int food;  
   public int troops;  
   public int weapons;  
   public int gold;  
   public DefaultCells()  
   {      
     IsEmpy();  
   }  
   public void IsTundra()  
   {  
     version = 1;  
     type = 1;  
     water = 0;  
     food = 5;  
     troops = 100;  
     weapons = 50;  
     gold = 0;  
   }  
   public void IsDessert()  
   {  
     version = 1;  
     type = 2;  
     water = 0;  
     food = 0;  
     troops = -100;  
     weapons = 0;  
     gold = 0;  
   }  
   public void IsWood()  
   {  
     version = 1;  
     type = 3;  
     water = 5;  
     food = 10;  
     troops = 0;  
     weapons = 5;  
     gold = 0;  
   }  
   public void IsTown()  
   {  
     version = 1;  
     type = 4;  
     water = 10;  
     food = 50;  
     troops = 50;  
     weapons = 50;  
     gold = 50;  
   }  
   public void IsCity()  
   {  
     version = 1;  
     type = 5;  
     water = 100;  
     food = 500;  
     troops = 800;  
     weapons = 100;  
     gold = 500;  
   }  
   public void IsArmy()  
   {  
     version = 1;  
     type = 6;  
     water = 0;  
     food = 50;  
     troops = 1000;  
     weapons = 500;  
     gold = 800;  
   }  
   public void IsCrop()  
   {  
     version = 1;  
     type = 7;  
     water = 5;  
     food = 500;  
     troops = 10;  
     weapons = 5;  
     gold = 5;  
   }  
   public void IsLake()  
   {  
     version = 1;  
     type = 8;  
     water = 500;  
     food = 10;  
     troops = 0;  
     weapons = 0;  
     gold = 5;  
   }  
   public void IsRiver()  
   {  
     version = 1;  
     type = 9;  
     water = 500;  
     food = 10;  
     troops = 0;  
     weapons = 0;  
     gold = 5;  
   }  
   public void IsMine()  
   {  
     version = 1;  
     type = 10;  
     water = 5;  
     food = 5;  
     troops = 20;  
     weapons = 20;  
     gold = 500;  
   }  
   public void IsObjective()  
   {  
     version = 1;  
     type = 11;  
     water = 50;  
     food = 50;  
     troops = 50;  
     weapons = 50;  
     gold = 50;  
   }  
   public void IsMountain()  
   {  
     version = 1;  
     type = 12;  
     water = 50;  
     food = 50;  
     troops = -50;  
     weapons = 5;  
     gold = 10;  
   }  
   public void IsEmpy()  
   {  
     version = 1;  
     type = 0;  
     water = 0;  
     food = 0;  
     troops = 0;  
     weapons = 0;  
     gold = 0;  
   }  
 }  
 public class Editor : MonoBehaviour  
 {  
   public GameObject saveBox;  
   public GameObject newBox;  
   public GameObject fileInfoItem;  
   public GameObject editTile;  
   private Stage editorStage;  
   private void ShowFiles(string filePath, string where)  
   {  
     DirectoryInfo dir = new DirectoryInfo(filePath);  
     FileInfo[] info = dir.GetFiles("*.json");  
     GameObject itemsParent = GameObject.Find(where);  
     foreach (Transform child in itemsParent.transform)  
     {  
       Destroy(child.gameObject);  
     }  
     //Find files  
     foreach (FileInfo f in info)  
     {  
       Debug.Log(f.ToString());  
       //Read File info  
       Stage stage = new Stage();  
       stage = DataSaver.loadData<Stage>(f.ToString(),"");  
       //Edit Prefab before Instantiate  
       Transform levelTitle = fileInfoItem.GetComponentInChildren<Transform>().Find("Name");  
       levelTitle.GetComponent<Text>().text = stage.nameStage;  
       Transform levelNum = fileInfoItem.GetComponentInChildren<Transform>().Find("Num");  
       levelNum.GetComponent<Text>().text = stage.numStage.ToString();  
       Transform levelFilename = fileInfoItem.GetComponentInChildren<Transform>().Find("File");  
       levelFilename.GetComponent<Text>().text = Path.GetFileName(f.ToString());  
       Instantiate(fileInfoItem, new Vector3(0, 0, 0), Quaternion.identity, itemsParent.transform);        
     }  
   }  
   public void OpenNewPanel()  
   {  
     newBox.SetActive(true);  
     GameObject inputNumField = GameObject.Find("InputStageNum");  
     inputNumField.GetComponent<InputField>().text = "0";  
   }  
   public void NewStage()  
   {  
     GameObject inputNumField = GameObject.Find("InputStageNum");      
     if (inputNumField.GetComponent<InputField>().text != "0")  
     {  
       editorStage.numStage = int.Parse(inputNumField.GetComponent<InputField>().text);  
       GameObject.Find("Grid").GetComponent<Grid>().PaintCells();  
       GameObject[] cells = GameObject.FindGameObjectsWithTag("EditorCell");  
       int iAux = 0;        
       foreach (GameObject cell in cells)  
       {          
         editorStage.gridStage[iAux].x = cell.GetComponent<Cell>().info.x;  
         editorStage.gridStage[iAux].y = cell.GetComponent<Cell>().info.y;  
         editorStage.gridStage[iAux].listPos = cell.GetComponent<Cell>().info.listPos;  
         editorStage.gridStage[iAux].type = cell.GetComponent<Cell>().info.type;  
         editorStage.gridStage[iAux].isFinal = cell.GetComponent<Cell>().info.isFinal;  
         editorStage.gridStage[iAux].isObjective = cell.GetComponent<Cell>().info.isObjective;  
         editorStage.gridStage[iAux].water = cell.GetComponent<Cell>().info.water;  
         editorStage.gridStage[iAux].food = cell.GetComponent<Cell>().info.food;  
         editorStage.gridStage[iAux].troops = cell.GetComponent<Cell>().info.troops;  
         editorStage.gridStage[iAux].weapons = cell.GetComponent<Cell>().info.weapons;  
         editorStage.gridStage[iAux].gold = cell.GetComponent<Cell>().info.gold;  
         iAux++;  
       }  
       GameObject.Find("StageNumText").GetComponent<Text>().text = editorStage.numStage.ToString();  
       CloseNewPanel();  
     }  
   }  
   public void CloseNewPanel()  
   {  
     newBox.SetActive(false);  
   }  
   public void OpenSavePanel()  
   {  
     if (editorStage.numStage > 0)  
     {  
       saveBox.SetActive(true);  
       GameObject inputNameField = GameObject.Find("InputStageName");  
       inputNameField.GetComponent<InputField>().text = editorStage.nameStage;  
       GameObject inputFileNameField = GameObject.Find("InputFileName");  
       inputFileNameField.GetComponent<InputField>().text = editorStage.fileName;  
     }      
   }  
   public void SaveFile()  
   {  
     GameObject inputNameField = GameObject.Find("InputStageName");      
     GameObject inputFileNameField = GameObject.Find("InputFileName");  
     if (inputFileNameField.GetComponent<InputField>().text !="")  
     {  
       editorStage.nameStage = inputNameField.GetComponent<InputField>().text;  
       editorStage.fileName = inputFileNameField.GetComponent<InputField>().text;        
       string fileName = Path.Combine(Application.persistentDataPath, "levels");  
       fileName = Path.Combine(fileName, editorStage.fileName + "." + "json");        
       DataSaver.saveData(editorStage, fileName, "");  
       ShowFiles(Path.Combine(Application.persistentDataPath, "levels"), "ObjectGrid");  
       CloseSavePanel();  
     }      
   }  
   public void CloseSavePanel()  
   {  
     saveBox.SetActive(false);  
   }  
   // Start is called before the first frame update  
   void Start()  
   {  
     editorStage = new Stage();  
     string filePath = Path.Combine(Application.persistentDataPath, "levels");  
     ShowFiles(filePath, "ObjectGrid");  
     editorStage.nameStage = "";  
     editorStage.fileName = "";  
     editorStage.numStage = 0;  
     DefaultCells cond = new DefaultCells();  
     editorStage.version = cond.version;  
   }  
 }  

PixelPerfect en Unity 2D

Si os poneis manos a la obra en haver un proyecto con estética de los años 80 del siglo pasado lo mas probable es que opteis por una de las...