Objetivo: escribir y ejecutar tus primeras líneas de Lua dentro de Project Zomboid, en un archivo de mod y en la consola de debug, y entender desde el primer día por qué casi todo lo que escribas debe ser local.
1. Dónde corre tu Lua
Project Zomboid trae su propio Lua: se llama Kahlua, es Lua 5.1 escrito en Java y corre dentro del juego. No necesitas instalar nada. Hay dos lugares donde vas a escribir Lua:
| Lugar | Cuándo corre | Para qué sirve |
|---|---|---|
Un archivo .lua de tu mod | Cuando el juego carga los mods (y después, cuando ocurren los eventos a los que te enganchas) | El código "de verdad" de tu mod |
| La consola Lua del modo debug | En el momento en que apretas Enter | Probar algo rápido, inspeccionar el mundo, experimentar |
Durante todo el curso trabajas en un mod de práctica llamado CursoLab. La forma mínima es esta (el módulo 00 · Entorno de trabajo la explica completa, con el modo debug y los logs):
~/Zomboid/Workshop/CursoLab/
└── Contents/mods/CursoLab/
└── 42/
├── mod.info
└── media/lua/
├── shared/
├── client/
└── server/mod.info mínimo:
name=Curso Lab
id=CursoLab
description=Mod de práctica del curso.
versionMin=42.0.0Para abrir la consola Lua necesitas lanzar el juego con la opción -debug (en Steam: Propiedades, Opciones de lanzamiento). En una partida, la tecla ` `` (a la izquierda del 1) abre la consola.
2. print: tu primera línea
print escribe texto en el log del juego (~/Zomboid/console.txt) y, si estás en la consola, también ahí.
Crea el archivo 42/media/lua/client/CursoLab_L01.lua:
print("[CursoLab] hola desde el archivo")Activa CursoLab en la lista de mods, entra a una partida y busca en console.txt la línea [CursoLab] hola desde el archivo. El prefijo [CursoLab] no es obligatorio, pero te permite filtrar tus mensajes entre los miles que escribe el juego. Úsalo siempre.
print acepta varios valores separados por coma y los escribe en la misma línea:
print("[CursoLab]", "uno", 2, true, nil)Ahora prueba lo mismo en la consola (` ``):
print("[CursoLab] hola desde la consola")
print(1 + 2)-- 42/media/lua/client/CursoLab_L01.lua
print("[CursoLab] archivo cargado")
Events.OnGameStart.Add(function()
print("[CursoLab] empezó la partida")
print("[CursoLab] jugador:", getPlayer():getFullName())
end)El primer mensaje sale al cargar el archivo. El segundo y el tercero salen cuando la partida termina de cargar, y para entonces getPlayer() ya devuelve tu personaje.
3. Comentarios
Todo lo que va después de -- hasta el fin de la línea es un comentario: Lua lo ignora.
-- Esto es un comentario de una línea.
print("[CursoLab] sí se ejecuta") -- y esto también es comentario
--[[
Esto es un comentario de varias líneas.
Sirve para explicar un archivo o para "apagar" un bloque de código mientras pruebas.
print("esto no se ejecuta")
]]El juego usa los dos tipos. media/lua/shared/Foraging/forageSystem.lua empieza con un comentario largo --[[ MODDING FAQ: ... que explica a los modders cómo agregar ítems al forrajeo.
4. Variables
Una variable es un nombre que guarda un valor. Se crea la primera vez que le asignas algo:
local nombre = "Sopaipilla"
local cantidad = 4
local cocinada = true
print("[CursoLab]", nombre, cantidad, cocinada)
cantidad = cantidad + 1 -- cambiar el valor: sin "local" otra vez
print("[CursoLab] ahora hay", cantidad)Reglas de los nombres:
- Letras, dígitos y
_, y no pueden empezar con un dígito:zombisMuertos,max_peso,item2. - Mayúsculas y minúsculas son distintas:
cantidadyCantidadson dos variables. - No puedes usar palabras reservadas como nombre:
and,break,do,else,elseif,end,false,for,function,if,in,local,nil,not,or,repeat,return,then,true,until,while. - Evita las tildes y la ñ en los nombres. Úsalas solo dentro de los textos (
"Ñandú"está bien;local añono).
Puedes asignar varias variables a la vez. Si sobran nombres, los que no reciben valor quedan en nil:
local x, y, z = 10, 20
print(x, y, z) -- 10 20 nil
x, y = y, x -- intercambio en una línea
print(x, y) -- 20 105. Tipos: type()
Cada valor tiene un tipo. type(valor) te lo dice como texto:
print(type(42)) -- number
print(type("hola")) -- string
print(type(true)) -- boolean
print(type(nil)) -- nil
print(type({})) -- table
print(type(print)) -- function
print(type(getPlayer())) -- userdata (en una partida)| Tipo | Ejemplo | Lo ves en |
|---|---|---|
nil | nil | "no hay valor" (sección 6) |
number | 3, 2.5, -1 | L02 |
string | "Base.Axe" | L02 |
boolean | true, false | L02 y L03 |
table | {} | L05 |
function | print | L04 |
userdata | getPlayer() | L06: los objetos de Java del juego (jugador, ítems, zombis) |
coroutine | L10. En Kahlua type() dice "coroutine", no "thread" como en el Lua estándar |
Un detalle propio de Kahlua: print de una tabla muestra su contenido (un nivel), no solo una dirección como en el Lua estándar. Pruébalo en la consola:
print({ nombre = "Sopaipilla", cantidad = 4 })Verás algo como table 0x12345{ nombre=Sopaipilla, cantidad=4 } (el número cambia cada vez). Muy útil para depurar.
6. nil: "aquí no hay nada"
nil es el valor de todo lo que no existe. Una variable que nunca asignaste vale nil, y leerla no da error:
print(nadaPorAqui) -- nil, sin error
print(type(nadaPorAqui)) -- nilEso es cómodo y peligroso a la vez: si escribes mal el nombre de una variable, Lua no te avisa, solo te da nil.
local zombisMuertos = 10
print(zombisMuerto) -- nil: falta la "s" y Lua no se quejaDonde sí revienta es cuando intentas usar un nil como si fuera otra cosa. Los mensajes de Kahlua que vas a ver en console.txt:
| Haces | Kahlua dice |
|---|---|
local p = nil; p:getX() | attempted index: getX of non-table: null |
funcionQueNoExiste() | Object tried to call nil in ... |
nil + 1 | __add not defined for operands in ... |
"hola " .. nil | __concat not defined for operands: ... |
Aprende a reconocerlos: el 80 % de los errores de un mod nuevo son un nil donde esperabas otra cosa. El más típico es getPlayer() en el menú principal, antes de entrar a una partida: ahí devuelve nil.
7. local y global: el entorno compartido
Esta es la sección más importante del módulo.
Una variable sin local es global. Las globales viven en una sola tabla, el entorno global, y en Project Zomboid ese entorno es uno solo para el juego entero y para TODOS los mods activos. Tu global contador es la misma variable que el contador de cualquier otro mod, y que cualquier contador del propio juego.
contador = 0 -- global: la ve (y la puede pisar) todo el mundo
local miContador = 0 -- local: solo existe en este archivo (o en este bloque)Una variable local existe desde la línea donde la declaras hasta el final del bloque que la contiene: el archivo, una función, un if, un bucle (los bloques los ves en L03).
local nivel = 1
if nivel > 0 then
local mensaje = "dentro del if"
print(mensaje) -- dentro del if
end
print(mensaje) -- nil: "mensaje" ya no existe aquíPor qué pisar un global es grave
El juego define funciones globales en Lua que otros archivos del juego usan. Si tu mod define una global con el mismo nombre, reemplazas la del juego para todos. Lo ves en el recuadro de abajo con round.
La regla
- Todo lo tuyo va en
local, salvo una tabla global con el nombre de tu mod. - Esa tabla se crea así, para que varios archivos del mod la compartan sin borrarse entre ellos:
CursoLab = CursoLab or {} -- la única global del mod (el "or" lo entiendes en L02 y L03)
CursoLab.version = "0.1.0" -- lo que quieras compartir entre archivos va DENTRO de la tabla
local secreto = 42 -- lo demás, localLas tablas las ves a fondo en L05 y la organización de un mod en L07. Por ahora quédate con la idea: una global por mod, todo lo demás local.
Un detalle de la consola
Cada vez que ejecutas algo en la consola de debug, el juego lo compila como un trozo de código aparte. Por eso un local de una ejecución no existe en la siguiente:
local prueba = 5 -- primera ejecución
print(prueba) -- segunda ejecución: nilEn la consola, si quieres conservar un valor entre líneas, guárdalo en tu tabla: CursoLab.prueba = 5.
function round(num, idp)
local mult = 10^(idp or 0)
return math.floor(num * mult + 0.5) / mult
endY media/lua/shared/Camping/ISCampingMenu.lua la usa para mostrar cuánto dura el combustible de una fogata:
local hours = round(math.floor(timeInMinutes / 60),2)
local minutes = round(timeInMinutes % 60,2)Si un mod define su propia function round(n) return string.format("%.0f", n) end (sin local), reemplaza la del juego. Desde ese momento round devuelve texto, la línea if hours > 0 de ISCampingMenu.timeString compara texto con número y el menú de la fogata empieza a tirar errores. El culpable no aparece en el error: aparece el archivo del juego. Fíjate también en local mult: el propio juego usa local dentro de la función.
8. Resumen
- Tu Lua corre en un archivo del mod (al cargar y en eventos) o en la consola de debug (al instante).
print("[CursoLab] ...")escribe enconsole.txt.--comentario de línea,--[[ ... ]]comentario largo.type(v)devuelve"nil","number","string","boolean","table","function","userdata"o"coroutine".- Leer algo que no existe da
nil. Usar unnilda error. - Una sola global por mod (
CursoLab). Todo lo demás,local.
Ejercicios
Ejercicio L1.1 · Hola, consola
En una partida con -debug, abre la consola y ejecuta, una por una:
print("[CursoLab] hola")
print(type(10), type("10"), type(nil), type(getPlayer()))
print(getPlayer():getFullName())
print({ a = 1, b = "dos" })Listo cuando: puedes decir qué tipo tiene cada valor y por qué getPlayer() no es "table".
Solución
La segunda línea imprime number string nil userdata. getPlayer() devuelve un objeto de Java (la clase IsoPlayer) y Kahlua reporta los objetos de Java como userdata. No es una tabla de Lua: lo ves a fondo en L06. La cuarta línea muestra el contenido de la tabla porque el tostring de Kahlua recorre un nivel de la tabla.
Ejercicio L1.2 · El archivo del mod
Crea 42/media/lua/client/CursoLab_L01.lua con:
- un comentario largo arriba que diga qué hace el archivo,
- un
printsuelto que diga[CursoLab] L01 cargado, - un
Events.OnGameStart.Add(...)que imprima el nombre del jugador ytype(getPlayer()).
Listo cuando: en console.txt aparecen tus mensajes y el de L01 cargado sale antes que el del nombre.
Solución
--[[
CursoLab_L01.lua
Primeros pasos: print, comentarios y un evento.
]]
print("[CursoLab] L01 cargado")
Events.OnGameStart.Add(function()
local jugador = getPlayer()
print("[CursoLab] jugador:", jugador:getFullName(), type(jugador))
end)El print suelto corre cuando el juego carga el archivo, mucho antes de que exista la partida. El del evento corre cuando la partida ya está cargada.
Ejercicio L1.3 · Cazar globales
Este archivo tiene tres variables que se volvieron globales sin querer. Encuéntralas y corrígelas sin cambiar lo que imprime.
CursoLab = CursoLab or {}
local nombre = "CursoLab"
version = "0.1.0"
local maxItems = 10
contador = 0
Events.OnGameStart.Add(function()
jugador = getPlayer()
print("[CursoLab]", nombre, version, maxItems, contador, jugador:getFullName())
end)Listo cuando: después de cargar la partida, en la consola print(version, contador, jugador) imprime nil nil nil.
Solución
Las globales eran version, contador y jugador (esta última se crea dentro del evento, pero sin local igual es global).
CursoLab = CursoLab or {}
local nombre = "CursoLab"
local version = "0.1.0"
local maxItems = 10
local contador = 0
Events.OnGameStart.Add(function()
local jugador = getPlayer()
print("[CursoLab]", nombre, version, maxItems, contador, jugador:getFullName())
end)Si de verdad necesitas la versión desde otro archivo, ponla dentro de la tabla del mod: CursoLab.version = "0.1.0".
Ejercicio L1.4 · Pisar un global (y deshacerlo)
En la consola, sin tocar archivos:
- Ejecuta
print(ISCampingMenu.timeString(90))y anota el resultado. - Guarda la función original:
CursoLab = CursoLab or {}y luegoCursoLab.roundOriginal = round. - Pisa la global:
function round(n) return string.format("%.0f", n) end. - Vuelve a ejecutar el paso 1. Lee el error.
- Restaura:
round = CursoLab.roundOriginaly comprueba que el paso 1 funciona otra vez.
Listo cuando: puedes explicar con tus palabras por qué un mod que nunca toca la fogata rompe el menú de la fogata.
Solución
En el paso 1 ves algo como 1 hora, 30 minutos (según el idioma). En el paso 4, round devuelve el texto "1" en vez del número 1, y ISCampingMenu.timeString hace if hours > 0: comparar texto con número no está permitido y Kahlua da un error del tipo __lt not defined for operand. El error apunta a ISCampingMenu.lua, no a quien pisó round. Como el entorno global es uno solo para el juego y todos los mods, cualquier global que definas puede chocar con el juego o con otro mod. Por eso: una tabla global por mod y todo lo demás local.
Desafío L1
Escribe 42/media/lua/client/CursoLab_Ficha.lua que, al empezar la partida, imprima una "ficha" del jugador en varias líneas, todas con el prefijo [CursoLab]: nombre completo, tipo del objeto jugador, coordenadas (getX(), getY(), getZ()) y el tipo de cada coordenada. Agrega un comentario largo al principio, usa solo variables local (más la tabla CursoLab) y guarda el nombre del jugador en CursoLab.nombreJugador para poder leerlo después desde la consola.
Listo cuando: ves la ficha en console.txt y print(CursoLab.nombreJugador) en la consola muestra tu nombre.
Una solución
--[[
CursoLab_Ficha.lua
Imprime una ficha del jugador al empezar la partida.
]]
CursoLab = CursoLab or {}
Events.OnGameStart.Add(function()
local jugador = getPlayer()
local x, y, z = jugador:getX(), jugador:getY(), jugador:getZ()
CursoLab.nombreJugador = jugador:getFullName()
print("[CursoLab] ---- ficha ----")
print("[CursoLab] nombre:", CursoLab.nombreJugador)
print("[CursoLab] tipo:", type(jugador))
print("[CursoLab] posicion:", x, y, z)
print("[CursoLab] tipos:", type(x), type(y), type(z))
end)Las coordenadas son number: en Lua todos los números son del mismo tipo, como ves en el próximo módulo.
Anterior: Temario · Siguiente: L02 · Números, texto y booleanos