Objetivo: trabajar con los objetos que te da el juego (el jugador, los ítems, los zombis, las listas) sin los errores típicos: el punto en vez de los dos puntos, el índice que empieza en 0, el nil que no revisaste y la referencia vieja a algo que ya no está en el mundo.
1. Lo que te devuelve el juego no es una tabla
Casi todo lo interesante del juego vive en Java: el jugador es un IsoPlayer, una manzana es un InventoryItem, el inventario es un ItemContainer. Cuando llamas a getPlayer(), Lua no recibe una copia del jugador: recibe una referencia a ese objeto de Java. Para Lua es un valor de tipo userdata.
local p = getPlayer()
print(type(p)) -- userdata
print(typeof(p)) -- IsoPlayer (typeof es una global del juego: el nombre de la clase Java)
print(type({})) -- tabletype() solo te dice "esto viene de Java". Para saber qué clase es, usa typeof(obj) (te da el nombre corto de la clase, por ejemplo "IsoPlayer") o instanceof (sección 5).
Un userdata no se comporta como una tabla:
local p = getPlayer()
p.miContador = 1 -- ERROR: no puedes agregarle campos a un objeto de Java
local md = p:getModData() -- esto sí: una tabla Lua asociada al objeto
md.miContador = (md.miContador or 0) + 1Lo único que haces con un objeto de Java es llamar a sus métodos. Si quieres guardar datos tuyos "pegados" a un objeto, usa getModData(), que devuelve una tabla Lua normal (lo ves a fondo en 14 · Datos persistentes y rendimiento).
2. : y .: la diferencia que más errores causa
En Lua, obj:metodo(a, b) es solo una forma corta de escribir obj.metodo(obj, a, b). Los dos puntos pasan el objeto como primer argumento, de forma invisible.
local p = getPlayer()
local inv = p:getInventory() -- bien
local inv = p.getInventory(p) -- también funciona: es lo mismo, escrito largo
local inv = p.getInventory() -- ERROR: Java no sabe de qué jugador hablasCon objetos de Java, siempre :. El error de la tercera línea lo arma Kahlua (se.krka.kahlua.integration.expose.LuaJavaInvoker) y empieza así:
getInventory: Expected a method call but got a function call."Esperaba una llamada a método (con :) y recibí una llamada a función (con .)". Cuando veas ese texto en console.txt, busca un punto que debería ser dos puntos. Si el método tiene varias versiones en Java (sobrecarga), el error puede salir en cambio como No implementation found for function: .... La causa es la misma.
Los otros dos errores que vas a confundir con este
| Escribiste | Qué pasa | El error en console.txt empieza con |
|---|---|---|
p.getInventory() | Falta el objeto | getInventory: Expected a method call but got a function call. |
p:getInventario() (no existe) | Pedir un método que no existe da nil, y llamar a nil falla | Object tried to call nil in ... |
x:getInventory() con x == nil | No hay objeto al que pedirle nada | attempted index: getInventory of non-table: null |
La segunda fila tiene un uso bueno: como pedir un método inexistente da nil sin error, puedes preguntar si un método existe antes de usarlo (útil cuando un método cambia entre versiones del juego):
local f = item.getCurrentAmmoCount -- con punto: solo lo pedimos, no lo llamamos
if f then
print(f(item)) -- y si existe, lo llamamos pasándole el objeto
endEl mismo tropiezo con tus propias tablas
Con funciones que tú defines con : pasa lo mismo, pero el error sale más lejos. Si defines function CursoLab.Caja:abrir() y la llamas con CursoLab.Caja.abrir(), self vale nil y el error aparece en la primera línea que usa self. El patrón de clases con : lo ves en L09 · Metatablas y clases.
Events.OnZombieDead.Add(FightStep.OnMomDead);function FightStep:OnMomDead(zed)
Events.OnZombieDead.Remove(FightStep.OnMomDead);
FightStep.momDead = true;
endEl evento llama a la función con el zombi como primer argumento, así que adentro self es el zombi y zed queda en nil. Funciona solo porque el cuerpo no usa ni self ni zed. Si lo usara, sería un bug difícil de ver.
3. null es nil: revisa antes de encadenar
Cuando un método de Java devuelve null, en Lua recibes nil. Muchos métodos lo hacen de forma normal: getPrimaryHandItem() con la mano vacía, getVehicle() si vas a pie, getSpecificPlayer(n) si no hay un jugador n.
-- Revienta si no tienes nada en la mano:
print(getPlayer():getPrimaryHandItem():getDisplayName())
-- Bien: guarda cada paso en una local y revisa
local p = getPlayer()
local item = p and p:getPrimaryHandItem()
if item then
print(item:getDisplayName())
else
print("mano vacía")
endLa regla: cada : de una cadena es un lugar donde puede aparecer un nil. Si el método puede devolver null (casi todo lo que empieza con get y devuelve un objeto), corta la cadena y revisa.
El juego lo hace todo el tiempo. En media/lua/shared/RadioCom/ISRadioInteractions.lua:
for playerNum=1,4 do
local player = getSpecificPlayer(playerNum-1)
if player and player:isDead() then player = nil endgetSpecificPlayer(n) devuelve nil si no hay jugador en ese lugar de la pantalla dividida, así que el juego revisa player and ... antes de llamar a isDead().
Al revés también vale: si le pasas nil a un método de Java, llega como null. Algunos métodos lo aceptan y otros fallan por dentro, en Java, con un error mucho más difícil de leer. Revisa tus valores antes de pasarlos.
4. Listas de Java: size() y get(i) desde 0
Muchos métodos devuelven un ArrayList de Java, no una tabla Lua: getItems() de un inventario, getMovingObjects() de una baldosa, getOnlinePlayers() en un servidor. Una lista de Java:
| Quieres | Tabla Lua | ArrayList de Java |
|---|---|---|
| Tamaño | #t | lista:size() |
| Primer elemento | t[1] | lista:get(0) |
| Último elemento | t[#t] | lista:get(lista:size() - 1) |
| ¿Está vacía? | table.isempty(t) | lista:isEmpty() |
| ¿Contiene x? | recorrer | lista:contains(x) |
| Recorrer | ipairs(t) | for i = 0, lista:size() - 1 do |
#lista, ipairs(lista) y pairs(lista) no funcionan con una lista de Java: dan error, porque esperan una tabla. Y lista[1] no es el primer elemento: es pedirle a un objeto de Java un "método" llamado 1.
Los dos bucles correctos
local items = getPlayer():getInventory():getItems()
-- Forma 1: i va de 0 a size-1
for i = 0, items:size() - 1 do
local item = items:get(i)
print(i, item:getDisplayName())
end
-- Forma 2: i va de 1 a size, y restas 1 al pedir
for i = 1, items:size() do
local item = items:get(i - 1)
print(i, item:getDisplayName())
endLas dos están bien y el juego usa las dos. El bug es mezclarlas: for i = 1, items:size() do items:get(i) se salta el primer elemento y al final pide uno que no existe. Ese último get lanza un error de Java (IndexOutOfBoundsException), no un nil.
function ISPutOutFire:complete()
for _,square in pairs(self.squares) do
local used = false
for i=1,square:getMovingObjects():size() do
local chr = square:getMovingObjects():get(i-1)
if instanceof(chr, "IsoGameCharacter") and chr:isOnFire() thenFíjate en los dos mundos juntos: self.squares es una tabla Lua (se recorre con pairs) y getMovingObjects() es una lista de Java (se recorre con size() y get).
Pasar una lista de Java a una tabla Lua
Si vas a recorrer una lista muchas veces, ordenarla con table.sort o guardarla, conviértela una vez:
function CursoLab.aTabla(lista)
local t = {}
for i = 0, lista:size() - 1 do
t[#t + 1] = lista:get(i)
end
return t
end
local items = CursoLab.aTabla(getPlayer():getInventory():getItems())
table.sort(items, function(a, b) return a:getDisplayName() < b:getDisplayName() end)El juego trae una utilidad parecida en media/lua/shared/env.lua: la global xpairs(t), que recorre igual una lista de Java o una tabla Lua. Ojo: con una lista de Java el índice que entrega empieza en 0, y con una tabla, en 1.
No cambies una lista mientras la recorres
Si sacas elementos de una lista mientras la recorres hacia adelante, los índices se corren y te saltas elementos. Recorre hacia atrás:
local inv = getPlayer():getInventory()
local items = inv:getItems()
for i = items:size() - 1, 0, -1 do
local item = items:get(i)
if item:getFullType() == "Base.Apple" then
inv:Remove(item) -- el contenedor saca el ítem; la lista se achica detrás de ti
end
endPara agregar o sacar ítems usa los métodos del contenedor (AddItem, AddItems, Remove), no add/remove sobre la lista que te devolvió getItems(): esa lista es la del juego, y el contenedor tiene que enterarse del cambio.
Crear tu propia lista de Java
Algunos métodos del juego piden un ArrayList como argumento. Lo creas con ArrayList.new():
local lista = ArrayList.new()
lista:add("uno")
lista:add("dos")
print(lista:size(), lista:get(0)) -- 2 uno5. instanceof: qué clase de objeto es
Cuando una lista mezcla cosas (en una baldosa hay jugadores, zombis, animales, vehículos), necesitas saber qué es cada una antes de llamarle un método que solo existe en algunas.
local sq = getPlayer():getCurrentSquare()
local cosas = sq and sq:getMovingObjects()
if cosas then
for i = 0, cosas:size() - 1 do
local o = cosas:get(i)
if instanceof(o, "IsoZombie") then
print("un zombi")
elseif instanceof(o, "IsoPlayer") then
print("un jugador:", o:getUsername())
end
end
endLo que tienes que saber de instanceof(obj, "Clase") (en Java es LuaManager.GlobalObject.instof):
- Responde
truetambién con clases padre: un zombi esIsoZombie, pero tambiénIsoGameCharactereIsoMovingObject. instanceof(nil, "IsoZombie")dafalse, sin error. Puedes usarlo sin revisarnilantes.- Si escribes mal el nombre (
"IsoZombi"), dafalseen silencio. Si uninstanceofnunca es verdadero, revisa la ortografía. - Solo conoce clases que Java expone a Lua (lo vimos en 07 · La frontera Lua ↔ Java).
- Es para objetos de Java. Para tus tablas Lua con clases (
ISPanely compañía) se usa otra cosa, que ves en L09.
6. Números entre Lua y Java
En Lua todo número es un double. Java tiene varios tipos de número, y al cruzar la frontera se convierten.
De Lua a Java, un número se trunca si el método pide un entero: Kahlua usa intValue() (o longValue()), que corta los decimales hacia cero. 2.9 llega como 2 y -2.9 como -2. No redondea.
local inv = getPlayer():getInventory()
inv:AddItems("Base.Apple", 2.9) -- AddItems(String, int): agrega 2 manzanas, no 3
local n = 10 * 0.29 -- 2.9: un cálculo que "debería" dar 3 no siempre lo da
inv:AddItems("Base.Apple", math.floor(n + 0.5)) -- redondea tú: 3El juego tiene una global para esto en media/lua/shared/env.lua: round(num, idp), que redondea a idp decimales.
De Java a Lua, todo llega como double. Un int de Java se ve igual (print(item:getID()) muestra un entero sin decimales). Pero un float de Java se convierte con doubleValue(), y los float no guardan decimales exactos: si un ítem pesa 0.2, print(item:getWeight()) puede mostrar 0.20000000298023224.
local w = item:getWeight()
if w == 0.2 then ... end -- puede ser falso aunque el juego diga 0.2
if math.abs(w - 0.2) < 0.0001 then ... end -- compara con tolerancia
print(round(w, 2)) -- o redondea para mostrar7. == entre objetos de Java
Para dos objetos de Java, == pregunta si son el mismo objeto, no si "se parecen". Los textos son la excepción: dos strings iguales siempre son ==, vengan de donde vengan.
print(getPlayer() == getSpecificPlayer(0)) -- true en una partida de un jugador: es el mismo IsoPlayer
-- Dos manzanas distintas nunca son ==, aunque sean del mismo tipo:
print(a == b) -- false
print(a:getFullType() == b:getFullType()) -- true: compara los textosEl juego compara así a su jugador: if _player == getPlayer() and instanceof(_item, "Radio") then (media/lua/client/RadioCom/ISRadioWindow.lua).
8. No guardes objetos de Java para siempre
Guardar un objeto de Java en una tabla tuya es tentador: "me acuerdo de este zombi", "me acuerdo de esta fogata". El problema es que el objeto sigue existiendo para tu tabla aunque ya no exista para el mundo, y eso trae tres problemas:
- Fugas de memoria. Mientras tu tabla apunte al objeto, Java no lo puede liberar. Y en Kahlua no hay tablas débiles:
setmetatable(t, { __mode = "k" })no hace nada. Si no borras tú la entrada, queda para siempre. - Objetos que salieron del mundo. El ítem se comió, el mueble se desarmó, el auto se borró. Tu referencia sigue ahí y sus métodos pueden devolver datos viejos o fallar.
- Objetos reciclados. El juego reutiliza objetos para ahorrar memoria. Los zombis son el caso más claro:
VirtualZombieManager(en Java) guarda una cola deIsoZombie"reutilizables" (reusableZombies). El objeto del zombi que mataste puede volver al mundo como otro zombi, y tu tabla seguirá creyendo que es el tuyo.
Qué guardar en su lugar
Guarda algo que identifique al objeto y búscalo de nuevo cuando lo necesites:
| Objeto | Guarda | Para volver a encontrarlo |
|---|---|---|
| Ítem en un inventario | item:getID() | inv:getItemById(id) |
| Jugador | p:getUsername() o p:getPlayerNum() | recorrer los jugadores, o getSpecificPlayer(n) en local |
| Objeto del mundo | sus coordenadas x, y, z | getCell():getGridSquare(x, y, z) y buscar en la baldosa |
-- Mal: la referencia puede quedar vieja
CursoLab.favorito = item
-- Bien: guarda el número y vuelve a buscar
CursoLab.favoritoID = item:getID()
function CursoLab.getFavorito()
local p = getPlayer()
if not p or not CursoLab.favoritoID then return nil end
return p:getInventory():getItemById(CursoLab.favoritoID) -- nil si ya no está
endfunction ISDetachItemHotbar:start()
if isClient() and self.item then
self.item = self.character:getInventory():getItemById(self.item:getID())
endSi igual necesitas guardarlo un rato, revísalo antes de usarlo
A veces sí tiene sentido guardar el objeto: una ventana abierta sobre una fogata, una acción que dura unos segundos. Entonces revisa que siga en el mundo cada vez que lo uses:
| Objeto | Pregunta |
|---|---|
Objeto del mundo (IsoObject: muebles, fogatas, árboles) | obj:getObjectIndex() == -1 significa que ya no está en su baldosa |
| Personaje o zombi | chr:isDead() |
| Animal u otro objeto que debe estar en el mundo | obj:isExistInTheWorld() |
function ISBBQInfoWindow:update()
ISCollapsableWindow.update(self)
if self:getIsVisible() and (not self.object or self.object:getObjectIndex() == -1) thenRevisa las dos cosas: que la referencia exista (not self.object) y que el objeto siga en el mundo (getObjectIndex() == -1).
Y limpia tus tablas cuando el objeto se va: el juego lo hace en media/lua/client/ISUI/ISMakeUpUI.lua, que al morir el jugador cierra su ventana y borra la entrada:
function ISMakeUpUI.OnPlayerDeath(playerObj)
local ui = ISMakeUpUI.windows[playerObj:getPlayerNum()+1]
if ui then
ui:removeFromUIManager()
ISMakeUpUI.windows[playerObj:getPlayerNum()+1] = nil
end
endEjercicios
Todos se hacen en la consola Lua (con -debug) o en un archivo de CursoLab.
Ejercicio L6.1 · ¿Qué es esto?
Con algo en la mano principal, ejecuta en la consola:
local p = getPlayer()
local item = p:getPrimaryHandItem()
print(type(p), typeof(p), type(item), typeof(item))
print(instanceof(p, "IsoPlayer"), instanceof(p, "IsoGameCharacter"), instanceof(p, "IsoZombie"))
print(instanceof(item, "InventoryItem"), instanceof(nil, "InventoryItem"), instanceof(p, "IsoPlayr"))Después suelta el ítem y vuelve a ejecutar el primer bloque.
Listo cuando: puedes explicar cada true/false de las dos últimas líneas y por qué la segunda vez type(item) dice nil (y typeof(item) también).
Solución
typediceuserdatapara los dos objetos de Java;typeofda la clase:IsoPlayery la clase del ítem (por ejemploHandWeaponpara un arma oFoodpara comida).instanceof(p, "IsoGameCharacter")estrueporqueIsoPlayerhereda deIsoGameCharacter.IsoZombieesfalse.instanceof(nil, ...)esfalsesin error.instanceof(p, "IsoPlayr")esfalseporque la clase no existe: el juego no te avisa del error de ortografía.- Con la mano vacía,
getPrimaryHandItem()devuelvenullde Java, que en Lua esnil.
Ejercicio L6.2 · Arregla los puntos
Este código tiene cuatro errores. Encuéntralos sin ejecutarlo y después compruébalo en la consola.
function CursoLab.contarManzanas()
local items = getPlayer().getInventory():getItems()
local n = 0
for i = 1, items:size() do
local item = items:get(i)
if item:getFullType() == "Base.Apple" then
n = n + 1
end
end
print("manzanas: " .. n .. " de " .. #items)
getPlayer():getPrimaryHandItem():getDisplayName()
endListo cuando: la función corre con y sin manzanas, con y sin algo en la mano, y cuenta bien.
Solución
getPlayer().getInventory()usa punto: esgetPlayer():getInventory().for i = 1, items:size()conitems:get(i): mezcla las dos formas. Se salta el primero y al final pide un índice que no existe.#itemsno sirve con una lista de Java: esitems:size().- La última línea revienta con la mano vacía (y además no hace nada con el resultado).
function CursoLab.contarManzanas()
local p = getPlayer()
if not p then return 0 end
local items = p:getInventory():getItems()
local n = 0
for i = 0, items:size() - 1 do
if items:get(i):getFullType() == "Base.Apple" then
n = n + 1
end
end
print("manzanas: " .. n .. " de " .. items:size())
local mano = p:getPrimaryHandItem()
if mano then print("en la mano: " .. mano:getDisplayName()) end
return n
endEjercicio L6.3 · De Java a Lua y ordenado
Escribe CursoLab.inventarioOrdenado() que devuelva una tabla Lua con los nombres visibles (getDisplayName()) de todos los ítems del inventario principal, ordenados alfabéticamente y sin repetir. Imprime el resultado con table.concat(t, ", ").
Listo cuando: con tres manzanas y un martillo ves Manzana, Martillo (o los nombres en tu idioma) y no Manzana, Manzana, Manzana, Martillo.
Solución
function CursoLab.inventarioOrdenado()
local p = getPlayer()
if not p then return {} end
local items = p:getInventory():getItems()
local vistos, nombres = {}, {}
for i = 0, items:size() - 1 do
local nombre = items:get(i):getDisplayName()
if not vistos[nombre] then
vistos[nombre] = true
nombres[#nombres + 1] = nombre
end
end
table.sort(nombres)
return nombres
end
print(table.concat(CursoLab.inventarioOrdenado(), ", "))Usamos un conjunto (vistos) para no repetir, como en L05 · Tablas. Los nombres son strings de Lua, así que table.sort los ordena sin función de comparación.
Ejercicio L6.4 · Un favorito que no se pierde
Escribe dos funciones:
CursoLab.marcarFavorito(): guarda como favorito el ítem que tienes en la mano principal (si no tienes nada, dilo congetPlayer():Say(...)).CursoLab.dondeEstaFavorito(): dice conSaysi el favorito sigue en tu inventario principal.
Prueba: márcalo, muévelo dentro del inventario, suéltalo al suelo, vuelve a tomarlo.
Listo cuando: nunca guardas el objeto del ítem, solo su ID, y ninguna de las pruebas produce un error.
Solución
function CursoLab.marcarFavorito()
local p = getPlayer()
if not p then return end
local item = p:getPrimaryHandItem()
if not item then
p:Say("No tengo nada en la mano")
return
end
CursoLab.favoritoID = item:getID()
p:Say("Favorito: " .. item:getDisplayName())
end
function CursoLab.dondeEstaFavorito()
local p = getPlayer()
if not p then return end
if not CursoLab.favoritoID then
p:Say("No tengo favorito")
return
end
local item = p:getInventory():getItemById(CursoLab.favoritoID)
if item then
p:Say("Lo tengo: " .. item:getDisplayName())
else
p:Say("No está en mi inventario")
end
endgetItemById busca solo en el inventario principal: si guardas el ítem en una mochila, dirá que no está. Para buscar también dentro de mochilas, mira cómo recorre el juego los contenedores anidados en luautils.countItemsRecursive (media/lua/shared/luautils.lua).
Desafío L6
Escribe CursoLab.describir(x), un inspector rápido para la consola que:
- Si
xesnil, imprimanil. - Si es una tabla Lua, imprima
tablay cuántas claves tiene. - Si es un objeto de Java, imprima su clase (
typeof) y, si es una lista de Java (instanceof(x, "ArrayList")), su tamaño y la clase de sus primeros 5 elementos. - Para cualquier otro valor, imprima
type(x)y el valor.
Pruébalo con getPlayer(), getPlayer():getInventory():getItems(), getPlayer():getModData(), {} y 42. Después agrégale CursoLab.metodo(obj, "nombre"), que diga si un objeto de Java tiene un método con ese nombre, usando el truco de la sección 2 (pedirlo con punto y ver si es nil).
Anterior: L05 · Tablas · Siguiente: L07 · Organizar un mod