Jump to content
Sign in to follow this  
  • entries
    5
  • comments
    0
  • views
    228

About this blog

Material para aprender y perfeccionar nuestros conocimientos.

Entries in this blog

francoe1

Diseño, Producción y Publicación.

justin_worthe_game_image1@2x.png

Enfrentarnos a estos tres principales pilares del desarrollo muchas veces nos genera incertidumbre, la falta de experiencia nos puede llevar al fracaso y en muchos casos no somos capaces de finalizar esas ideas que tenemos en la cabeza, la razón de este blog viene debido a la consulta realizada por @nomoregames y visto que es un problema recurrente me gustaría compartir con base en mi experiencia.

Lo más importante es conocer y entender los tres principales pilares. 

  • Diseño: en esta etapa vamos a encargarnos de plasmar todas nuestras ideas, funcionalidades, tecnologías y referencias. No es importante que a la primera el documento quede perfecto, esta etapa lleva mucho tiempo de ajuste y reestructurar, es vital dedicarle el tiempo suficiente, días o semanas para que el documento nos sirva de mapa para la siguiente etapa. 
  • Producción: esta etapa se puede dividir en dos, planificación y ejecución, como todo en la vida, lo más importante es planificar, especificar los recursos necesarios. La ejecución es el trabajo de ir tomando completando de forma secuencial las tareas planificada, sabiendo que nos llevaran a buen puerto.
  • Publicación: la publicación es una de las fases más controversiales, llegar a este punto presenta un gran desafío, debido a este esfuerzo acompañado de frustración y auto superación, al llegar a este punto la ansiedad nos hace cometer graves errores. La publicación del proyecto debe ser premeditada, diseñada y planificada (déjà vu) para lograr captar la atención de los consumidores, para esta etapa es importante captar la esencia del juego, demostrar el potencial y hacer sentir que el consumidor necesita probarlo. En esta etapa es importante buscar información sobre marketing, ventas de productos digitales, etc. 

Ahora hablaremos un poco más a detalle de como se lleva a cabo lo antes comentado para el desarrollo de videojuegos. Todo empieza desde la experiencia de explorar una idea en nuestra cabeza, en esta etapa está bueno ir escribiendo todo lo que se nos ocurre (no pensar en la implementación), compartirlo con otras personas e ir apuntando sus respuestas/reacciones, a esto se lo conoce como "tormenta de ideas" recomiendo por experiencia que este proceso lo realices sin prisa, intenta ir de a poco, puedes empezar ahora mismo y dedicarle un ratito cada vez que te sientas inspirado, esto no es algo que se realice con un tiempo preestablecido. Verás que con el tiempo la idea principal fue mutando tanto, que puede hasta perder su personalidad, en este punto existen dos alternativas, eliminar todo y empezar nuevamente o seguir adelante con el diseño.

Para el diseño siempre se habla del GDD pero muchos se preguntan ¿cuál es su estructura?. GDD es un documento que consta de varias partes, dependiendo de la complejidad del proyecto, si bien es algo muy comentado, las grandes empresas siguen otro patrón de diseño algo que para desarrolladores independientes sería impensable, comentando esto se puede decir que el GDD se interpreta como un resumen del juego, tecnología y características, para entender esto más a fondo dejaré de ejemplo los siguientes GDD DIABLO 1  |  GTA. Lo importante del GDD es que se conserve la idea inicial de forma intacta y explique de forma resumida que aspecto lo hace único, que se entienda en principio que este documento sirve como referencia, y a la misma vez para promocionar su desarrollo.

