16 julio 2025

Los live-games y el riesgo del power creep

Internet ha cambiado radicalmente el mundo del desarrollo de los videojuegos, desde aquellos tiempos en que la promoción de estos se basaba en artículos en revistas especializadas que ofrecían discos con demos. Gracias a la hiperconectividad que tenemos en casa, el acceso a los juegos se ha vuelto inmediato y estamos sometidos a un bombardeo constante de anuncios de otros nuevos. Para el jugador, el reto ya no es enterarse de qué juegos salen, sino elegir los que más le puedan gustar entre un universo casi infinito de posibilidades. Para los desarrolladores, el reto sigue siendo llegar al jugador, pero no por falta de medios para hacerlo sino por lo complicado que es despuntar en un mercado saturado.

En un ecosistema así, muchas casas desarrolladoras prefieren no estar sacando nuevos juegos continuamente, sino centrarse en uno sólo y hacerlo evolucionar con el tiempo, ganando nuevos jugadores poco a poco y fidelizándolos durante años. La idea es que, al centrar los esfuerzos en un único juego a lo largo de un tiempo prolongado, hay más posibilidades de despuntar entre la competencia e ir ganando notoriedad. Dicha evolución se suele hacer a partir del análisis de una telemetría exhaustiva de los jugadores, con la que los desarrolladores puedan entender cómo interactúan con el juego, que partes disfrutan más, cuáles menos y qué echan de menos. Con esos datos, los desarrolladores pueden hacer pivotar al juego hacia los gustos cambiantes de su público, ir ganando nuevos jugadores y reteniendo a los anteriores. Con frecuencia, este proceso de evolución hace que, con el paso de los años, el juego no se parezca en nada a lo que era inicialmente.

Este tipo de juegos es lo que se conoce como live-game (o juego en vivo), término que se usa principalmente en el contexto de los videojuegos para referirse a un título que recibe actualizaciones continuas, contenido nuevo y soporte activo por parte de los desarrolladores después de su lanzamiento. También conocido como juego como servicio (Game as a Service, GaaS), se caracteriza por mantener una experiencia dinámica y evolutiva para los jugadores, fomentando una comunidad activa a lo largo del tiempo. 

Fortnite
Fortnite es uno de los ejemplos más claros de live-game actuales

El modelo de negocio

Esta continuidad en el servicio supone mantener servidores desde los que funcione el juego y se haga telemetría de los usuarios, contar con un equipo de desarrolladores que sigan actualizando el juego con nuevos contenidos y mecánicas, así como realizar campañas de marketing constantes que atraigan nuevos jugadores. Por tanto, estos juegos necesitan una inversión continua que no es sostenible con el modelo clásico de monetizar el juego con su venta inicial al jugador. Con estos juegos los desarrolladores tienen que acudir a modelos de monetización continua. 

El más clásico de ellos es el de la subscripción, en el que el World of Warcraft de Blizzard fue un pionero que aún pervive. El problema de las subscripciones es que el gasto recurrente que conllevan puede suponer una barrera económica o psicológica a la entrada de nuevos jugadores, bien porque no puedan asumir ese gasto o bien porque no tengan claro que vayan a jugar tanto al juego como para amortizarlo.

World of Warcraft
World of Warcraft fue el pionero

Otras casas desarrolladoras prefieren eliminar esa barrera de entrada haciendo completamente gratuito el juego, sin costes de adquisición ni subscripciones. En estos casos la monetización puede venir de la venta de grandes paquetes de contenidos opcionales, en forma de DLC, o de contenidos con un volumen mucho menor, con un coste también reducido... y aquí llegamos a las microtransacciones.

Las microtransacciones son compras pequeñas dentro de un videojuego (o aplicación) que permiten a los jugadores adquirir contenido digital adicional usando dinero real. Suelen ser opcionales y se supone que están diseñadas para mejorar la experiencia de juego. Su coste es bajo, generalmente van desde céntimos hasta unos pocos euros por transacción. Los ejemplos más habituales de contenidos ofrecidos a través de transacciones son:

  • Cambios cosméticos respecto al juego base: Skins, atuendos o personalizaciones visuales de las armas (ejemplo: skins en Fortnite).
  • Ventajas de juego: Objetos que facilitan el progreso, como potenciadores o recursos (ejemplo: gemas en Clash of Clans).
  • Contenido adicional: Niveles, personajes o historias extras.
