Objetivo: escribir Lua para PZ sabiendo qué dialecto es, qué falta respecto del Lua "normal", cómo se organiza un mod de verdad (namespace, clases, eventos) y qué patrones usa el propio juego. Ya sabes programar: aquí solo va lo que cambia.
1. Qué Lua es este
PZ no usa el intérprete oficial de Lua. Usa Kahlua: una implementación de Lua 5.1 escrita en Java que corre dentro de la JVM del juego. Consecuencias:
| Tema | En PZ | Por qué importa |
|---|---|---|
| Versión | Lua 5.1 | No hay goto (el juego tiene un comentario quejándose: server/metazones/metazoneHandler.lua:92), ni //, ni operadores de bits nativos, ni table.unpack (es unpack) |
| Números | Todos son double de Java | 3 / 2 == 1.5; para enteros usa math.floor. Los IDs grandes pierden precisión |
os, io | No existen (ninguna línea del juego los usa) | Para archivos: getFileWriter / getFileReader (sección 5) |
loadstring, dofile | No se usan | El código se carga solo con require y la carga del mod |
| Velocidad | Interpretado sobre la JVM | Barato llamar a Java; caro hacer bucles grandes en Lua cada tick (módulo 14) |
| Errores | Los atrapa Kahlua y los muestra en el contador rojo (debug) y en console.txt | Un error dentro de un evento no detiene el juego, pero corta tu función a la mitad |
Lo que sí está y usa el juego: string.format (65 archivos), setmetatable (451), unpack (18), select("#", ...), pcall, closures, varargs.
2. Funciones globales que vienen de Java
Muchas funciones "globales" no están definidas en ningún .lua: las pone Java en el entorno de Lua. Las que vas a usar desde el primer día:
| Función | Qué hace |
|---|---|
getPlayer() / getSpecificPlayer(n) | El jugador local (o el número n en pantalla dividida) |
getText("Clave") | Traducción |
ZombRand(n), ZombRandFloat(a, b) | Aleatorios del juego (480 usos). Úsalos en vez de math.random |
getTimestampMs() | Reloj en milisegundos |
isClient(), isServer() | En qué lado corre el código (882 usos; módulo 12) |
instanceof(obj, "IsoPlayer") | Pregunta la clase Java de un objeto (221 usos) |
getTexture(nombre), getCore(), getGameTime(), getCell(), getWorld() | Acceso al motor |
triggerEvent("OnX", ...) | Dispara un evento a mano |
En el módulo 07 aprendes a sacar la lista completa de estas funciones del .jar.
3. Organizar un mod: el patrón de nuestros mods
Todos nuestros mods con varios archivos siguen el mismo patrón, y es el correcto:
-- shared/CursoLab_Core.lua
CursoLab = CursoLab or {} -- una sola tabla global por mod, creada de forma idempotente
local CL = CursoLab -- alias local: más corto y más rápido
CL.CONFIG = { maxSopaipillas = 4 }
local function helperPrivado(x) -- todo lo que no es API va como local
return x * 2
end
function CL.api(x)
return helperPrivado(x)
end- Una tabla global con el nombre del mod. Nunca variables globales sueltas: el entorno global es compartido con el juego y con todos los mods.
X = X or {}permite que varios archivos del mod agreguen cosas a la misma tabla sin importar el orden, y que recargar con F11 no borre el estado.localpara todo lo demás: en Kahlua una variable local es más rápida que una global (que es una búsqueda en tabla).
4. Clases: derive y :new
El juego no tiene clases: tiene un patrón con metatablas en shared/ISBaseObject.lua, que es corto y vale la pena leer completo:
function ISBaseObject:derive(type)
local o = {}
setmetatable(o, self)
self.__index = self
o.Type = type
o.SuperType = self
return o
endTodo lo que es "clase" en el Lua del juego (ISPanel, ISBaseTimedAction, ISContextMenu) sale de aquí. Una clase tuya:
CursoLab.Contador = ISBaseObject:derive("CursoLabContador")
local Contador = CursoLab.Contador
function Contador:new(inicio)
local o = ISBaseObject.new(self)
o.n = inicio or 0
return o
end
function Contador:sumar(k)
self.n = self.n + (k or 1)
return self.n
endPara llamar al método del padre se hace explícito: ISPanel.render(self). No hay super.
Dos instanceof distintos: instanceof(obj, "IsoZombie") (global, para objetos Java) y ISBaseObject:instanceof(obj, "ISPanel") (para tablas Lua con campo Type). Confundirlos es un clásico.
5. Eventos
El 90 % de un mod es reaccionar a eventos:
local function onEveryTen()
CursoLab.log("diez minutos de juego")
end
Events.EveryTenMinutes.Add(onEveryTen)
-- y para sacarlo: Events.EveryTenMinutes.Remove(onEveryTen)- Cada evento pasa argumentos distintos. No los adivines: busca en el juego un
Events.X.Adddel mismo evento y mira la firma. La referencia del sitio (/referencia/eventos.html) lista dónde los usa el juego. - Eventos propios: el juego los crea así (
server/Foraging/forageServer.lua:497):
LuaEventManager.AddEvent("OnForageSpot")
-- ...
triggerEvent("OnForageSpot", arg1, arg2)Así otros mods (o tus propios archivos) se enganchan sin depender de tus funciones internas.
- Protege tus handlers con
pcallsi hacen algo arriesgado. Wanaco, uno de nuestros mods, lo hace en cada comando de servidor: un error en un comando no tumba el listener.
6. Archivos: getFileWriter / getFileReader
Como no hay io, el juego usa (en shared/Foraging/forageSystem.lua:681):
local w = getFileWriter("forageMissingItems.log", true, false) -- (nombre, crear si no existe, agregar al final)
w:write("texto\n")
w:close()Los archivos quedan en ~/Zomboid/Lua/. Sirven para logs propios, exportar datos o configuración local. Para datos que tienen que viajar con la partida o sincronizarse en multijugador se usa ModData (módulos 12 y 14).
7. La librería del juego que puedes usar
No reinventes: el juego trae utilidades en Lua.
shared/luautils.lua:luautils.split,luautils.round,luautils.tableContains,luautils.walkAdj(caminar al lado de algo antes de una acción)...shared/Util/:LuaTableUtil,StringReplacer,AdjacentFreeTileFinder...
Úsalas con require "luautils" o require "Util/LuaTableUtil" si las necesitas antes de que carguen.
Ejercicios
Ejercicio 6.1 · El dialecto
En la consola Lua, comprueba y anota el resultado de:
print(7 / 2, math.floor(7 / 2), 2^53 + 1)print(type(getPlayer()), tostring(getPlayer()))print(coroutine, os, io, bit)print(pcall(function() error("boom") end))
Listo cuando: puedes explicar por qué type(getPlayer()) no dice "table".
Pista para la 2
getPlayer() devuelve un objeto Java (IsoPlayer) envuelto para Lua: Kahlua debería reportarlo como userdata (compruébalo). No es una tabla: no puedes ponerle campos (getPlayer().x = 1 falla). Para guardar datos en un objeto Java se usa obj:getModData(), que sí devuelve una tabla.
Ejercicio 6.2 · Una clase propia
Escribe CursoLab.Bitacora, una clase derivada de ISBaseObject que guarde los últimos N eventos (con hora de juego: getGameTime():getWorldAgeHours()) y que tenga :agregar(texto), :ultimos(k) y :volcar() (este último escribe todo a ~/Zomboid/Lua/CursoLab_bitacora.log con getFileWriter).
Listo cuando: después de jugar 10 minutos tienes un log con lo que pasó.
Ejercicio 6.3 · Escuchar el juego
Usando la Bitácora, registra:
- cuando el jugador come algo (pista: busca en
ISEatFoodAction.luaqué pasa al terminar; ¿hay un evento o tienes que engancharte a la acción?), - cuando sube de nivel una habilidad (busca un evento con
LeveloPerken la referencia), - cuando mata un zombi.
Listo cuando: los tres aparecen en la bitácora. Para el primero quizás no haya evento: anota cómo lo resolviste (el módulo 09 te da la herramienta limpia).
Ejercicio 6.4 · Un evento propio
Crea el evento OnCursoLabSopaipilla que se dispara cuando alguien come una sopaipilla, y engánchate a él desde otro archivo de CursoLab para mostrar un mensaje sobre la cabeza del jugador (getPlayer():Say("...")).
Listo cuando: el archivo que escucha no llama a ninguna función interna del que dispara.
Ejercicio 6.5 · Leer Lua del juego
Lee completo shared/TimedActions/ISEatFoodAction.lua (o el que te tocó en el 6.3) y escribe, en 10 líneas, qué hace cada función y en qué orden las llama el juego.
Desafío 6
Escribe un "inspector" para la consola: CursoLab.dump(valor, profundidad) que imprima tablas de forma legible (con ciclos detectados) y, si el valor es un objeto Java, imprima su clase. Lo vas a usar en todo el resto del curso. (El juego tiene uno en client/DebugUIs/ObjectViewer.lua: mira cómo lista los campos de un objeto Java con getNumClassFields y getClassFieldVal.)
Anterior: 05 · Modelos 3D · Siguiente: 07 · La frontera Lua ↔ Java