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;  
   }  
 }  

Editor de niveles (Parte 1)

Hoy empezaremos con el editor de niveles. Este editor nos permitirá crear las etapas del juego a partir de ficheros JSON guardados en la carpeta de recursos. Este editor nos permitirá generar información parar poder proseguir con el juego donde lo habíamos dejado.


En este artículo sólo me centraré en la creación de la pantalla visual del editor, poniendo atención en la creación automática de cuadrículas (grids) a partir de una rutina pasando el objeto casilla como un prefab de la aplicación y definiendo el tamaño de cuadrícula con un Box collider.



Podéis ver el vídeo del proceso aquí.


Gizmo del Box Collider:
 using System.Collections;  
 using System.Collections.Generic;  
 using UnityEngine;  
 public class ShowColliders : MonoBehaviour {  
   private BoxCollider2D objectCollider2D;  
   private void OnDrawGizmos()  
   {  
     objectCollider2D = GetComponent<BoxCollider2D>();  
     Gizmos.color = Color.green;  
     Gizmos.DrawWireCube(objectCollider2D.bounds.center, new Vector2(objectCollider2D.size.x, objectCollider2D.size.y));  
   }  
 }  

Grid dinámico:
 using UnityEngine;  
 public class Grid : MonoBehaviour  
 {  
   //Grid  
   public int rows;  
   public int cols;    
   public Vector2 gridOffset;  
   public GameObject cell;  
   private BoxCollider2D gridSize;  
   private Vector2 originalCellSize;  
   private Vector2 cellScale;  
   private Vector2 cellSize;  
   private Vector2 offset;  
   // Start is called before the first frame update  
   void Start()  
   {  
     PaintCells();  
   }  
   public void Clean()  
   {  
     foreach (Transform child in gameObject.transform)  
     {  
       Destroy(child.gameObject);  
     }  
   }  
   public void PaintCells()  
   {  
     gridSize = GetComponent<BoxCollider2D>();  
     originalCellSize = cell.GetComponent<SpriteRenderer>().sprite.bounds.size;  
     cellSize = new Vector2(gridSize.size.x / (float)cols, gridSize.size.y / (float)rows);  
     cellScale.x = cellSize.x / (originalCellSize.x + gridOffset.x);  
     cellScale.y = cellSize.y / (originalCellSize.y + gridOffset.y);  
     cell.transform.localScale = new Vector2(cellScale.x, cellScale.y);  
     offset.x = gridSize.offset.x - ((gridSize.size.x) / 2 ) + cellSize.x / 2;  
     offset.y = gridSize.offset.y - ((gridSize.size.y) / 2 ) + cellSize.y / 2;  
     int sufixName = 0;  
     for (int row = 0; row < rows; row++)  
     {  
       for (int col = 0; col < cols; col++)  
       {  
         sufixName++;  
         //add the cell size so that no two cells will have the same x and y position  
         Vector2 pos = new Vector2(col * cellSize.x + transform.position.x + offset.x, row * cellSize.y + transform.position.y + offset.y);  
         //Add Cell information  
         cell.name = "Cell" + sufixName.ToString();  
         //instantiate the game object, at position pos, with rotation set to identity  
         GameObject cO = Instantiate(cell, pos, Quaternion.identity) as GameObject;  
         //set the parent of the cell to GRID so you can move the cells together with the grid;  
         cO.transform.parent = transform;  
       }  
     }      
   }  
 }  

El resto del editor sólo se utilizan elementos de la UI parar definir la interfaz de trabajo.

Tipos Abstractos de Datos (TAD) para el juego

Antes de seguir con la parte visual del juego necesito 2 elementos básicos que todo juego necesita. Por un lado tenemos los tipos abstractos de datos (TAD) que guardaran en sus estructuras internas la evolución de la partida. Una vez definidas estas estructuras necesitaremos un editor de niveles para implementar estas estructuras con información para poder trabajar con ella.


En nuestros caso, para Attila, tierra quemada necesitamos dos elementos claves: por un lado la información de nuestro ejército, que aunque visual estará representada por un caballo contienen un conjunto de variables que configuran su estado de salud durante el proceso del juego.

   public int water;  
   public int food;  
   public int troops;  
   public int weapons;  
   public int gold;  

Por otro lado necesitamos saber cómo está compuesto el tablero o sea, el nivel de juego que estamos jugando en este momento. Cada etapa estará compuesto de un tablero de 8x8 casillas que pueden estar llenas o no, donde cada una tendrá unos atributos y eventos asociados cuando interactúan con el jugador.

Las casillas de este tablero tendrán unas variables que definirán estos aspectos: 

   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;  


