Aplicación base para móviles

En esta entrada del bloc voy a explicar cómo hacer una base para un proyecto móvil que nos sirva para futuros proyectos. En concreto yo utilizo esta base en Unity para hacer juegos móviles para ahorrarme algo de tiempo y centrarme en el juego.


En concreto el proyecto consiste en 3 escenas que definen la pantalla de Splash, el título del juego y el menú principal. Evidentemente esto sólo es una base, en el titulo podemos poner más cosas: efectos, animaciones, etc. Pero esto ya será en una segunda fase. Ahora solo necesitamos una estructura que funcionen bien en dispositivos móviles y que nos cargue el fichero de configuración del juego y actualice las variables globales del proyecto.



Clase de los datos de configuración:
 using System.Collections.Generic;  
 using System;  
 [Serializable]  
 public class PlayerInfo  
 {  
   public int version = 1;  
   public string gameDateFirstTime;  
   public string playDateFirstTime;  
   public int sessionsCount = 0;  
   public string language = "en";  
   public bool soundPlay = true;    
 }  

Variables globales actuales:
 using UnityEngine;  
 public class GlobalInfo : MonoBehaviour  
 {  
   //General Config  
   public static string configFile = "AttilaCfg";    
   public static bool gameFirstTime;  
   public static bool playFirstTime;  
   public static int sessionsCount;  
   public static int gamesCount;  
   public static string language;  
   public static bool soundPlay = true;  
   //Game  
 }  

Carga de datos:
 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 = "";  
       //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.gamesCount = 0;  
     } 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.gamesCount = 0;  
     }  
   }  
 }  

Salvar, leer y convertir datos en disco:
 using UnityEngine;  
 using System;  
 using System.IO;  
 using System.Text;  
 public class DataSaver  
 {  
   //Save Data  
   public static void saveData<T>(T dataToSave, string dataFileName)  
   {  
     string tempPath = Path.Combine(Application.persistentDataPath, "data");  
     tempPath = Path.Combine(tempPath, dataFileName + ".txt");  
     //Convert To Json then to bytes  
     string jsonData = JsonUtility.ToJson(dataToSave, true);  
     byte[] jsonByte = Encoding.ASCII.GetBytes(jsonData);  
     //Create Directory if it does not exist  
     if (!Directory.Exists(Path.GetDirectoryName(tempPath)))  
     {  
       Directory.CreateDirectory(Path.GetDirectoryName(tempPath));  
     }  
     try  
     {  
       File.WriteAllBytes(tempPath, jsonByte);  
       Debug.Log("Saved Data to: " + tempPath.Replace("/", "\\"));  
     }  
     catch (Exception e)  
     {  
       Debug.LogWarning("Failed To PlayerInfo Data to: " + tempPath.Replace("/", "\\"));  
       Debug.LogWarning("Error: " + e.Message);  
     }  
   }  
   //Load Data  
   public static T loadData<T>(string dataFileName)  
   {  
     string tempPath = Path.Combine(Application.persistentDataPath, "data");  
     tempPath = Path.Combine(tempPath, dataFileName + ".txt");  
     //Exit if Directory or File does not exist  
     if (!Directory.Exists(Path.GetDirectoryName(tempPath)))  
     {  
       Debug.LogWarning("Directory does not exist");  
       return default(T);  
     }  
     if (!File.Exists(tempPath))  
     {  
       Debug.Log("File does not exist");  
       return default(T);  
     }  
     //Load saved Json  
     byte[] jsonByte = null;  
     try  
     {  
       jsonByte = File.ReadAllBytes(tempPath);  
       Debug.Log("Loaded Data from: " + tempPath.Replace("/", "\\"));  
     }  
     catch (Exception e)  
     {  
       Debug.LogWarning("Failed To Load Data from: " + tempPath.Replace("/", "\\"));  
       Debug.LogWarning("Error: " + e.Message);  
     }  
     //Convert to json string  
     string jsonData = Encoding.ASCII.GetString(jsonByte);  
     //Convert to Object  
     object resultValue = JsonUtility.FromJson<T>(jsonData);  
     return (T)Convert.ChangeType(resultValue, typeof(T));  
   }  
   public static bool deleteData(string dataFileName)  
   {  
     bool success = false;  
     //Load Data  
     string tempPath = Path.Combine(Application.persistentDataPath, "data");  
     tempPath = Path.Combine(tempPath, dataFileName + ".txt");  
     //Exit if Directory or File does not exist  
     if (!Directory.Exists(Path.GetDirectoryName(tempPath)))  
     {  
       Debug.LogWarning("Directory does not exist");  
       return false;  
     }  
     if (!File.Exists(tempPath))  
     {  
       Debug.Log("File does not exist");  
       return false;  
     }  
     try  
     {  
       File.Delete(tempPath);  
       Debug.Log("Data deleted from: " + tempPath.Replace("/", "\\"));  
       success = true;  
     }  
     catch (Exception e)  
     {  
       Debug.LogWarning("Failed To Delete Data: " + e.Message);  
     }  
     return success;  
   }  
 }  

