singe/docs/es/Reference.adoc

20033 lines
796 KiB
Text

= Referencia de Singe
Scott Duensing <scott@kangaroopunch.com>
:revnumber: 3.00
:revdate: 2026
:doctype: book
:toc: left
:toclevels: 3
:sectnums:
:sectnumlevels: 3
:title-logo-image: image:images/singeLogo.png[Singe, pdfwidth=2.6in, align=center]
:source-highlighter: rouge
:icons: font
:experimental:
:lang: es
:imagesdir: ..
include::locale.adoc[]
[#_game_development]
== Desarrollo de juegos
[#_lua]
=== Lua
Singe usa el lenguaje de programación Lua (http://www.lua.org) para programar la
lógica del juego. Lua es rápido, ligero, orientado a objetos, fácil de usar y se
usa de verdad en la industria del videojuego. Este libro no lo enseña;
_Aprende a programar con Singe_, incluido en la misma descarga que este, sí lo
hace, desde cero hasta un juego terminado en treinta y una lecciones. En el canal
de YouTube de Kangaroo Punch hay videotutoriales de Lua, y la referencia del
propio lenguaje está en http://www.lua.org.
[#_basic_rules]
=== Reglas básicas
* Singe es multiplataforma. Cosas de las que normalmente no tienes que
preocuparte en Windows son importantes. Los nombres de archivo distinguen entre
mayúsculas y minúsculas -- `MyScript.singe` y `myscript.singe` no son el mismo
archivo. También deberías usar los separadores de ruta de UNIX -- `/` en lugar
de `\`. Así tu código queda más limpio (sin barras invertidas con doble escape)
y funciona en todos los sistemas en los que Singe está soportado.
* Elige un nombre distintivo para la carpeta de tu juego. No uses espacios.
* Usa el framework de Singe incluido. No copies el framework en los scripts de tu
juego. Al incluir el framework proporcionado ayudas a la compatibilidad futura a
medida que Singe se actualiza. Empieza (o termina) el script de tu juego con:
+
[source,lua]
----
dofile("Singe/Framework.singe")
----
* Mantén tu juego autocontenido. Si construyes sobre un framework de terceros o
compartes código entre tus juegos, cópialo en la carpeta de tu juego. Nunca
hagas referencia a una carpeta que esté junto a tu juego; el único archivo de
fuera de tu juego que puede cargar un script es `Singe/Framework.singe`. Un
juego de un solo archivo (véase <<singlefile,Juegos de un solo archivo>>)
empaqueta exactamente una carpeta, y un juego que sale de ella no se puede
empaquetar.
* No toques la carpeta `Singe/`. La gestiona Singe, y todo lo que se añada o se
cambie ahí puede borrarse en el futuro.
* Incluye un `games.dat`. Esto es extremadamente importante para los usuarios
nuevos. Aunque existen otros front-ends y sistemas de menús basados en Singe, es
vital que incluyas los datos para el sistema de menús integrado. Casi todos los
problemas de soporte se deben a archivos `games.dat` ausentes o mal formados.
* No distribuyas archivos que no sean imprescindibles. Nunca incluyas binarios de
Singe ni scripts proporcionados por Singe con tu juego. No incluyas archivos de
índice generados a partir de los vídeos.
[#reloading]
=== Recargar mientras trabajas
Ejecuta el juego con `--reload` (`-R`) y Singe vigila todos los archivos de script
sueltos que el juego ha cargado. Guarda un cambio en uno de ellos y, un momento
después, el juego vuelve a empezar desde su script: todo lo que hizo el script
desaparece (los sprites, los sonidos, las fuentes, los vídeos, las GUI, la
escena 3D, la física, los emisores; el overlay vuelve a su tamaño por defecto, y
el volumen de efectos y el ratón a su estado inicial), el motor se queda (la
ventana, la GPU, los mandos), el disco vuelve a quedar aparcado en su primer
fotograma como al arrancar, y el script se ejecuta de nuevo, así que un modelo de
52 MB vuelve a estar en pantalla en lo que tarda en cargarse en lugar de en lo que
tarda Singe en arrancar. Una tecla o un botón que se mantiene pulsado durante la
recarga se ignora hasta que se suelta, como en un arranque nuevo. `F5` recarga
cuando quieras, y un script puede pedirlo con `singeReload()`. Los juegos
empaquetados no tienen nada que vigilar; la opción es para un juego en una
carpeta. Un error de script al recargar se imprime y el juego se queda vacío
hasta que la siguiente recarga lo arregla; el archivo sigue vigilado, así que
basta con guardar la corrección. Como la recarga hace el mismo desmontaje que al
salir, cualquier fuga de un script aparece aquí primero.
[[deterministic]]
=== Modo de prueba determinista
`--deterministic` es una herramienta de pruebas, no una forma de jugar. Hace que
el motor ignore por completo el tiempo real: en lugar de preguntar al ordenador
qué hora es, el motor funciona con un reloj virtual que avanza un paso fijo -- 15
milisegundos por defecto -- exactamente una vez por fotograma. Todo lo que se rige
por el tiempo sigue ese reloj: los pasos de la física, la animación de sprites y
modelos, las partículas, la navegación, las animaciones y transiciones de la
GUI, `os.clock()`, `singeGetTicks()` y un `os.time()` sin argumentos (un
juego que siembra su generador con el reloj al empezar una partida lo siembra con
el virtual, así que obtiene los mismos números en cada ejecución; `os.time` con
una tabla de fecha la convierte como Lua ha hecho siempre). El disco también lo
sigue: mientras se reproduce, cada fotograma de vídeo permanece en pantalla
durante tantos fotogramas del motor como valga su tiempo de presentación en ese
reloj (dos o tres con el paso por defecto para un disco de treinta fotogramas),
como al jugar de verdad, y un disco en pausa o tras una búsqueda se queda donde
se dejó. El generador de Lua y el del propio motor se siembran con un número
fijo. Nada espera a nada, así que el juego va tan rápido como le deja el
ordenador.
El objetivo de todo esto es que una captura de pantalla tomada en un número de
fotograma fijo sea la misma imagen en cada ejecución. Sin él, el fotograma 40 es
un momento distinto cada vez -- la física ha dado un número distinto de pasos y el
disco está en otro fotograma de vídeo -- así que dos ejecuciones del mismo juego
sin cambios pueden diferir en un millón de píxeles, y un cambio real puede
esconderse en ese ruido.
Lo que cuesta es todo lo que depende del tiempo real. El audio sigue sonando con
el reloj del propio dispositivo, al que no llega ningún reloj virtual, así que el
sonido se desfasa de la imagen y no significa nada -- salvo que la ejecución se
grabe con `--record`, en cuyo caso el sonido se mezcla en memoria paso a paso
junto con la imagen, de modo que una grabación hecha así es exacta fotograma a
fotograma y muestra a muestra (véase <<singerecordstart,singeRecordStart>>). La
tasa de fotogramas tampoco significa nada, ni nada que un juego mida en segundos.
Nunca informes de una cifra de rendimiento, de un problema de audio ni de un
problema de sincronización a partir de una ejecución en este modo, y nunca
distribuyas un lanzador que lo use.
El número que lleva la opción, si lleva uno, es a la vez el tamaño del paso y la
semilla con la que empiezan los generadores, de `1` a `1000`:
----
Singe --deterministic el paso por defecto de 15 ms, con semilla 15
Singe --deterministic=20 un paso de 20 ms, con semilla 20
----
Dos ejecuciones con el mismo número son idénticas; dos ejecuciones con números
distintos son cada una repetible pero no iguales entre sí, que es como una prueba
pide una ejecución distinta pero igual de reproducible. Puede ponerse en un
archivo de ajustes como `deterministic`, igual que cualquier otra opción.
[#_reference_screenshots]
==== Capturas de pantalla de referencia
Cada una de las escenas de `testScripts` toma una o dos capturas de pantalla en un
número de fotograma fijo y sale por sí sola, así que en este modo forman un
conjunto de imágenes de referencia con las que comparar píxel a píxel un cambio
posterior. Para grabarlas, ejecuta todas las escenas con la opción y guarda lo que
acabe en la carpeta de datos:
[source,sh]
----
for n in 2 3 4 5 6 7 8 9 10 11 13 14 15 16 18 19 21 22 23 24 25 26 \
36 37 38 39 40 41 42 43 44 45 48 49 50 51; do
Singe -w -d data --deterministic -v Singe/menuBackground.mkv \
testScripts/scene$n.singe
done
----
Las escenas que se quedan fuera de ese bucle quieren sus propias opciones, las mismas que les
da la tabla de `testScripts/README.md`: `scene12` se ejecuta sin disco,
`scene17` con el disco y `scene20` con `-C 720x480`, las tres con
`SDL_GPU_DRIVER=nothing`; de `scene27` a `scene35` quieren `-x 1280 -y 720
-C 1280x720` y ningún disco; `scene46` quiere `-g '10 5'`; y `scene47` quiere
`--bezeldir=testScripts/bezels --bezel=cabinet.png`. Copia los archivos `singe*.png`
fuera de `data/testScripts` después de cada ejecución, ya que la siguiente vuelve
a numerar sus capturas desde cero.
Comparar una compilación posterior consiste entonces en ejecutar los mismos
comandos y pedir la diferencia, que debe ser nula:
[source,sh]
----
compare -metric AE reference/scene36-singe000.png new/scene36-singe000.png null:
----
[#_where_files_live]
=== Dónde están los archivos
Las rutas dentro de un script son relativas a la carpeta desde la que se inició
Singe, no al script. Usa la variable global `DIR`, que `Framework.singe` fija a la
carpeta del propio script, para que un juego funcione esté donde esté
instalado:
[source,lua]
----
crosshair = spriteLoad(DIR .. "sprite_Crosshair.png")
----
Todo lo que escribe Singe va a la carpeta de datos (`singeGetDataPath()`),
`data/` junto a los juegos salvo que `--datadir` diga otra cosa, así que un juego
puede estar en un soporte de solo lectura. Singe 2.x escribía las partidas
guardadas y los ajustes en la propia carpeta del juego; un juego que aún lo
hace falla en una instalación de solo lectura, así que mantén todo `io.output`
dentro de `singeGetDataPath()`.
[#_packaging_your_game]
=== Empaquetar tu juego
Un juego terminado se distribuye como un único archivo `.game`, que se describe a
continuación. No hace falta nada más: el jugador copia el archivo junto al binario
de Singe y el menú lo muestra. Singe ya no instala archivos comprimidos.
[#singlefile]
=== Juegos de un solo archivo
Un juego puede ser un único archivo. `singe --pack` escribe una carpeta de juego
en una base de datos SQLite, y Singe lo ejecuta desde ahí: los scripts, los
sprites, los sonidos, las fuentes, los framefiles y el vídeo se cargan
directamente de la base de datos, e instalar el juego es copiar el archivo junto
al ejecutable. El menú muestra todos los archivos `.game` que encuentra junto a
las carpetas de los juegos.
----
Singe --pack MyGame MyGame.game Empaqueta la carpeta MyGame en MyGame.game
Singe MyGame.game Ejecuta la primera entrada de su games.dat
Singe --entry=2 MyGame.game Ejecuta su segunda entrada
Singe --unpack MyGame.game MyGame Vuelve a escribir los archivos fuera
Singe --patch MyGame.game fixes Sustituye archivos desde una carpeta (o una base de datos de parche)
----
Para empaquetar un juego no hay que cambiar nada en él. Todo nombre que usa un
script, ya sea mediante `dofile`, `require`, `io.open` y sus parientes,
`spriteLoad`, `soundLoad`, `fontLoad`, `videoLoad`, un framefile o un
`controls.cfg`, pasa por una única búsqueda. Para un juego suelto esa búsqueda es
el sistema de archivos sin más, relativa a la carpeta desde la que se inició
Singe, exactamente como antes. Para un juego empaquetado, un nombre se busca en
tres sitios, en este orden, y gana el primero que lo tiene:
. Una carpeta suelta con el nombre de la base de datos sin su extensión
(`MyGame/` junto a `MyGame.game`). Así es como sigues editando un juego
empaquetado, y como un jugador le hace un mod: los archivos que se dejan ahí
ganan a las copias empaquetadas.
. La carpeta de datos del juego, dentro de `files/`. Todo lo que un juego
escribe con `io.output` o `io.open(name, "w")` acaba aquí, así que un
`game.cfg` guardado gana al original empaquetado y la base de datos nunca se
escribe.
. La propia base de datos.
Dentro de una base de datos, los nombres se comparan sin distinguir mayúsculas de
minúsculas, con cualquiera de los dos separadores de ruta y de forma relativa a la
raíz del juego. Un primer componente igual al nombre de la propia carpeta del
juego se ignora, así que `MyGame/Cfg/game.cfg`, `Cfg/game.cfg` y
`DIR .. "Cfg/game.cfg"` encuentran el mismo archivo. El nombre de la propia base
de datos (`MyGame.game`, con o sin separador final) es la raíz del juego, como lo
es `.` desde dentro. Los nombres que empiezan por `Singe/` o por la carpeta de
datos se quedan en el sistema de archivos; `Singe/Framework.singe` y
`singeGetDataPath()` funcionan como siempre, y un nombre así puede usar `..`
siempre que se quede por debajo de ese prefijo (el `controls.cfg` de la raíz de la
carpeta de datos se encuentra así). Cualquier otro nombre relativo con un
componente `..` no se resuelve en nada dentro de un juego empaquetado: `io.open`
devuelve `nil` con un mensaje que dice que el nombre sale del juego,
`lfs.attributes` devuelve `nil`, y `spriteLoad` y las de su clase informan de que
el archivo falta. La raíz del juego es una carpeta, no un archivo: `io.open`
sobre ella devuelve `nil`, y `lfs.mkdir` y `lfs.rmdir` la rechazan. `lfs.dir`
sobre una carpeta empaquetada lista la unión de los tres sitios, ordenada sin
distinguir mayúsculas de minúsculas; cuando un archivo suelto y uno empaquetado
solo difieren en mayúsculas y minúsculas, se lista la grafía del suelto.
`lfs.attributes` sobre un archivo empaquetado responde como lo hace el `lfs` real:
la tabla completa con un argumento, un campo por su nombre (un nombre desconocido
lanza un error), o una tabla tuya rellenada; un archivo que falta devuelve `nil`,
un mensaje y el número de error.
`require("name")` encuentra `name.lua`, `name/init.lua` o `name.singe` en la
carpeta del script y después en la raíz del juego.
`io.open`, `io.lines` e `io.input` sobre un nombre que solo existe dentro de la
base de datos devuelven un archivo propio de Singe en lugar de uno de Lua. El
handle de archivo de Lua es un `FILE *` de C, y una fila de una base de datos no
es un archivo, así que leer una suponía antes escribirla junto al juego y abrir
esa copia; el handle de Singe lee la fila donde está. Responde a todo lo que
responde el de Lua -- `read` con todos los formatos (`"a"`, `"l"`, `"L"`, `"n"` y
un número de bytes), `lines`, `seek`, `close`, `flush` y `setvbuf` -- `io.type` lo
considera un archivo, `io.read` e `io.close` lo aceptan, y se cierra solo cuando
el recolector de basura lo recoge o cuando una variable `<close>` sale de su
ámbito. La única diferencia es que escribir en él es un error, porque lo que lee
está dentro de la base de datos y la base de datos nunca se escribe. Un juego que
escribe un archivo obtiene uno real en su carpeta de datos exactamente como
antes, y lo mismo ocurre con todo nombre que no está empaquetado.
`testScripts/packedIo` es la prueba que ata todo esto al comportamiento del propio
Lua; su cabecera explica cómo ejecutarla.
El empaquetador rechaza una carpeta que no tiene `games.dat`, que contiene
`controls.dat`, `Framework.singe`, un archivo cuya extensión es `exe`, `sh`,
`bat` o `cmd`, o un archivo sin extensión cuyo nombre empieza por `singe`, que
tiene una entrada de primer nivel con el mismo nombre que la propia carpeta
(eso haría ambigua la regla de la propia carpeta), o cuyos scripts y archivos de
datos salen del juego con `..`; e informa de cada uno de esos archivos, con la
línea. Dos archivos cuyos nombres solo difieren en mayúsculas y minúsculas no se
pueden empaquetar juntos. Los archivos `.index` obsoletos se omiten. `--unpack`
rechaza una base de datos cuyos nombres guardados acabarían fuera de la carpeta
de destino (una ruta absoluta, una letra de unidad o un componente `..`). Un
juego que hace referencia a la carpeta de otro juego (un framework compartido
junto a él) debe copiar antes esa carpeta dentro. Antes de confirmar la base
de datos se comprueba su `games.dat`: toda entrada necesita un `SCRIPT`, y cada
`SCRIPT`, `VIDEO`, `CABINET`, `MARQUEE` y `ATTRACT` que nombra debe ser un
archivo de la base de datos (o de `Singe/`); una ruta errónea se notifica con el
título de la entrada y el empaquetado se rechaza. `--patch` hace la misma
comprobación sobre el juego parcheado, así que un parche tampoco puede dejar
atrás una ruta colgante.
`--patch` acepta o bien una carpeta organizada como el juego, cuyos archivos
sustituyen a los empaquetados, o bien un archivo `.patch` hecho por `--pack` a
partir de una carpeta así (`Singe --pack fixes MyGame-1.1.patch`; un parche no
necesita `games.dat`). Los archivos que el parche no nombra se dejan como están.
En ambos casos el cambio es una sola transacción: el juego queda intacto si se
interrumpe. Un parche también puede llevar una tabla `removed(path)` que nombra
archivos que borrar. Un `.patch` se puede desempaquetar como un juego, pero no se
puede ejecutar y el menú lo ignora.
Un parche que sustituye un archivo grande por uno pequeño deja dentro de la base
de datos el espacio que usaba el contenido antiguo, donde SQLite lo guarda para lo
siguiente que se escriba en lugar de devolverlo. Eso está bien para un parche que
cambia un script, y es un desperdicio para un desarrollador que vuelve a
empaquetar un vídeo una docena de veces: el `.game` puede acabar ocupando varias
veces el tamaño de su contenido. Así que `--patch` mira el espacio libre cuando la
transacción se ha confirmado y, cuando hay al menos un megabyte, compacta la base
de datos e indica cuánto ha recuperado:
----
>>> Recovered 8007680 bytes of space the old contents were using
----
El resultado tiene, byte a byte, el tamaño de un empaquetado nuevo de los mismos
archivos. Compactar reescribe el archivo entero, así que se omite cuando hay poco
que ganar. Nada más en Singe lo necesita; un juego solo lo escribe el
empaquetador.
La carpeta de datos de un juego empaquetado lleva el nombre de la base de
datos y después el de la carpeta del script dentro de ella: `MyGame/` para un
`MyGame.game` cuyo script está en su raíz, dentro de la carpeta de datos
(`data/MyGame/` por defecto), que es donde la tiene el `MyGame/MyGame.singe`
suelto, así que las partidas guardadas y los ajustes se comparten entre una
instalación suelta y una empaquetada; un script empaquetado en una subcarpeta
(`Game/Main.singe`) recibe `MyGame/Game/`. El índice del vídeo también va ahí.
NOTE: Las compilaciones 3.00 anteriores daban a un juego empaquetado cuyo script
estaba en una subcarpeta la carpeta de datos `Game/`, bajo la base, y esa
carpeta la compartían todos los archivos con esa estructura. Las partidas guardadas de un
juego así pertenecen ahora a `MyGame/Game/`; muévelas a mano. Las compilaciones
3.00 anteriores también conservaban la carpeta propia del juego, aquella bajo la
que su `games.dat` nombra el script, así que un `MyGame.game` empaquetado desde
`MyGame/` escribía en `MyGame/MyGame/`; sube un nivel lo que haya ahí.
Cuando Singe ejecuta un juego desde una base de datos, el menú pasa `CONTAINER` en
la entrada de `games.dat` que lanza; el campo lo pone el menú, nunca se pone a
mano.
Para tus propios datos, `require("sqlite3")` da a los scripts el binding
https://lua.sqlite.org[lsqlite3] para el mismo SQLite que usa el motor. Guarda las
bases de datos que escribe el juego en `singeGetDataPath()`:
[source,lua]
----
local sqlite3 = require("sqlite3")
local db = sqlite3.open(singeGetDataPath() .. "scores.db")
db:exec("CREATE TABLE IF NOT EXISTS scores (name TEXT, score INTEGER)")
for row in db:nrows("SELECT name, score FROM scores ORDER BY score DESC LIMIT 10") do
print(row.name, row.score)
end
db:close()
----
`sqlite3.open` encuentra un nombre igual que `io.open`, así que una base de datos
puede ir dentro del juego -- tablas de niveles, diálogos, listas de objetos -- y
seguir abriéndose cuando el juego está empaquetado. Abierta de solo lectura, una
base de datos empaquetada se lee donde está, sin copiar nada:
[source,lua]
----
local levels = sqlite3.open(DIR .. "levels.db", sqlite3.OPEN_READONLY)
----
Abierta para escritura, que es lo que hace `sqlite3.open` por defecto, una base de
datos empaquetada se copia una vez a la carpeta de datos del juego, dentro de
`files/` como todo lo demás que escribe un juego empaquetado, y se abre la copia.
A partir de entonces el nombre se refiere a la copia, sea de solo lectura o no, y
el original empaquetado nunca cambia. La base de datos de un juego suelto se abre
en su sitio exactamente como antes. `testScripts/packedSql` es la prueba; su
cabecera explica cómo ejecutarla.
El formato de archivo, para quien escriba herramientas: tres tablas.
`meta(key, value)` contiene `version` (1), `gamedir` (el nombre de la carpeta
empaquetada), `chunk` (el tamaño de bloque en bytes, 4194304) y `packer`.
`assets(path, name, size, data)` tiene una fila por archivo, donde `path` es la
clave en minúsculas con barras normales y `name` la grafía del autor; los archivos
no mayores que el tamaño de bloque están en `data`, y los más grandes tienen
`data` a NULL y viven en `chunks(path, chunk, data)` como trozos numerados
consecutivos. Singe abre la base de datos de solo lectura e inmutable, así que
funciona desde soportes de solo lectura y nunca crea un diario junto a ella.
[#_event_driven_or_not]
=== ¿Dirigido por eventos... o no?
Tradicionalmente, Singe usaba un modelo de programación dirigido por
eventos -- Singe se encarga de todo y solo llama a tu código cuando necesita decirte algo o
actualizar la pantalla. Es eficiente y es como funcionan la mayoría de los motores
de juegos basados en Lua, pero puede resultar prolijo. Desde Singe 2.10 hay un
nuevo modelo _con hilo_ («threaded») que te permite escribir código
procedimental.
[#_event_driven]
==== Dirigido por eventos
Con el modelo de programación dirigido por eventos, Singe controla el bucle
principal del programa y decide el orden de ejecución. Singe se encarga
automáticamente de todos los detalles de decodificar y presentar el vídeo y el
audio. Gestiona los mandos, los ratones y la entrada de teclado. Cuando Singe
necesita algo propio del juego, llama a una parte de tu script. El script de Singe
más básico que muestra todos los callbacks existentes tiene este aspecto:
[source,lua]
----
-- Esqueleto de un juego de Singe.
-- Carga el framework de Singe.
dofile("Singe/Framework.singe")
-- Declara aquí las variables globales que necesites.
function onControllerMoved(axis, value, which)
--[[
Informa de qué eje del mando se ha movido y de su valor actual.
(Rango: de -32768 a 32767.) Se usa para dispositivos analógicos. La
entrada digital se gestiona con onInput y onKey.
--]]
end
function onInputPressed(what, device)
--[[
En el modo de teclado MODE_NORMAL, los eventos de entrada se notifican aquí
cuando la tecla o el botón se pulsa por primera vez. Para la lista completa
de teclas, botones y mandos, consulta Singe/Framework.singe. En MODE_FULL,
este evento se llama con el keysym de la tecla pulsada. Cuando el interruptor
viene de un ratón o de una pistola de luz, device es el índice de ese
dispositivo (0 para el primero), como lo pasa Hypseus, para que un juego de
dos pistolas distinga a sus jugadores; para una tecla o un mando es nil.
--]]
end
function onInputReleased(what, device)
--[[
En el modo de teclado MODE_NORMAL, los eventos de entrada se notifican aquí
cuando la tecla o el botón se suelta. Para la lista completa de teclas,
botones y mandos, consulta Singe/Framework.singe. En MODE_FULL, este evento
se llama con el keysym de la tecla soltada. device funciona igual que en
onInputPressed.
--]]
end
function onKeyPressed(key, scancode)
--[[
En el modo de teclado MODE_FULL, los eventos de entrada se notifican aquí
cuando se pulsa la tecla. Se devuelven tanto el keysym como el scancode.
Para la lista de scancodes disponibles, consulta Singe/Framework.singe.
--]]
end
function onKeyReleased(key, scancode)
--[[
En el modo de teclado MODE_FULL, los eventos de entrada se notifican aquí
cuando se suelta la tecla. Se devuelven tanto el keysym como el scancode.
Para la lista de scancodes disponibles, consulta Singe/Framework.singe.
--]]
end
function onMouseMoved(x, y, xr, yr, which)
--[[
Se llama cuando se mueve el ratón. En el modo SINGLE_MOUSE se devuelven
los valores absolutos de X e Y y también el cambio relativo de posición.
En el modo MANY_MOUSE solo está disponible el cambio relativo, además de
qué ratón se ha movido.
--]]
end
function onOverlayUpdate()
--[[
Este es el único sitio donde puedes dibujar con seguridad. Si quieres
mostrar un cursor de puntería, tendrás que guardar la posición del ratón
de onMouseMoved en variables globales y después usarlas aquí para
dibujar el cursor.
--]]
-- Dile a Singe si hemos cambiado la pantalla o no.
return(OVERLAY_UPDATED) -- O OVERLAY_NOT_UPDATED si no se ha dibujado nada.
end
function onShutdown()
-- Se llama cuando el usuario sale de tu juego. Libera aquí los recursos cargados.
end
function onSoundCompleted(channel)
-- El sonido del canal "channel" acaba de terminar de reproducirse.
end
-- Nota: No hay ningún evento "onStartup".
-- Cualquier código de arranque que necesites puede ir aquí.
----
[#threaded]
==== Con hilo
Desde Singe 2.10 puedes usar el clásico modelo de programación procedimental. Para
usar este modelo, declara una función `singeMain()` e incluye el framework de
Singe al *final* de tu programa en lugar de al principio. Lua no es realmente
multihilo, así que este modelo depende de que el desarrollador del juego haga
multitarea cooperativa llamando a `singeYield()` en cualquier sitio donde el
código consuma una cantidad de tiempo considerable. Igual que con el modelo
dirigido por eventos, Singe sigue gestionando toda la entrada, la decodificación
del vídeo y la del audio. Un programa de ejemplo con hilo tiene este aspecto:
[source,lua]
----
function singeMain()
local x = 10
local y = 10
while true do
colorBackground(0, 0, 0, 255)
overlayClear()
colorForeground(255, 255, 255, 255)
overlayPrint(x, y, "+")
if keyboardGetLastUp() == SCANCODE.LEFT.value then x = x - 1 end
if keyboardGetLastUp() == SCANCODE.RIGHT.value then x = x + 1 end
if keyboardGetLastUp() == SCANCODE.UP.value then y = y - 1 end
if keyboardGetLastUp() == SCANCODE.DOWN.value then y = y + 1 end
singeYield()
end
end
dofile("Singe/Framework.singe")
----
[#_hybrid]
==== Híbrido
Los dos modelos se combinan libremente. Cuando existe `singeMain`,
`Framework.singe` instala su propio `onOverlayUpdate`, pero todos los demás
callbacks se siguen disparando: define `onInputPressed`, `onSoundCompleted` u
`onShutdown` junto a `singeMain` y se les llama entre las cesiones de la
corrutina. Un reparto típico mantiene procedimental el flujo del juego mientras
reacciona a los eventos de entrada y de audio:
[source,lua]
----
local fired = false
function onInputPressed(what)
if what == SWITCH_BUTTON1 then
fired = true
end
end
function onSoundCompleted(channel)
if channel == gunshotChannel then
soundPlay(reloadSound)
end
end
function singeMain()
while true do
if fired then
fired = false
gunshotChannel = soundPlay(gunshotSound)
end
singeYield()
end
end
dofile("Singe/Framework.singe")
----
No definas tú mismo `onOverlayUpdate` en este modelo; dibuja desde dentro de
`singeMain`. Un error lanzado dentro de `singeMain` termina el juego con una traza
de la pila, y volver de `singeMain` sale limpiamente.
[#pausing]
=== Pausa
Todo juego tiene gratis una pausa que funciona. Mientras la tecla de pausa está
activada (lo está por defecto), pulsar la tecla asignada a `INPUT_PAUSE` hace que
el motor pause el disco, todos los vídeos cargados y todos los sonidos, dibuje un
indicador PAUSED sobre el último fotograma y deje de ejecutar el script: ni
`onOverlayUpdate`, ni reanudación de `singeMain`, ni callbacks de entrada, ratón,
mando o sonido. Los temporizadores construidos sobre bibliotecas de Lua se
detienen con él porque nada los ejecuta. Pulsar la tecla otra vez reanuda todo lo
que se estaba reproduciendo y el script continúa donde lo dejó. Solo los
interruptores propios del motor (pausa, salir, captura de pantalla, captura del
ratón) siguen funcionando mientras está congelado.
Las entradas se mantienen fieles a la realidad durante la congelación. Todo lo que
el script cree pulsado se suelta con `onInputReleased` (u `onKeyReleased` en
`MODE_FULL`) cuando empieza la pausa, y lo que sigue pulsado físicamente cuando
termina la pausa se vuelve a pulsar. Un joystick mantenido durante una pausa nunca
se queda atascado.
Un juego que quiere su propio comportamiento de pausa (un menú de pausa, una pausa
de la historia que el jugador no puede saltarse) llama a
`singeSetPauseKeyEnabled(false)`. Entonces la tecla se entrega al script como
`SWITCH_PAUSE` y el propio juego maneja `singeSetPauseFlag`, que pausa los medios
sin congelar el script.
En `MODE_FULL` el teclado pertenece por completo al juego, así que las teclas
asignadas a los interruptores del motor no hacen nada ahí (una `p` tecleada al
introducir un récord debe seguir siendo una `p`). Los botones del mando y del
ratón asignados a esos interruptores siguen funcionando en cualquiera de los dos
modos, ya que no se pueden teclear.
[#gamesdat]
=== games.dat
El archivo `games.dat` permite a Singe localizar automáticamente los juegos nuevos
cuando el usuario final los instala. Este archivo es extremadamente importante y
debe incluirse con todos los juegos de Singe. Pon `games.dat` en la carpeta
superior de tu juego. Un ejemplo que contiene varios juegos:
[source,lua]
----
GAMES = {
{
TITLE = "Asteroid Patrol",
SCRIPT = "Collection/AsteroidPatrol.singe",
VIDEO = "Collection/frame_AsteroidPatrol.txt",
STRETCH = false,
NO_MOUSE = false,
RESOLUTION_X = 720,
RESOLUTION_Y = 480,
SINDEN_GUN = "",
GAME_ID = "6f1e7b62-0a4e-4d9c-9b2f-1c7a5e3d8a10",
CABINET = "Collection/cabinet_AsteroidPatrol.png",
MARQUEE = "Collection/marquee_Collection.png",
ATTRACT = "Collection/video_AsteroidPatrol.mkv",
ATTRACT_START = 3000,
ATTRACT_END = 3500,
YEAR = 2026,
PLATFORM = "Laserdisc",
DEVELOPER = "Your Studio",
PUBLISHER = "Your Studio",
GENRE = "Shooter",
DESCRIPTION = "Keep the asteroids away from the station for as long as you can.",
CREATOR = "Scott Duensing",
SOURCE = "http://kangaroopunch.com"
},
-- ... más entradas ...
}
----
Las claves `SCRIPT`, `VIDEO`, `CANVAS_X`, `CANVAS_Y`, `STRETCH`,
`NO_MOUSE`, `RESOLUTION_X`, `RESOLUTION_Y`, `SINDEN_GUN`, `AUDIO_TRACK`,
`AUDIO_DELAY`, `AUDIO_SUFFIX`, `GAME_ID`, `LEGACY_SPRITE_ARGS`, `REQUIRES` y `TIER` las lee el motor cuando el menú (o tu propio
script, mediante `scriptExecute` / `scriptPush`) lanza la entrada; tienen
prioridad sobre la línea de comandos. La línea `VIDEO` es el disco: una entrada
que lo nombra es un juego de laserdisc, y una entrada que lo omite o lo deja en
blanco se ejecuta sin disco y puede fijar `CANVAS_X` / `CANVAS_Y` (por defecto
720x480); véase <<withoutadisc,Juegos sin disco>>. `AUDIO_SUFFIX = "-es"` reproduce
`<base>-es.ogg`, junto al vídeo del disco, en lugar del audio que lleva dentro,
para una edición cuyos otros idiomas se distribuyen como archivos aparte; es lo
mismo que hace `--altaudio`. `LEGACY_SPRITE_ARGS = true` ejecuta un juego escrito
para Singe 2.10 con el antiguo orden de argumentos de los sprites (véase
<<migrating,Migración desde Singe 2.10>>). El menú lee las claves restantes para
mostrarlas.
`REQUIRES` dice lo que el juego necesita de una máquina, como una lista de nombres:
`"SCENE"` (la escena 3D), `"GUI"` (documentos de GUI), `"SHADOWS"`, `"POST"`
(el bloom y los demás efectos que quieren destinos de renderizado en coma
flotante), `"SKIN"` (modelos con skinning) y `"MORPH"` (morph targets). Sin
`REQUIRES` se entiende 2D. El menú marca un juego que esta máquina no puede
ejecutar y dice por qué en lugar de iniciarlo, y rara vez escribes la lista tú
mismo: el motor se da cuenta de lo que usa un juego mientras se ejecuta, avisa en
la traza cuando el juego usa algo que `REQUIRES` no incluye, y `--pack` escribe la
lista en una entrada que no tiene ninguna. `TIER` (de 0 a
3) es cuánta máquina pide el juego; el menú mide el nivel de esta máquina con su
propio fondo y marca un juego que pide más como uno que puede ir lento. Véase
<<singecapabilities,singeCapabilities>>.
[source,lua]
----
REQUIRES = { "SCENE", "SHADOWS" },
TIER = 1,
----
Un script lanzado directamente, por su nombre o por su carpeta, recibe el mismo
trato: Singe busca un `games.dat` en la carpeta del script y hasta tres
carpetas por encima, y si el `SCRIPT` de una entrada nombra el script lanzado
(las rutas de las entradas cuentan desde la carpeta que está por encima del
`games.dat`, así que `Collection/games.dat` nombra
`Collection/AsteroidPatrol.singe`), se aplican las claves de esa entrada. La única
diferencia con un lanzamiento desde el menú es la prioridad: una opción escrita en
la línea de comandos (`--framefile`, `--disc`, `--stretch`, `--nomouse`,
`--xresolution`, `--yresolution`, `--sindengun`, `--audio`, `--audiodelay`,
`--altaudio`, `--canvas`) conserva su valor, ya que quien la escribió lo hizo a
propósito. Un valor de un archivo de ajustes no: es un valor por defecto, y la
entrada describe mejor el juego (véase xref:Manual.adoc#settings[El archivo de ajustes]).
Un `games.dat` que no se puede ejecutar se notifica y se ignora en un lanzamiento
directo.
[#withoutadisc]
=== Juegos sin disco
Singe ya no da por hecho un laserdisc. Un juego es un juego de laserdisc solo
cuando nombra uno: un `VIDEO` en su entrada de `games.dat`, o `--disc` (o un
`--framefile`) en la línea de comandos. Todo lo demás se ejecuta sin disco, y el
mundo es un *lienzo* en lugar de un fotograma de vídeo: `CANVAS_X` / `CANVAS_Y` en
`games.dat` o `--canvas=WxH` en la línea de comandos, 720x480 por defecto, para que
un juego sin disco parezca un juego de laserdisc de definición estándar salvo que
pida otra cosa.
Sin disco, el motor dibuja sobre negro, el overlay ocupa por defecto la mitad del
lienzo como siempre, y las funciones `disc*` y `vldp*` se vuelven inofensivas:
`discGetState` informa de `DISC_EJECTED`, `discGetFrame` devuelve `0`,
`discGetWidth` / `discGetHeight` devuelven el lienzo, `vldpGetPixel` devuelve
negro, y las llamadas de transporte escriben en la traza y no hacen nada. Todo lo
demás -- el overlay, los sprites, las fuentes, los sonidos, los vídeos adicionales
mediante `videoLoad`, la entrada y los dos modelos de programación -- es idéntico.
La variable global `SINGE_DISC` le dice a un script como qué tipo de juego se está
ejecutando.
[#gui]
=== GUI
Los menús, las páginas de opciones, los HUD y la introducción de récords se
construían antes con `fontPrint`, `overlayBox` y un cursor hecho a mano. Singe 3
añade una GUI: un documento escrito en RML (un lenguaje de marcado parecido a
HTML) y con el estilo de RCSS (una hoja de estilo parecida a CSS), maquetado y
dibujado por RmlUi, con ajuste de línea del texto, flexbox, paneles nine-slice
hechos con tus propios gráficos, transiciones, animaciones, controles de
formulario y un modelo de foco que un mando puede manejar. Una *GUI* es un
contexto de RmlUi de tamaño fijo renderizado en su propia textura, que se vuelve a
dibujar cada vez que cambia su imagen (véase <<guirefresh,guiRefresh>>), y esa
textura va a cualquier sitio al que pueda ir una textura: compuesta sobre el
overlay con
`guiDraw`, o mostrada en un material de la escena 3D con `materialSetGui`, de modo
que la misma página de opciones funciona plana en la pantalla y en el monitor de
una máquina recreativa modelada. Una GUI necesita el dispositivo GPU que usa la
escena 3D, así que no está disponible en una máquina que no lo tenga (véase
<<sceneperformance,Rendimiento y requisitos>>); `guiNew` termina el script en
lugar de responder false, así que pregunta antes a <<singehasgpu,singeHasGpu>> si
tu juego puede pasar sin ella. El menú incluido hace exactamente eso, y se dibuja
en el overlay cuando la respuesta es no (véase
xref:Manual.adoc#menurenderers[Los dos renderizadores]). Todas las llamadas que
se nombran aquí tienen una entrada en la sección <<guiref,Gui>> de la referencia.
La referencia del lenguaje para RML y RCSS es la propia documentación de RmlUi en
https://mikke89.github.io/RmlUiDoc/, y vale la pena leerla antes del primer
documento; este capítulo cubre lo que Singe añade encima.
[#_documents_and_style_sheets]
==== Documentos y hojas de estilo
`guiNew(width, height)` crea una GUI y devuelve un handle entero; puede haber hasta
dieciséis a la vez. `guiLoad(gui, name)` carga en ella un documento RML, lo
muestra y devuelve un handle de documento (hasta treinta y dos por GUI);
`guiHide`, `guiShow` y `guiClose` hacen lo que dicen sus nombres, y
`guiDelete` elimina la GUI, sus documentos y su textura. Un documento suele tener
el tamaño de su GUI, y una GUI suele tener el tamaño del overlay para una página a
pantalla completa o el del panel para un elemento del HUD:
[source,lua]
----
local gui = guiNew(overlayGetWidth(), overlayGetHeight())
local doc = guiLoad(gui, DIR .. "options.rml")
----
El nombre del documento pasa por el sistema de archivos virtual como el de un
sprite, así que puede estar suelto en disco o dentro de un juego empaquetado, y se
resuelve de forma relativa a la carpeta en la que se inició Singe, así que
antepón `DIR` a los archivos que se distribuyen con tu juego, exactamente como
con `spriteLoad`. Los nombres *dentro* de un documento (el `href` de una hoja de
estilo, una imagen en un decorador, el `src` de una hoja de sprites) se resuelven
de forma relativa al archivo que los nombra, como hace RmlUi, y después mediante
la misma búsqueda, así que un documento, su hoja de estilo y sus imágenes están
uno junto a otro en la carpeta del juego y un juego empaquetado los encuentra
todos. Dos tipos de nombre se toman tal cual: los archivos propios del motor bajo
`Singe/` y las rutas absolutas.
La misma regla, leída al revés, permite que un documento que está bajo `Singe/`
llegue a los archivos de un juego: un nombre relativo se resuelve desde la propia
carpeta del documento, así que desde `Singe/Menu.rml` el nombre
`../Collection/cabinet.png` es `Collection/cabinet.png` en la carpeta del
juego, y un nombre bajo `../MyGame.game/` es un archivo dentro de ese juego
empaquetado. El menú incluido hace exactamente esto con las imágenes de
recreativa y de marquesina del juego seleccionado, poniendo desde Lua un
decorador de imagen en el panel de imagen para que la imagen quepa dentro sea cual
sea su tamaño:
[source,lua]
----
-- Menu.singe: encaja la imagen de un juego en uno de los paneles de imagen.
function menuArt(id, file)
local path = tostring(file or "")
if path:sub(1, 6):lower() ~= "singe/" then
path = "../" .. path
end
menuElement(id).style.decorator = 'image("' .. path .. '" contain)'
end
----
Un documento RML es un `<head>` con un título, enlaces a hojas de estilo o un
`<style>` en línea, y un `<body>` con elementos. Uno mínimo, con el tema
incluido:
[source,html]
----
<rml>
<head>
<title>Options</title>
<link type="text/rcss" href="Singe/gui.rcss"/>
<style>
body { width: 100%; height: 100%; }
.panel { margin: 30dp; }
</style>
</head>
<body id="options">
<div class="panel">
<h1>Options</h1>
<p class="muted" id="status">Choose a difficulty and press Start.</p>
<button id="go">Start</button>
</div>
</body>
</rml>
----
RmlUi no dibuja nada cuyo tamaño no se le haya dicho, y todo elemento es
`display: inline` hasta que un estilo diga otra cosa, así que una hoja de estilo
no es opcional. `Singe/gui.rcss` es el tema que incluye el motor: da al cuerpo, a
los encabezados y a los párrafos un tamaño y un color, hace de `div` un bloque y
da estilo a todos los controles de formulario. Enlázalo primero y sobrescribe lo
que quieras en una hoja tuya o en un `<style>` en línea; la regla posterior gana,
como en CSS. Las longitudes del tema están en `dp`, el píxel independiente de la
densidad de RmlUi, que es un píxel al tamaño propio de la GUI.
El texto necesita una fuente. El motor carga `Singe/FreeSansBold.ttf` para cada
GUI como fuente de *reserva* (la que sirve cualquier glifo que no tenga ninguna
otra fuente); su nombre de familia es `FreeSans` y su peso es negrita, que es lo
que pide la regla `body` del tema. `guiLoadFont(name)` añade una de las fuentes
propias del juego para todas las GUI, y `guiLoadFont(name, true)` la convierte en
la de reserva. Un documento nombra una fuente por el nombre de familia que hay
dentro del archivo, no por el nombre del archivo, así que carga las fuentes antes
que los documentos que las usan:
[source,lua]
----
guiLoadFont(DIR .. "fonts/Orbitron-Bold.ttf")
----
[source,css]
----
h1 { font-family: Orbitron; font-weight: bold; }
----
Los efectos RCSS de RmlUi se dibujan completos. Los hijos de un elemento
redondeado o transformado se recortan a su forma (una lista con desplazamiento y
`border-radius` se une a su marco en la curva), `filter: opacity()` atenúa un
grupo como un todo (la propiedad `opacity` atenúa cada pieza por separado, tal
como la define RmlUi), `mask-image` recorta un elemento según el alfa de una
imagen, `box-shadow` proyecta sombras duras o suaves, y `filter` y
`backdrop-filter` aceptan `blur`, `drop-shadow`, `brightness`, `contrast`,
`invert`, `grayscale`, `sepia`, `hue-rotate` y `saturate`. Los decoradores
`linear-gradient`, `radial-gradient` y `conic-gradient` (repetidos o no) se
dibujan como tales; los paneles, los botones y las barras de progreso del tema
incluido los usan. Tres límites: el decorador personalizado `shader` no está
soportado (se rechaza con un aviso), un degradado lleva como mucho dieciséis
paradas de color (las demás se descartan), y los efectos se anidan como mucho en
ocho capas de profundidad (un elemento más profundo se dibuja sin su propia capa).
Cada efecto se renderiza a través de una capa del tamaño de la GUI, y un
desenfoque a través de varias pasadas, así que en una Pi limítalos a unos pocos
elementos y vigila <<guigetstats,guiGetStats>>, que cuenta las capas, las pasadas
y los bytes que ha costado un fotograma.
[#_theming]
==== Temas
Un tema es una carpeta de RCSS e imágenes, y un juego distribuye su propio
aspecto igual que distribuye sprites. La forma habitual es una hoja enlazada
después de `Singe/gui.rcss` que vuelve a dar estilo a las clases del tema y añade
las suyas, con los gráficos al lado. El tema incluido define:
* `.panel` -- una caja con borde, redondeada y translúcida, con relleno: el marco
de una página o de un diálogo.
* `.list` -- una caja con desplazamiento cuyos elementos `div` hijos son filas:
toman el foco, se resaltan al pasar el ratón por encima y al tener el foco, y
una fila con la clase `selected` permanece resaltada. Aparece una barra de
desplazamiento vertical cuando las filas no caben.
* `.muted` -- texto más tenue, para pistas y líneas de estado.
* Los controles de formulario: `button` e `input.submit`, `input.text`,
`input.password` y `textarea`, `input.checkbox` e `input.radio`,
`input.range` con su pista y su deslizador, `select` con su flecha y su cuadro
desplegable, y `progress` con su parte llena; cada uno con estados hover, focus
y active, y cada uno capaz de recibir el foco con `tab-index: auto` para que un
mando pueda llegar a él.
Un panel hecho con tus propios gráficos es un decorador nine-slice sobre una hoja
de sprites, y las animaciones son keyframes de RCSS, así que una barra de carga
que se llena sola no necesita ningún script:
[source,css]
----
@spritesheet crate {
src: crate.png;
crate-outer: 0px 0px 40px 40px;
crate-inner: 10px 10px 20px 20px;
}
.crate {
display: block;
decorator: ninepatch(crate-outer, crate-inner, 1.0);
padding: 14dp;
}
@keyframes fill {
from { width: 0%; }
to { width: 100%; }
}
.bar fill {
animation: 3s cubic-in-out infinite alternate fill;
}
----
[#_scripting_a_document]
==== Programar un documento
Hay dos API, y conviene tener claro cuál es cuál. Las llamadas planas
`gui*`, al estilo propio de Singe, crean una GUI, cargan y dibujan documentos,
encaminan la entrada y llegan a los elementos *por id*: `guiGetValue(gui, doc, id)`
lee el valor de un control de formulario, o el RML interior de cualquier otro
elemento, como cadena; `guiSetValue(gui, doc, id, value)` escribe uno; y `guiSetHandler(gui, doc,
id, event, function)` llama a una función de Lua cuando el elemento dispara un evento
(`"click"`, `"change"`, `"submit"` o cualquier otro nombre de evento de RmlUi). La
función recibe `(gui, document, id, event, value)`, donde `value` es el valor del
elemento como cadena en el momento del evento: el texto de un campo de texto, el
valor de una opción de `select`, un rango con el formato propio de RmlUi
(`"48.000000"`), el RML interior de un botón. Todo lo que una página necesita en
el día a día lo cubren esas tres llamadas:
[source,lua]
----
guiSetHandler(gui, doc, "go", "click", function(g, d, id, event, value)
local name = guiGetValue(g, d, "name")
local hard = guiGetValue(g, d, "difficulty") == "hard"
startGame(name, hard)
end)
guiSetHandler(gui, doc, "volume", "change", function(g, d, id, event, value)
soundSetVolume(math.floor(tonumber(value)))
guiSetValue(g, d, "status", "Volume " .. math.floor(tonumber(value)))
end)
----
Todo lo demás dentro de un documento es la API de Lua de RmlUi, que se ejecuta en
el propio estado de Lua del juego: un bloque `<script>` en un documento, y
`onclick` y los demás atributos `on*`, se ejecutan con las variables globales del
juego, así que pueden llamar a tus funciones y a las llamadas `gui*` por igual.
Desde el lado del script se llega a los mismos objetos mediante la variable
global `rmlui`: los contextos se llaman `gui0`, `gui1`, ... según sus handles, y
los documentos se indexan por el `id` de su `<body>`, así que el botón de arriba
es `rmlui.contexts["gui0"].documents["options"]:GetElementById("go")`, y
`:DispatchEvent("click", {})` sobre él lo pulsa desde el script (útil para
pruebas que se ejecutan sin ratón). Los elementos, sus atributos, clases, estilos
e hijos, los enlaces `data-model` que reflejan una tabla de Lua en un documento, y
el resto del modelo de objetos están documentados con el plugin de Lua de RmlUi en
https://mikke89.github.io/RmlUiDoc/; `guiSetHandler` es el atajo por id, no un
sustituto. Un documento cerrado mediante esa API (`document:Close()`) lo olvida el
motor, y a partir de entonces su handle lanza un error.
[#_input]
==== Entrada
Una GUI nueva solo muestra, que es lo adecuado para un HUD o un cartel;
`guiSetInput(gui, true)` hace que acepte entrada, y `guiSetInput(gui, false)`
lo vuelve a desactivar. Para una GUI que acepta entrada:
* *El ratón y una pistola de luz* (que es un ratón) llegan en coordenadas del
overlay, convertidas a través del borde Sinden y la escala de overscan
exactamente igual que para `onMouseMoved`, en el modo `MOUSE_SINGLE`. El motor
encuentra la GUI que está bajo el puntero -- la última dibujada plana ahí en
este fotograma queda encima y, en su defecto, una superficie de la escena cuyo
material muestra una (véase más abajo) -- y escala la posición a los píxeles
propios de esa GUI. Una pulsación sobre un elemento es de la GUI; los botones
izquierdo, derecho y central hacen clic, y la rueda desplaza.
* *Las teclas* van a todas las GUI que aceptan entrada, y una tecla que ha usado
un elemento con el foco (un carácter en un campo de texto, una flecha que ha
movido el foco, Intro sobre un botón) se consume. Mientras un campo de texto o
de contraseña o un `textarea` tiene el foco, el motor activa la entrada de texto
de SDL, así que el texto escrito llega como texto, respetando la distribución
del teclado y cualquier teclado en pantalla o IME.
* *Los interruptores del mando y del ratón* (las asignaciones `SWITCH_*` del
framework desde un botón del mando o del ratón) se traducen a las teclas que
entiende un documento: las cuatro direcciones se convierten en las teclas de
flecha, `INPUT_ACTION_1` es Intro y `INPUT_ACTION_2` es Esc. Un interruptor asignado a una
tecla del teclado ya ha llegado a la GUI como esa tecla.
La regla que hace seguro un HUD es que *un evento que ha usado un elemento nunca
llega al juego*: una tecla consumida no llega como `onInputPressed` ni como
`onKeyPressed`, un clic en un botón no llega como interruptor del ratón, una
dirección traducida que ha movido el foco no llega como `SWITCH_UP`. Todo lo que
un documento no ha usado pasa a los callbacks de siempre, así que un HUD que
acepta entrada sigue dejando pasar el botón de disparo, y una página sin nada con
el foco deja pasar todas las teclas.
El foco y la navegación son asunto del propio documento. El tema incluido da
`tab-index: auto` a todos los controles y a todas las filas de `.list`, así que
Tab se mueve entre ellos, Intro activa el control con el foco, y las teclas de
flecha se mueven entre controles con las propiedades `nav` de RmlUi (`nav-up`,
`nav-down`, `nav-left`, `nav-right`, o `nav: auto` para las cuatro). Una página
para una recreativa con un stick y dos botones quiere esas propiedades en todo lo
que espera que alcance el jugador:
[source,css]
----
button, .list div { nav: auto; }
----
[#guiscene]
==== Una GUI en la escena
`materialSetGui(material, gui)` muestra la textura de una GUI como textura de
color base de un material, actualizada en cada fotograma, junto a
`materialSetVideo` y `materialSetView`: un monitor, un panel de cabina, un cartel.
Combínala con `materialSetUnlit` cuando la imagen no deba sombrearse, y déjala
iluminada cuando sí. Nada más cambia; el material se ilumina, se mezcla y se
refleja como cualquier otro, y la misma GUI puede dibujarse plana sobre el overlay
en el mismo fotograma.
`sceneProbeGui(x, y)` responde qué GUI está en la superficie bajo un punto del
overlay y en qué lugar de ella: un rayo desde la cámara a través del punto contra
todas las mallas visibles cuyo material muestra una GUI, que devuelve el handle y
el píxel en las coordenadas propias de esa GUI, o `nil` cuando el punto no está
sobre ninguna de esas superficies. El motor se hace la misma pregunta cuando el
ratón o una pistola de luz apuntan a la escena y no hay nada dibujado plano bajo
el puntero, así que un clic en el monitor de una recreativa 3D pulsa el botón que
hay debajo sin que intervenga ningún script; la llamada está ahí para un juego que
quiera saberlo por sí mismo, para una mira láser, un efecto al pasar por encima o
una prueba.
[source,lua]
----
local screen = materialNew()
materialSetGui(screen, gui)
materialSetUnlit(screen, true)
nodeSetMesh(monitor, meshPlane(3.0, 2.0), screen)
nodeSetRotation(monitor, 90, 0, 0)
----
[#_reloading_performance_and_limits]
==== Recarga, rendimiento y límites
Una GUI pertenece al script que la creó. Salir, `scriptPush`, `scriptExecute` y
una recarga (véase <<reloading,Recargar mientras trabajas>>) desmontan todas las
GUI y documentos junto con el resto del estado del script, y el script los vuelve
a crear cuando se ejecuta; nada del modelo de objetos de `rmlui` sobrevive, así
que no guardes esos objetos en variables globales que una recarga reutilizaría.
Toda GUI se actualiza en cada fotograma tanto si se dibuja en algún sitio como si
no, así que un documento sigue ejecutando sus animaciones y scripts fuera de la
pantalla, y una GUI que ya no se necesita debe borrarse en lugar de solo
ocultarse. Solo se vuelve a dibujar en su textura cuando cambia su imagen, así que
una página o un HUD que no se mueve no le cuesta casi nada a la GPU (véase
<<guirefresh,guiRefresh>>). El coste de un fotograma que sí cambia es el tamaño de
la textura y la complejidad del documento: en un ordenador de sobremesa una página
a pantalla completa es barata; en una Raspberry Pi, limita una GUI al tamaño del
overlay o menos y prefiere una GUI con varios documentos a varias GUI. Cada fuente
cuesta un atlas por cada tamaño en que se usa, así que una página que usa una
fuente en tres tamaños paga tres atlas; una docena de tamaños son una docena.
Puede haber dieciséis GUI a la vez, cada una con treinta y dos documentos, y se
atienden dieciséis llamadas a `guiDraw` por fotograma.
Lo que un documento puede mostrar, y lo que no:
* *Las imágenes vienen de archivos.* Las imágenes, las hojas de sprites y los
decoradores de imagen cargan PNG y los demás formatos que lee Singe, desde disco
o desde un juego empaquetado. Un documento no puede mostrar un sprite de Singe,
un vídeo, la escena 3D ni nada que se dibuje mientras el juego se ejecuta, y un
script no puede pasarle píxeles. Al revés sí funciona: la textura de una GUI va
a un material 3D con `materialSetGui`.
* *Decoradores:* `image`, `text`, `tiled-horizontal`, `tiled-vertical`,
`tiled-box`, `ninepatch`, el `gradient` de dos colores (y sus nombres
`horizontal-gradient` y `vertical-gradient`), y `linear-gradient`,
`radial-gradient` y `conic-gradient` con sus formas `repeating-`, con un máximo
de dieciséis paradas de color. El decorador personalizado `shader` se rechaza.
* *Filtros:* `opacity`, `blur`, `drop-shadow`, `brightness`, `contrast`,
`invert`, `grayscale`, `sepia`, `hue-rotate` y `saturate`. Cualquier otro
nombre se ignora con un aviso en la traza.
* *Capas:* los filtros, una `opacity` inferior a 1, las imágenes de máscara y las
sombras de caja se dibujan cada uno a través de una capa del tamaño de la GUI, y
como mucho se apilan ocho a la vez; una más se dibuja sin su capa, y por tanto
sin su efecto.
* *Las imágenes* de más de 64 MB se rechazan.
* *RML y RCSS* son los de RmlUi, parecidos a HTML y CSS pero no iguales: hay
flexbox y no hay CSS grid, y un documento se programa en Lua, mediante
`guiSetHandler` y el objeto `rmlui`, en lugar de en JavaScript. La propia
documentación de RmlUi es la autoridad sobre lo que contienen los lenguajes.
[#_a_worked_example]
==== Un ejemplo completo
Una página de opciones sobre el disco: un panel del tema incluido con un campo de
nombre, un deslizador de volumen, una lista de dificultad y dos botones, dibujada
sobre el overlay y manejada con el ratón o el stick. El documento,
`options.rml`, junto al script:
[source,html]
----
<rml>
<head>
<title>Options</title>
<link type="text/rcss" href="Singe/gui.rcss"/>
<style>
body { width: 100%; height: 100%; }
.panel { margin: 30dp; }
.field { display: block; margin: 6dp 0; }
label { display: inline-block; width: 140dp; }
input.text { width: 300dp; }
button, input { nav: auto; }
#status { min-height: 30dp; }
</style>
</head>
<body id="options">
<div class="panel">
<h1>Options</h1>
<div class="field"><label>Name</label><input type="text" id="name" value="Dirk"/></div>
<div class="field"><label>Volume</label><input type="range" id="volume" min="0" max="63" step="1" value="48"/></div>
<div class="field"><label>Difficulty</label>
<select id="difficulty">
<option value="easy">Easy</option>
<option value="normal" selected>Normal</option>
<option value="hard">Hard</option>
</select>
</div>
<div class="field"><label>Attract</label><input type="checkbox" id="attract" checked/> plays the intro</div>
<div class="field"><button id="go">Start</button><button id="back">Back</button></div>
<p id="status" class="muted">Press Start when ready.</p>
</div>
</body>
</rml>
----
El script la carga en una GUI del tamaño del overlay, conecta los tres elementos
que le interesan y la dibuja en cada fotograma hasta que el jugador pulsa Start;
el disco se reproduce debajo todo el tiempo:
[source,lua]
----
dofile("Singe/Framework.singe")
local gui = guiNew(overlayGetWidth(), overlayGetHeight())
local doc = guiLoad(gui, DIR .. "options.rml")
local settings = { name = "Dirk", volume = 48, hard = false }
local inMenu = true
local function report(text)
guiSetValue(gui, doc, "status", text)
end
guiSetHandler(gui, doc, "volume", "change", function(g, d, id, event, value)
settings.volume = math.floor(tonumber(value))
soundSetVolume(settings.volume)
report("Volume " .. settings.volume)
end)
guiSetHandler(gui, doc, "difficulty", "change", function(g, d, id, event, value)
settings.hard = (value == "hard")
report("Difficulty " .. value)
end)
guiSetHandler(gui, doc, "go", "click", function(g, d)
settings.name = guiGetValue(g, d, "name")
guiHide(g, d)
guiSetInput(g, false)
inMenu = false
discPlay()
end)
guiSetHandler(gui, doc, "back", "click", function()
singeQuit()
end)
function onInputPressed(what)
-- Aquí solo llega lo que la página no ha usado: nada mientras tiene
-- el foco, todo en cuanto se oculta.
if not inMenu and what == SWITCH_BUTTON1 then
fire()
end
end
function onOverlayUpdate()
overlayClear()
if inMenu then
guiDraw(gui)
else
fontPrint(10, 10, settings.name .. " " .. (settings.hard and "HARD" or "NORMAL"))
end
return OVERLAY_UPDATED
end
----
La misma página en la pantalla de una recreativa modelada son las cinco líneas de
<<guiscene,Una GUI en la escena>> en lugar de `guiDraw`, y un clic en el monitor
pulsa el botón que hay debajo.
[#subtitles]
=== Subtítulos
Un juego puede incluir un archivo de subtítulos SubRip y hacer que Singe lo muestre sobre el disco mientras se reproduce. Tres llamadas hacen todo el trabajo: <<srtload,`srtLoad`>> lee el archivo, <<srtenable,`srtEnable`>> lo activa y <<srtposition,`srtPosition`>> dice dónde se colocan las líneas. <<srtclear,`srtClear`>> vuelve a quitar lo que esté en pantalla. Esta característica viene de Hypseus Singe y las cuatro llamadas son las suyas.
[source,lua]
----
if srtLoad("subs/english.srt") then
srtPosition(80)
srtEnable(true)
end
----
[#_the_file_format]
==== El formato del archivo
SubRip (`.srt`) es un archivo de texto plano con entradas numeradas:
----
1
00:00:04,120 --> 00:00:06,880
Stay away from the caves.
2
00:00:07,000 --> 00:00:09,500
I mean it -- the caves
are where they nest.
----
Cada entrada es un número en una línea propia, después una línea con dos marcas de tiempo separadas por `-->`, después una o más líneas de texto y después una línea en blanco. Una marca de tiempo es `HH:MM:SS,mmm`: horas, minutos y segundos separados por dos puntos, y milisegundos tras una coma. El número de la entrada se lee y se descarta, así que los números no tienen por qué estar en orden, ni siquiera ser correctos.
El analizador es tolerante a propósito en los sitios donde falla un archivo editado a mano. Los retornos de carro y los espacios iniciales y finales se eliminan de cada línea, así que un archivo escrito en Windows se carga sin cambios. Una entrada cuya línea de tiempo no tiene `-->`, o cuyas marcas de tiempo no se pueden analizar, se omite y el archivo sigue cargándose. Una entrada sin texto se omite. Las líneas de una entrada se unen con saltos de línea y se muestran tal como están escritas, y `<`, `>` y `&` llegan a la pantalla como tales en lugar de tomarse por marcado.
Lo que *no* está soportado: las coordenadas de posición que algunas herramientas añaden a la línea de tiempo se ignoran (la altura la decide `srtPosition`), y las etiquetas `<i>` y `<b>` que SubRip lleva de vez en cuando se muestran como texto en lugar de obedecerse. Las entradas pueden solaparse en el tiempo; gana la última que ha empezado.
`srtLoad` responde `false` y no carga nada cuando el archivo no se puede leer, cuando no se puede analizar nada de él o cuando no hay disco -- así que un archivo roto es un `false` con el que un juego puede actuar y no un cierre inesperado. En cada caso imprime un aviso que nombra el archivo.
[#_the_timing]
==== La sincronización
Las marcas de tiempo se convierten en *números de fotograma del disco* al cargar el archivo, usando la tasa de fotogramas del disco que esté cargado en ese momento, y el motor compara cada fotograma dibujado con esos números de fotograma. De eso se siguen dos cosas.
Primero, **un subtítulo sobrevive a un salto.** Mandar el disco a cualquier sitio -- `discSearch`, `discSkipForward`, un paso, un framefile que pasa a otro segmento -- pone en pantalla la entrada que cubre el sitio donde ha caído, en el siguiente fotograma dibujado, hacia delante o hacia atrás. No hay que recargar nada ni poner a cero ningún reloj.
Segundo, **el archivo debe estar sincronizado con el disco, no con el reloj.** Un `.srt` preparado para un vídeo con otra tasa de fotogramas cae en el sitio equivocado, y uno que se carga antes de abrir el disco no se carga en absoluto. Carga los subtítulos después del disco, lo que para un juego normal significa desde el cuerpo del script.
Una entrada se muestra mientras el fotograma del disco está entre sus dos fotogramas, ambos incluidos. El subtítulo solo se vuelve a dibujar cuando cambia la entrada, así que los subtítulos no cuestan nada mientras una línea está en pantalla.
[#_styling]
==== Estilo
Las líneas son un documento de RmlUi, no una línea de texto copiada con un blit, así que se les da estilo igual que a una GUI. En la carpeta `Singe` se incluyen dos archivos, que se extraen junto a los demás en el primer arranque:
* `Singe/subtitle.rml` -- el documento. Tiene un único elemento, `#slot`, en el que el motor escribe la entrada o el rótulo actual.
* `Singe/subtitle.rcss` -- el aspecto. `.cue` es el bloque que lleva la altura que pidió `srtPosition`, como un `top` en línea; `.cue .text` es la barra de texto que hay dentro, que por defecto es blanca sobre un panel oscuro redondeado que se ciñe al texto en lugar de ocupar todo el ancho de la imagen.
Los dos se escriben de nuevo en cada arranque, como todo lo que hay en la carpeta `Singe`, así que editarlos no cambia nada por mucho tiempo. Para cambiar el estilo, un juego trae sus propias copias: `subtitle.rml` junto a su script, con su enlace apuntando a una hoja propia (`subtitle.rcss` junto a él, por ejemplo), y Singe dibuja ese documento en lugar del suyo; la traza del programa lo indica. El motor solo fija el contenido de `#slot` y el `top` de `.cue`, así que todo lo demás -- la fuente, el tamaño, los colores, el panel, un borde, una sombra -- pertenece a la hoja del juego y sobrevive a una actualización del motor. El documento se dibuja sobre la imagen, por encima del overlay y por debajo de las GUI en espacio de pantalla, sigue al rectángulo del vídeo allí donde lo pongan `--scalefactor`, `--shiftx`, un recorte del bezel o `--rotate`, y no acepta entrada del ratón, así que un clic lo atraviesa sin más.
<<overlaybanner,`overlayBanner`>> usa el mismo documento, así que un rótulo y un subtítulo tienen el mismo aspecto, y un juego que cambia el estilo de uno cambia el de los dos.
En una máquina sin GPU no hay GUI en absoluto; `srtLoad` sigue analizando el archivo y respondiendo con la verdad, y no se dibuja nada. Un aviso lo dice una vez.
[#scenes3d]
=== Escenas 3D
Singe 3 dibuja una escena 3D entre el vídeo del laserdisc y el overlay 2D. Todo lo
que ya sabes sigue valiendo: los sprites, el texto y el overlay quedan encima de la
escena, y el vídeo se ve allí donde el fondo de la escena es transparente. Un
juego que nunca llama a una función 3D no se ve afectado, y un juego sin disco
(véase <<withoutadisc,Juegos sin disco>>) puede ser directamente un juego 3D.
Las secciones siguientes construyen una escena en el orden en que la escribirás:
los nodos, después las mallas y los materiales, las luces, la cámara, el aspecto
del fotograma en su conjunto, los modelos y, por último, lo que va encima de todo
eso. Todas las llamadas que se nombran aquí tienen una entrada en la
<<reference,Referencia de la API de Lua>>.
[#_what_a_scene_is_made_of]
==== De qué está hecha una escena
Todo en una escena es un *nodo* de un único árbol, y el nodo `0` es la raíz. Un
nodo tiene una posición, una rotación y una escala relativas a su padre, y puede
llevar una cosa: una malla con un material, una luz, un sprite o una línea de
texto. Un modelo cargado se convierte en un subárbol de nodos bajo una raíz, así
que las mismas llamadas mueven por igual un personaje modelado y un cubo creado
por script.
La escena es de mano derecha, con +Y hacia arriba y -Z hacia delante, como glTF y
la exportación de Blender. Las unidades son las que usen tus modelos, y las
primitivas usan las mismas unidades; la física (véase <<physics,Física>>) supone
metros. Las rotaciones son en grados, dadas como tres ángulos de Euler aplicados
como guiñada, cabeceo y alabeo, o como un cuaternión cuando el valor viene de un
modelo. La dirección de avance propia de un nodo es su -Z: la cámara mira a lo
largo de ella, las luces direccionales y los focos alumbran a lo largo de ella, y
`nodeLookAt` la gira hacia un punto del mundo.
Hay dos formas de llenar una escena, que se pueden mezclar libremente:
* *Construirla desde el script* con las mallas primitivas (`meshBox`, `meshSphere`,
`meshPlane`, `meshCylinder`, `meshCone`, `meshTorus`, `meshHeightmap`) o con
tus propios datos de vértices (`meshNew`), además de materiales, luces y una
cámara.
* *Cargar modelos* en formato binario glTF 2.0 (`.glb`) con `modelLoad` y después
colocarlos con `modelInstance` tantas veces como quieras. Blender, Maya, 3ds
Max y todos los motores importantes exportan `.glb`.
Una escena mínima es una malla, una luz y una cámara:
[source,lua]
----
sceneEnable(true)
sceneSetBackground(0, 0, 0, 0) -- Transparente: el disco se ve detrás de la escena.
local red = materialNew()
materialSetColor(red, 220, 40, 40)
local cube = nodeNew()
nodeSetMesh(cube, meshBox(1, 1, 1), red)
local sun = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(sun, 2, 4, 3)
nodeLookAt(sun, 0, 0, 0)
local camera = nodeNew()
nodeSetPosition(camera, 0, 1.5, 5)
nodeLookAt(camera, 0, 0, 0)
cameraSet(camera)
function onOverlayUpdate()
nodeRotate(cube, 0, 1, 0) -- Un grado por fotograma alrededor de su propio eje Y.
return OVERLAY_UPDATED
end
----
`sceneEnable(true)` es la única llamada que hacen todos los juegos 3D, y en una
máquina que no puede dibujar 3D es la llamada que falla (véase
<<sceneperformance,Rendimiento y requisitos>>). Sin cámara, la escena se ve desde
`(0, 0, 5)` mirando al origen, y sin una llamada que fije el fondo, el fondo de la
escena es negro opaco.
[#nodes]
==== Nodos
La transformación de un nodo se fija directamente (`nodeSetPosition`,
`nodeSetRotation`, `nodeSetQuaternion`, `nodeSetScale`) o se ajusta poco a poco
(`nodeMove`, `nodeRotate`), siempre en el espacio de su padre. `nodeSetParent`
cuelga un nodo de otro padre conservando su transformación local;
`nodeGetWorldPosition` lee dónde ha acabado después de que todos sus padres hayan
dicho la suya. `nodeSetVisible` oculta un nodo y todo lo que hay debajo de él, y
`nodeDelete` lo libera junto con sus hijos y con cualquier cuerpo, emisor o sonido
unido a ellos.
Los nombres vienen de los modelos: `nodeFind(name, root)` encuentra una
articulación o una pieza por el nombre que tenía en el archivo, bajo la instancia
dada como `root`. Un nodo creado por script también puede tener nombre
(`nodeSetName`), y eso es lo que busca `nodeFind` sin raíz.
[source,lua]
----
-- Una torreta sobre un tanque: el cañón es hijo de la torreta, y la torreta del casco,
-- así que girar la torreta arrastra el cañón y mover el casco arrastra a los dos.
local hull = nodeNew()
local turret = nodeNew()
local barrel = nodeNew()
nodeSetMesh(hull, meshBox(2, 0.8, 3.2), olive)
nodeSetMesh(turret, meshCylinder(0.7, 0.5, 16), olive)
nodeSetMesh(barrel, meshCylinder(0.1, 1.8, 8), steel)
nodeSetParent(turret, hull)
nodeSetParent(barrel, turret)
nodeSetPosition(turret, 0, 0.65, 0)
nodeSetPosition(barrel, 0, 0, -1.2)
nodeSetRotation(barrel, 90, 0, 0) -- Tumba el cilindro a lo largo de -Z.
function onOverlayUpdate()
local stick = controllerGetAxis(0, GAMEPAD_AXIS_RIGHT_X) / 32767 -- De -1 a 1.
nodeRotate(turret, 0, stick * 2, 0)
return OVERLAY_NOT_UPDATED
end
----
Dos llamadas unen la escena y el overlay. `sceneProject(x, y, z)` convierte un
punto del mundo en coordenadas del overlay, para que una etiqueta de nombre o una
barra de vida dibujada con `fontPrint` pueda seguir a un objeto 3D.
`sceneUnproject(sx, sy, distance)` va en sentido contrario: el punto del mundo a
esa distancia a lo largo del rayo que pasa por un píxel del overlay, lo que junto
con `physicsRaycast` es la selección con el ratón.
[source,lua]
----
function onMouseMoved(x, y, dx, dy, which)
-- El ratón apunta a lo largo de un rayo desde la cámara; lo que toca es el objetivo.
local cx, cy, cz = nodeGetWorldPosition(camera)
local fx, fy, fz = sceneUnproject(x, y, 50)
local hit = physicsRaycast(cx, cy, cz, fx - cx, fy - cy, fz - cz, 50)
highlighted = hit
end
----
[#_meshes_and_materials]
==== Mallas y materiales
Una malla es geometría; un material es su aspecto; un nodo une uno de cada
(`nodeSetMesh(node, mesh, material)`). Comparte ambos libremente: un `meshBox` y
un material pueden estar en mil nodos, y compartirlos es lo que hace rápido al
renderizador (véase <<sceneperformance,Rendimiento y requisitos>>).
`meshNew` construye una malla a partir de tablas de posiciones, normales,
coordenadas de textura e índices cuando las primitivas no bastan.
Un material empieza siendo un color base con alfa (`materialSetColor`) y el
aspecto metallic-roughness de glTF: `materialSetMetallic` y
`materialSetRoughness` de `0` a `1`. Una rugosidad cercana a `0` da un brillo
nítido como de espejo y `1` una superficie mate; un metal tiñe sus brillos y
reflejos con su propio color y no tiene color difuso propio.
Además:
* `materialSetEmissive` hace que una superficie emita luz de su propio color (una
pantalla, una lámpara, metal al rojo); no ilumina otras cosas, pero sí produce
bloom (véase <<scenelook,El aspecto del fotograma>>).
* `materialSetUnlit` muestra el color tal cual, ignorando todas las luces:
elementos de HUD en la escena, el brillo propio del cristal de una bombilla.
* `materialSetDoubleSided` dibuja la cara trasera de las formas abiertas, como un
plano visto desde abajo o una bandera.
* `materialSetBlend` hace que el alfa cuente, para cristal, fantasmas y agua.
Las mallas con mezcla se dibujan después de todo lo opaco, ordenadas por
distancia, y no proyectan sombras.
* `materialSetCutoff` conserva solo los téxeles cuyo alfa alcanza el umbral y
descarta el resto, que es como se dibujan las hojas, las vallas, las rejillas y
las alambradas: un quad, con la mayor parte eliminada. A diferencia de la
mezcla, no necesita ordenación y sigue proyectando sombra, y la sombra también
se recorta, así que un árbol proyecta sombras de hojas en lugar de la sombra de
su quad. Lo que supera el umbral es opaco: un téxel semitransparente o se
conserva entero o se descarta, nunca se mezcla, así que nada de lo que hay
detrás de la escena (la imagen del disco, sobre todo) tiñe una hoja.
Un umbral de 0 (el valor por defecto) desactiva el enmascarado. Un modelo
cargado desde glTF trae el suyo: `MASK` fija el umbral desde el archivo,
`BLEND` activa la mezcla y `OPAQUE` no hace ninguna de las dos cosas.
La textura de un material puede ser la imagen de cualquier sprite cargado
(`materialSetTexture`), el propio laserdisc o un vídeo cargado
(`materialSetVideo`), la imagen de una segunda cámara (`materialSetView`, véase
<<scenecamera,La cámara y las vistas>>) o un documento de GUI (`materialSetGui`,
véase <<gui,GUI>>). Una máquina recreativa modelada puede reproducir el disco en
su propia pantalla, o mostrar allí su página de opciones. Las coordenadas de
textura vienen de la malla; las primitivas proyectan una imagen una vez en cada
cara, y `materialSetTiling` la repite, que es lo que quieren el suelo y las
paredes. Las texturas tienen mipmaps y filtrado anisótropo, así que un suelo se ve
nítido hasta la distancia; `materialSetFilter(material, FILTER_NEAREST)`, en
cambio, conserva los bloques del pixel art.
[source,lua]
----
local stone = materialNew()
materialSetTexture(stone, spriteLoad(DIR .. "flagstones.png"))
materialSetTiling(stone, 12, 12) -- Doce repeticiones a lo ancho del plano.
materialSetRoughness(stone, 0.9)
local floor = nodeNew()
nodeSetMesh(floor, meshPlane(24, 24), stone)
local glass = materialNew()
materialSetColor(glass, 180, 220, 255, 90) -- Alfa 90 de 255.
materialSetBlend(glass, true)
materialSetRoughness(glass, 0.05)
----
Cuatro mapas más dan a una superficie el detalle que le falta a su geometría, y un
modelo cargado los trae consigo cuando su archivo los tiene:
* `materialSetNormalMap` desvía la iluminación con un mapa de normales en espacio
tangente, para que los ladrillos sobresalgan de una pared plana. Una intensidad
superior a `1` lo exagera.
* `materialSetOcclusionMap` oscurece las grietas a las que no llegan la luz
ambiental ni el cielo.
* `materialSetMetallicRoughnessMap` varía la metalicidad y la rugosidad a lo largo
de la superficie, empaquetadas a la manera de glTF (la rugosidad en el verde, la
metalicidad en el azul) y multiplicadas por los factores propios del material,
así que pon esos a `1` cuando deba mandar el mapa.
* `materialSetEmissiveMap` dice qué partes brillan, teñidas y escaladas por
`materialSetEmissive`.
[source,lua]
----
local brick = materialNew()
materialSetTexture(brick, spriteLoad(DIR .. "brick.png"))
materialSetNormalMap(brick, spriteLoad(DIR .. "brickNormal.png"), 1.5)
materialSetMetallicRoughnessMap(brick, spriteLoad(DIR .. "brickMetalRough.png"))
materialSetMetallic(brick, 1)
materialSetRoughness(brick, 1)
materialSetEmissiveMap(brick, spriteLoad(DIR .. "brickEmissive.png"))
materialSetEmissive(brick, 180, 100, 40) -- La argamasa brilla en naranja.
----
.Texturas comprimidas
Un PNG se decodifica a cuatro bytes por téxel en la GPU, más otro tercio para sus
mipmaps; un nivel del tamaño de Sponza ocupa así 390 MB de texturas, más de lo que
una Raspberry Pi 4 comparte con su pantalla. Las texturas distribuidas como
archivos KTX2 (Basis Universal, hechos por `util/compressTextures.py` a partir de
PNG o de un `.gltf` entero) permanecen comprimidas en la GPU a una cuarta parte
del tamaño o menos, en el formato de bloque que tenga la máquina: BC7 en los
ordenadores de sobremesa, ASTC en las GPU de Apple y en la Pi 4, ETC2 en cualquier
chip OpenGL ES 3 (la Mali de todas las consolas portátiles, la Pi con GLES), BC3
en las tarjetas más antiguas y RGBA sin más donde no exista nada de eso, todo a
partir del mismo archivo. Pasa un nombre `.ktx2` en lugar de un sprite a
`materialSetTexture` y a las llamadas de mapas, y un modelo cuyo archivo usa
`KHR_texture_basisu` las carga por sí solo. `sceneGetStats` informa de la memoria
de texturas en uso, así que puedes ver la diferencia.
[source,lua]
----
materialSetTexture(ground, DIR .. "flagstones.ktx2")
local draws, drawn, calls, textureKB = sceneGetStats()
debugPrint("textures: " .. textureKB .. " KB")
----
[#_lights_and_shadows]
==== Luces y sombras
Hasta ocho luces alumbran a la vez, y cada una es un nodo, así que las luces
tienen padres y se animan como cualquier otra cosa:
* `LIGHT_DIRECTIONAL` es un sol: solo importa su dirección, y todo se ilumina
igual. Una intensidad de `1` ilumina hasta el blanco una superficie blanca
orientada hacia ella.
* `LIGHT_POINT` es una bombilla, que se atenúa con la distancia, y
`lightSetRange` la apaga por completo más allá de una distancia. Su intensidad
es su brillo en la propia bombilla, y la mitad a una unidad de distancia, así
que una bombilla que tenga que iluminar una habitación necesita un número de
varias decenas.
* `LIGHT_SPOT` es un cono con un ángulo interior totalmente iluminado y un ángulo
exterior en el que la luz se ha atenuado hasta desaparecer (`lightSetCone`).
`sceneSetAmbient` añade un poco de luz desde todas partes para que las sombras no
sean negras; un cielo lo hace mejor (véase <<scenelook,El aspecto del fotograma>>).
Los colores dados de `0` a `255` son sRGB, como todo lo demás en Singe, y se
convierten a luz lineal al entrar, así que una luz de `128, 128, 128` tiene una
cuarta parte del brillo del blanco, no la mitad.
Cualquier luz puede proyectar sombras de bordes suaves desde todo lo opaco
(`lightSetShadow`). Las luces direccionales y los focos proyectan a través de un
mapa de sombras; las luces puntuales, a través de un mapa cúbico que llega en
todas las direcciones, así que una bombilla puede estar dentro de lo que ilumina
(una habitación, una pantalla de lámpara, la recreativa): las mallas cerradas
también proyectan desde sus caras traseras, y el rango de profundidad de la
sombra se ajusta a los objetos que proyectan sombra alrededor de la luz.
`nodeSetShadow(node, false)` exime a una malla de proyectar: el propio cristal de
la bombilla, un cartel luminoso, una etiqueta de nombre. `sceneSetShadowSize` fija
la resolución del mapa (de `256` a `4096`, por defecto `1024`) y cambia nitidez
por velocidad.
La sombra de una luz direccional se divide en cascadas a lo largo de la vista
(`sceneSetShadowCascades`, tres por defecto), cada una un mapa ajustado a una
porción de lo que ve la cámara, así que las sombras siguen nítidas de cerca en un
nivel grande y aun así llegan hasta `sceneSetShadowDistance`. Una sola cascada
ajusta un único mapa a toda la escena, lo que va bien para escenas pequeñas y
cámaras ortográficas. Una vista (`viewNew`) dibuja con los mismos mapas, tomando
para cada punto la cascada más fina que lo cubre, así que un monitor muestra las
mismas sombras que la ventana.
[source,lua]
----
local sun = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(sun, 30, 40, 20)
nodeLookAt(sun, 0, 0, 0)
lightSetIntensity(sun, 2.5)
lightSetColor(sun, 255, 240, 220)
lightSetShadow(sun, true)
sceneSetShadowSize(2048)
sceneSetShadowDistance(80) -- Tres cascadas hasta 80 unidades.
local lamp = lightNew(LIGHT_POINT)
nodeSetPosition(lamp, 3, 3.1, -2)
lightSetColor(lamp, 255, 230, 190)
lightSetIntensity(lamp, 12)
lightSetRange(lamp, 14)
lightSetShadow(lamp, true) -- Un mapa cúbico: seis pasadas por fotograma.
nodeSetShadow(bulbGlass, false) -- El cristal que la rodea no proyecta nada.
----
[#scenecamera]
==== La cámara y las vistas
Cualquier nodo puede ser la cámara (`cameraSet`); mira a lo largo de su propio -Z,
así que `nodeLookAt` la apunta, y colgarla de un nodo en movimiento crea una
cámara de persecución. `cameraSetPerspective(fovDegrees, near, far)` es el
objetivo por defecto; `cameraSetOrthographic(height, near, far)` dibuja sin
perspectiva, para diagramas, escenas tipo HUD y juegos de vista lateral. Mantén
`near` tan grande como permita la escena, porque la precisión del búfer de
profundidad se reparte entre `near` y `far`.
[source,lua]
----
local camera = nodeNew()
nodeSetParent(camera, car)
nodeSetPosition(camera, 0, 2.5, 7) -- Detrás y encima del coche, en su espacio.
nodeLookAt(camera, 0, 1, 0)
cameraSet(camera)
cameraSetPerspective(55, 0.1, 300)
----
Una *vista* es una segunda cámara renderizada a una textura en cada fotograma y
mostrada en un material: un monitor de seguridad, un retrovisor, un portal, una
imagen dentro de otra. `viewNew(width, height)` crea una a esa resolución,
`viewSetCamera` le da un nodo de cámara, y `materialSetView` pone su imagen en
cualquier malla. Las vistas comparten las sombras del fotograma y se saltan el
bloom, y cada una renderiza la escena otra vez, así que conviene que sean pocas y
pequeñas. Cada
vista orienta los billboards (véase <<scenesprites,Sprites y texto en la escena>>)
hacia su propia cámara y dibuja los materiales transparentes de atrás hacia
delante desde ella, así que un monitor ve las etiquetas de nombre de frente y el
cristal en el orden correcto.
[source,lua]
----
local rearCamera = nodeNew()
nodeSetParent(rearCamera, car)
nodeSetPosition(rearCamera, 0, 1.2, -2)
nodeSetRotation(rearCamera, 0, 180, 0) -- Mirando hacia atrás.
local rear = viewNew(384, 256)
viewSetCamera(rear, rearCamera)
local mirror = materialNew()
materialSetView(mirror, rear)
materialSetUnlit(mirror, true)
local panel = nodeNew()
nodeSetParent(panel, car)
nodeSetMesh(panel, meshPlane(0.6, 0.2), mirror)
nodeSetPosition(panel, 0, 1.6, 0.5)
nodeSetRotation(panel, 90, 0, 0) -- El plano mira hacia arriba; ponlo de pie mirando al conductor.
----
[#scenelook]
==== El aspecto del fotograma
La escena se ilumina en luz lineal. Los colores que das de `0` a `255` son sRGB y
se decodifican al entrar, la GPU decodifica las texturas al muestrearlas, las
luces se suman sin recortarse y, al final del fotograma, una curva tonal devuelve
el resultado a la pantalla. Lo que eso significa en la práctica:
* `sceneSetExposure(stops)` aclara u oscurece todo el fotograma como la
exposición de una cámara: `+1` duplica la luz y `-1` la reduce a la mitad, así
que una cueva y una playa pueden compartir las mismas luces y materiales.
* `sceneSetTonemap` elige la curva: `TONEMAP_NEUTRAL` (la de por defecto, que
deja los colores donde los dejan los visores de glTF y solo comprime la luz más
intensa), `TONEMAP_ACES` (un aspecto cinematográfico con más contraste) o
`TONEMAP_NONE` (recorta en el blanco).
* `sceneSetBloom(threshold, strength)` hace que brille todo lo que supere el
umbral: el sol a través de una ventana, una bombilla, un cartel emisivo, una
llama. El brillo se toma de la luz de la escena antes de la curva tonal, así que
solo produce bloom lo que es realmente brillante; un umbral de `1` es el blanco.
.El cielo y su luz
`sceneSetSky(file)` envuelve la escena en un panorama equirrectangular. Un `.hdr`
de Radiance conserva todo su rango; un PNG o un JPEG también sirven. El cielo se
dibuja detrás de todo y, salvo con `sceneSetEnvironment(false)`, también ilumina
la escena: su color general desde todas las direcciones sustituye a la luz
ambiental plana, y su reflejo se ve en los metales y en las superficies
brillantes, nítido en un espejo y más borroso a medida que aumenta la rugosidad.
Sin cielo, los metales no tienen nada que reflejar y solo les dan forma la luz
ambiental y las luces.
`sceneSetSkyIntensity` escala el cielo y su luz a la vez, así que un mismo
panorama sirve para el mediodía y para el anochecer.
.Niebla
`sceneSetFog(r, g, b, near, far)` funde todo, partículas incluidas, hacia un color
entre dos distancias. El cielo nunca tiene niebla, así que haz coincidir el color
de la niebla con el horizonte del cielo y el borde lejano de la escena se perderá
en él.
[source,lua]
----
sceneSetSky(DIR .. "harbor.hdr")
sceneSetSkyIntensity(0.8)
sceneSetFog(190, 200, 215, 40, 160) -- El color del cielo en el horizonte.
sceneSetExposure(-0.5) -- Un poco más oscuro de lo que lo dejaría el sol.
sceneSetBloom(1.0, 0.35) -- Solo brilla lo que supera al blanco.
----
[#_models_and_animation]
==== Modelos y animación
`modelLoad` acepta un `.glb` por el mismo nombre relativo al juego que acepta
`spriteLoad`, y un `.game` empaquetado lo lleva consigo. El archivo debe ser
autocontenido: mallas, skins, animaciones y texturas, todo dentro del mismo
archivo, que es la exportación por defecto de Blender. Un modelo que hace
referencia a un `.bin` o a una imagen que tiene al lado se rechaza con un mensaje
que nombra el archivo, por la misma razón por la que un juego empaquetado tiene
que ser autocontenido. Los materiales usan el modelo metallic-roughness con
`KHR_materials_emissive_strength` y `KHR_texture_basisu`; una textura
direccionada mediante un segundo conjunto de
coordenadas de textura lee el primero, y `KHR_texture_transform` se ignora, cada
caso con una línea en la traza del programa. Un skin puede tener hasta `128`
articulaciones. La carga es la parte cara; después `modelInstance` coloca una
copia del árbol de nodos del modelo, tantas veces como quieras, y todas las
instancias comparten las mallas y los materiales del archivo. `modelDelete` libera
el modelo; las instancias conservan sus nodos, desnudos.
[source,lua]
----
local foxModel = modelLoad(DIR .. "Fox.glb")
local pack = {}
for i = 1, 6 do
local fox = modelInstance(foxModel)
nodeSetScale(fox, 0.02) -- El archivo está en centímetros.
nodeSetPosition(fox, i * 1.5 - 5, 0, 0)
pack[i] = fox
end
----
`modelGetAnimations` lista las animaciones de un modelo, que se manejan por
instancia con `animationPlay(node, nameOrIndex, loop, speed, fade)` sobre el nodo
raíz de la instancia, además de llamadas para pausar, reanudar, detener y saltar.
La animación de nodos (cosas que se mueven), el skinning (personajes que se
deforman) y los morph targets (blend shapes: caras, sincronización labial,
músculos que se hinchan) funcionan todos. Las animaciones avanzan con el tiempo
real y se detienen mientras el juego está en pausa. El peso de un morph también se
puede fijar desde el script con `nodeSetMorph`, por nombre o por número, en el
nodo que lleva la malla.
Un `fade` en segundos hace un fundido cruzado desde lo que se estuviera
reproduciendo, así que andar pasa a correr sin un tirón. Cuando no se estaba
reproduciendo nada (después de `animationStop`, o cuando se ha desactivado un
ragdoll), el fundido empieza desde la pose en que están los nodos, así que un
personaje se levanta suavemente desde donde cayó. Hasta cuatro clips más pueden
reproducirse a la vez en capas por encima de la base (`animationPlayLayer`), cada
uno mezclado según `animationSetLayerWeight` y limitado a una parte del esqueleto
con `animationSetLayerMask`: mirar alrededor con el cuello mientras las piernas
caminan, saludar con un brazo durante una carrera.
[source,lua]
----
animationPlay(fox, "Walk", true)
animationPlayLayer(fox, 1, "Survey", true, 1, 0.5) -- La cabeza gira, mezclada durante medio segundo
animationSetLayerMask(fox, 1, nodeFind("b_Neck_04", fox)) -- ... solo en el cuello y por encima.
function onCollision(a, b, x, y, z, speed)
if (a == fox or b == fox) and speed > 3 then
animationPlay(fox, "Run", true, 1.2, 0.4) -- Más rápido, con un fundido cruzado de 0.4 s.
end
end
----
[#_sound_in_the_scene]
==== Sonido en la escena
Un efecto de sonido se puede colocar en la escena para que se oiga desde donde
está. `soundSetPosition(channel, x, y, z)` pone un canal que está sonando en un
punto del mundo, `soundSetNode(channel, node)` hace en cambio que siga a un nodo,
y `soundSetRange(channel, near, far)` dice dónde está a todo volumen y dónde se ha
atenuado hasta el silencio. Los sonidos se oyen desde la cámara salvo que
`soundSetListener` nombre otro nodo (la cabeza del jugador, en un juego en
tercera persona), y se desplazan a izquierda y derecha cuando el oyente gira. Un
canal que no está colocado en la escena suena como siempre, con `soundSetPan`
para el estéreo.
[source,lua]
----
local crackle = soundPlay(fireSound, -1) -- Se repite sin fin.
soundSetNode(crackle, brazier)
soundSetRange(crackle, 2, 18) -- A todo volumen hasta 2 unidades, inaudible a 18.
soundSetListener(hero)
----
[#scenesprites]
==== Sprites y texto en la escena
Cualquier nodo puede mostrar una imagen en lugar de una malla. `nodeSetSprite(node, sprite,
height)` pone un sprite cargado (todos los fotogramas de uno animado, con
`nodeSetSpriteFrame` para elegir) en un quad de esa altura en unidades del mundo,
iluminado como todo lo demás y proyectando sombras salvo que `nodeSetShadow` diga
lo contrario. `nodeSetText(node, text, height)` hace lo mismo con una línea de
texto en la fuente y el color seleccionados, sin iluminar. Ambos son mallas para
el resto del motor: tienen padres, se escalan y aceptan la mezcla de un material.
`nodeSetBillboard` gira un nodo para que mire a la cámara en cada fotograma:
`BILLBOARD_ALL` de frente del todo (etiquetas de nombre, brillos, impostores),
`BILLBOARD_Y` solo alrededor de su propio eje vertical (árboles, barras de vida,
cualquier cosa que deba mantenerse derecha), y `BILLBOARD_NONE` para dejar de
hacerlo. También funciona con mallas.
[source,lua]
----
local tag = nodeNew()
nodeSetParent(tag, fox)
nodeSetPosition(tag, 0, 110, 0) -- En los centímetros del Fox, por encima de su cabeza.
nodeSetScale(tag, 50) -- Deshace la escala de 0.02 de la instancia.
nodeSetText(tag, "FOX", 0.6)
nodeSetBillboard(tag, BILLBOARD_ALL)
nodeSetShadow(tag, false)
local icon = nodeNew()
nodeSetParent(icon, fox)
nodeSetPosition(icon, 0, 150, 0)
nodeSetScale(icon, 50)
nodeSetSprite(icon, spriteLoad(DIR .. "crate.png"), 0.5)
nodeSetBillboard(icon, BILLBOARD_Y)
----
[#scenefront]
==== Delante del overlay
La escena se dibuja normalmente por debajo del overlay, así que todo lo que se
dibuja en el overlay la tapa. `nodeSetLayer(node, LAYER_FRONT)` mueve en cambio un
nodo, y todo lo que cuelga de él, a la capa frontal: la misma cámara lo dibuja
después del overlay, iluminado por las mismas luces y acabado con la
misma exposición, la misma curva tonal y el mismo bloom, así que un logotipo que
gira, un título 3D o un modelo en una tabla de récords pueden quedar sobre los
gráficos 2D. La capa frontal es una imagen aparte, borrada a transparente, así que
el overlay se ve allí donde ella no dibuja nada, y nada de la escena que hay
detrás del overlay puede taparla. Las GUI y las partículas dibujadas sobre el
overlay siguen quedando encima de ella.
Los nodos frontales siguen proyectando sombras sobre la escena. El cielo, las
partículas 3D y las líneas de depuración se quedan en la escena, y las vistas
(`viewNew`) solo dibujan los nodos de la escena. La capa cuesta una segunda pasada
de renderizado, y solo en los fotogramas en que hay algo en ella.
[source,lua]
----
-- Una moneda de oro girando sobre la pantalla de título 2D.
local coin = nodeNew()
nodeSetMesh(coin, meshCylinder(1, 0.2, 48), gold)
nodeSetRotation(coin, 90, 0, 0)
nodeSetLayer(coin, LAYER_FRONT)
function onOverlayUpdate()
overlayClear()
spriteDraw(titleArt, 0, 0)
nodeRotate(coin, 0, 2, 0)
return OVERLAY_UPDATED
end
----
[#sceneterrain]
==== Terreno
`meshHeightmap(image, sizeX, sizeY, sizeZ)` convierte una imagen en escala de
grises en un terreno ondulado: un vértice por píxel, el negro abajo y el blanco a
`sizeY` de altura, con normales sacadas de las pendientes y coordenadas de textura
a lo largo de todo él. Da a su material una textura y `materialSetTiling` para
repetirla cada pocas unidades. Las imágenes pueden tener hasta 1025 píxeles en
cada dirección; un mapa de 257 píxeles sobre 120 unidades da un vértice cada media
unidad, que es de sobra para unas colinas.
Un cuerpo `BODY_STATIC` con `SHAPE_MESH` en ese nodo se convierte en un campo de
alturas, mucho más barato de lo que serían los triángulos, así que los coches
circulan por él y los jugadores caminan por él tal cual.
`terrainGetHeight(node, x, z)` dice a qué altura está el suelo en un punto, para
colocar cosas encima sin lanzar un rayo.
[source,lua]
----
local ground = materialNew()
materialSetTexture(ground, DIR .. "grass.ktx2")
materialSetTiling(ground, 24, 24)
local hills = nodeNew()
nodeSetMesh(hills, meshHeightmap(DIR .. "hills.png", 120, 14, 120), ground)
bodyNew(hills, BODY_STATIC, SHAPE_MESH) -- Un campo de alturas.
for i = 1, 20 do
local x, z = math.random(-50, 50), math.random(-50, 50)
local tree = modelInstance(treeModel)
nodeSetPosition(tree, x, terrainGetHeight(hills, x, z), z)
end
----
[#_navigation]
==== Navegación
Una malla de navegación les dice a los personajes por dónde pueden caminar. `navNew(radius,
height, maxSlope, maxStep)` empieza una para un tamaño de agente dado, `navAddNode`
le pasa la geometría del nivel (la raíz de una instancia de modelo aporta el
modelo entero), y `navBuild` precalcula la superficie transitable: suelos, rampas
y escaleras que el agente puede subir, menos un margen alrededor de cada pared y
columna. Precalcular Sponza lleva una décima de segundo en un ordenador de
sobremesa; `navSave` y `navLoad` guardan una malla precalculada en la carpeta de
datos del juego para las máquinas en las que tarda más, y una malla que se
distribuye con el juego se carga del mismo modo. `navDraw` muestra la malla sobre
la escena mientras ajustas los parámetros.
Después, las consultas responden a las preguntas que hace un juego: `navPath`
devuelve las esquinas de la ruta más corta entre dos puntos, `navNearest` ajusta
un punto a la malla, `navRaycast` dice si un camino en línea recta está
bloqueado, y `navRandomPoint` elige un sitio al que ir de paseo.
Los agentes (`navAgentNew`) recorren la malla por ti. `navAgentMoveTo` envía a uno
a algún sitio, y este esquiva a los demás agentes por el camino, moviendo su nodo
(o manejando un controlador de jugador sobre él, con `navAgentSetPlayer`) y
llamando a `onNavArrived` cuando llega. `navAgentGetVelocity` te dice si
reproducir la animación de andar o la de correr.
[source,lua]
----
local nav = navNew(0.4, 1.6, 45, 0.3) -- Un agente de 0.8 de ancho y 1.6 de alto, que sube 0.3.
navAddNode(nav, level)
navBuild(nav)
local guards = {}
for i = 1, 6 do
local guard = modelInstance(guardModel)
local x, y, z = navRandomPoint(nav)
nodeSetPosition(guard, x, y, z)
animationPlay(guard, "Walk", true)
local agent = navAgentNew(nav, guard, 0.35, 1.6, 1.4)
navAgentMoveTo(agent, navRandomPoint(nav))
guards[agent] = guard
end
function onNavArrived(agent)
navAgentMoveTo(agent, navRandomPoint(nav)) -- Al siguiente sitio.
end
----
[#sceneperformance]
==== Rendimiento y requisitos
La escena necesita una GPU que hable Vulkan (Linux), Direct3D 12 (Windows 10 y
posteriores), Metal (macOS) u OpenGL ES 3.1. El backend de ES es el que trae las
consolas portátiles Linux baratas y la Raspberry Pi, cuyos chips Mali y VideoCore
tienen un controlador GLES maduro y, o bien ningún Vulkan, o bien uno inmaduro; se
elige automáticamente cuando ninguno de los otros tres está disponible, y dibuja
la misma imagen con los mismos shaders. La Raspberry Pi 4 sigue siendo la Pi
mínima para 3D: la Pi 3 solo tiene OpenGL ES 2.0, que no puede expresar el
skinning ni los morph targets de la escena. En una máquina sin ninguno de ellos,
los juegos 2D funcionan exactamente igual que antes (física 2D incluida) y la
primera llamada 3D termina el juego con un error que nombra el problema.
Las mallas que quedan fuera de la vista de la cámara se omiten, y las copias de la
misma malla con el mismo material se dibujan como una sola llamada instanciada,
así que un bosque de un mismo árbol cuesta más o menos lo que cuesta un árbol.
`sceneGetStats` informa de los dibujos recogidos, de cuántos estaban a la vista,
de cuántas llamadas de dibujo han hecho falta y de la memoria de texturas en uso.
Para ponérselo fácil:
* Comparte mallas y materiales entre nodos (un `meshBox`, muchos nodos; un
modelo, muchas llamadas a `modelInstance`) en lugar de crear uno por objeto.
* Las mallas con skinning, las que tienen morphs y las que usan mezcla se dibujan
de una en una; limítalas a lo que necesite la escena.
* El antialiasing (multimuestreo 4x) está activado allí donde la GPU lo ofrece;
`sceneSetAntialias(false)` recupera velocidad en una Pi, igual que un
`sceneSetShadowSize` más pequeño y menos luces puntuales que proyecten sombras,
cada una de las cuales cuesta seis pasadas.
* Vigila el número de triángulos y el tamaño de las texturas en las placas
pequeñas, distribuye texturas KTX2 y carga un modelo una vez en lugar de dos.
[source,lua]
----
function onOverlayUpdate()
local draws, drawn, calls, textureKB = sceneGetStats()
overlayClear()
local text = string.format("%d/%d meshes, %d calls, %d KB", drawn, draws, calls, textureKB)
fontPrint(10, 10, text)
return OVERLAY_UPDATED
end
----
[#physics]
=== Física
Los cuerpos, las articulaciones, los activadores y los lanzamientos de rayos viven en la escena 3D
y funcionan sobre Jolt Physics. Un cuerpo se une a un nodo igual que una malla, y desde ese momento
el nodo y el cuerpo son una sola cosa: la forma del cuerpo se mide en unidades del mundo y la escala
el nodo, y se mueva el que se mueva de los dos, el otro lo sigue.
Las unidades son metros, kilogramos y segundos, y la gravedad es por defecto
`0, -9.81, 0`. La física no necesita GPU; véase <<physics2d,Juegos 2D>>.
[#_bodies]
==== Cuerpos
`bodyNew(node, type, shape, a, b, c)` da a un nodo un cuerpo de uno de tres
tipos:
* Un cuerpo *estático* (`BODY_STATIC`) se queda donde estaba el nodo cuando se creó
el cuerpo: suelos, paredes, la recreativa. Mover su nodo después no mueve
el cuerpo.
* Un cuerpo *cinemático* (`BODY_KINEMATIC`) sigue a su nodo dondequiera que lo muevan el script
o una animación, y aparta de su camino a los cuerpos dinámicos sin que a él
lo empujen: una pala, una plataforma móvil, la mano de un personaje.
* Un cuerpo *dinámico* (`BODY_DYNAMIC`) lo mueve la simulación, y él mueve
su nodo: cae, rebota, se desliza y recibe empujones. No muevas tú su nodo;
dale en cambio velocidades, fuerzas o impulsos
(`bodySetVelocity`, `bodyApplyForce`, `bodyApplyImpulse`).
La forma es una caja, una esfera, una cápsula o un cilindro con las medidas `a`, `b` y `c`
(`SHAPE_BOX` toma anchura, altura y profundidad; `SHAPE_SPHERE`, un radio;
`SHAPE_CAPSULE` y `SHAPE_CYLINDER`, un radio y una altura a lo largo de Y), una
envolvente convexa de la malla del nodo (`SHAPE_HULL`, todo lo que hay bajo el nodo, así que
sirve la raíz de una instancia de modelo), o los triángulos exactos de la malla
(`SHAPE_MESH`, solo para cuerpos estáticos y cinemáticos; sobre una malla de mapa de alturas es
un campo de alturas, véase <<sceneterrain,Terreno>>).
[source,lua]
----
floor = nodeNew()
nodeSetMesh(floor, meshBox(14, 0.2, 14), stone)
nodeSetPosition(floor, 0, -1.9, 0)
bodyNew(floor, BODY_STATIC, SHAPE_BOX, 14, 0.2, 14)
for i = 1, 6 do
local crate = nodeNew()
nodeSetMesh(crate, meshBox(0.6, 0.6, 0.6), wood)
nodeSetPosition(crate, -1, -1.5 + (i - 1) * 0.62, 0)
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.6, 0.6, 0.6)
bodySetMass(crate, 5)
end
paddle = nodeNew()
nodeSetMesh(paddle, meshBox(0.3, 1.2, 1.6), red)
nodeSetPosition(paddle, -4, -1.2, 0)
bodyNew(paddle, BODY_KINEMATIC, SHAPE_BOX, 0.3, 1.2, 1.6)
function onOverlayUpdate()
nodeMove(paddle, 0.06, 0, 0) -- La pala cinemática barre la pila.
return OVERLAY_UPDATED
end
----
.Masa y materiales
La masa de un cuerpo dinámico sale del volumen de su forma con la densidad del
agua, así que una caja del tamaño de una puerta pesa 140 kilogramos y un empujoncito no
la mueve. Usa `bodySetMass` en todo lo que quieras que se sienta ligero, y ajusta los
impulsos a la masa: un impulso de `mass * speed` fija esa velocidad.
`bodySetFriction` y `bodySetBounce` hacen hielo y goma; `bodySetEnabled`
saca un cuerpo del mundo y lo vuelve a poner sin perderlo.
.Sincronización
El mundo avanza a un ritmo fijo de sesenta pasos por segundo según el reloj, después de que
las animaciones hayan puesto en pose sus nodos y antes de que se renderice la escena, así que un
cuerpo cinemático movido por una animación empuja con la velocidad correcta.
Poner el juego en pausa pausa la física; `physicsSetEnabled(false)` la pausa por
sí sola. `bodyIsResting` dice cuándo un cuerpo se ha asentado, que es cuando
algo lanzado se ha detenido y se puede contar una puntuación. Un cuerpo, jugador,
ragdoll, cuerpo blando o vehículo cuyo nodo se ha borrado se libera al
principio del siguiente fotograma, esté activado o no y en pausa o no, así que su hueco
vuelve a quedar libre; un handle de nodo que `nodeNew` vuelva a entregar más tarde no lleva
nada de la física del nodo anterior, y `playerExists` y las de su clase responden
`false` para él hasta que tenga la suya propia.
[#_events_and_queries]
==== Eventos y consultas
Define `onCollision(nodeA, nodeB, x, y, z, speed)` y Singe la llama una vez
por cada contacto nuevo, con el punto y la velocidad a la que se encontraron los dos cuerpos,
para que un golpecito y un choque puedan sonar distinto. Un cuerpo convertido en activador con
`bodySetTrigger` no empuja nada, pero informa de lo que entra en él y sale de él
mediante `onTrigger(trigger, other, entered)`: una línea de meta, una puerta, una
zona de peligro. `physicsRaycast` encuentra el primer cuerpo a lo largo de un rayo; con
`sceneUnproject` para el rayo, eso es seleccionar con el ratón (véase <<nodes,Nodos>>).
[source,lua]
----
finish = nodeNew()
nodeSetPosition(finish, 0, 1, -40)
bodyNew(finish, BODY_STATIC, SHAPE_BOX, 12, 2, 0.5)
bodySetTrigger(finish, true)
function onTrigger(trigger, other, entered)
if trigger == finish and other == car and entered then
lapDone()
end
end
function onCollision(a, b, x, y, z, speed)
if speed > 6 then
local channel = soundPlay(crashSound)
soundSetPosition(channel, x, y, z)
end
end
----
[#_joints]
==== Articulaciones
`jointHinge`, `jointBall` y `jointSlider` unen dos cuerpos, o un cuerpo
y el mundo cuando el segundo nodo es `-1`: puertas, péndulos, cajones, las
ruedas de un coche visto de lado. El anclaje y el eje se dan en espacio del mundo,
allí donde están los cuerpos cuando se crea la articulación. Las bisagras y las correderas
aceptan límites (`jointSetLimits`): un ángulo de bisagra positivo es un giro del primer nodo
alrededor del eje según la regla de la mano derecha, y una distancia de corredera positiva va
en el sentido del eje.
[source,lua]
----
door = nodeNew()
nodeSetMesh(door, meshBox(0.9, 2.0, 0.05), oak)
nodeSetPosition(door, 0.45, 1.0, 0)
bodyNew(door, BODY_DYNAMIC, SHAPE_BOX, 0.9, 2.0, 0.05)
bodySetMass(door, 20)
local hinge = jointHinge(door, -1, 0, 1.0, 0, 0, 1, 0) -- En el marco, alrededor de Y.
jointSetLimits(hinge, 0, 110) -- Se abre hacia un lado, 110 grados.
----
Los cuerpos en instancias de modelo funcionan como cualquier otro: `bodyNew(instanceRoot,
BODY_DYNAMIC, SHAPE_HULL)` envuelve el modelo entero en una envolvente convexa. Un personaje
con skinning queda mejor como jugador (más abajo) o como una cápsula cinemática que empuja
las cosas en lugar de dejarse empujar.
[#physics2d]
==== Juegos 2D
La física no está atada a la escena 3D. Los nodos existen en todas las máquinas, con GPU o
sin ella, así que un juego 2D puede dar cuerpos a los nodos, llamar a `physicsSet2D(true)` para que
solo se muevan en X e Y y solo giren alrededor de Z, trabajar en coordenadas del overlay con
la gravedad apuntando hacia abajo en la pantalla, y dibujar sprites donde están los nodos.
Esta es la forma que funciona en una Raspberry Pi 3, que no tiene 3D.
[source,lua]
----
physicsSet2D(true)
physicsSetGravity(0, 900, 0) -- Píxeles por segundo al cuadrado; la Y del overlay crece hacia abajo.
floor = nodeNew()
nodeSetPosition(floor, 360, 470, 0)
bodyNew(floor, BODY_STATIC, SHAPE_BOX, 720, 20, 50)
crate = nodeNew()
nodeSetPosition(crate, 200, -40, 0)
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 40, 40, 50)
bodySetMass(crate, 1)
function onOverlayUpdate()
local x, y = nodeGetPosition(crate)
local _, _, angle = nodeGetRotation(crate)
spriteRotate(crateArt, angle)
spriteDraw(crateArt, x, y, true)
return OVERLAY_UPDATED
end
----
Da a cada cuerpo 2D algo de profundidad (la tercera medida) para que las formas se solapen en Z;
`50` vale. Los círculos son `SHAPE_SPHERE`. Los jugadores y los emisores 2D (véase
<<particles,Partículas>>) también funcionan en este mundo, y un juego de plataformas es un
jugador con `playerMove(node, vx)`.
[#_players]
==== Jugadores
Un cuerpo va adonde lo mandan las fuerzas; un *jugador* va adonde dice el script.
`playerNew` pone un controlador de personaje en un nodo: una cápsula por defecto
(`playerNew(node, radius, height)`), o cualquier forma convexa de `bodyNew`
(`playerNew(node, SHAPE_BOX, w, h, d)` para un robot, `playerNew(node,
SHAPE_SPHERE, r, 0)` para algo que rueda y aun así sube escalones,
`SHAPE_HULL` para el contorno del propio modelo; tres argumentos siempre significan una
cápsula, así que una esfera escribe todas sus medidas). La forma se apoya en el origen del
nodo, que son los pies. Cada fotograma el script da a `playerMove` la velocidad horizontal
que quiere, y el controlador camina por los suelos, sube escalones de hasta `playerSetStep`,
se desliza a lo largo de las paredes, rechaza pendientes más empinadas que `playerSetSlope`,
cae por la gravedad, viaja sobre plataformas móviles y aparta a empujones de su camino los
cuerpos dinámicos ligeros (`playerSetPush`). `playerJump` solo funciona con suelo bajo los pies
y dice si ha saltado. La rotación del nodo es cosa del script: gíralo para que mire hacia donde
camina, y cuelga el modelo debajo como hijo.
[source,lua]
----
hero = nodeNew()
nodeSetPosition(hero, 0, 0, 0)
playerNew(hero, 0.3, 1.0) -- Una cápsula de radio 0.3 y 1 de alto, con los pies en el nodo.
fox = modelInstance(modelLoad(DIR .. "Fox.glb"))
nodeSetParent(fox, hero)
function onInputPressed(switch)
if switch == SWITCH_BUTTON1 then
playerJump(hero, 4.5) -- Necesita suelo bajo los pies.
end
end
function onOverlayUpdate()
local vx = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_X) / 32767 * 3
local vz = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_Y) / 32767 * 3
playerMove(hero, vx, vz)
if vx ~= 0 or vz ~= 0 then
nodeSetRotation(hero, 0, math.deg(math.atan(-vx, -vz)), 0)
end
return OVERLAY_NOT_UPDATED
end
----
Los jugadores avisan con `onCollision` cuando empiezan a tocar un cuerpo y con
`onTrigger` cuando entran en un sensor o salen de él, con su nodo en el lugar
que ocuparía el nodo de un cuerpo, así que una puerta, un objeto que recoger y un pozo de lava
funcionan igual para un jugador que para una pelota. `playerIsOnGround` y `playerGetGround`
dicen sobre qué está el jugador, para los sonidos de pasos y para dejarse caer de una
plataforma a propósito. En un mundo 2D un jugador es un personaje de plataformas:
`playerMove(node, vx)` corre a lo largo de X, la gravedad apunta hacia abajo en la pantalla y
todo lo demás es igual. Las tolerancias del controlador escalan con la
forma, así que una cápsula de 30 píxeles en un juego de sprites se comporta como una de
0,3 metros en un juego 3D.
[#_vehicles]
==== Vehículos
Un *vehículo* es un cuerpo dinámico (el chasis, creado con `bodyNew` como cualquier
cuerpo, mirando hacia -Z con la Y hacia arriba) que Jolt conduce mediante ruedas:
`vehicleNew(node, VEHICLE_CAR)` para cualquier cosa con ejes,
`VEHICLE_MOTORCYCLE` para dos ruedas en línea que se inclinan en las curvas y
mantienen el equilibrio, o `VEHICLE_TANK` para ruedas sobre dos orugas que giran sin moverse
del sitio.
Las ruedas son nodos: `vehicleAddWheel(node, wheelNode, radius, width,
suspension)` toma la posición de la rueda de dónde está su nodo respecto al
chasis, y desde ese momento el motor coloca ese nodo en cada fotograma (su eje X
es el eje de la rueda, así que una rueda hecha con `meshCylinder` necesita un hijo girado 90
grados). Las ruedas encontradas en un modelo con `nodeFind("wheelFL", car)` funcionan
igual. Por defecto las ruedas delanteras giran y todas dan tracción;
`vehicleSetWheel` dispone otra cosa.
Cada fotograma el script hace de conductor: `vehicleDrive(node, forward,
right, brake, handBrake)`. El motor, la caja de cambios automática y los diferenciales
son los de Jolt, con los valores por defecto de un coche de 1500 kg; `vehicleSetEngine`,
`vehicleSetGears`, `vehicleSetSuspension`, `vehicleSetSteering`,
`vehicleSetBrakes` y `vehicleSetAntiRoll` los ajustan, y cualquier cambio
reconstruye el vehículo en su sitio en el siguiente paso. `vehicleGetSpeed`,
`vehicleGetRpm` y `vehicleGetGear` alimentan un cuadro de instrumentos. Un vehículo sigue siendo
un cuerpo: salta rampas, choca con paredes, aparta cajas y avisa con `onCollision`.
[source,lua]
----
car = nodeNew()
nodeSetMesh(car, meshBox(1.8, 0.5, 4.0), paint)
nodeSetPosition(car, 0, 1, 0)
bodyNew(car, BODY_DYNAMIC, SHAPE_BOX, 1.8, 0.5, 4.0)
bodySetMass(car, 1500)
vehicleNew(car, VEHICLE_CAR)
local corners = {
{ -0.95, -0.2, -1.4 }, { 0.95, -0.2, -1.4 }, -- Delanteras izquierda y derecha ...
{ -0.95, -0.2, 1.4 }, { 0.95, -0.2, 1.4 }, -- ... traseras izquierda y derecha.
}
for i, p in ipairs(corners) do
local wheel = nodeNew()
nodeSetParent(wheel, car)
nodeSetPosition(wheel, p[1], p[2], p[3])
vehicleAddWheel(car, wheel, 0.35, 0.25, 0.4)
end
function onOverlayUpdate()
local throttle = controllerGetAxis(0, GAMEPAD_AXIS_RIGHT_TRIGGER) / 32767
local brake = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_TRIGGER) / 32767
local steer = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_X) / 32767
vehicleDrive(car, throttle, steer, brake, 0)
return OVERLAY_NOT_UPDATED
end
----
Los tanques toman `forward` como acelerador y `right` como giro (sin acelerador
pivotan sobre sí mismos), e ignoran el ajuste de dirección, frenos y antivuelco, ya que
sus orugas frenan y giran por su cuenta. El muelle de inclinación de la moto se
escala según el chasis, así que una moto ligera se equilibra tan bien como una pesada.
Los vehículos con ruedas necesitan un mundo 3D; un coche 2D visto de lado
es un cuerpo con dos ruedas con bisagra, que las llamadas de articulaciones ya construyen.
[#_water]
==== Agua
`bodySetWater(node, density, linearDrag, angularDrag)` llena de agua un cuerpo estático:
la cara superior de su caja es la superficie, y cada cuerpo dinámico que haya dentro
flota, se hunde o se balancea por la flotabilidad de Jolt respecto a esa superficie (cuenta la
parte sumergida de la forma, así que una caja inclinada se endereza sola),
frenado por las resistencias. `bodySetCurrent` da una corriente al agua. `density` es
un multiplicador, `1` para el agua, y el `bodySetBuoyancy` propio de cada cuerpo que flota
(por defecto `1.2`, que flota suavemente; por debajo de `1` se hunde) lo escala. El volumen
es un activador, así que `onTrigger` se dispara al entrar y al salir, para las salpicaduras.
El agua no se dibuja: da al nodo del volumen un material con mezcla, o ninguno.
Un jugador en el agua nada: `playerIsSwimming` lo dice, `playerMove(node, vx,
vy, vz)` dirige entonces en tres ejes, se hunde despacio si no hay entrada vertical
(`playerSetSwim` fija ese ritmo y la resistencia del agua), y la corriente
lo arrastra. Un barco es un casco flotante al que `vehicleNew(node, VEHICLE_BOAT)`
da una hélice y un timón: `vehicleSetThrust` dice con cuánta fuerza empuja
y en qué punto del casco, `vehicleSetRudder` con cuánta fuerza gira, y
`vehicleDrive` funciona como en un coche, todo ello solo mientras la hélice está bajo una
superficie.
[source,lua]
----
pool = nodeNew()
nodeSetMesh(pool, meshBox(8, 3, 12), blue) -- Azul con mezcla, dibujado como el agua.
nodeSetPosition(pool, 0, -1.5, 0) -- Su cara superior, en Y = 0, es la superficie.
bodyNew(pool, BODY_STATIC, SHAPE_BOX, 8, 3, 12)
bodySetWater(pool, 1.0, 0.6, 0.2)
bodySetCurrent(pool, 0.5, 0, 0) -- Deriva hacia +X.
bodySetBuoyancy(crate, 1.6) -- Flota alto.
bodySetBuoyancy(anchor, 0.3) -- Se hunde.
----
[#_ragdolls]
==== Ragdolls
`ragdollNew(node)` busca el skin bajo una instancia de modelo y convierte el
esqueleto en una receta de ragdoll: cada articulación con una articulación hija se convierte en
una cápsula a lo largo de ese hueso, unida al hueso más cercano por encima de ella mediante una
articulación de giro y torsión (swing-twist) con límites razonables, y las articulaciones
terminales (dedos, orejas, la punta de una cola) van con sus padres. Nada se mueve hasta
`ragdollActivate`: entonces la animación se detiene, se crean cuerpos donde están los
huesos, y desde ese momento la física mueve los nodos de las articulaciones, así que la
malla con skinning se desploma, golpea el suelo y queda en reposo con el resto del
mundo.
`ragdollApplyImpulse` empuja un hueso por el nombre de su articulación.
`ragdollSetStrength` impulsa cada articulación hacia la pose en la que se
activó el ragdoll, lo que convierte una caída flácida en un tambaleo o deja que un
personaje caído recoja las extremidades; no pone de pie un cuerpo, ya que
nada empuja contra el suelo. `ragdollDeactivate` devuelve el esqueleto
a la animación desde dondequiera que esté tirado, y un `animationPlay` con fundido
lo levanta suavemente desde ahí. Ajusta con `ragdollSetJoint` un hueso que se vea mal
antes de activarlo.
[source,lua]
----
fox = modelInstance(foxModel)
ragdollNew(fox) -- Receta a partir del skin; sigue animado.
function onCollision(a, b, x, y, z, speed)
if (a == hero or b == hero) and speed > 3 then
playerSetEnabled(hero, false)
ragdollActivate(fox) -- Ahora la física mueve el esqueleto.
ragdollApplyImpulse(fox, "b_Spine02_03", 40, 30, 0)
end
end
----
[#_soft_bodies]
==== Cuerpos blandos
La tela, la cuerda y la gelatina son mallas cuyos vértices se simulan.
`softNew(node, SOFT_CLOTH)` convierte la malla del nodo en tela, y sus vértices
(soldados por posición) en partículas unidas por restricciones de estiramiento, cizalla y
flexión, allí donde el nodo la ha puesto en el mundo; un
`meshPlane(width, depth, columns, rows)` subdividido es el punto de partida habitual. `softNew(node,
SOFT_BODY)` hace lo mismo con una malla cerrada, y
`softSetPressure` la infla: un globo, una gelatina. `softNew(node, SOFT_ROPE, x, y, z, segments,
radius)` tiende una cadena de partículas desde la posición del nodo hasta ese punto
y da al nodo una malla de tubo a su alrededor (su aspecto se fija con
`nodeSetMaterial`).
Los cuerpos blandos chocan con todo lo rígido, caen por la gravedad y se
dibujan con la malla de la que salieron, que el motor reescribe en cada fotograma.
`softPin` sujeta la partícula más cercana donde está o a un nodo (el borde de la
bandera al mástil, el extremo de la cuerda a la grúa); `softSetStiffness`,
`softSetMass` y `softSetDamping` ajustan el material. Un nodo cuya malla
comparten otros nodos los cambia también a ellos, así que da a un cuerpo blando una malla
propia.
[source,lua]
----
flag = nodeNew()
nodeSetMesh(flag, meshPlane(1.6, 1.0, 16, 10), red)
nodeSetPosition(flag, -4.2, 2.4, 0)
nodeSetRotation(flag, 90, 0, 0) -- Colgada en vertical.
softNew(flag, SOFT_CLOTH)
for _, y in ipairs({ 1.9, 2.15, 2.4, 2.65, 2.9 }) do
softPin(flag, -5, y, 0) -- A lo largo del mástil.
end
----
El coste crece con el número de partículas: una lámina de 20 por 20 son 441 partículas y
va bien en una Raspberry Pi 4; limita la tela a unos cientos de partículas y una cuerda
a una docena de segmentos. Las partículas chocan con cuerpos rígidos, no con caras, así que
una malla lo bastante fina (con las partículas más juntas que lo más pequeño que vaya a caer
encima) es lo que impide que una caja la atraviese. No hay autocolisión:
la tela puede atravesarse a sí misma cuando se arruga. Las mallas con skinning no pueden ser
blandas.
[#_seeing_the_physics]
==== Ver la física
Cuando una caja atraviesa el suelo o una articulación se dobla hacia el lado equivocado,
`physicsSetDebug` dibuja lo que la simulación contiene de verdad como líneas sobre
la escena en cada fotograma:
* `DEBUG_SHAPES`: los cuerpos en alambre, coloreados según su estado (dormido,
en movimiento, estático), con los jugadores en amarillo.
* `DEBUG_CONSTRAINTS`: las articulaciones, las articulaciones de ragdoll y las restricciones de
los vehículos, con sus límites.
* `DEBUG_CONTACTS`: una cruz roja donde se han encontrado cuerpos en este paso.
* `DEBUG_VELOCITIES`: una línea a lo largo de la velocidad de cada cuerpo en movimiento.
* `DEBUG_STATIC`: también las formas de los cuerpos estáticos, que si no se quedan
ocultas porque la malla de un nivel son muchas líneas.
Súmalas, o usa `DEBUG_ALL` para las cuatro primeras. `DEBUG_NONE`
lo desactiva, y no cuesta nada mientras está desactivado. Un script dibuja sus propias líneas
del mismo modo con `lineDraw` (un lanzamiento de rayo, un camino, una caja envolvente), un
fotograma cada vez, como el overlay, y `navDraw` muestra una malla de navegación.
[source,lua]
----
if debugging then
physicsSetDebug(DEBUG_SHAPES + DEBUG_CONTACTS)
end
function onOverlayUpdate()
local hit, hx, hy, hz = physicsRaycast(gunX, gunY, gunZ, aimX, aimY, aimZ, 60)
if hit then
lineDraw(gunX, gunY, gunZ, hx, hy, hz, 255, 255, 0)
end
return OVERLAY_NOT_UPDATED
end
----
[#_where_it_runs]
==== Dónde funciona
La física funciona en la CPU y no necesita GPU, pero la biblioteca está compilada para
SSE4.1 en x86 (de 2008 en adelante) y NEON en la Pi; una máquina por debajo de eso
recibe un error en el primer `bodyNew`. La simulación usa todos los núcleos menos
uno, así que una Pi de cuatro núcleos avanza los pasos en tres hilos; la traza del
programa dice cuántos.
[#particles]
=== Partículas
Chispas, humo, fuego, lluvia, polvo, magia: todo lo que está hecho de muchas cosas pequeñas y
de vida corta es trabajo para un *emisor*. Un emisor guarda una receta
(cuántas por segundo, cuánto viven, a qué velocidad y en qué dirección
salen, qué tira de ellas, cómo crecen, se desvanecen y giran) y una reserva de
partículas que gestiona según esa receta. El script fija la receta una vez y luego
arranca el emisor para un flujo constante (`emitterStart`), lo dispara en ráfaga para una
bocanada suelta (`emitterBurst`), o ambas cosas. El motor mueve cada partícula
viva en cada fotograma, y deja de moverlas mientras el juego está en pausa.
Una sola API de emisores sirve para el overlay y para la escena 3D; solo cambian las unidades
y el dibujo. Todo lo que se da como un mínimo y un máximo
(`emitterSetLife`, `emitterSetSpeed`, `emitterSetSize`) se elige al azar
entre los dos para cada partícula.
[#_2d_emitters]
==== Emisores 2D
`emitterNew()` sin argumento crea un emisor 2D. Vive en coordenadas del
overlay, con la Y hacia abajo, así que la gravedad de una fuente es Y positiva. Muévelo con
`emitterSetPosition`, y dibújalo desde `onOverlayUpdate` con
`emitterDraw`. El overlay en sí es una imagen que pinta la CPU, así que las partículas
no se pintan en él: las dibuja la GPU, o bien por encima de todo el overlay
(`PARTICLE_OVER`, por defecto) o bien por debajo de él y por encima del vídeo y de la escena 3D
(`PARTICLE_UNDER`, para humo detrás de la puntuación). Los emisores 2D funcionan en
todas las máquinas en las que funciona Singe, la Raspberry Pi 3 incluida, y combinan bien con
la física 2D: lee la posición del nodo de un cuerpo y pásasela a `emitterSetPosition`.
[source,lua]
----
sparks = emitterNew()
emitterSetBlend(sparks, PARTICLE_ADD)
emitterSetLife(sparks, 0.3, 0.7)
emitterSetSpeed(sparks, 80, 200)
emitterSetDirection(sparks, 0, -1) -- Hacia arriba en la pantalla.
emitterSetSpread(sparks, 70)
emitterSetGravity(sparks, 0, 500)
emitterSetSize(sparks, 5, 1)
emitterSetColor(sparks, 255, 255, 200, 255, 255, 120, 0, 0)
function onOverlayUpdate()
overlayClear()
emitterSetPosition(sparks, grinderX, grinderY)
emitterDraw(sparks)
return OVERLAY_UPDATED
end
----
[#_3d_emitters]
==== Emisores 3D
`emitterNew(node)` crea un emisor 3D unido a un nodo. Las partículas nacen
en la posición del nodo en el mundo (repartidas según `emitterSetRadius`), en unidades
del mundo, y se dibujan en la escena como billboards orientados a la cámara después de
todas las mallas: con prueba de profundidad contra la geometría, sin escribir nunca la
profundidad, sin iluminar salvo que se pida, sin proyectar ni recibir sombras. Por defecto una
partícula se queda donde nació una vez que deja el emisor (el humo sigue flotando donde se
hizo mientras la antorcha avanza); `emitterSetLocal` hace en cambio que toda la nube
siga al nodo. Borrar el nodo borra sus emisores.
[source,lua]
----
flame = emitterNew(torchTip)
emitterSetBlend(flame, PARTICLE_ADD)
emitterSetRate(flame, 140)
emitterSetLife(flame, 0.4, 0.9)
emitterSetSpeed(flame, 0.6, 1.4)
emitterSetDirection(flame, 0, 1, 0)
emitterSetSpread(flame, 15)
emitterSetSize(flame, 0.35, 0.05, 0.3)
emitterSetColor(flame, 255, 200, 90, 255, 255, 40, 0, 0)
emitterStart(flame)
----
[#_the_picture]
==== La imagen
Una partícula es un quad con textura. Sin textura es un disco suave que
proporciona el motor; `emitterSetTexture` le da en su lugar un sprite cargado (una
bocanada, un trazo de chispa, un copo de nieve), y para un sprite animado
`emitterSetFrames` elige un fotograma al azar por partícula, así que un solo atlas de
bocanadas da una nube variada. La mezcla es normal (`PARTICLE_ALPHA`) o aditiva
(`PARTICLE_ADD`, para todo lo que brilla). `emitterSetColor` da el
color y el alfa al nacer y al morir, y la partícula pasa de uno a otro
a lo largo de su vida; `emitterSetSize` hace lo mismo con su tamaño, con un tercer
valor para la velocidad a la que gira.
[#_trails_collisions_lit_and_soft_particles]
==== Estelas, colisiones, partículas iluminadas y suaves
`emitterSetTrail(emitter, length, width)` guarda las últimas posiciones de
cada partícula y dibuja una cinta a través de ellas, que se desvanece hacia la cola:
chispas, balas trazadoras, cometas, en 2D y en 3D por igual. `emitterSetCollide` hace que
las partículas reboten: en un suelo horizontal a la altura que indiques
(`COLLIDE_FLOOR`, gratis), o en el mundo físico mediante lanzamientos de rayos
(`COLLIDE_SCENE`, solo 3D, con un tope por fotograma para que un emisor grande reparta el
coste a lo largo de unos cuantos fotogramas).
Otras dos son solo para 3D. `emitterSetLit` sombrea cada partícula con las luces y el cielo de
la escena como si fuera una pequeña esfera orientada a la cámara, así que el humo toma el color
de un brasero y el polvo destella al sol (las sombras no se leen).
`emitterSetSoftness` desvanece una partícula a lo largo de una distancia allí donde se encuentra
con la geometría, para que el humo no se corte contra un suelo ni una fuente contra su estanque;
esto cuesta una pasada de profundidad extra de la escena por fotograma mientras algún emisor
lo use.
[source,lua]
----
smoke = emitterNew(brazier)
emitterSetTexture(smoke, spriteLoad(DIR .. "puff.png"))
emitterSetRate(smoke, 30)
emitterSetLife(smoke, 2, 4)
emitterSetSpeed(smoke, 0.3, 0.6)
emitterSetDirection(smoke, 0, 1, 0)
emitterSetSize(smoke, 0.3, 1.2, 0.2)
emitterSetColor(smoke, 90, 90, 90, 160, 60, 60, 60, 0)
emitterSetLit(smoke, true) -- Toma desde abajo el color del fuego.
emitterSetSoftness(smoke, 0.4) -- Sin borde duro donde toca el cuenco.
emitterStart(smoke)
embers = emitterNew(brazier)
emitterSetBlend(embers, PARTICLE_ADD)
emitterSetTrail(embers, 6, 0.02) -- Una estela corta detrás de cada brasa.
emitterSetCollide(embers, COLLIDE_FLOOR, 0.3, 0.8, 0) -- Rebota en el suelo en Y = 0.
emitterStart(embers)
----
[#_cost]
==== Coste
Las partículas son baratas de simular y cuestan lo que cubren en pantalla. Unos
miles de partículas pequeñas no son nada; un puñado de partículas grandes y aditivas sobre toda
la pantalla es lo que ralentiza una Raspberry Pi 4. La reserva tiene un tope que fija
`emitterSetMax` (1000 por defecto); cuando está llena, no nacen más hasta que algunas
mueren.
[#authoring]
=== Describir un juego en vez de escribirlo
Un juego se puede escribir como una *descripción* -- una tabla de capas, entidades y
reglas -- y compilarse en un juego de Singe corriente con Forge, la herramienta de
creación. Forge es un proyecto aparte con su propia descarga y su propio libro.
[[migrating]]
=== Migración desde Singe 2.10
Singe 3.00 pasó el handle del sprite al primer argumento de `spriteDraw`,
`spriteLoop`, `spriteQuality`, `spriteRotate`, `spriteRotateAndScale`,
`spriteScale` y `spriteSetFrame`, así que ahora todas las llamadas de sprites coinciden con
la familia `video*`. Para actualizar un juego, pasa al principio el último argumento de cada
una de esas llamadas:
[source,lua]
----
spriteDraw(x, y, cursor) -- 2.10
spriteDraw(cursor, x, y) -- 3.00
spriteRotate(angle, cursor) -- 2.10
spriteRotate(cursor, angle) -- 3.00
----
Un juego que no puedes editar puede optar en cambio por el orden antiguo. O bien define la
variable global antes de cargar el framework:
[source,lua]
----
SINGE_LEGACY_SPRITE_ARGS = true
dofile("Singe/Framework.singe")
----
o bien añade `LEGACY_SPRITE_ARGS = true` a su entrada de `games.dat`. Nada más
cambió de forma, pero algunos comportamientos sí:
* Los handles y los recuentos se devuelven como enteros de Lua; `videoIsPlaying` devuelve
un booleano; `mouseSetMode` no devuelve nada.
* Los errores dentro de los callbacks ahora terminan el juego con una traza de la pila en lugar
de imprimirse e ignorarse, igual que se han comportado siempre los errores de argumentos.
* Las teclas mantenidas pulsadas ya no se repiten en `MODE_NORMAL`.
* No se escribe nada junto al juego. Sin `--datadir`, la carpeta de
datos es `data/<game>/` dentro de la carpeta del juego, donde 2.x usaba la
propia carpeta del juego; las partidas guardadas, los ajustes y los récords van dentro de
`singeGetDataPath()`.
* `math.randomseed` acepta una semilla fraccionaria, como hacía con el Lua de 2.10;
se redondea hacia abajo antes de que la vea el generador de Lua 5.4. `os.clock` es tiempo
real desde que arrancó el motor, no tiempo de procesador.
* El motor deja el disco en el fotograma 1 antes de que se ejecute tu script;
`Framework.singe` ya no hace un salto al cargarse.
* `onMouseMoved` en el modo `MANY_MOUSE` pasa el movimiento real en su tercer y
cuarto argumentos, y `onControllerMoved` pasa el índice del mando
(de `0` a `3`) en lugar del ID de instancia de SDL.
* `mouseEnable` / `mouseDisable` y `singeEnablePauseKey` /
`singeDisablePauseKey` siguen funcionando, como alias de `mouseSetEnabled` y
`singeSetPauseKeyEnabled`.
* Ya no se da por supuesto un laserdisc. Una entrada de `games.dat` con `VIDEO` es un
juego de laserdisc, como antes; una sin él se ejecuta sobre un lienzo. En la
línea de comandos pasa `--disc` (o `--framefile`), ya que un vídeo encontrado junto
al script ya no implica un disco. Véase <<withoutadisc,Juegos sin disco>>.
* La tecla de pausa ahora pausa el juego entero, no solo el contenido multimedia: el script
queda congelado hasta que se vuelve a pulsar la tecla, y `SWITCH_PAUSE` solo se entrega
al script cuando la tecla se ha desactivado. Actúa al pulsar la tecla
y no al soltarla. Véase <<pausing,Pausa>>.
* `fontPrint` conserva las cadenas que renderiza, y `overlayClear` vuelve a rellenar solo
lo que se ha dibujado desde el último borrado, así que un HUD que se vuelve a imprimir en cada
fotograma es barato. Renderizar texto con `fontToSprite` por rendimiento está desaconsejado;
sigue funcionando, y sigue siendo la forma de obtener texto que se puede medir, escalar
o girar. Véase <<fonttosprite,fontToSprite>>.
[#_engine_constants]
=== Constantes del motor
El motor define estas variables globales antes de que se ejecute ningún script, así que están
disponibles tanto para `controls.cfg` como para `Framework.singe`:
[cols="2,3",options="header"]
|===
| Constante | Significado
| `SWITCH_UP` ... `SWITCH_RECORD` | Valores que se pasan a `onInputPressed` / `onInputReleased` en `MODE_NORMAL`. `SWITCH_MOUSE_DISCONNECT` lo genera el propio motor cuando se desconecta un ratón o una pistola de luz, una vez como pulsación y otra como liberación; ninguna tecla ni botón lo produce. `SWITCH_RECORD` es el interruptor de grabación, sobre el que actúa el propio motor, igual que con el interruptor de captura de pantalla.
| `FONT_QUALITY_SOLID`, `FONT_QUALITY_SHADED`, `FONT_QUALITY_BLENDED` | Argumentos para `fontQuality`.
| `MODE_NORMAL`, `MODE_FULL` | Argumentos para `keyboardSetMode`.
| `MOUSE_SINGLE`, `MOUSE_MANY` (también `SINGLE_MOUSE`, `MANY_MOUSE`) | Argumentos para `mouseSetMode`.
| `OVERLAY_NOT_UPDATED`, `OVERLAY_UPDATED` | Valores de retorno de `onOverlayUpdate`.
| `RENDER_PIXELATED`, `RENDER_SMOOTH` | Argumentos para `spriteQuality` / `videoQuality`.
| `SPRITE_ALPHA`, `SPRITE_ADD`, `SPRITE_MULTIPLY` | Mezcla para `spriteSetBlend`.
| `DISC_STOPPED`, `DISC_PLAYING`, `DISC_PAUSED`, `DISC_EJECTED` | Valores de retorno de `discGetState`.
| `LIGHT_DIRECTIONAL`, `LIGHT_POINT`, `LIGHT_SPOT` | Argumentos para `lightNew`.
| `BODY_STATIC`, `BODY_DYNAMIC`, `BODY_KINEMATIC` | Tipos de cuerpo para `bodyNew`.
| `SHAPE_BOX`, `SHAPE_SPHERE`, `SHAPE_CAPSULE`, `SHAPE_CYLINDER`, `SHAPE_HULL`, `SHAPE_MESH` | Formas de colisión para `bodyNew`.
| `JOINT_HINGE`, `JOINT_BALL`, `JOINT_SLIDER` | Tipos de articulación (las llamadas `joint*` los nombran directamente).
| `DEBUG_NONE`, `DEBUG_SHAPES`, `DEBUG_CONSTRAINTS`, `DEBUG_CONTACTS`, `DEBUG_VELOCITIES`, `DEBUG_STATIC`, `DEBUG_ALL` | Lo que dibuja `physicsSetDebug`; se suman entre sí.
| `PARTICLE_ALPHA`, `PARTICLE_ADD` | Mezcla para `emitterSetBlend`.
| `PARTICLE_OVER`, `PARTICLE_UNDER` | Dónde se dibujan las partículas 2D, para `emitterSetLayer`.
| `COLLIDE_NONE`, `COLLIDE_FLOOR`, `COLLIDE_SCENE` | Contra qué rebotan las partículas, para `emitterSetCollide`.
| `BILLBOARD_NONE`, `BILLBOARD_ALL`, `BILLBOARD_Y` | Cómo se orienta un nodo hacia la cámara, para `nodeSetBillboard`.
| `LAYER_SCENE`, `LAYER_FRONT` | Si un nodo se dibuja por debajo del overlay o delante de él, para `nodeSetLayer`.
| `VEHICLE_CAR`, `VEHICLE_MOTORCYCLE`, `VEHICLE_TANK`, `VEHICLE_BOAT` | Clases de vehículo para `vehicleNew`.
| `SOFT_CLOTH`, `SOFT_BODY`, `SOFT_ROPE` | Clases de cuerpo blando para `softNew`.
| `FILTER_LINEAR`, `FILTER_NEAREST` | Muestreo de texturas para `materialSetFilter`.
| `TONEMAP_NEUTRAL`, `TONEMAP_ACES`, `TONEMAP_NONE` | Curvas de tono para `sceneSetTonemap`.
| `SOUND_ERROR_INVALID`, `SOUND_REMOVE_HANDLE` | `-1`, lo que devuelve `soundPlay` cuando no hay ningún canal libre.
| `SINGE_VERSION_MAJOR`, `SINGE_VERSION_MINOR`, `SINGE_VERSION_STRING`, `SINGE_FRAMEWORK_VERSION` | La versión del motor, como enteros, como cadena (`"v3.00"`) y como el número que devuelve `singeVersion()`.
| `SINGE_DEAD_ZONE` | El `DEAD_ZONE` de `controls.cfg`, en unidades brutas de eje.
| `SINGE_TRIGGER_THRESHOLD` | El umbral de los gatillos analógicos en esas mismas unidades brutas; `0` cuando los gatillos usan `SINGE_DEAD_ZONE`, que es lo predeterminado.
| `SINGE_LEGACY_SPRITE_ARGS` | Verdadero cuando el juego ha pedido el orden de argumentos de sprites de 2.10.
| `SINGE_DISC` | Verdadero cuando el juego tiene un laserdisc; falso cuando el lienzo es el mundo.
| `SINGE_GAMEPAD_BASE`, `SINGE_GAMEPAD_STRIDE`, `SINGE_AXIS_STRIDE`, `SINGE_GAMEPAD_BUTTON_OFFSET`, `SINGE_MOUSE_BASE`, `SINGE_MOUSE_STRIDE`, `SINGE_MAX_CONTROLLERS`, `SINGE_MAX_MICE`, `SINGE_CONTROLLER_AXES`, `SINGE_CONTROLLER_BUTTONS` | Disposición de los códigos de entrada de los mandos y los ratones, y cuántos ejes y botones tiene un mando; `Framework.singe` construye con ellas las tablas `GAMEPAD_N` y `MOUSE_N`, y una pantalla de servicio recorre un mando con ellas.
| `SINGE_SCALE_MIN`, `SINGE_SCALE_MAX`, `SINGE_SHIFT_MIN`, `SINGE_SHIFT_MAX`, `SINGE_ROTATE_STEP` | Lo que aceptan `vldpSetScale`, `vldpSetShift` y `vldpSetRotate`. Limita los valores a estas y no a tus propias copias: un desplazamiento fuera de rango termina el script.
|===
`Framework.singe` añade las tablas `SCANCODE` y `MODIFIER` (los valores de teclas y
modificadores de SDL), de `GAMEPAD_0` a `GAMEPAD_3`, de `MOUSE_0` a `MOUSE_3`, los
índices `GAMEPAD_AXIS_*` para `controllerGetAxis`, y `DIR`.
[#_included_libraries]
=== Bibliotecas incluidas
Además de las bibliotecas estándar de Lua y de la API de Singe, las siguientes
bibliotecas también están disponibles para los programas de Singe sin necesidad de
instalar ningún software adicional:
[cols="1,3",options="header"]
|===
| Biblioteca | Uso
| Copas | Red asíncrona
| LuaFileSystem | Soporte ampliado del sistema de archivos
| json.lua | Codificación y decodificación de JavaScript Object Notation
| binaryheap | Estructura de datos de montículo binario
| timerwheel | Planificación eficiente de temporizadores
| LuaSec | Comunicación TLS / SSL
| LuaSocket | TCP y UDP
| LuaRS232 | Acceso al puerto serie RS232
| lsqlite3 | Bases de datos SQLite, `require("sqlite3")`
| md5 | `require("md5")` para `sum`, `hexsum`, `exor`, `crypt` y `decrypt`; la biblioteca de PUC-Rio que incluye Hypseus, cuya función `sumhexa` se llama aquí `hexsum`
| LuaBitOp | `require("bit")` para las operaciones a nivel de bits de Lua 5.1, que 5.4 tiene como operadores
| lrandom | `require("random")` para un generador Mersenne Twister, independiente de `math.random`
| Lua CJSON | `require("cjson")` para codificar y decodificar JSON, junto al módulo `json` de arriba
| LPeg | `require("lpeg")` para gramáticas de análisis sintáctico por expresiones
| middleclass | `require("middleclass")` para clases, si prefieres construir las entidades con ellas en lugar de con tablas
| lume | `require("lume")` para las cosas pequeñas que necesita todo juego: `lerp`, `clamp`, `round`, `shuffle`, `split`, `serialize`, `weightedchoice`
| inspect | `require("inspect")` para imprimir una tabla con una forma que una persona pueda leer, para cuando `debugPrint` es el depurador
| bump | `require("bump")` para mover cosas que no deben atravesarse unas a otras -- responde dónde acaba algo que se mueve, cosa que <<collide,las llamadas de colisión>> no hacen a propósito
| RmlUi | El modelo de objetos propio de la GUI, la variable global `rmlui` (véase <<gui,GUI>>)
|===
Su uso queda fuera del alcance de este documento. Las bibliotecas md5, LuaBitOp,
lrandom, Lua CJSON y LPeg están ahí porque Hypseus las expone, así que un juego
escrito para Hypseus encuentra los mismos módulos con los mismos nombres; nada en
Singe las necesita.
[#audiosync]
=== Sincronización del audio
Singe presenta el vídeo según el reloj del audio: un fotograma se muestra cuando se oye el
sonido que le corresponde. El motor mide su propia ruta de audio al arrancar. SDL no puede
informar de cuánto audio hay en cola entre el mezclador y el altavoz, así que Singe vacía el
dispositivo una vez, antes de que suene nada audible, y cuenta la ráfaga de búferes que usa
SDL para volver a llenarlo; esa ráfaga es la profundidad de la cola, y el reloj la resta.
Ejecuta con `--program` para ver la medida en la traza (`Audio device queue: ...`).
Lo que el motor no puede ver es todo lo que hay después del servidor de audio del sistema
operativo: un DAC, un receptor HDMI, un enlace Bluetooth. Estos añaden un retardo fijo que
varía según la máquina. Si las acciones en pantalla se adelantan visiblemente a su sonido,
pasa los milisegundos que faltan con `--audiodelay=MS`, pon `AUDIO_DELAY
= MS` en
`games.dat`, o deja que el juego lo fije desde un menú de servicio con
<<singesetaudiodelay,singeSetAudioDelay>>. Los valores positivos significan que el audio llega
tarde; el vídeo se retrasa para igualarlo. Un valor negativo cubre el caso raro de que el
audio llegue antes.
[#menucalibration]
==== Calibrar desde el menú
El menú incluido tiene una pantalla de calibración para ese retardo posterior. Pulsa la
tecla asignada a `INPUT_SERVICE` (la tecla `9` por defecto) en la lista de juegos y
elige Audio Delay en las herramientas de servicio (véase xref:Manual.adoc#servicetools[Las
herramientas de servicio]). La pantalla es una página del documento del menú: cubre el menú
de negro, muestra el retardo que se está probando y la cola del dispositivo que ha medido el
motor, reproduce un clic una vez por segundo y se pone en blanco durante un fotograma en cada
destello. Ajusta con izquierda y derecha (10 ms) y arriba y abajo (1 ms) hasta que el destello
y el clic coincidan, y luego pulsa el botón 1 para guardar. Start lo pone a cero
y el botón 2 (o de nuevo la tecla de servicio) cancela. El valor se guarda en
`machine.cfg`, en la raíz de datos, y el motor lo aplica a todos los juegos de esa
máquina, tanto si se lanzan desde el menú como desde la línea de comandos; se suma
a cualquier `AUDIO_DELAY` por juego.
Vuelve a calibrar después de cambiar de altavoces, auriculares o pantallas.
La pantalla funciona porque el clic y el audio del disco comparten un dispositivo mezclador
y una cola: el destello se programa a la cola medida más el valor candidato después del clic,
así que cuando los dos coinciden el candidato es exactamente el retardo que el motor no
puede ver. El retardo de la pantalla entra en la cuenta sin coste. Un juego puede
leer los valores con <<singegetaudiocalibration,singeGetAudioCalibration>> y
<<singegetaudiolatency,singeGetAudioLatency>>, u ofrecer su propia pantalla con
<<singesetaudiocalibration,singeSetAudioCalibration>>.
También importan dos propiedades del propio archivo de vídeo. El audio AAC lleva muestras de
cebado del codificador que el contenedor debe indicar a los decodificadores que se salten; los
archivos multiplexados sin esa información (un MP4 sin lista de edición, un archivo Matroska
sin retardo de códec) reproducen su audio unos 20 milisegundos tarde, cosa que `AUDIO_DELAY`
puede absorber. Y cuando el primer fotograma de vídeo tiene una marca de tiempo posterior a la
primera muestra de audio, Singe respeta la sincronización del contenedor, así que el audio
inicial no se pierde.
[#formats]
=== Formatos de vídeo, audio y contenedor
Singe decodifica el vídeo directamente con las bibliotecas de FFmpeg, usando el decodificador
por hardware de la plataforma cuando ofrece uno para el códec (VA-API o VDPAU en Linux, D3D11VA
en Windows, VideoToolbox en macOS y, en una placa ARM, los decodificadores V4L2 -- el de
memoria a memoria, que en una Pi 4 cubre H.264, el de la Request API sin estado que usa una
Pi 5 para HEVC, y el propio de Rockchip en un RK3588) y, si no, su propio decodificador por
software; la traza del programa indica cuál, y `--softwarevideo` fuerza el
software. Cualquier contenedor
y códec que el
FFmpeg incluido pueda demultiplexar y decodificar se reproduce: MP4, MKV, flujos de programa
MPEG, AVI, AV1 y los clásicos flujos elementales `.m2v` de Daphne con un archivo de audio
`.ogg` a juego al lado. Todas las pistas de audio del archivo están disponibles para
`discSetAudioTrack` / `videoSetAudioTrack`; todas las pistas deben compartir un mismo formato
de muestra, número de canales y frecuencia.
Un laserdisc guardaba campos entrelazados, y una copia que los conservó muestra el efecto
peine en cualquier pantalla progresiva. `--deinterlace` decide lo que pasa: `auto`, el valor
por defecto, aplica bwdif a los fotogramas que el archivo marca como entrelazados y deja todo
lo demás tal cual, así que un disco progresivo no paga nada; `on` es para un archivo cuyas
marcas están mal; `off` nunca toca la imagen. Sale una imagen por cada una que
entra -- un disco se direcciona por número de fotograma, así que un desentrelazador que
convirtiera cada fotograma en dos campos movería todos los fotogramas del índice.
La primera vez que se abre un vídeo, Singe lo indexa y guarda el índice junto a los demás
datos del juego (`<name>-<hash>.index`, con el hash hecho a partir del nombre completo del
vídeo para que dos vídeos con el mismo nombre base mantengan índices separados). Indexar es una
pasada por el archivo sin decodificar, unos segundos incluso para un disco de largometraje, y
vuelve a ocurrir si el vídeo cambia. Cuando se carga un vídeo, Singe
informa en la traza del programa (`--program`) del espaciado entre sus fotogramas clave, con un
aviso si los fotogramas clave están separados más de dos segundos, porque un salto
tiene que decodificar hacia delante desde el fotograma clave anterior. La decodificación ocurre
en un hilo aparte, así que un salto lento muestra el fotograma anterior un poco más de tiempo
en lugar de detener el juego.
Los segmentos de un framefile se abren a medida que el disco llega a ellos, no todos a la vez:
el que se está reproduciendo, el siguiente, y margen para que un salto se mueva entre dos. Un
framefile largo puede tener cientos de segmentos -- un juego publicado tiene 213 -- y
mantener un decodificador abierto para cada uno de ellos cuesta cientos de archivos abiertos
sin ganar nada. Un framefile que nombra un archivo que no existe sigue avisando al arrancar.
Para las imágenes de laserdisc las restricciones son la precisión de fotograma y la velocidad
de salto, no la compresión. H.264 en MP4 o MKV con un intervalo de fotogramas clave corto (uno
o dos segundos) salta rápido y se reproduce en todas las plataformas compatibles; los
intervalos de fotogramas clave largos hacen que `discSearch` sea visiblemente lento porque el
decodificador debe avanzar desde el fotograma clave anterior. La definición estándar (720x480 o
720x576) es el punto óptimo. Las fuentes de alta definición funcionan, pero cuestan
proporcionalmente más CPU en hardware de la clase de la Raspberry Pi, y el overlay tiene por
defecto la mitad de la resolución del vídeo, así que un vídeo sobredimensionado aporta poco.
Los efectos de sonido y la música los lee primero SDL_mixer, y FFmpeg lee todo lo que
este rechaza, así que entre los dos cubren:
* WAV, AIFF, AU y VOC para audio sin comprimir. Los archivos WAV cortos sin comprimir
dan la latencia más baja, y es lo que debería ser un disparo.
* Ogg Vorbis, Opus, MP3, FLAC y WavPack.
* AAC (`.m4a`, `.aac`), ALAC, AC-3, WMA, APE, TTA, Speex y AMR -- todo lo
demás que decodifica el FFmpeg incluido.
* Módulos de tracker mediante libxmp: MOD, S3M, XM e IT, y otros cincuenta y tantos,
entre ellos 669, AMF, MED, OKT, PTM, STM y ULT.
* Chiptunes mediante game-music-emu: NSF y NSFE, SPC, VGM, GBS, AY, GYM, HES,
KSS y SAP. La forma VGZ, comprimida con gzip, no se lee; descomprímela antes a VGM.
* MIDI, sintetizado a partir de una SoundFont. Véase <<midi,MIDI>>.
La línea `Audio:` de la cabecera de la traza (`--program` o `--trace`) enumera los
decodificadores que lleva realmente la compilación.
Las imágenes funcionan igual: primero SDL_image, y FFmpeg para el resto. Eso cubre
PNG, JPEG, WebP, GIF, BMP, TGA, PCX, PNM, LBM, QOI, XCF, XPM y SVG desde
SDL_image, y TIFF, OpenEXR, JPEG 2000, DDS, PSD, DPX, AVIF y HEIC desde
FFmpeg. Un GIF o WEBP animado se carga como una animación de sprite; véase
<<spriteload,`spriteLoad`>>, que también acepta un tamaño para rasterizar un SVG.
Tanto Radiance `.hdr` como OpenEXR funcionan como cielo (<<scenesetsky,`sceneSetSky`>>)
y conservan todo su rango.
Las fuentes las lee FreeType: TrueType, OpenType, WOFF, WOFF2, Type 1, BDF y
PCF, con glifos en color (tanto de mapa de bits como vectoriales) y conformación de texto
con HarfBuzz para el árabe, el hebreo y las escrituras índicas.
[#reference]
== Referencia de la API de Lua
Este capítulo documenta la API de Lua que expone el motor Singe -- cada
función a la que puede llamar el script de tu juego, y cada callback con el que Singe
llamará a tu código. Está dirigido a desarrolladores que escriben juegos nuevos o que portan
juegos con mucho vídeo desde otros motores.
[#conventions]
=== Convenciones
Cada entrada de la API que sigue tiene la misma estructura:
* *Firma(s)* en un bloque de código, con todas las formas aceptadas.
* *Descripción* de lo que hace la función y de sus efectos secundarios.
* *Parámetros* enumerados en línea, o en una tabla cuando hay más de dos.
* Línea *Devuelve*, que describe el valor que se devuelve a Lua (se omite cuando la función no devuelve nada).
* *Notas*, si hay trampas que merezca la pena señalar.
* *Desde* -- la versión de Singe en la que apareció la función por primera vez.
* *Véase también* -- funciones relacionadas que probablemente usarás en el mismo punto de llamada.
* *Ejemplo* -- un fragmento corto y realista con la forma del código de un juego de verdad.
Una entrada que empieza por *Extensión de Hypseus* está ahí para que un juego escrito para
Hypseus Singe funcione aquí sin cambios. Lo mismo vale para una forma de argumentos marcada
como propia de Hypseus en una función que, por lo demás, acepta las de Singe.
IMPORTANT: *No escribas juegos nuevos para la API de Hypseus.* Esas entradas existen
por compatibilidad, no como recomendación. Varias de ellas responden un valor fijo
porque lo que describen no existe en Singe, algunas se aceptan y se
ignoran, y otras llevan escalas y órdenes de argumentos heredados de otro
motor -- `videoGetVolume()` cuenta hasta `64` donde el resto de Singe cuenta hasta
`100`, y los nombres `spriteAnim` toman el sprite en último lugar donde todas las llamadas de
sprites de Singe lo toman primero. Cada entrada nombra el equivalente de Singe que hay que usar
en su lugar. El código nuevo que se ciñe a la API de Singe es más corto, coherente consigo
mismo, y no se llevará sorpresas cuando una respuesta de compatibilidad no cambie.
Unas cuantas reglas se aplican a toda la API:
* Los *colores* son de 8 bits por canal: rojo, verde, azul y opacidad son cada uno enteros de `0` a `255`.
* Las *coordenadas del overlay* están en la resolución actual del overlay (fijada con `overlaySetResolution`), no en píxeles de pantalla. La resolución predeterminada del overlay es la mitad de la resolución del vídeo en cada dimensión; llama a `overlaySetResolution` para cambiarla.
* Los *handles* que devuelven las funciones `*Load` (sprites, sonidos, fuentes, vídeos) son enteros opacos. Devuélvelos sin cambios a las demás funciones de ese espacio de nombres. No hagas cuentas con ellos.
* *Los argumentos no válidos abortan el script.* Si se llama a una función con un número de argumentos incorrecto, con tipos incorrectos o con un handle que ya se ha descargado, Singe termina el script en ejecución con un error -- no devuelve `nil` ni un código de fallo. Valida la entrada del usuario en tu script antes de pasársela a la API.
* *Hay que dibujar desde `onOverlayUpdate`.* Las llamadas a cualquier función `overlay*`, `spriteDraw`, `videoDraw` o `fontPrint` desde otros callbacks no se reflejarán en pantalla y pueden corromper el overlay. Esta es la regla que pilla a todo desarrollador nuevo de Singe.
[#animation]
=== Animación
Estas llamadas controlan las animaciones que lleva un modelo, instancia por instancia. Todas
toman el *nodo raíz* que devuelve `modelInstance`; otros nodos provocan un
error, salvo en `animationIsPlaying`, que responde `false`. Las animaciones se
numeran desde `1` en el orden en que las enumera `modelGetAnimations`, o se nombran como
las nombra el archivo. Un clip se reproduce en la capa base, y hasta cuatro más pueden
reproducirse en las capas `1` a `4` por encima de ella; las llamadas de tiempo, pausa,
reanudación y salto actúan solo sobre la capa base. El tiempo va en segundos, avanza con el
reloj como mucho una décima de segundo por fotograma, y se detiene mientras el juego está
en pausa. Se controlan la animación de nodos, el skinning y los
pesos de los morph targets. Véase el apartado Animación de
<<scenes3d,Escenas 3D>>.
[#animationgettime]
==== animationGetTime
[source,text]
----
seconds = animationGetTime(node)
----
El tiempo actual dentro del clip de la capa base, en reproducción o en pausa, en los segundos propios del clip (un `speed` de `2` lo hace avanzar el doble de rápido). Un clip en bucle vuelve a `0` al llegar al final; uno sin bucle se detiene en su duración. Si aún no se ha reproducido nada, da `0`. Los clips de las capas superiores no se informan. Un nodo que no es raíz de instancia termina el script.
*Devuelve:* Los segundos, como número.
*Desde:* 3.00.
*Véase también:* <<animationsettime,animationSetTime>>, <<animationisplaying,animationIsPlaying>>
.Ejemplo
[source,lua]
----
-- Reproduce el paso una vez cada vez que el ciclo de andar pasa por sus dos puntos de contacto.
function onOverlayUpdate()
local t = animationGetTime(hero)
if (lastStepTime < 0.25 and t >= 0.25) or (lastStepTime < 0.75 and t >= 0.75) then
soundPlay(footstep)
end
lastStepTime = t
return OVERLAY_NOT_UPDATED
end
----
[#animationisplaying]
==== animationIsPlaying
[source,text]
----
playing = animationIsPlaying(node)
----
Si el clip de la capa base está avanzando en esta instancia. Un clip sin bucle deja de reproducirse cuando llega a su final, y tanto `animationPause` como `animationStop` hacen que esto sea `false`; un clip en bucle sigue siendo `true` hasta que se detiene. A diferencia de las demás llamadas de aquí, un nodo que no es raíz de instancia no termina el script, sino que responde `false`.
*Devuelve:* Booleano.
*Desde:* 3.00.
*Véase también:* <<animationplay,animationPlay>>, <<animationpause,animationPause>>
.Ejemplo
[source,lua]
----
-- Vuelve a la espera cuando el ataque de una sola vez ha terminado.
function onOverlayUpdate()
if attacking and not animationIsPlaying(hero) then
attacking = false
animationPlay(hero, "Idle", true, 1, 0.2)
end
return OVERLAY_NOT_UPDATED
end
----
[#animationpause]
==== animationPause
[source,text]
----
animationPause(node)
----
Congela el clip de la capa base en su tiempo actual; los nodos mantienen esa pose hasta que `animationResume` lo continúa o se reproduce otro clip. Los clips de las capas superiores siguen en marcha. Pausar una instancia que no tiene nada en reproducción no hace daño. Un nodo que no es raíz de instancia termina el script.
*Desde:* 3.00.
*Véase también:* <<animationresume,animationResume>>, <<animationsettime,animationSetTime>>
.Ejemplo
[source,lua]
----
-- Una escena cinemática congela a la multitud mientras la cámara se mueve.
function beginCutscene()
for _, npc in ipairs(crowd) do
animationPause(npc)
end
cameraSet(cutsceneCamera)
end
----
[#animationplay]
==== animationPlay
[source,text]
----
animationPlay(node, nameOrIndex)
animationPlay(node, nameOrIndex, loop)
animationPlay(node, nameOrIndex, loop, speed)
animationPlay(node, nameOrIndex, loop, speed, fade)
----
Empieza desde el principio uno de los clips del modelo en la capa base de esta instancia, sustituyendo lo que se estuviera reproduciendo ahí; la pose se actualiza en el acto. Con un `fade`, el clip antiguo sigue en marcha mientras se desvanece durante ese número de segundos, y cuando no se estaba reproduciendo nada el fundido parte de la pose en la que están ahora los nodos (un ragdoll que se acaba de desactivar, una pose puesta por el script), y los nodos que el clip nuevo no anima vuelven suavemente a la pose de reposo del modelo. Un nodo que no es raíz de instancia, un nombre que el modelo no tiene o un número fuera del intervalo de `1` al número de clips termina el script.
*Parámetros:*
* `node` -- el nodo raíz de la instancia, de `modelInstance`.
* `nameOrIndex` -- un nombre de clip tal como lo guarda el archivo, o su número desde `1` en el orden de `modelGetAnimations`.
* `loop` -- booleano, por defecto `true`. Con `false` el clip se detiene en su último fotograma clave y `animationIsPlaying` pasa a ser `false`.
* `speed` -- velocidad de reproducción, por defecto `1`. `2` es el doble de rápido; `0.5`, la mitad.
* `fade` -- segundos de fundido cruzado desde lo que se estaba reproduciendo, por defecto `0` (un corte seco).
*Desde:* 3.00.
*Véase también:* <<modelgetanimations,modelGetAnimations>>, <<animationplaylayer,animationPlayLayer>>, <<animationstop,animationStop>>, <<animationsettime,animationSetTime>>
.Ejemplo
[source,lua]
----
-- Un guardia que espera quieto hasta que ve al jugador, y entonces corre.
guardModel = modelLoad(DIR .. "Models/Guard.glb")
guard = modelInstance(guardModel)
animationPlay(guard, "Idle")
function onSpotted()
animationPlay(guard, "Run", true, 1.3, 0.4)
end
----
[#animationplaylayer]
==== animationPlayLayer
[source,text]
----
animationPlayLayer(node, layer, nameOrIndex)
animationPlayLayer(node, layer, nameOrIndex, loop)
animationPlayLayer(node, layer, nameOrIndex, loop, speed)
animationPlayLayer(node, layer, nameOrIndex, loop, speed, fade)
----
Reproduce un clip en una de las cuatro capas por encima de la base, mezclado sobre lo que haya debajo según el peso de la capa (`animationSetLayerWeight`, por defecto `1`) y limitado a una parte del esqueleto cuando hay una máscara (`animationSetLayerMask`). Las capas más altas se mezclan sobre las más bajas; cada capa lleva su propio clip, tiempo y bucle, independientes de los de la base. `nameOrIndex`, `loop`, `speed` y `fade` significan lo mismo que en `animationPlay`, con una diferencia: un `fade` en una capa que estaba vacía sube el peso de la capa desde `0` en lugar de hacer un fundido cruzado desde una pose. Una capa fuera del intervalo de `1` a `4`, un nodo que no es raíz de instancia o un clip que el modelo no tiene termina el script.
*Parámetros:*
* `node` -- el nodo raíz de la instancia.
* `layer` -- de `1` a `4`.
* `nameOrIndex` -- un nombre o número de clip, como en `animationPlay`.
* `loop` -- por defecto `true`.
* `speed` -- por defecto `1`.
* `fade` -- segundos, por defecto `0`.
*Desde:* 3.00.
*Véase también:* <<animationplay,animationPlay>>, <<animationsetlayermask,animationSetLayerMask>>, <<animationsetlayerweight,animationSetLayerWeight>>, <<animationstop,animationStop>>
.Ejemplo
[source,lua]
----
-- Camina con todo el cuerpo y mira alrededor solo con la cabeza.
animationPlay(fox, "Walk", true)
animationPlayLayer(fox, 1, "Survey", true, 1, 0.5)
animationSetLayerMask(fox, 1, nodeFind("b_Neck_04", fox))
-- Más tarde, saluda con un brazo por encima de lo que estén haciendo las piernas.
function onGreet()
animationPlayLayer(fox, 2, "Wave", false, 1, 0.2)
end
----
[#animationresume]
==== animationResume
[source,text]
----
animationResume(node)
----
Continúa el clip de la capa base desde donde lo congeló `animationPause`, o desde donde lo haya movido entretanto `animationSetTime`. Reanudar un clip que ya se está reproduciendo no cambia nada. Una instancia sin clip en su capa base (nunca reproducido, o detenido) termina el script, igual que un nodo que no es raíz de instancia.
*Desde:* 3.00.
*Véase también:* <<animationpause,animationPause>>, <<animationsettime,animationSetTime>>
.Ejemplo
[source,lua]
----
function endCutscene()
for _, npc in ipairs(crowd) do
animationResume(npc)
end
cameraSet(gameCamera)
end
----
[#animationsetlayermask]
==== animationSetLayerMask
[source,text]
----
animationSetLayerMask(node, layer, maskNode)
animationSetLayerMask(node, layer)
----
Limita una capa a `maskNode` y a todo lo que hay debajo de él, de modo que el clip de esa capa solo mueve esa parte del modelo y el resto sigue a las capas de debajo. `maskNode` debe ser uno de los nodos de la propia instancia, encontrado con `nodeFind(name, node)`; la raíz de la instancia en sí no cuenta. Omitir `maskNode`, o pasar `nil`, quita el límite. La máscara se queda en la capa a lo largo de las llamadas posteriores a `animationPlayLayer`. Una capa fuera del intervalo de `1` a `4` termina el script, y también un nodo que no es raíz de instancia o un `maskNode` que no forma parte de esa instancia.
*Parámetros:*
* `node` -- el nodo raíz de la instancia.
* `layer` -- de `1` a `4`.
* `maskNode` -- una articulación o un nodo de la instancia; omitido o `nil` para el modelo entero.
*Desde:* 3.00.
*Véase también:* <<animationplaylayer,animationPlayLayer>>, <<nodefind,nodeFind>>, <<animationsetlayerweight,animationSetLayerWeight>>
.Ejemplo
[source,lua]
----
-- Apunta al objetivo con la parte superior del cuerpo mientras las piernas siguen andando.
spine = nodeFind("b_Spine02_03", hero)
animationSetLayerMask(hero, 1, spine)
animationPlayLayer(hero, 1, "AimForward", true)
function onHolster()
animationStop(hero, 1)
animationSetLayerMask(hero, 1)
end
----
[#animationsetlayerweight]
==== animationSetLayerWeight
[source,text]
----
animationSetLayerWeight(node, layer, weight)
----
Cuánto se ve el clip de una capa sobre lo que hay debajo, de `0` (nada) a `1` (del todo, el valor por defecto); los valores fuera de ese intervalo se recortan. El peso pertenece a la capa, no al clip, así que sobrevive a `animationPlayLayer` y `animationStop` en esa capa. Cámbialo un poco en cada fotograma para hacer aparecer o desaparecer una capa a mano, o deja que lo haga una vez un `fade` en `animationPlayLayer`. Una capa fuera del intervalo de `1` a `4` o un nodo que no es raíz de instancia termina el script.
*Desde:* 3.00.
*Véase también:* <<animationplaylayer,animationPlayLayer>>, <<animationsetlayermask,animationSetLayerMask>>
.Ejemplo
[source,lua]
----
-- La cojera empeora a medida que baja la salud.
function onHealthChanged(health)
local limp = 1 - health / maxHealth
animationSetLayerWeight(hero, 1, limp)
end
----
[#animationsettime]
==== animationSetTime
[source,text]
----
animationSetTime(node, seconds)
----
Lleva el clip de la capa base a un tiempo en sus propios segundos; los valores negativos se convierten en `0`. La pose se actualiza en el acto, se esté reproduciendo o no el clip, así que una instancia en pausa se puede llevar hasta un fotograma exacto, y una en reproducción continúa desde el nuevo tiempo. Cualquier fundido cruzado en curso en la capa base se corta. Una instancia sin clip en su capa base termina el script, igual que un nodo que no es raíz de instancia.
*Desde:* 3.00.
*Véase también:* <<animationgettime,animationGetTime>>, <<animationpause,animationPause>>, <<animationresume,animationResume>>
.Ejemplo
[source,lua]
----
-- Una puerta cuyo clip de apertura sigue a una palanca que arrastra el jugador.
animationPlay(door, "Open", false)
animationPause(door)
function onLeverMoved(fraction)
animationSetTime(door, fraction * doorClipLength)
end
----
[#animationstop]
==== animationStop
[source,text]
----
animationStop(node)
animationStop(node, layer)
----
Detiene y olvida todos los clips de la instancia, o solo el clip de una capa por encima de la base (de `1` a `4`); la capa base sola no se puede detener sin las capas que tiene encima. Los nodos conservan su última pose, de la que sale suavemente un `animationPlay` posterior con `fade`. Después de detenerlo todo, `animationResume` y `animationSetTime` terminan el script hasta que se vuelva a reproducir algo. Los pesos y las máscaras de las capas se conservan. Una capa fuera del intervalo de `1` a `4` o un nodo que no es raíz de instancia termina el script.
*Desde:* 3.00.
*Véase también:* <<animationplay,animationPlay>>, <<animationplaylayer,animationPlayLayer>>, <<animationpause,animationPause>>
.Ejemplo
[source,lua]
----
-- Pasa el cuerpo al ragdoll de la física: congela la pose y luego deja que caiga.
function onKilled()
animationStop(hero)
ragdollActivate(hero)
end
-- El saludo de la capa 2 ha terminado; el paso de debajo continúa.
function onWaveFinished()
animationStop(hero, 2)
end
----
[#bezelref]
=== Bezel
La ilustración de la recreativa y el marcador arcade que la acompaña. `mainBezelLoaded`
pregunta por la ilustración que ha cargado `--bezel`; la familia `scoreBezel` controla el
panel de puntuación, un documento RmlUi que el motor incluye y que un juego puede sustituir
por su propia copia, `scoreBezel.rml` junto a su script. Todas las llamadas de aquí son extensiones de Hypseus Singe,
que se mantienen para que sus juegos funcionen sin cambios, y todas se describen desde el lado
del jugador en xref:Manual.adoc#bezels[Bezels].
El panel empieza apagado, como en Hypseus, y solo un juego lo enciende. Los valores que se le
dan se recuerdan tanto si se muestra como si no, así que un juego puede fijarlos antes de
activar el panel. Cada indicador tiene el número de dígitos que tiene el marcador de
Hypseus -- seis para una puntuación, dos para los créditos, uno para un recuento de
vidas -- y un valor se muestra en ellos alineado a la derecha y con ceros a la izquierda; una
puntuación de más de seis dígitos muestra los seis últimos. También se entienden los valores
negativos de Hypseus: `-2` enciende un guion en cada dígito, `-4` una `H` y `-5` una `A`,
y `-1`, `-3` y cualquier otro número negativo no encienden nada.
[#mainbezelloaded]
==== mainBezelLoaded
[source,text]
----
loaded, id = mainBezelLoaded()
----
Extensión de Hypseus. Si `--bezel` ha cargado una ilustración de recreativa, y el identificador
del archivo que cargó. El identificador es el hash FNV-1a de 64 bits de los bytes del archivo
escrito como cadena decimal, que es lo que informa Hypseus, así que un juego que reconoce un
bezel concreto por su número reconoce aquí el mismo archivo.
Sin ninguna ilustración cargada, las respuestas son `false` y `"0"`.
*Devuelve:* booleano, y el identificador como cadena.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<setoverlayontop,setOverlayOnTop>>, xref:Manual.adoc#bezels[Bezels]
.Ejemplo
[source,lua]
----
-- Dibuja el marco de sprites solo cuando el jugador no tiene una ilustración propia.
local loaded, id = mainBezelLoaded()
if not loaded then
spriteDraw(frameSprite, 0, 0)
end
----
[#scorebezelclear]
==== scoreBezelClear
[source,text]
----
scoreBezelClear()
----
Extensión de Hypseus. Deja en blanco todos los indicadores del panel de puntuación: los
créditos, las dos puntuaciones y los dos recuentos de vidas no muestran nada en absoluto, y solo
quedan los dígitos tenues de detrás. Los valores no se olvidan, pero el siguiente
`scoreBezelCredits`, `scoreBezelScore` o `scoreBezelLives` vuelve a encender el panel con lo que
tenga en ese momento. Se puede llamar sin peligro con el panel apagado.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<scorebezelenable,scoreBezelEnable>>
[#scorebezelcredits]
==== scoreBezelCredits
[source,text]
----
scoreBezelCredits(credits)
----
Extensión de Hypseus. Fija el indicador de créditos, que tiene dos dígitos. También enciende un
panel que `scoreBezelClear` dejó en blanco.
*Parámetros:*
* `credits` -- entero; se muestran los dos últimos dígitos.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<scorebezelscore,scoreBezelScore>>
[#scorebezelenable]
==== scoreBezelEnable
[source,text]
----
scoreBezelEnable(enabled)
scoreBezelEnable(enabled, type)
----
Extensión de Hypseus. Muestra u oculta el panel de puntuación. El primer `true` lo construye
a partir del `scoreBezel.rml` propio del juego, junto a su script, si lo hay, o si no a partir
de `Singe/scoreBezel.rml`; ocultarlo y volver a mostrarlo después no cuesta nada y
conserva sus valores. Empieza oculto, que es también como viene en Hypseus, así que un juego
que nunca llama a esto se comporta exactamente como antes -- y debería preguntar a
<<scorebezelgetstate,`scoreBezelGetState`>> antes de dar por hecho que tiene uno.
El `type` de Hypseus elige entre sus cinco implementaciones de marcador. Singe tiene una, un
documento RmlUi, así que el argumento se comprueba y se ignora. Una máquina sin GPU no tiene
ninguna GUI; allí el panel no se puede construir, `scoreBezelGetState` sigue
respondiendo `false` y se imprime un aviso una sola vez. Un primer argumento que no sea booleano
aborta el script.
*Parámetros:*
* `enabled` -- booleano.
* `type` -- entero; se acepta y se ignora.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<scorebezelgetstate,scoreBezelGetState>>, xref:Manual.adoc#bezels[Bezels]
.Ejemplo
[source,lua]
----
-- El panel del motor cuando lo hay, sprites cuando no.
scoreBezelEnable(true, 0)
if scoreBezelGetState() then
scoreBezelTwinScoreOn(twoPlayer)
scoreBezelCredits(credits)
else
drawSpriteScoreboard()
end
----
[#scorebezelgetstate]
==== scoreBezelGetState
[source,text]
----
state = scoreBezelGetState()
----
Extensión de Hypseus. Si el panel de puntuación se está mostrando. Todos los juegos de Hypseus
preguntan esto antes de usar el panel y dibujan su propio marcador con sprites cuando la
respuesta es `false`, que es la alternativa correcta y no un fallo.
*Devuelve:* booleano.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<scorebezelenable,scoreBezelEnable>>
[#scorebezellives]
==== scoreBezelLives
[source,text]
----
scoreBezelLives(player, lives)
----
Extensión de Hypseus. Fija el indicador de vidas de un jugador, que tiene un dígito. `player` es
`1` o `2`; cualquier otro número se ignora, y también `2` hasta que
<<scorebezeltwinscoreon,`scoreBezelTwinScoreOn(true)`>> haya puesto en el panel los
indicadores del segundo jugador. También enciende un panel que `scoreBezelClear`
dejó en blanco.
*Parámetros:*
* `player` -- `1` o `2`.
* `lives` -- entero; se muestra el último dígito.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<scorebezelscore,scoreBezelScore>>
[#scorebezelscore]
==== scoreBezelScore
[source,text]
----
scoreBezelScore(player, score)
----
Extensión de Hypseus. Fija el indicador de puntuación de un jugador, que tiene seis dígitos.
`player` es `1` o `2`, con la misma regla para `2` que en `scoreBezelLives`. También enciende
un panel que `scoreBezelClear` dejó en blanco.
*Parámetros:*
* `player` -- `1` o `2`.
* `score` -- entero; se muestran los seis últimos dígitos.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<scorebezellives,scoreBezelLives>>
.Ejemplo
[source,lua]
----
function addScore(points)
score = score + points
if scoreBezelGetState() then
scoreBezelScore(1, score)
end
end
----
[#scorebezeltwinscoreon]
==== scoreBezelTwinScoreOn
[source,text]
----
scoreBezelTwinScoreOn(enabled)
----
Extensión de Hypseus. Pone en el panel los indicadores de puntuación y de vidas del segundo
jugador, o los vuelve a quitar y deja el sitio a los otros dos. Apagarlos también deja el panel
en blanco, exactamente como en Hypseus, así que fija después un valor si los indicadores del
primer jugador deben seguir encendidos. Mientras están apagados,
`scoreBezelScore(2, ...)` y `scoreBezelLives(2, ...)` no hacen nada. Un argumento que no sea
booleano aborta el script.
*Parámetros:*
* `enabled` -- booleano.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<scorebezelscore,scoreBezelScore>>
[#body]
=== Cuerpo
Un cuerpo da a un nodo de la escena un lugar en el mundo físico. Hay un cuerpo por nodo, al que se llega por el handle del nodo: un cuerpo estático se queda donde estaba el nodo cuando se creó el cuerpo, un cuerpo cinemático sigue a su nodo en cada paso y empuja sin que lo empujen, y un cuerpo dinámico lo mueve la simulación, y él mueve su nodo. Los tamaños, las posiciones y las velocidades van en unidades del mundo (metros en una escena 3D, píxeles del overlay en un mundo 2D), las masas en kilogramos y el tiempo en segundos. Todas las llamadas de aquí terminan el script cuando el nodo no tiene cuerpo; una llamada que solo tiene sentido para una clase de cuerpo (una fuerza sobre un cuerpo estático, por ejemplo) se ignora en las demás, salvo `bodySetMass`, que da un error. Véase <<physics,Física>> en Desarrollo de juegos para ver cómo encajan los cuerpos, los nodos y el fotograma.
[#bodynew]
==== bodyNew
[source,text]
----
bodyNew(node, type, shape)
bodyNew(node, type, shape, a)
bodyNew(node, type, shape, a, b)
bodyNew(node, type, shape, a, b, c)
----
Da al nodo un cuerpo del tipo indicado, colocado donde está el nodo en el mundo en este momento (con sus padres incluidos), con una forma de colisión de las medidas `a`, `b` y `c` en unidades del mundo y escalada por la escala del nodo en el mundo. Una segunda llamada sobre el mismo nodo sustituye a la primera, y un nodo que tenía un jugador lo pierde. La masa de un cuerpo dinámico sale del volumen de su forma con la densidad del agua, así que usa `bodySetMass` en todo lo que deba sentirse ligero. Cambiar más tarde la escala del nodo no cambia el tamaño del cuerpo. Termina el script con un tipo o una forma desconocidos, cuando la física no está disponible en esta máquina, cuando el mundo ya tiene `4096` cuerpos o cuando no se puede construir la forma (un `SHAPE_MESH` dinámico, o una envolvente sin malla bajo el nodo).
*Parámetros:*
* `type` -- `BODY_STATIC`, `BODY_DYNAMIC` o `BODY_KINEMATIC`.
* `shape` -- `SHAPE_BOX` (`a`, `b`, `c` son anchura, altura y profundidad), `SHAPE_SPHERE` (`a` es el radio), `SHAPE_CAPSULE` o `SHAPE_CYLINDER` (`a` es el radio, `b` la altura total a lo largo de Y, con los extremos incluidos), `SHAPE_HULL` (una envolvente convexa de la malla del nodo y de todo lo que hay bajo él, sin medidas) o `SHAPE_MESH` (los triángulos exactos, solo para cuerpos estáticos y cinemáticos; una malla de mapa de alturas se convierte en un campo de alturas).
* `a`, `b`, `c` -- las medidas que necesita la forma; las que faltan son `0`, y cualquier medida se eleva como mínimo a `0.001`.
*Notas:* Una esfera se escala por el eje de escala más grande del nodo, y el radio de una cápsula o de un cilindro por el mayor de X y Z. Una cápsula más corta que el doble de su radio es una esfera con una lámina en medio.
*Desde:* 3.00.
*Véase también:* <<bodysetmass,bodySetMass>>, <<bodydelete,bodyDelete>>, <<physicssetgravity,physicsSetGravity>>, <<playernew,playerNew>>
.Ejemplo
[source,lua]
----
-- Una caja ligera y un modelo de pato como envolvente, soltados sobre el suelo.
crate = nodeNew()
nodeSetMesh(crate, meshBox(0.6, 0.6, 0.6), wood)
nodeSetPosition(crate, -1, 1, 0)
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.6, 0.6, 0.6)
bodySetMass(crate, 5)
duck = modelInstance(modelLoad(DIR .. "Models/Duck.glb"))
nodeSetPosition(duck, 3, 2, 0)
bodyNew(duck, BODY_DYNAMIC, SHAPE_HULL)
----
[#bodyapplyforce]
==== bodyApplyForce
[source,text]
----
bodyApplyForce(node, fx, fy, fz)
bodyApplyForce(node, fx, fy, fz, px, py, pz)
----
Añade una fuerza, en newtons, a un cuerpo dinámico solo durante los pasos de este fotograma; llámala en cada fotograma para seguir empujando. Sin un punto, la fuerza actúa a través del centro de masas y solo desplaza el cuerpo; con un punto del mundo `(px, py, pz)` también lo hace girar alrededor de su centro. Una fuerza acelera según la fuerza dividida entre la masa, así que ajústala a `bodySetMass`. Los cuerpos estáticos y cinemáticos la ignoran.
*Parámetros:*
* `fx`, `fy`, `fz` -- la fuerza en newtons a lo largo de cada eje del mundo.
* `px`, `py`, `pz` -- punto del mundo opcional en el que actúa la fuerza; omítelo para el centro de masas.
*Desde:* 3.00.
*Véase también:* <<bodyapplyimpulse,bodyApplyImpulse>>, <<bodysetmass,bodySetMass>>
.Ejemplo
[source,lua]
----
-- Un ventilador empuja el globo por la habitación mientras el interruptor está encendido.
function onOverlayUpdate()
if fanOn then
local bx, by, bz = nodeGetPosition(balloon)
if bx < fanReach then
bodyApplyForce(balloon, 2.5, 0.4, 0)
end
end
return OVERLAY_NOT_UPDATED
end
----
[#bodyapplyimpulse]
==== bodyApplyImpulse
[source,text]
----
bodyApplyImpulse(node, ix, iy, iz)
bodyApplyImpulse(node, ix, iy, iz, px, py, pz)
----
Cambia de golpe la cantidad de movimiento de un cuerpo dinámico: un impulso de masa por velocidad añade esa velocidad. Sin un punto actúa a través del centro de masas; con un punto del mundo `(px, py, pz)` también hace girar el cuerpo, así que golpear una caja cerca de su parte superior la vuelca. Esta es la llamada para un golpe, una patada, una explosión o un lanzamiento, mientras que `bodyApplyForce` es para un empuje que dura. Los cuerpos estáticos y cinemáticos lo ignoran.
*Parámetros:*
* `ix`, `iy`, `iz` -- el impulso en kilogramos-metro por segundo a lo largo de cada eje del mundo.
* `px`, `py`, `pz` -- punto del mundo opcional en el que actúa el impulso; omítelo para el centro de masas.
*Desde:* 3.00.
*Véase también:* <<bodyapplyforce,bodyApplyForce>>, <<bodysetvelocity,bodySetVelocity>>, <<physicsraycast,physicsRaycast>>
.Ejemplo
[source,lua]
----
-- Lanza hacia arriba lo que haya bajo el ratón, en el punto donde se ha tocado.
local mx, my = mouseGetPosition(0)
local ox, oy, oz = sceneUnproject(mx, my, 0)
local fx, fy, fz = sceneUnproject(mx, my, 10)
local hit, hx, hy, hz = physicsRaycast(ox, oy, oz, fx - ox, fy - oy, fz - oz)
if hit and hit ~= floor then
bodyApplyImpulse(hit, 0, 3, 0, hx, hy, hz)
end
----
[#bodydelete]
==== bodyDelete
[source,text]
----
bodyDelete(node)
----
Quita el cuerpo del mundo; el nodo y su malla se quedan donde están. Todas las articulaciones unidas al cuerpo, y cualquier vehículo construido sobre él, se van con él. Borrar el nodo también quita su cuerpo, así que esto solo hace falta para conservar un nodo sin su cuerpo. Termina el script cuando el nodo no tiene cuerpo.
*Desde:* 3.00.
*Véase también:* <<bodynew,bodyNew>>, <<bodysetenabled,bodySetEnabled>>, <<jointdelete,jointDelete>>
.Ejemplo
[source,lua]
----
-- Una moneda recogida deja de ser sólida y sube flotando como un simple nodo.
function onTrigger(trigger, other, entered)
if trigger == coin and other == hero and entered then
bodyDelete(coin)
collected = collected + 1
coinRising = true
end
end
----
[#bodygetangularvelocity]
==== bodyGetAngularVelocity
[source,text]
----
x, y, z = bodyGetAngularVelocity(node)
----
A qué velocidad gira el cuerpo alrededor de cada eje del mundo, en radianes por segundo, según el último paso. Un cuerpo estático informa de cero. Útil para el tono del sonido de una rueda o para un contador de vueltas.
*Devuelve:* Tres números, radianes por segundo alrededor de X, Y y Z.
*Desde:* 3.00.
*Véase también:* <<bodysetangularvelocity,bodySetAngularVelocity>>, <<bodygetvelocity,bodyGetVelocity>>
.Ejemplo
[source,lua]
----
-- La ruleta se ha parado cuando apenas gira.
function onOverlayUpdate()
local _, spin = bodyGetAngularVelocity(wheel)
if spinning and math.abs(spin) < 0.05 then
spinning = false
announceWinner(pocketUnder(ball))
end
return OVERLAY_NOT_UPDATED
end
----
[#bodygetvelocity]
==== bodyGetVelocity
[source,text]
----
x, y, z = bodyGetVelocity(node)
----
A qué velocidad se mueve el cuerpo a lo largo de cada eje del mundo, en unidades del mundo por segundo, según el último paso. Un cuerpo estático informa de cero; un cuerpo cinemático informa de la velocidad que implicaba el movimiento de su nodo.
*Devuelve:* Tres números, unidades por segundo a lo largo de X, Y y Z.
*Desde:* 3.00.
*Véase también:* <<bodysetvelocity,bodySetVelocity>>, <<bodygetangularvelocity,bodyGetAngularVelocity>>
.Ejemplo
[source,lua]
----
-- Reproduce un silbido cuando la pelota se mueve de verdad.
function onOverlayUpdate()
local vx, vy, vz = bodyGetVelocity(ball)
local speed = math.sqrt(vx * vx + vy * vy + vz * vz)
if speed > 8 and not whooshing then
soundPlay(whoosh)
whooshing = true
elseif speed < 2 then
whooshing = false
end
return OVERLAY_NOT_UPDATED
end
----
[#bodyisresting]
==== bodyIsResting
[source,text]
----
resting = bodyIsResting(node)
----
Si la simulación ha dormido el cuerpo porque ha dejado de moverse. Los cuerpos estáticos y los cuerpos sacados del mundo con `bodySetEnabled` cuentan como en reposo. Un cuerpo en reposo se despierta solo cuando algo lo toca o cuando se le aplica una velocidad, una fuerza o un impulso.
*Devuelve:* Booleano.
*Desde:* 3.00.
*Véase también:* <<bodysetenabled,bodySetEnabled>>, <<bodysetvelocity,bodySetVelocity>>
.Ejemplo
[source,lua]
----
-- La ronda termina cuando todos los bolos se han asentado.
function onOverlayUpdate()
local settled = true
for _, pin in ipairs(pins) do
settled = settled and bodyIsResting(pin)
end
if settled then
scoreFrame()
end
return OVERLAY_NOT_UPDATED
end
----
[#bodysetangularvelocity]
==== bodySetAngularVelocity
[source,text]
----
bodySetAngularVelocity(node, x, y, z)
----
Fija el giro alrededor de cada eje del mundo, en radianes por segundo, y despierta el cuerpo. Los cuerpos estáticos lo ignoran. El giro de un cuerpo cinemático se sobrescribe a partir de su nodo en el siguiente paso, así que la llamada solo perdura en los cuerpos dinámicos.
*Desde:* 3.00.
*Véase también:* <<bodygetangularvelocity,bodyGetAngularVelocity>>, <<bodysetvelocity,bodySetVelocity>>
.Ejemplo
[source,lua]
----
-- Lanza la moneda: hacia arriba, y dando vueltas sobre sí misma.
function tossCoin()
nodeSetPosition(coin, 0, 1.2, 0)
bodySetVelocity(coin, 0, 4, 0)
bodySetAngularVelocity(coin, 25, 0, 0)
tossed = true
end
----
[#bodysetbounce]
==== bodySetBounce
[source,text]
----
bodySetBounce(node, bounce)
----
Fija la restitución del cuerpo, de `0` (se para en seco) a `1` (rebota con todo lo que traía); los valores fuera de ese intervalo se recortan. El valor por defecto es `0.1`. Los valores de los dos cuerpos que se tocan se combinan, así que una pelota viva sobre un suelo muerto sigue rebotando.
*Desde:* 3.00.
*Véase también:* <<bodysetfriction,bodySetFriction>>, <<bodynew,bodyNew>>
.Ejemplo
[source,lua]
----
-- Una pelota de goma que cae en la escena.
ball = nodeNew()
nodeSetMesh(ball, meshSphere(0.3, 32), blue)
nodeSetPosition(ball, 0, 3, 0)
bodyNew(ball, BODY_DYNAMIC, SHAPE_SPHERE, 0.3)
bodySetMass(ball, 0.5)
bodySetBounce(ball, 0.7)
----
[#bodysetbuoyancy]
==== bodySetBuoyancy
[source,text]
----
bodySetBuoyancy(node, factor)
----
Cómo flota el cuerpo cuando está dentro de un volumen de agua: `1` es neutro, más flota más alto, menos se hunde, y `0` es una piedra. El valor por defecto es `1.2`, que flota suavemente. El factor multiplica la densidad propia del agua dada con `bodySetWater`, y los valores negativos se recortan a `0`. El agua solo mueve los cuerpos dinámicos, pero el valor se puede fijar en cualquier cuerpo.
*Desde:* 3.00.
*Véase también:* <<bodysetwater,bodySetWater>>, <<bodysetcurrent,bodySetCurrent>>
.Ejemplo
[source,lua]
----
-- Una caja que flota alto, un ancla que se hunde, una pelota que cabecea.
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.8, 0.8, 0.8)
bodySetMass(crate, 20)
bodySetBuoyancy(crate, 1.6)
bodyNew(anchor, BODY_DYNAMIC, SHAPE_BOX, 0.5, 0.5, 0.5)
bodySetMass(anchor, 200)
bodySetBuoyancy(anchor, 0.3)
bodyNew(ball, BODY_DYNAMIC, SHAPE_SPHERE, 0.4)
bodySetBuoyancy(ball, 2.0)
----
[#bodysetcurrent]
==== bodySetCurrent
[source,text]
----
bodySetCurrent(node, x, y, z)
----
Fija el flujo dentro de un volumen de agua, en unidades del mundo por segundo a lo largo de cada eje del mundo; los cuerpos dinámicos que flotan en él derivan con el flujo, y un jugador que nada se deja llevar por él. Por defecto no hay flujo. Fíjalo en el nodo sobre el que se llamó a `bodySetWater`; en cualquier otro cuerpo se guarda y no hace nada.
*Desde:* 3.00.
*Véase también:* <<bodysetwater,bodySetWater>>, <<bodysetbuoyancy,bodySetBuoyancy>>
.Ejemplo
[source,lua]
----
-- Un río que arrastra corriente abajo todo lo que cae en él.
river = nodeNew()
nodeSetMesh(river, meshBox(6, 3, 12), water)
nodeSetPosition(river, 10, -1.5, 0)
bodyNew(river, BODY_STATIC, SHAPE_BOX, 6, 3, 12)
bodySetWater(river, 1.0, 0.8, 0.2)
bodySetCurrent(river, 0, 0, 2.5)
----
[#bodysetenabled]
==== bodySetEnabled
[source,text]
----
bodySetEnabled(node, enabled)
----
Saca el cuerpo del mundo con `false`: deja de chocar, deja de moverse, ya no mueve ni sigue a su nodo, y cuenta como en reposo. `true` lo vuelve a poner donde está, despierto. El cuerpo conserva su forma, su masa y sus ajustes, así que esto es más barato que borrarlo y volver a crearlo para algo que va y viene.
*Desde:* 3.00.
*Véase también:* <<bodyisresting,bodyIsResting>>, <<bodydelete,bodyDelete>>
.Ejemplo
[source,lua]
----
-- El puente levadizo solo es sólido mientras está bajado.
function lowerBridge()
animationPlay(bridge, "Lower", false)
bodySetEnabled(bridgeDeck, true)
end
function raiseBridge()
animationPlay(bridge, "Raise", false)
bodySetEnabled(bridgeDeck, false)
end
----
[#bodysetfriction]
==== bodySetFriction
[source,text]
----
bodySetFriction(node, friction)
----
Fija la fricción de la superficie: `0` es hielo, `0.5` (el valor por defecto) es lo normal, y los valores más altos agarran más. Los valores negativos se recortan a `0`. Los valores de los dos cuerpos que se tocan se combinan, así que un suelo resbaladizo necesita un valor bajo en el suelo, no en todo lo que camina sobre él.
*Desde:* 3.00.
*Véase también:* <<bodysetbounce,bodySetBounce>>, <<bodynew,bodyNew>>
.Ejemplo
[source,lua]
----
-- Un estanque helado en mitad del nivel.
pond = nodeNew()
nodeSetMesh(pond, meshBox(6, 0.1, 6), ice)
nodeSetPosition(pond, 8, -1.95, 0)
bodyNew(pond, BODY_STATIC, SHAPE_BOX, 6, 0.1, 6)
bodySetFriction(pond, 0.02)
----
[#bodysetmass]
==== bodySetMass
[source,text]
----
bodySetMass(node, kilograms)
----
Cambia la masa de un cuerpo dinámico y, con ella, su inercia. Hazlo con todo lo que deba sentirse ligero o pesado: el valor por defecto es el volumen de la forma con la densidad del agua, así que una caja del tamaño de una puerta pesa unos 140 kilogramos y un empujoncito no la mueve. Las fuerzas y los impulsos escalan con la masa, así que fija la masa antes de ajustarlos. Termina el script cuando el cuerpo no es dinámico o la masa no es positiva.
*Desde:* 3.00.
*Véase también:* <<bodynew,bodyNew>>, <<bodyapplyimpulse,bodyApplyImpulse>>, <<playersetpush,playerSetPush>>
.Ejemplo
[source,lua]
----
-- Una caja que el héroe puede empujar y un yunque que nadie puede mover.
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.6, 0.6, 0.6)
bodySetMass(crate, 5)
bodyNew(anvil, BODY_DYNAMIC, SHAPE_BOX, 0.5, 0.3, 0.3)
bodySetMass(anvil, 200)
----
[#bodysettrigger]
==== bodySetTrigger
[source,text]
----
bodySetTrigger(node, trigger)
----
Convierte el cuerpo en un activador con `true`: no empuja nada ni detiene nada, pero `onTrigger` informa de cada cuerpo o jugador que entra en él y sale de él. `false` lo vuelve a hacer sólido: lo que hubiera dentro se olvida sin una salida, mientras que un activador en el que esté el propio cuerpo sigue informando de él. Un activador suele ser una caja o una esfera estática colocada sobre una puerta, una línea de meta o una zona de peligro. Los rayos de `physicsRaycast` alcanzan a los activadores como a cualquier otro cuerpo.
*Notas:* Se informa de que un jugador está dentro de un activador cuando sus cajas envolventes se solapan, así que para los jugadores un activador redondo es uno cuadrado.
*Desde:* 3.00.
*Véase también:* <<ontrigger,onTrigger>>, <<bodynew,bodyNew>>, <<bodysetwater,bodySetWater>>
.Ejemplo
[source,lua]
----
-- Una línea de meta que cruza la pista.
line = nodeNew()
nodeSetPosition(line, 0, 0, -20)
bodyNew(line, BODY_STATIC, SHAPE_BOX, 6, 3, 0.5)
bodySetTrigger(line, true)
function onTrigger(trigger, other, entered)
if trigger == line and other == car and entered then
lapDone()
end
end
----
[#bodysetvelocity]
==== bodySetVelocity
[source,text]
----
bodySetVelocity(node, x, y, z)
----
Fija la velocidad del cuerpo a lo largo de cada eje del mundo, en unidades del mundo por segundo, y lo despierta. Esto sustituye la velocidad que tuviera el cuerpo, mientras que `bodyApplyImpulse` se la suma. Los cuerpos estáticos lo ignoran, y la velocidad de un cuerpo cinemático se sobrescribe a partir del movimiento de su nodo en el siguiente paso.
*Desde:* 3.00.
*Véase también:* <<bodygetvelocity,bodyGetVelocity>>, <<bodyapplyimpulse,bodyApplyImpulse>>, <<bodysetangularvelocity,bodySetAngularVelocity>>
.Ejemplo
[source,lua]
----
-- Saca la pelota hacia el lado que perdió el último punto.
function serve()
nodeSetPosition(ball, 0, 0.5, 0)
bodyNew(ball, BODY_DYNAMIC, SHAPE_SPHERE, 0.15)
bodySetMass(ball, 0.1)
local dir = (lastWinner == 1) and -1 or 1
bodySetVelocity(ball, 4 * dir, 2, (math.random() - 0.5) * 2)
end
----
[#bodysetwater]
==== bodySetWater
[source,text]
----
bodySetWater(node, density)
bodySetWater(node, density, linearDrag)
bodySetWater(node, density, linearDrag, angularDrag)
----
Llena de agua un cuerpo estático y lo convierte en activador: la parte superior de su forma es la superficie, y cada cuerpo dinámico que haya dentro flota, se hunde o se balancea por la flotabilidad respecto a esa superficie (cuenta la parte sumergida de la forma, así que una caja inclinada se endereza sola), frenado por las resistencias. Un jugador cuyo centro está por debajo de la superficie nada. Como el volumen es un activador, `onTrigger` se dispara al entrar y al salir. El agua no se dibuja: da al nodo un material con mezcla, o ninguno. Termina el script cuando el cuerpo no es estático.
*Parámetros:*
* `density` -- un multiplicador, `1` para el agua; el `bodySetBuoyancy` propio de cada cuerpo lo escala. Los valores negativos se recortan a `0`.
* `linearDrag` -- lo rápido que el agua frena el movimiento a través de ella; por defecto `0.5`.
* `angularDrag` -- lo rápido que frena los giros; por defecto `0.1`.
*Notas:* Los jugadores y los barcos ven como máximo ocho volúmenes de agua; los que pasen de ahí siguen haciendo flotar cuerpos, pero no se puede nadar en ellos.
*Desde:* 3.00.
*Véase también:* <<bodysetbuoyancy,bodySetBuoyancy>>, <<bodysetcurrent,bodySetCurrent>>, <<bodysettrigger,bodySetTrigger>>, <<playerisswimming,playerIsSwimming>>
.Ejemplo
[source,lua]
----
-- Una piscina hundida en la cubierta; su cara superior, en Y = 0, es la superficie.
pool = nodeNew()
nodeSetMesh(pool, meshBox(8, 3, 12), water)
nodeSetPosition(pool, 0, -1.5, 0)
bodyNew(pool, BODY_STATIC, SHAPE_BOX, 8, 3, 12)
bodySetWater(pool, 1.0, 0.6, 0.2)
function onTrigger(trigger, other, entered)
if trigger == pool and entered then
soundPlay(splash)
end
end
----
[#camera]
=== Cámara
La cámara es un nodo como cualquier otro: colócala con `nodeSetPosition`, apúntala con `nodeLookAt`, hazla hija de un objeto en movimiento para una vista de persecución, y desígnala con `cameraSet`. Mira hacia su propio -Z. Sin un nodo de cámara, la escena se ve desde `(0, 0, 5)` mirando al origen. La proyección se fija por separado y se aplica a cualquier nodo que sea la cámara: en perspectiva, con un campo de visión vertical de 60 grados de `0.1` a `1000` unidades por defecto, u ortográfica con `cameraSetOrthographic`. Las vistas renderizadas (`viewNew`) comparten la proyección, pero miran desde sus propios nodos. Véase <<scenes3d,Escenas 3D>>.
[#cameraset]
==== cameraSet
[source,text]
----
cameraSet(node)
cameraSet(-1)
----
Convierte un nodo en la cámara: la escena se dibuja desde su posición en el mundo mirando hacia su propio -Z, así que cuentan el movimiento y la rotación de sus padres. Sirve cualquier nodo, incluido uno que también lleve una malla o una luz, y cambiar de cámara es una sola llamada. `-1` restablece la vista por defecto desde `(0, 0, 5)` mirando al origen. Borrar el nodo de la cámara también vuelve a la vista por defecto. Termina el script con un handle de nodo que no sea válido.
*Desde:* 3.00.
*Véase también:* <<nodelookat,nodeLookAt>>, <<camerasetperspective,cameraSetPerspective>>, <<viewsetcamera,viewSetCamera>>, <<nodenew,nodeNew>>
.Ejemplo
[source,lua]
----
-- Una cámara de persecución detrás del coche del jugador, y una fija para la repetición.
chaseCamera = nodeNew(car)
nodeSetPosition(chaseCamera, 0, 2, 6)
nodeLookAt(chaseCamera, nodeGetWorldPosition(car))
cameraSet(chaseCamera)
function onReplayStart()
replayCamera = nodeNew()
nodeSetPosition(replayCamera, 30, 12, 30)
nodeLookAt(replayCamera, 0, 0, 0)
cameraSet(replayCamera)
end
----
[#camerasetorthographic]
==== cameraSetOrthographic
[source,text]
----
cameraSetOrthographic(height, near, far)
----
Cambia a una proyección paralela que muestra `height` unidades del mundo de arriba abajo, con la anchura según la relación de aspecto de la capa, de `near` a `far` delante de la cámara. La distancia no encoge las cosas, lo que va bien para diagramas, vistas isométricas y laterales, y escenas tipo HUD. El ajuste se aplica al nodo de cámara actual y a cualquiera posterior hasta que se llame a `cameraSetPerspective`. La sombra de una luz direccional usa un solo mapa con una cámara ortográfica, diga lo que diga `sceneSetShadowCascades`.
*Parámetros:*
* `height` -- la altura visible, en unidades del mundo.
* `near` -- la distancia más cercana que se dibuja, en unidades del mundo delante de la cámara.
* `far` -- la distancia más lejana que se dibuja.
*Desde:* 3.00.
*Véase también:* <<camerasetperspective,cameraSetPerspective>>, <<cameraset,cameraSet>>, <<scenesetshadowcascades,sceneSetShadowCascades>>
.Ejemplo
[source,lua]
----
-- Una vista isométrica del tablero: 12 unidades de alto, desde arriba y a un lado.
boardCamera = nodeNew()
nodeSetPosition(boardCamera, 20, 20, 20)
nodeLookAt(boardCamera, 0, 0, 0)
cameraSet(boardCamera)
cameraSetOrthographic(12, 1, 100)
----
[#camerasetperspective]
==== cameraSetPerspective
[source,text]
----
cameraSetPerspective(fov, near, far)
----
Cambia a la proyección en perspectiva habitual, la predeterminada. `fov` es el campo de visión vertical en grados (por defecto `60`); `near` y `far` (por defecto `0.1` y `1000`) limitan lo que se dibuja, en unidades del mundo delante de la cámara. La precisión de profundidad se reparte entre los dos, así que mantén `near` tan grande como permita la escena y `far` no más grande de lo necesario: de `0.05` a `100` para una habitación, de `0.1` a `400` para un paisaje. El ajuste se aplica al nodo de cámara actual y a cualquiera posterior, y a las vistas renderizadas.
*Parámetros:*
* `fov` -- campo de visión vertical en grados; un `70` amplio da sensación de velocidad, un `40` estrecho parece un teleobjetivo.
* `near` -- la distancia más cercana que se dibuja.
* `far` -- la distancia más lejana que se dibuja.
*Desde:* 3.00.
*Véase también:* <<camerasetorthographic,cameraSetOrthographic>>, <<cameraset,cameraSet>>, <<sceneproject,sceneProject>>
.Ejemplo
[source,lua]
----
-- Un objetivo más cerrado para la vista de cabina, el predeterminado para el exterior.
function setView(inside)
if inside then
cameraSet(cockpitCamera)
cameraSetPerspective(45, 0.05, 300)
else
cameraSet(chaseCamera)
cameraSetPerspective(60, 0.1, 400)
end
end
----
[#collide]
=== Colisión
Si dos cosas planas se tocan. Jolt lo resuelve en tres dimensiones para los cuerpos
(véase <<body,Cuerpo>>); estas funciones lo resuelven para el overlay, que es donde
viven las zonas de impacto de una pistola de luz y todo lo de un juego 2D.
Son funciones simples: sin handles, sin estado, nada que crear ni liberar y nada
que actualizar en cada fotograma. Las coordenadas son aquellas en las que ya estás
dibujando -- normalmente, píxeles del overlay. Un rectángulo es una esquina y un
tamaño, igual que lo recibe toda llamada de dibujo del motor. Tocarse justo en un
borde cuenta como tocarse, y un rectángulo sin anchura o sin altura no toca nada
en absoluto.
Ninguna de ellas mueve nada. Cuando no solo quieres saber *si* dos cosas se
encontraron, sino *dónde acaba lo que se mueve* -- que se deslice a lo largo de una
pared en lugar de quedarse clavado en ella -- usa `require("bump")` y deja que se
encargue él; viene incluido precisamente por eso.
[#collidecircles]
==== collideCircles
[source,text]
----
touching = collideCircles(x1, y1, radius1, x2, y2, radius2)
----
Indica si dos círculos se solapan. Es más barato de lo que parece: nada calcula una raíz cuadrada para responder a una pregunta de sí o no.
*Parámetros:*
* `x1`, `y1`, `radius1` -- números; el primer círculo.
* `x2`, `y2`, `radius2` -- números; el segundo.
*Devuelve:* `true` o `false`.
*Desde:* 3.00.
*Véase también:* <<colliderectcircle,collideRectCircle>>
[#collidepointcircle]
==== collidePointCircle
[source,text]
----
inside = collidePointCircle(pointX, pointY, x, y, radius)
----
Indica si un punto cae dentro de un círculo. El borde cuenta como dentro.
*Parámetros:*
* `pointX`, `pointY` -- números; el punto.
* `x`, `y`, `radius` -- números; el círculo.
*Devuelve:* `true` o `false`.
*Desde:* 3.00.
[#collidepointpolygon]
==== collidePointPolygon
[source,text]
----
inside = collidePointPolygon(pointX, pointY, points)
----
Indica si un punto cae dentro de un polígono de cualquier forma, incluidos los cóncavos. La tabla es una lista plana de `x`, `y`, `x`, `y` -- la forma que ya tiene una zona de impacto en la mayoría de los juegos -- y el sentido en que se recorren los vértices no importa. Con menos de tres vértices no contiene nada. Como máximo, 256 vértices.
*Parámetros:*
* `pointX`, `pointY` -- números; el punto.
* `points` -- tabla; un número par de números, x e y alternados.
*Devuelve:* `true` o `false`.
*Desde:* 3.00.
.Ejemplo
[source,lua]
----
-- ¿Ha dado el disparo en el dragón, cuyo contorno no es un rectángulo?
local dragon = { 120,40, 200,60, 210,140, 150,180, 100,120 }
function onInputPressed(key)
if key == SWITCH_BUTTON1 then
local x, y = mouseGetPosition(0)
if collidePointPolygon(x, y, dragon) then
hit()
end
end
end
----
[#collidepointrect]
==== collidePointRect
[source,text]
----
inside = collidePointRect(pointX, pointY, x, y, width, height)
----
Indica si un punto cae dentro de un rectángulo. El borde cuenta como dentro.
*Parámetros:*
* `pointX`, `pointY` -- números; el punto.
* `x`, `y`, `width`, `height` -- números; el rectángulo.
*Devuelve:* `true` o `false`.
*Desde:* 3.00.
.Ejemplo
[source,lua]
----
-- Una zona de impacto del tamaño del sprite al que pertenece.
local x, y = mouseGetPosition(0)
if collidePointRect(x, y, enemyX, enemyY, spriteGetWidth(enemy), spriteGetHeight(enemy)) then
kill(enemy)
end
----
[#colliderectcircle]
==== collideRectCircle
[source,text]
----
touching = collideRectCircle(x, y, width, height, circleX, circleY, radius)
----
Indica si un rectángulo y un círculo se solapan, buscando el punto del rectángulo más cercano al centro del círculo.
*Parámetros:*
* `x`, `y`, `width`, `height` -- números; el rectángulo.
* `circleX`, `circleY`, `radius` -- números; el círculo.
*Devuelve:* `true` o `false`.
*Desde:* 3.00.
[#colliderects]
==== collideRects
[source,text]
----
touching = collideRects(x1, y1, width1, height1, x2, y2, width2, height2)
----
Indica si dos rectángulos se solapan. La que más vas a usar.
*Parámetros:*
* `x1`, `y1`, `width1`, `height1` -- números; el primer rectángulo.
* `x2`, `y2`, `width2`, `height2` -- números; el segundo.
*Devuelve:* `true` o `false`.
*Desde:* 3.00.
*Véase también:* <<collidepointrect,collidePointRect>>
[#collidesegments]
==== collideSegments
[source,text]
----
crossing = collideSegments(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2)
----
Indica si dos segmentos se cruzan: un disparo a lo largo de una trayectoria contra una pared, o el paso que quiere dar algo que se mueve contra el borde que no debe cruzar. Dos segmentos superpuestos uno a lo largo del otro responden `false`, porque entonces «¿dónde se cruzan?» no tiene una única respuesta.
*Parámetros:*
* `ax1`, `ay1`, `ax2`, `ay2` -- números; los extremos del primer segmento.
* `bx1`, `by1`, `bx2`, `by2` -- números; los del segundo.
*Devuelve:* `true` o `false`.
*Desde:* 3.00.
[#color]
=== Color
Los colores de primer plano y de fondo son globales del motor y no argumentos de cada llamada: se fijan con `colorForeground` y `colorBackground`, y todas las llamadas de dibujo posteriores los leen hasta que cambian. El color de primer plano lo usan las primitivas de formas `overlay*` y el texto TrueType de `fontPrint` y `fontToSprite`; el color de fondo lo usan `overlayClear` y el texto `FONT_QUALITY_SHADED`. `overlayPrint` no usa ninguno de los dos. Los canales son enteros de `0` a `255`, limitados a ese rango, y ambos colores aceptan un alfa opcional. Consulta las secciones <<overlay,Overlay>> y <<font,Fuente>> para ver las llamadas que los usan.
[#colorbackground]
==== colorBackground
[source,text]
----
colorBackground(r, g, b)
colorBackground(r, g, b, a)
----
Fija el color de fondo: aquel con el que `overlayClear` rellena el overlay y el que el texto `FONT_QUALITY_SHADED` de `fontPrint` y `fontToSprite` pinta detrás de sus glifos. Al arrancar es negro transparente, y el alfa vale `0` por defecto cuando se omite, así que `colorBackground(0, 0, 0)` seguido de `overlayClear()` vuelve a hacer invisible el overlay. Un alfa parcial sobre negro oscurece el vídeo; `255` lo oculta. Los valores de canal fuera de rango se limitan en lugar de rechazarse.
*Parámetros:*
* `r`, `g`, `b` -- rojo, verde y azul, de `0` a `255`.
* `a` -- opacidad, de `0` (transparente) a `255` (opaco); por defecto `0`.
*Desde:* 1.x
*Véase también:* <<colorforeground,colorForeground>>, <<overlayclear,overlayClear>>, <<fontquality,fontQuality>>
.Ejemplo
[source,lua]
----
-- Negro sólido detrás de la pantalla de calibración, transparente durante la partida.
function onOverlayUpdate()
if calibrating then
colorBackground(0, 0, 0, 255)
else
colorBackground(0, 0, 0, 0)
end
overlayClear()
drawHud()
return OVERLAY_UPDATED
end
----
[#colorforeground]
==== colorForeground
[source,text]
----
colorForeground(r, g, b)
colorForeground(r, g, b, a)
----
Fija el color de primer plano que usan `overlayBox`, `overlayCircle`, `overlayEllipse`, `overlayLine` y `overlayPlot`, y el color del texto de `fontPrint` y `fontToSprite`. Al arrancar es blanco opaco, y el alfa vale `255` por defecto cuando se omite, al contrario que en `colorBackground`. Los sprites y `overlayPrint` se dibujan con sus propios píxeles y no se tiñen. Los valores de canal fuera de rango se limitan en lugar de rechazarse.
*Parámetros:*
* `r`, `g`, `b` -- rojo, verde y azul, de `0` a `255`.
* `a` -- opacidad, de `0` (transparente) a `255` (opaco); por defecto `255`.
*Desde:* 1.x
*Véase también:* <<colorbackground,colorBackground>>, <<overlaybox,overlayBox>>, <<fontprint,fontPrint>>, <<fonttosprite,fontToSprite>>
.Ejemplo
[source,lua]
----
-- La puntuación en blanco y el aviso de última vida en rojo, ambos con la misma fuente.
function onOverlayUpdate()
overlayClear()
fontSelect(hudFont)
colorForeground(255, 255, 255)
fontPrint(8, 8, "Score: " .. score)
if lives == 1 then
colorForeground(255, 32, 32)
fontPrint(8, 32, "LAST LIFE")
end
return OVERLAY_UPDATED
end
----
[#commandlineref]
=== Línea de comandos
Tres llamadas informan de números que el usuario eligió al arrancar Singe. Existen
para que un juego pueda ajustarse desde el lanzador en lugar de editando el juego.
Las tres son extensiones de Hypseus Singe, y Singe no hace nada con los valores por
sí mismo: el juego decide qué significan. Las opciones se describen en
xref:Manual.adoc#commandlineoptions[Opciones de línea de comandos], y también pueden
fijarse en un archivo de ajustes (véase xref:Manual.adoc#settings[El archivo de ajustes]).
Ninguna de las tres cambia durante una ejecución, así que lee cada una una sola vez
y guárdala.
[#getfvalue]
==== getFValue
[source,text]
----
value = getFValue()
----
Extensión de Hypseus. Devuelve el número dado con `--fvalue`, o `0` si no se usó
la opción. Es un canal de un solo número desde quien arrancó Singe hasta el juego,
pensado para un valor que un front-end quiere variar de un lanzamiento a otro --
una dificultad, un número inicial de créditos, un índice de menú. El motor solo lo
transporta.
El valor va de `0` a `100000` y conserva tres decimales, que es lo que conserva
Hypseus, así que un juego que compare el número ve el mismo que veía allí.
*Devuelve:* número.
*Desde:* 3.00 (Hypseus)
*Véase también:* <<ratiogetx,ratioGetX>>, <<ratiogety,ratioGetY>>
.Ejemplo
[source,lua]
----
-- El lanzador pasa --fvalue 2 para la recreativa difícil.
local difficulty = getFValue()
if difficulty == 0 then
difficulty = 1
end
----
[#ratiogetx]
==== ratioGetX
[source,text]
----
factor = ratioGetX()
----
Extensión de Hypseus. Devuelve el número dado con `--xratio`, o `0` si no se usó
la opción. Los juegos de pistola de luz escritos para Hypseus lo leen y estiran con
él sus propias coordenadas de pistola, para una recreativa cuya pantalla no tiene
la forma con la que se masterizó el vídeo -- `1.33` lleva un panel 16:9 a imágenes
4:3. Singe no lo aplica a nada; el juego hace las cuentas, exactamente como en
Hypseus, y por eso todo juego que lo lee trata `0` y cualquier valor por debajo de
`1` como `1`.
El valor va de `0` a `100` y conserva dos decimales, como lo conserva Hypseus.
*Devuelve:* número.
*Desde:* 3.00 (Hypseus)
*Véase también:* <<ratiogety,ratioGetY>>, <<getfvalue,getFValue>>, <<mousegetposition,mouseGetPosition>>
.Ejemplo
[source,lua]
----
-- La forma de las coordenadas de pistola que quiere esta recreativa.
local ratioX = ratioGetX()
if ratioX <= 1 then
ratioX = 1
end
local offsetX = (ratioX - 1) * (overlayGetWidth() / 2)
----
[#ratiogety]
==== ratioGetY
[source,text]
----
factor = ratioGetY()
----
Extensión de Hypseus. La mitad vertical de la pareja: el número dado con
`--yratio`, o `0` si no se usó la opción. Véase <<ratiogetx,ratioGetX>>.
*Devuelve:* número.
*Desde:* 3.00 (Hypseus)
*Véase también:* <<ratiogetx,ratioGetX>>, <<getfvalue,getFValue>>
.Ejemplo
[source,lua]
----
local ratioY = ratioGetY()
if ratioY <= 1 then
ratioY = 1
end
----
[#controller]
=== Mando
Singe admite hasta cuatro mandos, con índices de `0` a `3`, que se abren automáticamente al arrancar y cada vez que se conecta o se retira uno; no existe `controllerLoad`. Los ejes analógicos se notifican mediante `onControllerMoved` (véase <<enginecallbacks,Callbacks del motor>>) y se consultan con `controllerGetAxis`, en el rango en bruto de SDL, de `-32768` a `32767`. Los botones digitales, y cada dirección de cada eje, son interruptores como las teclas: llegan a `onInputPressed` y `onInputReleased` cuando `controls.cfg` asigna un código de las tablas `GAMEPAD_0` a `GAMEPAD_3` (por ejemplo `INPUT_ACTION_1 = { GAMEPAD_0.BUTTON_A }`), y `controllerGetButton` consulta el estado actual de un botón. `SINGE_DEAD_ZONE`, de la entrada `DEAD_ZONE` de `controls.cfg`, es el umbral que usa el propio motor antes de que una dirección de un eje cuente como interruptor pulsado; trata el movimiento del eje por debajo de él como ruido. A cada eje por separado se le puede dar un umbral distinto con `DEAD_ZONES`, y a los gatillos analógicos un umbral propio con `TRIGGER_THRESHOLD` (que se lee como `SINGE_TRIGGER_THRESHOLD`); véase xref:Manual.adoc#deadzones[Zonas muertas y gatillos]. Un dispositivo que SDL no reconoce como mando se abre igualmente como tal, mediante una asignación que Singe escribe para él, de modo que una placa encoder de recreativa o un joystick de vuelo aparece en estas ranuras como cualquier mando -- véase xref:Manual.adoc#devices[Todo dispositivo es un mando].
[#controllerdorumble]
==== controllerDoRumble
[source,text]
----
controllerDoRumble(strength, duration)
controllerDoRumble(controller, strength, duration)
----
Extensión de Hypseus. Hace vibrar un mando con ambos motores a una de cuatro intensidades durante una de cuatro duraciones. La forma de dos argumentos se dirige al mando `0`. Una ranura sin mando no hace nada (Hypseus aborta ahí, así que sus juegos se protegen con `controllerIsValid`); una intensidad o una duración fuera de `1` a `4`, o un índice fuera de `0` a `3`, aborta el script. Un mando sin vibración ignora la llamada.
*Parámetros:*
* `controller` -- índice del mando, de `0` a `3`.
* `strength` -- de `1` a `4`, de un cuarto a toda la intensidad.
* `duration` -- de `1` a `4`, que dan 150, 300, 600 o 1200 ms.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<controllerisvalid,controllerIsValid>>, <<controllergetbutton,controllerGetButton>>
.Ejemplo
[source,lua]
----
-- Un golpe corto en cada impacto y uno largo a toda intensidad cuando el jugador muere.
function onHit()
if controllerIsValid(0) then
controllerDoRumble(0, 2, 1)
end
end
function onPlayerDied()
if controllerIsValid(0) then
controllerDoRumble(0, 4, 4)
end
end
----
[#controllergetaxis]
==== controllerGetAxis
[source,text]
----
value = controllerGetAxis(controller, axis)
----
Devuelve la posición de un eje analógico tal como la vio el motor por última vez. El valor se actualiza con cada evento de eje, incluido el movimiento dentro de la zona muerta, y es `0` para un mando que no está conectado o un eje que nunca se ha movido. Los sticks van de `-32768` a `32767`; los gatillos reposan en `0` y llegan a `32767` cuando se pulsan a fondo. Un índice fuera de `0` a `3`, o un eje fuera de `0` a `5`, aborta el script.
*Parámetros:*
* `controller` -- índice del mando, de `0` a `3`.
* `axis` -- `GAMEPAD_AXIS_LEFT_X` (`0`), `GAMEPAD_AXIS_LEFT_Y` (`1`), `GAMEPAD_AXIS_RIGHT_X` (`2`), `GAMEPAD_AXIS_RIGHT_Y` (`3`), `GAMEPAD_AXIS_LEFT_TRIGGER` (`4`) o `GAMEPAD_AXIS_RIGHT_TRIGGER` (`5`).
*Devuelve:* entero, de `-32768` a `32767`.
*Desde:* 2.00
*Véase también:* <<controllergetbutton,controllerGetButton>>, <<oncontrollermoved,onControllerMoved>>
.Ejemplo
[source,lua]
----
-- Dirección con el stick izquierdo, acelerador con el gatillo derecho.
function onOverlayUpdate()
local steer = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_X)
local throttle = controllerGetAxis(0, GAMEPAD_AXIS_RIGHT_TRIGGER)
if math.abs(steer) > SINGE_DEAD_ZONE then
heading = heading + (steer / 32768) * turnRate
end
speed = speed + (throttle / 32767) * acceleration
drawHud()
return OVERLAY_UPDATED
end
----
[#controllergetbutton]
==== controllerGetButton
[source,text]
----
pressed = controllerGetButton(controller, button)
----
Devuelve si un botón está pulsado en este momento, leído directamente del mando y no de la cola de eventos. Úsalo para mantener pulsado y cargar, y para casos parecidos en los que `onInputPressed` y `onInputReleased` resultan incómodos. Una ranura sin mando devuelve `false`. El código del botón debe venir de la tabla `GAMEPAD_N` del mismo índice de mando; un código de la tabla de otro mando, o un código de eje, cae fuera del rango de botones y aborta el script, igual que un índice fuera de `0` a `3`.
*Parámetros:*
* `controller` -- índice del mando, de `0` a `3`.
* `button` -- un valor `BUTTON_*` o `DPAD_*` de la tabla correspondiente, como `GAMEPAD_0.BUTTON_A.value`.
*Devuelve:* booleano.
*Desde:* 2.10
*Véase también:* <<controllergetaxis,controllerGetAxis>>, <<keyboardisdown,keyboardIsDown>>, <<oninputpressedoninputreleased,onInputPressed>>
.Ejemplo
[source,lua]
----
-- Carga mientras A está pulsado y dispara al soltarlo.
function onOverlayUpdate()
if controllerGetButton(0, GAMEPAD_0.BUTTON_A.value) then
charge = math.min(charge + 1, chargeMax)
elseif charge > 0 then
fireShot(charge)
charge = 0
end
drawChargeBar(charge)
return OVERLAY_UPDATED
end
----
[#controllergetname]
==== controllerGetName
[source,text]
----
name = controllerGetName(controller)
----
El nombre del dispositivo de una ranura de mando, tal como lo notifica SDL, o `nil` si la ranura está vacía. Un dispositivo que SDL no reconoció notifica su propio nombre mediante la asignación que Singe escribió para él, así que una pantalla de servicio puede listar una placa encoder de recreativa por su nombre con la misma facilidad que un mando de marca. Un índice fuera de `0` a `3` aborta el script.
*Parámetros:*
* `controller` -- índice del mando, de `0` a `3`.
*Devuelve:* cadena, o `nil`.
*Desde:* 3.00
*Véase también:* <<controllerisvalid,controllerIsValid>>, <<mousegetname,mouseGetName>>
.Ejemplo
[source,lua]
----
-- Una pantalla de servicio que lista lo que está conectado.
for slot = 0, 3 do
if controllerIsValid(slot) then
fontPrint(20, 20 + slot * 18, string.format("Pad %d: %s", slot + 1, controllerGetName(slot)))
else
fontPrint(20, 20 + slot * 18, string.format("Pad %d: not connected", slot + 1))
end
end
----
[#controllergetpadding]
==== controllerGetPadding
[source,text]
----
padding = controllerGetPadding()
----
Extensión de Hypseus, siempre `0` en Singe. Hypseus suma este desplazamiento al código de un interruptor que vino de un mando, para que un juego pueda saber qué dispositivo pulsó qué y aun así recuperar el interruptor restándolo. Singe entrega la entrada de los mandos como interruptores asignados corrientes que no llevan esa marca, así que responde `0`: un juego que comprueba el rango desplazado toma su otra rama y lee el interruptor tal cual.
*Devuelve:* un entero, siempre `0`.
*Desde:* 3.00
*Véase también:* <<controllersetpadding,controllerSetPadding>>
[#controllerhowmany]
==== controllerHowMany
[source,text]
----
count = controllerHowMany()
----
Extensión de Hypseus. Cuántos mandos están abiertos, de las cuatro ranuras que tiene Singe. Como todo joystick que Singe puede abrir se convierte en mando (véase xref:Manual.adoc#devices[Todo dispositivo es un mando]), una placa encoder de recreativa o un spinner también se cuentan aquí.
La cuenta cambia a medida que se conectan y desconectan dispositivos, así que léela cuando la necesites y no una sola vez al arrancar. No dice nada de qué ranuras están ocupadas: un dispositivo puede dejar vacía la ranura 0 mientras se usa la ranura 1, así que <<controllerisvalid,`controllerIsValid`>> sigue siendo la forma de preguntar por una ranura concreta.
*Parámetros:* ninguno.
*Devuelve:* número, de `0` a `4`.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<controllerisvalid,controllerIsValid>>, <<controllergetname,controllerGetName>>, <<mousehowmany,mouseHowMany>>
.Ejemplo
[source,lua]
----
-- Ofrece dos jugadores solo cuando el jugador dos tiene algo que sujetar.
local twoPlayer = controllerHowMany() >= 2
----
[#controllerisvalid]
==== controllerIsValid
[source,text]
----
valid = controllerIsValid(controller)
----
Extensión de Hypseus. Devuelve si hay un mando abierto en la ranura en este momento; los mandos van y vienen con la conexión en caliente, así que pregunta antes de cada `controllerDoRumble`. Un índice fuera de `0` a `3` aborta el script.
*Parámetros:*
* `controller` -- índice del mando, de `0` a `3`.
*Devuelve:* booleano.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<controllerdorumble,controllerDoRumble>>, <<controllergetaxis,controllerGetAxis>>
.Ejemplo
[source,lua]
----
-- Muestra las indicaciones del mando solo cuando hay un mando al que se refieran.
function drawAttract()
if controllerIsValid(0) then
fontPrint(HINT_X, HINT_Y, "Press A to start")
else
fontPrint(HINT_X, HINT_Y, "Press 1 to start")
end
end
----
[#controllersetpadding]
==== controllerSetPadding
[source,text]
----
controllerSetPadding(boolean)
----
Extensión de Hypseus, se acepta y se ignora. Consulta <<controllergetpadding,controllerGetPadding>> para saber por qué el desplazamiento que pide no existe en Singe. Activarlo sin marcar los eventos sería peor que dejarlo desactivado, porque el juego restaría entonces un desplazamiento a códigos de interruptor que nunca lo llevaron.
*Devuelve:* nada.
*Desde:* 3.00
*Véase también:* <<controllergetpadding,controllerGetPadding>>
[#debug]
=== Depuración
Dos llamadas para ver qué hace un script: `debugPrint` escribe texto, y `lineDraw` dibuja una línea sobre la escena 3D durante un fotograma (`physicsSetDebug` y `navDraw` dibujan con esas mismas líneas). La salida impresa va a la consola desde la que se arrancó Singe, así que se ve durante el desarrollo y es inofensiva en una recreativa, donde `--noconsole` la silencia. Cuando Singe se ejecuta con `--trace`, cada llamada a la API, incluida esta, se escribe también con sus argumentos en `trace.txt`, en la carpeta de datos, que es la forma de obtener un registro permanente. Las opciones de línea de comandos se enumeran en el capítulo sobre cómo ejecutar Singe; la integración con ZeroBrane Studio de `zbstudio/` ofrece un depurador cuando imprimir no basta.
[[traceheader]]
==== La cabecera de la traza
Tanto si lo abrió `--program` como `--trace`, `trace.txt` empieza con un bloque
que describe la máquina y la ejecución, entre dos líneas horizontales para que
pueda pegarse entero en un informe de errores:
----
----------------------------------------------------------------
Singe: v3.00
Command: ./Singe -w -p -d data --screen 2 MyGame
OS: Linux 6.8.0-124-generic (x86_64)
CPU: Intel(R) Xeon(R) CPU E5-2680 v4 @ 2.40GHz, 8 cores, 20177 MB
Renderer: gpu, GPU driver vulkan
Decoder: hardware vaapi or vdpau where the codec allows, software otherwise
SDL: built 3.4.16, linked 3.4.16
Settings: haptic=0 screen=2 volume_vldp=80
Game: MyGame/MyGame.singe
----------------------------------------------------------------
----
`Renderer` es el renderizador que se creó realmente y el backend de GPU que
eligió, o una nota de que no hay dispositivo GPU y el 3D no está disponible.
`Decoder` es lo que se intentará para el vídeo en esta máquina; el decodificador
que obtuvo realmente un vídeo concreto se registra en la traza al abrirse ese
vídeo, unas líneas más abajo, ya que un códec puede no ofrecer ninguna vía por
hardware. `Settings` lista lo que aportó el archivo de ajustes, o `none` si no
había ninguno. Cada juego de una cola tiene su propio archivo y su propia cabecera.
[#debugprint]
==== debugPrint
[source,text]
----
debugPrint(message)
----
Imprime una línea en la consola (la salida estándar). No se imprime nada si Singe se arrancó con `--noconsole`; con `--trace`, la llamada y su mensaje también acaban en `trace.txt`. El argumento debe ser una cadena o un número; cualquier otra cosa aborta el script, así que envuelve los valores en `tostring` o usa `string.format`. Es lo bastante barata como para dejarla en el código que se publica, pero no está limitada, así que una llamada dentro de `onOverlayUpdate` imprime en cada fotograma.
*Parámetros:*
* `message` -- el texto que se imprime; un número se convierte.
*Desde:* 1.x
*Véase también:* <<singegetticks,singeGetTicks>>
.Ejemplo
[source,lua]
----
-- Registra por qué falló el disparo, con el fotograma en que ocurrió.
function resolveShot(x, y)
local hit = targetAt(x, y)
if not hit then
debugPrint(string.format("miss at %d,%d on frame %d", x, y, discGetFrame()))
soundPlay(missClip)
return
end
score = score + hit.points
end
----
[#linedraw]
==== lineDraw
[source,text]
----
lineDraw(x0, y0, z0, x1, y1, z1)
lineDraw(x0, y0, z0, x1, y1, z1, r, g, b)
----
Dibuja una línea recta entre dos puntos del espacio del mundo sobre la escena, solo durante el fotograma actual, blanca salvo que se indique un color. Llámala en cada fotograma para mantener una línea en pantalla, como con el overlay. La línea pasa la prueba de profundidad contra la escena pero no escribe profundidad, así que se oculta tras las paredes como cualquier otra cosa y nunca tapa nada. No se dibuja nada mientras la escena está desactivada. Sirve para ver qué hace un script (un rayo lanzado, un camino, una caja envolvente); `physicsSetDebug` y `navDraw` dibujan con esas mismas líneas.
*Parámetros:*
* `x0, y0, z0` -- el punto inicial, en unidades del mundo.
* `x1, y1, z1` -- el punto final, en unidades del mundo.
* `r, g, b` -- el color de la línea, de `0` a `255` por canal, limitado a ese rango; blanco si se omite.
*Desde:* 3.00.
*Véase también:* <<physicssetdebug,physicsSetDebug>>, <<navdraw,navDraw>>, <<physicsraycast,physicsRaycast>>
.Ejemplo
[source,lua]
----
-- Muestra hacia dónde apunta la torreta y si el disparo acertaría.
function onOverlayUpdate()
local tx, ty, tz = nodeGetWorldPosition(turret)
local hit, hx, hy, hz = physicsRaycast(tx, ty, tz, aimX, aimY, aimZ, 40)
if hit then
lineDraw(tx, ty, tz, hx, hy, hz, 255, 255, 0)
else
lineDraw(tx, ty, tz, tx + aimX * 40, ty + aimY * 40, tz + aimZ * 40, 120, 120, 120)
end
return OVERLAY_UPDATED
end
----
[#disc]
=== Disco
El *disco* es el vídeo principal de laserdisc, el que Singe se creó originalmente para emular. Un juego tiene como mucho un disco, que se abre al arrancar a partir del `VIDEO` indicado en `games.dat` o de `--disc` / `--framefile` en la línea de comandos; un juego que no indica ninguno funciona sin disco (véase <<withoutadisc,Juegos sin disco>>). Todas las funciones `disc*` operan sobre este único disco implícito y no reciben ningún handle. Los números de fotograma son posiciones, empezando en cero, en el archivo de vídeo (o en el framefile combinado) y son la referencia de tiempo de cada decisión de ramificación en un juego de laserdisc. Sin disco, las funciones de consulta devuelven `0`, `discGetState` notifica `DISC_EJECTED` y las funciones de transporte escriben en la traza y no hacen nada. Los clips adicionales superpuestos al disco son cosa de la familia <<video,Vídeo>>.
[#discaudio]
==== discAudio
[source,text]
----
discAudio(channel, enabled)
----
Activa o desactiva un canal del sonido del disco. El canal se pone al volumen de laserdisc configurado (`--volume_vldp`, por defecto `100`) cuando `enabled` es `true` y a `0` cuando es `false`; el otro canal se queda como está. Muchos títulos originales llevaban narración o una pista de datos en un solo canal, y esta es la llamada que la silencia. Un canal distinto de `1` o `2` termina el script. Sin disco la llamada no hace nada.
*Parámetros:*
* `channel` -- `1` para el canal izquierdo, `2` para el derecho.
* `enabled` -- booleano. `true` devuelve el canal al volumen de laserdisc configurado, `false` lo silencia.
*Desde:* 1.x
*Véase también:* <<discsetaudiotrack,discSetAudioTrack>>, <<discgetaudiotracks,discGetAudioTracks>>, <<videosetvolume,videoSetVolume>>
.Ejemplo
[source,lua]
----
-- El canal derecho de este disco lleva la pista de datos de la recreativa.
function startGame()
discAudio(1, true)
discAudio(2, false)
discSkipToFrame(SCENE_FIRST_FRAME)
end
----
[#discaudiosuffix]
==== discAudioSuffix
[source,text]
----
opened = discAudioSuffix(suffix)
----
Extensión de Hypseus. Cambia el disco a un archivo de audio aparte, junto a su vídeo: `<base><suffix>.ogg`, donde la base es el nombre del archivo de vídeo sin su extensión, así que `movie.m2v` con `"-es"` reproduce `movie-es.ogg`. El archivo se abre en el fotograma actual y la reproducción se realinea, como hace `discSetAudioTrack`, conservando el volumen, el estado de reproducción y el índice de pista si el nuevo archivo tiene tantas pistas. Con un framefile cambian todos los segmentos. Un sufijo vacío restaura el audio por defecto: el `.ogg` junto a un `.m2v`, o el audio de dentro de cualquier otro contenedor. Devuelve `true` si el archivo se abrió; si falta, el audio anterior sigue sonando, no se recuerda nada y se devuelve `false` (Hypseus deja silencio en ese caso). El sufijo se olvida cuando el script se recarga. Sin disco la llamada devuelve `false`. Úsala para un menú de idioma cuyas pistas se distribuyen como archivos separados; las pistas dentro de un mismo archivo son cosa de `discSetAudioTrack`.
*Parámetros:*
* `suffix` -- cadena que se añade al nombre base del vídeo, como `"-es"`; `""` para el audio por defecto.
*Devuelve:* booleano.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<discsetaudiotrack,discSetAudioTrack>>, <<discgetaudiotracks,discGetAudioTracks>>, <<discaudio,discAudio>>
.Ejemplo
[source,lua]
----
-- Los archivos de idioma se distribuyen como movie.ogg, movie-es.ogg y movie-fr.ogg.
languages = { { "English", "" }, { "Spanish", "-es" }, { "French", "-fr" } }
function applyLanguage(index)
if not discAudioSuffix(languages[index][2]) then
discAudioSuffix("")
index = 1
end
settings.language = index
end
----
[#discchangespeed]
==== discChangeSpeed
[source,text]
----
discChangeSpeed(...)
----
No hace nada. Los reproductores de laserdisc reales podían funcionar a velocidades distintas de 1x; el reproductor por software de Singe no puede, así que la llamada acepta cualquier argumento, escribe una línea en la traza y vuelve. Se mantiene para que los scripts anteriores a la 2.00 se carguen sin modificaciones.
*Notas:*
WARNING: *Sin implementar.* Se conserva solo por compatibilidad con versiones anteriores. No hay sustituto; la velocidad de reproducción es fija, la tasa de fotogramas del archivo de vídeo.
*Desde:* 1.x
*Véase también:* <<discplay,discPlay>>, <<discskiptoframe,discSkipToFrame>>
.Ejemplo
[source,lua]
----
-- Los scripts antiguos a veces rodeaban una repetición a cámara lenta con cambios de velocidad.
-- Hoy las llamadas son inofensivas, pero la repetición se reproduce a velocidad normal.
discChangeSpeed(0.5)
discSkipToFrame(REPLAY_FIRST_FRAME)
discChangeSpeed(1)
----
[#discgetaudiotrack]
==== discGetAudioTrack
[source,text]
----
track = discGetAudioTrack()
----
Devuelve el índice, empezando en cero, de la pista de audio que el disco está reproduciendo. Al arrancar, el motor selecciona la pista indicada por `--audio` o por la entrada `AUDIO_TRACK` de `games.dat` (por defecto `0`) si el disco tiene tantas pistas. Devuelve `0` sin disco.
*Devuelve:* índice de pista, entero.
*Desde:* 2.10
*Véase también:* <<discgetaudiotracks,discGetAudioTracks>>, <<discsetaudiotrack,discSetAudioTrack>>, <<discgetlanguage,discGetLanguage>>
.Ejemplo
[source,lua]
----
-- Pasa al siguiente doblaje cada vez que el jugador pulsa la tecla de idioma.
function onInputPressed(what)
if what == SWITCH_BUTTON2 then
local next = (discGetAudioTrack() + 1) % discGetAudioTracks()
discSetAudioTrack(next)
languageLabel = discGetLanguageDescription(discGetLanguage(next))
end
end
----
[#discgetaudiotracks]
==== discGetAudioTracks
[source,text]
----
count = discGetAudioTracks()
----
Devuelve cuántos flujos de audio contiene el archivo de vídeo del disco. Cuando hay varios, suelen ser doblajes a otros idiomas. Un vídeo sin sonido devuelve `0`, igual que un juego sin disco, así que protege los bucles y la aritmética de módulo contra el cero.
*Devuelve:* número entero, `0` o más.
*Desde:* 2.10
*Véase también:* <<discgetaudiotrack,discGetAudioTrack>>, <<discgetlanguage,discGetLanguage>>, <<discsetaudiotrack,discSetAudioTrack>>
.Ejemplo
[source,lua]
----
-- Construye un menú de selección de idioma a partir de las pistas de audio del disco.
languages = {}
for track = 0, discGetAudioTracks() - 1 do
local code = discGetLanguage(track)
local name = discGetLanguageDescription(code)
table.insert(languages, { track = track, label = name })
end
----
[#discgetsubtitlelanguage]
==== discGetSubtitleLanguage
[source,text]
----
language = discGetSubtitleLanguage(track)
----
El idioma con que está etiquetada una pista de subtítulos dentro del propio contenedor del disco, como el código ISO 639 que lleva el archivo (`eng`, `fra`, `spa`). Una pista que el archivo no etiqueta responde con una cadena vacía, lo cual no es un error -- muchísimos archivos no etiquetan nada.
*Parámetros:*
* `track` -- número; de `0` a `discGetSubtitleTracks() - 1`.
*Devuelve:* una cadena.
*Desde:* 3.00.
*Véase también:* <<discgetsubtitletracks,discGetSubtitleTracks>>, <<srtloadtrack,srtLoadTrack>>
[#discgetsubtitletracks]
==== discGetSubtitleTracks
[source,text]
----
count = discGetSubtitleTracks()
----
Cuántas pistas de subtítulos contiene el propio contenedor del disco. `0` sin disco, y `0` para un disco cuyos subtítulos se distribuyen como un `.srt` aparte -- que es lo habitual, y para lo que sirve <<srtload,`srtLoad`>>.
Las pistas que contienen imágenes en lugar de palabras (las de DVD y Blu-ray, VobSub y PGS) se cuentan aquí para que la numeración coincida con la que muestra cualquier otro reproductor, pero <<srtloadtrack,`srtLoadTrack`>> no puede leerlas: no hay nada que leer, solo mapas de bits de las palabras.
*Parámetros:*
* Ninguno.
*Devuelve:* un número.
*Desde:* 3.00.
*Véase también:* <<srtloadtrack,srtLoadTrack>>, <<discgetsubtitlelanguage,discGetSubtitleLanguage>>
.Ejemplo
[source,lua]
----
-- Ofrece los idiomas que traiga el propio disco.
for track = 0, discGetSubtitleTracks() - 1 do
languages[#languages + 1] = { track = track, code = discGetSubtitleLanguage(track) }
end
----
[#discgetframe]
==== discGetFrame
[source,text]
----
frame = discGetFrame()
----
Devuelve el fotograma que el disco está mostrando. Con un framefile, el número es la posición en la secuencia combinada, así que los límites entre segmentos son invisibles para el script. Devuelve `0` mientras el disco está detenido y sin disco. Consúltalo desde `onOverlayUpdate` para reaccionar cuando el vídeo llegue a un punto de decisión.
*Devuelve:* número de fotograma, entero.
*Desde:* 1.x
*Véase también:* <<discsearch,discSearch>>, <<discskiptoframe,discSkipToFrame>>, <<discgetstate,discGetState>>
.Ejemplo
[source,lua]
----
-- Repite en bucle la secuencia de atracción y muestra un aviso durante la ventana para agacharse.
function onOverlayUpdate()
local frame = discGetFrame()
if frame >= ATTRACT_LAST_FRAME then
discSkipToFrame(ATTRACT_FIRST_FRAME)
elseif frame >= DUCK_FIRST_FRAME and frame <= DUCK_LAST_FRAME then
drawPrompt("DUCK")
end
return OVERLAY_UPDATED
end
----
[#discgetframecount]
==== discGetFrameCount
[source,text]
----
frames = discGetFrameCount()
----
El número de fotogramas del disco, que es uno más que el fotograma más alto que
encontrará <<discsearch,discSearch>>. Un framefile responde por todo el disco y no
por el segmento que se está reproduciendo, así que la cuenta no cambia a medida que
los segmentos van y vienen. Sin disco responde `0`.
*Devuelve:* número de fotogramas, entero; `0` sin disco.
*Desde:* 3.00.
*Véase también:* <<discgetframe,discGetFrame>>, <<discsearch,discSearch>>
.Ejemplo
[source,lua]
----
-- Salta a la mitad del disco, esté donde esté.
discSearch(math.floor(discGetFrameCount() / 2))
----
[#discgetheight]
==== discGetHeight
[source,text]
----
height = discGetHeight()
----
Devuelve la altura del vídeo del disco en píxeles. Sin disco devuelve la altura del lienzo (`CANVAS_Y` en `games.dat` o `--canvas`, por defecto `480`), así que siempre es el tamaño del fondo sobre el que se estira el overlay. Combínala con `discGetWidth` para dar al overlay la misma resolución que el vídeo.
*Devuelve:* altura en píxeles, entero.
*Desde:* 2.00
*Véase también:* <<discgetwidth,discGetWidth>>, <<overlaysetresolution,overlaySetResolution>>, <<vldpgetheight,vldpGetHeight>>
.Ejemplo
[source,lua]
----
-- Dibuja a la resolución nativa del disco para que las comprobaciones de píxeles y el dibujo compartan cuadrícula.
overlaySetResolution(discGetWidth(), discGetHeight())
crosshairLimitX = discGetWidth() - 1
crosshairLimitY = discGetHeight() - 1
----
[#discgetlanguage]
==== discGetLanguage
[source,text]
----
code = discGetLanguage(track)
----
Devuelve la etiqueta de idioma guardada en el archivo de vídeo para una de las pistas de audio del disco. La etiqueta es un código ISO 639 de tres letras, como `"eng"` o `"jpn"`. Las pistas sin una etiqueta de tres letras devuelven `"unk"`, igual que todas las pistas cuando no hay disco. Con disco, una pista fuera de `0` a `discGetAudioTracks() - 1` termina el script.
*Devuelve:* código de idioma, cadena.
*Desde:* 2.10
*Véase también:* <<videogetlanguagedescription,videoGetLanguageDescription>>, <<discgetaudiotracks,discGetAudioTracks>>, <<discsetaudiotrack,discSetAudioTrack>>
.Ejemplo
[source,lua]
----
-- Prefiere el doblaje japonés cuando el disco lo trae.
for track = 0, discGetAudioTracks() - 1 do
if discGetLanguage(track) == "jpn" then
discSetAudioTrack(track)
break
end
end
----
[#discgetstate]
==== discGetState
[source,text]
----
state = discGetState()
----
Devuelve el estado de transporte del disco como una de las constantes `DISC_*`. Un juego sin disco notifica `DISC_EJECTED`; tras `discStop`, el disco notifica `DISC_STOPPED` hasta el siguiente `discPlay`, `discSearch` o `discSkipToFrame`; en los demás casos, el estado es `DISC_PLAYING` mientras el vídeo avanza y `DISC_PAUSED` cuando no avanza. No hay un estado de búsqueda aparte; un disco que está saltando notifica `DISC_PAUSED` o `DISC_PLAYING` según lo que se pidiera.
*Devuelve:* una de las siguientes constantes, definidas por el motor y disponibles para todo script.
[cols="1,1,1",options="header"]
|===
| Constante | Valor | Significado
| `DISC_STOPPED` | `2` | Detenido (se llamó a `discStop`)
| `DISC_PLAYING` | `3` | Reproduciendo
| `DISC_PAUSED` | `4` | En pausa
| `DISC_EJECTED` | `5` | El juego no tiene disco (véase <<withoutadisc,Juegos sin disco>>)
|===
*Desde:* 1.x (RDG)
*Véase también:* <<discplay,discPlay>>, <<discpause,discPause>>, <<discstop,discStop>>
.Ejemplo
[source,lua]
----
-- La tecla de pausa no debe reanudar una congelación que pidió el script de la historia.
function onInputPressed(what)
if what == SWITCH_PAUSE then
if discGetState() == DISC_PAUSED and not storyPaused then
discPlay()
elseif discGetState() == DISC_PLAYING then
discPause()
end
end
end
----
[#discgetwidth]
==== discGetWidth
[source,text]
----
width = discGetWidth()
----
Devuelve la anchura del vídeo del disco en píxeles. Sin disco devuelve la anchura del lienzo (`CANVAS_X` en `games.dat` o `--canvas`, por defecto `720`). Junto con `discGetHeight`, es el tamaño sobre el que se escala el overlay, y por eso el menú incluido pasa ambos a `overlaySetResolution`.
*Devuelve:* anchura en píxeles, entero.
*Desde:* 2.00
*Véase también:* <<discgetheight,discGetHeight>>, <<overlaysetresolution,overlaySetResolution>>, <<vldpgetwidth,vldpGetWidth>>
.Ejemplo
[source,lua]
----
-- Centra un sprite de título sea cual sea la anchura del disco.
overlaySetResolution(discGetWidth(), discGetHeight())
titleSprite = spriteLoad("graphics/title.png")
titleX = (discGetWidth() - spriteGetWidth(titleSprite)) / 2
----
[#discpause]
==== discPause
[source,text]
----
discPause()
----
Pone en pausa la reproducción en el fotograma actual, que se queda en pantalla. El audio del disco se pausa con ella. La llamada se ignora mientras el disco está detenido y no hace nada sin disco. Se reanuda con `discPlay`.
*Desde:* 1.x
*Véase también:* <<discplay,discPlay>>, <<discstop,discStop>>, <<singesetpauseflag,singeSetPauseFlag>>
.Ejemplo
[source,lua]
----
-- Mantiene el último fotograma de la escena mientras el jugador elige una puerta.
function onOverlayUpdate()
if discGetFrame() >= SCENE_LAST_FRAME and not choosing then
discPause()
choosing = true
end
return OVERLAY_UPDATED
end
----
[#discpauseatframe]
==== discPauseAtFrame
[source,text]
----
discPauseAtFrame(frame)
----
Salta a `frame`, lo muestra y se pone en pausa. Es la misma función que `discSearch` con un segundo nombre; las dos son intercambiables en todos los aspectos, incluidas las reglas de límites que se describen allí. Prefiere `discSearch` en código nuevo y conserva `discPauseAtFrame` solo al portar scripts antiguos.
*Notas:*
WARNING: *Alias heredado.* Usa <<discsearch,discSearch>> en código nuevo.
*Desde:* 1.18
*Véase también:* <<discsearch,discSearch>>, <<discplay,discPlay>>
.Ejemplo
[source,lua]
----
-- Código 1.x portado: se queda en el primer fotograma de la escena hasta que el jugador esté listo.
function onInputPressed(what)
if what == SWITCH_START1 then
discPauseAtFrame(SCENE_FIRST_FRAME)
waitingForPlayer = true
end
end
----
[#discplay]
==== discPlay
[source,text]
----
discPlay()
----
Inicia o reanuda la reproducción desde el fotograma actual y borra el estado de detenido que fija `discStop`. El audio se reanuda con el vídeo, y el reloj de reproducción vuelve a empezar desde el fotograma actual, de modo que una pausa larga no provoca un salto. Sin disco no hace nada, aparte de borrar la marca de detenido.
*Desde:* 1.x
*Véase también:* <<discpause,discPause>>, <<discstop,discStop>>, <<discskiptoframe,discSkipToFrame>>
.Ejemplo
[source,lua]
----
-- Arranque habitual del modo de atracción: prepara el primer fotograma y a rodar.
function attractBegin()
discSearch(ATTRACT_FIRST_FRAME)
overlayClear()
discPlay()
attractRunning = true
end
----
[#discsearch]
==== discSearch
[source,text]
----
discSearch(frame)
----
Salta con el disco a `frame`, muestra ese fotograma y se pone en pausa en él, borrando el estado de detenido si el disco estaba detenido. El disco mantiene el fotograma hasta `discPlay`, `discSkipToFrame` u otra llamada de transporte. Un fotograma negativo se limita al primero y uno más allá del final se limita al último; con un framefile se selecciona el segmento que contiene el fotograma y la posición se limita dentro de él. Sin disco no hace nada.
*Parámetros:*
* `frame` -- número del fotograma de destino, empezando en cero.
*Desde:* 1.x
*Véase también:* <<discskiptoframe,discSkipToFrame>>, <<discplay,discPlay>>, <<discgetframe,discGetFrame>>
.Ejemplo
[source,lua]
----
-- El jugador eligió el pasillo izquierdo: prepara la escena y espera la señal.
function goLeft()
discSearch(CORRIDOR_LEFT_FIRST_FRAME)
soundPlay(SOUND_DOOR)
waitingForPlayer = true
end
----
[#discsearchblanking]
==== discSearchBlanking
[source,text]
----
discSearchBlanking(...)
----
No hace nada. En el hardware de laserdisc elegía si la pantalla se apagaba mientras el reproductor buscaba un fotograma; el reproductor por software de Singe salta sin apagar la pantalla, así que la llamada acepta cualquier argumento, escribe una línea en la traza y vuelve.
*Notas:*
WARNING: *Sin implementar.* Se conserva solo por compatibilidad con versiones anteriores. No hay sustituto.
*Desde:* 1.x
*Véase también:* <<discsearch,discSearch>>, <<discskipblanking,discSkipBlanking>>
.Ejemplo
[source,lua]
----
-- Los scripts anteriores a la 2.00 solían fijar los modos de apagado una vez al arrancar. Inofensivo.
discSearchBlanking(false)
discSkipBlanking(false)
discSearch(TITLE_FRAME)
----
[#discsetaudiotrack]
==== discSetAudioTrack
[source,text]
----
discSetAudioTrack(track)
----
Cambia el disco a otra pista de audio. Se descarta el audio en cola de la pista anterior y la reproducción se realinea, así que el cambio se oye casi de inmediato. Con disco, una pista fuera de `0` a `discGetAudioTracks() - 1` termina el script; sin disco la llamada no hace nada. Úsala para implementar un menú de idioma o para respetar una preferencia guardada entre sesiones.
*Parámetros:*
* `track` -- índice de la pista de audio, empezando en cero.
*Desde:* 2.10
*Véase también:* <<discgetaudiotrack,discGetAudioTrack>>, <<discgetaudiotracks,discGetAudioTracks>>, <<discgetlanguage,discGetLanguage>>
.Ejemplo
[source,lua]
----
-- Aplica el idioma que eligió el jugador en la pantalla de opciones.
function applyLanguage(choice)
if choice >= 0 and choice < discGetAudioTracks() then
discSetAudioTrack(choice)
settings.language = choice
saveSettings()
end
end
----
[#discsetfps]
==== discSetFPS
[source,text]
----
discSetFPS(...)
----
No hace nada. La tasa de fotogramas viene del archivo de vídeo, así que la llamada acepta cualquier argumento, escribe una línea en la traza y vuelve. Nada en el `Framework.singe` incluido la llama; solo lo hacen los scripts anteriores a la 2.00.
*Notas:*
WARNING: *Sin implementar.* Se conserva solo por compatibilidad con versiones anteriores. La tasa de fotogramas se lee del archivo de vídeo y no se puede fijar desde un script.
*Desde:* 1.x
*Véase también:* <<discgetframe,discGetFrame>>, <<discskiptoframe,discSkipToFrame>>
.Ejemplo
[source,lua]
----
-- Un script 1.x portado. La llamada se ignora; los fotogramas por segundo son una propiedad del archivo.
discSetFPS(29.97)
discSearch(TITLE_FRAME)
discPlay()
----
[#discskipbackward]
==== discSkipBackward
[source,text]
----
discSkipBackward(frames)
----
Salta hacia atrás `frames` fotogramas desde el actual sin cambiar el estado de reproducción o pausa: un disco que se está reproduciendo sigue reproduciéndose desde la nueva posición, y uno en pausa sigue en pausa en ella. La llamada se ignora mientras el disco está detenido y sin disco. El desplazamiento no se valida; un valor negativo salta hacia delante, y un desplazamiento mayor que el fotograma actual se limita al fotograma `0`. Con un framefile, el desplazamiento se aplica al número de fotograma que notifica `discGetFrame`, así que un salto puede pasar a un segmento anterior.
*Parámetros:*
* `frames` -- número de fotogramas que se restan al fotograma actual.
*Desde:* 1.x
*Véase también:* <<discskipforward,discSkipForward>>, <<discstepbackward,discStepBackward>>, <<discgetframe,discGetFrame>>
.Ejemplo
[source,lua]
----
-- Repite el último segundo (30 fotogramas) cuando el jugador falla un aviso.
function missedPrompt()
lives = lives - 1
discSkipBackward(30)
promptShown = false
end
----
[#discskipblanking]
==== discSkipBlanking
[source,text]
----
discSkipBlanking(...)
----
No hace nada. En su día elegía si la pantalla se apagaba durante un salto en el hardware de laserdisc; el reproductor por software de Singe nunca apaga la pantalla, así que la llamada acepta cualquier argumento, escribe una línea en la traza y vuelve.
*Notas:*
WARNING: *Sin implementar.* Se conserva solo por compatibilidad con versiones anteriores. No hay sustituto.
*Desde:* 1.x
*Véase también:* <<discsearchblanking,discSearchBlanking>>, <<discskiptoframe,discSkipToFrame>>
.Ejemplo
[source,lua]
----
-- Los scripts anteriores a la 2.00 solían fijar los modos de apagado una vez al arrancar. Inofensivo.
discSearchBlanking(false)
discSkipBlanking(false)
discSearch(TITLE_FRAME)
----
[#discskipforward]
==== discSkipForward
[source,text]
----
discSkipForward(frames)
----
Salta hacia delante `frames` fotogramas desde el actual sin cambiar el estado de reproducción o pausa. Es el reflejo de `discSkipBackward`: se ignora mientras está detenido y sin disco, el desplazamiento no se valida, y un destino más allá del último fotograma se limita al último. Con un framefile, el desplazamiento se aplica al número de fotograma que notifica `discGetFrame`, así que un salto puede pasar a un segmento posterior.
*Parámetros:*
* `frames` -- número de fotogramas que se suman al fotograma actual.
*Desde:* 1.x
*Véase también:* <<discskipbackward,discSkipBackward>>, <<discstepforward,discStepForward>>, <<discgetframe,discGetFrame>>
.Ejemplo
[source,lua]
----
-- Deja que un jugador que vuelve se salte los dos segundos de introducción de la escena.
function onInputPressed(what)
if what == SWITCH_BUTTON1 and inSceneIntro then
discSkipForward(60)
inSceneIntro = false
end
end
----
[#discskiptoframe]
==== discSkipToFrame
[source,text]
----
discSkipToFrame(frame)
----
Salta a `frame` y empieza a reproducir desde él sea cual sea el estado del disco, borrando el estado de detenido si hace falta. Compárala con `discSearch`, que salta y se pone en pausa. Un fotograma anterior al principio o posterior al final se limita al primero o al último; con un framefile se selecciona el segmento que contiene el fotograma y la posición se limita dentro de él. Sin disco no hace nada, aparte de borrar la marca de detenido.
*Parámetros:*
* `frame` -- número del fotograma de destino, empezando en cero.
*Desde:* 1.x
*Véase también:* <<discsearch,discSearch>>, <<discplay,discPlay>>, <<discgetframe,discGetFrame>>
.Ejemplo
[source,lua]
----
-- Se salta la introducción al volver a jugar.
function startGame()
if hasPlayedBefore then
discSkipToFrame(GAMEPLAY_FIRST_FRAME)
else
discSkipToFrame(INTRO_FIRST_FRAME)
end
end
----
[#discstepbackward]
==== discStepBackward
[source,text]
----
discStepBackward()
----
Retrocede el disco exactamente un fotograma y se pone en pausa en él, sea cual sea el estado de reproducción anterior. Retroceder desde el fotograma `0` se queda en el fotograma `0`. Se ignora mientras el disco está detenido, como `discSkipBackward`. Pensada para depurar con precisión de fotograma y para pantallas de servicio. Sin disco no hace nada.
*Desde:* 1.x
*Véase también:* <<discstepforward,discStepForward>>, <<discskipbackward,discSkipBackward>>, <<discgetframe,discGetFrame>>
.Ejemplo
[source,lua]
----
-- Pantalla de servicio: izquierda y derecha mueven el disco fotograma a fotograma.
function onInputPressed(what)
if what == SWITCH_LEFT then
discStepBackward()
elseif what == SWITCH_RIGHT then
discStepForward()
end
end
----
[#discstepforward]
==== discStepForward
[source,text]
----
discStepForward()
----
Avanza el disco exactamente un fotograma y se pone en pausa en él, sea cual sea el estado de reproducción anterior. Avanzar desde el último fotograma se queda en el último fotograma. Como `discStepBackward`, se ignora mientras el disco está detenido. Sin disco no hace nada.
*Desde:* 1.x
*Véase también:* <<discstepbackward,discStepBackward>>, <<discskipforward,discSkipForward>>, <<discgetframe,discGetFrame>>
.Ejemplo
[source,lua]
----
-- Buscador de fotogramas: avanza por una escena e imprime el número de fotograma.
function onOverlayUpdate()
overlayClear()
overlayPrint(8, 8, "Frame " .. discGetFrame())
if stepping then
discStepForward()
end
return OVERLAY_UPDATED
end
----
[#discstop]
==== discStop
[source,text]
----
discStop()
----
Detiene el disco. La reproducción se pausa, el vídeo se sustituye por la clásica pantalla azul, `discGetState` notifica `DISC_STOPPED` y `discGetFrame` devuelve `0` hasta que el disco se vuelve a poner en marcha. Mientras está detenido, se ignoran `discPause`, `discSkipForward`, `discSkipBackward`, `discStepForward` y `discStepBackward`. Cualquiera de `discPlay`, `discSearch` o `discSkipToFrame` borra el estado de detenido. Un segundo `discStop` se ignora.
*Desde:* 1.x
*Véase también:* <<discplay,discPlay>>, <<discpause,discPause>>, <<discgetstate,discGetState>>
.Ejemplo
[source,lua]
----
-- Fin de partida: pasa a la pantalla azul detrás de la tabla de puntuaciones.
function gameOver()
discStop()
soundPlay(SOUND_GAME_OVER)
showScores = true
end
----
[#emitter]
=== Emisor
Los emisores generan, mueven y dibujan partículas; véase <<particles,Partículas>>.
Los handles son enteros. Un emisor 2D vive en coordenadas del overlay, con la Y
hacia abajo, y dibuja cuando `emitterDraw` se lo pide desde `onOverlayUpdate`; un
emisor 3D está sobre un nodo, vive en unidades del mundo y se dibuja solo en la
escena. Las llamadas de la receta pueden hacerse en cualquier orden, antes o
después de que el emisor arranque, y afectan a las siguientes partículas que
nazcan (unas pocas, indicadas abajo, actúan también sobre las vivas). Todo lo que
se da como mínimo y máximo se sortea entre ambos para cada partícula. Las
partículas avanzan con el reloj, como mucho una décima de segundo por fotograma,
y se detienen mientras el juego está en pausa.
[#emitternew]
==== emitterNew
[source,text]
----
emitter = emitterNew()
emitter = emitterNew(node)
----
Crea un emisor. Sin argumentos es uno 2D en la posición del overlay `(0, 0)`, que se mueve con `emitterSetPosition`; con un nodo es uno 3D cuyas partículas nacen en la posición en el mundo de ese nodo, y necesita la escena 3D, así que en una máquina sin ella la llamada termina el script (los emisores 2D funcionan en todas partes). Un emisor nuevo no está emitiendo; llama a `emitterStart` o a `emitterBurst`. Borrar el nodo borra sus emisores. Valores por defecto: 50 partículas por segundo, que viven de uno a dos segundos, dispersión de 30 grados, de blanco a transparente, mezcla normal, sin gravedad, resistencia, giro, radio, textura, estela ni colisión, y una reserva de 1000; un emisor 2D las lanza hacia arriba en la pantalla a entre 50 y 100 unidades del overlay por segundo, encogiendo de 16 a 4, y uno 3D hacia arriba por el eje Y a entre 1 y 2 unidades del mundo por segundo, encogiendo de 0,2 a 0,05.
*Devuelve:* el handle del emisor.
*Desde:* 3.00.
*Véase también:* <<emitterdelete,emitterDelete>>, <<emitterstart,emitterStart>>, <<emitterburst,emitterBurst>>, <<emitterdraw,emitterDraw>>
.Ejemplo
[source,lua]
----
-- Gases de escape del nodo del motor de la nave; la receta se fija antes de que arranque el flujo.
exhaust = emitterNew(nodeFind("Engine", ship))
emitterSetBlend(exhaust, PARTICLE_ADD)
emitterSetRate(exhaust, 120)
emitterSetLife(exhaust, 0.3, 0.6)
emitterSetDirection(exhaust, 0, 0, 1)
emitterSetSpeed(exhaust, 3, 5)
emitterSetSize(exhaust, 0.3, 0.05)
emitterSetColor(exhaust, 120, 180, 255, 255, 30, 60, 255, 0)
emitterStart(exhaust)
----
[#emitterdelete]
==== emitterDelete
[source,text]
----
emitterDelete(emitter)
----
Elimina el emisor, sus partículas vivas y su copia de la textura de inmediato; nada se desvanece. Usa `emitterStop` en su lugar para dejar que terminen las últimas partículas. Un emisor 3D también se elimina por sí solo cuando se borra su nodo. Un handle que no es un emisor vivo termina el script, así que vacía la variable después.
*Desde:* 3.00.
*Véase también:* <<emitternew,emitterNew>>, <<emitterstop,emitterStop>>, <<emitterisactive,emitterIsActive>>
.Ejemplo
[source,lua]
----
-- La explosión ha terminado: libera su emisor cuando se haya apagado la última chispa.
function onOverlayUpdate()
if explosion ~= nil and not emitterIsActive(explosion) then
emitterDelete(explosion)
explosion = nil
end
return OVERLAY_NOT_UPDATED
end
----
[#emittersettexture]
==== emitterSetTexture
[source,text]
----
emitterSetTexture(emitter, sprite)
emitterSetTexture(emitter)
----
La imagen que lleva cada partícula: un sprite cargado, o ninguno (un solo argumento, o `nil`) para el disco difuso integrado. El emisor guarda su propia copia de los píxeles del sprite, incluidos todos los fotogramas de un sprite animado, así que el sprite puede descargarse después. Fijar la textura devuelve el rango de fotogramas al fotograma `0` y pasa todas las partículas vivas al fotograma `0`, así que llama a `emitterSetFrames` después, no antes. La mezcla y el tinte se aplican a la imagen igual que al disco.
*Desde:* 3.00.
*Véase también:* <<emittersetframes,emitterSetFrames>>, <<spriteload,spriteLoad>>, <<emittersetblend,emitterSetBlend>>
.Ejemplo
[source,lua]
----
-- Nieve: una hoja con seis formas de copo, una elegida al azar para cada partícula.
flakes = spriteLoadFrames(6, DIR .. "Sprites/flakes.png")
snow = emitterNew()
emitterSetTexture(snow, flakes)
emitterSetFrames(snow, 0, 5)
emitterSetDirection(snow, 0, 1)
emitterSetSpeed(snow, 20, 40)
emitterSetLife(snow, 6, 9)
emitterSetSize(snow, 6, 6, 0.4)
emitterStart(snow)
----
[#emittersetframes]
==== emitterSetFrames
[source,text]
----
emitterSetFrames(emitter, first, last)
----
Con un sprite animado como textura, cada partícula nueva toma un fotograma al azar de `first` a `last` y lo conserva toda su vida; los fotogramas se cuentan desde `0`. Los valores se limitan a los fotogramas que tiene la textura, y `last` se sube hasta `first` si es menor. Por defecto: solo el fotograma `0`, que es también a lo que vuelve `emitterSetTexture`. Con el disco integrado o con un sprite de un solo fotograma, el rango no tiene efecto.
*Desde:* 3.00.
*Véase también:* <<emittersettexture,emitterSetTexture>>, <<spriteload,spriteLoad>>
.Ejemplo
[source,lua]
----
-- La hoja de bocanadas tiene cuatro bocanadas suaves y luego cuatro trozos de escombros más duros.
emitterSetTexture(smoke, puffSheet)
emitterSetFrames(smoke, 0, 3)
emitterSetTexture(debris, puffSheet)
emitterSetFrames(debris, 4, 7)
----
[#emittersetblend]
==== emitterSetBlend
[source,text]
----
emitterSetBlend(emitter, mode)
----
Cómo se combinan las partículas con lo que tienen detrás. `PARTICLE_ALPHA` (por defecto) mezcla de forma normal, para humo, polvo, lluvia y escombros; `PARTICLE_ADD` suma luz, para fuego, chispas, resplandores y magia, y nunca oscurece. Afecta de inmediato a las partículas vivas. Cualquier cosa que no sea una de esas dos constantes termina el script.
*Desde:* 3.00.
*Véase también:* <<emittersetcolor,emitterSetColor>>, <<emittersettexture,emitterSetTexture>>
.Ejemplo
[source,lua]
----
-- Las chispas brillan; el humo que hay encima de ellas, no.
emitterSetBlend(sparks, PARTICLE_ADD)
emitterSetColor(sparks, 255, 220, 120, 255, 255, 80, 20, 0)
emitterSetBlend(smoke, PARTICLE_ALPHA)
emitterSetColor(smoke, 160, 160, 170, 120, 120, 120, 130, 0)
----
[#emittersetlayer]
==== emitterSetLayer
[source,text]
----
emitterSetLayer(emitter, layer)
----
Solo 2D: dónde pone `emitterDraw` las partículas. `PARTICLE_OVER` (por defecto) dibuja por encima de todo lo del overlay; `PARTICLE_UNDER` dibuja por debajo del overlay y por encima del vídeo y de la escena 3D, para humo detrás de la puntuación. En un emisor 3D el ajuste se acepta y se ignora. Cualquier cosa que no sea una de esas dos constantes termina el script.
*Desde:* 3.00.
*Véase también:* <<emitterdraw,emitterDraw>>, <<emittersetposition,emitterSetPosition>>
.Ejemplo
[source,lua]
----
-- La lluvia cae detrás del HUD; las chispas de los impactos, delante.
rain = emitterNew()
emitterSetLayer(rain, PARTICLE_UNDER)
hits = emitterNew()
emitterSetLayer(hits, PARTICLE_OVER)
function onOverlayUpdate()
overlayClear()
emitterDraw(rain)
drawHud()
emitterDraw(hits)
return OVERLAY_UPDATED
end
----
[#emittersetrate]
==== emitterSetRate
[source,text]
----
emitterSetRate(emitter, perSecond)
----
Cuántas partículas por segundo crea un emisor arrancado; por defecto `50`, y los valores negativos pasan a `0`. Las fracciones se acumulan, así que `0.5` crea una cada dos segundos. Cámbialo mientras emite para regular una llama o un propulsor. Una reserva llena (`emitterSetMax`) no crea más hasta que mueran algunas, sea cual sea la tasa.
*Desde:* 3.00.
*Véase también:* <<emitterstart,emitterStart>>, <<emitterburst,emitterBurst>>, <<emittersetmax,emitterSetMax>>
.Ejemplo
[source,lua]
----
-- El penacho del propulsor sigue al acelerador.
function onControllerMoved(axis, value, which)
if axis == GAMEPAD_AXIS_RIGHT_TRIGGER then
throttle = value / 32767
emitterSetRate(plume, 30 + 300 * throttle)
end
end
----
[#emittersetlife]
==== emitterSetLife
[source,text]
----
emitterSetLife(emitter, minSeconds, maxSeconds)
----
Cuánto vive cada partícula nueva, sorteado entre los dos valores; por defecto de `1` a `2`. Su tamaño y su color van de su inicio a su final a lo largo de ese tiempo, así que la vida marca el ritmo de cada desvanecimiento. El mínimo se mantiene en `0.001` o más, y el máximo se sube hasta el mínimo si es menor. Las partículas vivas conservan la vida con la que nacieron.
*Desde:* 3.00.
*Véase también:* <<emittersetsize,emitterSetSize>>, <<emittersetcolor,emitterSetColor>>, <<emittersetrate,emitterSetRate>>
.Ejemplo
[source,lua]
----
-- Fogonazo: una ráfaga que desaparece en una décima de segundo.
flash = emitterNew(muzzle)
emitterSetBlend(flash, PARTICLE_ADD)
emitterSetLife(flash, 0.05, 0.1)
emitterSetSpeed(flash, 2, 6)
emitterSetSize(flash, 0.15, 0.4)
function onFire()
emitterBurst(flash, 12)
end
----
[#emittersetsoftness]
==== emitterSetSoftness
[source,text]
----
emitterSetSoftness(emitter, distance)
----
Solo 3D: desvanece una partícula a lo largo de `distance` unidades del mundo donde se encuentra con la geometría que tiene detrás, para que una bocanada de humo no dibuje una línea dura donde atraviesa una pared y una fuente se hunda en su estanque en lugar de cortarlo. `0` (el valor por defecto) lo desactiva; los valores negativos pasan a `0`. Mientras algún emisor lo use, la profundidad de la escena se renderiza una vez más por fotograma, así que en una Raspberry Pi conviene usarlo con moderación. Un emisor 2D guarda el valor y lo ignora.
*Desde:* 3.00.
*Véase también:* <<emittersetlit,emitterSetLit>>, <<emittersetblend,emitterSetBlend>>
.Ejemplo
[source,lua]
----
-- El humo del brasero se suaviza contra el poste junto al que sube.
smoke = emitterNew(brazierTop)
emitterSetRate(smoke, 25)
emitterSetLife(smoke, 1.5, 2.5)
emitterSetSize(smoke, 0.2, 0.9, 0.2)
emitterSetColor(smoke, 160, 160, 170, 120, 120, 120, 130, 0)
emitterSetLit(smoke, true)
emitterSetSoftness(smoke, 0.3)
emitterStart(smoke)
----
[#emittersetspeed]
==== emitterSetSpeed
[source,text]
----
emitterSetSpeed(emitter, min, max)
----
Velocidad inicial de cada partícula nueva, sorteada entre los dos valores, en unidades del overlay por segundo (2D) o unidades del mundo por segundo (3D); por defecto de `50` a `100` en 2D y de `1` a `2` en 3D. El máximo se sube hasta el mínimo si es menor. La velocidad determina hasta dónde llega una partícula antes de que tomen el relevo la resistencia, la gravedad y su vida.
*Desde:* 3.00.
*Véase también:* <<emittersetdirection,emitterSetDirection>>, <<emittersetdrag,emitterSetDrag>>, <<emittersetgravity,emitterSetGravity>>
.Ejemplo
[source,lua]
----
-- Una fuente lanza las gotas con fuerza y rectas; el polvo apenas se mueve.
emitterSetSpeed(fountain, 4.0, 5.0)
emitterSetSpread(fountain, 8)
emitterSetSpeed(dust, 0.05, 0.2)
emitterSetSpread(dust, 180)
----
[#emittersetdirection]
==== emitterSetDirection
[source,text]
----
emitterSetDirection(emitter, x, y)
emitterSetDirection(emitter, x, y, z)
----
La dirección media en que salen las partículas nuevas; solo importa la dirección, no la longitud. Por defecto en 2D `(0, -1)`, hacia arriba en la pantalla, ya que la Y del overlay crece hacia abajo; por defecto en 3D `(0, 1, 0)`, hacia arriba por el eje Y del mundo. Un emisor 2D ignora `z`; un emisor 3D que recibe dos valores toma `z = 0`. Un vector nulo se trata como `(0, -1, 0)`. En un emisor 3D, la dirección está en los ejes del mundo, no en los del nodo, así que gírala tú cuando gire el nodo.
*Desde:* 3.00.
*Véase también:* <<emittersetspread,emitterSetSpread>>, <<emittersetspeed,emitterSetSpeed>>
.Ejemplo
[source,lua]
----
-- Los gases de escape salen por detrás de la nave, mire hacia donde mire.
function onOverlayUpdate()
local heading = math.rad(shipYaw)
emitterSetDirection(exhaust, -math.sin(heading), 0, -math.cos(heading))
return OVERLAY_NOT_UPDATED
end
----
[#emittersetspread]
==== emitterSetSpread
[source,text]
----
emitterSetSpread(emitter, degrees)
----
La mitad del ángulo del cono (3D) o del abanico (2D) alrededor de la dirección en que se lanzan las partículas nuevas, de `0` a `180`; `0` es una línea recta y `180`, todas las direcciones. Por defecto `30`; los valores fuera del rango se limitan. En 3D las direcciones se reparten uniformemente sobre el casquete del cono, así que un cono ancho no se amontona hacia su eje.
*Desde:* 3.00.
*Véase también:* <<emittersetdirection,emitterSetDirection>>, <<emittersetradius,emitterSetRadius>>
.Ejemplo
[source,lua]
----
-- Los fuegos artificiales van a todas partes; la llama de la antorcha se queda en una columna estrecha.
emitterSetSpread(fireworks, 180)
emitterSetSpread(flame, 15)
----
[#emittersetgravity]
==== emitterSetGravity
[source,text]
----
emitterSetGravity(emitter, x, y)
emitterSetGravity(emitter, x, y, z)
----
Una aceleración constante sobre cada partícula, en unidades del overlay (2D) o unidades del mundo (3D) por segundo al cuadrado: `(0, 500)` tira de las chispas 2D hacia abajo en la pantalla, `(0, -20)` deja que el humo 2D suba, `(0, -9.8, 0)` es la Tierra en la escena. Por defecto, ninguna. Se aplica a las partículas vivas desde el siguiente fotograma. Las partículas no son cuerpos físicos; dales `emitterSetCollide` para que reboten en un suelo o en la escena.
*Desde:* 3.00.
*Véase también:* <<emittersetdrag,emitterSetDrag>>, <<emittersetcollide,emitterSetCollide>>, <<emittersetspeed,emitterSetSpeed>>
.Ejemplo
[source,lua]
----
-- Las chispas caen y el humo sube, ambos de la misma amoladora.
emitterSetGravity(sparks, 0, 500)
emitterSetGravity(smoke, 0, -20)
-- Bajo el agua, las burbujas van hacia la superficie.
emitterSetGravity(bubbles, 0, 3, 0)
----
[#emittersetdrag]
==== emitterSetDrag
[source,text]
----
emitterSetDrag(emitter, perSecond)
----
Lo rápido que las partículas pierden su velocidad: en cada fotograma la velocidad se multiplica por `1 - perSecond * dt`, que nunca baja de `0`, así que `1` les quita casi toda la velocidad en un segundo y `0` (el valor por defecto) la conserva para siempre. Los valores negativos pasan a `0`. El humo, las brasas y los fuegos artificiales se frenan; las chispas y la lluvia, no. Se aplica a las partículas vivas desde el siguiente fotograma.
*Desde:* 3.00.
*Véase también:* <<emittersetgravity,emitterSetGravity>>, <<emittersetspeed,emitterSetSpeed>>
.Ejemplo
[source,lua]
----
-- Una carcasa de fuegos artificiales: sale rápida y luego queda suspendida y cae.
fireworks = emitterNew()
emitterSetBlend(fireworks, PARTICLE_ADD)
emitterSetSpeed(fireworks, 120, 320)
emitterSetSpread(fireworks, 180)
emitterSetGravity(fireworks, 0, 220)
emitterSetDrag(fireworks, 0.8)
emitterSetLife(fireworks, 0.8, 1.6)
----
[#emittersetsize]
==== emitterSetSize
[source,text]
----
emitterSetSize(emitter, start, finish)
emitterSetSize(emitter, start, finish, variation)
----
Tamaño al nacer y al morir, en unidades del overlay (2D) o unidades del mundo (3D), que cambia de manera uniforme a lo largo de la vida de la partícula; por defecto de `16` a `4` en 2D y de `0.2` a `0.05` en 3D. `variation` (de `0` a `1`, por defecto `0`) escala los dos tamaños de cada partícula nueva por un mismo factor aleatorio entre `1 - variation` y `1 + variation`, así que una bocanada es un poco más grande o más pequeña durante toda su vida. Los tamaños negativos pasan a `0` y `variation` se limita a su rango. Una partícula con textura se dibuja como un cuadrado de ese tamaño; una imagen más ancha se aplasta.
*Desde:* 3.00.
*Véase también:* <<emittersetlife,emitterSetLife>>, <<emittersetcolor,emitterSetColor>>, <<emittersetspin,emitterSetSpin>>
.Ejemplo
[source,lua]
----
-- El humo crece a medida que se aclara; las chispas encogen hasta desaparecer.
emitterSetSize(smoke, 12, 48, 0.2)
emitterSetSize(sparks, 5, 1)
----
[#emittersetcollide]
==== emitterSetCollide
[source,text]
----
emitterSetCollide(emitter, mode)
emitterSetCollide(emitter, mode, bounce)
emitterSetCollide(emitter, mode, bounce, friction)
emitterSetCollide(emitter, mode, bounce, friction, floor)
----
Contra qué rebotan las partículas. `COLLIDE_NONE` (por defecto) es nada; `COLLIDE_FLOOR` es un plano horizontal a la altura `floor`, por debajo del cual (Y mayor en 2D, Y menor en 3D) una partícula se devuelve al plano y se refleja; `COLLIDE_SCENE` es el mundo físico, solo para emisores 3D, mediante un rayo por partícula a lo largo de su movimiento en cada fotograma, con un tope de `200` rayos por emisor y fotograma que se reanudan donde se quedaron, así que un emisor grande reparte el coste entre unos pocos fotogramas. En un emisor 2D, `COLLIDE_SCENE` se acepta y no hace nada. Un modo distinto de esos tres termina el script.
*Parámetros:*
* `mode` -- `COLLIDE_NONE`, `COLLIDE_FLOOR` o `COLLIDE_SCENE`.
* `bounce` -- cuánta velocidad sobrevive a un choque, de `0` a `1`, por defecto `0.5`; se limita a ese rango.
* `friction` -- cuánta velocidad de deslizamiento se pierde en cada choque, de `0` a `1`, por defecto `0.2`; se limita a ese rango.
* `floor` -- altura del plano del suelo, en unidades del mundo para un emisor 3D o en unidades del overlay para uno 2D, por defecto `0`. Solo la usa `COLLIDE_FLOOR`.
*Desde:* 3.00.
*Véase también:* <<emittersetgravity,emitterSetGravity>>, <<emittersettrail,emitterSetTrail>>, <<emittersetsoftness,emitterSetSoftness>>
.Ejemplo
[source,lua]
----
-- Las chispas rebotan en el suelo del taller (fila 440 del overlay) y dejan estelas.
sparks = emitterNew()
emitterSetBlend(sparks, PARTICLE_ADD)
emitterSetGravity(sparks, 0, 500)
emitterSetCollide(sparks, COLLIDE_FLOOR, 0.4, 0.2, 440)
emitterSetTrail(sparks, 6, 3)
-- Las brasas de la escena rebotan en lo que contenga el mundo físico.
emitterSetCollide(embers, COLLIDE_SCENE, 0.3, 0.5)
----
[#emittersetcolor]
==== emitterSetColor
[source,text]
----
emitterSetColor(emitter, r, g, b, a, r2, g2, b2, a2)
----
Color y opacidad al nacer (`r, g, b, a`) y al morir (`r2, g2, b2, a2`), cada uno de `0` a `255`, que cambian de manera uniforme a lo largo de la vida de la partícula; los valores fuera del rango se limitan. Por defecto, de blanco opaco a blanco transparente. El tinte multiplica la textura o el disco integrado, así que una imagen blanca toma el color exacto. Un fundido de entrada y salida necesita dos emisores o una textura que lleve esa forma; una receta sigue una sola rampa recta.
*Desde:* 3.00.
*Véase también:* <<emittersetblend,emitterSetBlend>>, <<emittersetlife,emitterSetLife>>, <<emittersetsize,emitterSetSize>>
.Ejemplo
[source,lua]
----
-- Llama: amarillo intenso en la base, que se apaga hasta un rojo tenue y desaparece.
emitterSetColor(flame, 255, 200, 90, 255, 255, 40, 0, 0)
-- Las motas de polvo aparecen desde la nada y se quedan flotando; su vida les pone fin.
emitterSetColor(dust, 255, 240, 200, 0, 255, 240, 200, 160)
----
[#emittersetspin]
==== emitterSetSpin
[source,text]
----
emitterSetSpin(emitter, min, max)
----
Velocidad de giro de cada partícula nueva en grados por segundo, sorteada entre los dos valores; los valores negativos giran en el otro sentido, y un rango que abarca el `0` da ambos. Toda partícula empieza con un ángulo aleatorio, gire o no. Por defecto, ninguno. El máximo se sube hasta el mínimo si es menor. El giro se nota en una partícula con textura o en una estela; el disco integrado es redondo y se ve igual en cualquier ángulo.
*Desde:* 3.00.
*Véase también:* <<emittersettexture,emitterSetTexture>>, <<emittersetsize,emitterSetSize>>
.Ejemplo
[source,lua]
----
-- Las hojas dan vueltas; las brasas apenas giran.
emitterSetTexture(leaves, leafSprite)
emitterSetSpin(leaves, -180, 180)
emitterSetSpin(embers, -20, 20)
----
[#emittersetradius]
==== emitterSetRadius
[source,text]
----
emitterSetRadius(emitter, radius)
----
Las partículas nuevas nacen en cualquier punto dentro de esta distancia del origen del emisor, repartidas uniformemente por un disco (2D) o una esfera (3D), en unidades del overlay o del mundo. Por defecto `0`, un punto; los valores negativos pasan a `0`. Un radio llena un volumen, la cabeza de una antorcha o una nube de polvo en el aire, donde un punto daría una aguja.
*Desde:* 3.00.
*Véase también:* <<emittersetspread,emitterSetSpread>>, <<emittersetposition,emitterSetPosition>>
.Ejemplo
[source,lua]
----
-- Polvo en una bola de aire del tamaño de una habitación, casi todo ya presente cuando empieza el nivel.
air = nodeNew()
nodeSetPosition(air, 0, 2, 0)
dust = emitterNew(air)
emitterSetRadius(dust, 2.5)
emitterSetSpread(dust, 180)
emitterSetSpeed(dust, 0.05, 0.2)
emitterSetLife(dust, 4, 6)
emitterStart(dust)
emitterBurst(dust, 120)
----
[#emittersetlit]
==== emitterSetLit
[source,text]
----
emitterSetLit(emitter, lit)
----
Solo 3D: sombrea cada partícula según las luces y el cielo de la escena como si fuera una pequeña esfera orientada hacia la cámara, así que el humo toma el color de un brasero y el polvo destella al sol. Desactivado por defecto; no se tienen en cuenta las sombras. Con `PARTICLE_ADD`, la iluminación se aplica igualmente al tinte antes de sumarlo. Un emisor 2D guarda el valor y lo ignora.
*Desde:* 3.00.
*Véase también:* <<emittersetsoftness,emitterSetSoftness>>, <<emittersetblend,emitterSetBlend>>
.Ejemplo
[source,lua]
----
-- El humo sobre cada brasero recoge la luz de su fuego.
for _, brazier in ipairs(braziers) do
local smoke = emitterNew(brazier.top)
emitterSetRate(smoke, 12)
emitterSetLife(smoke, 2.5, 4)
emitterSetColor(smoke, 90, 80, 75, 90, 50, 45, 45, 0)
emitterSetLit(smoke, true)
emitterStart(smoke)
end
----
[#emittersetlocal]
==== emitterSetLocal
[source,text]
----
emitterSetLocal(emitter, local)
----
Si las partículas vivas siguen al emisor. `false` (por defecto) deja cada una donde nació, así que una antorcha llevada por una habitación va dejando humo detrás; `true` mantiene toda la nube relativa al origen del emisor, así que el penacho de un propulsor se mueve con la nave. Cambiarlo pasa las partículas vivas de un sistema de referencia a otro sin saltos. En un emisor 3D, el origen es la posición en el mundo del nodo; en uno 2D es el punto de `emitterSetPosition`, así que una nube 2D local también se desliza con él. Las estelas siguen la misma regla.
*Desde:* 3.00.
*Véase también:* <<emittersetposition,emitterSetPosition>>, <<emittersetradius,emitterSetRadius>>
.Ejemplo
[source,lua]
----
-- Un aura mágica que sigue envolviendo a la hechicera mientras camina.
aura = emitterNew(nodeFind("Chest", wizard))
emitterSetLocal(aura, true)
emitterSetRadius(aura, 0.6)
emitterSetSpeed(aura, 0.05, 0.15)
emitterSetSpread(aura, 180)
emitterSetBlend(aura, PARTICLE_ADD)
emitterStart(aura)
----
[#emittersetmax]
==== emitterSetMax
[source,text]
----
emitterSetMax(emitter, count)
----
La reserva: cuántas partículas pueden estar vivas a la vez, por defecto `1000`. Una reserva llena no crea más, ni del flujo ni de una ráfaga, hasta que mueran algunas. Cambiar la cantidad reconstruye la reserva y mata todas las partículas vivas, así que fíjala antes de arrancar. Dimensiona la reserva como la tasa por la vida más larga, más la ráfaga más grande. Una cantidad menor que `1` termina el script.
*Desde:* 3.00.
*Véase también:* <<emittersetrate,emitterSetRate>>, <<emitterburst,emitterBurst>>, <<emittergetcount,emitterGetCount>>
.Ejemplo
[source,lua]
----
-- 300 gotas por segundo que viven hasta 1,6 segundos necesitan unas 480; deja margen.
fountain = emitterNew(spout)
emitterSetRate(fountain, 300)
emitterSetLife(fountain, 1.0, 1.6)
emitterSetMax(fountain, 600)
emitterStart(fountain)
----
[#emittersetposition]
==== emitterSetPosition
[source,text]
----
emitterSetPosition(emitter, x, y)
----
Solo 2D: dónde nacen las partículas nuevas, en coordenadas del overlay; por defecto `(0, 0)`. Fíjala en cada fotograma para seguir a un sprite o a un cuerpo físico 2D. Las partículas vivas se quedan donde están, salvo que `emitterSetLocal` esté activado. Un emisor 3D sigue a su nodo y aquí termina el script; mueve el nodo en su lugar.
*Desde:* 3.00.
*Véase también:* <<emitterdraw,emitterDraw>>, <<emittersetlocal,emitterSetLocal>>, <<emittersetradius,emitterSetRadius>>
.Ejemplo
[source,lua]
----
-- Las chispas salen de donde esté el sprite de la amoladora en este fotograma.
function onOverlayUpdate()
overlayClear()
spriteDraw(grinder, grinderX, grinderY)
emitterSetPosition(sparks, grinderX + 24, grinderY + 8)
emitterDraw(sparks)
return OVERLAY_UPDATED
end
----
[#emittersettrail]
==== emitterSetTrail
[source,text]
----
emitterSetTrail(emitter, length, width)
----
Una cinta detrás de cada partícula a través de sus últimas `length` posiciones, registradas una por fotograma (de `2` a `16`; `0` o `1` para ninguna, el valor por defecto), de `width` de ancho en unidades del mundo o del overlay, igual en 2D que en 3D. La cinta lleva el tinte actual de la partícula, se desvanece hasta transparente hacia su cola y se texturiza con la columna central de la imagen de la partícula, de modo que el borde suave del disco pasa a ser el de la cinta. Los valores se limitan al rango. Cambiar la longitud reconstruye el almacenamiento de las estelas, así que las partículas vivas empiezan de nuevo sus cintas. Chispas, brasas, balas trazadoras, cometas.
*Desde:* 3.00.
*Véase también:* <<emittersetcollide,emitterSetCollide>>, <<emittersetsize,emitterSetSize>>, <<emittersetlocal,emitterSetLocal>>
.Ejemplo
[source,lua]
----
-- Balas trazadoras: diminutas, rápidas y todo estela.
tracers = emitterNew(gunMuzzle)
emitterSetBlend(tracers, PARTICLE_ADD)
emitterSetSpeed(tracers, 40, 50)
emitterSetSpread(tracers, 2)
emitterSetSize(tracers, 0.04, 0.04)
emitterSetLife(tracers, 0.8, 1.0)
emitterSetTrail(tracers, 8, 0.03)
----
[#emitterstart]
==== emitterStart
[source,text]
----
emitterStart(emitter)
----
Inicia el flujo constante a la tasa fijada; a partir de entonces el emisor crea partículas en cada fotograma hasta `emitterStop`. Arrancar un emisor que ya está emitiendo no cambia nada. El flujo no necesita ninguna llamada de dibujo para funcionar; un emisor 2D que emite pero no se dibuja sigue gastando su reserva.
*Desde:* 3.00.
*Véase también:* <<emitterstop,emitterStop>>, <<emittersetrate,emitterSetRate>>, <<emitterburst,emitterBurst>>
.Ejemplo
[source,lua]
----
-- El motor se enciende cuando el jugador acelera y se apaga cuando corta el gas.
function onThrottle(open)
if open then
emitterStart(exhaust)
else
emitterStop(exhaust)
end
end
----
[#emitterstop]
==== emitterStop
[source,text]
----
emitterStop(emitter)
----
Termina el flujo; las partículas vivas acaban sus vidas y se descarta cualquier fracción de partícula pendiente. `emitterBurst` sigue funcionando en un emisor detenido, y `emitterIsActive` sigue siendo `true` hasta que muere la última partícula. Usa `emitterClear` para eliminar además las partículas de inmediato.
*Desde:* 3.00.
*Véase también:* <<emitterclear,emitterClear>>, <<emitterstart,emitterStart>>, <<emitterisactive,emitterIsActive>>
.Ejemplo
[source,lua]
----
-- La antorcha se apaga: detén la llama y deja que el último humo se aleje.
function onTorchDoused()
emitterStop(flame)
emitterStop(smoke)
soundPlay(hiss)
end
----
[#emitterburst]
==== emitterBurst
[source,text]
----
emitterBurst(emitter, count)
----
Crea `count` partículas a la vez con la receta actual, haya flujo o no: una explosión, una salpicadura, una bocanada. Solo se crean tantas como quepan en la reserva; `emitterSetMax` fija el espacio. Un emisor 2D lanza la ráfaga en su punto de `emitterSetPosition`, y uno 3D en su nodo, repartida según `emitterSetRadius`.
*Desde:* 3.00.
*Véase también:* <<emitterstart,emitterStart>>, <<emittersetmax,emitterSetMax>>, <<emittersetradius,emitterSetRadius>>
.Ejemplo
[source,lua]
----
-- Fuegos artificiales: una carcasa estalla en algún punto sobre el disco cada segundo, más o menos.
function onOverlayUpdate()
if frames % 60 == 0 then
emitterSetPosition(fireworks, 120 + (frames * 7) % (width - 240), 80 + (frames * 13) % 160)
emitterBurst(fireworks, 260)
end
overlayClear()
emitterDraw(fireworks)
frames = frames + 1
return OVERLAY_UPDATED
end
----
[#emitterclear]
==== emitterClear
[source,text]
----
emitterClear(emitter)
----
Mata ahora todas las partículas vivas y olvida cualquier fracción de partícula pendiente. El flujo no se detiene: un emisor arrancado sigue creando partículas nuevas en el siguiente fotograma, así que llama también a `emitterStop` para vaciarlo definitivamente. Úsala cuando cambie una escena y las chispas antiguas no deban quedarse.
*Desde:* 3.00.
*Véase también:* <<emitterstop,emitterStop>>, <<emitterdelete,emitterDelete>>
.Ejemplo
[source,lua]
----
-- Reiniciar el nivel: los emisores se quedan, las partículas antiguas se van.
function resetLevel()
for _, emitter in ipairs(levelEmitters) do
emitterStop(emitter)
emitterClear(emitter)
end
placePlayer()
end
----
[#emitterdraw]
==== emitterDraw
[source,text]
----
emitterDraw(emitter)
----
Solo 2D, desde `onOverlayUpdate`: pone en cola las partículas del emisor para dibujarlas en este fotograma, por encima o por debajo del overlay según `emitterSetLayer`, en coordenadas del overlay llevadas a la ventana. Los emisores de la misma capa se dibujan en el orden en que se pusieron en cola. La cola se vacía en cada fotograma, así que la llamada va en cada fotograma en que deban verse las partículas; no dibujar un emisor no lo detiene. Se dibujan hasta `64` emisores por capa en un fotograma. Un emisor 3D se dibuja solo en la escena y aquí termina el script.
*Desde:* 3.00.
*Véase también:* <<emittersetlayer,emitterSetLayer>>, <<emittersetposition,emitterSetPosition>>, <<onoverlayupdate,onOverlayUpdate>>
.Ejemplo
[source,lua]
----
function onOverlayUpdate()
overlayClear()
emitterSetPosition(smoke, chimneyX, chimneyY)
emitterDraw(smoke)
emitterDraw(sparks)
fontPrint(20, 20, "Score " .. score)
return OVERLAY_UPDATED
end
----
[#emittergetcount]
==== emitterGetCount
[source,text]
----
count = emitterGetCount(emitter)
----
Cuántas partículas del emisor están vivas en este fotograma, de `0` al tamaño de la reserva. Útil para ajustar una reserva, para un presupuesto en pantalla o para saber cuándo se ha apagado una ráfaga.
*Devuelve:* la cantidad de partículas vivas, un entero.
*Desde:* 3.00.
*Véase también:* <<emittersetmax,emitterSetMax>>, <<emitterisactive,emitterIsActive>>
.Ejemplo
[source,lua]
----
-- Overlay de desarrollo: cuánto de cada reserva está en uso.
function onOverlayUpdate()
overlayClear()
fontPrint(20, 20, string.format("live %d / %d", emitterGetCount(fireworks), fireworksPool))
return OVERLAY_UPDATED
end
----
[#emitterisactive]
==== emitterIsActive
[source,text]
----
active = emitterIsActive(emitter)
----
Si el emisor está emitiendo o aún tiene partículas vivas: `true` desde `emitterStart` o `emitterBurst` hasta que se detiene el flujo y muere la última partícula, `false` cuando la explosión ha terminado. Un emisor que está emitiendo y cuya reserva resulta estar vacía sigue activo.
*Devuelve:* booleano.
*Desde:* 3.00.
*Véase también:* <<emitterstop,emitterStop>>, <<emittergetcount,emitterGetCount>>, <<emitterdelete,emitterDelete>>
.Ejemplo
[source,lua]
----
-- Las explosiones de un solo uso viven en una lista y se liberan cuando se consumen.
function onOverlayUpdate()
for i = #explosions, 1, -1 do
if not emitterIsActive(explosions[i]) then
emitterDelete(explosions[i])
table.remove(explosions, i)
end
end
return OVERLAY_NOT_UPDATED
end
----
[#font]
=== Fuente
Singe tiene dos renderizadores de texto. La fuente de consola integrada la dibuja <<overlayprint,overlayPrint>> y no necesita recursos; la familia `font*` carga archivos TrueType con un tamaño en puntos fijo y los renderiza directamente sobre el overlay con `fontPrint` o en un sprite reutilizable con `fontToSprite`. Los handles de fuente son enteros que devuelve `fontLoad`, y el motor mantiene una única fuente seleccionada global, que `fontLoad` fija a la fuente que acaba de cargar y que `fontSelect` cambia. El texto se renderiza en el color de primer plano actual con la calidad elegida con `fontQuality`, y las posiciones del texto son coordenadas del overlay. Libera las fuentes con `fontUnload`; `Singe/FreeSansBold.ttf` viene con el motor y está siempre disponible.
[#fontload]
==== fontLoad
[source,text]
----
result = fontLoad(filename, pointSize)
----
Abre una fuente TrueType con un tamaño en puntos y la convierte en la fuente seleccionada. El tamaño es fijo durante toda la vida del handle, así que carga el mismo archivo una vez por cada tamaño que necesites. El nombre pasa por el sistema de archivos virtual como cualquier otro recurso; antepón `DIR` para los archivos que se distribuyen con tu juego, o usa `Singe/FreeSansBold.ttf` para el tipo de letra incluido. Un archivo que no se puede abrir termina el script con `Unable to open` y el nombre.
*Parámetros:*
* `filename` -- ruta a un archivo `.ttf`.
* `pointSize` -- tamaño en puntos, entero.
*Devuelve:* handle de fuente, entero.
*Desde:* 1.x
*Véase también:* <<fontselect,fontSelect>>, <<fontunload,fontUnload>>, <<fontquality,fontQuality>>
.Ejemplo
[source,lua]
----
-- Dos tamaños del tipo de letra incluido: grande para el título, pequeño para el HUD.
fontQuality(FONT_QUALITY_BLENDED)
titleFont = fontLoad("Singe/FreeSansBold.ttf", 32)
hudFont = fontLoad("Singe/FreeSansBold.ttf", 18)
fontSelect(titleFont)
titleSprite = fontToSprite("GAME OVER")
fontSelect(hudFont)
----
[#fontprint]
==== fontPrint
[source,text]
----
fontPrint(x, y, text)
----
Renderiza una cadena con la fuente seleccionada, la calidad actual y el color de primer plano actual (más el color de fondo con `FONT_QUALITY_SHADED`), y luego la vuelca sobre el overlay con su esquina superior izquierda en `(x, y)`. Llámala desde `onOverlayUpdate`, en cada fotograma si hace falta: el motor guarda cada cadena que renderiza, indexada por la fuente, la calidad, los colores y el texto, y una llamada posterior que pida la misma solo la vuelca. Las cadenas que no se imprimen durante un segundo, más o menos, se olvidan, y las cadenas de una fuente desaparecen cuando esta se descarga, así que el texto que cambia no cuesta más de lo que siempre costó y el que no cambia sale casi gratis. Reimprimir en cada fotograma una página entera de récords no es ningún problema. Sin una fuente seleccionada, el script termina con un error.
*Parámetros:*
* `x`, `y` -- esquina superior izquierda en coordenadas del overlay; las fracciones se truncan.
* `text` -- la cadena que se renderiza.
*Desde:* 1.x
*Véase también:* <<fonttosprite,fontToSprite>>, <<fontselect,fontSelect>>, <<colorforeground,colorForeground>>, <<overlayprint,overlayPrint>>
.Ejemplo
[source,lua]
----
-- Lectura en directo de la calibración de audio; el texto cambia en cada fotograma, así que se renderiza directamente.
function onOverlayUpdate()
overlayClear()
colorForeground(255, 255, 255, 255)
fontSelect(hudFont)
fontPrint(MARGIN_X, MARGIN_Y, "AUDIO DELAY CALIBRATION")
fontPrint(MARGIN_X, MARGIN_Y + 30, "Delay: " .. calibrationDelay .. " ms")
fontPrint(MARGIN_X, MARGIN_Y + 60, "Left / Right: 10 ms Up / Down: 1 ms")
return OVERLAY_UPDATED
end
----
[#fontquality]
==== fontQuality
[source,text]
----
fontQuality(mode)
----
Selecciona cómo rasterizan el texto `fontPrint` y `fontToSprite` a partir de ahora. El ajuste es global, no por fuente, y empieza en `FONT_QUALITY_SOLID`. Los sprites que ya creó `fontToSprite` conservan la calidad con la que se renderizaron. Un valor que no sea una de las tres constantes termina el script con un error.
*Parámetros:*
* `mode` -- `FONT_QUALITY_SOLID` (`1`): la más rápida, bordes de un bit, sin suavizado. `FONT_QUALITY_SHADED` (`2`): glifos suavizados sobre un rectángulo sólido del color de fondo. `FONT_QUALITY_BLENDED` (`3`): glifos suavizados con alfa por píxel que se mezclan sobre cualquier cosa; la opción habitual.
*Desde:* 1.x
*Véase también:* <<fontprint,fontPrint>>, <<fonttosprite,fontToSprite>>, <<colorbackground,colorBackground>>
.Ejemplo
[source,lua]
----
-- Texto mezclado sobre el vídeo, pero un recuadro de etiqueta sombreado para la lectura de depuración.
fontQuality(FONT_QUALITY_BLENDED)
titleSprite = fontToSprite("HIGH SCORES")
colorBackground(0, 0, 0, 255)
fontQuality(FONT_QUALITY_SHADED)
debugLabel = fontToSprite("FRAME")
fontQuality(FONT_QUALITY_BLENDED)
----
[#fontselect]
==== fontSelect
[source,text]
----
fontSelect(id)
----
Hace que una fuente cargada sea la que usan `fontPrint` y `fontToSprite`. La selección se mantiene hasta el siguiente `fontSelect` o `fontLoad`, ya que cargar también selecciona. Puede cambiarse tantas veces como haga falta, incluso varias veces dentro de un mismo `onOverlayUpdate`. Un handle desconocido o descargado (o ningún número) deja seleccionada la fuente actual e imprime un aviso, porque Hypseus Singe lo deja pasar.
*Desde:* 1.x
*Véase también:* <<fontload,fontLoad>>, <<fontprint,fontPrint>>, <<fonttosprite,fontToSprite>>
.Ejemplo
[source,lua]
----
-- El título con el tipo de letra grande y la puntuación con el pequeño, en cada fotograma.
function onOverlayUpdate()
overlayClear()
fontSelect(titleFont)
fontPrint(TITLE_X, 40, "HIGH SCORES")
fontSelect(hudFont)
fontPrint(8, 8, "Score: " .. score)
return OVERLAY_UPDATED
end
----
[#fonttosprite]
==== fontToSprite
[source,text]
----
result = fontToSprite(text)
----
Renderiza una cadena con la fuente, la calidad y los colores seleccionados en un sprite nuevo y devuelve su handle. A partir de ese momento el sprite es independiente de la fuente, así que sobrevive a `fontUnload`, y funciona con todas las llamadas `sprite*`: dibújalo, mídelo, escálalo o gíralo, y libéralo con `spriteUnload`. Para una cadena que cambia, renderiza un sprite nuevo y descarga el antiguo. Sin una fuente seleccionada, el script termina con un error.
NOTE: Renderizar texto en un sprite por rendimiento está desaconsejado. `fontPrint` ahora guarda lo que renderiza, así que imprimir la misma cadena en cada fotograma cuesta más o menos lo mismo que dibujar un sprite hecho con ella, sin el sprite que gestionar. Usa `fontToSprite` cuando el texto tenga que ser un sprite: para escalarlo, girarlo, voltearlo o medirlo con `spriteGetWidth` y `spriteGetHeight`.
*Devuelve:* handle de sprite, entero.
*Desde:* 1.x
*Véase también:* <<fontprint,fontPrint>>, <<spritedraw,spriteDraw>>, <<spritegetwidth,spriteGetWidth>>, <<spriteunload,spriteUnload>>
.Ejemplo
[source,lua]
----
-- Un título que entra girando y creciendo: texto que tiene que ser un sprite para poder girarlo y escalarlo.
titleSprite = fontToSprite("GAME OVER")
titleFrames = 0
function onOverlayUpdate()
titleFrames = titleFrames + 1
spriteRotate(titleSprite, (titleFrames * 12) % 360)
spriteScale(titleSprite, math.min(titleFrames / 30, 1))
spriteDraw(titleSprite, overlayGetWidth() / 2, overlayGetHeight() / 2, true)
return OVERLAY_UPDATED
end
----
[#fontunload]
==== fontUnload
[source,text]
----
fontUnload(id)
----
Cierra una fuente e invalida su handle. Los sprites creados a partir de ella con `fontToSprite` no se ven afectados. Si la fuente descargada era la seleccionada, después no queda ninguna fuente seleccionada, y el siguiente `fontPrint` o `fontToSprite` termina el script con un error hasta que `fontSelect` o `fontLoad` elija otra.
*Desde:* 2.00
*Véase también:* <<fontload,fontLoad>>, <<fontselect,fontSelect>>, <<onshutdown,onShutdown>>
.Ejemplo
[source,lua]
----
-- Libera las fuentes y los sprites de texto que produjeron.
function onShutdown()
spriteUnload(titleSprite)
for _, line in ipairs(descriptionLines) do
spriteUnload(line)
end
fontUnload(titleFont)
fontUnload(hudFont)
end
----
[#guiref]
=== Gui
Una GUI es un árbol de documentos de RmlUi de tamaño fijo que se renderiza en su propia textura en cada fotograma: `guiNew` crea una y devuelve un handle entero, `guiLoad` pone en ella un documento RML y devuelve un handle de documento, `guiDraw` la compone sobre el overlay y `materialSetGui` la muestra en un material de la escena. Los documentos son RML, parecido a HTML, con estilo en RCSS, parecido a CSS, y se cargan a través del sistema de archivos virtual como los sprites; `Singe/gui.rcss` es el tema incluido y `Singe/FreeSansBold.ttf` el tipo de letra de respaldo. Las funciones auxiliares por id `guiGetValue`, `guiSetValue` y `guiSetHandler` llegan a los elementos sin pasar por el modelo de objetos; todo lo demás es la API de Lua de RmlUi en la global `rmlui`, donde una GUI es el contexto llamado `gui` seguido de su handle. Puede haber hasta dieciséis GUI a la vez, cada una con treinta y dos documentos. Una GUI necesita el dispositivo GPU, así que `guiNew` termina el script en una máquina sin él; un handle de GUI incorrecto provoca `No GUI N`, y un handle de documento incorrecto, `No document N in GUI G`. Véase <<gui,GUI>> en Desarrollo de juegos.
[#guiclose]
==== guiClose
[source,text]
----
guiClose(gui, document)
----
Descarga el documento de la GUI, y con él sus elementos, sus manejadores y sus scripts. Después, el handle del documento ya no es válido. Un documento que cierra su propio script mediante la API de RmlUi (`document:Close()`) se descarga del mismo modo. Provoca `No document N in GUI G` con un handle que no es válido.
*Desde:* 3.00.
*Véase también:* <<guiload,guiLoad>>, <<guihide,guiHide>>, <<guidelete,guiDelete>>
.Ejemplo
[source,lua]
----
-- La página para introducir el récord solo se necesita una vez; descártala cuando esté el nombre.
guiSetHandler(gui, entryPage, "enter", "click", function(g, d)
scores[#scores + 1] = { name = guiGetValue(g, d, "name"), score = score }
guiClose(g, d)
entryPage = nil
end)
----
[#guidelete]
==== guiDelete
[source,text]
----
guiDelete(gui)
----
Libera la GUI, todos sus documentos y su textura, y detiene la actualización y el renderizado de cada fotograma que costaba. Los materiales que la mostraban mediante `materialSetGui` vuelven a su propia textura. Después, el handle ya no es válido y termina el script si se vuelve a usar; como una GUI sigue renderizándose se dibuje o no, borra las que un juego ya no necesite en lugar de ocultarlas.
*Desde:* 3.00.
*Véase también:* <<guinew,guiNew>>, <<guiclose,guiClose>>, <<materialsetgui,materialSetGui>>
.Ejemplo
[source,lua]
----
-- La página de opciones solo existe mientras el jugador está en ella.
function closeOptions()
guiDelete(optionsGui)
optionsGui = nil
discPlay()
end
----
[#guidraw]
==== guiDraw
[source,text]
----
guiDraw(gui)
guiDraw(gui, x, y)
guiDraw(gui, x, y, width, height)
----
Compone la textura de la GUI sobre el overlay en este fotograma, en coordenadas del overlay, así que respeta el borde Sinden y la escala de overscan como todo lo demás del overlay: cubriendo todo el overlay con un argumento, a su propio tamaño con una esquina en `x, y`, o escalada al rectángulo de `width` por `height`. El lugar de la llamada dentro del dibujo del script decide qué queda debajo de la GUI y qué encima: todo lo dibujado en el overlay antes de ella queda debajo, y todo lo dibujado después -- hasta el siguiente `guiDraw` o el siguiente turno del script -- va a una capa propia compuesta sobre esa GUI. Un puntero dibujado después del HUD se dibuja sobre el HUD; un segundo documento dibujado después de eso queda sobre el puntero, con su propia capa encima. Hasta dieciséis capas así por fotograma; una llamada que pase de ahí dibuja su GUI sin nada encima. `overlayClear` borra la capa en la que se esté dibujando, y `overlaySetMonochrome` solo pone en gris el overlay base. La llamada dura un fotograma; hazla desde `onOverlayUpdate` (o `singeMain`) en cada fotograma en que deba verse la GUI. Las GUI se dibujan sobre el overlay en el orden de las llamadas (la última, encima), por debajo de las partículas `PARTICLE_OVER` y del indicador de pausa, y la última GUI dibujada bajo el puntero es la que recibe el ratón. Se atienden hasta dieciséis llamadas por fotograma; las demás se ignoran. Cualquier otro número de argumentos provoca `Expected 1, 3 or 5 arguments`.
*Parámetros:*
* `x`, `y` -- la esquina superior izquierda, en coordenadas del overlay.
* `width`, `height` -- el tamaño al que se dibuja, en píxeles del overlay; el tamaño propio de la GUI si se omiten.
*Desde:* 3.00.
*Véase también:* <<guinew,guiNew>>, <<materialsetgui,materialSetGui>>, <<overlaysetresolution,overlaySetResolution>>
.Ejemplo
[source,lua]
----
-- El HUD está en la esquina inferior derecha, a un tercio de la anchura del overlay, y la mira de
-- la pistola se dibuja después, así que el panel nunca tapa la mira.
function onOverlayUpdate()
overlayClear()
guiSetValue(hud, hudPage, "lives", tostring(lives))
guiDraw(hud, overlayGetWidth() * 0.66, overlayGetHeight() * 0.8, overlayGetWidth() * 0.33, overlayGetHeight() * 0.2)
local x, y = mouseGetPosition(0)
spriteDraw(sight, x, y, true)
return OVERLAY_UPDATED
end
----
[#guidrawscreen]
==== guiDrawScreen
[source,text]
----
guiDrawScreen(gui)
guiDrawScreen(gui, x, y)
guiDrawScreen(gui, x, y, width, height)
----
Igual que <<guidraw,`guiDraw`>>, salvo que el rectángulo se mide sobre toda la ventana en lugar de sobre la imagen. Las coordenadas siguen siendo coordenadas del overlay -- `0, 0` es la esquina superior izquierda de la ventana y `overlayGetWidth(), overlayGetHeight()` la inferior derecha -- así que una GUI puede colocarse sobre la ilustración del bezel, junto a la imagen, encima o debajo de ella. Sin bezel, y sin `--scalefactor`, `--shiftx` ni `--shifty`, la imagen es toda la ventana y esto es exactamente `guiDraw`.
Las GUI en espacio de pantalla se dibujan después de todo lo demás, incluida la ilustración del bezel, y comparten con `guiDraw` los dieciséis dibujos por fotograma. Un clic las encuentra del mismo modo: el puntero se lleva al espacio de pantalla antes de comprobarlo, así que una GUI en espacio de pantalla que acepta entrada recibe el ratón dondequiera que se haya dibujado.
*Parámetros:*
* `x`, `y` -- la esquina superior izquierda, en coordenadas del overlay medidas sobre la ventana.
* `width`, `height` -- el tamaño al que se dibuja; el tamaño propio de la GUI si se omiten.
*Desde:* 3.00.
*Véase también:* <<guidraw,guiDraw>>, xref:Manual.adoc#bezels[Bezels], <<scorebezelenable,scoreBezelEnable>>
.Ejemplo
[source,lua]
----
-- Un mapa en el pilar izquierdo de la ilustración de la recreativa, fuera de la imagen.
function onOverlayUpdate()
overlayClear()
guiDrawScreen(mapGui, 8, 44, 38, 114)
return OVERLAY_UPDATED
end
----
[#guigetheight]
==== guiGetHeight
[source,text]
----
height = guiGetHeight(gui)
----
La altura de la GUI en sus propios píxeles, tal como se dio a `guiNew`. Termina el script con un handle que no es válido.
*Devuelve:* un entero.
*Desde:* 3.00.
*Véase también:* <<guigetwidth,guiGetWidth>>, <<guinew,guiNew>>
.Ejemplo
[source,lua]
----
-- Centra un pequeño cuadro de diálogo en el overlay.
local x = (overlayGetWidth() - guiGetWidth(dialog)) / 2
local y = (overlayGetHeight() - guiGetHeight(dialog)) / 2
guiDraw(dialog, x, y)
----
[#guigetstats]
==== guiGetStats
[source,text]
----
stats = guiGetStats()
----
Lo que costó renderizar todas las GUI en el último fotograma completo, para ajustar documentos que usan filtros, máscaras y sombras (cada uno de los cuales dibuja a través de una capa del tamaño de la GUI). Los contadores abarcan todas las GUI juntas. Todos valen cero en un fotograma en el que no cambió la imagen de ninguna GUI, ya que no se volvió a dibujar nada (véase <<guirefresh,guiRefresh>>).
*Devuelve:* una tabla con los campos `drawCalls` (dibujos indexados, incluidas las escrituras de máscara, las composiciones y las pasadas de filtro), `layerPushes` (capas en las que se entra por filtros, imágenes de máscara y sombras de caja), `filterPasses` (pasadas a tamaño completo y copias dentro de las cadenas de filtros; un desenfoque cuesta varias), `maskWrites` (dibujos en la máscara de recorte del stencil), `layerBytes` (bytes de texturas de capa reservados en este momento; cada una tiene el tamaño de su GUI a cuatro bytes por píxel) y `passes` (pasadas de renderizado iniciadas; cada cambio de capa inicia una).
*Desde:* 3.00.
*Véase también:* <<guinew,guiNew>>, <<gui,GUI>>
.Ejemplo
[source,lua]
----
-- Observa lo que cuesta una pantalla llena de efectos.
local s = guiGetStats()
debugPrint(string.format("gui draws %d layers %d filters %d passes %d, %d KB of layers", s.drawCalls, s.layerPushes, s.filterPasses, s.passes, s.layerBytes // 1024))
----
[#guigetvalue]
==== guiGetValue
[source,text]
----
value = guiGetValue(gui, document, id)
----
El valor del elemento con ese `id` en el documento, como cadena: el valor de un control de formulario (el texto de un campo de texto, el `value` de una opción de `select`, un control de rango con el formato propio de RmlUi, como `"85.000000"`, el atributo `value` de una casilla de verificación o de un botón de opción, o `"on"` cuando no lo tiene, esté marcado o no; su estado es su atributo `checked`, que se lee mediante la API de RmlUi) o, para cualquier otro elemento, su RML interno. Devuelve `nil` cuando ningún elemento tiene ese id, así que se puede sondear una página sin terminar el script; un handle de GUI o de documento incorrecto sí lo termina. Los valores de más de 4095 caracteres se recortan.
*Devuelve:* una cadena, o `nil`.
*Desde:* 3.00.
*Véase también:* <<guisetvalue,guiSetValue>>, <<guisethandler,guiSetHandler>>
.Ejemplo
[source,lua]
----
-- Lee todo el formulario cuando se pulsa Start.
guiSetHandler(gui, doc, "go", "click", function(g, d)
player.name = guiGetValue(g, d, "name")
player.hard = guiGetValue(g, d, "difficulty") == "hard"
player.volume = math.floor(tonumber(guiGetValue(g, d, "volume")))
end)
----
[#guigetwidth]
==== guiGetWidth
[source,text]
----
width = guiGetWidth(gui)
----
La anchura de la GUI en sus propios píxeles, tal como se dio a `guiNew`. Termina el script con un handle que no es válido.
*Devuelve:* un entero.
*Desde:* 3.00.
*Véase también:* <<guigetheight,guiGetHeight>>, <<guinew,guiNew>>
.Ejemplo
[source,lua]
----
-- ¿Está el punto sondeado en la mitad derecha de la página del monitor?
local g, px, py = sceneProbeGui(mouseX, mouseY)
if g ~= nil and px > guiGetWidth(g) / 2 then
showRightTooltip()
end
----
[#guihide]
==== guiHide
[source,text]
----
guiHide(gui, document)
----
Oculta el documento: no se dibuja ni recibe entrada, pero conserva sus elementos y valores, y `guiShow` lo devuelve tal como estaba. Provoca `No document N in GUI G` con un handle que no es válido.
*Desde:* 3.00.
*Véase también:* <<guishow,guiShow>>, <<guiclose,guiClose>>
.Ejemplo
[source,lua]
----
-- Dos páginas en una GUI: la página de opciones se oculta mientras se muestra la de controles.
guiSetHandler(gui, options, "controls", "click", function()
guiHide(gui, options)
guiShow(gui, controls)
end)
----
[#guiload]
==== guiLoad
[source,text]
----
document = guiLoad(gui, filename)
----
Carga un documento RML en la GUI, lo muestra y devuelve un handle de documento entero para las demás llamadas. El nombre pasa por el sistema de archivos virtual como el de un sprite, relativo a la carpeta desde la que se arrancó Singe, así que antepón `DIR` para los archivos que se distribuyen con tu juego; las hojas de estilo, imágenes y fuentes nombradas dentro del documento se resuelven relativas al archivo que las nombra y luego mediante la misma búsqueda, así que un juego empaquetado las encuentra. Los bloques `<script>` y los atributos `on*` del documento se ejecutan en el estado Lua del juego, y el documento es accesible como `rmlui.contexts["guiN"].documents["<body id>"]`. Un documento que no se puede cargar termina el script con el nombre y el motivo que da RmlUi (un archivo que falta, un error de análisis); los propios avisos de RmlUi sobre el marcado y el estilo van a la consola. Cada GUI admite hasta treinta y dos documentos.
*Devuelve:* el handle del documento, un entero.
*Desde:* 3.00.
*Véase también:* <<guinew,guiNew>>, <<guishow,guiShow>>, <<guihide,guiHide>>, <<guiclose,guiClose>>, <<guiloadfont,guiLoadFont>>
.Ejemplo
[source,lua]
----
-- La página del título y el HUD comparten una GUI del tamaño del overlay.
gui = guiNew(overlayGetWidth(), overlayGetHeight())
title = guiLoad(gui, DIR .. "gui/title.rml")
hud = guiLoad(gui, DIR .. "gui/hud.rml")
guiHide(gui, hud)
----
[#guiloadfont]
==== guiLoadFont
[source,text]
----
guiLoadFont(filename)
guiLoadFont(filename, fallback)
----
Carga un tipo de letra TrueType u OpenType para todas las GUI, de modo que los documentos puedan nombrarlo en `font-family` por el nombre de familia que lleva el archivo (con `font-weight` y `font-style` eligiendo entre los tipos de una misma familia). Con `fallback` a `true`, el tipo de letra también sirve cualquier glifo que no tenga ningún otro, que es lo que hace hasta entonces el propio `Singe/FreeSansBold.ttf` del motor (familia `FreeSans`, peso negrita, el tipo de letra que usa el tema incluido). Carga las fuentes antes que los documentos que las usan. Cada tipo de letra cuesta un atlas de glifos por cada tamaño en que se use. Un archivo que no se puede abrir termina el script con `Unable to load` y el nombre.
*Parámetros:*
* `filename` -- el archivo de la fuente, a través del sistema de archivos virtual, como en `fontLoad`.
* `fallback` -- `true` para que sea el tipo de letra de respaldo; `false` (por defecto) para uno corriente.
*Desde:* 3.00.
*Véase también:* <<guiload,guiLoad>>, <<fontload,fontLoad>>
.Ejemplo
[source,lua]
----
-- El tipo de letra de exhibición del juego para los títulos; el FreeSans incluido se queda para el texto.
guiLoadFont(DIR .. "fonts/Orbitron-Bold.ttf")
gui = guiNew(overlayGetWidth(), overlayGetHeight())
menu = guiLoad(gui, DIR .. "gui/menu.rml")
----
[#guinew]
==== guiNew
[source,text]
----
gui = guiNew(width, height)
----
Crea una GUI de `width` por `height` píxeles, que se renderiza en su propia textura en cada fotograma se dibuje o no, y devuelve su handle. El tamaño es el espacio de coordenadas del documento: el `100%` del body es toda la textura, y un `dp` es uno de sus píxeles. Una GUI del tamaño del overlay sirve para una página a pantalla completa; un elemento del HUD o un monitor en la escena piden una más pequeña, y una Raspberry Pi pide que todo sea modesto. Una GUI nueva solo muestra, hasta que `guiSetInput` activa la entrada. Provoca `The size must be positive` con un tamaño cero o negativo, y `No GUI available` cuando ya hay dieciséis en uso o la máquina no tiene dispositivo GPU (una GUI necesita el mismo dispositivo que la escena 3D).
*Parámetros:*
* `width` -- la anchura de la textura en píxeles.
* `height` -- la altura de la textura en píxeles.
*Devuelve:* el handle de la GUI, un entero.
*Desde:* 3.00.
*Véase también:* <<guiload,guiLoad>>, <<guidraw,guiDraw>>, <<guidelete,guiDelete>>, <<materialsetgui,materialSetGui>>
.Ejemplo
[source,lua]
----
-- Una página del tamaño del overlay, dibujada sobre el disco.
overlaySetResolution(discGetWidth(), discGetHeight())
gui = guiNew(overlayGetWidth(), overlayGetHeight())
doc = guiLoad(gui, DIR .. "gui/attract.rml")
function onOverlayUpdate()
overlayClear()
guiDraw(gui)
return OVERLAY_UPDATED
end
----
[#guirefresh]
==== guiRefresh
[source,text]
----
guiRefresh(gui)
----
Vuelve a dibujar la GUI en el siguiente fotograma aunque nada en ella parezca haber cambiado. Una GUI solo se vuelve a dibujar cuando cambia su imagen: cuando el documento de un fotograma dibuja exactamente lo que dibujó el fotograma anterior y no se ha creado nada nuevo entretanto, la textura ya contiene esa imagen y se conserva, lo que ahorra a la GPU la mayor parte del coste de una GUI en los fotogramas en que un HUD o un menú están quietos. Se detectan todos los cambios hechos mediante RML, RCSS, la interfaz Lua de RmlUi o las llamadas `gui*`. Esta llamada es la salida si alguna vez se pasara uno por alto -- una GUI que deja de actualizarse cuando debería -- y un juego no debería necesitarla; si la necesita, merece la pena informar de ello. Una GUI que no existe termina el script.
*Desde:* 3.00.
*Véase también:* <<guidraw,guiDraw>>, <<guigetstats,guiGetStats>>
.Ejemplo
[source,lua]
----
-- Un apaño mientras se informa de un cambio no detectado: vuelve a dibujar el HUD en cada fotograma.
function onOverlayUpdate()
guiRefresh(hud)
guiDraw(hud)
return OVERLAY_UPDATED
end
----
[#guisethandler]
==== guiSetHandler
[source,text]
----
guiSetHandler(gui, document, id, event, function)
guiSetHandler(gui, document, id, event, nil)
----
Llama a `function(gui, document, id, event, value)` cada vez que el elemento con ese `id` dispara el evento indicado: `"click"` para botones y filas, `"change"` para controles de formulario, `"submit"` para un formulario, o cualquier otro nombre de evento de RmlUi (`"focus"`, `"mouseover"`, `"keydown"`, ...). `value` es el valor del elemento en ese momento, como cadena, tal como lo devolvería `guiGetValue` (un control de rango llega como lo formatea RmlUi, `"85.000000"`, así que pásalo por `tonumber`). Se guarda una función por elemento y evento; fijar otra la sustituye, y `nil` la quita. El manejador pertenece al `id`, no a un elemento concreto: cuando el elemento se sustituye -- una lista reconstruida mediante `inner_rml` crea elementos nuevos -- vuelve a fijar el manejador después de la reconstrucción y se engancha al elemento que lleva ahora ese id. Los manejadores se ejecutan a medida que ocurren los eventos, entre los callbacks del propio juego, y un error en uno de ellos termina el juego como un error en cualquier callback. Los ids tienen como máximo 63 caracteres. Provoca `No element "id" in document N of GUI G` cuando el elemento no existe, y `Argument 5 must be a function or nil` con cualquier otra cosa en esa posición.
*Parámetros:*
* `id` -- el atributo `id` del elemento.
* `event` -- el nombre del evento de RmlUi.
* `function` -- la función Lua a la que llamar, o `nil` para dejar de llamar a ninguna.
*Desde:* 3.00.
*Véase también:* <<guigetvalue,guiGetValue>>, <<guisetvalue,guiSetValue>>, <<guisetinput,guiSetInput>>
.Ejemplo
[source,lua]
----
-- Un control deslizante de volumen surte efecto mientras se mueve; Start lee el resto del formulario.
guiSetHandler(gui, doc, "volume", "change", function(g, d, id, event, value)
soundSetVolume(math.floor(tonumber(value)))
end)
guiSetHandler(gui, doc, "go", "click", function(g, d)
startGame(guiGetValue(g, d, "name"), guiGetValue(g, d, "difficulty"))
end)
----
[#guisetinput]
==== guiSetInput
[source,text]
----
guiSetInput(gui, enabled)
----
Si el ratón y la pistola de luz, las teclas, el texto escrito y los interruptores del framework procedentes de un mando o de un ratón llegan a la GUI. Una GUI nueva solo muestra, lo que sirve para un HUD o un cartel; activa la entrada para un menú o un formulario. Mientras está activada, el puntero llega a la GUI dondequiera que se dibuje en plano (la última dibujada, encima) o se muestre sobre una superficie de la escena; todas las teclas van a ella excepto `Tab`, que la GUI solo recibe mientras se escribe en un campo de texto (RmlUi la usa para mover el foco y nunca la devuelve, y un juego cuyas teclas incluyan `Tab` la perdería si no); un campo de texto con el foco activa la entrada de texto de SDL; y las direcciones del mando llegan como teclas de flecha, con `ACTION_1` como Intro y `ACTION_2` como Esc. Lo que usa un elemento nunca llega a `onInputPressed`, `onKeyPressed` ni a los callbacks del ratón; lo que nada usó les llega como de costumbre. Termina el script con un handle que no es válido.
*Desde:* 3.00.
*Véase también:* <<guisethandler,guiSetHandler>>, <<guidraw,guiDraw>>, <<sceneprobegui,sceneProbeGui>>
.Ejemplo
[source,lua]
----
-- El HUD nunca recibe entrada; la página de pausa sí, mientras está a la vista.
guiSetInput(hud, false)
function onInputPressed(what)
if what == SWITCH_START1 then
guiShow(gui, pausePage)
guiSetInput(gui, true)
singeSetPauseFlag(true)
end
end
----
[#guisetvalue]
==== guiSetValue
[source,text]
----
guiSetValue(gui, document, id, value)
----
Fija el valor del elemento con ese `id`: el valor de un control de formulario (el texto de un campo de texto, el número de un control de rango, el `value` de una opción de `select`; el estado `checked` de una casilla de verificación es un atributo y se fija mediante la API de RmlUi) o, para cualquier otro elemento, su RML interno, así que un párrafo o un `div` pueden reescribirse con texto simple o con marcado. El documento se vuelve a maquetar en el siguiente fotograma. Provoca `No element "id" in document N of GUI G` cuando el elemento no existe.
*Parámetros:*
* `id` -- el atributo `id` del elemento.
* `value` -- el nuevo valor, una cadena; los números se convierten.
*Desde:* 3.00.
*Véase también:* <<guigetvalue,guiGetValue>>, <<guisethandler,guiSetHandler>>
.Ejemplo
[source,lua]
----
-- La línea de puntuación y la fila de vidas se reescriben en cada fotograma; la fila es marcado.
function onOverlayUpdate()
guiSetValue(hud, hudPage, "score", string.format("%08d", score))
guiSetValue(hud, hudPage, "lives", string.rep("<img src=\"ship.png\"/>", lives))
overlayClear()
guiDraw(hud)
return OVERLAY_UPDATED
end
----
[#guishow]
==== guiShow
[source,text]
----
guiShow(gui, document)
----
Muestra un documento ocultado con `guiHide` (un documento ya se muestra cuando `guiLoad` vuelve, así que entonces la llamada no hace falta). Un documento visible se dibuja dondequiera que se dibuje su GUI y recibe entrada cuando su GUI la recibe. Provoca `No document N in GUI G` con un handle que no es válido.
*Desde:* 3.00.
*Véase también:* <<guihide,guiHide>>, <<guiload,guiLoad>>
.Ejemplo
[source,lua]
----
-- La página de fin de partida aparece cuando se pierde la última vida.
function onLastLifeLost()
guiSetValue(gui, gameOver, "final", string.format("%08d", score))
guiShow(gui, gameOver)
end
----
[#joint]
=== Articulación
Las articulaciones unen dos cuerpos, o un cuerpo y el mundo cuando el segundo nodo es `-1`, y devuelven un handle de articulación entero que reciben las demás llamadas. Los anclajes y los ejes se dan en espacio del mundo en el momento en que se crea la articulación, con los cuerpos donde estén entonces, así que coloca los nodos y dales cuerpo antes de unirlos. Las distancias van en unidades del mundo y los ángulos de bisagra en grados. Borrar cualquiera de los dos cuerpos, o su nodo, borra la articulación; un handle que se ha borrado termina el script cuando se vuelve a pasar. Véase <<physics,Física>> en Desarrollo de juegos, Articulaciones.
[#jointhinge]
==== jointHinge
[source,text]
----
joint = jointHinge(nodeA, nodeB, ax, ay, az, dx, dy, dz)
----
Crea una bisagra entre los dos cuerpos que pasa por el punto del mundo `(ax, ay, az)` a lo largo del eje `(dx, dy, dz)`: una puerta, una rueda, una palanca, un balancín. Los cuerpos comparten ese punto y ese eje y solo pueden girar alrededor de él. El eje se normaliza, así que su longitud no importa; un eje nulo se convierte en `(0, 1, 0)`. Los dos nodos deben llevar un cuerpo, o `nodeB` puede ser `-1` para unir `nodeA` al mundo con una bisagra. Termina el script cuando un nodo no tiene cuerpo o no se puede crear la articulación.
*Parámetros:*
* `nodeA` -- el cuerpo cuyo giro se mide; un ángulo positivo de `jointSetLimits` es su giro alrededor del eje según la regla de la mano derecha.
* `nodeB` -- el otro cuerpo, o `-1` para el mundo.
* `ax`, `ay`, `az` -- el anclaje, un punto del mundo por el que pasa el eje de la bisagra.
* `dx`, `dy`, `dz` -- el eje de la bisagra en espacio del mundo.
*Devuelve:* El handle de la articulación.
*Desde:* 3.00.
*Véase también:* <<jointsetlimits,jointSetLimits>>, <<jointdelete,jointDelete>>, <<jointball,jointBall>>
.Ejemplo
[source,lua]
----
-- Una puerta unida al mundo con una bisagra en su borde izquierdo, que se abre 100 grados hacia un lado.
door = nodeNew()
nodeSetMesh(door, meshBox(1.4, 2.2, 0.1), wood)
nodeSetPosition(door, -0.1, -0.62, 1.4)
bodyNew(door, BODY_DYNAMIC, SHAPE_BOX, 1.4, 2.2, 0.1)
bodySetMass(door, 8)
hinge = jointHinge(door, -1, -0.85, -0.62, 1.4, 0, 1, 0)
jointSetLimits(hinge, 0, 100)
-- Más tarde, un empujón cerca de la manilla la abre.
bodyApplyImpulse(door, 0, 0, -60, 0.5, -0.62, 1.45)
----
[#jointball]
==== jointBall
[source,text]
----
joint = jointBall(nodeA, nodeB, ax, ay, az)
----
Crea una articulación de rótula en el punto del mundo `(ax, ay, az)`: los cuerpos pueden girar en cualquier sentido alrededor de él, pero no pueden separarse. Un péndulo, un eslabón de cadena, un letrero colgante, el hombro de un ragdoll. Los dos nodos deben llevar un cuerpo, o `nodeB` puede ser `-1` para colgar `nodeA` de un punto fijo del mundo. Una rótula no tiene límites, así que `jointSetLimits` la rechaza. Termina el script cuando un nodo no tiene cuerpo o no se puede crear la articulación.
*Parámetros:*
* `nodeA` -- el primer cuerpo.
* `nodeB` -- el otro cuerpo, o `-1` para el mundo.
* `ax`, `ay`, `az` -- el anclaje, el punto del mundo alrededor del cual pivotan los cuerpos.
*Devuelve:* El handle de la articulación.
*Desde:* 3.00.
*Véase también:* <<jointhinge,jointHinge>>, <<jointdelete,jointDelete>>, <<bodyapplyimpulse,bodyApplyImpulse>>
.Ejemplo
[source,lua]
----
-- Una cadena de cuatro eslabones colgada de un gancho, cada uno unido con rótula al de arriba.
local above = -1
for i = 1, 4 do
local link = nodeNew()
nodeSetMesh(link, meshSphere(0.12, 16), iron)
nodeSetPosition(link, 2, 3 - i * 0.3, 0)
bodyNew(link, BODY_DYNAMIC, SHAPE_SPHERE, 0.12)
bodySetMass(link, 1)
jointBall(link, above, 2, 3.15 - i * 0.3, 0)
above = link
end
----
[#jointslider]
==== jointSlider
[source,text]
----
joint = jointSlider(nodeA, nodeB, ax, ay, az, dx, dy, dz)
----
Deja que los cuerpos se deslicen uno respecto al otro solo a lo largo del eje `(dx, dy, dz)` que pasa por el anclaje `(ax, ay, az)`, conservando su orientación: un cajón, un pistón, un ascensor, una puerta corredera. El eje se normaliza, así que su longitud no importa; un eje nulo se convierte en `(0, 1, 0)`. Los dos nodos deben llevar un cuerpo, o `nodeB` puede ser `-1` para hacer correr `nodeA` por un raíl fijo en el mundo. Termina el script cuando un nodo no tiene cuerpo o no se puede crear la articulación.
*Parámetros:*
* `nodeA` -- el cuerpo cuyo recorrido se mide; una distancia positiva de `jointSetLimits` va en el sentido del eje.
* `nodeB` -- el otro cuerpo, o `-1` para el mundo.
* `ax`, `ay`, `az` -- un punto del mundo sobre el raíl.
* `dx`, `dy`, `dz` -- la dirección del recorrido en espacio del mundo.
*Devuelve:* El handle de la articulación.
*Desde:* 3.00.
*Véase también:* <<jointsetlimits,jointSetLimits>>, <<jointhinge,jointHinge>>, <<bodysetvelocity,bodySetVelocity>>
.Ejemplo
[source,lua]
----
-- Una caja sobre un raíl a lo largo de X, empujada de lado al principio y detenida 2.4 unidades más allá.
crate = nodeNew()
nodeSetMesh(crate, meshBox(0.5, 0.5, 0.5), wood)
nodeSetPosition(crate, -0.6, 0.9, -3.0)
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.5, 0.5, 0.5)
slider = jointSlider(crate, -1, -0.6, 0.9, -3.0, 1, 0, 0)
jointSetLimits(slider, 0, 2.4)
bodySetVelocity(crate, 3, 0, 0)
----
[#jointsetlimits]
==== jointSetLimits
[source,text]
----
jointSetLimits(joint, low, high)
----
Limita el ángulo de una bisagra en grados, o el recorrido de una corredera en unidades del mundo, a cada lado de donde empezó la articulación. Un ángulo de bisagra positivo es un giro de `nodeA` alrededor del eje según la regla de la mano derecha; una distancia de corredera positiva va en el sentido del eje. `low` se recorta a `0` como mucho y `high` a `0` como poco, así que la postura inicial siempre queda dentro del rango. Llámala otra vez para cambiar el rango en cualquier momento. Termina el script para una rótula o un handle que ya no existe.
*Parámetros:*
* `joint` -- el handle de `jointHinge` o `jointSlider`.
* `low` -- el ángulo o la distancia negativos más lejanos, `0` o menos.
* `high` -- el ángulo o la distancia positivos más lejanos, `0` o más.
*Notas:* Los dos cuerpos intercambian sus papeles si se dan al revés, lo que invierte el signo del rango.
*Desde:* 3.00.
*Véase también:* <<jointhinge,jointHinge>>, <<jointslider,jointSlider>>
.Ejemplo
[source,lua]
----
-- Una palanca que oscila 30 grados a cada lado y se bloquea una vez resuelto el puzle.
lever = nodeNew()
nodeSetMesh(lever, meshBox(0.1, 0.8, 0.1), iron)
nodeSetPosition(lever, 4, 1.2, 0)
bodyNew(lever, BODY_DYNAMIC, SHAPE_BOX, 0.1, 0.8, 0.1)
pivot = jointHinge(lever, -1, 4, 0.8, 0, 0, 0, 1)
jointSetLimits(pivot, -30, 30)
function lockLever()
jointSetLimits(pivot, 0, 0)
end
----
[#jointdelete]
==== jointDelete
[source,text]
----
jointDelete(joint)
----
Elimina la articulación; los cuerpos vuelven a quedar libres el uno del otro y conservan la velocidad que tuvieran. Las articulaciones también desaparecen con cualquiera de sus cuerpos, así que esto solo hace falta para liberar dos cuerpos que se quedan los dos. Termina el script para un handle que no existe o que ya se borró.
*Desde:* 3.00.
*Véase también:* <<jointhinge,jointHinge>>, <<jointball,jointBall>>, <<bodydelete,bodyDelete>>
.Ejemplo
[source,lua]
----
-- La cuerda se rompe cuando la caja que cuelga de ella recibe un golpe lo bastante fuerte.
function onCollision(a, b, x, y, z, speed)
if (a == crate or b == crate) and speed > 6 and rope then
jointDelete(rope)
rope = nil
soundPlay(snap)
end
end
----
[#joystick]
=== Joystick
Todo dispositivo de juego que abre Singe es un mando y se lee con las llamadas
`controller*`; un dispositivo que SDL no reconoce llega a ellas mediante una asignación
que Singe escribe para él, así que no hay nada más que llamar. Véase xref:Manual.adoc#devices[Todo
dispositivo es un mando]. Las dos llamadas de aquí sirven para que un stick haga de ratón.
[#joymouseenable]
==== joyMouseEnable
[source,text]
----
joyMouseEnable(enabled)
----
Extensión de Hypseus. Activa o desactiva el joystick como ratón mientras el juego se ejecuta. Activado, el stick izquierdo del mando de la ranura `0` mueve el puntero que ve el juego, mediante el mismo `onMouseMoved` al que llega un ratón real, sin salir de la imagen; `--js_range` es la velocidad y el eje respeta su propia zona muerta. Es lo que hace jugable un juego de pistola de luz en una recreativa con un stick y sin ratón.
Empieza según `--joymouse`, que está desactivado por defecto, y un `singeReload` lo devuelve ahí. Singe difiere de Hypseus en ese valor por defecto: el `controls.cfg` que se distribuye ya asigna ese stick a los cuatro interruptores de dirección, y mover además el cursor sin que nadie lo pida sorprendería a todos los juegos existentes.
*Parámetros:*
* `enabled` -- booleano.
*Devuelve:* nada.
*Desde:* 3.00 (Hypseus)
*Véase también:* <<joymouseisenabled,joyMouseIsEnabled>>, <<onmousemoved,onMouseMoved>>, <<mousegetposition,mouseGetPosition>>
.Ejemplo
[source,lua]
----
-- Un juego de pistola que deja apuntar con el stick cuando no hay ningún ratón conectado.
if mouseHowMany() == 0 then
joyMouseEnable(true)
end
----
[#joymouseisenabled]
==== joyMouseIsEnabled
[source,text]
----
enabled = joyMouseIsEnabled()
----
Si el stick está moviendo el cursor del ratón en este momento, tanto si lo decidió `--joymouse` como `joyMouseEnable`.
*Devuelve:* booleano.
*Desde:* 3.00
*Véase también:* <<joymouseenable,joyMouseEnable>>
[#keyboard]
=== Teclado
Singe lee el teclado en uno de dos modos, elegido con `keyboardSetMode`. En `MODE_NORMAL` (el modo por defecto) las teclas se traducen mediante las asignaciones de `controls.cfg` a los valores `SWITCH_*` que llegan a `onInputPressed` y `onInputReleased`, y las teclas sin asignar se ignoran; en `MODE_FULL` cada tecla llega al script en bruto, mediante `onKeyPressed` y `onKeyReleased` además de `onInputPressed` y `onInputReleased`. Sea cual sea el modo, `keyboardIsDown`, `keyboardGetLastDown` y `keyboardGetLastUp` consultan el estado del teclado en vivo, que es lo que quiere el modelo con hilo (véase <<threaded,Con hilo>>). Un scancode nombra la posición física de una tecla (la tecla `A` es el scancode `4` en cualquier distribución), mientras que un keysym es el carácter que produce la tecla; la tabla `SCANCODE` de `Framework.singe` nombra cada scancode, `keyboardIsDown` recibe scancodes y `onKeyPressed` entrega ambos. Los propios callbacks se describen en <<enginecallbacks,Callbacks del motor>>.
[#keyboardcatchquit]
==== keyboardCatchQuit
[source,text]
----
keyboardCatchQuit(catch)
----
Extensión de Hypseus. Cede el interruptor de salida al script, exactamente como hace `singeSetQuitKeyEnabled(not catch)`: `keyboardCatchQuit(true)` impide que el motor salga con `INPUT_QUIT`, y `false` lo restablece. Es una función definida en `Framework.singe`, que se conserva para que los juegos escritos para Hypseus Singe funcionen sin cambios.
*Notas:* Alias heredado.
WARNING: El código nuevo debería llamar a <<singesetquitkeyenabled,singeSetQuitKeyEnabled>>.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<singesetquitkeyenabled,singeSetQuitKeyEnabled>>, <<singequit,singeQuit>>
.Ejemplo
[source,lua]
----
-- Código de un juego de Hypseus; la forma moderna es singeSetQuitKeyEnabled(false).
if keyboardCatchQuit ~= nil then
keyboardCatchQuit(true)
end
function onInputPressed(what)
if what == SWITCH_QUIT then
askToQuit()
end
end
----
[#keyboardgetlastdown]
==== keyboardGetLastDown
[source,text]
----
scancode = keyboardGetLastDown()
----
Devuelve el scancode de la última tecla pulsada en el fotograma actual, o `0` si no se ha pulsado nada desde el fotograma anterior. El valor se borra después de cada llamada a `onOverlayUpdate`, lo que lo convierte en una consulta de un solo uso, «¿se acaba de pulsar una tecla?», para el modelo con hilo. Los botones del mando y del ratón también se registran aquí, con sus códigos `GAMEPAD_N` y `MOUSE_N`, ya que el motor los encamina por la misma vía que las teclas. El valor se guarda sea cual sea el modo de teclado y al margen de las asignaciones de `controls.cfg`.
*Devuelve:* scancode entero, o `0`.
*Desde:* 2.10
*Véase también:* <<keyboardgetlastup,keyboardGetLastUp>>, <<keyboardisdown,keyboardIsDown>>
.Ejemplo
[source,lua]
----
-- Modelo con hilo: reacciona a una pulsación de tecla por fotograma.
function singeMain()
while true do
local key = keyboardGetLastDown()
if key == SCANCODE.ESCAPE.value then
singeQuit()
elseif key == SCANCODE.SPACE.value then
jump()
end
singeYield()
end
end
----
[#keyboardgetlastup]
==== keyboardGetLastUp
[source,text]
----
scancode = keyboardGetLastUp()
----
Devuelve el scancode de la última tecla soltada en el fotograma actual, o `0` si no se ha soltado nada desde el fotograma anterior. Se borra después de cada llamada a `onOverlayUpdate`, como `keyboardGetLastDown`, y registra de la misma forma los códigos de los botones del mando y del ratón.
*Devuelve:* scancode entero, o `0`.
*Desde:* 2.10
*Véase también:* <<keyboardgetlastdown,keyboardGetLastDown>>, <<keyboardisdown,keyboardIsDown>>
.Ejemplo
[source,lua]
----
-- Un disparo cargado sale cuando se suelta la tecla de disparo.
function singeMain()
while true do
if keyboardIsDown(SCANCODE.LCTRL.value) then
charge = math.min(charge + 1, chargeMax)
elseif keyboardGetLastUp() == SCANCODE.LCTRL.value then
fireShot(charge)
charge = 0
end
singeYield()
end
end
----
[#keyboardgetmode]
==== keyboardGetMode
[source,text]
----
mode = keyboardGetMode()
----
Devuelve el modo de teclado en vigor: `MODE_NORMAL` (`0`) o `MODE_FULL` (`1`). Todo script empieza en `MODE_NORMAL`, también después de una recarga.
*Devuelve:* modo, entero.
*Desde:* 1.x (RDG)
*Véase también:* <<keyboardsetmode,keyboardSetMode>>
.Ejemplo
[source,lua]
----
-- Función auxiliar compartida de entrada de texto que restaura el modo que tuviera quien la llama.
function promptForName()
local previous = keyboardGetMode()
keyboardSetMode(MODE_FULL)
entering = true
restoreMode = previous
end
----
[#keyboardgetmodifiers]
==== keyboardGetModifiers
[source,text]
----
modifiers = keyboardGetModifiers()
----
Devuelve las teclas modificadoras pulsadas en este momento como una máscara de bits `KMOD_*` de SDL. Compruébala con un and a nivel de bits contra los valores de la tabla `MODIFIER`: las entradas de una sola tecla como `MODIFIER.LSHIFT` y las combinadas `MODIFIER.SHIFT`, `MODIFIER.CTRL`, `MODIFIER.ALT` y `MODIFIER.GUI`, que cubren cualquiera de los dos lados. Lee el estado en vivo, así que funciona en cualquier modo de teclado y desde cualquier callback.
*Devuelve:* máscara de bits, entero.
*Desde:* 2.10
*Véase también:* <<keyboardgetlastdown,keyboardGetLastDown>>, <<onkeypressedonkeyreleased,onKeyPressed>>
.Ejemplo
[source,lua]
----
-- Mayús+Tab muestra u oculta el overlay de estadísticas.
function onKeyPressed(keysym, scancode)
if scancode == SCANCODE.TAB.value then
if keyboardGetModifiers() & MODIFIER.SHIFT.value ~= 0 then
showStats = not showStats
end
end
end
----
[#keyboardisdown]
==== keyboardIsDown
[source,text]
----
down = keyboardIsDown(scancode)
----
Comprueba si una tecla está pulsada físicamente en este momento, a partir del estado que el motor guarda de cada evento de tecla que ve. No depende del modo de teclado ni de `controls.cfg`, y funciona desde cualquier callback, así que sirve para un movimiento que debe continuar mientras se mantiene pulsada una tecla. Los scancodes fuera del rango que define SDL devuelven `false` en lugar de abortar.
*Parámetros:*
* `scancode` -- un valor de la tabla `SCANCODE`, como `SCANCODE.LEFT.value`.
*Devuelve:* booleano.
*Desde:* 2.10
*Véase también:* <<keyboardgetlastdown,keyboardGetLastDown>>, <<controllergetbutton,controllerGetButton>>
.Ejemplo
[source,lua]
----
-- Movimiento en ocho direcciones con teclas mantenidas.
function onOverlayUpdate()
local dx = 0
local dy = 0
if keyboardIsDown(SCANCODE.LEFT.value) then dx = dx - 1 end
if keyboardIsDown(SCANCODE.RIGHT.value) then dx = dx + 1 end
if keyboardIsDown(SCANCODE.UP.value) then dy = dy - 1 end
if keyboardIsDown(SCANCODE.DOWN.value) then dy = dy + 1 end
playerX = playerX + dx * speed
playerY = playerY + dy * speed
drawPlayer()
return OVERLAY_UPDATED
end
----
[#keyboardsetmode]
==== keyboardSetMode
[source,text]
----
keyboardSetMode(mode)
----
Cambia entre los dos modelos de teclado. En `MODE_NORMAL` solo las teclas asignadas en `controls.cfg` llegan al script, como valores `SWITCH_*`, la repetición de teclas se descarta para que cada pulsación llegue una sola vez, y el motor actúa sobre sus propios interruptores (pausa, salir, captura de pantalla, captura del ratón). En `MODE_FULL` cada tecla llega a `onInputPressed` y `onInputReleased` como su keysym y a `onKeyPressed` y `onKeyReleased` como keysym y scancode, el texto que produce una pulsación llega a `onTextInput`, la repetición de teclas se entrega para la entrada de texto, y las asignaciones de teclado de los propios interruptores del motor se ignoran para que el juego se quede con todas las teclas; las asignaciones de botones del mando y del ratón siguen actuando, y esos botones siguen llegando al script como sus valores `SWITCH_*`, ya que no se pueden teclear. La mayoría de los juegos fijan el modo una vez al arrancar; un juego con entrada de texto cambia a `MODE_FULL` mientras dura y luego vuelve. Cualquier otro valor aborta el script. En `MODE_FULL` los keysyms de cuatro teclas son también números de interruptor -- Retroceso es `8` (`SWITCH_BUTTON3`), Tab `9` (`SWITCH_COIN1`), Intro `13` y Esc `27` -- así que un script que lee `onInputPressed` para un interruptor del mando o del ratón y además espera esas teclas debería comprobar `keyboardIsDown` para la tecla antes de tratar el valor como el interruptor.
*Parámetros:*
* `mode` -- `MODE_NORMAL` (`0`) o `MODE_FULL` (`1`).
*Desde:* 1.x (RDG)
*Véase también:* <<keyboardgetmode,keyboardGetMode>>, <<oninputpressedoninputreleased,onInputPressed>>, <<onkeypressedonkeyreleased,onKeyPressed>>
.Ejemplo
[source,lua]
----
-- La entrada de iniciales del récord recibe las teclas en bruto y luego vuelve el modo de juego.
function beginInitials()
initials = ""
keyboardSetMode(MODE_FULL)
end
function onKeyPressed(keysym, scancode)
if scancode == SCANCODE.RETURN.value then
keyboardSetMode(MODE_NORMAL)
saveScore(initials, score)
elseif keysym >= string.byte("a") and keysym <= string.byte("z") and #initials < 3 then
initials = initials .. string.char(keysym):upper()
end
end
----
[#light]
=== Luz
Las luces son nodos que llevan una luz, creados con `lightNew` y movidos, emparentados y animados con las llamadas de nodos. Brillan como mucho ocho luces visibles por fotograma, en orden de nodos; `sceneSetAmbient` y un cielo de `sceneSetSky` añaden luz desde todas partes. Las luces direccionales y los focos alumbran a lo largo del -Z del nodo, así que oriéntalos con `nodeLookAt` o `nodeSetRotation`; las luces puntuales solo necesitan una posición. Los colores son enteros de `0` a `255`, las intensidades son multiplicadores en la luz lineal de la escena, los ángulos van en grados y las distancias en unidades del mundo. Toda llamada `lightSet*` termina el script para un nodo que no lleve luz. Véase <<scenes3d,Escenas 3D>> para la iluminación, la exposición y las sombras.
[#lightnew]
==== lightNew
[source,text]
----
node = lightNew(type)
node = lightNew(type, parent)
----
Crea un nodo nuevo que lleva una luz y devuelve el handle del nodo. La luz empieza blanca con intensidad `1`, sin límite de alcance y, para un foco, con un cono de `20` a `30` grados. Colócala y oriéntala con las llamadas de nodos: `nodeSetPosition` para una luz puntual o un foco, `nodeLookAt` para una luz direccional o un foco. Una luz emparentada con un nodo en movimiento se mueve con él, así que el faro de un coche o una antorcha en una mano son una sola llamada. Termina el script con un tipo desconocido o cuando no se puede crear ningún nodo.
*Parámetros:*
* `type` -- `LIGHT_DIRECTIONAL` (un sol; solo importa su dirección), `LIGHT_POINT` (una bombilla que se atenúa con la distancia en todas las direcciones) o `LIGHT_SPOT` (un cono a lo largo del -Z del nodo).
* `parent` -- el nodo padre; la raíz si se omite.
*Devuelve:* El handle del nodo de la luz, un entero.
*Desde:* 3.00.
*Véase también:* <<lightsetcolor,lightSetColor>>, <<lightsetintensity,lightSetIntensity>>, <<lightsetrange,lightSetRange>>, <<lightsetcone,lightSetCone>>, <<scenesetambient,sceneSetAmbient>>
.Ejemplo
[source,lua]
----
-- Luz principal cálida desde arriba a la izquierda, un relleno frío desde la derecha
-- y un faro que va con el coche.
key = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(key, -3, 4, 3)
nodeLookAt(key, 0, 0, 0)
lightSetColor(key, 255, 235, 200)
lightSetIntensity(key, 1.4)
fill = lightNew(LIGHT_POINT)
nodeSetPosition(fill, 3, 1, 2)
lightSetColor(fill, 150, 190, 255)
lightSetIntensity(fill, 4)
lightSetRange(fill, 10)
headlight = lightNew(LIGHT_SPOT, car)
nodeSetPosition(headlight, 0.6, 0.5, -1.8)
lightSetIntensity(headlight, 30)
----
[#lightsetcolor]
==== lightSetColor
[source,text]
----
lightSetColor(node, r, g, b)
----
El color de la luz, de `0` a `255` por canal (los valores fuera de rango se recortan), tratado como sRGB; blanco por defecto. El brillo es cosa de `lightSetIntensity`, así que deja el color para el tono (una bombilla cálida `255, 230, 190`, una luna fría `150, 190, 255`) y deja al menos un canal cerca de `255`. Termina el script cuando el nodo no lleva luz.
*Desde:* 3.00.
*Véase también:* <<lightsetintensity,lightSetIntensity>>, <<lightnew,lightNew>>
.Ejemplo
[source,lua]
----
-- El letrero de neón recorre sus colores.
signLight = lightNew(LIGHT_POINT)
nodeSetPosition(signLight, 2, 3, -1)
lightSetIntensity(signLight, 8)
lightSetRange(signLight, 6)
neon = { { 255, 40, 120 }, { 40, 220, 255 }, { 255, 210, 40 } }
function onOverlayUpdate()
local phase = math.floor(singeGetTicks() / 700) % #neon + 1
lightSetColor(signLight, neon[phase][1], neon[phase][2], neon[phase][3])
return OVERLAY_UPDATED
end
----
[#lightsetcone]
==== lightSetCone
[source,text]
----
lightSetCone(node, innerDegrees, outerDegrees)
----
La forma del cono de un foco, con los dos ángulos medidos desde el eje (el -Z del nodo): brillo completo dentro de `innerDegrees`, que se atenúa suavemente hasta nada en `outerDegrees`. Los valores por defecto son `20` y `30`. Una diferencia estrecha entre los dos da un foco de teatro de borde duro; una amplia, un charco de luz suave. Los valores se guardan para toda luz, pero solo los usa un `LIGHT_SPOT`. Termina el script cuando el nodo no lleva luz.
*Parámetros:*
* `node` -- el nodo de la luz.
* `innerDegrees` -- semiángulo de brillo completo, en grados desde el eje.
* `outerDegrees` -- semiángulo en el que la luz llega a nada, en grados desde el eje; se sube a `innerDegrees` si es menor.
*Desde:* 3.00.
*Véase también:* <<lightnew,lightNew>>, <<lightsetrange,lightSetRange>>, <<nodelookat,nodeLookAt>>
.Ejemplo
[source,lua]
----
-- Un foco de escenario sobre el cantante, de borde duro, desde la parrilla de iluminación.
spot = lightNew(LIGHT_SPOT)
nodeSetPosition(spot, 0, 8, 4)
nodeLookAt(spot, nodeGetWorldPosition(singer))
lightSetColor(spot, 255, 245, 220)
lightSetIntensity(spot, 80)
lightSetCone(spot, 8, 10)
lightSetShadow(spot, true)
----
[#lightsetintensity]
==== lightSetIntensity
[source,text]
----
lightSetIntensity(node, intensity)
----
El multiplicador de brillo de la luz en la luz lineal de la escena, por defecto `1`. Una luz direccional de intensidad `1` ilumina hasta el blanco una superficie blanca orientada hacia ella. Una luz puntual o un foco se atenúa con la distancia como `1 / (1 + d^2)`, así que su intensidad es el brillo en la propia luz, la mitad a una unidad de distancia y una centésima parte a diez unidades; las bombillas suelen querer valores muy por encima de `1`, de `10` a `50` para una habitación, y `sceneSetExposure` devuelve toda la escena al rango. Termina el script cuando el nodo no lleva luz.
*Desde:* 3.00.
*Véase también:* <<lightsetrange,lightSetRange>>, <<scenesetexposure,sceneSetExposure>>, <<lightsetcolor,lightSetColor>>
.Ejemplo
[source,lua]
----
-- Un farol que parpadea con el viento.
lantern = lightNew(LIGHT_POINT, lanternNode)
lightSetColor(lantern, 255, 200, 140)
lightSetRange(lantern, 9)
function onOverlayUpdate()
local flicker = 10 + math.random() * 3
lightSetIntensity(lantern, flicker)
return OVERLAY_UPDATED
end
----
[#lightsetrange]
==== lightSetRange
[source,text]
----
lightSetRange(node, range)
----
Hasta dónde llega una luz puntual o un foco, en unidades del mundo; `0` (por defecto) significa sin límite. La luz se atenúa exactamente hasta nada en el alcance, además de su caída con la distancia, así que las luces no aparecen de golpe cuando los objetos lo cruzan. Un alcance también limita hasta dónde tiene que llegar el mapa de sombras de una luz que proyecta sombras, lo que mantiene nítidas sus sombras. Las luces direccionales lo ignoran. Termina el script cuando el nodo no lleva luz.
*Desde:* 3.00.
*Véase también:* <<lightsetintensity,lightSetIntensity>>, <<lightsetshadow,lightSetShadow>>
.Ejemplo
[source,lua]
----
-- Farolas a lo largo de la carretera, cada una iluminando su propio tramo.
for i = 0, 9 do
local lamp = lightNew(LIGHT_POINT)
nodeSetPosition(lamp, 4, 5, -i * 20)
lightSetColor(lamp, 255, 220, 170)
lightSetIntensity(lamp, 20)
lightSetRange(lamp, 14)
end
----
[#lightsetshadow]
==== lightSetShadow
[source,text]
----
lightSetShadow(node, shadow)
----
Hace que la luz proyecte sombras de todo lo opaco de la escena, o deja de hacerlo; desactivado por defecto. Cualquiera de las ocho luces puede proyectarlas, en cualquier combinación. Un foco recibe un mapa de sombras; una luz direccional recibe un mapa por cascada (`sceneSetShadowCascades`); una luz puntual recibe un mapa cúbico de seis vistas, que cubre todas las direcciones (una bombilla dentro de una habitación) a seis veces el coste. `nodeSetShadow` exime a una malla de proyectar sombras. Termina el script cuando el nodo no lleva luz.
*Notas:* Cada mapa de sombras cuesta una pasada extra por la escena en cada fotograma, un mapa cúbico seis, y un sol en cascadas tantas como cascadas tenga; el tamaño de los mapas lo fija `sceneSetShadowSize`. Una GPU integrada moderna maneja ocho luces con sombras con holgura; una Raspberry Pi prefiere una o dos a `512`.
*Desde:* 3.00.
*Véase también:* <<scenesetshadowsize,sceneSetShadowSize>>, <<scenesetshadowcascades,sceneSetShadowCascades>>, <<nodesetshadow,nodeSetShadow>>, <<lightnew,lightNew>>
.Ejemplo
[source,lua]
----
-- El sol proyecta sombras; las lámparas solo iluminan. El propio cristal de la bombilla
-- no debe sombrear su lámpara.
sun = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(sun, 3, 6, 4)
nodeLookAt(sun, 0, 0, 0)
lightSetShadow(sun, true)
lamp = lightNew(LIGHT_POINT)
nodeSetPosition(lamp, 3, 3.1, -2)
lightSetIntensity(lamp, 12)
lightSetRange(lamp, 14)
nodeSetShadow(bulbGlass, false)
----
[#material]
=== Material
Un material es el aspecto de una malla: un color base con alfa, una textura base opcional a partir de un sprite, un archivo KTX2, un vídeo o una vista renderizada, y los parámetros metálico-rugosidad de glTF con cuatro mapas opcionales. `materialNew` devuelve un handle entero; pásalo a `nodeSetMesh` o a `nodeSetMaterial`, y a toda llamada `materialSet*`. Los materiales se comparten, así que cambiar uno cambia todas las mallas que se dibujan con él, y un nodo sin material se dibuja blanco y a media rugosidad. Los colores son enteros de `0` a `255` por canal y se tratan como sRGB; los factores como la rugosidad van de `0` a `1`. Un handle erróneo termina el script. Véase <<scenes3d,Escenas 3D>> para cómo se iluminan los materiales.
[#materialnew]
==== materialNew
[source,text]
----
material = materialNew()
----
Reserva un material nuevo y devuelve su handle. Empieza blanco y opaco (color `255, 255, 255, 255`), con rugosidad `0.5`, metálico `0`, sin color emisivo, repetición `1, 1`, filtrado lineal, de una sola cara, iluminado y sin textura ni mapas. Fija lo que difiera de eso y pasa el handle a `nodeSetMesh`. Los modelos cargados traen sus propios materiales, así que esto es para mallas construidas desde el script. Termina el script en una máquina sin GPU utilizable.
*Devuelve:* El handle del material, un entero.
*Desde:* 3.00.
*Véase también:* <<materialsetcolor,materialSetColor>>, <<materialsettexture,materialSetTexture>>, <<materialdelete,materialDelete>>, <<nodesetmesh,nodeSetMesh>>
.Ejemplo
[source,lua]
----
-- Oro cepillado para un trofeo y cristal azul translúcido para su vitrina.
gold = materialNew()
materialSetColor(gold, 255, 200, 60)
materialSetMetallic(gold, 1)
materialSetRoughness(gold, 0.35)
glass = materialNew()
materialSetColor(glass, 80, 160, 255, 90)
materialSetBlend(glass, true)
materialSetRoughness(glass, 0.1)
trophy = nodeNew()
nodeSetMesh(trophy, meshCone(0.3, 0.8, 24), gold)
----
[#materialdelete]
==== materialDelete
[source,text]
----
materialDelete(material)
----
Libera el material y cualquier textura que haya subido. Todo nodo que se dibujaba con él vuelve al aspecto por defecto (blanco, a media rugosidad), así que reasigna antes esos nodos con `nodeSetMaterial` si deben conservar un aspecto. Después el handle deja de ser válido y termina el script si se vuelve a usar. El material privado que un nodo crea para sí con `nodeSetSprite` o `nodeSetText` pertenece al nodo y no se puede borrar; la llamada termina el script, y quitar el sprite o el texto lo libera. Los materiales son baratos; borrarlos importa sobre todo para los que llevaban texturas grandes.
*Desde:* 3.00.
*Véase también:* <<materialnew,materialNew>>, <<nodesetmaterial,nodeSetMaterial>>
.Ejemplo
[source,lua]
----
-- Cambia el aspecto del suelo del nivel cuando cambia la estación.
function setSeason(texture)
local look = materialNew()
materialSetTexture(look, texture)
materialSetTiling(look, 16, 16)
nodeSetMaterial(ground, look)
if groundLook then
materialDelete(groundLook)
end
groundLook = look
end
----
[#materialsetblend]
==== materialSetBlend
[source,text]
----
materialSetBlend(material, blend)
----
Con `true` el material se dibuja translúcido, mezclado según el alfa del color base multiplicado por el alfa de la textura, después de todas las mallas opacas y ordenado del más lejano al más cercano. Las mallas con mezcla no escriben profundidad, así que déjalo en `false` (por defecto) para las superficies sólidas: una malla opaca con la mezcla activada puede dejar ver a través de ella cosas que tiene detrás. Úsalo con un alfa por debajo de `255` de `materialSetColor` o con una textura con transparencia.
*Desde:* 3.00.
*Véase también:* <<materialsetcolor,materialSetColor>>, <<materialsettexture,materialSetTexture>>
.Ejemplo
[source,lua]
----
-- Un campo de fuerza que se hace visible cuando recibe un impacto.
field = materialNew()
materialSetColor(field, 60, 200, 255, 0)
materialSetBlend(field, true)
materialSetDoubleSided(field, true)
nodeSetMesh(shield, meshSphere(2, 32), field)
function onShieldHit()
materialSetColor(field, 60, 200, 255, 160)
end
----
[#materialsetcolor]
==== materialSetColor
[source,text]
----
materialSetColor(material, r, g, b)
materialSetColor(material, r, g, b, a)
----
Fija el color base, de `0` a `255` por canal (los valores fuera de rango se recortan), con `a` a `255` por defecto. El color se trata como sRGB y se ilumina en luz lineal. Cuando el material tiene una textura, un vídeo o una vista, el color la multiplica, así que el blanco deja la imagen como está y un color más oscuro la tiñe. El alfa solo se nota con `materialSetBlend`.
*Parámetros:*
* `material` -- el handle del material.
* `r, g, b` -- rojo, verde y azul, de `0` a `255`.
* `a` -- opacidad, de `0` a `255`; `255` si se omite.
*Desde:* 3.00.
*Véase también:* <<materialsettexture,materialSetTexture>>, <<materialsetblend,materialSetBlend>>, <<materialsetemissive,materialSetEmissive>>
.Ejemplo
[source,lua]
----
-- Colores de equipo sobre la misma malla de coche.
teamLooks = {}
for team, rgb in ipairs({ { 220, 40, 40 }, { 40, 90, 220 }, { 240, 200, 30 } }) do
local look = materialNew()
materialSetColor(look, rgb[1], rgb[2], rgb[3])
materialSetMetallic(look, 0.6)
materialSetRoughness(look, 0.3)
teamLooks[team] = look
end
nodeSetMesh(playerCar, carMesh, teamLooks[1])
----
[#materialsetdoublesided]
==== materialSetDoubleSided
[source,text]
----
materialSetDoubleSided(material, doubleSided)
----
Dibuja las dos caras de cada triángulo en lugar de descartar la cara trasera. Hace falta para las formas abiertas vistas desde detrás, como un único `meshPlane` visto desde abajo, una bandera o una tarjeta de hojas. Las formas cerradas se ven igual de las dos maneras, así que déjalo en `false` (por defecto) para ellas: un material de doble cara cuesta tasa de relleno y las sombras también se proyectan desde las dos caras.
*Desde:* 3.00.
*Véase también:* <<meshplane,meshPlane>>, <<materialsetblend,materialSetBlend>>
.Ejemplo
[source,lua]
----
-- Una pancarta colgada sobre la calle, legible por los dos lados.
cloth = materialNew()
materialSetTexture(cloth, spriteLoad(DIR .. "Art/banner.png"))
materialSetDoubleSided(cloth, true)
materialSetRoughness(cloth, 0.9)
banner = nodeNew()
nodeSetMesh(banner, meshPlane(3, 1), cloth)
nodeSetPosition(banner, 0, 4, -6)
nodeSetRotation(banner, 90, 0, 0)
----
[#materialsetemissive]
==== materialSetEmissive
[source,text]
----
materialSetEmissive(material, r, g, b)
----
La luz que la superficie emite por sí misma, de `0` a `255` por canal (los valores fuera de rango se recortan), sumada encima de la iluminación: pantallas, bombillas, neón, paneles de instrumentos. Por defecto es negro, sin brillo. No ilumina nada más ni proyecta luz; para eso, pon un nodo de luz en el mismo sitio. Con `materialSetEmissiveMap` el mapa indica en qué partes de la superficie se aplica este color. Las superficies emisivas lo bastante brillantes como para superar el umbral de `sceneSetBloom` resplandecen.
*Notas:* Un material sin iluminación (`materialSetUnlit`) muestra solo su color base; en él se ignora el color emisivo.
*Desde:* 3.00.
*Véase también:* <<materialsetemissivemap,materialSetEmissiveMap>>, <<materialsetunlit,materialSetUnlit>>, <<scenesetbloom,sceneSetBloom>>
.Ejemplo
[source,lua]
----
-- Una luz de aviso que late en rojo cuando el reactor se sobrecalienta.
lampLook = materialNew()
materialSetColor(lampLook, 60, 10, 10)
nodeSetMesh(lamp, meshSphere(0.15, 16), lampLook)
function onOverlayUpdate()
local pulse = (math.sin(singeGetTicks() / 120) + 1) / 2
if overheating then
materialSetEmissive(lampLook, math.floor(255 * pulse), 20, 20)
else
materialSetEmissive(lampLook, 0, 0, 0)
end
return OVERLAY_UPDATED
end
----
[#materialsetemissivemap]
==== materialSetEmissiveMap
[source,text]
----
materialSetEmissiveMap(material, sprite)
materialSetEmissiveMap(material, "file.ktx2")
materialSetEmissiveMap(material, nil)
materialSetEmissiveMap(material)
----
Una textura que indica qué partes de la superficie brillan. Su color se multiplica por el color de `materialSetEmissive`, así que fíjalo también: blanco para usar los colores propios del mapa, o un tinte. La imagen viene de un sprite cargado (se sube una copia, así que el sprite se puede descargar después) o de un archivo `.ktx2` por su nombre, que se mantiene comprimido en la GPU. Con `nil` o sin segundo argumento se quita el mapa. Termina el script cuando la textura no se puede subir o el archivo no es una textura KTX2.
*Desde:* 3.00.
*Véase también:* <<materialsetemissive,materialSetEmissive>>, <<materialsettexture,materialSetTexture>>, <<materialsettiling,materialSetTiling>>
.Ejemplo
[source,lua]
----
-- Un panel de control cuyos botones e indicadores se iluminan.
panel = materialNew()
materialSetTexture(panel, spriteLoad(DIR .. "Art/panel.png"))
materialSetEmissiveMap(panel, spriteLoad(DIR .. "Art/panelLights.png"))
materialSetEmissive(panel, 255, 255, 255)
materialSetRoughness(panel, 0.6)
nodeSetMesh(nodeFind("Panel", cockpit), meshPlane(1.2, 0.4), panel)
function onPowerDown()
materialSetEmissive(panel, 0, 0, 0)
end
----
[#materialsetfilter]
==== materialSetFilter
[source,text]
----
materialSetFilter(material, filter)
----
Cómo se muestrean las texturas del material. `FILTER_LINEAR` (por defecto) mezcla entre texels y entre niveles de mipmap, con filtrado anisótropo en las superficies vistas con un ángulo rasante. `FILTER_NEAREST` toma el texel más cercano y el nivel de mipmap más cercano, para que el pixel art se mantenga nítido en un cuadrilátero. Toda textura lleva una cadena de mipmaps completa, así que una textura vista pequeña nunca centellea, sea cual sea el filtro. Cualquier otro valor termina el script.
*Desde:* 3.00.
*Véase también:* <<materialsettexture,materialSetTexture>>, <<materialsetunlit,materialSetUnlit>>
.Ejemplo
[source,lua]
----
-- Una baldosa de 16 por 16 píxeles dibujada en un bloque sin emborronarse.
pixelLook = materialNew()
materialSetTexture(pixelLook, spriteLoad(DIR .. "Art/dirt16.png"))
materialSetFilter(pixelLook, FILTER_NEAREST)
materialSetRoughness(pixelLook, 1)
block = nodeNew()
nodeSetMesh(block, meshBox(1, 1, 1), pixelLook)
----
[#materialsetgui]
==== materialSetGui
[source,text]
----
materialSetGui(material, gui)
materialSetGui(material, nil)
materialSetGui(material)
----
Muestra una interfaz de `guiNew` como textura de color base del material, actualizada en cada fotograma: un monitor, un panel de cabina, un letrero, con el mismo documento que `guiDraw` mostraría en plano. Suele ir junto a `materialSetUnlit` para que las luces no sombreen la página, y el color base la tiñe como a cualquier textura. Con `nil` o sin segundo argumento el material vuelve a su propia textura, que se conserva debajo; borrar la interfaz hace lo mismo. Fijar una interfaz descarta cualquier vídeo o vista que mostrara el material. Mientras la interfaz recibe entrada, el ratón o una pistola de luz que apunte a una superficie dibujada con el material llega a ella mediante el mismo sondeo que `sceneProbeGui`. Termina el script para un handle de interfaz que no es válido.
*Desde:* 3.00.
*Véase también:* <<guinew,guiNew>>, <<guidraw,guiDraw>>, <<sceneprobegui,sceneProbeGui>>, <<materialsetview,materialSetView>>, <<materialsetvideo,materialSetVideo>>, <<materialsetunlit,materialSetUnlit>>
.Ejemplo
[source,lua]
----
-- La página de opciones en la pantalla de la recreativa modelada.
options = guiNew(720, 480)
page = guiLoad(options, DIR .. "gui/options.rml")
screen = materialNew()
materialSetGui(screen, options)
materialSetUnlit(screen, true)
nodeSetMesh(nodeFind("Screen", cabinet), meshPlane(3.0, 2.0), screen)
----
[#materialsetmetallic]
==== materialSetMetallic
[source,text]
----
materialSetMetallic(material, metallic)
----
Cuán metálica es la superficie, de `0` para los dieléctricos (pintura, plástico, madera, piel) a `1` para los metales, que tiñen sus brillos y reflejos con el color base. Por defecto es `0`. Los valores intermedios son para mezclas y superficies desgastadas; la mayoría de los materiales quieren un extremo o el otro. Con `materialSetMetallicRoughnessMap` el canal azul del mapa multiplica este valor. Un metal tiene poco que reflejar sin un cielo de `sceneSetSky`, así que usa los dos juntos.
*Desde:* 3.00.
*Véase también:* <<materialsetroughness,materialSetRoughness>>, <<materialsetmetallicroughnessmap,materialSetMetallicRoughnessMap>>, <<scenesetsky,sceneSetSky>>
.Ejemplo
[source,lua]
----
-- Parachoques cromado, carrocería pintada.
chrome = materialNew()
materialSetColor(chrome, 240, 240, 245)
materialSetMetallic(chrome, 1)
materialSetRoughness(chrome, 0.15)
paint = materialNew()
materialSetColor(paint, 30, 80, 200)
materialSetMetallic(paint, 0)
materialSetRoughness(paint, 0.4)
nodeSetMaterial(nodeFind("Bumper", car), chrome)
nodeSetMaterial(nodeFind("Body", car), paint)
----
[#materialsetmetallicroughnessmap]
==== materialSetMetallicRoughnessMap
[source,text]
----
materialSetMetallicRoughnessMap(material, sprite)
materialSetMetallicRoughnessMap(material, "file.ktx2")
materialSetMetallicRoughnessMap(material, nil)
materialSetMetallicRoughnessMap(material)
----
Una textura que varía el acabado a lo largo de la superficie: su canal verde multiplica la rugosidad de `materialSetRoughness` y su canal azul el valor metálico de `materialSetMetallic`, tal como los empaqueta glTF, así que un mapa de una herramienta glTF funciona tal cual. Pon los dos factores a `1` para usar solo los valores del mapa. La imagen viene de un sprite o de un archivo `.ktx2` por su nombre; `nil` o la falta de segundo argumento quita el mapa. Termina el script cuando la textura no se puede subir.
*Desde:* 3.00.
*Véase también:* <<materialsetmetallic,materialSetMetallic>>, <<materialsetroughness,materialSetRoughness>>, <<materialsettexture,materialSetTexture>>
.Ejemplo
[source,lua]
----
-- Chapa metálica rayada: brilla donde la pintura se ha desgastado.
plating = materialNew()
materialSetTexture(plating, DIR .. "Textures/plating.ktx2")
materialSetMetallicRoughnessMap(plating, DIR .. "Textures/platingMetalRough.ktx2")
materialSetMetallic(plating, 1)
materialSetRoughness(plating, 1)
materialSetNormalMap(plating, DIR .. "Textures/platingNormal.ktx2")
nodeSetMesh(hullPanel, meshPlane(4, 2), plating)
----
[#materialsetnormalmap]
==== materialSetNormalMap
[source,text]
----
materialSetNormalMap(material, sprite)
materialSetNormalMap(material, sprite, strength)
materialSetNormalMap(material, "file.ktx2")
materialSetNormalMap(material, "file.ktx2", strength)
materialSetNormalMap(material, nil)
materialSetNormalMap(material)
----
Un mapa de normales en espacio tangente (el habitual, azulado, plano en `128, 128, 255`) que inclina la iluminación por texel para que los ladrillos, las ranuras y los remaches reciban la luz sin geometría adicional. Toda malla tiene tangentes para él: las del propio modelo cuando el archivo las lleva; si no, calculadas a partir de las UV. La imagen viene de un sprite o de un archivo `.ktx2` por su nombre; `nil` o la falta de segundo argumento quita el mapa. Termina el script cuando la textura no se puede subir.
*Parámetros:*
* `material` -- el handle del material.
* `sprite` o `"file.ktx2"` -- el mapa, de un sprite cargado o de un archivo KTX2; `nil` o su omisión lo quita.
* `strength` -- cuánto inclinan los relieves la iluminación: `1` (por defecto) tal como se creó, `0` plano, por encima de `1` exagerado.
*Desde:* 3.00.
*Véase también:* <<materialsettexture,materialSetTexture>>, <<materialsetocclusionmap,materialSetOcclusionMap>>, <<materialsettiling,materialSetTiling>>
.Ejemplo
[source,lua]
----
-- Una pared de ladrillo a partir de un plano, con las juntas de mortero recibiendo el sol bajo.
brick = materialNew()
materialSetTexture(brick, spriteLoad(DIR .. "Art/brick.png"))
materialSetNormalMap(brick, spriteLoad(DIR .. "Art/brickNormal.png"), 1.5)
materialSetRoughness(brick, 0.9)
materialSetTiling(brick, 6, 2)
wall = nodeNew()
nodeSetMesh(wall, meshPlane(12, 4), brick)
nodeSetPosition(wall, 0, 2, -3)
nodeSetRotation(wall, 90, 0, 0)
----
[#materialsetoccluder]
==== materialSetOccluder
[source,text]
----
materialSetOccluder(material, occluder)
----
Hace que el material escriba profundidad y ningún color. Lo que cubre la malla
queda oculto como si la malla fuera sólida, pero la propia malla no se ve: lo que
se dibujó detrás de ella -- un fondo pintado, el disco -- se ve a través. Es el
sustituto de algo que está en una imagen pero no en la escena: una columna en
una sala pintada por detrás de la cual tiene que pasar un personaje 3D, un vano de puerta, una mesa.
Un oclusor no proyecta sombra, porque la pintura ya tiene una, y no recibe
luz. `false` (por defecto) devuelve el material a la normalidad.
*Desde:* 3.00.
*Véase también:* <<materialsetblend,materialSetBlend>>, <<materialsetunlit,materialSetUnlit>>
.Ejemplo
[source,lua]
----
-- Una columna que está en la pintura: una caja invisible donde se alza, para que
-- el héroe desaparezca detrás y reaparezca por el otro lado.
hidden = materialNew()
materialSetOccluder(hidden, true)
pillar = nodeNew()
nodeSetMesh(pillar, meshBox(0.8, 4, 0.8), hidden)
nodeSetPosition(pillar, 2, 2, -3)
----
[#materialsetocclusionmap]
==== materialSetOcclusionMap
[source,text]
----
materialSetOcclusionMap(material, sprite)
materialSetOcclusionMap(material, sprite, strength)
materialSetOcclusionMap(material, "file.ktx2")
materialSetOcclusionMap(material, "file.ktx2", strength)
materialSetOcclusionMap(material, nil)
materialSetOcclusionMap(material)
----
Un mapa de oclusión ambiental, con la oclusión en el canal rojo (blanco para lo abierto, oscuro para las grietas), que oscurece la luz ambiente y la del cielo en rincones y pliegues. La luz directa de los nodos de luz no se ve afectada, como en glTF. La imagen viene de un sprite o de un archivo `.ktx2` por su nombre; `nil` o la falta de segundo argumento quita el mapa. Termina el script cuando la textura no se puede subir.
*Parámetros:*
* `material` -- el handle del material.
* `sprite` o `"file.ktx2"` -- el mapa, de un sprite cargado o de un archivo KTX2; `nil` o su omisión lo quita.
* `strength` -- cuánto del mapa se aplica, de `0`, nada, a `1` (por defecto), del todo.
*Desde:* 3.00.
*Véase también:* <<materialsetnormalmap,materialSetNormalMap>>, <<scenesetambient,sceneSetAmbient>>, <<scenesetenvironment,sceneSetEnvironment>>
.Ejemplo
[source,lua]
----
-- Sombreado precalculado para las paredes de la cueva, a media intensidad para que las lámparas sigan llegando a los rincones.
rock = materialNew()
materialSetTexture(rock, DIR .. "Textures/caveRock.ktx2")
materialSetNormalMap(rock, DIR .. "Textures/caveRockNormal.ktx2")
materialSetOcclusionMap(rock, DIR .. "Textures/caveRockAO.ktx2", 0.5)
materialSetRoughness(rock, 1)
nodeSetMaterial(nodeFind("Walls", cave), rock)
----
[#materialsetroughness]
==== materialSetRoughness
[source,text]
----
materialSetRoughness(material, roughness)
----
Cuán rugosa es la superficie, de `0` para un brillo concentrado como el de un espejo a `1` para mate. Por defecto es `0.5`. Los brillos siguen el modelo GGX, así que los valores bajos dan un punto pequeño y brillante y reflejos del cielo nítidos, y los altos un brillo amplio y suave y reflejos borrosos. Con `materialSetMetallicRoughnessMap` el canal verde del mapa multiplica este valor.
*Desde:* 3.00.
*Véase también:* <<materialsetmetallic,materialSetMetallic>>, <<materialsetmetallicroughnessmap,materialSetMetallicRoughnessMap>>
.Ejemplo
[source,lua]
----
-- Moja la carretera cuando llueve: el mismo asfalto, brillos más concentrados.
asphalt = materialNew()
materialSetColor(asphalt, 50, 50, 55)
materialSetRoughness(asphalt, 0.95)
nodeSetMesh(road, meshPlane(8, 200), asphalt)
function onWeatherChange(raining)
if raining then
materialSetRoughness(asphalt, 0.25)
else
materialSetRoughness(asphalt, 0.95)
end
end
----
[#materialsettexture]
==== materialSetTexture
[source,text]
----
materialSetTexture(material, sprite)
materialSetTexture(material, "file.ktx2")
materialSetTexture(material, nil)
materialSetTexture(material)
----
Usa la imagen de un sprite como textura de color base (se sube una copia, así que el sprite se puede descargar después), o un archivo KTX2 por su nombre, que se mantiene comprimido en la GPU en el formato de bloques que tenga la máquina (véase Texturas comprimidas en <<scenes3d,Escenas 3D>>). Con `nil` o sin segundo argumento se quita la textura y el material muestra solo su color base. Fijar una textura también descarta cualquier vídeo o vista que mostrara el material. Las coordenadas de textura vienen de la malla; las primitivas proyectan una imagen una vez sobre cada cara, y `materialSetTiling` la repite. Un nombre que no es un archivo KTX2 termina el script (carga otras imágenes con `spriteLoad`), igual que una subida fallida.
*Desde:* 3.00.
*Véase también:* <<spriteload,spriteLoad>>, <<materialsettiling,materialSetTiling>>, <<materialsetvideo,materialSetVideo>>, <<materialsetview,materialSetView>>
.Ejemplo
[source,lua]
----
-- Una caja texturizada con un sprite que el juego ya carga para el menú,
-- y un suelo con una textura comprimida que se distribuye con el juego.
crateArt = spriteLoad(DIR .. "Art/crate.png")
crateLook = materialNew()
materialSetTexture(crateLook, crateArt)
crate = nodeNew()
nodeSetMesh(crate, meshBox(1, 1, 1), crateLook)
floorLook = materialNew()
materialSetTexture(floorLook, DIR .. "Textures/tiles.ktx2")
materialSetTiling(floorLook, 10, 10)
nodeSetMesh(floor, meshPlane(20, 20), floorLook)
----
[#materialsettiling]
==== materialSetTiling
[source,text]
----
materialSetTiling(material, u, v)
----
Cuántas veces se repiten las texturas del material (la base y los mapas por igual) a lo largo del rango UV de `0` a `1` de una superficie, `1` y `1` por defecto. `24, 24` sobre un mapa de alturas de 120 unidades de ancho repite una textura cada cinco unidades. Las texturas se repiten más allá de sus bordes, así que la imagen tiene que encajar sin costuras. Los valores por debajo de `1` muestran parte de la imagen; los negativos la reflejan.
*Parámetros:*
* `material` -- el handle del material.
* `u` -- repeticiones en la dirección U de la superficie.
* `v` -- repeticiones en la dirección V de la superficie.
*Desde:* 3.00.
*Véase también:* <<materialsettexture,materialSetTexture>>, <<meshheightmap,meshHeightmap>>
.Ejemplo
[source,lua]
----
-- Hierba cada cinco unidades por las colinas.
ground = materialNew()
materialSetTexture(ground, DIR .. "Textures/grass.ktx2")
materialSetTiling(ground, 24, 24)
materialSetRoughness(ground, 0.95)
hills = nodeNew()
nodeSetMesh(hills, meshHeightmap(DIR .. "Art/hills.png", 120, 14, 120), ground)
bodyNew(hills, BODY_STATIC, SHAPE_MESH)
----
[#materialsetunlit]
==== materialSetUnlit
[source,text]
----
materialSetUnlit(material, unlit)
----
Muestra el color base y la textura exactamente como son, ignorando toda luz, el ambiente y el cielo: para pantallas de vídeo, monitores que muestran una vista, skyboxes y aspectos de sombreado plano. La niebla se sigue aplicando. El color emisivo y los mapas no se usan en un material sin iluminación. Por defecto es `false`.
*Desde:* 3.00.
*Véase también:* <<materialsetemissive,materialSetEmissive>>, <<materialsetvideo,materialSetVideo>>, <<materialsetview,materialSetView>>
.Ejemplo
[source,lua]
----
-- La marquesina de la recreativa: una imagen iluminada desde dentro, así que
-- las luces de la sala no deben sombrearla.
marquee = materialNew()
materialSetTexture(marquee, spriteLoad(DIR .. "Art/marquee.png"))
materialSetUnlit(marquee, true)
nodeSetMesh(nodeFind("Marquee", cabinet), meshPlane(0.7, 0.2), marquee)
----
[#materialsetview]
==== materialSetView
[source,text]
----
materialSetView(material, view)
materialSetView(material, nil)
materialSetView(material)
----
Muestra una vista renderizada de `viewNew` como textura de color base del material, actualizada en cada fotograma: un monitor de seguridad, un retrovisor, un portal. Suele ir junto a `materialSetUnlit` para que las luces no sombreen la imagen, y el color base la tiñe como a cualquier textura. Con `nil` o sin segundo argumento el material vuelve a su propia textura, que se conserva debajo. Fijar una vista descarta cualquier vídeo que mostrara el material. Termina el script para un handle de vista que no es válido.
*Desde:* 3.00.
*Véase también:* <<viewnew,viewNew>>, <<viewsetcamera,viewSetCamera>>, <<materialsetvideo,materialSetVideo>>, <<materialsetunlit,materialSetUnlit>>
.Ejemplo
[source,lua]
----
-- Un retrovisor en el coche del jugador.
rearCamera = nodeNew(car)
nodeSetPosition(rearCamera, 0, 1.2, -0.5)
nodeSetRotation(rearCamera, 0, 180, 0)
rearView = viewNew(384, 256)
viewSetCamera(rearView, rearCamera)
mirror = materialNew()
materialSetView(mirror, rearView)
materialSetUnlit(mirror, true)
nodeSetMesh(nodeFind("Mirror", car), meshPlane(0.3, 0.1), mirror)
----
[#materialsetvideo]
==== materialSetVideo
[source,text]
----
materialSetVideo(material)
materialSetVideo(material, video)
----
Usa el laserdisc (sin `video`) o un vídeo cargado con `videoLoad` como textura de color base, actualizada en cada fotograma mientras se reproduce. Suele ir junto a `materialSetUnlit` para que las luces no tiñan la imagen. El vídeo sigue reproduciéndose tanto si también se dibuja en el overlay como si no, y varios materiales pueden mostrar el mismo. Fijar un vídeo descarta cualquier textura o vista que tuviera el material; `materialSetTexture` lo vuelve a quitar.
*Notas:* La forma del disco termina el script en un juego sin disco, y lo mismo hace un handle de vídeo erróneo.
*Desde:* 3.00.
*Véase también:* <<videoload,videoLoad>>, <<materialsetunlit,materialSetUnlit>>, <<materialsettexture,materialSetTexture>>
.Ejemplo
[source,lua]
----
-- Una recreativa modelada cuya pantalla muestra el laserdisc, y una pared de
-- monitores que repiten un clip del modo de atracción.
cabinet = modelInstance(modelLoad(DIR .. "Models/Cabinet.glb"))
screen = materialNew()
materialSetVideo(screen)
materialSetUnlit(screen, true)
nodeSetMesh(nodeFind("Screen", cabinet), meshPlane(0.6, 0.45), screen)
attract = videoLoad(DIR .. "Video/attract.mp4")
videoPlay(attract)
monitors = materialNew()
materialSetVideo(monitors, attract)
materialSetUnlit(monitors, true)
----
[#mesh]
=== Malla
Una malla es geometría en la GPU: una primitiva que construye el motor, un terreno
a partir de una imagen o datos de vértices de tu script. Los handles son enteros
que devuelven los constructores `mesh*`; cualquier número de nodos puede compartir una
malla, y los nodos que dibujan la misma malla con el mismo material se dibujan en una sola
llamada instanciada, así que crea una malla y muchos nodos. Los tamaños van en unidades de la escena
y cada primitiva está centrada en el origen de su nodo salvo que se indique
lo contrario, así que la transformación del nodo la coloca, la gira y la escala. Las coordenadas
de textura van de `0` a `1` una vez a lo largo de cada cara o superficie. Todo
constructor necesita el renderizador 3D descrito en <<scenes3d,Escenas 3D>> y
aborta el script cuando no está disponible.
[#meshbox]
==== meshBox
[source,text]
----
mesh = meshBox(width, height, depth)
----
Una caja de `width` a lo largo de X, `height` a lo largo de Y y `depth` a lo largo de Z, centrada en
el origen. Cada una de las seis caras tiene sus propios cuatro vértices con una normal
plana, así que las aristas se sombrean con nitidez, y la textura se aplica una vez sobre cada
cara. Las cajas de embalaje, las paredes, los suelos y las carrocerías son todos cajas.
*Devuelve:* El handle de la malla.
*Desde:* 3.00.
*Véase también:* <<nodesetmesh,nodeSetMesh>>, <<meshplane,meshPlane>>, <<bodynew,bodyNew>>
.Ejemplo
[source,lua]
----
-- Una losa de piedra como suelo y una pila de cajas que comparten una malla.
local floor = nodeNew()
nodeSetMesh(floor, meshBox(16, 0.2, 10), stone)
nodeSetPosition(floor, 0, -0.1, 0)
local crateMesh = meshBox(0.6, 0.6, 0.6)
for i = 1, 4 do
local crate = nodeNew()
nodeSetMesh(crate, crateMesh, wood)
nodeSetPosition(crate, 0, i * 0.6 - 0.3, -2)
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.6, 0.6, 0.6)
end
----
[#meshcone]
==== meshCone
[source,text]
----
mesh = meshCone(radius, height)
mesh = meshCone(radius, height, segments)
----
Un cono de pie sobre Y con la base cerrada: el círculo de la base, de radio `radius`, en
`-height / 2` y el vértice en `+height / 2`. `segments` (por defecto `24`, como
mínimo `3`) es cuántas facetas lo rodean; los lados se sombrean suavemente a través
de ellas. La textura da una vuelta a los lados y se aplica una vez sobre la
base.
*Devuelve:* El handle de la malla.
*Desde:* 3.00.
*Véase también:* <<meshcylinder,meshCylinder>>, <<nodesetmesh,nodeSetMesh>>
.Ejemplo
[source,lua]
----
-- Una pantalla de lámpara sobre la bombilla, y una fila de pinos de un cono cada uno.
local shade = nodeNew(stand)
nodeSetMesh(shade, meshCone(0.45, 0.5, 24), brass)
nodeSetPosition(shade, 0, 0.9, 0)
local foliage = meshCone(0.8, 2.0, 12)
for i = 1, 5 do
local tree = nodeNew()
nodeSetMesh(tree, foliage, pine)
nodeSetPosition(tree, i * 3 - 9, 1.0, -6)
end
----
[#meshcylinder]
==== meshCylinder
[source,text]
----
mesh = meshCylinder(radius, height)
mesh = meshCylinder(radius, height, segments)
----
Un cilindro a lo largo de Y, cerrado por los dos extremos, de `-height / 2` a
`+height / 2`. `segments` (por defecto `24`, como mínimo `3`) es cuántas facetas
lo rodean, sombreadas suavemente. Para una rueda o una moneda, gira el nodo `90`
grados alrededor de X o de Z para que el eje quede donde lo necesitas.
*Devuelve:* El handle de la malla.
*Desde:* 3.00.
*Véase también:* <<meshcone,meshCone>>, <<meshtorus,meshTorus>>, <<nodesetrotation,nodeSetRotation>>
.Ejemplo
[source,lua]
----
-- Una farola, y un neumático puesto de lado como rueda.
local post = nodeNew()
nodeSetMesh(post, meshCylinder(0.08, 3, 10), dark)
nodeSetPosition(post, 3, 1.5, -2)
local tire = nodeNew(wheel)
nodeSetMesh(tire, meshCylinder(0.35, 0.25, 16), rubber)
nodeSetRotation(tire, 0, 0, 90)
----
[#meshdelete]
==== meshDelete
[source,text]
----
meshDelete(mesh)
----
Libera los búferes de la malla en la GPU y su copia en la CPU. Los nodos que la usaban
se quedan sin malla (y sin pesos de morph) y no dibujan nada hasta que reciben
otra; el handle deja de ser válido y puede reutilizarlo un constructor
posterior. Las mallas no se liberan cuando se borra un nodo, así que borra tú
una malla cuando ya nada la vaya a usar.
*Desde:* 3.00.
*Véase también:* <<meshnew,meshNew>>, <<nodesetmesh,nodeSetMesh>>, <<nodedelete,nodeDelete>>
.Ejemplo
[source,lua]
----
-- Reconstruye la pista a partir de un nuevo mapa de alturas entre rondas.
function loadTrack(image)
if trackMesh then
meshDelete(trackMesh)
end
trackMesh = meshHeightmap(image, 200, 20, 200)
nodeSetMesh(track, trackMesh, tarmac)
bodyNew(track, BODY_STATIC, SHAPE_MESH)
end
----
[#meshheightmap]
==== meshHeightmap
[source,text]
----
mesh = meshHeightmap(image, sizeX, sizeY, sizeZ)
----
Una malla de terreno a partir de una imagen en escala de grises, de `2` a `1025` píxeles por lado:
un vértice por píxel sobre `sizeX` por `sizeZ` unidades centradas en el nodo,
cada uno elevado según el canal rojo del píxel (de `0`, negro, a `255`, blanco) multiplicado por
`sizeY`, con la fila superior de la imagen en el borde lejano (`-Z`). Las normales se
calculan a partir de las pendientes y las coordenadas de textura van de `0` a `1` sobre el
conjunto, así que `materialSetTiling` fija la repetición de la textura. Las muestras de altura
se conservan: `terrainGetHeight` las lee, y un cuerpo `SHAPE_MESH` en el
nodo se convierte en un campo de alturas. Un archivo que no se puede abrir o decodificar, un
tamaño incorrecto o la falta de renderizador 3D abortan el script.
*Parámetros:*
* `image` -- el nombre de la imagen relativo al juego, en cualquier formato que lea `spriteLoad`.
* `sizeX` -- el ancho del terreno a lo largo de X en unidades del mundo.
* `sizeY` -- la altura de un píxel blanco sobre uno negro.
* `sizeZ` -- la profundidad del terreno a lo largo de Z en unidades del mundo.
*Devuelve:* El handle de la malla.
*Desde:* 3.00.
*Véase también:* <<terraingetheight,terrainGetHeight>>, <<materialsettiling,materialSetTiling>>, <<bodynew,bodyNew>>
.Ejemplo
[source,lua]
----
local ground = materialNew()
materialSetTexture(ground, spriteLoad("grass.png"))
materialSetTiling(ground, 24, 24)
materialSetRoughness(ground, 0.95)
local hills = nodeNew()
nodeSetMesh(hills, meshHeightmap("hills.png", 120, 14, 120), ground)
bodyNew(hills, BODY_STATIC, SHAPE_MESH)
-- Los pies del dragón en la pendiente, a la altura que tenga allí el suelo.
nodeSetPosition(dragon, -8, terrainGetHeight(hills, -8, 3) or 0, 3)
----
[#meshnew]
==== meshNew
[source,text]
----
mesh = meshNew(positions, normals, uvs, indices)
----
Construye geometría a partir de tablas de números. Las tangentes para el mapeado de normales
se calculan por ti, y también las normales suaves cuando `normals` es `nil`, promediando
los triángulos que comparten cada vértice. Los triángulos son frontales
cuando sus vértices van en sentido antihorario vistos desde fuera; da al
material `materialSetDoubleSided` si la cara trasera debe verse. Una tabla de forma
incorrecta, o un índice fuera de la lista de vértices, aborta el script con un
mensaje que dice cuál.
*Parámetros:*
* `positions` -- `x, y, z` de cada vértice, uno tras otro; al menos un vértice.
* `normals` -- `x, y, z` por vértice, tantos como en `positions`, o `nil` para que se calculen.
* `uvs` -- `u, v` por vértice (de `0` a `1`, con `v` hacia abajo en la imagen), o `nil` para ninguna (todos los vértices en `0, 0`).
* `indices` -- tres números de vértice por triángulo, contados desde `1` en el orden en que aparecen los vértices en `positions`; al menos un triángulo.
*Devuelve:* El handle de la malla.
*Desde:* 3.00.
*Véase también:* <<nodesetmesh,nodeSetMesh>>, <<materialsetdoublesided,materialSetDoubleSided>>, <<meshdelete,meshDelete>>
.Ejemplo
[source,lua]
----
-- Una flecha plana que apunta a lo largo de +X, vista desde arriba (de doble cara para que también funcione desde abajo).
local arrow = meshNew(
{ 0, 0, -0.5, 1, 0, 0, 0, 0, 0.5, -0.6, 0, -0.5, -0.6, 0, 0.5 },
nil,
nil,
{ 1, 2, 3, 4, 1, 3, 4, 3, 5 })
local marker = materialNew()
materialSetColor(marker, 255, 200, 40)
materialSetDoubleSided(marker, true)
local pointer = nodeNew()
nodeSetMesh(pointer, arrow, marker)
nodeSetPosition(pointer, 0, 0.05, 0)
----
[#meshplane]
==== meshPlane
[source,text]
----
mesh = meshPlane(width, depth)
mesh = meshPlane(width, depth, columns)
mesh = meshPlane(width, depth, columns, rows)
----
Un rectángulo plano en el plano XZ orientado hacia +Y, de `width` a lo largo de X y `depth`
a lo largo de Z, centrado en el origen, con la textura aplicada una vez sobre él.
Dos argumentos dan un solo cuadrilátero. `columns` (y `rows`, que por defecto vale
`columns`) lo subdividen en ese número de cuadriláteros en cada sentido, que es lo que necesita un cuerpo blando
(`softNew`) para doblarse como una tela; los dos deben valer al menos `1`. Gira el
nodo `90` grados alrededor de X para ponerlo de pie mirando hacia +Z. Es de una sola cara
salvo que su material sea de doble cara.
*Devuelve:* El handle de la malla.
*Desde:* 3.00.
*Véase también:* <<materialsetdoublesided,materialSetDoubleSided>>, <<materialsetvideo,materialSetVideo>>, <<softnew,softNew>>
.Ejemplo
[source,lua]
----
-- El suelo, una pantalla que muestra el disco y una bandera que será de tela.
local plain = nodeNew()
nodeSetMesh(plain, meshPlane(60, 60), ground)
local tv = nodeNew()
nodeSetMesh(tv, meshPlane(3.0, 2.2), screen)
nodeSetPosition(tv, 0, 1.6, -4)
nodeSetRotation(tv, 90, 0, 0)
local flag = nodeNew()
nodeSetMesh(flag, meshPlane(1.6, 1.0, 16, 10), cloth)
nodeSetPosition(flag, 0.8, 2.5, 0)
nodeSetRotation(flag, 90, 0, 0)
----
[#meshsphere]
==== meshSphere
[source,text]
----
mesh = meshSphere(radius)
mesh = meshSphere(radius, segments)
----
Una esfera de radio `radius`, centrada en el origen, construida con bandas de latitud y
longitud: `segments` (por defecto `32`, como mínimo `3`) alrededor del
ecuador y la mitad de polo a polo. La textura da una vuelta a su
alrededor, con `v` del polo superior al inferior. Menos segmentos dan una malla
más ligera para bolas vistas de lejos; más dan una silueta más suave de cerca.
*Devuelve:* El handle de la malla.
*Desde:* 3.00.
*Véase también:* <<meshtorus,meshTorus>>, <<nodesetmesh,nodeSetMesh>>, <<bodynew,bodyNew>>
.Ejemplo
[source,lua]
----
-- Una bola de cristal en el suelo y una pequeña esfera de marca que comparte un aspecto.
local ball = nodeNew()
nodeSetMesh(ball, meshSphere(0.7, 48), glass)
nodeSetPosition(ball, -1.5, 0.7, 0)
bodyNew(ball, BODY_DYNAMIC, SHAPE_SPHERE, 0.7)
local dot = nodeNew()
nodeSetMesh(dot, meshSphere(0.08, 16), marker)
nodeSetPosition(dot, 0, 1.2, 0)
----
[#meshtorus]
==== meshTorus
[source,text]
----
mesh = meshTorus(radius, tubeRadius)
mesh = meshTorus(radius, tubeRadius, segments)
----
Un anillo tumbado en el plano XZ alrededor de Y, centrado en el origen: `radius`
desde el centro hasta el medio del tubo y `tubeRadius` el del propio tubo.
`segments` (por defecto `32`, como mínimo `3`) rodean el anillo y la mitad
(como mínimo `3`) rodean el tubo. La textura da una vuelta al anillo
en `u` y una vuelta al tubo en `v`.
*Devuelve:* El handle de la malla.
*Desde:* 3.00.
*Véase también:* <<meshsphere,meshSphere>>, <<meshcylinder,meshCylinder>>
.Ejemplo
[source,lua]
----
-- Un aro dorado de canto para que la bola pase rodando a través de él.
local gold = materialNew()
materialSetColor(gold, 255, 200, 80)
materialSetMetallic(gold, 1)
materialSetRoughness(gold, 0.25)
local ring = nodeNew()
nodeSetMesh(ring, meshTorus(1.2, 0.25, 40), gold)
nodeSetPosition(ring, 0, 1.2, -2)
nodeSetRotation(ring, 90, 0, 0)
----
[#_microphone]
=== Micrófono
Un micrófono, para un juego de cantar o cualquier cosa que escuche. Se abre un solo dispositivo
de grabación a la vez, que se oye como un canal a 48000 muestras por segundo
sea lo que sea lo que dé el hardware; SDL convierte. Singe guarda los ocho segundos más recientes
de lo que oye, y todas las llamadas de abajo responden a partir de eso, así que preguntar no cuesta
nada que el juego no haya pagado ya.
Lo que un juego suele querer es <<micpitch,`micPitch`>> -- la nota que se canta --
y <<miclevel,`micLevel`>>. Un juego que analiza el sonido por su cuenta, o lo envía
a algún sitio, lee las muestras con <<micread,`micRead`>>; las llamadas de
<<voice,Voz>> las codifican para una red.
El micrófono se cierra cuando termina el juego. No se abre nada hasta que un script lo pide.
[NOTE]
====
Una nota cantada llega al juego un poco después de cantarse: la tarjeta de sonido, el
controlador y los búferes la retienen cada uno un momento. Un juego de cantar que juzga
la sincronización lo resta. <<miccalibrate,`micCalibrate`>> lo mide reproduciendo
clics por los altavoces y cronometrando su vuelta, y el resultado se guarda
para la máquina, como la <<singesetaudiocalibration,calibración de audio>>.
La monitorización y la calibración solo se han probado sin hardware real por ahora;
el tono, el nivel, la lectura y la grabación se han probado con una señal conocida.
====
[#miccalibrate]
==== micCalibrate
[source,text]
----
started = micCalibrate()
----
Mide la latencia del micrófono: reproduce cinco clics por los altavoces, separados
una fracción de segundo, y cronometra cuánto tarda cada uno en volver por el
micrófono. Tarda unos segundos; <<miccalibration,`micCalibration`>> dice
cuándo ha terminado. El resultado es el valor central de los clics oídos, y se guarda para
esta máquina. Hazlo en una habitación silenciosa, con los altavoces lo bastante altos para que el
micrófono los oiga.
*Devuelve:* `true` cuando ha empezado; `false` si no hay ningún micrófono abierto o ningún
altavoz para reproducir los clics.
*Desde:* 3.00.
*Véase también:* <<miccalibration,micCalibration>>, <<miclatency,micLatency>>
[#miccalibration]
==== micCalibration
[source,text]
----
state, latency = micCalibration()
----
Cómo va una calibración.
*Devuelve:* `state`, uno de `"idle"`, `"running"`, `"done"` o `"failed"`
(se oyeron de vuelta menos de tres clics), y la latencia en
milisegundos una vez que se conoce; si no, `nil`.
*Desde:* 3.00.
*Véase también:* <<miccalibrate,micCalibrate>>
.Ejemplo
[source,lua]
----
local state, latency = micCalibration()
if state == "done" then
message = "Your microphone runs " .. latency .. " ms behind."
elseif state == "failed" then
message = "I could not hear the clicks. Turn the speakers up and try again."
end
----
[#micclose]
==== micClose
[source,text]
----
micClose()
----
Deja de escuchar, y detiene con ello cualquier grabación y monitorización. El micrófono
también se cierra cuando termina el juego.
*Desde:* 3.00.
[#micisopen]
==== micIsOpen
[source,text]
----
open = micIsOpen()
----
*Devuelve:* `true` mientras haya un micrófono abierto.
*Desde:* 3.00.
[#miclatency]
==== micLatency
[source,text]
----
milliseconds = micLatency()
----
*Devuelve:* el tiempo que pasa entre que se produce un sonido y el juego lo oye, tal como
lo midió en esta máquina <<miccalibrate,`micCalibrate`>> (en cualquier momento, en cualquier
juego), o lo fijó <<micsetlatency,`micSetLatency`>>; `nil` hasta entonces.
*Desde:* 3.00.
[#miclevel]
==== micLevel
[source,text]
----
rms, peak = micLevel()
----
Lo fuerte que sonó el último instante oído.
*Devuelve:* el RMS y el pico del último bloque de muestras, cada uno de `0` a `1`.
Un tono constante a escala completa da unos `0.71` de RMS. Los dos son `0` sin
ningún micrófono abierto.
*Desde:* 3.00.
[#miclist]
==== micList
[source,text]
----
names = micList()
----
*Devuelve:* una lista con los nombres de los micrófonos que tiene esta máquina, para
<<micopen,`micOpen`>>. Se obtiene de nuevo cada vez, así que aparece un micrófono conectado
desde la última llamada.
*Desde:* 3.00.
[#micmonitor]
==== micMonitor
[source,text]
----
ok = micMonitor(on [, volume])
----
Reproduce por los altavoces lo que oye el micrófono, para que quien canta se oiga
por encima de la música. Desactivado hasta que se pide: en la mayoría del hardware la voz vuelve
con el retraso suficiente para resultar molesta, y en un portátil los altavoces se acoplan al
micrófono. Todo lo que lleve esperando a reproducirse más de una décima de segundo se
descarta en lugar de oírse tarde.
*Parámetros:*
* `on` -- booleano.
* `volume` -- número, de `0` a `1`; `1` si no se indica.
*Devuelve:* `true`, o `false` si no hay ningún micrófono abierto o ningún altavoz por el que reproducir.
*Desde:* 3.00.
[#micopen]
==== micOpen
[source,text]
----
opened = micOpen([name])
----
Empieza a escuchar, por el micrófono indicado (de <<miclist,`micList`>>) o por el
predeterminado de la máquina. Si ya hay un micrófono abierto, se cierra primero.
*Devuelve:* `true` cuando se ha abierto.
*Desde:* 3.00.
.Ejemplo
[source,lua]
----
if not micOpen() then
message = "No microphone. This game needs one."
end
----
[#micpitch]
==== micPitch
[source,text]
----
hz, clarity = micPitch()
----
La nota que se canta, buscada en los 43 milisegundos más recientes oídos. Encuentra una
fundamental entre 50 y 2000 Hz, lo que cubre cualquier voz cantada, y
responde con un margen de uno o dos hercios con un tono limpio.
*Devuelve:* la fundamental en Hz, y lo claramente que el sonido es una sola nota, de `0` a
`1`. Sin nada entonado -- silencio, una respiración, una tos, una multitud -- `hz` es `0`.
Convierte Hz en un número de nota con `69 + 12 * math.log(hz / 440, 2)`: 69 es el la
por encima del do central, y cada 1 es un semitono.
*Desde:* 3.00.
.Ejemplo
[source,lua]
----
local hz, clarity = micPitch()
if hz > 0 and clarity > 0.8 then
local note = 69 + 12 * math.log(hz / 440, 2)
local off = note - targetNote -- semitonos por encima (positivo) o por debajo
if math.abs(off) < 0.5 then
score = score + 1
end
end
----
[#micread]
==== micRead
[source,text]
----
samples = micRead([max])
----
Lo que se ha oído desde la última llamada, lo más antiguo primero.
*Parámetros:*
* `max` -- número, el máximo de muestras que devolver; todo lo que esté esperando si no se indica.
*Devuelve:* una cadena de muestras de 16 bits, dos bytes cada una, little endian, un
canal a 48000 por segundo: `#samples / 2` muestras. Lee una con
`string.unpack("<i2", samples, n * 2 - 1)`. Un juego que pasa más de ocho segundos
sin leer pierde las más antiguas.
*Desde:* 3.00.
[#micrecordstart]
==== micRecordStart
[source,text]
----
started = micRecordStart(name)
----
Escribe todo lo que se oiga a partir de ahora en un archivo WAV (un canal, 16 bits, 48000 por
segundo) hasta <<micrecordstop,`micRecordStop`>>. El nombre es cualquier archivo que el juego
pueda escribir, y va donde van los demás archivos que escribe el juego.
*Devuelve:* `true` cuando se ha abierto el archivo.
*Desde:* 3.00.
[#micrecordstop]
==== micRecordStop
[source,text]
----
micRecordStop()
----
Termina la grabación.
*Desde:* 3.00.
[#micsetlatency]
==== micSetLatency
[source,text]
----
micSetLatency(milliseconds | nil)
----
Fija la latencia del micrófono de esta máquina, como hace un menú de servicio a partir de una
medición hecha de otra forma; `nil` la olvida. Se guarda con los demás
ajustes de la máquina.
*Parámetros:*
* `milliseconds` -- número, de `0` a `1000`, o `nil`.
*Desde:* 3.00.
[#midi]
=== MIDI
Dos cosas sin relación comparten este nombre, y merece la pena mantenerlas separadas.
Un **archivo MIDI** es música, y se reproduce como cualquier otra música: pasa el `.mid` a
<<musicload,`musicLoad`>>. Singe lo sintetiza a partir de una SoundFont, porque un archivo
MIDI contiene notas y no sonido. No se distribuye con ninguna SoundFont -- una buena ocupa decenas
de megabytes para un formato que casi ningún juego usa -- así que indica una con
`--soundfont`, pon una en `Singe/soundfont.sf2` para que un juego empaquetado lleve la suya
o deja que Singe encuentre la que instaló tu distribución. La línea `SoundFont:`
de la cabecera de la traza dice cuál se usó. Sin ninguna SoundFont en ningún sitio,
cargar un `.mid` falla y lo dice.
**Los puertos MIDI** son las llamadas de abajo. Envían y reciben mensajes en vivo por un
puerto real: una recreativa que controla un módulo de sonido externo, o un teclado, una caja de faders
o una superficie de control leídos como otro dispositivo de entrada. Nada de esto sintetiza
nada, y nada de esto toca un archivo MIDI.
Los puertos se numeran desde `0`. No se abre nada hasta que un script lo pide por primera vez -- un
puerto MIDI cuesta un descriptor de archivo y una ronda de configuración del controlador, y un
juego que nunca menciona MIDI nunca lo paga -- así que la primera de estas llamadas
es la que hace inventario de la máquina, y <<midirescan,`midiRescan`>> vuelve a buscar
por si se ha conectado un dispositivo después. Puede haber abiertos a la vez un puerto de entrada
y un puerto de salida. Los canales van de `1` a `16`, como los cuenta una
persona, no de `0` a `15` como en el cable, y todos los demás valores van de
`0` a `127`. En una máquina sin MIDI en absoluto -- sin puertos, o sin biblioteca MIDI
instalada -- los recuentos son `0`, las llamadas de apertura responden `false` y las de envío
responden `false`; nada falla.
En Linux se llega a todos los puertos por el secuenciador ALSA, que abarca tanto las interfaces
de hardware como cualquier sintetizador u otro programa que registre uno. macOS usa
CoreMIDI y Windows las llamadas MIDI multimedia, donde el dispositivo de salida `0` suele ser
el sintetizador que proporciona el propio sistema.
[#midicloseinput]
==== midiCloseInput
[source,text]
----
midiCloseInput()
----
Deja de escuchar el puerto de entrada. <<onmidimessage,`onMidiMessage`>> deja de dispararse. Cerrar un puerto que no está abierto no hace nada.
*Parámetros:*
* Ninguno.
*Devuelve:* nada.
*Desde:* 3.00.
*Véase también:* <<midiopeninput,midiOpenInput>>
[#midicloseoutput]
==== midiCloseOutput
[source,text]
----
midiCloseOutput()
----
Libera el puerto de salida. A partir de ahora, las llamadas de envío responden `false`. Cerrar un puerto que no está abierto no hace nada.
*Parámetros:*
* Ninguno.
*Devuelve:* nada.
*Desde:* 3.00.
*Véase también:* <<midiopenoutput,midiOpenOutput>>
[#midicontrolchange]
==== midiControlChange
[source,text]
----
sent = midiControlChange(channel, control, value)
----
Mueve un controlador en el puerto de salida abierto: el volumen es el controlador `7`, el panorama el `10` y el pedal de sostenido el `64`.
*Parámetros:*
* `channel` -- número; de `1` a `16`.
* `control` -- número; de `0` a `127`.
* `value` -- número; de `0` a `127`.
*Devuelve:* `true` cuando el mensaje ha salido.
*Desde:* 3.00.
*Véase también:* <<midisend,midiSend>>
.Ejemplo
[source,lua]
----
-- Baja el volumen del módulo mientras habla un personaje.
midiControlChange(1, 7, 40)
----
[#midiinputcount]
==== midiInputCount
[source,text]
----
count = midiInputCount()
----
Cuántos puertos de entrada MIDI tiene esta máquina. `0` cuando no hay ninguno, o cuando la plataforma no tiene MIDI en absoluto.
*Parámetros:*
* Ninguno.
*Devuelve:* un número.
*Desde:* 3.00.
*Véase también:* <<midiinputname,midiInputName>>
[#midiinputname]
==== midiInputName
[source,text]
----
name = midiInputName(index)
----
El nombre de un puerto de entrada, para una pantalla de ajustes que deje al jugador elegir uno.
*Parámetros:*
* `index` -- número; de `0` a `midiInputCount() - 1`.
*Devuelve:* una cadena, o `nil` cuando el índice no corresponde a ningún puerto.
*Desde:* 3.00.
*Véase también:* <<midiinputcount,midiInputCount>>
.Ejemplo
[source,lua]
----
-- Una pantalla de ajustes que lista los puertos de entrada, uno por fila.
function onOverlayUpdate()
overlayClear()
for i = 0, midiInputCount() - 1 do
overlayPrint(1, i + 1, midiInputName(i))
end
return OVERLAY_UPDATED
end
----
[#midiisinputopen]
==== midiIsInputOpen
[source,text]
----
open = midiIsInputOpen()
----
Si hay un puerto de entrada abierto.
*Parámetros:*
* Ninguno.
*Devuelve:* `true` o `false`.
*Desde:* 3.00.
*Véase también:* <<midiopeninput,midiOpenInput>>
[#midiisoutputopen]
==== midiIsOutputOpen
[source,text]
----
open = midiIsOutputOpen()
----
Si hay un puerto de salida abierto. Conviene preguntarlo antes de una ráfaga de mensajes, ya que si no cada uno respondería `false` por separado.
*Parámetros:*
* Ninguno.
*Devuelve:* `true` o `false`.
*Desde:* 3.00.
*Véase también:* <<midiopenoutput,midiOpenOutput>>
[#midinoteoff]
==== midiNoteOff
[source,text]
----
sent = midiNoteOff(channel, key)
----
Suelta una nota en el puerto de salida abierto. Una nota que se deja sonando suena hasta que algo la para, así que cada `midiNoteOn` necesita una de estas.
*Parámetros:*
* `channel` -- número; de `1` a `16`.
* `key` -- número; de `0` a `127`, donde `60` es el do central.
*Devuelve:* `true` cuando el mensaje ha salido.
*Desde:* 3.00.
*Véase también:* <<midinoteon,midiNoteOn>>
[#midinoteon]
==== midiNoteOn
[source,text]
----
sent = midiNoteOn(channel, key, velocity)
----
Hace sonar una nota en el puerto de salida abierto.
*Parámetros:*
* `channel` -- número; de `1` a `16`. El canal `10` es percusión en un dispositivo General MIDI.
* `key` -- número; de `0` a `127`, donde `60` es el do central.
* `velocity` -- número; de `0` a `127`, la fuerza con que se pulsó la tecla.
*Devuelve:* `true` cuando el mensaje ha salido.
*Desde:* 3.00.
*Véase también:* <<midinoteoff,midiNoteOff>>
.Ejemplo
[source,lua]
----
-- Un golpe de efecto en un módulo externo cuando alcanzan al jugador.
midiNoteOn(10, 49, 110)
midiNoteOff(10, 49)
----
[#midiopeninput]
==== midiOpenInput
[source,text]
----
opened = midiOpenInput(index)
----
Escucha un puerto de entrada. A partir de entonces <<onmidimessage,`onMidiMessage`>> se dispara con cada mensaje que llega. El puerto de entrada que estuviera abierto se cierra primero.
*Parámetros:*
* `index` -- número; de `0` a `midiInputCount() - 1`.
*Devuelve:* `true` cuando el puerto se ha abierto.
*Desde:* 3.00.
*Véase también:* <<midicloseinput,midiCloseInput>>, <<onmidimessage,onMidiMessage>>
.Ejemplo
[source,lua]
----
if midiInputCount() > 0 then
midiOpenInput(0)
end
----
[#midiopenoutput]
==== midiOpenOutput
[source,text]
----
opened = midiOpenOutput(index)
----
Toma un puerto de salida para las llamadas de envío. El puerto de salida que estuviera abierto se cierra primero.
*Parámetros:*
* `index` -- número; de `0` a `midiOutputCount() - 1`.
*Devuelve:* `true` cuando el puerto se ha abierto.
*Desde:* 3.00.
*Véase también:* <<midicloseoutput,midiCloseOutput>>
[#midioutputcount]
==== midiOutputCount
[source,text]
----
count = midiOutputCount()
----
Cuántos puertos de salida MIDI tiene esta máquina.
*Parámetros:*
* Ninguno.
*Devuelve:* un número.
*Desde:* 3.00.
*Véase también:* <<midioutputname,midiOutputName>>
[#midioutputname]
==== midiOutputName
[source,text]
----
name = midiOutputName(index)
----
El nombre de un puerto de salida.
*Parámetros:*
* `index` -- número; de `0` a `midiOutputCount() - 1`.
*Devuelve:* una cadena, o `nil` cuando el índice no corresponde a ningún puerto.
*Desde:* 3.00.
*Véase también:* <<midioutputcount,midiOutputCount>>
[#midipitchbend]
==== midiPitchBend
[source,text]
----
sent = midiPitchBend(channel, value)
----
Aplica pitch bend a un canal. `8192` es el centro, es decir, sin inflexión; hasta dónde llegan `0` y `16383` es un ajuste del propio dispositivo receptor, normalmente dos semitonos.
*Parámetros:*
* `channel` -- número; de `1` a `16`.
* `value` -- número; de `0` a `16383`.
*Devuelve:* `true` cuando el mensaje ha salido.
*Desde:* 3.00.
[#midiprogramchange]
==== midiProgramChange
[source,text]
----
sent = midiProgramChange(channel, program)
----
Elige el sonido que toca un canal. Los números son los del dispositivo receptor; en un dispositivo General MIDI, `0` es un piano de cola.
*Parámetros:*
* `channel` -- número; de `1` a `16`.
* `program` -- número; de `0` a `127`.
*Devuelve:* `true` cuando el mensaje ha salido.
*Desde:* 3.00.
[#midirescan]
==== midiRescan
[source,text]
----
midiRescan()
----
Vuelve a buscar puertos, para un dispositivo conectado mientras el juego está en marcha. Un puerto abierto sigue abierto. Los índices solo son tan estables como la lista, así que vuelve a leer los nombres después de esto en lugar de dar por hecho que el puerto `2` sigue siendo el que era.
*Parámetros:*
* Ninguno.
*Devuelve:* nada.
*Desde:* 3.00.
*Véase también:* <<midiinputcount,midiInputCount>>, <<midioutputcount,midiOutputCount>>
[#midisend]
==== midiSend
[source,text]
----
sent = midiSend(byte [, byte [, byte]])
sent = midiSend(string)
----
Cualquier mensaje, para los que no cubren las llamadas con nombre. Con números, los envía tal cual, es decir, un byte de estado y hasta dos bytes de datos -- y ojo: el byte de estado lleva el canal en sus cuatro bits bajos, contando desde `0`, a diferencia de todas las demás llamadas de aquí. Con una cadena, envía la cadena entera, que es como sale un mensaje exclusivo de sistema.
*Parámetros:*
* `byte` -- número; de `0` a `255`, de uno a tres.
* `string` -- una cadena de bytes, enviada sin cambios.
*Devuelve:* `true` cuando el mensaje ha salido.
*Desde:* 3.00.
*Véase también:* <<midinoteon,midiNoteOn>>, <<midicontrolchange,midiControlChange>>
.Ejemplo
[source,lua]
----
-- Todas las notas apagadas en el canal 1, que no tiene llamada con nombre.
midiSend(0xB0, 123, 0)
-- Un mensaje exclusivo de sistema, con todos sus bytes.
midiSend(string.char(0xF0, 0x7E, 0x7F, 0x09, 0x01, 0xF7))
----
[#model]
=== Modelo
Los modelos son archivos binarios glTF 2.0 (`.glb`) que se cargan una vez y se colocan cualquier número
de veces. Un modelo debe ser autónomo (búferes e imágenes dentro del
archivo); uno que hace referencia a un archivo que tiene al lado se rechaza. Cargarlo da un handle
entero; `modelInstance` construye los nodos del modelo en la escena y devuelve
su raíz, que reciben después todas las llamadas `node*` y `animation*`. Las posiciones
y los tamaños dentro de un modelo van en unidades del mundo, tal como los guarda el archivo; el
nodo raíz escala la instancia entera. Véase <<scenes3d,Escenas 3D>>.
[#modelload]
==== modelLoad
[source,text]
----
model = modelLoad(name)
----
Lee un `.glb` mediante la misma búsqueda que cualquier otro recurso (carpeta del juego, carpeta de datos, base de datos empaquetada), sube sus mallas y materiales a la GPU una sola vez y guarda su árbol de nodos, sus skins y sus animaciones para instanciarlo. Llámala una vez al arrancar e instancia el handle tantas veces como haga falta; cargar el mismo archivo dos veces crea dos copias en la GPU. Necesita la escena 3D, así que en una máquina sin una GPU adecuada falla como cualquier otra llamada 3D. Un archivo erróneo termina el script con un mensaje que nombra el problema: el archivo no se pudo leer, no es glTF, hace referencia a un búfer o una imagen externos, no pasa la validación, contiene una primitiva que no se pudo subir, tiene un skin con más de `128` articulaciones o tiene una malla con skin que nombra una articulación con número `128` o superior. Una malla con skin que nombra una articulación más allá de los skins que la usan se carga, igual que una animación cuyos fotogramas clave no se pueden leer (ese sampler se omite); cualquiera de los dos casos se anota en la traza del programa. Una textura que no se puede decodificar, o una imagen KTX2 que falta, no hace fallar la carga; el material se queda sin ese mapa y la traza del programa (`--program`) dice cuál.
*Devuelve:* El handle del modelo, un entero.
*Desde:* 3.00.
*Véase también:* <<modelinstance,modelInstance>>, <<modelgetanimations,modelGetAnimations>>, <<modeldelete,modelDelete>>
.Ejemplo
[source,lua]
----
-- Se carga una vez y se coloca tres veces.
treeModel = modelLoad(DIR .. "Models/Tree.glb")
for i = 1, 3 do
local tree = modelInstance(treeModel)
nodeSetPosition(tree, i * 3 - 6, 0, -4)
nodeSetRotation(tree, 0, i * 70, 0)
end
----
[#modeldelete]
==== modelDelete
[source,text]
----
modelDelete(model)
----
Libera las mallas, los materiales y los datos analizados del modelo. Toda instancia creada a partir de él se olvida a la vez: sus nodos se quedan en la escena (con la posición, el padre y los hijos intactos) pero no dibujan nada, sus animaciones se detienen donde están, y una llamada `animation*` posterior sobre su raíz termina el script porque el nodo ya no es una instancia de modelo. Borra las raíces de las instancias con `nodeDelete` cuando tampoco quieras los nodos. Un handle que no es un modelo cargado termina el script.
*Desde:* 3.00.
*Véase también:* <<modelload,modelLoad>>, <<modelinstance,modelInstance>>, <<nodedelete,nodeDelete>>
.Ejemplo
[source,lua]
----
-- Al salir del nivel del hangar: se descartan sus objetos de atrezo y el modelo del que salen.
function unloadHangar()
for _, crate in ipairs(hangarCrates) do
nodeDelete(crate)
end
hangarCrates = {}
modelDelete(crateModel)
crateModel = nil
end
----
[#modelgetanimations]
==== modelGetAnimations
[source,text]
----
names = modelGetAnimations(model)
----
Lista los clips de animación del modelo en el orden del archivo como una tabla de cadenas, así que `names[3]` es el clip que reproduce `animationPlay(node, 3)`. Un clip que el exportador dejó sin nombre da una cadena vacía; se puede reproducir igualmente por su número. Un modelo sin animaciones da una tabla vacía. Un handle que no es un modelo cargado termina el script.
*Devuelve:* Una tabla de cadenas indexada desde `1`.
*Desde:* 3.00.
*Véase también:* <<animationplay,animationPlay>>, <<modelload,modelLoad>>
.Ejemplo
[source,lua]
----
-- Busca los clips por nombre una vez, para que el resto del juego use números.
guardModel = modelLoad(DIR .. "Models/Guard.glb")
guardClips = {}
for index, name in ipairs(modelGetAnimations(guardModel)) do
guardClips[name] = index
end
if guardClips["Idle"] == nil then
debugPrint("Guard.glb has no Idle clip")
end
----
[#modelinstance]
==== modelInstance
[source,text]
----
node = modelInstance(model)
node = modelInstance(model, parent)
----
Construye el árbol de nodos del modelo bajo un nodo raíz nuevo, que a su vez cuelga de `parent` o de la raíz de la escena. La raíz es un nodo normal: colócala, gírala, escálala o emparéntala como cualquier otro, y bórrala con `nodeDelete` para quitar la instancia entera. Los propios nodos del modelo toman sus nombres de glTF (o el nombre de su malla cuando el nodo no tiene nombre) para que `nodeFind(name, node)` llegue a ellos; una malla con varios materiales se convierte en un nodo hijo por cada parte; las luces del archivo se convierten en luces de la escena con el color, la intensidad, el alcance y el cono del archivo; los skins se enlazan y se fijan los pesos de morph. Los nodos se quedan en la pose del archivo hasta que se reproduce una animación. Un handle que no es un modelo cargado o un `parent` que no es un nodo termina el script, igual que quedarse sin nodos.
*Devuelve:* El handle del nodo raíz de la instancia.
*Desde:* 3.00.
*Véase también:* <<modelload,modelLoad>>, <<nodefind,nodeFind>>, <<animationplay,animationPlay>>, <<nodedelete,nodeDelete>>
.Ejemplo
[source,lua]
----
-- Una torreta sobre el casco del tanque, para que gire con el casco y pivote por su cuenta.
tank = modelInstance(hullModel)
turret = modelInstance(turretModel, tank)
nodeSetPosition(turret, 0, 1.1, -0.3)
barrel = nodeFind("Barrel", turret)
function onOverlayUpdate()
nodeSetRotation(turret, 0, aimYaw, 0)
nodeSetRotation(barrel, aimPitch, 0, 0)
return OVERLAY_UPDATED
end
----
[#mouse]
=== Ratón
Singe lee hasta cuatro ratones y trata las pistolas de luz como ratones. En el modo de un solo ratón, el predeterminado, todo dispositivo apuntador mueve un único cursor que se notifica como ratón `0`; `mouseSetMode(MOUSE_MANY)` distingue los dispositivos, que es lo que necesita una recreativa de pistolas de luz para dos jugadores. Las posiciones llegan en coordenadas del overlay, no en píxeles de pantalla, mediante `onMouseMoved` (véase <<enginecallbacks,Callbacks del motor>>) y a petición desde `mouseGetPosition`. Los botones del ratón y la rueda son interruptores como las teclas: llegan a `onInputPressed` y `onInputReleased` cuando `controls.cfg` asigna un código de las tablas `MOUSE_0` a `MOUSE_3`, y los valores por defecto que se distribuyen asignan el botón izquierdo a `SWITCH_BUTTON3`, el derecho a `SWITCH_BUTTON1` y el central a `SWITCH_BUTTON2`. El motor arranca con el ratón capturado, y activado salvo que se haya dado `--nomouse` o no se haya encontrado ningún ratón.
[#mousedisable]
==== mouseDisable
[source,text]
----
mouseDisable()
----
Desactiva la entrada del ratón, exactamente como hace `mouseSetEnabled(false)`. Es una función definida en `Framework.singe`, que se conserva para que los juegos antiguos funcionen sin cambios.
*Notas:* Alias heredado.
WARNING: El código nuevo debería llamar a <<mousesetenabled,mouseSetEnabled>> con `false`.
*Desde:* 1.18 (RDG)
*Véase también:* <<mousesetenabled,mouseSetEnabled>>, <<mouseenable,mouseEnable>>
.Ejemplo
[source,lua]
----
-- Código de un juego antiguo; la forma moderna es mouseSetEnabled(false).
function cutsceneBegin()
mouseDisable()
discSearch(CUTSCENE_START)
end
----
[#mouseenable]
==== mouseEnable
[source,text]
----
mouseEnable()
----
Activa la entrada del ratón, exactamente como hace `mouseSetEnabled(true)`, y, como ella, deja el ratón desactivado si Singe se arrancó con `--nomouse`. Es una función definida en `Framework.singe`, que se conserva para que los juegos antiguos funcionen sin cambios.
*Notas:* Alias heredado.
WARNING: El código nuevo debería llamar a <<mousesetenabled,mouseSetEnabled>> con `true`.
*Desde:* 1.18 (RDG)
*Véase también:* <<mousesetenabled,mouseSetEnabled>>, <<mousedisable,mouseDisable>>
.Ejemplo
[source,lua]
----
-- Código de un juego antiguo; la forma moderna es mouseSetEnabled(true).
function cutsceneEnd()
mouseEnable()
discSearch(LEVEL_START)
end
----
[#mousegetname]
==== mouseGetName
[source,text]
----
name = mouseGetName(mouse)
----
El nombre que el controlador del ratón da a un dispositivo, o `nil` cuando no hay ningún dispositivo con ese índice. Con él se distinguen dos pistolas de una recreativa en una pantalla de servicio, algo que de otro modo es adivinar cuando las dos se notifican como ratón `0` y `1`. Un índice fuera de `0` a `3` aborta el script.
*Parámetros:*
* `mouse` -- índice del dispositivo, de `0` a `3`.
*Devuelve:* cadena, o `nil`.
*Desde:* 3.00
*Véase también:* <<mousehowmany,mouseHowMany>>, <<mousesetmode,mouseSetMode>>, <<controllergetname,controllerGetName>>
.Ejemplo
[source,lua]
----
-- Nombra las pistolas en la pantalla de calibración.
mouseSetMode(MOUSE_MANY)
for device = 0, mouseHowMany() - 1 do
fontPrint(20, 20 + device * 18, string.format("Gun %d: %s", device + 1, mouseGetName(device)))
end
----
[#mousegetpadding]
==== mouseGetPadding
[source,text]
----
padding = mouseGetPadding()
----
Extensión de Hypseus, siempre `0`, por la razón que da <<controllergetpadding,`controllerGetPadding`>>. Hypseus suma este desplazamiento a los códigos de entrada que notifican sus ratones en modo relativo; los códigos de Singe no llevan tal desplazamiento, así que no hay nada que restar.
*Devuelve:* un entero, siempre `0`.
*Desde:* 3.00
*Véase también:* <<controllergetpadding,controllerGetPadding>>
[#mousegetposition]
==== mouseGetPosition
[source,text]
----
x, y = mouseGetPosition(index)
----
Devuelve la última posición notificada de un ratón en coordenadas del overlay, los mismos valores que llevaba el `onMouseMoved` más reciente de ese dispositivo. En el modo de un solo ratón solo se mueve el ratón `0`; en el modo de muchos ratones cada dispositivo tiene su propia posición, que empieza en el centro del vídeo. La posición deja de actualizarse mientras la entrada del ratón está desactivada. Consúltala desde `onOverlayUpdate` o `singeMain` cuando eso sea más sencillo que seguir el callback. Un índice fuera de `0` a `3` aborta el script.
*Parámetros:*
* `index` -- índice del ratón, de `0` a `3`.
*Devuelve:* dos enteros, `x` e `y`, en coordenadas del overlay.
*Desde:* 2.00
*Véase también:* <<mousehowmany,mouseHowMany>>, <<onmousemoved,onMouseMoved>>, <<singewantscrosshairs,singeWantsCrosshairs>>
.Ejemplo
[source,lua]
----
-- Dibuja la mira donde apunta la pistola.
function onOverlayUpdate()
overlayClear()
drawTargets()
if singeWantsCrosshairs() then
local x, y = mouseGetPosition(0)
spriteDraw(crosshair, x, y, true)
end
return OVERLAY_UPDATED
end
----
[#mousehowmany]
==== mouseHowMany
[source,text]
----
count = mouseHowMany()
----
Devuelve el número de ratones que el motor encontró al arrancar, de `0` a `4`. Los dispositivos conectados después no cuentan. Si no se encontró ninguno, el ratón sigue desactivado. Úsalo para decidir si ofrecer un segundo jugador con pistola de luz.
*Devuelve:* entero, de `0` a `4`.
*Desde:* 1.18 (RDG)
*Véase también:* <<mousesetmode,mouseSetMode>>, <<mousegetposition,mouseGetPosition>>
.Ejemplo
[source,lua]
----
-- Hay dos pistolas: cada jugador recibe un cursor.
if mouseHowMany() >= 2 then
mouseSetMode(MOUSE_MANY)
players = 2
else
players = 1
end
----
[#mousesetcaptured]
==== mouseSetCaptured
[source,text]
----
mouseSetCaptured(captured)
----
Captura el ratón, confinándolo a la ventana de Singe y ocultando el cursor, o lo libera y vuelve a mostrar el cursor. El motor captura el ratón al arrancar, así que la mayoría de los juegos nunca llaman a esto; un editor de niveles en ventana o un juego que quiere ver el cursor del escritorio lo libera. El interruptor asignado a `INPUT_GRAB` en `controls.cfg` alterna el mismo estado desde el teclado.
*Parámetros:*
* `captured` -- booleano; `true` captura y oculta, `false` libera y muestra.
*Desde:* 2.00
*Véase también:* <<mousesetenabled,mouseSetEnabled>>, <<mousegetposition,mouseGetPosition>>
.Ejemplo
[source,lua]
----
-- El editor de niveles quiere el cursor del escritorio; al jugar se vuelve a ocultar.
function editorToggle()
editing = not editing
mouseSetCaptured(not editing)
end
----
[#mousesetenabled]
==== mouseSetEnabled
[source,text]
----
mouseSetEnabled(enabled)
----
Activa o desactiva la entrada del ratón. Mientras está desactivada, los eventos de movimiento, de botones y de rueda se descartan antes de llegar al script: `onMouseMoved` deja de dispararse, los botones del ratón ya no producen interruptores y `mouseGetPosition` sigue devolviendo la última posición que tenía. El propio cursor se sigue moviendo. Al activarla se respeta `--nomouse`; si Singe se arrancó con esa opción, el ratón sigue desactivado. Un argumento que no sea booleano aborta el script.
*Parámetros:*
* `enabled` -- booleano.
*Desde:* 3.00
*Véase también:* <<mousesetcaptured,mouseSetCaptured>>, <<mousesetmode,mouseSetMode>>, <<mouseenable,mouseEnable>>
.Ejemplo
[source,lua]
----
-- Ignora la pistola mientras se reproduce el bucle del modo de atracción.
function attractBegin()
mouseSetEnabled(false)
discSearch(ATTRACT_START)
end
function attractEnd()
mouseSetEnabled(true)
end
----
[#mousesetmode]
==== mouseSetMode
[source,text]
----
mouseSetMode(mode)
----
Elige cómo se leen los ratones. En `MOUSE_SINGLE` (por defecto) los eventos del puntero de la ventana mueven un único cursor que se notifica como dispositivo `0`, con las posiciones absolutas y el movimiento relativo convertidos a coordenadas del overlay. En `MOUSE_MANY` cada dispositivo físico se lee por separado: el movimiento relativo se integra en una posición por dispositivo limitada al área del vídeo, los dispositivos absolutos como las tabletas y algunas pistolas de luz se proyectan sobre el vídeo, y `onMouseMoved` notifica el índice del dispositivo. Los dos modos entregan los mismos cinco botones y la rueda vertical como interruptores. `SINGLE_MOUSE` y `MANY_MOUSE` se aceptan como grafías antiguas; cualquier otro valor aborta el script. `--manymouse` es decisión del propio usuario y prevalece sobre esta llamada: si se da, el modo es `MOUSE_MANY` desde el principio y `mouseSetMode(MOUSE_SINGLE)` no lo desactiva, así que una recreativa de dos pistolas se puede hacer funcionar desde fuera de un juego que nunca lo pide.
*Parámetros:*
* `mode` -- `MOUSE_SINGLE` (`100`) o `MOUSE_MANY` (`200`).
*Desde:* 1.18 (RDG)
*Véase también:* <<mousehowmany,mouseHowMany>>, <<onmousemoved,onMouseMoved>>, <<mousegetposition,mouseGetPosition>>
.Ejemplo
[source,lua]
----
-- Juego cooperativo de pistolas de luz: cada pistola mueve su propio cursor.
mouseSetMode(MOUSE_MANY)
function onMouseMoved(x, y, xr, yr, which)
if which < 2 then
cursorX[which + 1] = x
cursorY[which + 1] = y
end
end
----
[#musicref]
=== Música
Música de fondo, cargada y reproducida aparte de los efectos de sonido y con su propio volumen. Hypseus pone la música en un segundo mezclador precisamente por eso, y Singe le da en cambio su propia ganancia: <<musicsetvolume,`musicSetVolume`>> nunca mueve un efecto de sonido y <<soundsetvolume,`soundSetVolume`>> nunca mueve la música.
Un handle de música es un tipo de handle propio, ni un handle de sonido ni un canal: un handle es una pista, y se reproduce, se pausa, se reanuda y se detiene por su cuenta. No hay límite de canales, a diferencia de los dieciséis canales de efectos de sonido.
La escala de volumen es la de Hypseus, de `0` a `128`, casi el doble del `0` a `63` que usa la familia `sound`, así que un juego escrito para Hypseus conserva sus propios números. La música empieza tan alta como los efectos de sonido, al nivel que haya fijado `--volume_nonvldp`; Hypseus, en cambio, arranca su mezclador de música a un tercio, cosa que todo juego de Hypseus sobrescribe de inmediato con `musicSetVolume(soundGetVolume() * 2)`.
Aquí se lee todo formato que lee la familia de sonido: WAV, Ogg Vorbis, Opus, FLAC, MP3, WavPack y los demás. La cabecera de la traza que escriben `--program` y `--trace` indica exactamente qué decodificadores lleva la compilación, en su línea `Audio:`.
Pausar el motor (la tecla de pausa, o `singeSetPauseFlag`) pausa la música con él, y reanudarlo la devuelve; una recarga detiene y libera todas las pistas.
[#musicgetposition]
==== musicGetPosition
[source,text]
----
seconds = musicGetPosition(id)
----
Dónde está el oyente en una pista que se reproduce, en segundos: la posición del propio mezclador menos lo que todavía va de camino a los altavoces -- la cola del dispositivo, la calibración de audio de la máquina y el retardo de audio del juego -- para que una imagen sincronizada con ella coincida con el sonido. Es el reloj al que animar cuando manda la música, como en una demo o un juego de ritmo, y se mantiene con la canción por mucho que dure, mientras que contar fotogramas o ticks se desvía. El mezclador mueve su posición un búfer cada vez, así que entre movimientos la respuesta sigue avanzando con el reloj, nunca más de 50 ms, y se mueve con suavidad en cada fotograma. Una pista en pausa se queda quieta. Una pista que no se está reproduciendo responde `-1`.
*Parámetros:*
* `id` -- número; un handle de música.
*Devuelve:* número; segundos desde el principio de la pista, o `-1`.
*Desde:* 3.00
*Véase también:* <<musicplay,musicPlay>>, <<singegetaudiolatency,singeGetAudioLatency>>, <<singegetaudiocalibration,singeGetAudioCalibration>>
.Ejemplo
[source,lua]
----
-- Un destello en cada pulso de una canción de 120 pulsos por minuto, en el pulso tal como se oye.
function onOverlayUpdate()
colorBackground(0, 0, 0)
overlayClear()
local t = musicGetPosition(song)
if t >= 0 and (t * 2) % 1 < 0.1 then
colorBackground(255, 255, 255, 120)
overlayClear()
end
return OVERLAY_UPDATED
end
----
[#musicisplaying]
==== musicIsPlaying
[source,text]
----
playing = musicIsPlaying([id])
----
Extensión de Hypseus. Si esa pista se está reproduciendo o -- sin handle -- si se está reproduciendo alguna música. Una pista en pausa no se está reproduciendo.
*Parámetros:*
* `id` -- número opcional; un handle de música.
*Devuelve:* booleano.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<musicplay,musicPlay>>, <<musicpause,musicPause>>
.Ejemplo
[source,lua]
----
if not musicIsPlaying(theme) then
musicPlay(theme, -1)
end
----
[#musicload]
==== musicLoad
[source,text]
----
id = musicLoad(filename)
----
Extensión de Hypseus. Carga una pista de música mediante el sistema de archivos virtual y devuelve su handle. El archivo entero se decodifica de antemano, como un sonido, así que una pista larga cuesta memoria; el handle es válido hasta <<musicunload,`musicUnload`>> o una recarga.
Un archivo que falta, o que ningún decodificador incluido en la compilación puede leer, termina el script, como hace <<soundload,`soundLoad`>>.
*Parámetros:*
* `filename` -- cadena; una ruta a la que pueda llegar el sistema de archivos virtual.
*Devuelve:* número, el handle de música.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<musicplay,musicPlay>>, <<musicunload,musicUnload>>, <<soundload,soundLoad>>
.Ejemplo
[source,lua]
----
local theme = musicLoad("music/attract.mp3")
musicSetVolume(96)
musicPlay(theme, -1)
----
[#musicpause]
==== musicPause
[source,text]
----
musicPause([id])
----
Extensión de Hypseus. Pausa esa pista donde está, o toda la música cuando no se da ningún handle. <<musicresume,`musicResume`>> la retoma.
*Parámetros:*
* `id` -- número opcional; un handle de música.
*Devuelve:* nada.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<musicresume,musicResume>>, <<musicstop,musicStop>>
.Ejemplo
[source,lua]
----
-- El jugador ha abierto el mapa; la música se retiene en vez de reiniciarse.
musicPause()
----
[#musicplay]
==== musicPlay
[source,text]
----
id = musicPlay(id [, loops])
----
Extensión de Hypseus. Reproduce una pista desde el principio y responde con su handle, que es como lo guardan los juegos de Hypseus (`sndHandle = musicPlay(music)`) para detener la pieza después. `loops` significa lo mismo que en <<soundplay,`soundPlay`>>: `0`, el valor por defecto, reproduce una vez, `-1` reproduce para siempre y `N` repite `N` veces más. La cuenta se limita a entre `-1` y `64`, como la limita Hypseus, así que un juego que pide más obtiene lo que habría obtenido allí.
Reproducir una pista que ya se está reproduciendo la vuelve a empezar desde el principio.
*Parámetros:*
* `id` -- número; un handle de música.
* `loops` -- número opcional; de `-1` a `64`. Por defecto `0`.
*Devuelve:* número; el handle que se le dio.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<musicstop,musicStop>>, <<musicsetvolume,musicSetVolume>>
.Ejemplo
[source,lua]
----
musicPlay(theme, -1) -- Vuelta tras vuelta hasta que algo la pare.
----
[#musicresume]
==== musicResume
[source,text]
----
musicResume([id])
----
Extensión de Hypseus. Reanuda una pista en pausa, o toda la música en pausa cuando no se da ningún handle.
La forma sin argumentos del propio Hypseus no hace nada en absoluto; la de Singe lo reanuda todo, que es lo que hace pareja con <<musicpause,`musicPause`>>.
*Parámetros:*
* `id` -- número opcional; un handle de música.
*Devuelve:* nada.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<musicpause,musicPause>>
.Ejemplo
[source,lua]
----
musicResume()
----
[#musicsetvolume]
==== musicSetVolume
[source,text]
----
volume = musicSetVolume(volume)
----
Extensión de Hypseus. Fija la ganancia de toda la música a la vez, en la escala de `0` a `128` de Hypseus, y responde con el nivel que ha tomado. No mueve nada más que la música: los efectos de sonido conservan lo que les diera <<soundsetvolume,`soundSetVolume`>>, y `soundSetVolume` deja en paz la música.
Un nivel fuera de `0` a `128` termina el script. El nivel se aplica a las pistas que ya se están reproduciendo y a las que se carguen después, y vuelve al nivel inicial con una recarga.
*Parámetros:*
* `volume` -- número; de `0` a `128`.
*Devuelve:* número, el nivel que se fijó.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<soundsetvolume,soundSetVolume>>, <<musicplay,musicPlay>>
.Ejemplo
[source,lua]
----
-- Lo que escribe un juego de Hypseus: el nivel de sonido, duplicado a la escala de la música.
musicSetVolume(soundGetVolume() * 2)
----
[#musicstop]
==== musicStop
[source,text]
----
musicStop([id [, fadeMs]])
----
Extensión de Hypseus. Detiene esa pista, o toda la música cuando no se da ningún handle (un handle `nil` cuenta como ninguno). `fadeMs` la desvanece durante ese número de milisegundos en lugar de cortarla; `0`, el valor por defecto, la detiene en el acto. Un fundido negativo termina el script.
*Parámetros:*
* `id` -- número opcional; un handle de música.
* `fadeMs` -- número opcional; milisegundos que dura el fundido de salida. Por defecto `0`.
*Devuelve:* nada.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<musicplay,musicPlay>>, <<musicpause,musicPause>>
.Ejemplo
[source,lua]
----
musicStop(theme, 1500) -- Se apaga en segundo y medio.
----
[#musicunload]
==== musicUnload
[source,text]
----
musicUnload(id)
----
Extensión de Hypseus. Detiene la pista y la libera. Después el handle deja de ser válido y usarlo termina el script.
*Parámetros:*
* `id` -- número; un handle de música.
*Devuelve:* nada.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<musicload,musicLoad>>
.Ejemplo
[source,lua]
----
musicUnload(theme)
----
[#nav]
=== Navegación
Mallas de navegación precalculadas a partir de la geometría de la escena, y los agentes de multitud que las recorren (véase <<scenes3d,Escenas 3D>>, Navegación). `navNew` y `navLoad` devuelven un handle de malla y `navAgentNew` un handle de agente; ambos son enteros, se comprueban en cada llamada, y uno no válido termina el script. Las posiciones van en unidades del mundo, las velocidades en unidades por segundo y las pendientes en grados. Hasta ocho mallas a la vez, 128 agentes en cada una y 256 en total. Las consultas y los agentes necesitan una malla que `navBuild` haya precalculado o `navLoad` haya leído; antes de eso las consultas devuelven `nil`, `navDraw` no dibuja nada y `navAgentNew` termina el script.
[#navaddnode]
==== navAddNode
[source,text]
----
navAddNode(nav, node)
----
Añade la malla del nodo, y la de cada nodo que cuelga de él, a la geometría a partir de la cual se precalculará la malla de navegación, tal como están en el mundo en este momento, así que colócalos antes. El nodo raíz de una instancia de modelo añade el modelo entero, y un nivel construido con primitivas se puede añadir nodo a nodo. Solo cuenta la geometría de malla: los cuerpos, los sprites y el texto no añaden nada. Termina el script cuando la malla ya se ha construido.
*Desde:* 3.00.
*Véase también:* <<navnew,navNew>>, <<navbuild,navBuild>>, <<modelinstance,modelInstance>>
.Ejemplo
[source,lua]
----
-- El suelo y las paredes son geometría transitable; las cajas no, porque se mueven.
local nav = navNew(0.4, 1.8, 45, 0.4)
navAddNode(nav, ground)
for _, wall in ipairs(walls) do
navAddNode(nav, wall)
end
navAddNode(nav, modelInstance(towerModel))
navBuild(nav)
----
[#navagentdelete]
==== navAgentDelete
[source,text]
----
navAgentDelete(agent)
----
Quita el agente de la malla y libera su handle; el nodo se queda donde está y ya no se vuelve a mover. Borrar la malla con `navDelete` hace esto con todos los agentes que tiene. Un handle no válido termina el script.
*Desde:* 3.00.
*Véase también:* <<navagentnew,navAgentNew>>, <<navdelete,navDelete>>
.Ejemplo
[source,lua]
----
-- Un guardia que muere deja de navegar; a partir de aquí el ragdoll se hace cargo del modelo.
function killGuard(guard)
navAgentDelete(guard.agent)
guard.agent = nil
ragdollActivate(guard.model)
ragdollApplyImpulse(guard.model, "b_Spine02_03", 0, 20, -30)
end
----
[#navagentgetvelocity]
==== navAgentGetVelocity
[source,text]
----
x, y, z = navAgentGetVelocity(agent)
----
Cómo mueve la multitud al agente en este fotograma, en unidades del mundo por segundo a lo largo de cada eje; su longitud llega hasta la velocidad del agente y cae a cero cuando se detiene. Sirve para elegir entre una animación de reposo, de andar o de correr, o para ajustar el ciclo de andar al terreno recorrido.
*Devuelve:* Tres números.
*Desde:* 3.00.
*Véase también:* <<navagentmoveto,navAgentMoveTo>>, <<animationplay,animationPlay>>
.Ejemplo
[source,lua]
----
function onOverlayUpdate()
for agent, fox in pairs(foxes) do
local vx, vy, vz = navAgentGetVelocity(agent)
local speed = math.sqrt(vx * vx + vz * vz)
local clip = (speed < 0.1) and "Survey" or ((speed < 2.5) and "Walk" or "Run")
if fox.clip ~= clip then
animationPlay(fox.node, clip, true, 1, 0.2)
fox.clip = clip
end
end
return OVERLAY_NOT_UPDATED
end
----
[#navagentisarrived]
==== navAgentIsArrived
[source,text]
----
arrived = navAgentIsArrived(agent)
----
Si el agente ha llegado al último destino que le dio `navAgentMoveTo`: a menos de uno y medio de sus radios del destino y casi parado. Pasa a `true` en el momento en que se dispara `onNavArrived` y sigue en `true` hasta el siguiente `navAgentMoveTo`; es `false` para un agente al que nunca se envió a ningún sitio o que se detuvo por el camino.
*Devuelve:* Verdadero o falso.
*Desde:* 3.00.
*Véase también:* <<onnavarrived,onNavArrived>>, <<navagentmoveto,navAgentMoveTo>>, <<navagentstop,navAgentStop>>
.Ejemplo
[source,lua]
----
-- La patrulla espera un rato en cada puesto antes de seguir.
function onOverlayUpdate()
if navAgentIsArrived(patrol) then
waitFrames = waitFrames + 1
if waitFrames > 180 then
post = post % #posts + 1
waitFrames = 0
navAgentMoveTo(patrol, posts[post][1], posts[post][2], posts[post][3])
end
end
return OVERLAY_NOT_UPDATED
end
----
[#navagentmoveto]
==== navAgentMoveTo
[source,text]
----
ok = navAgentMoveTo(agent, x, y, z)
----
Envía al agente a caminar hasta el punto de la malla más cercano a `x, y, z`, buscando uno hasta 2 unidades a los lados y 4 hacia arriba o hacia abajo. La multitud planifica la ruta en los fotogramas siguientes y guía al agente alrededor de los demás agentes por el camino; `onNavArrived` se dispara cuando llega. Devuelve `false`, y deja al agente como estaba, cuando ningún punto de la malla está lo bastante cerca o la multitud rechaza la petición. Un destino nuevo sustituye al anterior en cualquier momento.
*Parámetros:*
* `agent` -- el handle de `navAgentNew`.
* `x`, `y`, `z` -- el destino en unidades del mundo; se ajusta a la malla.
*Devuelve:* Verdadero cuando el agente se ha puesto en marcha.
*Desde:* 3.00.
*Véase también:* <<navagentstop,navAgentStop>>, <<navnearest,navNearest>>, <<navpath,navPath>>
.Ejemplo
[source,lua]
----
-- El compañero sigue al héroe, con el destino corregido dos veces por segundo.
function onOverlayUpdate()
ticks = ticks + 1
if ticks % 30 == 0 then
local hx, hy, hz = nodeGetWorldPosition(hero)
if not navAgentMoveTo(sidekickAgent, hx, hy, hz) then
navAgentStop(sidekickAgent) -- El héroe está fuera de la malla: saltando, nadando
end
end
return OVERLAY_NOT_UPDATED
end
----
[#navagentnew]
==== navAgentNew
[source,text]
----
agent = navAgentNew(nav, node, radius, height, speed)
----
Pone en la malla un agente de ese tamaño en el punto más cercano a la posición en el mundo del nodo (la posición tal cual cuando no hay nada a menos de 2 unidades a los lados y 4 hacia arriba o hacia abajo), que camina a `speed` unidades por segundo y acelera a 8 unidades por segundo al cuadrado. El radio y la altura se mantienen en `0.01` o más, como hace `navNew` con los suyos. A partir de ahí el motor mueve el nodo por la malla en cada fotograma, en el sistema de referencia de su padre, y lo gira alrededor de Y para que mire hacia donde va siempre que se mueve a más de `0.05` unidades por segundo; los nodos miran hacia -Z, como los modelos. La multitud guía a los agentes unos alrededor de otros, anticipa los giros y los mantiene separados. Para manejar un controlador de jugador sobre el nodo en vez de colocar el nodo, véase `navAgentSetPlayer`. Termina el script cuando la malla no está construida o ya hay 128 agentes en ella.
*Parámetros:*
* `nav` -- una malla construida, de `navNew` o de `navLoad`.
* `node` -- el nodo que se mueve; normalmente la raíz de una instancia de modelo.
* `radius` -- el radio del agente para mantenerse apartado de los demás, en unidades del mundo; el radio de la propia malla o menos.
* `height` -- la altura del agente, en unidades del mundo.
* `speed` -- la velocidad máxima al caminar, en unidades del mundo por segundo.
*Devuelve:* El handle del agente, un entero.
*Desde:* 3.00.
*Véase también:* <<navagentmoveto,navAgentMoveTo>>, <<navagentsetplayer,navAgentSetPlayer>>, <<navagentdelete,navAgentDelete>>, <<navrandompoint,navRandomPoint>>
.Ejemplo
[source,lua]
----
for i = 1, 6 do
local fox = modelInstance(foxModel)
nodeSetScale(fox, 0.01)
nodeSetPosition(fox, navRandomPoint(nav))
animationPlay(fox, "Walk", true)
local agent = navAgentNew(nav, fox, 0.35, 1.0, 1.4)
navAgentMoveTo(agent, navRandomPoint(nav))
end
function onNavArrived(agent)
navAgentMoveTo(agent, navRandomPoint(nav))
end
----
[#navagentsetplayer]
==== navAgentSetPlayer
[source,text]
----
navAgentSetPlayer(agent, player)
----
Con `true`, el agente deja de colocar su nodo y, en su lugar, pasa su velocidad en cada fotograma al controlador de jugador del nodo como un `playerMove` (solo horizontal), así que las escaleras, las pendientes, los empujones y las colisiones siguen funcionando; antes de cada actualización de la multitud se lee la posición en el mundo del controlador como la del propio agente, así que la planificación tiene en cuenta que lo saquen de su rumbo a empujones. El nodo sigue girando para mirar hacia donde va. El nodo necesita un controlador de `playerNew`, o nada se mueve. Con `false` el agente vuelve a colocar el nodo él mismo.
*Desde:* 3.00.
*Véase también:* <<playernew,playerNew>>, <<playermove,playerMove>>, <<navagentnew,navAgentNew>>
.Ejemplo
[source,lua]
----
-- Un compañero que sigue al héroe por el nivel y al que se puede zarandear.
local companion = nodeNew()
nodeSetPosition(companion, 2, 0.05, 0)
playerNew(companion, 0.3, 1.0)
local companionFox = modelInstance(foxModel)
nodeSetParent(companionFox, companion)
nodeSetScale(companionFox, 0.01)
local companionAgent = navAgentNew(nav, companion, 0.3, 1.0, 2.0)
navAgentSetPlayer(companionAgent, true)
----
[#navagentstop]
==== navAgentStop
[source,text]
----
navAgentStop(agent)
----
Olvida el destino del agente: la multitud deja de guiarlo hacia ningún sitio, frena hasta quedarse parado donde está y `onNavArrived` no se dispara. Los demás agentes siguen rodeándolo al caminar. Envíalo a algún sitio con `navAgentMoveTo` para que vuelva a ponerse en marcha.
*Desde:* 3.00.
*Véase también:* <<navagentmoveto,navAgentMoveTo>>, <<navagentisarrived,navAgentIsArrived>>
.Ejemplo
[source,lua]
----
-- Los guardias se quedan quietos cuando suena la alarma y se vuelven hacia el ruido.
function raiseAlarm(x, y, z)
for _, guard in ipairs(guards) do
navAgentStop(guard.agent)
nodeLookAt(guard.node, x, y, z)
animationPlay(guard.node, "Survey", true, 1, 0.3)
end
soundPlay(alarm)
end
----
[#navbuild]
==== navBuild
[source,text]
----
navBuild(nav)
----
Precalcula la malla transitable a partir de todo lo que añadió `navAddNode`: suelos y pendientes hasta el ángulo de la malla, escalones hasta su altura, con el radio del agente recortado alrededor de cada pared y cada desnivel, y dejando fuera todo lo que tenga encima menos espacio que la altura del agente. El precálculo trabaja con celdas de un tercio del radio del agente de ancho y cubre como mucho 4096 por 4096 de ellas, así que un radio pequeño sobre un nivel grande es lento y puede fallar; una décima de segundo para Sponza en un ordenador de sobremesa, unos segundos en una Raspberry Pi. La geometría añadida se libera después y ya no se puede añadir más; precalcula otra malla en su lugar. `navSave` guarda el resultado. Termina el script cuando no se ha añadido nada, la malla ya está construida o el precálculo falla.
*Desde:* 3.00.
*Véase también:* <<navnew,navNew>>, <<navaddnode,navAddNode>>, <<navsave,navSave>>, <<navdraw,navDraw>>
.Ejemplo
[source,lua]
----
-- El precálculo se hace una vez, mientras se ve la pantalla de título, y se guarda para la próxima vez.
local sponza = modelInstance(modelLoad("Models/Sponza.glb"))
local nav = navNew(0.4, 1.6, 45, 0.3)
navAddNode(nav, sponza)
navBuild(nav)
navSave(nav, "sponza.nav")
if debugging then
navDraw(nav) -- Un fotograma, para echar un vistazo al precálculo
end
----
[#navdelete]
==== navDelete
[source,text]
----
navDelete(nav)
----
Libera la malla y todos los agentes que tiene; sus nodos se quedan donde están. El handle de la malla y los de los agentes dejan de ser válidos y terminan el script si se usan. La ranura queda libre para un nuevo `navNew` o `navLoad`.
*Desde:* 3.00.
*Véase también:* <<navnew,navNew>>, <<navagentdelete,navAgentDelete>>
.Ejemplo
[source,lua]
----
function loadLevel(index)
if levelNav then
navDelete(levelNav) -- Se lleva consigo los agentes del nivel anterior
end
nodeDelete(levelRoot)
levelRoot = modelInstance(levels[index].model)
levelNav = navNew(0.4, 1.6, 45, 0.3)
navAddNode(levelNav, levelRoot)
navBuild(levelNav)
end
----
[#navdraw]
==== navDraw
[source,text]
----
navDraw(nav)
navDraw(nav, r, g, b)
----
Dibuja los polígonos de la malla precalculada como líneas sobre la escena en este fotograma, en cian salvo que se indique un color, cada polígono como un abanico de triángulos, de modo que un polígono de más de tres lados muestra sus diagonales. Llámala en cada fotograma mientras miras; cuesta tres líneas por triángulo, y una malla que aún no está construida no dibuja nada. Sirve para comprobar que un precálculo cubre el suelo y no se sube a los muebles, junto con `physicsSetDebug`.
*Parámetros:*
* `nav` -- la malla.
* `r`, `g`, `b` -- el color de las líneas, de `0` a `255` cada uno; por defecto `0, 255, 255`.
*Desde:* 3.00.
*Véase también:* <<linedraw,lineDraw>>, <<navbuild,navBuild>>, <<physicssetdebug,physicsSetDebug>>
.Ejemplo
[source,lua]
----
function onOverlayUpdate()
if showNavMesh then
navDraw(nav) -- Cian sobre el suelo
local path = navPath(nav, heroX, heroY, heroZ, goalX, goalY, goalZ)
if path then
for i = 2, #path do
lineDraw(path[i - 1][1], path[i - 1][2], path[i - 1][3], path[i][1], path[i][2], path[i][3], 255, 60, 40)
end
end
end
return OVERLAY_NOT_UPDATED
end
----
[#navload]
==== navLoad
[source,text]
----
nav = navLoad(name, agentRadius, agentHeight)
----
Carga una malla que escribió `navSave`, buscándola primero entre los archivos del juego (así una malla precalculada puede distribuirse con el juego, empaquetado o no) y después en la carpeta de datos del juego. La malla vuelve construida, lista para agentes y consultas, y ya no admite más `navAddNode`. La pendiente y el escalón con los que se precalculó están en el archivo; el radio y la altura del agente son los valores con los que guía la multitud, así que da los valores para los que se precalculó. Termina el script cuando el archivo no se puede leer, no es una malla de navegación, es más corto que la malla que describe su cabecera (truncado o editado a mano) o no queda ninguna ranura de malla libre.
*Parámetros:*
* `name` -- el nombre del archivo, relativo a la carpeta del juego o a la carpeta de datos.
* `agentRadius` -- el radio de agente para el que se precalculó la malla, en unidades del mundo.
* `agentHeight` -- la altura de agente para la que se precalculó, en unidades del mundo.
*Devuelve:* El handle de la malla, un entero.
*Desde:* 3.00.
*Véase también:* <<navsave,navSave>>, <<navnew,navNew>>
.Ejemplo
[source,lua]
----
-- El precálculo del museo que se distribuye con el juego; agentes de 0.8 de ancho y 1.7 de alto.
local museum = modelInstance(modelLoad("museum.glb"))
local nav = navLoad("museum.nav", 0.4, 1.7)
for _, visitor in ipairs(visitors) do
visitor.agent = navAgentNew(nav, visitor.node, 0.35, 1.7, 1.2)
navAgentMoveTo(visitor.agent, navRandomPoint(nav))
end
----
[#navnearest]
==== navNearest
[source,text]
----
x, y, z = navNearest(nav, x, y, z)
----
El punto de la malla más cercano a un punto, buscando hasta 2 unidades a los lados y 4 hacia arriba o hacia abajo, o `nil` cuando no hay nada tan cerca. Sirve para ajustar al suelo un punto de aparición, un objeto soltado o un clic antes de dárselo a un agente, y para hallar la altura del suelo bajo un punto.
*Parámetros:*
* `nav` -- la malla.
* `x`, `y`, `z` -- el punto que se ajusta, en unidades del mundo.
*Devuelve:* Tres números, o `nil`.
*Desde:* 3.00.
*Véase también:* <<navpath,navPath>>, <<navagentmoveto,navAgentMoveTo>>, <<sceneunproject,sceneUnproject>>
.Ejemplo
[source,lua]
----
-- Hace aparecer al enemigo en el suelo más cercano al generador, o no lo hace aparecer.
function spawnEnemy(spawner)
local sx, sy, sz = nodeGetWorldPosition(spawner)
local x, y, z = navNearest(nav, sx, sy, sz)
if x then
local enemy = modelInstance(enemyModel)
nodeSetPosition(enemy, x, y, z)
enemies[#enemies + 1] = { node = enemy, agent = navAgentNew(nav, enemy, 0.4, 1.8, 2.5) }
end
end
----
[#navnew]
==== navNew
[source,text]
----
nav = navNew(agentRadius, agentHeight, maxSlope, maxStep)
----
Empieza una malla de navegación para agentes de ese radio y esa altura que pueden subir escalones de hasta `maxStep` unidades y pendientes de hasta `maxSlope` grados. El radio y la altura se mantienen en `0.01` o más, la pendiente entre `0` y `89` y el escalón en `0` o más. El radio fija también la resolución del precálculo, pues una celda mide un tercio de él, así que decide cuánto tarda `navBuild` y qué tamaño de nivel puede cubrir. Añade geometría con `navAddNode` y después llama a `navBuild`. Termina el script cuando ya existen ocho mallas.
*Parámetros:*
* `agentRadius` -- el radio de los agentes, en unidades del mundo; las paredes se rodean de este margen.
* `agentHeight` -- lo altos que son; los techos más bajos no son transitables.
* `maxSlope` -- la pendiente transitable más empinada, en grados.
* `maxStep` -- el escalón más alto que suben, en unidades del mundo.
*Devuelve:* El handle de la malla, un entero.
*Desde:* 3.00.
*Véase también:* <<navaddnode,navAddNode>>, <<navbuild,navBuild>>, <<navload,navLoad>>
.Ejemplo
[source,lua]
----
-- Sponza para un agente de 0.8 de ancho y 1.6 de alto que sube 0.3 y camina por pendientes de hasta 45 grados.
local sponza = modelInstance(modelLoad("Models/Sponza.glb"))
local nav = navNew(0.4, 1.6, 45, 0.3)
navAddNode(nav, sponza)
navBuild(nav)
----
[#navpath]
==== navPath
[source,text]
----
points = navPath(nav, x0, y0, z0, x1, y1, z1)
----
Las esquinas del camino a pie más corto de un punto a otro, ajustados antes ambos puntos a la malla, como una tabla de tablas `{ x, y, z }` con la primera en el inicio y la última en el final; hasta 256 esquinas. Cuando no se puede llegar al final, el camino se detiene en el punto alcanzable más cercano por el camino, así que la última esquina siempre está en un sitio donde un agente puede estar de pie. `nil` cuando cualquiera de los dos extremos está lejos de la malla o no se encuentra ningún camino. Sirve para dibujar una ruta, medir a qué distancia está algo a pie o mover algo por ella tú mismo; los agentes lo hacen solos.
*Parámetros:*
* `nav` -- la malla.
* `x0`, `y0`, `z0` -- dónde empieza el camino.
* `x1`, `y1`, `z1` -- dónde termina.
*Devuelve:* Una tabla de tablas `{ x, y, z }`, o `nil`.
*Desde:* 3.00.
*Véase también:* <<navagentmoveto,navAgentMoveTo>>, <<navraycast,navRaycast>>, <<navnearest,navNearest>>
.Ejemplo
[source,lua]
----
-- A qué distancia está la salida a pie, para el indicador de distancia del mapa.
function walkingDistance(fromX, fromY, fromZ, toX, toY, toZ)
local path = navPath(nav, fromX, fromY, fromZ, toX, toY, toZ)
local total = 0
for i = 2, #(path or {}) do
local dx = path[i][1] - path[i - 1][1]
local dz = path[i][3] - path[i - 1][3]
total = total + math.sqrt(dx * dx + dz * dz)
end
return path and total or nil
end
----
[#navrandompoint]
==== navRandomPoint
[source,text]
----
x, y, z = navRandomPoint(nav)
----
Un punto al azar de la malla transitable, en cualquier sitio al que llegue, para deambular, esparcir objetos que recoger o colocar un punto de aparición. `nil` solo cuando la malla no está construida.
*Devuelve:* Tres números, o `nil`.
*Desde:* 3.00.
*Véase también:* <<navagentmoveto,navAgentMoveTo>>, <<navnearest,navNearest>>
.Ejemplo
[source,lua]
----
-- Esparce veinte monedas por el suelo, cada una un poco por encima de él.
for i = 1, 20 do
local x, y, z = navRandomPoint(nav)
local coin = nodeNew()
nodeSetMesh(coin, coinMesh, gold)
nodeSetPosition(coin, x, y + 0.3, z)
coins[#coins + 1] = coin
end
----
[#navraycast]
==== navRaycast
[source,text]
----
blocked, x, y, z = navRaycast(nav, x0, y0, z0, x1, y1, z1)
----
Si un camino en línea recta de un punto a otro se sale de la malla, en una pared o un desnivel, y dónde lo haría. El inicio se ajusta a la malla; `false` a secas significa que el camino está despejado, o que el inicio está lejos de la malla. Si está bloqueado, el punto es donde la línea cruza el borde de la malla, con su altura interpolada entre los dos extremos. Una comprobación barata de si existe un atajo antes de preguntar a `navPath`.
*Parámetros:*
* `nav` -- la malla.
* `x0`, `y0`, `z0` -- dónde empieza el camino.
* `x1`, `y1`, `z1` -- dónde terminaría.
*Devuelve:* `false`, o `true` y el punto donde el camino sale de la malla.
*Desde:* 3.00.
*Véase también:* <<navpath,navPath>>, <<physicsraycast,physicsRaycast>>
.Ejemplo
[source,lua]
----
-- Una granada lanzada solo rueda hasta donde se lo permite el suelo.
function landingSpot(fromX, fromY, fromZ, toX, toY, toZ)
local blocked, hx, hy, hz = navRaycast(nav, fromX, fromY, fromZ, toX, toY, toZ)
if blocked then
return hx, hy, hz
end
return toX, toY, toZ
end
----
[#navsave]
==== navSave
[source,text]
----
navSave(nav, name)
----
Escribe la malla precalculada en la carpeta de datos del juego con el nombre `name`, para `navLoad` la próxima vez, o para copiarla a la carpeta del juego y distribuirla. La malla tiene que estar construida. Termina el script cuando no lo está o cuando no se puede escribir el archivo.
*Desde:* 3.00.
*Véase también:* <<navload,navLoad>>, <<navbuild,navBuild>>
.Ejemplo
[source,lua]
----
-- Se precalcula una vez en esta máquina y después se carga de la carpeta de datos en las ejecuciones siguientes.
local nav = navNew(0.4, 1.6, 45, 0.3)
navAddNode(nav, level)
navBuild(nav)
navSave(nav, "level1.nav")
debugPrint("Navigation mesh baked and saved.")
----
[#net]
=== Red
Lo que el motor añade por debajo de `Singe/Net.singe`, el cliente HTTP del menú.
La mayoría de los juegos nunca lo tocan: publicar una puntuación u obtener un catálogo pasa
por `Net.singe`, que lo llama por ti.
[#nettrustsystem]
==== netTrustSystem
[source,text]
----
lists, report = netTrustSystem(context)
----
Añade los certificados raíz de confianza de la propia máquina a un contexto de cliente de LuaSec,
además de lo que ya tenga el contexto (normalmente el
`Singe/cacerts.pem` que se distribuye con Singe). Una conexión hecha con el contexto se verifica entonces cuando
cualquiera de las dos listas conoce la raíz del servidor. La lista de la máquina es, por orden: el archivo
o la carpeta que indique `SSL_CERT_FILE` o `SSL_CERT_DIR` cuando están definidas; si no, el
paquete de certificados de la distribución en Linux (el primero de los seis sitios habituales que
exista), el almacén raíz del sistema en Windows o los anclajes del sistema en macOS.
`cacerts.local.pem` en la carpeta de datos se añade también cuando existe; el
motor nunca lo escribe.
`lists` es cuántas listas han añadido algo. `report` nombra lo que se ha consultado,
una línea `what path` por cada cosa (`what` es `system` o `local`), para un mensaje de error
con el que una persona pueda hacer algo.
*Desde:* 3.00.
*Véase también:* <<utilsha256,utilSha256>>
.Ejemplo
[source,lua]
----
-- Un contexto para todas las peticiones: el paquete que se distribuye y, encima, las raíces de esta máquina.
local ssl = require("ssl")
local context = ssl.newcontext({ mode = "client", protocol = "any", verify = "peer", cafile = "Singe/cacerts.pem" })
local lists, report = netTrustSystem(context)
debugPrint("Trusting the shipped bundle and " .. lists .. " more:\n" .. report)
----
[#node]
=== Nodo
Todo objeto de una escena es un nodo de un único árbol: el nodo `0` es la raíz, y
todos los demás cuelgan de un padre. Un nodo lleva una posición, una
rotación y una escala relativas a ese padre, y puede dibujar una malla con un
material, mostrar un sprite o una línea de texto, o brillar como una luz; los hijos
heredan la transformación de todo lo que tienen por encima. Los handles son enteros
que devuelven `nodeNew`, `modelInstance` y `lightNew`; un handle cuyo nodo
se ha borrado aborta el script cuando se vuelve a pasar, y un handle liberado puede
reutilizarlo un `nodeNew` posterior. Las posiciones y los tamaños van en unidades de la escena y
las rotaciones en grados, con +Y hacia arriba y -Z hacia delante, como se describe en
<<scenes3d,Escenas 3D>>.
[#nodenew]
==== nodeNew
[source,text]
----
node = nodeNew()
node = nodeNew(parent)
----
Crea un nodo vacío en el origen de su padre, sin rotar, con escala `1`,
visible, proyectando sombras, sin malla ni material. Sin `parent` el
nodo va bajo la raíz; con él se convierte en el último hijo de ese nodo y
se mueve con él a partir de entonces. Los nodos vacíos sirven de pivotes y de grupos: coloca
uno, cuelga de él mallas, luces o una cámara y mueve el grupo
entero.
*Devuelve:* El handle del nodo.
*Desde:* 3.00.
*Véase también:* <<nodesetmesh,nodeSetMesh>>, <<nodesetparent,nodeSetParent>>, <<nodedelete,nodeDelete>>, <<lightnew,lightNew>>
.Ejemplo
[source,lua]
----
-- Una torreta cuyo cañón pivota sobre la torreta, que gira sobre la base.
local steel = materialNew()
materialSetColor(steel, 140, 145, 150)
local base = nodeNew()
local turret = nodeNew(base)
local barrel = nodeNew(turret)
nodeSetMesh(base, meshCylinder(1, 0.3), steel)
nodeSetMesh(turret, meshSphere(0.6), steel)
nodeSetMesh(barrel, meshCylinder(0.1, 1.2), steel)
nodeSetPosition(barrel, 0, 0, -0.8)
nodeSetRotation(barrel, 90, 0, 0)
----
[#nodedelete]
==== nodeDelete
[source,text]
----
nodeDelete(node)
----
Quita el nodo y todo lo que cuelga de él, liberando todos los handles de ese
subárbol; también se libera cualquier sprite o texto que tenga alguno de ellos. Las mallas y los
materiales que dibujaban los nodos no se tocan, ya que otros nodos pueden compartirlos.
Si el nodo borrado era la cámara, la escena vuelve a la vista
por defecto. El nodo raíz no se puede borrar; pedirlo aborta el script.
*Desde:* 3.00.
*Véase también:* <<nodenew,nodeNew>>, <<nodesetvisible,nodeSetVisible>>, <<meshdelete,meshDelete>>
.Ejemplo
[source,lua]
----
-- Los disparos viven dos segundos y después sus nodos desaparecen.
function fireShot(ship)
local shot = nodeNew()
nodeSetMesh(shot, shotMesh, tracer)
nodeSetPosition(shot, nodeGetWorldPosition(ship))
shots[#shots + 1] = { node = shot, born = os.clock() }
end
function reapShots()
for i = #shots, 1, -1 do
if os.clock() - shots[i].born > 2 then
nodeDelete(shots[i].node)
table.remove(shots, i)
end
end
end
----
[#nodefind]
==== nodeFind
[source,text]
----
node = nodeFind(name)
node = nodeFind(name, root)
----
Busca en profundidad el primer nodo llamado `name`, empezando por `root`
(la raíz de la escena si se omite) y comprobando el propio `root` antes que sus
hijos, y después el subárbol de cada hijo por turno. Devuelve `nil` cuando nada
coincide. Los nodos de un modelo llevan los nombres de su archivo glTF, así que un hueso,
una malla o un objeto vacío colocado en Blender como marca se pueden encontrar por ese nombre
bajo la raíz de la instancia. Los nombres puestos con `nodeSetName` se encuentran del mismo
modo.
*Devuelve:* Un handle de nodo, o `nil`.
*Desde:* 3.00.
*Véase también:* <<nodesetname,nodeSetName>>, <<nodegetname,nodeGetName>>, <<modelinstance,modelInstance>>, <<nodegetchildren,nodeGetChildren>>
.Ejemplo
[source,lua]
----
-- Cuelga una antorcha de la mano derecha del héroe, que se encuentra por el nombre de su hueso.
local hero = modelInstance(heroModel)
local hand = nodeFind("hand_R", hero)
if hand then
local torch = modelInstance(torchModel)
nodeSetParent(torch, hand)
nodeSetPosition(torch, 0, 0.05, 0)
nodeSetRotation(torch, 0, 0, -90)
end
----
[#nodegetchildren]
==== nodeGetChildren
[source,text]
----
children = nodeGetChildren(node)
----
Los hijos directos del nodo como una tabla de handles, en el orden en que se
engancharon; cambiar de padre un nodo lo pone al final de la lista de su nuevo padre.
Los nietos no se incluyen; usa recursión para recorrer un subárbol entero. Una
tabla vacía significa que el nodo no tiene hijos.
*Devuelve:* Una tabla de handles de nodo.
*Desde:* 3.00.
*Véase también:* <<nodegetparent,nodeGetParent>>, <<nodefind,nodeFind>>, <<nodesetparent,nodeSetParent>>
.Ejemplo
[source,lua]
----
-- Da a cada parte de un objeto cargado el mismo material desgastado.
local function paint(node, material)
nodeSetMaterial(node, material)
for _, child in ipairs(nodeGetChildren(node)) do
paint(child, material)
end
end
local crate = modelInstance(crateModel)
paint(crate, wornWood)
----
[#nodegetmorph]
==== nodeGetMorph
[source,text]
----
weight = nodeGetMorph(node, nameOrIndex)
----
El peso actual de un morph target de la malla del nodo, dado por su nombre o
por su número desde `1` en el orden en que los enumera `nodeGetMorphs`. El nodo es el
que lleva la malla, no la raíz de una instancia de modelo. Un nombre o un número que la
malla no tiene aborta el script.
*Devuelve:* Un número, `0` hasta que algo fija ese morph target.
*Desde:* 3.00.
*Véase también:* <<nodesetmorph,nodeSetMorph>>, <<nodegetmorphs,nodeGetMorphs>>
.Ejemplo
[source,lua]
----
-- Hace aparecer la sonrisa poco a poco, a lo largo de varios fotogramas, en vez de ponerla de golpe.
function onOverlayUpdate()
local now = nodeGetMorph(face, "smile")
local target = happy and 1 or 0
nodeSetMorph(face, "smile", now + (target - now) * 0.1)
return OVERLAY_UPDATED
end
----
[#nodegetmorphs]
==== nodeGetMorphs
[source,text]
----
names = nodeGetMorphs(node)
----
Los morph targets de la malla del nodo como una tabla de nombres, en el orden en que los
guarda el archivo; ese orden es la numeración que aceptan `nodeSetMorph` y
`nodeGetMorph`. Los exportadores que no escriben nombres dan cadenas vacías,
y esos targets se pueden fijar igualmente por número. Un nodo sin malla, o una
malla sin targets, da una tabla vacía.
*Devuelve:* Una tabla de cadenas.
*Desde:* 3.00.
*Véase también:* <<nodesetmorph,nodeSetMorph>>, <<nodegetmorph,nodeGetMorph>>, <<modelload,modelLoad>>
.Ejemplo
[source,lua]
----
-- Asocia una vez los nombres de los targets de la cara a números, para que la sincronización labial pueda indexarlos.
local head = nodeFind("Head", hero)
local visemes = {}
for index, name in ipairs(nodeGetMorphs(head)) do
if name ~= "" then
visemes[name] = index
end
end
debugPrint(string.format("%d morph targets on the head", #nodeGetMorphs(head)))
----
[#nodegetname]
==== nodeGetName
[source,text]
----
name = nodeGetName(node)
----
El nombre del nodo: el que le dio `nodeSetName` o, en un nodo de modelo, su nombre
glTF; una cadena vacía cuando no tiene ninguno de los dos. El nodo raíz se llama
`root`.
*Devuelve:* Una cadena.
*Desde:* 3.00.
*Véase también:* <<nodesetname,nodeSetName>>, <<nodefind,nodeFind>>
.Ejemplo
[source,lua]
----
-- Imprime en la consola el árbol de nodos de un modelo mientras averiguas los nombres de los huesos.
local function dump(node, depth)
debugPrint(string.rep(" ", depth) .. nodeGetName(node) .. " (" .. node .. ")")
for _, child in ipairs(nodeGetChildren(node)) do
dump(child, depth + 1)
end
end
dump(modelInstance(heroModel), 0)
----
[#nodegetparent]
==== nodeGetParent
[source,text]
----
parent = nodeGetParent(node)
----
El handle del padre del nodo, o `nil` para la raíz. Todos los demás nodos
tienen padre, aunque solo sea la raíz.
*Devuelve:* Un handle de nodo, o `nil`.
*Desde:* 3.00.
*Véase también:* <<nodesetparent,nodeSetParent>>, <<nodegetchildren,nodeGetChildren>>
.Ejemplo
[source,lua]
----
-- Sube desde un nodo de malla elegido hasta lo más alto de su instancia de modelo.
local function instanceRoot(node)
local parent = nodeGetParent(node)
while parent and parent ~= 0 do
node = parent
parent = nodeGetParent(node)
end
return node
end
----
[#nodegetposition]
==== nodeGetPosition
[source,text]
----
x, y, z = nodeGetPosition(node)
----
La posición del nodo relativa a su padre, tal como la fijó por última vez
`nodeSetPosition`, `nodeMove` o el motor (un cuerpo, un vehículo o una
animación que lo mueve). Para saber dónde está el nodo en el mundo, usa
`nodeGetWorldPosition`.
*Devuelve:* Tres números.
*Desde:* 3.00.
*Véase también:* <<nodesetposition,nodeSetPosition>>, <<nodegetworldposition,nodeGetWorldPosition>>, <<nodemove,nodeMove>>
.Ejemplo
[source,lua]
----
-- La cámara sigue al coche, tres unidades por encima, tres a la derecha y nueve por detrás.
function onOverlayUpdate()
local cx, cy, cz = nodeGetPosition(car)
nodeSetPosition(camera, cx + 3, cy + 3, cz + 9)
nodeLookAt(camera, cx, cy, cz - 3)
return OVERLAY_UPDATED
end
----
[#nodegetquaternion]
==== nodeGetQuaternion
[source,text]
----
x, y, z, w = nodeGetQuaternion(node)
----
La rotación del nodo relativa a su padre como un cuaternión unitario, la forma en que
la guardan los modelos y las animaciones. Úsala para copiar una rotación entre nodos o
para conservar una pose sin la ambigüedad de los ángulos de Euler.
*Devuelve:* Cuatro números.
*Desde:* 3.00.
*Véase también:* <<nodesetquaternion,nodeSetQuaternion>>, <<nodegetrotation,nodeGetRotation>>
.Ejemplo
[source,lua]
----
-- Recuerda la pose de la puerta antes de la prueba del ragdoll y después la restaura.
local px, py, pz = nodeGetPosition(door)
local qx, qy, qz, qw = nodeGetQuaternion(door)
function resetDoor()
nodeSetPosition(door, px, py, pz)
nodeSetQuaternion(door, qx, qy, qz, qw)
end
----
[#nodegetrotation]
==== nodeGetRotation
[source,text]
----
x, y, z = nodeGetRotation(node)
----
La rotación del nodo relativa a su padre como ángulos de Euler en grados,
descompuesta del mismo modo en que `nodeSetRotation` la compone: girada alrededor de Y,
después inclinada alrededor de X y después alabeada alrededor de Z. Los ángulos describen la misma
rotación que se fijó, pero no tienen por qué ser los mismos números: `X` vuelve
entre `-90` y `90`, y cuando llega a `90` o `-90` el giro se
asigna a `Y` y `Z` se devuelve como `0`.
*Devuelve:* Tres números.
*Desde:* 3.00.
*Véase también:* <<nodesetrotation,nodeSetRotation>>, <<nodegetquaternion,nodeGetQuaternion>>
.Ejemplo
[source,lua]
----
-- Una brújula en el overlay a partir del rumbo de la nave.
function onOverlayUpdate()
local _, heading = nodeGetRotation(ship)
overlayClear()
fontPrint(20, 20, string.format("Heading %03d", (360 - heading) % 360))
return OVERLAY_UPDATED
end
----
[#nodegetscale]
==== nodeGetScale
[source,text]
----
x, y, z = nodeGetScale(node)
----
La escala propia del nodo en cada eje, tal como la fijó `nodeSetScale` (`1, 1, 1` en un
nodo nuevo). Las escalas de los padres no se incluyen.
*Devuelve:* Tres números.
*Desde:* 3.00.
*Véase también:* <<nodesetscale,nodeSetScale>>
.Ejemplo
[source,lua]
----
-- Hace crecer el power-up hacia su tamaño completo, un poco en cada fotograma.
function onOverlayUpdate()
local size = nodeGetScale(powerUp)
if size < 1 then
nodeSetScale(powerUp, math.min(1, size + 0.05))
end
return OVERLAY_UPDATED
end
----
[#nodegetworldposition]
==== nodeGetWorldPosition
[source,text]
----
x, y, z = nodeGetWorldPosition(node)
----
Dónde está el origen del nodo en el espacio del mundo, con la transformación de cada padre
aplicada, según el último fotograma renderizado. Los cambios hechos en este fotograma (tuyos o
de un cuerpo) se ven después del siguiente renderizado, y un nodo creado en este fotograma devuelve
`0, 0, 0` hasta entonces. Esta es la posición que hay que pasar a `sceneProject` o a una
comprobación de distancia.
*Devuelve:* Tres números.
*Desde:* 3.00.
*Véase también:* <<sceneproject,sceneProject>>, <<nodelookat,nodeLookAt>>, <<nodegetposition,nodeGetPosition>>
.Ejemplo
[source,lua]
----
-- Una etiqueta con el nombre en el overlay sobre el pato, dondequiera que lo haya llevado la física.
function onOverlayUpdate()
local dx, dy, dz = nodeGetWorldPosition(duck)
local sx, sy = sceneProject(dx, dy + 0.8, dz)
overlayClear()
if sx then
fontPrint(sx - 20, sy, "DUCK")
end
return OVERLAY_UPDATED
end
----
[#nodelookat]
==== nodeLookAt
[source,text]
----
nodeLookAt(node, x, y, z)
----
Gira el nodo para que su propio -Z apunte a una posición del mundo, manteniendo su +Y
tan cerca de la vertical del mundo como lo permita la dirección. Es lo que quieren las cámaras y
las luces, y sirve también para una torreta o una cabeza que sigue un objetivo.
Solo cambia la rotación del nodo; su posición se queda como está. Si el objetivo
coincide con la posición del nodo, no cambia nada.
*Notas:* El punto del mundo se convierte al espacio del padre usando la
transformación del padre del último fotograma renderizado; un nodo cuyo padre se movió
en este mismo fotograma apunta desde donde estaba el padre.
*Desde:* 3.00.
*Véase también:* <<cameraset,cameraSet>>, <<lightnew,lightNew>>, <<nodesetrotation,nodeSetRotation>>
.Ejemplo
[source,lua]
----
local sun = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(sun, -8, 3, 6)
nodeLookAt(sun, 0, 0, 0)
lightSetShadow(sun, true)
local camera = nodeNew()
nodeSetPosition(camera, 0, 2.2, 7)
nodeLookAt(camera, 0, 1.3, 0)
cameraSet(camera)
----
[#nodemove]
==== nodeMove
[source,text]
----
nodeMove(node, dx, dy, dz)
----
Mueve el nodo a lo largo de sus propios ejes: el desplazamiento se gira según la rotación
actual del nodo y se suma a su posición, así que `nodeMove(ship, 0, 0, -0.1)`
hace avanzar la nave hacia donde apunte y `nodeMove(ship, 0.1, 0,
0)` la desplaza de lado hacia la derecha. Usa `nodeSetPosition` para un lugar absoluto en el
espacio del padre.
*Desde:* 3.00.
*Véase también:* <<nodesetposition,nodeSetPosition>>, <<noderotate,nodeRotate>>
.Ejemplo
[source,lua]
----
-- Controles de tanque: gira con izquierda y derecha, avanza con arriba y abajo.
function onOverlayUpdate()
if keyboardIsDown(SCANCODE_LEFT) then nodeRotate(tank, 0, 2, 0) end
if keyboardIsDown(SCANCODE_RIGHT) then nodeRotate(tank, 0, -2, 0) end
if keyboardIsDown(SCANCODE_UP) then nodeMove(tank, 0, 0, -0.08) end
if keyboardIsDown(SCANCODE_DOWN) then nodeMove(tank, 0, 0, 0.04) end
return OVERLAY_UPDATED
end
----
[#noderotate]
==== nodeRotate
[source,text]
----
nodeRotate(node, dx, dy, dz)
----
Gira el nodo los grados dados alrededor de sus propios ejes, sobre su
rotación actual: `nodeRotate(wheel, 5, 0, 0)` en cada fotograma hace girar la rueda
sobre su propio X, esté como esté inclinado su eje. Los tres ángulos se combinan del
modo en que los combina `nodeSetRotation` (Y, después X, después Z) antes de
aplicarse. Los ángulos positivos giran en sentido antihorario mirando a lo largo del eje
hacia el origen.
*Desde:* 3.00.
*Véase también:* <<nodesetrotation,nodeSetRotation>>, <<nodemove,nodeMove>>, <<nodesetquaternion,nodeSetQuaternion>>
.Ejemplo
[source,lua]
----
-- El logotipo gira medio grado por fotograma alrededor de su propio eje vertical.
local logo = modelInstance(logoModel)
nodeSetPosition(logo, 0, -2.5, 0)
nodeSetRotation(logo, 0, -30, 0)
function onOverlayUpdate()
nodeRotate(logo, 0, 0.5, 0)
return OVERLAY_UPDATED
end
----
[#nodesetbillboard]
==== nodeSetBillboard
[source,text]
----
nodeSetBillboard(node, mode)
----
Gira el nodo para que mire a la cámara en cada fotograma. `BILLBOARD_ALL` la mira
de frente, tomando los tres ejes de la cámara; `BILLBOARD_Y` gira solo alrededor de
la vertical del mundo, así que el nodo se mantiene derecho (árboles, barras de vida);
`BILLBOARD_NONE`, el valor por defecto, no gira nada. La posición en el mundo
y la escala del nodo se conservan, y su propia rotación se ignora mientras haya un modo
activo. Cualquier otro valor aborta el script. Funciona con mallas además de con
sprites y texto, y se orienta hacia la cámara que esté dibujando: una vista también ve
el billboard de frente, mientras que su sombra sigue a la cámara de la ventana.
*Desde:* 3.00.
*Véase también:* <<nodesetsprite,nodeSetSprite>>, <<nodesettext,nodeSetText>>, <<cameraset,cameraSet>>
.Ejemplo
[source,lua]
----
-- Un nombre sobre el zorro que mira de frente a la cámara, y un icono de caja que
-- solo gira alrededor de su propio eje vertical.
local tag = nodeNew()
nodeSetParent(tag, fox)
nodeSetPosition(tag, 0, 110, 0)
nodeSetScale(tag, 50)
nodeSetText(tag, "FOX", 0.6)
nodeSetBillboard(tag, BILLBOARD_ALL)
nodeSetShadow(tag, false)
local icon = nodeNew()
nodeSetParent(icon, fox)
nodeSetPosition(icon, 0, 150, 0)
nodeSetScale(icon, 50)
nodeSetSprite(icon, spriteLoad("crate.png"), 0.5)
nodeSetBillboard(icon, BILLBOARD_Y)
----
[#nodesetlayer]
==== nodeSetLayer
[source,text]
----
nodeSetLayer(node, layer)
----
Elige en qué parte de la ventana se dibuja el nodo. `LAYER_SCENE`, el valor por defecto,
lo dibuja en la escena, por debajo del overlay; `LAYER_FRONT` lo dibuja después del
overlay, con la misma cámara y las mismas luces, en una capa transparente
propia. Un nodo en la capa frontal se lleva consigo a sus hijos, tengan el
ajuste que tengan. Cualquier otro valor aborta el script. Véase
<<scenefront,Delante del overlay>>.
*Desde:* 3.00.
*Véase también:* <<sceneenable,sceneEnable>>, <<nodesetvisible,nodeSetVisible>>, <<emittersetlayer,emitterSetLayer>>
.Ejemplo
[source,lua]
----
-- La nave del jugador se queda sobre la pantalla 2D del mapa mientras está abierta, y
-- vuelve al mundo cuando se cierra.
function showMap(open)
nodeSetLayer(ship, open and LAYER_FRONT or LAYER_SCENE)
end
----
[#nodesetmaterial]
==== nodeSetMaterial
[source,text]
----
nodeSetMaterial(node, material)
nodeSetMaterial(node, nil)
----
Pone un material en la malla que tenga el nodo, conservando la malla. `nil`
lo quita y deja el aspecto por defecto: blanco, medio rugoso, sin textura. Úsalo
para cambiar el estilo de una parte de un modelo, para poner y quitar un material de resaltado o
para vestir una malla que el motor ha hecho por ti, como el tubo de una cuerda de
`softNew`. Un handle de material que no existe aborta el script.
*Desde:* 3.00.
*Véase también:* <<nodesetmesh,nodeSetMesh>>, <<materialnew,materialNew>>, <<softnew,softNew>>
.Ejemplo
[source,lua]
----
-- La cuerda cuelga de la grúa; el motor hace su tubo y nosotros lo vestimos.
local hemp = materialNew()
materialSetColor(hemp, 170, 140, 90)
materialSetRoughness(hemp, 1)
local rope = nodeNew()
nodeSetPosition(rope, 4, 4, -1)
softNew(rope, SOFT_ROPE, 6.5, 4, -1, 16, 0.04)
nodeSetMaterial(rope, hemp)
softSetMass(rope, 1.5)
softPin(rope, 4, 4, -1, crane)
----
[#nodesetmesh]
==== nodeSetMesh
[source,text]
----
nodeSetMesh(node, mesh)
nodeSetMesh(node, mesh, material)
----
Da al nodo una malla que dibujar, con un material o, si se omite, con el
aspecto por defecto (blanco, medio rugoso). Los dos argumentos tienen que ser handles vivos; una
malla o un material que no existe aborta el script. Cualquier nodo puede dibujar, incluido un
nodo dentro de una instancia de modelo, y cualquier número de nodos puede compartir una malla.
Los pesos de morph del nodo se dimensionan según los targets de la nueva malla y empiezan en
`0`.
*Desde:* 3.00.
*Véase también:* <<meshbox,meshBox>>, <<materialnew,materialNew>>, <<nodesetmaterial,nodeSetMaterial>>, <<nodesetvisible,nodeSetVisible>>
.Ejemplo
[source,lua]
----
-- Una carrocería con cuatro ruedas, cada una un cilindro girado para que su X sea el eje.
local car = nodeNew()
nodeSetMesh(car, meshBox(1.8, 0.5, 4.0), paint)
nodeSetPosition(car, 0, 0.9, 0)
local tireMesh = meshCylinder(0.35, 0.25, 16)
for _, p in ipairs({ { -0.95, -0.2, -1.4 }, { 0.95, -0.2, -1.4 }, { -0.95, -0.2, 1.4 }, { 0.95, -0.2, 1.4 } }) do
local wheel = nodeNew(car)
nodeSetPosition(wheel, p[1], p[2], p[3])
local tire = nodeNew(wheel)
nodeSetMesh(tire, tireMesh, rubber)
nodeSetRotation(tire, 0, 0, 90)
end
----
[#nodesetmorph]
==== nodeSetMorph
[source,text]
----
nodeSetMorph(node, nameOrIndex, weight)
----
Fija cuánto muestra la malla del nodo de un morph target (blend shape),
por nombre o por número desde `1`; los pesos suelen ir de `0` a `1` y varios
targets se suman. El nodo es el que lleva la malla, que en un modelo es
un hijo de la raíz de la instancia (`nodeFind` por el nombre de la malla, o
`nodeGetChildren`). Un target que la malla no tiene aborta el script.
Una animación con un canal de pesos escribe los pesos en cada fotograma mientras
se reproduce, por encima de lo que hayas fijado.
*Notas:* Se aplican hasta ocho targets con peso distinto de cero por malla en un
fotograma; si hay más, se usan los ocho más fuertes por peso absoluto.
*Desde:* 3.00.
*Véase también:* <<nodegetmorph,nodeGetMorph>>, <<nodegetmorphs,nodeGetMorphs>>, <<animationplay,animationPlay>>
.Ejemplo
[source,lua]
----
-- Sincronización labial a partir de una tabla de aperturas de la boca por fotograma del disco.
local face = nodeFind("Head", hero)
function onOverlayUpdate()
nodeSetMorph(face, "mouthOpen", mouthByFrame[discGetFrame()] or 0)
return OVERLAY_UPDATED
end
----
[#nodesetname]
==== nodeSetName
[source,text]
----
nodeSetName(node, name)
----
Pone nombre al nodo para que `nodeFind` pueda encontrarlo después, sustituyendo el nombre que tuviera
(incluido el nombre glTF de un nodo de modelo). Los nombres no tienen por qué ser únicos; `nodeFind`
devuelve la primera coincidencia en el orden de la búsqueda en profundidad.
*Desde:* 3.00.
*Véase también:* <<nodefind,nodeFind>>, <<nodegetname,nodeGetName>>
.Ejemplo
[source,lua]
----
-- Las cajas creadas llevan nombre para que el script del nivel pueda encontrarlas por su número.
for slot = 1, 6 do
local crate = nodeNew()
nodeSetMesh(crate, crateMesh, wood)
nodeSetPosition(crate, slot * 1.5 - 5, 0.5, -3)
nodeSetName(crate, "crate" .. slot)
end
nodeSetVisible(nodeFind("crate4"), false)
----
[#nodesetparent]
==== nodeSetParent
[source,text]
----
nodeSetParent(node, parent)
----
Mueve el nodo, con todo su subárbol, bajo otro padre, del que pasa a ser
el último hijo. La posición, la rotación y la escala locales del nodo
se conservan tal cual, así que ahora queda a ese desplazamiento del nuevo padre y
se mueve con él; pasa `0` para devolverlo a la raíz. La raíz no se puede mover,
y un nodo no puede ir bajo sí mismo ni bajo uno de sus propios descendientes; cualquiera de las dos cosas
aborta el script.
*Desde:* 3.00.
*Véase también:* <<nodegetparent,nodeGetParent>>, <<nodenew,nodeNew>>, <<nodesetposition,nodeSetPosition>>
.Ejemplo
[source,lua]
----
-- Recoger una caja: a partir de ahora va en la mano, hasta que se deja en otro sitio.
function pickUp(crate)
nodeSetParent(crate, nodeFind("hand_R", hero))
nodeSetPosition(crate, 0, 0.3, 0)
nodeSetRotation(crate, 0, 0, 0)
end
function putDown(crate, x, y, z)
nodeSetParent(crate, 0)
nodeSetPosition(crate, x, y, z)
end
----
[#nodesetposition]
==== nodeSetPosition
[source,text]
----
nodeSetPosition(node, x, y, z)
----
Coloca el nodo en `x, y, z` en el espacio de su padre; para un nodo bajo la
raíz, ese es el espacio del mundo. Los hijos lo acompañan. A un nodo con un cuerpo dinámico
lo mueve después la física; fija su posición antes de
`bodyNew`, o teletranspórtalo con las llamadas de cuerpos.
*Desde:* 3.00.
*Véase también:* <<nodemove,nodeMove>>, <<nodegetposition,nodeGetPosition>>, <<nodesetrotation,nodeSetRotation>>
.Ejemplo
[source,lua]
----
-- Cajas soltadas a lo largo de una cresta, cada una seis unidades por encima del suelo en ese punto.
for i = 1, 10 do
local crate = nodeNew()
local x, z = -20 + i * 4, -12
nodeSetMesh(crate, meshBox(1, 1, 1), wood)
nodeSetPosition(crate, x, (terrainGetHeight(hills, x, z) or 0) + 6, z)
nodeSetRotation(crate, i * 17, i * 31, 0)
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 1, 1, 1)
end
----
[#nodesetquaternion]
==== nodeSetQuaternion
[source,text]
----
nodeSetQuaternion(node, x, y, z, w)
----
Fija la rotación del nodo relativa a su padre como un cuaternión, para
valores que vienen de `nodeGetQuaternion`, de un modelo o de tus propios cálculos. El
cuaternión se normaliza al entrar, así que no hace falta que tenga longitud unitaria.
*Desde:* 3.00.
*Véase también:* <<nodesetrotation,nodeSetRotation>>, <<nodegetquaternion,nodeGetQuaternion>>
.Ejemplo
[source,lua]
----
-- El fantasma copia exactamente la pose del héroe, con un poco de retraso.
local qx, qy, qz, qw = nodeGetQuaternion(hero)
local px, py, pz = nodeGetPosition(hero)
history[#history + 1] = { px, py, pz, qx, qy, qz, qw }
if #history > 30 then
local old = table.remove(history, 1)
nodeSetPosition(ghost, old[1], old[2], old[3])
nodeSetQuaternion(ghost, old[4], old[5], old[6], old[7])
end
----
[#nodesetrotation]
==== nodeSetRotation
[source,text]
----
nodeSetRotation(node, x, y, z)
----
Fija la rotación del nodo relativa a su padre como ángulos de Euler en
grados, sustituyendo la que tuviera: el nodo se gira primero alrededor de su Y (guiñada),
después se inclina alrededor de su X (cabeceo) y después se alabea alrededor de su Z. Los ángulos
positivos giran en sentido antihorario mirando a lo largo del eje hacia el origen, así que
`nodeSetRotation(plane, 90, 0, 0)` levanta un plano de suelo para que mire hacia +Z.
*Desde:* 3.00.
*Véase también:* <<noderotate,nodeRotate>>, <<nodesetquaternion,nodeSetQuaternion>>, <<nodelookat,nodeLookAt>>
.Ejemplo
[source,lua]
----
-- Una pantalla de monitor sobre un soporte, mirando a la cámara en +Z.
local monitor = nodeNew()
nodeSetMesh(monitor, meshPlane(2.0, 1.5), screen)
nodeSetPosition(monitor, -3, 2.1, 1.06)
nodeSetRotation(monitor, 90, 0, 0)
-- Una rampa inclinada 25 grados alrededor de Z.
local ramp = nodeNew()
nodeSetMesh(ramp, meshBox(6, 0.2, 3), asphalt)
nodeSetRotation(ramp, 0, 0, 25)
----
[#nodesetscale]
==== nodeSetScale
[source,text]
----
nodeSetScale(node, scale)
nodeSetScale(node, x, y, z)
----
Escala el nodo y todo lo que cuelga de él, uniformemente con un factor o por
eje con tres. Los modelos cargados suelen estar en centímetros o en metros; escala
una vez la raíz de la instancia para que encaje en tu escena y coloca las cosas bajo ella en las
unidades propias del modelo. Los sprites y el texto del nodo también se escalan, además de
su `height`.
*Desde:* 3.00.
*Véase también:* <<nodegetscale,nodeGetScale>>, <<modelinstance,modelInstance>>
.Ejemplo
[source,lua]
----
-- El zorro está modelado en centímetros: una centésima lo pasa a metros.
local fox = modelInstance(foxModel)
nodeSetParent(fox, player)
nodeSetScale(fox, 0.01)
nodeSetRotation(fox, 0, 90, 0)
-- Una esfera aplastada para un charco.
local puddle = nodeNew()
nodeSetMesh(puddle, meshSphere(0.5, 24), water)
nodeSetScale(puddle, 1.5, 0.05, 1.2)
----
[#nodesetshadow]
==== nodeSetShadow
[source,text]
----
nodeSetShadow(node, casts)
----
Si la malla del nodo se dibuja en los mapas de sombras; `true` en todo nodo
nuevo. Desactívalo para la malla que rodea una luz (una bombilla, una llama), para que no
haga sombra a su propia luz, y para todo lo que brilla. El nodo sigue
dibujándose y sigue recibiendo las sombras de los demás. Se aplica solo al nodo,
no a sus hijos.
*Desde:* 3.00.
*Véase también:* <<lightsetshadow,lightSetShadow>>, <<nodesetvisible,nodeSetVisible>>, <<materialsetunlit,materialSetUnlit>>
.Ejemplo
[source,lua]
----
-- Una bombilla dentro de su pantalla: el cristal no proyecta sombra, la pantalla sí.
local shade = nodeNew(stand)
nodeSetMesh(shade, meshCone(0.45, 0.5, 24), brass)
nodeSetPosition(shade, 0, 0.9, 0)
local bulbMesh = nodeNew(stand)
nodeSetMesh(bulbMesh, meshSphere(0.08, 12), glow)
nodeSetPosition(bulbMesh, 0, 0.8, 0)
nodeSetShadow(bulbMesh, false)
local bulb = lightNew(LIGHT_POINT)
nodeSetParent(bulb, bulbMesh)
lightSetShadow(bulb, true)
----
[#nodesetsprite]
==== nodeSetSprite
[source,text]
----
nodeSetSprite(node, sprite)
nodeSetSprite(node, sprite, height)
nodeSetSprite(node, sprite, height, lit)
nodeSetSprite(node, nil)
nodeSetSprite(node)
----
Muestra un sprite cargado en el nodo: un quad de `height` unidades del mundo de alto
(por defecto `1`) y tan ancho como lo haga la proporción de la imagen, mezclado según el
alfa de la imagen, dibujado por las dos caras e iluminado por la escena salvo que `lit`
sea `false`. Se suben todos los fotogramas de un sprite animado y se muestra
el primero; `nodeSetSpriteFrame` elige otro. El quad se convierte en la
malla del nodo con un material privado, sustituyendo cualquier malla, sprite o texto que tuviera;
`nil` o la falta de segundo argumento quita la imagen y deja el nodo
vacío. Aborta el script cuando el renderizador 3D no está disponible.
*Parámetros:*
* `sprite` -- un handle de `spriteLoad`, o `nil` para quitarlo.
* `height` -- la altura del quad en unidades del mundo, por defecto `1`; la escala del nodo la multiplica.
* `lit` -- `true` (por defecto) sombrea la imagen con las luces y las sombras de la escena; `false` la muestra tal cual.
*Desde:* 3.00.
*Véase también:* <<nodesetspriteframe,nodeSetSpriteFrame>>, <<nodesetbillboard,nodeSetBillboard>>, <<spriteload,spriteLoad>>, <<nodesettext,nodeSetText>>
.Ejemplo
[source,lua]
----
-- Un icono de caja flotando sobre el zorro, que se gira para mirar al jugador.
local icon = nodeNew()
nodeSetParent(icon, fox)
nodeSetPosition(icon, 0, 150, 0)
nodeSetScale(icon, 50)
nodeSetSprite(icon, spriteLoad("crate.png"), 0.5)
nodeSetBillboard(icon, BILLBOARD_Y)
-- Un resplandor sin iluminación que se ve igual en la sombra.
local flare = nodeNew(torchTip)
nodeSetSprite(flare, spriteLoad("flare.png"), 0.3, false)
nodeSetBillboard(flare, BILLBOARD_ALL)
----
[#nodesetspriteframe]
==== nodeSetSpriteFrame
[source,text]
----
nodeSetSpriteFrame(node, frame)
----
Qué fotograma del sprite animado del nodo se muestra, contando desde `0`. El
sprite del nodo no avanza solo; manéjalo desde
`onOverlayUpdate`. Un nodo sin sprite, o un fotograma que no tiene,
aborta el script; el texto y un sprite fijo solo tienen el fotograma `0`.
*Desde:* 3.00.
*Véase también:* <<nodesetsprite,nodeSetSprite>>, <<spriteload,spriteLoad>>
.Ejemplo
[source,lua]
----
-- Un sprite de hoguera que recorre sus fotogramas a diez por segundo.
local fire = nodeNew()
nodeSetPosition(fire, 2, 0.4, -1)
nodeSetSprite(fire, spriteLoad("campfire.gif"), 0.8, false)
nodeSetBillboard(fire, BILLBOARD_Y)
function onOverlayUpdate()
nodeSetSpriteFrame(fire, math.floor(os.clock() * 10) % fireFrames)
return OVERLAY_UPDATED
end
----
[#nodesettext]
==== nodeSetText
[source,text]
----
nodeSetText(node, text)
nodeSetText(node, text, height)
nodeSetText(node, nil)
nodeSetText(node)
----
Muestra una línea de texto en el nodo, renderizada en la fuente seleccionada
(`fontSelect`) con la calidad actual (`fontQuality`) en el color
de primer plano (`colorForeground`), sobre un quad de `height` unidades del mundo de alto (por defecto
`1`) y tan ancho como lo haga el texto renderizado. No recibe iluminación, se mezcla y se
dibuja por las dos caras: etiquetas con nombres, carteles, una puntuación sobre una mesa. Como un
sprite, se convierte en la malla del nodo, sustituyendo cualquier malla, sprite o texto
que hubiera; llámala otra vez para cambiar el texto, y con `nil` o sin segundo argumento para
quitarlo. Sin ninguna fuente seleccionada, o sin el renderizador 3D, aborta el
script.
*Desde:* 3.00.
*Véase también:* <<nodesetsprite,nodeSetSprite>>, <<nodesetbillboard,nodeSetBillboard>>, <<fontselect,fontSelect>>, <<colorforeground,colorForeground>>
.Ejemplo
[source,lua]
----
-- Un cartel fijo junto a la lámpara, y una puntuación que se actualiza cuando cambia.
fontSelect(signFont)
colorForeground(255, 240, 200)
local sign = nodeNew()
nodeSetPosition(sign, 4.2, 1.2, -1.2)
nodeSetRotation(sign, 0, -30, 0)
nodeSetText(sign, "LAMP", 0.5)
function setScore(points)
nodeSetText(scoreboard, string.format("%06d", points), 0.4)
end
----
[#nodesetvisible]
==== nodeSetVisible
[source,text]
----
nodeSetVisible(node, visible)
----
Oculta o muestra el nodo y todo lo que cuelga de él; el ajuste propio de un hijo sigue
valiendo cuando su padre se vuelve a mostrar. Las mallas ocultas no se dibujan y
las luces ocultas dejan de alumbrar. Úsalo para tener lista una marca o un objeto de repuesto
sin borrarlo y reconstruirlo.
*Desde:* 3.00.
*Véase también:* <<nodesetmesh,nodeSetMesh>>, <<nodedelete,nodeDelete>>
.Ejemplo
[source,lua]
----
-- Una marca que aparece donde la bola golpeó la pala por última vez.
local mark = nodeNew()
nodeSetMesh(mark, meshSphere(0.08, 12), markLook)
nodeSetVisible(mark, false)
function onCollision(a, b, x, y, z)
if (a == ball and b == paddle) or (a == paddle and b == ball) then
nodeSetPosition(mark, x, y, z)
nodeSetVisible(mark, true)
end
end
----
[#overlay]
=== Overlay
El overlay es una superficie RGBA de 32 bits que se compone sobre el vídeo en cada fotograma; todo el dibujo 2D va a parar a ella. Sus coordenadas son la resolución del overlay fijada con `overlaySetResolution`, que por defecto es la mitad del ancho y del alto del vídeo, y se escalan a la ventana al mostrarse. Dibuja desde `onOverlayUpdate` (o desde `singeMain` en el modelo <<threaded,Con hilo>>) y devuelve `OVERLAY_UPDATED` cuando algo haya cambiado. Las primitivas `overlay*` dibujan contornos de un píxel en el color de primer plano actual y recortan sin avisar todo lo que cae fuera de la superficie. `overlayPrint` es la excepción: toma coordenadas de celdas de carácter y copia tal cual la fuente de consola incorporada, sin hacer caso de los ajustes de color.
[#overlaybanner]
==== overlayBanner
[source,text]
----
overlayBanner(text [, height])
----
Extensión de Hypseus. Muestra un mensaje corto sobre la imagen durante unos treinta fotogramas dibujados y después lo vuelve a quitar. `height` es dónde se coloca, como porcentaje de la altura de la imagen contado desde arriba, de 1 a 95; el valor por defecto es 47, el mismo que usa Hypseus, y un número fuera de ese intervalo se sustituye por el valor por defecto en vez de rechazarse.
Un mensaje de más de 60 caracteres se ignora, exactamente igual que lo ignora Hypseus. El rótulo lo dibuja el mismo documento incluido que usan los subtítulos (`Singe/subtitle.rml`, con el estilo de `Singe/subtitle.rcss`), así que parece un subtítulo y comparte su estilo; mientras está a la vista ocupa el lugar de cualquier subtítulo, y el subtítulo vuelve cuando el rótulo caduca. El texto se dibuja como texto, así que `<`, `>` y `&` llegan a la pantalla tal cual.
*Parámetros:*
* `text` -- cadena; hasta 60 caracteres.
* `height` -- número opcional; de 1 a 95, un porcentaje de la altura de la imagen desde arriba. Por defecto 47.
*Devuelve:* nada.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<srtload,srtLoad>>, <<subtitles,Subtítulos>>, <<overlayprint,overlayPrint>>
.Ejemplo
[source,lua]
----
-- Cuenta al jugador lo que acaba de pasar sin montar un HUD para ello.
overlayBanner("Extra life!", 20)
----
[#overlaybox]
==== overlayBox
[source,text]
----
overlayBox(x1, y1, x2, y2)
----
Dibuja el contorno del rectángulo alineado con los ejes que tiene esquinas opuestas en `(x1, y1)` y `(x2, y2)`, como cuatro líneas de un píxel en el color de primer plano. Las dos esquinas se incluyen y se pueden dar en cualquier orden. No hay variante rellena; rellena el overlay entero con `overlayClear` sobre un color de fondo, o una zona con un sprite estirado.
*Desde:* 2.00
*Véase también:* <<overlayline,overlayLine>>, <<colorforeground,colorForeground>>, <<overlayclear,overlayClear>>
.Ejemplo
[source,lua]
----
-- Enmarca el overlay en rojo y perfila cada bloque de plataforma.
function onOverlayUpdate()
overlayClear()
colorForeground(255, 0, 0, 255)
overlayBox(0, 0, overlayGetWidth() - 1, overlayGetHeight() - 1)
colorForeground(90, 90, 110, 255)
for _, b in ipairs(blocks) do
overlayBox(b.x - b.w / 2, b.y - b.h / 2, b.x + b.w / 2, b.y + b.h / 2)
end
return OVERLAY_UPDATED
end
----
[#overlaycircle]
==== overlayCircle
[source,text]
----
overlayCircle(x, y, radius)
----
Dibuja el contorno de un círculo de un píxel en el color de primer plano, centrado en `(x, y)`, con el algoritmo del punto medio. Un radio de `0` o menos no dibuja nada. Las coordenadas y el radio son enteros en unidades del overlay; las fracciones se truncan.
*Parámetros:*
* `x`, `y` -- el centro en coordenadas del overlay.
* `radius` -- el radio en unidades del overlay.
*Desde:* 2.00
*Véase también:* <<overlayellipse,overlayEllipse>>, <<overlayplot,overlayPlot>>, <<colorforeground,colorForeground>>
.Ejemplo
[source,lua]
----
-- Un punto de mira que se vuelve rojo mientras al jugador le están dando.
function onOverlayUpdate()
if hitTimer > 0 then
colorForeground(255, 0, 0, 255)
hitTimer = hitTimer - 1
else
colorForeground(255, 255, 255, 255)
end
overlayCircle(aimX, aimY, 8)
overlayLine(aimX - 12, aimY, aimX + 12, aimY)
overlayLine(aimX, aimY - 12, aimX, aimY + 12)
return OVERLAY_UPDATED
end
----
[#overlayclear]
==== overlayClear
[source,text]
----
overlayClear()
----
Rellena todo el overlay con el color de fondo actual. Cuando el color es el mismo que el del último borrado, solo se vuelve a rellenar lo que se ha dibujado desde entonces, así que borrar un overlay que contiene unas pocas líneas de texto no cuesta casi nada, sea cual sea su tamaño. El fondo empieza siendo negro transparente, así que sin ninguna llamada a `colorBackground` esto vuelve a dejar el overlay invisible y el vídeo se ve a través. Un fondo parcialmente opaco oscurece el vídeo; uno totalmente opaco lo oculta. La mayoría de los juegos la llaman lo primero en `onOverlayUpdate`. Los argumentos se ignoran.
*Desde:* 1.x
*Véase también:* <<colorbackground,colorBackground>>, <<onoverlayupdate,onOverlayUpdate>>
.Ejemplo
[source,lua]
----
-- Oscurece el vídeo en pausa a media intensidad detrás del menú de servicio.
function onOverlayUpdate()
colorBackground(0, 0, 0, 127)
overlayClear()
spriteDraw(serviceTitle, (overlayGetWidth() - spriteGetWidth(serviceTitle)) / 2, 25)
return OVERLAY_UPDATED
end
----
[#overlayellipse]
==== overlayEllipse
[source,text]
----
overlayEllipse(x1, y1, x2, y2)
----
Dibuja el contorno de una elipse de un píxel en el color de primer plano, inscrita en el rectángulo alineado con los ejes que tiene esquinas opuestas en `(x1, y1)` y `(x2, y2)`. Las esquinas se pueden dar en cualquier orden. Un rectángulo de igual ancho y alto da un círculo, así que esta es también la forma de dibujar un círculo a partir del rectángulo que lo contiene en vez de a partir de su centro.
*Desde:* 2.00
*Véase también:* <<overlaycircle,overlayCircle>>, <<overlaybox,overlayBox>>
.Ejemplo
[source,lua]
----
-- Resalta la recreativa seleccionada con un óvalo que se ajusta a su panel.
function onOverlayUpdate()
spriteDraw(cabinetSprite, CABINET_X, CABINET_Y)
if selected == CABINET then
colorForeground(255, 220, 0, 255)
overlayEllipse(CABINET_X - 6, CABINET_Y - 6, CABINET_X + CABINET_W + 6, CABINET_Y + CABINET_H + 6)
end
return OVERLAY_UPDATED
end
----
[#overlaygetfontheight]
==== overlayGetFontHeight
[source,text]
----
result = overlayGetFontHeight()
----
Devuelve la altura en píxeles de una celda de carácter de la fuente de consola incorporada, que es la unidad con la que cuenta `overlayPrint`. La fuente de consola es una única imagen incrustada de 256 glifos que se carga una vez antes de que se ejecute ningún script, así que este valor no cambia nunca mientras el programa está en marcha. Los argumentos se ignoran.
*Devuelve:* altura entera de una celda, en píxeles.
*Desde:* 3.00
*Véase también:* <<overlaygetfontwidth,overlayGetFontWidth>>, <<overlayprint,overlayPrint>>, <<overlaygetheight,overlayGetHeight>>
.Ejemplo
[source,lua]
----
-- Cuántas filas de texto de consola caben, sea cual sea el tamaño del overlay.
ROWS = overlayGetHeight() // overlayGetFontHeight()
overlayPrint(0, ROWS - 1, "along the bottom")
----
[#overlaygetfontwidth]
==== overlayGetFontWidth
[source,text]
----
result = overlayGetFontWidth()
----
Devuelve el ancho en píxeles de una celda de carácter de la fuente de consola incorporada, que es la unidad con la que cuenta `overlayPrint`. Úsala con `overlayGetWidth` para calcular cuántas columnas caben, o para colocar texto de consola junto a algo dibujado en píxeles. Divide con `//` en vez de con `/`: la división normal de Lua da un número con decimales, y una posición de celda tiene que ser entera. Los argumentos se ignoran.
*Devuelve:* ancho entero de una celda, en píxeles.
*Desde:* 3.00
*Véase también:* <<overlaygetfontheight,overlayGetFontHeight>>, <<overlayprint,overlayPrint>>, <<overlaygetwidth,overlayGetWidth>>
.Ejemplo
[source,lua]
----
-- Centra una línea de texto de consola sin escribir a fuego el número de columnas.
local columns = overlayGetWidth() // overlayGetFontWidth()
local text = "GAME OVER"
overlayPrint((columns - #text) // 2, 10, text)
----
[#overlaygetheight]
==== overlayGetHeight
[source,text]
----
result = overlayGetHeight()
----
Devuelve la altura de la superficie del overlay en unidades del overlay: el último valor dado a `overlaySetResolution`, o la mitad de la altura del vídeo antes de cualquier llamada. Léela al arrancar, después de fijar la resolución, para situar las posiciones en función del área de dibujo real. Los argumentos se ignoran.
*Devuelve:* altura, entera.
*Desde:* 1.x
*Véase también:* <<overlaygetwidth,overlayGetWidth>>, <<overlaysetresolution,overlaySetResolution>>
.Ejemplo
[source,lua]
----
-- Ancla el panel de texto al margen inferior, sea cual sea el tamaño del overlay.
overlaySetResolution(discGetWidth(), discGetHeight())
TEXT_Y = 200
TEXT_H = overlayGetHeight() - MARGIN_Y - TEXT_Y
TEXT_LINE_LIMIT = math.floor(TEXT_H / LINE_HEIGHT)
----
[#overlaygetwidth]
==== overlayGetWidth
[source,text]
----
result = overlayGetWidth()
----
Devuelve el ancho de la superficie del overlay en unidades del overlay: el último valor dado a `overlaySetResolution`, o la mitad del ancho del vídeo antes de cualquier llamada. Úsala para alinear a la derecha o centrar elementos en vez de escribir un ancho a fuego. Los argumentos se ignoran.
*Devuelve:* ancho, entero.
*Desde:* 1.x
*Véase también:* <<overlaygetheight,overlayGetHeight>>, <<overlaysetresolution,overlaySetResolution>>
.Ejemplo
[source,lua]
----
-- Alinea la marquesina a la derecha y centra el título, sea cual sea la resolución.
function onOverlayUpdate()
local marqueeX = overlayGetWidth() - MARGIN_X - spriteGetWidth(marqueeSprite)
spriteDraw(marqueeSprite, marqueeX, MARGIN_Y)
spriteDraw(titleSprite, (overlayGetWidth() - spriteGetWidth(titleSprite)) / 2, 25)
return OVERLAY_UPDATED
end
----
[#overlayline]
==== overlayLine
[source,text]
----
overlayLine(x1, y1, x2, y2)
----
Dibuja una línea de Bresenham de un píxel en el color de primer plano de `(x1, y1)` a `(x2, y2)`, con los dos extremos incluidos. Los píxeles fuera del overlay se descartan, así que los extremos pueden quedar fuera de la superficie. Las coordenadas se truncan a enteros.
*Desde:* 2.00
*Véase también:* <<overlayplot,overlayPlot>>, <<overlaybox,overlayBox>>, <<colorforeground,colorForeground>>
.Ejemplo
[source,lua]
----
-- Dibuja el láser desde el arma hasta donde apunta, y una retícula sencilla.
function onOverlayUpdate()
overlayClear()
if firing then
colorForeground(255, 64, 64, 255)
overlayLine(GUN_X, GUN_Y, aimX, aimY)
end
colorForeground(255, 255, 255, 255)
overlayCircle(aimX, aimY, 6)
return OVERLAY_UPDATED
end
----
[#overlayplot]
==== overlayPlot
[source,text]
----
overlayPlot(x, y)
----
Pone un píxel del overlay en el color de primer plano. Un punto fuera de la superficie se ignora. Cada llamada bloquea y desbloquea el overlay, así que dibujar miles de puntos por fotograma desde Lua es lento; usa un sprite para todo lo que tenga estructura y deja esto para estrellas, chispas y marcas de depuración.
*Desde:* 2.00
*Véase también:* <<overlayline,overlayLine>>, <<colorforeground,colorForeground>>
.Ejemplo
[source,lua]
----
-- Un campo de estrellas que titilan despacio detrás del título.
function onOverlayUpdate()
overlayClear()
for _, star in ipairs(stars) do
local shade = 128 + math.floor(127 * math.sin(frameCount * star.speed))
colorForeground(shade, shade, shade, 255)
overlayPlot(star.x, star.y)
end
frameCount = frameCount + 1
return OVERLAY_UPDATED
end
----
[#overlayprint]
==== overlayPrint
[source,text]
----
overlayPrint(column, row, text)
----
Imprime una cadena con la fuente de consola de ancho fijo incorporada en el motor. A diferencia de todas las demás llamadas de dibujo, la posición es una celda de carácter: el motor multiplica `column` y `row` por el ancho y el alto del glifo, así que `(0, 0)` es la celda de arriba a la izquierda y `(1, 0)` está un glifo a la derecha. Los glifos se copian tal cual de la imagen de la fuente; no se aplican los colores de primer plano y de fondo actuales, y solo es transparente el propio fondo de la fuente. Cada byte de la cadena elige uno de los 256 glifos de la fuente, así que el texto se trata como bytes, no como UTF-8. Los caracteres que se saldrían por el borde derecho del overlay se descartan, y un texto que empieza más allá del borde no dibuja nada.
*Parámetros:*
* `column`, `row` -- la celda de carácter, `0` arriba a la izquierda.
* `text` -- la cadena que se imprime.
*Notas:* La fuente de consola está pensada para diagnósticos y pantallas de servicio. Usa `fontPrint` o `fontToSprite` para texto en color o proporcional. El tamaño de una celda en píxeles es `overlayGetFontWidth` por `overlayGetFontHeight`, así que `overlayGetWidth() // overlayGetFontWidth()` es cuántas columnas caben.
*Desde:* 1.x
*Véase también:* <<fontprint,fontPrint>>, <<overlayclear,overlayClear>>, <<colorbackground,colorBackground>>
.Ejemplo
[source,lua]
----
-- Diagnósticos en la esquina mientras termina una descarga.
function onOverlayUpdate()
colorBackground(0, 0, 0, 127)
overlayClear()
overlayPrint(1, 1, "FRAME " .. discGetFrame())
overlayPrint(1, 2, download.url .. " " .. tostring(download.finished))
if download.finished then
overlayPrint(1, 3, download.result)
end
return OVERLAY_UPDATED
end
----
[#overlaysetmonochrome]
==== overlaySetMonochrome
[source,text]
----
overlaySetMonochrome(enabled)
----
Extensión de Hypseus. Muestra el overlay en gris: antes de cada subida, el motor convierte una copia a luma con los pesos de BT.601, dejando el canal alfa como está (Hypseus le aplica un umbral). Los píxeles del propio overlay siguen en color, así que no cambia nada de lo que el script lee de vuelta, y el dibujo sigue como siempre. Combínala con `vldpSetMonochrome` para un modo en blanco y negro. Un argumento que no sea booleano aborta el script.
*Parámetros:*
* `enabled` -- booleano.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<vldpsetmonochrome,vldpSetMonochrome>>, <<overlayclear,overlayClear>>
.Ejemplo
[source,lua]
----
-- Un solo ajuste controla tanto el disco como el overlay.
function applyChroma(color)
vldpSetMonochrome(not color)
overlaySetMonochrome(not color)
end
----
[#overlaysetresolution]
==== overlaySetResolution
[source,text]
----
overlaySetResolution(width, height)
----
Sustituye el overlay por una superficie nueva del tamaño dado y descarta todo lo dibujado hasta entonces. A partir de ahí, toda coordenada del overlay, toda posición de sprite y todo resultado de `overlayGetWidth` o `overlayGetHeight` está en el nuevo espacio, que el motor escala a la ventana al mostrarlo. Llámala una vez durante el arranque, antes de dibujar; por defecto es la mitad de la resolución del vídeo en cada dimensión. Un ancho o un alto de `0` o menos termina el script con un error. Las resoluciones más altas dan una colocación más fina y un texto más nítido a cambio de más trabajo de composición por fotograma; el tamaño nativo del disco, de `discGetWidth` y `discGetHeight`, es la elección habitual.
*Parámetros:*
* `width` -- el nuevo ancho del overlay en píxeles, mayor que `0`.
* `height` -- la nueva altura del overlay en píxeles, mayor que `0`.
*Desde:* 2.00
*Véase también:* <<overlaygetwidth,overlayGetWidth>>, <<overlaygetheight,overlayGetHeight>>, <<discgetwidth,discGetWidth>>
.Ejemplo
[source,lua]
----
-- Iguala el tamaño del disco y después distribuye el menú en ese espacio de coordenadas.
overlaySetResolution(discGetWidth(), discGetHeight())
MARGIN_X = 16
MARGIN_Y = 16
MARQUEE_W = 200
MARQUEE_X = overlayGetWidth() - MARGIN_X - MARQUEE_W
CABINET_H = overlayGetHeight() - MARGIN_Y - CABINET_Y
----
[#setoverlayfullalpha]
==== setOverlayFullAlpha
[source,text]
----
setOverlayFullAlpha()
----
Extensión de Hypseus, aceptada e ignorada. Hypseus copia su overlay con el canal alfa umbralizado en `128`, así que un píxel semitransparente o es totalmente opaco o desaparece, y esto le pide que suba la superficie tal como está y la mezcle como es debido. El overlay de Singe siempre ha sido RGBA completo con alfa real por píxel, así que lo que esto pide ya está en vigor.
*Devuelve:* nada.
*Desde:* 3.00
[#setoverlaylinearscale]
==== setOverlayLinearScale
[source,text]
----
setOverlayLinearScale(enabled)
----
Extensión de Hypseus. Elige cómo se filtra el overlay de camino a la ventana. Con `true`, el valor por defecto y el que restaura una recarga, se muestrea linealmente, así que un overlay de baja resolución se suaviza en vez de verse en bloques; con `false` se toma el píxel más cercano, lo que mantiene nítidos el pixel art, los contornos de un píxel y el texto pequeño. Lineal es el valor por defecto porque así es como ha dibujado el overlay cada versión de Singe; Hypseus usa por defecto el píxel más cercano, así que un juego traído de Hypseus que nunca llama a esto se verá algo más suave hasta que lo haga. No cambia nada del dibujo, solo el escalado final de la resolución del overlay a la ventana, así que todo lo que hay en el overlay -- las primitivas, las fuentes, los sprites y la capa 3D -- pasa junto por el mismo filtro, y la imagen del disco que hay detrás no se toca. El ajuste sobrevive a `overlaySetResolution`, que sustituye el overlay. Un argumento que no sea booleano aborta el script.
*Parámetros:*
* `enabled` -- booleano.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<overlaysetresolution,overlaySetResolution>>, <<spritequality,spriteQuality>>
.Ejemplo
[source,lua]
----
-- Un overlay de 320x240 en una pantalla 4K; el título pintado suave y el pixel art del juego nítido.
overlaySetResolution(320, 240)
function showTitle()
setOverlayLinearScale(true)
mode = "title"
end
function startGame()
setOverlayLinearScale(false)
mode = "play"
end
----
[#setoverlayontop]
==== setOverlayOnTop
[source,text]
----
loaded, id = setOverlayOnTop(enabled)
----
Extensión de Hypseus. Elige si el overlay y las GUI dibujadas sobre la imagen quedan por encima o por debajo del arte del bezel. Solo el arte dibujado delante de la imagen (`--bezelflip`) llega a quedar por encima de ellos, así que con el arte detrás -- que es lo predeterminado -- esto no cambia nada. `false` es el ajuste con el que empieza un juego, y el valor por defecto del propio Hypseus. Se recuerda hasta que se recarga el script.
El ajuste solo se registra mientras hay arte cargado; sin él, la llamada no hace nada y responde `false` y `"0"`, exactamente como Hypseus. Un argumento que no sea booleano aborta el script.
*Parámetros:*
* `enabled` -- booleano; `true` pone el overlay y las GUI por encima del arte.
*Devuelve:* booleano, si hay arte cargado, y su identificador como cadena (`"0"` cuando no lo hay).
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<mainbezelloaded,mainBezelLoaded>>, <<guidrawscreen,guiDrawScreen>>, xref:Manual.adoc#bezels[Bezels]
.Ejemplo
[source,lua]
----
-- El marco de la recreativa tapa parte de la imagen, pero la línea de puntuación tiene que seguir siendo legible.
if mainBezelLoaded() then
setOverlayOnTop(true)
end
----
[#setoverlayopacity]
==== setOverlayOpacity
[source,text]
----
setOverlayOpacity(alpha)
----
Extensión de Hypseus. Lo sólido que es el overlay entero, de `0` (invisible) a `255` (tal como lo dibujó el juego). Multiplica el alfa que ya llevan los propios píxeles del overlay, así que un sprite semitransparente sigue siendo la mitad de sólido que el resto; no cambia el contenido del overlay, y no hay que volver a dibujar nada.
El nivel se limita al intervalo de `0` a `255` en vez de rechazarse, y un script empieza en `255`. Se recuerda hasta que se recarga el script, y sobrevive a un cambio de resolución del overlay.
Las GUI, la escena 3D y el arte del bezel se dibujan por separado y no se ven afectados.
*Parámetros:*
* `alpha` -- número; de `0` a `255`.
*Devuelve:* nada.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<setoverlaylinearscale,setOverlayLinearScale>>, <<overlaysetresolution,overlaySetResolution>>
.Ejemplo
[source,lua]
----
-- Desvanece todo el HUD en medio segundo.
local fade = 255
function onOverlayUpdate()
fade = math.max(0, fade - 8)
setOverlayOpacity(fade)
return OVERLAY_UPDATED
end
----
[#physicsref]
=== Física
Llamadas que actúan sobre el mundo entero: la consulta de rayos, la gravedad, el modo 2D, la pausa y el dibujo de depuración. La física se inicializa con el motor, así que no hay nada que activar antes del primer `bodyNew`; el mundo avanza sesenta pasos por segundo según el reloj, después de las animaciones y antes de que se renderice la escena. Las posiciones y las distancias van en unidades del mundo y la gravedad en unidades por segundo al cuadrado. Véase <<physics,Física>> en Desarrollo de juegos para saber cómo encajan los cuerpos, los nodos y el fotograma.
[#physicsraycast]
==== physicsRaycast
[source,text]
----
node, hx, hy, hz, nx, ny, nz = physicsRaycast(x, y, z, dx, dy, dz)
node, hx, hy, hz, nx, ny, nz = physicsRaycast(x, y, z, dx, dy, dz, maxDistance)
----
Encuentra el primer cuerpo a lo largo de un rayo que sale de `(x, y, z)` en la dirección `(dx, dy, dz)`, a no más de `maxDistance` unidades del mundo. La dirección se normaliza, así que vale cualquier longitud, pero una dirección nula no alcanza nada. Los activadores y los jugadores se alcanzan como cualquier otro cuerpo; un jugador se devuelve por su nodo. Con `sceneUnproject` aportando dos puntos a lo largo de la línea del ratón, esto sirve para seleccionar objetos con el ratón; lanzado hacia abajo desde un punto, es una sonda de altura.
*Parámetros:*
* `x`, `y`, `z` -- el inicio del rayo en el espacio del mundo.
* `dx`, `dy`, `dz` -- su dirección; solo importa la dirección.
* `maxDistance` -- hasta dónde buscar; por defecto `1000`, y `0` o menos significa el valor por defecto.
*Devuelve:* El nodo del cuerpo, el punto de impacto y la normal de la superficie en ese punto, o un único `nil` cuando no se alcanza nada.
*Desde:* 3.00.
*Véase también:* <<sceneunproject,sceneUnproject>>, <<bodyapplyimpulse,bodyApplyImpulse>>, <<linedraw,lineDraw>>
.Ejemplo
[source,lua]
----
-- Pone la marca sobre el suelo llano que haya bajo el cursor.
function onOverlayUpdate()
local mx, my = mouseGetPosition(0)
local ox, oy, oz = sceneUnproject(mx, my, 0)
local fx, fy, fz = sceneUnproject(mx, my, 10)
local hit, hx, hy, hz, nx, ny, nz = physicsRaycast(ox, oy, oz, fx - ox, fy - oy, fz - oz, 60)
nodeSetVisible(marker, hit ~= nil and ny > 0.7)
if hit then
nodeSetPosition(marker, hx, hy + 0.05, hz)
end
return OVERLAY_NOT_UPDATED
end
----
[#physicsset2d]
==== physicsSet2D
[source,text]
----
physicsSet2D(planar)
----
Con `true`, los cuerpos creados a partir de ese momento solo se mueven en X e Y y solo giran alrededor de Z: el mundo de un juego 2D, normalmente en coordenadas del overlay con la gravedad apuntando hacia +Y para que las cosas caigan pantalla abajo. Los cuerpos ya creados conservan su libertad, así que llámala antes de cualquier `bodyNew`; `false` vuelve al 3D completo para los cuerpos creados después. Los jugadores son distintos: mientras el modo está activo, todo jugador, se creara cuando se creara, se mueve a lo largo de X con su Z fija en `0`, y `playerMove` toma un solo valor. El mundo 2D funciona sin GPU, también en la Raspberry Pi 3.
*Desde:* 3.00.
*Véase también:* <<physicssetgravity,physicsSetGravity>>, <<bodynew,bodyNew>>, <<playermove,playerMove>>
.Ejemplo
[source,lua]
----
-- Un mundo de plataformas en píxeles del overlay: gravedad pantalla abajo, un suelo y paredes.
physicsSet2D(true)
physicsSetGravity(0, 900, 0)
local width, height = overlayGetWidth(), overlayGetHeight()
floor = nodeNew()
nodeSetPosition(floor, width / 2, height - 10, 0)
bodyNew(floor, BODY_STATIC, SHAPE_BOX, width, 20, 50)
leftWall = nodeNew()
nodeSetPosition(leftWall, -10, height / 2, 0)
bodyNew(leftWall, BODY_STATIC, SHAPE_BOX, 20, height, 50)
----
[#physicssetdebug]
==== physicsSetDebug
[source,text]
----
physicsSetDebug(mask)
----
Dibuja lo que contiene la simulación como líneas sobre la escena en cada fotograma, mientras la máscara esté puesta: `DEBUG_SHAPES` para la forma de cada cuerpo móvil en alambre (incluidos los cuerpos blandos, y los jugadores activos en amarillo), `DEBUG_CONSTRAINTS` para las articulaciones, las articulaciones de ragdoll y las restricciones de vehículos con sus límites, `DEBUG_CONTACTS` para una cruz roja donde los cuerpos se han tocado en este paso, y `DEBUG_VELOCITIES` para una línea a lo largo de la velocidad de cada cuerpo móvil. Suma las constantes; `DEBUG_ALL` son las cuatro. Las formas de los cuerpos estáticos no se dibujan salvo que se sume `DEBUG_STATIC`, porque la malla de un nivel son muchas líneas. `DEBUG_NONE` lo apaga, y mientras está apagado no se dibuja nada ni cuesta nada.
*Notas:* Una compilación de Singe hecha sin el renderizador de depuración de Jolt imprime un mensaje en la consola y no dibuja nada.
*Desde:* 3.00.
*Véase también:* <<linedraw,lineDraw>>, <<navdraw,navDraw>>, <<physicssetenabled,physicsSetEnabled>>
.Ejemplo
[source,lua]
----
-- Muestra y oculta el dibujo de la física con una tecla de depuración; también las formas estáticas, porque el nivel es pequeño.
function onInputPressed(switch)
if switch == SWITCH_BUTTON3 then
showPhysics = not showPhysics
if showPhysics then
physicsSetDebug(DEBUG_ALL + DEBUG_STATIC)
else
physicsSetDebug(DEBUG_NONE)
end
end
end
----
[#physicssetenabled]
==== physicsSetEnabled
[source,text]
----
physicsSetEnabled(enabled)
----
Pausa la simulación con `false` y la reanuda con `true`. Mientras está en pausa no se ejecuta ningún paso: los cuerpos dinámicos se quedan quietos y conservan sus velocidades, los cuerpos cinemáticos dejan de seguir a sus nodos, los jugadores dejan de moverse y no se dispara ningún `onCollision` ni `onTrigger`. El tiempo no se acumula durante la pausa, así que al reanudar no se recuperan los pasos perdidos. La pausa propia del juego hace lo mismo sin esta llamada; úsala para un menú, una cinemática o una imagen congelada que deja funcionando el resto del juego.
*Desde:* 3.00.
*Véase también:* <<physicssetgravity,physicsSetGravity>>, <<bodyisresting,bodyIsResting>>
.Ejemplo
[source,lua]
----
-- Congela el mundo mientras el inventario está abierto.
function openInventory()
inventoryOpen = true
physicsSetEnabled(false)
end
function closeInventory()
inventoryOpen = false
physicsSetEnabled(true)
end
----
[#physicssetgravity]
==== physicsSetGravity
[source,text]
----
physicsSetGravity(x, y, z)
----
Fija la aceleración que sienten todos los cuerpos dinámicos y todos los jugadores, en unidades del mundo por segundo al cuadrado a lo largo de cada eje del mundo. El valor por defecto es `0, -9.81, 0`, la gravedad de la Tierra en metros; `0, 0, 0` es el espacio, y un juego 2D en píxeles del overlay quiere algo como `0, 900, 0`, ya que la Y del overlay va pantalla abajo. La dirección opuesta a la gravedad es «arriba» para los jugadores: hacia donde se ponen de pie, saltan y suben escalones.
Un cambio de gravedad despierta a todos los cuerpos, incluidos los blandos, así que una caja que se había quedado en reposo cae cuando la gravedad se tuerce hacia un lado, y la tela ondea cuando cambia un viento hecho de gravedad. Volver a fijar la misma gravedad no despierta nada.
*Desde:* 3.00.
*Véase también:* <<bodynew,bodyNew>>, <<physicsset2d,physicsSet2D>>, <<playersetgravityscale,playerSetGravityScale>>
.Ejemplo
[source,lua]
----
-- El nivel de la luna: una sexta parte de la atracción de la Tierra, y un salto más largo a juego.
function startLevel(name)
if name == "moon" then
physicsSetGravity(0, -1.62, 0)
jumpSpeed = 3.0
else
physicsSetGravity(0, -9.81, 0)
jumpSpeed = 4.5
end
end
----
[#player]
=== Jugador
Un jugador es un controlador de personaje sobre un nodo: mientras que un cuerpo va adonde lo mandan las fuerzas, un jugador va adonde dice el script, camina por los suelos, sube escalones, se desliza a lo largo de las paredes, viaja sobre plataformas y aparta a empujones los cuerpos ligeros. Los jugadores se identifican por su nodo, y un nodo lleva un cuerpo o un jugador, no los dos. Los tamaños y las velocidades van en unidades del mundo (metros en una escena 3D, píxeles del overlay en un mundo 2D) y las tolerancias del controlador se escalan con la forma, así que una cápsula de 30 píxeles se comporta como una de 0,3 metros. El jugador maneja la posición de su nodo en cada paso y deja la rotación al script. Todas las llamadas de esta sección terminan el script cuando el nodo no tiene jugador. Véase <<physics,Física>> en Desarrollo de juegos, Jugadores.
[#playernew]
==== playerNew
[source,text]
----
playerNew(node, radius, height)
playerNew(node, SHAPE_CAPSULE, radius, height)
playerNew(node, SHAPE_CYLINDER, radius, height)
playerNew(node, SHAPE_SPHERE, radius, 0)
playerNew(node, SHAPE_BOX, width, height, depth)
playerNew(node, SHAPE_HULL)
----
Pone un jugador en el nodo, donde está el nodo en el mundo en este momento: una cápsula de ese radio y esa altura total, o cualquier forma convexa de `bodyNew` con sus medidas, escalada por la escala en el mundo del nodo. La forma se apoya en el origen del nodo, así que el nodo son los pies. Empieza con una masa de `70` kilogramos, un empuje de `300` newtons, un límite de pendiente de `45` grados, una altura de escalón de `0.3` escalada por la forma, una escala de gravedad de `1` y los valores de natación por defecto. Sustituye cualquier cuerpo o jugador que tuviera el nodo. Termina el script con `SHAPE_MESH` (un jugador necesita una forma convexa; usa `SHAPE_HULL`), cuando la física no está disponible en esta máquina o cuando no se puede crear el jugador (una envolvente con muy poca malla, o ya hay dieciséis jugadores).
*Parámetros:*
* `radius`, `height` -- una cápsula: el radio y la altura total a lo largo de la vertical, con los casquetes incluidos.
* `SHAPE_*` -- una constante de forma seguida de sus medidas de `bodyNew`; `SHAPE_HULL` no lleva ninguna y envuelve la malla del nodo y todo lo que cuelga de él.
*Notas:* Una llamada con exactamente tres argumentos es siempre una cápsula, así que una esfera necesita un cuarto argumento (cualquier número) después de su radio para leerse como `SHAPE_SPHERE`.
*Desde:* 3.00.
*Véase también:* <<playermove,playerMove>>, <<playerdelete,playerDelete>>, <<bodynew,bodyNew>>, <<nodesetparent,nodeSetParent>>
.Ejemplo
[source,lua]
----
-- El héroe: una cápsula con el modelo Fox colgado del nodo y los pies en el suelo.
hero = nodeNew()
nodeSetPosition(hero, 0, 0.05, 0)
playerNew(hero, 0.3, 1.0)
playerSetStep(hero, 0.35)
fox = modelInstance(modelLoad(DIR .. "Models/Fox.glb"))
nodeSetParent(fox, hero)
nodeSetScale(fox, 0.01)
animationPlay(fox, "Walk", true)
-- Un robot es una caja; dobla las esquinas en ángulo recto y no puede rodar.
robot = nodeNew()
playerNew(robot, SHAPE_BOX, 0.8, 1.6, 0.6)
----
[#playerdelete]
==== playerDelete
[source,text]
----
playerDelete(node)
----
Quita el jugador; el nodo y todo lo que cuelga de él se quedan donde están. Borrar el nodo también quita su jugador, y lo mismo hace darle al nodo un cuerpo con `bodyNew`. Termina el script cuando el nodo no tiene jugador.
*Desde:* 3.00.
*Véase también:* <<playernew,playerNew>>, <<playersetenabled,playerSetEnabled>>
.Ejemplo
[source,lua]
----
-- El héroe se sube al coche: el controlador desaparece y el modelo viaja con él.
function boardCar()
playerDelete(hero)
nodeSetParent(hero, car)
nodeSetPosition(hero, 0, 0.4, 0)
driving = true
end
----
[#playermove]
==== playerMove
[source,text]
----
playerMove(node, vx, vz)
playerMove(node, vx, vy, vz)
playerMove(node, vx)
----
Le dice al jugador la velocidad que quiere en este fotograma, en unidades del mundo por segundo. En el suelo o en el aire, los dos valores horizontales se usan tal cual, sumados a los de la plataforma sobre la que esté; la parte vertical la ponen la gravedad, el suelo y `playerJump`, y `vy` se ignora. Cuando el jugador está nadando, los tres valores lo guían, y el agua acerca su velocidad a ellos con la resistencia de `playerSetSwim`, y se hunde a la velocidad de hundimiento cuando `vy` es `0`. En un mundo 2D, un único `vx` lo mueve a lo largo de X y cualquier `vz` se ignora. Llámala en cada fotograma desde `onOverlayUpdate`: la petición la consumen los pasos de ese fotograma, y en un fotograma sin petición el jugador se queda quieto.
*Parámetros:*
* `vx`, `vz` -- la velocidad horizontal deseada, en unidades del mundo por segundo (2D: píxeles del overlay a lo largo de X).
* `vy` -- la velocidad vertical deseada mientras nada; positiva es hacia arriba.
*Desde:* 3.00.
*Véase también:* <<playerjump,playerJump>>, <<playersetvelocity,playerSetVelocity>>, <<playerisswimming,playerIsSwimming>>, <<physicsset2d,physicsSet2D>>
.Ejemplo
[source,lua]
----
-- Camina con el stick izquierdo, nada hacia arriba mientras se mantiene el botón y mira hacia donde va.
function onOverlayUpdate()
local vx = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_X) / 32767 * 3
local vz = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_Y) / 32767 * 3
if playerIsSwimming(hero) then
playerMove(hero, vx, surfacing and 1.5 or 0, vz)
else
playerMove(hero, vx, vz)
end
nodeSetRotation(hero, 0, math.deg(math.atan(-vz, vx)), 0)
return OVERLAY_NOT_UPDATED
end
----
[#playerisswimming]
==== playerIsSwimming
[source,text]
----
swimming = playerIsSwimming(node)
----
Si el jugador estaba en el agua en el último paso: el centro de su forma por debajo de la superficie de un volumen de `bodySetWater`. Mientras nada, `playerMove` lo guía en los tres ejes, la gravedad deja paso a la velocidad de hundimiento y la corriente del agua arrastra al jugador.
*Devuelve:* Booleano.
*Desde:* 3.00.
*Véase también:* <<playersetswim,playerSetSwim>>, <<bodysetwater,bodySetWater>>, <<playermove,playerMove>>
.Ejemplo
[source,lua]
----
-- Cambia la animación cuando el héroe se mete en el agua.
function onOverlayUpdate()
local swimming = playerIsSwimming(hero)
if swimming ~= wasSwimming then
animationPlay(fox, swimming and "Swim" or "Walk", true)
wasSwimming = swimming
end
return OVERLAY_NOT_UPDATED
end
----
[#playerjump]
==== playerJump
[source,text]
----
jumped = playerJump(node, speed)
----
Pide al jugador que despegue del suelo a esa velocidad hacia arriba, en unidades del mundo por segundo, en el siguiente paso. Solo se concede cuando hay suelo transitable bajo los pies en ese momento, así que mantener pulsado un botón no hace rebotar al jugador y un salto en el aire no hace nada. Una velocidad negativa se trata como `0`. El jugador conserva su velocidad horizontal durante el salto y `playerMove` sigue guiándolo en el aire.
*Devuelve:* `true` cuando se ha concedido el salto.
*Desde:* 3.00.
*Véase también:* <<playerisonground,playerIsOnGround>>, <<playermove,playerMove>>, <<playersetgravityscale,playerSetGravityScale>>
.Ejemplo
[source,lua]
----
-- Salta con el botón de acción, con sonido solo cuando el salto ha ocurrido de verdad.
function onInputPressed(switch)
if switch == SWITCH_BUTTON1 then
if playerJump(hero, 4.5) then
soundPlay(jumpSound)
end
end
end
----
[#playersetswim]
==== playerSetSwim
[source,text]
----
playerSetSwim(node, sinkSpeed, drag)
----
Fija cómo se comporta el jugador en el agua: lo rápido que se hunde, en unidades del mundo por segundo, cuando `playerMove` no pide movimiento vertical (por defecto `0.3`), y lo deprisa que el agua lleva su velocidad hacia lo que pide `playerMove`, como tasa por segundo (por defecto `2`, así que la mayor parte de la diferencia desaparece en menos de un segundo; `0` no cambia nunca la velocidad). Ambos valores se limitan a `0` como mínimo. Un buzo pesado se hunde rápido con una resistencia alta; un corcho no se hunde nada.
*Parámetros:*
* `sinkSpeed` -- la deriva hacia abajo sin entrada vertical, en unidades por segundo.
* `drag` -- la tasa a la que el agua lleva la velocidad hacia la petición, por segundo.
*Desde:* 3.00.
*Véase también:* <<playerisswimming,playerIsSwimming>>, <<playermove,playerMove>>, <<bodysetwater,bodySetWater>>
.Ejemplo
[source,lua]
----
-- La armadura arrastra al héroe hacia el fondo; quítatela para flotar.
function wearArmor(on)
armored = on
if on then
playerSetSwim(hero, 1.2, 4)
else
playerSetSwim(hero, 0.3, 2)
end
end
----
[#playersetvelocity]
==== playerSetVelocity
[source,text]
----
playerSetVelocity(node, x, y, z)
----
Fija la velocidad completa del jugador, en unidades del mundo por segundo a lo largo de cada eje del mundo, y descarta cualquier petición de `playerMove` hecha en este fotograma. Lo que dura es la parte vertical: una velocidad hacia arriba levanta al jugador del suelo y la gravedad lo hace volver a caer, lo que da una plataforma de lanzamiento o un rebote. La parte horizontal solo se mantiene hasta el siguiente paso, cuando se sustituye por lo que pida `playerMove` (nada, si no se llama), porque un jugador no tiene inercia horizontal propia. Un retroceso por golpe o un acelerón son, por tanto, un impulso hacia arriba aquí más una velocidad de `playerMove` aplicada durante los fotogramas siguientes.
*Desde:* 3.00.
*Véase también:* <<playergetvelocity,playerGetVelocity>>, <<playermove,playerMove>>, <<playerjump,playerJump>>
.Ejemplo
[source,lua]
----
-- Un golpe del ogro lanza al héroe hacia arriba y, en los fotogramas siguientes, lejos de él.
function onCollision(a, b, x, y, z, speed)
if (a == hero and b == ogre) or (a == ogre and b == hero) then
local hx, hy, hz = nodeGetPosition(hero)
local ox, oy, oz = nodeGetPosition(ogre)
local dx, dz = hx - ox, hz - oz
local len = math.sqrt(dx * dx + dz * dz)
playerSetVelocity(hero, 0, 3, 0)
knockX, knockZ, knockFrames = dx / len * 6, dz / len * 6, 20
health = health - 10
end
end
----
[#playergetvelocity]
==== playerGetVelocity
[source,text]
----
x, y, z = playerGetVelocity(node)
----
La velocidad del jugador después del último paso, en unidades del mundo por segundo a lo largo de cada eje del mundo, incluida la que le ha dado el suelo sobre el que va. Lee la parte vertical para distinguir si sube o si cae, o la velocidad entera para el golpe sordo de un aterrizaje.
*Devuelve:* Tres números.
*Desde:* 3.00.
*Véase también:* <<playersetvelocity,playerSetVelocity>>, <<playerisonground,playerIsOnGround>>
.Ejemplo
[source,lua]
----
-- Daño por caída según la velocidad a la que el héroe ha llegado al suelo.
function onOverlayUpdate()
local _, vy = playerGetVelocity(hero)
local onGround = playerIsOnGround(hero)
if onGround and not wasOnGround and lastVy < -8 then
health = health - math.floor((-lastVy - 8) * 5)
end
wasOnGround = onGround
lastVy = vy
return OVERLAY_NOT_UPDATED
end
----
[#playerisonground]
==== playerIsOnGround
[source,text]
----
onGround = playerIsOnGround(node)
----
Si el jugador está de pie sobre suelo transitable según el último paso: una superficie no más empinada que `playerSetSlope`. En el aire o en una pendiente demasiado empinada para subirla es `false`. Es la misma comprobación que hace `playerJump`.
*Devuelve:* Booleano.
*Desde:* 3.00.
*Véase también:* <<playergetground,playerGetGround>>, <<playerjump,playerJump>>, <<playersetslope,playerSetSlope>>
.Ejemplo
[source,lua]
----
-- Tiempo de coyote: se sigue permitiendo saltar unos fotogramas después de salirse de un borde.
function onOverlayUpdate()
if playerIsOnGround(hero) then
framesSinceGround = 0
else
framesSinceGround = framesSinceGround + 1
end
if jumpPressed and framesSinceGround < 6 then
playerSetVelocity(hero, vx, 4.5, vz)
end
return OVERLAY_NOT_UPDATED
end
----
[#playergetground]
==== playerGetGround
[source,text]
----
ground, nx, ny, nz = playerGetGround(node)
----
Sobre qué está de pie el jugador y la normal de la superficie del suelo en ese punto. El nodo es el cuerpo que tiene bajo los pies, sea transitable o no; es `nil` en el aire, y entonces la normal es `0, 1, 0`. Usa el nodo para distinguir del suelo una plataforma móvil, el hielo o una placa de presión, y la normal para inclinar un modelo según la pendiente.
*Devuelve:* El nodo que pisa, o `nil`, y después la normal.
*Desde:* 3.00.
*Véase también:* <<playerisonground,playerIsOnGround>>, <<bodysetfriction,bodySetFriction>>
.Ejemplo
[source,lua]
----
-- Los pasos suenan distinto en el puente de madera, y la placa abre la verja.
function onOverlayUpdate()
local ground = playerGetGround(hero)
if ground == bridge then
footstep = woodStep
elseif ground == plate then
openGate()
else
footstep = stoneStep
end
return OVERLAY_NOT_UPDATED
end
----
[#playersetstep]
==== playerSetStep
[source,text]
----
playerSetStep(node, height)
----
El escalón más alto que sube el jugador sin saltar, en unidades del mundo; lo que sea más alto es una pared a lo largo de la que se desliza. El valor por defecto es `0.3` para una forma cuya semimedida más pequeña es `0.3` (una cápsula de radio `0.3`), y se escala con la forma, así que un jugador de 30 píxeles en un mundo 2D empieza con 30 píxeles. La misma distancia es lo que el jugador baja pegado al suelo al pasar el borde de un escalón, así que un escalón mayor también lo mantiene pegado al bajar pendientes. Los valores negativos se limitan a `0`.
*Desde:* 3.00.
*Véase también:* <<playersetslope,playerSetSlope>>, <<playernew,playerNew>>
.Ejemplo
[source,lua]
----
-- Las escaleras de este nivel suben 0.35 por peldaño; asegúrate de que el héroe puede con ellas.
playerNew(hero, 0.3, 1.0)
playerSetStep(hero, 0.4)
-- Los salientes del juego de plataformas 2D miden 26 píxeles.
playerNew(runner, 16, 48)
playerSetStep(runner, 26)
----
[#playersetslope]
==== playerSetSlope
[source,text]
----
playerSetSlope(node, degrees)
----
La pendiente más empinada que sube el jugador caminando, en grados desde la horizontal; el suelo más empinado no cuenta como estar de pie, así que el jugador resbala por él y no puede saltar desde él. El valor por defecto es `45`, y el valor se limita entre `0` y `89`. Un valor bajo convierte las colinas en obstáculos; uno alto deja que el jugador suba casi cualquier cosa que no sea una pared.
*Desde:* 3.00.
*Véase también:* <<playersetstep,playerSetStep>>, <<playerisonground,playerIsOnGround>>
.Ejemplo
[source,lua]
----
-- La cabra montés sube por donde el héroe resbala.
playerNew(goat, 0.25, 0.8)
playerSetSlope(goat, 70)
playerNew(hero, 0.3, 1.0)
playerSetSlope(hero, 40)
----
[#playersetmass]
==== playerSetMass
[source,text]
----
playerSetMass(node, kilograms)
----
La masa del jugador, que se usa cuando empuja cuerpos dinámicos y como su peso sobre cualquiera en el que se apoye. El valor por defecto es `70` kilogramos; los valores por debajo de `0.001` se suben hasta ese mínimo. La masa no afecta a cómo cae o camina el jugador, solo a los empujones, así que combínala con `playerSetPush`.
*Desde:* 3.00.
*Véase también:* <<playersetpush,playerSetPush>>, <<bodysetmass,bodySetMass>>
.Ejemplo
[source,lua]
----
-- El trol se abre paso a empujones por donde el héroe tiene que dar un rodeo.
troll = nodeNew()
nodeSetPosition(troll, 6, 0, -3)
playerNew(troll, 0.6, 2.4)
playerSetMass(troll, 400)
playerSetPush(troll, 2000)
playerSetStep(troll, 0.8)
----
[#playersetpush]
==== playerSetPush
[source,text]
----
playerSetPush(node, strength)
----
Con cuánta fuerza empuja el jugador los cuerpos dinámicos contra los que camina, en newtons. El valor por defecto, `300`, mueve cajas y pelotas pero no armarios; con `0` rodea todo como si fuera estático. Los valores negativos se limitan a `0`. El propio `bodySetMass` de un cuerpo decide qué puede mover una fuerza dada.
*Desde:* 3.00.
*Véase también:* <<playersetmass,playerSetMass>>, <<bodysetmass,bodySetMass>>
.Ejemplo
[source,lua]
----
-- La poción de fuerza deja que el héroe mueva la roca durante diez segundos.
function drinkStrength()
playerSetPush(hero, 3000)
strengthUntil = frames + 600
end
function onOverlayUpdate()
if strengthUntil and frames > strengthUntil then
playerSetPush(hero, 300)
strengthUntil = nil
end
return OVERLAY_NOT_UPDATED
end
----
[#playersetgravityscale]
==== playerSetGravityScale
[source,text]
----
playerSetGravityScale(node, scale)
----
Multiplica la gravedad del mundo solo para este jugador: con `1` (el valor por defecto) cae como todo lo demás, con `0` flota donde está (una escalera de mano, una sala sin gravedad, una montura voladora), con más cae más deprisa y da sensación de peso, y con menos el salto es más flotante. No cambia lo que hace `playerJump` al despegar, solo lo deprisa que vuelve a bajar el jugador. Mientras nada, la velocidad de hundimiento del agua ocupa el lugar de la gravedad.
*Desde:* 3.00.
*Véase también:* <<physicssetgravity,physicsSetGravity>>, <<playerjump,playerJump>>, <<playersetswim,playerSetSwim>>
.Ejemplo
[source,lua]
----
-- En la escalera de mano el héroe sube con el stick en vez de caer.
function onTrigger(trigger, other, entered)
if trigger == ladder and other == hero then
onLadder = entered
playerSetGravityScale(hero, entered and 0 or 1)
end
end
----
[#playersetposition]
==== playerSetPosition
[source,text]
----
playerSetPosition(node, x, y, z)
----
Teletransporta al jugador a una posición del mundo, sin tocar nada por el camino, y lo detiene en seco. El nodo se mueve con él y conserva su rotación. Úsala para hacer aparecer, reaparecer y pasar de un nivel a otro; si en su lugar mueves el nodo con `nodeSetPosition`, el jugador se queda donde estaba y el nodo vuelve con él en el siguiente paso.
*Desde:* 3.00.
*Véase también:* <<playersetvelocity,playerSetVelocity>>, <<nodesetposition,nodeSetPosition>>
.Ejemplo
[source,lua]
----
-- De vuelta al último punto de control después de caer al foso.
function onTrigger(trigger, other, entered)
if trigger == pit and other == hero and entered then
lives = lives - 1
playerSetPosition(hero, checkpointX, checkpointY, checkpointZ)
end
end
----
[#playersetenabled]
==== playerSetEnabled
[source,text]
----
playerSetEnabled(node, enabled)
----
Con `false` el jugador sale de la simulación: su cuerpo se quita del mundo, así que no se mueve, no cae, no maneja su nodo ni bloquea nada, `onTrigger` informa de que sale de todos los activadores en los que estaba, y queda fuera del dibujo de depuración. `true` vuelve a poner el cuerpo y lo retoma desde donde esté el nodo. Desactiva un jugador mientras un ragdoll, una cinemática o un vehículo se hace cargo del personaje.
*Desde:* 3.00.
*Véase también:* <<playerdelete,playerDelete>>, <<ragdollnew,ragdollNew>>, <<bodysetenabled,bodySetEnabled>>
.Ejemplo
[source,lua]
----
-- Un golpe fuerte entrega el esqueleto del héroe al ragdoll.
function onCollision(a, b, x, y, z, speed)
if (a == hero or b == hero) and speed > 6 then
playerSetEnabled(hero, false)
ragdollActivate(fox)
end
end
----
[#ragdoll]
=== Ragdoll
El esqueleto de un modelo con skinning entregado a la física (véase <<physics,Física>>, Ragdolls). Un ragdoll se identifica por el nodo que se dio a `ragdollNew`, normalmente la raíz de una instancia de modelo, y un nodo sin ragdoll termina el script en cualquier llamada posterior a `ragdollNew`. Los huesos se nombran por los nodos de sus articulaciones, los nombres que ve `nodeFind`, comparados sin distinguir mayúsculas de minúsculas; las distancias van en unidades del mundo, los ángulos en grados, los impulsos en newton-segundos y la fuerza de los motores en newton-metros. Hasta ocho ragdolls a la vez, de hasta 48 huesos cada uno.
[#ragdollnew]
==== ragdollNew
[source,text]
----
ragdollNew(node)
----
Busca en el nodo y debajo de él la primera malla con skinning y construye una receta de ragdoll a partir de su esqueleto: cada articulación que tiene una articulación hija se convierte en un hueso, una cápsula que va desde la articulación hacia sus hijas, unida al hueso más cercano por encima mediante una articulación de giro y torsión limitada a 45 grados de giro y 30 de torsión. Las articulaciones sin articulación hija, como las yemas de los dedos y la punta de una cola, van con su hueso padre y no se pueden nombrar en las demás llamadas. Todavía no se simula nada: el modelo sigue animándose hasta `ragdollActivate`. Volver a llamarla sobre el mismo nodo descarta antes el ragdoll anterior. Termina el script cuando la física no está disponible, cuando no hay ninguna malla con skinning y huesos bajo el nodo o cuando ya existen ocho ragdolls.
*Desde:* 3.00.
*Véase también:* <<ragdollactivate,ragdollActivate>>, <<ragdollsetjoint,ragdollSetJoint>>, <<modelinstance,modelInstance>>
.Ejemplo
[source,lua]
----
-- Un guardia que corre hacia el jugador; el ragdoll está listo antes del placaje.
local guard = nodeNew()
nodeSetPosition(guard, -1, 0.05, -1)
playerNew(guard, 0.3, 0.9)
local guardFox = modelInstance(foxModel)
nodeSetParent(guardFox, guard)
nodeSetScale(guardFox, 0.01)
nodeSetRotation(guardFox, 0, 90, 0)
animationPlay(guardFox, "Run", true)
ragdollNew(guardFox) -- Solo la receta; sigue corriendo
----
[#ragdolldelete]
==== ragdollDelete
[source,text]
----
ragdollDelete(node)
----
Quita el ragdoll: sus cuerpos y articulaciones si está activo, y después la receta. Los nodos de las articulaciones se quedan donde los dejó la física, así que el modelo queda tal como cayó hasta que una animación lo mueve, y el nodo queda libre para un nuevo `ragdollNew`. Un nodo sin ragdoll termina el script.
*Desde:* 3.00.
*Véase también:* <<ragdollnew,ragdollNew>>, <<ragdolldeactivate,ragdollDeactivate>>, <<nodedelete,nodeDelete>>
.Ejemplo
[source,lua]
----
function onOverlayUpdate()
for i = #fallen, 1, -1 do
local body = fallen[i]
body.restingFrames = ragdollIsResting(body.node) and body.restingFrames + 1 or 0
if body.restingFrames > 300 then
ragdollDelete(body.node) -- Quieto cinco segundos: se retira
nodeDelete(body.node)
table.remove(fallen, i)
end
end
return OVERLAY_NOT_UPDATED
end
----
[#ragdollsetjoint]
==== ragdollSetJoint
[source,text]
----
ragdollSetJoint(node, jointName, radius, swingDegrees, twistDegrees)
----
Ajusta la receta de un hueso. Los valores se leen cuando se crean los cuerpos, así que llámala antes de `ragdollActivate`; un ragdoll que ya está activo conserva sus cuerpos hasta que se desactiva y se vuelve a activar. El radio `0` pide la cápsula automática, de 0,22 veces la longitud del hueso y nunca más fina de `0.02` unidades. El giro es el semiángulo del cono por el que puede moverse el hueso respecto a su padre, y la torsión, cuánto puede rotar sobre su propio eje; ambos se limitan entre `0` y `179`. Solo se pueden nombrar huesos, no las articulaciones terminales; el nombre se compara sin distinguir mayúsculas de minúsculas, y un nombre sin hueso termina el script.
*Parámetros:*
* `node` -- el nodo del ragdoll.
* `jointName` -- el nombre del nodo de la articulación, como en el modelo.
* `radius` -- el radio de la cápsula en unidades del mundo, o `0` para el automático.
* `swingDegrees` -- cuánto puede girar el hueso respecto a su padre; por defecto `45`.
* `twistDegrees` -- cuánto puede torcerse sobre su propio eje; por defecto `30`.
*Desde:* 3.00.
*Véase también:* <<ragdollnew,ragdollNew>>, <<ragdollactivate,ragdollActivate>>, <<nodefind,nodeFind>>
.Ejemplo
[source,lua]
----
ragdollNew(fox)
ragdollSetJoint(fox, "b_Neck_04", 0, 30, 20) -- Un cuello más rígido
ragdollSetJoint(fox, "b_Tail01_012", 0.03, 80, 60) -- Una cola que azota como un látigo
for _, leg in ipairs({ "b_LeftLeg01_015", "b_RightLeg01_019" }) do
ragdollSetJoint(fox, leg, 0, 60, 15) -- Las caderas giran mucho y apenas se tuercen
end
ragdollActivate(fox)
----
[#ragdollactivate]
==== ragdollActivate
[source,text]
----
ragdollActivate(node)
----
Entrega el esqueleto a la física desde su pose actual: se detienen todas las capas de animación del nodo, se crea una cápsula dinámica para cada hueso donde está su articulación en ese momento, se crean entre ellas las articulaciones de giro y torsión, y desde entonces cada paso de la física mueve los nodos de las articulaciones a partir de los cuerpos, así que la malla con skinning los sigue. Los huesos chocan con el mundo y entre sí, salvo cada hueso con su padre. La pose de ese momento es la pose hacia la que tira `ragdollSetStrength`. Un ragdoll que ya está activo se deja como está. Termina el script cuando no se puede crear el cuerpo de un hueso.
*Desde:* 3.00.
*Véase también:* <<ragdolldeactivate,ragdollDeactivate>>, <<ragdollapplyimpulse,ragdollApplyImpulse>>, <<ragdollsetstrength,ragdollSetStrength>>
.Ejemplo
[source,lua]
----
local crashed = false
function onCollision(a, b, x, y, z, speed)
if not crashed and (a == guard or b == guard) and speed > 1.5 then
crashed = true
playerSetEnabled(guard, false) -- El controlador lo suelta
ragdollActivate(guardFox) -- A partir de aquí la física se encarga del esqueleto
ragdollApplyImpulse(guardFox, "b_Spine02_03", 40, 30, 0)
end
end
----
[#ragdolldeactivate]
==== ragdollDeactivate
[source,text]
----
ragdollDeactivate(node)
----
Quita los cuerpos y las articulaciones y devuelve el esqueleto. Los nodos de las articulaciones conservan la pose en la que los dejó el ragdoll, y nada los mueve hasta que se reproduce una animación; un `animationPlay` con fundido mezcla a partir de esa pose, así que un personaje caído puede ponerse de pie. La receta se queda, así que `ragdollActivate` puede volver a tomar el control más tarde desde donde la animación haya llevado el esqueleto. No hace nada con un ragdoll que no está activo.
*Desde:* 3.00.
*Véase también:* <<ragdollactivate,ragdollActivate>>, <<animationplay,animationPlay>>
.Ejemplo
[source,lua]
----
-- Cuando el cuerpo se ha quedado quieto, el héroe se levanta en medio segundo.
function onOverlayUpdate()
if downed and ragdollIsResting(hero) then
downed = false
ragdollDeactivate(hero)
animationPlay(hero, "GetUp", false, 1, 0.5)
playerSetEnabled(heroController, true)
end
return OVERLAY_NOT_UPDATED
end
----
[#ragdollsetstrength]
==== ragdollSetStrength
[source,text]
----
ragdollSetStrength(node, strength)
----
Dota a cada articulación de un motor que tira hacia la pose en la que se activó el ragdoll, limitado a ese número de newton-metros; `0` (por defecto) apaga los motores y el cuerpo queda flácido. Los motores son muelles a 4 Hz, con amortiguamiento crítico. Se puede fijar antes de la activación o mientras el ragdoll se mueve, y tiene efecto en el siguiente paso. No pone de pie un cuerpo, ya que nada empuja contra el suelo: unas decenas de newton-metros convierten una caída en un tambaleo y dejan que un personaje caído recoja las extremidades.
*Desde:* 3.00.
*Véase también:* <<ragdollactivate,ragdollActivate>>, <<ragdollisresting,ragdollIsResting>>
.Ejemplo
[source,lua]
----
-- Derribado y flácido; un momento después, el luchador se tensa y se encoge.
ragdollActivate(fighter)
ragdollSetStrength(fighter, 0)
downFrames = 0
function onOverlayUpdate()
downFrames = downFrames + 1
if downFrames == 60 then
ragdollSetStrength(fighter, 60)
end
return OVERLAY_NOT_UPDATED
end
----
[#ragdollapplyimpulse]
==== ragdollApplyImpulse
[source,text]
----
hit = ragdollApplyImpulse(node, jointName, x, y, z)
----
Suma un impulso en newton-segundos, según los ejes del mundo, al cuerpo del hueso que empieza en la articulación nombrada: el puñetazo, el disparo, la explosión. El ragdoll tiene que estar activo, así que llama primero a `ragdollActivate` y después empuja. Devuelve `false`, y no hace nada, cuando el ragdoll no está activo o cuando ningún hueso tiene ese nombre (las articulaciones terminales no son huesos); el nombre se compara sin distinguir mayúsculas de minúsculas.
*Parámetros:*
* `node` -- el nodo del ragdoll.
* `jointName` -- el nombre del nodo de la articulación, como en el modelo.
* `x`, `y`, `z` -- el impulso en newton-segundos, según los ejes del mundo.
*Devuelve:* Verdadero cuando se ha encontrado el hueso y el ragdoll estaba activo.
*Desde:* 3.00.
*Véase también:* <<ragdollactivate,ragdollActivate>>, <<bodyapplyimpulse,bodyApplyImpulse>>, <<nodefind,nodeFind>>
.Ejemplo
[source,lua]
----
-- Un impacto desde la posición del tirador: el impulso apunta en sentido contrario al arma.
function shoot(target, gunX, gunY, gunZ, boneName)
local tx, ty, tz = nodeGetWorldPosition(target)
local dx = tx - gunX
local dz = tz - gunZ
local length = math.sqrt(dx * dx + dz * dz)
if not ragdollIsActive(target) then
ragdollActivate(target)
end
ragdollApplyImpulse(target, boneName, dx / length * 30, 10, dz / length * 30)
end
----
[#ragdollisactive]
==== ragdollIsActive
[source,text]
----
active = ragdollIsActive(node)
----
Si la física está moviendo el esqueleto en este momento: `true` desde `ragdollActivate` hasta `ragdollDeactivate` o `ragdollDelete`. Un nodo sin ragdoll termina el script.
*Devuelve:* Verdadero o falso.
*Desde:* 3.00.
*Véase también:* <<ragdollactivate,ragdollActivate>>, <<ragdollisresting,ragdollIsResting>>
.Ejemplo
[source,lua]
----
-- La barra de vida flota sobre el modelo solo mientras está de pie.
function onOverlayUpdate()
overlayClear()
for _, enemy in ipairs(enemies) do
if not ragdollIsActive(enemy.node) then
local x, y, z = nodeGetWorldPosition(enemy.node)
local sx, sy = sceneProject(x, y + 1.2, z)
drawHealthBar(sx, sy, enemy.health)
end
end
return OVERLAY_UPDATED
end
----
[#ragdollisresting]
==== ragdollIsResting
[source,text]
----
resting = ragdollIsResting(node)
----
Si el cuerpo de cada hueso se ha dormido, cosa que hace la física cuando un cuerpo lleva un momento quieto. `false` mientras el ragdoll no está activo, así que no sirve para distinguir un modelo parado de uno que se ha asentado; comprueba también `ragdollIsActive`.
*Devuelve:* Verdadero o falso.
*Desde:* 3.00.
*Véase también:* <<ragdollisactive,ragdollIsActive>>, <<ragdolldeactivate,ragdollDeactivate>>
.Ejemplo
[source,lua]
----
-- La ronda termina cuando el perdedor ha dejado de moverse.
function onOverlayUpdate()
if roundOver and ragdollIsResting(loser) then
restingFrames = restingFrames + 1
if restingFrames == 90 then
showResults()
end
end
return OVERLAY_NOT_UPDATED
end
----
[#save]
=== Partida guardada
Todo lo que el juego debe seguir sabiendo la próxima vez que arranque: los
récords, las opciones del jugador, hasta dónde llegó. Una tabla por juego,
guardada como JSON en la propia carpeta de datos del juego (véase
<<singegetdatapath,`singeGetDataPath`>>), así que un `.game` empaquetado sigue
siendo de solo lectura y dos juegos nunca se pisan.
Las claves las eliges tú. Los valores pueden ser números, cadenas, booleanos o
tablas de ellos, anidadas tan hondo como quieras -- una tabla de récords entera
entra como un solo valor. Una función, o un handle de algo que pertenece al
motor, no: ninguno de los dos significa nada en la siguiente ejecución, y
guardar uno es un error en vez de una sorpresa más adelante.
**Lo que fijas es lo que se guarda, y lo que recibes es tuyo.** Las tablas se
copian a la entrada y a la salida, así que cambiar una tabla que has leído no
cambia nada hasta que la vuelves a guardar, y cambiar una que has guardado no
afecta después a la partida guardada. Por eso, conservar una vista previa que
el jugador luego cancela es simplemente cuestión de no guardarla.
La escritura ocurre una vez al final de cualquier fotograma que haya cambiado
algo, por muchas claves que fijes en él, y otra vez cuando el juego se cierra --
incluido todo lo que se fije dentro de <<onshutdown,`onShutdown`>>. Para una
recreativa que puede quedarse sin corriente en cualquier momento,
<<saveflush,`saveFlush`>> escribe de inmediato. El archivo se escribe junto al
antiguo y se renombra encima de él, así que un corte de corriente durante una
escritura deja la partida guardada anterior y no la mitad de una nueva.
Los números enteros vuelven como enteros. JSON tiene un solo tipo de número y
Lua tiene dos, así que, si no, una puntuación guardada como `100` volvería como
`100.0` y se imprimiría con punto decimal.
[#saveclear]
==== saveClear
[source,text]
----
saveClear()
----
Lo olvida todo y escribe la partida guardada vacía. Para el «borrar récords» de un menú de servicio.
*Parámetros:*
* Ninguno.
*Devuelve:* nada.
*Desde:* 3.00.
[#savedelete]
==== saveDelete
[source,text]
----
saveDelete(key)
----
Quita una clave. Borrar una clave que nunca existió no es un error.
*Parámetros:*
* `key` -- la clave que se quita.
*Devuelve:* nada.
*Desde:* 3.00.
[#saveflush]
==== saveFlush
[source,text]
----
saveFlush()
----
Escribe la partida guardada ahora en vez de al final del fotograma. Úsala después de algo que no querrías perder por nada -- un récord nuevo en una recreativa que se apaga desde el enchufe.
*Parámetros:*
* Ninguno.
*Devuelve:* nada.
*Desde:* 3.00.
*Véase también:* <<saveset,saveSet>>
[#saveget]
==== saveGet
[source,text]
----
value = saveGet(key [, default])
----
Lo que se guardó con esa clave. Si no se guardó nada, devuelve el valor por defecto, o `nil` si no hay valor por defecto -- y así se distingue la primera ejecución de las siguientes.
*Parámetros:*
* `key` -- la clave que se lee.
* `default` -- opcional; lo que se responde cuando la clave no existe.
*Devuelve:* el valor guardado, el valor por defecto o `nil`.
*Desde:* 3.00.
*Véase también:* <<saveset,saveSet>>
.Ejemplo
[source,lua]
----
-- Cuenta las ejecuciones y recuerda las diez mejores puntuaciones.
local runs = saveGet("runs", 0) + 1
saveSet("runs", runs)
local best = saveGet("highScores", {})
best[#best + 1] = { name = initials, score = score }
table.sort(best, function(a, b) return a.score > b.score end)
while #best > 10 do table.remove(best) end
saveSet("highScores", best)
saveFlush()
----
[#savegetall]
==== saveGetAll
[source,text]
----
everything = saveGetAll()
----
Una copia de toda la partida guardada, para leerla: volcarla mientras depuras o recorrer claves cuyos nombres el juego no conoce de antemano. Cambiar lo que devuelve no cambia nada -- lo que escribe una tabla entera es <<savesetall,`saveSetAll`>>.
*Parámetros:*
* Ninguno.
*Devuelve:* una tabla; vacía cuando no se ha guardado nada.
*Desde:* 3.00.
*Véase también:* <<savesetall,saveSetAll>>, <<saveget,saveGet>>
.Ejemplo
[source,lua]
----
-- ¿Qué hay ahí dentro? inspect viene incluido justo para esto.
debugPrint(require("inspect")(saveGetAll()))
----
[#saveset]
==== saveSet
[source,text]
----
saveSet(key, value)
----
Conserva un valor hasta que se desinstala el juego. La escritura ocurre al final del fotograma, así que fijar veinte claves en un bucle escribe un solo archivo.
*Parámetros:*
* `key` -- la clave que se escribe.
* `value` -- un número, una cadena, un booleano o una tabla de ellos.
*Devuelve:* nada.
*Desde:* 3.00.
*Véase también:* <<saveget,saveGet>>, <<saveflush,saveFlush>>
[#savesetall]
==== saveSetAll
[source,text]
----
saveSetAll(everything)
----
Sustituye toda la partida guardada por esta tabla, de una vez. Las claves que existían y no están en la tabla que das desaparecen.
Existe para pasar una partida guardada antigua a una estructura nueva. Hacerlo con <<saveclear,`saveClear`>> y un bucle de <<saveset,`saveSet`>> deja un momento en que la partida guardada está vacía, y una máquina que se apague en ese momento lo pierde todo; esto no tiene ese momento, porque el archivo se escribe una sola vez al final del fotograma a partir de la tabla terminada.
*Parámetros:*
* `everything` -- tabla; lo que debe contener ahora la partida guardada. Los mismos valores que acepta `saveSet`.
*Devuelve:* nada.
*Desde:* 3.00.
*Véase también:* <<savegetall,saveGetAll>>, <<saveclear,saveClear>>
.Ejemplo
[source,lua]
----
-- La versión 1 guardaba una puntuación en dos claves; la versión 2 guarda una tabla de ellas.
if saveGet("version", 1) < 2 then
local old = saveGetAll()
saveSetAll({
version = 2,
scores = { { name = old.hiName, score = old.hiScore } },
runs = old.runs,
})
saveFlush()
end
----
[#scene]
=== Escena
La escena es la capa 3D que se dibuja entre el vídeo del disco y el overlay; los nodos que se ponen en la capa frontal con `nodeSetLayer` se dibujan en cambio después del overlay. Está desactivada hasta `sceneEnable(true)`, tiene el tamaño del overlay y se borra a `sceneSetBackground` en cada fotograma. Las llamadas `sceneSet*` fijan el aspecto de toda la capa: luz ambiente, cielo, niebla, exposición y curva tonal, bloom, antialiasing y calidad de las sombras; los colores son enteros de `0` a `255` y las distancias van en unidades del mundo. `sceneProject` y `sceneUnproject` hacen de puente entre el espacio del mundo y las coordenadas del overlay, y `sceneGetStats` informa del trabajo del último fotograma. Véase <<scenes3d,Escenas 3D>>.
[#sceneenable]
==== sceneEnable
[source,text]
----
sceneEnable(enabled)
----
Activa o desactiva la capa 3D. Los nodos, las mallas y los materiales se pueden construir antes de activarla y sobreviven a que se desactive; solo se deja de dibujar. Termina el script si se activa sin una GPU utilizable (véase <<scenes3d,Escenas 3D>>), así que un juego con una alternativa 2D pregunta antes a `singeCapabilities().SCENE`. En los dos casos, la pantalla se actualiza en el siguiente fotograma.
*Desde:* 3.00.
*Véase también:* <<scenesetbackground,sceneSetBackground>>, <<scenesetantialias,sceneSetAntialias>>
.Ejemplo
[source,lua]
----
-- Recurre al overlay simple cuando el 3D no está disponible.
has3d = singeCapabilities().SCENE
if has3d then
sceneEnable(true)
buildArena()
else
debugPrint("No 3D here; using the 2D attract mode.")
end
function onPause()
sceneEnable(false)
end
----
[#scenegetsize]
==== sceneGetSize
[source,text]
----
width, height = sceneGetSize()
----
El tamaño de la capa, que es la resolución del overlay: el espacio en el que trabajan `sceneProject` y `sceneUnproject`, y el tamaño al que se renderiza la escena. Se cambia con `overlaySetResolution`. Antes de que la capa tenga tamaño, los dos valores son `0`.
*Devuelve:* Dos enteros, el ancho y el alto en coordenadas del overlay.
*Desde:* 3.00.
*Véase también:* <<overlaysetresolution,overlaySetResolution>>, <<sceneunproject,sceneUnproject>>
.Ejemplo
[source,lua]
----
-- Un punto de mira en el centro de la vista, y lo que hay debajo.
function onOverlayUpdate()
local width, height = sceneGetSize()
local cx = width / 2
local cy = height / 2
local ox, oy, oz = sceneUnproject(cx, cy, 0)
local fx, fy, fz = sceneUnproject(cx, cy, 50)
target = physicsRaycast(ox, oy, oz, fx - ox, fy - oy, fz - oz)
fontPrint(cx - 4, cy - 8, "+")
return OVERLAY_UPDATED
end
----
[#scenegetstats]
==== sceneGetStats
[source,text]
----
total, drawn, calls, textureKB = sceneGetStats()
----
Las cifras del último fotograma de la cámara de la ventana: las mallas reunidas para dibujar, cuántas de ellas estaban dentro de la vista de la cámara, las llamadas de dibujo en que se convirtieron tras la instanciación y la memoria de texturas que ocupa la escena, en kilobytes. Para afinar: una escena con cientos de `drawn` pero un puñado de `calls` comparte bien sus mallas, y un `textureKB` que no cabe en la memoria de una Raspberry Pi pide texturas KTX2. Las vistas renderizadas no cuentan.
*Devuelve:* Cuatro enteros.
*Desde:* 3.00.
*Véase también:* <<sceneenable,sceneEnable>>, <<modelinstance,modelInstance>>, <<materialsettexture,materialSetTexture>>
.Ejemplo
[source,lua]
----
-- Un indicador de depuración en la esquina mientras se afina el nivel.
function onOverlayUpdate()
if showStats then
local total, drawn, calls, textureKB = sceneGetStats()
fontPrint(10, 10, string.format("%d meshes, %d in view, %d calls, %d MB textures", total, drawn, calls, math.floor(textureKB / 1024)))
end
return OVERLAY_UPDATED
end
----
[#scenemeasure]
==== sceneMeasure
[source,text]
----
sceneMeasure(on)
ms = sceneMeasure(false)
----
Cronometra la escena 3D. `sceneMeasure(true)` empieza: cada fotograma posterior
espera a que la GPU termine la escena y va sumando ese tiempo, así que lo que se
mide es el trabajo y no el ritmo de la pantalla. `sceneMeasure(false)` para y
devuelve los milisegundos que tardó de media un fotograma, o `nil` si no se
dibujó ningún fotograma. La espera cuesta tasa de fotogramas, así que cronometra
unos segundos, no una partida entera.
*Devuelve:* nada al empezar; un número o `nil` al parar.
*Desde:* 3.00.
*Véase también:* <<singesavetier,singeSaveTier>>, <<scenegetstats,sceneGetStats>>
.Ejemplo
[source,lua]
----
-- Dos segundos de la pantalla de título dicen si caben todos los efectos.
sceneMeasure(true)
-- ... dos segundos después:
local ms = sceneMeasure(false)
if ms and (ms > 8) then
sceneSetBloom(1.0, 0.0)
end
----
[#sceneprobegui]
==== sceneProbeGui
[source,text]
----
gui, x, y = sceneProbeGui(sx, sy)
----
Qué GUI hay en la superficie bajo un punto del overlay, y en qué lugar de ella: un rayo desde la cámara a través del punto, con la cámara del último fotograma renderizado, contra los triángulos de todos los nodos visibles cuyo material muestra una GUI (`materialSetGui`), y las coordenadas de textura del impacto más cercano dan el lugar en la GUI. Devuelve el handle de la GUI y el píxel en las coordenadas propias de esa GUI (`0, 0` en su esquina superior izquierda, `guiGetWidth`, `guiGetHeight` en la inferior derecha), o solo `nil` cuando el punto no cae en ninguna de esas superficies, la escena está desactivada o todavía no se ha renderizado nada. El motor se hace la misma pregunta cuando el ratón o una pistola de luz apuntan a la escena mientras una GUI que recibe entrada está sobre una superficie y no hay nada dibujado en plano bajo el puntero, así que los clics sobre un monitor de la escena no necesitan script; la llamada es para un juego que quiere la respuesta él mismo, para una mira láser, un efecto al pasar por encima o una prueba.
*Parámetros:*
* `sx, sy` -- el punto, en coordenadas del overlay (una posición del ratón o de la pistola de luz).
*Devuelve:* Un entero y dos enteros, o `nil`.
*Desde:* 3.00.
*Véase también:* <<materialsetgui,materialSetGui>>, <<guisetinput,guiSetInput>>, <<sceneproject,sceneProject>>, <<sceneunproject,sceneUnproject>>
.Ejemplo
[source,lua]
----
-- El punto de una pistola de luz brilla donde cae en la pantalla de la cabina.
function onMouseMoved(x, y)
local g, px, py = sceneProbeGui(x, y)
if g == screenGui then
guiSetValue(g, screenPage, "dot", string.format("%d,%d", px, py))
end
end
----
[#sceneproject]
==== sceneProject
[source,text]
----
x, y, depth, inFront = sceneProject(wx, wy, wz)
----
Dónde cae un punto del mundo en coordenadas del overlay, con la cámara del último fotograma renderizado. `depth` es `0` en el plano cercano y `1` en el plano lejano, para ordenar etiquetas o desvanecerlas con la distancia. `inFront` es `false` cuando el punto está detrás de la cámara, y entonces `x` e `y` no significan nada; antes de que se renderice el primer fotograma no hay nada contra lo que proyectar, y todos los valores vuelven como `0` con `inFront` a `false`. Los puntos que están delante pero fuera de la vista vuelven con coordenadas fuera de los bordes del overlay, así que compruébalas con `sceneGetSize` antes de dibujar. El uso habitual es una etiqueta con un nombre o una barra de vida dibujada con `fontPrint` o `spriteDraw` encima de un objeto 3D.
*Parámetros:*
* `wx, wy, wz` -- el punto del mundo, en unidades del mundo.
*Devuelve:* Dos números, un número y un booleano.
*Desde:* 3.00.
*Véase también:* <<sceneunproject,sceneUnproject>>, <<nodegetworldposition,nodeGetWorldPosition>>, <<scenegetsize,sceneGetSize>>
.Ejemplo
[source,lua]
----
-- Un nombre y una barra de vida flotando sobre cada personaje.
function onOverlayUpdate()
for _, enemy in ipairs(enemies) do
local x, y, depth, visible = sceneProject(nodeGetWorldPosition(enemy.node))
if visible then
fontPrint(x - 30, y - 80, enemy.name)
overlayBox(x - 30, y - 60, x - 30 + enemy.health * 0.6, y - 54)
end
end
return OVERLAY_UPDATED
end
----
[#sceneunproject]
==== sceneUnproject
[source,text]
----
x, y, z = sceneUnproject(sx, sy, distance)
----
El punto del mundo a `distance` unidades a lo largo del rayo que pasa por un píxel del overlay, con la cámara del último fotograma renderizado. Dos distancias dan un rayo para seleccionar con `physicsRaycast`; una coloca un objeto «donde hizo clic el jugador». Antes de que se dibuje el primer fotograma devuelve el origen.
*Parámetros:*
* `sx, sy` -- el píxel del overlay, en coordenadas del overlay.
* `distance` -- a qué distancia a lo largo del rayo, en unidades del mundo; `0` es el plano cercano.
*Devuelve:* Tres números, el punto del mundo.
*Desde:* 3.00.
*Véase también:* <<sceneproject,sceneProject>>, <<mousegetposition,mouseGetPosition>>, <<physicsraycast,physicsRaycast>>
.Ejemplo
[source,lua]
----
-- Pon un marcador en aquello donde hizo clic el jugador, o a 8 unidades si no hay nada.
function onMouseDown(button)
local mx, my = mouseGetPosition(0)
local ox, oy, oz = sceneUnproject(mx, my, 0)
local fx, fy, fz = sceneUnproject(mx, my, 100)
local hit, hx, hy, hz = physicsRaycast(ox, oy, oz, fx - ox, fy - oy, fz - oz)
if hit then
nodeSetPosition(marker, hx, hy, hz)
else
nodeSetPosition(marker, sceneUnproject(mx, my, 8))
end
end
----
[#scenesetambient]
==== sceneSetAmbient
[source,text]
----
sceneSetAmbient(r, g, b)
----
Luz que llega de todas partes, de `0` a `255` por canal, tratada como sRGB; por defecto es un gris tenue de `26, 26, 26`. Aclara los lados a los que no llega ninguna luz para que no queden negros, y `materialSetOcclusionMap` la oscurece en las grietas. Cuando hay un cielo y `sceneSetEnvironment` está activado, la luz del cielo sustituye a este valor.
*Desde:* 3.00.
*Véase también:* <<lightnew,lightNew>>, <<scenesetenvironment,sceneSetEnvironment>>, <<scenesetsky,sceneSetSky>>
.Ejemplo
[source,lua]
----
-- Un patio a la luz de la luna: un azul tenue por todas partes, una luna direccional fría.
sceneSetAmbient(20, 25, 45)
moon = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(moon, -5, 10, 3)
nodeLookAt(moon, 0, 0, 0)
lightSetColor(moon, 170, 190, 255)
lightSetIntensity(moon, 0.4)
----
[#scenesetantialias]
==== sceneSetAntialias
[source,text]
----
sceneSetAntialias(antialias)
----
Activa o desactiva el multimuestreo 4x; está activado por defecto allí donde la GPU lo ofrece para los formatos de la capa, y falta sin aviso donde no. Desactivarlo gana velocidad en una Raspberry Pi. Los destinos de renderizado se reconstruyen en el acto, así que llámala durante la preparación y no en cada fotograma.
*Desde:* 3.00.
*Véase también:* <<sceneenable,sceneEnable>>, <<scenesetshadowsize,sceneSetShadowSize>>
.Ejemplo
[source,lua]
----
-- Un ajuste de calidad de la pantalla de opciones del juego.
function applyQuality(level)
if level == "low" then
sceneSetAntialias(false)
sceneSetShadowSize(512)
sceneSetShadowCascades(2)
else
sceneSetAntialias(true)
sceneSetShadowSize(2048)
sceneSetShadowCascades(3)
end
end
----
[#scenesetbackground]
==== sceneSetBackground
[source,text]
----
sceneSetBackground(r, g, b)
sceneSetBackground(r, g, b, a)
----
El color con el que se borra la capa en cada fotograma, de `0` a `255` por canal. `a` es `255` cuando se omite, un fondo opaco; por debajo de `255` deja ver el vídeo, y `0` (lo habitual en un juego con disco) hace el fondo totalmente transparente. Hasta que se llama, la capa se borra a negro transparente. Un cielo de `sceneSetSky` se dibuja sobre el fondo, así que el vídeo deja de verse mientras haya uno.
*Parámetros:*
* `r, g, b` -- el color de borrado.
* `a` -- el alfa de borrado; `255` cuando se omite.
*Desde:* 3.00.
*Véase también:* <<sceneenable,sceneEnable>>, <<scenesetsky,sceneSetSky>>, <<scenesetfog,sceneSetFog>>
.Ejemplo
[source,lua]
----
-- El disco se ve detrás de la cabina 3D; un cielo nocturno sólido para el menú.
function showCockpit()
sceneSetBackground(0, 0, 0, 0)
discPlay()
end
function showMenu()
discPause()
sceneSetBackground(10, 12, 30)
end
----
[#scenesetbloom]
==== sceneSetBloom
[source,text]
----
sceneSetBloom(threshold, strength)
----
Un resplandor alrededor de todo lo que sea más brillante que `threshold`, tomado de la luz lineal de la escena antes de la exposición y de la curva tonal (donde `1` es el blanco), y que se vuelve a sumar con la intensidad `strength`. Por defecto, el umbral es `1` y la intensidad `0`, sin bloom; de `0.2` a `0.4` es sutil, `1` es una neblina. El resplandor se difumina en una cadena de pasadas a mitad de tamaño, así que cuesta poco, y solo lo recibe la cámara de la ventana: las vistas renderizadas se lo saltan. Los valores negativos se limitan a `0`.
*Parámetros:*
* `threshold` -- el brillo lineal por encima del cual brilla un píxel; `1` es el blanco antes de la curva tonal.
* `strength` -- cuánto resplandor se vuelve a sumar; `0` para nada.
*Desde:* 3.00.
*Véase también:* <<scenesetexposure,sceneSetExposure>>, <<materialsetemissive,materialSetEmissive>>, <<scenesettonemap,sceneSetTonemap>>
.Ejemplo
[source,lua]
----
-- El neón y los faros brillan; el asfalto no.
sceneSetBloom(1.0, 0.35)
neonLook = materialNew()
materialSetColor(neonLook, 255, 40, 120)
materialSetEmissive(neonLook, 255, 40, 120)
nodeSetMesh(sign, meshBox(2, 0.3, 0.05), neonLook)
----
[#scenesetenvironment]
==== sceneSetEnvironment
[source,text]
----
sceneSetEnvironment(lit)
----
Si el cielo fijado con `sceneSetSky` ilumina la escena: su luz difusa desde todas las direcciones en lugar de `sceneSetAmbient`, y sus reflejos en los metales y las superficies brillantes, nítidos en un espejo y difuminados a medida que aumenta la rugosidad. Activado por defecto, así que un cielo ilumina la escena en cuanto se fija; `false` deja el cielo solo como telón de fondo y vuelve la luz ambiente plana. No tiene efecto sin un cielo.
*Desde:* 3.00.
*Véase también:* <<scenesetsky,sceneSetSky>>, <<scenesetambient,sceneSetAmbient>>, <<scenesetskyintensity,sceneSetSkyIntensity>>
.Ejemplo
[source,lua]
----
-- Un telón de fondo pintado que no debe teñir el cromado.
sceneSetSky(DIR .. "Skies/matteBackdrop.png")
sceneSetEnvironment(false)
sceneSetAmbient(40, 40, 45)
----
[#scenesetexposure]
==== sceneSetExposure
[source,text]
----
sceneSetExposure(stops)
----
Aclara u oscurece toda la escena en pasos fotográficos, de `-10` a `10` (los valores fuera de ese rango se limitan): `+1` duplica la luz, `-1` la reduce a la mitad. Por defecto es `0`. Se aplica después del bloom y antes de la curva tonal, así que una escena brillante a `+2` se comprime hacia el blanco en vez de recortarse, y una cueva y una playa pueden compartir las mismas luces. Anímala para un ajuste lento al pasar de un interior a la luz del día.
*Desde:* 3.00.
*Véase también:* <<scenesettonemap,sceneSetTonemap>>, <<lightsetintensity,lightSetIntensity>>, <<scenesetskyintensity,sceneSetSkyIntensity>>
.Ejemplo
[source,lua]
----
-- Los ojos se adaptan: acerca poco a poco la exposición al objetivo de la zona en cada fotograma.
targetStops = 0
currentStops = 0
function onEnterArea(area)
targetStops = area.indoors and 1.5 or -0.5
end
function onOverlayUpdate()
currentStops = currentStops + (targetStops - currentStops) * 0.05
sceneSetExposure(currentStops)
return OVERLAY_UPDATED
end
----
[#scenesetfog]
==== sceneSetFog
[source,text]
----
sceneSetFog(r, g, b, near, far)
sceneSetFog()
----
Niebla por distancia: despejado hasta `near` unidades de la cámara, del todo del color de la niebla a partir de `far`, tanto en las mallas (también las que no tienen iluminación) como en las partículas; las partículas aditivas se desvanecen en vez de volverse grises. Sin argumentos, o con un `far` que no sea mayor que `near`, no hay niebla. El cielo no recibe niebla, así que elige un color parecido al de su horizonte, o al del fondo cuando no hay cielo. Cualquier otro número de argumentos termina el script.
*Parámetros:*
* `r, g, b` -- el color de la niebla, de `0` a `255`.
* `near` -- la distancia a la que empieza la niebla, en unidades del mundo desde la cámara.
* `far` -- la distancia a la que no queda más que niebla.
*Desde:* 3.00.
*Véase también:* <<scenesetsky,sceneSetSky>>, <<scenesetbackground,sceneSetBackground>>, <<camerasetperspective,cameraSetPerspective>>
.Ejemplo
[source,lua]
----
-- Bruma matinal sobre las colinas que se disipa a medida que avanza el nivel.
sceneSetSky(DIR .. "Skies/dawn.hdr")
sceneSetFog(190, 200, 215, 40, 160)
function onOverlayUpdate()
if levelTime > 60 then
sceneSetFog()
end
return OVERLAY_UPDATED
end
----
[#scenesetshadowcascades]
==== sceneSetShadowCascades
[source,text]
----
sceneSetShadowCascades(count)
----
En cuántos mapas se divide la sombra de una luz direccional a lo largo de la vista, de `1` a `4` (los valores fuera de ese rango se limitan; por defecto es `3`). Cada cascada cubre un tramo de la vista de la cámara hasta `sceneSetShadowDistance`, el tramo más cercano el más pequeño, así que las sombras cerca de la cámara son nítidas por grande que sea el nivel. `1` ajusta un solo mapa a toda la escena, lo adecuado para escenas pequeñas, y es lo que se usa siempre con una cámara ortográfica. Cada cascada es una pasada de sombras más por cada luz direccional que proyecta sombras; `2` es una elección razonable en una Raspberry Pi.
*Desde:* 3.00.
*Véase también:* <<scenesetshadowdistance,sceneSetShadowDistance>>, <<scenesetshadowsize,sceneSetShadowSize>>, <<lightsetshadow,lightSetShadow>>
.Ejemplo
[source,lua]
----
-- Un nivel exterior grande: sombras nítidas cerca del jugador, que llegan lejos.
sun = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(sun, 30, 40, 20)
nodeLookAt(sun, 0, 0, 0)
lightSetShadow(sun, true)
sceneSetShadowCascades(4)
sceneSetShadowDistance(120)
sceneSetShadowSize(2048)
----
[#scenesetshadowdistance]
==== sceneSetShadowDistance
[source,text]
----
sceneSetShadowDistance(distance)
----
Hasta dónde llegan desde la cámara las sombras en cascada, en unidades del mundo; por defecto es `60` y los valores por debajo de `1` se limitan a `1`. Más allá, una luz direccional no proyecta sombra. Las distancias más cortas hacen más nítida cada cascada, ya que los mismos mapas cubren menos terreno, así que fíjala más o menos hasta donde el jugador alcanza a distinguir una sombra. Solo importa con más de una cascada bajo una cámara en perspectiva.
*Desde:* 3.00.
*Véase también:* <<scenesetshadowcascades,sceneSetShadowCascades>>, <<scenesetshadowsize,sceneSetShadowSize>>
.Ejemplo
[source,lua]
----
-- Las sombras solo tienen que llegar hasta la pared del fondo de la arena.
sceneSetShadowCascades(2)
sceneSetShadowDistance(35)
sun = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(sun, 10, 20, 8)
nodeLookAt(sun, 0, 0, 0)
lightSetShadow(sun, true)
----
[#scenesetshadowsize]
==== sceneSetShadowSize
[source,text]
----
sceneSetShadowSize(size)
----
Texels por lado de cada mapa de sombras, cascada y cara de cubo, de `256` a `4096` (los valores fuera de ese rango se limitan; por defecto es `1024`). Cuanto más grande, más nítido y más lento, en memoria además de en tiempo, ya que cada luz que proyecta sombras tiene sus propios mapas de este tamaño. `2048` va bien en un ordenador de sobremesa, `512` en una Raspberry Pi. Los mapas se reconstruyen en el siguiente fotograma.
*Desde:* 3.00.
*Véase también:* <<lightsetshadow,lightSetShadow>>, <<scenesetshadowcascades,sceneSetShadowCascades>>, <<scenesetantialias,sceneSetAntialias>>
.Ejemplo
[source,lua]
----
-- Ajusta el tamaño de los mapas de sombras a la máquina.
if singeGetPlatform() == "raspberrypi" then
sceneSetShadowSize(512)
else
sceneSetShadowSize(2048)
end
----
[#scenesetsky]
==== sceneSetSky
[source,text]
----
sceneSetSky(file)
sceneSetSky(nil)
sceneSetSky()
----
Envuelve la escena en una panorámica equirrectangular: un `.hdr` de Radiance (como los que distribuyen los sitios de HDRI, conservando su rango), o un PNG, un JPEG u otra imagen que lea SDL_image, decodificada desde sRGB. El nombre es relativo al juego, como en `spriteLoad`. El cielo se dibuja detrás de todo en lugar del color de fondo (así que el vídeo deja de verse) e ilumina la escena salvo con `sceneSetEnvironment(false)`. Su centro mira hacia -Z y su parte superior hacia arriba. Se remuestrea en un cubo de hasta 1024 texels por cara, así que una imagen de 2048 por 1024 es más que suficiente y las más grandes tardan un momento en prepararse en una Raspberry Pi. Sin argumento, o con `nil`, se quita el cielo. Termina el script cuando el archivo no se puede leer o el 3D no está disponible.
*Desde:* 3.00.
*Véase también:* <<scenesetskyintensity,sceneSetSkyIntensity>>, <<scenesetenvironment,sceneSetEnvironment>>, <<scenesetfog,sceneSetFog>>, <<scenesetbackground,sceneSetBackground>>
.Ejemplo
[source,lua]
----
-- Un cielo nublado para el nivel del puerto, con la niebla a juego con su horizonte.
sceneEnable(true)
sceneSetSky(DIR .. "Skies/overcastHarbor.hdr")
sceneSetSkyIntensity(0.8)
sceneSetFog(190, 200, 215, 25, 90)
function onLevelEnd()
sceneSetSky()
sceneSetBackground(0, 0, 0, 0)
end
----
[#scenesetskyintensity]
==== sceneSetSkyIntensity
[source,text]
----
sceneSetSkyIntensity(scale)
----
El brillo del cielo y de la luz que proyecta, `1` tal como se cargó; los valores negativos se limitan a `0`. Sirve para atenuar un cielo PNG demasiado brillante, para hacer de un cielo `.hdr` la luz principal de la escena o para hacer aparecer y desaparecer el cielo poco a poco. Multiplica el propio cielo, a diferencia de `sceneSetExposure`, que lo escala todo.
*Desde:* 3.00.
*Véase también:* <<scenesetsky,sceneSetSky>>, <<scenesetexposure,sceneSetExposure>>, <<scenesetenvironment,sceneSetEnvironment>>
.Ejemplo
[source,lua]
----
-- Anochecer: el cielo y su luz se atenúan a lo largo de diez segundos.
sceneSetSky(DIR .. "Skies/sunset.hdr")
duskStart = singeGetTicks()
function onOverlayUpdate()
local t = math.min((singeGetTicks() - duskStart) / 10000, 1)
sceneSetSkyIntensity(1 - 0.8 * t)
return OVERLAY_UPDATED
end
----
[#scenesettonemap]
==== sceneSetTonemap
[source,text]
----
sceneSetTonemap(curve)
----
La curva que lleva la luz lineal de la escena a la pantalla, aplicada después de la exposición. `TONEMAP_NEUTRAL` (por defecto) mantiene fieles los colores y comprime solo la luz más brillante; `TONEMAP_ACES` es la curva cinematográfica, con más contraste y una caída más cálida hacia el blanco; `TONEMAP_NONE` recorta en el blanco. Los materiales de vídeo, las superficies sin iluminación y el cielo pasan por la misma curva, así que un juego cuyo disco debe verse exactamente como se filmó necesita `TONEMAP_NONE` con una exposición de `0`. Cualquier otro valor termina el script.
*Desde:* 3.00.
*Véase también:* <<scenesetexposure,sceneSetExposure>>, <<scenesetbloom,sceneSetBloom>>
.Ejemplo
[source,lua]
----
-- Un aspecto cinematográfico para la cinemática, color fiel para jugar.
function onCutsceneStart()
sceneSetTonemap(TONEMAP_ACES)
sceneSetExposure(0.5)
end
function onCutsceneEnd()
sceneSetTonemap(TONEMAP_NEUTRAL)
sceneSetExposure(0)
end
----
[#script]
=== Script
WARNING: Estas funciones permiten que un script de Singe ceda el control a otro script de Singe: el mecanismo que hay detrás del lanzador integrado `Menu.singe`. Los desarrolladores de juegos no deberían recurrir a ellas; construye tu juego como un único script y deja que el sistema de menús se encargue del encadenamiento. Se documentan aquí para que la referencia esté completa y para quien mantenga el propio menú.
Las dos llamadas reciben una tabla con la forma de una entrada de `GAMES[]` en `games.dat` y la ponen en la cola del ejecutor de scripts del motor. La configuración nueva parte de la que está en marcha, así que las opciones de línea de comandos como la carpeta de datos y la traza se mantienen, pero el disco, el vídeo y el contenedor nunca: cada entrada trae los suyos. El script actual termina al final del fotograma, se dispara `onShutdown` y el script de la cola empieza con un estado de Lua nuevo (véase <<enginecallbacks,Callbacks del motor>>). La carpeta de datos del script nuevo sigue la misma regla que un arranque desde la línea de comandos (véase `singeGetDataPath`); cuando no se puede crear, se aborta el script que hizo la llamada.
[#scriptexecute]
==== scriptExecute
[source,text]
----
scriptExecute(config)
----
Sustituye el script en marcha por el que describe la tabla. El script actual se detiene al final de este fotograma y se ejecuta `onShutdown`; el control nunca vuelve a él, y cuando el script nuevo sale, el motor pasa a lo siguiente que haya en la cola o termina. Las claves de cadena que conoce el motor son `SCRIPT`, `CONTAINER`, `VIDEO`, `STRETCH`, `NO_MOUSE`, `RESOLUTION_X`, `RESOLUTION_Y`, `SINDEN_GUN`, `AUDIO_TRACK`, `AUDIO_DELAY`, `LEGACY_SPRITE_ARGS`, `CANVAS_X` y `CANVAS_Y`; las demás claves se ignoran y las que no son cadenas se saltan. Un `SCRIPT`, `CONTAINER` o `VIDEO` que no es una cadena aborta el script, y lo mismo hace un argumento que no es una tabla.
*Parámetros:*
* `config` -- una tabla de campos de `games.dat`, como mínimo `SCRIPT` o `CONTAINER`.
*Desde:* 2.00
*Véase también:* <<scriptpush,scriptPush>>, <<singequit,singeQuit>>
.Ejemplo
[source,lua]
----
-- Un lanzador que nunca vuelve a sí mismo.
function onInputPressed(what)
if what == SWITCH_START1 then
scriptExecute({
SCRIPT = DIR .. "../MyGame/MyGame.singe",
VIDEO = DIR .. "../MyGame/MyGame.mp4",
})
end
end
----
[#scriptpush]
==== scriptPush
[source,text]
----
scriptPush(config)
----
Arranca el script que describe la tabla y pone la configuración actual en la cola detrás de él, así que cuando el script nuevo sale, el actual vuelve a ejecutarse desde el principio. No se conserva nada: el `onShutdown` del script actual se ejecuta ahora, su estado de Lua se descarta y la ejecución posterior empieza de cero, y por eso `Menu.singe` guarda su selección en un archivo antes de apilar el otro. La tabla acepta las mismas claves que `scriptExecute`.
*Parámetros:*
* `config` -- una tabla de campos de `games.dat`, como para `scriptExecute`.
*Desde:* 2.00
*Véase también:* <<scriptexecute,scriptExecute>>, <<singequit,singeQuit>>, <<onshutdown,onShutdown>>
.Ejemplo
[source,lua]
----
-- Menu.singe: arranca el juego seleccionado y vuelve después.
function onInputPressed(what)
if what == SWITCH_START1 or what == SWITCH_BUTTON1 then
SHUTDOWN_FROM_PUSH = true
scriptPush(GAME_LIST[GAME_SELECTED])
end
end
----
[#singe]
=== Singe
Las funciones del espacio de nombres `singe*` controlan el propio motor: la ventana, las capturas de pantalla, el sistema de pausa, salir y recargar, el retardo de audio que se usa para la sincronización labial, y unas cuantas rutas e indicadores que vienen de la línea de comandos y de `games.dat`. Nada de lo que hay aquí recibe un handle. El comportamiento de la pausa se describe en <<pausing,Pausa>> y los valores del retardo de audio en <<audiosync,Sincronización del audio>>.
[#allowsocketcall]
==== allowSocketCall
[source,text]
----
allowed = allowSocketCall()
----
Extensión de Hypseus, siempre `false` en Singe. Hypseus responde si el usuario le permitió enviar datos a un servidor de estadísticas, algo que su `-noserversend` desactiva; allí el propio envío está comentado. Singe no envía nada a ninguna parte, así que no hay nada permitido y la respuesta nunca cambia. Existe para que un juego que lo pregunte no se detenga por una función que falta.
*Devuelve:* un booleano, siempre `false`.
*Desde:* 3.00
[#getuserstring]
==== getUserString
[source,text]
----
id = getUserString()
----
Extensión de Hypseus, siempre una cadena vacía. A pesar del nombre, no es una cadena que haya fijado el usuario: Hypseus responde con el ID de instalación de la máquina que envía con las estadísticas de red, tomado del `MachineGuid` de Windows o del identificador de máquina de Linux. Singe no envía nada a ninguna parte y no le dará a un juego una huella de la máquina en la que se ejecuta.
*Devuelve:* una cadena, siempre vacía.
*Desde:* 3.00
*Véase también:* <<allowsocketcall,allowSocketCall>>
[#singedisablepausekey]
==== singeDisablePauseKey
[source,text]
----
singeDisablePauseKey()
----
Cede la tecla de pausa al script, exactamente como hace `singeSetPauseKeyEnabled(false)`. Es una función definida en `Framework.singe`, que se conserva para que los juegos antiguos funcionen sin cambios.
*Notas:* Alias heredado.
WARNING: El código nuevo debería llamar a <<singesetpausekeyenabled,singeSetPauseKeyEnabled>> con `false`.
*Desde:* 1.18 (RDG)
*Véase también:* <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>, <<singeenablepausekey,singeEnablePauseKey>>
.Ejemplo
[source,lua]
----
-- Código de un juego antiguo; la forma moderna es singeSetPauseKeyEnabled(false).
singeDisablePauseKey()
function onInputPressed(what)
if what == SWITCH_PAUSE then
togglePauseMenu()
end
end
----
[#singeenablepausekey]
==== singeEnablePauseKey
[source,text]
----
singeEnablePauseKey()
----
Devuelve la tecla de pausa al motor, exactamente como hace `singeSetPauseKeyEnabled(true)`. Es una función definida en `Framework.singe`, que se conserva para que los juegos antiguos funcionen sin cambios.
*Notas:* Alias heredado.
WARNING: El código nuevo debería llamar a <<singesetpausekeyenabled,singeSetPauseKeyEnabled>> con `true`.
*Desde:* 1.18 (RDG)
*Véase también:* <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>, <<singedisablepausekey,singeDisablePauseKey>>
.Ejemplo
[source,lua]
----
-- Código de un juego antiguo; la forma moderna es singeSetPauseKeyEnabled(true).
function pauseMenuClose()
singeSetPauseFlag(false)
singeEnablePauseKey()
end
----
[#utilsha256]
==== utilSha256
[source,text]
----
digest = utilSha256(data)
----
El SHA-256 de una cadena, como 64 caracteres hexadecimales en minúscula. Admite datos binarios, así que
funciona con los bytes de un archivo igual de bien que con texto.
Sirve para decidir si los bytes que tienes son los que te prometieron: un juego descargado frente
al hash que lleva su entrada del catálogo, o la clave pública de un servidor frente a un pin. El
módulo `md5` incluido sigue ahí para una suma de comprobación, pero un hash con el que se decide si
*fiarse* de algo tiene que ser uno del que todavía valga la pena fiarse.
*Parámetros:*
* `data` -- una cadena de bytes. Si está vacía, aborta el script.
*Devuelve:* cadena, 64 caracteres hexadecimales en minúscula.
*Desde:* 3.00.
.Ejemplo
[source,lua]
----
-- Rechaza una descarga que no ha llegado intacta.
local file = io.open("game.part", "rb")
local bytes = file:read("*a")
file:close()
if utilSha256(bytes) ~= expected then
os.remove("game.part")
end
----
[#singecapabilities]
==== singeCapabilities
[source,text]
----
capabilities = singeCapabilities()
----
Lo que esta máquina puede ejecutar. La tabla tiene una entrada por cada nombre de
capacidad que puede usar una lista `REQUIRES` de `games.dat` (`SCENE`, `GUI`,
`SHADOWS`, `POST`, `SKIN`, `MORPH`), cada una `true` o `false`, y `tier`: la clase
de rendimiento medida de la máquina, de 0 (sin 3D) a 3 (una GPU de sobremesa), o
`nil` antes de que el menú la haya medido. Un juego pregunta para poder prescindir
de algo en vez de ser rechazado por ello.
*Devuelve:* una tabla.
*Desde:* 3.00.
*Véase también:* <<singehasgpu,singeHasGpu>>, <<singesavetier,singeSaveTier>>, <<scenemeasure,sceneMeasure>>
.Ejemplo
[source,lua]
----
-- Sombras donde la máquina las tiene, un sol sencillo donde no.
local machine = singeCapabilities()
lightSetShadow(sun, machine.SHADOWS)
if (machine.tier or 0) < 2 then
sceneSetBloom(1.0, 0.0)
end
----
[#singegetaudiocalibration]
==== singeGetAudioCalibration
[source,text]
----
milliseconds = singeGetAudioCalibration()
----
Devuelve el retardo de audio de la máquina que está en vigor, en milisegundos: el valor que la pantalla de calibración del menú guardó en `machine.cfg` en la raíz de datos, o uno fijado después con `singeSetAudioCalibration`. Cero cuando la máquina nunca se ha calibrado. Se aplica a todos los juegos de la máquina, además del retardo propio de cada juego de `singeGetAudioDelay`.
*Devuelve:* milisegundos, entero.
*Desde:* 3.00
*Véase también:* <<singesetaudiocalibration,singeSetAudioCalibration>>, <<singegetaudiodelay,singeGetAudioDelay>>, <<singegetaudiolatency,singeGetAudioLatency>>
.Ejemplo
[source,lua]
----
-- Menu.singe: empieza la pantalla de calibración desde el valor guardado.
function calibrationBegin()
CALIBRATING = true
CAL_ORIGINAL = singeGetAudioCalibration()
CAL_VALUE = CAL_ORIGINAL
CAL_LATENCY = singeGetAudioLatency()
CAL_BEAT = singeGetTicks() - CAL_PERIOD
end
----
[#singegetaudiodelay]
==== singeGetAudioDelay
[source,text]
----
milliseconds = singeGetAudioDelay()
----
Devuelve el retardo de audio propio del juego que está en vigor, en milisegundos, venga de `--audiodelay`, de la clave `AUDIO_DELAY` de la entrada del juego en `games.dat` o de `singeSetAudioDelay`. Cero cuando no se ha fijado ninguno. Se suma a la calibración de la máquina y a la latencia medida del dispositivo cuando el motor decide cuándo mostrar cada fotograma de vídeo.
*Devuelve:* milisegundos, entero.
*Desde:* 3.00
*Véase también:* <<singesetaudiodelay,singeSetAudioDelay>>, <<singegetaudiocalibration,singeGetAudioCalibration>>
.Ejemplo
[source,lua]
----
-- Menú de servicio: muestra el desfase de sincronización actual junto a su control deslizante.
function drawSyncSetting(y)
local delay = singeGetAudioDelay()
fontPrint(40, y, string.format("A/V offset: %+d ms", delay))
end
----
[#singegetaudiolatency]
==== singeGetAudioLatency
[source,text]
----
milliseconds = singeGetAudioLatency()
----
Devuelve la cola del dispositivo de audio que midió el motor al abrir el mezclador, en milisegundos: el tiempo que pasa entre entregar las muestras al mezclador y que lleguen al dispositivo, antes de cualquier calibración o retardo propio del juego. El motor ya lo compensa; una pantalla de calibración lo necesita para programar su clic y su destello, y vale la pena mostrarlo en una vista de diagnóstico.
*Devuelve:* milisegundos, entero.
*Desde:* 3.00
*Véase también:* <<singegetaudiocalibration,singeGetAudioCalibration>>, <<singegetaudiodelay,singeGetAudioDelay>>
.Ejemplo
[source,lua]
----
-- Menu.singe: el destello sigue al clic tras la cola del dispositivo más el valor candidato.
function calibrationUpdate()
local now = singeGetTicks()
local gap = CAL_LATENCY + CAL_VALUE
if now - CAL_BEAT >= CAL_PERIOD then
CAL_BEAT = now
CAL_CLICK_AT = now + math.max(0, -gap)
CAL_FLASH_AT = now + math.max(0, gap)
end
end
----
[#singegetdatapath]
==== singeGetDataPath
[source,text]
----
path = singeGetDataPath()
----
Devuelve la carpeta en la que Singe escribe para este juego, con un separador de ruta al final para que se pueda añadir directamente un nombre de archivo. Es una subcarpeta de la base de `--datadir` con el nombre del juego (para un juego empaquetado, el nombre de la base de datos y, dentro de ella, la carpeta del script); sin `--datadir`, la base es `data` en la carpeta de juegos. Allí van las capturas de pantalla, los índices de vídeo y `trace.txt`, y allí deberían ir también las partidas guardadas, los récords y los ajustes; Singe 2.x los escribía junto al juego, que puede ser de solo lectura.
*Devuelve:* ruta, cadena, terminada en un separador.
*Desde:* 2.00
*Véase también:* <<singegetscriptpath,singeGetScriptPath>>, <<singescreenshot,singeScreenshot>>
.Ejemplo
[source,lua]
----
-- Menu.singe conserva su última selección entre ejecuciones.
CONFIG_FILE = singeGetDataPath() .. "menu.dat"
function saveConfig()
local cfg = io.open(CONFIG_FILE, "w")
if cfg then
cfg:write("GAME_SELECTED = " .. GAME_SELECTED .. "\n")
cfg:close()
else
debugPrint("Unable to write " .. CONFIG_FILE)
end
end
----
[#singegetgameid]
==== singeGetGameId
[source,text]
----
id = singeGetGameId()
----
El `GAME_ID` de la entrada de este juego en `games.dat`, o `nil` cuando no tiene. Es el id con el
que el servicio en línea conoce el juego, y la clave bajo la que se guarda una tabla de récords.
**El formato es un UUID**, en la forma canónica de 36 caracteres -- ocho dígitos hexadecimales,
tres grupos de cuatro y luego doce, separados por guiones:
----
GAME_ID = "6f1e7b62-0a4e-4d9c-9b2f-1c7a5e3d8a10"
----
No se acepta nada más. El id es un segmento de ruta en las URL del servicio, así que uno que
contuviera una barra o un signo de interrogación se publicaría y luego nunca se podría alcanzar, y
dos ids que solo se diferenciaran en mayúsculas y minúsculas serían dos juegos para la base de datos
y uno solo para cualquiera que los leyera. La comparación no distingue mayúsculas de minúsculas y se
almacena en minúsculas, así que un id escrito en mayúsculas es el mismo juego.
Vale cualquier UUID -- genéralo como quieras, o deja que `publish.lua` imprima uno. Un juego sin
él se puede jugar, pero no puede entrar en una clasificación: no habría nada estable con lo que
identificar una tabla de récords, y un id inventado en tiempo de ejecución crearía una tabla
distinta cada vez que se movieran los archivos.
**Un `.game`, un id.** Un `.game` cuyo `games.dat` tiene varias entradas -- una colección, o un
juego en dos idiomas -- es una sola descarga, y todas sus entradas llevan el mismo `GAME_ID`. El
servicio en línea muestra cada entrada como un juego propio, y no acepta un `.game` cuyas entradas
discrepen sobre su id o en el que alguna no lo lleve. Aun así, sus puntuaciones se mantienen
separadas: véase <<onlinescores,Récords en línea>>.
*Devuelve:* cadena, o `nil`.
*Desde:* 3.00.
*Véase también:* <<onlinescores,Récords en línea>>, <<gamesdat,games.dat>>
.Ejemplo
[source,lua]
----
-- Ofrece la tabla de récords solo cuando el juego tiene una identidad con la que clasificarse.
if singeGetGameId() then
showLeaderboardButton()
end
----
[#singegetheight]
==== singeGetHeight
[source,text]
----
height = singeGetHeight()
----
Devuelve la altura de la ventana de Singe en píxeles de pantalla, no en unidades del overlay, tal como es la ventana en el momento de la llamada. Para dibujar, usa `overlayGetHeight`; esto es para una pantalla de ajustes que informe sobre la pantalla, o para decidir cuánto detalle se puede permitir una escena.
*Devuelve:* píxeles, entero.
*Desde:* 1.x
*Véase también:* <<singegetwidth,singeGetWidth>>, <<overlaygetheight,overlayGetHeight>>
.Ejemplo
[source,lua]
----
-- Informa sobre la pantalla en el menú de servicio.
function drawDisplayInfo(y)
fontPrint(40, y, string.format("Window %dx%d, overlay %dx%d", singeGetWidth(), singeGetHeight(), overlayGetWidth(), overlayGetHeight()))
end
----
[#singegetpauseflag]
==== singeGetPauseFlag
[source,text]
----
paused = singeGetPauseFlag()
----
Devuelve el indicador de pausa del motor: `true` mientras el juego está en pausa por la tecla de pausa o por `singeSetPauseFlag`. Es algo distinto de si el disco está detenido en un fotograma en pausa (`discGetState`); léelo para saber si se pidió una pausa a propósito. Mientras la tecla de pausa mantiene congelado el juego no se ejecuta ningún callback, así que desde dentro de un callback el indicador solo puede verse como `true` después del propio `singeSetPauseFlag(true)` del script.
*Devuelve:* booleano.
*Desde:* 1.x (RDG)
*Véase también:* <<singesetpauseflag,singeSetPauseFlag>>, <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>
.Ejemplo
[source,lua]
----
-- Los temporizadores se quedan quietos mientras dura la pausa propia del juego.
function onOverlayUpdate()
if not singeGetPauseFlag() then
elapsed = elapsed + 1
updateEnemies()
end
drawFrame()
return OVERLAY_UPDATED
end
----
[#singegetscriptpath]
==== singeGetScriptPath
[source,text]
----
path = singeGetScriptPath()
----
Devuelve la ruta del archivo del script en marcha tal como la resolvió el motor. `Framework.singe` obtiene de este valor la global `DIR`, la carpeta del script con su separador al final, y `DIR` es lo que usan la mayoría de los juegos para construir las rutas de sus recursos.
*Devuelve:* ruta, cadena.
*Desde:* 1.15 (RDG)
*Véase también:* <<singegetdatapath,singeGetDataPath>>
.Ejemplo
[source,lua]
----
-- Framework.singe obtiene DIR así; luego los juegos cargan de forma relativa a ella.
DIR = singeGetScriptPath():match("(.*[/\\])") or "./"
titleSprite = spriteLoad(DIR .. "images/title.png")
----
[#singegetsysteminfo]
==== singeGetSystemInfo
[source,text]
----
info = singeGetSystemInfo()
----
Todo lo que sabe la cabecera de la traza sobre esta máquina y esta ejecución, en
una tabla. Está pensada para una pantalla de servicio y para los informes de
errores: la respuesta a «¿en qué se estaba ejecutando?» sin pedirle al jugador
que busque un registro.
*Devuelve:* una tabla con estos campos, todos cadenas salvo los cuatro tamaños:
* `version` -- la versión del motor, como `SINGE_VERSION_STRING`.
* `os` -- el nombre y la versión del sistema operativo, y la arquitectura.
* `cpu` -- el procesador, su número de núcleos y la memoria en megabytes.
* `renderer` -- el nombre del renderizador 2D de SDL que se está usando.
* `gpu` -- el controlador de SDL_GPU que hay detrás de la escena 3D, o una nota
de que el 3D no está disponible.
* `decoder` -- qué decodificadores de vídeo por hardware probará esta compilación, en palabras.
* `audio` -- los formatos de audio incluidos en la compilación, separados por comas.
* `soundFont` -- la SoundFont que usa la reproducción MIDI, o por qué no hay ninguna.
* `midi` -- los puertos MIDI abiertos, o una nota de que no se ha pedido ninguno.
* `dataPath` -- la carpeta de datos de este juego.
* `dataRoot` -- la carpeta de datos que se le dio al motor, un nivel por encima de
`dataPath`, dentro de la cual escriben todos los juegos de la máquina.
* `platform` -- la parte de plataforma del nombre de la versión publicada, como
`Linux-x86_64`: qué versión publicada es este ejecutable, y la que hay que pedir al servicio.
* `executable` -- la ruta absoluta del ejecutable en marcha.
* `windowWidth`, `windowHeight` -- enteros, la ventana en píxeles.
* `canvasWidth`, `canvasHeight` -- enteros, el lienzo en píxeles.
*Desde:* 3.00.
*Véase también:* <<singegetdatapath,singeGetDataPath>>, xref:Manual.adoc#servicetools[Las herramientas de servicio]
.Ejemplo
[source,lua]
----
-- Una pantalla de diagnóstico a una sola tecla.
local info = singeGetSystemInfo()
fontPrint(10, 10, "Singe " .. info.version .. " on " .. info.os)
fontPrint(10, 30, info.cpu)
fontPrint(10, 50, info.renderer .. " / " .. info.gpu)
----
[#singegetticks]
==== singeGetTicks
[source,text]
----
milliseconds = singeGetTicks()
----
Devuelve el reloj en milisegundos desde que arrancó el motor. Es el reloj para temporizadores, antirrebotes y animaciones, y sigue corriendo mientras la tecla de pausa tiene el juego congelado, así que un temporizador que no deba contar el tiempo en pausa debería leer `singeGetPauseFlag` o medir desde el momento en que se descongela. El `os.clock()` de Lua mide normalmente el tiempo de procesador, que se detiene mientras el motor está inactivo; Singe lo sustituye por los segundos de reloj desde que arrancó el motor, así que los juegos existentes que lo usaban como reloj siguen funcionando.
*Devuelve:* milisegundos, entero.
*Desde:* 3.00
*Véase también:* <<singegetpauseflag,singeGetPauseFlag>>, <<debugprint,debugPrint>>
.Ejemplo
[source,lua]
----
-- Hace parpadear el aviso «insert coin» dos veces por segundo.
function drawAttractPrompt()
local phase = (singeGetTicks() // 250) % 2
if phase == 0 then
fontPrint(120, 200, "INSERT COIN")
end
end
----
[#singegetwidth]
==== singeGetWidth
[source,text]
----
width = singeGetWidth()
----
Devuelve el ancho de la ventana de Singe en píxeles de pantalla, la contrapartida de `singeGetHeight`. Para dibujar en el overlay se usa en cambio `overlayGetWidth`.
*Devuelve:* píxeles, entero.
*Desde:* 1.x
*Véase también:* <<singegetheight,singeGetHeight>>, <<overlaygetwidth,overlayGetWidth>>
.Ejemplo
[source,lua]
----
-- Elige un presupuesto de texturas según el tamaño de la ventana.
local pixels = singeGetWidth() * singeGetHeight()
if pixels > 1920 * 1080 then
textureSet = "high"
else
textureSet = "low"
end
----
[#singehasgpu]
==== singeHasGpu
[source,text]
----
present = singeHasGpu()
----
Si el dispositivo de la GPU llegó a arrancar. Sin él no está disponible nada de
lo que se dibuja a través de él: la escena 3D, las partículas 3D y todos los
documentos GUI. Las partículas 2D se dibujan con el overlay y no necesitan
dispositivo. Pregúntalo antes de pedir cualquiera de ellos, porque <<guinew,guiNew>>
y las llamadas de la escena terminan el script en vez de responder `false` -- no
hay forma de intentarlo y recuperarse.
Una máquina puede no tenerlo porque no tiene ningún controlador de Vulkan,
Direct3D 12 ni Metal, o porque se dio `--nogpu`. El menú incluido llama a esta
función al arrancar y elige un renderizador según la respuesta (véase xref:Manual.adoc#menurenderers[Los dos renderizadores]).
*Devuelve:* booleano, verdadero cuando hay un dispositivo de GPU.
*Desde:* 3.00.
*Véase también:* <<guinew,guiNew>>, <<singegetsysteminfo,singeGetSystemInfo>>, <<sceneperformance,Rendimiento y requisitos>>
.Ejemplo
[source,lua]
----
-- Un juego con una pantalla de título 3D y otra plana de reserva.
if singeHasGpu() then
buildSceneTitle()
else
buildSpriteTitle()
end
----
[#singeisrecording]
==== singeIsRecording
[source,text]
----
recording = singeIsRecording()
----
Si hay una grabación en curso, o se ha pedido y empieza con el siguiente fotograma que se dibuje. Véase <<singerecordstart,singeRecordStart>>.
*Devuelve:* booleano.
*Desde:* 3.00
*Véase también:* <<singerecordstart,singeRecordStart>>, <<singerecordstop,singeRecordStop>>
.Ejemplo
[source,lua]
----
-- Un punto rojo en la esquina de la pantalla de pausa del propio juego mientras se está grabando.
if singeIsRecording() then
overlayCircle(overlayGetWidth() - 12, 12, 5)
end
----
[#singepack]
==== singePack
[source,text]
----
result = singePack(directory, database)
----
Empaqueta el juego de `directory` en la base de datos de un solo archivo `database`, exactamente como hace `singe --pack` desde la línea de comandos, y devuelve `true` cuando lo ha conseguido. La carpeta tiene que ser la propia carpeta de un juego: una que no contenga la carpeta de soporte `Singe/`, ni un lanzador, ni nada más que el empaquetador rechace, y los motivos de un rechazo se imprimen en la consola igual que desde la línea de comandos. Esto es lo que permite que una herramienta de autoría publique un juego sin salir del motor.
*Parámetros:*
* `directory` -- la carpeta del juego, tal como la acepta el empaquetador.
* `database` -- el archivo `.game` que se escribe, que se sustituye si ya existe.
*Devuelve:* `true` si lo consigue, `false` si no.
*Desde:* 3.00
*Véase también:* <<singlefile,Juegos de un solo archivo>>
.Ejemplo
[source,lua]
----
-- Publica el juego junto a su carpeta e informa.
if singePack(singeGetDataPath() .. "CoinRun", singeGetDataPath() .. "CoinRun.game") then
debugPrint("packed")
end
----
[#singequit]
==== singeQuit
[source,text]
----
singeQuit()
----
Termina el script en marcha al final de este fotograma: el bucle del juego se detiene, se ejecuta `onShutdown` y el motor pasa al siguiente script de la cola o sale si no hay ninguno. Por eso, un juego lanzado desde el menú con `scriptPush` vuelve al menú, que se reinicia desde el principio. En el modelo con hilo, volver de `singeMain` la llama por ti. El interruptor asignado a `INPUT_QUIT` en `controls.cfg` y cerrar la ventana terminan el script de la misma manera. No la llames desde `onShutdown`, que ya es el camino de salida.
*Desde:* 1.x (RDG)
*Véase también:* <<onshutdown,onShutdown>>, <<singereload,singeReload>>, <<scriptpush,scriptPush>>
.Ejemplo
[source,lua]
----
function onInputPressed(what)
if what == SWITCH_QUIT then
singeQuit()
elseif what == SWITCH_START1 and gameOver then
startGame()
end
end
----
[#singerelaunch]
==== singeRelaunch
[source,text]
----
result = singeRelaunch(path)
----
Termina el motor como hace `singeQuit` y, una vez desmontado todo, arranca el ejecutable de `path` en lugar de este, con los mismos argumentos. Así es como la herramienta Engine Update del menú pasa una recreativa a un Singe más reciente que ha puesto junto al que está en marcha: el archivo nuevo se hace ejecutable y se arranca, y en Windows, que no reemplaza una imagen en ejecución, se arranca junto a este con su propio nombre. El archivo ya tiene que existir; si no existe, la llamada responde `false` sin hacer nada. Aquí no se desempaqueta ni se copia nada: el Singe nuevo desempaqueta sus propios archivos de soporte en su primera ejecución.
*Parámetros:*
* `path` -- el ejecutable que se arranca.
*Devuelve:* `true` cuando el rearranque ha quedado preparado, `false` cuando el archivo no está.
*Desde:* 3.00
*Véase también:* <<singequit,singeQuit>>, <<singegetsysteminfo,singeGetSystemInfo>>
.Ejemplo
[source,lua]
----
-- Se ha descargado y comprobado un Singe más reciente; pásale el relevo.
local folder = singeGetSystemInfo().executable:match("^(.*[/\\])")
singeRelaunch(folder .. "Singe-v3.01-Linux-x86_64")
----
[#singerecordstart]
==== singeRecordStart
[source,text]
----
singeRecordStart([name])
----
Empieza a grabar el juego, imagen y sonido juntos, en un MP4 en la carpeta de datos del juego (véase `singeGetDataPath`), a partir del siguiente fotograma que se dibuje. Sin nombre, los archivos se llaman `singe000.mp4` en adelante, como las capturas de pantalla; con uno, la grabación va a ese archivo, con `.mp4` añadido cuando el nombre no tiene extensión, y sustituye cualquier archivo con ese nombre, y un nombre que incluya una carpeta aborta el script. Una llamada mientras hay una grabación en curso se ignora. El interruptor asignado a `INPUT_RECORD` en `controls.cfg`, `F10` por defecto, empieza y detiene las mismas grabaciones.
La imagen es la ventana entera tal como se muestra, con las bandas negras incluidas, al tamaño de la ventana, exactamente lo que toma una captura de pantalla; el sonido es la mezcla terminada, con la música, los efectos y las bandas sonoras de los vídeos juntos. Los dos se codifican en un hilo propio, así que el juego paga por copiar cada fotograma desde la GPU y poco más. La imagen es H.264 allí donde la máquina tiene un codificador por hardware que Singe puede usar -- VA-API en Linux, VideoToolbox en macOS, Media Foundation en Windows, V4L2 en las placas ARM que lo tienen -- y MPEG-4 del propio codificador de FFmpeg en todas las demás; el sonido es AAC. La traza nombra el codificador cuando empieza la grabación, y cuenta los fotogramas descartados cuando se detiene.
Durante el juego, la grabación va a 60 fotogramas por segundo según el reloj real. Un fotograma que llega mientras el codificador sigue ocupado se descarta de la grabación en vez de hacer esperar al juego, y el sonido se retrasa lo que dura la cola del dispositivo de audio, así que los dos coinciden como coincidían en la pantalla. Con `--deterministic`, la grabación va en cambio a un fotograma por paso de reloj, y el motor espera al codificador en vez de descartar nada; si la inicia `--record`, una ejecución determinista también mezcla su sonido en memoria paso a paso, así que la grabación es exacta fotograma a fotograma y muestra a muestra.
*Parámetros:*
* `name` -- cadena opcional; un nombre de archivo, no una ruta.
*Devuelve:* nada.
*Desde:* 3.00
*Véase también:* <<singerecordstop,singeRecordStop>>, <<singeisrecording,singeIsRecording>>, <<singescreenshot,singeScreenshot>>, <<deterministic,Modo de prueba determinista>>
.Ejemplo
[source,lua]
----
-- El modo de atracción graba su propio tráiler: una vuelta y se detiene.
function attractBegin()
singeRecordStart("trailer")
end
function attractEnd()
singeRecordStop()
end
----
[#singerecordstop]
==== singeRecordStop
[source,text]
----
singeRecordStop()
----
Termina la grabación en el siguiente fotograma que se dibuje: lo que aún espera se codifica y el archivo se cierra, así que se reproduce de principio a fin. Se ignora cuando no se está grabando nada. Una grabación que sigue en curso cuando el juego termina se cierra de la misma manera.
*Devuelve:* nada.
*Desde:* 3.00
*Véase también:* <<singerecordstart,singeRecordStart>>, <<singeisrecording,singeIsRecording>>
.Ejemplo
[source,lua]
----
-- Graba una partida: desde la primera moneda hasta el fin de partida.
function onGameOver()
singeRecordStop()
end
----
[#singereload]
==== singeReload
[source,text]
----
singeReload()
----
Vuelve a ejecutar el juego desde su script al final de este fotograma, como hacen `F5` y un archivo modificado con `--reload`; esta llamada funciona sin la opción. Todos los sonidos se detienen, el estado de Lua se descarta sin llamar a `onShutdown`, se libera todo lo que cargó el script, se reinician la escena, la física (incluido su dibujo de depuración), las partículas y la navegación, el overlay vuelve a su resolución por defecto, y el volumen de los efectos de sonido, el modo del ratón, la captura del ratón, el modo del teclado, la tecla de pausa, el oyente del sonido y el indicador de pausa vuelven a sus valores por defecto. Las teclas y los botones que se mantienen pulsados durante la recarga se ignoran hasta que se sueltan, como en un arranque desde cero, y el disco se queda aparcado en su primer fotograma, en pausa, con la pista de audio por defecto seleccionada, como al arrancar. El propio motor (la ventana, los mandos, los ratones) se queda como está. Va bien para la opción de reiniciar de un menú de depuración o para un editor de niveles que recarga lo que acaba de guardar.
*Desde:* 3.00.
*Véase también:* <<singequit,singeQuit>>
.Ejemplo
[source,lua]
----
-- Editor: guarda el nivel y luego reinicia el juego con él.
function onKeyPressed(keysym, scancode)
if scancode == SCANCODE.F9.value then
saveLevel(singeGetDataPath() .. "level.lua")
singeReload()
end
end
----
[#singesavegeometry]
==== singeSaveGeometry
[source,text]
----
singeSaveGeometry()
----
Conserva para esta máquina la escala, el desplazamiento y la rotación actuales de
la imagen. Van a `machine.cfg` en la raíz de datos, junto al retardo de audio, y
el motor los aplica a todos los juegos que arranque después, desde el menú o
desde la línea de comandos, y a través de `singeReload`. Una opción escrita de
verdad en la línea de comandos gana: `--scalefactor` se impone a la escala
guardada, `--shiftx` o `--shifty` al desplazamiento guardado, `--rotate` a la
rotación guardada. El archivo se escribe entero, así que el retardo de audio se
conserva tal como está.
Es un ajuste de la recreativa, no del juego: es la forma de cuadrar un monitor
una sola vez en lugar de una vez por juego. La herramienta Display del menú
incluido es exactamente esta llamada detrás del botón 1.
*Devuelve:* nada.
*Desde:* 3.00.
*Véase también:* <<vldpsetscale,vldpSetScale>>, <<vldpsetshift,vldpSetShift>>, <<vldpsetrotate,vldpSetRotate>>, <<singesetaudiocalibration,singeSetAudioCalibration>>
.Ejemplo
[source,lua]
----
-- Al operador le gusta la imagen: se conserva para la recreativa.
function onInputPressed(input)
if input == SWITCH_BUTTON1 then
singeSaveGeometry()
end
end
----
[#singesavetier]
==== singeSaveTier
[source,text]
----
singeSaveTier(tier)
----
Guarda la clase de rendimiento de esta máquina, de 0 a 3, en `machine.cfg` junto
al retardo de audio y la geometría, donde <<singecapabilities,singeCapabilities>>
la indica y el menú la compara con el `TIER` de cada juego. El menú incluido mide
él mismo el nivel, según lo que tarda en dibujarse su fondo 3D
(<<scenemeasure,sceneMeasure>>), y solo lo guarda cuando cambia; no se le
pregunta nada al jugador.
*Devuelve:* nada.
*Desde:* 3.00.
*Véase también:* <<singecapabilities,singeCapabilities>>, <<singesavegeometry,singeSaveGeometry>>
.Ejemplo
[source,lua]
----
-- Una máquina sin nada de 3D es de nivel 0.
if not singeHasGpu() then
singeSaveTier(0)
end
----
[#singescreenshot]
==== singeScreenshot
[source,text]
----
singeScreenshot()
singeScreenshot(name)
----
Pide una captura de pantalla de la ventana entera, con las bandas negras incluidas, que se toma después de dibujar el siguiente fotograma y se guarda como PNG en la carpeta de datos del juego (véase `singeGetDataPath`). Sin nombre, los archivos se llaman `singe000.png` en adelante; cada ejecución del script busca desde cero el primer nombre libre y las capturas posteriores siguen a partir de la última guardada. Con un nombre, la captura va a ese archivo, con `.png` añadido cuando el nombre no tiene extensión, y sustituye cualquier archivo con ese nombre; un nombre que incluya una carpeta aborta el script. Dar nombre a la captura sirve a los scripts que producen imágenes a propósito, como los que ilustran los tutoriales de un libro, para que al volver a ejecutarlos se sustituya la imagen en vez de añadir otra. La llamada fuerza un redibujado para que la captura se tome incluso mientras la pantalla está inactiva. El interruptor asignado a `INPUT_SCREENSHOT` en `controls.cfg` hace lo mismo.
*Desde:* 1.x
*Véase también:* <<singegetdatapath,singeGetDataPath>>
.Ejemplo
[source,lua]
----
-- Un script de prueba captura unos cuantos fotogramas mientras recorre la escena en vuelo.
function onOverlayUpdate()
frames = frames + 1
if frames == 40 or frames == 85 or frames == 130 then
singeScreenshot()
end
if frames == 180 then
singeQuit()
end
end
----
[#singesetaudiocalibration]
==== singeSetAudioCalibration
[source,text]
----
singeSetAudioCalibration(milliseconds)
----
Fija el retardo de audio de la máquina, lo aplica en el acto y lo escribe en `machine.cfg` en la raíz de datos para que lo recojan todos los juegos de la máquina. La llama la pantalla de calibración del menú; también puede llamarla un juego con su propio menú de servicio. El valor es positivo cuando el audio se oye más tarde de lo que indica el dispositivo; el motor retrasa la presentación del vídeo esa misma cantidad. Los valores fuera del intervalo de `-1000` a `1000` abortan el script. Si no se puede escribir `machine.cfg`, el valor se aplica igualmente en esta ejecución y sale un mensaje en la consola.
*Parámetros:*
* `milliseconds` -- entero, de `-1000` a `1000`, positivo cuando el audio va con retraso.
*Desde:* 3.00
*Véase también:* <<singegetaudiocalibration,singeGetAudioCalibration>>, <<singesetaudiodelay,singeSetAudioDelay>>
.Ejemplo
[source,lua]
----
-- Menu.singe: conserva o descarta la calibración que ha ajustado el jugador.
function calibrationEnd(save)
if save then
singeSetAudioCalibration(CAL_VALUE)
end
CALIBRATING = false
end
----
[#singesetaudiodelay]
==== singeSetAudioDelay
[source,text]
----
singeSetAudioDelay(milliseconds)
----
Fija el retardo de audio propio del juego para esta ejecución: cuánto más tarde (positivo) o más pronto (negativo) se oye el audio de lo que el motor puede medir, para que la presentación del vídeo se desplace en consonancia. Se aplica al disco y a todos los vídeos que reproduzca el script, además de la calibración de la máquina. No se guarda nada; un juego que ofrezca el ajuste en un menú de servicio guarda el valor con sus demás ajustes y vuelve a llamar a esta función al arrancar. Los valores fuera del intervalo de `-1000` a `1000` abortan el script.
*Parámetros:*
* `milliseconds` -- entero, de `-1000` a `1000`, positivo cuando el audio va con retraso.
*Desde:* 3.00
*Véase también:* <<singegetaudiodelay,singeGetAudioDelay>>, <<singesetaudiocalibration,singeSetAudioCalibration>>
.Ejemplo
[source,lua]
----
-- Menú de servicio: retoca el desfase y recuérdalo.
function onInputPressed(what)
if what == SWITCH_LEFT then
settings.audioDelay = math.max(-1000, settings.audioDelay - 10)
elseif what == SWITCH_RIGHT then
settings.audioDelay = math.min(1000, settings.audioDelay + 10)
end
singeSetAudioDelay(settings.audioDelay)
saveSettings()
end
----
[#singesetgamename]
==== singeSetGameName
[source,text]
----
singeSetGameName(title)
----
Fija el título de la ventana de Singe. Llámala una vez al arrancar con el nombre visible del juego; nada más usa ese valor. El argumento tiene que ser una cadena o un número.
*Parámetros:*
* `title` -- el título de la ventana.
*Desde:* 1.15 (RDG)
*Véase también:* <<singeversion,singeVersion>>
.Ejemplo
[source,lua]
----
singeSetGameName("Asteroid Patrol")
keyboardSetMode(MODE_NORMAL)
hudFont = fontLoad(DIR .. "fonts/FreeSansBold.ttf", 18)
----
[#singesetpauseflag]
==== singeSetPauseFlag
[source,text]
----
singeSetPauseFlag(paused)
----
Fija el indicador de pausa del motor desde el script. `true` pausa el disco si se está reproduciendo, todos los vídeos cargados y todos los canales de efectos de sonido, y recuerda cuáles estaban en marcha; `false` reanuda exactamente esos. A diferencia de la tecla de pausa, esto no congela el script: los callbacks se siguen disparando para que el script pueda dibujar su menú de pausa y decidir cuándo quitar el indicador. Los juegos que gestionan su propia pausa deberían quedarse también con la tecla de pausa mediante `singeSetPauseKeyEnabled(false)`, porque la tecla del motor alterna el mismo indicador y, si no, se pelearía con el script. Un argumento que no sea booleano aborta el script.
*Parámetros:*
* `paused` -- booleano.
*Desde:* 1.x (RDG)
*Véase también:* <<singegetpauseflag,singeGetPauseFlag>>, <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>
.Ejemplo
[source,lua]
----
singeSetPauseKeyEnabled(false)
function onInputPressed(what)
if what == SWITCH_PAUSE then
menuOpen = not menuOpen
singeSetPauseFlag(menuOpen)
elseif menuOpen and what == SWITCH_QUIT then
singeQuit()
end
end
----
[#singesetpausekeyenabled]
==== singeSetPauseKeyEnabled
[source,text]
----
singeSetPauseKeyEnabled(enabled)
----
Elige quién es el dueño de la tecla de pausa asignada a `INPUT_PAUSE` en `controls.cfg`. Mientras está activada (por defecto, y se restablece en cada recarga) es del motor: pulsar la tecla congela el juego por completo (véase <<pausing,Pausa>>), y en `MODE_NORMAL` el script nunca recibe `SWITCH_PAUSE`. Mientras está desactivada, el motor ignora la tecla y `SWITCH_PAUSE` llega a `onInputPressed` y a `onInputReleased` como cualquier otro interruptor, así que el juego puede llevar su propia pausa con `singeSetPauseFlag`. Un argumento que no sea booleano aborta el script.
*Parámetros:*
* `enabled` -- booleano.
*Desde:* 3.00
*Véase también:* <<singesetpauseflag,singeSetPauseFlag>>, <<singegetpauseflag,singeGetPauseFlag>>, <<singedisablepausekey,singeDisablePauseKey>>
.Ejemplo
[source,lua]
----
-- El motor puede pausar durante la partida, pero no mientras el jugador escribe sus iniciales.
function beginInitials()
singeSetPauseKeyEnabled(false)
keyboardSetMode(MODE_FULL)
end
function endInitials()
keyboardSetMode(MODE_NORMAL)
singeSetPauseKeyEnabled(true)
end
----
[#singesetquitkeyenabled]
==== singeSetQuitKeyEnabled
[source,text]
----
singeSetQuitKeyEnabled(enabled)
----
Extensión de Hypseus. Elige si el motor sale con el interruptor asignado a `INPUT_QUIT` en `controls.cfg`, desde el teclado o desde un mando. Mientras está activada (por defecto, y se restablece en cada recarga), pulsarlo termina el juego. Mientras está desactivada, el motor lo ignora, así que un juego puede pedir confirmación o guardar antes; el script recibe `SWITCH_QUIT` por `onInputPressed` y `onInputReleased` en los dos casos, y `singeQuit` sigue terminando el juego. El botón de cerrar de la ventana siempre sale. `Framework.singe` define `keyboardCatchQuit(catch)`, el nombre que da Hypseus Singe al mismo ajuste con el sentido contrario. Un argumento que no sea booleano aborta el script.
*Parámetros:*
* `enabled` -- booleano.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<keyboardcatchquit,keyboardCatchQuit>>, <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>, <<singequit,singeQuit>>
.Ejemplo
[source,lua]
----
-- Pregunta antes de abandonar una partida en curso.
singeSetQuitKeyEnabled(false)
function onInputPressed(what)
if what == SWITCH_QUIT then
if confirmQuit() then
singeQuit()
end
end
end
----
[#singeversion]
==== singeVersion
[source,text]
----
version = singeVersion()
----
Devuelve la versión del motor como un número, la mayor más la menor como fracción, así que `3.00` vuelve como `3`. Compárala con `<` y `>=` para rechazar un motor demasiado antiguo o para tomar caminos distintos según las funciones disponibles; `SINGE_VERSION_MAJOR`, `SINGE_VERSION_MINOR` y `SINGE_VERSION_STRING` también se definen como globales para los scripts que quieran las partes o el texto.
*Devuelve:* número.
*Desde:* 1.x (RDG)
*Véase también:* <<singesetgamename,singeSetGameName>>
.Ejemplo
[source,lua]
----
if singeVersion() < 3 then
error("This game needs Singe 3.00 or newer, found " .. SINGE_VERSION_STRING)
end
----
[#singewantscrosshairs]
==== singeWantsCrosshairs
[source,text]
----
wanted = singeWantsCrosshairs()
----
Devuelve `true` salvo que Singe se haya arrancado con `--nocrosshair`. Dibuja el punto de mira por software solo cuando devuelva `true`; los jugadores con pistolas de luz que ponen su propio punto de mira en pantalla desactivan el del software con esa opción. El valor no cambia durante una ejecución, así que léelo una vez.
*Devuelve:* booleano.
*Desde:* 2.00
*Véase también:* <<mousegetposition,mouseGetPosition>>, <<onmousemoved,onMouseMoved>>
.Ejemplo
[source,lua]
----
drawCrosshair = singeWantsCrosshairs()
function onOverlayUpdate()
overlayClear()
drawTargets()
if drawCrosshair then
spriteDraw(reticle, cursorX, cursorY, true)
end
return OVERLAY_UPDATED
end
----
[#soft]
=== Cuerpo blando
Tela, cuerpos a presión y cuerdas: mallas cuyos vértices se simulan como partículas (véase <<physics,Física>>, Cuerpos blandos). A un cuerpo blando se accede por su nodo, y un nodo que no lo tiene termina el script en cualquier llamada posterior a `softNew`. Las posiciones van en unidades del mundo, la masa en kilogramos, y los valores de rigidez van de `0` a `1`. El motor reescribe la malla del nodo a partir de las partículas después de cada paso, y la simulación termina por sí sola cuando el nodo se borra o recibe una malla distinta. Hasta 16 cuerpos blandos a la vez, con hasta 32 sujeciones cada uno.
[#softnew]
==== softNew
[source,text]
----
softNew(node, SOFT_CLOTH)
softNew(node, SOFT_BODY)
softNew(node, SOFT_ROPE, x, y, z, segments, radius)
----
Con `SOFT_CLOTH` y `SOFT_BODY`, la malla del nodo, de tres vértices o más, se convierte en las partículas: los vértices que están en la misma posición (con una tolerancia de `0.0001` unidades) se sueldan en uno, se colocan donde los pone en el mundo la transformación del nodo, y los triángulos dan las restricciones de estiramiento, cizalla y flexión. La tela choca por las dos caras y es lo que necesita una lámina abierta; un cuerpo es una malla cerrada que `softSetPressure` puede inflar. Con `SOFT_ROPE`, el nodo no necesita malla: `segments + 1` partículas forman una línea desde la posición del nodo en el mundo hasta el punto del mundo, el nodo recibe a su alrededor una malla nueva de tubo de seis lados de ese radio (y conserva el material que tuviera), y las partículas chocan como esferas de ese radio, mientras que las partículas de la tela y de los cuerpos son esferas de radio `0.03`. Todos los tipos empiezan con estiramiento `0.9`, flexión `0.2`, masa de `1` kilogramo, amortiguación `0.1` y presión `0`. Un cuerpo blando que ya tenga el nodo se quita antes de crear el nuevo, así que una llamada que falla deja el nodo sin ninguno; la malla de tubo de una cuerda se va con él, y por eso el nodo de una cuerda no puede convertirse en tela ni en cuerpo (la llamada falla por falta de malla). Termina el script cuando la física no está disponible, el tipo es desconocido, el número de argumentos no corresponde al tipo, falta la malla, una cuerda no tiene segmentos o no tiene radio, o ya existen 16 cuerpos blandos.
*Parámetros:*
* `node` -- el nodo que lleva la malla (tela, cuerpo), o el punto de partida de la cuerda.
* `x`, `y`, `z` -- el otro extremo de la cuerda, en unidades del mundo.
* `segments` -- cuántos eslabones tiene la cuerda, `1` o más; con una docena basta.
* `radius` -- el radio de la cuerda, en unidades del mundo; también el tamaño con el que chocan sus partículas.
*Notas:* Una malla que comparten varios nodos se reescribe para todos ellos, así que da a un cuerpo blando una malla propia. Las mallas con skinning no pueden ser blandas.
*Desde:* 3.00.
*Véase también:* <<softpin,softPin>>, <<softsetstiffness,softSetStiffness>>, <<softsetpressure,softSetPressure>>, <<meshplane,meshPlane>>
.Ejemplo
[source,lua]
----
-- Una bandera colgada del mástil: una cuadrícula vertical, sujeta a lo largo de su borde izquierdo.
local flag = nodeNew()
nodeSetMesh(flag, meshPlane(1.6, 1.0, 16, 10), cloth)
nodeSetPosition(flag, -4.16, 2.4, 0)
nodeSetRotation(flag, 90, 0, 0)
softNew(flag, SOFT_CLOTH)
softSetStiffness(flag, 0.95, 0.1)
softSetMass(flag, 0.5)
for _, y in ipairs({ 1.9, 2.15, 2.4, 2.65, 2.9 }) do
softPin(flag, -4.96, y, 0) -- A lo largo del mástil
end
----
[#softdelete]
==== softDelete
[source,text]
----
softDelete(node)
----
Termina la simulación y libera su cuerpo. El nodo conserva su malla con la forma en que se escribió por última vez, así que una sábana que ha caído se queda drapeada sobre la caja y una cuerda conserva su tubo; la forma original de la malla no se restaura. Un nodo sin cuerpo blando termina el script.
*Desde:* 3.00.
*Véase también:* <<softnew,softNew>>, <<nodedelete,nodeDelete>>
.Ejemplo
[source,lua]
----
-- Cuando la sábana se ha asentado sobre la estatua, congélala y deja de pagar por ella.
function onOverlayUpdate()
if covering then
settleFrames = settleFrames + 1
if settleFrames > 240 then
softDelete(sheet) -- El drapeado se queda como está
covering = false
end
end
return OVERLAY_NOT_UPDATED
end
----
[#softpin]
==== softPin
[source,text]
----
softPin(node, x, y, z)
softPin(node, x, y, z, otherNode)
----
Busca la partícula más cercana al punto del mundo, estén donde estén las partículas en ese momento, y la fija. Sin más, la partícula se queda sujeta donde está; con `otherNode` se mueve a la posición en el mundo de ese nodo antes de cada paso, así que una cuerda cuelga de una grúa que se balancea y una capa sigue a los hombros. El punto no tiene por qué estar sobre la malla, ya que gana la partícula más cercana, y volver a sujetar la misma partícula solo cambia lo que sigue. Pasadas 32 sujeciones, la llamada no hace nada.
*Parámetros:*
* `node` -- el nodo del cuerpo blando.
* `x`, `y`, `z` -- un punto del mundo; se sujeta la partícula más cercana a él.
* `otherNode` -- un nodo al que la partícula seguirá a partir de ahora.
*Desde:* 3.00.
*Véase también:* <<softunpin,softUnpin>>, <<softnew,softNew>>, <<nodegetworldposition,nodeGetWorldPosition>>
.Ejemplo
[source,lua]
----
-- Una cuerda desde el gancho de la grúa hacia fuera y hacia abajo, que se balancea con la grúa.
local crane = nodeNew()
nodeSetPosition(crane, 4, 4, -1)
local rope = nodeNew()
nodeSetPosition(rope, 4, 4, -1)
softNew(rope, SOFT_ROPE, 6.5, 4, -1, 16, 0.04)
nodeSetMaterial(rope, hemp)
softSetMass(rope, 1.5)
softPin(rope, 4, 4, -1, crane) -- El extremo superior va con el gancho
----
[#softunpin]
==== softUnpin
[source,text]
----
softUnpin(node, x, y, z)
----
Suelta la partícula sujeta más cercana al punto del mundo. No pasa nada cuando esa partícula no está sujeta. El cuerpo se rehace alrededor de la forma actual, lo que además detiene en seco todas las partículas, así que una bandera que se suelta empieza a caer desde el reposo.
*Desde:* 3.00.
*Véase también:* <<softpin,softPin>>
.Ejemplo
[source,lua]
----
-- La esquina del estandarte se suelta cuando llega la ráfaga de viento.
function onOverlayUpdate()
gustFrames = gustFrames + 1
if gustFrames == 200 then
softUnpin(banner, 3.0, 4.5, 0)
soundPlay(ripSound)
end
return OVERLAY_NOT_UPDATED
end
----
[#softsetstiffness]
==== softSetStiffness
[source,text]
----
softSetStiffness(node, stretch, bend)
----
Cuánto se resiste el material a estirarse (y, en la tela, a la cizalla) y a doblarse, de `0` a `1`; los valores fuera de ese rango se limitan. `1` es rígido, `0` lo más flojo que permite el solucionador. Por defecto, `0.9` y `0.2`. El cuerpo se rehace alrededor de la forma actual: las longitudes en reposo pasan a ser las longitudes de ese momento, y todas las partículas se detienen, así que prepara el material antes de que la tela empiece a moverse. Una cuerda solo tiene estiramiento.
*Parámetros:*
* `node` -- el nodo del cuerpo blando.
* `stretch` -- resistencia al estiramiento, de `0` a `1`; por defecto `0.9`.
* `bend` -- resistencia a doblarse, de `0` a `1`; por defecto `0.2`.
*Desde:* 3.00.
*Véase también:* <<softsetmass,softSetMass>>, <<softsetdamping,softSetDamping>>
.Ejemplo
[source,lua]
----
-- Seda, lona y una lámina de goma a partir de la misma cuadrícula.
local silk = clothNode(silkMaterial)
softNew(silk, SOFT_CLOTH)
softSetStiffness(silk, 0.7, 0.02)
local canvas = clothNode(canvasMaterial)
softNew(canvas, SOFT_CLOTH)
softSetStiffness(canvas, 1.0, 0.4)
local rubber = clothNode(rubberMaterial)
softNew(rubber, SOFT_CLOTH)
softSetStiffness(rubber, 0.3, 0.1)
----
[#softsetpressure]
==== softSetPressure
[source,text]
----
softSetPressure(node, pressure)
----
Infla un `SOFT_BODY`: `0` (por defecto) es una bolsa desinflada y unas pocas unidades, un globo firme; un valor negativo pasa a ser `0`. Cambia el cuerpo vivo sin rehacerlo y lo despierta, así que un globo se puede hinchar o deshinchar mientras rebota. La tela y la cuerda no tienen volumen que inflar.
*Desde:* 3.00.
*Véase también:* <<softnew,softNew>>, <<softsetstiffness,softSetStiffness>>, <<meshsphere,meshSphere>>
.Ejemplo
[source,lua]
----
softNew(balloon, SOFT_BODY) -- Una meshSphere de 0.5
softSetMass(balloon, 0.6)
softSetPressure(balloon, 8) -- Un globo firme
function onOverlayUpdate()
if punctured then
pressure = math.max(pressure - 0.1, 0)
softSetPressure(balloon, pressure) -- Se deshincha silbando en un par de segundos
end
return OVERLAY_NOT_UPDATED
end
----
[#softsetmass]
==== softSetMass
[source,text]
----
softSetMass(node, kilograms)
----
La masa de todo el cuerpo, repartida a partes iguales entre sus partículas; por defecto `1`, y nunca menos de `0.001`. Las partículas sujetas no pesan nada en cualquier caso. El cuerpo se rehace alrededor de la forma actual y todas las partículas se detienen, así que fíjala de entrada.
*Desde:* 3.00.
*Véase también:* <<softsetstiffness,softSetStiffness>>, <<softsetdamping,softSetDamping>>
.Ejemplo
[source,lua]
----
-- Una sábana sujeta por las cuatro esquinas atrapa una caja que cae sobre ella.
local sheet = nodeNew()
nodeSetMesh(sheet, meshPlane(3, 3, 20, 20), linen)
nodeSetPosition(sheet, 0, 1.6, 0)
softNew(sheet, SOFT_CLOTH)
softSetStiffness(sheet, 1.0, 0.05)
softSetMass(sheet, 4) -- Lo bastante pesada para no ondear
for _, c in ipairs({ { -1.5, -1.5 }, { 1.5, -1.5 }, { -1.5, 1.5 }, { 1.5, 1.5 } }) do
softPin(sheet, c[1], 1.6, c[2])
end
----
[#softsetdamping]
==== softSetDamping
[source,text]
----
softSetDamping(node, damping)
----
Lo rápido que se apaga el movimiento de las partículas, de `0` en adelante; por defecto `0.1`, y un valor negativo pasa a ser `0`. Los valores más altos calman una bandera que no deja de ondear o una cuerda que se balancea sin fin. El cuerpo se rehace alrededor de la forma actual y todas las partículas se detienen.
*Desde:* 3.00.
*Véase también:* <<softsetmass,softSetMass>>, <<softsetstiffness,softSetStiffness>>
.Ejemplo
[source,lua]
----
-- Una cortina en aire quieto se asienta rápido; la misma cortina junto a la ventana abierta sigue moviéndose.
local curtain = nodeNew()
nodeSetMesh(curtain, meshPlane(1.2, 2.4, 12, 24), velvet)
nodeSetPosition(curtain, 2, 2.4, -3)
nodeSetRotation(curtain, 90, 0, 0)
softNew(curtain, SOFT_CLOTH)
softSetDamping(curtain, windowOpen and 0.05 or 0.4)
for x = -0.6, 0.6, 0.1 do
softPin(curtain, 2 + x, 3.6, -3)
end
----
[#sound]
=== Sonido
Los efectos de sonido son clips de audio (WAV es la opción segura) cargados en memoria y mezclados con SDL_mixer, aparte del audio del propio disco y del audio de los recursos `video*`. `soundLoad` devuelve un handle de sonido que identifica los datos del clip; `soundPlay` arranca ese clip en uno de los dieciséis canales del mezclador y devuelve el número de canal, de `0` a `15`, que es lo que reciben `soundPause`, `soundResume`, `soundStop`, `soundIsPlaying` y todas las llamadas `soundSet*`. Un mismo clip puede sonar en varios canales a la vez, y un número de canal se reutiliza en cuanto el canal queda libre, así que no confundas los dos tipos de número. Un volumen general de los efectos (`soundSetVolume`, de `0` a `63`) escala todos los canales. Un canal que está sonando también puede situarse en la escena 3D, siguiendo a un nodo o fijo en un punto en unidades del mundo, donde se panoramiza y se atenúa según su distancia al oyente (véase <<scenes3d,Escenas 3D>>); en los juegos 2D, `soundSetPan` panoramiza un canal sin escena.
[#soundfullstop]
==== soundFullStop
[source,text]
----
soundFullStop()
----
Detiene de golpe todos los canales de efectos, incluidos los que están en pausa, y los libera para reutilizarlos. No se descarga ningún clip; los handles de `soundLoad` siguen siendo válidos. Cada canal que estaba sonando o en pausa avisa a través de `onSoundCompleted`, exactamente como después de `soundStop`. Llámala en un cambio de escena o antes de una cinemática en vez de llevar la cuenta de cada canal que hayas arrancado.
*Desde:* 1.16
*Véase también:* <<soundstop,soundStop>>, <<onsoundcompleted,onSoundCompleted>>
.Ejemplo
[source,lua]
----
function enterAttractMode()
soundFullStop()
ambientChannel = -1
engineChannel = -1
discPlay()
end
----
[#soundgetposition]
==== soundGetPosition
[source,text]
----
x, y, z, gain = soundGetPosition(channel)
----
Indica dónde está un canal posicionado respecto al oyente, como una dirección de longitud unidad en el sistema de referencia del oyente (`x` positiva hacia la derecha, `y` positiva hacia arriba, `z` positiva detrás del oyente), y el factor de volumen que le da ahora su distancia, de `0` a `1`. Los valores se actualizan una vez por fotograma a partir del oyente y del nodo o el punto del canal. Un canal que no se ha colocado en la escena desde que empezó devuelve `0, 0, 0, 1`. Pensado para indicadores de tipo radar y para comprobar una mezcla desde un script de prueba; un canal fuera del rango de `0` a `15` aborta el script.
*Devuelve:* tres números para la dirección y uno para la ganancia por distancia.
*Desde:* 3.00.
*Véase también:* <<soundsetnode,soundSetNode>>, <<soundsetposition,soundSetPosition>>, <<soundsetrange,soundSetRange>>
.Ejemplo
[source,lua]
----
-- Apunta una flecha del HUD hacia el enemigo audible más cercano.
function onOverlayUpdate()
overlayClear()
local x, y, z, gain = soundGetPosition(growlChannel)
if gain > 0 then
local angle = math.atan(x, -z)
drawThreatArrow(angle, gain)
end
return OVERLAY_UPDATED
end
----
[#soundgetvolume]
==== soundGetVolume
[source,text]
----
volume = soundGetVolume()
----
Devuelve el volumen general de los efectos, de `0` a `63`. Al arrancar refleja el volumen de efectos de la línea de comandos, así que un juego que muestre un ajuste de volumen debería leerlo en vez de dar por hecho el máximo.
*Devuelve:* entero, de `0` a `63`.
*Desde:* 1.16
*Véase también:* <<soundsetvolume,soundSetVolume>>
.Ejemplo
[source,lua]
----
-- Baja los efectos mientras habla el narrador y luego devuélvelos a su nivel.
function narratorBegin()
savedVolume = soundGetVolume()
soundSetVolume(math.floor(savedVolume / 3))
narratorChannel = soundPlay(narratorClip)
end
----
[#soundisplaying]
==== soundIsPlaying
[source,text]
----
playing = soundIsPlaying(channel)
----
Comprueba si un canal está sonando en este momento. Un canal en pausa da `false`, igual que un canal cuyo clip ha terminado. Un canal fuera del rango de `0` a `15` aborta el script; un canal que simplemente se ha quedado en silencio, no.
*Devuelve:* booleano.
*Desde:* 1.16 (RDG)
*Véase también:* <<soundplay,soundPlay>>, <<soundpause,soundPause>>
.Ejemplo
[source,lua]
----
-- No vuelvas a arrancar la alarma mientras sigue sonando.
function raiseAlarm()
if alarmChannel < 0 or not soundIsPlaying(alarmChannel) then
alarmChannel = soundPlay(alarmClip)
end
end
----
[#soundload]
==== soundLoad
[source,text]
----
id = soundLoad(filename)
----
Carga un clip de audio en memoria y devuelve su handle. La ruta pasa por el sistema de archivos virtual del juego, así que funciona igual desde una carpeta suelta que desde un juego empaquetado. Carga los clips una vez al arrancar, no dentro de un callback que se dispara en cada fotograma. Un archivo que no se puede abrir o decodificar aborta el script.
*Devuelve:* handle de sonido, entero. Es un handle de datos, no un número de canal.
*Desde:* 1.x
*Véase también:* <<soundplay,soundPlay>>, <<soundunload,soundUnload>>
.Ejemplo
[source,lua]
----
-- Handles de clips, cargados una vez y descargados en onShutdown.
shotClip = soundLoad(DIR .. "sounds/shot.wav")
missClip = soundLoad(DIR .. "sounds/miss.wav")
coinClip = soundLoad(DIR .. "sounds/coin.wav")
ambientClip = soundLoad(DIR .. "sounds/wind.wav")
----
[#soundloaddata]
==== soundLoadData
[source,text]
----
id = soundLoadData(data)
----
Extensión de Hypseus. Crea un sonido a partir de bytes guardados en una cadena de Lua en vez de a partir de un archivo. Aquí se lee todo formato que lea <<soundload,`soundLoad`>>, y el handle que devuelve se usa con el resto de la familia `sound` exactamente de la misma manera.
La cadena puede contener bytes cero; lo que se lee lo decide su longitud, no su primer cero. Los bytes se decodifican durante la llamada y la cadena no se conserva, así que se puede descartar después. Unos bytes que no son audio, o una cadena vacía, terminan el script, igual que un archivo ilegible en `soundLoad`.
Úsala para el audio que un juego genera, desempaqueta o descifra por su cuenta. El audio al que el sistema de archivos virtual puede llegar por su nombre -- incluido el que está dentro de un `.game` empaquetado -- se carga mejor con `soundLoad`, que no guarda el archivo entero en una cadena de Lua.
*Parámetros:*
* `data` -- cadena de bytes.
*Devuelve:* número, el handle del sonido.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<soundload,soundLoad>>, <<spriteloaddata,spriteLoadData>>, <<soundplay,soundPlay>>
.Ejemplo
[source,lua]
----
-- Un efecto de sonido que el juego desempaqueta de su propio archivo de datos.
local bytes = myArchive:extract("beep.wav")
local beep = soundLoadData(bytes)
soundPlay(beep)
----
[#soundpause]
==== soundPause
[source,text]
----
wasPlaying = soundPause(channel)
----
Pausa un canal donde está; `soundResume` lo continúa desde el mismo punto. Un canal en pausa sigue contando como ocupado, así que `soundPlay` no se lo dará a otro clip, y `soundIsPlaying` da `false` para él. Pausar un canal que no está sonando no hace nada. Un canal fuera del rango de `0` a `15` aborta el script.
*Devuelve:* booleano, `true` cuando el canal estaba sonando en el momento de la llamada.
*Desde:* 1.16 (RDG)
*Véase también:* <<soundresume,soundResume>>, <<soundisplaying,soundIsPlaying>>
.Ejemplo
[source,lua]
----
-- Detén el ruido del motor mientras suena el mensaje de radio.
function radioBegin(messageClip)
engineWasRunning = soundPause(engineChannel)
radioChannel = soundPlay(messageClip)
end
----
[#soundplay]
==== soundPlay
[source,text]
----
channel = soundPlay(id)
channel = soundPlay(id, loops)
channel = soundPlay(id, loops, volume)
----
Arranca un clip cargado en el primer canal libre y devuelve el número de ese canal. Un canal cuenta como libre cuando ni está sonando ni está en pausa. El canal nuevo empieza al volumen general (o a `volume` por debajo de él), centrado, sin posición en la escena y con el alcance por defecto, fijara lo que fijara el uso anterior del canal. Cuando los dieciséis canales están ocupados no suena nada y se devuelve `-1` (`SOUND_ERROR_INVALID`); un juego que lanza muchos sonidos superpuestos debería comprobarlo. Un handle que no está cargado aborta el script.
*Parámetros:*
* `id` -- handle de sonido de `soundLoad`.
* `loops` -- `0` (por defecto) reproduce el clip una vez, un número positivo lo repite esas veces más, `-1` lo repite en bucle hasta que se detiene. Cualquier valor por debajo de `-1` aborta el script.
* `volume` -- el volumen propio del canal, de `0` a `63`, por debajo del general; por defecto es `63`, el clip tan alto como permite el volumen general. Un valor fuera de ese rango aborta el script. `soundSetChannelVolume` lo cambia mientras suena el clip.
*Devuelve:* número de canal, entero, de `0` a `15`, o `-1` cuando no hay ningún canal libre.
*Desde:* 1.x; `volume` desde 3.00.
*Véase también:* <<soundload,soundLoad>>, <<soundstop,soundStop>>, <<soundsetchannelvolume,soundSetChannelVolume>>, <<soundsetnode,soundSetNode>>
.Ejemplo
[source,lua]
----
function onInputPressed(what)
if what == SWITCH_BUTTON1 then
local channel = soundPlay(shotClip)
if channel < 0 then
debugPrint("All sound channels busy; shot dropped")
end
elseif what == SWITCH_COIN1 then
soundPlay(coinClip)
credits = credits + 1
end
end
----
[#soundresume]
==== soundResume
[source,text]
----
wasPaused = soundResume(channel)
----
Continúa un canal que `soundPause` detuvo, desde donde se quedó. Reanudar un canal que no está en pausa no hace nada. Un canal fuera del rango de `0` a `15` aborta el script.
*Devuelve:* booleano, `true` cuando el canal estaba en pausa en el momento de la llamada.
*Desde:* 1.16 (RDG)
*Véase también:* <<soundpause,soundPause>>
.Ejemplo
[source,lua]
----
function onSoundCompleted(channel)
if channel == radioChannel then
radioChannel = -1
if engineWasRunning then
soundResume(engineChannel)
end
end
end
----
[#soundsetchannelvolume]
==== soundSetChannelVolume
[source,text]
----
soundSetChannelVolume(channel, volume)
----
Fija el volumen propio de un canal, además del general de `soundSetVolume`: el canal suena al volumen general multiplicado por este, y por su atenuación por distancia cuando está en la escena. Todo canal empieza al máximo cuando `soundPlay` lo toma, salvo que se diera un volumen a `soundPlay`, así que un paso suave y un disparo fuerte pueden compartir un mismo volumen general. Los valores fuera del rango de `0` a `63` abortan el script, y lo mismo un canal fuera del rango de `0` a `15`.
*Parámetros:*
* `channel` -- número de canal de `soundPlay`.
* `volume` -- entero, de `0` (silencio) a `63` (tan alto como el volumen general).
*Desde:* 3.00
*Véase también:* <<soundplay,soundPlay>>, <<soundsetvolume,soundSetVolume>>
.Ejemplo
[source,lua]
----
-- Un motor que suena más fuerte con el acelerador: un clip en bucle y el volumen de su canal
-- siguiendo al pedal en cada fotograma.
engine = soundPlay(engineClip, -1, 20)
function onOverlayUpdate()
soundSetChannelVolume(engine, math.floor(20 + throttle * 43))
end
----
[#soundsetlistener]
==== soundSetListener
[source,text]
----
soundSetListener(node)
soundSetListener()
----
Elige el nodo desde el que se oyen los canales posicionados, mirando hacia donde mira él. Sin argumento, o con `nil`, el oyente es la cámara de la escena, lo adecuado para un juego en primera persona o de cámara fija; un juego en tercera persona suele preferir el nodo del jugador para que los sonidos se panoramicen alrededor del personaje y no de la cámara. Si el nodo se borra más tarde, el oyente vuelve a ser la cámara. Un handle de nodo que no existe aborta el script.
*Desde:* 3.00.
*Véase también:* <<soundsetnode,soundSetNode>>, <<soundsetposition,soundSetPosition>>, <<cameraset,cameraSet>>
.Ejemplo
[source,lua]
----
-- Cámara de persecución, pero el mundo se oye desde el coche.
local car = modelInstance(carModel)
local chaseCamera = nodeNew(car)
nodeSetPosition(chaseCamera, 0, 2, 6)
cameraSet(chaseCamera)
soundSetListener(car)
----
[#soundsetnode]
==== soundSetNode
[source,text]
----
soundSetNode(channel, node)
soundSetNode(channel)
----
Une un canal que está sonando a un nodo de la escena para que se oiga desde donde esté el nodo en cada fotograma: el canal se panoramiza hacia la dirección del nodo vista desde el oyente y se atenúa según su distancia, de acuerdo con `soundSetRange`. Llamarla sin nodo, o con `nil`, saca el canal de la escena al volumen general completo. Un canal cuyo nodo se borra vuelve por sí solo a la reproducción normal. La unión dura hasta que el canal se detiene o `soundPlay` lo reutiliza. Un canal fuera del rango de `0` a `15`, o un nodo que no existe, aborta el script.
*Desde:* 3.00.
*Véase también:* <<soundsetposition,soundSetPosition>>, <<soundsetrange,soundSetRange>>, <<soundsetlistener,soundSetListener>>
.Ejemplo
[source,lua]
----
-- Cada brasero crepita desde donde está.
for i, brazier in ipairs(braziers) do
local channel = soundPlay(crackleClip, -1)
if channel >= 0 then
soundSetNode(channel, brazier)
soundSetRange(channel, 1.5, 14)
end
end
----
[#soundsetpan]
==== soundSetPan
[source,text]
----
soundSetPan(channel, pan)
----
Panoramiza un canal entre los dos altavoces estéreo, para juegos sin escena: `-1` es todo a la izquierda, `1` todo a la derecha, `0` centrado, y los valores fuera de ese rango se limitan. Alejarse del centro solo reduce el altavoz del lado contrario; con `pan = 0.5` el altavoz izquierdo suena a media ganancia y el derecho a ganancia completa. La llamada saca el canal de la escena si `soundSetNode` o `soundSetPosition` lo habían puesto en ella. Un canal fuera del rango de `0` a `15` aborta el script.
*Desde:* 3.00.
*Véase también:* <<soundplay,soundPlay>>, <<soundsetnode,soundSetNode>>
.Ejemplo
[source,lua]
----
-- Panoramiza un coche que pasa según dónde está en el overlay.
function carPassBy(x)
local channel = soundPlay(carClip)
if channel >= 0 then
soundSetPan(channel, (x / overlayGetWidth()) * 2 - 1)
end
end
----
[#soundsetposition]
==== soundSetPosition
[source,text]
----
soundSetPosition(channel, x, y, z)
----
Coloca un canal que está sonando en un punto fijo de la escena, en unidades del mundo, en lugar de cualquier nodo al que siguiera. A partir de ahí se panoramiza y se atenúa respecto al oyente como un canal unido a un nodo. Va bien para un sonido que no se mueve, como una cascada o un teléfono que suena; una fuente en movimiento es más fácil con `soundSetNode`. Un canal fuera del rango de `0` a `15` aborta el script.
*Desde:* 3.00.
*Véase también:* <<soundsetnode,soundSetNode>>, <<soundsetrange,soundSetRange>>, <<soundgetposition,soundGetPosition>>
.Ejemplo
[source,lua]
----
-- La cascada nunca se mueve, así que no hace falta un nodo.
waterfallChannel = soundPlay(waterfallClip, -1)
if waterfallChannel >= 0 then
soundSetPosition(waterfallChannel, 42, 3, -18)
soundSetRange(waterfallChannel, 6, 80)
end
----
[#soundsetrange]
==== soundSetRange
[source,text]
----
soundSetRange(channel, near, far)
----
Fija cómo se atenúa un canal posicionado con la distancia al oyente. Dentro de `near` suena a todo volumen; más allá, su ganancia es `near` dividido por la distancia; en la última quinta parte de `far` además se atenúa linealmente, hasta el silencio en `far`. Por defecto son `1` y `30` unidades del mundo, que se restablecen cada vez que `soundPlay` reutiliza el canal. Un susurro pide un `far` pequeño; una cascada, uno grande.
*Parámetros:*
* `channel` -- número de canal de `soundPlay`.
* `near` -- distancia dentro de la cual el canal está a todo volumen, mayor que `0`.
* `far` -- distancia a la que queda en silencio, mayor que `near`. Cualquier otro par aborta el script.
*Desde:* 3.00.
*Véase también:* <<soundsetnode,soundSetNode>>, <<soundsetposition,soundSetPosition>>, <<soundgetposition,soundGetPosition>>
.Ejemplo
[source,lua]
----
-- Los pasos de un guardia solo se oyen de cerca; la sirena llega lejos.
local steps = soundPlay(footstepClip, -1)
soundSetNode(steps, guard)
soundSetRange(steps, 0.5, 8)
local siren = soundPlay(sirenClip, -1)
soundSetNode(siren, tower)
soundSetRange(siren, 10, 200)
----
[#soundsetvolume]
==== soundSetVolume
[source,text]
----
soundSetVolume(volume)
----
Fija el volumen general de los efectos y lo aplica en el acto a todos los canales, sonando o en pausa. El volumen propio de cada canal de `soundSetChannelVolume` y, en los canales posicionados, la atenuación por distancia se siguen aplicando encima. Los valores fuera del rango de `0` a `63` abortan el script.
*Parámetros:*
* `volume` -- entero, de `0` (silencio) a `63` (lo más alto).
*Desde:* 1.16
*Véase también:* <<soundgetvolume,soundGetVolume>>, <<soundsetchannelvolume,soundSetChannelVolume>>
.Ejemplo
[source,lua]
----
-- Control deslizante de volumen del menú de servicio, manejado con los interruptores izquierdo y derecho.
function onInputPressed(what)
local volume = soundGetVolume()
if what == SWITCH_LEFT then
soundSetVolume(math.max(0, volume - 4))
elseif what == SWITCH_RIGHT then
soundSetVolume(math.min(63, volume + 4))
end
soundPlay(clickClip)
end
----
[#soundstop]
==== soundStop
[source,text]
----
wasPlaying = soundStop(channel)
----
Detiene un canal de inmediato, esté sonando o en pausa, y lo libera para reutilizarlo. El canal avisa a través de `onSoundCompleted` igual que cuando el clip termina por sí solo, así que un script que encadena sonidos desde ese callback debería comprobar si fue él mismo quien detuvo el canal. El siguiente `soundPlay` puede volver a dar el mismo número, así que descarta el número en el lado de Lua. Un canal fuera del rango de `0` a `15` aborta el script.
*Devuelve:* booleano, `true` cuando el canal estaba sonando en el momento de la llamada. Un canal en pausa devuelve `false`, pero se detiene igualmente.
*Desde:* 1.x (RDG)
*Véase también:* <<soundfullstop,soundFullStop>>, <<onsoundcompleted,onSoundCompleted>>
.Ejemplo
[source,lua]
----
function playerDied()
if engineChannel >= 0 then
soundStop(engineChannel)
engineChannel = -1
end
soundPlay(explosionClip)
lives = lives - 1
end
----
[#soundunload]
==== soundUnload
[source,text]
----
soundUnload(id)
----
Libera un clip cargado e invalida su handle. El mezclador mantiene vivos los datos del clip hasta que se han detenido todos los canales que todavía lo reproducen, así que descargarlo no corta un canal que está sonando. Cualquier uso posterior del handle, incluido otro `soundUnload`, aborta el script. Su sitio habitual es `onShutdown`.
*Parámetros:*
* `id` -- handle de sonido de `soundLoad`, no un número de canal.
*Desde:* 2.00
*Véase también:* <<soundload,soundLoad>>, <<onshutdown,onShutdown>>
.Ejemplo
[source,lua]
----
function onShutdown()
soundUnload(shotClip)
soundUnload(missClip)
soundUnload(coinClip)
soundUnload(ambientClip)
fontUnload(hudFont)
end
----
[#sprite]
=== Sprite
Los sprites son mapas de bits 2D cargados de archivos con `spriteLoad`, o renderizados a partir de texto con `fontToSprite`, y dibujados sobre el overlay en coordenadas del overlay. Toda llamada `sprite*` recibe el handle entero como primer argumento (Singe 2.10 lo ponía el último; véase <<migrating,Migración desde Singe 2.10>>), y un handle ya descargado termina el script con un error. Se acepta cualquier formato que lea SDL_image; un GIF o WEBP animado con dos o más fotogramas se carga como una animación que se queda en el fotograma `0`, parada y sin bucle, hasta que `spritePlay` y `spriteLoop` digan otra cosa. El color `0` se dibuja transparente, como en Hypseus: el índice de paleta `0`, o el negro puro, incluido el negro opaco de una imagen con canal alfa, cuyo alfa también se respeta. Para conservar el negro opaco de una imagen, cárgala después de <<drawtransparent,`drawTransparent(true)`>>. Libera cada sprite con `spriteUnload`, normalmente desde <<onshutdown,onShutdown>>.
*Hojas de sprites.* Hypseus Singe amplía la familia con hojas de sprites: `spriteLoadFrames` corta una tira de una sola fila de fotogramas iguales en una animación que no se está reproduciendo, y `spriteDrawFrame` dibuja un fotograma elegido (contando desde 1) sin tocar la reproducción. Por lo demás, una hoja es una animación corriente, así que `spriteSetFrame` (contando desde 0), `spritePlay`, `spriteDraw`, `nodeSetSprite` y `emitterSetTexture` la aceptan, y `spriteFrameWidth` y `spriteFrameHeight` dan el tamaño sin escalar de un fotograma. Las entradas marcadas como «Extensión de Hypseus» existen para que los juegos escritos para Hypseus Singe funcionen sin cambios.
[#drawtransparent]
==== drawTransparent
[source,text]
----
drawTransparent(boolean)
----
Extensión de Hypseus. Decide si el color 0 se dibuja transparente en los sprites que se cargan, y en el texto que se renderiza, después de ella. `false`, el valor por defecto, hace transparente el color 0 como hace Hypseus: el índice de paleta 0, o el negro puro, incluido el negro opaco de una imagen con canal alfa, así que unas letras negras o un contorno negro desaparecen. `true` los dibuja solo según su canal alfa, así que el negro opaco sigue siendo negro. Los sprites ya cargados conservan el ajuste con el que se cargaron, y todo script empieza en `false`.
*Devuelve:* nada.
*Desde:* 3.00
.Ejemplo
[source,lua]
----
-- Las letras del logotipo son negro opaco: cárgalo solo según su alfa y luego vuelve
-- a la transparencia habitual para el arte propio del juego.
drawTransparent(true)
logo = spriteLoad("art/logo.png")
drawTransparent(false)
----
[#spriteblend]
==== spriteBlend
[source,text]
----
spriteBlend(boolean, sprite)
----
Extensión de Hypseus, aceptada e ignorada. Es un apaño para ese motor: Hypseus carga los sprites con el modo de mezcla de SDL desactivado, y esto lo vuelve a activar para un sprite. Singe ya dibuja todos los sprites con su canal alfa, así que la mezcla nunca está desactivada de entrada. El handle del sprite se sigue comprobando, así que uno incorrecto sigue siendo un error. Para sumar la luz de un sprite o multiplicar por él, véase <<spritesetblend,`spriteSetBlend`>>.
*Devuelve:* nada.
*Desde:* 3.00
[#spritedraw]
==== spriteDraw
[source,text]
----
spriteDraw(id, x, y)
spriteDraw(id, x, y, centered)
spriteDraw(id, x, y, x2, y2)
spriteDraw(id, x, y, x2, y2, centered)
----
Copia el sprite sobre el overlay con su rotación y su escala actuales. Llámala desde `onOverlayUpdate`. Las dos primeras formas dibujan al tamaño que dan `spriteGetWidth` y `spriteGetHeight`, con `(x, y)` como esquina superior izquierda; las dos últimas estiran esa imagen dentro del rectángulo de `(x, y)` a `(x2, y2)`, ambos incluidos, siempre con muestreo del vecino más cercano, diga lo que diga `spriteQuality`. Si el sprite es una animación en reproducción, el dibujo primero la avanza según el tiempo real transcurrido desde el dibujo anterior (o desde `spritePlay`), recorriendo tantos fotogramas como abarque ese tiempo, y muestra cada fotograma al menos 10 ms aunque el archivo pida menos. Al pasar el último fotograma, la reproducción vuelve al fotograma `0` si `spriteLoop` está activado; si no, se queda en el último fotograma y se detiene. Las partes que caen fuera del overlay se recortan.
*Parámetros:*
* `id` -- handle de sprite de `spriteLoad` o `fontToSprite`.
* `x`, `y` -- anclaje en coordenadas del overlay; las fracciones se truncan.
* `x2`, `y2` -- esquina opuesta del rectángulo de destino, incluida, para las formas estiradas.
* `centered` -- booleano. Con `true`, la imagen (o el rectángulo estirado) se desplaza para que `(x, y)` sea su centro en lugar de su esquina superior izquierda.
*Notas:* Una animación en reproducción que no se dibuja durante un rato se pone al día en su siguiente dibujo; llama a `spritePause` si debe quedarse en su fotograma. Usa la forma centrada para los sprites rotados, porque la rotación cambia la caja delimitadora y un anclaje en la esquina superior izquierda parece desplazarse.
*Desde:* 1.x. Las formas centrada y estirada centrada se añadieron en 2.10.
*Véase también:* <<spriteload,spriteLoad>>, <<spriterotate,spriteRotate>>, <<spritegetwidth,spriteGetWidth>>, <<videodraw,videoDraw>>
.Ejemplo
[source,lua]
----
-- El arte de la recreativa a tamaño natural, la imagen fija del modo de atracción encajada en su caja,
-- y un cursor anclado por su centro.
function onOverlayUpdate()
overlayClear()
spriteDraw(cabinetSprite, CABINET_X, CABINET_Y)
spriteDraw(attractStill, THUMB_X, THUMB_Y, THUMB_X + THUMB_W - 1, THUMB_Y + THUMB_H - 1)
spriteDraw(cursorSprite, cursorX, cursorY, true)
return OVERLAY_UPDATED
end
----
[#spritedrawframe]
==== spriteDrawFrame
[source,text]
----
spriteDrawFrame(id, x, y, frame)
spriteDrawFrame(id, x, y, frame, scale)
spriteDrawFrame(id, x, y, frame, scaleX, scaleY)
----
Extensión de Hypseus. Copia un fotograma de una hoja de sprites (o de cualquier animación) sobre el overlay con `(x, y)` como esquina superior izquierda, sin cambiar el fotograma en el que está detenido el sprite ni si se está reproduciendo. Los fotogramas se numeran desde `1`, como los cuenta Hypseus; un fotograma fuera del rango de `1` al número de fotogramas dibuja el fotograma `1`. Una imagen fija se dibuja a sí misma e ignora `frame`. Se aplica el espejo puesto con `spriteFlip`. La rotación y la escala propias del sprite, de `spriteRotate` y `spriteScale`, se ignoran: la primera forma dibuja el fotograma a su tamaño natural; las otras multiplican el ancho del fotograma por `scaleX` (o `scale`) y su alto por `scaleY` (o `scale`) con muestreo del vecino más cercano. Llámala desde `onOverlayUpdate`.
*Parámetros:*
* `id` -- handle de sprite de `spriteLoadFrames` o `spriteLoad`.
* `x`, `y` -- esquina superior izquierda en coordenadas del overlay; las fracciones se truncan.
* `frame` -- número de fotograma, de `1` al número de fotogramas; las fracciones se truncan.
* `scale`, `scaleX`, `scaleY` -- multiplicadores de tamaño, `1` para el tamaño natural.
*Notas:* `spriteSetFrame` cuenta los fotogramas desde `0`; `spriteDrawFrame` cuenta desde `1`.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<spriteloadframes,spriteLoadFrames>>, <<spritedraw,spriteDraw>>, <<spritesetframe,spriteSetFrame>>, <<spriteframewidth,spriteFrameWidth>>
.Ejemplo
[source,lua]
----
-- Una tira de diez fotogramas de dígitos dibuja la puntuación, un dígito por fotograma.
digits = spriteLoadFrames(10, DIR .. "digits.png")
function drawScore(x, y, score)
local text = tostring(score)
for i = 1, #text do
spriteDrawFrame(digits, x + (i - 1) * spriteFrameWidth(digits), y, tonumber(text:sub(i, i)) + 1)
end
end
----
[#spritedrawgrid]
==== spriteDrawGrid
[source,text]
----
spriteDrawGrid(id, x, y, sourceX, sourceY, width, height)
spriteDrawGrid(id, x, y, sourceX, sourceY, width, height, x2, y2)
----
Extensión de Hypseus. Dibuja un rectángulo sacado de un sprite con su esquina superior izquierda en `x`, `y`: a su propio tamaño o, si se dan `x2` e `y2`, estirado para llenar la caja hasta esa esquina, como <<spritedraw,`spriteDraw`>> estira un sprite entero. Es la única llamada que recibe un rectángulo de origen: `spriteDraw` toma el sprite entero y <<spritedrawframe,`spriteDrawFrame`>> toma un fotograma entero de una hoja, mientras que esta toma cualquier rectángulo. Así es como un juego recorta una baldosa, un dígito o una letra de una sola imagen sin cargarla muchas veces.
La forma estirada es propia de Singe, y es la base de un juego de carreras por carretera. Cada fila de la pantalla por debajo del horizonte es una franja de carretera a cierta distancia, así que el juego recorta una fila de una imagen de la sección transversal de la carretera y la estira al ancho de la carretera a esa distancia, centrada donde la hayan llevado las curvas. Un centenar largo de llamadas por fotograma dibuja la carretera entera, con curvas y colinas incluidas, y no se reconstruye nada. La misma forma dibuja un sprite medio oculto tras la cima de una colina: recorta su parte superior y estírala a lo que queda a la vista. <<spritescale,`spriteScale`>> no puede hacer ninguna de las dos cosas, porque reconstruye la imagen entera cada vez que cambia el tamaño.
El rectángulo está en los píxeles propios del sprite y debe quedar dentro de él; un rectángulo que no lo esté, o uno sin ancho o sin alto, termina el script indicando el tamaño del sprite, y lo mismo pasa con un `x2` sin `y2`. Un estiramiento cuya caja no tiene ancho o alto, porque `x2` está a la izquierda de `x` o `y2` por encima de `y`, no dibuja nada. El estiramiento toma el píxel más cercano en lugar de mezclar, lo que mantiene los colores de una fila de un píxel exactamente como se dibujaron. El sprite del que se dibuja es el sprite tal como se dibuja ahora, así que un <<spriterotate,`spriteRotate`>> o un `spriteScale` cambia lo que significan las coordenadas.
Con `LEGACY_SPRITE_ARGS` el handle va tercero, `spriteDrawGrid(x, y, spr, sourceX, sourceY, width, height)`, que es donde lo pone Hypseus, y `x2`, `y2` van al final igual que aquí.
*Parámetros:*
* `id` -- número; un handle de sprite.
* `x`, `y` -- números; dónde cae la esquina superior izquierda del rectángulo, en coordenadas del overlay.
* `sourceX`, `sourceY` -- números; la esquina superior izquierda del rectángulo en el sprite.
* `width`, `height` -- números; su tamaño, como mínimo 1.
* `x2`, `y2` -- números, opcionales y juntos; la esquina inferior derecha de la caja en la que estirarlo, incluida.
*Devuelve:* nada.
*Desde:* 3.00, como extensión de Hypseus Singe; la forma estirada es nueva en 3.00 y propia de Singe.
*Véase también:* <<spritedraw,spriteDraw>>, <<spritedrawframe,spriteDrawFrame>>
.Ejemplo
[source,lua]
----
-- Un número dibujado a partir de una tira de diez dígitos, cada uno de 16 por 24.
local function drawNumber(sheet, x, y, value)
local text = tostring(value)
for i = 1, #text do
local digit = tonumber(text:sub(i, i))
spriteDrawGrid(sheet, x + (i - 1) * 16, y, digit * 16, 0, 16, 24)
end
end
----
.Ejemplo
[source,lua]
----
-- Una carretera recta hasta el horizonte, fila a fila de la pantalla. road.png mide 256 píxeles de ancho y dos
-- filas de alto: una sección transversal clara de carretera y otra oscura, que se alternan a lo largo de la
-- carretera para mostrar que se mueve. La fila y de la pantalla es el suelo que está a EYE * FOCAL / (y - HORIZON).
local EYE = 60
local FOCAL = 160
local HORIZON = 110
local HALF = 100
local function drawRoad(road, traveled)
for y = HORIZON + 1, 239 do
local z = EYE * FOCAL / (y - HORIZON)
local half = HALF * FOCAL / z
local light = math.floor((z + traveled) / 60) % 2
spriteDrawGrid(road, 160 - half, y, 0, light, 256, 1, 160 + half, y)
end
end
----
[#spritedrawrotatedframe]
==== spriteDrawRotatedFrame
[source,text]
----
spriteDrawRotatedFrame(id, x, y)
spriteDrawRotatedFrame(id, x, y, scale)
spriteDrawRotatedFrame(id, x, y, scaleX, scaleY)
----
Extensión de Hypseus. Dibuja el último fotograma que giró <<spriterotateframe,`spriteRotateFrame`>>, *centrado* en `x`, `y`. Es la pareja de esa llamada y la razón de que existan las dos: <<spritedrawframe,`spriteDrawFrame`>> ignora a propósito la rotación y la escala de un sprite, así que un juego que quiere girado un fotograma de una hoja usa estas dos en su lugar.
Si se da una escala, el fotograma girado se estira según ella al dibujarse -- además de la escala con la que se giró -- y el resultado sigue centrado en `x`, `y`. `scaleX` sola escala los dos ejes.
Dibujar antes de cualquier `spriteRotateFrame` termina el script, porque no hay ningún fotograma girado que dibujar.
Con `LEGACY_SPRITE_ARGS` el handle va el último, como lo escribe Hypseus.
*Parámetros:*
* `id` -- número; un handle de sprite.
* `x`, `y` -- números; el *centro* del fotograma dibujado, en coordenadas del overlay.
* `scale` o `scaleX`, `scaleY` -- números opcionales; una escala adicional al dibujar.
*Devuelve:* nada.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<spriterotateframe,spriteRotateFrame>>, <<spritedrawframe,spriteDrawFrame>>
.Ejemplo
[source,lua]
----
-- La aguja de una brújula: un fotograma de la hoja, que apunta hacia donde va el jugador.
spriteRotateFrame(needle, heading, 1)
spriteDrawRotatedFrame(needle, 320, 240)
----
[#spriteflip]
==== spriteFlip
[source,text]
----
spriteFlip(id, horizontal, vertical)
----
Refleja el sprite de izquierda a derecha, de arriba abajo, o en ambos sentidos, hasta que se la vuelve a llamar con `false, false`. El espejo se aplica a todos los dibujos del sprite: `spriteDraw`, cada fotograma de `spriteDrawFrame` y el fotograma girado de `spriteDrawRotatedFrame`. Se aplica después de la rotación y la escala de `spriteRotate` y `spriteScale`, así que un sprite girado se refleja tal como aparece en pantalla, y un fotograma girado simplemente se refleja hacia el otro lado en lugar de girarse de nuevo. La imagen reflejada solo se reconstruye cuando cambian esos dos valores; un fotograma dibujado por `spriteDrawFrame` se refleja una vez y se guarda hasta que se pide otro fotograma. `spriteDrawGrid` ignora el espejo, porque su rectángulo se recorta de la imagen sin reflejar.
Un personaje que camina en los dos sentidos solo necesita arte que mire hacia un lado: refleja el sprite cuando se da la vuelta.
*Parámetros:*
* `id` -- handle de sprite.
* `horizontal` -- `true` refleja de izquierda a derecha.
* `vertical` -- `true` refleja de arriba abajo.
*Devuelve:* nada.
*Desde:* 3.00
*Véase también:* <<spriterotate,spriteRotate>>, <<spritedrawframe,spriteDrawFrame>>, <<spritedrawrotatedframe,spriteDrawRotatedFrame>>
.Ejemplo
[source,lua]
----
-- La hoja del héroe mira a la derecha; mirar a la izquierda son los mismos fotogramas, reflejados.
function onOverlayUpdate()
overlayClear()
spriteFlip(hero, heroVx < 0, false)
spriteDrawFrame(hero, heroX, heroY, walkFrame)
return OVERLAY_UPDATED
end
----
[#spriteframeheight]
==== spriteFrameHeight
[source,text]
----
height = spriteFrameHeight(id)
----
Extensión de Hypseus. Devuelve el alto de un fotograma del sprite antes de cualquier escala o rotación: el alto de la tira para una hoja de `spriteLoadFrames`, el alto de un fotograma para una animación y el alto de la imagen para una imagen fija (Hypseus da aquí la superficie dibujada). `spriteGetHeight`, en cambio, da el tamaño tal como se dibuja.
*Devuelve:* entero.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<spriteframewidth,spriteFrameWidth>>, <<spritegetheight,spriteGetHeight>>, <<spriteloadframes,spriteLoadFrames>>
.Ejemplo
[source,lua]
----
-- Centra un fotograma en un punto.
function drawCentered(sheet, frame, x, y)
spriteDrawFrame(sheet, x - spriteFrameWidth(sheet) / 2, y - spriteFrameHeight(sheet) / 2, frame)
end
----
[#spriteframewidth]
==== spriteFrameWidth
[source,text]
----
width = spriteFrameWidth(id)
----
Extensión de Hypseus. Devuelve el ancho de un fotograma del sprite antes de cualquier escala o rotación: el ancho de la tira dividido entre el número de fotogramas para una hoja de `spriteLoadFrames`, el ancho de un fotograma para una animación y el ancho de la imagen para una imagen fija (Hypseus da `0` en ese caso). `spriteGetWidth`, en cambio, da el tamaño tal como se dibuja.
*Devuelve:* entero.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<spriteframeheight,spriteFrameHeight>>, <<spritegetwidth,spriteGetWidth>>, <<spriteloadframes,spriteLoadFrames>>
.Ejemplo
[source,lua]
----
-- Coloca los fotogramas de una tira uno al lado de otro.
function drawStrip(sheet, count, x, y)
for frame = 1, count do
spriteDrawFrame(sheet, x + (frame - 1) * spriteFrameWidth(sheet), y, frame)
end
end
----
[#spritegetframe]
==== spriteGetFrame
[source,text]
----
result = spriteGetFrame(id)
----
Devuelve el índice, contado desde cero, del fotograma que muestra el sprite en este momento. Las imágenes fijas y las animaciones que no han avanzado dan `0`. Los fotogramas solo avanzan dentro de `spriteDraw`, así que el valor cambia entre dibujos, no entre llamadas cualesquiera. Úsala para hacer coincidir un sonido o la lógica del juego con un fotograma concreto de una animación.
*Devuelve:* índice de fotograma entero, `0` para una imagen fija.
*Desde:* 2.10
*Véase también:* <<spritesetframe,spriteSetFrame>>, <<spriteisplaying,spriteIsPlaying>>, <<spritedraw,spriteDraw>>
.Ejemplo
[source,lua]
----
-- Suena el disparo en el fotograma en que aparece el fogonazo.
function onOverlayUpdate()
spriteDraw(muzzleFlash, gunX, gunY, true)
local frame = spriteGetFrame(muzzleFlash)
if frame == FLASH_FRAME and frame ~= lastFlashFrame then
soundPlay(gunshot)
end
lastFlashFrame = frame
return OVERLAY_UPDATED
end
----
[#spritegetframes]
==== spriteGetFrames
[source,text]
----
count = spriteGetFrames(id)
----
Extensión de Hypseus. Cuántos fotogramas tiene un sprite: el número de fotogramas de una hoja cargada con <<spriteloadframes,`spriteLoadFrames`>> o de un GIF o WEBP animado, y `0` para una imagen fija. Es el límite hasta el que llega el número de fotograma de <<spritesetframe,`spriteSetFrame`>>, que cuenta desde cero, y uno más que el mayor que acepta.
*Parámetros:*
* `id` -- número; un handle de sprite.
*Devuelve:* número; `0` para una imagen fija.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<spriteloadframes,spriteLoadFrames>>, <<spritegetframe,spriteGetFrame>>, <<spritesetframe,spriteSetFrame>>
.Ejemplo
[source,lua]
----
-- Pasa al fotograma siguiente, sea como sea la hoja que haya resultado.
local frames = spriteGetFrames(walk)
if frames > 0 then
spriteSetFrame(walk, (spriteGetFrame(walk) + 1) % frames)
end
----
[#spritegetheight]
==== spriteGetHeight
[source,text]
----
result = spriteGetHeight(id)
----
Devuelve el alto del sprite tal como se dibujaría ahora mismo, en coordenadas del overlay. Incluye la escala y la rotación aplicadas con `spriteScale`, `spriteRotate` o `spriteRotateAndScale`, así que un sprite rotado da el alto de su caja delimitadora rotada, no el de la imagen de origen. En una animación se mide el fotograma actual. Las formas estiradas de `spriteDraw` no afectan al valor.
*Devuelve:* alto entero.
*Desde:* 2.00
*Véase también:* <<spritegetwidth,spriteGetWidth>>, <<spritescale,spriteScale>>, <<spriterotate,spriteRotate>>
.Ejemplo
[source,lua]
----
-- Apila líneas de descripción ya renderizadas, separadas por el alto de los sprites de texto.
function onOverlayUpdate()
local y = TEXT_Y
for _, line in ipairs(descriptionLines) do
spriteDraw(line, TEXT_X, y)
y = y + spriteGetHeight(line) + 1
end
return OVERLAY_UPDATED
end
----
[#spritegetwidth]
==== spriteGetWidth
[source,text]
----
result = spriteGetWidth(id)
----
Devuelve el ancho del sprite tal como se dibujaría ahora mismo, en coordenadas del overlay, incluidas la escala y la rotación aplicadas en este momento. En una animación se mide el fotograma actual. Junto con `spriteGetHeight`, es la forma habitual de centrar un sprite en una caja o de medir el texto renderizado por `fontToSprite`.
*Devuelve:* ancho entero.
*Desde:* 2.00
*Véase también:* <<spritegetheight,spriteGetHeight>>, <<spritescale,spriteScale>>, <<fonttosprite,fontToSprite>>
.Ejemplo
[source,lua]
----
-- Centra la imagen de la recreativa dentro de su panel, y el título a lo ancho del overlay.
function onOverlayUpdate()
local x = CABINET_X + (CABINET_W - spriteGetWidth(cabinetSprite)) * 0.5
local y = CABINET_Y + (CABINET_H - spriteGetHeight(cabinetSprite)) * 0.5
spriteDraw(cabinetSprite, x, y)
spriteDraw(titleSprite, (overlayGetWidth() - spriteGetWidth(titleSprite)) / 2, 25)
return OVERLAY_UPDATED
end
----
[#spriteisplaying]
==== spriteIsPlaying
[source,text]
----
result = spriteIsPlaying(id)
----
Indica si la animación del sprite está avanzando en este momento. Un sprite recién cargado da `false`; `spritePlay` la pone en marcha, y `spritePause` o el final de una animación sin bucle la detienen. Consúltala periódicamente para saber cuándo ha terminado una animación de una sola pasada.
*Devuelve:* booleano, `true` mientras la animación se reproduce.
*Notas:* `spritePlay` y `spritePause` no comprueban si hay datos de animación, así que una imagen fija da lo que esas llamadas pusieran por última vez.
*Desde:* 2.10
*Véase también:* <<spriteplay,spritePlay>>, <<spritepause,spritePause>>, <<spriteloop,spriteLoop>>
.Ejemplo
[source,lua]
----
-- Retén el menú hasta que termine la animación del logotipo, que se reproduce una sola vez.
function onOverlayUpdate()
overlayClear()
spriteDraw(logoReveal, overlayGetWidth() / 2, overlayGetHeight() / 2, true)
if not spriteIsPlaying(logoReveal) then
drawMenu()
end
return OVERLAY_UPDATED
end
----
[#spriteload]
==== spriteLoad
[source,text]
----
result = spriteLoad(filename)
result = spriteLoad(filename, width, height)
----
Carga una imagen y devuelve un handle entero para las demás llamadas `sprite*`. El nombre pasa por el sistema de archivos virtual del motor, así que el archivo puede estar suelto en el disco o dentro de un juego empaquetado; se resuelve respecto a la carpeta en la que se inició Singe, así que antepón `DIR` a los archivos que distribuyes con tu juego. Se lee todo formato de <<formats,Formatos de vídeo, audio y contenedor>>. Un GIF o WEBP animado con dos o más fotogramas se carga como animación, detenida en el fotograma `0`, sin reproducirse y sin bucle; cualquier otro archivo, incluido un GIF de un solo fotograma, se carga como imagen fija. Una imagen con más de 8 bits por canal, o con píxeles de coma flotante, se convierte a RGBA de 8 bits al cargarse y se trata como tal desde entonces. Los píxeles con un valor bruto de `0` en el formato de píxel propio del archivo se vuelven transparentes. La carga es síncrona, y un archivo que no se puede abrir o decodificar termina el script con el mensaje de error del cargador en lugar de devolver `nil`.
El tamaño opcional es para una imagen vectorial -- un SVG -- que no tiene un tamaño propio que valga la pena: la imagen se rasteriza para encajar en esa caja, conservando sus propias proporciones, en lugar de rasterizarse al tamaño del archivo y ampliarse después. Pregunta a <<spritegetwidth,`spriteGetWidth`>> y <<spritegetheight,`spriteGetHeight`>> qué tamaño ha salido, porque encajar un dibujo cuadrado en una caja ancha deja parte del ancho sin usar. Todos los demás formatos ignoran el tamaño.
*Parámetros:*
* `filename` -- cadena; la imagen que se carga.
* `width` -- número opcional; el ancho en el que encajar un SVG, `1` o más.
* `height` -- número opcional; el alto en el que encajar un SVG, `1` o más. Hay que dar los dos juntos.
*Devuelve:* handle de sprite entero.
*Notas:* Las animaciones no empiezan solas. Llama a `spriteLoop(id, true)` y `spritePlay(id)` después de cargar si la animación debe ejecutarse y repetirse.
*Desde:* 1.x. Soporte de GIF/WEBP animado añadido en 2.10; el tamaño vectorial, en 3.00.
*Véase también:* <<spriteunload,spriteUnload>>, <<spriteplay,spritePlay>>, <<spriteloop,spriteLoop>>, <<fonttosprite,fontToSprite>>
.Ejemplo
[source,lua]
----
-- Carga el arte del menú una vez al arrancar; el bucle del modo de atracción es un WEBP animado.
cabinetSprite = spriteLoad(DIR .. "cabinet.png")
marqueeSprite = spriteLoad(DIR .. "marquee.png")
attractLoop = spriteLoad(DIR .. "attract.webp")
spriteQuality(attractLoop, RENDER_SMOOTH)
spriteLoop(attractLoop, true)
spritePlay(attractLoop)
----
[#spriteloaddata]
==== spriteLoadData
[source,text]
----
id = spriteLoadData(data)
----
Extensión de Hypseus. Crea un sprite a partir de una imagen guardada en una cadena de Lua en lugar de en un archivo. Aquí se lee todo formato que lee <<spriteload,`spriteLoad`>>, GIF y WEBP animados incluidos, y una animación cargada así se comporta exactamente igual que una cargada de un archivo.
La cadena puede contener bytes cero; es su longitud, no su primer cero, lo que decide cuánto se lee. La imagen se decodifica durante la llamada y la cadena no se guarda. Unos bytes que no son una imagen, o una cadena vacía, terminan el script, igual que un archivo ilegible en `spriteLoad`.
Úsala para el arte que un juego genera, desempaqueta o descifra por sí mismo. El arte al que el sistema de archivos virtual llega por su nombre -- incluido el arte que está dentro de un `.game` empaquetado -- se carga mejor con `spriteLoad`, que no guarda el archivo entero en una cadena de Lua.
*Parámetros:*
* `data` -- cadena de bytes.
*Devuelve:* número, el handle del sprite.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<spriteload,spriteLoad>>, <<soundloaddata,soundLoadData>>
.Ejemplo
[source,lua]
----
-- Una imagen que el juego guarda dentro de su propio archivo de datos.
local bytes = myArchive:extract("logo.png")
local logo = spriteLoadData(bytes)
spriteDraw(logo, 10, 10)
----
[#spriteloadframes]
==== spriteLoadFrames
[source,text]
----
result = spriteLoadFrames(count, filename)
----
Extensión de Hypseus. Carga una imagen que contiene `count` fotogramas iguales en una sola fila y devuelve un handle de sprite. El ancho del fotograma es el ancho de la imagen dividido entre `count` (división entera; las columnas sobrantes de la derecha se descartan) y el alto del fotograma es el alto de la imagen. La hoja se convierte en una animación corriente detenida en el fotograma `0`, sin reproducirse y sin bucle, así que `spriteDraw` muestra el fotograma actual en lugar de la tira entera (como la dibuja Hypseus), y `spriteDrawFrame` elige cualquier fotograma directamente. El nombre pasa por el sistema de archivos virtual como en `spriteLoad`, y los píxeles con un valor bruto de `0` se vuelven transparentes en todos los fotogramas. Un número fuera del rango de `2` a `512`, una imagen más estrecha que `count` píxeles o un archivo que no se puede leer terminan el script con un error.
*Parámetros:*
* `count` -- número de fotogramas a lo ancho de la imagen, de `2` a `512`.
* `filename` -- la imagen de la tira.
*Devuelve:* handle de sprite entero.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<spritedrawframe,spriteDrawFrame>>, <<spriteframewidth,spriteFrameWidth>>, <<spriteload,spriteLoad>>, <<spriteunload,spriteUnload>>
.Ejemplo
[source,lua]
----
-- Una tira de cinco fotogramas de un coche; el fotograma 3 es el coche mirando de frente.
carSprite = spriteLoadFrames(5, DIR .. "car.png")
carWidth = spriteFrameWidth(carSprite)
function onOverlayUpdate()
overlayClear()
spriteDrawFrame(carSprite, (overlayGetWidth() - carWidth) / 2, CAR_Y, carFrame)
return OVERLAY_UPDATED
end
----
[#spriteloop]
==== spriteLoop
[source,text]
----
spriteLoop(id, loop)
----
Establece si la animación vuelve a empezar desde el fotograma `0` después del último o se detiene en él. Los sprites nuevos no se repiten. El valor se consulta cada vez que `spriteDraw` pasa del último fotograma, así que se puede cambiar mientras la animación se ejecuta. Activarlo en un sprite que ya se ha detenido no reanuda la reproducción; para eso, llama a `spritePlay`. El argumento debe ser un booleano de Lua.
*Parámetros:*
* `id` -- handle de sprite.
* `loop` -- booleano. `true` vuelve al fotograma `0`; `false` (el valor por defecto) se detiene en el último fotograma y `spriteIsPlaying` empieza a devolver `false`.
*Desde:* 2.10
*Véase también:* <<spriteplay,spritePlay>>, <<spriteisplaying,spriteIsPlaying>>, <<spritesetframe,spriteSetFrame>>
.Ejemplo
[source,lua]
----
-- La antorcha parpadea sin fin; la explosión se reproduce una vez por impacto.
torchFlame = spriteLoad(DIR .. "torch.gif")
explosion = spriteLoad(DIR .. "explosion.gif")
spriteLoop(torchFlame, true)
spriteLoop(explosion, false)
spritePlay(torchFlame)
----
[#spritenew]
==== spriteNew
[source,text]
----
id = spriteNew(width, height)
----
Crea un sprite en blanco, con todos los píxeles transparentes, para que <<spritesetpixels,`spriteSetPixels`>> lo rellene: una imagen que el script hace él mismo en lugar de cargarla. Cada lado mide de 1 a 4096 píxeles; cualquier otro valor termina el script. El sprite es una imagen fija y hace todo lo que hace cualquier otra: se dibuja, se gira, se escala, se estira y se tiñe, y puede ser la textura de un material.
*Parámetros:*
* `width`, `height` -- números; su tamaño en píxeles.
*Devuelve:* número, el handle del sprite.
*Desde:* 3.00
*Véase también:* <<spritesetpixels,spriteSetPixels>>, <<spriteloaddata,spriteLoadData>>
.Ejemplo
[source,lua]
----
local minimap = spriteNew(64, 64)
----
[#spritepause]
==== spritePause
[source,text]
----
spritePause(id)
----
Detiene la animación en su fotograma actual. `spriteDraw` sigue dibujando ese fotograma, y el tiempo en pausa no se cuenta cuando `spritePlay` la reanuda, así que el fotograma no salta hacia delante. Pausar un sprite que no se está reproduciendo no cambia nada.
*Desde:* 2.10
*Véase también:* <<spriteplay,spritePlay>>, <<spriteisplaying,spriteIsPlaying>>
.Ejemplo
[source,lua]
----
-- Congela al enemigo a mitad de paso mientras se ve el destello del golpe.
function onOverlayUpdate()
if playerHit and spriteIsPlaying(enemyWalk) then
spritePause(enemyWalk)
end
spriteDraw(enemyWalk, enemyX, enemyY, true)
return OVERLAY_UPDATED
end
----
[#spriteplay]
==== spritePlay
[source,text]
----
spritePlay(id)
----
Inicia o reanuda la animación desde su fotograma actual, contando el primer paso desde el momento de la llamada. Un sprite que ya se está reproduciendo se deja como está, así que el temporizador de fotogramas no se reinicia. Un sprite nuevo debe iniciarse con esta llamada; cargarlo no basta para animarlo.
*Notas:* Una animación sin bucle que ha llegado a su último fotograma se queda en él cuando se vuelve a reproducir. Para repetirla, llama antes a `spriteSetFrame(id, 0)`.
*Desde:* 2.10
*Véase también:* <<spritepause,spritePause>>, <<spritesetframe,spriteSetFrame>>, <<spriteloop,spriteLoop>>
.Ejemplo
[source,lua]
----
-- Reinicia la explosión de una sola pasada en el punto de impacto.
function onCollision(a, b, x, y, z, speed)
if speed > IMPACT_SPEED then
blastX = x
blastY = y
spriteSetFrame(explosion, 0)
spritePlay(explosion)
soundPlay(boom)
end
end
----
[#spritequality]
==== spriteQuality
[source,text]
----
spriteQuality(id, mode)
----
Elige el muestreo que se usa cuando el sprite se reconstruye para rotarlo o escalarlo. `RENDER_PIXELATED` mantiene los bordes duros de los píxeles; `RENDER_SMOOTH` interpola. Los sprites nuevos son pixelados. El valor solo importa mientras haya en efecto una rotación o una escala distinta de `1.0`; un sprite sin transformar se dibuja directamente desde su imagen de origen, y las formas estiradas de `spriteDraw` usan siempre muestreo del vecino más cercano. Cambiar el modo reconstruye en el acto la imagen transformada, así que fíjalo una vez después de cargar y no en cada fotograma.
*Parámetros:*
* `id` -- handle de sprite.
* `mode` -- `RENDER_PIXELATED` (`0`) o `RENDER_SMOOTH` (`1`). Cualquier número distinto de cero selecciona el suavizado.
*Desde:* 2.10
*Véase también:* <<spritescale,spriteScale>>, <<spriterotate,spriteRotate>>, <<spriterotateandscale,spriteRotateAndScale>>
.Ejemplo
[source,lua]
----
-- El arte fotográfico de la marquesina debe interpolarse; el pixel art debe seguir nítido.
marqueeSprite = spriteLoad(DIR .. "marquee.jpg")
playerSprite = spriteLoad(DIR .. "player.png")
spriteQuality(marqueeSprite, RENDER_SMOOTH)
spriteQuality(playerSprite, RENDER_PIXELATED)
spriteScale(marqueeSprite, 0.5)
spriteScale(playerSprite, 3)
----
[#spriteresetcolorkey]
==== spriteResetColorKey
[source,text]
----
spriteResetColorKey(id, enabled)
----
Extensión de Hypseus. Se acepta por compatibilidad y no hace nada. En Hypseus, esta opción vuelve a aplicar el color transparente a las copias que producen `spriteRotate` y `spriteScale`, que de otro modo dibujarían su fondo opaco; las copias transformadas de Singe llevan la transparencia en su canal alfa, así que no hay nada que volver a hacer transparente. Un handle incorrecto o un argumento no booleano aborta el script, como en todas las demás llamadas de sprites.
*Parámetros:*
* `id` -- handle de sprite.
* `enabled` -- booleano, se ignora.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<spriterotate,spriteRotate>>, <<spritescale,spriteScale>>, <<spriteload,spriteLoad>>
.Ejemplo
[source,lua]
----
-- Código de un juego de Hypseus; inofensivo en Singe.
badge = spriteLoad(DIR .. "badge.png")
spriteResetColorKey(badge, true)
----
[#spriterotate]
==== spriteRotate
[source,text]
----
spriteRotate(id, degrees)
----
Rota el sprite en el sentido de las agujas del reloj alrededor de su centro. El ángulo se reduce módulo 360 y puede ser fraccionario o negativo. Se conserva la escala actual. La imagen rotada solo se reconstruye cuando el ángulo cambia de verdad, así que repetir el mismo ángulo en cada fotograma no cuesta nada, mientras que un ángulo nuevo en cada fotograma reconstruye en cada fotograma. La rotación agranda la caja delimitadora, así que `spriteGetWidth` y `spriteGetHeight` cambian y la forma centrada de `spriteDraw` es la que conviene usar.
*Desde:* 2.10
*Véase también:* <<spriterotateandscale,spriteRotateAndScale>>, <<spritedraw,spriteDraw>>, <<spritequality,spriteQuality>>
.Ejemplo
[source,lua]
----
-- Cajas de una escena de física 2D, dibujadas con el ángulo de sus cuerpos.
function onOverlayUpdate()
overlayClear()
for _, crate in ipairs(crates) do
local x, y = nodeGetPosition(crate)
local _, _, angle = nodeGetRotation(crate)
spriteRotate(crateSprite, angle)
spriteDraw(crateSprite, x, y, true)
end
return OVERLAY_UPDATED
end
----
[#spriterotateandscale]
==== spriteRotateAndScale
[source,text]
----
spriteRotateAndScale(id, degrees, scale)
spriteRotateAndScale(id, degrees, scaleX, scaleY)
----
Fija la rotación y la escala a la vez. El resultado es el mismo que `spriteRotate` seguido de `spriteScale`, pero la imagen transformada se reconstruye una vez en lugar de dos, y ninguna si no ha cambiado nada. Los factores de escala deberían ser mayores que `0`; uno negativo se toma sin su signo, y uno que no dejaría ningún píxel, cero incluido, deja uno en su lugar.
*Parámetros:*
* `id` -- handle de sprite.
* `degrees` -- rotación en el sentido de las agujas del reloj, reducida módulo 360.
* `scale` -- factor uniforme, `1.0` para el tamaño natural.
* `scaleX`, `scaleY` -- factores horizontal y vertical por separado.
*Desde:* 2.10
*Véase también:* <<spriterotate,spriteRotate>>, <<spritescale,spriteScale>>, <<spritequality,spriteQuality>>
.Ejemplo
[source,lua]
----
-- Una moneda que gira y encoge mientras vuela hacia el contador de puntuación.
function onOverlayUpdate()
if coinFlying then
coinT = math.min(coinT + 0.02, 1)
spriteRotateAndScale(coinSprite, coinT * 720, 1 - coinT * 0.75)
spriteDraw(coinSprite, coinX + (SCORE_X - coinX) * coinT, coinY + (SCORE_Y - coinY) * coinT, true)
coinFlying = coinT < 1
end
return OVERLAY_UPDATED
end
----
[#spriterotateframe]
==== spriteRotateFrame
[source,text]
----
spriteRotateFrame(id, degrees, frame)
----
Extensión de Hypseus. Saca un fotograma de una hoja de sprites, lo gira `degrees` en el sentido de las agujas del reloj, lo escala según el <<spritescale,`spriteScale`>> propio del sprite y guarda el resultado para <<spritedrawrotatedframe,`spriteDrawRotatedFrame`>>. La superficie dibujada propia del sprite no se toca, así que <<spritedraw,`spriteDraw`>> y `spriteDrawFrame` siguen mostrando lo que mostraban.
Los fotogramas se numeran desde 1, como los numera `spriteDrawFrame` (y a diferencia de `spriteGetFrame` y `spriteSetFrame`, que cuentan desde 0); un fotograma fuera de la hoja es el fotograma 1 en lugar de un error. Una imagen fija gira su imagen entera.
Girar no es gratis: el fotograma se vuelve a dibujar píxel a píxel cada vez, así que gíralo cuando cambie el ángulo y no en cada fotograma. El ángulo se toma módulo 360, y <<spritequality,`spriteQuality`>> decide si se suavizan los bordes girados.
Con `LEGACY_SPRITE_ARGS` el handle va el último, `spriteRotateFrame(degrees, frame, spr)`, como lo escribe Hypseus.
*Parámetros:*
* `id` -- número; un handle de sprite.
* `degrees` -- número; en el sentido de las agujas del reloj.
* `frame` -- número; de 1 al número de fotogramas.
*Devuelve:* nada.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<spritedrawrotatedframe,spriteDrawRotatedFrame>>, <<spriterotate,spriteRotate>>, <<spritescale,spriteScale>>
.Ejemplo
[source,lua]
----
-- Un sprite de nave cuyo casco es el fotograma 1 y cuyo empuje es el fotograma 2.
spriteRotateFrame(ship, angle, thrusting and 2 or 1)
spriteDrawRotatedFrame(ship, shipX, shipY)
----
[#spritescale]
==== spriteScale
[source,text]
----
spriteScale(id, scale)
spriteScale(id, scaleX, scaleY)
----
Escala el sprite alrededor de su centro, de forma uniforme o por eje, conservando la rotación actual. Los factores deben ser mayores que `0`; `1.0` es el tamaño natural, y un factor de exactamente `1.0` en los dos ejes sin rotación dibuja directamente la imagen de origen. La imagen escalada solo se reconstruye cuando cambia un factor, con el muestreo elegido con `spriteQuality`. Prefiere la forma estirada de `spriteDraw` cuando el tamaño de destino cambia en cada fotograma, porque no reconstruye nada.
*Parámetros:*
* `id` -- handle de sprite.
* `scale` -- factor uniforme.
* `scaleX`, `scaleY` -- factores horizontal y vertical por separado.
*Desde:* 2.10
*Véase también:* <<spriterotateandscale,spriteRotateAndScale>>, <<spritequality,spriteQuality>>, <<spritedraw,spriteDraw>>
.Ejemplo
[source,lua]
----
-- Agranda la entrada del menú resaltada; la reconstrucción solo ocurre cuando se mueve la selección.
function onOverlayUpdate()
for i, label in ipairs(menuLabels) do
if i == selected then
spriteScale(label, 1.25)
else
spriteScale(label, 1)
end
spriteDraw(label, MENU_X, MENU_Y + i * LINE_HEIGHT, true)
end
return OVERLAY_UPDATED
end
----
[#spritesetalpha]
==== spriteSetAlpha
[source,text]
----
spriteSetAlpha(id, alpha)
----
Multiplica el alfa de cada píxel por `alpha / 255` en cada dibujo del sprite, en todas sus formas: `spriteDraw`, `spriteDrawFrame`, `spriteDrawGrid` y `spriteDrawRotatedFrame`, girado o no. `255`, el valor por defecto, lo dibuja tal cual y `0` no lo dibuja en absoluto, así que un logotipo aparece y desaparece con un fundido sin una segunda imagen. Los valores fuera de rango se limitan al rango.
*Parámetros:*
* `id` -- número; un handle de sprite.
* `alpha` -- número; de `0` a `255`.
*Devuelve:* nada.
*Desde:* 3.00
*Véase también:* <<spritesetblend,spriteSetBlend>>, <<spritesettint,spriteSetTint>>
.Ejemplo
[source,lua]
----
-- El título aparece con un fundido de dos segundos.
spriteSetAlpha(title, math.floor(255 * math.min(1, elapsed / 2)))
spriteDraw(title, 120, 80)
----
[#spritesetblend]
==== spriteSetBlend
[source,text]
----
spriteSetBlend(id, SPRITE_ALPHA | SPRITE_ADD | SPRITE_MULTIPLY)
----
Cómo coloca cada dibujo del sprite la imagen sobre lo que ya hay en el overlay. `SPRITE_ALPHA`, el valor por defecto, la dibuja encima según su alfa. `SPRITE_ADD` suma su color, escalado por su alfa, a lo que hay, así que solo puede aclarar: resplandores, luz, chispas y los shadebobs de las demos de antes, donde una bola dibujada sobre sí misma una y otra vez se quema hasta el blanco. `SPRITE_MULTIPLY` oscurece lo que hay según su color, para sombras y cristal tintado.
Los píxeles sumados y multiplicados conservan el alfa propio del overlay, así que solo se ven donde ya hay algo dibujado en el overlay: sobre un overlay transparente, encima de un vídeo o de la escena 3D, no cambian nada. Dibuja antes algo opaco, o usa `emitterSetBlend` con `PARTICLE_ADD`, que suma sobre todo. Cualquier valor que no sea una de las tres constantes termina el script.
*Parámetros:*
* `id` -- número; un handle de sprite.
* `blend` -- `SPRITE_ALPHA`, `SPRITE_ADD` o `SPRITE_MULTIPLY`.
*Devuelve:* nada.
*Desde:* 3.00
*Véase también:* <<spritesetalpha,spriteSetAlpha>>, <<spritesettint,spriteSetTint>>, <<emittersetblend,emitterSetBlend>>
.Ejemplo
[source,lua]
----
-- Shadebobs: una bola blanca suave sumada a lo largo de un recorrido, sin borrar nunca, así que la estela se acumula hasta el blanco.
spriteSetBlend(ball, SPRITE_ADD)
spriteSetAlpha(ball, 40)
for i = 0, 15 do
local a = t * 2 + i * 0.4
spriteDraw(ball, 320 + math.cos(a) * 200, 240 + math.sin(a * 1.3) * 150, true)
end
----
[#spritesetframe]
==== spriteSetFrame
[source,text]
----
spriteSetFrame(id, frame)
----
Salta en una animación al fotograma dado y reinicia el temporizador de espera de ese fotograma, así que se muestra durante toda su duración antes del siguiente. El estado de reproducción no cambia: un sprite en pausa sigue en pausa en el nuevo fotograma y uno que se está reproduciendo continúa desde él. La llamada se ignora en las imágenes fijas, para fotogramas fuera del rango de `0` al último fotograma y cuando el sprite ya muestra ese fotograma.
*Notas:* Aquí los fotogramas se numeran desde `0`; `spriteDrawFrame`, la extensión de Hypseus, los numera desde `1`.
*Desde:* 2.10
*Véase también:* <<spritegetframe,spriteGetFrame>>, <<spriteplay,spritePlay>>, <<spritedrawframe,spriteDrawFrame>>
.Ejemplo
[source,lua]
----
-- Muestra la puerta en el estado en que la lógica del juego dice que está.
function onOverlayUpdate()
if doorOpen then
spriteSetFrame(doorSprite, DOOR_OPEN_FRAME)
else
spriteSetFrame(doorSprite, DOOR_CLOSED_FRAME)
end
spriteDraw(doorSprite, DOOR_X, DOOR_Y)
return OVERLAY_UPDATED
end
----
[#spritesetpixels]
==== spriteSetPixels
[source,text]
----
spriteSetPixels(id, pixels)
spriteSetPixels(id, pixels, palette)
----
Sustituye todos los píxeles de un sprite fijo a partir de una cadena, fila a fila desde arriba. Sin paleta, la cadena tiene cuatro bytes por píxel: rojo, verde, azul y alfa. Con paleta tiene un byte por píxel, cada uno el número de un color de la paleta, que tiene 256 colores de tres bytes (rojo, verde y azul, todos opacos) o de cuatro (con alfa). Una cadena de longitud incorrecta, una paleta de longitud incorrecta o un sprite animado terminan el script.
Es la forma en que un script dibuja una imagen píxel a píxel, lo bastante rápido para hacerlo en cada fotograma: los plasmas, túneles, fuego y agua de las demos de antes, un minimapa o un mapa de alturas en edición. Construir la cadena es trabajo del script, así que mantén la imagen pequeña y estírala al dibujarla: un bucle de Lua rellena una imagen de 320 por 240 en unos pocos milisegundos en un ordenador de sobremesa. El modo de paleta es la vieja forma de animar a bajo coste: vuelve a poner los mismos píxeles con la paleta rotada un color, y todo cambia de color sin recalcular nada. Un sprite girado o escalado se vuelve a girar y escalar con sus nuevos píxeles; un material que muestra el sprite no ve el cambio hasta que se llama de nuevo a <<materialsettexture,`materialSetTexture`>> con el sprite, lo que lo vuelve a subir.
A una imagen cargada también se le pueden dar píxeles nuevos; tiene el tamaño con el que se cargó.
*Parámetros:*
* `id` -- número; un sprite fijo.
* `pixels` -- cadena; `width * height * 4` bytes, o `width * height` con paleta.
* `palette` -- cadena opcional; 768 o 1024 bytes.
*Devuelve:* nada.
*Desde:* 3.00
*Véase también:* <<spritenew,spriteNew>>, <<materialsettexture,materialSetTexture>>
.Ejemplo
[source,lua]
----
-- Fuego: cada píxel es la media de los de debajo, y se enfría al subir, con una paleta que va del
-- negro al blanco pasando por el rojo y el amarillo. Se estira sobre la parte baja de la pantalla en cada fotograma.
local W = 160
local H = 60
local fire = spriteNew(W, H)
local heat = {}
local color = {}
for i = 0, 255 do
color[#color + 1] = string.char(math.min(255, i * 3), math.max(0, math.min(255, i * 3 - 255)), math.max(0, i * 3 - 510))
end
local palette = table.concat(color)
for i = 0, W * (H + 1) - 1 do
heat[i] = 0
end
function onOverlayUpdate()
for x = 0, W - 1 do
heat[H * W + x] = math.random(0, 255)
end
local rows = {}
for y = 0, H - 1 do
local row = {}
for x = 0, W - 1 do
local below = (y + 1) * W
local v = (heat[below + (x - 1) % W] + heat[below + x] + heat[below + (x + 1) % W] + heat[y * W + x]) // 4
heat[y * W + x] = math.max(0, v - 1)
row[x + 1] = heat[y * W + x]
end
rows[y + 1] = string.char(table.unpack(row))
end
spriteSetPixels(fire, table.concat(rows), palette)
overlayClear()
spriteDraw(fire, 0, 240, 639, 479)
return OVERLAY_UPDATED
end
----
[#spritesettint]
==== spriteSetTint
[source,text]
----
spriteSetTint(id, r, g, b)
----
Multiplica el color de cada píxel por `r`, `g`, `b` / 255 en cada dibujo del sprite, en todas sus formas. Un sprite blanco se dibuja de cualquier color, y cualquier sprite se puede oscurecer o desplazar hacia un color, sin una segunda imagen: una bola blanca se convierte en un arcoíris de bolas, y un sprite blanco de un píxel estirado con `spriteDraw` se convierte en un rectángulo relleno de cualquier color. `255, 255, 255`, el valor por defecto, lo dibuja tal cual. Los valores fuera de rango se limitan al rango.
*Parámetros:*
* `id` -- número; un handle de sprite.
* `r`, `g`, `b` -- números; de `0` a `255`.
*Devuelve:* nada.
*Desde:* 3.00
*Véase también:* <<spritesetalpha,spriteSetAlpha>>, <<spritesetblend,spriteSetBlend>>
.Ejemplo
[source,lua]
----
-- Un anillo de bolas, cada una de su color, a partir de una sola imagen blanca.
for i = 1, 12 do
local hue = i / 12
spriteSetTint(ball, 128 + 127 * math.sin(hue * 6.28), 128 + 127 * math.sin(hue * 6.28 + 2.1), 128 + 127 * math.sin(hue * 6.28 + 4.2))
spriteDraw(ball, 320 + math.cos(hue * 6.28) * 150, 240 + math.sin(hue * 6.28) * 150, true)
end
spriteSetTint(ball, 255, 255, 255)
----
[#spriteunload]
==== spriteUnload
[source,text]
----
spriteUnload(id)
----
Libera el sprite: la imagen de origen, la copia transformada si existe y todos los fotogramas de la animación. Después, el handle no es válido y cualquier uso posterior termina el script con un error. Los sprites creados con `fontToSprite` se liberan igual. El motor libera los sprites que siguen cargados cuando termina el juego, pero los juegos que crean sprites durante la partida (texto que se vuelve a renderizar al cambiar la puntuación, por ejemplo) deben descargar los antiguos o la memoria crece sin límite.
*Desde:* 2.00
*Véase también:* <<spriteload,spriteLoad>>, <<fonttosprite,fontToSprite>>, <<onshutdown,onShutdown>>
.Ejemplo
[source,lua]
----
-- Vuelve a renderizar la puntuación solo cuando cambia, liberando el sprite anterior.
function updateScoreSprite()
if scoreSprite ~= nil then
spriteUnload(scoreSprite)
end
scoreSprite = fontToSprite("SCORE " .. score)
end
function onShutdown()
spriteUnload(scoreSprite)
spriteUnload(cabinetSprite)
end
----
[#subtitleref]
=== Subtítulo
Subtítulos SubRip sobre la imagen del disco. Un juego carga un `.srt` con <<srtload,`srtLoad`>>, lo activa con <<srtenable,`srtEnable`>>, y el motor muestra cada entrada y la vuelve a quitar a medida que el disco llega a ella. Véase <<subtitles,Subtítulos>> para el formato del archivo, la sincronización y cómo cambiarles el estilo.
Los subtítulos y <<overlaybanner,`overlayBanner`>> comparten un documento que viene con Singe, `Singe/subtitle.rml`, con el estilo de `Singe/subtitle.rcss`; un juego que quiere otro aspecto trae su propio `subtitle.rml` junto a su script, y Singe usa ese en su lugar (véase <<subtitles,Subtítulos>>). Ambos se dibujan sobre la imagen, por encima del overlay y por debajo de las GUI en espacio de pantalla, y no reciben entrada del ratón.
[#srtclear]
==== srtClear
[source,text]
----
srtClear()
----
Extensión de Hypseus. Quita al instante lo que haya en pantalla -- una entrada o un rótulo -- y deja como están las entradas cargadas y el estado de activación. La entrada que se mostraba sigue oculta hasta que el disco llega a la siguiente; no hay que volver a cargar nada ni volver a activar nada.
*Parámetros:* ninguno.
*Devuelve:* nada.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<srtenable,srtEnable>>, <<overlaybanner,overlayBanner>>
.Ejemplo
[source,lua]
----
-- Una escena cinemática termina antes de tiempo; quita la línea que seguía en pantalla.
srtClear()
----
[#srtenable]
==== srtEnable
[source,text]
----
srtEnable(enabled)
----
Extensión de Hypseus. Si las entradas cargadas se muestran mientras el disco se reproduce. Desactivarla quita también la entrada que se esté mostrando; volver a activarla muestra la entrada que cubra el fotograma en el que está el disco. Un script empieza con los subtítulos desactivados, así que un juego que los carga debe activarlos.
Un argumento no booleano termina el script.
*Parámetros:*
* `enabled` -- booleano.
*Devuelve:* nada.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<srtload,srtLoad>>, <<srtclear,srtClear>>
.Ejemplo
[source,lua]
----
srtEnable(SETTINGS.subtitles)
----
[#srtload]
==== srtLoad
[source,text]
----
loaded = srtLoad(filename)
----
Extensión de Hypseus. Lee un archivo SubRip a través del sistema de archivos virtual y convierte sus marcas de tiempo en fotogramas del disco a la tasa de fotogramas propia del disco. Cargar sustituye lo que se hubiera cargado antes; después, <<srtenable,`srtEnable`>> lo muestra.
Responde `false`, sin haber cargado nada, cuando el archivo no se puede leer, cuando no contiene ninguna entrada que se pueda analizar o cuando no hay disco con el que sincronizarlo -- así que un archivo mal formado es un `false`, no un fallo. En todos esos casos se imprime un aviso con el nombre del archivo. Las entradas que no se pueden analizar dentro de un archivo por lo demás correcto se saltan, como las salta Hypseus.
Como las marcas de tiempo se convierten en números de fotograma al cargar el archivo, y el número de fotograma es lo que compara el motor, una entrada se vuelve a encontrar adondequiera que se mande el disco: una búsqueda, un salto o un paso caen en la línea correcta al instante.
*Parámetros:*
* `filename` -- cadena; una ruta a la que llegue el sistema de archivos virtual.
*Devuelve:* booleano, si se cargó alguna entrada.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<srtenable,srtEnable>>, <<srtposition,srtPosition>>, <<subtitles,Subtítulos>>
.Ejemplo
[source,lua]
----
if srtLoad("subs/" .. language .. ".srt") then
srtEnable(true)
end
----
[#srtloadtrack]
==== srtLoadTrack
[source,text]
----
loaded = srtLoadTrack(track)
----
Igual que <<srtload,`srtLoad`>>, salvo que los subtítulos vienen de dentro del propio contenedor del disco en lugar de un archivo `.srt` a su lado. Todo lo posterior a la carga es idéntico: las entradas se convierten en números de fotograma del disco a la tasa de fotogramas del disco, así que se vuelven a encontrar después de cualquier salto, y <<srtenable,`srtEnable`>>, <<srtposition,`srtPosition`>> y <<srtclear,`srtClear`>> funcionan igual.
El archivo entero se recorre una vez para reunir la pista, sin decodificar ninguna imagen, así que un disco largo tarda un momento -- hazlo mientras hay otra cosa en pantalla y no entre dos fotogramas de juego.
Una pista que contiene imágenes en lugar de palabras (VobSub o PGS) no se puede leer: responde `false`, como un `.srt` ilegible, y deja los subtítulos vacíos. Lo mismo ocurre con un número de pista que no corresponde a ninguna, y con un juego sin disco.
*Parámetros:*
* `track` -- número; de `0` a `discGetSubtitleTracks() - 1`.
*Devuelve:* `true` cuando se ha leído al menos una entrada.
*Desde:* 3.00.
*Véase también:* <<srtload,srtLoad>>, <<discgetsubtitletracks,discGetSubtitleTracks>>, <<srtenable,srtEnable>>
.Ejemplo
[source,lua]
----
-- Los subtítulos que traía la copia del disco, con un .srt junto al juego como reserva.
if discGetSubtitleTracks() > 0 then
srtLoadTrack(0)
else
srtLoad(DIR .. "subtitles.srt")
end
srtEnable(true)
----
[#srtposition]
==== srtPosition
[source,text]
----
srtPosition(percent)
----
Extensión de Hypseus. Dónde se coloca un subtítulo, como porcentaje desde lo alto de la imagen, de 1 a 95. El valor por defecto es 80. Un número fuera de ese rango se ignora, como lo ignora Hypseus, en lugar de limitarse o rechazarse.
La altura se aplica en el acto a la entrada que hay en pantalla y a todas las siguientes. <<overlaybanner,`overlayBanner`>> lleva su propia altura y no se ve afectado.
*Parámetros:*
* `percent` -- número; de 1 a 95.
*Devuelve:* nada.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<srtload,srtLoad>>, <<subtitles,Subtítulos>>
.Ejemplo
[source,lua]
----
-- Mantén las líneas apartadas del marcador de la parte de abajo.
srtPosition(70)
----
[#onlinescores]
=== Récords en línea
Un juego publica una puntuación en una línea:
[source,lua]
----
scoreSubmit(playerScore)
----
Todo lo demás se configuró una vez, en el menú: qué servicio, qué cuenta y con qué
nombre aparece el jugador (véase
xref:Manual.adoc#onlineservice[En línea: cuentas, juegos y puntuaciones]). Un juego solo necesita un
`GAME_ID` en su entrada de `games.dat` -- un UUID, con el que se guarda la tabla de récords. Véase
<<singegetgameid,singeGetGameId>> para el formato.
|===
| Llamada | Qué hace
| `scoreBegin()`
| Avisa de que empieza una partida. Es opcional, y vale la pena llamarla: véase más abajo.
| `scoreSubmit(value [, board [, meta]])`
| Pone una puntuación en cola. Devuelve `false` si la recreativa no tiene la sesión iniciada o el juego no tiene `GAME_ID`.
| `scoreLowestWins([board])`
| Indica que una tabla de récords pone primero la puntuación más baja. Llámala antes de la primera puntuación de la tabla.
| `scoreBoard(onDone [, board])`
| Obtiene la tabla de récords. `onDone(ok, result)` con `result.top` (una lista de `{name, value}`),
`result.standing` (el `value` de este jugador, su `rank` y el número de `players`) y
`result.order`, `"highest"` o `"lowest"`, según cómo ordene la tabla. Una recreativa sin
sesión iniciada también puede obtenerla: véase más abajo.
| `scorePlayerName()`
| El nombre con el que publica esta recreativa, o `nil` si no se ha elegido ninguno.
| `scoreSignedIn()`
| Si esta recreativa tiene la sesión iniciada en una cuenta en línea.
| `scoreWaiting()`
| Cuántas puntuaciones hay en cola sin enviar todavía.
| `scoreUpdate()`
| Una vez por fotograma, desde `onOverlayUpdate`. Sin ella no avanza nada.
|===
**Una puntuación se pone en cola, no se envía.** Se escribe en el disco y se entrega al
servicio cuando hay conexión, porque una recreativa a menudo está sin conexión y un juego
nunca debe quedarse esperando a la red para mostrar su propia tabla de récords. Una
puntuación en cola sobrevive a que se apague la máquina.
**Cualquiera puede leer una tabla de récords.** Una recreativa sin sesión iniciada sigue
obteniendo la tabla de un juego que el servicio ofrece a todo el mundo, sin `standing`
porque no hay ningún jugador al que dar una posición, así que un juego puede mostrar dónde
publicaría y pedir al jugador que inicie sesión. `scoreSignedIn()` distingue los dos casos:
una recreativa con la sesión iniciada que no ha elegido nombre tampoco tiene
`scorePlayerName()`, y pedirle que inicie sesión sería un error. Un juego que el servicio
reserva para cuentas concretas no tiene tabla de récords para una recreativa que no tenga
la sesión iniciada en una de ellas; la petición falla como fallaría con un juego que el
servicio no conoce. Publicar siempre requiere una cuenta.
**Un juego puede tener más de una tabla de récords.** Pasa un nombre de tabla para mantener
separado cada modo de juego -- una contrarreloj aparte de un total de puntos, fácil aparte de
difícil -- y léela con el mismo nombre. Las tablas también se separan según el script que se
está ejecutando: sin nombre, una puntuación va a la tabla que lleva el nombre del script, su
nombre de archivo sin la extensión, así que `Collection/Raid.singe`
publica en `raid`; con nombre, el del script va primero, así que
`scoreSubmit(points, "hard")` desde ese script publica en `raid.hard`.
Todos los títulos de un `.game` llevan el mismo `GAME_ID`, y esto es lo que mantiene los
juegos de una colección en sus propias tablas aunque dos de ellos llamen igual a sus modos,
mientras que un mismo juego listado dos veces (en dos idiomas, por ejemplo: dos entradas de
`games.dat`, un script) conserva un solo conjunto. Los nombres de tabla van en minúsculas y
contienen letras, dígitos, `-`, `_` y `.`; cualquier otra cosa se convierte en `-`. El nombre
completo tiene como máximo 32 caracteres, y se acorta primero la parte del script para que
sobreviva el nombre del modo.
**Una tabla de récords puede poner primero la puntuación más baja.** Golpes, muertes,
segundos: algunos juegos cuentan algo de lo que el jugador quiere menos. Llama a
`scoreLowestWins` con el nombre de la tabla, tal como lo recibe `scoreSubmit`, antes de la
primera puntuación de la tabla, y todas las puntuaciones enviadas a esa tabla y todas las
consultas de ella irán en ese sentido. La primera puntuación de una tabla fija su sentido
para siempre, así que ninguna tabla se ordena nunca de las dos formas: una puntuación
enviada después en el otro sentido se rechaza, y la cola la descarta en lugar de volver a
intentarlo. En una tabla así el cero es una puntuación, y también lo es un número por
debajo de cero, una vuelta bajo par.
.Ejemplo
[source,lua]
----
-- Un juego de golf: gana quien da menos golpes, y el par tiene su propia tabla de récords.
scoreLowestWins("strokes")
scoreLowestWins("par")
function roundOver(strokes, coursePar)
scoreSubmit(strokes, "strokes")
scoreSubmit(strokes - coursePar, "par") -- -2 son dos bajo par
end
----
**El nombre de una tabla de récords es un alias, nunca una dirección.** Se elige en la
herramienta Online Account, se guarda aparte de la cuenta, y es lo único de un jugador que
ven los demás jugadores. De tres a veinte caracteres entre letras, dígitos, guiones y
guiones bajos, empezando y terminando con una letra o un dígito -- sin espacios, sin puntos
y sin `@`, así que un alias ni siquiera puede parecer una dirección de correo electrónico.
Las mayúsculas y minúsculas no cuentan para que sea único, así que `Ann` y `ANN` son un mismo nombre.
**El alias se elige una vez.** Poder cambiarlo libremente significaría que la historia de
una tabla de récords no es realmente de nadie, así que un jugador no puede cambiarlo
después, y una dirección de correo electrónico no se puede cambiar en absoluto.
Un alias que nadie debería reclamar (`admin`, `staff`), o uno con palabras que no pintan
nada en una pantalla de un local público, se rechaza al elegirlo. Una puntuación se rechaza
mientras no se haya elegido un nombre, porque una puntuación sin el nombre de nadie no está
en una tabla de récords en ningún sentido útil.
**Lo que significa una puntuación.** La recreativa es la máquina del jugador, así que una
puntuación enviada es una *afirmación* -- nada que pueda hacer el cliente la convierte en
algo más, y firmarla con un secreto incluido en el cliente solo aparentaría seguridad.
Así que aquí nada finge verificarla, y un juego no debe escribirse como si otra cosa fuera
a hacerlo.
**Vale la pena llamar a `scoreBegin()`** por lo único que no es una afirmación. Llámala
cuando empiece una partida; la puntuación que venga después lleva cuánto duró la partida,
cronometrado en el otro extremo y no en esta máquina. Ese número no ha pasado por la
máquina del jugador, y eso es lo que hace que valga la pena tenerlo.
No se rechaza nada por ese dato, así que llamarla le cuesta a un juego una línea y nunca
puede hacer que se rechace una puntuación legítima. Una recreativa que está sin conexión
cuando empieza una partida simplemente no registra ningún tiempo, lo que es honesto, no un fallo.
.Ejemplo
[source,lua]
----
function startLevel()
scoreBegin() -- el cronometraje empieza ahora, en el otro extremo
...
end
----
.Ejemplo
[source,lua]
----
-- Al final de una partida, publica la puntuación y luego muestra dónde ha quedado.
function gameOver(points)
scoreSubmit(points)
scoreBoard(function(ok, board)
if not ok then
showMessage("Not signed in; score kept for later.")
return
end
for place, row in ipairs(board.top) do
showLine(place .. ". " .. row.name .. " " .. row.value)
end
end)
end
function onOverlayUpdate()
scoreUpdate()
return OVERLAY_UPDATED
end
----
[#stats]
=== Estadísticas
El overlay del desarrollador, en la esquina superior izquierda: cuánto tardó el fotograma
y su peor tiempo en los dos últimos segundos, la tasa de fotogramas, la memoria de Lua,
cuántos sprites, sonidos y temporizadores están vivos, cuántos nodos de la escena se
dibujaron y en cuántos lotes, cuánta memoria de texturas ocupa y qué está haciendo el disco.
Se dibuja con la fuente de depuración propia de SDL, así que no necesita ningún archivo de
fuente, ningún recurso ni ningún documento de GUI, y aparece incluso en una máquina en la
que el dispositivo 3D nunca llegó a arrancar. Se dibuja después de que
<<singescreenshot,`singeScreenshot`>> tome su imagen, así que nunca acaba en una captura de
pantalla.
Déjalo desactivado en un juego publicado. Nada te impide ponerlo tras una tecla en tu
propio menú de servicio.
[#statsenable]
==== statsEnable
[source,text]
----
statsEnable(enabled)
----
Muestra u oculta el overlay. Mientras se muestra, la imagen se vuelve a dibujar en cada fotograma, así que los números siguen moviéndose aunque no se mueva nada más en pantalla.
*Parámetros:*
* `enabled` -- booleano.
*Devuelve:* nada.
*Desde:* 3.00.
*Véase también:* <<statsisenabled,statsIsEnabled>>
.Ejemplo
[source,lua]
----
-- Una tecla de desarrollador, condicionada a una opción para que el juego publicado no tenga esa tecla.
function onInputPressed(key)
if DEVELOPER and key == SWITCH_SERVICE then
statsEnable(not statsIsEnabled())
end
end
----
[#statsisenabled]
==== statsIsEnabled
[source,text]
----
shown = statsIsEnabled()
----
Si el overlay está activado.
*Parámetros:*
* Ninguno.
*Devuelve:* `true` o `false`.
*Desde:* 3.00.
[#terrain]
=== Terreno
Suelo de mapa de alturas hecho con `meshHeightmap`, que convierte una imagen en escala de grises en una malla con un vértice por píxel; `terrainGetHeight` vuelve a leer esa malla a través del nodo que la lleva. Las posiciones y las alturas van en unidades del mundo, y se tienen en cuenta la posición y la escala del nodo. Véase Terreno en <<scenes3d,Escenas 3D>>.
[#terraingetheight]
==== terrainGetHeight
[source,text]
----
height = terrainGetHeight(node, x, z)
----
La altura en el mundo de la malla de mapa de alturas de `node` en la posición del mundo `x, z`, interpolada entre las cuatro muestras más cercanas, o `nil` más allá de sus bordes o cuando la malla del nodo no es un mapa de alturas. Cuentan la posición y la escala del nodo en el mundo; su rotación se ignora, porque los terrenos están planos. Úsala para colocar árboles, hacer aparecer cosas sobre el suelo o una cámara que siga la superficie, sin el coste de un lanzamiento de rayos. Termina el script si el handle de nodo no es válido.
*Parámetros:*
* `node` -- el nodo cuya malla salió de `meshHeightmap`.
* `x, z` -- la posición del mundo en la que se mide.
*Devuelve:* Un número, o `nil` fuera del terreno.
*Desde:* 3.00.
*Véase también:* <<meshheightmap,meshHeightmap>>, <<physicsraycast,physicsRaycast>>, <<nodesetposition,nodeSetPosition>>
.Ejemplo
[source,lua]
----
-- Árboles en las colinas, y una cámara que nunca baja del suelo.
for i = 1, 40 do
local x = math.random(-50, 50)
local z = math.random(-50, 50)
local tree = modelInstance(treeModel)
nodeSetPosition(tree, x, terrainGetHeight(hills, x, z) or 0, z)
end
function onOverlayUpdate()
local cx, cy, cz = nodeGetWorldPosition(camera)
local ground = terrainGetHeight(hills, cx, cz)
if ground and cy < ground + 1.5 then
nodeSetPosition(camera, cx, ground + 1.5, cz)
end
return OVERLAY_UPDATED
end
----
[#timer]
=== Temporizador
Algo que ha de ocurrir más tarde, sin que tengas que contar fotogramas. Los temporizadores
van con el reloj del motor, que es el virtual con `--deterministic`, así que un juego
cronometrado se repite exactamente de una ejecución a otra. Se disparan con el resto de los
callbacks del fotograma, en el mismo hilo que todo lo demás -- no hay nada que bloquear ni
nada que pueda dispararse mientras tu script está a medias con algo.
Un temporizador que se ha quedado atrás -- por una carga larga, un punto de
interrupción -- se dispara una vez cuando el juego se pone al día en lugar de dispararse
las veinte veces que se perdió.
Todo se olvida cuando el script se recarga, así que un temporizador de la ejecución anterior
nunca se dispara en la siguiente.
[#timerafter]
==== timerAfter
[source,text]
----
id = timerAfter(milliseconds, function)
----
Llama a la función una vez, más tarde. La función recibe el id del propio temporizador, así que una función puede servir a varios temporizadores.
*Parámetros:*
* `milliseconds` -- número; cero o menos se dispara en el fotograma siguiente.
* `function` -- se llama como `function(id)`.
*Devuelve:* un número; el id del temporizador.
*Desde:* 3.00.
*Véase también:* <<timercancel,timerCancel>>, <<timerevery,timerEvery>>
.Ejemplo
[source,lua]
----
-- Deja que termine la explosión antes de la pantalla de fin de partida.
soundPlay(bang)
timerAfter(1500, function()
showGameOver()
end)
----
[#timercancel]
==== timerCancel
[source,text]
----
timerCancel(id)
----
Detiene un temporizador. Cancelar uno que ya se ha disparado no es un error, así que no hay que comprobar nada antes.
*Parámetros:*
* `id` -- número; de `timerAfter` o `timerEvery`.
*Devuelve:* nada.
*Desde:* 3.00.
[#timerevery]
==== timerEvery
[source,text]
----
id = timerEvery(milliseconds, function)
----
Llama a la función una y otra vez hasta que se cancela.
*Parámetros:*
* `milliseconds` -- número; cuánto tiempo pasa entre llamadas.
* `function` -- se llama como `function(id)`.
*Devuelve:* un número; el id del temporizador.
*Desde:* 3.00.
*Véase también:* <<timercancel,timerCancel>>
.Ejemplo
[source,lua]
----
-- Descuenta la cuenta atrás una vez por segundo, y detenla cuando se acabe.
local left = 30
local clock
clock = timerEvery(1000, function()
left = left - 1
if left <= 0 then
timerCancel(clock)
outOfTime()
end
end)
----
[#timerisactive]
==== timerIsActive
[source,text]
----
running = timerIsActive(id)
----
Si el temporizador volverá a dispararse: `true` para uno que se repite hasta que se cancela, `false` para uno de una sola vez que ya se ha disparado.
*Parámetros:*
* `id` -- número.
*Devuelve:* `true` o `false`.
*Desde:* 3.00.
[#tween]
=== Interpolación
Llevar un número de un valor a otro con el tiempo, siguiendo una curva. Fundidos,
deslizamientos, movimientos de cámara, una barra de vida que se vacía, un menú que entra
deslizándose -- todo es lo mismo.
`tweenValue` te da el número y te deja decidir qué significa. Es a propósito: el motor
tiene nodos, sprites, elementos de GUI, materiales y luces, y una interpolación que
conociera cada uno de ellos serían cinco juegos de llamadas que mantener a la par. Una que
entrega un número no es ninguno, y funciona con cosas de las que el motor nunca ha oído hablar.
Las curvas son las habituales. `EASE_LINEAR` no es ninguna curva;
`EASE_QUAD_*`, `EASE_CUBIC_*`, `EASE_SINE_*` y `EASE_EXPO_*` van de suave a brusca;
`EASE_BACK_*` se pasa y vuelve; `EASE_ELASTIC_*` rebota como un muelle;
`EASE_BOUNCE_*` cae y rebota. Cada una viene en `_IN` (lenta al principio),
`_OUT` (lenta al final) e `_IN_OUT` (las dos cosas).
[#tweencancel]
==== tweenCancel
[source,text]
----
tweenCancel(id)
----
Detiene una interpolación donde está. No se vuelve a llamar a ninguna de las dos funciones, y el valor se queda donde hubiera llegado.
*Parámetros:*
* `id` -- número; de `tweenValue`.
*Devuelve:* nada.
*Desde:* 3.00.
[#tweenisactive]
==== tweenIsActive
[source,text]
----
running = tweenIsActive(id)
----
Si la interpolación sigue en marcha.
*Parámetros:*
* `id` -- número.
*Devuelve:* `true` o `false`.
*Desde:* 3.00.
[#tweenvalue]
==== tweenValue
[source,text]
----
id = tweenValue(from, to, milliseconds, easing, onUpdate [, onDone])
----
Lleva un número de `from` a `to` en `milliseconds`, llamando a `onUpdate(value, progress)` en cada fotograma -- `progress` va de 0 a 1 e ignora la curva -- y a `onDone()` una vez al final. El último `onUpdate` cae siempre exactamente en `to`, así que nada se queda a un píxel de su destino.
*Parámetros:*
* `from`, `to` -- números.
* `milliseconds` -- número; cero o menos llega en el fotograma siguiente.
* `easing` -- uno de los valores `EASE_`.
* `onUpdate` -- se llama como `function(value, progress)`.
* `onDone` -- opcional; se llama sin argumentos cuando termina.
*Devuelve:* un número; el id de la interpolación.
*Desde:* 3.00.
*Véase también:* <<tweencancel,tweenCancel>>
.Ejemplo
[source,lua]
----
-- Haz entrar el título deslizándose y sube el overlay con un fundido a la vez.
tweenValue(-200, 40, 600, EASE_BACK_OUT, function(x)
titleX = x
end)
tweenValue(0, 255, 600, EASE_SINE_OUT, function(alpha)
setOverlayOpacity(math.floor(alpha))
end, function()
startAttractLoop()
end)
----
[#vehicle]
=== Vehículo
Coches, motos, tanques y barcos construidos sobre un cuerpo que hace de chasis (véase <<physics,Física>>, Vehículos y Agua). Un vehículo no tiene handle propio: se le designa por el nodo de su chasis, que ya debe llevar un cuerpo `BODY_DYNAMIC` de `bodyNew`, y un nodo sin vehículo termina el script en todas las llamadas que vienen después de `vehicleNew`. El chasis mira hacia -Z con la Y hacia arriba; las distancias van en unidades del mundo, las fuerzas en newtons, los pares en newton metros y las velocidades del motor en revoluciones por minuto. Cada llamada de ajuste marca el vehículo para reconstruirlo en el siguiente paso de física, alrededor de las posiciones de las ruedas registradas cuando se añadieron. Hasta 16 vehículos a la vez, cada uno con hasta 16 ruedas.
[#vehiclenew]
==== vehicleNew
[source,text]
----
vehicleNew(node, kind)
----
Convierte el cuerpo dinámico de `node` en el chasis de un vehículo. Un coche, una moto o un tanque necesitan al menos dos ruedas de `vehicleAddWheel` antes de que el siguiente paso de física lo construya; un barco no tiene ruedas y lo empuja una hélice que solo funciona bajo una superficie hecha con `bodySetWater`. El vehículo nuevo parte de los valores por defecto: un motor de 500 Nm con su máximo a 6000 rpm y un ralentí de 1000, la caja de cambios automática de cinco marchas de Jolt, 35 grados de giro máximo de la dirección, frenos de 1500 Nm y un freno de mano de 4000 Nm, suspensión de 1,5 Hz con amortiguación de 0,5 y sin barras antivuelco. Llamarla otra vez sobre el mismo nodo descarta antes el vehículo anterior y sus ruedas. Termina el script para un tipo desconocido, un nodo cuyo cuerpo no es dinámico, un tipo con ruedas en un mundo 2D, o cuando ya existen 16 vehículos.
*Parámetros:*
* `node` -- el nodo del chasis, que lleva un cuerpo `BODY_DYNAMIC`.
* `kind` -- `VEHICLE_CAR` (cualquier número de ejes), `VEHICLE_MOTORCYCLE` (dos ruedas en línea, que se inclina en las curvas), `VEHICLE_TANK` (ruedas sobre dos orugas, que gira sin moverse del sitio) o `VEHICLE_BOAT` (un casco flotante con hélice y timón).
*Desde:* 3.00.
*Véase también:* <<vehicleaddwheel,vehicleAddWheel>>, <<vehicledrive,vehicleDrive>>, <<vehiclesetthrust,vehicleSetThrust>>, <<bodynew,bodyNew>>
.Ejemplo
[source,lua]
----
local car = nodeNew()
nodeSetMesh(car, meshBox(1.8, 0.5, 4.0), paint)
nodeSetPosition(car, 0, 0.9, 0)
bodyNew(car, BODY_DYNAMIC, SHAPE_BOX, 1.8, 0.5, 4.0)
bodySetMass(car, 1500)
vehicleNew(car, VEHICLE_CAR)
for _, p in ipairs({ { -0.95, -0.2, -1.4 }, { 0.95, -0.2, -1.4 }, { -0.95, -0.2, 1.4 }, { 0.95, -0.2, 1.4 } }) do
local hub = nodeNew()
nodeSetParent(hub, car)
nodeSetPosition(hub, p[1], p[2], p[3])
vehicleAddWheel(car, hub, 0.35, 0.25, 0.4)
end
----
[#vehicledelete]
==== vehicleDelete
[source,text]
----
vehicleDelete(node)
----
Quita el vehículo de su chasis: la restricción de las ruedas, la entrada del conductor y todo el ajuste. El cuerpo sigue siendo un cuerpo dinámico corriente, así que unos restos siguen deslizándose y dando vueltas, y los nodos de las ruedas se quedan donde los colocó el último paso; nada los vuelve a mover salvo que lo haga el script. Un nodo sin vehículo termina el script.
*Desde:* 3.00.
*Véase también:* <<vehiclenew,vehicleNew>>, <<bodydelete,bodyDelete>>
.Ejemplo
[source,lua]
----
function onCollision(a, b, x, y, z, speed)
if (a == car or b == car) and speed > 25 and not wrecked then
wrecked = true
vehicleDelete(car) -- El chasis sigue dando tumbos como cuerpo
soundPlay(crashSound)
emitterStart(smoke)
end
end
----
[#vehicleaddwheel]
==== vehicleAddWheel
[source,text]
----
index = vehicleAddWheel(node, wheelNode, radius, width, suspension)
----
Añade al vehículo una rueda en el lugar que ocupe `wheelNode` respecto al chasis la próxima vez que se construya el vehículo. La suspensión cuelga la rueda `suspension` unidades por debajo de ese punto sin carga y se comprime hasta la mitad. A partir de ahí el motor escribe la transformación en el mundo del nodo de la rueda tras cada paso, con X como eje de la rueda e Y hacia arriba, así que el neumático visible va en un nodo hijo: un `meshCylinder` girado 90 grados alrededor de Z. Las ruedas que quedan por delante de la posición media de las ruedas en la dirección del morro cuentan como delanteras: giran por defecto y no llevan freno de mano. Un tanque reparte sus ruedas entre la oruga izquierda y la derecha según el lado del chasis en que estén y necesita al menos una a cada lado. Los valores por debajo de `0.001` se suben a ese valor. Termina el script con la rueda número 17.
*Parámetros:*
* `node` -- el nodo del chasis.
* `wheelNode` -- el nodo del que cuelga la rueda; su posición respecto al chasis es el punto de anclaje, y a partir de ahora lo coloca el motor.
* `radius` -- el radio de la rueda, en unidades del mundo.
* `width` -- el ancho de la rueda, en unidades del mundo.
* `suspension` -- la longitud de la suspensión totalmente extendida, en unidades del mundo; se comprime hasta la mitad.
*Devuelve:* El índice de la rueda, desde `0`, para `vehicleSetWheel`, `vehicleIsWheelOnGround` y `vehicleGetWheelSlip`.
*Notas:* Un vehículo solo se construye cuando tiene dos ruedas, y un coche o una moto necesitan además una rueda motriz; hasta entonces es un cuerpo normal y las llamadas de conducción no hacen nada.
*Desde:* 3.00.
*Véase también:* <<vehiclesetwheel,vehicleSetWheel>>, <<vehiclesetsuspension,vehicleSetSuspension>>, <<nodefind,nodeFind>>
.Ejemplo
[source,lua]
----
-- El modelo del camión lleva nodos vacíos con el nombre de sus ruedas.
local truck = modelInstance(truckModel)
local wheels = {}
nodeSetPosition(truck, 0, 1.2, -8)
bodyNew(truck, BODY_DYNAMIC, SHAPE_BOX, 2.4, 1.4, 6.0)
bodySetMass(truck, 4000)
vehicleNew(truck, VEHICLE_CAR)
for i, name in ipairs({ "wheelFL", "wheelFR", "wheelRL", "wheelRR" }) do
wheels[i] = vehicleAddWheel(truck, nodeFind(name, truck), 0.5, 0.35, 0.45)
end
----
[#vehiclesetthrust]
==== vehicleSetThrust
[source,text]
----
vehicleSetThrust(node, maxForce, x, y, z)
----
Ajusta la hélice de un barco: la fuerza con la que empuja a todo gas, por defecto `2000` newtons, y el punto del sistema de referencia del casco en el que empuja, por defecto `0, -0.2, 1`, justo debajo de la cubierta en la popa. El empuje va en la dirección de la proa y solo se produce mientras ese punto está bajo una superficie de agua; el mismo punto decide si actúan el timón y el freno. Una fuerza negativa se convierte en `0`. Los tipos con ruedas guardan los valores y los ignoran.
*Parámetros:*
* `node` -- el nodo del casco.
* `maxForce` -- el empuje a todo gas, en newtons.
* `x`, `y`, `z` -- dónde está la hélice, en el sistema de referencia del casco; la popa es +Z.
*Desde:* 3.00.
*Véase también:* <<vehiclesetrudder,vehicleSetRudder>>, <<bodysetwater,bodySetWater>>, <<bodysetbuoyancy,bodySetBuoyancy>>
.Ejemplo
[source,lua]
----
local raft = nodeNew()
nodeSetMesh(raft, meshBox(1.6, 0.3, 2.4), wood)
nodeSetPosition(raft, 1, 0.3, 4)
bodyNew(raft, BODY_DYNAMIC, SHAPE_BOX, 1.6, 0.3, 2.4)
bodySetMass(raft, 120)
bodySetBuoyancy(raft, 3.0) -- Flota alto en la piscina
vehicleNew(raft, VEHICLE_BOAT)
vehicleSetThrust(raft, 600, 0, -0.1, 1.2) -- Un fueraborda colgado de la popa
vehicleSetRudder(raft, 300)
----
[#vehiclesetwheel]
==== vehicleSetWheel
[source,text]
----
vehicleSetWheel(node, index, steered, driven)
----
Indica si una rueda gira con la entrada de dirección y si el motor la mueve, sustituyendo los valores por defecto de esa rueda: las ruedas delanteras giran, y todas las ruedas son motrices salvo la delantera de una moto. Las ruedas motrices se emparejan de un lado a otro del chasis en diferenciales, uno por eje, y el par del motor se reparte a partes iguales entre los ejes; una rueda motriz sin pareja es un eje por sí sola. Un tanque ignora los dos valores, porque sus orugas mueven todas las ruedas y hacen girar el casco. Surte efecto en la reconstrucción del siguiente paso. Termina el script para un índice que no corresponde a ninguna rueda del vehículo.
*Parámetros:*
* `node` -- el nodo del chasis.
* `index` -- el índice de la rueda de `vehicleAddWheel`.
* `steered` -- `true` para que la rueda gire hasta el ángulo de `vehicleSetSteering` con la dirección al máximo.
* `driven` -- `true` para que el motor la haga girar.
*Desde:* 3.00.
*Véase también:* <<vehicleaddwheel,vehicleAddWheel>>, <<vehiclesetsteering,vehicleSetSteering>>, <<vehiclesetantiroll,vehicleSetAntiRoll>>
.Ejemplo
[source,lua]
----
-- Un deportivo de propulsión trasera: las ruedas delanteras solo giran.
vehicleSetWheel(car, wheels[1], true, false)
vehicleSetWheel(car, wheels[2], true, false)
vehicleSetWheel(car, wheels[3], false, true)
vehicleSetWheel(car, wheels[4], false, true)
if fourWheelSteer then
vehicleSetWheel(car, wheels[3], true, true)
vehicleSetWheel(car, wheels[4], true, true)
end
----
[#vehiclesetengine]
==== vehicleSetEngine
[source,text]
----
vehicleSetEngine(node, maxTorque, maxRpm)
vehicleSetEngine(node, maxTorque, maxRpm, minRpm)
----
Ajusta el motor: el par máximo en newton metros, y el régimen máximo y el ralentí en revoluciones por minuto. Los valores por defecto son `500`, `6000` y `1000`, y el ralentí vuelve a ser `1000` cuando se omite. El par se mantiene en `1` o más, el régimen máximo en `100` o más y el ralentí entre `1` y el régimen máximo. Un barco no tiene motor e ignora la llamada. Surte efecto en la reconstrucción del siguiente paso.
*Parámetros:*
* `node` -- el nodo del chasis.
* `maxTorque` -- par máximo, en newton metros.
* `maxRpm` -- el régimen máximo, en revoluciones por minuto.
* `minRpm` -- el ralentí, en revoluciones por minuto; por defecto `1000`.
*Desde:* 3.00.
*Véase también:* <<vehiclesetgears,vehicleSetGears>>, <<vehiclegetrpm,vehicleGetRpm>>
.Ejemplo
[source,lua]
----
vehicleSetEngine(tank, 1500, 4000, 800) -- Un diésel: par a bajas vueltas
vehicleSetEngine(bike, 150, 9000, 1200) -- Uno que sube mucho de vueltas
function onOverlayUpdate()
local rpm = vehicleGetRpm(bike)
overlayClear()
fontPrint(20, 20, string.format("%5.0f rpm", rpm))
if rpm > 8500 then
spriteDraw(shiftLight, 200, 20) -- Hora de subir de marcha
end
return OVERLAY_UPDATED
end
----
[#vehiclesetgears]
==== vehicleSetGears
[source,text]
----
vehicleSetGears(node, { ratio, ... })
vehicleSetGears(node, { ratio, ... }, reverseRatio)
vehicleSetGears(node, { ratio, ... }, reverseRatio, automatic)
----
Ajusta la caja de cambios: de una a ocho relaciones hacia delante, la primera marcha en primer lugar, la relación de la marcha atrás (por defecto `2.9`, dada como número positivo) y si la caja cambia sola (por defecto `true`). Un vehículo que nunca recibe esta llamada usa la de cinco marchas de Jolt, `2.66`, `1.78`, `1.3`, `1.0` y `0.74`, con la misma marcha atrás. Con `automatic` a falso la caja ya no cambia sola, y no hay ninguna llamada para cambiar de marcha desde el script. Un barco no tiene caja de cambios e ignora la llamada. Surte efecto en la reconstrucción del siguiente paso. Termina el script cuando la tabla está vacía o contiene más de ocho relaciones.
*Parámetros:*
* `node` -- el nodo del chasis.
* `{ ratio, ... }` -- relaciones de las marchas hacia delante, vueltas del motor por vuelta de la caja, la primera marcha en primer lugar; de una a ocho.
* `reverseRatio` -- la relación de la marcha atrás; por defecto `2.9`.
* `automatic` -- `true` (por defecto) para que la caja cambie sola.
*Desde:* 3.00.
*Véase también:* <<vehiclegetgear,vehicleGetGear>>, <<vehiclesetengine,vehicleSetEngine>>
.Ejemplo
[source,lua]
----
-- Una caja de seis marchas cerradas para el coche de carreras, una de tres marchas largas para el autobús.
vehicleSetGears(raceCar, { 3.2, 2.3, 1.8, 1.4, 1.1, 0.9 }, 3.0)
vehicleSetGears(bus, { 2.4, 1.3, 0.8 })
function onOverlayUpdate()
overlayClear()
fontPrint(20, 20, string.format("Gear %d %.0f rpm", vehicleGetGear(raceCar), vehicleGetRpm(raceCar)))
return OVERLAY_UPDATED
end
----
[#vehiclesetrudder]
==== vehicleSetRudder
[source,text]
----
vehicleSetRudder(node, maxTorque)
----
Ajusta el timón de un barco: el par alrededor del eje vertical del casco con la dirección al máximo, por defecto `800` newton metros. El timón agarra según la velocidad, una quinta parte del par en reposo y todo él a partir de 3 unidades del mundo por segundo, y gira al revés mientras el barco avanza de popa. Solo actúa mientras el punto de la hélice está bajo el agua. Un par negativo se convierte en `0`; los tipos con ruedas ignoran la llamada.
*Desde:* 3.00.
*Véase también:* <<vehiclesetthrust,vehicleSetThrust>>, <<vehicledrive,vehicleDrive>>
.Ejemplo
[source,lua]
----
vehicleNew(patrolBoat, VEHICLE_BOAT)
vehicleSetThrust(patrolBoat, 4000, 0, -0.4, 3.5)
vehicleSetRudder(patrolBoat, 2500) -- Casco pesado, timón grande
function onOverlayUpdate()
local helm = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_X) / 32767
local gas = controllerGetAxis(0, GAMEPAD_AXIS_RIGHT_TRIGGER) / 32767
vehicleDrive(patrolBoat, gas, helm, 0)
return OVERLAY_NOT_UPDATED
end
----
[#vehiclesetsuspension]
==== vehicleSetSuspension
[source,text]
----
vehicleSetSuspension(node, frequency, damping)
----
Ajusta el muelle de todas las ruedas: su frecuencia natural en hercios (por defecto `1.5`, mantenida en `0.1` o más; cuanto más dura, más alta) y su coeficiente de amortiguación (por defecto `0.5`; `0` rebota, `1` se asienta sin pasarse). Surte efecto en la reconstrucción del siguiente paso, en todas las ruedas a la vez; no hay muelle por rueda. Un barco no tiene suspensión e ignora la llamada.
*Parámetros:*
* `node` -- el nodo del chasis.
* `frequency` -- la frecuencia natural del muelle, en hercios.
* `damping` -- el coeficiente de amortiguación, de `0` en adelante.
*Desde:* 3.00.
*Véase también:* <<vehicleaddwheel,vehicleAddWheel>>, <<vehiclesetantiroll,vehicleSetAntiRoll>>
.Ejemplo
[source,lua]
----
-- El buggy flota sobre las dunas; el coche de carreras apenas se mueve sobre sus muelles.
vehicleSetSuspension(buggy, 1.0, 0.3)
vehicleSetSuspension(raceCar, 3.0, 0.7)
if surface == "gravel" then
vehicleSetSuspension(raceCar, 2.0, 0.5)
end
----
[#vehiclesetsteering]
==== vehicleSetSteering
[source,text]
----
vehicleSetSteering(node, maxDegrees)
----
Ajusta cuánto giran las ruedas directrices con la dirección al máximo, en grados; por defecto `35`, limitado de `0` a `89`. Una entrada de dirección de `-1` o `1` de `vehicleDrive` alcanza este ángulo, y las entradas menores una parte proporcional. Surte efecto en la reconstrucción del siguiente paso. Un tanque gira con sus orugas y un barco con su timón, así que los dos ignoran la llamada.
*Desde:* 3.00.
*Véase también:* <<vehiclesetwheel,vehicleSetWheel>>, <<vehicledrive,vehicleDrive>>
.Ejemplo
[source,lua]
----
-- El kart gira en un palmo; el camión necesita un arco amplio.
vehicleSetSteering(kart, 45)
vehicleSetSteering(truck, 25)
vehicleSetSteering(bus, 20)
if difficulty == "arcade" then
vehicleSetSteering(truck, 35) -- Manejo indulgente
end
----
[#vehiclesetbrakes]
==== vehicleSetBrakes
[source,text]
----
vehicleSetBrakes(node, brakeTorque, handBrakeTorque)
----
Ajusta el par de frenado que cada rueda puede aplicar con el pedal a fondo, por defecto `1500` newton metros, y el par del freno de mano en las ruedas traseras, por defecto `4000`; las ruedas delanteras, las que están por delante de la posición media de las ruedas, no tienen freno de mano. Los valores negativos se convierten en `0`. Un tanque frena sus orugas con los valores por defecto de Jolt para orugas e ignora la llamada, y un barco frena oponiendo resistencia a su propia velocidad, así que también la ignora. Surte efecto en la reconstrucción del siguiente paso.
*Parámetros:*
* `node` -- el nodo del chasis.
* `brakeTorque` -- par del freno de pie en cada rueda, en newton metros.
* `handBrakeTorque` -- par del freno de mano en las ruedas traseras, en newton metros.
*Desde:* 3.00.
*Véase también:* <<vehicledrive,vehicleDrive>>, <<vehiclegetwheelslip,vehicleGetWheelSlip>>
.Ejemplo
[source,lua]
----
vehicleSetBrakes(car, 2200, 6000) -- Pastillas de carreras y un freno de mano listo para derrapar
function onOverlayUpdate()
local brake = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_TRIGGER) / 32767
local handBrake = controllerGetButton(0, GAMEPAD_0.BUTTON_B.value) and 1 or 0
vehicleDrive(car, throttle(), steering(), brake, handBrake)
return OVERLAY_NOT_UPDATED
end
----
[#vehiclesetantiroll]
==== vehicleSetAntiRoll
[source,text]
----
vehicleSetAntiRoll(node, stiffness)
----
Añade una barra antivuelco en cada eje, en newtons por metro; `0` (por defecto) no pone ninguna y un valor negativo se convierte en `0`. Una barra une las dos ruedas motrices de un eje, así que un eje cuyas ruedas no son motrices no recibe barra, y una moto no tiene ninguna. Unos cientos mantienen un coche plano en las curvas; demasiado levanta las ruedas interiores. Un tanque ignora la llamada. Surte efecto en la reconstrucción del siguiente paso.
*Desde:* 3.00.
*Véase también:* <<vehiclesetsuspension,vehicleSetSuspension>>, <<vehiclesetwheel,vehicleSetWheel>>
.Ejemplo
[source,lua]
----
local car = nodeNew()
nodeSetMesh(car, meshBox(1.8, 0.5, 4.0), paint)
nodeSetPosition(car, 0, 0.9, 0)
bodyNew(car, BODY_DYNAMIC, SHAPE_BOX, 1.8, 0.5, 4.0)
bodySetMass(car, 1500)
vehicleNew(car, VEHICLE_CAR)
addFourWheels(car)
vehicleSetEngine(car, 600, 6500, 1000)
vehicleSetAntiRoll(car, 800) -- Se mantiene plano en la chicane
----
[#vehicledrive]
==== vehicleDrive
[source,text]
----
vehicleDrive(node, forward, right)
vehicleDrive(node, forward, right, brake)
vehicleDrive(node, forward, right, brake, handBrake)
----
Fija la entrada del conductor, que se mantiene hasta la siguiente llamada, así que llámala en cada fotograma desde `onOverlayUpdate`. El acelerador y la dirección van de `-1` a `1`, el freno y el freno de mano de `0` a `1`; los valores fuera de rango se limitan y las entradas omitidas valen `0`. Un coche o una moto reciben las cuatro como entrada del conductor de Jolt, y con un acelerador negativo retroceden engranando la marcha atrás. Un tanque toma `forward` como su acelerador y `right` frena la oruga interior; sin acelerador las orugas giran una contra otra para que el casco pivote sobre sí mismo, y el freno de mano se ignora. Un barco toma `forward` como acelerador de la hélice, `right` como timón y `brake` como una resistencia contra la velocidad del casco, ignora el freno de mano y no hace nada en absoluto mientras la hélice está fuera del agua. Cualquier entrada distinta de cero despierta un chasis que se ha dormido.
*Parámetros:*
* `node` -- el nodo del chasis.
* `forward` -- acelerador, de `-1` a `1`; negativo va marcha atrás.
* `right` -- dirección, de `-1` (izquierda) a `1` (derecha).
* `brake` -- freno de pie, de `0` a `1`; por defecto `0`.
* `handBrake` -- freno de mano, de `0` a `1`; por defecto `0`.
*Desde:* 3.00.
*Véase también:* <<vehiclegetspeed,vehicleGetSpeed>>, <<vehiclesetbrakes,vehicleSetBrakes>>, <<controllergetaxis,controllerGetAxis>>
.Ejemplo
[source,lua]
----
function onOverlayUpdate()
local stick = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_X)
local steer = (math.abs(stick) > SINGE_DEAD_ZONE) and stick / 32767 or 0
local gas = controllerGetAxis(0, GAMEPAD_AXIS_RIGHT_TRIGGER) / 32767
local brake = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_TRIGGER) / 32767
local handBrake = controllerGetButton(0, GAMEPAD_0.BUTTON_B.value) and 1 or 0
if controllerGetButton(0, GAMEPAD_0.BUTTON_A.value) then
gas = -gas -- Marcha atrás mientras se mantiene A
end
vehicleDrive(car, gas, steer, brake, handBrake)
return OVERLAY_NOT_UPDATED
end
----
[#vehiclegetspeed]
==== vehicleGetSpeed
[source,text]
----
speed = vehicleGetSpeed(node)
----
La velocidad del cuerpo del chasis en la dirección del morro, en unidades del mundo por segundo, negativa mientras va marcha atrás; el movimiento lateral en un derrape no cuenta. Funciona con todos los tipos, barcos incluidos, desde el momento en que se llama a `vehicleNew`.
*Devuelve:* Un número.
*Desde:* 3.00.
*Véase también:* <<vehiclegetrpm,vehicleGetRpm>>, <<vehiclegetgear,vehicleGetGear>>, <<bodygetvelocity,bodyGetVelocity>>
.Ejemplo
[source,lua]
----
function onOverlayUpdate()
local speed = vehicleGetSpeed(car)
overlayClear()
fontPrint(20, 20, string.format("%3.0f km/h", math.abs(speed) * 3.6))
if speed < -1 then
spriteDraw(reversingLamp, 300, 20)
end
return OVERLAY_UPDATED
end
----
[#vehiclegetrpm]
==== vehicleGetRpm
[source,text]
----
rpm = vehicleGetRpm(node)
----
El régimen del motor en revoluciones por minuto, entre el ralentí y el régimen máximo de `vehicleSetEngine`, para un cuentarrevoluciones o el tono de un bucle de sonido del motor. `0` para un barco, que no tiene motor, y para un vehículo que aún no se ha construido porque le faltan ruedas.
*Devuelve:* Un número.
*Desde:* 3.00.
*Véase también:* <<vehiclesetengine,vehicleSetEngine>>, <<vehiclegetgear,vehicleGetGear>>
.Ejemplo
[source,lua]
----
-- Una barra de cuentarrevoluciones que se pone roja por encima de 6000.
function onOverlayUpdate()
local rpm = vehicleGetRpm(car)
overlayClear()
if rpm > 6000 then
colorForeground(255, 40, 40)
else
colorForeground(60, 220, 60)
end
overlayBox(20, 20, 20 + math.floor(rpm / 6500 * 200), 32)
return OVERLAY_UPDATED
end
----
[#vehiclegetgear]
==== vehicleGetGear
[source,text]
----
gear = vehicleGetGear(node)
----
La marcha que tiene engranada la caja: `0` en punto muerto, `-1` en marcha atrás, de `1` en adelante para las marchas hacia delante en el orden en que las dio `vehicleSetGears`. `0` para un barco y para un vehículo que aún no se ha construido.
*Devuelve:* Un entero.
*Desde:* 3.00.
*Véase también:* <<vehiclesetgears,vehicleSetGears>>, <<vehiclegetrpm,vehicleGetRpm>>
.Ejemplo
[source,lua]
----
local lastGear = 0
function onOverlayUpdate()
local gear = vehicleGetGear(car)
if gear ~= lastGear then
soundPlay(gearChange)
lastGear = gear
end
overlayClear()
fontPrint(20, 20, (gear == -1) and "R" or ((gear == 0) and "N" or tostring(gear)))
return OVERLAY_UPDATED
end
----
[#vehicleiswheelonground]
==== vehicleIsWheelOnGround
[source,text]
----
touching = vehicleIsWheelOnGround(node, index)
----
Si el lanzamiento de la suspensión de esa rueda ha encontrado algo contra lo que apoyarse en este paso. `false` para un índice que no corresponde a ninguna rueda del vehículo y para un vehículo que aún no se ha construido; en ninguno de los dos casos se produce un error.
*Devuelve:* Verdadero o falso.
*Desde:* 3.00.
*Véase también:* <<vehicleaddwheel,vehicleAddWheel>>, <<vehiclegetwheelslip,vehicleGetWheelSlip>>
.Ejemplo
[source,lua]
----
-- En el aire cuando ninguna rueda toca el suelo; aterrizar tras un salto sacude la cámara.
function onOverlayUpdate()
local airborne = true
for i = 0, 3 do
airborne = airborne and not vehicleIsWheelOnGround(car, i)
end
if wasAirborne and not airborne then
cameraShake = 12
end
wasAirborne = airborne
return OVERLAY_NOT_UPDATED
end
----
[#vehiclegetwheelslip]
==== vehicleGetWheelSlip
[source,text]
----
slip = vehicleGetWheelSlip(node, index)
----
Cuánto patina o derrapa la rueda en su dirección de rodadura, de `0` con agarre a `1` patinando o bloqueada, para el chirrido de los neumáticos, el humo y las marcas de frenada. Siempre `0` para las ruedas de un tanque, para un índice que no corresponde a ninguna rueda del vehículo y para un vehículo que aún no se ha construido.
*Devuelve:* Un número.
*Desde:* 3.00.
*Véase también:* <<vehicleiswheelonground,vehicleIsWheelOnGround>>, <<vehiclegetspeed,vehicleGetSpeed>>
.Ejemplo
[source,lua]
----
function onOverlayUpdate()
local slip = math.max(vehicleGetWheelSlip(car, 2), vehicleGetWheelSlip(car, 3))
if slip > 0.4 and vehicleIsWheelOnGround(car, 2) then
emitterStart(tireSmoke)
if not soundIsPlaying(squealChannel) then
squealChannel = soundPlay(squeal)
end
else
emitterStop(tireSmoke)
end
return OVERLAY_NOT_UPDATED
end
----
[#view]
=== Vista
Una vista es una segunda cámara que se renderiza en una textura en cada fotograma, para un monitor, un espejo o un portal en la escena: `viewNew` crea una y devuelve un handle entero, `viewSetCamera` la asocia a un nodo y `materialSetView` la muestra en un material. Puede haber hasta cuatro a la vez; cada una vuelve a renderizar la escena entera a su propio tamaño, en píxeles independientes del overlay, con la proyección de la cámara principal, las sombras del fotograma (incluidas las cascadas de una luz direccional: cada punto toma la cascada más fina que lo cubre) y sin bloom, así que usa pocas y pequeñas. Cada vista orienta los billboards (`nodeSetBillboard`) hacia su propia cámara y ordena los dibujos con mezcla de atrás adelante desde ella, así que una vista cuya cámara mira desde otro sitio sigue viendo los billboards de frente y los objetos transparentes en el orden correcto. Un handle erróneo termina el script. Véase <<scenes3d,Escenas 3D>>.
[#viewdelete]
==== viewDelete
[source,text]
----
viewDelete(view)
----
Libera la vista y sus texturas, y detiene la pasada de renderizado adicional que costaba. Los materiales que la mostraban vuelven a su propia textura. Después, el handle no es válido y termina el script si se vuelve a usar; como solo puede haber cuatro vistas a la vez, borra las que un nivel ya no necesite.
*Desde:* 3.00.
*Véase también:* <<viewnew,viewNew>>, <<materialsetview,materialSetView>>
.Ejemplo
[source,lua]
----
-- Los monitores de la sala de seguridad solo se renderizan mientras el jugador está en ella.
function onEnterSecurityRoom()
feed = viewNew(512, 384)
viewSetCamera(feed, hallwayCamera)
materialSetView(monitorLook, feed)
end
function onLeaveSecurityRoom()
viewDelete(feed)
feed = nil
end
----
[#viewnew]
==== viewNew
[source,text]
----
view = viewNew(width, height)
----
Crea una cámara que se renderiza en una textura de `width` por `height` en cada fotograma (cada uno limitado de `1` a `4096`), desde la vista por defecto hasta que `viewSetCamera` indique un nodo, y devuelve el handle de la vista para `materialSetView`. La vista usa la proyección de `cameraSetPerspective` o `cameraSetOrthographic` con su propia relación de aspecto. La escena se renderiza una vez más por cada vista, así que `512` por `384` basta de sobra para un monitor y una Raspberry Pi necesita menos. Termina el script cuando el 3D no está disponible o ya hay cuatro vistas en uso.
*Parámetros:*
* `width` -- el ancho de la textura en píxeles.
* `height` -- el alto de la textura en píxeles.
*Devuelve:* El handle de la vista, un entero.
*Desde:* 3.00.
*Véase también:* <<viewsetcamera,viewSetCamera>>, <<materialsetview,materialSetView>>, <<viewdelete,viewDelete>>
.Ejemplo
[source,lua]
----
-- Un monitor de seguridad que muestra una cámara situada detrás del jugador.
eye = nodeNew()
nodeSetPosition(eye, 0, 2.2, -5)
nodeLookAt(eye, 0, 0.6, 0)
feed = viewNew(512, 384)
viewSetCamera(feed, eye)
screen = materialNew()
materialSetView(screen, feed)
materialSetUnlit(screen, true)
nodeSetMesh(monitor, meshPlane(2, 1.5), screen)
nodeSetRotation(monitor, 90, 0, 0)
----
[#viewsetcamera]
==== viewSetCamera
[source,text]
----
viewSetCamera(view, node)
viewSetCamera(view, nil)
viewSetCamera(view)
----
El nodo desde el que mira la vista, a lo largo de su propio -Z como la cámara principal, así que cuenta el movimiento de sus padres. Con `nil` o sin segundo argumento la vista vuelve a la vista por defecto desde `(0, 0, 5)` mirando al origen. Emparenta el nodo con un coche para tener un retrovisor, o refleja la posición de la cámara principal respecto a una pared para tener un espejo en ella. Termina el script si el handle de nodo no es válido.
*Desde:* 3.00.
*Véase también:* <<viewnew,viewNew>>, <<cameraset,cameraSet>>, <<nodelookat,nodeLookAt>>
.Ejemplo
[source,lua]
----
-- El monitor del vestíbulo va pasando por tres cámaras de seguridad.
cameras = { lobbyCamera, garageCamera, roofCamera }
current = 1
function onOverlayUpdate()
if singeGetTicks() - lastSwitch > 4000 then
current = current % #cameras + 1
lastSwitch = singeGetTicks()
viewSetCamera(feed, cameras[current])
end
return OVERLAY_UPDATED
end
----
[#video]
=== Vídeo
La familia `video*` se ocupa de los clips de vídeo adicionales superpuestos al disco: un personaje que habla en una esquina, una animación prerrenderizada en un momento de la historia, varias secuencias a la vez. Con `videoLoad` se puede cargar cualquier número de vídeos, y cada uno devuelve un handle entero opaco que todas las demás funciones `video*` reciben como primer argumento; un handle que ya se ha descargado termina el script. Los vídeos se dibujan en el overlay con `videoDraw`, en coordenadas del overlay, y se componen como los sprites, y un vídeo solo avanza mientras se dibuja (o se usa como material mediante <<materialsetvideo,materialSetVideo>>). Cada vídeo tiene su propia selección de pista de audio y su propio volumen estéreo, independientes del disco. Igual que los sprites, los vídeos deben dibujarse desde `onOverlayUpdate` y liberarse al final con `videoUnload`; las reglas de dibujo están en <<overlay,Overlay>>.
[#videodraw]
==== videoDraw
[source,text]
----
videoDraw(id, x, y, x2, y2)
videoDraw(id, x, y, centered)
----
Avanza un vídeo hasta el fotograma que pide su reloj y dibuja ese fotograma en el overlay. El reloj sigue el audio del vídeo si lo tiene y el tiempo de reloj si no, así que una llamada puede avanzar cero, uno o varios fotogramas; un vídeo que llega al final vuelve al fotograma `0`. La forma de cinco argumentos estira el fotograma en el rectángulo que va de `(x, y)` a `(x2, y2)`, ambos incluidos, siempre con muestreo por vecino más cercano y sin tener en cuenta `videoScale`, `videoRotate` ni `videoQuality`. La forma de cuatro argumentos dibuja el fotograma a su tamaño nativo transformado por `videoScale` y `videoRotate`, anclado por su esquina superior izquierda o, cuando `centered` es `true`, por su centro; el fotograma transformado solo se reconstruye cuando cambia el fotograma o la transformación, y las esquinas que deja el giro son transparentes. Si el decodificador aún no ha producido ningún fotograma, no se dibuja nada.
Un fotograma que `videoScale` o `videoRotate` no escalan ni giran, dibujado en el overlay donde no se ha dibujado nada desde un `overlayClear` a un color transparente -- un vídeo de fondo dibujado el primero, por ejemplo -- lo dibuja en cambio la GPU, bajo el overlay, y no cuesta casi nada; todo lo que se dibuje después sigue quedando encima. En ese caso se escala a la pantalla igual que el overlay, en lugar de pasar antes al overlay por vecino más cercano. En cualquier otro caso el fotograma se copia en el overlay en la CPU, lo que en una máquina pequeña cuesta varios milisegundos por fotograma, así que dibuja un vídeo antes de lo que vaya encima de él y no después.
*Parámetros:*
* `id` -- handle de vídeo devuelto por `videoLoad`.
* `x`, `y` -- punto de anclaje en coordenadas del overlay.
* `x2`, `y2` -- esquina opuesta del rectángulo de destino, incluida.
* `centered` -- booleano. `true` coloca el centro del fotograma dibujado en `(x, y)`, `false` su esquina superior izquierda. Usa `true` para los vídeos girados.
*Notas:* Un vídeo que no se dibuja no avanza, así que la pausa es implícita mientras un clip está oculto. Dibuja solo desde `onOverlayUpdate`.
*Desde:* 2.00. La forma escalada/girada con el parámetro `centered` se añadió en 2.10.
*Véase también:* <<videoload,videoLoad>>, <<videoscale,videoScale>>, <<videorotate,videoRotate>>, <<spritedraw,spriteDraw>>
.Ejemplo
[source,lua]
----
-- Clip del modo de atracción en un recuadro fijo, en bucle entre sus propios fotogramas de inicio y fin.
function onOverlayUpdate()
overlayClear()
videoDraw(attractVideo, VIDEO_X, VIDEO_Y, VIDEO_X + VIDEO_W - 1, VIDEO_Y + VIDEO_H - 1)
if videoGetFrame(attractVideo) > ATTRACT_END then
videoSeek(attractVideo, ATTRACT_START)
end
return OVERLAY_UPDATED
end
----
[#videogetaudiotrack]
==== videoGetAudioTrack
[source,text]
----
track = videoGetAudioTrack(id)
----
Devuelve el índice, empezando en cero, de la pista de audio que está reproduciendo un vídeo cargado. `videoLoad` selecciona la pista indicada por `--audio` o por la entrada `AUDIO_TRACK` de `games.dat` (por defecto `0`) si el archivo tiene tantas pistas; un vídeo sin ningún audio devuelve `-1`.
*Devuelve:* índice de pista, entero.
*Desde:* 2.10
*Véase también:* <<videosetaudiotrack,videoSetAudioTrack>>, <<videogetaudiotracks,videoGetAudioTracks>>, <<videogetlanguage,videoGetLanguage>>
.Ejemplo
[source,lua]
----
-- Muestra en el overlay de opciones qué doblaje usa la cinemática.
function drawAudioOption()
local track = videoGetAudioTrack(cutscene)
local label = "None"
if track >= 0 then
label = videoGetLanguageDescription(videoGetLanguage(cutscene, track))
end
overlayPrint(OPTION_X, OPTION_Y, "Cutscene audio: " .. label)
end
----
[#videogetaudiotracks]
==== videoGetAudioTracks
[source,text]
----
count = videoGetAudioTracks(id)
----
Devuelve cuántos flujos de audio contiene un vídeo cargado. Un clip mudo devuelve `0`; protege frente a ese caso los bucles y la aritmética de módulo.
*Devuelve:* número entero, `0` o más.
*Desde:* 2.10
*Véase también:* <<videogetlanguage,videoGetLanguage>>, <<videosetaudiotrack,videoSetAudioTrack>>, <<videogetaudiotrack,videoGetAudioTrack>>
.Ejemplo
[source,lua]
----
-- Solo ofrece el menú de idioma cuando la cinemática tiene de verdad entre qué elegir.
cutscene = videoLoad("videos/briefing.mkv")
if videoGetAudioTracks(cutscene) > 1 then
buildLanguageMenu(cutscene)
else
languageMenuEnabled = false
end
----
[#videogetframe]
==== videoGetFrame
[source,text]
----
frame = videoGetFrame(id)
----
Devuelve el fotograma en el que está situado un vídeo cargado, empezando en cero. La posición solo cambia mientras el vídeo se reproduce y se dibuja, y vuelve a `0` cuando el clip da la vuelta. Compárala con tus propias marcas de inicio y fin para repetir un tramo, o con `videoGetFrameCount` para detectar el final de un clip que se reproduce una sola vez.
*Devuelve:* número de fotograma, entero.
*Desde:* 2.00
*Véase también:* <<videoseek,videoSeek>>, <<videogetframecount,videoGetFrameCount>>, <<videodraw,videoDraw>>
.Ejemplo
[source,lua]
----
-- Devuelve el control al jugador cuando la presentación de la misión se ha reproducido una vez.
function onOverlayUpdate()
if briefingRunning then
videoDraw(briefing, 0, 0, false)
if videoGetFrame(briefing) >= videoGetFrameCount(briefing) - 1 then
briefingRunning = false
videoPause(briefing)
end
end
return OVERLAY_UPDATED
end
----
[#videogetframecount]
==== videoGetFrameCount
[source,text]
----
count = videoGetFrameCount(id)
----
Devuelve el número total de fotogramas de un vídeo cargado, contados a partir de la tabla de fotogramas del archivo cuando se indexó. Los fotogramas van de `0` a `count - 1`. Úsala para barras de progreso, para cerrar un bucle o para detectar cuándo está a punto de terminar un clip que se reproduce una sola vez.
*Devuelve:* número de fotogramas, entero.
*Desde:* 2.00
*Véase también:* <<videogetframe,videoGetFrame>>, <<videoseek,videoSeek>>, <<videoisplaying,videoIsPlaying>>
.Ejemplo
[source,lua]
----
-- Línea de progreso bajo un clip de carga: un marco de contorno y un relleno que crece.
function drawProgress()
local total = videoGetFrameCount(loadingClip)
local done = videoGetFrame(loadingClip) / (total - 1)
colorForeground(255, 255, 255, 255)
overlayBox(BAR_X - 1, BAR_Y - 1, BAR_X + BAR_W + 1, BAR_Y + 1)
overlayLine(BAR_X, BAR_Y, BAR_X + BAR_W * done, BAR_Y)
end
----
[#videogetheight]
==== videoGetHeight
[source,text]
----
height = videoGetHeight(id)
----
Devuelve la altura en píxeles de los fotogramas de un vídeo cargado: el tamaño nativo del archivo, no el tamaño al que se dibuja. Combínala con `videoScale` para encajar un clip en un recuadro conservando su relación de aspecto.
*Devuelve:* altura en píxeles, entero.
*Desde:* 2.00
*Véase también:* <<videogetwidth,videoGetWidth>>, <<videoscale,videoScale>>
.Ejemplo
[source,lua]
----
-- Encaja el clip en una ventana de 320x240 sin deformarlo.
local fitX = 320 / videoGetWidth(clip)
local fitY = 240 / videoGetHeight(clip)
local fit = math.min(fitX, fitY)
videoScale(clip, fit)
----
[#videogetlanguage]
==== videoGetLanguage
[source,text]
----
code = videoGetLanguage(id, track)
----
Devuelve la etiqueta de idioma guardada en el archivo para una pista de audio de un vídeo cargado. La etiqueta es un código ISO 639 de tres letras, como `"eng"` o `"jpn"`; una pista sin etiqueta de tres letras devuelve `"unk"`. Una pista fuera de `0` a `videoGetAudioTracks(id) - 1` termina el script.
*Parámetros:*
* `id` -- handle de vídeo.
* `track` -- índice de la pista de audio, empezando en cero.
*Devuelve:* código de idioma, cadena.
*Desde:* 2.10
*Véase también:* <<videogetlanguagedescription,videoGetLanguageDescription>>, <<videogetaudiotracks,videoGetAudioTracks>>, <<videosetaudiotrack,videoSetAudioTrack>>
.Ejemplo
[source,lua]
----
-- Hace coincidir el doblaje de la cinemática con el idioma elegido para el disco.
local wanted = discGetLanguage(discGetAudioTrack())
for track = 0, videoGetAudioTracks(cutscene) - 1 do
if videoGetLanguage(cutscene, track) == wanted then
videoSetAudioTrack(cutscene, track)
break
end
end
----
[#videogetlanguagedescription]
==== videoGetLanguageDescription
[source,text]
----
name = videoGetLanguageDescription(code)
----
Busca el nombre en inglés de un código de idioma. A diferencia de las demás funciones `video*`, no recibe ningún handle; es una simple consulta a una tabla que acepta códigos ISO 639-1 de dos letras y códigos ISO 639-2 de tres letras (tanto la forma T como la B), comparados sin distinguir mayúsculas de minúsculas. Los códigos desconocidos, incluido el `"unk"` que devuelven las pistas sin etiqueta, dan `"Unknown"`. `Framework.singe` le da el alias `discGetLanguageDescription` por simetría con la API del disco.
*Parámetros:*
* `code` -- código de idioma, como `"en"`, `"eng"` o `"jpn"`.
*Devuelve:* nombre del idioma, cadena, o `"Unknown"`.
*Desde:* 2.10
*Véase también:* <<videogetlanguage,videoGetLanguage>>, <<discgetlanguage,discGetLanguage>>
.Ejemplo
[source,lua]
----
-- Construye un menú de selección de idioma a partir de las pistas de un vídeo.
options = {}
for track = 0, videoGetAudioTracks(cutscene) - 1 do
local code = videoGetLanguage(cutscene, track)
local name = videoGetLanguageDescription(code)
table.insert(options, { track = track, label = name })
end
----
[#videogetvolume]
==== videoGetVolume
[source,text]
----
left, right = videoGetVolume(id)
volume = videoGetVolume()
----
Devuelve el volumen estéreo de un vídeo cargado como dos porcentajes. Un vídeo recién cargado está al nivel de `--volume_nonvldp` en los dos canales (por defecto `100`). Ten en cuenta que vuelven dos valores, primero el izquierdo.
Llamada sin argumentos, es una extensión de Hypseus que responde en cambio con el volumen del propio laserdisc, como un único número en la escala de `0` a `64` de Hypseus y no como porcentaje. Sus juegos lo duplican para <<musicsetvolume,`musicSetVolume`>>, cuyo rango va de `0` a `128`. El nivel sigue a `--volume_vldp`.
*Devuelve:* dos enteros, izquierdo y derecho, cada uno de `0` a `100`; o, sin argumentos, un entero de `0` a `64`.
*Desde:* 2.00; la forma sin argumentos para el disco, 3.00
*Véase también:* <<videosetvolume,videoSetVolume>>, <<videoload,videoLoad>>
.Ejemplo
[source,lua]
----
-- Baja la cinemática mientras suena una frase de voz, y luego la restaura.
function playVoiceOver(sound)
savedLeft, savedRight = videoGetVolume(cutscene)
videoSetVolume(cutscene, savedLeft / 4, savedRight / 4)
soundPlay(sound)
duckingCutscene = true
end
----
[#videogetwidth]
==== videoGetWidth
[source,text]
----
width = videoGetWidth(id)
----
Devuelve la anchura en píxeles de los fotogramas de un vídeo cargado: el tamaño nativo del archivo, no el tamaño al que se dibuja.
*Devuelve:* anchura en píxeles, entero.
*Desde:* 2.00
*Véase también:* <<videogetheight,videoGetHeight>>, <<videoscale,videoScale>>
.Ejemplo
[source,lua]
----
-- Centra en la parte superior del overlay un clip dibujado a tamaño nativo.
local x = (overlayGetWidth() - videoGetWidth(clip)) / 2
function onOverlayUpdate()
videoDraw(clip, x, 16, false)
return OVERLAY_UPDATED
end
----
[#videoisplaying]
==== videoIsPlaying
[source,text]
----
playing = videoIsPlaying(id)
----
Indica si un vídeo cargado está en estado de reproducción, es decir, si se ha llamado a `videoPlay` desde el último `videoPause`. Aun así, un vídeo en reproducción solo avanza mientras se dibuja. Los vídeos empiezan en pausa después de `videoLoad`.
*Devuelve:* booleano.
*Desde:* 2.00
*Véase también:* <<videoplay,videoPlay>>, <<videopause,videoPause>>, <<videogetframe,videoGetFrame>>
.Ejemplo
[source,lua]
----
-- Una tecla activa y desactiva el clip de imagen en imagen.
function onInputPressed(what)
if what == SWITCH_BUTTON2 then
if videoIsPlaying(pipVideo) then
videoPause(pipVideo)
else
videoPlay(pipVideo)
end
end
end
----
[#videoload]
==== videoLoad
[source,text]
----
id = videoLoad(filename)
----
Abre un archivo de vídeo y devuelve su handle. Se acepta cualquier contenedor y códec que pueda decodificar el FFmpeg incluido. El motor construye o lee un índice de fotogramas en una carpeta de datos que lleva el nombre de la ubicación del vídeo bajo la raíz de datos de Singe, así que la primera carga de un archivo grande puede tardar un momento; no poder crear esa carpeta termina el script. El vídeo empieza en pausa en el fotograma `0`, con la pista de audio fijada a partir de `--audio` o de la entrada `AUDIO_TRACK` de `games.dat` (por defecto `0`) si el archivo tiene tantas pistas, y con los dos canales al nivel de `--volume_nonvldp`. Los fotogramas se decodifican a RGB para poder dibujarlos en el overlay.
*Parámetros:*
* `filename` -- ruta del archivo de vídeo, relativa a la carpeta del juego o dentro de un juego empaquetado.
*Devuelve:* handle de vídeo, entero.
*Desde:* 2.00
*Véase también:* <<videoplay,videoPlay>>, <<videodraw,videoDraw>>, <<videounload,videoUnload>>
.Ejemplo
[source,lua]
----
-- Carga el clip de atracción del juego seleccionado y lo sitúa en el inicio de su bucle.
function loadGameAssets()
attractVideo = videoLoad(GAME_LIST[GAME_SELECTED].ATTRACT)
if GAME_LIST[GAME_SELECTED].AUDIO_TRACK then
videoSetAudioTrack(attractVideo, GAME_LIST[GAME_SELECTED].AUDIO_TRACK)
end
videoPlay(attractVideo)
videoSeek(attractVideo, GAME_LIST[GAME_SELECTED].ATTRACT_START)
end
----
[#videopause]
==== videoPause
[source,text]
----
videoPause(id)
----
Pone en pausa un vídeo cargado. Su reloj se detiene, su pista de audio se pausa y `videoDraw` sigue dibujando el fotograma en el que se paró. Pausar un vídeo que ya está en pausa no hace ningún daño.
*Desde:* 2.00
*Véase también:* <<videoplay,videoPlay>>, <<videoisplaying,videoIsPlaying>>
.Ejemplo
[source,lua]
----
-- Congela el clip de atracción mientras está abierta la pantalla de calibración.
function calibrationBegin()
calibrating = true
if attractVideo then
videoPause(attractVideo)
end
end
----
[#videoplay]
==== videoPlay
[source,text]
----
videoPlay(id)
----
Inicia o reanuda un vídeo cargado desde su fotograma actual. El reloj se reinicia en ese fotograma, así que un vídeo reanudado tras una pausa larga continúa donde se paró en lugar de saltar hacia delante, y la pista de audio se reanuda con él. El vídeo solo avanza mientras se dibuja con `videoDraw` o se usa como material.
*Desde:* 2.00
*Véase también:* <<videopause,videoPause>>, <<videoseek,videoSeek>>, <<videodraw,videoDraw>>
.Ejemplo
[source,lua]
----
-- Inicia la presentación de la misión sobre el disco en pausa.
function startBriefing()
discPause()
videoSeek(briefing, 0)
videoPlay(briefing)
briefingRunning = true
end
----
[#videoquality]
==== videoQuality
[source,text]
----
videoQuality(id, smooth)
----
Selecciona el filtrado que se usa cuando la forma de cuatro argumentos de `videoDraw` escala o gira el vídeo. Cualquier valor distinto de cero selecciona `RENDER_SMOOTH` (bilineal); cero selecciona `RENDER_PIXELATED` (vecino más cercano), el valor por defecto. Un cambio marca el fotograma transformado para reconstruirlo en el siguiente dibujado. No afecta a la forma estirada de cinco argumentos, que siempre usa el vecino más cercano, ni a un vídeo sin transformar.
*Parámetros:*
* `id` -- handle de vídeo.
* `smooth` -- `RENDER_PIXELATED` (`0`) o `RENDER_SMOOTH` (`1`).
*Desde:* 2.10
*Véase también:* <<videoscale,videoScale>>, <<videorotate,videoRotate>>, <<videodraw,videoDraw>>
.Ejemplo
[source,lua]
----
-- Un clip fotográfico reducido en una esquina se ve mejor filtrado.
pipVideo = videoLoad("videos/copilot.mkv")
videoScale(pipVideo, 0.4)
videoQuality(pipVideo, RENDER_SMOOTH)
videoPlay(pipVideo)
----
[#videorotate]
==== videoRotate
[source,text]
----
videoRotate(id, degrees)
----
Fija el giro que aplica la forma de cuatro argumentos de `videoDraw`, en grados en el sentido de las agujas del reloj, con la misma convención que `spriteRotate`. El ángulo se reduce módulo `360` y conserva su signo, así que `450` y `90` son lo mismo y `-90` gira un cuarto de vuelta en sentido contrario a las agujas del reloj. Se conserva la escala fijada por `videoScale`. El giro se aplica alrededor del centro del fotograma, así que dibuja los vídeos girados con `centered` a `true`. Un ángulo distinto marca el fotograma transformado para reconstruirlo en el siguiente dibujado.
*Parámetros:*
* `id` -- handle de vídeo.
* `degrees` -- giro en grados, en el sentido de las agujas del reloj.
*Desde:* 2.10
*Véase también:* <<videorotateandscale,videoRotateAndScale>>, <<videoscale,videoScale>>, <<videodraw,videoDraw>>
.Ejemplo
[source,lua]
----
-- Hace girar despacio la imagen del radar mientras se reproduce.
function onOverlayUpdate()
radarAngle = radarAngle + 0.5
videoRotate(radarVideo, radarAngle)
videoDraw(radarVideo, RADAR_CENTER_X, RADAR_CENTER_Y, true)
return OVERLAY_UPDATED
end
----
[#videorotateandscale]
==== videoRotateAndScale
[source,text]
----
videoRotateAndScale(id, degrees, scale)
videoRotateAndScale(id, degrees, scaleX, scaleY)
----
Fija el giro y la escala en una sola llamada, con las mismas reglas que `videoRotate` y `videoScale`: grados en el sentido de las agujas del reloj reducidos módulo `360` y factores de escala que deben ser mayores que `0`. Úsala cuando los dos cambian en cada fotograma, para que el fotograma transformado se reconstruya una vez y no dos.
*Parámetros:*
* `id` -- handle de vídeo.
* `degrees` -- giro en grados, en el sentido de las agujas del reloj.
* `scale` -- factor de escala uniforme, `1.0` para el tamaño nativo.
* `scaleX`, `scaleY` -- factores horizontal y vertical por separado.
*Desde:* 2.10
*Véase también:* <<videorotate,videoRotate>>, <<videoscale,videoScale>>, <<videodraw,videoDraw>>
.Ejemplo
[source,lua]
----
-- Un clip da vueltas y se encoge mientras se descarta.
function onOverlayUpdate()
if dismissing then
dismissTime = dismissTime + 1
videoRotateAndScale(popupVideo, dismissTime * 6, 1.0 - dismissTime / 60)
videoDraw(popupVideo, POPUP_CENTER_X, POPUP_CENTER_Y, true)
end
return OVERLAY_UPDATED
end
----
[#videoscale]
==== videoScale
[source,text]
----
videoScale(id, scale)
videoScale(id, scaleX, scaleY)
----
Fija la escala que aplica la forma de cuatro argumentos de `videoDraw`, como factor del tamaño nativo; `1.0` es sin escalar. Se conserva el giro fijado por `videoRotate`. Una escala distinta marca el fotograma transformado para reconstruirlo en el siguiente dibujado. Los factores de `0` o menos no se pueden renderizar y abortan el motor en el siguiente `videoDraw`, así que mantén las escalas animadas por encima de cero. La forma estirada de cinco argumentos de `videoDraw` no tiene en cuenta este ajuste.
*Parámetros:*
* `id` -- handle de vídeo.
* `scale` -- factor de escala uniforme, mayor que `0`.
* `scaleX`, `scaleY` -- factores horizontal y vertical por separado, cada uno mayor que `0`.
*Desde:* 2.10
*Véase también:* <<videorotateandscale,videoRotateAndScale>>, <<videoquality,videoQuality>>, <<videogetwidth,videoGetWidth>>
.Ejemplo
[source,lua]
----
-- Muestra la cinemática a un cuarto de su tamaño en la esquina mientras la partida sigue.
pipVideo = videoLoad("videos/copilot.mkv")
videoScale(pipVideo, 0.25)
videoPlay(pipVideo)
function onOverlayUpdate()
videoDraw(pipVideo, PIP_X, PIP_Y, false)
return OVERLAY_UPDATED
end
----
[#videoseek]
==== videoSeek
[source,text]
----
videoSeek(id, frame)
----
Sitúa un vídeo cargado en `frame` y reinicia allí su reloj. El estado de reproducción o de pausa no cambia: un vídeo en reproducción continúa desde el nuevo fotograma en su siguiente dibujado, y uno en pausa lo muestra. El número de fotograma da la vuelta módulo el número de fotogramas, así que `-1` es el último fotograma y un valor más allá del final vuelve a caer dentro del clip.
*Parámetros:*
* `id` -- handle de vídeo.
* `frame` -- fotograma de destino, empezando en cero.
*Desde:* 2.00
*Véase también:* <<videogetframe,videoGetFrame>>, <<videogetframecount,videoGetFrameCount>>, <<videoplay,videoPlay>>
.Ejemplo
[source,lua]
----
-- Repite el clip de atracción entre las dos marcas de games.dat.
function onOverlayUpdate()
videoDraw(attractVideo, VIDEO_X, VIDEO_Y, VIDEO_X + VIDEO_W - 1, VIDEO_Y + VIDEO_H - 1)
if videoGetFrame(attractVideo) > GAME_LIST[GAME_SELECTED].ATTRACT_END then
videoSeek(attractVideo, GAME_LIST[GAME_SELECTED].ATTRACT_START)
end
return OVERLAY_UPDATED
end
----
[#videosetaudiotrack]
==== videoSetAudioTrack
[source,text]
----
videoSetAudioTrack(id, track)
----
Cambia un vídeo cargado a otra de sus pistas de audio. Se descarta el audio en cola de la pista anterior y la reproducción se realinea, así que el cambio se oye casi de inmediato. Seleccionar la pista que ya está en uso no hace nada. Una pista fuera de `0` a `videoGetAudioTracks(id) - 1` termina el script, lo que en un clip mudo significa cualquier valor.
*Parámetros:*
* `id` -- handle de vídeo.
* `track` -- índice de la pista de audio, empezando en cero.
*Desde:* 2.10
*Véase también:* <<videogetaudiotrack,videoGetAudioTrack>>, <<videogetaudiotracks,videoGetAudioTracks>>, <<videogetlanguage,videoGetLanguage>>
.Ejemplo
[source,lua]
----
-- El menú guarda para cada juego una pista de audio para su clip de atracción.
attractVideo = videoLoad(GAME_LIST[GAME_SELECTED].ATTRACT)
local wanted = GAME_LIST[GAME_SELECTED].AUDIO_TRACK
if wanted and wanted < videoGetAudioTracks(attractVideo) then
videoSetAudioTrack(attractVideo, wanted)
end
videoPlay(attractVideo)
----
[#videosetvolume]
==== videoSetVolume
[source,text]
----
videoSetVolume(id, left, right)
videoSetVolume(volume)
----
Fija el volumen estéreo de un vídeo cargado en porcentajes. Los valores se recortan al intervalo de `0` a `100` sin ningún aviso. El ajuste se recuerda incluso en un clip mudo, y `videoGetVolume` lo devuelve.
Llamada con un solo argumento, es una extensión de Hypseus que fija en cambio el volumen del propio laserdisc, en la escala de `0` a `64` de Hypseus y no como porcentaje: la gemela de <<videogetvolume,`videoGetVolume()`>> sin argumentos. Un nivel fuera de `0` a `64` se ignora en lugar de ser fatal, que es lo que hace Hypseus con él.
*Parámetros:*
* `id` -- handle de vídeo.
* `left`, `right` -- volumen de cada canal, de `0` (silencio) a `100` (máximo).
* `volume` -- el volumen del laserdisc, de `0` (silencio) a `64` (máximo).
*Desde:* 2.00
*Véase también:* <<videogetvolume,videoGetVolume>>, <<discaudio,discAudio>>
.Ejemplo
[source,lua]
----
-- Desvanece el audio de la cinemática en menos de medio segundo mientras se devuelve el control.
function onOverlayUpdate()
if fadingOut then
fadeLevel = fadeLevel - 4
videoSetVolume(cutscene, fadeLevel, fadeLevel)
if fadeLevel <= 0 then
fadingOut = false
videoPause(cutscene)
end
end
return OVERLAY_UPDATED
end
----
[#videounload]
==== videoUnload
[source,text]
----
videoUnload(id)
----
Detiene el decodificador y libera todo lo que ocupa un vídeo cargado: el decodificador, la pista de audio, los fotogramas en caché y la superficie transformada. A partir de entonces el handle no es válido y pasarlo a cualquier función `video*` termina el script, así que borra la variable que lo guardaba. El motor libera los vídeos que siguen cargados cuando termina el script.
*Parámetros:*
* `id` -- handle de vídeo.
*Desde:* 2.00
*Véase también:* <<videoload,videoLoad>>, <<videopause,videoPause>>
.Ejemplo
[source,lua]
----
-- Descarta los recursos del juego seleccionado antes de cargar el siguiente.
function unloadGameAssets()
if attractVideo then
videoUnload(attractVideo)
attractVideo = nil
end
end
----
[#vldp]
=== VLDP
El espacio de nombres `vldp*` es anterior a Singe 2.00 y existe para que los scripts antiguos sigan funcionando. `vldpGetHeight` y `vldpGetWidth` son las mismas funciones que `discGetHeight` y `discGetWidth`, `vldpSetVerbose` no hace nada, y `vldpGetPixel` es el único miembro sin equivalente moderno: lee un píxel del fotograma actual del disco en coordenadas del overlay y sigue siendo la forma habitual de detectar impactos sobre vídeo pregrabado. Sin disco devuelve negro (véase <<withoutadisc,Juegos sin disco>>).
WARNING: *Heredado.* En código nuevo, usa preferentemente <<discgetheight,discGetHeight>> y <<discgetwidth,discGetWidth>>. `vldpGetPixel` sigue vigente.
Hypseus Singe añadió once miembros más, que se mantienen bajo este prefijo para que sus juegos funcionen sin cambios: `vldpFocusArea` y `vldpResetFocus` recortan la imagen del disco, `vldpGetScale` y `vldpSetScale` informan del factor de escala de la imagen y lo cambian, `vldpGetRotate` y `vldpSetRotate` hacen lo mismo con el giro de la presentación, `vldpGetYUVPixel` lee muestras en bruto, y `vldpFlash`, `vldpSetBlend`, `vldpSetLuma` y `vldpSetMonochrome` cambian el aspecto de la propia imagen. Cada uno lleva abajo la marca «Extensión de Hypseus». La escala y el giro son dos de los datos de partida de xref:Manual.adoc#videorect[El rectángulo de vídeo].
Los cuatro controles de imagen actúan sobre el fotograma decodificado, sobre sus planos de luma y de crominancia, antes de que llegue a la pantalla: `vldpSetBlend` y `vldpSetLuma` se aplican uno detrás de otro a medida que se sube cada fotograma, `vldpSetMonochrome` sustituye la crominancia y `vldpFlash` sustituye la imagen entera durante un fotograma. Mientras están activos cuestan una pasada por el plano de luma en cada fotograma, y nada en absoluto mientras están desactivados; dejan que `vldpGetPixel` y `vldpGetYUVPixel` lean la imagen tal como se decodificó, y con un framefile siguen a la reproducción de un segmento a otro. Los cuatro vuelven a desactivarse cuando el script se recarga (salvo el monocromo, que vuelve a lo que pidiera `--monochrome`) y ninguno afecta a un vídeo dibujado con `videoDraw`.
[#vldpflash]
==== vldpFlash
[source,text]
----
vldpFlash()
----
Extensión de Hypseus. Pone una imagen blanca en el disco, como el destello de un reproductor de laserdisc real cuando hace una búsqueda. Aparece en el acto y la siguiente actualización del disco la quita, así que dura un fotograma dibujado, tanto si el disco se está reproduciendo como si está en pausa o detenido.
El blanco es el valor del destello de Hypseus, un 90 por ciento de luma con crominancia neutra, no blanco puro, que es lo que de verdad es el blanco de una señal de vídeo. El overlay, las GUI y el arte del bezel se dibujan encima como siempre, así que un HUD sigue siendo legible durante el destello.
Sin disco, o con un disco cuyo primer fotograma aún no se ha decodificado, la llamada no hace nada.
*Parámetros:* ninguno.
*Devuelve:* nada.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<discsearch,discSearch>>, <<vldpsetluma,vldpSetLuma>>
.Ejemplo
[source,lua]
----
-- Subraya un impacto.
vldpFlash()
----
[#vldpfocusarea]
==== vldpFocusArea
[source,text]
----
vldpFocusArea(x, y, width, height)
vldpFocusArea()
----
Extensión de Hypseus. Muestra solo un rectángulo del fotograma del disco, dado en píxeles del disco, estirado al espacio que suele ocupar el fotograma entero, con el escalado suave habitual del renderizador; la proporción es la que den `width` y `height`. El rectángulo se mantiene dentro del fotograma, y uno que queda del todo fuera aborta el script. Sin argumentos se vuelve a mostrar el fotograma entero (Hypseus ignora esa llamada y usa `vldpResetFocus`; Singe acepta las dos). El enfoque es solo un efecto de presentación: `vldpGetPixel`, `vldpGetYUVPixel`, la correspondencia del ratón y el disco sobre un material 3D conservan las coordenadas sin enfocar, así que quien llama debe escalar la detección de impactos sobre una imagen ampliada. Sin disco la llamada no hace nada. Se restablece cuando el script se recarga.
*Parámetros:*
* `x`, `y` -- esquina superior izquierda en píxeles del disco.
* `width`, `height` -- tamaño del rectángulo en píxeles del disco.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<vldpresetfocus,vldpResetFocus>>, <<vldpgetwidth,vldpGetWidth>>, <<vldpgetpixel,vldpGetPixel>>
.Ejemplo
[source,lua]
----
-- Amplía el 80% central de la imagen durante la persecución.
local w, h = vldpGetWidth(), vldpGetHeight()
vldpFocusArea(w * 0.1, h * 0.1, w * 0.8, h * 0.8)
function onChaseEnded()
vldpFocusArea()
end
----
[#vldpgetheight]
==== vldpGetHeight
[source,text]
----
height = vldpGetHeight()
----
Devuelve la altura en píxeles del vídeo del disco, o la altura del lienzo si no hay disco. Es la misma función que `discGetHeight`, registrada con su nombre anterior a 2.00.
*Devuelve:* altura en píxeles, entero.
*Notas:*
WARNING: *Alias heredado.* Usa <<discgetheight,discGetHeight>> en código nuevo.
*Desde:* 1.x
*Véase también:* <<discgetheight,discGetHeight>>, <<vldpgetwidth,vldpGetWidth>>
.Ejemplo
[source,lua]
----
-- Un script de 1.x que dimensiona su overlay a partir del disco.
overlaySetResolution(vldpGetWidth(), vldpGetHeight())
centerX = vldpGetWidth() / 2
centerY = vldpGetHeight() / 2
----
[#vldpgetpixel]
==== vldpGetPixel
[source,text]
----
r, g, b = vldpGetPixel(x, y)
----
Lee el color de un píxel del fotograma del disco que está en pantalla. Las coordenadas son coordenadas del overlay: el motor las divide por la escala entre el overlay y el vídeo y trunca el resultado, así que pasas los mismos números con los que dibujas. Devuelve `0, 0, 0` cuando no hay disco, cuando aún no se ha decodificado ningún fotograma o cuando las coordenadas escaladas quedan fuera del vídeo. Los juegos que la usan suelen marcar los blancos en el metraje original con un color característico y comprobar el píxel que hay bajo el punto de mira al apretar el gatillo, en lugar de crear zonas de impacto para cada fotograma.
*Parámetros:*
* `x`, `y` -- coordenadas del overlay; se aceptan valores fraccionarios.
*Devuelve:* tres enteros, rojo, verde y azul, cada uno de `0` a `255`.
*Desde:* 1.x
*Véase también:* <<discgetframe,discGetFrame>>, <<mousegetposition,mouseGetPosition>>, <<overlaysetresolution,overlaySetResolution>>
.Ejemplo
[source,lua]
----
-- Comprobación de impacto con pistola de luz: los blancos van pintados de magenta puro en los fotogramas con enemigos.
function onInputPressed(what)
if what == SWITCH_BUTTON3 then
local x, y = mouseGetPosition(0)
local r, g, b = vldpGetPixel(x, y)
if r > 200 and g < 50 and b > 200 then
scoreHit()
else
scoreMiss()
end
end
end
----
[#vldpgetrotate]
==== vldpGetRotate
[source,text]
----
degrees = vldpGetRotate()
----
Extensión de Hypseus. Devuelve el giro de la presentación en grados en el sentido de las agujas del reloj: la opción `--rotate`, o lo último que fijó `vldpSetRotate`, y `0` si no hay ninguno de los dos. Solo devuelve `0`, `90`, `180` o `270`, porque son los únicos ángulos a los que gira Singe.
*Devuelve:* entero, `0`, `90`, `180` o `270`.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<vldpsetrotate,vldpSetRotate>>, <<vldpgetscale,vldpGetScale>>
.Ejemplo
[source,lua]
----
-- Una recreativa con el monitor en vertical quiere su panel de puntuación a lo largo del lado largo.
if vldpGetRotate() == 90 or vldpGetRotate() == 270 then
layoutPanel(PANEL_TALL)
else
layoutPanel(PANEL_WIDE)
end
----
[#vldpgetscale]
==== vldpGetScale
[source,text]
----
scale = vldpGetScale()
----
Extensión de Hypseus. Devuelve el factor de escala de la imagen como porcentaje: la opción `--scalefactor`, o lo último que aplicó `vldpSetScale`, y `100` si no se ha dado ninguno de los dos. Los juegos de Hypseus dividen `100` entre él para ajustar la geometría de su overlay a la imagen reducida. No tiene nada que ver con `vldpFocusArea` ni con la resolución del overlay.
*Devuelve:* entero, de `25` a `100`.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<vldpsetscale,vldpSetScale>>, <<vldpfocusarea,vldpFocusArea>>, <<overlaygetwidth,overlayGetWidth>>
.Ejemplo
[source,lua]
----
-- Código de un juego de Hypseus: las miras de la pistola se escalan con la imagen.
if vldpGetScale ~= nil then
gunScale = 100 / vldpGetScale()
else
gunScale = 1
end
----
[#vldpgetshift]
==== vldpGetShift
[source,text]
----
x, y = vldpGetShift()
----
El desplazamiento de la imagen dentro de la ventana en porcentajes: las opciones
`--shiftx` y `--shifty` o lo último que aplicó <<vldpsetshift,vldpSetShift>>, y
`0, 0` si no se ha dado ninguno de los dos. En el extremo, un desplazamiento
ocupa exactamente el espacio que dejó `--scalefactor`, así que `100` con la
escala a `100` no mueve nada.
*Devuelve:* dos enteros, cada uno de `-100` a `100`.
*Desde:* 3.00.
*Véase también:* <<vldpsetshift,vldpSetShift>>, <<vldpgetscale,vldpGetScale>>
.Ejemplo
[source,lua]
----
-- Informa de dónde ha puesto la imagen el operador.
local x, y = vldpGetShift()
fontPrint(10, 10, string.format("shift %d, %d", x, y))
----
[#vldpgetwidth]
==== vldpGetWidth
[source,text]
----
width = vldpGetWidth()
----
Devuelve la anchura en píxeles del vídeo del disco, o la anchura del lienzo si no hay disco. Es la misma función que `discGetWidth`, registrada con su nombre anterior a 2.00.
*Devuelve:* anchura en píxeles, entero.
*Notas:*
WARNING: *Alias heredado.* Usa <<discgetwidth,discGetWidth>> en código nuevo.
*Desde:* 1.x
*Véase también:* <<discgetwidth,discGetWidth>>, <<vldpgetheight,vldpGetHeight>>
.Ejemplo
[source,lua]
----
-- Un script de 1.x que dimensiona su overlay a partir del disco.
overlaySetResolution(vldpGetWidth(), vldpGetHeight())
centerX = vldpGetWidth() / 2
centerY = vldpGetHeight() / 2
----
[#vldpgetyuvpixel]
==== vldpGetYUVPixel
[source,text]
----
y, u, v = vldpGetYUVPixel(x, y)
----
Extensión de Hypseus. Lee las muestras en bruto de un píxel del fotograma del disco que se está mostrando, antes de cualquier conversión de color: la muestra de luma y las dos de crominancia, cada una de `0` a `255`, con la crominancia a la resolución 4:2:0 de la imagen decodificada (el valor neutro es `128`). Las coordenadas son coordenadas del overlay, escaladas y truncadas exactamente como hace `vldpGetPixel`. Devuelve `-1, -1, -1` cuando no hay disco, cuando aún no se ha decodificado ningún fotograma o cuando las coordenadas escaladas quedan fuera del vídeo. No le afectan `vldpFocusArea` ni `vldpSetMonochrome`. Es más barata que `vldpGetPixel` cuando solo importa el brillo.
*Parámetros:*
* `x`, `y` -- coordenadas del overlay; se aceptan valores fraccionarios.
*Devuelve:* tres enteros, `Y`, `U` y `V`, cada uno de `0` a `255`, o `-1, -1, -1`.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<vldpgetpixel,vldpGetPixel>>, <<discgetframe,discGetFrame>>
.Ejemplo
[source,lua]
----
-- La carretera es oscura; cualquier cosa clara bajo el coche es un obstáculo.
function carHitsSomething()
local y = vldpGetYUVPixel(carX + carWidth / 2, carY + carHeight)
return y >= 120
end
----
[#vldpresetfocus]
==== vldpResetFocus
[source,text]
----
vldpResetFocus()
----
Extensión de Hypseus. Vuelve a mostrar el fotograma entero del disco después de `vldpFocusArea`, exactamente como hace `vldpFocusArea()` sin argumentos. Es el nombre que da Hypseus a esta vuelta atrás, y se conserva para que sus juegos funcionen sin cambios.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<vldpfocusarea,vldpFocusArea>>
.Ejemplo
[source,lua]
----
function onChaseEnded()
vldpResetFocus()
end
----
[#vldpsetblend]
==== vldpSetBlend
[source,text]
----
vldpSetBlend(enabled)
----
Extensión de Hypseus. Suaviza la imagen del disco en vertical, de una fila a otra: cada muestra de luma pasa a ser la media de sí misma y de las muestras que tiene justo encima y justo debajo. Es el filtro de mezcla de tres coeficientes propio de Hypseus, y existe para quitar los bordes duros de una transferencia entrelazada sin suavizar la imagen en horizontal.
Los planos de crominancia no se tocan, así que el suavizado cambia el detalle del brillo y no el color. Desactivarlo devuelve la imagen exactamente a como se decodificó.
Sin disco la llamada no hace nada, pero se recuerda igualmente, así que surte efecto si después se carga un disco. Un argumento que no sea booleano termina el script.
*Parámetros:*
* `enabled` -- booleano.
*Devuelve:* nada.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<vldpsetluma,vldpSetLuma>>, <<vldpsetmonochrome,vldpSetMonochrome>>
.Ejemplo
[source,lua]
----
-- Una opción de la pantalla de servicio para una transferencia con ruido.
vldpSetBlend(SETTINGS.softenVideo)
----
[#vldpsetluma]
==== vldpSetLuma
[source,text]
----
vldpSetLuma(enabled [, level])
----
Extensión de Hypseus. Aclara u oscurece la imagen del disco. `level` va de 0 a 8, y 4, el valor por defecto, no cambia nada: la distancia a 4 se suma a cada muestra de luma en octavos, así que 0 reduce el brillo a la mitad, 8 lo aumenta en la mitad, y los pasos intermedios son iguales. Un nivel fuera de 0 a 8 termina el script.
Como solo cambia la luma, el color se queda donde está: subir el nivel aclara la imagen sin desvaír el color, que es lo que hace el control de imagen de un reproductor real y lo que no haría un multiplicador de color. Desactivarlo, o pedir el nivel 4, devuelve la imagen exactamente a como se decodificó.
Por tanto, `vldpSetLuma(true)` sin nivel no hace nada visible, que es también lo que hace Hypseus.
Sin disco la llamada no hace nada, pero se recuerda igualmente, así que surte efecto si después se carga un disco.
*Parámetros:*
* `enabled` -- booleano.
* `level` -- número opcional; de 0 a 8, siendo 4 el neutro. Por defecto, 4.
*Devuelve:* nada.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<vldpsetblend,vldpSetBlend>>, <<vldpsetmonochrome,vldpSetMonochrome>>, <<vldpflash,vldpFlash>>
.Ejemplo
[source,lua]
----
-- Una escena nocturna que el jugador no llega a ver; deja que la aclare.
vldpSetLuma(true, 6)
----
[#vldpsetmonochrome]
==== vldpSetMonochrome
[source,text]
----
vldpSetMonochrome(enabled)
----
Extensión de Hypseus. Muestra el disco solo con la luma: los planos de crominancia se sustituyen por muestras neutras cuando se sube cada fotograma, así que un disco en pausa cambia en el acto. Con un framefile, el ajuste sigue a la reproducción de un segmento a otro. `vldpGetPixel` y `vldpGetYUVPixel` siguen devolviendo las muestras de color, y los vídeos adicionales dibujados con `videoDraw` no se ven afectados. Combínala con `overlaySetMonochrome` para un modo en blanco y negro. Sin disco la llamada no hace nada; se restablece cuando el script se recarga. Un argumento que no sea booleano aborta el script.
*Parámetros:*
* `enabled` -- booleano.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<overlaysetmonochrome,overlaySetMonochrome>>, <<vldpgetpixel,vldpGetPixel>>
.Ejemplo
[source,lua]
----
-- Un DIP switch elige entre color y blanco y negro.
vldpSetMonochrome(not dipChroma)
overlaySetMonochrome(not dipChroma)
----
[#vldpsetrotate]
==== vldpSetRotate
[source,text]
----
applied = vldpSetRotate(degrees)
----
Extensión de Hypseus. Gira toda la presentación en el sentido de las agujas del reloj: el disco, el overlay, las GUI, la escena 3D y las partículas giran juntos, y las posiciones del ratón y de la pistola de luz se giran de vuelta para que un clic siga cayendo donde se dibuja. Un cuarto de vuelta intercambia la forma del área presentada, así que una ventana vertical se llena en lugar de quedar reducida a una franja entre bandas negras.
Hypseus acepta cualquier ángulo de `0` a `359`; Singe solo acepta `0`, `90`, `180` y `270`, y responde `false` a cualquier otro en lugar de terminar el script. Un ángulo arbitrario dejaría sin un significado razonable la correspondencia del ratón y el borde Sinden, y ningún juego de la biblioteca de Hypseus pide uno.
Mientras no es cero, el giro cuesta un destino de renderizado del tamaño de la pantalla y una copia (blit) adicional por fotograma, y nada en absoluto mientras lo es. Vuelve al valor de `--rotate` cuando el script se recarga.
*Parámetros:*
* `degrees` -- entero, `0`, `90`, `180` o `270`.
*Devuelve:* booleano, verdadero si se aplicó el giro.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<vldpgetrotate,vldpGetRotate>>, <<vldpsetscale,vldpSetScale>>
.Ejemplo
[source,lua]
----
-- Una fase de shooter vertical en un monitor que pivota.
if not vldpSetRotate(90) then
debugPrint("This build will not turn the screen.")
end
----
[#vldpsetscale]
==== vldpSetScale
[source,text]
----
applied = vldpSetScale(percent)
----
Extensión de Hypseus. Reduce la imagen alrededor de su centro al `percent` de la ventana, el equivalente en ejecución de `--scalefactor`, y reconstruye el rectángulo de vídeo para que el disco, el overlay, las GUI, la escena 3D, las partículas, el borde Sinden si lo hay y la correspondencia del ratón lo sigan. Los valores fuera de `25` a `100` responden `false` y no cambian nada, en lugar de terminar el script.
Los cambios se limitan a uno cada 15 milisegundos, igual que Hypseus limita su propio zoom con tecla mantenida; una llamada dentro de ese intervalo responde `false`, así que un script que maneje la escala con una tecla mantenida debe seguir llamando hasta que surta efecto. Vuelve al valor de `--scalefactor` cuando el script se recarga.
*Parámetros:*
* `percent` -- entero, de `25` a `100`.
*Devuelve:* booleano, verdadero si se aplicó la escala.
*Desde:* 3.00, como extensión de Hypseus Singe.
*Véase también:* <<vldpgetscale,vldpGetScale>>, <<vldpsetrotate,vldpSetRotate>>
.Ejemplo
[source,lua]
----
-- Aleja la imagen un paso cada vez que el jugador pulsa la tecla de ajuste.
function onInputPressed(input)
if input == SWITCH_BUTTON3 then
vldpSetScale(vldpGetScale() - 5)
end
end
----
[#vldpsetshift]
==== vldpSetShift
[source,text]
----
vldpSetShift(x, y)
----
Mueve la imagen dentro de la ventana, el equivalente en ejecución de `--shiftx` y
`--shifty`, y reconstruye el rectángulo de vídeo para que el disco, el overlay,
las GUI, la escena 3D, las partículas, el borde Sinden si lo hay y la
correspondencia del ratón lo sigan. Los valores son porcentajes del espacio que
dejó `--scalefactor`, así que con la escala a `100` no hay adónde moverla. Un
valor fuera de `-100` a `100` termina el script, a diferencia de
<<vldpsetscale,vldpSetScale>>, que responde `false`.
*Parámetros:*
* `x` -- entero, de `-100` a `100`.
* `y` -- entero, de `-100` a `100`.
*Devuelve:* nada.
*Desde:* 3.00.
*Véase también:* <<vldpgetshift,vldpGetShift>>, <<vldpsetscale,vldpSetScale>>, <<singesavegeometry,singeSaveGeometry>>
.Ejemplo
[source,lua]
----
-- Empuja la imagen hacia la izquierda cada vez que el operador pulsa la tecla.
function onInputPressed(input)
if input == SWITCH_LEFT then
local x, y = vldpGetShift()
vldpSetShift(math.max(-100, x - 1), y)
end
end
----
[#vldpsetverbose]
==== vldpSetVerbose
[source,text]
----
vldpSetVerbose(...)
----
No hace nada. En su día activaba y desactivaba el registro del reproductor de vídeo original; la llamada acepta cualquier argumento, escribe una línea en la traza y vuelve. Las trazas del motor y del script son ahora opciones de línea de comandos (`--program` y `--trace`).
*Notas:*
WARNING: *Sin implementar.* Se conserva solo por compatibilidad con versiones anteriores. Usa `--trace` en la línea de comandos para registrar las llamadas a la API.
*Desde:* 1.x
*Véase también:* <<debugprint,debugPrint>>, <<discgetstate,discGetState>>
.Ejemplo
[source,lua]
----
-- Código de arranque portado de 1.x. La llamada se ignora; ejecuta Singe con --trace en su lugar.
vldpSetVerbose(true)
overlaySetResolution(discGetWidth(), discGetHeight())
discSearch(TITLE_FRAME)
----
[#voice]
=== Voz
Las piezas con las que se construye un chat de voz, sin la red: Opus, para hacer
una voz lo bastante pequeña como para enviarla y luego recuperarla, un procesador
que limpia lo que capta un micrófono antes de enviarlo, y un búfer de jitter para
reproducir en el orden correcto lo que llega tarde y desordenado. Enviar los
paquetes y encontrar a los demás jugadores es cosa del propio juego, o de una
versión posterior de Singe.
Las muestras son cadenas de valores de 16 bits, dos bytes cada uno, little endian,
tal como las da <<micread,`micRead`>>; con dos canales se alternan izquierda y
derecha. Una *trama* es un trozo fijo de tiempo: 20 milisegundos, 960 muestras a
48000, es la habitual para una voz.
Todos los objetos de aquí son handles. <<voicefree,`voiceFree`>> libera uno; todos
se liberan cuando termina el juego.
.Un micrófono, limpiado, codificado y decodificado de nuevo
[source,lua]
----
micOpen()
local processor = voiceProcessorNew()
local encoder = opusEncoderNew()
local decoder = opusDecoderNew()
local pending = ""
function onOverlayUpdate()
pending = pending .. micRead()
while #pending >= 960 * 2 do
local frame = pending:sub(1, 960 * 2)
pending = pending:sub(960 * 2 + 1)
local cleaned, voiced = voiceProcess(processor, frame)
if voiced then
local packet = opusEncode(encoder, cleaned) -- esto es lo que envías
local heard = opusDecode(decoder, packet) -- y esto es lo que reproduce el otro extremo
end
end
return OVERLAY_UPDATED
end
----
[#jitterget]
==== jitterGet
[source,text]
----
packet, status = jitterGet(jitter [, span])
----
El paquete que toca reproducir ahora, con el búfer avanzado un paso. Llámala una
vez por cada trama de tiempo, haya llegado lo que haya llegado.
*Parámetros:*
* `span` -- número, las muestras que se toman; el paso si no se indica.
*Devuelve:* el paquete, o `nil`, y un estado: `"ok"`, `"missing"` (no toca nada:
pasa `nil` a <<opusdecode,`opusDecode`>> para cubrir el hueco) o
`"insertion"` (el búfer está creciendo para absorber los paquetes que llegan tarde:
reproduce un poco de silencio).
*Desde:* 3.00.
[#jitternew]
==== jitterNew
[source,text]
----
jitter = jitterNew([step])
----
Un búfer de jitter: los paquetes entran según su marca de tiempo a medida que
llegan y salen en orden, un paso cada vez, con un poco de retraso para que uno que
llegue tarde aún llegue a tiempo.
*Parámetros:*
* `step` -- número, las muestras que avanza cada <<jitterget,`jitterGet`>>; `960`
si no se indica.
*Devuelve:* el handle del búfer de jitter.
*Desde:* 3.00.
[#jitterput]
==== jitterPut
[source,text]
----
jitterPut(jitter, packet, timestamp [, span])
----
Un paquete que ha llegado. El búfer guarda su propia copia.
*Parámetros:*
* `packet` -- cadena, de 1 a 4000 bytes.
* `timestamp` -- número, dónde empieza el paquete, en muestras: las cuenta el
emisor, `0`, `960`, `1920` y así sucesivamente.
* `span` -- número, las muestras que cubre el paquete; el paso si no se indica.
*Desde:* 3.00.
[#opusdecode]
==== opusDecode
[source,text]
----
samples = opusDecode(decoder, packet | nil)
----
Un paquete devuelto a muestras. Un `nil` en lugar de un paquete que nunca llegó
pide a Opus que cubra el hueco con algo verosímil, de la misma duración que el
último paquete.
*Devuelve:* las muestras.
*Desde:* 3.00.
[#opusdecodernew]
==== opusDecoderNew
[source,text]
----
decoder = opusDecoderNew([rate [, channels]])
----
*Parámetros:*
* `rate` -- número: 8000, 12000, 16000, 24000 o 48000; `48000` si no se indica.
* `channels` -- número, `1` o `2`; `1` si no se indica.
*Devuelve:* el handle del decodificador.
*Desde:* 3.00.
[#opusencode]
==== opusEncode
[source,text]
----
packet = opusEncode(encoder, samples)
----
Una trama de muestras convertida en un paquete. Una trama son 2,5 milisegundos de
muestras, o 5, 10, 20, 40 o 60; cualquier otra cosa se rechaza.
*Devuelve:* el paquete, una cadena.
*Desde:* 3.00.
[#opusencodernew]
==== opusEncoderNew
[source,text]
----
encoder = opusEncoderNew([rate [, channels [, application]]])
----
*Parámetros:*
* `rate` -- número: 8000, 12000, 16000, 24000 o 48000; `48000` si no se indica.
* `channels` -- número, `1` o `2`; `1` si no se indica.
* `application` -- cadena: `"voip"` para la voz, ajustado para que se entienda;
`"audio"` para música; `"lowdelay"` para el menor retardo posible a costa de algo
de calidad. `"voip"` si no se indica.
*Devuelve:* el handle del codificador.
*Desde:* 3.00.
[#opusencoderset]
==== opusEncoderSet
[source,text]
----
opusEncoderSet(encoder, bitrate [, complexity])
----
*Parámetros:*
* `bitrate` -- número, en bits por segundo; `0` lo deja como está. Una voz se oye
clara entre 16000 y 24000.
* `complexity` -- número, de `0` a `10`: cuánto trabajo dedicar a la calidad.
*Desde:* 3.00.
[#voicefree]
==== voiceFree
[source,text]
----
voiceFree(handle)
----
Libera un codificador o un decodificador Opus, un procesador de voz o un búfer de
jitter.
*Desde:* 3.00.
[#voiceprocess]
==== voiceProcess
[source,text]
----
cleaned, voiced = voiceProcess(processor, mic [, speaker])
----
Una trama del micrófono, limpia: sin el eco de lo que reproducían los altavoces,
con el ruido de fondo suprimido y el nivel igualado.
*Parámetros:*
* `mic` -- cadena, una trama de muestras del micrófono.
* `speaker` -- cadena, la misma trama de lo que reprodujeron los altavoces, si
hubo algo: las voces de los demás jugadores, por ejemplo. Lo que se quita es su eco.
*Devuelve:* la trama limpia, y `true` cuando suena como una voz -- la forma de no
enviar nada mientras nadie habla.
*Desde:* 3.00.
[#voiceprocessornew]
==== voiceProcessorNew
[source,text]
----
processor = voiceProcessorNew([frame [, rate [, tailMs]]])
----
*Parámetros:*
* `frame` -- número, muestras por llamada; `960` si no se indica.
* `rate` -- número; `48000` si no se indica.
* `tailMs` -- número, el eco más largo que cancelar, en milisegundos: cuánto
tarda un sonido en apagarse en la sala. `200` si no se indica.
*Devuelve:* el handle del procesador.
*Desde:* 3.00.
[#enginecallbacks]
=== Callbacks del motor
Son funciones que define el script y que Singe llama cuando ocurre el evento correspondiente; todas son opcionales, y una que no está definida se omite. Los callbacks de entrada se disparan a medida que se leen los eventos, `onOverlayUpdate` se ejecuta como mucho una vez cada 15 milisegundos, y ninguno se ejecuta mientras la tecla de pausa mantiene el juego congelado (véase <<pausing,Pausa>>). Un error dentro de un callback termina el juego con una traza de la pila. En el modelo con hilo, `singeMain` ocupa el lugar de la mayor parte de la lógica de cada fotograma (véase <<threaded,Con hilo>>): los demás callbacks se siguen disparando, pero `Framework.singe` instala su propio `onOverlayUpdate` para mover la corrutina `singeMain`, así que un juego con hilo define `singeMain`, `onShutdown` y los callbacks de entrada que quiera.
[#oncontrollermoved]
==== onControllerMoved
[source,text]
----
function onControllerMoved(axis, value, which)
end
----
Se llama con cada evento de movimiento de eje de un mando conectado, incluido el movimiento dentro de la zona muerta, así que un stick bajo un pulgar lo dispara muchas veces por segundo. Las direcciones más allá de `SINGE_DEAD_ZONE` también actúan como interruptores cuando `controls.cfg` las asigna, así que un juego que solo necesita dirección digital puede asignar `GAMEPAD_0.AXIS_LEFT_X_L` y `GAMEPAD_0.AXIS_LEFT_X_R` y no hacer caso de este callback. El valor se guarda para `controllerGetAxis` antes de que se ejecute el callback.
*Parámetros:*
* `axis` -- de `0` a `5`, según las constantes `GAMEPAD_AXIS_*`.
* `value` -- posición en bruto, de `-32768` a `32767`; los gatillos reposan en `0`.
* `which` -- índice del mando, de `0` a `3`, el índice que usan `controllerGetAxis` y las tablas `GAMEPAD_N`.
*Véase también:* <<controllergetaxis,controllerGetAxis>>, <<controllergetbutton,controllerGetButton>>
.Ejemplo
[source,lua]
----
-- Dirección analógica solo con el stick izquierdo del primer mando.
function onControllerMoved(axis, value, which)
if which ~= 0 or axis ~= GAMEPAD_AXIS_LEFT_X then
return
end
if math.abs(value) < SINGE_DEAD_ZONE then
steering = 0
else
steering = value / 32768
end
end
----
[#oninputpressedoninputreleased]
==== onInputPressed / onInputReleased
[source,text]
----
function onInputPressed(what)
end
function onInputReleased(what)
end
----
Se llama cuando una entrada lógica baja o sube. Lo que llega depende del modo del teclado. En `MODE_NORMAL`, `what` es un valor `SWITCH_*`: las teclas, los botones del mando, las direcciones de eje más allá de la zona muerta, los botones del ratón y la rueda del ratón llegan todos así, traducidos mediante `controls.cfg`, cada pulsación una sola vez y sin repetición de teclas, y las entradas que no están asignadas a ningún interruptor se ignoran. `SWITCH_PAUSE` solo llega después de `singeSetPauseKeyEnabled(false)`; si no, el motor congela el juego con esa tecla y el script nunca la ve. En `MODE_FULL`, `what` es el keysym de la tecla (el valor del carácter, no el scancode), se entregan las repeticiones, y los botones del mando y del ratón pasan `0`; cada evento llega también a `onKeyPressed` y `onKeyReleased`, que llevan el scancode o el código del botón. Cuando la tecla de pausa congela el juego, todos los interruptores que el script tiene pulsados se sueltan antes mediante `onInputReleased`, y al descongelarlo todo lo que sigue físicamente pulsado se vuelve a pulsar, así que un juego nunca ve un botón desfasado. Las pulsaciones del primer segundo después de que arranque el script, o de que la ventana recupere el foco, se tratan como algo que ya venía pulsado de antes y se descartan junto con su liberación. Hay un interruptor que no viene de ningún control: el motor genera `SWITCH_MOUSE_DISCONNECT` cuando se desenchufa un ratón o una pistola de luz, una vez como pulsación e inmediatamente otra como liberación, para que un juego de pistola pueda darse cuenta de que su pistola se ha ido (véase xref:Manual.adoc#mousedevices[Ratones, pistolas y quién decide]).
*Véase también:* <<keyboardsetmode,keyboardSetMode>>, <<onkeypressedonkeyreleased,onKeyPressed>>, <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>, <<controllergetbutton,controllerGetButton>>
.Ejemplo
[source,lua]
----
function onInputPressed(what)
if what == SWITCH_COIN1 then
credits = credits + 1
soundPlay(coinClip)
elseif what == SWITCH_START1 and credits > 0 then
startGame()
elseif what == SWITCH_BUTTON3 then
fireShot(cursorX, cursorY)
elseif what == SWITCH_QUIT then
singeQuit()
end
end
function onInputReleased(what)
if what == SWITCH_BUTTON1 then
thrusting = false
end
end
----
[#onkeypressedonkeyreleased]
==== onKeyPressed / onKeyReleased
[source,text]
----
function onKeyPressed(keysym, scancode)
end
function onKeyReleased(keysym, scancode)
end
----
Se llama solo en `MODE_FULL`, con cada tecla que baja o sube, con el keysym lógico (el carácter que produce la tecla en la distribución actual) y con el scancode físico (la posición de la tecla, que se compara con la tabla `SCANCODE`). En este modo las teclas mantenidas se repiten, que es lo que quiere la introducción de texto. También llegan los botones del mando y del ratón, con `keysym` a `0` y su código `GAMEPAD_N` o `MOUSE_N` como `scancode`. Para texto usa `keysym`; para controles en los que una tecla hace de botón usa `scancode`, para que la asignación sea la misma en todas las distribuciones. Las asignaciones de teclado de los interruptores propios del motor no actúan en este modo, así que un juego en `MODE_FULL` debe ofrecer su propia forma de salir.
*Véase también:* <<keyboardsetmode,keyboardSetMode>>, <<keyboardgetmodifiers,keyboardGetModifiers>>, <<oninputpressedoninputreleased,onInputPressed>>
[#ontextinput]
==== onTextInput
[source,text]
----
function onTextInput(text)
end
----
Se llama solo en `MODE_FULL`, con el texto que produjo una pulsación -- un carácter o unos pocos, en UTF-8 -- después de aplicar el estado de Mayús y la distribución del teclado, así que una mayúscula, unas comillas o una letra de una distribución no inglesa llegan tal como las tecleó la persona. `onKeyPressed` da la misma pulsación como su keysym sin Mayús y su scancode, y es el que hay que usar para las teclas que son órdenes; este es el que hay que usar para las teclas que son texto. Una GUI con un campo de texto que tiene el foco recibe el texto primero, y el script no se entera de nada.
*Desde:* 3.00.
*Véase también:* <<onkeypressedonkeyreleased,onKeyPressed>>, <<keyboardsetmode,keyboardSetMode>>
.Ejemplo
[source,lua]
----
-- Iniciales del récord: las letras como texto; Intro y Retroceso por su posición.
function onTextInput(text)
if text:match("^%a$") and #initials < 3 then
initials = initials .. text:upper()
end
end
function onKeyPressed(keysym, scancode)
if scancode == SCANCODE.RETURN.value then
finishInitials()
elseif scancode == SCANCODE.BACKSPACE.value then
initials = initials:sub(1, -2)
end
end
----
[#onmidimessage]
==== onMidiMessage
[source,text]
----
function onMidiMessage(status, data1, data2, bytes)
end
----
Se llama con cada mensaje que llega al puerto de entrada MIDI que abrió
<<midiopeninput,`midiOpenInput`>>, una vez por mensaje, junto con el resto de la
entrada del fotograma. Los tres números son el mensaje tal como lo envía un
dispositivo: `status` lleva el tipo en sus cuatro bits altos y el canal en los
cuatro bajos, contando desde `0`, así que una nota en el primer canal llega como `0x90`.
`bytes` es el mensaje entero como cadena, que es lo que necesita un mensaje de
sistema exclusivo y de donde hay que leer un mensaje de más de tres bytes.
Los mensajes que llegan mientras no hay ningún puerto abierto no se ponen en cola;
no se entrega nada de antes de abrir el puerto.
*Parámetros:*
* `status` -- número; el byte de estado, de `0x80` a `0xFF`.
* `data1` -- número; el primer byte de datos, o `0` si el mensaje no tiene.
* `data2` -- número; el segundo byte de datos, o `0` si el mensaje no tiene.
* `bytes` -- cadena; el mensaje entero, sea cual sea su longitud.
*Desde:* 3.00.
*Véase también:* <<midiopeninput,midiOpenInput>>, <<midisend,midiSend>>
.Ejemplo
[source,lua]
----
-- Un teclado MIDI controla el juego: el do central y las tres teclas siguientes son los cuatro botones de disparo.
local MIDI_NOTE_ON = 0x90
function onMidiMessage(status, data1, data2, bytes)
-- El tipo son los cuatro bits altos; el canal, en los cuatro bajos, aquí se ignora.
if (status & 0xF0) == MIDI_NOTE_ON and data2 > 0 then
local button = data1 - 60
if button >= 0 and button <= 3 then
fire(button)
end
end
end
----
[#onmousemoved]
==== onMouseMoved
[source,text]
----
function onMouseMoved(x, y, xRelative, yRelative, which)
end
----
Se llama con cada evento de movimiento del ratón, con la posición y el movimiento desde el evento anterior en coordenadas del overlay. En el modo `MOUSE_SINGLE`, la posición del puntero en la ventana se convierte a coordenadas del overlay y `which` es siempre `0`. En el modo `MOUSE_MANY`, el movimiento relativo de cada dispositivo se acumula en su propia posición, limitada al área del vídeo, y `which` es el índice del dispositivo; un dispositivo absoluto, como una tableta, informa de su posición asignada con `xRelative` y `yRelative` a `0`. La posición se guarda para `mouseGetPosition` antes de que se ejecute el callback, así que un juego puede no hacer caso de este callback y consultar la posición cuando la necesite. Aun así, el dibujo debe esperar a `onOverlayUpdate`: guarda aquí la última posición en variables globales y dibuja allí el cursor.
*Parámetros:*
* `x`, `y` -- posición en coordenadas del overlay.
* `xRelative`, `yRelative` -- movimiento desde el evento anterior, en coordenadas del overlay.
* `which` -- índice del ratón, de `0` a `3`.
*Véase también:* <<mousegetposition,mouseGetPosition>>, <<mousesetmode,mouseSetMode>>, <<mousesetenabled,mouseSetEnabled>>
.Ejemplo
[source,lua]
----
cursorX = overlayGetWidth() // 2
cursorY = overlayGetHeight() // 2
function onMouseMoved(x, y, xRelative, yRelative, which)
cursorX = x
cursorY = y
end
function onOverlayUpdate()
overlayClear()
spriteDraw(reticle, cursorX, cursorY, true)
return OVERLAY_UPDATED
end
----
[#oncollision]
==== onCollision
[source,text]
----
function onCollision(nodeA, nodeB, x, y, z, speed)
end
----
Se llama una vez por cada contacto nuevo entre dos cuerpos después de un paso de la física, y nunca más para una pareja que sigue en contacto. Una caja que aterriza, una bola que choca contra la máquina, el coche del jugador que roza una pared. Los contactos con activadores se notifican en cambio mediante `onTrigger`. Los eventos de un paso se entregan juntos después de él, antes de dibujar el fotograma.
*Parámetros:*
* `nodeA`, `nodeB` -- los nodos de los dos cuerpos.
* `x`, `y`, `z` -- dónde se tocaron, en unidades del mundo.
* `speed` -- a qué velocidad se encontraron, en unidades del mundo por segundo.
*Desde:* 3.00.
*Véase también:* <<ontrigger,onTrigger>>, <<bodynew,bodyNew>>
.Ejemplo
[source,lua]
----
-- Los aterrizajes bruscos hacen ruido donde ocurren.
function onCollision(nodeA, nodeB, x, y, z, speed)
if speed > 4 and (nodeA == crate or nodeB == crate) then
local channel = soundPlay(thudClip)
if channel >= 0 then
soundSetPosition(channel, x, y, z)
end
end
end
----
[#ontrigger]
==== onTrigger
[source,text]
----
function onTrigger(trigger, other, entered)
end
----
Se llama cuando un cuerpo entra en un activador creado con `bodySetTrigger` o sale de él, una vez al entrar y otra al salir, sean cuales sean las formas implicadas (una forma de malla o compuesta que toca en varios puntos sigue contando como un solo cuerpo). Los activadores nunca empujan nada, así que esta es la forma de notar que un cuerpo cruza una puerta, llega a un punto de control o cae en un volumen letal.
*Parámetros:*
* `trigger` -- el nodo del cuerpo activador.
* `other` -- el nodo que ha entrado o salido.
* `entered` -- `true` al entrar, `false` al salir.
*Desde:* 3.00.
*Véase también:* <<bodysettrigger,bodySetTrigger>>, <<oncollision,onCollision>>
.Ejemplo
[source,lua]
----
function onTrigger(trigger, other, entered)
if trigger == finishLine and other == playerCar and entered then
lap = lap + 1
soundPlay(lapClip)
elseif trigger == pit and entered then
respawn(other)
end
end
----
[#onnavarrived]
==== onNavArrived
[source,text]
----
function onNavArrived(agent)
end
----
Se llama cuando un agente de navegación llega al destino que le dio `navAgentMoveTo`, una vez por llegada, después de la actualización de la navegación del fotograma. Manda al agente a otro sitio, o reproduce su animación de reposo. Un script que prefiera consultar puede llamar en su lugar a `navAgentIsArrived`.
*Parámetros:*
* `agent` -- el handle de `navAgentNew`.
*Desde:* 3.00.
*Véase también:* <<navagentmoveto,navAgentMoveTo>>, <<navagentisarrived,navAgentIsArrived>>
.Ejemplo
[source,lua]
----
-- Patrulla entre puntos de ruta para siempre.
function onNavArrived(agent)
local guard = guards[agent]
if guard then
guard.waypoint = guard.waypoint % #guard.route + 1
local next = guard.route[guard.waypoint]
navAgentMoveTo(agent, next.x, next.y, next.z)
end
end
----
[#onoverlayupdate]
==== onOverlayUpdate
[source,text]
----
function onOverlayUpdate()
return OVERLAY_UPDATED
end
----
El único callback en el que se permite dibujar. Singe lo llama como mucho una vez cada 15 milisegundos mientras el juego no está congelado, contando desde que empezó la llamada anterior, así que el tiempo que tarda el propio script cae dentro de esos 15 milisegundos en lugar de sumarse a ellos; cualquier llamada `overlay*`, `spriteDraw`, `videoDraw`, `fontPrint` o similar hecha en otro sitio no aparecerá en pantalla y puede corromper el overlay. Devuelve `OVERLAY_UPDATED` (`1`) cuando el overlay ha cambiado, para que Singe lo vuelva a componer sobre el vídeo, u `OVERLAY_NOT_UPDATED` (`0`) cuando no ha cambiado nada, para que se reutilice la composición anterior; no devolver nada cuenta como `OVERLAY_NOT_UPDATED`. La pantalla se vuelve a dibujar de todos modos cada vez que llega un fotograma de vídeo nuevo o hay una escena 3D activada, así que el valor devuelto es una optimización para pantallas estáticas. Cuando vuelve, el motor borra los valores que hay detrás de `keyboardGetLastDown` y `keyboardGetLastUp`. En el modelo con hilo, `Framework.singe` define esta función por su cuenta para reanudar `singeMain` y siempre devuelve `OVERLAY_UPDATED`; no la definas tú también.
*Devuelve:* `OVERLAY_UPDATED` u `OVERLAY_NOT_UPDATED`.
*Véase también:* <<singemain,singeMain>>, <<keyboardgetlastdown,keyboardGetLastDown>>, <<onshutdown,onShutdown>>
.Ejemplo
[source,lua]
----
function onOverlayUpdate()
if not hudDirty then
return OVERLAY_NOT_UPDATED
end
overlayClear()
fontPrint(20, 20, "SCORE " .. score)
fontPrint(20, 44, "LIVES " .. lives)
spriteDraw(reticle, cursorX, cursorY, true)
hudDirty = false
return OVERLAY_UPDATED
end
----
[#onshutdown]
==== onShutdown
[source,text]
----
function onShutdown()
end
----
Se llama una vez cuando termina el script, ya sea por `singeQuit`, el interruptor de salida, el cierre de la ventana, `scriptExecute` o `scriptPush`, después del último fotograma y antes de descartar el estado de Lua. `singeReload` no lo llama. Guarda aquí los ajustes y libera los sprites, sonidos, fuentes y vídeos que cargó el script; después, el motor detiene todos los sonidos y libera lo que quede, así que un handle olvidado no es una fuga, pero descargar de forma explícita deja el orden de cierre bajo el control del script. No llames a `singeQuit` desde aquí.
*Véase también:* <<singequit,singeQuit>>, <<soundunload,soundUnload>>, <<scriptpush,scriptPush>>
.Ejemplo
[source,lua]
----
function onShutdown()
saveHighScores()
spriteUnload(reticle)
fontUnload(hudFont)
soundUnload(shotClip)
soundUnload(coinClip)
end
----
[#onsoundcompleted]
==== onSoundCompleted
[source,text]
----
function onSoundCompleted(channel)
end
----
Se llama con el número de canal cada vez que un canal se detiene: cuando termina su clip, cuando acaba la última repetición y cuando lo para `soundStop` o `soundFullStop`, así que un script que encadena sonidos desde aquí debe saber qué canales ha parado él mismo. El mezclador anota la parada en su propio hilo y el motor entrega el callback en el hilo del juego entre fotogramas; mientras la tecla de pausa mantiene el juego congelado, los avisos esperan, y se descartan cuando un script termina o se recarga. Se ponen en cola hasta 64 finalizaciones entre fotogramas; las que pasen de ahí en un mismo fotograma se pierden.
*Parámetros:*
* `channel` -- el número de canal que devolvió la llamada a `soundPlay`.
*Véase también:* <<soundplay,soundPlay>>, <<soundstop,soundStop>>, <<soundisplaying,soundIsPlaying>>
.Ejemplo
[source,lua]
----
-- Reproduce la lista de música en orden, para siempre.
function onSoundCompleted(channel)
if channel == musicChannel and not musicStopped then
track = track % #playlist + 1
musicChannel = soundPlay(playlist[track])
end
end
----
[#singemain]
==== singeMain
[source,text]
----
function singeMain()
while true do
singeYield()
end
end
----
El punto de entrada del modelo con hilo. Cuando un script la define, `Framework.singe` la envuelve en una corrutina e instala un `onOverlayUpdate` que reanuda la corrutina una vez por fotograma, así que la función se ejecuta como una línea recta de lógica de juego que llama a `singeYield()` (un alias de `coroutine.yield`) allí donde quiera dejar pasar un fotograma. Todo lo que se dibuja antes de una cesión acaba en el overlay de ese fotograma. Volver de `singeMain` llama a `singeQuit`; un error dentro de ella termina el juego con una traza de la pila. Los callbacks de entrada, sonido, física y navegación se siguen disparando entre reanudaciones, así que un juego con hilo puede combinarlos con consultas.
*Véase también:* <<onoverlayupdate,onOverlayUpdate>>, <<keyboardgetlastdown,keyboardGetLastDown>>, <<singequit,singeQuit>>
.Ejemplo
[source,lua]
----
function singeMain()
showTitle()
while credits == 0 do
singeYield()
end
for level = 1, #levels do
playLevel(levels[level])
if lives == 0 then
break
end
end
showGameOver()
end
----
[#_hypseus_compatibility]
=== Compatibilidad con Hypseus
IMPORTANT: Todo lo de esta sección existe para que un juego escrito para Hypseus
Singe funcione sin cambios. *Nada de ello se recomienda para un juego nuevo.* Usa
preferentemente la llamada de Singe que nombra cada entrada: es la que seguirá
ganando funciones, y no arrastra las escalas, los órdenes de argumentos ni las
respuestas fijas de otro motor.
Hypseus Singe registra en su motor unos cuantos nombres que Singe atiende en cambio
desde `Singe/Framework.singe`, para que un juego escrito para Hypseus funcione
sin cambios. Cada uno se define solo si el motor no lo proporciona ya, así que una
futura implementación en el motor tiene prioridad. `keyboardCatchQuit` se documenta
en <<keyboard,Teclado>>; `setOverlayLinearScale` y `setOverlayOnTop` son
llamadas reales del motor en <<overlay,Overlay>>; `mainBezelLoaded` y las siete
llamadas `scoreBezel` son llamadas reales del motor en <<bezelref,Bezel>>;
`getFValue`, `ratioGetX` y `ratioGetY` son llamadas reales del motor en
<<commandlineref,Línea de comandos>>; `joyMouseEnable` es una llamada real del motor
en <<joystick,Joystick>>; `allowSocketCall` y `getUserString` son llamadas reales
del motor en <<singe,Singe>>, y `drawTransparent` en <<sprite,Sprite>>;
y toda la familia `music` es ahora un conjunto de llamadas reales del motor en
<<musicref,Música>>. Los seis nombres `spriteAnim` de abajo son
simples alias que se fijan después del envoltorio heredado de los sprites, así que siguen
el orden de argumentos que esté siguiendo el resto de la familia de sprites: el propio
de Hypseus (el handle al final) con `LEGACY_SPRITE_ARGS`, y el de Singe 3.00 (el handle
primero) sin él.
[cols="1,1",options="header"]
|===
| Función | Lo proporciona Framework.singe como
| `setOverlaySize(preset [, width, height])` | `overlaySetResolution` con un tamaño elegido: `1` es el tamaño del vídeo del disco, `3` es `360x240`, `4` es el tamaño indicado (que se ignora si falta, como en Hypseus) y cualquier otro valor, `2` incluido, es la mitad del disco en los dos ejes. Sin disco, `discGetWidth` y `discGetHeight` informan del lienzo, que es donde dibuja un juego sin disco.
| `setOverlayResolution(width, height)` | Un alias de <<overlaysetresolution,`overlaySetResolution`>>. Hypseus registra una sola función con los dos nombres.
| `rewriteStatus()` | Función vacía que devuelve `false`. En Hypseus indica si se están reescribiendo las rutas de los recursos para una instalación `-espath`; el sistema de archivos virtual de Singe encuentra los archivos de un juego donde se instalaron, así que no se reescribe nada y el juego conserva su propia carpeta de recursos.
| `hypseusGetHeight()` | Un alias de <<singegetheight,`singeGetHeight`>>.
| `hypseusGetWidth()` | Un alias de <<singegetwidth,`singeGetWidth`>>.
| `takeScreenshot()` | Un alias de <<singescreenshot,`singeScreenshot`>>. Hypseus rechaza una segunda captura de pantalla en menos de cinco segundos; Singe no tiene ese límite.
| `mouseHowManyReal()` | Un alias de <<mousehowmany,`mouseHowMany`>>. Hypseus cuenta los ratones reales aparte de los que deriva de un mando; Singe no tiene ratones derivados, así que su recuento ya es el real.
| `spriteAnimGetFrame(spr)` | Un alias de <<spritegetframe,`spriteGetFrame`>>.
| `spriteAnimIsPlaying(spr)` | Un alias de <<spriteisplaying,`spriteIsPlaying`>>.
| `spriteAnimLoop(loop, spr)` | Un alias de <<spriteloop,`spriteLoop`>>.
| `spriteAnimPause(spr)` | Un alias de <<spritepause,`spritePause`>>.
| `spriteAnimPlay(spr)` | Un alias de <<spriteplay,`spritePlay`>>.
| `spriteAnimSetFrame(frame, spr)` | Un alias de <<spritesetframe,`spriteSetFrame`>>.
|===
[#unimplementedandlegacy]
=== Sin implementar y heredado
Un puñado de funciones de la API se conservan por compatibilidad con los scripts, y las marcadas «No hace nada» no hacen nada. Se pueden llamar sin peligro; simplemente escriben un mensaje en la traza y vuelven. En código nuevo, usa preferentemente los sustitutos de la lista.
[cols="1,1,1",options="header"]
|===
| Función | Estado | Sustituto
| `discChangeSpeed` | No hace nada | -- (servía para la reproducción a velocidad variable en hardware LD real)
| `discSearchBlanking` | No hace nada | -- (servía para apagar la pantalla durante una búsqueda en hardware LD real)
| `discSkipBlanking` | No hace nada | -- (por el mismo motivo)
| `discSetFPS` | No hace nada | La tasa de fotogramas se lee del archivo de vídeo.
| `vldpGetHeight` | Funciona | <<discgetheight,`discGetHeight`>>
| `vldpGetWidth` | Funciona | <<discgetwidth,`discGetWidth`>>
| `vldpSetVerbose` | No hace nada | --
| `colorBackground(r,g,b)` | Funciona | Usa preferentemente la forma de 4 argumentos, `colorBackground(r, g, b, a)`.
| `colorForeground(r,g,b)` | Funciona | Usa preferentemente la forma de 4 argumentos, `colorForeground(r, g, b, a)`.
| `daphneGetWidth` / `daphneGetHeight` / `daphneScreenshot` | Alias en `Framework.singe` | Usa preferentemente los equivalentes `singe*`.
| `discPauseAtFrame` | Funciona (alias) | <<discsearch,`discSearch`>>.
|===
Además, `Framework.singe` vuelve a enlazar `random = { new = math.random }` para que los scripts anteriores a 2.00 que usan `random.new()` sigan funcionando. El código nuevo debe usar `math.random` directamente.