WENOPAL

11 · Timed actions

Cómo escribir acciones con barra de progreso que se animen, se cancelen bien, se encolen sin trabarse y funcionen en el multijugador de Build 42.

Curso técnico · Build 42 · Actualizado el

Objetivo: escribir acciones con barra de progreso (comer, tallar, reparar) que se animen bien, se puedan cancelar, se encadenen en la cola y funcionen en multijugador de B42.

1. Qué es una timed action

Todo lo que el personaje "hace durante un rato" es una timed action: una tabla de Lua derivada de ISBaseTimedAction (shared/TimedActions/ISBaseTimedAction.lua), con su mitad Java (zombie.characters.CharacterTimedActions.LuaTimedActionNew), igual que las ventanas del módulo 10. Las del juego están en shared/TimedActions/ (143 archivos) y client/TimedActions/ (21).

Se encolan, no se ejecutan:

lua
ISTimedActionQueue.add(MiAccion:new(player, item))

La cola (client/TimedActions/ISTimedActionQueue.lua) las corre una tras otra. Otras funciones útiles: addAfter(anterior, accion), clear(character), hasActionType(character, "ISEatFoodAction"), isPlayerDoingAction(player).

2. El ciclo de vida

Java llama a tus funciones por nombre en este orden:

new()            ← tú, al crearla: guarda datos y calcula maxTime
  │
isValidStart()   ← ¿se puede empezar?
waitToStart()    ← devuelve true mientras haya que esperar (por ejemplo, girarse hacia algo)
start()          ← una vez: animación, sonido, modelos en las manos
update()         ← cada tick: progreso, comprobaciones
isValid()        ← cada tick: si devuelve false, se cancela
  │
  ├── si se cancela (te mueves, te atacan, isValid falla) → stop()
  │
  └── si termina → perform()   (cliente: limpieza visible)
                   complete()  (el efecto real; ver sección 4)

Campos de ISBaseTimedAction:new que puedes cambiar: stopOnWalk, stopOnRun, stopOnAim (todos true por defecto), caloriesModifier y maxTime. Java lee además, si existen, useProgressBar, forceProgressBar, loopedAction y allowedWhileDraggingCorpses (lo ves en el constructor de LuaTimedActionNew). Comer, por ejemplo, pone stopOnWalk = false: se puede comer caminando.

Pumpkin Z DIY completo, como referencia

En Pumpkin Z DIY, uno de nuestros mods, .../shared/TimedActions/PumpkinZCarveAction.lua es una acción B42 correcta y corta. Las piezas:

lua
PumpkinZCarveAction = ISBaseTimedAction:derive("PumpkinZCarveAction")

function PumpkinZCarveAction:isValid()                       -- ¿todavía tengo la calabaza y el cuchillo?
    local inv = self.character:getInventory()
    return self.pumpkin and self.knife and inv:contains(self.pumpkin) and inv:contains(self.knife)
        and PumpkinZ.isValidCarve(self.carve)
end

function PumpkinZCarveAction:update()
    self.pumpkin:setJobDelta(self:getJobDelta())              -- la barrita sobre el ícono del ítem
end

function PumpkinZCarveAction:start()
    self.pumpkin:setJobType(getText("IGUI_PumpkinZ_Carving"))
    self:setActionAnim("Craft")                               -- animación del personaje
    self:setOverrideHandModels(self.knife, self.pumpkin)      -- qué tiene en cada mano
    self.sound = self.character:playSound("SliceFood")
end

function PumpkinZCarveAction:stop()                           -- cancelada
    self:stopSound()
    self.pumpkin:setJobDelta(0.0)
    ISBaseTimedAction.stop(self)                              -- ¡siempre llamar al padre!
end

function PumpkinZCarveAction:perform()                        -- terminó (lado visible)
    self:stopSound()
    self.pumpkin:setJobDelta(0.0)
    ISBaseTimedAction.perform(self)                           -- ¡siempre! avisa a la cola que siga
end

function PumpkinZCarveAction:complete()                       -- terminó (el efecto)
    ... quita la calabaza, agrega la tallada, sincroniza ...
    return true
end

function PumpkinZCarveAction:getDuration()
    if self.character:isTimedActionInstant() then return 1 end   -- modo debug "acciones instantáneas"
    return math.min(1800, 90 + PumpkinZ.countCuts(self.carve) * 8)
end

function PumpkinZCarveAction:new(character, pumpkin, knife, carve)
    local o = ISBaseTimedAction.new(self, character)
    o.pumpkin, o.knife, o.carve = pumpkin, knife, carve
    o.maxTime = o:getDuration()
    return o
end

Olvidar ISBaseTimedAction.perform(self) es el bug más común: la acción termina pero la cola queda trabada y el personaje no hace nada más. ISBaseTimedAction.perform es lo que llama a onCompleted en la cola.

3. La duración

maxTime no es en segundos: son unidades de tiempo de acción. Además, ISBaseTimedAction:create() la pasa por adjustMaxTime, que la alarga según el estado del personaje:

