Noticias

¡RECUERDA QUE SI ERES UN NUEVO USUARIO, DEBES PRESENTARTE PARA PODER PUBLICAR MENSAJES! | TENEMOS CANAL OFICIAL DE TELEGRAM: t.me/unity3dspain

Mejor forma de crear un Manager de Timers personalizado?

Iniciado por pioj, Abril 01, 2016, 02:15:36 PM

Tema anterior - Siguiente tema
Llevo bastantes días estrujándome el cocotero y no se me ocurre cómo sacarlo, así que os lo explico por aquí todo, en busca de opiniones:-Estoy creando una Clase genérica de myTimer, para crear y actualizar cualquier tipo posible de contador/Timer en un juego, independiente del género que sea.  Es decir, trato de crear un contenedor que permita crear en tu escena o en tu juego, un contador. Para hacerlo todavía más genérico, he decidido catalogarlos de la siguiente forma:
  • Interminable. El contador empieza desde cero, y sólo se pausa cuando se para el juego, continuando en cada sesión. Llevas jugadas 500 horas...
  • Reloj. El contador empieza en 0, y cuando llega a cierto valor, se resetea y comienza de nuevo, y así indefinidamente. Cada 500 seg, aparecerá un enemigo.
  • Alerta. El tipo de contador más conocido; cuando el contador llegue a cierto valor, se dispara el evento. No podrás volver a disparar hasta que pasen 10 segs.
  • CuentaAtrás. Simple variación de la Alerta, pero en la que la cuenta va hacia atrás, y siempre se para en 0. La bomba estallará en 30 segs!
 Con ésto y unas pocas propiedades más, ya tengo mi clase. Lo ideal es tener acceso al estado actual de cada contador, para presentar la clásica barra de progreso,etc, y recibir y mandar eventos conforme ese contador ha terminado y debe ser destruído, pausado, o lo que sea...Ahora le toca el turno al Manager, que simplemente es un Script Monobehaviour, que guardará una lista global de objetos de tipo myTimer. Por tanto, he querido separarlo todo en 2 scripts:
  1. Clase normal myTimer, que debe crear, alojar y gestionar un objeto de tipo myTimer, o sea un contador. No quiero que herede de Monobehaviour.
  2. Script TimersManager, de tipo Monobehaviour, que mantiene una lista de objetos myTimer.
 El objetivo principal del Manager es gestionar globalmente la LISTA de contadores y recibir notificaciones de cada objeto. Por lo tanto, lo correcto sería dejar que la propia Clase sea la que gestione el estado del contador, tras ser creado. Eso es ser eficiente. Y aquí es dónde aparece el problema.  Quiero que sea la propia Clase la que actualice el objeto. Pero ocurre lo siguiente:
  • InvokeRepeating NO es eficiente (pica mucha CPU) y es una función que requiere estar heredando de MonoBehavior. 
  • Time.deltaTime + valor--, no es ni eficiente ni fiable, pero podría funcionar y me daría control sobre cómo va pasando ese tiempo. Pero puede provocar errores.
  • StartCoroutine + IEnumerator sí son eficientes, pero también requieren heredar de MonoBehavior, y sólo devuelven cuando acaban. Por tanto, no puedo usarlos en la Clase.
  • System.Timer.CreateTimer() es lo más eficiente según C#, pero también depende de MonoBehaviour, y además genera errores de API en Unity.
 Así es cómo estoy, con la duda constante de por cuál de estos métodos me decido y, por lo tanto, qué cambios hago en la clase y en el Manager para adaptarlos. Imaginad un metroidvania o un Ogame o CookieClicker en el que tengo que estar gestionando tranquilamente 200 contadores.  La decisión no es nada fácil... ¿Algún consejo de qué hacer? 

Abril 01, 2016, 02:38:54 PM #1 Ultima modificación: Abril 01, 2016, 02:43:42 PM por Braltor
muy util!... pero creo q te pasas de perfeccionista xDa menos q alguien necesite esa clase para mediciones cientificas no necesita nada tan preciso, asi q es mejor irse por lo q consume menos: incrementos de deltatimecon una DIFERENCIA! que si sumas floats la precision comienza a decaer arriba de 100 mil (el mismo unity te advierte)... en segundos eso es apenas unas 24 horas, quien no ha jugado mas de 24 h? quien??entonces es mejor incrementar un doubley te falta algo mas para juegos: una pausa... con los incrementos es muy facil crearla y eso permite pausar actividades del juego mientras se mantienen otras, es como un tiempo relativo... imagina que hay una bomba atomica a punto de estallar... no importa, mi vejiga es mas importante y necesito pausar para ir al baño!!eso es lo q yo hago en mis proyectosquedo a la espera de tu modulo xD

Quieres que explique como llamar a Instantiate, una Coroutine o cualquier cosa de MonoBehaviour desde una clase que no esté heredada de MonoBehaviour?