La secuencia básica consiste en:
  1. Carga de la escena Splash
  2. Carga de la escena Título
  3. Carga del fichero de configuración
  4. Actualización de las variables globales
  5. Carga la escena del Menú principal

Vídeo completo del proceso:


Para hacer la carga entre escenas utilizo una función más una animación parar crear un efecto Fade entre ellas, tanto en la entrada como en la salida y en la escena del título un slider para cargar la escena del juego en segundo plano para mostrar la evolución en el slider.

Carga asíncrona de escenas:
 using System.Collections;  
 using UnityEngine;  
 using UnityEngine.UI;  
 using UnityEngine.SceneManagement;  
 public class LoadingBar : MonoBehaviour {  
   private bool loadScene = false;  
   public string loadingSceneName;  
   public float waitSecondsPrev;  
   public float waitSecondsPost;  
   public Slider sliderBar;  
   // Use this for initialization  
      void Start ()  
   {  
     sliderBar.gameObject.SetActive(false);  
     Invoke("Loading", waitSecondsPrev);  
   }  
      // Update is called once per frame  
      void Loading()  
   {  
        if(loadScene == false)  
     {  
       loadScene = true;  
       sliderBar.gameObject.SetActive(true);  
       StartCoroutine("LoadNewScene");  
     }  
      }  
   IEnumerator LoadNewScene()  
   {  
     yield return new WaitForSeconds(waitSecondsPost);  
     AsyncOperation async = SceneManager.LoadSceneAsync(loadingSceneName);  
     while (!async.isDone)  
     {  
       float progress = Mathf.Clamp01(async.progress / 0.9f);  
       sliderBar.value = progress;  
       yield return null;  
     }  
   }  
 }  


Icono y logotipo de Attila, Tierra quemada

Antes de empezar con el proyecto de Unity, personalmente me gusta indagar un poco y buscar que línea gráfica quiero utilizar en el juego. Es una manera que impregnarme de ejemplos, estilos y encontrar en ellos la inspiración para empezar a crear el juego. Para ello hoy me he dedicado a probar aplicaciones, mirar algún que otro vídeo histórico y mirar en web de referencia que utilizo para los elementos gráficos buscando que podía ayudarme a crear el icono y el logotipo de la aplicación.



Para ello yo la web que utilizo es Freepik, una de los muchos bancos gráficos que hay por Internet. Personalmente no me gusta utilizar bancos de imágenes muy conocidos ya que sino al final todos los que no sabemos dibujar terminamos utilizando los mismas imágenes, cosa que quita un poco de originalidad a la parte gráfica del juego.



Una vez tengo claro la composición que quiero hacer, descargo las imágenes y con la ayuda de Adobe Illustrator separo los elementos que necesito para hacer una nueva composición. Yo acostumbro a apoyarme en Adobe Animate por si necesito hacer alguna pequeña animación  y posteriormente exporto el resultado en el tamaño deseado para utilizarlo en Unity.

Podéis ver todo el proceso en el vídeo:



