Saltar al contenido principal

Integrar Lua para programar la lógica del juego

· 6 min de lectura
Desarrollador de Unity y creador de Polygon Engine

Quería que los usuarios pudieran programar el comportamiento de sus juegos sin modificar ni recompilar el código del engine. Una puerta, por ejemplo, necesita reaccionar a la entrada, recordar su estado, reproducir un sonido y actualizar un mensaje. Lua me permitía ofrecer una forma de conectar esas acciones desde el proyecto.

Al integrarlo, tuve que decidir qué trabajo quedaría en los scripts y qué sistemas seguirían implementados en código nativo. La facilidad para probar una idea era importante, pero también necesitaba controlar el uso de memoria y el tiempo de ejecución en PS1. Esa división define buena parte de la API de Polygon Engine.

Repartir el trabajo entre scripts y sistemas nativos​

Para una puerta, el script puede decidir si el jugador tiene permiso de abrirla, elegir su siguiente estado, solicitar movimiento y actualizar la interfaz. Esas decisiones están muy relacionadas con las reglas de ese juego en particular.

Los cálculos que se repiten sobre muchos datos, como transformar vértices o consultar geometría de colisión, conviene resolverlos en sistemas nativos con estructuras preparadas para ese trabajo. La API permite utilizarlos desde Lua. Así, el script puede solicitar una acción sin implementar de nuevo los cálculos que ya realiza el engine.

Aun así, una llamada nativa puede requerir bastante trabajo. Si una función realiza una búsqueda amplia y el script la invoca en cada actualización, su costo se acumula. Por eso, al revisar un comportamiento, necesito considerar tanto la frecuencia de las llamadas como lo que hace cada función.

El manual de Lua explica el modelo de integración del lenguaje. Polygon Engine proporciona la API del juego y decide qué capacidades están disponibles en ese entorno. Un script escrito para otra aplicación con Lua no tiene automáticamente las mismas librerías ni funciones del engine aquí.

El ciclo de vida de los scripts​

Para organizar los comportamientos utilicé callbacks que resultan familiares si se viene de Unity. Awake se encarga de la preparación inicial, Start se ejecuta antes de la primera actualización y Update se utiliza para el comportamiento de cada cuadro. FixedUpdate trabaja con el paso de simulación acotado de 30 Hz del engine.

Ese paso fijo define cómo avanza la simulación, pero no garantiza que el juego dibuje treinta cuadros por segundo con cualquier carga. Si la máquina se retrasa, intentar recuperar todas las actualizaciones pendientes puede empeorar el problema. Por eso el engine limita la cantidad de trabajo de recuperación.

También busco que los scripts actualicen cada sistema cuando hace falta. Al abrir una puerta, su sonido debería activarse una vez; una etiqueta solo necesita cambiar cuando cambia el mensaje. Repetir esas operaciones en cada cuadro consume recursos y puede producir errores de audio o interfaz difíciles de relacionar con el script que los provoca.

La introducción a scripting documenta los callbacks y las propiedades expuestas. Esa estructura me permite separar la inicialización de las actualizaciones y de los eventos de interacción. Al volver a un script, resulta más fácil encontrar dónde se configura el estado inicial y qué funciones lo modifican durante el juego.

Controlar el uso de memoria y la ejecución​

El manejo de memoria también necesita atención. Cada tabla temporal requiere espacio y después debe ser liberada por el recolector de Lua. Si un script reconstruye tablas o genera cadenas de diagnóstico en cada actualización, esas asignaciones se repiten durante toda la partida y pueden afectar al rendimiento.

Cuando un comportamiento lo permite, prefiero preparar sus datos al inicializarlo y reutilizarlos durante el juego. Algunas funciones necesitan crear valores nuevos, así que la intención no es prohibir las asignaciones. Lo que reviso es si ese trabajo realmente necesita repetirse con tanta frecuencia.

Polygon Engine establece un límite para la memoria dinámica de Lua. Si un script lo alcanza, el error debe ayudar a localizar el comportamiento que está consumiendo demasiado. Además de contener el problema, necesito que la información permita corregirlo desde el proyecto.

El engine también limita las instrucciones para contener ciclos de Lua que no terminan o ejecuciones excesivas. Esa protección no sustituye la medición de rendimiento: el número de instrucciones no representa directamente el tiempo transcurrido, y las llamadas nativas pueden realizar trabajo adicional. Son dos aspectos que hay que revisar por separado.

Preparar el bytecode de Lua para PS1​

Durante la compilación del juego se prepara el bytecode de Lua que utilizará PS1. Esto evita compilar el código fuente en la consola y permite que la versión de distribución prescinda del analizador completo. El resultado sigue ejecutándose en la máquina virtual de Lua; no se convierte en instrucciones MIPS nativas.

Para que funcione, el bytecode debe generarse con la versión de Lua y la configuración numérica que espera el entorno de ejecución. Un archivo compilado con una instalación cualquiera de Lua en la computadora podría no ser compatible. Por eso la preparación del bytecode forma parte del proceso de compilación del engine.

La configuración numérica también influye en el rendimiento. La CPU de PS1 no tiene una unidad de punto flotante por hardware, aunque puede realizar esas operaciones mediante software. Son operaciones posibles, pero su costo importa cuando se repiten muchas veces. Los sistemas geométricos pueden utilizar punto fijo y gestionar las conversiones al recibir datos desde la API de scripting.

Conectar los scripts con una interacción jugable​

El tutorial de dos habitaciones permite revisar esa integración en un caso pequeño. Conecta la entrada, un trigger, una puerta móvil, sonido, mensajes de interfaz y progreso guardado. Cada parte tiene una función concreta y se puede seguir el recorrido desde el evento del jugador hasta el cambio que aparece en pantalla.

Al revisar un script así, puedo identificar qué funciones de la API facilitan la interacción y dónde todavía hay trabajo innecesario. Si hace falta repetir mucho código para una operación habitual, conviene revisar qué debería resolver el engine. Si una llamada consume demasiado, necesito reducir su frecuencia o mejorar el sistema que la implementa.

Integrar Lua me permitió acercarme al flujo de trabajo que quería: cambiar las reglas de una interacción desde el proyecto y probarlas rápidamente. Para mantener esa comodidad en PS1, la API y los sistemas nativos necesitan resolver bien el trabajo repetitivo, mientras los scripts se concentran en el comportamiento de cada juego.