Por eso mismo os pregunto por vuestra opiniones, sugerencias de lo que haríais vosotros si quisierais eso.. A las malas, claro que podré hacerlo derivar de Monobehavior...

A ver, a mi me ha pasado algo parecido, solo que en vez de Invokes o Time's eran otras funciones como Instantiate. Al final el problema se resuelve de la misma forma, ya que el handicap es no poder llamar a nada de estas funciones desde una clase pura. Esto es lo que preguntaba si queríais saber.Respecto a tus opciones, pues depende de la resolución que quieras obtener. Como veo que hablas de segundos con una alerta o un timer de 500secs no creo que tengas problemas con Time + delta.time. El resto de opciones me parecen demasiado "cargantes de proceso" para n timers en paralelo. Esta para mí sería la más ligera y es precisa para resolución de segundos. 

Yo hice uno que funciona bastante bien http://unitydoc.blogspot.com.ar/2016/03/timer-para-c-ejemplo-con-arma.htmlLo único que necesitas sería poder retornar un valor numérico para ver el progresoCodigo FUENTE
using System;
using UnityEngine;
using System.Collections.Generic;
using Unit***itor;

namespace CustomTool
{
[AddComponentMenu("System/Timer Control")]
public class Timer : MonoBehaviour
{
   private static Timer                _singletone;
   private List<TimerItem>             _timers = new List<TimerItem>();

   public static Timer Singletone{get{ return _singletone;}}
   public List<TimerItem> GetAllTimer {get{return _timers;}}

   private void Awake()
   {
      if (_singletone == null)
      {
         _singletone = this;
         DontDestroyOnLoad (this.gameObject);
      }
      else
      {
         Destroy (this.gameObject);
         return;
      }
   }

   private void Update()
   {
      foreach(var timer in _timers)
      {
         if(timer.IsRuning)timer.Run ();
      }
   }

   /// <summary>
   /// Stops all.
   /// Utilizar cuando queremos parar todos los Timers que estan siendo ejecutados
   /// </summary>
   public void StopAll()
   {
      foreach(var timer in _timers)
      {
         if(!timer.IsManual)
         {
            timer.Stop ();
         }
      }
   }

   /// <summary>
   /// Clears all.
   /// Utilizar para Detener y Eliminar todos los TIMER no manuales, es decir todos aquellos timer que creamos con la funcion manual activada no tendrar efecto.
   /// </summary>
   public void ClearAll()
   {
      foreach(var timer in _timers)
      {
         if(!timer.IsManual)
         {
            timer.Stop ();
            _timers.Remove (timer);
         }
      }
   }

   /// <summary>
   /// Detener un Timer pasando el ID correspondiente, es equivalente a utilizar STOP de nuestro TimerItem.
   /// </summary>
   /// <param name="TimerID">Timer I.</param>
   public void Stop(int TimerID)
   {
      _timers [TimerID].Stop ();
      _timers.RemoveAt (TimerID);
   }

   /// <summary>
   /// Quicks the timer.
   /// </summary>
   /// <returns>Retorna un nuevo TimerItem </returns>
   ///
   /// <param name="TimeWait">Tiempo que deseamos esperar.</param>
   /// <param name="Repeat">If set to <c>true</c> Una vez empieza el Timer solo se detiene cuando es destruydo y forzado a parar utilizando la funcion Stop</param>
   /// <param name="Manual">If set to <c>true</c> Las Funciones de Stop no pueden ser utilizadas por TimerManager, por lo que deberiamos hacernos cargo de detener el timer.</param>
   public TimerItem QuickTimer(float TimeWait,bool Repeat = false, bool Manual = false)
   {
      var item = new TimerItem (TimeWait, _timers.Count, Repeat, Manual);
      _timers.Add (item);
      return item;
   }
}

public class TimerItem
{
   private int                   _timerID;
   private float                   _yieldTime;
   private bool                   _isFinish;
   private bool                   _isRuning;
   private bool                   _repeat;
   private bool                     _manual;
   private float                  _currentWait;

   public delegate void DelegateFinish ();
   public delegate void DelegateStop ();
   public delegate void DelegateStart ();

   /// <summary>
   /// Se llama cuando el Contador a Finalizado.
   /// </summary>
   public event DelegateFinish       EventFinish;
   /// <summary>
   /// Se llama cuando el Contador es Detenido.
   /// </summary>
   public event DelegateStop          EventStop;
   /// <summary>
   /// Se llama cuando el Contador es Iniciado.
   /// </summary>
   public event DelegateStart          EventStart;

   public bool IsManual{get{ return _manual; }}
   public bool IsFinish{get{ return _isFinish; }}
   public bool IsRuning{get{ return _isRuning; }}
   public int GetID{get{ return _timerID;}}