Como somos independientes no nos basta solo con tener la idea y el diseño, también queremos producirlo. En este punto tenemos que tener mucho cuidado, intentaré describir las señales que puedes interpretar como un fracaso asegurado, pero antes, me gustaría darte una idea del primer paso de la planificación.

  1. Recursos visuales: deberías especificar y detallar los recursos visuales que serán utilizados para lograr el núcleo del juego, esto puede ser, el personaje principal y los secundarios, elementos claves del entorno, elementos requeridos para el gameplay, en este punto evitar pensar en usar "cajas" para remplazar algún modelo del juego, para esta tarea es indispensable buscar referencia, identificar el estilo artístico y otras características sobre las cuales deberían basarse los modeladores y/o diseñadores gráficos.  
  2. Recursos sonoros: deberías buscar múltiples referencia y explicar en forma de Storyboard como se usaría, esto ayuda mucho a la hora de crear conceptos. Esta tarea también se puede realizar a lo último, haciendo pruebas con material ya existente como referencia. 
  3. Diseñadores de Niveles: dependiendo de si el proyecto lo requiera a o no, es importante contar con la planificación de que papel tendrá cada escenario, las mecánicas que se implementarán, el tiempo que el jugador deberá permanecer de forma promedio, esto ayuda al diseñador de niveles poder balancear mecánicas y detalles.
  4. Diseñadores de Mecánica: dependiendo de si el proyecto lo requiera, tener una persona dedicada a explotar las mecánicas del juego siempre es una gran idea. Para esta tarea no es necesario que la sepa programar.
  5. Con lo anterior es importante definir un objetivo como prototipo, esto puede ser el desarrollo de las mecánicas principales.

¿Por qué no hable de programación?, en UnitySpain prevalecen los programadores, muchos aún están empezando a aprender y desconocen la magnitud de llevar a cabo un proyecto. La programación es solo una parte de lo que realmente es el producto finalizado, por esta razón decidí dejar este aspecto para profundizar en otra entrega.

"Las señales del fracaso"

Si crees que es imposible cumplir con algún punto de lo anteriormente comentado entonces estas en graves problemas, te recomendaría no continuar con la producción, es importante conocer nuestras limitaciones y crear proyectos acordes a nuestras habilidades, también se aplica para cuando estamos dentro de un equipo y las tareas que nos asignan nos terminan abrumando.

 

 - Si les interesa este contenido y quieren saber más sobre algún punto en concreto sepan que estaré pendiente a los comentarios…

 

- Pequeña aclaración -
Este blog fue escrito con la intención de introducir algunos vagos conceptos, es una percepción personal basada en mi experiencia, intentaré explicar más a fondo los conceptos que generen incertidumbre. 

francoe1

Cuando nuestros proyectos empiezan a crecer un error común es duplicar la información, ¿de qué manera? instanciando componentes.

Pensemos un escenario donde instanciamos 20 veces el siguiente componente.

public class BulletBehaviour : MonoBehaviour
{
    public Texture2D Texture;
    public LayerMask CollisionMask;
    public AnimationCurve CurveSpeed;
    public string String1 = "ABCDE";
    public int Int1 = 15600;
    public string[] StringArray = new string[] { "A", "B", "AB", "CD", "oisisisiisisis" };
}

Como resultado estaríamos clonando en memoria 20 veces los mismo valores, ahora supongamos que son 2000 objetos y cada uno está consumiendo 3000 bytes tendríamos como resultado un consumo de 6MB.

Unity nos ofrece una solución que se adapta perfectamente al motor, esto nos permite desacoplar la información manteniendo el mismo workflow, estoy hablando de ScriptableObject, esta esta pensada para evitar repetir información en runtime. La lógica es simple, para cada bloque de información se crea un activo que luego puede ser utilizado como referencia desde cualquier componente en escena. También, utilizando esta característica logramos centralizar la información lo cual es muy importante a la hora de desarrollar cualquier tipo de aplicación.

 


Implementación

Es importante tener en mente que un ScriptableObject es un activo, esto quiere decir que las alteración nos son persistentes fuera del editor. Para esta implementación intentaremos mejorar BulletBehaviour.

  1. Programamos nuestro ScriptableObject
    [CreateAssetMenu(fileName = "BulletInfo", menuName = "BulletInfo")]
    public class BulletInfoSO : ScriptableObject
    {    
        public Texture2D Texture;
        public LayerMask CollisionMask;
        public AnimationCurve CurveSpeed;
        public string String1 = "ABCDE";
        public int Int1 = 15600;
        public string[] StringArray = new string[] { "A", "B", "AB", "CD", "oisisisiisisis" };
    }

     

  2. Creamos un activo de BulletInfoSO
    image.png
  3. Asignamos los valores.
    image.png

Ahora crearemos una versión optimizada de BulletBehaviour para realizar las pruebas de rendimiento.

public class BulleOptimizedBehaviour : MonoBehaviour
{
    public BulletInfoSO Info;
}

Como se puede apreciar, en la versión optimizada solo tendríamos una referencia al activo, esto implica que para cada instancia de BulletOptimizedBehaviour solo existiria 1 o las posibles variantes de BulletInfoSO.

 


Pruebas de Rendimiento

5.000 Bullet vs 5.000 BulletOptimized

image.png

image.png

50.000 Bullet vs 50.000 BulletOptimized

image.png

image.png

Con estos dos ejemplos se puede apreciar que el consumo de los objetos sin la implementación de monobehaviour consumen aproximadamente el doble, hay que tener en cuenta que este ejemplo es muy simple, esto quiere decir que en producción unos cientos de objetos podrían ocupar 100mb y con miles estaríamos hablando de gigas.

La ventajas de desacoplar la información del componente es que centralizamos y optimizamos el uso de la memoria.

 

francoe1

Problemática

La parte más compleja del desarrollo de un juego es mantener el rendimiento del mismo a medida que esté escala, la técnica del pool object nos permite reutilizar objetos ya creados, esto evita generar basura y omite los costos de creación del mismo. Se suele utilizar para cosas a groso modo, pero tambien se puede y debe utilizar cada vez que detectemos que en nuestro código se están creando y destruyendo objetos dentro del mismo bloque. 

Ejemplo

public void Method()
{
  for (int i = 0; i < Items.Count(); i ++)
  { 	
    ItemProcessor processor  = new ItemProcessor();
    Items[i].Process(processor);
  }
}

En el ejemplo se puede ver que ItemProcessor se crea en cada iteración del bucle, al finalizar esa iteración el objeto se marca para ser eliminado cuando pase el recolector de basura. Si esta función se utiliza muchas veces a lo largo del juego obtendremos como resultado bajadas de frames cada x cantidad de tiempo. 


Implementación

Existen múltiples formas de implementar un sistema de PoolObject, en este blog intentaré explicar la mas optima, abstracta y escalable. 

  1. Definimos una IRecyclable, esta tendrá la definición para saber si esta disponible, la función de reciclar y restaurar.
    public interface IRecyclable
    {
      bool IsAvailable();
      void Recycle();
      void Restore();
    }

     

  2. Creamos la clase PoolObject

    Spoiler
    
    public class PoolObject
    {
      private IRecyclable[] m_objects { get; set; }
    
      public delegate IRecyclable CreateObjectDelegate();
      public CreateObjectDelegate CreateObjectHanlder { private get; set; }
    
      public PoolObject(int capacity)
      {
        m_objects = new IRecyclable[capacity];            
      }
    
      public void RecicleAll()
      {
        foreach (IRecyclable element in Query(x => !x.IsAvailable()))
          element.Recycle();
      }
    
      public IRecyclable GetObject()
      {
        IRecyclable query = SingleOrDefault(x => x.IsAvailable());
        if (query is null)
        {
          int index = GetEmptyIndex();
          if (index == -1) throw new Exception("Poolobject se quedo sin espacio");
          query = CreateObjectHanlder();
          m_objects[index] = query;
        }
        return query;
      }
    
      public IEnumerable<IRecyclable> Query(Func<IRecyclable, bool> query)
      {
        for (int i = 0; i < m_objects.Length; i++)
          if (m_objects[i] is object && query(m_objects[i]))
          yield return m_objects[i];
      }
    
      private IRecyclable SingleOrDefault(Func<IRecyclable, bool> query)
      {
        for (int i = 0; i < m_objects.Length; i++)
          if (m_objects[i] is object && query(m_objects[i]))
          return m_objects[i];
        return default;
      }
    
      private int GetEmptyIndex()
      {
        for (int i = 0; i < m_objects.Length; i++)
          if (m_objects[i] is null)
          return i;
        return -1;
      }
    }

     

    La implementacion cuenta de un arreglo que define su tamaño dentro del constructor, Luego cuenta con funciones para trabajar con el arreglo de forma óptima y estructurada, la compresión del algoritmo no requiere más explicaciones. Cada vez que se crea un Objecto se llama el handler "CreateObjectHandler".

  3. Implementamos la interface IRecyclable según la necesidad del proyecto.


Ejemplos