El modelo de negocio

Para el desarrollador, las microtransacciones suponen un riesgo mucho menor que los DLC. Con un DLC hay que desarrollar mucho contenido y, si no triunfa, las pérdidas pueden ser muy importantes. Las microtransacciones son mucho más granulares, al ser tan pequeñas, su coste es reducido y el riesgo también. Combinadas con una buena telemetría que permita seguir su aceptación entre los jugadores, se pueden usar varias microtransacciones fallidas para centrar el tiro de lo que le gusta a los usuarios y, a partir de ahí generar otras muchas que den en el blanco de lo que estos quieren comprar. Es mucho más fácil entrar en un balance de caja positivo si se cuenta con un equipo de análisis competente y unos desarrolladores y artistas creativos.

Las críticas

Para el jugador, las microtransacciones tienen el atractivo del capricho a coste nimio. Son "bombones" que resultan fáciles de comprar porque a primera vista su coste parece irrelevante, si bien, con el paso del tiempo, el coste acumulado puede ser considerable. Esa facilidad en el gasto ha hecho que muchos acusasen a las microtransacciones de generar adicción. Estas acusaciones tuvieron especial virulencia con las loot-boxes (o cajas de botín), microtransacciones donde los jugadores pagan (con dinero real o moneda virtual del juego) para obtener una caja virtual que contiene recompensas aleatorias. Estas recompensas pueden incluir objetos cosméticos, personajes, armas, potenciadores u otros elementos, pero el contenido exacto es desconocido hasta abrir la caja, lo que introduce un elemento de azar similar a las apuestas. Esa similitud ha llevado a que algunos países (como Bélgica y Países Bajos) hayan restringido o prohibido las loot boxes al considerarlas apuestas. Otros exigen que se publiquen las probabilidades de obtener ciertos ítems. Al final, debido a las críticas, muchos desarrolladores han reducido el uso de loot boxes o han optado por sistemas más transparentes, como pases de batalla o tiendas directas donde los jugadores compran exactamente lo que quieren. Algunos juegos, como Rocket League, eliminaron las loot boxes tras incluirlas. No es el único caso, en Star Wars Battlefront II (2017), las loot boxes generaron una gran controversia por su impacto en la jugabilidad, lo que llevó a EA a modificarlas tras críticas masivas, a pesar de que todo el modelo de monetización giraba en torno  ellas, por lo que su retirada supuso el final del atractivo económico del juego para su productora.



La adicción no es la única crítica que reciben las microtransacciones. Otra de las más recurrentes es que, al dar ventajas injustas a quienes gastan más, reduzcan el juego a un pay-to-win que frustre a los que no estén dispuestos a pagar en la misma proporción. El pay-to-win puede disuadir incluso a los que sí tuvieran poder adquisitivo para permitirse las microtransacciones al no sentirse interesados por una experiencia de juego en la que primase más la cartera que la habilidad.

El riesgo del Power Creep

Otro peligro de las microtransacciones es lo que se denomina el power creep. Cuando un juego usa las microtransacciones como modelo de monetización, tiene que sacar contenido continuamente y además hacer más atractivo que el anterior para que los jugadores vayan comprando lo nuevo y no se conformen con lo que ya tienen. Hasta aquí lo normal, pero hay una posibilidad de que esta dinámica torne en un círculo vicioso que puede acabar siendo perjudicial para el juego, tanto desde el punto de vista de los desarrolladores como de los jugadores. El power creep (o "escalada de poder") se refiere al fenómeno en el que los desarrolladores introducen nuevo contenido (personajes, armas, habilidades, etc.) que es significativamente más poderoso que el contenido anterior, haciendo que los elementos previos queden obsoletos o menos competitivos. Por tanto, es un fenómeno que afecta sobre todo a las microtransacciones enfocadas a ofrecer ventajas en el juego.

Cuando el power creep se prolonga en el tiempo, puede introducir graves inconvenientes en el juego. Los más destacados son:

  • Impacto en la imagen del juego: Aplicado a juegos multijugador competitivos, donde los jugadores que no gastan dinero se enfrentan a desventajas claras, supondrá que el juego acabe teniendo la mala prensa de ser considerado un pay-to-win.
  • Impacto en la mecánica de juego: Los nuevos personajes, armas o ítems suelen tener estadísticas, habilidades o efectos superiores a los existentes, lo que desplaza el equilibrio del juego. Con el tiempo, esa "carrera armamentística" puede hacer evolucionar la mecánica de juego hasta que no se parezca en nada a como estaba concebida inicialmente... y probablemente no para bien.
  • Impacto en el desarrollo: Es muy difícil mantener indefinidamente una dinámica de desarrollo en la que en cada iteración hay que sacarse de la chistera nuevas ventajas, más poderosas que las anteriores, pero que no den al traste con la diversión de la mecánica de juego.
  • Impacto en los jugadores: la obsolescencia del contenido antiguo hará que los jugadores que usaban objetos o personajes previos pueden sentirse en desventaja, ya que estos ya no son tan efectivos conforme pase el tiempo. Quienes invirtieron tiempo o dinero en contenido antiguo pueden sentir que su esfuerzo fue inútil. Esto puede hacer que acaben desencantándose del juego y lo abandonen.
Power creep en las cartas de Pokemon
Power creep en las cartas de Pokemon

El power creep fuera de los videojuegos

En realidad, el power creep no es exclusivo de los live-games. Si hacemos memoria podemos encontrar ejemplos similares en el mundo de las series de televisión. Si te pones a pensar en las que has visto hasta el momento, te darás cuenta de que muchas se enfocan a una estructura de "vencer-al-villano-de-la-temporada". Para mantener el interés, dichos villanos son cada vez más poderosos, lo que hace las situaciones más dramáticas y extremas. Sin embargo, ese enfoque no es sostenible. Conforme pasan las temporadas los villanos de las primeras empiezan a parecer irrelevantes, comparándolos con los de las últimas. Por otro lado, el creciente dramatismo necesario para vencer a enemigos tan poderosos puede alcanzar un punto que desborde hacia el ridículo y la incredulidad. 

Hay muchos ejemplos de series que han sufrido este fenómeno, pero quizás una de las más evidentes es Dragon Ball. Los fans de Akira Toriyama recordarán como la serie empieza siendo un relato alegre y travieso, con un niño como protagonista, y unos enfrentamientos que se limitan a golpes de artes marciales. Con el tiempo, aparece la técnica de la "onda vital" o "Kame Hame Ha" (según la traducción e la serie que hayas visto) como un rayo legendario y definitivo. 

Dragon Ball al principio

Pero como la serie no para ahí, tampoco lo hacen las técnicas de combate ni los enemigos. Conforme la serie va avanzando, los enemigos se hacen más fuertes, ya no basta con pegarles para vencerles, la "onda vital" se queda corta, así que los protagonistas se hacen adultos, aprenden a volar y a lanzar nuevos tipos de rayos (la wikipedia identifica hasta 18 versiones distintas de la "onda vital", cada una de ellas de creciente poder). Con tanto despliegue de poder, los combates de la serie se vuelven tan aparatosos que un golpe mal dado puede acabar partiendo un planeta en dos. Al llegar, a este punto la serie ya había perdido su alegría y vitalismo y se había reducido a combatir a una larga sucesión de villanos más poderosos que los anteriores. Fue cuando perdí el interés y me desconecté de la serie, aunque me consta que siguió durante mucho tiempo y con abundantes spin-offs. 

Dragon Ball, ya víctima del power creep

Posibles soluciones al Power Creep

Esto, que ya pasaba con las series, puede pasarle también a un live-game, minándolo hasta su fin. Evitar o mitigar el power creep exige que los desarrolladores diseñen estrategias que equilibre la mecánica de juego y mantengan la experiencia justa, divertida y sostenible a largo plazo. Los enfoques que se suelen seguir son:

  • Enfocar las microtransacciones a la venta de contenido cosmético: Al tratarse de cambios meramente visuales (como skins o efectos visuales) no hay cambios funcionales que puedan afectar a la mecánica de juego, lo que mantiene su equilibrio. Un ejemplo de este enfoque es Fortnite, que centra la mayoría de sus microtransacciones en skins y bailes, evitando que el poder del jugador dependa de compras, por lo que estos no sienten presión por gastar para mantenerse competitivos, y el power creep funcional es mínimo.
  • Diseño de contenido lateral (sidegrading) en lugar de mejoras directas: En lugar de introducir nuevos elementos (personajes, armas, habilidades) que sean estrictamente más poderosos, los desarrolladores crean contenido con características diferentes pero equilibradas, que ofrecen nuevas formas de jugar sin superar al contenido existente. Por ejemplo, en Overwatch, los nuevos héroes se diseñan con habilidades únicas que no necesariamente son más fuertes, sino que aportan estilos de juego distintos. Esto minimiza la obsolescencia del contenido anterior, ya que la elección entre uno u otro se basa en preferencias o estrategias, no en poder bruto.
  • Ajustes de equilibrio regulares (buffs y nerfs): Los desarrolladores monitorean el meta del juego y ajustan las estadísticas o habilidades de elementos nuevos y antiguos para mantener un equilibrio competitivo. Por ejemplo, en League of Legends, los parches regulares ajustan a los campeones para evitar que los nuevos dominen el juego, fortaleciendo a los menos usados (buffs) o debilitando a los dominantes (nerfs). Con esta técnica se mantiene la relevancia de contenido antiguo y evita que el nuevo sea abrumadoramente superior.
  • Limitar el impacto de la progresión: Se trata de diseñar el juego para que las mejoras de poder tengan un impacto limitado o estén restringidas a ciertos modos (como PvE en lugar de PvP). Por ejemplo, en Destiny 2, el equipo nuevo puede ser más fuerte, pero los modos competitivos a menudo tienen límites de poder para igualar a los jugadores. Esto reduce la percepción de pay-to-win y mantiene la equidad en entornos competitivos.
  • Implementar sistemas de rotación o meta cambiante: Se basa en introducir cambios en las reglas del juego, modos o eventos que alteren qué elementos son más efectivos, sin necesidad de hacerlos más poderosos. Por ejemplo, en Hearthstone, la rotación de cartas entre formatos estándar y salvaje asegura que las cartas antiguas no compitan directamente con las nuevas, controlando el power creep.
  • Reutilización y mejora de contenido antiguo: Se trata de actualizar personajes, armas o ítems antiguos para que sigan siendo relevantes, en lugar de reemplazarlos con contenido nuevo más fuerte. Es el enfoque que sigue World of Warcraft, modificando las clases antiguas para asegurar que sigan siendo viables en nuevas expansiones. De esta manera, los jugadores perciben que la inversión realizada en contenido antigua mantiene su valor.
Soluciones al problema

Conclusión

Para mi gusto, el enfoque más sencillo es el seguido por Fortnite, de limitarse a vender contenidos cosméticos. Al no afectar a la jugabilidad, no requiere un esfuerzo de análisis y de revisión del contenido con el paso del tiempo. Aunque también es verdad que la mecánica de juego de Fortnite es tan sencilla, y su marco escenográfico tan plano, que puede permitirse la heterogeneidad loca de personajes que se dan en sus batallas (que más bien parecen un extracto de Ready Player One). 

Sin embargo, otros juegos basados en mundos con una ambientación mucho más marcada, como pueden ser los de World of Warcraft o Star Wars, probablemente no tengan tanto margen para ofrecer heterogeneidad visual sin que la cosa degenere en un delirio estético en el que no se reconozca el mundo en el que quieren sumergirnos.

Se trata por tanto de estudiar nuestro juego y ver hasta donde podemos llegar con cada una de las estrategias anteriores. La solución no tiene por qué ser apostar por una sola de las estrategias, sino que podemos optar por enfoques mixtos. El peso que le demos a cada una de las estrategias en nuestra mezcla final dependerá del marco argumental de nuestro juego, de nuestra disponibilidad de creativos para crear contenidos visuales, y de analistas y programadores para revisar y ajustar aquellos contenidos que pudieran afectar a la mecanica de juego.

12 julio 2025

Uso de temporizadores en Unity y Godot

Portada del artículo
Los temporizadores (o timers en inglés) en el desarrollo de videojuegos son herramientas o mecanismos que permiten medir y controlar el tiempo dentro del juego. Se utilizan para gestionar eventos, acciones o comportamientos que dependen del tiempo, ya sea en tiempo real o en ciclos de actualización del juego (frames).

Son una herramienta fundamental para controlar la lógica temporal en videojuegos, desde mecánicas básicas hasta sistemas complejos, y por tanto son esenciales en muchos aspectos del diseño y desarrollo de videojuegos. 

Son muchas sus utilidades y casos de uso. Veamos algunos de los ejemplos más destacados:

  • Control de eventos temporales: Como, por ejemplo, ejecutar acciones después de un tiempo específico, como activar una animación, generar enemigos (spawning), o mostrar un mensaje en pantalla.
  • Mecánicas de juego basadas en tiempo: Por ejemplo, implementar límites de tiempo para completar una misión, resolver un puzzle o sobrevivir a una oleada de enemigos.
  • Sincronización de animaciones y efectos: Para coordinar animaciones para que ocurran en el momento adecuado, como explosiones, transiciones o efectos visuales.
  • Gestión de cooldowns: Para controlar el tiempo de reutilización (cooldown) de habilidades, ataques especiales, recarga de armas o acciones del jugador.
  • Frecuencia de actualización de sistemas: Con el fin de regular la frecuencia con la que ciertos sistemas (como la IA de enemigos o la generación de objetos) se actualizan para optimizar el rendimiento
  • Eventos cíclicos o repetitivos: Para crear patrones que se repiten en intervalos regulares, como oleadas de enemigos, cambios climáticos en el juego o ciclos día/noche.
  • Sincronización en multijugador: Se usan para asegurar que los eventos en juegos multijugador estén sincronizados entre todos los clientes, como el inicio de una partida o la actualización de estados.
  • Efectos de retardo o espera: Con ellos se busca introducir pausas intencionadas para mejorar la jugabilidad o la narrativa, como esperar antes de mostrar un diálogo o activar un evento.

Teniendo en cuenta lo anterior, podemos distinguir los siguientes tipos de temporizadores (no excluyentes entre si):

  • Ligados al tiempo de juego: Su avance trascurre al mismo ritmo que el tiempo del juego. Por ejemplo, si el tiempo del juego está ralentizado, por encontrarse en tiempo-bala, entonces el avance del temporizador se ralentizará de igual manera.
  • Ligados al tiempo real: Toman su referencia del reloj del sistema y avanzarán de manera uniforme independientemente de que el tiempo de juego trascurra más lento.
  • Cuenta regresiva: Comienzan con un valor y disminuyen hasta cero, desencadenando un evento al finalizar.
  • Temporizadores cíclicos: Se reinician automáticamente para eventos repetitivos, como disparos automáticos o regeneración de vida.

Su implementación general es sencilla. Un temporizador no es más que una variable o sistema que registra el paso del tiempo, ya sea en segundos, milisegundos o ciclos de juego (frames). En la práctica, suelen implementarse como contadores que se actualizan en cada iteración del bucle del juego (game loop) o mediante funciones específicas proporcionadas por los motores de juego. Vamos a ver, algunas sus posibles implementaciones.

Implementación general

Todos los engines ofrecen una función que se ejecuta periódicamente. Unreal la llama Tick(), Unity Update() y Godot C# _Process(). A esas funciones se le suele pasar un valor, denominado deltaTime, que suele ser transcurrido entre esa llamada a la función y la anterior.
Suponiendo que llamásemos a esa función Actualizar(), podríamos utilizarla para crear un temporizador con el siguiente pseudocódigo:


Pseudocódigo para implementar un temporizador
Pseudocódigo para implementar un temporizador

Muchos engines ofrecen dos versiones diferentes de esa función periódica: una que se ejecuta al renderizarse un nuevo fotograma gráfico, y otra que se ejecuta tras periodos fijos de tiempo. El momento en el que se ejecuta la función que depende de los fotogramas se denomina idle frame, mientras que el momento en el que se ejecuta la función que sigue periodos fijos se denomina physics frame (porque esta última función periódica se usa para cálculos relacionados con la física del juego, mientras que la primera función suele ir más ligada a operaciones visuales).

Ten en cuenta que nuestra tarjeta gráfica no genera fotogramas a un ritmo constante. Por eso, a la hora de implementar temporizadores, es mejor utilizar la función basada en periodos fijos.

Implementación en Godot

Siguiendo su filosofía de "un nodo para cada tarea", Godot cuenta con un nodo temporizador especializado que, ¡oh, sorpresa!, se llama Timer. Este nodo es ideal, para usar temporizadores sin necesidad de implementarlos desde cero.

El nodo ofrece los siguientes campos para su configuración:

Campos de un nodo Timer
Campos de un nodo Timer


  • Process Callback: Define si queremos que el avance del Timer se contabilice al generar los idle frames o los physics frames, según lo que vimos en el apartado anterior.
  • Wait Time: Es el número de segundos de duración del temporizador.
  • One Shot: Si está marcado el temporizador sólo funcionará una vez y se desactivará. Para que vuelva a contar, habrá que arrancarlo manualmente. Si marcamos esta casilla, el temporizador volverá a empezar desde cero, una vez que alcance el tiempo marcado, repitiendo ese ciclo indefinidamente.
  • AutoStart: Si está marcado, el nodo empezará a contar conforme se introduzca en la jerarquía de la escena principal del juego. De otra manera, el nodo estará desactivado hasta que lo arranquemos manualmente.
  • Ignore Time Scale: Si lo marcamos, el temporizador se ligará al tiempo real, mientras que si lo dejamos sin marcar estará ligado al tiempo de juego.

El nodo ofrece la señal timeout(), la cual se dispara cuando el temporizador alcanza el tiempo marcado en el campo Wait Time.

Señal timeout

Configurando los campos del nodo Timer desde el inspector, y enlazando a su señal timeout() aquel método de otro nodo que queramos ejecutar al finalizar el tiempo, ya tendremos cubierta la mayor parte de los casos de uso de este nodo. 

Este nodo también puede ser manipulado desde el código. Vamos a ver un ejemplo de uso. Imaginemos que queremos programar un cooldown, según el cual una vez que se ejecute un determinado método, este no debe poder ejecutarse hasta que finalice el tiempo del temporizador. Supongamos que ese método se denomina GetSteering() y que se llame desde el método _Process() de otro nodo.

Para arrancar el cooldown, podríamos llamar a una función de activación justo al final del método GetSteering().

Ejemplo de arranque de un cooldown
Ejemplo de arranque de un cooldown

La función de activación del cooldown se llama en este caso StartAvoidanceTimer() y se limita a arrancar la cuenta del nodo Timer y a fijar a verdadero una bandera:

Implementación del método de arranque del cooldown
Implementación del método de arranque del cooldown

Si un Timer no tiene marcado el campo AutoStart, tendremos que llamar a su método Start() para que arranque, como se puede ver en la línea 235.

La bandera fijada a verdadero en la línea 236 se puede usar a modo de guarda al comienzo del método cuya ejecución queramos limitar. 

Uso de la bandera como guarda para controlar si se ejecuta o no el método


El ejemplo de la captura puede ser un poco alambicado si no se conoce el contexto del desarrollo en el que estoy trabajando, pero viene a funcionar calculando la ruta hacia un objetivo (línea 157). Si no hay otro agente con el que podamos chocar, y no hay activo un temporizador, se sigue la ruta calculada (línea 160). Si por el contrario hay un temporizador en marcha, se continua con la ruta de escape que se calculó en la anterior llamada a GetSteering() (línea 162). El método sólo se ejecuta por completo si se ha detectado una potencial colisión y no hay activo un temporizador, en cuyo caso se continua calculando una ruta de escape para evitar la colisión (línea 164 en adelante).

Sin un cooldown, mi agente detectaría una posible colisión por delante de él, calcularía un ruta de escape y giraría para evitar la colisión. El problema sería que en el siguiente fotograma ya no tendría el obstáculo delante de él (al haber girado antes para evitarlo) por lo que volvería a calcular la ruta más corta a su objetivo y giraría para encararlo, encontrándose de nuevo justo enfrente con el obstáculo que pretendíamos evitar. A partir de entonces se repetiría el ciclo. Para evitarlo, se calcula una ruta de escape y esta se sigue durante un tiempo (el de cooldown) para que el agente cambie sensiblemente de posición, evitando la colisión; entonces, y sólo entonces, se vuelve a reevaluar la ruta para alcanzar el objetivo. 

La configuración del Timer se puede hacer desde el código del método _Ready() del nodo del que cuelgue el Timer.

Configuración de un Timer en el método _Ready
Configuración de un Timer en el método _Ready

Ignora las líneas previas a la 83, no tienen nada que ver con el Timer, pero las dejo para que veas un ejemplo real de uso de un Timer.

En la línea 83, cargo una referencia al nodo Timer que cuelga del nodo que ejecuta este script. Con esa referencia podríamos configurar todos los campos disponibles en el inspector del Timer. En mi caso no ha hecho falta, porque no tenía que variar la configuración del Timer que ya había metido en el inspector, pero sí que he enlazado manualmente un método a la señal del Timer (línea 84). En este caso, el método enlazado es OnAvoidanceTimeout(). Podría haberlo enlazado usando la pestaña "Node" del inspector, pero en este caso he preferido hacerlo por código.

El método OnAvoidanceTimeout() no puede ser más sencillo, ya que se limita a poner a falso el flag, para que en el próximo fotograma el método GetSteering() sepa que ya no queda ningún temporizador activo.

Callback enlazado a la señal del Timer
Callback enlazado a la señal del Timer


Implementación en Unity

Unity no cuenta con componente especializado, como el nodo Timer de Godot, pero crearse uno es sencillísimo, haciendo uso de corutinas.

Vamos a ver cómo se podría crear un componente que emulase la funcionalidad ofrecida por el nodo de Godot. Podríamos llamar a nuestro componente CustomTimer:

 

Comienzo de nuestra clase Timer para Unity
Comienzo de nuestra clase Timer para Unity

Los campos que podría ofrecer este componente serían los mismos que los del citado nodo.

Campos de la clase, ofrecidos al inspector
Campos de la clase, ofrecidos al inspector

Con esto, lo que veríamos desde el inspector sería lo siguiente:

Vista desde el inspector de nuestra clase CustomTimer
Vista desde el inspector de nuestra clase CustomTimer

Si autoStart estuviera fijado a verdadero, el componente se limitaría a llamar al método StartTimer(), desde el Awake(), conforme se arrancase el script.

Arranque del temporizador
Arranque del temporizador

El método llamado en la línea 50 del método Awake se limita a arrancar una corutina y a guardar una referencia a ella para poder controlarla desde fuera (línea 55).

El cuerpo de la corutina lanzada es muy sencillo:

Cuerpo de la corutina del Timer
Cuerpo de la corutina del Timer

La corutina se limita a esperar los segundos que hayamos fijado en el waitTime. En función del tipo de tiempo que queramos usar (tiempo de juego o tiempo real) se esperan segundos de juego (línea 65) o segundos de tiempo real (línea 68).

Pasado ese tiempo, se lanza el evento de timeout (línea 72).

Si el temporizador es de un sólo uso se finaliza la corutina (línea 74) o, en caso contrario, se vuelve a repetir el ciclo (línea 60).

¿Para qué sirve la referencia que guardamos en la línea 55 del método StartTimer()? Pues sirve para parar la corutina antes de tiempo, si fuese necesario, usando el método StopCoroutine.

Código para la parada anticipada del temporizador
Código para la parada anticipada del temporizador

Un GameObject que tuviese un componente CustomTimer como el descrito podría hacer uso él, desde otro script de GameObject, sacando una referencia al componente:

Obtención de una referencia a CustomTimer
Obtención de una referencia a CustomTimer

A partir de esa referencia, el uso del Timer sería calcado al que ya vimos con el nodo de Godot.

Uso del componente CustomTimer

Como el ejemplo es la versión en Unity de lo ya visto para Godot, no me voy a extender más.

Implementación en C#

¿Qué tienen en común Godot C# y Unity? Efectivamente, su lenguaje de programación: C#. Este lenguaje es multipropósito y viene con "pilas incluidas", es decir con un montón de módulos y librerías para hacer múltiples cosas. Uno de esos módulos ofrece un temporizador con funcionalidades muy similares a las ya vistas. 

Si C# ya ofrece un temporizador, ¿por qué es tan habitual ver implementaciones en Godot y en Unity como las de antes? ¿por qué no usan directamente el temporizador de C#?

Creo que la respuesta más sencilla es para el caso de Godot. El lenguaje por defecto de Godot es GDScript y este no ofrece viene con "pilas incluidas" o, mejor dicho, estas son los miles de nodos que ofrece Godot. Por eso, Godot C# hereda el acceso al nodo Timer gracias a su disponibilidad para el código en GDScript.

El caso de Unity es más complicado de responder y creo que se debe al desconocimiento de lo que ofrece C# por si mismo. Además, ya hemos visto que crear un temporizador personalizado es muy sencillo si usamos un mecanismo tan familiar para los desarrolladores de Unity como lo son las corutinas. Creo que esa sencillez para crearse uno mismo su propio temporizador ha hecho que muchos no llegasen a indagar y a conocer lo que le ofrecía C# en este sentido.

El temporizador que ofrece C# es System.Timers.Timer y su uso es muy similar al que ya hemos visto en el caso de Unity y Godot.

Uso del temporizador nativo de C#
Uso del temporizador nativo de C#

En la línea 103 de la captura se puede ver que hay que pasarle a su constructor el tiempo que queremos que dure el temporizador. 

El campo AutoReset de la línea 105 es completamente equivalente Autostart del nodo de Godot.

En la línea 106 se puede ver que el evento que lanza este temporizador es Elapsed (equivalente a la señal timeout del nodo de Godot). A dicho evento se pueden subscribir callbacks (como OnTimerTimeout) de manera similar a como hacíamos en las versiones anteriores para Godot y Unity.

Por último, este temporizador de C# también tiene métodos Start() (línea 117) y Stop(), para arrancarlo y pararlo.

La única precaución que hay que tener con este temporizador es que nunca debemos llamar a su método Start() si no estamos seguros de que haya acabado la cuenta anterior. Si lo que necesitamos es iniciar una nueva cuenta sin que haya acabado la anterior, primero deberemos llamar a Stop() y luego Start(). El problema es que este temporizador está optimizado para ser utilizado en entornos multihilo, así que cuando llamas a varios Start() seguidos no va finalizando las cuentas anteriores, sino que las va acumulando en hilos distintos, haciendo que las cuentas continúen en paralelo y que los eventos vayan saltando conforme dichas cuentas vayan finalizando (aunque el efecto visible será que los eventos saltarán sin una periodicidad evidente, por ser de cuentas diferentes).

Conclusión

Si programas en C# tanto en Godot, como en Unity, hay muy pocos argumentos para no utilizar el temporizador de C#. Es una versión ligera, eficiente y, al ser multiplataforma, te permitirá reutilizar código entre Unity y Godot con mayor facilidad.

La única razón que se me ocurre para seguir utilizando el nodo de Godot es para que pueda ser manipulado desde el inspector al integrarse con otros elementos del juego; pero aparte de eso, no veo la utilidad de seguir usando ni el nodo de Godot, ni los temporizadores basados en corutinas de Unity.