   /// <summary>
   /// Run this instance.
   /// Esta funcion es llamada por el TimerControl
   /// </summary>
   internal void Run()
   {
      if (_isFinish && !_isRuning)return;

      if(Time.time > _currentWait)
      {
         if(EventFinish != null)EventFinish ();

         _isFinish = true;
         _isRuning = false;

         if (_repeat)Start ();
         return;
      }
   }

   /// <summary>
   /// Initializes a new instance of the <see cref="Timer.TimerItem"/> class.
   /// </summary>
   /// <param name="WaitingSecond">Waiting second.</param>
   /// <param name="ID">I.</param>
   /// <param name="Repeat">If set to <c>true</c> repeat.</param>
   /// <param name="Manual">If set to <c>true</c> manual.</param>
   internal TimerItem (float WaitingSecond, int ID, bool Repeat = false, bool Manual = false)
   {
      _timerID = ID;
      _yieldTime = WaitingSecond;
      _repeat = Repeat;
      _manual = Manual;
   }

   /// <summary>
   /// Start this instance.
   /// Iniciar el Contador
   /// </summary>
   public void Start()
   {
      _currentWait = _yieldTime + Time.time;
      _isRuning = true;
      _isFinish = false;
      if(EventStart != null)EventStart ();
   }

   /// <summary>
   /// Detener el Contador
   /// </summary>
   public void Stop()
   {
      _isFinish = true;
      _isRuning = false;
      if(EventStop != null)EventStop ();
   }
}

[CustomEditor(typeof(Timer))]
public class TimerEditor : Editor
{
   Timer Target;

   private void OnEnable()
   {
      Target = (Timer)target;
   }

   public override void OnInspectorGUI ()
   {
      EditorGUILayout.HelpBox ("Controlador del Timer, Para acceder utilizar el singletone",MessageType.None);

      EditorGUILayout.HelpBox ("Estado Actual",MessageType.None);
      foreach(var timer in Target.GetAllTimer)
      {
         GUILayout.Box ("TIMER ["+timer.GetID +"] Finish "+ timer.IsFinish + " Runing "+timer.IsRuning + " IsManual " + timer.IsManual,GUILayout.ExpandWidth(true));
      }
   }
}
}

Espero te sirva! Si no avisame y vemos como lo solucionamos. Esto esta compilado en una DLL por lo tanto puede darte un problema en el TimerEditor. Suerte!
Desde la web del Blog podes ver un Ejemplo, no es muy complicado de utilizar! y lo he probado con unos 80 timers más o menos y todo correcto en Android.

Está muy guapo, @francoe1, muchas gracias. Voy a ver si puedo adaptar un fork con lo que necesito...

[quote author=pioj" data-ipsquote-contapp="forums" data-ipsquote-contenttype="forums" data-ipsquote-contentclass="forums_Topic" data-ipsquote-contentid="35543" data-ipsquote-contentcommentid="129005">Por eso mismo os pregunto por vuestra opiniones, sugerencias de lo que haríais vosotros si quisierais eso.. A las malas, claro que podré hacerlo derivar de Monobehavior...[/quote]Puedes crear una rutina contador(alarma) desde el Update de un script o desde una corrutina. Pero, en ambos casos no podrás destuir el script desde el que generaste el contador. Si destruyes el script las corrutinas y el propio script desaparecen.Ese es el mayor inconveniente de las corrutinas, que no puedes destuir el script desde el que la generaste.Solucion, utilizar métodos anónimos que no dependan de un script o instancia de una clase MonoBehaviour: https://msdn.microsoft.com/es-es/library/0yw3tz5k.aspx He creador un script Manager(Gestor) tipo Singleton(único) de alarmas:
using UnityEngine;
using System.Collections;

public class Script : MonoBehaviour {

public int contador = 0;


}
 Si creamos una alarma desde otro script directamente(Update) o mediante una corrutina, no podremos destuir ese script externo si queremos mantener la alarma.Pero mediante los métodos anónimos no ocurre esto. Por ejemplo, en el siguiente script le paso la variable Script donde se encuentra el dato contador desde un método anónimo:
using UnityEngine;
using System.Collections;

public class CreacionTimer : MonoBehaviour {

public Script script;

// Use this for initialization
void Start () {

   TimerManager.Singleton.CreateAlarm(1f,true,()=>{ this.script.contador++; });
   Destroy(this);

}


}
 

Abril 02, 2016, 10:20:20 AM #8 Ultima modificación: Abril 02, 2016, 10:34:34 AM por pioj
Ahora mismo (realtime) lo estoy implementando mediante StartCoroutine+Ienumerator, tras haber hecho que la clase herede de MonoBehaviour (  ).  De momento la cosa tira. Estoy acabando el update de cada tick, que es variable dependiendo de si es un reloj o una alarma, y me he puesto con el CustomEditor, para mostrar barras de progreso...

Etiquetas: