WENOPAL

L09 · Metatablas y clases

Qué son las metatablas, cómo arma el juego sus clases con ISBaseObject:derive y :new, herencia, self y los metamétodos que sí funcionan en Kahlua.

Curso técnico · Build 42 · Actualizado el

Objetivo: entender las metatablas lo suficiente para leer cualquier "clase" del juego (ISPanel, ISBaseTimedAction, LuaList), escribir las tuyas con el mismo patrón, heredar y llamar al padre sin errores, y usar metamétodos como __tostring, __eq o __add sabiendo qué hace Kahlua con ellos.

1. Lua no tiene clases: tiene metatablas

En Lua no existe la palabra class. Lo que hay son tablas y un mecanismo para cambiar cómo se comporta una tabla: la metatabla. Una metatabla es una tabla normal con claves especiales que empiezan con dos guiones bajos (__index, __tostring, __add...). A esas claves se les llama metamétodos.

lua
local datos = { nombre = "sopaipilla" }
local meta  = {}

setmetatable(datos, meta)          -- desde ahora, meta es la metatabla de datos
print(getmetatable(datos) == meta) -- true

setmetatable devuelve la misma tabla que recibe, así que puedes crear y asignar en una línea:

lua
local datos = setmetatable({ nombre = "sopaipilla" }, meta)

Solo las tablas de Lua aceptan setmetatable. Con un objeto de Java (un IsoPlayer, un ítem) falla con un error: esos objetos ya traen su propio comportamiento desde Java (L06 · Lua y objetos de Java).

2. __index: dónde buscar lo que falta

Cuando lees un campo que no está en la tabla (t.algo da nil), Lua mira si la metatabla tiene __index. Si es una tabla, busca ahí:

lua
local valoresPorDefecto = { hambre = 10, peso = 0.2 }
local comida = setmetatable({ peso = 0.5 }, { __index = valoresPorDefecto })

print(comida.peso)    -- 0.5  (está en comida)
print(comida.hambre)  -- 10   (no está: lo saca de valoresPorDefecto)
print(comida.sabor)   -- nil  (no está en ninguna)

Fíjate en tres cosas:

  • __index solo se usa al leer un campo que falta. Si el campo existe en la tabla, gana siempre.
  • Escribir no pasa por __index: comida.hambre = 20 crea el campo en comida, no cambia valoresPorDefecto.
  • Si valoresPorDefecto a su vez tuviera una metatabla con __index, la búsqueda sigue en cadena. Esa cadena es exactamente la herencia.

__index también puede ser una función (tabla, clave) que calcula el valor. El juego lo usa en client/ISUI/ISUIElementJoypad.lua:166 para buscar primero en una clase, después en otra:

lua
    setmetatable(o, {
        __index = function (table, key)
            local result;
            if NewClass then
                result = rawget(NewClass, key);
                if result then
                    return result;
                end
            end
            result = rawget(ISUIElementJoypad, key);
            if result then
                return result;
            elseif Class then
                return Class[key];
            end
            return nil;
        end,
    });

rawget(t, k) lee el campo sin pasar por __index: lo necesitas dentro de una función __index para no caer en una búsqueda infinita.

3. : y self

Antes de armar una clase, hace falta una pieza de sintaxis. Estas dos líneas son idénticas para Lua:

lua
function Contador.sumar(self, k) self.n = self.n + k end
function Contador:sumar(k)       self.n = self.n + k end   -- el ":" agrega self como primer parámetro

Y estas dos llamadas también:

lua
c.sumar(c, 5)
c:sumar(5)        -- el ":" pasa c como primer argumento

self no tiene nada de mágico: es el nombre del primer parámetro que agrega el :. Es el mismo : que usas con los objetos de Java (jugador:getX()), y el error típico es el mismo: llamar con . un método escrito con :.

lua
c.sumar(5)        -- self = 5, k = nil  ->  error al hacer self.n

Míralo en el juego: un método como handler de evento

client/Tutorial/Steps.lua:913:

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

Registrado en la línea 840 con Events.OnZombieDead.Add(FightStep.OnMomDead);. El evento llama a la función con un argumento, el zombi. Como la función está escrita con :, ese zombi cae en self, y zed queda en nil. Aquí no rompe nada porque el cuerpo no usa ni self ni zed (escribe FightStep.momDead con el nombre de la tabla). Pero si hubiera escrito self.momDead = true, le habría puesto el campo... al zombi, que es un objeto de Java: error.

