WENOPAL

L01 · Tu primer Lua en el juego

Dónde corre tu Lua en Project Zomboid, print, comentarios, variables, tipos, nil y la diferencia entre local y global en un entorno compartido.

Curso técnico · Build 42 · Actualizado el

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:

LugarCuándo correPara qué sirve
Un archivo .lua de tu modCuando 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 debugEn el momento en que apretas EnterProbar 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:

ini
name=Curso Lab
id=CursoLab
description=Mod de práctica del curso.
versionMin=42.0.0

Para 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:

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:

lua
print("[CursoLab]", "uno", 2, true, nil)

Ahora prueba lo mismo en la consola (` ``):

lua
print("[CursoLab] hola desde la consola")
print(1 + 2)
lua
-- 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.

lua
-- 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:

lua
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: cantidad y Cantidad son 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ño no).

Puedes asignar varias variables a la vez. Si sobran nombres, los que no reciben valor quedan en nil:

lua
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  10

5. Tipos: type()

Cada valor tiene un tipo. type(valor) te lo dice como texto:

lua
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)
TipoEjemploLo ves en
nilnil"no hay valor" (sección 6)
number3, 2.5, -1L02
string"Base.Axe"L02
booleantrue, falseL02 y L03
table{}L05
functionprintL04
userdatagetPlayer()L06: los objetos de Java del juego (jugador, ítems, zombis)
coroutineL10. 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:

lua
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:

lua
print(nadaPorAqui)          -- nil, sin error
print(type(nadaPorAqui))    -- nil

Eso es cómodo y peligroso a la vez: si escribes mal el nombre de una variable, Lua no te avisa, solo te da nil.

lua
local zombisMuertos = 10
print(zombisMuerto)         -- nil: falta la "s" y Lua no se queja

Donde sí revienta es cuando intentas usar un nil como si fuera otra cosa. Los mensajes de Kahlua que vas a ver en console.txt:

HacesKahlua 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.

lua
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).

lua
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:
lua
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, local

Las 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:

lua
local prueba = 5     -- primera ejecución
print(prueba)        -- segunda ejecución: nil

En la consola, si quieres conservar un valor entre líneas, guárdalo en tu tabla: CursoLab.prueba = 5.

lua
function round(num, idp)
  local mult = 10^(idp or 0)
  return math.floor(num * mult + 0.5) / mult
end

Y media/lua/shared/Camping/ISCampingMenu.lua la usa para mostrar cuánto dura el combustible de una fogata:

lua
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 en console.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 un nil da 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:

lua
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:

  1. un comentario largo arriba que diga qué hace el archivo,
  2. un print suelto que diga [CursoLab] L01 cargado,
  3. un Events.OnGameStart.Add(...) que imprima el nombre del jugador y type(getPlayer()).

Listo cuando: en console.txt aparecen tus mensajes y el de L01 cargado sale antes que el del nombre.

Solución
lua
--[[
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.

lua
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).

lua
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:

  1. Ejecuta print(ISCampingMenu.timeString(90)) y anota el resultado.
  2. Guarda la función original: CursoLab = CursoLab or {} y luego CursoLab.roundOriginal = round.
  3. Pisa la global: function round(n) return string.format("%.0f", n) end.
  4. Vuelve a ejecutar el paso 1. Lee el error.
  5. Restaura: round = CursoLab.roundOriginal y 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
lua
--[[
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