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.
local datos = { nombre = "sopaipilla" }
local meta = {}
setmetatable(datos, meta) -- desde ahora, meta es la metatabla de datos
print(getmetatable(datos) == meta) -- truesetmetatable devuelve la misma tabla que recibe, así que puedes crear y asignar en una línea:
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í:
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:
__indexsolo se usa al leer un campo que falta. Si el campo existe en la tabla, gana siempre.- Escribir no pasa por
__index:comida.hambre = 20crea el campo encomida, no cambiavaloresPorDefecto. - Si
valoresPorDefectoa 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:
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:
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ámetroY estas dos llamadas también:
c.sumar(c, 5)
c:sumar(5) -- el ":" pasa c como primer argumentoself 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 :.
c.sumar(5) -- self = 5, k = nil -> error al hacer self.nMíralo en el juego: un método como handler de evento
client/Tutorial/Steps.lua:913:
function FightStep:OnMomDead(zed)
Events.OnZombieDead.Remove(FightStep.OnMomDead);
FightStep.momDead = true;
endRegistrado 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:
function FishingManager:eventHooks()
self.onTickFunc = function()
self:update()
end
Events.OnTick.Add(self.onTickFunc)
end
function FishingManager:removeEventHooks()
Events.OnTick.Remove(self.onTickFunc)
endLa 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
__indexapuntando 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:
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
endderive(type)crea una clase hija: una tabla vacía cuya metatabla es la clase padre (self). Le poneType(su nombre, un texto) ySuperType(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:
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;
endY al final del archivo (línea 136):
function LuaList:new ()
local o = {}
setmetatable(o, self)
self.__index = self
o.items = {}
o.count = 0;
return o
endSigue el camino de lista:add("x") con lista = LuaList:new():
| Paso | Dónde busca add | Resultado |
|---|---|---|
| 1 | En lista (solo tiene items y count) | No está |
| 2 | Metatabla de lista = LuaList, su __index = LuaList | Está: LuaList.add |
| 3 | Llama 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?
-- 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 itemsa.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
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 2ISBaseObject.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):
ISPanel = ISUIElement:derive("ISPanel");
function ISPanel:initialise()
ISUIElement.initialise(self);
endEsa 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):
function ISPanel:new (x, y, width, height)
local o = {}
o = ISUIElement:new(x, y, width, height);
setmetatable(o, self)
self.__index = selfAquí 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:
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)
endo 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
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) -- 3Es 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:
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 JavaEl 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:
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
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()) -- 10Lo que Kahlua hace distinto, o que sorprende
Todo esto sale del código de la VM (KahluaThread y KahluaUtil en el .jar):
| Metamétodo | Detalle |
|---|---|
__tostring | print 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 __tostring | tostring 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 |
__eq | Solo 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 |
__mode | No 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:
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 propioSi quieres que la hija tenga los operadores del padre, cópialos (y casi siempre conviene redefinirlos para la hija):
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):
UI._mt.__call = function(self, _args)
local res = setmetatable(UI._mergeTables(UI._copyValue(self), UI._copyValue(_args)), UI._mt)
res.super = self
return res
endUn uso práctico para CursoLab: un log que se llama como función pero guarda configuración.
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:
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 nuevo7. 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.
-- 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
endUso, desde la consola de debug:
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")) -- trueFí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.
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
ISBaseObject:new(self): con:, elselfdeISBaseObject.newesISBaseObjecty tuself(la clase Caja) llega como argumento ignorado. La instancia queda con metatablaISBaseObjecty no encuentrameter. VaISBaseObject.new(self).c.meter("sopaipilla"):selfes el texto"sopaipilla"ycosaesnil. Vac:meter("sopaipilla").Events.EveryOneMinute.Add(c.meter): el evento llamameter()sin argumentos, así queselfesnil. Hace falta una closure que recuerdec.
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
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
endSi 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):
__mulque aceptevec * numeroynumero * vec(escala el vector).- Un método
:distancia(otro). - Úsalo para imprimir a cuántos cuadros está el jugador del punto donde estaba hace 10 segundos (usa
CursoLab.cadade L08 o unTemporizador).
Listo cuando: print(Vec:new(1, 2) * 3, 3 * Vec:new(1, 2)) imprime (3, 6) (3, 6).
Solución
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
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 segundosNo 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:
- ¿Qué índice usa
LuaList:get(index): desde 0 o desde 1? ¿Por qué crees que lo hicieron así? - ¿Qué imprime
print(LuaList:new())en la consola de Kahlua? Predícelo antes de probarlo (pista: la tabla de la sección 6). - ¿Qué pasa si llamas
lista:remove(x)con unxque está dos veces?
Listo cuando: comprobaste las tres respuestas en la consola.
Solución
- Desde 0:
return self.items[index+1]. Imita a las listas de Java (ArrayList.getempieza en 0, L06), así el código que la usa se parece al que recorre listas de Java. - Algo como
LuaList 0x12345{ items=table 0x6789, count=0 }: no hay__tostring, así que Kahlua muestra el contenido, y en vez detablepone elTypeque la instancia hereda de la clase. Los números exactos cambian en cada ejecución. - Quita solo la primera aparición: el
forhacereturn itemapenas 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