Aplicado a ItemProcessor una clase limpia sin herencias.

public class ItemProccesor : IRecyclable
{
  private bool m_isAvailable { get; set; }
  public int Pass { get; set; }

  public bool IsAvailable()
  {
    return m_isAvailable;
  }

  public void Recycle()
  {
    m_isAvailable = true;
    Pass = 0;
  }

  public void Restore()
  {
    m_isAvailable = false;
  }
}

Usando PoolObject

PoolObject pool = new PoolObject(5);
pool.CreateObjectHanlder = () => new ItemProccesor();

for (int i = 0; i < 2; i++)
{
  ItemProccesor item = (ItemProccesor)pool.GetObject();
  item.Pass += 1;
}

for (int i = 0; i < 2; i++)
{
  ItemProccesor item = (ItemProccesor)pool.GetObject();
  item.Pass += 1;
  item.Recycle();
}

pool.RecicleAll();

 


Conclusión 

Esta implementación se mantiene abstracta lo que permite escalar y ser implementada en cualquier ámbito, la delegación del estado nos da un completo control.

 

 

francoe1

Este blog es una continuación de Estructuras de datos - Básico Parte 1

Introducción

En este blog aprenderemos trabajar con listas de forma óptima, empezaremos a utilizar métodos de búsqueda, y daremos una introducción a los enumeradores.

Cuando definimos una estructura se debe a que vamos a crear múltiples objetos de un mismo tipo para un mismo propósito, estas estructuras se suelen consumir haciendo uso de bucles, el problema es cuando necesitamos filtrar la información de la estructura para evitar tener condiciones dentro del bucle, para este tipo de tarea en .NET se suele implementar Linq, pero esto es una mala idea si lo que estamos buscando es velocidad y rendimiento, es por eso, que intentaremos explicar como crear implementaciones personalizadas de alto rendimiento.


Implementación

Imaginemos que nuestro juego tiene un lista del tipo abstracto EntityNPC, para la lógica de nuestro juego necesitamos obtener e iterar todos los NPC activos.

Estructura inicial.

public abstract class EntityNPC
{
    public bool IsActive { get; set; }
}

public class EntityContainer
{
    public List<EntityNPC> Entitys = new List<EntityNPC>();
}

Para poder filtrar información de la lista implementaremos un IEnumerable, donde el parámetro será un método encapsulado (Func<T, R>) que usaremos para crear nuestra query.

Implementación en la clase EntityContainer

public IEnumerable<EntityNPC> Where(Func<EntityNPC, bool> query)
{
  for(int i = 0; i < Entitys.Count; i++)
    if (query(Entitys[i]))
      yield return Entitys[i];
}

Ahora podemos filtrar los elementos de la lista antes de ser iterados.

foreach(EntityNPC entity in Where(x => x.IsActive))
{

}

Los IEnumerables tienen un gran potencial, es importante leer la documentación oficial.


Pruebas de rendimiento

Para poder finalizar me gustaría compartir una pequeña prueba que realice implementando este sistema. Se realizó la prueba con una lista de 100.000 elementos.

  1. Utilizando Bucle FOR y filtrando el contenido dentro con un IF. (vs) Implementación personalizada.
    image.png
    La implementación personalizada tiene un mayor costo, pero genera menos basura.
     
  2. Filtrando contenido con Linq.Where (vs) Implementación personalizada.
    image.png
    Ambos tienen el mismo costo, pero se puede apreciar que la implementación personalizada genera la mitad de basura.
     
  3. Contador de elementos aplicando filtros.
    image.png
    La primer evaluación se trata de la implementación completa con Linq, generando 200B de basura.
    La segunda evaluación se trata de la implementación de Where Personalizado y el contador de Linq, generando 128B.
    La tercer evaluación se trata de la implementación de un Contador personalizado, generando 0B.

 

 

 

 

francoe1

 

Teoría

Para cualquier proyecto necesitamos utilizar estructuras de datos, por lo general listas y arreglos. En este blog intentaré compartir mi experiencia y algunas técnicas para trabajar con listas de forma estructurada y organizada, explicare como implementar querys y veremos ejemplos comparativos entre diferentes alternativas.

Existen múltiples tipos de estructura de datos, diferentes tipos de listas, pero en este blog solo hablaremos de arreglos y listas enlazadas.