Cuando quieras que un evento llame a un método de un objeto concreto, envuélvelo en una closure que capture el objeto. Es lo que hace client/Fishing/FishingManager.lua:45:

lua
function FishingManager:eventHooks()
    self.onTickFunc = function()
        self:update()
    end
    Events.OnTick.Add(self.onTickFunc)
end

function FishingManager:removeEventHooks()
    Events.OnTick.Remove(self.onTickFunc)
end

La closure recuerda self (lo viste en L08 · Eventos y estado) y se guarda en el propio objeto para poder quitarla después con Remove.

4. El patrón de clases del juego: derive y :new

Con __index y : ya se puede armar una clase:

  • La clase es una tabla con los métodos.
  • Cada instancia es una tabla con sus datos, cuya metatabla es la clase.
  • La clase tiene __index apuntando a sí misma: lo que la instancia no tiene, se busca en la clase.

Todo el juego usa la misma receta, que está en shared/ISBaseObject.lua:

lua
ISBaseObject = {};

ISBaseObject.Type = "ISBaseObject";

function ISBaseObject:initialise()

end

function ISBaseObject:derive(type)
    local o = {}
    setmetatable(o, self)
    self.__index = self
	o.Type = type
	o.SuperType = self
    return o
end

function ISBaseObject:new()
	local o = {}
	setmetatable(o, self)
	self.__index = self
	o.SuperType = nil
	return o
end
  • derive(type) crea una clase hija: una tabla vacía cuya metatabla es la clase padre (self). Le pone Type (su nombre, un texto) y SuperType (la tabla del padre).
  • new() crea una instancia: una tabla vacía cuya metatabla es la clase.
  • Los dos hacen self.__index = self, para que la búsqueda siga hacia arriba.

Míralo en el juego: LuaList

shared/Util/LuaList.lua es una clase completa y corta. Lo esencial:

lua
LuaList = ISBaseObject:derive("LuaList");

function LuaList:add(item)
    if self.debug == true then
        print("add");
    end
    table.insert(self.items, item);
	self.count = self.count + 1;
end

function LuaList:size()
	return self.count;
end

Y al final del archivo (línea 136):

lua
function LuaList:new ()
	local o = {}
	setmetatable(o, self)
	self.__index = self
	o.items = {}
	o.count = 0;
	return o
end

Sigue el camino de lista:add("x") con lista = LuaList:new():

PasoDónde busca addResultado
1En lista (solo tiene items y count)No está
2Metatabla de lista = LuaList, su __index = LuaListEstá: LuaList.add
3Llama LuaList.add(lista, "x")self es lista

Si pidieras lista.Type, la búsqueda daría con LuaList.Type ("LuaList"); y lista:initialise() subiría un nivel más, hasta ISBaseObject.initialise, porque la metatabla de LuaList es ISBaseObject.

La trampa: tablas en la clase

Fíjate en que LuaList:new crea o.items = {} dentro de new. ¿Qué pasaría si la lista estuviera en la clase?

lua
-- MAL
LuaList.items = {}
function LuaList:new()
    local o = ISBaseObject.new(self)
    return o
end