En concreto al ser una aplicación para dispositivos móviles voy a crear un logotipo de unos 900 pixeles de ancho y un icono de 512 x 512 pixeles que será genérico para el juego móvil. Evidentemente esto es sólo la primera aproximación; una puerta a la inspiración que poco a poco iremos modelando hasta que el juego transmita la imagen adecuada para el tipo de juego que estamos haciendo.


GitHub y Unity

Para el proyecto que estoy desarrollando voy a utilizar GitHub como repositorio de archivos y versiones. Aunque muchas veces uso el propio sistema de Unity, por el motivo que sea también vamos a utilizar un sistema general ampliamente utilizado por los desarrolladores de software.


En primer lugar debemos tener una cuenta GitHub que nos podemos hacer gratuitamente. Una vez hecha vamos a utilizar un programa de gestión de versiones como es Sourcetree para gestionar el flujo de las versiones, ramas, etc. de nuestro juego.

Una vez tenemos la cuenta de GitJub, si queremos utilizar SourcreTree necesitas una cuenta de Atlassian Bitbucket. Sin no disponemos de ella la podemos crear en el momento de la instalación o previamente a través de su página web.


A partir de aquí solo tenemos que ejecutar Sourcetree y enlazar las cuentas. En el vídeo tenéis todo el proceso completo. Una vez tenemos en enlace hecho debemos enlazar con la cuenta de GitHub con la opción de Edit account. Aquí diremos que nuestro hostint es GitHub y nos autenticaremos para poder gestionar los proyectos de Git a través de Sourcetree.




Si se trata de un proyecto nuevo, como es el caso, crearemos (+) un proyecto nuevo. Decimos cual es nuestra carpeta local (bebe estar vacía) y le pedimos que cree un repositorio en la cuenta de GitHub.
Una vez creada la cuenta deberíamos poner un fichero de exclusión de ficheros ya que Unity crea muchos archivos temporales que no necesitamos sincronizar con la cuenta de GitHub. Para ello podemos utilizar nuestro propio fichero o utilizar el que podemos encontrar en el proyecto de GitHub de ficheros de exclusión:


Este archivo lo copiaremos en la carpeta raíz de nuestro proyecto. 

 # This .gitignore file should be placed at the root of your Unity project directory  
 /[Ll]ibrary/  
 /[Tt]emp/  
 /[Oo]bj/  
 /[Bb]uild/  
 /[Bb]uilds/  
 /[Ll]ogs/  
 # Never ignore Asset meta data  
 !/[Aa]ssets/**/*.meta  
 # Uncomment this line if you wish to ignore the asset store tools plugin  
 # /[Aa]ssets/AssetStoreTools*  
 # TextMesh Pro files  
 [Aa]ssets/TextMesh*Pro/  
 # Visual Studio cache directory  
 .vs/  
 # Gradle cache directory  
 .gradle/  
 # Autogenerated VS/MD/Consulo solution and project files  
 ExportedObj/  
 .consulo/  
 *.csproj  
 *.unityproj  
 *.sln  
 *.suo  
 *.tmp  
 *.user  
 *.userprefs  
 *.pidb  
 *.booproj  
 *.svd  
 *.pdb  
 *.mdb  
 *.opendb  
 *.VC.db  
 # Unity3D generated meta files  
 *.pidb.meta  
 *.pdb.meta  
 *.mdb.meta  
 # Unity3D generated file on crash reports  
 sysinfo.txt  
 # Builds  
 *.apk  
 *.unitypackage  
 # Crashlytics generated file  
 crashlytics-build.properties  

Ahora ya estas preparados para ir a Unity y crear el proyecto en blanco para empezar a trabajar. Una vez abierto el proyecto de Unity, debemos configurar en Edit > Project Settings para que no guarde archivos en formato binario que no podríamos sincronizar con GitHub.


Finalmente podemos hacer la primera sincronización con GitHub a través de un Stage All y un  Commit en Sourcetree para tener un copia del proyecto en la nube.


Attila, Tierra quemada

Después de terminar el proyecto Numbers Swap, hoy empiezo un nuevo proyecto que me gustaría ir explicando paso a paso desde la idea de proyecto hasta su publicación. Creo que a veces para las personas que empiezan es difícil ver la magnitud que tiene un proyecto ya que nos focalizamos en las partes que más nos gustan o nos interesan.