lua
maxTime = maxTime * (1 + infeliz / 4)              -- nivel del moodle de infelicidad
maxTime = maxTime * (1 + borracho / 4)
maxTime = maxTime * (1 + dolorDeManos / 300)       -- salvo que pongas ignoreHandsWounds = true
maxTime = maxTime * character:getTimedActionTimeModifier()   -- temperatura del cuerpo

Si tu acción debe durar exactamente lo mismo siempre (por ejemplo, sincronizada con una animación), sobrescribe adjustMaxTime para que devuelva maxTime sin cambios. En Batalla de Aura, uno de nuestros mods, lo hacemos en DanceAction para los bailes.

4. Multijugador: perform vs complete

Este es el cambio grande de B42, y está a la vista en LuaTimedActionNew (decompílala como en el módulo 08):

java
public void complete() {
   super.complete();
   if (!GameClient.client) {                                  // ← solo si NO soy un cliente
      LuaManager.caller.pcall(LuaManager.thread, this.table.rawget("complete"), this.table);
   }
}

y en el constructor:

java
if (table.getMetatable().rawget("complete") == null) {
   this.useCustomRemoteTimedActionSync = true;                // ← sin complete(): modo antiguo
}

En palabras:

Dónde correperform()complete()
Un jugadorsísí
Cliente de un servidorsíno
Servidor—sí
  • Si tu acción tiene complete(), el cliente le pide la acción al servidor (Java maneja la transacción con ActionManager) y el servidor ejecuta complete(). El efecto va en complete(): así lo decide el servidor y no se puede hacer trampa desde el cliente.
  • Para eso el servidor tiene que poder crear tu acción por nombre: la clase tiene que estar en shared/. El comentario de Pumpkin Z DIY lo dice: "En shared: en multijugador B42 el servidor crea la accion por nombre y ejecuta complete()."
  • Si tu acción no tiene complete(), funciona a la antigua (todo en el cliente, sin sincronizar). Sirve para acciones puramente locales; para cualquier cosa que cambie el mundo o el inventario, no.
  • Lo que hace complete() tiene que avisarse a los clientes: Pumpkin Z DIY llama sendRemoveItemFromContainer y sendAddItemToContainer después de tocar el inventario. El módulo 12 explica la sincronización.
  • isValid() corre en los dos lados. Pumpkin Z DIY revalida el dibujo (isValidCarve) porque viene del cliente y no se confía en él.

5. Moverse antes de actuar

Para acciones sobre algo del mundo, primero hay que caminar hasta ahí. El juego encola primero un ISWalkToTimedAction (52 usos) o usa luautils.walkAdj(player, square) (281 usos), que camina a un cuadro adyacente libre, y después encola la acción:

lua
if luautils.walkAdj(player, objeto:getSquare()) then
    ISTimedActionQueue.add(MiAccion:new(player, objeto))
end

Ejercicios

Ejercicio 11.1 · Freír sopaipillas

Haz CursoLab.FreirAction: con una sartén y una sopaipilla cruda en el inventario, la fríes (resultado: sopaipilla cocida). Requisitos:

  • animación y sonido mientras dura,
  • la sartén se ve en la mano,
  • se cancela si sueltas la sartén,
  • la duración baja con tu nivel de cocina.

Listo cuando: funciona, se cancela bien, y después de cancelar puedes encolar otra cosa sin que el personaje quede trabado.

Ejercicio 11.2 · El bug de la cola

Quita ISBaseTimedAction.perform(self) de tu acción y encola dos frituras seguidas. Describe qué pasa. Después busca en ISTimedActionQueue.lua la función que no se llamó y explica la causa.

Ejercicio 11.3 · Sobre el mundo

Haz una acción "Golpear la puerta" sobre una puerta: el personaje camina hasta ella (luautils.walkAdj), golpea durante unos segundos y al final se emite un ruido que atrae zombis (busca cómo hace ruido el juego: addSound).

Ejercicio 11.4 · Mídelo

Con print en start y perform y getTimestampMs(), mide cuántos segundos reales dura tu acción con maxTime = 100, a velocidad normal y estando muy infeliz. Compáralo con la fórmula de adjustMaxTime.

Ejercicio 11.5 · Prepararla para multijugador

Revisa tu acción del 11.1 contra esta lista y corrígela:

  1. ¿Está en shared/?
  2. ¿El efecto está en complete() y devuelve true?
  3. ¿isValid() comprueba todo lo que viene del cliente?
  4. ¿Avisas los cambios de inventario (sendAddItemToContainer, sendRemoveItemFromContainer)?

La prueba real la haces en el módulo 12 con un servidor local.

Desafío 11

Una acción en bucle: "Pescar hasta que pare" o "Picar leña hasta llenar el inventario" que se repite sola hasta que se cumpla una condición o el jugador la cancele. Busca cómo lo hace el juego (pista: en el constructor de LuaTimedActionNew se lee un campo loopedAction).


Anterior: 10 · Menús e interfaz · Siguiente: 12 · Multijugador