Objetivo: guardar datos de tu mod que sobrevivan a cerrar el juego (en el lugar correcto), y escribir código que corre seguido sin bajar los FPS. Las dos cosas tienen que ver con lo mismo: entender qué cuesta cada llamada.
1. Dónde guardar datos
| Necesitas guardar… | Dónde | Se guarda con… |
|---|---|---|
| Algo de un objeto (un ítem, un mueble, un zombi, el jugador) | obj:getModData() | El objeto, en la partida |
| Algo del mod en general (contadores, registros, estado del mundo) | ModData.getOrCreate("CursoLab") | global_mod_data.bin en la carpeta de la partida |
| Algo de esta instalación, no de la partida (preferencias, posición de una ventana) | getFileWriter / getFileReader | Un archivo en ~/Zomboid/Lua/ |
| Algo que solo dura esta sesión | Una tabla de Lua normal | Nada: se pierde al salir |
Qué se puede guardar
getModData() y ModData devuelven tablas de Lua que Java serializa con KahluaTableImpl.save. Solo se guardan:
- claves de texto o número;
- valores de texto, número, booleano o tabla (anidadas).
Cualquier otra cosa (un IsoPlayer, un ítem, una función) se descarta en silencio al guardar: el método canSave la salta. Para "recordar" un objeto, guarda algo que lo identifique: el getID() de un ítem, el nombre de usuario de un jugador, las coordenadas de un objeto del mundo.
getModData() crea la tabla
Esto es IsoObject.getModData() en el bytecode:
1: getfield table
4: ifnonnull 17 // si ya hay tabla, la devuelve
11: J2SEPlatform.newTable()
14: putfield table // si no, CREA una y la guarda en el objeto
18: getfield table
21: areturnLlamar getModData() solo para preguntar si un objeto tiene tus datos le crea una tabla vacía a cada objeto que miras. Para preguntar existe hasModData() (devuelve true solo si la tabla existe y no está vacía). Un mod nuestro sin publicar (WarFactions) lo hace bien en WowFactions.lua:
-- Camino caliente: OnZombieUpdate llama esto para CADA zombi del mapa en CADA
-- frame. hasModData() es una consulta barata y descarta de inmediato a los zombis
-- de vanilla (no tienen modData) sin crear la tabla con getModData().
if actor.hasModData ~= nil then
local okH, has = pcall(actor.hasModData, actor)
if okH and has == false then return nil end
endNombres y versiones
- Pon prefijo a tus claves:
md.CursoLab_nivel, o mejor una subtablamd.CursoLab = { nivel = 2 }. El ModData de un objeto lo comparten todos los mods. - Guarda una versión de formato:
md.CursoLab.v = 2. Cuando cambies cómo guardas los datos, una partida vieja trae el formato anterior y tu código tiene que migrarlo (módulo 17).
ModData global: cuándo existe
El juego dispara OnInitGlobalModData(isNewGame) cuando carga los datos globales (en GlobalModData.init, con WorldDictionary.isIsNewGame()). Es el momento de crear tus tablas o migrarlas:
Events.OnInitGlobalModData.Add(function(isNewGame)
local d = ModData.getOrCreate("CursoLab")
d.v = d.v or 1
d.sopaipillasComidas = d.sopaipillasComidas or 0
end)2. Qué cuesta en Kahlua
Kahlua es un intérprete sobre la JVM: cada línea de Lua hace bastante trabajo de Java por debajo. Lo que más cuesta, de mayor a menor:
- Hacer algo en todos los objetos de algo, seguido. Recorrer todos los zombis o todos los cuadros cargados cada tick.
- Crear basura: tablas nuevas, strings concatenados (
"a" .. x), closures dentro de un bucle. Todo eso lo tiene que limpiar el recolector de Java, y las pausas se notan como tirones. - Llamadas a Java dentro de bucles grandes: cada
obj:metodo()es una búsqueda del método más una llamada por reflexión. Guarda el resultado en una variable local si lo usas varias veces. - Variables globales: cada acceso es una búsqueda en una tabla.
locales más rápido.
3. Elegir el evento correcto
| Evento | Frecuencia | Usos en el juego |
|---|---|---|
OnZombieUpdate | cada zombi cargado, cada frame | 0 |
OnTick, OnPlayerUpdate, OnRenderTick | cada frame (o cada frame por jugador) | 22, 3, 3 |
EveryOneMinute | cada minuto de juego | 2 |
EveryTenMinutes, EveryHours, EveryDays | cada 10 minutos, hora o día de juego | 7, 4, 4 |
Eventos de cosas (OnFillContainer, OnClientCommand, OnEat...) | solo cuando pasan | la mayoría |
El juego base no usa OnZombieUpdate ni una vez, y OnTick solo 22 veces en miles de archivos. Si tu idea necesita OnTick, primero busca un evento de la cosa que te interesa (la referencia de eventos del sitio te ayuda). Si de verdad necesitas revisar algo seguido:
Limitar la frecuencia (como en Wanaco, módulo 12):
local last = 0
Events.OnTick.Add(function()
local now = getTimestampMs()
if now - last < 1000 then return end -- una vez por segundo como mucho
last = now
...
end)Repartir el trabajo: si tienes que revisar 300 cosas, revisa 20 por tick en vez de 300 cada 15 ticks. El costo total es el mismo, pero sin picos.
Salir lo antes posible: en los eventos calientes, la primera línea debería descartar el 99 % de los casos con la comprobación más barata. WarFactions revisa cada 15 frames (Fence.observeEvery = 15) y descarta a los zombis normales con hasModData() antes de hacer nada más.
Tablas débiles para no tener fugas
Si guardas datos en una tabla indexada por objeto Java (cache[zombi] = ...), esa tabla mantiene vivo al objeto aunque el juego lo borre: en una partida larga crece sin límite. WarFactions lo resuelve con claves débiles:
Fence.observeSkip = setmetatable({}, { __mode = "k" }) -- si el zombi deja de existir, su entrada se va sola4. Medir antes de optimizar
- Cronómetro manual:
getTimestampMs()antes y después, acumulado y escrito cada N llamadas. Es lo más simple y suele alcanzar. - Eventos lentos: en las opciones de debug (
Checks→SlowLuaEvents), el juego mide cada función registrada a un evento y avisa en la consola conSLOW Lua event callback <archivo> <función> <ms>mssi una tarda más de 250 ms (lo viste enzombie.Lua.Event.trigger, módulo 09). 250 ms es muchísimo: sirve para encontrar desastres, no para afinar. - Perfilador del juego:
Event.triggerenvuelve cada evento enGameProfiler.profile("Lua - <evento>"). Busca en el menú de debug cómo ver elGameProfiler(opciónGameProfiler.Enabled).
Ejercicios
Ejercicio 14.1 · Una nevera que recuerda
Haz que cada refrigerador recuerde cuántas veces lo abrió el jugador, y muéstralo en el menú contextual del refrigerador. Usa ModData del objeto, sincronízalo (módulo 12) y comprueba que sobrevive a salir y volver a entrar.
Listo cuando: el número persiste, y mirar refrigeradores que nunca abriste no les crea ModData (compruébalo con hasModData()).
Ejercicio 14.2 · Lo que no se guarda
Guarda en el ModData del jugador una tabla con un número, un texto, una subtabla, un ítem y una función. Sal de la partida, vuelve a entrar y revisa qué quedó. Explícalo con la lista de tipos de KahluaTableImpl.
Ejercicio 14.3 · Medir un evento caliente
- Registra una función en
OnZombieUpdateque haga algo moderado (por ejemplo, leer la distancia al jugador y guardarla en una tabla). - Mide con
getTimestampMs()cuánto tiempo total consume por segundo con 10, 100 y 500 zombis (usa el menú de debug para generarlos). - Optimízala: salida temprana, menos llamadas a Java, revisar solo cada N frames. Mide de nuevo.
Listo cuando: tienes una tabla antes/después y puedes decir cuántos zombis aguanta cada versión sin bajar de 60 FPS.
Ejercicio 14.4 · La fuga
Escribe a propósito una caché cache[zombi] = true sin claves débiles, recorre el mapa un rato para que se carguen y descarguen zombis, e imprime cuántas entradas tiene. Repite con __mode = "k". ¿Qué diferencia ves? (Pista: forzar al recolector no es fácil desde Kahlua; deja pasar tiempo.)
Ejercicio 14.5 · Migración de formato
Tu ModData global de CursoLab guardaba { comidas = 5 } (versión 1). Ahora quieres { v = 2, comidas = { total = 5, porJugador = {} } }. Escribe la migración en OnInitGlobalModData y pruébala con una partida guardada con la versión 1.
Desafío 14
Un perfilador para tus propios mods: CursoLab.prof(nombre, fn) envuelve una función, acumula llamadas y tiempo total, y una ventana (módulo 10) muestra la tabla ordenada por tiempo, actualizada cada segundo. Úsalo para encontrar la función más cara de un mod grande.
Anterior: 13 · Loot, sandbox y traducciones · Siguiente: 15 · Depuración en serio