¿Listas o Arreglos?
Dependiendo de la implementación lo más importante es entender de forma sencilla la principal diferencia.

¿Cómo funciona un arreglo? un arreglo es un grupo de elementos que se escriben en memoria de forma continua reservando el lugar que va a ocupar según su dimensión. Si creamos un arreglo del tipo Int32 quiere decir que la memoria deberá asignar 32bits multiplicado el largo del arreglo, entonces ¿cómo se separa la información para luego poder ser indexada?, esto es bastante simple, el arreglo almacena únicamente la dirección de memoria del primer elemento, para acceder a otro elemento tiene que sumar el tamaño del tipo por el índice requerido.

main-qimg-717d971bdb4bb63c97c04cbceb1ac2f2.png

Entonces se puede decir, en simples palabras, que un arreglo guarda la posición del primer elemento y reserva la memoria para poder escribir los siguientes elementos de forma continua.

¿Cómo funcionan las listas enlazadas?, esto es más fácil de entender, las listas guardar la dirección de memoria del primer elemento, pero a diferencia de un arreglo cada elemento de una lista se define como un nodo que contiene, la dirección de memoria de su propio valor y la dirección de memoria del nodo de su predecesor, de esta forma es posible iterar las listas.

image.png

Con el breve resumen anterior podemos llegar a una conclusión.

  • Arreglos
    • Asigna la memoria al ser inicializado
    • No se pueden agregar o eliminar elementos de forma dinámica
    • No requiere información extra para acceder a cada elemento.
  • Listas
    • Asigna la memoria de forma dinámica
    • Se puede agregar y eliminar elementos de forma dinámica. 
    • Requiere información extra para acceder a cada elemento.

¿Que debería utilizar?
Lo mejor siempre es identificar el contexto, se debería utilizar listas cuando: no conocemos la cantidad de elementos que puede contener o necesitamos agregar y eliminar elementos constantemente, para lo demás se debería utilizar arreglos. es importante destacar que un arreglo itera más rápido que una lista, las razones son obvias.


Implementación en videojuegos

Pensemos en un contexto simple, un juego de destruir naves. Cuando inicia debe crear 5 naves, una vez destruidas se volverán a crear agregando 5, el limite máximo de naves será de 30.

Implementación con arreglos.

Spoiler

public class Nave
{
    
}

public class GameManager
{
    public Nave[] Naves = new Nave[30];
    
    public int CantidadAnterior = 0;
    
    
    public void IniciarOleada()
    {
        CantidadAnterior += 5;
        for(int i = 0; i < CantidadAnterior; i++)
            Crear();
    }    
    
    public int NavesActivas()
    {
        int count = 0;
        for(int i = 0; i < Naves.Length; i++)
        {
            if (Naves[i] != null)
                count ++;
        }
        return count;
    }
    
    public void Destruir(Nave nave)
    {
        for(int i = 0; i < Naves.Length; i++)
        {
            if (Naves[i] == nave)
            {
                Naves[i] = null;
                break;
            }
        }
    }
    
    public Nave Crear()
    {
        for(int i = 0; i < Naves.Length; i++)
        {
            if (Naves[i] is null)
            {
                Naves[i] = new Nave();
                return Naves[i];                
            }
        }
        
        throw new Exection("No hay espacio para crear una nueva nave");
    }
}

 

Implementación con Listas.

Spoiler

public class Nave
{
    
}

public class GameManger
{
    public List<Nave> Naves = new List<Nave>();    
    public int CantidadAnterior = 0;   
    
    public void IniciarOleada()
    {
        CantidadAnterior += 5;
        for(int i = 0; i < CantidadAnterior; i++)
            Crear();
    }    
    
    public int NavesActivas()
    {
        return Naves.Count;
    }
    
    public void Destruir(Nave nave)
    {
        Naves.Remove(name);
    }
    
    public Nave Crear()
    {
        if (Naves.count >= 30)
            throw new Exection("No hay espacio para crear una nueva nave");
        
        Nave nave  = new Nave();
        Naves.Add(nave);         
        return nave;
    }
}

 

 

Sign in to follow this  
UnitySpain © Todos los derechos reservados 2020
×
×
  • Create New...