Attila, Tierra quemada es un juego de estrategia basado en el movimiento del caballo del ajedrez. En mi época de estudiante jugaba a este juego de lógica con una hoja cuadriculada y un bolígrafo. Hacía un cuadrado de 8x8 y llenaba los cuadritos interiores con números haciendo el salto del caballo del ajedrez.

El problema del caballo ha despertado el interés de muchos matemáticos a lo largo de la historia. Uno de los más famosos que halló solución al enigma fue el mejor matemático del siglo XVIII, el suizo Leonhart Euler. 

Este problema matemático consiste en recorrer un tablero de ajedrez recorriendo todas las casillas utilizando un caballo de ajedrez sin poder repetir ninguna. Podemos empezar por la casilla que queramos, pero debemos pasar por todas las casillas del tablero.


A partir de este problema querría desarrollar un juego de estrategia por niveles donde a partir de un punto inicial debemos recorrer las máximas casillas posibles antes de quedarnos atascados o llenar todo el tablero. Cada avance nos reportará puntos que iremos acumulando. 

A partir de un nivel encontraremos elementos/casillas con propiedades particulares que modificaran de alguna manera la mecánica del juego (ciudades, ríos, montañas, suministros, …) permitiendo ganar más puntos, eliminar/poner casillas,  deshacer movimientos, etc. Además no todos los tableros serán de 8x8 y rectangulares. Podremos encontrar tableros con otras formas y tamaños que nos permitan ajustar la dificultad del juego en función de la etapa.


A través de un conjunto de niveles con dificultad progresiva iremos avanzando por la historia de conquista de los ejércitos de Atila, has llegar a Roma si conseguimos terminar el juego con un nivel final que sería el desafío de Euler con la opción de recrear el reto original del reto del caballo.

Atila, rey los hunos, llevo su ejército de jinetes desde la estepa rusa hasta las puertas de Roma y Constantinopla. En el juego querría representar el movimiento del ejército huno a través de la Europa oriental proponiéndole al jugador objetivos en cada etapa que condicionen el juego en etapas posteriores bajo la premisa que nunca podrá pasar dos veces por la misma casilla. 

Parte de la historia de Atila es desconocida por los historiadores ya que sólo han llegado a la actualidad básicamente textos romanos de la época, con lo cual tenemos mucha libertad creativa para construir la historia como queramos.

El juego busca por un lado el contraste ente una mecánica muy simple para un juego de estrategia (el caballo del ajedrez y la no repetición de casillas) y un trasfondo ficcional de la construcción de una historia a través de objetivos en cada nivel que el jugador debe cumplir para seguir con la aventura.

Básicamente hay un solo personaje activo en el juego, el caballo que representa el ejército de los hunos. Este ejército tiene unas propiedades que reflejan las necesidades básicas de cualquier ejército (agua, comida, efectivos, armas, oro).

El resto de elementos son elementos sobre el tablero que interactúan con el personaje cuando el caballo pasa por una casilla con otro elemento interactuarle y pueden alterar las propiedades del caballo de Atila.

En el juego podremos encontrar elementos pasivos que modelan nuestro camino como elementos naturales o elementos activos que modificaran las propiedades del ejército y que forman parte del objetivo de la etapa actual del juego. 

Algunos ejemplos: 
  • Ciudades. Podemos saquearlas para obtener recursos si nuestro ejército es superior a sus defensas o pagar tributo para cruzar sus fronteras o ser derrotados si perdemos la batalla. 
  • Casillas objetivo. Casillas que debemos pasar obligatoriamente para terminar la etapa.
  • Ejército enemigo. La batalla condicionará cómo evoluciona el juego.
  • Campos. Podemos obtener comida y agua.
  • Ríos y lagos. Podemos obtener agua.
  • Minas y canteras. Podemos obtener oro.

Mecánica básica:
  1. El jugador mueve el caballo
  2. Recoge los recursos de la casilla destino
  3. Interactúa con la casilla si es necesario
  4. Modifica las propiedades en función de la interacción
  5. La casilla de origen se destruye.