local a = LuaList:new()
local b = LuaList:new()
table.insert(a.items, "sopaipilla")
print(#b.items)   -- 1: a y b comparten LA MISMA tabla items

a.items no existe en a, así que la búsqueda llega a LuaList.items: la única tabla, compartida por todas las instancias. Los números y textos en la clase sirven como valores por defecto (al asignarlos, cada instancia crea el suyo), pero toda tabla que sea de cada instancia se crea en new.

Tu primera clase

lua
require "ISBaseObject"

CursoLab = CursoLab or {}
CursoLab.Contador = ISBaseObject:derive("CursoLabContador")
local Contador = CursoLab.Contador

function Contador:new(inicio)
    local o = ISBaseObject.new(self)   -- self es Contador (o una hija, ver sección 5)
    o.n = inicio or 0
    o.historial = {}                   -- tabla propia de cada instancia
    return o
end

function Contador:sumar(k)
    k = k or 1
    self.n = self.n + k
    table.insert(self.historial, k)
    return self.n
end

local c = Contador:new(10)
c:sumar()
c:sumar(5)
print(c.n, #c.historial)   -- 16  2

ISBaseObject.new(self) (con punto y pasando self a mano) llama al new del padre pero con tu clase como self: la instancia queda con metatabla Contador. Es la misma forma que usa el juego en shared/TimedActions/ISEatFoodAction.lua:295: local o = ISBaseTimedAction.new(self, character);.

5. Herencia: ISPanel:derive y llamar al padre

Una clase hija se crea con derive desde la clase padre. El derive se hereda, así que cualquier clase del juego lo tiene. client/ISUI/ISPanel.lua, después del require, empieza así (línea 3):

lua
ISPanel = ISUIElement:derive("ISPanel");

function ISPanel:initialise()
	ISUIElement.initialise(self);
end

Esa segunda función es la forma de llamar al método del padre. Lua no tiene super: nombras la tabla del padre, usas punto y pasas self a mano. Con : (ISUIElement:initialise()) el método recibiría la clase ISUIElement como self, no tu objeto. En el Lua del juego hay más de 200 líneas ISPanel.initialise(self).

La trampa de new en una clase hija

Mira ISPanel:new (línea 96):

lua
function ISPanel:new (x, y, width, height)
	local o = {}
	o = ISUIElement:new(x, y, width, height);
    setmetatable(o, self)
    self.__index = self

Aquí el juego llama al padre con : y después arregla la metatabla con setmetatable(o, self). Funciona, pero si copias solo la primera parte te pasa esto:

lua
MiPanel = ISPanel:derive("MiPanel")

function MiPanel:new(x, y, w, h)
    local o = ISPanel:new(x, y, w, h)   -- MAL: self dentro de ISPanel:new es ISPanel
    o.titulo = "Hola"
    return o
end

function MiPanel:render()
    self:drawText(self.titulo, 10, 10, 1, 1, 1, 1, UIFont.Small)
end

o queda con metatabla ISPanel, no MiPanel: tu render nunca se llama (Java busca render en la tabla y encuentra el de ISPanel). No hay error, simplemente tu código no corre. La forma correcta es ISPanel.new(self, x, y, w, h), como en el ejemplo de 10 · Menús contextuales e interfaz.

Sobrescribir y extender

lua
CursoLab.ContadorConTope = Contador:derive("CursoLabContadorConTope")
local ConTope = CursoLab.ContadorConTope

function ConTope:new(inicio, tope)
    local o = Contador.new(self, inicio)   -- el new del padre, con mi clase como self
    o.tope = tope
    return o
end

function ConTope:sumar(k)                  -- sobrescribe el método del padre...
    if self.n >= self.tope then
        return self.n                      -- ...cambia el comportamiento en mi caso...
    end
    return Contador.sumar(self, k)         -- ...y en el resto llama al padre
end

local t = ConTope:new(0, 3)
for i = 1, 10 do t:sumar() end
print(t.n)                                  -- 3

Es la misma idea que envolver una función del juego (09 · Sobrescribir comportamiento), pero dentro de tu propia jerarquía.

Type, SuperType y los dos instanceof

Cada clase sabe su nombre (Type) y su padre (SuperType), y ISBaseObject:instanceof(obj, "Nombre") sube por esa cadena:

lua
print(t.Type)                                              -- CursoLabContadorConTope
print(t.SuperType == Contador)                             -- true
print(ISBaseObject:instanceof(t, "CursoLabContador"))      -- true
print(instanceof(getPlayer(), "IsoPlayer"))                -- true: este es el global, para objetos de Java

El juego también lee la clase de una instancia con getmetatable: client/Entity/ISEntityUI.lua:398 hace local _windowInstanceType = getmetatable(_windowInstance).Type.

6. Metamétodos: operadores para tus tablas

Además de __index, la VM de Kahlua soporta __newindex, __call, __add, __sub, __mul, __div, __mod, __pow, __unm, __concat, __eq, __lt, __le, __len, __tostring y __metatable. En el patrón del juego la metatabla de una instancia es la clase, así que los metamétodos se definen como campos de la clase.

Míralo en el juego: Point2D

client/erosion/debug/Point2D.lua, una clase de depuración con operadores:

lua
Point2D = {};
Point2D.meta = {
    __tostring 	= function (self) 		return tostring(self.x)..":"..tostring(self.y); 								end,
    __add 		= function (self,targ) 	return self.Point2D:new(self.x + targ.x, self.y + targ.y); 						end,
    __sub 		= function (self,targ) 	return self.Point2D:new(self.x - targ.x, self.y - targ.y); 						end,
    __mul 		= function (self,targ) 	return self.Point2D:new(self.x * targ.x, self.y * targ.y); 						end,
    __div 		= function (self,targ) 	return self.Point2D:new(self.x / targ.x, self.y / targ.y); 						end,
    __unm 		= function (self) 		return self.Point2D:new(-self.x, -self.y); 										end,
    __eq 		= function (self,targ) 	if self.x == targ.x and self.y == targ.y then 	return true; end return false; 	end,

No usa derive: tiene una metatabla aparte (Point2D.meta) y define los métodos dentro de new, uno por instancia (function self.copy() ... end, que se llaman con punto: p.copy()). Es otro estilo válido; el costo es crear funciones nuevas por cada punto.

Tu versión con el patrón del juego

lua
CursoLab.Vec = ISBaseObject:derive("CursoLabVec")
local Vec = CursoLab.Vec

function Vec:new(x, y)
    local o = ISBaseObject.new(self)
    o.x, o.y = x, y
    return o
end

function Vec:largo()
    return math.sqrt(self.x * self.x + self.y * self.y)
end

Vec.__add = function(a, b) return Vec:new(a.x + b.x, a.y + b.y) end
Vec.__sub = function(a, b) return Vec:new(a.x - b.x, a.y - b.y) end
Vec.__unm = function(a)    return Vec:new(-a.x, -a.y) end
Vec.__eq  = function(a, b) return a.x == b.x and a.y == b.y end
Vec.__tostring = function(v) return "(" .. v.x .. ", " .. v.y .. ")" end

local p = getPlayer()
local yo = Vec:new(p:getX(), p:getY())
local norte = Vec:new(0, -10)
print(yo + norte)                         -- usa __add y después __tostring
print(Vec:new(1, 2) == Vec:new(1, 2))     -- true gracias a __eq
print((yo - (yo + norte)):largo())        -- 10

Lo que Kahlua hace distinto, o que sorprende

Todo esto sale del código de la VM (KahluaThread y KahluaUtil en el .jar):

MetamétodoDetalle
__tostringprint y tostring lo usan. Pero antes Kahlua busca en la tabla (siguiendo __index) un campo llamado tostring (sin guiones): si es una función, usa esa. No llames tostring a un método tuyo si no es para eso
Sin __tostringtostring de una tabla no da table: 0x... como en Lua estándar: da un identificador y el contenido, algo como table 0x12345{ x=1, y=2 }. Si la tabla (o su clase) tiene un campo Type de texto, sale ese nombre en vez de table
__eqSolo se llama si las dos tablas tienen metatabla y en las dos __eq es la misma función. Comparar con un número o con una tabla sin metatabla da false sin llamarlo
__add, __sub...Se usa el del primer operando; si no tiene, el del segundo. Así 2 + v también llega a tu __add, con los argumentos en ese orden: revisa los tipos
__concat"Pos: " .. v no usa __tostring. Escribe "Pos: " .. tostring(v) y te ahorras __concat
__len#t sobre una tabla nunca lo consulta: Kahlua cuenta la tabla directamente. shared/Util/ISPriorityTable.lua:173 define __len = self.size, pero # sobre esa tabla no lo llama
__modeNo existe en Kahlua: no hay tablas débiles (L08, sección 7)

Los metamétodos no se heredan solos

La VM busca un metamétodo con rawget en la metatabla, sin seguir __index. En el patrón del juego, la metatabla de una instancia de la hija es la hija, no el padre:

lua
CursoLab.Vec3 = Vec:derive("CursoLabVec3")
local Vec3 = CursoLab.Vec3

function Vec3:new(x, y, z)
    local o = Vec.new(self, x, y)
    o.z = z
    return o
end

local v = Vec3:new(1, 2, 3)
print(v:largo())   -- funciona: los métodos normales sí se heredan por __index
print(tostring(v)) -- NO usa Vec.__tostring: Vec3 no tiene __tostring propio

Si quieres que la hija tenga los operadores del padre, cópialos (y casi siempre conviene redefinirlos para la hija):

lua
Vec3.__tostring = function(v) return "(" .. v.x .. ", " .. v.y .. ", " .. v.z .. ")" end
Vec3.__eq = Vec.__eq   -- la MISMA función, así un Vec y un Vec3 se pueden comparar

__call: una tabla que se puede llamar

__call hace que una tabla se pueda usar como función. El juego lo usa en la interfaz nueva (client/PZAPI/ui/atoms/Meta.lua:123):

lua
UI._mt.__call = function(self, _args)
    local res = setmetatable(UI._mergeTables(UI._copyValue(self), UI._copyValue(_args)), UI._mt)
    res.super = self
    return res
end

Un uso práctico para CursoLab: un log que se llama como función pero guarda configuración.

lua
CursoLab.log = setmetatable({ activo = true }, {
    __call = function(self, ...)
        if not self.activo then return end
        print("[CursoLab]", ...)
    end,
})

CursoLab.log("hola", 42)      -- [CursoLab]  hola  42
CursoLab.log.activo = false   -- y se apaga sin tocar el resto del mod

¿Por qué no hacer Contador(5) en vez de Contador:new(5)? Porque __call tendría que estar en la metatabla de la clase, que es ISBaseObject: tendrías que modificar una tabla global del juego que usan todas las clases de todos los mods. No lo hagas: :new es la convención y todo el mundo la entiende.

__newindex: controlar las escrituras

__newindex se llama cuando asignas un campo que no existe en la tabla. Sirve para una tabla de configuración de solo lectura, o para detectar errores de tipeo:

lua
local CONFIG = { maxSopaipillas = 4 }
CursoLab.CONFIG = setmetatable({}, {
    __index = CONFIG,
    __newindex = function(t, k, v)
        print("[CursoLab] CONFIG es de solo lectura, ignoré", k)
    end,
})

print(CursoLab.CONFIG.maxSopaipillas)   -- 4
CursoLab.CONFIG.maxSopaipilas = 10      -- mal escrito: avisa en vez de crear un campo nuevo

7. CursoLab: una clase Temporizador

Juntemos todo: una clase que corre algo dentro de N milisegundos reales, una hija que se repite, y un handler por instancia como el de FishingManager.

lua
-- shared/CursoLab_Temporizador.lua
require "ISBaseObject"

CursoLab = CursoLab or {}
CursoLab.Temporizador = ISBaseObject:derive("CursoLabTemporizador")
local Temporizador = CursoLab.Temporizador

function Temporizador:new(nombre, ms)
    local o = ISBaseObject.new(self)
    o.nombre = nombre
    o.ms = ms
    o.vueltas = 0
    o.activo = false
    return o
end

function Temporizador:iniciar()
    if self.activo then return end
    self.activo = true
    self.proximo = getTimestampMs() + self.ms
    self.onTickFunc = function() self:tick() end   -- closure: recuerda self
    Events.OnTick.Add(self.onTickFunc)
end

function Temporizador:detener()
    if not self.activo then return end
    self.activo = false
    Events.OnTick.Remove(self.onTickFunc)
    self.onTickFunc = nil
end

function Temporizador:tick()
    local ahora = getTimestampMs()
    if ahora < self.proximo then return end
    self.vueltas = self.vueltas + 1
    self:alSonar()
    if self:debeSeguir() then
        self.proximo = ahora + self.ms
    else
        self:detener()
    end
end

function Temporizador:alSonar()
    print("[CursoLab] sonó", tostring(self))
end

function Temporizador:debeSeguir()
    return false                    -- un temporizador simple suena una vez
end

Temporizador.__tostring = function(t)
    return t.Type .. "(" .. t.nombre .. ", " .. t.ms .. " ms, vueltas=" .. t.vueltas .. ")"
end

-- Hija: suena varias veces
CursoLab.Repetidor = Temporizador:derive("CursoLabRepetidor")
local Repetidor = CursoLab.Repetidor
Repetidor.__tostring = Temporizador.__tostring     -- los metamétodos no se heredan

function Repetidor:new(nombre, ms, veces)
    local o = Temporizador.new(self, nombre, ms)
    o.veces = veces
    return o
end

function Repetidor:debeSeguir()
    return self.vueltas < self.veces
end

function Repetidor:alSonar()
    Temporizador.alSonar(self)       -- lo que hace el padre...
    local p = getPlayer()
    if p then p:Say(self.vueltas .. " de " .. self.veces) end   -- ...y algo más
end

Uso, desde la consola de debug:

lua
local t = CursoLab.Temporizador:new("té", 3000)
t:iniciar()

local r = CursoLab.Repetidor:new("pasos", 1000, 5)
r:iniciar()
print(r)                                                     -- CursoLabRepetidor(pasos, 1000 ms, vueltas=0)
print(ISBaseObject:instanceof(r, "CursoLabTemporizador"))    -- true

Fíjate en cómo tick (escrito una sola vez, en el padre) llama a self:alSonar() y self:debeSeguir(): como self es un Repetidor, la búsqueda encuentra primero los métodos de la hija. Así la hija cambia el comportamiento sin copiar tick.

Cada instancia registra su propio handler de OnTick. Para unos pocos temporizadores está bien; si fueran cientos, conviene un solo handler que recorra una lista, como CursoLab.cada de L08.


Ejercicios

Ejercicio L9.1 · Puntos y dos puntos

Este código tiene tres errores de . y :. Encuéntralos sin ejecutarlo, explica qué valor termina en self en cada uno y corrígelos.

lua
CursoLab.Caja = ISBaseObject:derive("CursoLabCaja")

function CursoLab.Caja:new(capacidad)
    local o = ISBaseObject:new(self)
    o.capacidad = capacidad
    o.cosas = {}
    return o
end

function CursoLab.Caja:meter(cosa)
    if #self.cosas >= self.capacidad then return false end
    table.insert(self.cosas, cosa)
    return true
end

local c = CursoLab.Caja:new(2)
c.meter("sopaipilla")
Events.EveryOneMinute.Add(c.meter)

Listo cuando: la caja acepta dos cosas, rechaza la tercera, y cada minuto de juego mete un "minuto" en esa caja.

Solución
  1. ISBaseObject:new(self): con :, el self de ISBaseObject.new es ISBaseObject y tu self (la clase Caja) llega como argumento ignorado. La instancia queda con metatabla ISBaseObject y no encuentra meter. Va ISBaseObject.new(self).
  2. c.meter("sopaipilla"): self es el texto "sopaipilla" y cosa es nil. Va c:meter("sopaipilla").
  3. Events.EveryOneMinute.Add(c.meter): el evento llama meter() sin argumentos, así que self es nil. Hace falta una closure que recuerde c.
lua
CursoLab.Caja = ISBaseObject:derive("CursoLabCaja")

function CursoLab.Caja:new(capacidad)
    local o = ISBaseObject.new(self)
    o.capacidad = capacidad
    o.cosas = {}
    return o
end

function CursoLab.Caja:meter(cosa)
    if #self.cosas >= self.capacidad then return false end
    table.insert(self.cosas, cosa)
    return true
end

local c = CursoLab.Caja:new(2)
c:meter("sopaipilla")
local alMinuto = function() c:meter("minuto") end
Events.EveryOneMinute.Add(alMinuto)

Ejercicio L9.2 · Una pila con clase

Escribe CursoLab.Pila, derivada de ISBaseObject, con :poner(x), :sacar() (devuelve el último y lo quita, o nil si está vacía), :mirar() y :tamano(). Crea dos pilas y comprueba que no comparten datos.

Listo cuando: a:poner(1); b:poner(2); print(a:tamano(), b:tamano()) imprime 1 1.

Solución
lua
CursoLab.Pila = ISBaseObject:derive("CursoLabPila")
local Pila = CursoLab.Pila

function Pila:new()
    local o = ISBaseObject.new(self)
    o.items = {}            -- en new, no en la clase
    return o
end

function Pila:poner(x)
    table.insert(self.items, x)
end

function Pila:sacar()
    return table.remove(self.items)    -- nil si está vacía
end

function Pila:mirar()
    return self.items[#self.items]
end

function Pila:tamano()
    return #self.items
end

Si hubieras puesto Pila.items = {} en la clase, las dos pilas compartirían la misma tabla y el resultado sería 2 2.

Ejercicio L9.3 · Operadores para Vec

Agrega a CursoLab.Vec (sección 6):

  1. __mul que acepte vec * numero y numero * vec (escala el vector).
  2. Un método :distancia(otro).
  3. Úsalo para imprimir a cuántos cuadros está el jugador del punto donde estaba hace 10 segundos (usa CursoLab.cada de L08 o un Temporizador).

Listo cuando: print(Vec:new(1, 2) * 3, 3 * Vec:new(1, 2)) imprime (3, 6) (3, 6).

Solución
lua
local Vec = CursoLab.Vec

Vec.__mul = function(a, b)
    if type(a) == "number" then a, b = b, a end   -- 3 * v llega como (3, v)
    return Vec:new(a.x * b, a.y * b)
end

function Vec:distancia(otro)
    return (self - otro):largo()
end

local antes
CursoLab.cada(10000, function()
    local p = getPlayer()
    if not p then return end
    local ahora = Vec:new(p:getX(), p:getY())
    if antes then
        print("[CursoLab] me moví", math.floor(ahora:distancia(antes)), "cuadros")
    end
    antes = ahora
end)

__mul es el del primer operando que tenga uno; con 3 * v, el número no tiene __mul, así que se usa el de v, pero los argumentos llegan en el orden original.

Ejercicio L9.4 · Una alarma que hereda

Crea CursoLab.Alarma, hija de CursoLab.Temporizador, que al sonar haga lo mismo que el padre y además haga que el jugador diga el nombre de la alarma. Agrega un método :posponer(ms) que mueva la próxima vez que suena sin reiniciar las vueltas.

Listo cuando: print(alarma) muestra CursoLabAlarma(...) y ISBaseObject:instanceof(alarma, "CursoLabTemporizador") da true.

Solución
lua
local Temporizador = CursoLab.Temporizador

CursoLab.Alarma = Temporizador:derive("CursoLabAlarma")
local Alarma = CursoLab.Alarma
Alarma.__tostring = Temporizador.__tostring

function Alarma:alSonar()
    Temporizador.alSonar(self)
    local p = getPlayer()
    if p then p:Say(self.nombre) end
end

function Alarma:posponer(ms)
    if self.activo then
        self.proximo = self.proximo + ms
    end
end

local alarma = Alarma:new("¡A comer!", 5000)
alarma:iniciar()
alarma:posponer(2000)    -- suena a los ~7 segundos

No hace falta escribir Alarma:new: se hereda el de Temporizador, y como lo llamas con Alarma:new(...), ahí adentro self es Alarma.

Ejercicio L9.5 · Leer una clase del juego

Abre shared/Util/LuaList.lua completo y responde:

  1. ¿Qué índice usa LuaList:get(index): desde 0 o desde 1? ¿Por qué crees que lo hicieron así?
  2. ¿Qué imprime print(LuaList:new()) en la consola de Kahlua? Predícelo antes de probarlo (pista: la tabla de la sección 6).
  3. ¿Qué pasa si llamas lista:remove(x) con un x que está dos veces?

Listo cuando: comprobaste las tres respuestas en la consola.

Solución
  1. Desde 0: return self.items[index+1]. Imita a las listas de Java (ArrayList.get empieza en 0, L06), así el código que la usa se parece al que recorre listas de Java.
  2. Algo como LuaList 0x12345{ items=table 0x6789, count=0 }: no hay __tostring, así que Kahlua muestra el contenido, y en vez de table pone el Type que la instancia hereda de la clase. Los números exactos cambian en cada ejecución.
  3. Quita solo la primera aparición: el for hace return item apenas encuentra una.

Desafío L9

Escribe CursoLab.clase(nombre, padre): crea una clase con (padre or ISBaseObject):derive(nombre) y copia automáticamente a la hija los metamétodos que el padre tenga como campos propios (__tostring, __eq, __add, __sub, __mul, __div, __unm, __lt, __le). Reescribe Vec3, Repetidor y Alarma con ella y borra las líneas que copiaban metamétodos a mano. Después responde: ¿qué pasa si el padre define un metamétodo después de que creaste la hija? Propón una solución.


Anterior: L08 · Eventos y estado · Siguiente: L10 · Lua avanzado en Kahlua