Esta información estará guardada en una lista de 100 posiciones donde los elementos serán objetos de la clase serializada StageCell.


Por otro lado podríamos trabajar con un Array de estas dimensiones para el mismo propósito, pero mucho más complejo de serializar para Unity. 


Por otro lado debemos tener claro donde guardar la información de cada elemento. Por un lado la información del jugador estará guardada en el archivo general de configuración. Así mismo los niveles serán archivos independientes que accederemos a ellos a través de la carpeta Resources del proyecto para que puedan ser copiados externamente cuando generemos el fichero APK para Android.

 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 List<StageCell> gridStage;  
 }  
 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;  
   }  
 }  

Para ellos tenemos implementada una rutina en la clase Levels que lee un fichero pasado por parámetro de la carpeta Resources y traslada la información del nivel actual al fichero de variables globales para poder acceder a él a través de cualquier parte del proyecto.

Ahora ya sólo resta modificarlas rutinas de acceso a la configuración en la escena del Título parar que lea esta información, actualice el fichero de variables globales i lea el nivel actual de juego.

 using UnityEngine;  
 using System.IO;  
 using System;  
 public class LoadConfig : MonoBehaviour {  
   private string configFileName;  
   private string highScoreFileName;  
   private string fileName;  
      // Use this for initialization  
      void Awake ()  
   {  
     //Configuration  
     configFileName = GlobalInfo.configFile;  
     fileName = Path.Combine(Application.persistentDataPath, "data");  
     fileName = Path.Combine(fileName, configFileName + ".txt");  
     if (!File.Exists(fileName))  
     {  
       PlayerInfo saveData = new PlayerInfo();  
       saveData.gameDateFirstTime = DateTime.Now.ToBinary().ToString();  
       saveData.playDateFirstTime = "";  
       IntialConditions cond = new IntialConditions();  
       saveData.maxStagesGame = cond.maxStagesGame;  
       saveData.actualStage = cond.actualStage;  
       saveData.maxStageCompleted = cond.maxStageCompleted;  
       saveData.score = cond.score;  
       saveData.water = cond.water;  
       saveData.food = cond.food;  
       saveData.troops = cond.troops;  
       saveData.weapons = cond.weapons;  
       saveData.gold = cond.gold;  
       //Save data from PlayerInfo to a file named players  
       DataSaver.saveData(saveData, configFileName);  
       GlobalInfo.gameFirstTime = true;  
       GlobalInfo.playFirstTime = true;  
       GlobalInfo.language = saveData.language;  
       GlobalInfo.soundPlay = true;  
       GlobalInfo.sessionsCount = 0;  
       GlobalInfo.stagesCount = 0;  
       GlobalInfo.maxStageCompleted = 0;  
       //Initial conditions        
       GlobalInfo.maxStagesGame = cond.maxStagesGame;  
       GlobalInfo.actualStage = cond.actualStage;  
       GlobalInfo.score = cond.score;  
       GlobalInfo.water = cond.water;  
       GlobalInfo.food = cond.food;  
       GlobalInfo.troops = cond.troops;  
       GlobalInfo.weapons = cond.weapons;  
       GlobalInfo.gold = cond.gold;  
     } else  
     {  
       PlayerInfo loadedData = DataSaver.loadData<PlayerInfo>(configFileName);  
       if (loadedData == null)  
       {  
         return;  
       }  
       GlobalInfo.gameFirstTime = false;  
       if (loadedData.playDateFirstTime != "")  
       {  
         GlobalInfo.playFirstTime = false;  
       } else  
       {  
         GlobalInfo.playFirstTime = true;  
       }  
       GlobalInfo.language = loadedData.language;  
       GlobalInfo.soundPlay = loadedData.soundPlay;  
       GlobalInfo.sessionsCount = loadedData.sessionsCount;  
       GlobalInfo.stagesCount = 0;  
       GlobalInfo.maxStagesGame = loadedData.maxStagesGame;  
       GlobalInfo.maxStageCompleted = loadedData.maxStageCompleted;  
       GlobalInfo.actualStage = loadedData.actualStage;  
       GlobalInfo.score = loadedData.score;  
       GlobalInfo.water = loadedData.water;  
       GlobalInfo.food = loadedData.food;  
       GlobalInfo.troops = loadedData.troops;  
       GlobalInfo.weapons = loadedData.weapons;  
       GlobalInfo.gold = loadedData.gold;  
     }  
   }  
 }  

Este punto nos plantea el siguiente reto del proyecto. Necesitamos un editor de información para crear de una manera fácil de información de cada etapa y así poder crear, corregir o ajustar el proceso y la dificultad del juego en cada una de sus etapas. Esto será en el próximo artículo.

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 aquell...