Objetivos
El proyecto está en varias fases para garantizar la focalización del esfuerzo en etapas diferentes del proyecto. 
  • El primer objetivo es hacer un esqueleto de aplicación para móviles que contenga los elementos básicos de una aplicación profesional. Configuración del motor para dicho propósito, generar las rutinas de ficheros para gestionar valores de configuración y del juego en formato fichero. Crear el flujo básico de la aplicación.
  • El segundo objetivo es hacer una estructura interna para configurar los niveles: capas, elementos, objetivos y que el engine sea capaz de sobreponer la información sobre el nivel generado a través de tilesets isométricos para tener estructuras internas que reflejen la realidad del escenario.
  • En tercer lugar poner la figura del caballo y dotarle de movilidad por encima del nivel teniendo en cuenta las peculiaridades del movimiento del caballo en el ajedrez.
  • En cuarto lugar, ser capaz de relacionar el movimiento del caballo con la información almacenada del nivel en las estructuras internas. Crear y gestionar los triggers que generen los movimientos en relación al escenario. Destrucción de información dinámica.
  • Seguidamente deberíamos gestionar esos triggers entre el caballo y el escenario en función a los eventos asociados y cómo influyen en las propiedades del ejército del personaje definidas como condiciones de victoria y derrota del nivel.
  • El sexto objetivo es dotar al escenario de elementos de usabilidad de usuario, para el movimiento a través del mapa e interfaz de usuario de las diferentes escenas del proyecto.
  • En séptimo lugar es necesario crear al menos dos etapas de juego para definir la interacción entre ellas y definir el progreso en el juego y mostrar en ellas todas las mecánicas previstas (elementos, eventos, condiciones de victoria/derrota…).
  • En octavo lugar saber definir correctamente una coherencia visual entre todos elementos del juego para tener un producto cerrado y completo. Pantallas de inicio i fin, menús, elementos inter etapas.
  • En noveno lugar colocar los elementos visuales y sonoros, efectos, partículas, shaders que dotan al juego de un envoltorio estético uniforme y funcional.
  • En decimo y último lugar hacer todas la pruebas, análisis del equilibrio y dificultad entre niveles para determinar que el juego es coherente y divertido.

Numbers swap

Hace algunos años, el primer juego que hice fue Numbers Swaps, un juego puzle donde debías ir juntando números iguales para a partir de sus sumas ir ganando puntos y evitar quedarte sin casillas vacías. Este primer proyecto lo desarrollé en Delphi y ahora, años más tarde he decidido rehacerlo de nuevo pero usando Unity y aprovechar tecnologías que en ese momento no existían y menos en Delphi.



Esta vez he tardado 3 semanas en terminarlo y está publicado en Google Play. El proyecto tiene un poco de todo: gestión de tableros, Drag and Drop, ficheros de apoyo, publicidad, remarketing, partículas, etc. 


Si os interesa algún apartado en concreto no tengo inconveniente de explicar cómo está hecho para aquellos que tengan curiosidad o si queréis saber algún tema.



Os leo en vuestros comentarios.

Recursos de Dilmer Valecillos

Del creador de Cubiques podemos encontrar su faceta de comunicador y desarrollador de videojuegos. Dilmer Valecillos tienen canales de las redes sociales con interesantísimos recursos, vídeos, tutoriales y un espléndido canal de YouTube.

Además en Facebook es el administrador de un canal activo sobre desarrollo de videojuegos (Indie Game Developers en Español).

Para aquellos que queréis estar siempre al día en las novedades de Unity y buscáis interesantes tutoriales os recomiendo que le deis una oportunidad.

Datos 
Página webhttps://www.dilmergames.com/blog/
Twitterhttps://twitter.com/Dilmerv
Facebookhttps://www.facebook.com/dilmer.valecillos.9
Youtubehttps://www.youtube.com/c/dilmervalecillos

Como hacer copias de tu código de Unity con GitHub

Podriamos escribir un libro entero de las bondades de Git para el trabajo colaborativo y la gestión de versiones en un entorno como Unity. D...