WENOPAL

L06 · Lua y objetos de Java

Qué es un objeto de Java en tu Lua, cuándo usar : y cuándo ., cómo recorrer listas de Java desde 0 y por qué no debes guardar esos objetos para siempre.

Curso técnico · Build 42 · Actualizado el

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.

lua
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({}))       -- table

type() 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:

lua
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) + 1

Lo ú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.

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

Con 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

EscribisteQué pasaEl error en console.txt empieza con
p.getInventory()Falta el objetogetInventory: 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 fallaObject tried to call nil in ...
x:getInventory() con x == nilNo hay objeto al que pedirle nadaattempted 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):

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

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

lua
Events.OnZombieDead.Add(FightStep.OnMomDead);
lua
function FightStep:OnMomDead(zed)
    Events.OnZombieDead.Remove(FightStep.OnMomDead);
    FightStep.momDead = true;
end

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

lua
-- 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")
end

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

lua
for playerNum=1,4 do
    local player = getSpecificPlayer(playerNum-1)
    if player and player:isDead() then player = nil end

getSpecificPlayer(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:

QuieresTabla LuaArrayList de Java
Tamaño#tlista:size()
Primer elementot[1]lista:get(0)
Último elementot[#t]lista:get(lista:size() - 1)
¿Está vacía?table.isempty(t)lista:isEmpty()
¿Contiene x?recorrerlista:contains(x)
Recorreripairs(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

lua
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())
end

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

lua
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() then

Fí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:

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

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

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

lua
local lista = ArrayList.new()
lista:add("uno")
lista:add("dos")
print(lista:size(), lista:get(0))   -- 2   uno

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

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

Lo que tienes que saber de instanceof(obj, "Clase") (en Java es LuaManager.GlobalObject.instof):

  • Responde true también con clases padre: un zombi es IsoZombie, pero también IsoGameCharacter e IsoMovingObject.
  • instanceof(nil, "IsoZombie") da false, sin error. Puedes usarlo sin revisar nil antes.
  • Si escribes mal el nombre ("IsoZombi"), da false en silencio. Si un instanceof nunca 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 (ISPanel y 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.

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

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

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

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

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

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

  1. 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.
  2. 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.
  3. Objetos reciclados. El juego reutiliza objetos para ahorrar memoria. Los zombis son el caso más claro: VirtualZombieManager (en Java) guarda una cola de IsoZombie "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:

ObjetoGuardaPara volver a encontrarlo
Ítem en un inventarioitem:getID()inv:getItemById(id)
Jugadorp:getUsername() o p:getPlayerNum()recorrer los jugadores, o getSpecificPlayer(n) en local
Objeto del mundosus coordenadas x, y, zgetCell():getGridSquare(x, y, z) y buscar en la baldosa
lua
-- 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á
end
lua
function ISDetachItemHotbar:start()
    if isClient() and self.item then
        self.item = self.character:getInventory():getItemById(self.item:getID())
    end

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

ObjetoPregunta
Objeto del mundo (IsoObject: muebles, fogatas, árboles)obj:getObjectIndex() == -1 significa que ya no está en su baldosa
Personaje o zombichr:isDead()
Animal u otro objeto que debe estar en el mundoobj:isExistInTheWorld()
lua
function ISBBQInfoWindow:update()
	ISCollapsableWindow.update(self)

	if self:getIsVisible() and (not self.object or self.object:getObjectIndex() == -1) then

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

lua
function ISMakeUpUI.OnPlayerDeath(playerObj)
	local ui = ISMakeUpUI.windows[playerObj:getPlayerNum()+1]
	if ui then
		ui:removeFromUIManager()
		ISMakeUpUI.windows[playerObj:getPlayerNum()+1] = nil
	end
end

Ejercicios

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:

lua
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
  • type dice userdata para los dos objetos de Java; typeof da la clase: IsoPlayer y la clase del ítem (por ejemplo HandWeapon para un arma o Food para comida).
  • instanceof(p, "IsoGameCharacter") es true porque IsoPlayer hereda de IsoGameCharacter. IsoZombie es false.
  • instanceof(nil, ...) es false sin error.
  • instanceof(p, "IsoPlayr") es false porque la clase no existe: el juego no te avisa del error de ortografía.
  • Con la mano vacía, getPrimaryHandItem() devuelve null de Java, que en Lua es nil.

Ejercicio L6.2 · Arregla los puntos

Este código tiene cuatro errores. Encuéntralos sin ejecutarlo y después compruébalo en la consola.

lua
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()
end

Listo cuando: la función corre con y sin manzanas, con y sin algo en la mano, y cuenta bien.

Solución
  1. getPlayer().getInventory() usa punto: es getPlayer():getInventory().
  2. for i = 1, items:size() con items:get(i): mezcla las dos formas. Se salta el primero y al final pide un índice que no existe.
  3. #items no sirve con una lista de Java: es items:size().
  4. La última línea revienta con la mano vacía (y además no hace nada con el resultado).
lua
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
end

Ejercicio 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
lua
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 con getPlayer():Say(...)).
  • CursoLab.dondeEstaFavorito(): dice con Say si 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
lua
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
end

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

  1. Si x es nil, imprima nil.
  2. Si es una tabla Lua, imprima tabla y cuántas claves tiene.
  3. 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.
  4. 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