19300 lines
800 KiB
Text
19300 lines
800 KiB
Text
= Singe-Referenz
|
|
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: de
|
|
:imagesdir: ..
|
|
include::locale.adoc[]
|
|
|
|
|
|
[#_game_development]
|
|
== Spieleentwicklung
|
|
|
|
[#_lua]
|
|
=== Lua
|
|
|
|
Singe verwendet die Programmiersprache Lua (http://www.lua.org), um die Spiellogik zu skripten. Lua ist schnell, schlank, objektorientiert, einfach zu benutzen und wird in der Spielebranche tatsächlich eingesetzt. Dieses Buch bringt dir Lua nicht bei; _Programmieren lernen mit Singe_, im selben Download wie dieses, tut es, von null bis zu einem fertigen Spiel in einunddreißig Lektionen. Video-Tutorials zu Lua gibt es auf dem YouTube-Kanal von Kangaroo Punch, und die Referenz der Sprache selbst steht unter http://www.lua.org.
|
|
|
|
[#_basic_rules]
|
|
=== Grundregeln
|
|
|
|
* Singe ist plattformübergreifend. Dinge, um die du dich unter Windows normalerweise nicht kümmern musst, sind wichtig. Dateinamen unterscheiden zwischen Groß- und Kleinschreibung -- `MyScript.singe` und `myscript.singe` sind nicht dieselbe Datei. Verwende außerdem UNIX-Pfadtrenner -- `/` statt `\`. Das macht deinen Code sauberer (keine doppelt maskierten Backslashes) und funktioniert überall, wo Singe unterstützt wird.
|
|
* Wähle einen unverwechselbaren Namen für deinen Spielordner. Verwende keine Leerzeichen.
|
|
* Verwende das mitgelieferte Singe-Framework. Kopiere das Framework nicht in deine Spielskripte. Indem du das mitgelieferte Framework einbindest, hilfst du der künftigen Kompatibilität, wenn Singe aktualisiert wird. Beginne (oder beende) dein Spielskript mit:
|
|
+
|
|
[source,lua]
|
|
----
|
|
dofile("Singe/Framework.singe")
|
|
----
|
|
* Halte dein Spiel in sich abgeschlossen. Wenn du auf einem Framework eines Drittanbieters aufbaust oder Code zwischen deinen Spielen teilst, kopiere ihn in deinen Spielordner. Verweise nie auf einen Ordner neben deinem Spiel; die einzige Datei außerhalb deines Spiels, die ein Skript laden darf, ist `Singe/Framework.singe`. Ein Spiel in einer Datei (siehe <<singlefile,Spiele in einer Datei>>) packt genau einen Ordner, und ein Spiel, das über ihn hinausgreift, lässt sich nicht packen.
|
|
* Halte dich aus dem Ordner `Singe/` heraus. Er wird von Singe verwaltet, und alles, was dort hinzugefügt oder geändert wird, kann künftig gelöscht werden.
|
|
* Liefere eine `games.dat` mit. Das ist für neue Benutzer äußerst wichtig. Es gibt zwar andere Frontends und auf Singe basierende Menüsysteme, aber es ist unerlässlich, dass du Daten für das eingebaute Menüsystem mitlieferst. Fast alle Supportanfragen gehen auf fehlende oder fehlerhafte `games.dat`-Dateien zurück.
|
|
* Verteile keine unnötigen Dateien. Liefere nie Singe-Programmdateien oder von Singe mitgelieferte Skripte mit deinem Spiel aus. Liefere keine aus den Videos erzeugten Indexdateien mit.
|
|
|
|
[#reloading]
|
|
=== Neu laden während der Arbeit
|
|
|
|
Starte das Spiel mit `--reload` (`-R`), und Singe überwacht jede ungepackte Skriptdatei, die das Spiel geladen hat. Speichere eine Änderung an einer davon, und das Spiel beginnt einen Moment später wieder bei seinem Skript: Alles, was das Skript angelegt hat, verschwindet (Sprites, Sounds, Schriftarten, Videos, GUIs, die 3D-Szene, die Physik, Emitter, das Overlay kehrt zu seiner Standardgröße zurück, die Effektlautstärke und die Maus zu ihrem Zustand beim Start), die Engine bleibt (das Fenster, die GPU, die Controller), die Disc steht wie beim Start wieder auf ihrem ersten Frame, und das Skript läuft von vorn, sodass ein 52-MB-Modell in der Zeit wieder auf dem Bildschirm ist, die sein Laden dauert, statt in der Zeit, die der Start von Singe dauert. Eine Taste oder ein Knopf, die über das Neuladen hinweg gedrückt bleiben, werden ignoriert, bis sie losgelassen werden, wie bei einem frischen Start. `F5` lädt auf Wunsch neu, und ein Skript kann es mit `singeReload()` anfordern. Gepackte Spiele haben nichts, was sich überwachen ließe; die Option ist für ein Spiel in einem Ordner. Ein Skriptfehler beim Neuladen wird ausgegeben, und das Spiel bleibt leer, bis das nächste Neuladen ihn behebt; die Datei bleibt überwacht, das Speichern der Korrektur genügt also. Weil das Neuladen denselben Abbau durchläuft wie das Beenden, zeigt sich jedes Leck eines Skripts zuerst hier.
|
|
|
|
[[deterministic]]
|
|
=== Deterministischer Testmodus
|
|
|
|
`--deterministic` ist eine Testhilfe, keine Art zu spielen. Die Option lässt die Engine die echte Zeit ganz ignorieren: Statt den Rechner zu fragen, wie spät es ist, läuft die Engine auf einer virtuellen Uhr, die sich um einen festen Schritt -- standardmäßig 15 Millisekunden -- genau einmal pro Frame weiterbewegt. Alles, was nach der Zeit getaktet ist, folgt dieser Uhr: Physikschritte, Sprite- und Modellanimation, Partikel, Navigation, die Animationen und Übergänge der GUI, `os.clock()`, `singeGetTicks()` und ein bloßes `os.time()` (ein Spiel, das seinen Zufallsgenerator beim Spielstart nach der Uhr initialisiert, initialisiert ihn nach der virtuellen und zieht also bei jedem Lauf dieselben Zahlen; `os.time` mit einer Datumstabelle rechnet diese um, wie Lua es immer getan hat). Auch die Disc folgt ihr: Während sie läuft, bleibt jeder Videoframe so viele Engine-Frames lang stehen, wie seine Darstellungszeit auf dieser Uhr wert ist (zwei oder drei beim Standardschritt für eine Disc mit dreißig Frames pro Sekunde), wie im echten Spiel, und eine pausierte oder per Sprung versetzte Disc bleibt, wo sie hingestellt wurde. Der Zufallsgenerator von Lua und der eigene der Engine werden mit einer festen Zahl initialisiert. Nichts wartet auf irgendetwas, das Spiel läuft also so schnell, wie der Rechner es zulässt.
|
|
|
|
Der Sinn des Ganzen ist, dass ein Screenshot, der bei einer festen Frame-Nummer aufgenommen wird, in jedem Lauf dasselbe Bild ist. Ohne diesen Modus ist Frame 40 jedes Mal ein anderer Moment -- die Physik hat eine andere Zahl von Schritten gemacht und die Disc steht auf einem anderen Videoframe -- und so können sich zwei Läufe desselben unveränderten Spiels um eine Million Pixel unterscheiden, und eine echte Änderung kann sich in diesem Rauschen verstecken.
|
|
|
|
Der Preis ist alles, was von der echten Zeit abhängt. Audio spielt weiterhin nach der eigenen Uhr des Geräts, die keine virtuelle Uhr erreicht, der Ton driftet also vom Bild weg und sagt nichts aus -- es sei denn, der Lauf wird mit `--record` aufgezeichnet; dann wird der Ton zusammen mit dem Bild Schritt für Schritt in den Speicher gemischt, sodass eine so gemachte Aufnahme Frame für Frame und Sample für Sample exakt ist (siehe <<singerecordstart,singeRecordStart>>). Auch die Bildrate sagt nichts aus, und ebenso wenig alles, was ein Spiel in Sekunden misst. Melde nie eine Leistungszahl, ein Audioproblem oder ein Problem mit der Zeitsteuerung aus einem Lauf in diesem Modus, und liefere nie einen Launcher aus, der ihn verwendet.
|
|
|
|
Die Zahl, die die Option trägt, wenn sie eine trägt, ist sowohl die Größe des Schritts als auch der Startwert der Zufallsgeneratoren, `1` bis `1000`:
|
|
|
|
----
|
|
Singe --deterministic der Standardschritt von 15 ms, Startwert 15
|
|
Singe --deterministic=20 ein Schritt von 20 ms, Startwert 20
|
|
----
|
|
|
|
Zwei Läufe mit derselben Zahl sind identisch; zwei Läufe mit verschiedenen Zahlen sind jeder für sich wiederholbar, aber nicht gleich, und so verlangt ein Test einen anderen, aber ebenso reproduzierbaren Lauf. Die Option lässt sich wie jede andere als `deterministic` in einer Einstellungsdatei setzen.
|
|
|
|
[#_reference_screenshots]
|
|
==== Referenz-Screenshots
|
|
|
|
Die Szenen in `testScripts` machen jeweils einen oder zwei Screenshots bei einer festen Frame-Nummer und beenden sich selbst, in diesem Modus ergeben sie also einen Satz Referenzbilder, mit dem sich eine spätere Änderung Pixel für Pixel vergleichen lässt. Um sie aufzunehmen, starte jede Szene mit der Option und behalte, was im Datenordner landet:
|
|
|
|
[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
|
|
----
|
|
|
|
Die Szenen, die diese Schleife auslässt, wollen eigene Optionen, dieselben, die ihnen die Tabelle in `testScripts/README.md` gibt: `scene12` läuft ganz ohne Disc, `scene17` mit der Disc und `scene20` mit `-C 720x480`, alle drei mit `SDL_GPU_DRIVER=nothing`; `scene27` bis `scene35` wollen `-x 1280 -y 720 -C 1280x720` und keine Disc; `scene46` will `-g '10 5'`; und `scene47` will `--bezeldir=testScripts/bezels --bezel=cabinet.png`. Kopiere nach jedem Lauf die `singe*.png`-Dateien aus `data/testScripts` heraus, denn der nächste Lauf nummeriert seine Screenshots wieder ab null.
|
|
|
|
Einen späteren Build zu vergleichen heißt dann, dieselben Befehle auszuführen und nach dem Unterschied zu fragen, der null sein muss:
|
|
|
|
[source,sh]
|
|
----
|
|
compare -metric AE reference/scene36-singe000.png new/scene36-singe000.png null:
|
|
----
|
|
|
|
[#_where_files_live]
|
|
=== Wo Dateien liegen
|
|
|
|
Pfade in einem Skript sind relativ zu dem Ordner, aus dem Singe gestartet wurde, nicht zum Skript. Verwende die globale Variable `DIR`, die `Framework.singe` auf den eigenen Ordner des Skripts setzt, damit ein Spiel funktioniert, egal wo es installiert ist:
|
|
|
|
[source,lua]
|
|
----
|
|
crosshair = spriteLoad(DIR .. "sprite_Crosshair.png")
|
|
----
|
|
|
|
Alles, was Singe schreibt, landet im Datenordner (`singeGetDataPath()`), `data/` neben den Spielen, sofern `--datadir` nichts anderes sagt, damit ein Spiel auch auf schreibgeschützten Medien liegen kann. Singe 2.x schrieb Spielstände und Einstellungen in den eigenen Ordner des Spiels; ein Spiel, das das noch tut, scheitert an einer schreibgeschützten Installation, halte also jedes `io.output` unter `singeGetDataPath()`.
|
|
|
|
[#_packaging_your_game]
|
|
=== Dein Spiel verpacken
|
|
|
|
Ein fertiges Spiel wird als eine einzige `.game`-Datei ausgeliefert, die als Nächstes beschrieben wird. Mehr ist nicht nötig: Der Spieler kopiert die Datei neben die Singe-Programmdatei, und das Menü listet sie auf. Singe installiert keine Archive mehr.
|
|
|
|
[#singlefile]
|
|
=== Spiele in einer Datei
|
|
|
|
Ein Spiel kann eine einzige Datei sein. `singe --pack` schreibt einen Spielordner in eine SQLite-Datenbank, und Singe startet das Spiel von dort: Skripte, Sprites, Sounds, Schriftarten, Frame-Dateien und Video werden alle direkt aus der Datenbank geladen, und das Spiel zu installieren heißt, die Datei neben die ausführbare Datei zu kopieren. Das Menü listet jede `.game`-Datei auf, die es neben den Spielordnern findet.
|
|
|
|
----
|
|
Singe --pack MyGame MyGame.game Den Ordner MyGame in MyGame.game packen
|
|
Singe MyGame.game Den ersten games.dat-Eintrag darin starten
|
|
Singe --entry=2 MyGame.game Seinen zweiten Eintrag starten
|
|
Singe --unpack MyGame.game MyGame Die Dateien wieder herausschreiben
|
|
Singe --patch MyGame.game fixes Dateien aus einem Ordner (oder einer Patch-Datenbank) ersetzen
|
|
----
|
|
|
|
Nichts an einem Spiel muss sich ändern, damit es gepackt werden kann. Jeder Name, den ein Skript verwendet, ob über `dofile`, `require`, `io.open` und seine Verwandten, `spriteLoad`, `soundLoad`, `fontLoad`, `videoLoad`, eine Frame-Datei oder eine `controls.cfg`, geht durch ein und dieselbe Suche. Bei einem ungepackten Spiel ist diese Suche das gewöhnliche Dateisystem, relativ zu dem Ordner, aus dem Singe gestartet wurde, genau wie zuvor. Bei einem gepackten Spiel wird ein Name an drei Stellen versucht, in dieser Reihenfolge, und der erste Treffer gewinnt:
|
|
|
|
. Ein ungepackter Ordner, der wie die Datenbank ohne ihre Endung heißt (`MyGame/` neben `MyGame.game`). So bearbeitest du ein gepacktes Spiel weiter, und so moddet ein Spieler eines: Dateien, die dort abgelegt werden, haben Vorrang vor den gepackten Kopien.
|
|
. Der Datenordner des Spiels, unter `files/`. Alles, was ein Spiel mit `io.output` oder `io.open(name, "w")` schreibt, landet hier, sodass eine gespeicherte `game.cfg` Vorrang vor dem gepackten Original hat und die Datenbank nie beschrieben wird.
|
|
. Die Datenbank selbst.
|
|
|
|
Innerhalb einer Datenbank werden Namen ohne Rücksicht auf Groß- und Kleinschreibung abgeglichen, mit beiden Pfadtrennern und relativ zur Wurzel des Spiels. Eine führende Komponente, die dem eigenen Ordnernamen des Spiels gleicht, wird ignoriert, sodass `MyGame/Cfg/game.cfg`, `Cfg/game.cfg` und `DIR .. "Cfg/game.cfg"` alle dieselbe Datei finden. Der Name der Datenbank selbst (`MyGame.game`, mit oder ohne nachgestellten Trenner) ist die Wurzel des Spiels, so wie `.` von innen. Namen, die mit `Singe/` oder mit dem Datenordner beginnen, bleiben im Dateisystem; `Singe/Framework.singe` und `singeGetDataPath()` funktionieren wie immer, und ein solcher Name darf `..` verwenden, solange er unterhalb dieses Präfixes bleibt (die `controls.cfg` im obersten Datenordner wird so gefunden). Jeder andere relative Name mit einer `..`-Komponente führt innerhalb eines gepackten Spiels nirgendwohin: `io.open` gibt `nil` mit einer Meldung zurück, dass der Name aus dem Spiel hinausreicht, `lfs.attributes` gibt `nil` zurück, und `spriteLoad` und seinesgleichen melden die Datei als fehlend. Die Wurzel des Spiels ist ein Ordner, keine Datei: `io.open` darauf gibt `nil` zurück, und `lfs.mkdir` und `lfs.rmdir` lehnen sie ab. `lfs.dir` auf einem gepackten Ordner listet die Vereinigung der drei Stellen auf, ohne Rücksicht auf Groß- und Kleinschreibung sortiert; wenn sich eine ungepackte und eine gepackte Datei nur in der Schreibweise unterscheiden, wird die Schreibweise der ungepackten aufgelistet. `lfs.attributes` auf einer gepackten Datei antwortet so wie das echte `lfs`: mit der ganzen Tabelle bei einem Argument, mit einem Feld nach Namen (ein unbekannter Name löst einen Fehler aus) oder indem es eine Tabelle von dir ausfüllt; eine fehlende Datei gibt `nil`, eine Meldung und die Fehlernummer zurück. `require("name")` findet `name.lua`, `name/init.lua` oder `name.singe` unter dem Ordner des Skripts, dann unter der Wurzel des Spiels.
|
|
|
|
`io.open`, `io.lines` und `io.input` geben bei einem Namen, der nur in der Datenbank liegt, eine Datei von Singe selbst zurück statt einer von Lua. Das Datei-Handle von Lua ist ein `FILE *` von C, und eine Datenbankzeile ist keine Datei, deshalb hieß das Lesen einer solchen Zeile früher, sie neben das Spiel zu schreiben und diese Kopie zu öffnen; das Handle von Singe liest die Zeile dort, wo sie liegt. Es versteht alles, was das von Lua versteht -- `read` mit jedem Format (`"a"`, `"l"`, `"L"`, `"n"` und einer Byteanzahl), `lines`, `seek`, `close`, `flush` und `setvbuf` -- `io.type` nennt es eine Datei, `io.read` und `io.close` nehmen es an, und es schließt sich selbst, wenn es vom Garbage Collector eingesammelt wird oder wenn eine `<close>`-Variable ihren Gültigkeitsbereich verlässt. Der einzige Unterschied ist, dass Schreiben darauf ein Fehler ist, denn was es liest, liegt in der Datenbank, und die Datenbank wird nie beschrieben. Ein Spiel, das eine Datei schreibt, bekommt genau wie zuvor eine echte in seinem Datenordner, und ebenso jeder Name, der nicht gepackt ist. `testScripts/packedIo` ist der Test, der all das am eigenen Verhalten von Lua misst; sein Kopfkommentar sagt, wie man ihn ausführt.
|
|
|
|
Der Packer lehnt einen Ordner ab, der keine `games.dat` hat, der `controls.dat`, `Framework.singe`, eine Datei mit der Endung `exe`, `sh`, `bat` oder `cmd` oder eine Datei ohne Endung enthält, deren Name mit `singe` beginnt, der auf oberster Ebene einen Eintrag hat, der wie der Ordner selbst heißt (das würde die Regel vom eigenen Ordner mehrdeutig machen), oder dessen Skripte und Datendateien mit `..` aus dem Spiel hinausgreifen; er meldet jede solche Datei, mit der Zeile. Zwei Dateien, deren Namen sich nur in der Groß- und Kleinschreibung unterscheiden, können nicht beide gepackt werden. Veraltete `.index`-Dateien werden übersprungen. `--unpack` lehnt eine Datenbank ab, deren gespeicherte Namen außerhalb des Zielordners landen würden (ein absoluter Pfad, ein Laufwerksbuchstabe oder eine `..`-Komponente). Ein Spiel, das auf den Ordner eines anderen Spiels verweist (ein gemeinsames Framework daneben), muss diesen Ordner zuerst hineinkopieren. Bevor die Datenbank festgeschrieben wird, wird ihre `games.dat` geprüft: Jeder Eintrag braucht ein `SCRIPT`, und jedes `SCRIPT`, `VIDEO`, `CABINET`, `MARQUEE` und `ATTRACT`, das er nennt, muss eine Datei in der Datenbank sein (oder unter `Singe/`); ein falscher Pfad wird mit dem Titel des Eintrags gemeldet, und das Packen wird verweigert. `--patch` führt dieselbe Prüfung am gepatchten Spiel durch, sodass auch ein Patch keinen ins Leere zeigenden Pfad hinterlassen kann.
|
|
|
|
`--patch` nimmt entweder einen Ordner, der wie das Spiel aufgebaut ist und dessen Dateien die gepackten ersetzen, oder eine `.patch`-Datei, die `--pack` aus einem solchen Ordner erzeugt (`Singe --pack fixes MyGame-1.1.patch`; ein Patch braucht keine `games.dat`). Dateien, die der Patch nicht nennt, bleiben unberührt. So oder so ist die Änderung eine einzige Transaktion: Das Spiel bleibt intakt, wenn sie unterbrochen wird. Ein Patch kann auch eine Tabelle `removed(path)` mitbringen, die zu löschende Dateien nennt. Eine `.patch` lässt sich wie ein Spiel entpacken, aber sie kann nicht gestartet werden, und das Menü ignoriert sie.
|
|
|
|
Ein Patch, der eine große Datei durch eine kleine ersetzt, lässt den Platz, den der alte Inhalt belegt hat, in der Datenbank zurück, wo SQLite ihn für das Nächste behält, was geschrieben wird, statt ihn zurückzugeben. Für einen Patch, der ein Skript ändert, ist das in Ordnung, und für einen Entwickler, der ein Video ein Dutzend Mal neu packt, verschwenderisch: Die `.game` kann am Ende ein Mehrfaches der Größe ihres Inhalts haben. Deshalb sieht sich `--patch` den freien Platz an, sobald die Transaktion festgeschrieben ist, und wenn es mindestens ein Megabyte davon gibt, verdichtet es die Datenbank und sagt, wie viel es zurückgewonnen hat:
|
|
|
|
----
|
|
>>> Recovered 8007680 bytes of space the old contents were using
|
|
----
|
|
|
|
Das Ergebnis hat aufs Byte genau die Größe eines frischen Packens derselben Dateien. Das Verdichten schreibt die ganze Datei neu, deshalb wird es übersprungen, wenn es wenig zu gewinnen gibt. Nichts sonst in Singe braucht das; ein Spiel wird immer nur vom Packer geschrieben.
|
|
|
|
Der Datenordner eines gepackten Spiels ist nach der Datenbank benannt und dann nach dem Ordner des Skripts darin: `MyGame/` für eine `MyGame.game`, deren Skript an ihrer Wurzel liegt, unter dem Datenordner (standardmäßig `data/MyGame/`), also dort, wo ihn auch das ungepackte `MyGame/MyGame.singe` hat, sodass Spielstände und Einstellungen zwischen einer ungepackten und einer gepackten Installation geteilt werden; ein in einem Unterordner gepacktes Skript (`Game/Main.singe`) bekommt `MyGame/Game/`. Auch der Videoindex kommt dorthin.
|
|
|
|
NOTE: Frühere 3.00-Builds gaben einem gepackten Spiel, dessen Skript in einem Unterordner lag, den Datenordner `Game/` unter der Basis, den sich jedes Archiv mit diesem Aufbau teilte. Die Spielstände eines solchen Spiels gehören jetzt unter `MyGame/Game/`; verschiebe sie von Hand. Frühere 3.00-Builds behielten außerdem den eigenen Ordner des Spiels bei, den, unter dem seine `games.dat` das Skript nennt, sodass eine aus `MyGame/` gepackte `MyGame.game` nach `MyGame/MyGame/` schrieb; verschiebe, was dort liegt, eine Ebene nach oben.
|
|
|
|
Wenn Singe ein Spiel aus einer Datenbank startet, übergibt das Menü `CONTAINER` im `games.dat`-Eintrag, den es startet; das Feld wird vom Menü gesetzt, nie von Hand.
|
|
|
|
Für deine eigenen Daten gibt `require("sqlite3")` Skripten die Anbindung https://lua.sqlite.org[lsqlite3] an dasselbe SQLite, das die Engine verwendet. Lege Datenbanken, die das Spiel schreibt, in `singeGetDataPath()` ab:
|
|
|
|
[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` findet einen Namen so wie `io.open`, eine Datenbank kann also im Spiel mitgeliefert werden -- Level-Tabellen, Dialoge, Gegenstandslisten -- und lässt sich trotzdem öffnen, wenn das Spiel gepackt ist. Nur zum Lesen geöffnet, wird eine gepackte Datenbank dort gelesen, wo sie liegt, ohne dass etwas kopiert wird:
|
|
|
|
[source,lua]
|
|
----
|
|
local levels = sqlite3.open(DIR .. "levels.db", sqlite3.OPEN_READONLY)
|
|
----
|
|
|
|
Zum Schreiben geöffnet, was die Voreinstellung von `sqlite3.open` ist, wird eine gepackte Datenbank einmal in den Datenordner des Spiels kopiert, unter `files/` wie alles andere, was ein gepacktes Spiel schreibt, und die Kopie wird geöffnet. Von da an bezeichnet der Name die Kopie, ob nur zum Lesen oder nicht, und das gepackte Original wird nie verändert. Die Datenbank eines ungepackten Spiels öffnet sich genau wie zuvor an Ort und Stelle. `testScripts/packedSql` ist der Test; sein Kopfkommentar sagt, wie man ihn ausführt.
|
|
|
|
Das Dateiformat, für alle, die Werkzeuge schreiben: drei Tabellen. `meta(key, value)` enthält `version` (1), `gamedir` (den Namen des gepackten Ordners), `chunk` (die Chunk-Größe in Bytes, 4194304) und `packer`. `assets(path, name, size, data)` hat eine Zeile pro Datei, wobei `path` der kleingeschriebene Schlüssel mit Schrägstrichen ist und `name` die Schreibweise des Autors; Dateien, die nicht größer als die Chunk-Größe sind, stehen in `data`, größere haben `data` NULL und liegen in `chunks(path, chunk, data)` als fortlaufend nummerierte Stücke. Singe öffnet die Datenbank nur lesend und unveränderlich, sodass es von schreibgeschützten Medien läuft und nie ein Journal daneben anlegt.
|
|
|
|
[#_event_driven_or_not]
|
|
=== Ereignisgesteuert ... oder nicht?
|
|
|
|
Traditionell verwendete Singe ein ereignisgesteuertes Programmiermodell -- Singe kümmert sich um alles und ruft deinen Code nur auf, wenn es dir etwas mitteilen oder den Bildschirm aktualisieren muss. Das ist effizient und entspricht der Arbeitsweise der meisten Lua-basierten Spiel-Engines, kann aber umständlich sein. Seit Singe 2.10 gibt es ein neues _Thread-Modell_ („threaded“), mit dem du prozeduralen Code schreiben kannst.
|
|
|
|
[#_event_driven]
|
|
==== Ereignisgesteuert
|
|
|
|
Beim ereignisgesteuerten Programmiermodell steuert Singe die Hauptschleife des Programms und bestimmt die Reihenfolge der Ausführung. Singe kümmert sich automatisch um alle Einzelheiten des Decodierens und Darstellens von Video und Audio. Es verwaltet Controller, Mäuse und Tastatureingaben. Wenn Singe etwas Spielspezifisches braucht, ruft es einen Teil deines Skripts auf. Das einfachste Singe-Skript, das alle vorhandenen Callbacks zeigt, sieht so aus:
|
|
|
|
[source,lua]
|
|
----
|
|
-- Singe-Spielgerüst.
|
|
|
|
-- Das Singe-Framework laden.
|
|
dofile("Singe/Framework.singe")
|
|
|
|
-- Deklariere hier alle globalen Variablen, die du brauchst.
|
|
|
|
function onControllerMoved(axis, value, which)
|
|
--[[
|
|
Meldet, welche Controller-Achse bewegt wurde, und ihren aktuellen Wert.
|
|
(Bereich: -32768 bis 32767.) Das wird für analoge Geräte verwendet.
|
|
Digitale Eingaben werden von onInput und onKey behandelt.
|
|
--]]
|
|
end
|
|
|
|
function onInputPressed(what, device)
|
|
--[[
|
|
Im Tastaturmodus MODE_NORMAL werden Eingabeereignisse hier gemeldet, sobald
|
|
die Taste oder der Knopf zuerst gedrückt wird. Eine vollständige Liste der
|
|
Tasten, Knöpfe und Controller steht in Singe/Framework.singe. Bei MODE_FULL
|
|
wird dieses Ereignis mit dem Keysym der gedrückten Taste aufgerufen. Kam der
|
|
Schalter von einer Maus oder einer Lightgun, ist device der Index dieses Geräts
|
|
(0 für das erste), so wie Hypseus ihn übergibt, damit ein Spiel mit zwei Lightguns
|
|
seine Spieler unterscheiden kann; bei einer Taste oder einem Pad ist es nil.
|
|
--]]
|
|
end
|
|
|
|
function onInputReleased(what, device)
|
|
--[[
|
|
Im Tastaturmodus MODE_NORMAL werden Eingabeereignisse hier gemeldet, wenn
|
|
die Taste oder der Knopf losgelassen wird. Eine vollständige Liste der Tasten,
|
|
Knöpfe und Controller steht in Singe/Framework.singe. Bei MODE_FULL wird
|
|
dieses Ereignis mit dem Keysym der losgelassenen Taste aufgerufen. device ist
|
|
wie bei onInputPressed.
|
|
--]]
|
|
end
|
|
|
|
function onKeyPressed(key, scancode)
|
|
--[[
|
|
Im Tastaturmodus MODE_FULL werden Eingabeereignisse hier gemeldet, wenn die
|
|
Taste gedrückt wird. Sowohl der Keysym als auch der Scancode werden geliefert.
|
|
Eine Liste der verfügbaren Scancodes steht in Singe/Framework.singe.
|
|
--]]
|
|
end
|
|
|
|
function onKeyReleased(key, scancode)
|
|
--[[
|
|
Im Tastaturmodus MODE_FULL werden Eingabeereignisse hier gemeldet, wenn die
|
|
Taste losgelassen wird. Sowohl der Keysym als auch der Scancode werden
|
|
geliefert. Eine Liste der verfügbaren Scancodes steht in Singe/Framework.singe.
|
|
--]]
|
|
end
|
|
|
|
function onMouseMoved(x, y, xr, yr, which)
|
|
--[[
|
|
Wird aufgerufen, wenn die Maus bewegt wird. Im Modus SINGLE_MOUSE werden
|
|
absolute X- und Y-Werte sowie die relative Positionsänderung geliefert.
|
|
Im Modus MANY_MOUSE ist nur die relative Änderung verfügbar, dazu die
|
|
Angabe, welche Maus bewegt wurde.
|
|
--]]
|
|
end
|
|
|
|
function onOverlayUpdate()
|
|
--[[
|
|
Dies ist die einzige Stelle, an der du gefahrlos Zeichenoperationen ausführen
|
|
kannst. Wenn du einen Zielcursor anzeigen willst, musst du die Mausposition
|
|
aus onMouseMoved in globalen Variablen speichern und diese dann hier
|
|
verwenden, um den Cursor zu zeichnen.
|
|
--]]
|
|
|
|
-- Singe mitteilen, ob wir die Anzeige verändert haben oder nicht.
|
|
return(OVERLAY_UPDATED) -- Oder OVERLAY_NOT_UPDATED, wenn nichts gezeichnet wurde.
|
|
end
|
|
|
|
function onShutdown()
|
|
-- Wird aufgerufen, wenn der Benutzer dein Spiel beendet. Gib hier geladene Ressourcen frei.
|
|
end
|
|
|
|
function onSoundCompleted(channel)
|
|
-- Der Sound auf „channel“ ist gerade zu Ende gespielt.
|
|
end
|
|
|
|
-- Hinweis: Es gibt kein Ereignis „onStartup“.
|
|
-- Code, den du beim Start brauchst, kann hier stehen.
|
|
----
|
|
|
|
[#threaded]
|
|
==== Thread-Modell
|
|
|
|
Seit Singe 2.10 kannst du das klassische prozedurale Programmiermodell verwenden. Um dieses Modell zu nutzen, deklariere eine Funktion `singeMain()` und binde das Singe-Framework am *Ende* deines Programms statt am Anfang ein. Lua beherrscht kein echtes Multithreading, deshalb verlässt sich dieses Modell darauf, dass der Spieleentwickler kooperatives Multitasking betreibt, indem er überall dort `singeYield()` aufruft, wo Code nennenswert Zeit verbraucht. Wie beim ereignisgesteuerten Modell verwaltet Singe weiterhin alle Eingaben, die Videodecodierung und die Audiodecodierung. Ein Beispielprogramm im Thread-Modell sieht so aus:
|
|
|
|
[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]
|
|
==== Hybrid
|
|
|
|
Die beiden Modelle lassen sich frei kombinieren. Wenn `singeMain` existiert, installiert `Framework.singe` sein eigenes `onOverlayUpdate`, aber jeder andere Callback wird weiterhin ausgelöst: Definiere `onInputPressed`, `onSoundCompleted` oder `onShutdown` neben `singeMain`, und sie werden zwischen den Yields der Koroutine aufgerufen. Eine typische Aufteilung hält den Ablauf des Spiels prozedural und reagiert dabei auf Eingabe- und Audioereignisse:
|
|
|
|
[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")
|
|
----
|
|
|
|
Definiere `onOverlayUpdate` in diesem Modell nicht selbst; zeichne stattdessen aus `singeMain` heraus. Ein Fehler, der in `singeMain` ausgelöst wird, beendet das Spiel mit einem Traceback, und eine Rückkehr aus `singeMain` beendet es sauber.
|
|
|
|
[#pausing]
|
|
=== Pausieren
|
|
|
|
Jedes Spiel bekommt umsonst eine funktionierende Pause. Solange die Pausentaste aktiviert ist (die Voreinstellung), lässt ein Druck auf die Taste, die `INPUT_PAUSE` zugeordnet ist, die Engine die Disc, jedes geladene Video und jeden Sound anhalten, eine Anzeige PAUSED über den letzten Frame zeichnen und das Skript nicht mehr ausführen: kein `onOverlayUpdate`, kein Fortsetzen von `singeMain`, keine Eingabe-, Maus-, Controller- oder Sound-Callbacks. Timer, die auf Lua-Bibliotheken aufbauen, halten mit an, weil nichts sie ausführt. Ein erneuter Druck auf die Taste setzt alles fort, was lief, und das Skript macht dort weiter, wo es aufgehört hat. Nur die eigenen Schalter der Engine (Pause, Beenden, Screenshot, Mausfang) funktionieren im eingefrorenen Zustand noch.
|
|
|
|
Eingaben bleiben über das Einfrieren hinweg wahrheitsgetreu. Alles, was das Skript für gedrückt hält, wird beim Beginn der Pause mit `onInputReleased` (oder `onKeyReleased` in `MODE_FULL`) losgelassen, und was beim Ende der Pause noch tatsächlich gehalten wird, wird erneut gedrückt. Ein Joystick, der während einer Pause gehalten wird, bleibt nie hängen.
|
|
|
|
Ein Spiel, das sein eigenes Pausenverhalten will (ein Pausenmenü, einen Halt in der Handlung, den der Spieler nicht überspringen darf), ruft `singeSetPauseKeyEnabled(false)` auf. Die Taste wird dann als `SWITCH_PAUSE` an das Skript geliefert, und das Spiel steuert `singeSetPauseFlag` selbst, was die Medien anhält, ohne das Skript einzufrieren.
|
|
|
|
In `MODE_FULL` gehört die Tastatur ganz dem Spiel, Tasten, die den Schaltern der Engine zugeordnet sind, bewirken dort also nichts (ein `p`, das in eine Highscore-Eingabe getippt wird, muss ein `p` bleiben). Gamepad- und Mausknöpfe, die diesen Schaltern zugeordnet sind, funktionieren in beiden Modi weiter, da sie sich nicht tippen lassen.
|
|
|
|
[#gamesdat]
|
|
=== games.dat
|
|
|
|
Die Datei `games.dat` ermöglicht es Singe, neue Spiele automatisch zu finden, wenn der Endbenutzer sie installiert. Diese Datei ist äußerst wichtig und muss jedem Singe-Spiel beiliegen. Lege `games.dat` in den obersten Ordner deines Spiels. Ein Beispiel mit mehreren Spielen:
|
|
|
|
[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"
|
|
},
|
|
-- ... weitere Einträge ...
|
|
}
|
|
----
|
|
|
|
Die Schlüssel `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` und `TIER` liest die Engine, wenn das Menü (oder dein eigenes Skript, über `scriptExecute` / `scriptPush`) den Eintrag startet; sie haben Vorrang vor der Befehlszeile. Die Zeile `VIDEO` ist die Disc: Ein Eintrag, der eine nennt, ist ein Laserdisc-Spiel, und ein Eintrag, der sie weglässt oder leer lässt, läuft ohne Disc und kann `CANVAS_X` / `CANVAS_Y` setzen (Standard 720x480); siehe <<withoutadisc,Spiele ohne Disc>>. `AUDIO_SUFFIX = "-es"` spielt `<base>-es.ogg` neben dem Video der Disc ab statt des Tons darin, für eine Veröffentlichung, deren andere Sprachen als eigene Dateien geliefert werden; das ist dasselbe, was `--altaudio` tut. `LEGACY_SPRITE_ARGS = true` führt ein für Singe 2.10 geschriebenes Spiel mit der alten Reihenfolge der Sprite-Argumente aus (siehe <<migrating,Umstieg von Singe 2.10>>). Die übrigen Schlüssel liest das Menü für die Anzeige.
|
|
|
|
`REQUIRES` sagt, was das Spiel von einem Rechner braucht, als Liste von Namen: `"SCENE"` (die 3D-Szene), `"GUI"` (GUI-Dokumente), `"SHADOWS"`, `"POST"` (Bloom und die anderen Effekte, die Float-Renderziele brauchen), `"SKIN"` (geskinnte Modelle) und `"MORPH"` (Morph-Ziele). Kein `REQUIRES` bedeutet 2D. Das Menü kennzeichnet ein Spiel, das dieser Rechner nicht ausführen kann, und sagt warum, statt es zu starten, und du schreibst die Liste selten selbst: Die Engine bemerkt beim Laufen, was ein Spiel verwendet, meldet im Trace, wenn das Spiel etwas verwendet, das `REQUIRES` nicht aufführt, und `--pack` schreibt die Liste in einen Eintrag, der keine hat. `TIER` (0 bis 3) gibt an, wie viel Rechner das Spiel will; das Menü misst die Stufe dieses Rechners an seinem eigenen Hintergrund und kennzeichnet ein Spiel, das mehr will, als eines, das langsam laufen könnte. Siehe <<singecapabilities,singeCapabilities>>.
|
|
|
|
[source,lua]
|
|
----
|
|
REQUIRES = { "SCENE", "SHADOWS" },
|
|
TIER = 1,
|
|
----
|
|
|
|
Ein direkt gestartetes Skript, ob über seinen Namen oder seinen Ordner, wird genauso behandelt: Singe sucht eine `games.dat` im Ordner des Skripts und bis zu drei Ordner darüber, und wenn das `SCRIPT` eines Eintrags das gestartete Skript nennt (Pfade in Einträgen zählen ab dem Ordner über der `games.dat`, sodass `Collection/games.dat` das Skript als `Collection/AsteroidPatrol.singe` nennt), gelten die Schlüssel dieses Eintrags. Der eine Unterschied zum Start aus dem Menü ist der Vorrang: Eine auf der Befehlszeile eingegebene Option (`--framefile`, `--disc`, `--stretch`, `--nomouse`, `--xresolution`, `--yresolution`, `--sindengun`, `--audio`, `--audiodelay`, `--altaudio`, `--canvas`) behält ihren Wert, denn wer sie eintippt, meint sie so. Ein Wert aus einer Einstellungsdatei nicht: Er ist eine Voreinstellung, und der Eintrag beschreibt das Spiel besser (siehe xref:Manual.adoc#settings[Die Einstellungsdatei]). Eine `games.dat`, die sich nicht ausführen lässt, wird bei einem direkten Start gemeldet und ignoriert.
|
|
|
|
[#withoutadisc]
|
|
=== Spiele ohne Disc
|
|
|
|
Singe setzt keine Laserdisc mehr voraus. Ein Spiel ist nur dann ein Laserdisc-Spiel, wenn es eine nennt: ein `VIDEO` in seinem `games.dat`-Eintrag oder `--disc` (oder ein `--framefile`) auf der Befehlszeile. Alles andere läuft ohne Disc, und die Welt ist eine *Zeichenfläche* statt eines Videoframes: `CANVAS_X` / `CANVAS_Y` in `games.dat` oder `--canvas=WxH` auf der Befehlszeile, standardmäßig 720x480, damit ein Spiel ohne Disc wie ein Laserdisc-Spiel in Standardauflösung aussieht, sofern es nichts anderes verlangt.
|
|
|
|
Ohne Disc zeichnet die Engine auf Schwarz, das Overlay ist wie üblich standardmäßig halb so groß wie die Zeichenfläche, und die Funktionen `disc*` und `vldp*` werden harmlos: `discGetState` meldet `DISC_EJECTED`, `discGetFrame` gibt `0` zurück, `discGetWidth` / `discGetHeight` geben die Zeichenfläche zurück, `vldpGetPixel` gibt Schwarz zurück, und die Transportaufrufe schreiben in den Trace und tun nichts. Alles andere -- das Overlay, Sprites, Schriftarten, Sounds, zusätzliche Videos über `videoLoad`, die Eingabe und beide Programmiermodelle -- ist identisch. Die globale Variable `SINGE_DISC` sagt einem Skript, als welche Art von Spiel es läuft.
|
|
|
|
[#gui]
|
|
=== GUI
|
|
|
|
Menüs, Optionsseiten, HUDs und die Highscore-Eingabe wurden früher aus `fontPrint`, `overlayBox` und einem selbst gebauten Cursor zusammengesetzt. Singe 3 bringt eine GUI mit: ein Dokument, geschrieben in RML (einem HTML-ähnlichen Markup) und gestaltet mit RCSS (einem CSS-ähnlichen Stylesheet), gesetzt und gezeichnet von RmlUi, mit Textumbruch, Flexbox, Nine-Slice-Panels aus deinen eigenen Grafiken, Übergängen, Animationen, Formularelementen und einem Fokusmodell, das ein Gamepad steuern kann. Eine *GUI* ist ein RmlUi-Kontext fester Größe, der in seine eigene Textur gerendert und neu gezeichnet wird, wann immer sich sein Bild ändert (siehe <<guirefresh,guiRefresh>>), und diese Textur kommt überallhin, wo eine Textur hinkann: mit `guiDraw` über das Overlay gelegt oder mit `materialSetGui` auf einem Material in der 3D-Szene gezeigt, sodass dieselbe Optionsseite flach auf dem Bildschirm und auf dem Monitor eines modellierten Arcade-Automaten funktioniert. Eine GUI braucht das GPU-Gerät, das die 3D-Szene verwendet, und ist deshalb auf einem Rechner ohne ein solches nicht verfügbar (siehe <<sceneperformance,Leistung und Anforderungen>>); `guiNew` beendet das Skript, statt mit false zu antworten, frage also zuerst <<singehasgpu,singeHasGpu>>, wenn dein Spiel ohne auskommt. Das mitgelieferte Menü tut genau das und zeichnet sich stattdessen ins Overlay, wenn die Antwort nein ist (siehe xref:Manual.adoc#menurenderers[Die zwei Renderer]). Jeder hier genannte Aufruf hat einen Eintrag im Abschnitt <<guiref,Gui>> der Referenz.
|
|
|
|
Die Sprachreferenz für RML und RCSS ist die eigene Dokumentation von RmlUi unter https://mikke89.github.io/RmlUiDoc/, und sie lohnt die Lektüre vor dem ersten Dokument; dieses Kapitel behandelt, was Singe obendrauf setzt.
|
|
|
|
[#_documents_and_style_sheets]
|
|
==== Dokumente und Stylesheets
|
|
|
|
`guiNew(width, height)` erzeugt eine GUI und gibt ein Ganzzahl-Handle zurück; bis zu sechzehn können gleichzeitig existieren. `guiLoad(gui, name)` lädt ein RML-Dokument hinein, zeigt es an und gibt ein Dokument-Handle zurück (bis zu zweiunddreißig pro GUI); `guiHide`, `guiShow` und `guiClose` tun, was ihre Namen sagen, und `guiDelete` verwirft die GUI, ihre Dokumente und ihre Textur. Ein Dokument ist meist so groß wie seine GUI, und eine GUI ist meist so groß wie das Overlay, für eine Vollbildseite, oder so groß wie das Panel, für ein HUD-Element:
|
|
|
|
[source,lua]
|
|
----
|
|
local gui = guiNew(overlayGetWidth(), overlayGetHeight())
|
|
local doc = guiLoad(gui, DIR .. "options.rml")
|
|
----
|
|
|
|
Der Dokumentname geht wie der eines Sprites durch das virtuelle Dateisystem, er kann also ungepackt auf der Festplatte oder in einem gepackten Spiel liegen, und er wird relativ zu dem Ordner aufgelöst, in dem Singe gestartet wurde; stelle also für Dateien, die mit deinem Spiel ausgeliefert werden, `DIR` voran, genau wie bei `spriteLoad`. Namen *innerhalb* eines Dokuments (das `href` eines Stylesheets, ein Bild in einem Decorator, das `src` eines Sprite-Sheets) werden relativ zu der Datei aufgelöst, die sie nennt, wie bei RmlUi, und dann über dieselbe Suche, sodass ein Dokument, sein Stylesheet und seine Bilder nebeneinander im Spielordner liegen und ein gepacktes Spiel sie alle findet. Zwei Arten von Namen werden genommen, wie sie sind: die eigenen Dateien der Engine unter `Singe/` und absolute Pfade.
|
|
|
|
Dieselbe Regel, andersherum gelesen, lässt ein Dokument, das unter `Singe/` liegt, die Dateien eines Spiels erreichen: Ein relativer Name wird vom eigenen Ordner des Dokuments aus aufgelöst, also ist von `Singe/Menu.rml` aus der Name `../Collection/cabinet.png` die Datei `Collection/cabinet.png` im Spieleverzeichnis, und ein Name unter `../MyGame.game/` ist eine Datei in diesem gepackten Spiel. Das mitgelieferte Menü tut genau das für die Automaten- und Marquee-Grafik des gewählten Spiels und setzt aus Lua einen Bild-Decorator auf das Grafikfeld, sodass das Bild unabhängig von seiner Größe hineinpasst:
|
|
|
|
[source,lua]
|
|
----
|
|
-- Menu.singe: das Bild eines Spiels in eines der Grafikfelder einpassen.
|
|
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
|
|
----
|
|
|
|
Ein RML-Dokument besteht aus einem `<head>` mit einem Titel, Links auf Stylesheets oder einem eingebetteten `<style>` und einem `<body>` aus Elementen. Ein minimales, mit dem mitgelieferten Theme:
|
|
|
|
[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 zeichnet nichts, dessen Größe ihm nicht mitgeteilt wurde, und jedes Element ist `display: inline`, bis ein Stil etwas anderes sagt; ein Stylesheet ist also nicht optional. `Singe/gui.rcss` ist das Theme, das die Engine mitliefert: Es gibt Body, Überschriften und Absätzen eine Größe und eine Farbe, macht `div` zu einem Block und gestaltet jedes Formularelement. Binde es zuerst ein und überschreibe, was du willst, in einem eigenen Stylesheet oder einem eingebetteten `<style>`; die spätere Regel gewinnt wie in CSS. Längen im Theme sind in `dp` angegeben, dem dichteunabhängigen Pixel von RmlUi, das bei der eigenen Größe der GUI ein Pixel ist.
|
|
|
|
Text braucht eine Schrift. Die Engine lädt `Singe/FreeSansBold.ttf` für jede GUI als *Ersatzschrift* (die Schrift, die jedes Zeichen liefert, das keine andere Schrift hat); ihr Familienname ist `FreeSans` und ihre Stärke fett, was die `body`-Regel des Themes verlangt. `guiLoadFont(name)` fügt eine der eigenen Schriften des Spiels für jede GUI hinzu, und `guiLoadFont(name, true)` macht sie stattdessen zur Ersatzschrift. Ein Dokument nennt eine Schrift über den Familiennamen in der Datei, nicht über den Dateinamen; lade Schriftarten also vor den Dokumenten, die sie verwenden:
|
|
|
|
[source,lua]
|
|
----
|
|
guiLoadFont(DIR .. "fonts/Orbitron-Bold.ttf")
|
|
----
|
|
|
|
[source,css]
|
|
----
|
|
h1 { font-family: Orbitron; font-weight: bold; }
|
|
----
|
|
|
|
Die RCSS-Effekte von RmlUi werden vollständig gezeichnet. Kinder eines abgerundeten oder transformierten Elements werden auf dessen Form beschnitten (eine scrollende Liste mit `border-radius` trifft ihren Rahmen an der Rundung), `filter: opacity()` blendet eine Gruppe als Ganzes aus (die Eigenschaft `opacity` blendet jedes Teil für sich aus, so wie RmlUi sie definiert), `mask-image` schneidet ein Element nach dem Alpha eines Bildes aus, `box-shadow` wirft harte oder weiche Schatten, und `filter` und `backdrop-filter` nehmen `blur`, `drop-shadow`, `brightness`, `contrast`, `invert`, `grayscale`, `sepia`, `hue-rotate` und `saturate`. Die Decorators `linear-gradient`, `radial-gradient` und `conic-gradient` (wiederholend oder nicht) werden als solche gezeichnet; die Panels, Buttons und Fortschrittsbalken des mitgelieferten Themes verwenden sie. Drei Grenzen: Der benutzerdefinierte Decorator `shader` wird nicht unterstützt (er wird mit einer Warnung abgelehnt), ein Verlauf trägt höchstens sechzehn Farbstopps (weitere werden verworfen), und Effekte verschachteln sich höchstens acht Ebenen tief (ein tieferes Element wird ohne eigene Ebene gezeichnet). Jeder Effekt wird über eine Ebene in der Größe der GUI gerendert und ein Weichzeichner über mehrere Durchgänge; beschränke sie auf einem Pi also auf wenige Elemente und behalte <<guigetstats,guiGetStats>> im Blick, das die Ebenen, Durchgänge und Bytes zählt, die ein Frame gekostet hat.
|
|
|
|
[#_theming]
|
|
==== Themes
|
|
|
|
Ein Theme ist ein Ordner mit RCSS und Bildern, und ein Spiel liefert sein eigenes Aussehen so aus, wie es Sprites ausliefert. Die übliche Form ist ein Stylesheet, das nach `Singe/gui.rcss` eingebunden wird, die Klassen des Themes umgestaltet und eigene hinzufügt, mit den Grafiken daneben. Das mitgelieferte Theme definiert:
|
|
|
|
* `.panel` -- ein umrandeter, abgerundeter, durchscheinender Kasten mit Innenabstand, der Rahmen einer Seite oder eines Dialogs.
|
|
* `.list` -- ein scrollender Kasten, dessen `div`-Kindelemente Zeilen sind: Sie nehmen den Fokus an, werden bei Hover und Fokus hervorgehoben, und eine Zeile mit der Klasse `selected` bleibt hervorgehoben. Eine senkrechte Bildlaufleiste erscheint, wenn die Zeilen überlaufen.
|
|
* `.muted` -- gedämpfterer Text, für Hinweise und Statuszeilen.
|
|
* Die Formularelemente: `button` und `input.submit`, `input.text`, `input.password` und `textarea`, `input.checkbox` und `input.radio`, `input.range` mit Schiene und Schieberegler, `select` mit Pfeil und Aufklappliste und `progress` mit seiner Füllung; jedes mit den Zuständen Hover, Fokus und aktiv, und jedes mit `tab-index: auto` fokussierbar, damit ein Gamepad es erreichen kann.
|
|
|
|
Ein Panel aus deinen eigenen Grafiken ist ein Nine-Slice-Decorator über einem Sprite-Sheet, und Animationen sind RCSS-Keyframes; ein Ladebalken, der sich von selbst füllt, braucht also überhaupt kein Skript:
|
|
|
|
[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]
|
|
==== Ein Dokument skripten
|
|
|
|
Es gibt zwei APIs, und es hilft, genau zu wissen, welche welche ist. Die flachen `gui*`-Aufrufe im eigenen Stil von Singe erzeugen eine GUI, laden und zeichnen Dokumente, leiten Eingaben weiter und erreichen Elemente *über ihre ID*: `guiGetValue(gui, doc, id)` liest den Wert eines Formularelements oder das innere RML jedes anderen Elements als String; `guiSetValue(gui, doc, id, value)` schreibt einen; und `guiSetHandler(gui, doc, id, event, function)` ruft eine Lua-Funktion auf, wenn das Element ein Ereignis auslöst (`"click"`, `"change"`, `"submit"` oder jeder andere Ereignisname von RmlUi). Die Funktion erhält `(gui, document, id, event, value)`, mit `value` als dem Wert des Elements als String im Moment des Ereignisses: der Text eines Textfelds, der Wert einer `select`-Option, ein Range-Element in der eigenen Formatierung von RmlUi (`"48.000000"`), das innere RML eines Buttons. Alles, was eine Seite im Alltag braucht, decken diese drei Aufrufe ab:
|
|
|
|
[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)
|
|
----
|
|
|
|
Alles andere in einem Dokument ist die Lua-API von RmlUi, die im eigenen Lua-Zustand des Spiels läuft: Ein `<script>`-Block in einem Dokument sowie `onclick` und die anderen `on*`-Attribute laufen mit den globalen Variablen des Spiels und können deshalb deine Funktionen ebenso aufrufen wie die `gui*`-Aufrufe. Von der Skriptseite aus sind dieselben Objekte über die globale Variable `rmlui` erreichbar: Kontexte heißen nach ihren Handles `gui0`, `gui1`, ..., und Dokumente sind nach der `id` ihres `<body>` geschlüsselt; der Button oben ist also `rmlui.contexts["gui0"].documents["options"]:GetElementById("go")`, und `:DispatchEvent("click", {})` darauf drückt ihn aus dem Skript heraus (nützlich für Tests, die ohne Maus laufen). Elemente, ihre Attribute, Klassen, Stile und Kinder, `data-model`-Bindungen, die eine Lua-Tabelle in ein Dokument spiegeln, und der Rest des Objektmodells sind beim Lua-Plugin von RmlUi unter https://mikke89.github.io/RmlUiDoc/ dokumentiert; `guiSetHandler` ist die Abkürzung über die ID, kein Ersatz. Ein Dokument, das über diese API geschlossen wird (`document:Close()`), vergisst die Engine, und sein Handle löst danach einen Fehler aus.
|
|
|
|
[#_input]
|
|
==== Eingabe
|
|
|
|
Eine neue GUI zeigt nur an, was für ein HUD oder ein Schild richtig ist; `guiSetInput(gui, true)` lässt sie Eingaben annehmen, und `guiSetInput(gui, false)` schaltet das wieder ab. Für eine GUI, die Eingaben annimmt:
|
|
|
|
* *Die Maus und eine Lightgun* (die eine Maus ist) kommen in Overlay-Koordinaten an, umgerechnet über den Sinden-Rand und den Overscan-Maßstab, genau wie für `onMouseMoved`, im Modus `MOUSE_SINGLE`. Die Engine findet die GUI unter dem Zeiger -- die letzte, die in diesem Frame dort flach gezeichnet wurde, liegt oben, und ansonsten eine Fläche in der Szene, deren Material eine zeigt (siehe unten) -- und skaliert die Position in die eigenen Pixel dieser GUI. Ein Druck auf ein Element gehört der GUI; die linke, rechte und mittlere Taste klicken, und das Rad scrollt.
|
|
* *Tasten* gehen an jede GUI, die Eingaben annimmt, und eine Taste, die ein fokussiertes Element verwendet hat (ein Zeichen in einem Textfeld, ein Pfeil, der den Fokus bewegt hat, die Eingabetaste auf einem Button), wird verbraucht. Solange ein Text- oder Passwortfeld oder eine `textarea` den Fokus hat, betreibt die Engine die Texteingabe von SDL, sodass getippter Text als Text ankommt, unter Beachtung des Tastaturlayouts und jeder Bildschirmtastatur oder IME.
|
|
* *Pad- und Mausschalter* (die `SWITCH_*`-Belegungen des Frameworks von einem Gamepad- oder Mausknopf) werden in die Tasten übersetzt, die ein Dokument versteht: Die vier Richtungen werden zu den Pfeiltasten, `INPUT_ACTION_1` ist die Eingabetaste und `INPUT_ACTION_2` ist Esc. Ein Schalter, der einer Tastaturtaste zugeordnet ist, hat die GUI bereits als diese Taste erreicht.
|
|
|
|
Die Regel, die ein HUD sicher macht, ist, dass *ein Ereignis, das ein Element verwendet hat, nie das Spiel erreicht*: Eine verbrauchte Taste kommt nicht als `onInputPressed` oder `onKeyPressed` an, ein Klick auf einen Button kommt nicht als Mausschalter an, eine übersetzte Richtung, die den Fokus bewegt hat, kommt nicht als `SWITCH_UP` an. Alles, was ein Dokument nicht verwendet hat, fällt zu den üblichen Callbacks durch, sodass ein HUD, das Eingaben annimmt, den Feuerknopf trotzdem durchlässt und eine Seite, auf der nichts fokussiert ist, jede Taste durchlässt.
|
|
|
|
Fokus und Navigation sind Sache des Dokuments. Das mitgelieferte Theme gibt jedem Bedienelement und jeder `.list`-Zeile `tab-index: auto`, sodass Tab zwischen ihnen wechselt, die Eingabetaste das fokussierte Bedienelement auslöst und die Pfeiltasten mit den `nav`-Eigenschaften von RmlUi (`nav-up`, `nav-down`, `nav-left`, `nav-right` oder `nav: auto` für alle vier) zwischen Bedienelementen wechseln. Eine Seite für einen Automaten mit einem Stick und zwei Knöpfen will diese auf allem, was der Spieler erreichen soll:
|
|
|
|
[source,css]
|
|
----
|
|
button, .list div { nav: auto; }
|
|
----
|
|
|
|
[#guiscene]
|
|
==== Eine GUI in der Szene
|
|
|
|
`materialSetGui(material, gui)` zeigt die Textur einer GUI als Grundfarbtextur eines Materials, in jedem Frame aktualisiert, neben `materialSetVideo` und `materialSetView`: ein Monitor, eine Cockpitkonsole, ein Schild. Kombiniere es mit `materialSetUnlit`, wenn das Bild nicht schattiert werden soll, und lass es beleuchtet, wenn es schattiert werden soll. Sonst ändert sich nichts; das Material wird beleuchtet, überblendet und gespiegelt wie jedes andere, und dieselbe GUI kann im selben Frame flach über das Overlay gezeichnet werden.
|
|
|
|
`sceneProbeGui(x, y)` beantwortet, welche GUI auf der Fläche unter einem Overlay-Punkt liegt und wo darauf: ein Strahl von der Kamera durch den Punkt gegen jedes sichtbare Mesh, dessen Material eine GUI zeigt, der das Handle und das Pixel in den eigenen Koordinaten dieser GUI zurückgibt, oder `nil`, wenn der Punkt neben jeder solchen Fläche liegt. Die Engine stellt dieselbe Frage, wenn die Maus oder eine Lightgun auf die Szene zeigt und nichts flach Gezeichnetes unter dem Zeiger liegt, sodass ein Klick auf den Monitor in einem 3D-Automaten den Button darunter drückt, ohne dass ein Skript beteiligt ist; der Aufruf ist für ein Spiel da, das es selbst wissen will, für ein Laservisier, einen Hover-Effekt oder einen Test.
|
|
|
|
[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]
|
|
==== Neuladen, Leistung und Grenzen
|
|
|
|
Eine GUI gehört dem Skript, das sie erzeugt hat. Beenden, `scriptPush`, `scriptExecute` und ein Neuladen (siehe <<reloading,Neu laden während der Arbeit>>) bauen jede GUI und jedes Dokument zusammen mit dem übrigen Zustand des Skripts ab, und das Skript erzeugt sie neu, wenn es läuft; nichts aus dem Objektmodell `rmlui` überlebt, halte diese Objekte also nicht in globalen Variablen, die ein Neuladen wiederverwenden würde.
|
|
|
|
Jede GUI wird in jedem Frame aktualisiert, ob sie irgendwo gezeichnet wird oder nicht, sodass ein Dokument seine Animationen und Skripte auch außerhalb des Bildschirms weiterlaufen lässt, und eine GUI, die nicht mehr gebraucht wird, sollte gelöscht statt nur versteckt werden. In ihre Textur neu gezeichnet wird sie nur, wenn sich ihr Bild ändert; eine Seite oder ein HUD, das stillsteht, kostet die GPU also fast nichts (siehe <<guirefresh,guiRefresh>>). Die Kosten eines Frames, der sich ändert, bestimmen die Größe der Textur und die Komplexität des Dokuments: Auf einem Desktop ist eine Vollbildseite billig, auf einem Raspberry Pi halte eine GUI auf der Größe des Overlays oder kleiner und ziehe eine GUI mit mehreren Dokumenten mehreren GUIs vor. Jede Schrift kostet einen Atlas pro Größe, in der sie verwendet wird; eine Seite, die eine Schrift in drei Größen verwendet, zahlt also für drei Atlanten, und ein Dutzend Größen sind ein Dutzend. Sechzehn GUIs können gleichzeitig existieren, jede mit zweiunddreißig Dokumenten, und sechzehn `guiDraw`-Aufrufe werden pro Frame berücksichtigt.
|
|
|
|
Was ein Dokument zeigen kann und was nicht:
|
|
|
|
* *Bilder kommen aus Dateien.* Bilder, Sprite-Sheets und Bild-Decorators laden PNG und die anderen Formate, die Singe liest, von der Festplatte oder aus einem gepackten Spiel. Ein Dokument kann kein Singe-Sprite, kein Video, nicht die 3D-Szene und nichts zeigen, was während des Spiels gezeichnet wird, und ein Skript kann ihm keine Pixel übergeben. Andersherum geht es: Die Textur einer GUI kommt mit `materialSetGui` auf ein 3D-Material.
|
|
* *Decorators:* `image`, `text`, `tiled-horizontal`, `tiled-vertical`, `tiled-box`, `ninepatch`, der zweifarbige `gradient` (und seine Namen `horizontal-gradient` und `vertical-gradient`) sowie `linear-gradient`, `radial-gradient` und `conic-gradient` mit ihren `repeating-`-Formen, mit höchstens sechzehn Farbstopps. Der benutzerdefinierte Decorator `shader` wird abgelehnt.
|
|
* *Filter:* `opacity`, `blur`, `drop-shadow`, `brightness`, `contrast`, `invert`, `grayscale`, `sepia`, `hue-rotate` und `saturate`. Jeder andere Name wird mit einer Warnung im Trace ignoriert.
|
|
* *Ebenen:* Filter, `opacity` unter 1, Maskenbilder und Box-Schatten werden jeweils über eine Ebene in der Größe der GUI gezeichnet, und höchstens acht stapeln sich gleichzeitig; eine weitere darüber hinaus wird ohne ihre Ebene gezeichnet und damit ohne ihren Effekt.
|
|
* *Bilder* über 64 MB werden abgelehnt.
|
|
* *RML und RCSS* sind die von RmlUi, nah an HTML und CSS, aber nicht dasselbe: Es gibt Flexbox und kein CSS Grid, und ein Dokument wird in Lua geskriptet, über `guiSetHandler` und das Objekt `rmlui`, statt in JavaScript. Die eigene Dokumentation von RmlUi ist maßgeblich dafür, was die Sprachen enthalten.
|
|
|
|
[#_a_worked_example]
|
|
==== Ein durchgearbeitetes Beispiel
|
|
|
|
Eine Optionsseite über der Disc: ein Panel aus dem mitgelieferten Theme mit einem Namensfeld, einem Lautstärkeregler, einer Schwierigkeitsliste und zwei Buttons, über das Overlay gezeichnet und mit der Maus oder dem Stick bedient. Das Dokument, `options.rml`, neben dem Skript:
|
|
|
|
[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>
|
|
----
|
|
|
|
Das Skript lädt es in eine GUI von der Größe des Overlays, verdrahtet die drei Elemente, die es interessieren, und zeichnet es in jedem Frame, bis der Spieler Start drückt; die Disc läuft die ganze Zeit darunter:
|
|
|
|
[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)
|
|
-- Hier kommt nur an, was die Seite nicht verwendet hat: nichts, solange sie
|
|
-- den Fokus hat, alles, sobald sie versteckt ist.
|
|
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
|
|
----
|
|
|
|
Dieselbe Seite auf dem Bildschirm eines modellierten Automaten sind die fünf Zeilen aus <<guiscene,Eine GUI in der Szene>> anstelle von `guiDraw`, und ein Klick auf den Monitor drückt den Button darunter.
|
|
|
|
[#subtitles]
|
|
=== Untertitel
|
|
|
|
Ein Spiel kann eine SubRip-Untertiteldatei mitliefern und Singe sie beim Abspielen über der Disc anzeigen lassen. Drei Aufrufe erledigen die ganze Arbeit: <<srtload,`srtLoad`>> liest die Datei, <<srtenable,`srtEnable`>> schaltet sie ein und <<srtposition,`srtPosition`>> legt fest, wo die Zeilen stehen. <<srtclear,`srtClear`>> nimmt wieder weg, was gerade angezeigt wird. Die Funktion stammt aus Hypseus Singe, und die vier Aufrufe sind die von dort.
|
|
|
|
[source,lua]
|
|
----
|
|
if srtLoad("subs/english.srt") then
|
|
srtPosition(80)
|
|
srtEnable(true)
|
|
end
|
|
----
|
|
|
|
[#_the_file_format]
|
|
==== Das Dateiformat
|
|
|
|
SubRip (`.srt`) ist eine reine Textdatei aus nummerierten Cues:
|
|
|
|
----
|
|
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.
|
|
----
|
|
|
|
Jeder Cue besteht aus einer Nummer auf einer eigenen Zeile, dann einer Zeile mit zwei durch `-->` getrennten Zeitstempeln, dann einer oder mehreren Textzeilen, dann einer Leerzeile. Ein Zeitstempel hat die Form `HH:MM:SS,mmm`: Stunden, Minuten und Sekunden durch Doppelpunkte getrennt, die Millisekunden nach einem Komma. Die Cue-Nummer wird gelesen und verworfen, die Nummern müssen also weder in der richtigen Reihenfolge noch überhaupt korrekt sein.
|
|
|
|
Der Parser ist absichtlich nachsichtig an den Stellen, an denen eine von Hand bearbeitete Datei schiefgeht. Wagenrückläufe sowie führende und nachgestellte Leerzeichen werden von jeder Zeile entfernt, sodass eine unter Windows geschriebene Datei unverändert lädt. Ein Cue, dessen Zeitzeile kein `-->` enthält oder dessen Zeitstempel sich nicht parsen lassen, wird übersprungen, und die Datei wird weiter geladen. Ein Cue ohne Text wird übersprungen. Die Zeilen eines Cues werden mit Zeilenumbrüchen verbunden und so angezeigt, wie sie geschrieben sind, und `<`, `>` und `&` erscheinen als sie selbst auf dem Bildschirm, statt als Markup verstanden zu werden.
|
|
|
|
Was *nicht* unterstützt wird: Die Positionskoordinaten, die manche Werkzeuge an die Zeitzeile anhängen, werden ignoriert (`srtPosition` bestimmt die Höhe), und die gelegentlichen `<i>`- und `<b>`-Tags von SubRip werden als Text angezeigt statt befolgt. Cues dürfen sich zeitlich überschneiden; der zuletzt begonnene gewinnt.
|
|
|
|
`srtLoad` antwortet mit `false` und lädt nichts, wenn die Datei nicht gelesen werden kann, wenn sich nichts darin parsen lässt oder wenn keine Disc da ist -- eine kaputte Datei ist also ein `false`, auf das ein Spiel reagieren kann, und kein Absturz. In jedem dieser Fälle gibt es eine Warnung aus, die die Datei nennt.
|
|
|
|
[#_the_timing]
|
|
==== Die Zeitsteuerung
|
|
|
|
Zeitstempel werden beim Laden der Datei in *Frame-Nummern der Disc* umgerechnet, mit der Bildrate der Disc, die zu diesem Zeitpunkt geladen ist, und mit diesen Frame-Nummern gleicht die Engine jeden gezeichneten Frame ab. Daraus folgen zwei Dinge.
|
|
|
|
Erstens: **Ein Untertitel übersteht einen Sprung.** Die Disc irgendwohin zu schicken -- `discSearch`, `discSkipForward`, ein Einzelschritt, eine Frame-Datei, die in ein anderes Segment übergeht -- bringt beim nächsten gezeichneten Frame den Cue auf den Bildschirm, der die Stelle abdeckt, an der sie gelandet ist, vorwärts wie rückwärts. Nichts muss neu geladen und keine Uhr zurückgesetzt werden.
|
|
|
|
Zweitens: **Die Datei muss nach der Disc getaktet sein, nicht nach der Uhr.** Eine `.srt`, die für ein Video mit einer anderen Bildrate erstellt wurde, landet an der falschen Stelle, und eine, die geladen wird, bevor die Disc geöffnet ist, lädt überhaupt nicht. Lade Untertitel nach der Disc, was bei einem gewöhnlichen Spiel heißt: aus dem Hauptteil des Skripts.
|
|
|
|
Ein Cue wird angezeigt, solange der Frame der Disc zwischen seinen beiden Frames liegt, beide eingeschlossen. Der Untertitel wird nur dann neu gezeichnet, wenn sich der Cue ändert, Untertitel kosten also nichts, solange eine Zeile steht.
|
|
|
|
[#_styling]
|
|
==== Gestaltung
|
|
|
|
Die Zeilen sind ein RmlUi-Dokument, keine geblittete Textzeile, und werden deshalb so gestaltet wie eine GUI. Zwei Dateien werden im `Singe`-Ordner mitgeliefert und beim ersten Start neben den anderen entpackt:
|
|
|
|
* `Singe/subtitle.rml` -- das Dokument. Es hat ein einziges Element, `#slot`, in das die Engine den aktuellen Cue oder das aktuelle Banner schreibt.
|
|
* `Singe/subtitle.rcss` -- das Aussehen. `.cue` ist der Block, der die von `srtPosition` angeforderte Höhe als inline gesetztes `top` trägt; `.cue .text` ist der Textbalken darin, standardmäßig weiß auf einem dunklen, abgerundeten Feld, das sich an den Text schmiegt, statt sich über das ganze Bild zu spannen.
|
|
|
|
Beide werden bei jedem Start neu geschrieben, wie alles im Ordner `Singe`, Änderungen daran halten also nicht lange. Um das Aussehen zu ändern, liefert ein Spiel eigene Kopien aus: `subtitle.rml` neben seinem Skript, dessen Link auf ein eigenes Stylesheet zeigt (etwa `subtitle.rcss` daneben), und Singe zeichnet dann dieses Dokument statt seines eigenen; der Programm-Trace meldet das. Die Engine setzt immer nur den Inhalt von `#slot` und das `top` von `.cue`, alles andere -- die Schriftart, die Größe, die Farben, das Feld, ein Rahmen, ein Schatten -- gehört also in das Stylesheet des Spiels und übersteht ein Engine-Update. Das Dokument wird über das Bild gezeichnet, über dem Overlay und unter den GUIs im Bildschirmraum, folgt dem Videorechteck, wohin auch immer `--scalefactor`, `--shiftx`, ein Bezel-Ausschnitt oder `--rotate` es setzen, und nimmt keine Mauseingaben an, ein Klick geht also direkt hindurch.
|
|
|
|
<<overlaybanner,`overlayBanner`>> verwendet dasselbe Dokument, sodass ein Banner und ein Untertitel gleich aussehen und ein Spiel, das eines von beiden umgestaltet, beide umgestaltet.
|
|
|
|
Auf einem Rechner ohne GPU gibt es überhaupt keine GUIs; `srtLoad` parst die Datei trotzdem und antwortet wahrheitsgemäß, und es wird nichts gezeichnet. Eine Warnung weist einmal darauf hin.
|
|
|
|
[#scenes3d]
|
|
=== 3D-Szenen
|
|
|
|
Singe 3 zeichnet eine 3D-Szene zwischen dem Laserdisc-Video und dem 2D-Overlay. Alles, was du schon weißt, gilt weiterhin: Sprites, Text und das Overlay liegen über der Szene, und das Video scheint überall dort durch, wo der Hintergrund der Szene transparent ist. Ein Spiel, das nie eine 3D-Funktion aufruft, ist nicht betroffen, und ein Spiel ohne Disc (siehe <<withoutadisc,Spiele ohne Disc>>) kann ganz und gar ein 3D-Spiel sein.
|
|
|
|
Die folgenden Abschnitte bauen eine Szene in der Reihenfolge auf, in der du eine schreiben wirst: Knoten, dann Meshes und Materialien, Lichter, die Kamera, das Aussehen des Frames als Ganzes, Modelle und schließlich das, was auf all dem aufsetzt. Jeder hier genannte Aufruf hat einen Eintrag in der <<reference,Lua-API-Referenz>>.
|
|
|
|
[#_what_a_scene_is_made_of]
|
|
==== Woraus eine Szene besteht
|
|
|
|
Alles in einer Szene ist ein *Knoten* in einem einzigen Baum, und Knoten `0` ist die Wurzel. Ein Knoten hat eine Position, eine Rotation und eine Skalierung relativ zu seinem Elternknoten, und er kann eine Sache tragen: ein Mesh mit einem Material, ein Licht, ein Sprite oder eine Textzeile. Ein geladenes Modell wird zu einem Teilbaum von Knoten unter einer Wurzel, sodass dieselben Aufrufe eine modellierte Figur und einen geskripteten Würfel gleichermaßen bewegen.
|
|
|
|
Die Szene ist rechtshändig, mit +Y nach oben und -Z nach vorn, wie glTF und der Export von Blender. Die Einheiten sind die, die deine Modelle verwenden, und die Grundkörper nehmen dieselben Einheiten; die Physik (siehe <<physics,Physik>>) geht von Metern aus. Rotationen sind in Grad angegeben, als drei Euler-Winkel, die als Gieren, Nicken und Rollen angewendet werden, oder als Quaternion, wenn der Wert aus einem Modell stammt. Das eigene Vorne eines Knotens ist sein -Z: Die Kamera blickt daran entlang, Richtungs- und Spotlichter strahlen daran entlang, und `nodeLookAt` dreht es zu einem Punkt in der Welt.
|
|
|
|
Es gibt zwei Wege, eine Szene zu füllen, frei kombinierbar:
|
|
|
|
* *Aus dem Skript bauen*, mit den Grundkörper-Meshes (`meshBox`, `meshSphere`, `meshPlane`, `meshCylinder`, `meshCone`, `meshTorus`, `meshHeightmap`) oder deinen eigenen Vertexdaten (`meshNew`), dazu Materialien, Lichter und eine Kamera.
|
|
* *Modelle laden*, im binären glTF-2.0-Format (`.glb`) mit `modelLoad`, und sie dann mit `modelInstance` so oft platzieren, wie du willst. Blender, Maya, 3ds Max und jede größere Engine exportieren `.glb`.
|
|
|
|
Eine minimale Szene besteht aus einem Mesh, einem Licht und einer Kamera:
|
|
|
|
[source,lua]
|
|
----
|
|
sceneEnable(true)
|
|
sceneSetBackground(0, 0, 0, 0) -- Transparent: Die Disc ist hinter der Szene zu sehen.
|
|
|
|
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) -- Ein Grad pro Frame um die eigene Y-Achse.
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
`sceneEnable(true)` ist der eine Aufruf, den jedes 3D-Spiel macht, und auf einem Rechner, der kein 3D zeichnen kann, ist es der Aufruf, der scheitert (siehe <<sceneperformance,Leistung und Anforderungen>>). Ohne Kamera wird die Szene von `(0, 0, 5)` aus mit Blick auf den Ursprung betrachtet, und ohne einen Aufruf für den Hintergrund ist der Hintergrund der Szene undurchsichtig schwarz.
|
|
|
|
[#nodes]
|
|
==== Knoten
|
|
|
|
Die Transformation eines Knotens wird direkt gesetzt (`nodeSetPosition`, `nodeSetRotation`, `nodeSetQuaternion`, `nodeSetScale`) oder schrittweise verändert (`nodeMove`, `nodeRotate`), immer im Raum seines Elternknotens. `nodeSetParent` hängt einen Knoten um und behält dabei seine lokale Transformation; `nodeGetWorldPosition` liest, wo er gelandet ist, nachdem jeder Elternknoten mitgeredet hat. `nodeSetVisible` versteckt einen Knoten und alles unter ihm, und `nodeDelete` gibt ihn und seine Kinder frei, zusammen mit jedem Körper, Emitter oder Sound, der an ihnen hängt.
|
|
|
|
Namen kommen aus Modellen: `nodeFind(name, root)` findet ein Gelenk oder ein Teil über den Namen, den es in der Datei hatte, unter der als `root` angegebenen Instanz. Auch ein geskripteter Knoten kann einen Namen bekommen (`nodeSetName`), und diese Namen durchsucht `nodeFind` ohne Wurzel.
|
|
|
|
[source,lua]
|
|
----
|
|
-- Ein Turm auf einem Panzer: Das Rohr ist ein Kind des Turms, der Turm eines der Wanne,
|
|
-- sodass das Drehen des Turms das Rohr mitnimmt und das Bewegen der Wanne beides.
|
|
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) -- Den Zylinder entlang -Z legen.
|
|
|
|
function onOverlayUpdate()
|
|
local stick = controllerGetAxis(0, GAMEPAD_AXIS_RIGHT_X) / 32767 -- -1 bis 1.
|
|
nodeRotate(turret, 0, stick * 2, 0)
|
|
return OVERLAY_NOT_UPDATED
|
|
end
|
|
----
|
|
|
|
Zwei Aufrufe verbinden die Szene und das Overlay. `sceneProject(x, y, z)` wandelt einen Weltpunkt in Overlay-Koordinaten um, sodass ein Namensschild oder ein Lebensbalken, der mit `fontPrint` gezeichnet wird, einem 3D-Objekt folgen kann. `sceneUnproject(sx, sy, distance)` geht den anderen Weg: der Weltpunkt so weit entlang des Strahls durch ein Overlay-Pixel, was zusammen mit `physicsRaycast` Mausauswahl ergibt.
|
|
|
|
[source,lua]
|
|
----
|
|
function onMouseMoved(x, y, dx, dy, which)
|
|
-- Die Maus zeigt entlang eines Strahls von der Kamera; was er trifft, ist das Ziel.
|
|
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]
|
|
==== Meshes und Materialien
|
|
|
|
Ein Mesh ist Geometrie; ein Material ist, wie es aussieht; ein Knoten verbindet je eines davon (`nodeSetMesh(node, mesh, material)`). Teile beide nach Belieben: Ein `meshBox` und ein Material können auf tausend Knoten sitzen, und genau dieses Teilen macht den Renderer schnell (siehe <<sceneperformance,Leistung und Anforderungen>>). `meshNew` baut ein Mesh aus Tabellen von Positionen, Normalen, Texturkoordinaten und Indizes, wenn die Grundkörper nicht genügen.
|
|
|
|
Ein Material beginnt als Grundfarbe mit Alpha (`materialSetColor`) und dem Metallic-Roughness-Look von glTF: `materialSetMetallic` und `materialSetRoughness` von `0` bis `1`. Eine Rauheit nahe `0` ergibt ein spiegelscharfes Glanzlicht und `1` eine matte Oberfläche; ein Metall färbt seine Glanzlichter und Spiegelungen mit seiner eigenen Farbe und hat keine eigene diffuse Farbe. Darüber hinaus:
|
|
|
|
* `materialSetEmissive` lässt eine Oberfläche Licht in ihrer eigenen Farbe abgeben (ein Bildschirm, eine Lampe, heißes Metall); es beleuchtet keine anderen Dinge, erzeugt aber Bloom (siehe <<scenelook,Das Aussehen des Frames>>).
|
|
* `materialSetUnlit` zeigt die Farbe, wie sie ist, und ignoriert jedes Licht: HUD-Elemente in der Szene, das eingebaute Leuchten des Glases einer Glühbirne.
|
|
* `materialSetDoubleSided` zeichnet die Rückseite offener Formen, etwa einer von unten gesehenen Ebene oder einer Fahne.
|
|
* `materialSetBlend` lässt das Alpha zählen, für Glas, Geister und Wasser. Überblendete Meshes werden nach allem Undurchsichtigen gezeichnet, nach Entfernung sortiert, und werfen keine Schatten.
|
|
* `materialSetCutoff` behält nur die Texel, deren Alpha den Schwellwert erreicht, und verwirft den Rest; so werden Blätter, Zäune, Gitter und Maschendraht gezeichnet: ein Quad, das meiste davon weg. Anders als das Überblenden braucht es keine Sortierung und wirft trotzdem einen Schatten, und der Schatten wird ebenfalls ausgeschnitten, sodass ein Baum Blätterschatten wirft statt des Schattens seines Quads. Was den Schwellwert übersteht, ist undurchsichtig: Ein halbtransparentes Texel wird entweder ganz behalten oder verworfen, nie überblendet, sodass nichts hinter der Szene (am sichtbarsten das Bild der Disc) ein Blatt einfärbt. Ein Schwellwert von 0 (die Voreinstellung) schaltet die Maskierung ab. Ein aus glTF geladenes Modell bringt seinen eigenen mit: `MASK` setzt den Schwellwert aus der Datei, `BLEND` schaltet das Überblenden ein, und `OPAQUE` tut keines von beiden.
|
|
|
|
Die Textur eines Materials kann das Bild eines beliebigen geladenen Sprites sein (`materialSetTexture`), die Laserdisc selbst oder ein geladenes Video (`materialSetVideo`), das Bild einer zweiten Kamera (`materialSetView`, siehe <<scenecamera,Die Kamera und Ansichten>>) oder ein GUI-Dokument (`materialSetGui`, siehe <<gui,GUI>>). Ein modellierter Arcade-Automat kann die Disc auf seinem eigenen Bildschirm abspielen oder dort seine Optionsseite zeigen. Texturkoordinaten kommen aus dem Mesh; die Grundkörper bilden ein Bild einmal über jede Fläche ab, und `materialSetTiling` wiederholt es, was Boden und Wände wollen. Texturen haben Mipmaps und werden anisotrop gefiltert, sodass ein Boden bis in die Ferne scharf bleibt; `materialSetFilter(material, FILTER_NEAREST)` hält Pixel-Art stattdessen blockig.
|
|
|
|
[source,lua]
|
|
----
|
|
local stone = materialNew()
|
|
materialSetTexture(stone, spriteLoad(DIR .. "flagstones.png"))
|
|
materialSetTiling(stone, 12, 12) -- Zwölf Wiederholungen über die Ebene.
|
|
materialSetRoughness(stone, 0.9)
|
|
|
|
local floor = nodeNew()
|
|
nodeSetMesh(floor, meshPlane(24, 24), stone)
|
|
|
|
local glass = materialNew()
|
|
materialSetColor(glass, 180, 220, 255, 90) -- Alpha 90 von 255.
|
|
materialSetBlend(glass, true)
|
|
materialSetRoughness(glass, 0.05)
|
|
----
|
|
|
|
Vier weitere Maps geben einer Oberfläche Details, die ihrer Geometrie fehlen, und ein geladenes Modell bringt sie mit, wenn seine Datei sie hat:
|
|
|
|
* `materialSetNormalMap` beugt die Beleuchtung mit einer Normal-Map im Tangentenraum, sodass Ziegel aus einer flachen Wand hervortreten. Eine Stärke über `1` übertreibt den Effekt.
|
|
* `materialSetOcclusionMap` verdunkelt Ritzen, die das Umgebungslicht und der Himmel nicht erreichen.
|
|
* `materialSetMetallicRoughnessMap` variiert Metallic und Roughness über die Oberfläche, gepackt auf die glTF-Art (Roughness in Grün, Metallic in Blau) und mit den eigenen Faktoren des Materials multipliziert; setze diese also auf `1`, wenn die Map bestimmen soll.
|
|
* `materialSetEmissiveMap` sagt, welche Teile leuchten, eingefärbt und skaliert durch `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) -- Der Mörtel leuchtet orange.
|
|
----
|
|
|
|
.Komprimierte Texturen
|
|
Ein PNG wird auf der GPU zu vier Bytes pro Texel decodiert, plus ein weiteres Drittel für seine Mipmaps; ein Level von der Größe von Sponza belegt auf diese Weise 390 MB an Texturen, mehr, als ein Raspberry Pi 4 mit seiner Anzeige teilt. Texturen, die als KTX2-Dateien ausgeliefert werden (Basis Universal, erzeugt von `util/compressTextures.py` aus PNGs oder aus einem ganzen `.gltf`), bleiben auf der GPU komprimiert, bei einem Viertel der Größe oder weniger, in dem Blockformat, das der Rechner hat: BC7 auf Desktops, ASTC auf Apple-GPUs und dem Pi 4, ETC2 auf jedem Chip mit OpenGL ES 3 (dem Mali jedes Handhelds, dem Pi unter GLES), BC3 auf älteren Karten und einfaches RGBA, wo es nichts anderes gibt, alles aus der einen Datei. Gib `materialSetTexture` und den Map-Aufrufen einen `.ktx2`-Namen anstelle eines Sprites, und ein Modell, dessen Datei `KHR_texture_basisu` verwendet, lädt sie von selbst. `sceneGetStats` meldet den belegten Texturspeicher, sodass du den Unterschied sehen kannst.
|
|
|
|
[source,lua]
|
|
----
|
|
materialSetTexture(ground, DIR .. "flagstones.ktx2")
|
|
local draws, drawn, calls, textureKB = sceneGetStats()
|
|
debugPrint("textures: " .. textureKB .. " KB")
|
|
----
|
|
|
|
[#_lights_and_shadows]
|
|
==== Lichter und Schatten
|
|
|
|
Bis zu acht Lichter leuchten gleichzeitig, und jedes ist ein Knoten, sodass Lichter sich wie alles andere an Eltern hängen und animieren lassen:
|
|
|
|
* `LIGHT_DIRECTIONAL` ist eine Sonne: Nur ihre Richtung zählt, und alles wird gleich beleuchtet. Eine Intensität von `1` beleuchtet eine ihr zugewandte weiße Fläche weiß.
|
|
* `LIGHT_POINT` ist eine Glühbirne, die mit der Entfernung schwächer wird, und `lightSetRange` stoppt sie jenseits einer Entfernung ganz. Ihre Intensität ist ihre Helligkeit an der Glühbirne selbst, die Hälfte davon in einer Einheit Abstand; eine Glühbirne, die einen Raum beleuchten soll, braucht also eine Zahl im zweistelligen Bereich.
|
|
* `LIGHT_SPOT` ist ein Kegel mit einem inneren Winkel, der voll beleuchtet ist, und einem äußeren Winkel, an dem das Licht zu nichts verblasst ist (`lightSetCone`).
|
|
|
|
`sceneSetAmbient` fügt ein wenig Licht aus allen Richtungen hinzu, damit Schatten nicht schwarz sind; ein Himmel macht das besser (siehe <<scenelook,Das Aussehen des Frames>>). Farben, die als `0` bis `255` angegeben werden, sind sRGB, wie alles andere in Singe, und werden beim Hereinkommen in lineares Licht umgerechnet, sodass ein Licht von `128, 128, 128` ein Viertel so hell ist wie Weiß, nicht halb so hell.
|
|
|
|
Jedes Licht kann weichkantige Schatten von allem Undurchsichtigen werfen (`lightSetShadow`). Richtungs- und Spotlichter werfen sie über eine Shadow-Map, Punktlichter über eine Cube-Map, die in jede Richtung reicht, sodass eine Glühbirne in dem sitzen kann, was sie beleuchtet (ein Raum, ein Lampenschirm, der Automat): Geschlossene Meshes werfen auch von ihren Rückseiten Schatten, und der Tiefenbereich des Schattens wird an die Schattenwerfer um das Licht herum angepasst. `nodeSetShadow(node, false)` befreit ein Mesh vom Schattenwerfen: das eigene Glas der Glühbirne, ein leuchtendes Schild, ein Namensschild. `sceneSetShadowSize` setzt die Auflösung der Map (`256` bis `4096`, Standard `1024`) und tauscht Schärfe gegen Geschwindigkeit.
|
|
|
|
Der Schatten eines Richtungslichts wird entlang der Blickrichtung in Kaskaden aufgeteilt (`sceneSetShadowCascades`, standardmäßig drei), jede eine Map, die an einen Ausschnitt dessen angepasst ist, was die Kamera sieht, sodass Schatten in einem großen Level aus der Nähe scharf bleiben und trotzdem bis `sceneSetShadowDistance` reichen. Eine einzige Kaskade passt eine einzige Map an die ganze Szene an, was zu kleinen Szenen und orthografischen Kameras passt. Eine Ansicht (`viewNew`) zeichnet mit denselben Maps und nimmt für jeden Punkt die feinste Kaskade, die ihn abdeckt, sodass ein Monitor die Schatten zeigt, die das Fenster zeigt.
|
|
|
|
[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) -- Drei Kaskaden bis 80 Einheiten.
|
|
|
|
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) -- Eine Cube-Map: sechs Durchgänge pro Frame.
|
|
nodeSetShadow(bulbGlass, false) -- Das Glas um sie herum wirft nichts.
|
|
----
|
|
|
|
[#scenecamera]
|
|
==== Die Kamera und Ansichten
|
|
|
|
Jeder Knoten kann die Kamera sein (`cameraSet`); sie blickt entlang ihres eigenen -Z, also richtet `nodeLookAt` sie aus, und unter einen bewegten Knoten gehängt wird sie zur Verfolgerkamera. `cameraSetPerspective(fovDegrees, near, far)` ist das Standardobjektiv; `cameraSetOrthographic(height, near, far)` zeichnet ohne Perspektive, für Diagramme, HUD-artige Szenen und Spiele in Seitenansicht. Halte `near` so groß, wie die Szene es zulässt, denn die Genauigkeit des Tiefenpuffers verteilt sich zwischen `near` und `far`.
|
|
|
|
[source,lua]
|
|
----
|
|
local camera = nodeNew()
|
|
nodeSetParent(camera, car)
|
|
nodeSetPosition(camera, 0, 2.5, 7) -- Hinter und über dem Auto, in seinem Raum.
|
|
nodeLookAt(camera, 0, 1, 0)
|
|
cameraSet(camera)
|
|
cameraSetPerspective(55, 0.1, 300)
|
|
----
|
|
|
|
Eine *Ansicht* ist eine zweite Kamera, die in jedem Frame in eine Textur gerendert und auf einem Material gezeigt wird: ein Überwachungsmonitor, ein Rückspiegel, ein Portal, ein Bild im Bild. `viewNew(width, height)` erzeugt eine in dieser Auflösung, `viewSetCamera` gibt ihr einen Kameraknoten, und `materialSetView` legt ihr Bild auf ein beliebiges Mesh. Ansichten teilen die Schatten des Frames und lassen Bloom aus, und jede rendert die Szene noch einmal; halte sie also wenige und klein. Jede Ansicht dreht Billboards (siehe <<scenesprites,Sprites und Text in der Szene>>) zu ihrer eigenen Kamera und zeichnet transparente Materialien von ihr aus von hinten nach vorn, sodass ein Monitor Namensschilder von vorn und Glas in der richtigen Reihenfolge sieht.
|
|
|
|
[source,lua]
|
|
----
|
|
local rearCamera = nodeNew()
|
|
nodeSetParent(rearCamera, car)
|
|
nodeSetPosition(rearCamera, 0, 1.2, -2)
|
|
nodeSetRotation(rearCamera, 0, 180, 0) -- Blick nach hinten.
|
|
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) -- Die Ebene zeigt nach oben; stell sie auf, dem Fahrer zugewandt.
|
|
----
|
|
|
|
[#scenelook]
|
|
==== Das Aussehen des Frames
|
|
|
|
Die Szene wird in linearem Licht beleuchtet. Farben, die du als `0` bis `255` angibst, sind sRGB und werden beim Hereinkommen decodiert, Texturen werden von der GPU beim Abtasten decodiert, Lichter addieren sich ohne Abschneiden, und am Ende des Frames bringt eine Tonkurve das Ergebnis zurück auf den Bildschirm. Was das in der Praxis bedeutet:
|
|
|
|
* `sceneSetExposure(stops)` hellt den ganzen Frame auf oder dunkelt ihn ab wie die Belichtung einer Kamera: `+1` verdoppelt das Licht, `-1` halbiert es, sodass eine Höhle und ein Strand dieselben Lichter und Materialien teilen können.
|
|
* `sceneSetTonemap` wählt die Kurve: `TONEMAP_NEUTRAL` (die Voreinstellung, die Farben dort lässt, wo glTF-Viewer sie lassen, und nur das hellste Licht komprimiert), `TONEMAP_ACES` (ein filmischer Look mit mehr Kontrast) oder `TONEMAP_NONE` (bei Weiß abschneiden).
|
|
* `sceneSetBloom(threshold, strength)` lässt alles leuchten, was heller als der Schwellwert ist: die Sonne durch ein Fenster, eine Glühbirne, ein selbstleuchtendes Schild, eine Flamme. Das Leuchten wird aus dem Licht der Szene vor der Tonkurve genommen, sodass nur, was wirklich hell ist, Bloom erzeugt; ein Schwellwert von `1` ist Weiß.
|
|
|
|
.Der Himmel und sein Licht
|
|
`sceneSetSky(file)` hüllt die Szene in ein äquirektanguläres Panorama. Ein Radiance-Bild `.hdr` behält seinen vollen Umfang; ein PNG oder JPEG funktioniert auch. Der Himmel wird hinter allem gezeichnet, und sofern nicht `sceneSetEnvironment(false)` gilt, beleuchtet er auch die Szene: Seine Gesamtfarbe aus allen Richtungen ersetzt das flache Umgebungslicht, und seine Spiegelung zeigt sich auf Metallen und glänzenden Oberflächen, scharf auf einem Spiegel und umso unschärfer, je höher die Rauheit. Ohne Himmel haben Metalle nichts zu spiegeln, und nur das Umgebungslicht und die Lichter formen sie. `sceneSetSkyIntensity` skaliert den Himmel und sein Licht zusammen, sodass ein Panorama für Mittag und Abenddämmerung dient.
|
|
|
|
.Nebel
|
|
`sceneSetFog(r, g, b, near, far)` blendet alles, Partikel eingeschlossen, zwischen zwei Entfernungen zu einer Farbe hin aus. Der Himmel wird nie vernebelt; passe also die Farbe des Nebels an den Horizont des Himmels an, und der ferne Rand der Szene verschwindet darin.
|
|
|
|
[source,lua]
|
|
----
|
|
sceneSetSky(DIR .. "harbor.hdr")
|
|
sceneSetSkyIntensity(0.8)
|
|
sceneSetFog(190, 200, 215, 40, 160) -- Die Farbe des Himmels am Horizont.
|
|
sceneSetExposure(-0.5) -- Etwas dunkler, als die Sonne es machen würde.
|
|
sceneSetBloom(1.0, 0.35) -- Nur, was heller als Weiß ist, leuchtet.
|
|
----
|
|
|
|
[#_models_and_animation]
|
|
==== Modelle und Animation
|
|
|
|
`modelLoad` nimmt eine `.glb` über denselben spielrelativen Namen, den `spriteLoad` nimmt, und eine gepackte `.game` trägt sie mit. Die Datei muss in sich abgeschlossen sein: Meshes, Skins, Animationen und Texturen alle in der einen Datei, was der Standardexport von Blender ist. Ein Modell, das auf eine `.bin` oder ein Bild daneben verweist, wird mit einer Meldung abgelehnt, die die Datei nennt, aus demselben Grund, aus dem ein gepacktes Spiel in sich abgeschlossen sein muss. Materialien verwenden das Metallic-Roughness-Modell mit `KHR_materials_emissive_strength` und `KHR_texture_basisu`; eine Textur, die über einen zweiten Satz Texturkoordinaten adressiert wird, liest den ersten Satz, und `KHR_texture_transform` wird ignoriert, jeweils mit einer Zeile im Trace des Programms. Ein Skin kann bis zu `128` Gelenke haben. Das Laden ist der teure Teil; `modelInstance` platziert dann eine Kopie des Knotenbaums des Modells, so oft du willst, und jede Instanz teilt die Meshes und Materialien der Datei. `modelDelete` gibt das Modell frei; Instanzen behalten ihre Knoten, nackt.
|
|
|
|
[source,lua]
|
|
----
|
|
local foxModel = modelLoad(DIR .. "Fox.glb")
|
|
local pack = {}
|
|
for i = 1, 6 do
|
|
local fox = modelInstance(foxModel)
|
|
nodeSetScale(fox, 0.02) -- Die Datei ist in Zentimetern.
|
|
nodeSetPosition(fox, i * 1.5 - 5, 0, 0)
|
|
pack[i] = fox
|
|
end
|
|
----
|
|
|
|
Die Animationen eines Modells listet `modelGetAnimations` auf, und gesteuert werden sie pro Instanz mit `animationPlay(node, nameOrIndex, loop, speed, fade)` auf dem Wurzelknoten der Instanz, dazu Aufrufe zum Pausieren, Fortsetzen, Stoppen und Springen. Knotenanimation (Dinge, die sich bewegen), Skinning (Figuren, die sich verformen) und Morph-Ziele (Blend Shapes: Gesichter, Lippensynchronität, Muskelwölbungen) funktionieren alle. Animationen schreiten mit der echten Zeit fort und halten an, während das Spiel pausiert ist. Ein Morph-Gewicht lässt sich auch aus dem Skript mit `nodeSetMorph` setzen, nach Name oder Nummer, auf dem Knoten, der das Mesh trägt.
|
|
|
|
Ein `fade` in Sekunden blendet von dem über, was gerade lief, sodass ein Gehen ohne Ruck zu einem Laufen wird. Wenn nichts lief (nach `animationStop` oder wenn eine Ragdoll abgeschaltet wurde), beginnt die Überblendung bei der Pose, in der die Knoten sind, sodass sich eine Figur von dort aufrichtet, wo sie gefallen ist. Bis zu vier weitere Clips können gleichzeitig auf Ebenen über der Basis laufen (`animationPlayLayer`), jeder mit `animationSetLayerWeight` eingeblendet und mit `animationSetLayerMask` auf einen Teil des Skeletts beschränkt: ein Umsehen am Hals, während die Beine gehen, ein Winken mit einem Arm über einem Laufen.
|
|
|
|
[source,lua]
|
|
----
|
|
animationPlay(fox, "Walk", true)
|
|
animationPlayLayer(fox, 1, "Survey", true, 1, 0.5) -- Kopf dreht sich, über eine halbe Sekunde eingeblendet
|
|
animationSetLayerMask(fox, 1, nodeFind("b_Neck_04", fox)) -- ... nur am Hals und darüber.
|
|
|
|
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) -- Schneller, in 0,4 s überblendet.
|
|
end
|
|
end
|
|
----
|
|
|
|
[#_sound_in_the_scene]
|
|
==== Sound in der Szene
|
|
|
|
Ein Soundeffekt kann in der Szene platziert werden, sodass er von dort zu hören ist, wo er ist. `soundSetPosition(channel, x, y, z)` setzt einen spielenden Kanal an einen Weltpunkt, `soundSetNode(channel, node)` lässt ihn stattdessen einem Knoten folgen, und `soundSetRange(channel, near, far)` sagt, wo er in voller Lautstärke ist und wo er zu Stille verklungen ist. Sounds werden von der Kamera aus gehört, sofern `soundSetListener` nicht einen anderen Knoten nennt (den Kopf des Spielers, bei einem Third-Person-Spiel), und sie wandern nach links und rechts, wenn sich der Zuhörer dreht. Ein Kanal, der nicht in der Szene platziert ist, spielt wie immer, mit `soundSetPan` für Stereo.
|
|
|
|
[source,lua]
|
|
----
|
|
local crackle = soundPlay(fireSound, -1) -- Endlosschleife.
|
|
soundSetNode(crackle, brazier)
|
|
soundSetRange(crackle, 2, 18) -- Voll innerhalb von 2 Einheiten, weg bei 18.
|
|
soundSetListener(hero)
|
|
----
|
|
|
|
[#scenesprites]
|
|
==== Sprites und Text in der Szene
|
|
|
|
Jeder Knoten kann statt eines Meshes ein Bild zeigen. `nodeSetSprite(node, sprite, height)` legt ein geladenes Sprite (jeden Frame eines animierten, wobei `nodeSetSpriteFrame` auswählt) auf ein Quad, das so viele Welteinheiten hoch ist, beleuchtet wie alles andere und Schatten werfend, sofern `nodeSetShadow` nichts anderes sagt. `nodeSetText(node, text, height)` tut dasselbe mit einer Textzeile in der gewählten Schriftart und Farbe, unbeleuchtet. Beide sind für den Rest der Engine Meshes: Sie hängen an Eltern, skalieren und nehmen das Überblenden eines Materials an.
|
|
|
|
`nodeSetBillboard` dreht einen Knoten in jedem Frame zur Kamera: `BILLBOARD_ALL` frontal (Namensschilder, Leuchteffekte, Impostors), `BILLBOARD_Y` nur um seine eigene Hochachse (Bäume, Lebensbalken, alles, was aufrecht bleiben muss), und `BILLBOARD_NONE`, um aufzuhören. Es funktioniert auch bei Meshes.
|
|
|
|
[source,lua]
|
|
----
|
|
local tag = nodeNew()
|
|
nodeSetParent(tag, fox)
|
|
nodeSetPosition(tag, 0, 110, 0) -- In den Zentimetern des Fox-Modells, über seinem Kopf.
|
|
nodeSetScale(tag, 50) -- Die Skalierung 0,02 der Instanz aufheben.
|
|
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]
|
|
==== Vor dem Overlay
|
|
|
|
Die Szene wird normalerweise unter dem Overlay gezeichnet, sodass alles, was auf das Overlay gezeichnet wird, sie verdeckt. `nodeSetLayer(node, LAYER_FRONT)` verschiebt einen Knoten und alles, was an ihm hängt, stattdessen in die vordere Ebene: Dieselbe Kamera zeichnet ihn nach dem Overlay, von denselben Lichtern beleuchtet und mit derselben Belichtung, derselben Tonkurve und demselben Bloom fertiggestellt, sodass ein drehendes Logo, ein 3D-Titel oder ein Modell auf einer Highscore-Tabelle über 2D-Grafik stehen kann. Die vordere Ebene ist ein eigenes Bild, transparent gelöscht, sodass das Overlay überall dort sichtbar ist, wo sie nichts zeichnet, und nichts in der Szene hinter dem Overlay sie verdecken kann. GUIs und die über das Overlay gezeichneten Partikel liegen weiterhin darüber.
|
|
|
|
Vordere Knoten werfen weiterhin Schatten auf die Szene. Der Himmel, 3D-Partikel und Debug-Linien bleiben in der Szene, und Ansichten (`viewNew`) zeichnen nur die Knoten der Szene. Die Ebene kostet einen zweiten Renderdurchgang, und das nur in Frames, in denen etwas darin ist.
|
|
|
|
[source,lua]
|
|
----
|
|
-- Eine goldene Münze, die sich über dem 2D-Titelbildschirm dreht.
|
|
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]
|
|
==== Gelände
|
|
|
|
`meshHeightmap(image, sizeX, sizeY, sizeZ)` macht aus einem Graustufenbild hügeligen Boden: ein Vertex pro Pixel, Schwarz unten und Weiß `sizeY` hoch, mit Normalen aus den Steigungen und Texturkoordinaten über das Ganze. Gib seinem Material eine Textur und `materialSetTiling`, um sie alle paar Einheiten zu wiederholen. Bilder dürfen in jeder Richtung bis zu 1025 Pixel groß sein; eine Map von 257 Pixeln über 120 Einheiten ergibt einen Vertex pro halbe Einheit, was für Hügel reichlich ist.
|
|
|
|
Ein `BODY_STATIC`-Körper mit `SHAPE_MESH` auf diesem Knoten wird zu einem Höhenfeld, weit billiger, als die Dreiecke es wären, sodass Autos darauf fahren und Spieler darauf gehen, so wie sie sind. `terrainGetHeight(node, x, z)` sagt, wie hoch der Boden an einem Punkt ist, um Dinge darauf zu platzieren, ohne einen Strahl zu werfen.
|
|
|
|
[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) -- Ein Höhenfeld.
|
|
|
|
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]
|
|
==== Navigation
|
|
|
|
Ein Navigationsmesh sagt Figuren, wo sie gehen können. `navNew(radius, height, maxSlope, maxStep)` beginnt eines für eine gegebene Agentengröße, `navAddNode` füttert es mit der Geometrie des Levels (die Wurzel einer Modellinstanz liefert das ganze Modell), und `navBuild` backt die begehbare Fläche: Böden, Rampen und Treppen, die der Agent erklimmen kann, abzüglich eines Rands um jede Wand und Säule. Sponza zu backen dauert auf einem Desktop eine Zehntelsekunde; `navSave` und `navLoad` bewahren ein gebackenes Mesh im Datenordner des Spiels auf, für Rechner, auf denen es länger dauert, und ein mit dem Spiel ausgeliefertes Mesh lädt genauso. `navDraw` zeigt das Mesh über der Szene, während du die Parameter richtig einstellst.
|
|
|
|
Abfragen beantworten dann die Fragen, die ein Spiel stellt: `navPath` gibt die Ecken des kürzesten Wegs zwischen zwei Punkten zurück, `navNearest` setzt einen Punkt auf das Mesh, `navRaycast` sagt, ob ein gerader Weg blockiert ist, und `navRandomPoint` sucht irgendeinen Ort aus, zu dem man schlendern kann.
|
|
|
|
Agenten (`navAgentNew`) laufen das Mesh für dich ab. `navAgentMoveTo` schickt einen irgendwohin, und er steuert unterwegs um die anderen Agenten herum, bewegt seinen Knoten (oder steuert einen Spieler-Controller darauf, mit `navAgentSetPlayer`) und ruft `onNavArrived` auf, wenn er ankommt. `navAgentGetVelocity` sagt dir, ob du das Gehen oder das Laufen abspielen sollst.
|
|
|
|
[source,lua]
|
|
----
|
|
local nav = navNew(0.4, 1.6, 45, 0.3) -- Ein Agent 0,8 breit, 1,6 hoch, steigt 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)) -- Weiter zum nächsten Ort.
|
|
end
|
|
----
|
|
|
|
[#sceneperformance]
|
|
==== Leistung und Anforderungen
|
|
|
|
Die Szene braucht eine GPU, die Vulkan (Linux), Direct3D 12 (Windows 10 und neuer), Metal (macOS) oder OpenGL ES 3.1 spricht. Das ES-Backend ist es, das die billigen Linux-Handhelds und den Raspberry Pi hereinholt, deren Mali- und VideoCore-Chips einen ausgereiften GLES-Treiber haben und entweder gar kein Vulkan oder ein unausgereiftes; es wird automatisch gewählt, wenn keines der anderen drei verfügbar ist, und es zeichnet dasselbe Bild aus denselben Shadern. Der Raspberry Pi 4 bleibt der Mindest-Pi für 3D: Der Pi 3 hat nur OpenGL ES 2.0, das das Skinning und die Morph-Ziele der Szene nicht ausdrücken kann. Auf einem Rechner ohne eines davon laufen 2D-Spiele genau wie zuvor (2D-Physik eingeschlossen), und der erste 3D-Aufruf beendet das Spiel mit einem Fehler, der das Problem nennt.
|
|
|
|
Meshes außerhalb des Blickfelds der Kamera werden übersprungen, und Kopien desselben Meshes mit demselben Material werden als ein instanzierter Aufruf gezeichnet, sodass ein Wald aus ein und demselben Baum etwa so viel kostet wie ein Baum. `sceneGetStats` meldet die gesammelten Zeichenvorgänge, wie viele davon im Blickfeld waren, wie viele Draw Calls das brauchte und den belegten Texturspeicher. So hilfst du nach:
|
|
|
|
* Teile Meshes und Materialien zwischen Knoten (ein `meshBox`, viele Knoten; ein Modell, viele `modelInstance`-Aufrufe), statt für jedes Objekt eines zu erzeugen.
|
|
* Geskinnte und morphende Meshes und überblendete werden einzeln gezeichnet; beschränke sie auf das, was die Szene braucht.
|
|
* Kantenglättung (4x-Multisampling) ist überall eingeschaltet, wo die GPU sie anbietet; `sceneSetAntialias(false)` gewinnt auf einem Pi Geschwindigkeit zurück, ebenso ein kleineres `sceneSetShadowSize` und weniger schattenwerfende Punktlichter, von denen jedes sechs Durchgänge kostet.
|
|
* Behalte auf kleinen Boards die Dreieckszahlen und Texturgrößen im Auge, liefere KTX2-Texturen aus und lade ein Modell einmal statt zweimal.
|
|
|
|
[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]
|
|
=== Physik
|
|
|
|
Körper, Gelenke, Trigger und Raycasts leben in der 3D-Szene und laufen auf Jolt
|
|
Physics. Ein Körper hängt an einem Knoten, wie es ein Mesh tut, und von da an sind
|
|
Knoten und Körper eins: Die Form des Körpers wird in Welteinheiten bemessen
|
|
und vom Knoten skaliert, und welcher der beiden sich auch bewegt, der andere folgt.
|
|
Die Einheiten sind Meter, Kilogramm und Sekunden, und die Schwerkraft ist standardmäßig
|
|
`0, -9.81, 0`. Physik braucht keine GPU; siehe <<physics2d,2D-Spiele>>.
|
|
|
|
[#_bodies]
|
|
==== Körper
|
|
|
|
`bodyNew(node, type, shape, a, b, c)` gibt einem Knoten einen Körper von einem von drei
|
|
Typen:
|
|
|
|
* Ein *statischer* Körper (`BODY_STATIC`) bleibt, wo der Knoten war, als der Körper
|
|
erzeugt wurde: Böden, Wände, der Automat. Seinen Knoten danach zu bewegen, bewegt
|
|
den Körper nicht.
|
|
* Ein *kinematischer* Körper (`BODY_KINEMATIC`) folgt seinem Knoten, wohin das Skript
|
|
oder eine Animation ihn auch bewegt, und schiebt dynamische Körper aus dem Weg, ohne
|
|
selbst geschoben zu werden: ein Schläger, eine bewegliche Plattform, die Hand einer Figur.
|
|
* Ein *dynamischer* Körper (`BODY_DYNAMIC`) wird von der Simulation bewegt und treibt
|
|
seinen Knoten: Er fällt, prallt ab, rutscht und wird geschoben. Bewege seinen Knoten
|
|
nicht selbst; gib ihm stattdessen Geschwindigkeiten, Kräfte oder Impulse
|
|
(`bodySetVelocity`, `bodyApplyForce`, `bodyApplyImpulse`).
|
|
|
|
Die Form ist ein Quader, eine Kugel, eine Kapsel oder ein Zylinder, bemessen durch `a`, `b` und `c`
|
|
(`SHAPE_BOX` nimmt Breite, Höhe und Tiefe; `SHAPE_SPHERE` einen Radius;
|
|
`SHAPE_CAPSULE` und `SHAPE_CYLINDER` einen Radius und eine Höhe entlang Y), eine
|
|
konvexe Hülle um das Mesh des Knotens (`SHAPE_HULL`, alles unter dem Knoten, also
|
|
funktioniert die Wurzel einer Modellinstanz) oder die exakten Dreiecke des Meshes
|
|
(`SHAPE_MESH`, nur für statische und kinematische Körper; auf einem Heightmap-Mesh ist es
|
|
ein Höhenfeld, siehe <<sceneterrain,Gelände>>).
|
|
|
|
[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) -- Der kinematische Schläger fegt durch den Stapel.
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
.Masse und Materialien
|
|
Die Masse eines dynamischen Körpers ergibt sich aus dem Volumen seiner Form bei der Dichte
|
|
von Wasser, sodass ein Quader von der Größe einer Tür 140 Kilogramm wiegt und ein Stups ihn
|
|
nicht bewegt. Setze `bodySetMass` bei allem, was sich leicht anfühlen soll, und bemiss
|
|
Impulse nach der Masse: Ein Impuls von `mass * speed` setzt genau diese Geschwindigkeit.
|
|
`bodySetFriction` und `bodySetBounce` machen Eis und Gummi; `bodySetEnabled`
|
|
nimmt einen Körper aus der Welt und setzt ihn wieder ein, ohne ihn zu verlieren.
|
|
|
|
.Zeitsteuerung
|
|
Die Welt macht nach der Uhr feste sechzig Schritte pro Sekunde, nachdem Animationen
|
|
ihre Knoten in Pose gebracht haben und bevor die Szene gerendert wird, sodass ein von
|
|
einer Animation getriebener kinematischer Körper mit der richtigen Geschwindigkeit schiebt.
|
|
Das Spiel zu pausieren pausiert die Physik; `physicsSetEnabled(false)` pausiert sie
|
|
allein. `bodyIsResting` sagt, wann ein Körper zur Ruhe gekommen ist, also wann ein
|
|
geworfenes Ding liegen geblieben ist und Punkte gezählt werden können. Ein Körper, eine
|
|
Spielfigur, eine Ragdoll, ein Soft Body oder ein Fahrzeug, dessen Knoten gelöscht wurde,
|
|
wird zu Beginn des nächsten Frames freigegeben, ob aktiviert oder nicht und ob pausiert
|
|
oder nicht, sodass sein Platz wieder frei ist; ein Knoten-Handle, das `nodeNew` später
|
|
neu vergibt, trägt nichts von der Physik des alten Knotens, und `playerExists` und
|
|
seinesgleichen antworten dafür mit `false`, bis es eine eigene bekommt.
|
|
|
|
[#_events_and_queries]
|
|
==== Ereignisse und Abfragen
|
|
|
|
Definiere `onCollision(nodeA, nodeB, x, y, z, speed)`, und Singe ruft es für jeden
|
|
neuen Kontakt einmal auf, mit dem Punkt und der Geschwindigkeit, mit der die beiden
|
|
Körper aufeinandertrafen, sodass ein Rempler und ein Crash verschieden klingen können.
|
|
Ein Körper, der mit `bodySetTrigger` zum Trigger gemacht wurde, schiebt nichts, meldet
|
|
aber über `onTrigger(trigger, other, entered)`, was in ihn eintritt und ihn verlässt:
|
|
eine Ziellinie, eine Tür, eine Gefahrenzone. `physicsRaycast` findet den ersten Körper
|
|
entlang eines Strahls; mit `sceneUnproject` für den Strahl ist das die Objektauswahl
|
|
mit der Maus (siehe <<nodes,Knoten>>).
|
|
|
|
[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]
|
|
==== Gelenke
|
|
|
|
`jointHinge`, `jointBall` und `jointSlider` verbinden zwei Körper oder einen Körper
|
|
und die Welt, wenn der zweite Knoten `-1` ist: Türen, Pendel, Schubladen, die
|
|
Räder eines von der Seite gesehenen Autos. Anker und Achse werden in Weltkoordinaten
|
|
angegeben, dort, wo die Körper stehen, wenn das Gelenk erzeugt wird. Scharniere und
|
|
Schieber nehmen Grenzen (`jointSetLimits`): Ein positiver Scharnierwinkel ist eine
|
|
Drehung des ersten Knotens um die Achse nach der Rechte-Hand-Regel, und eine positive
|
|
Schieberstrecke verläuft entlang der Achse.
|
|
|
|
[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) -- Am Rahmen, um Y.
|
|
jointSetLimits(hinge, 0, 110) -- Öffnet in eine Richtung, 110 Grad.
|
|
----
|
|
|
|
Körper auf Modellinstanzen funktionieren wie alle anderen: `bodyNew(instanceRoot,
|
|
BODY_DYNAMIC, SHAPE_HULL)` hüllt das ganze Modell in eine konvexe Hülle. Eine geskinnte
|
|
Figur ist am besten eine Spielfigur (siehe unten) oder eine kinematische Kapsel, die
|
|
Dinge schiebt, statt geschoben zu werden.
|
|
|
|
[#physics2d]
|
|
==== 2D-Spiele
|
|
|
|
Physik ist nicht an die 3D-Szene gebunden. Knoten gibt es auf jedem Rechner, mit GPU
|
|
oder ohne, also kann ein 2D-Spiel Knoten Körper geben, `physicsSet2D(true)` aufrufen,
|
|
damit sie sich nur in X und Y bewegen und nur um Z drehen, in Overlay-Koordinaten
|
|
arbeiten, wobei die Schwerkraft den Bildschirm hinunter zeigt, und Sprites dort zeichnen,
|
|
wo die Knoten sind. Das ist die Form, die auf einem Raspberry Pi 3 läuft, der kein 3D hat.
|
|
|
|
[source,lua]
|
|
----
|
|
physicsSet2D(true)
|
|
physicsSetGravity(0, 900, 0) -- Pixel pro Sekunde zum Quadrat; Overlay-Y läuft nach unten.
|
|
|
|
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
|
|
----
|
|
|
|
Gib jedem 2D-Körper etwas Tiefe (die dritte Größe), damit sich Formen in Z überlappen;
|
|
`50` genügt. Kreise sind `SHAPE_SPHERE`. Spielfiguren und 2D-Emitter (siehe
|
|
<<particles,Partikel>>) funktionieren auch in dieser Welt, und ein Plattformspiel ist eine
|
|
Spielfigur mit `playerMove(node, vx)`.
|
|
|
|
[#_players]
|
|
==== Spielfiguren
|
|
|
|
Ein Körper geht, wohin Kräfte ihn schicken; eine *Spielfigur* geht, wohin das Skript sagt.
|
|
`playerNew` setzt einen Character-Controller auf einen Knoten: standardmäßig eine Kapsel
|
|
(`playerNew(node, radius, height)`) oder jede konvexe `bodyNew`-Form
|
|
(`playerNew(node, SHAPE_BOX, w, h, d)` für einen Roboter, `playerNew(node,
|
|
SHAPE_SPHERE, r, 0)` für etwas, das rollt und trotzdem Stufen steigt,
|
|
`SHAPE_HULL` für den eigenen Umriss des Modells; drei Argumente bedeuten immer eine
|
|
Kapsel, also schreibt eine Kugel ihre Größen aus). Die Form steht auf dem Ursprung des
|
|
Knotens, und der sind die Füße. Jeden Frame gibt das Skript `playerMove` die horizontale
|
|
Geschwindigkeit, die es will, und der Controller läuft Böden entlang, steigt Stufen bis
|
|
`playerSetStep` hinauf, gleitet an Wänden entlang, verweigert Hänge, die steiler als
|
|
`playerSetSlope` sind, fällt unter der Schwerkraft, fährt auf beweglichen Plattformen mit
|
|
und schiebt leichte dynamische Körper aus dem Weg (`playerSetPush`). `playerJump`
|
|
funktioniert nur mit Boden unter den Füßen und sagt, ob gesprungen wurde. Die Drehung des
|
|
Knotens gehört dem Skript: Dreh ihn so, dass er in seine Laufrichtung schaut, und häng
|
|
das Modell als Kind darunter.
|
|
|
|
[source,lua]
|
|
----
|
|
hero = nodeNew()
|
|
nodeSetPosition(hero, 0, 0, 0)
|
|
playerNew(hero, 0.3, 1.0) -- Eine Kapsel mit Radius 0.3, 1 hoch, Füße am Knoten.
|
|
fox = modelInstance(modelLoad(DIR .. "Fox.glb"))
|
|
nodeSetParent(fox, hero)
|
|
|
|
function onInputPressed(switch)
|
|
if switch == SWITCH_BUTTON1 then
|
|
playerJump(hero, 4.5) -- Braucht Boden unter den Füßen.
|
|
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
|
|
----
|
|
|
|
Spielfiguren melden `onCollision`, wenn sie einen Körper zu berühren beginnen, und
|
|
`onTrigger`, wenn sie einen Sensor betreten oder verlassen, mit ihrem Knoten an der
|
|
Stelle, die der Knoten eines Körpers einnähme, sodass eine Tür, ein Pickup und eine
|
|
Lavagrube für eine Spielfigur genauso funktionieren wie für einen Ball.
|
|
`playerIsOnGround` und `playerGetGround` sagen, worauf die Spielfigur steht, für
|
|
Schrittgeräusche und um absichtlich von einer Plattform zu fallen. In einer 2D-Welt ist
|
|
eine Spielfigur die Figur eines Plattformspiels: `playerMove(node, vx)` läuft entlang X,
|
|
die Schwerkraft zeigt den Bildschirm hinunter, und alles andere ist gleich. Die
|
|
Toleranzen des Controllers skalieren mit der Form, sodass sich eine 30 Pixel große Kapsel
|
|
in einem Sprite-Spiel wie eine von 0,3 Metern in einem 3D-Spiel verhält.
|
|
|
|
[#_vehicles]
|
|
==== Fahrzeuge
|
|
|
|
Ein *Fahrzeug* ist ein dynamischer Körper (das Chassis, mit `bodyNew` erzeugt wie jeder
|
|
Körper, mit Blick nach -Z und Y oben), den Jolt über Räder antreibt:
|
|
`vehicleNew(node, VEHICLE_CAR)` für alles mit Achsen,
|
|
`VEHICLE_MOTORCYCLE` für zwei Räder hintereinander, die sich in Kurven legen und
|
|
balancieren, oder `VEHICLE_TANK` für Räder auf zwei Ketten, die auf der Stelle wenden.
|
|
Räder sind Knoten: `vehicleAddWheel(node, wheelNode, radius, width,
|
|
suspension)` nimmt die Position des Rads daher, wo sein Knoten relativ zum Chassis
|
|
steht, und von da an stellt die Engine diesen Knoten jeden Frame (seine X-Achse ist die
|
|
Radachse, also will ein `meshCylinder`-Rad ein um 90 Grad gedrehtes Kind). Räder, die in
|
|
einem Modell mit `nodeFind("wheelFL", car)` gefunden werden, funktionieren genauso.
|
|
Standardmäßig lenken die Vorderräder, und alle treiben an; `vehicleSetWheel` legt es
|
|
anders fest.
|
|
|
|
Jeden Frame übergibt das Skript die Eingaben des Fahrers: `vehicleDrive(node, forward,
|
|
right, brake, handBrake)`. Motor, Automatikgetriebe und Differenziale sind die von Jolt
|
|
mit den Standardwerten eines 1500-kg-Autos; `vehicleSetEngine`,
|
|
`vehicleSetGears`, `vehicleSetSuspension`, `vehicleSetSteering`,
|
|
`vehicleSetBrakes` und `vehicleSetAntiRoll` stimmen sie ab, und jede Änderung baut das
|
|
Fahrzeug beim nächsten Schritt an Ort und Stelle neu auf. `vehicleGetSpeed`,
|
|
`vehicleGetRpm` und `vehicleGetGear` speisen ein Armaturenbrett. Ein Fahrzeug ist immer
|
|
noch ein Körper: Es springt über Rampen, prallt gegen Wände, schiebt Kisten und meldet
|
|
`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 }, -- Vorne links und rechts ...
|
|
{ -0.95, -0.2, 1.4 }, { 0.95, -0.2, 1.4 }, -- ... hinten links und rechts.
|
|
}
|
|
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
|
|
----
|
|
|
|
Panzer nehmen `forward` als Gas und `right` als Drehung (ohne Gas drehen sie auf der
|
|
Stelle) und ignorieren die Abstimmung von Lenkung, Bremsen und Stabilisator, da ihre
|
|
Ketten selbst bremsen und lenken. Die Neigefeder des Motorrads ist auf das Chassis
|
|
skaliert, sodass ein leichtes Motorrad genauso gut balanciert wie ein schweres.
|
|
Fahrzeuge mit Rädern brauchen eine 3D-Welt; ein von der Seite gesehenes 2D-Auto ist ein
|
|
Körper mit zwei per Scharnier befestigten Rädern, und das bauen die Gelenkaufrufe bereits.
|
|
|
|
[#_water]
|
|
==== Wasser
|
|
|
|
`bodySetWater(node, density, linearDrag, angularDrag)` füllt einen statischen Körper
|
|
mit Wasser: Die Oberseite seines Quaders ist die Oberfläche, und jeder dynamische Körper
|
|
darin schwimmt, sinkt oder rollt durch den Auftrieb von Jolt gegen diese Oberfläche (der
|
|
eingetauchte Teil der Form zählt, sodass eine schräge Kiste sich aufrichtet), gebremst
|
|
durch die Widerstände. `bodySetCurrent` gibt dem Wasser eine Strömung. `density` ist
|
|
ein Faktor, `1` für Wasser, und das eigene `bodySetBuoyancy` jedes schwimmenden Körpers
|
|
(Standard `1.2`, das sanft schwimmt; unter `1` sinkt er) skaliert ihn. Das Volumen
|
|
ist ein Trigger, also feuert `onTrigger` beim Hinein- und Hinausgehen, für Spritzer.
|
|
Wasser wird nicht gezeichnet: Gib dem Knoten des Volumens ein Material mit Blending
|
|
oder keines.
|
|
|
|
Eine Spielfigur im Wasser schwimmt: `playerIsSwimming` sagt es, `playerMove(node, vx,
|
|
vy, vz)` steuert dann in drei Achsen, ohne vertikale Eingabe sinkt sie langsam
|
|
(`playerSetSwim` setzt diese Rate und den Widerstand des Wassers), und die Strömung
|
|
trägt sie mit. Ein Boot ist ein schwimmender Rumpf, dem `vehicleNew(node, VEHICLE_BOAT)`
|
|
einen Propeller und ein Ruder gibt: `vehicleSetThrust` sagt, wie stark er schiebt
|
|
und wo im Rumpf, `vehicleSetRudder`, wie stark es dreht, und `vehicleDrive` funktioniert
|
|
wie bei einem Auto, alles nur, solange der Propeller unter einer Oberfläche ist.
|
|
|
|
[source,lua]
|
|
----
|
|
pool = nodeNew()
|
|
nodeSetMesh(pool, meshBox(8, 3, 12), blue) -- Blaues Material mit Blending, als Wasser gezeichnet.
|
|
nodeSetPosition(pool, 0, -1.5, 0) -- Seine Oberseite bei Y = 0 ist die Oberfläche.
|
|
bodyNew(pool, BODY_STATIC, SHAPE_BOX, 8, 3, 12)
|
|
bodySetWater(pool, 1.0, 0.6, 0.2)
|
|
bodySetCurrent(pool, 0.5, 0, 0) -- Treibt in Richtung +X.
|
|
bodySetBuoyancy(crate, 1.6) -- Liegt hoch im Wasser.
|
|
bodySetBuoyancy(anchor, 0.3) -- Geht unter.
|
|
----
|
|
|
|
[#_ragdolls]
|
|
==== Ragdolls
|
|
|
|
`ragdollNew(node)` sucht unter einer Modellinstanz nach ihrem Skin und macht aus dem
|
|
Skelett ein Ragdoll-Rezept: Jedes Gelenk mit einem Kindgelenk wird zu einer Kapsel
|
|
entlang dieses Knochens, durch ein Swing-Twist-Gelenk mit vernünftigen Grenzen mit dem
|
|
nächsten Knochen darüber verbunden, und Endgelenke (Finger, Ohren, die Schwanzspitze)
|
|
bewegen sich mit ihren Eltern mit. Nichts bewegt sich bis `ragdollActivate`: Dann
|
|
stoppt die Animation, Körper werden dort erzeugt, wo die Knochen sind, und von diesem
|
|
Moment an treibt die Physik die Gelenkknoten, sodass das geskinnte Mesh zusammensackt,
|
|
auf den Boden schlägt und mit dem Rest der Welt zur Ruhe kommt.
|
|
|
|
`ragdollApplyImpulse` stößt einen Knochen über den Namen seines Gelenks an.
|
|
`ragdollSetStrength` treibt jedes Gelenk zu der Pose hin, in der die Ragdoll aktiviert
|
|
wurde, was einen schlaffen Sturz in ein Taumeln verwandelt oder eine am Boden liegende
|
|
Figur ihre Gliedmaßen wieder einziehen lässt; einen Körper stellt es nicht auf, da sich
|
|
nichts vom Boden abstößt. `ragdollDeactivate` gibt das Skelett an die Animation zurück,
|
|
wo immer es liegt, und ein `animationPlay` mit Überblendung hebt es dann von dort sanft
|
|
an. Stimme einen Knochen, der falsch aussieht, vor dem Aktivieren mit `ragdollSetJoint`
|
|
ab.
|
|
|
|
[source,lua]
|
|
----
|
|
fox = modelInstance(foxModel)
|
|
ragdollNew(fox) -- Rezept aus dem Skin; noch animiert.
|
|
function onCollision(a, b, x, y, z, speed)
|
|
if (a == hero or b == hero) and speed > 3 then
|
|
playerSetEnabled(hero, false)
|
|
ragdollActivate(fox) -- Jetzt treibt die Physik das Skelett.
|
|
ragdollApplyImpulse(fox, "b_Spine02_03", 40, 30, 0)
|
|
end
|
|
end
|
|
----
|
|
|
|
[#_soft_bodies]
|
|
==== Soft Bodies
|
|
|
|
Stoff, Seil und Wackelpudding sind Meshes, deren Vertices simuliert werden.
|
|
`softNew(node, SOFT_CLOTH)` macht das Mesh des Knotens zu Stoff und seine Vertices
|
|
(nach Position verschweißt) zu Partikeln, verbunden durch Dehnungs-, Scher- und
|
|
Biege-Constraints, dort, wo der Knoten es in die Welt gesetzt hat; ein unterteiltes
|
|
`meshPlane(width, depth, columns, rows)` ist der übliche Anfang. `softNew(node,
|
|
SOFT_BODY)` macht dasselbe mit einem geschlossenen Mesh, und `softSetPressure` bläst
|
|
es auf: ein Ballon, ein Wackelpudding. `softNew(node, SOFT_ROPE, x, y, z, segments,
|
|
radius)` legt eine Kette von Partikeln von der Position des Knotens bis zu diesem Punkt
|
|
und gibt dem Knoten ein Röhren-Mesh um sie herum (sein Aussehen setzt du mit
|
|
`nodeSetMaterial`).
|
|
|
|
Soft Bodies kollidieren mit allem Starren, fallen unter der Schwerkraft und werden durch
|
|
das Mesh gezeichnet, aus dem sie stammen und das die Engine jeden Frame neu schreibt.
|
|
`softPin` hält das nächstgelegene Partikel fest, wo es ist oder an einem Knoten (die
|
|
Kante der Flagge an der Stange, das obere Ende des Seils am Kran); `softSetStiffness`,
|
|
`softSetMass` und `softSetDamping` stimmen das Material ab. Ein Knoten, dessen Mesh mit
|
|
anderen Knoten geteilt wird, verändert diese mit, also gib einem Soft Body ein eigenes
|
|
Mesh.
|
|
|
|
[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) -- Hängt aufrecht.
|
|
softNew(flag, SOFT_CLOTH)
|
|
for _, y in ipairs({ 1.9, 2.15, 2.4, 2.65, 2.9 }) do
|
|
softPin(flag, -5, y, 0) -- Entlang der Stange.
|
|
end
|
|
----
|
|
|
|
Die Kosten wachsen mit der Partikelzahl: Ein Tuch von 20 mal 20 hat 441 Partikel und
|
|
läuft gut auf einem Raspberry Pi 4; beschränke Stoff auf ein paar hundert Partikel und
|
|
ein Seil auf ein Dutzend Segmente. Mit starren Körpern kollidieren die Partikel, nicht
|
|
die Flächen, also hält erst ein ausreichend feines Mesh (Partikel näher beieinander als
|
|
das kleinste Ding, das darauf landen wird) eine Kiste vom Durchfallen ab. Es gibt keine
|
|
Selbstkollision: Stoff kann durch sich selbst hindurchgehen, wenn er zerknüllt wird.
|
|
Geskinnte Meshes können keine Soft Bodies sein.
|
|
|
|
[#_seeing_the_physics]
|
|
==== Die Physik sichtbar machen
|
|
|
|
Wenn eine Kiste durch den Boden fällt oder ein Gelenk sich in die falsche Richtung biegt,
|
|
zeichnet `physicsSetDebug` jeden Frame als Linien über die Szene, was die Simulation
|
|
tatsächlich enthält:
|
|
|
|
* `DEBUG_SHAPES`: die Körper als Drahtgitter, nach Zustand gefärbt (schlafend,
|
|
bewegt, statisch), die Spielfiguren in Gelb.
|
|
* `DEBUG_CONSTRAINTS`: die Gelenke, Ragdoll-Gelenke und Fahrzeug-Constraints
|
|
mit ihren Grenzen.
|
|
* `DEBUG_CONTACTS`: ein rotes Kreuz, wo sich Körper in diesem Schritt getroffen haben.
|
|
* `DEBUG_VELOCITIES`: eine Linie entlang der Geschwindigkeit jedes bewegten Körpers.
|
|
* `DEBUG_STATIC`: zusätzlich die Formen der statischen Körper, die sonst aus bleiben,
|
|
weil ein Level-Mesh sehr viele Linien ergibt.
|
|
|
|
Addiere sie, oder nimm `DEBUG_ALL` für die ersten vier. `DEBUG_NONE` schaltet es ab, und
|
|
solange es aus ist, kostet es nichts. Ein Skript zeichnet seine eigenen Linien genauso
|
|
mit `lineDraw` (einen Raycast, einen Pfad, eine Bounding Box), einen Frame nach dem
|
|
anderen wie das Overlay, und `navDraw` zeigt ein Navigationsmesh.
|
|
|
|
[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]
|
|
==== Wo sie läuft
|
|
|
|
Die Physik läuft auf der CPU und braucht keine GPU, aber die Bibliothek ist für
|
|
SSE4.1 auf x86 (2008 und später) und NEON auf dem Pi gebaut; ein Rechner darunter
|
|
bekommt beim ersten `bodyNew` einen Fehler. Die Simulation nutzt alle Kerne bis auf
|
|
einen, also rechnet ein Pi mit vier Kernen auf drei Threads; der Trace des Programms sagt, auf
|
|
wie vielen.
|
|
|
|
[#particles]
|
|
=== Partikel
|
|
|
|
Funken, Rauch, Feuer, Regen, Staub, Magie: Alles, was aus vielen kleinen, kurzlebigen
|
|
Dingen besteht, ist eine Aufgabe für einen *Emitter*. Ein Emitter hält ein Rezept (wie
|
|
viele pro Sekunde, wie lange sie leben, wie schnell und in welche Richtung sie starten,
|
|
was an ihnen zieht, wie sie wachsen, verblassen und sich drehen) und einen Pool von
|
|
Partikeln, die er nach diesem Rezept laufen lässt. Das Skript setzt das Rezept einmal und
|
|
startet dann den Emitter für einen gleichmäßigen Strom (`emitterStart`), lässt ihn für
|
|
einen einmaligen Puff ausstoßen (`emitterBurst`) oder beides. Die Engine bewegt jedes
|
|
lebende Partikel jeden Frame und hört damit auf, solange das Spiel pausiert ist.
|
|
|
|
Eine einzige Emitter-API dient dem Overlay und der 3D-Szene; nur die Einheiten und das
|
|
Zeichnen unterscheiden sich. Alles, was als Minimum und Maximum angegeben wird
|
|
(`emitterSetLife`, `emitterSetSpeed`, `emitterSetSize`), wird für jedes Partikel
|
|
zufällig zwischen den beiden gewählt.
|
|
|
|
[#_2d_emitters]
|
|
==== 2D-Emitter
|
|
|
|
`emitterNew()` ohne Argument erzeugt einen 2D-Emitter. Er lebt in Overlay-Koordinaten,
|
|
Y nach unten, also ist die Schwerkraft eines Springbrunnens positives Y. Bewege ihn mit
|
|
`emitterSetPosition` und zeichne ihn aus `onOverlayUpdate` mit `emitterDraw`. Das
|
|
Overlay selbst ist ein Bild, das die CPU malt, also werden Partikel nicht hineingemalt:
|
|
Die GPU zeichnet sie, entweder über dem ganzen Overlay (`PARTICLE_OVER`, der Standard)
|
|
oder darunter und über dem Video und der 3D-Szene (`PARTICLE_UNDER`, für Rauch hinter
|
|
der Punktzahl). 2D-Emitter laufen auf jedem Rechner, auf dem Singe läuft, den Raspberry
|
|
Pi 3 eingeschlossen, und passen gut zu 2D-Physik: Lies die Knotenposition eines Körpers
|
|
und gib sie an `emitterSetPosition`.
|
|
|
|
[source,lua]
|
|
----
|
|
sparks = emitterNew()
|
|
emitterSetBlend(sparks, PARTICLE_ADD)
|
|
emitterSetLife(sparks, 0.3, 0.7)
|
|
emitterSetSpeed(sparks, 80, 200)
|
|
emitterSetDirection(sparks, 0, -1) -- Den Bildschirm hinauf.
|
|
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]
|
|
==== 3D-Emitter
|
|
|
|
`emitterNew(node)` erzeugt einen an einen Knoten gehängten 3D-Emitter. Partikel entstehen
|
|
an der Weltposition des Knotens (über `emitterSetRadius` verteilt), in Welteinheiten,
|
|
und zeichnen sich in der Szene nach jedem Mesh als der Kamera zugewandte Billboards:
|
|
gegen die Geometrie tiefengetestet, ohne je Tiefe zu schreiben, unbeleuchtet, sofern
|
|
nicht verlangt, ohne Schatten zu werfen oder zu empfangen. Standardmäßig bleibt ein
|
|
Partikel, sobald es den Emitter verlässt, dort, wo es entstanden ist (Rauch treibt weiter
|
|
dort, wo er entstand, während die Fackel weiterzieht); `emitterSetLocal` lässt
|
|
stattdessen die ganze Wolke dem Knoten folgen. Wird der Knoten gelöscht, werden seine
|
|
Emitter mitgelöscht.
|
|
|
|
[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]
|
|
==== Das Bild
|
|
|
|
Ein Partikel ist ein texturiertes Quad. Ohne Textur ist es eine weiche Scheibe, die die
|
|
Engine bereitstellt; `emitterSetTexture` gibt ihm stattdessen ein geladenes Sprite
|
|
(einen Rauchpuff, einen Funkenstreifen, eine Schneeflocke), und bei einem animierten
|
|
Sprite wählt `emitterSetFrames` für jedes Partikel einen zufälligen Frame, sodass ein
|
|
Atlas von Puffs eine abwechslungsreiche Wolke ergibt. Das Blending ist normal
|
|
(`PARTICLE_ALPHA`) oder additiv (`PARTICLE_ADD`, für alles, was leuchtet).
|
|
`emitterSetColor` gibt Farbe und Alpha bei der Geburt und beim Tod an, und das Partikel
|
|
geht im Lauf seines Lebens von einem zum anderen über; `emitterSetSize` tut dasselbe für
|
|
seine Größe, mit einem dritten Wert dafür, wie schnell es sich dreht.
|
|
|
|
[#_trails_collisions_lit_and_soft_particles]
|
|
==== Spuren, Kollisionen, beleuchtete und weiche Partikel
|
|
|
|
`emitterSetTrail(emitter, length, width)` merkt sich die letzten paar Positionen jedes
|
|
Partikels und zeichnet ein Band durch sie, das zum Ende hin verblasst: Funken,
|
|
Leuchtspurgeschosse, Kometen, in 2D wie in 3D. `emitterSetCollide` lässt Partikel
|
|
abprallen: von einem waagerechten Boden in einer Höhe, die du angibst (`COLLIDE_FLOOR`,
|
|
kostenlos), oder von der Physikwelt per Raycasts (`COLLIDE_SCENE`, nur 3D, pro Frame
|
|
gedeckelt, sodass ein großer Emitter die Kosten über einige Frames verteilt).
|
|
|
|
Zwei weitere gibt es nur für 3D. `emitterSetLit` schattiert jedes Partikel als kleine,
|
|
der Kamera zugewandte Kugel nach den Lichtern und dem Himmel der Szene, sodass Rauch die
|
|
Farbe eines Kohlebeckens annimmt und Staub in der Sonne glitzert (Schatten werden nicht
|
|
gelesen). `emitterSetSoftness` blendet ein Partikel über eine Strecke aus, wo es auf
|
|
Geometrie trifft, sodass Rauch nicht in einen Boden schneidet und ein Springbrunnen nicht
|
|
in sein Becken; das kostet einen zusätzlichen Tiefendurchgang der Szene pro Frame,
|
|
solange irgendein Emitter es nutzt.
|
|
|
|
[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) -- Nimmt die Farbe des Feuers von unten an.
|
|
emitterSetSoftness(smoke, 0.4) -- Keine harte Kante, wo er auf die Schale trifft.
|
|
emitterStart(smoke)
|
|
|
|
embers = emitterNew(brazier)
|
|
emitterSetBlend(embers, PARTICLE_ADD)
|
|
emitterSetTrail(embers, 6, 0.02) -- Ein kurzer Streifen hinter jedem Glutfunken.
|
|
emitterSetCollide(embers, COLLIDE_FLOOR, 0.3, 0.8, 0) -- Prallen vom Boden bei Y = 0 ab.
|
|
emitterStart(embers)
|
|
----
|
|
|
|
[#_cost]
|
|
==== Kosten
|
|
|
|
Partikel sind billig zu simulieren und kosten, was sie auf dem Bildschirm bedecken. Ein
|
|
paar tausend kleine sind nichts; eine Handvoll großer additiver über dem ganzen
|
|
Bildschirm ist das, was einen Raspberry Pi 4 ausbremst. Der Pool wird durch
|
|
`emitterSetMax` gedeckelt (Standard 1000); wenn er voll ist, entstehen keine neuen, bis
|
|
einige sterben.
|
|
|
|
[#authoring]
|
|
=== Ein Spiel beschreiben statt es zu schreiben
|
|
|
|
Ein Spiel kann als *Beschreibung* geschrieben werden -- eine Tabelle aus Ebenen, Entitäten
|
|
und Regeln -- und von Forge, dem Autorenwerkzeug, in ein gewöhnliches Singe-Spiel kompiliert
|
|
werden. Forge ist ein eigenes Projekt mit eigenem Download und eigenem Buch.
|
|
|
|
|
|
[[migrating]]
|
|
=== Umstieg von Singe 2.10
|
|
|
|
Singe 3.00 hat das Sprite-Handle zum ersten Argument von `spriteDraw`,
|
|
`spriteLoop`, `spriteQuality`, `spriteRotate`, `spriteRotateAndScale`,
|
|
`spriteScale` und `spriteSetFrame` gemacht, sodass jeder Sprite-Aufruf jetzt zur
|
|
`video*`-Familie passt. Um ein Spiel anzupassen, verschiebe das letzte Argument jedes
|
|
dieser Aufrufe nach vorn:
|
|
|
|
[source,lua]
|
|
----
|
|
spriteDraw(x, y, cursor) -- 2.10
|
|
spriteDraw(cursor, x, y) -- 3.00
|
|
spriteRotate(angle, cursor) -- 2.10
|
|
spriteRotate(cursor, angle) -- 3.00
|
|
----
|
|
|
|
Ein Spiel, das du nicht bearbeiten kannst, kann sich stattdessen für die alte Reihenfolge
|
|
entscheiden. Setze entweder die globale Variable, bevor das Framework geladen wird:
|
|
|
|
[source,lua]
|
|
----
|
|
SINGE_LEGACY_SPRITE_ARGS = true
|
|
dofile("Singe/Framework.singe")
|
|
----
|
|
|
|
oder füge `LEGACY_SPRITE_ARGS = true` zu seinem Eintrag in `games.dat` hinzu. An keiner
|
|
anderen Aufrufform hat sich etwas geändert, wohl aber an einigen Verhaltensweisen:
|
|
|
|
* Handles und Anzahlen werden als Lua-Ganzzahlen zurückgegeben; `videoIsPlaying` gibt
|
|
einen Boolean zurück; `mouseSetMode` gibt nichts zurück.
|
|
* Fehler in Callbacks beenden das Spiel jetzt mit einem Traceback, statt ausgegeben und
|
|
ignoriert zu werden, so wie sich Argumentfehler schon immer verhalten haben.
|
|
* Gehaltene Tasten wiederholen sich in `MODE_NORMAL` nicht mehr.
|
|
* Neben das Spiel wird nichts geschrieben. Ohne `--datadir` ist der Datenordner
|
|
`data/<game>/` unter dem Spieleverzeichnis, wo 2.x den eigenen Ordner des Spiels benutzte;
|
|
Spielstände, Einstellungen und Highscores gehören unter
|
|
`singeGetDataPath()`.
|
|
* `math.randomseed` akzeptiert einen gebrochenen Startwert, wie unter dem Lua von 2.10;
|
|
er wird abgerundet, bevor der Generator von Lua 5.4 ihn sieht. `os.clock` ist die Zeit
|
|
nach der Uhr seit dem Start der Engine, nicht die Prozessorzeit.
|
|
* Die Disc wird von der Engine auf Frame 1 geparkt, bevor dein Skript läuft;
|
|
`Framework.singe` springt beim Laden nicht mehr.
|
|
* `onMouseMoved` übergibt im Modus `MANY_MOUSE` im dritten und vierten Argument die
|
|
echte Bewegung, und `onControllerMoved` übergibt den Controller-Index
|
|
(`0` bis `3`) statt der Instanz-ID von SDL.
|
|
* `mouseEnable` / `mouseDisable` und `singeEnablePauseKey` /
|
|
`singeDisablePauseKey` funktionieren weiterhin, als Aliase von `mouseSetEnabled` und
|
|
`singeSetPauseKeyEnabled`.
|
|
* Eine Laserdisc wird nicht mehr vorausgesetzt. Ein Eintrag in `games.dat` mit einem
|
|
`VIDEO` ist wie bisher ein Laserdisc-Spiel; einer ohne läuft stattdessen auf einer
|
|
Zeichenfläche. Auf der Befehlszeile übergib `--disc` (oder `--framefile`), da ein neben dem
|
|
Skript gefundenes Video keine Disc mehr bedeutet. Siehe <<withoutadisc,Spiele ohne
|
|
Disc>>.
|
|
* Die Pausentaste pausiert jetzt das ganze Spiel, nicht nur die Medien: Das Skript ist
|
|
eingefroren, bis die Taste erneut gedrückt wird, und `SWITCH_PAUSE` wird nur dann an
|
|
das Skript geliefert, wenn die Taste deaktiviert wurde. Sie wirkt beim Drücken der
|
|
Taste statt beim Loslassen. Siehe <<pausing,Pausieren>>.
|
|
* `fontPrint` behält die Strings, die es rendert, und `overlayClear` füllt nur neu, was
|
|
seit dem letzten Löschen gezeichnet wurde, sodass ein jeden Frame neu ausgegebenes HUD
|
|
billig ist. Text aus Performancegründen mit `fontToSprite` zu rendern ist veraltet; es
|
|
funktioniert weiterhin und ist immer noch der Weg zu Text, den du messen, skalieren
|
|
oder drehen kannst. Siehe <<fonttosprite,fontToSprite>>.
|
|
|
|
[#_engine_constants]
|
|
=== Engine-Konstanten
|
|
|
|
Die Engine definiert diese globalen Variablen, bevor irgendein Skript läuft, sodass sie
|
|
`controls.cfg` und `Framework.singe` gleichermaßen zur Verfügung stehen:
|
|
|
|
[cols="2,3",options="header"]
|
|
|===
|
|
| Konstante | Bedeutung
|
|
| `SWITCH_UP` ... `SWITCH_RECORD` | Werte, die in `MODE_NORMAL` an `onInputPressed` / `onInputReleased` übergeben werden. `SWITCH_MOUSE_DISCONNECT` löst die Engine selbst aus, wenn eine Maus oder Lightgun abgezogen wird, einmal als Drücken und einmal als Loslassen; keine Taste und kein Knopf erzeugt ihn. `SWITCH_RECORD` ist der Aufnahmeschalter, auf den die Engine selbst reagiert, wie auf den Screenshot-Schalter.
|
|
| `FONT_QUALITY_SOLID`, `FONT_QUALITY_SHADED`, `FONT_QUALITY_BLENDED` | Argumente für `fontQuality`.
|
|
| `MODE_NORMAL`, `MODE_FULL` | Argumente für `keyboardSetMode`.
|
|
| `MOUSE_SINGLE`, `MOUSE_MANY` (auch `SINGLE_MOUSE`, `MANY_MOUSE`) | Argumente für `mouseSetMode`.
|
|
| `OVERLAY_NOT_UPDATED`, `OVERLAY_UPDATED` | Rückgabewerte für `onOverlayUpdate`.
|
|
| `RENDER_PIXELATED`, `RENDER_SMOOTH` | Argumente für `spriteQuality` / `videoQuality`.
|
|
| `SPRITE_ALPHA`, `SPRITE_ADD`, `SPRITE_MULTIPLY` | Blending für `spriteSetBlend`.
|
|
| `DISC_STOPPED`, `DISC_PLAYING`, `DISC_PAUSED`, `DISC_EJECTED` | Rückgabewerte von `discGetState`.
|
|
| `LIGHT_DIRECTIONAL`, `LIGHT_POINT`, `LIGHT_SPOT` | Argumente für `lightNew`.
|
|
| `BODY_STATIC`, `BODY_DYNAMIC`, `BODY_KINEMATIC` | Körpertypen für `bodyNew`.
|
|
| `SHAPE_BOX`, `SHAPE_SPHERE`, `SHAPE_CAPSULE`, `SHAPE_CYLINDER`, `SHAPE_HULL`, `SHAPE_MESH` | Kollisionsformen für `bodyNew`.
|
|
| `JOINT_HINGE`, `JOINT_BALL`, `JOINT_SLIDER` | Gelenktypen (die `joint*`-Aufrufe nennen sie direkt).
|
|
| `DEBUG_NONE`, `DEBUG_SHAPES`, `DEBUG_CONSTRAINTS`, `DEBUG_CONTACTS`, `DEBUG_VELOCITIES`, `DEBUG_STATIC`, `DEBUG_ALL` | Was `physicsSetDebug` zeichnet; addiere sie.
|
|
| `PARTICLE_ALPHA`, `PARTICLE_ADD` | Blending für `emitterSetBlend`.
|
|
| `PARTICLE_OVER`, `PARTICLE_UNDER` | Wo 2D-Partikel gezeichnet werden, für `emitterSetLayer`.
|
|
| `COLLIDE_NONE`, `COLLIDE_FLOOR`, `COLLIDE_SCENE` | Wovon Partikel abprallen, für `emitterSetCollide`.
|
|
| `BILLBOARD_NONE`, `BILLBOARD_ALL`, `BILLBOARD_Y` | Wie ein Knoten sich der Kamera zuwendet, für `nodeSetBillboard`.
|
|
| `LAYER_SCENE`, `LAYER_FRONT` | Ob ein Knoten unter dem Overlay oder davor gezeichnet wird, für `nodeSetLayer`.
|
|
| `VEHICLE_CAR`, `VEHICLE_MOTORCYCLE`, `VEHICLE_TANK`, `VEHICLE_BOAT` | Fahrzeugarten für `vehicleNew`.
|
|
| `SOFT_CLOTH`, `SOFT_BODY`, `SOFT_ROPE` | Arten von Soft Bodies für `softNew`.
|
|
| `FILTER_LINEAR`, `FILTER_NEAREST` | Texturabtastung für `materialSetFilter`.
|
|
| `TONEMAP_NEUTRAL`, `TONEMAP_ACES`, `TONEMAP_NONE` | Tonkurven für `sceneSetTonemap`.
|
|
| `SOUND_ERROR_INVALID`, `SOUND_REMOVE_HANDLE` | `-1`, was `soundPlay` zurückgibt, wenn kein Kanal frei ist.
|
|
| `SINGE_VERSION_MAJOR`, `SINGE_VERSION_MINOR`, `SINGE_VERSION_STRING`, `SINGE_FRAMEWORK_VERSION` | Die Engine-Version, als Ganzzahlen, als String (`"v3.00"`) und als die Zahl, die `singeVersion()` zurückgibt.
|
|
| `SINGE_DEAD_ZONE` | Die `DEAD_ZONE` aus `controls.cfg`, in rohen Achseneinheiten.
|
|
| `SINGE_TRIGGER_THRESHOLD` | Die Schwelle der analogen Trigger in denselben rohen Einheiten; `0`, wenn die Trigger auf `SINGE_DEAD_ZONE` liegen, was der Standard ist.
|
|
| `SINGE_LEGACY_SPRITE_ARGS` | Wahr, wenn das Spiel die Sprite-Argumentreihenfolge von 2.10 verlangt hat.
|
|
| `SINGE_DISC` | Wahr, wenn das Spiel eine Laserdisc hat; falsch, wenn die Zeichenfläche die Welt ist.
|
|
| `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` | Aufbau der Eingabecodes für Controller und Maus und wie viele Achsen und Knöpfe ein Pad hat; `Framework.singe` baut daraus die Tabellen `GAMEPAD_N` und `MOUSE_N`, und ein Servicebildschirm geht damit ein Pad durch.
|
|
|
|
| `SINGE_SCALE_MIN`, `SINGE_SCALE_MAX`, `SINGE_SHIFT_MIN`, `SINGE_SHIFT_MAX`, `SINGE_ROTATE_STEP` | Was `vldpSetScale`, `vldpSetShift` und `vldpSetRotate` annehmen. Begrenze auf diese Werte statt auf eigene Kopien: Eine Verschiebung außerhalb des Bereichs beendet das Skript.
|
|
|===
|
|
|
|
`Framework.singe` fügt die Tabellen `SCANCODE` und `MODIFIER` hinzu (die Tasten- und
|
|
Modifikatorwerte von SDL), `GAMEPAD_0` bis `GAMEPAD_3`, `MOUSE_0` bis `MOUSE_3`, die
|
|
Indizes `GAMEPAD_AXIS_*` für `controllerGetAxis` und `DIR`.
|
|
|
|
[#_included_libraries]
|
|
=== Mitgelieferte Bibliotheken
|
|
|
|
Zusätzlich zu den Standardbibliotheken von Lua und der Singe-API stehen die folgenden
|
|
Bibliotheken in Singe-Programmen zur Verfügung, ohne dass zusätzliche Software
|
|
installiert werden muss:
|
|
|
|
[cols="1,3",options="header"]
|
|
|===
|
|
| Bibliothek | Zweck
|
|
| Copas | Asynchrone Netzwerkkommunikation
|
|
| LuaFileSystem | Erweiterte Dateisystemunterstützung
|
|
| json.lua | Kodieren und Dekodieren von JavaScript Object Notation
|
|
| binaryheap | Datenstruktur Binärheap
|
|
| timerwheel | Effiziente Timer-Planung
|
|
| LuaSec | TLS-/SSL-Kommunikation
|
|
| LuaSocket | TCP und UDP
|
|
| LuaRS232 | Zugriff auf serielle RS232-Schnittstellen
|
|
| lsqlite3 | SQLite-Datenbanken, `require("sqlite3")`
|
|
| md5 | `require("md5")` für `sum`, `hexsum`, `exor`, `crypt` und `decrypt`; die PUC-Rio-Bibliothek, die Hypseus mitliefert, deren `sumhexa` sie `hexsum` nennt
|
|
| LuaBitOp | `require("bit")` für die bitweisen Operationen von Lua 5.1, die 5.4 als Operatoren hat
|
|
| lrandom | `require("random")` für einen Mersenne-Twister-Generator, getrennt von `math.random`
|
|
| Lua CJSON | `require("cjson")` zum Kodieren und Dekodieren von JSON, neben dem Modul `json` oben
|
|
| LPeg | `require("lpeg")` für Parsing Expression Grammars
|
|
| middleclass | `require("middleclass")` für Klassen, falls du Entitäten lieber daraus baust als aus Tabellen
|
|
| lume | `require("lume")` für die kleinen Dinge, die jedes Spiel braucht: `lerp`, `clamp`, `round`, `shuffle`, `split`, `serialize`, `weightedchoice`
|
|
| inspect | `require("inspect")`, um eine Tabelle in einer Form auszugeben, die ein Mensch lesen kann, für den Fall, dass `debugPrint` der Debugger ist
|
|
| bump | `require("bump")` für Dinge in Bewegung, die nicht durcheinander hindurchgehen dürfen -- es beantwortet, wo ein bewegtes Ding landet, was <<collide,die collide-Aufrufe>> absichtlich nicht tun
|
|
| RmlUi | Das eigene Objektmodell der GUI, die globale Variable `rmlui` (siehe <<gui,GUI>>)
|
|
|===
|
|
|
|
Ihre Verwendung geht über den Rahmen dieses Dokuments hinaus. Die Bibliotheken md5, LuaBitOp,
|
|
lrandom, Lua CJSON und LPeg sind dabei, weil Hypseus sie anbietet, sodass ein gegen Hypseus geschriebenes Spiel dieselben
|
|
Module unter denselben Namen findet; nichts in Singe braucht sie.
|
|
|
|
[#audiosync]
|
|
=== Audio-Synchronisation
|
|
|
|
Singe zeigt Video nach der Audio-Uhr an: Ein Frame wird gezeigt, wenn der Ton zu hören
|
|
ist, der zu ihm gehört. Die Engine misst beim Start ihren eigenen Audioweg. SDL kann
|
|
nicht melden, wie viel Audio zwischen Mixer und Lautsprecher in der Warteschlange steht,
|
|
also leert Singe das Gerät einmal, bevor irgendetwas Hörbares spielt, und zählt den Schub
|
|
von Puffern, mit dem SDL es wieder auffüllt; dieser Schub ist die Tiefe der
|
|
Warteschlange, und die Uhr zieht sie ab. Starte mit `--program`, um die Messung im Trace
|
|
zu sehen (`Audio device queue: ...`).
|
|
|
|
Was die Engine nicht sehen kann, ist alles hinter dem Audioserver des Betriebssystems:
|
|
ein DAC, ein HDMI-Receiver, eine Bluetooth-Verbindung. Diese fügen eine feste
|
|
Verzögerung hinzu, die von Rechner zu Rechner verschieden ist. Wenn Aktionen auf dem
|
|
Bildschirm sichtbar vor ihrem Ton kommen, übergib die fehlenden Millisekunden mit
|
|
`--audiodelay=MS`, trag `AUDIO_DELAY
|
|
= MS` in `games.dat` ein, oder lass das Spiel den Wert aus einem Service-Menü mit
|
|
<<singesetaudiodelay,singeSetAudioDelay>> setzen. Positive Werte bedeuten, dass der Ton
|
|
zu spät kommt; das Video wird passend zurückgehalten. Ein negativer Wert deckt den
|
|
seltenen Fall ab, dass der Ton zu früh ankommt.
|
|
|
|
[#menucalibration]
|
|
==== Kalibrieren über das Menü
|
|
|
|
Das mitgelieferte Menü hat einen Kalibrierbildschirm für diese nachgelagerte Verzögerung.
|
|
Drück in der Spieleliste die Taste, die auf `INPUT_SERVICE` gelegt ist (standardmäßig die
|
|
Taste `9`), und wähle Audio Delay aus den Service-Tools (siehe
|
|
xref:Manual.adoc#servicetools[Die Service-Tools]). Der Bildschirm ist eine Seite im
|
|
Dokument des Menüs: Er deckt das Menü schwarz ab, zeigt die gerade erprobte Verzögerung
|
|
und die von der Engine gemessene Warteschlange des Geräts, spielt einmal pro Sekunde ein
|
|
Klicken und wird bei jedem Blitz für einen Frame weiß. Stell mit links und rechts
|
|
(10 ms) und hoch und runter (1 ms) nach, bis Blitz und Klicken zusammenfallen, und drück
|
|
dann Knopf 1, um zu speichern. Start setzt auf null zurück, und Knopf 2 (oder noch
|
|
einmal die Servicetaste) bricht ab. Der Wert wird in `machine.cfg` im obersten
|
|
Datenordner gespeichert und von der Engine auf jedes Spiel auf diesem Rechner angewendet, ob
|
|
es aus dem Menü oder von der Befehlszeile gestartet wird; er wird zu einem
|
|
`AUDIO_DELAY` pro Spiel hinzuaddiert.
|
|
Kalibriere neu, wenn du Lautsprecher, Kopfhörer oder Bildschirme wechselst.
|
|
|
|
Der Bildschirm funktioniert, weil das Klicken und der Ton der Disc ein Mixer-Gerät und
|
|
eine Warteschlange teilen: Der Blitz wird um die gemessene Warteschlange plus den
|
|
erprobten Wert nach dem Klicken angesetzt, sodass der erprobte Wert genau dann, wenn
|
|
beide zusammenfallen, die Verzögerung ist, die die Engine nicht sehen kann. Die
|
|
Verzögerung des Bildschirms ist gratis mit eingerechnet. Ein Spiel kann die Werte mit
|
|
<<singegetaudiocalibration,singeGetAudioCalibration>> und
|
|
<<singegetaudiolatency,singeGetAudioLatency>> lesen oder mit
|
|
<<singesetaudiocalibration,singeSetAudioCalibration>> einen eigenen Bildschirm anbieten.
|
|
|
|
Zwei Eigenschaften der Videodatei selbst spielen ebenfalls eine Rolle. AAC-Audio enthält
|
|
Priming-Samples des Encoders, die Decoder laut Container überspringen sollen; Dateien,
|
|
die ohne diese Angabe gemuxt wurden (ein MP4 ohne Edit-Liste, eine Matroska-Datei ohne
|
|
Codec-Verzögerung), spielen ihren Ton etwa 20 Millisekunden zu spät, was `AUDIO_DELAY`
|
|
auffangen kann. Und wenn der erste Videoframe einen späteren Zeitstempel hat als das
|
|
erste Audio-Sample, beachtet Singe die Zeitangaben des Containers, sodass vorauslaufender
|
|
Ton nicht verloren geht.
|
|
|
|
[#formats]
|
|
=== Video-, Audio- und Containerformate
|
|
|
|
Singe dekodiert Video direkt mit den Bibliotheken von FFmpeg und nutzt dabei den
|
|
Hardware-Decoder der Plattform, wenn sie einen für den Codec bietet (VA-API oder VDPAU
|
|
unter Linux, D3D11VA unter Windows, VideoToolbox unter macOS und auf einem ARM-Board die
|
|
V4L2-Decoder -- den Memory-to-Memory-Decoder, der auf einem Pi 4 H.264 abdeckt, den
|
|
zustandslosen Request-API-Decoder, den ein Pi 5 für HEVC nutzt, und den eigenen von
|
|
Rockchip auf einem RK3588), andernfalls seinen eigenen Software-Decoder; der
|
|
Programm-Trace meldet, welcher es ist, und `--softwarevideo` erzwingt Software. Jeder
|
|
Container und Codec, den das mitgelieferte FFmpeg demuxen und dekodieren kann, wird
|
|
abgespielt: MP4, MKV, MPEG-Programmstreams,
|
|
AVI, AV1 und die klassischen Daphne-Elementarstreams `.m2v` mit einer passenden
|
|
`.ogg`-Audiodatei daneben. Jede Audiospur in der Datei steht
|
|
`discSetAudioTrack` / `videoSetAudioTrack` zur Verfügung; alle Spuren müssen dasselbe
|
|
Sample-Format, dieselbe Kanalzahl und dieselbe Rate haben.
|
|
|
|
Eine Laserdisc enthielt Halbbilder im Zeilensprungverfahren, und ein Rip, der sie
|
|
beibehalten hat, zeigt auf jedem progressiven Display Kammartefakte. `--deinterlace`
|
|
entscheidet, was geschieht: `auto`, der Standard, lässt bwdif über die Frames laufen, die
|
|
die Datei als interlaced markiert, und lässt alles andere in Ruhe, sodass eine
|
|
progressive Disc nichts kostet; `on` ist für eine Datei, deren Markierungen falsch sind;
|
|
`off` rührt das Bild nie an. Für jedes Bild, das hineingeht, kommt eines heraus -- eine
|
|
Disc wird über Frame-Nummern angesprochen, also würde ein Deinterlacer, der aus jedem
|
|
Frame zwei Halbbilder macht, jeden Frame im Index verschieben.
|
|
|
|
Wenn ein Video zum ersten Mal geöffnet wird, indiziert Singe es und speichert den Index
|
|
neben den anderen Daten des Spiels (`<name>-<hash>.index`, wobei der Hash aus dem
|
|
vollen Namen des Videos gebildet wird, sodass zwei Videos mit demselben Basisnamen
|
|
getrennte Indizes behalten). Das Indizieren ist ein Durchgang über die Datei ohne
|
|
Dekodieren, ein paar Sekunden selbst für eine Disc in Spielfilmlänge, und geschieht
|
|
erneut, wenn sich das Video ändert. Wenn ein Video geladen wird, meldet Singe im
|
|
Programm-Trace (`--program`) den Abstand seiner Keyframes, mit einer Warnung dort, wenn
|
|
Keyframes mehr als zwei Sekunden auseinanderliegen, weil ein Sprung vom vorigen Keyframe
|
|
aus vorwärts dekodieren muss. Das Dekodieren geschieht in einem eigenen Thread, sodass
|
|
ein langsamer Sprung den vorigen Frame etwas länger zeigt, statt das Spiel anzuhalten.
|
|
|
|
Die Segmente einer Frame-Datei werden geöffnet, wenn die Disc sie erreicht, nicht alle auf
|
|
einmal: das, das gerade spielt, das danach und Platz für einen Sprung zwischen zweien. Eine
|
|
lange Frame-Datei kann Hunderte von Segmenten haben -- ein veröffentlichtes Spiel hat 213 --
|
|
und für jedes davon einen Decoder offen zu halten, kostet Hunderte offener Dateien ohne
|
|
jeden Gewinn. Eine Frame-Datei, die eine Datei nennt, die nicht da ist, meldet das weiterhin
|
|
beim Start.
|
|
|
|
Bei Laserdisc-Material zählen Frame-Genauigkeit und Sprunggeschwindigkeit, nicht
|
|
Kompression. H.264 in MP4 oder MKV mit kurzem Keyframe-Intervall (eine oder zwei
|
|
Sekunden) springt schnell und spielt auf jeder unterstützten Plattform; lange
|
|
Keyframe-Intervalle machen `discSearch` sichtbar langsam, weil der Decoder vom vorigen
|
|
Keyframe aus vorwärtsgehen muss. Standardauflösung (720x480 oder 720x576) ist das
|
|
Optimum. Quellen in hoher Auflösung funktionieren, kosten aber auf Hardware der
|
|
Raspberry-Pi-Klasse entsprechend mehr CPU, und das Overlay hat standardmäßig die halbe
|
|
Videoauflösung, sodass übergroßes Video wenig bringt.
|
|
|
|
Soundeffekte und Musik liest zuerst SDL_mixer und FFmpeg alles, was SDL_mixer ablehnt,
|
|
sodass beide zusammen Folgendes abdecken:
|
|
|
|
* WAV, AIFF, AU und VOC für unkomprimiertes Audio. Kurze unkomprimierte WAV-Dateien
|
|
haben die geringste Latenz und sind das, was ein Schuss sein sollte.
|
|
* Ogg Vorbis, Opus, MP3, FLAC und WavPack.
|
|
* AAC (`.m4a`, `.aac`), ALAC, AC-3, WMA, APE, TTA, Speex und AMR -- alles andere,
|
|
was das mitgelieferte FFmpeg dekodiert.
|
|
* Tracker-Module über libxmp: MOD, S3M, XM und IT sowie rund fünfzig weitere,
|
|
darunter 669, AMF, MED, OKT, PTM, STM und ULT.
|
|
* Chiptunes über game-music-emu: NSF und NSFE, SPC, VGM, GBS, AY, GYM, HES,
|
|
KSS und SAP. Die gzip-komprimierte Form VGZ wird nicht gelesen; entpacke sie vorher zu VGM.
|
|
* MIDI, aus einem SoundFont synthetisiert. Siehe <<midi,MIDI>>.
|
|
|
|
Die Zeile `Audio:` im Kopf des Trace (`--program` oder `--trace`) listet die Decoder
|
|
auf, die der Build tatsächlich enthält.
|
|
|
|
Bilder funktionieren genauso: zuerst SDL_image, FFmpeg für den Rest. Das deckt PNG,
|
|
JPEG, WebP, GIF, BMP, TGA, PCX, PNM, LBM, QOI, XCF, XPM und SVG über SDL_image ab und
|
|
TIFF, OpenEXR, JPEG 2000, DDS, PSD, DPX, AVIF und HEIC über FFmpeg. Ein animiertes GIF
|
|
oder WEBP wird als Sprite-Animation geladen; siehe <<spriteload,`spriteLoad`>>, das
|
|
auch eine Größe zum Rastern eines SVG annimmt. Radiance-`.hdr` und OpenEXR funktionieren
|
|
beide als Himmel (<<scenesetsky,`sceneSetSky`>>) und behalten ihren vollen Umfang.
|
|
|
|
Schriftarten liest FreeType: TrueType, OpenType, WOFF, WOFF2, Type 1, BDF und PCF, mit
|
|
Farbglyphen (als Bitmap wie als Vektor) und HarfBuzz-Shaping für Arabisch, Hebräisch und
|
|
die indischen Schriften.
|
|
|
|
[#reference]
|
|
== Lua-API-Referenz
|
|
|
|
Dieses Kapitel dokumentiert die Lua-API, die die Singe-Engine bereitstellt -- jede
|
|
Funktion, die dein Spielskript aufrufen kann, und jeden Callback, den Singe bei dir
|
|
aufruft. Es richtet sich an Entwickler, die neue Spiele schreiben oder videolastige
|
|
Spiele von anderen Engines portieren.
|
|
|
|
[#conventions]
|
|
=== Konventionen
|
|
|
|
Jeder API-Eintrag unten folgt demselben Aufbau:
|
|
|
|
* *Signatur(en)* in einem Codeblock, mit jeder akzeptierten Form.
|
|
* *Beschreibung* dessen, was die Funktion tut, und aller Nebenwirkungen.
|
|
* *Parameter*, im Fließtext aufgeführt oder als Tabelle, wenn es mehr als zwei sind.
|
|
* *Rückgabe*-Zeile, die den an Lua zurückgegebenen Wert beschreibt (entfällt, wenn die Funktion nichts zurückgibt).
|
|
* *Hinweise*, wenn es Fallstricke gibt, die einen Hinweis wert sind.
|
|
* *Seit* -- die Singe-Version, in der die Funktion zuerst erschienen ist.
|
|
* *Siehe auch* -- verwandte Funktionen, die du wahrscheinlich an derselben Stelle benutzt.
|
|
* *Beispiel* -- ein kurzer, realistischer Ausschnitt in der Form echten Spielcodes.
|
|
|
|
Ein Eintrag, der mit *Hypseus-Erweiterung* beginnt, ist da, damit ein für Hypseus Singe
|
|
geschriebenes Spiel hier unverändert läuft. Dasselbe gilt für eine als die von Hypseus
|
|
gekennzeichnete Argumentform bei einer Funktion, die sonst die von Singe nimmt.
|
|
|
|
IMPORTANT: *Schreib keine neuen Spiele gegen die Hypseus-API.* Diese Einträge gibt es
|
|
aus Gründen der Kompatibilität, nicht als Empfehlung. Mehrere von ihnen antworten mit
|
|
einem festen Wert, weil es das, was sie beschreiben, in Singe nicht gibt, manche werden
|
|
angenommen und ignoriert, und andere tragen Skalen und Argumentreihenfolgen, die von
|
|
einer anderen Engine geerbt sind -- `videoGetVolume()` zählt bis `64`, wo der Rest von
|
|
Singe bis `100` zählt, und die `spriteAnim`-Namen nehmen das Sprite zuletzt, wo jeder
|
|
Sprite-Aufruf von Singe es zuerst nimmt. Jeder Eintrag nennt das Singe-Gegenstück, das
|
|
du stattdessen benutzen sollst. Neuer Code, der bei der Singe-API bleibt, ist kürzer,
|
|
in sich einheitlich und wird nicht überrascht, wenn eine Kompatibilitätsantwort
|
|
konstant bleibt.
|
|
|
|
Ein paar Regeln gelten für die ganze API:
|
|
|
|
* *Farben* haben 8 Bit pro Kanal: Rot, Grün, Blau und Deckkraft sind jeweils Ganzzahlen von `0` bis `255`.
|
|
* *Overlay-Koordinaten* sind in der aktuellen Overlay-Auflösung angegeben (gesetzt mit `overlaySetResolution`), nicht in Bildschirmpixeln. Die Standardauflösung des Overlays ist in jeder Dimension die halbe Videoauflösung; rufe `overlaySetResolution` auf, um sie zu ändern.
|
|
* *Handles*, die `*Load`-Funktionen zurückgeben (Sprites, Sounds, Schriftarten, Videos), sind opake Ganzzahlen. Gib sie unverändert an die anderen Funktionen dieses Namensraums zurück. Rechne nicht mit ihnen.
|
|
* *Ungültige Argumente brechen das Skript ab.* Wird eine Funktion mit der falschen Anzahl von Argumenten, mit falschen Typen oder mit einem Handle aufgerufen, das bereits entladen wurde, beendet Singe das laufende Skript mit einem Fehler -- es gibt nicht `nil` oder einen Fehlercode zurück. Prüfe Benutzereingaben in deinem Skript, bevor du sie an die API übergibst.
|
|
* *Gezeichnet werden muss aus `onOverlayUpdate`.* Aufrufe einer Funktion `overlay*`, `spriteDraw`, `videoDraw` oder `fontPrint` aus anderen Callbacks erscheinen nicht auf dem Bildschirm und können das Overlay beschädigen. Das ist die eine Regel, über die jeder neue Singe-Entwickler stolpert.
|
|
|
|
[#animation]
|
|
=== Animation
|
|
|
|
Diese Aufrufe steuern die Animationen, die ein Modell mitbringt, pro Instanz. Jeder von
|
|
ihnen nimmt den *Wurzelknoten*, den `modelInstance` zurückgibt; andere Knoten lösen einen
|
|
Fehler aus, außer bei `animationIsPlaying`, das `false` antwortet. Animationen sind ab
|
|
`1` in der Reihenfolge nummeriert, in der `modelGetAnimations` sie auflistet, oder so
|
|
benannt, wie die Datei sie benennt. Ein Clip spielt auf der Basisebene, und bis zu vier
|
|
weitere können auf den Ebenen `1` bis `4` darüber spielen; die Aufrufe für Zeit, Pause,
|
|
Fortsetzen und Sprung betreffen nur die Basisebene. Die Zeit wird in Sekunden gemessen,
|
|
läuft mit der Uhr um höchstens eine Zehntelsekunde pro Frame weiter und steht still,
|
|
solange das Spiel pausiert ist. Knotenanimation, Skinning und
|
|
Morph-Target-Gewichte werden alle gesteuert. Siehe den Teil Animation in
|
|
<<scenes3d,3D-Szenen>>.
|
|
|
|
[#animationgettime]
|
|
==== animationGetTime
|
|
|
|
[source,text]
|
|
----
|
|
seconds = animationGetTime(node)
|
|
----
|
|
|
|
Die aktuelle Zeit im Clip der Basisebene, ob er spielt oder pausiert, in den eigenen Sekunden des Clips (ein `speed` von `2` bewegt sie doppelt so schnell). Ein Clip in Schleife springt an seinem Ende auf `0` zurück; einer ohne Schleife bleibt bei seiner Länge stehen. Wurde noch nichts gespielt, ergibt das `0`. Clips auf den oberen Ebenen werden nicht gemeldet. Ein Knoten, der keine Instanzwurzel ist, beendet das Skript.
|
|
|
|
*Rückgabe:* Sekunden als Zahl.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<animationsettime,animationSetTime>>, <<animationisplaying,animationIsPlaying>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Spiel den Schritt jedes Mal einmal, wenn der Laufzyklus seine zwei Aufsetzpunkte passiert.
|
|
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)
|
|
----
|
|
|
|
Ob der Clip der Basisebene auf dieser Instanz weiterläuft. Ein Clip ohne Schleife hört auf zu spielen, wenn er sein Ende erreicht, und `animationPause` und `animationStop` machen dies beide `false`; ein Clip in Schleife bleibt `true`, bis er gestoppt wird. Anders als bei den übrigen Aufrufen hier beendet ein Knoten, der keine Instanzwurzel ist, nicht das Skript, sondern bekommt `false` als Antwort.
|
|
|
|
*Rückgabe:* Boolean.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<animationplay,animationPlay>>, <<animationpause,animationPause>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Zurück zum Leerlauf, sobald der einmalige Angriff abgelaufen ist.
|
|
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)
|
|
----
|
|
|
|
Friert den Clip der Basisebene bei seiner aktuellen Zeit ein; die Knoten halten diese Pose, bis `animationResume` ihn fortsetzt oder ein anderer Clip gespielt wird. Clips auf den oberen Ebenen laufen weiter. Eine Instanz zu pausieren, auf der nichts spielt, ist harmlos. Ein Knoten, der keine Instanzwurzel ist, beendet das Skript.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<animationresume,animationResume>>, <<animationsettime,animationSetTime>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Zwischensequenz friert die Menge ein, während sich die Kamera bewegt.
|
|
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)
|
|
----
|
|
|
|
Startet einen der Clips des Modells von seinem Anfang an auf der Basisebene dieser Instanz und ersetzt, was dort gespielt hat; die Pose wird sofort aktualisiert. Mit einem `fade` läuft der alte Clip weiter, während er über so viele Sekunden ausgeblendet wird, und wenn nichts gespielt hat, beginnt die Überblendung bei der Pose, in der die Knoten gerade sind (eine eben abgeschaltete Ragdoll, eine per Skript gesetzte Pose), wobei Knoten, die der neue Clip nicht animiert, sanft in die Ruhepose des Modells zurückgleiten. Ein Knoten, der keine Instanzwurzel ist, ein Name, den das Modell nicht hat, oder eine Zahl außerhalb von `1` bis zur Anzahl der Clips beendet das Skript.
|
|
|
|
*Parameter:*
|
|
|
|
* `node` -- der Wurzelknoten der Instanz aus `modelInstance`.
|
|
* `nameOrIndex` -- ein Clipname, wie ihn die Datei speichert, oder seine Nummer ab `1` in der Reihenfolge von `modelGetAnimations`.
|
|
* `loop` -- Boolean, Standard `true`. Bei `false` bleibt der Clip bei seinem letzten Keyframe stehen, und `animationIsPlaying` wird `false`.
|
|
* `speed` -- Abspielrate, Standard `1`. `2` ist doppelt so schnell; `0.5` ist halb so schnell.
|
|
* `fade` -- Sekunden für die Überblendung von dem, was gespielt hat, Standard `0` (ein harter Wechsel).
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<modelgetanimations,modelGetAnimations>>, <<animationplaylayer,animationPlayLayer>>, <<animationstop,animationStop>>, <<animationsettime,animationSetTime>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Wache, die untätig herumsteht, bis der Spieler entdeckt wird, und dann rennt.
|
|
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)
|
|
----
|
|
|
|
Spielt einen Clip auf einer der vier Ebenen über der Basis, über alles darunter geblendet nach dem Gewicht der Ebene (`animationSetLayerWeight`, Standard `1`) und auf einen Teil des Skeletts beschränkt, wenn eine Maske gesetzt ist (`animationSetLayerMask`). Höhere Ebenen werden über niedrigere geblendet; jede Ebene führt ihren eigenen Clip, ihre eigene Zeit und Schleife unabhängig von der Basis. `nameOrIndex`, `loop`, `speed` und `fade` bedeuten dasselbe wie bei `animationPlay`, mit einem Unterschied: Ein `fade` auf einer Ebene, die leer war, fährt das Gewicht der Ebene von `0` hoch, statt von einer Pose überzublenden. Eine Ebene außerhalb von `1` bis `4`, ein Knoten, der keine Instanzwurzel ist, oder ein Clip, den das Modell nicht hat, beendet das Skript.
|
|
|
|
*Parameter:*
|
|
|
|
* `node` -- der Wurzelknoten der Instanz.
|
|
* `layer` -- `1` bis `4`.
|
|
* `nameOrIndex` -- ein Clipname oder eine Clipnummer, wie bei `animationPlay`.
|
|
* `loop` -- Standard `true`.
|
|
* `speed` -- Standard `1`.
|
|
* `fade` -- Sekunden, Standard `0`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<animationplay,animationPlay>>, <<animationsetlayermask,animationSetLayerMask>>, <<animationsetlayerweight,animationSetLayerWeight>>, <<animationstop,animationStop>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Mit dem ganzen Körper gehen, sich nur mit dem Kopf umsehen.
|
|
animationPlay(fox, "Walk", true)
|
|
animationPlayLayer(fox, 1, "Survey", true, 1, 0.5)
|
|
animationSetLayerMask(fox, 1, nodeFind("b_Neck_04", fox))
|
|
|
|
-- Später mit einem Arm winken, egal, was die Beine gerade tun.
|
|
function onGreet()
|
|
animationPlayLayer(fox, 2, "Wave", false, 1, 0.2)
|
|
end
|
|
----
|
|
|
|
[#animationresume]
|
|
==== animationResume
|
|
|
|
[source,text]
|
|
----
|
|
animationResume(node)
|
|
----
|
|
|
|
Setzt den Clip der Basisebene dort fort, wo `animationPause` ihn eingefroren hat, oder dort, wohin `animationSetTime` ihn inzwischen bewegt hat. Einen Clip fortzusetzen, der bereits spielt, ändert nichts. Eine Instanz ohne Clip auf ihrer Basisebene (nie gespielt oder gestoppt) beendet das Skript, ebenso ein Knoten, der keine Instanzwurzel ist.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<animationpause,animationPause>>, <<animationsettime,animationSetTime>>
|
|
|
|
.Beispiel
|
|
[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)
|
|
----
|
|
|
|
Beschränkt eine Ebene auf `maskNode` und alles darunter, sodass der Clip auf dieser Ebene nur diesen Teil des Modells bewegt und der Rest weiter den Ebenen darunter folgt. `maskNode` muss einer der eigenen Knoten der Instanz sein, gefunden mit `nodeFind(name, node)`; die Instanzwurzel selbst zählt nicht. `maskNode` wegzulassen oder `nil` zu übergeben hebt die Beschränkung auf. Die Maske bleibt über spätere Aufrufe von `animationPlayLayer` hinweg auf der Ebene. Eine Ebene außerhalb von `1` bis `4` beendet das Skript, ebenso ein Knoten, der keine Instanzwurzel ist, oder ein `maskNode`, der nicht zu dieser Instanz gehört.
|
|
|
|
*Parameter:*
|
|
|
|
* `node` -- der Wurzelknoten der Instanz.
|
|
* `layer` -- `1` bis `4`.
|
|
* `maskNode` -- ein Gelenk oder Knoten der Instanz; weggelassen oder `nil` für das ganze Modell.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<animationplaylayer,animationPlayLayer>>, <<nodefind,nodeFind>>, <<animationsetlayerweight,animationSetLayerWeight>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Den Oberkörper auf das Ziel richten, während die Beine weitergehen.
|
|
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)
|
|
----
|
|
|
|
Wie stark der Clip einer Ebene über dem durchscheint, was darunter liegt, `0` (gar nicht) bis `1` (voll, der Standard); Werte außerhalb dieses Bereichs werden begrenzt. Das Gewicht gehört zur Ebene, nicht zum Clip, also übersteht es `animationPlayLayer` und `animationStop` auf dieser Ebene. Ändere es jeden Frame ein wenig, um eine Ebene von Hand ein- oder auszublenden, oder lass einen `fade` bei `animationPlayLayer` das einmal erledigen. Eine Ebene außerhalb von `1` bis `4` oder ein Knoten, der keine Instanzwurzel ist, beendet das Skript.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<animationplaylayer,animationPlayLayer>>, <<animationsetlayermask,animationSetLayerMask>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Hinken wird schlimmer, je weniger Gesundheit bleibt.
|
|
function onHealthChanged(health)
|
|
local limp = 1 - health / maxHealth
|
|
animationSetLayerWeight(hero, 1, limp)
|
|
end
|
|
----
|
|
|
|
[#animationsettime]
|
|
==== animationSetTime
|
|
|
|
[source,text]
|
|
----
|
|
animationSetTime(node, seconds)
|
|
----
|
|
|
|
Lässt den Clip der Basisebene zu einer Zeit in seinen eigenen Sekunden springen; negative Werte werden zu `0`. Die Pose wird sofort aktualisiert, ob der Clip spielt oder nicht, sodass eine pausierte Instanz auf einen genauen Frame gespult werden kann und eine spielende von der neuen Zeit aus weiterläuft. Eine laufende Überblendung auf der Basisebene wird abgebrochen. Eine Instanz ohne Clip auf ihrer Basisebene beendet das Skript, ebenso ein Knoten, der keine Instanzwurzel ist.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<animationgettime,animationGetTime>>, <<animationpause,animationPause>>, <<animationresume,animationResume>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Tür, deren Öffnungsclip einem Hebel folgt, den der Spieler zieht.
|
|
animationPlay(door, "Open", false)
|
|
animationPause(door)
|
|
|
|
function onLeverMoved(fraction)
|
|
animationSetTime(door, fraction * doorClipLength)
|
|
end
|
|
----
|
|
|
|
[#animationstop]
|
|
==== animationStop
|
|
|
|
[source,text]
|
|
----
|
|
animationStop(node)
|
|
animationStop(node, layer)
|
|
----
|
|
|
|
Stoppt und vergisst jeden Clip auf der Instanz oder nur den Clip auf einer Ebene über der Basis (`1` bis `4`); die Basisebene allein lässt sich nicht ohne die Ebenen darüber stoppen. Die Knoten behalten ihre letzte Pose, aus der ein folgendes `animationPlay` mit einem `fade` sanft herausführt. Nachdem alles gestoppt wurde, beenden `animationResume` und `animationSetTime` das Skript, bis wieder etwas gespielt wird. Gewichte und Masken der Ebenen bleiben erhalten. Eine Ebene außerhalb von `1` bis `4` oder ein Knoten, der keine Instanzwurzel ist, beendet das Skript.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<animationplay,animationPlay>>, <<animationplaylayer,animationPlayLayer>>, <<animationpause,animationPause>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Den Körper an die Physik-Ragdoll übergeben: die Pose einfrieren, dann fallen lassen.
|
|
function onKilled()
|
|
animationStop(hero)
|
|
ragdollActivate(hero)
|
|
end
|
|
|
|
-- Das Winken auf Ebene 2 ist fertig; das Gehen darunter läuft weiter.
|
|
function onWaveFinished()
|
|
animationStop(hero, 2)
|
|
end
|
|
----
|
|
|
|
[#bezelref]
|
|
=== Bezel
|
|
|
|
Das Artwork des Automaten und die Arcade-Punktetafel, die dazugehört. `mainBezelLoaded`
|
|
fragt nach dem Artwork, das `--bezel` geladen hat; die `scoreBezel`-Familie steuert die
|
|
Punkteanzeige, ein RmlUi-Dokument, das die Engine mitliefert und das ein Spiel durch eine
|
|
eigene Kopie ersetzen kann, `scoreBezel.rml` neben seinem Skript. Jeder Aufruf hier ist eine Erweiterung von Hypseus Singe,
|
|
beibehalten, damit dessen Spiele unverändert laufen, und jeder davon wird aus Sicht des
|
|
Spielers in xref:Manual.adoc#bezels[Bezels] beschrieben.
|
|
|
|
Die Anzeige startet ausgeschaltet, wie in Hypseus, und nur ein Spiel schaltet sie ein.
|
|
Werte, die ihr übergeben werden, bleiben gespeichert, ob sie angezeigt wird oder nicht,
|
|
sodass ein Spiel sie setzen darf, bevor es die Anzeige einschaltet. Jedes Anzeigefeld hat
|
|
so viele Stellen wie die Punktetafel von Hypseus -- sechs für eine Punktzahl, zwei für die
|
|
Credits, eine für die Zahl der Leben -- und ein Wert wird darin rechtsbündig mit führenden
|
|
Nullen angezeigt; eine Punktzahl mit mehr als sechs Stellen zeigt ihre letzten sechs. Auch
|
|
die negativen Werte von Hypseus werden verstanden: `-2` lässt in jeder Stelle einen Strich
|
|
leuchten, `-4` ein `H` und `-5` ein `A`, und `-1`, `-3` und jede andere negative Zahl
|
|
lassen nichts leuchten.
|
|
|
|
[#mainbezelloaded]
|
|
==== mainBezelLoaded
|
|
|
|
[source,text]
|
|
----
|
|
loaded, id = mainBezelLoaded()
|
|
----
|
|
|
|
Hypseus-Erweiterung. Ob `--bezel` Artwork für den Automaten geladen hat, und die Kennung
|
|
der geladenen Datei. Die Kennung ist der 64-Bit-FNV-1a-Hash der Bytes der Datei, als
|
|
Dezimalstring ausgeschrieben, und genau das meldet auch Hypseus, sodass ein Spiel, das ein
|
|
bestimmtes Bezel an seiner Nummer erkennt, hier dieselbe Datei erkennt. Ist kein Artwork
|
|
geladen, lauten die Antworten `false` und `"0"`.
|
|
|
|
*Rückgabe:* Boolean und die Kennung als String.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<setoverlayontop,setOverlayOnTop>>, xref:Manual.adoc#bezels[Bezels]
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Den Sprite-Rahmen nur zeichnen, wenn der Spieler kein eigenes Artwork hat.
|
|
local loaded, id = mainBezelLoaded()
|
|
if not loaded then
|
|
spriteDraw(frameSprite, 0, 0)
|
|
end
|
|
----
|
|
|
|
[#scorebezelclear]
|
|
==== scoreBezelClear
|
|
|
|
[source,text]
|
|
----
|
|
scoreBezelClear()
|
|
----
|
|
|
|
Hypseus-Erweiterung. Leert jedes Anzeigefeld der Punkteanzeige: Die Credits, beide
|
|
Punktzahlen und beide Lebensanzeigen zeigen gar nichts mehr, nur die abgedunkelten Ziffern
|
|
dahinter bleiben. Die Werte werden nicht vergessen, aber das nächste `scoreBezelCredits`,
|
|
`scoreBezelScore` oder `scoreBezelLives` lässt die Anzeige wieder mit dem leuchten, was sie
|
|
dann enthält. Kann bei ausgeschalteter Anzeige gefahrlos aufgerufen werden.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<scorebezelenable,scoreBezelEnable>>
|
|
|
|
[#scorebezelcredits]
|
|
==== scoreBezelCredits
|
|
|
|
[source,text]
|
|
----
|
|
scoreBezelCredits(credits)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Setzt das Anzeigefeld für die Credits, das zwei Stellen hat. Lässt
|
|
außerdem eine Anzeige wieder leuchten, die `scoreBezelClear` geleert hat.
|
|
|
|
*Parameter:*
|
|
|
|
* `credits` -- Ganzzahl; die letzten zwei Stellen werden angezeigt.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<scorebezelscore,scoreBezelScore>>
|
|
|
|
[#scorebezelenable]
|
|
==== scoreBezelEnable
|
|
|
|
[source,text]
|
|
----
|
|
scoreBezelEnable(enabled)
|
|
scoreBezelEnable(enabled, type)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Blendet die Punkteanzeige ein oder aus. Das erste `true` baut sie
|
|
aus der eigenen `scoreBezel.rml` des Spiels neben seinem Skript, wenn es eine gibt, und
|
|
sonst aus `Singe/scoreBezel.rml`; sie danach aus- und wieder einzublenden kostet nichts und
|
|
behält ihre Werte. Sie startet ausgeblendet, so wie auch Hypseus ausgeliefert wird, sodass
|
|
sich ein Spiel, das dies nie aufruft, genau wie zuvor verhält -- und es sollte
|
|
<<scorebezelgetstate,`scoreBezelGetState`>> fragen, bevor es annimmt, dass es eine hat.
|
|
|
|
Das `type` von Hypseus wählt zwischen dessen fünf Backends für die Punktetafel. Singe hat
|
|
eines, ein RmlUi-Dokument, also wird das Argument geprüft und ignoriert. Ein Rechner ohne
|
|
GPU hat überhaupt keine GUIs; dort kann die Anzeige nicht gebaut werden,
|
|
`scoreBezelGetState` antwortet weiter `false`, und eine Warnung wird einmal ausgegeben. Ein
|
|
erstes Argument, das kein Boolean ist, bricht das Skript ab.
|
|
|
|
*Parameter:*
|
|
|
|
* `enabled` -- Boolean.
|
|
* `type` -- Ganzzahl, angenommen und ignoriert.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<scorebezelgetstate,scoreBezelGetState>>, xref:Manual.adoc#bezels[Bezels]
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Anzeige der Engine, wenn es eine gibt, Sprites, wenn nicht.
|
|
scoreBezelEnable(true, 0)
|
|
if scoreBezelGetState() then
|
|
scoreBezelTwinScoreOn(twoPlayer)
|
|
scoreBezelCredits(credits)
|
|
else
|
|
drawSpriteScoreboard()
|
|
end
|
|
----
|
|
|
|
[#scorebezelgetstate]
|
|
==== scoreBezelGetState
|
|
|
|
[source,text]
|
|
----
|
|
state = scoreBezelGetState()
|
|
----
|
|
|
|
Hypseus-Erweiterung. Ob die Punkteanzeige zu sehen ist. Jedes Hypseus-Spiel fragt dies,
|
|
bevor es die Anzeige benutzt, und zeichnet seine eigene Punktetafel mit Sprites, wenn die
|
|
Antwort `false` ist, was der richtige Ausweg und kein Fehler ist.
|
|
|
|
*Rückgabe:* Boolean.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<scorebezelenable,scoreBezelEnable>>
|
|
|
|
[#scorebezellives]
|
|
==== scoreBezelLives
|
|
|
|
[source,text]
|
|
----
|
|
scoreBezelLives(player, lives)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Setzt das Anzeigefeld für die Leben eines Spielers, das eine Stelle
|
|
hat. `player` ist `1` oder `2`; jede andere Zahl wird ignoriert, ebenso `2`, bis
|
|
<<scorebezeltwinscoreon,`scoreBezelTwinScoreOn(true)`>> die Anzeigefelder des zweiten
|
|
Spielers auf die Anzeige gebracht hat. Lässt außerdem eine Anzeige wieder leuchten, die
|
|
`scoreBezelClear` geleert hat.
|
|
|
|
*Parameter:*
|
|
|
|
* `player` -- `1` oder `2`.
|
|
* `lives` -- Ganzzahl; die letzte Stelle wird angezeigt.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<scorebezelscore,scoreBezelScore>>
|
|
|
|
[#scorebezelscore]
|
|
==== scoreBezelScore
|
|
|
|
[source,text]
|
|
----
|
|
scoreBezelScore(player, score)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Setzt das Anzeigefeld für die Punktzahl eines Spielers, das sechs
|
|
Stellen hat. `player` ist `1` oder `2`, mit derselben Regel für `2` wie bei
|
|
`scoreBezelLives`. Lässt außerdem eine Anzeige wieder leuchten, die `scoreBezelClear`
|
|
geleert hat.
|
|
|
|
*Parameter:*
|
|
|
|
* `player` -- `1` oder `2`.
|
|
* `score` -- Ganzzahl; die letzten sechs Stellen werden angezeigt.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<scorebezellives,scoreBezelLives>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
function addScore(points)
|
|
score = score + points
|
|
if scoreBezelGetState() then
|
|
scoreBezelScore(1, score)
|
|
end
|
|
end
|
|
----
|
|
|
|
[#scorebezeltwinscoreon]
|
|
==== scoreBezelTwinScoreOn
|
|
|
|
[source,text]
|
|
----
|
|
scoreBezelTwinScoreOn(enabled)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Bringt die Anzeigefelder für Punktzahl und Leben des zweiten Spielers
|
|
auf die Anzeige oder nimmt sie wieder herunter und überlässt den Platz den anderen beiden.
|
|
Sie abzuschalten leert auch die Anzeige, genau wie in Hypseus, also lass einen Wert folgen,
|
|
wenn die Anzeigefelder des ersten Spielers leuchten sollen. Solange sie aus sind, tun
|
|
`scoreBezelScore(2, ...)` und `scoreBezelLives(2, ...)` nichts. Ein Argument, das kein
|
|
Boolean ist, bricht das Skript ab.
|
|
|
|
*Parameter:*
|
|
|
|
* `enabled` -- Boolean.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<scorebezelscore,scoreBezelScore>>
|
|
|
|
[#body]
|
|
=== Körper
|
|
|
|
Ein Körper gibt einem Szenenknoten einen Platz in der Physikwelt. Es gibt einen Körper pro Knoten, angesprochen über das Knoten-Handle: Ein statischer Körper bleibt, wo der Knoten war, als der Körper erzeugt wurde, ein kinematischer Körper folgt bei jedem Schritt seinem Knoten und schiebt, ohne geschoben zu werden, und ein dynamischer Körper wird von der Simulation bewegt und treibt seinen Knoten. Größen, Positionen und Geschwindigkeiten sind in Welteinheiten angegeben (Meter in einer 3D-Szene, Overlay-Pixel in einer 2D-Welt), Massen in Kilogramm und Zeit in Sekunden. Jeder Aufruf hier beendet das Skript, wenn der Knoten keinen Körper hat; ein Aufruf, der nur für eine Art von Körper sinnvoll ist (etwa eine Kraft auf einen statischen Körper), wird bei den anderen ignoriert, außer `bodySetMass`, das einen Fehler auslöst. Wie Körper, Knoten und der Frame zusammenpassen, steht unter <<physics,Physik>> in Spieleentwicklung.
|
|
|
|
[#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)
|
|
----
|
|
|
|
Gibt dem Knoten einen Körper des angegebenen Typs, dort platziert, wo der Knoten gerade in der Welt ist (seine Eltern eingeschlossen), mit einer Kollisionsform, die durch `a`, `b` und `c` in Welteinheiten bemessen und mit der Weltskalierung des Knotens skaliert wird. Ein zweiter Aufruf auf demselben Knoten ersetzt den ersten, und ein Knoten, der eine Spielfigur hatte, verliert sie. Die Masse eines dynamischen Körpers ergibt sich aus dem Volumen seiner Form bei der Dichte von Wasser, also setze `bodySetMass` bei allem, was sich leicht anfühlen soll. Die Skalierung des Knotens später zu ändern, ändert die Größe des Körpers nicht. Beendet das Skript bei einem unbekannten Typ oder einer unbekannten Form, wenn auf diesem Rechner keine Physik verfügbar ist, wenn die Welt bereits `4096` Körper enthält oder wenn die Form nicht gebaut werden kann (ein dynamisches `SHAPE_MESH` oder eine Hülle ohne Mesh unter dem Knoten).
|
|
|
|
*Parameter:*
|
|
|
|
* `type` -- `BODY_STATIC`, `BODY_DYNAMIC` oder `BODY_KINEMATIC`.
|
|
* `shape` -- `SHAPE_BOX` (`a`, `b`, `c` sind Breite, Höhe und Tiefe), `SHAPE_SPHERE` (`a` ist der Radius), `SHAPE_CAPSULE` oder `SHAPE_CYLINDER` (`a` ist der Radius, `b` die Gesamthöhe entlang Y, Kappen eingeschlossen), `SHAPE_HULL` (eine konvexe Hülle um das Mesh des Knotens und alles darunter, keine Größen) oder `SHAPE_MESH` (die exakten Dreiecke, nur für statische und kinematische Körper; ein Heightmap-Mesh wird zu einem Höhenfeld).
|
|
* `a`, `b`, `c` -- die Größen, die die Form braucht; fehlende sind `0`, und jede Größe wird auf mindestens `0.001` angehoben.
|
|
|
|
*Hinweise:* Eine Kugel wird mit der größten Skalierungsachse des Knotens skaliert, der Radius einer Kapsel oder eines Zylinders mit der größeren von X und Z. Eine Kapsel, die kürzer als ihr doppelter Radius ist, ist eine Kugel mit einem schmalen Stück in der Mitte.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<bodysetmass,bodySetMass>>, <<bodydelete,bodyDelete>>, <<physicssetgravity,physicsSetGravity>>, <<playernew,playerNew>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine leichte Kiste und ein Entenmodell als Hülle, auf den Boden fallen gelassen.
|
|
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)
|
|
----
|
|
|
|
Übt auf einen dynamischen Körper eine Kraft in Newton aus, nur für die Schritte dieses Frames; ruf es jeden Frame auf, um weiter zu schieben. Ohne Punkt wirkt die Kraft durch den Schwerpunkt und bewegt den Körper nur; mit einem Weltpunkt `(px, py, pz)` dreht sie den Körper außerdem um seinen Mittelpunkt. Eine Kraft beschleunigt um Kraft geteilt durch Masse, also bemiss sie nach `bodySetMass`. Statische und kinematische Körper ignorieren sie.
|
|
|
|
*Parameter:*
|
|
|
|
* `fx`, `fy`, `fz` -- die Kraft in Newton entlang jeder Weltachse.
|
|
* `px`, `py`, `pz` -- optionaler Weltpunkt, an dem die Kraft angreift; weglassen für den Schwerpunkt.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<bodyapplyimpulse,bodyApplyImpulse>>, <<bodysetmass,bodySetMass>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Ventilator bläst den Ballon durch den Raum, solange der Schalter an ist.
|
|
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)
|
|
----
|
|
|
|
Ändert den Impuls eines dynamischen Körpers auf einen Schlag: Ein Impuls von Masse mal Geschwindigkeit fügt diese Geschwindigkeit hinzu. Ohne Punkt wirkt er durch den Schwerpunkt; mit einem Weltpunkt `(px, py, pz)` dreht er den Körper außerdem, sodass ein Treffer nahe der Oberkante einer Kiste sie umkippt. Das ist der Aufruf für einen Treffer, einen Tritt, eine Explosion oder einen Abschuss, während `bodyApplyForce` für ein Schieben ist, das anhält. Statische und kinematische Körper ignorieren ihn.
|
|
|
|
*Parameter:*
|
|
|
|
* `ix`, `iy`, `iz` -- der Impuls in Kilogramm mal Meter pro Sekunde entlang jeder Weltachse.
|
|
* `px`, `py`, `pz` -- optionaler Weltpunkt, an dem der Impuls angreift; weglassen für den Schwerpunkt.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<bodyapplyforce,bodyApplyForce>>, <<bodysetvelocity,bodySetVelocity>>, <<physicsraycast,physicsRaycast>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Schnipp, was unter der Maus ist, senkrecht nach oben, an der Stelle, an der es getroffen wurde.
|
|
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)
|
|
----
|
|
|
|
Entfernt den Körper aus der Welt; der Knoten und sein Mesh bleiben, wo sie sind. Jedes Gelenk, das am Körper hängt, und jedes darauf gebaute Fahrzeug verschwindet mit ihm. Den Knoten zu löschen entfernt auch seinen Körper, also wird dies nur gebraucht, um einen Knoten ohne seinen Körper zu behalten. Beendet das Skript, wenn der Knoten keinen Körper hat.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<bodynew,bodyNew>>, <<bodysetenabled,bodySetEnabled>>, <<jointdelete,jointDelete>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine eingesammelte Münze ist nicht mehr fest und schwebt als einfacher Knoten nach oben.
|
|
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)
|
|
----
|
|
|
|
Wie schnell sich der Körper um jede Weltachse dreht, in Radiant pro Sekunde, beim letzten Schritt. Ein statischer Körper meldet null. Nützlich für die Tonhöhe eines Rads oder einen Drehzähler.
|
|
|
|
*Rückgabe:* Drei Zahlen, Radiant pro Sekunde um X, Y und Z.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<bodysetangularvelocity,bodySetAngularVelocity>>, <<bodygetvelocity,bodyGetVelocity>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Der Roulettekessel steht, wenn er sich kaum noch dreht.
|
|
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)
|
|
----
|
|
|
|
Wie schnell sich der Körper entlang jeder Weltachse bewegt, in Welteinheiten pro Sekunde, beim letzten Schritt. Ein statischer Körper meldet null; ein kinematischer Körper meldet die Geschwindigkeit, die sich aus der Bewegung seines Knotens ergab.
|
|
|
|
*Rückgabe:* Drei Zahlen, Einheiten pro Sekunde entlang X, Y und Z.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<bodysetvelocity,bodySetVelocity>>, <<bodygetangularvelocity,bodyGetAngularVelocity>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Zischen abspielen, wenn der Ball richtig in Fahrt ist.
|
|
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)
|
|
----
|
|
|
|
Ob die Simulation den Körper schlafen gelegt hat, weil er sich nicht mehr bewegt. Statische Körper und mit `bodySetEnabled` aus der Welt genommene Körper gelten als ruhend. Ein ruhender Körper wacht von selbst auf, wenn ihn etwas berührt oder wenn eine Geschwindigkeit, eine Kraft oder ein Impuls angewendet wird.
|
|
|
|
*Rückgabe:* Boolean.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<bodysetenabled,bodySetEnabled>>, <<bodysetvelocity,bodySetVelocity>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Runde endet, sobald jeder Kegel zur Ruhe gekommen ist.
|
|
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)
|
|
----
|
|
|
|
Setzt die Drehung um jede Weltachse, in Radiant pro Sekunde, und weckt den Körper. Statische Körper ignorieren es. Die Drehung eines kinematischen Körpers wird beim nächsten Schritt von seinem Knoten überschrieben, also wirkt der Aufruf nur bei dynamischen Körpern fort.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<bodygetangularvelocity,bodyGetAngularVelocity>>, <<bodysetvelocity,bodySetVelocity>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Münze werfen: nach oben und sich dabei überschlagend.
|
|
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)
|
|
----
|
|
|
|
Setzt die Restitution des Körpers, von `0` (bleibt sofort liegen) bis `1` (prallt mit allem zurück, womit er ankam); Werte außerhalb dieses Bereichs werden begrenzt. Der Standard ist `0.1`. Die Werte der beiden sich berührenden Körper werden kombiniert, sodass ein lebhafter Ball auf einem dämpfenden Boden trotzdem springt.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<bodysetfriction,bodySetFriction>>, <<bodynew,bodyNew>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Gummiball, in die Szene fallen gelassen.
|
|
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)
|
|
----
|
|
|
|
Wie der Körper schwimmt, wenn er in einem Wasservolumen ist: `1` ist neutral, mehr schwimmt höher, weniger sinkt, und `0` ist ein Stein. Der Standard ist `1.2`, das sanft schwimmt. Der Faktor multipliziert die eigene Dichte des Wassers aus `bodySetWater`, und negative Werte werden auf `0` begrenzt. Nur dynamische Körper werden von Wasser bewegt, aber der Wert kann auf jedem Körper gesetzt werden.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<bodysetwater,bodySetWater>>, <<bodysetcurrent,bodySetCurrent>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Kiste, die hoch im Wasser liegt, ein Anker, der untergeht, ein Ball, der auf und ab tanzt.
|
|
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)
|
|
----
|
|
|
|
Setzt die Strömung in einem Wasservolumen, in Welteinheiten pro Sekunde entlang jeder Weltachse; dynamische Körper, die darin schwimmen, treiben mit der Strömung, und eine schwimmende Spielfigur wird von ihr mitgetragen. Der Standard ist keine Strömung. Setze sie auf dem Knoten, auf dem `bodySetWater` aufgerufen wurde; auf jedem anderen Körper wird sie gespeichert und bewirkt nichts.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<bodysetwater,bodySetWater>>, <<bodysetbuoyancy,bodySetBuoyancy>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Fluss, der alles, was hineinfällt, flussabwärts trägt.
|
|
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)
|
|
----
|
|
|
|
Nimmt den Körper mit `false` aus der Welt: Er kollidiert nicht mehr, bewegt sich nicht mehr, treibt seinen Knoten weder an noch folgt er ihm, und er gilt als ruhend. `true` setzt ihn dort wieder ein, wo er ist, und weckt ihn. Der Körper behält Form, Masse und Einstellungen, also ist das billiger, als ihn für etwas, das kommt und geht, zu löschen und neu zu erzeugen.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<bodyisresting,bodyIsResting>>, <<bodydelete,bodyDelete>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Zugbrücke ist nur fest, solange sie unten ist.
|
|
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)
|
|
----
|
|
|
|
Setzt die Oberflächenreibung: `0` ist Eis, `0.5` (der Standard) ist gewöhnlich, höhere Werte greifen stärker. Negative Werte werden auf `0` begrenzt. Die Werte der beiden sich berührenden Körper werden kombiniert, sodass ein rutschiger Boden einen niedrigen Wert auf dem Boden braucht, nicht auf allem, was darauf läuft.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<bodysetbounce,bodySetBounce>>, <<bodynew,bodyNew>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein zugefrorener Teich mitten im Level.
|
|
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)
|
|
----
|
|
|
|
Skaliert die Masse eines dynamischen Körpers und mit ihr seine Trägheit neu. Tu das bei allem, was sich leicht oder schwer anfühlen soll: Der Standard ist das Volumen der Form bei der Dichte von Wasser, sodass ein türgroßer Quader etwa 140 Kilogramm wiegt und ein Stups ihn nicht bewegt. Kräfte und Impulse skalieren mit der Masse, also setze die Masse, bevor du sie abstimmst. Beendet das Skript, wenn der Körper nicht dynamisch oder die Masse nicht positiv ist.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<bodynew,bodyNew>>, <<bodyapplyimpulse,bodyApplyImpulse>>, <<playersetpush,playerSetPush>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Kiste, die der Held schieben kann, und ein Amboss, den niemand schieben kann.
|
|
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)
|
|
----
|
|
|
|
Macht den Körper mit `true` zum Trigger: Er schiebt nichts und hält nichts auf, aber `onTrigger` meldet jeden Körper und jede Spielfigur, die ihn betreten und verlassen. `false` macht ihn wieder fest: Was in ihm war, wird ohne Meldung des Verlassens vergessen, während ein Trigger, in dem der Körper selbst steht, ihn weiter meldet. Ein Trigger ist meist ein statischer Quader oder eine statische Kugel über einer Tür, einer Ziellinie oder einer Gefahrenzone. Strahlen aus `physicsRaycast` treffen Trigger wie jeden anderen Körper.
|
|
|
|
*Hinweise:* Eine Spielfigur gilt als in einem Trigger, wenn sich ihre Bounding Boxes überlappen, also ist ein runder Trigger für Spielfiguren ein eckiger.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<ontrigger,onTrigger>>, <<bodynew,bodyNew>>, <<bodysetwater,bodySetWater>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Ziellinie quer über die Strecke.
|
|
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)
|
|
----
|
|
|
|
Setzt die Geschwindigkeit des Körpers entlang jeder Weltachse, in Welteinheiten pro Sekunde, und weckt ihn. Das ersetzt jede Geschwindigkeit, die der Körper hatte, während `bodyApplyImpulse` etwas hinzufügt. Statische Körper ignorieren es, und die Geschwindigkeit eines kinematischen Körpers wird beim nächsten Schritt aus der Bewegung seines Knotens überschrieben.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<bodygetvelocity,bodyGetVelocity>>, <<bodyapplyimpulse,bodyApplyImpulse>>, <<bodysetangularvelocity,bodySetAngularVelocity>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Den Ball zu der Seite hin aufschlagen, die den letzten Punkt verloren hat.
|
|
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)
|
|
----
|
|
|
|
Füllt einen statischen Körper mit Wasser und macht ihn zum Trigger: Die Oberseite seiner Form ist die Oberfläche, und jeder dynamische Körper darin schwimmt, sinkt oder rollt durch den Auftrieb gegen diese Oberfläche (der eingetauchte Teil der Form zählt, sodass eine schräge Kiste sich aufrichtet), gebremst durch die Widerstände. Eine Spielfigur, deren Mitte unter der Oberfläche liegt, schwimmt. Weil das Volumen ein Trigger ist, feuert `onTrigger` beim Hinein- und Hinausgehen. Das Wasser wird nicht gezeichnet: Gib dem Knoten ein Material mit Blending oder keines. Beendet das Skript, wenn der Körper nicht statisch ist.
|
|
|
|
*Parameter:*
|
|
|
|
* `density` -- ein Faktor, `1` für Wasser; das eigene `bodySetBuoyancy` jedes Körpers skaliert ihn. Negative Werte werden auf `0` begrenzt.
|
|
* `linearDrag` -- wie schnell das Wasser Bewegung durch sich hindurch bremst; Standard `0.5`.
|
|
* `angularDrag` -- wie schnell es Drehung bremst; Standard `0.1`.
|
|
|
|
*Hinweise:* Spielfiguren und Boote sehen höchstens acht Wasservolumen; weitere lassen Körper trotzdem schwimmen, aber in ihnen wird nicht geschwommen.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<bodysetbuoyancy,bodySetBuoyancy>>, <<bodysetcurrent,bodySetCurrent>>, <<bodysettrigger,bodySetTrigger>>, <<playerisswimming,playerIsSwimming>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein ins Deck eingelassenes Becken; seine Oberseite bei Y = 0 ist die Oberfläche.
|
|
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]
|
|
=== Kamera
|
|
|
|
Die Kamera ist ein Knoten wie jeder andere: Platziere sie mit `nodeSetPosition`, richte sie mit `nodeLookAt` aus, häng sie für eine Verfolgeransicht an ein bewegtes Objekt und benenne sie mit `cameraSet`. Sie blickt entlang ihrer eigenen -Z-Richtung. Ohne Kameraknoten wird die Szene von `(0, 0, 5)` aus mit Blick auf den Ursprung betrachtet. Die Projektion wird getrennt gesetzt und gilt für den Knoten, der gerade die Kamera ist: standardmäßig perspektivisch mit einem vertikalen Sichtfeld von 60 Grad von `0.1` bis `1000` Einheiten oder orthografisch mit `cameraSetOrthographic`. Gerenderte Ansichten (`viewNew`) teilen die Projektion, blicken aber von ihren eigenen Knoten aus. Siehe <<scenes3d,3D-Szenen>>.
|
|
|
|
[#cameraset]
|
|
==== cameraSet
|
|
|
|
[source,text]
|
|
----
|
|
cameraSet(node)
|
|
cameraSet(-1)
|
|
----
|
|
|
|
Macht einen Knoten zur Kamera: Die Szene wird von seiner Weltposition aus mit Blick entlang seiner eigenen -Z-Richtung gezeichnet, sodass Bewegung und Drehung seiner Eltern zählen. Jeder Knoten geht, auch einer, der zugleich ein Mesh oder ein Licht trägt, und die Kamera zu wechseln ist ein einziger Aufruf. `-1` stellt die Standardansicht von `(0, 0, 5)` mit Blick auf den Ursprung wieder her. Wird der Kameraknoten gelöscht, fällt die Ansicht ebenfalls auf die Standardansicht zurück. Beendet das Skript bei einem ungültigen Knoten-Handle.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<nodelookat,nodeLookAt>>, <<camerasetperspective,cameraSetPerspective>>, <<viewsetcamera,viewSetCamera>>, <<nodenew,nodeNew>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Verfolgerkamera hinter dem Auto des Spielers und eine feste für die Wiederholung.
|
|
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)
|
|
----
|
|
|
|
Schaltet auf eine Parallelprojektion um, die `height` Welteinheiten von oben bis unten zeigt, wobei die Breite dem Seitenverhältnis der Ebene folgt, von `near` bis `far` vor der Kamera. Entfernung lässt Dinge nicht schrumpfen, was zu Diagrammen, isometrischen und seitlichen Ansichten und HUD-artigen Szenen passt. Die Einstellung gilt für den aktuellen und jeden späteren Kameraknoten, bis `cameraSetPerspective` aufgerufen wird. Der Schatten eines gerichteten Lichts benutzt unter einer orthografischen Kamera eine einzige Shadow Map, egal, was `sceneSetShadowCascades` sagt.
|
|
|
|
*Parameter:*
|
|
|
|
* `height` -- die sichtbare Höhe, in Welteinheiten.
|
|
* `near` -- die kleinste gezeichnete Entfernung, in Welteinheiten vor der Kamera.
|
|
* `far` -- die größte gezeichnete Entfernung.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<camerasetperspective,cameraSetPerspective>>, <<cameraset,cameraSet>>, <<scenesetshadowcascades,sceneSetShadowCascades>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine isometrische Ansicht des Spielbretts: 12 Einheiten hoch, von weit oben und seitlich.
|
|
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)
|
|
----
|
|
|
|
Schaltet auf die übliche perspektivische Projektion um, den Standard. `fov` ist das vertikale Sichtfeld in Grad (Standard `60`); `near` und `far` (Standards `0.1` und `1000`) begrenzen, was gezeichnet wird, in Welteinheiten vor der Kamera. Die Tiefengenauigkeit verteilt sich zwischen beiden, also halte `near` so groß, wie die Szene es erlaubt, und `far` nicht größer als nötig: `0.05` bis `100` für einen Raum, `0.1` bis `400` für eine Landschaft. Die Einstellung gilt für den aktuellen und jeden späteren Kameraknoten und für gerenderte Ansichten.
|
|
|
|
*Parameter:*
|
|
|
|
* `fov` -- vertikales Sichtfeld in Grad; ein weites `70` wirkt schnell, ein enges `40` wie ein Teleobjektiv.
|
|
* `near` -- die kleinste gezeichnete Entfernung.
|
|
* `far` -- die größte gezeichnete Entfernung.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<camerasetorthographic,cameraSetOrthographic>>, <<cameraset,cameraSet>>, <<sceneproject,sceneProject>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein engeres Objektiv für die Cockpitansicht, der Standard für draußen.
|
|
function setView(inside)
|
|
if inside then
|
|
cameraSet(cockpitCamera)
|
|
cameraSetPerspective(45, 0.05, 300)
|
|
else
|
|
cameraSet(chaseCamera)
|
|
cameraSetPerspective(60, 0.1, 400)
|
|
end
|
|
end
|
|
----
|
|
|
|
[#collide]
|
|
=== Kollision
|
|
|
|
Ob sich zwei flache Dinge berühren. Für Körper beantwortet Jolt das in drei Dimensionen
|
|
(siehe <<body,Körper>>); diese Aufrufe beantworten es für das Overlay, wo die Hitboxen einer
|
|
Lightgun und bei einem 2D-Spiel schlicht alles zu Hause sind.
|
|
|
|
Es sind schlichte Funktionen: keine Handles, kein Zustand, nichts zu erzeugen oder freizugeben und
|
|
nichts, was in jedem Frame aktualisiert werden müsste. Die Koordinaten sind die, in denen du ohnehin
|
|
zeichnest -- meist Overlay-Pixel. Ein Rechteck ist eine Ecke und eine Größe, so wie jeder
|
|
Zeichenaufruf der Engine eines entgegennimmt. Eine Berührung genau an einer Kante zählt als
|
|
Berührung, und ein Rechteck ohne Breite oder Höhe berührt überhaupt nichts.
|
|
|
|
Keine davon bewegt etwas. Nimm `require("bump")` und lass es die Arbeit tun, wenn du nicht nur
|
|
wissen willst, *ob* sich zwei Dinge getroffen haben, sondern *wo das bewegte Objekt am Ende steht*
|
|
-- an einer Wand entlanggleitend, statt tot in ihr stecken zu bleiben; genau dafür wird es
|
|
mitgeliefert.
|
|
|
|
[#collidecircles]
|
|
==== collideCircles
|
|
|
|
[source,text]
|
|
----
|
|
touching = collideCircles(x1, y1, radius1, x2, y2, radius2)
|
|
----
|
|
|
|
Ob sich zwei Kreise überschneiden. Billiger, als es aussieht: Für eine Ja-oder-Nein-Frage wird keine Quadratwurzel gezogen.
|
|
|
|
*Parameter:*
|
|
|
|
* `x1`, `y1`, `radius1` -- Zahlen; der erste Kreis.
|
|
* `x2`, `y2`, `radius2` -- Zahlen; der zweite.
|
|
|
|
*Rückgabe:* `true` oder `false`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<colliderectcircle,collideRectCircle>>
|
|
|
|
[#collidepointcircle]
|
|
==== collidePointCircle
|
|
|
|
[source,text]
|
|
----
|
|
inside = collidePointCircle(pointX, pointY, x, y, radius)
|
|
----
|
|
|
|
Ob ein Punkt innerhalb eines Kreises liegt. Der Rand zählt als innen.
|
|
|
|
*Parameter:*
|
|
|
|
* `pointX`, `pointY` -- Zahlen; der Punkt.
|
|
* `x`, `y`, `radius` -- Zahlen; der Kreis.
|
|
|
|
*Rückgabe:* `true` oder `false`.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#collidepointpolygon]
|
|
==== collidePointPolygon
|
|
|
|
[source,text]
|
|
----
|
|
inside = collidePointPolygon(pointX, pointY, points)
|
|
----
|
|
|
|
Ob ein Punkt innerhalb eines Polygons beliebiger Form liegt, konkave eingeschlossen. Die Tabelle ist eine flache Liste aus `x`, `y`, `x`, `y` -- die Form, die eine Hitbox in den meisten Spielen ohnehin hat -- und der Umlaufsinn spielt keine Rolle. Weniger als drei Ecken enthalten nichts. Höchstens 256 Ecken.
|
|
|
|
*Parameter:*
|
|
|
|
* `pointX`, `pointY` -- Zahlen; der Punkt.
|
|
* `points` -- Tabelle; eine gerade Anzahl von Zahlen, abwechselnd x und y.
|
|
|
|
*Rückgabe:* `true` oder `false`.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Hat der Schuss den Drachen getroffen, dessen Umriss kein Rechteck ist?
|
|
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)
|
|
----
|
|
|
|
Ob ein Punkt innerhalb eines Rechtecks liegt. Der Rand zählt als innen.
|
|
|
|
*Parameter:*
|
|
|
|
* `pointX`, `pointY` -- Zahlen; der Punkt.
|
|
* `x`, `y`, `width`, `height` -- Zahlen; das Rechteck.
|
|
|
|
*Rückgabe:* `true` oder `false`.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Hitbox in der Größe des Sprites, zu dem sie gehört.
|
|
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)
|
|
----
|
|
|
|
Ob sich ein Rechteck und ein Kreis überschneiden; dazu wird der Punkt des Rechtecks gesucht, der dem Mittelpunkt des Kreises am nächsten liegt.
|
|
|
|
*Parameter:*
|
|
|
|
* `x`, `y`, `width`, `height` -- Zahlen; das Rechteck.
|
|
* `circleX`, `circleY`, `radius` -- Zahlen; der Kreis.
|
|
|
|
*Rückgabe:* `true` oder `false`.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#colliderects]
|
|
==== collideRects
|
|
|
|
[source,text]
|
|
----
|
|
touching = collideRects(x1, y1, width1, height1, x2, y2, width2, height2)
|
|
----
|
|
|
|
Ob sich zwei Rechtecke überschneiden. Die Funktion, zu der du am häufigsten greifen wirst.
|
|
|
|
*Parameter:*
|
|
|
|
* `x1`, `y1`, `width1`, `height1` -- Zahlen; das erste Rechteck.
|
|
* `x2`, `y2`, `width2`, `height2` -- Zahlen; das zweite.
|
|
|
|
*Rückgabe:* `true` oder `false`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<collidepointrect,collidePointRect>>
|
|
|
|
[#collidesegments]
|
|
==== collideSegments
|
|
|
|
[source,text]
|
|
----
|
|
crossing = collideSegments(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2)
|
|
----
|
|
|
|
Ob sich zwei Strecken kreuzen: ein Schuss entlang einer Bahn gegen eine Wand oder der Schritt, den ein bewegtes Objekt machen will, gegen die Kante, die es nicht überschreiten darf. Strecken, die aufeinander liegen, ergeben `false`, weil „wo kreuzen sie sich“ dann keine eindeutige Antwort hat.
|
|
|
|
*Parameter:*
|
|
|
|
* `ax1`, `ay1`, `ax2`, `ay2` -- Zahlen; die Enden der ersten Strecke.
|
|
* `bx1`, `by1`, `bx2`, `by2` -- Zahlen; die der zweiten.
|
|
|
|
*Rückgabe:* `true` oder `false`.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#color]
|
|
=== Farbe
|
|
|
|
Die Vordergrund- und die Hintergrundfarbe sind globale Werte der Engine statt Argumente jedes Aufrufs: du setzt sie mit `colorForeground` und `colorBackground`, und jeder spätere Zeichenaufruf liest sie, bis sie sich ändern. Die Vordergrundfarbe verwenden die Formprimitive `overlay*` und TrueType-Text aus `fontPrint` und `fontToSprite`; die Hintergrundfarbe verwenden `overlayClear` und Text in `FONT_QUALITY_SHADED`. `overlayPrint` verwendet keine von beiden. Kanäle sind Ganzzahlen von `0` bis `255`, auf diesen Bereich begrenzt, und beide Farben nehmen ein optionales Alpha an. Die Aufrufe, die sie verwenden, stehen in den Abschnitten <<overlay,Overlay>> und <<font,Schriftart>>.
|
|
|
|
[#colorbackground]
|
|
==== colorBackground
|
|
|
|
[source,text]
|
|
----
|
|
colorBackground(r, g, b)
|
|
colorBackground(r, g, b, a)
|
|
----
|
|
|
|
Setzt die Hintergrundfarbe: womit `overlayClear` das Overlay füllt und was Text in `FONT_QUALITY_SHADED` aus `fontPrint` und `fontToSprite` hinter seine Glyphen malt. Beim Start ist sie transparentes Schwarz, und Alpha ist ohne Angabe `0`, sodass `colorBackground(0, 0, 0)` gefolgt von `overlayClear()` das Overlay wieder unsichtbar macht. Ein teilweises Alpha über Schwarz dunkelt das Video ab; `255` verdeckt es. Kanalwerte außerhalb des Bereichs werden begrenzt statt abgewiesen.
|
|
|
|
*Parameter:*
|
|
|
|
* `r`, `g`, `b` -- Rot, Grün und Blau, `0` bis `255`.
|
|
* `a` -- Deckkraft, `0` (transparent) bis `255` (deckend); Standard `0`.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<colorforeground,colorForeground>>, <<overlayclear,overlayClear>>, <<fontquality,fontQuality>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Deckendes Schwarz hinter dem Kalibrierungsbildschirm, transparent während des Spiels.
|
|
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)
|
|
----
|
|
|
|
Setzt die Vordergrundfarbe, die `overlayBox`, `overlayCircle`, `overlayEllipse`, `overlayLine` und `overlayPlot` verwenden, und die Textfarbe für `fontPrint` und `fontToSprite`. Beim Start ist sie deckendes Weiß, und Alpha ist ohne Angabe `255`, das Gegenteil von `colorBackground`. Sprites und `overlayPrint` werden mit ihren eigenen Pixeln gezeichnet und nicht eingefärbt. Kanalwerte außerhalb des Bereichs werden begrenzt statt abgewiesen.
|
|
|
|
*Parameter:*
|
|
|
|
* `r`, `g`, `b` -- Rot, Grün und Blau, `0` bis `255`.
|
|
* `a` -- Deckkraft, `0` (transparent) bis `255` (deckend); Standard `255`.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<colorbackground,colorBackground>>, <<overlaybox,overlayBox>>, <<fontprint,fontPrint>>, <<fonttosprite,fontToSprite>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Punktestand in Weiß, Warnung beim letzten Leben in Rot, beides aus derselben Schriftart.
|
|
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]
|
|
=== Befehlszeile
|
|
|
|
Drei Aufrufe melden Zahlen, die der Benutzer beim Start von Singe gewählt hat. Es gibt sie, damit
|
|
sich ein Spiel vom Launcher aus abstimmen lässt, statt das Spiel bearbeiten zu müssen. Alle drei
|
|
sind Erweiterungen von Hypseus Singe, und Singe selbst macht nichts mit den Werten: was sie
|
|
bedeuten, entscheidet das Spiel. Die Optionen sind in
|
|
xref:Manual.adoc#commandlineoptions[Befehlszeilenoptionen] beschrieben, und sie lassen sich auch in einer
|
|
Einstellungsdatei setzen (siehe xref:Manual.adoc#settings[Die Einstellungsdatei]).
|
|
|
|
Keiner der drei Werte ändert sich während eines Laufs, lies also jeden einmal und behalte ihn.
|
|
|
|
[#getfvalue]
|
|
==== getFValue
|
|
|
|
[source,text]
|
|
----
|
|
value = getFValue()
|
|
----
|
|
|
|
Hypseus-Erweiterung. Gibt die als `--fvalue` angegebene Zahl zurück oder `0`, wenn die
|
|
Option nicht verwendet wurde. Sie ist ein Kanal für eine einzige Zahl von demjenigen, der Singe
|
|
gestartet hat, zum Spiel, gedacht für einen Wert, den ein Frontend von Start zu Start ändern will
|
|
-- einen Schwierigkeitsgrad, eine Anzahl Start-Credits, einen Menüindex. Die Engine reicht ihn nur
|
|
durch.
|
|
|
|
Der Wert reicht von `0` bis `100000` und behält drei Nachkommastellen, so viele, wie
|
|
Hypseus behält, sodass ein Spiel, das die Zahl vergleicht, dieselbe sieht wie dort.
|
|
|
|
*Rückgabe:* Zahl.
|
|
|
|
*Seit:* 3.00 (Hypseus)
|
|
*Siehe auch:* <<ratiogetx,ratioGetX>>, <<ratiogety,ratioGetY>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Der Launcher übergibt --fvalue 2 für den Automaten auf schwer.
|
|
local difficulty = getFValue()
|
|
if difficulty == 0 then
|
|
difficulty = 1
|
|
end
|
|
----
|
|
|
|
[#ratiogetx]
|
|
==== ratioGetX
|
|
|
|
[source,text]
|
|
----
|
|
factor = ratioGetX()
|
|
----
|
|
|
|
Hypseus-Erweiterung. Gibt die als `--xratio` angegebene Zahl zurück oder `0`, wenn die
|
|
Option nicht verwendet wurde. Für Hypseus geschriebene Lightgun-Spiele lesen sie und strecken
|
|
damit ihre eigenen Lightgun-Koordinaten, für einen Automaten, dessen Bildschirm nicht die Form hat,
|
|
in der das Video gemastert wurde -- `1.33` bildet ein 16:9-Panel auf 4:3-Material ab. Singe
|
|
wendet sie auf nichts an; die Rechnung macht das Spiel, genau wie in Hypseus, und deshalb behandelt
|
|
jedes Spiel, das sie liest, `0` und alles unter `1` als `1`.
|
|
|
|
Der Wert reicht von `0` bis `100` und behält zwei Nachkommastellen, so wie Hypseus ihn behält.
|
|
|
|
*Rückgabe:* Zahl.
|
|
|
|
*Seit:* 3.00 (Hypseus)
|
|
*Siehe auch:* <<ratiogety,ratioGetY>>, <<getfvalue,getFValue>>, <<mousegetposition,mouseGetPosition>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Form der Lightgun-Koordinaten, die dieser Automat will.
|
|
local ratioX = ratioGetX()
|
|
if ratioX <= 1 then
|
|
ratioX = 1
|
|
end
|
|
local offsetX = (ratioX - 1) * (overlayGetWidth() / 2)
|
|
----
|
|
|
|
[#ratiogety]
|
|
==== ratioGetY
|
|
|
|
[source,text]
|
|
----
|
|
factor = ratioGetY()
|
|
----
|
|
|
|
Hypseus-Erweiterung. Die senkrechte Hälfte des Paares: die als `--yratio` angegebene Zahl oder
|
|
`0`, wenn die Option nicht verwendet wurde. Siehe <<ratiogetx,ratioGetX>>.
|
|
|
|
*Rückgabe:* Zahl.
|
|
|
|
*Seit:* 3.00 (Hypseus)
|
|
*Siehe auch:* <<ratiogetx,ratioGetX>>, <<getfvalue,getFValue>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
local ratioY = ratioGetY()
|
|
if ratioY <= 1 then
|
|
ratioY = 1
|
|
end
|
|
----
|
|
|
|
[#controller]
|
|
=== Controller
|
|
|
|
Singe unterstützt bis zu vier Gamecontroller mit den Indizes `0` bis `3`, die beim Start und immer dann automatisch geöffnet werden, wenn einer eingesteckt oder entfernt wird; ein `controllerLoad` gibt es nicht. Analoge Achsen werden über `onControllerMoved` gemeldet (siehe <<enginecallbacks,Engine-Callbacks>>) und mit `controllerGetAxis` abgefragt, im Rohbereich von SDL von `-32768` bis `32767`. Digitale Tasten und jede Richtung jeder Achse sind Schalter wie die Tasten der Tastatur: sie erreichen `onInputPressed` und `onInputReleased`, wenn eine Belegung in `controls.cfg` einen Code aus den Tabellen `GAMEPAD_0` bis `GAMEPAD_3` verwendet (zum Beispiel `INPUT_ACTION_1 = { GAMEPAD_0.BUTTON_A }`), und `controllerGetButton` fragt den aktuellen Zustand einer Taste ab. `SINGE_DEAD_ZONE` aus dem Eintrag `DEAD_ZONE` in `controls.cfg` ist die Schwelle, die die Engine selbst verwendet, bevor eine Achsenrichtung als gedrückter Schalter zählt; behandle Achsenbewegungen darunter als Rauschen. Einzelnen Achsen kann mit `DEAD_ZONES` jeweils eine andere Schwelle gegeben werden und den analogen Triggern mit `TRIGGER_THRESHOLD` eine eigene (zurückgelesen als `SINGE_TRIGGER_THRESHOLD`); siehe xref:Manual.adoc#deadzones[Totzonen und Trigger]. Ein Gerät, das SDL nicht als Gamepad erkennt, wird trotzdem als eines geöffnet, über ein Mapping, das Singe dafür schreibt, sodass ein Arcade-Encoder oder ein Flightstick in diesen Slots wie jedes Pad erscheint -- siehe xref:Manual.adoc#devices[Jedes Gerät ist ein Gamepad].
|
|
|
|
[#controllerdorumble]
|
|
==== controllerDoRumble
|
|
|
|
[source,text]
|
|
----
|
|
controllerDoRumble(strength, duration)
|
|
controllerDoRumble(controller, strength, duration)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Lässt einen Controller mit beiden Motoren in einer von vier Stärken für eine von vier Dauern vibrieren. Die Form mit zwei Argumenten spricht Controller `0` an. Ein Slot ohne Controller tut nichts (Hypseus bricht dort ab, deshalb sichern sich seine Spiele mit `controllerIsValid` ab); eine Stärke oder Dauer außerhalb von `1` bis `4` oder ein Index außerhalb von `0` bis `3` bricht das Skript ab. Ein Controller ohne Vibrationsunterstützung ignoriert den Aufruf.
|
|
|
|
*Parameter:*
|
|
|
|
* `controller` -- Controller-Index, `0` bis `3`.
|
|
* `strength` -- `1` bis `4`, von einem Viertel bis zur vollen Stärke.
|
|
* `duration` -- `1` bis `4`, ergibt 150, 300, 600 oder 1200 ms.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<controllerisvalid,controllerIsValid>>, <<controllergetbutton,controllerGetButton>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein kurzer Stoß bei jedem Treffer, ein langer mit voller Stärke, wenn der Spieler stirbt.
|
|
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)
|
|
----
|
|
|
|
Gibt die Stellung einer analogen Achse zurück, wie die Engine sie zuletzt gesehen hat. Der Wert wird bei jedem Achsenereignis aktualisiert, auch bei Bewegung innerhalb der Totzone, und ist `0` für einen Controller, der nicht verbunden ist, oder eine Achse, die sich nie bewegt hat. Sticks reichen von `-32768` bis `32767`; Trigger ruhen bei `0` und erreichen voll gedrückt `32767`. Ein Index außerhalb von `0` bis `3` oder eine Achse außerhalb von `0` bis `5` bricht das Skript ab.
|
|
|
|
*Parameter:*
|
|
|
|
* `controller` -- Controller-Index, `0` bis `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`) oder `GAMEPAD_AXIS_RIGHT_TRIGGER` (`5`).
|
|
|
|
*Rückgabe:* Ganzzahl, `-32768` bis `32767`.
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<controllergetbutton,controllerGetButton>>, <<oncontrollermoved,onControllerMoved>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Lenken mit dem linken Stick, Gas geben mit dem rechten Trigger.
|
|
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)
|
|
----
|
|
|
|
Gibt zurück, ob eine Taste gerade gehalten wird, direkt vom Controller gelesen statt aus der Ereigniswarteschlange. Verwende es zum Aufladen durch Gedrückthalten und für ähnliche Fälle, in denen `onInputPressed` und `onInputReleased` umständlich sind. Ein Slot ohne Controller ergibt `false`. Der Tastencode muss aus der Tabelle `GAMEPAD_N` desselben Controller-Index stammen; ein Code aus der Tabelle eines anderen Controllers oder ein Achsencode fällt aus dem Tastenbereich und bricht das Skript ab, ebenso ein Index außerhalb von `0` bis `3`.
|
|
|
|
*Parameter:*
|
|
|
|
* `controller` -- Controller-Index, `0` bis `3`.
|
|
* `button` -- ein Wert `BUTTON_*` oder `DPAD_*` aus der passenden Tabelle, etwa `GAMEPAD_0.BUTTON_A.value`.
|
|
|
|
*Rückgabe:* Boolean.
|
|
|
|
*Seit:* 2.10
|
|
*Siehe auch:* <<controllergetaxis,controllerGetAxis>>, <<keyboardisdown,keyboardIsDown>>, <<oninputpressedoninputreleased,onInputPressed>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Aufladen, solange A gehalten wird, feuern beim Loslassen.
|
|
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)
|
|
----
|
|
|
|
Der Name des Geräts in einem Controller-Slot, wie SDL ihn meldet, oder `nil`, wenn der Slot leer ist. Ein Gerät, das SDL nicht erkannt hat, meldet seinen eigenen Namen über das Mapping, das Singe dafür geschrieben hat, sodass ein Service-Bildschirm einen Arcade-Encoder ebenso leicht mit Namen auflisten kann wie ein Markenpad. Ein Index außerhalb von `0` bis `3` bricht das Skript ab.
|
|
|
|
*Parameter:*
|
|
|
|
* `controller` -- Controller-Index, `0` bis `3`.
|
|
|
|
*Rückgabe:* String oder `nil`.
|
|
|
|
*Seit:* 3.00
|
|
*Siehe auch:* <<controllerisvalid,controllerIsValid>>, <<mousegetname,mouseGetName>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Service-Bildschirm, der auflistet, was eingesteckt ist.
|
|
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()
|
|
----
|
|
|
|
Hypseus-Erweiterung, in Singe immer `0`. Hypseus addiert diesen Offset zu einem Schaltercode, der von einem Gamepad kam, damit ein Spiel erkennen kann, welches Gerät was gedrückt hat, und den Schalter trotzdem durch Abziehen zurückgewinnt. Singe liefert Gamepad-Eingaben als gewöhnliche belegte Schalter ohne eine solche Markierung, deshalb antwortet es mit `0`: ein Spiel, das auf den verschobenen Bereich prüft, nimmt seinen anderen Zweig und liest den Schalter so, wie er ist.
|
|
|
|
*Rückgabe:* eine Ganzzahl, immer `0`.
|
|
|
|
*Seit:* 3.00
|
|
*Siehe auch:* <<controllersetpadding,controllerSetPadding>>
|
|
|
|
[#controllerhowmany]
|
|
==== controllerHowMany
|
|
|
|
[source,text]
|
|
----
|
|
count = controllerHowMany()
|
|
----
|
|
|
|
Hypseus-Erweiterung. Wie viele Gamepads geöffnet sind, von den vier Slots, die Singe hat. Da jeder Joystick, den Singe öffnen kann, zum Gamepad wird (siehe xref:Manual.adoc#devices[Jedes Gerät ist ein Gamepad]), werden hier auch ein Arcade-Encoder oder ein Spinner mitgezählt.
|
|
|
|
Die Anzahl ändert sich, wenn Geräte ein- und ausgesteckt werden, lies sie also, wenn du sie brauchst, statt einmal beim Start. Sie sagt nichts darüber, welche Slots besetzt sind: ein Gerät kann Slot 0 leer lassen, während Slot 1 in Gebrauch ist, deshalb ist <<controllerisvalid,`controllerIsValid`>> weiterhin der Weg, nach einem bestimmten Slot zu fragen.
|
|
|
|
*Parameter:* keine.
|
|
|
|
*Rückgabe:* Zahl, `0` bis `4`.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<controllerisvalid,controllerIsValid>>, <<controllergetname,controllerGetName>>, <<mousehowmany,mouseHowMany>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Zwei Spieler nur anbieten, wenn Spieler zwei etwas zum Festhalten hat.
|
|
local twoPlayer = controllerHowMany() >= 2
|
|
----
|
|
|
|
[#controllerisvalid]
|
|
==== controllerIsValid
|
|
|
|
[source,text]
|
|
----
|
|
valid = controllerIsValid(controller)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Gibt zurück, ob gerade ein Controller im Slot geöffnet ist; Controller kommen und gehen beim Ein- und Ausstecken im laufenden Betrieb, frag also vor jedem `controllerDoRumble`. Ein Index außerhalb von `0` bis `3` bricht das Skript ab.
|
|
|
|
*Parameter:*
|
|
|
|
* `controller` -- Controller-Index, `0` bis `3`.
|
|
|
|
*Rückgabe:* Boolean.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<controllerdorumble,controllerDoRumble>>, <<controllergetaxis,controllerGetAxis>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Pad-Hinweise nur zeigen, wenn es ein Pad gibt, auf das sie hinweisen können.
|
|
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)
|
|
----
|
|
|
|
Hypseus-Erweiterung, angenommen und ignoriert. Warum es den Offset, den sie verlangt, in Singe nicht gibt, steht bei <<controllergetpadding,controllerGetPadding>>. Ihn einzuschalten, ohne die Ereignisse zu markieren, wäre schlimmer, als ihn ausgeschaltet zu lassen, weil das Spiel dann einen Offset von Schaltercodes abziehen würde, die nie einen trugen.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00
|
|
*Siehe auch:* <<controllergetpadding,controllerGetPadding>>
|
|
|
|
[#debug]
|
|
=== Debugging
|
|
|
|
Zwei Aufrufe, um zu sehen, was ein Skript tut: `debugPrint` schreibt Text, und `lineDraw` zeichnet für einen Frame eine Linie über die 3D-Szene (`physicsSetDebug` und `navDraw` zeichnen über dieselben Linien). Ausgegebenes geht an die Konsole, von der aus Singe gestartet wurde, ist also während der Entwicklung sichtbar und in einem Automaten harmlos, wo `--noconsole` es stummschaltet. Läuft Singe mit `--trace`, wird außerdem jeder API-Aufruf, auch dieser, mit seinen Argumenten in `trace.txt` im Datenordner geschrieben; so bekommst du eine dauerhafte Aufzeichnung. Die Befehlszeilenoptionen stehen im Kapitel über das Starten von Singe; die Integration von ZeroBrane Studio in `zbstudio/` bietet einen Debugger, wenn Ausgeben nicht reicht.
|
|
|
|
[[traceheader]]
|
|
==== Der Trace-Kopf
|
|
|
|
Ganz gleich, ob `--program` oder `--trace` sie geöffnet hat: `trace.txt` beginnt mit einem
|
|
Block, der den Rechner und den Lauf beschreibt, zwischen zwei Linien, sodass er sich als Ganzes in
|
|
einen Fehlerbericht einfügen lässt:
|
|
|
|
----
|
|
----------------------------------------------------------------
|
|
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` ist der Renderer, der tatsächlich erzeugt wurde, und das GPU-Backend, das er
|
|
gewählt hat, oder ein Hinweis, dass es kein GPU-Gerät gibt und 3D nicht verfügbar ist. `Decoder`
|
|
ist das, was auf diesem Rechner für Video versucht wird; welchen Decoder ein bestimmtes Video
|
|
tatsächlich bekommen hat, hält der Trace beim Öffnen dieses Videos ein paar Zeilen weiter unten
|
|
fest, da ein Codec womöglich keinen Hardwareweg anbietet. `Settings` listet auf, was die
|
|
Einstellungsdatei geliefert hat, `none`, wenn es keine gab. Jedes Spiel in einer Warteschlange
|
|
bekommt seine eigene Datei und seinen eigenen Kopf.
|
|
|
|
[#debugprint]
|
|
==== debugPrint
|
|
|
|
[source,text]
|
|
----
|
|
debugPrint(message)
|
|
----
|
|
|
|
Gibt eine Zeile auf der Konsole (Standardausgabe) aus. Wurde Singe mit `--noconsole` gestartet, wird nichts ausgegeben; mit `--trace` landen der Aufruf und seine Nachricht außerdem in `trace.txt`. Das Argument muss ein String oder eine Zahl sein; alles andere bricht das Skript ab, verpacke Werte also in `tostring` oder verwende `string.format`. Der Aufruf ist billig genug, um im ausgelieferten Code zu bleiben, wird aber nicht gedrosselt, eine Ausgabe in `onOverlayUpdate` läuft also in jedem Frame.
|
|
|
|
*Parameter:*
|
|
|
|
* `message` -- der auszugebende Text; eine Zahl wird umgewandelt.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<singegetticks,singeGetTicks>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Protokollieren, warum der Schuss danebenging, mit dem Frame, in dem es passiert ist.
|
|
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)
|
|
----
|
|
|
|
Zeichnet eine gerade Linie zwischen zwei Punkten in Weltkoordinaten über die Szene, nur für den aktuellen Frame, weiß, sofern keine Farbe angegeben ist. Ruf es in jedem Frame auf, um eine Linie auf dem Bildschirm zu halten, wie beim Overlay. Die Linie wird gegen die Tiefe der Szene getestet, schreibt aber keine Tiefe, versteckt sich also wie alles andere hinter Wänden und verdeckt selbst nie etwas. Solange die Szene deaktiviert ist, wird nichts gezeichnet. Sie ist dafür gedacht, zu sehen, was ein Skript tut (einen Raycast, einen Pfad, eine Bounding Box); `physicsSetDebug` und `navDraw` zeichnen über dieselben Linien.
|
|
|
|
*Parameter:*
|
|
|
|
* `x0, y0, z0` -- der Startpunkt, in Welteinheiten.
|
|
* `x1, y1, z1` -- der Endpunkt, in Welteinheiten.
|
|
* `r, g, b` -- die Linienfarbe, `0` bis `255` je Kanal, begrenzt; weiß, wenn weggelassen.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<physicssetdebug,physicsSetDebug>>, <<navdraw,navDraw>>, <<physicsraycast,physicsRaycast>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Zeigen, wohin der Geschützturm zielt und ob der Schuss treffen würde.
|
|
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]
|
|
=== Disc
|
|
|
|
Die *Disc* ist das Haupt-Laserdisc-Video, das, für dessen Emulation Singe ursprünglich gebaut wurde. Ein Spiel hat höchstens eine Disc, die beim Start aus dem in `games.dat` genannten `VIDEO` oder aus `--disc` / `--framefile` auf der Befehlszeile geöffnet wird; ein Spiel, das keine nennt, läuft ohne Disc (siehe <<withoutadisc,Spiele ohne Disc>>). Alle `disc*`-Funktionen arbeiten mit dieser einen impliziten Disc und nehmen kein Handle. Frame-Nummern sind nullbasierte Positionen in der Videodatei (oder in der zusammengesetzten Frame-Datei) und der Zeitbezug für jede Verzweigungsentscheidung in einem Laserdisc-Spiel. Ohne Disc geben die Abfragefunktionen `0` zurück, `discGetState` meldet `DISC_EJECTED`, und die Transportfunktionen schreiben eine Trace-Zeile und tun nichts. Zusätzliche Clips über der Disc sind Aufgabe der Familie <<video,Video>>.
|
|
|
|
[#discaudio]
|
|
==== discAudio
|
|
|
|
[source,text]
|
|
----
|
|
discAudio(channel, enabled)
|
|
----
|
|
|
|
Schaltet einen Kanal der Tonspur der Disc ein oder aus. Der Kanal wird auf die eingestellte Laserdisc-Lautstärke (`--volume_vldp`, Standard `100`) gesetzt, wenn `enabled` `true` ist, und auf `0`, wenn es `false` ist; der andere Kanal bleibt, wie er ist. Viele Originaltitel hatten Erzählung oder eine Datenspur nur auf einem Kanal, und mit diesem Aufruf bringst du sie zum Schweigen. Ein anderer Kanal als `1` oder `2` beendet das Skript. Ohne Disc tut der Aufruf nichts.
|
|
|
|
*Parameter:*
|
|
|
|
* `channel` -- `1` für den linken Kanal, `2` für den rechten.
|
|
* `enabled` -- Boolean. `true` setzt den Kanal wieder auf die eingestellte Laserdisc-Lautstärke, `false` schaltet ihn stumm.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<discsetaudiotrack,discSetAudioTrack>>, <<discgetaudiotracks,discGetAudioTracks>>, <<videosetvolume,videoSetVolume>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Der rechte Kanal dieser Disc trägt die Datenspur des Arcade-Automaten.
|
|
function startGame()
|
|
discAudio(1, true)
|
|
discAudio(2, false)
|
|
discSkipToFrame(SCENE_FIRST_FRAME)
|
|
end
|
|
----
|
|
|
|
[#discaudiosuffix]
|
|
==== discAudioSuffix
|
|
|
|
[source,text]
|
|
----
|
|
opened = discAudioSuffix(suffix)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Schaltet die Disc auf eine eigene Audiodatei neben ihrem Video um: `<base><suffix>.ogg`, wobei die Basis der Name der Videodatei ohne Endung ist, sodass `movie.m2v` mit `"-es"` die Datei `movie-es.ogg` abspielt. Die Datei wird beim aktuellen Frame geöffnet, und die Wiedergabe richtet sich neu aus, wie bei `discSetAudioTrack`; Lautstärke, Wiedergabezustand und Spurindex bleiben erhalten, sofern die neue Datei so viele Spuren hat. Mit einer Frame-Datei schaltet jedes Segment um. Ein leeres Suffix stellt das Standardaudio wieder her: die `.ogg` neben einer `.m2v` oder den Ton in jedem anderen Container. Gibt `true` zurück, wenn die Datei geöffnet wurde; fehlt sie, läuft das bisherige Audio weiter, nichts wird gemerkt, und `false` kommt zurück (Hypseus hinterlässt in diesem Fall Stille). Das Suffix wird vergessen, wenn das Skript neu geladen wird. Ohne Disc gibt der Aufruf `false` zurück. Verwende ihn für ein Sprachmenü, dessen Spuren als eigene Dateien mitgeliefert werden; Spuren innerhalb einer Datei sind Aufgabe von `discSetAudioTrack`.
|
|
|
|
*Parameter:*
|
|
|
|
* `suffix` -- String, der an den Basisnamen des Videos angehängt wird, etwa `"-es"`; `""` für das Standardaudio.
|
|
|
|
*Rückgabe:* Boolean.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<discsetaudiotrack,discSetAudioTrack>>, <<discgetaudiotracks,discGetAudioTracks>>, <<discaudio,discAudio>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Sprachdateien werden als movie.ogg, movie-es.ogg und movie-fr.ogg mitgeliefert.
|
|
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(...)
|
|
----
|
|
|
|
Tut nichts. Echte Laserdisc-Player konnten mit einer anderen Geschwindigkeit als 1x laufen; der Software-Player von Singe kann das nicht, deshalb nimmt der Aufruf beliebige Argumente an, schreibt eine Trace-Zeile und kehrt zurück. Er bleibt erhalten, damit Skripte von vor 2.00 ohne Änderungen laden.
|
|
|
|
*Hinweise:*
|
|
|
|
WARNING: *Nicht implementiert.* Nur aus Gründen der Abwärtskompatibilität erhalten. Es gibt keinen Ersatz; die Wiedergabegeschwindigkeit ist auf die Bildrate der Videodatei festgelegt.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<discplay,discPlay>>, <<discskiptoframe,discSkipToFrame>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Alte Skripte umrahmten eine Zeitlupen-Wiederholung manchmal mit Geschwindigkeitswechseln.
|
|
-- Die Aufrufe sind heute harmlos, aber die Wiederholung läuft mit normaler Geschwindigkeit.
|
|
discChangeSpeed(0.5)
|
|
discSkipToFrame(REPLAY_FIRST_FRAME)
|
|
discChangeSpeed(1)
|
|
----
|
|
|
|
[#discgetaudiotrack]
|
|
==== discGetAudioTrack
|
|
|
|
[source,text]
|
|
----
|
|
track = discGetAudioTrack()
|
|
----
|
|
|
|
Gibt den nullbasierten Index der Tonspur zurück, die die Disc gerade abspielt. Beim Start wählt die Engine die durch `--audio` oder den Eintrag `AUDIO_TRACK` in `games.dat` genannte Spur (Standard `0`), sofern die Disc so viele Spuren hat. Gibt ohne Disc `0` zurück.
|
|
|
|
*Rückgabe:* ganzzahliger Spurindex.
|
|
|
|
*Seit:* 2.10
|
|
*Siehe auch:* <<discgetaudiotracks,discGetAudioTracks>>, <<discsetaudiotrack,discSetAudioTrack>>, <<discgetlanguage,discGetLanguage>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Bei jedem Druck auf die Sprachtaste zur nächsten Synchronfassung wechseln.
|
|
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()
|
|
----
|
|
|
|
Gibt zurück, wie viele Audiostreams die Videodatei der Disc enthält. Mehrere Streams sind typischerweise alternative Synchronfassungen. Ein stummes Video meldet `0`, ebenso ein Spiel ohne Disc, sichere Schleifen und Modulo-Rechnungen also gegen null ab.
|
|
|
|
*Rückgabe:* ganzzahlige Anzahl, `0` oder mehr.
|
|
|
|
*Seit:* 2.10
|
|
*Siehe auch:* <<discgetaudiotrack,discGetAudioTrack>>, <<discgetlanguage,discGetLanguage>>, <<discsetaudiotrack,discSetAudioTrack>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Aus den Tonspuren der Disc ein Sprachauswahlmenü bauen.
|
|
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)
|
|
----
|
|
|
|
Die Sprache, mit der eine Untertitelspur im eigenen Container der Disc gekennzeichnet ist, als der ISO-639-Code, den die Datei trägt (`eng`, `fra`, `spa`). Eine Spur, die die Datei mit nichts kennzeichnet, ergibt einen leeren String, und das ist kein Fehler -- viele Dateien kennzeichnen gar nichts.
|
|
|
|
*Parameter:*
|
|
|
|
* `track` -- Zahl; `0` bis `discGetSubtitleTracks() - 1`.
|
|
|
|
*Rückgabe:* ein String.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<discgetsubtitletracks,discGetSubtitleTracks>>, <<srtloadtrack,srtLoadTrack>>
|
|
|
|
[#discgetsubtitletracks]
|
|
==== discGetSubtitleTracks
|
|
|
|
[source,text]
|
|
----
|
|
count = discGetSubtitleTracks()
|
|
----
|
|
|
|
Wie viele Untertitelspuren der eigene Container der Disc enthält. `0` ohne Disc und `0` für eine Disc, deren Untertitel als eigene `.srt` mitgeliefert werden -- was die übliche Anordnung ist und wofür <<srtload,`srtLoad`>> da ist.
|
|
|
|
Spuren, die Bilder statt Wörter enthalten (die Arten von DVD und Blu-ray, VobSub und PGS), werden hier mitgezählt, damit die Nummerierung mit dem übereinstimmt, was jeder andere Player zeigt, aber <<srtloadtrack,`srtLoadTrack`>> kann sie nicht lesen: da gibt es nichts zu lesen, nur Bitmaps der Wörter.
|
|
|
|
*Parameter:*
|
|
|
|
* Keine.
|
|
|
|
*Rückgabe:* eine Zahl.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<srtloadtrack,srtLoadTrack>>, <<discgetsubtitlelanguage,discGetSubtitleLanguage>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Sprachen anbieten, die die Disc selbst mitbringt.
|
|
for track = 0, discGetSubtitleTracks() - 1 do
|
|
languages[#languages + 1] = { track = track, code = discGetSubtitleLanguage(track) }
|
|
end
|
|
----
|
|
|
|
[#discgetframe]
|
|
==== discGetFrame
|
|
|
|
[source,text]
|
|
----
|
|
frame = discGetFrame()
|
|
----
|
|
|
|
Gibt den Frame zurück, den die Disc gerade zeigt. Mit einer Frame-Datei ist die Nummer die Position in der zusammengesetzten Folge, Segmentgrenzen sind für das Skript also unsichtbar. Gibt `0` zurück, solange die Disc gestoppt ist, und ohne Disc. Frag ihn in `onOverlayUpdate` ab, um darauf zu reagieren, dass das Video einen Entscheidungspunkt erreicht.
|
|
|
|
*Rückgabe:* ganzzahlige Frame-Nummer.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<discsearch,discSearch>>, <<discskiptoframe,discSkipToFrame>>, <<discgetstate,discGetState>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Attract-Sequenz in einer Schleife abspielen und im Zeitfenster zum Ducken eine Aufforderung zeigen.
|
|
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()
|
|
----
|
|
|
|
Die Anzahl der Frames auf der Disc, also eins mehr als der höchste Frame, den
|
|
<<discsearch,discSearch>> findet. Eine Frame-Datei antwortet für die ganze Disc
|
|
statt für das laufende Segment, die Anzahl ändert sich also nicht, wenn Segmente
|
|
kommen und gehen. Ohne Disc antwortet sie mit `0`.
|
|
|
|
*Rückgabe:* ganzzahlige Anzahl der Frames, `0` ohne Disc.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<discgetframe,discGetFrame>>, <<discsearch,discSearch>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Zur Mitte der Disc springen, wo auch immer die liegt.
|
|
discSearch(math.floor(discGetFrameCount() / 2))
|
|
----
|
|
|
|
[#discgetheight]
|
|
==== discGetHeight
|
|
|
|
[source,text]
|
|
----
|
|
height = discGetHeight()
|
|
----
|
|
|
|
Gibt die Höhe des Disc-Videos in Pixeln zurück. Ohne Disc gibt es die Höhe der Zeichenfläche zurück (`CANVAS_Y` in `games.dat` oder `--canvas`, Standard `480`), es ist also immer die Größe des Hintergrunds, über den das Overlay gestreckt wird. Kombiniere es mit `discGetWidth`, um dem Overlay dieselbe Auflösung wie dem Video zu geben.
|
|
|
|
*Rückgabe:* ganzzahlige Höhe in Pixeln.
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<discgetwidth,discGetWidth>>, <<overlaysetresolution,overlaySetResolution>>, <<vldpgetheight,vldpGetHeight>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- In der nativen Auflösung der Disc zeichnen, damit Pixelprüfungen und Zeichnen ein Raster teilen.
|
|
overlaySetResolution(discGetWidth(), discGetHeight())
|
|
crosshairLimitX = discGetWidth() - 1
|
|
crosshairLimitY = discGetHeight() - 1
|
|
----
|
|
|
|
[#discgetlanguage]
|
|
==== discGetLanguage
|
|
|
|
[source,text]
|
|
----
|
|
code = discGetLanguage(track)
|
|
----
|
|
|
|
Gibt das in der Videodatei gespeicherte Sprach-Tag für eine der Tonspuren der Disc zurück. Das Tag ist ein dreibuchstabiger ISO-639-Code wie `"eng"` oder `"jpn"`. Spuren ohne dreibuchstabiges Tag ergeben `"unk"`, ebenso jede Spur, wenn es keine Disc gibt. Mit Disc beendet eine Spur außerhalb von `0` bis `discGetAudioTracks() - 1` das Skript.
|
|
|
|
*Rückgabe:* String mit dem Sprachcode.
|
|
|
|
*Seit:* 2.10
|
|
*Siehe auch:* <<videogetlanguagedescription,videoGetLanguageDescription>>, <<discgetaudiotracks,discGetAudioTracks>>, <<discsetaudiotrack,discSetAudioTrack>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die japanische Synchronfassung bevorzugen, wenn die Disc eine hat.
|
|
for track = 0, discGetAudioTracks() - 1 do
|
|
if discGetLanguage(track) == "jpn" then
|
|
discSetAudioTrack(track)
|
|
break
|
|
end
|
|
end
|
|
----
|
|
|
|
[#discgetstate]
|
|
==== discGetState
|
|
|
|
[source,text]
|
|
----
|
|
state = discGetState()
|
|
----
|
|
|
|
Gibt den Transportzustand der Disc als eine der Konstanten `DISC_*` zurück. Ein Spiel ohne Disc meldet `DISC_EJECTED`; nach `discStop` meldet die Disc `DISC_STOPPED` bis zum nächsten `discPlay`, `discSearch` oder `discSkipToFrame`; ansonsten ist der Zustand `DISC_PLAYING`, solange das Video vorrückt, und `DISC_PAUSED`, wenn nicht. Einen eigenen Suchzustand gibt es nicht; eine Disc, die gerade springt, meldet `DISC_PAUSED` oder `DISC_PLAYING`, je nachdem, was angefordert wurde.
|
|
|
|
*Rückgabe:* eine der folgenden Konstanten, die die Engine definiert und die jedem Skript zur Verfügung stehen.
|
|
|
|
[cols="1,1,1",options="header"]
|
|
|===
|
|
| Konstante | Wert | Bedeutung
|
|
| `DISC_STOPPED` | `2` | Gestoppt (`discStop` wurde aufgerufen)
|
|
| `DISC_PLAYING` | `3` | Läuft
|
|
| `DISC_PAUSED` | `4` | Pausiert
|
|
| `DISC_EJECTED` | `5` | Das Spiel hat keine Disc (siehe <<withoutadisc,Spiele ohne Disc>>)
|
|
|===
|
|
|
|
*Seit:* 1.x (RDG)
|
|
*Siehe auch:* <<discplay,discPlay>>, <<discpause,discPause>>, <<discstop,discStop>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Pausentaste darf kein Anhalten fortsetzen, das das Story-Skript verlangt hat.
|
|
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()
|
|
----
|
|
|
|
Gibt die Breite des Disc-Videos in Pixeln zurück. Ohne Disc gibt es die Breite der Zeichenfläche zurück (`CANVAS_X` in `games.dat` oder `--canvas`, Standard `720`). Zusammen mit `discGetHeight` ist das die Größe, auf die das Overlay skaliert wird, weshalb das mitgelieferte Menü beide an `overlaySetResolution` übergibt.
|
|
|
|
*Rückgabe:* ganzzahlige Breite in Pixeln.
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<discgetheight,discGetHeight>>, <<overlaysetresolution,overlaySetResolution>>, <<vldpgetwidth,vldpGetWidth>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Titel-Sprite zentrieren, egal wie breit die Disc gerade ist.
|
|
overlaySetResolution(discGetWidth(), discGetHeight())
|
|
titleSprite = spriteLoad("graphics/title.png")
|
|
titleX = (discGetWidth() - spriteGetWidth(titleSprite)) / 2
|
|
----
|
|
|
|
[#discpause]
|
|
==== discPause
|
|
|
|
[source,text]
|
|
----
|
|
discPause()
|
|
----
|
|
|
|
Hält die Wiedergabe beim aktuellen Frame an, der auf dem Bildschirm bleibt. Der Ton der Disc pausiert mit. Der Aufruf wird ignoriert, solange die Disc gestoppt ist, und tut ohne Disc nichts. Fortsetzen mit `discPlay`.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<discplay,discPlay>>, <<discstop,discStop>>, <<singesetpauseflag,singeSetPauseFlag>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Den letzten Frame der Szene halten, während der Spieler eine Tür wählt.
|
|
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)
|
|
----
|
|
|
|
Springt zu `frame`, zeigt ihn und pausiert. Das ist dieselbe Bindung wie `discSearch` unter einem zweiten Namen; die beiden sind in jeder Hinsicht austauschbar, einschließlich der dort beschriebenen Regeln für die Begrenzung. Bevorzuge `discSearch` in neuem Code und behalte `discPauseAtFrame` nur beim Portieren alter Skripte.
|
|
|
|
*Hinweise:*
|
|
|
|
WARNING: *Veralteter Alias.* Verwende <<discsearch,discSearch>> in neuem Code.
|
|
|
|
*Seit:* 1.18
|
|
*Siehe auch:* <<discsearch,discSearch>>, <<discplay,discPlay>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Portierter 1.x-Code: auf dem ersten Frame der Szene parken, bis der Spieler bereit ist.
|
|
function onInputPressed(what)
|
|
if what == SWITCH_START1 then
|
|
discPauseAtFrame(SCENE_FIRST_FRAME)
|
|
waitingForPlayer = true
|
|
end
|
|
end
|
|
----
|
|
|
|
[#discplay]
|
|
==== discPlay
|
|
|
|
[source,text]
|
|
----
|
|
discPlay()
|
|
----
|
|
|
|
Startet die Wiedergabe beim aktuellen Frame oder setzt sie fort und hebt den von `discStop` gesetzten Stoppzustand auf. Der Ton läuft mit dem Video weiter, und die Wiedergabeuhr startet beim aktuellen Frame neu, damit eine lange Pause keinen Sprung verursacht. Tut ohne Disc nichts, außer das Stopp-Flag zu löschen.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<discpause,discPause>>, <<discstop,discStop>>, <<discskiptoframe,discSkipToFrame>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Üblicher Start des Attract-Modus: den ersten Frame anfahren, dann losrollen.
|
|
function attractBegin()
|
|
discSearch(ATTRACT_FIRST_FRAME)
|
|
overlayClear()
|
|
discPlay()
|
|
attractRunning = true
|
|
end
|
|
----
|
|
|
|
[#discsearch]
|
|
==== discSearch
|
|
|
|
[source,text]
|
|
----
|
|
discSearch(frame)
|
|
----
|
|
|
|
Springt mit der Disc zu `frame`, zeigt diesen Frame und pausiert darauf; war die Disc gestoppt, wird der Stoppzustand aufgehoben. Die Disc hält den Frame bis zu `discPlay`, `discSkipToFrame` oder einem anderen Transportaufruf. Ein negativer Frame wird auf den ersten Frame begrenzt und einer hinter dem Ende auf den letzten; mit einer Frame-Datei wird das Segment gewählt, das den Frame enthält, und die Position innerhalb davon begrenzt. Tut ohne Disc nichts.
|
|
|
|
*Parameter:*
|
|
|
|
* `frame` -- Ziel-Frame-Nummer, nullbasiert.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<discskiptoframe,discSkipToFrame>>, <<discplay,discPlay>>, <<discgetframe,discGetFrame>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Der Spieler hat den linken Gang gewählt: die Szene anfahren und auf das Startsignal warten.
|
|
function goLeft()
|
|
discSearch(CORRIDOR_LEFT_FIRST_FRAME)
|
|
soundPlay(SOUND_DOOR)
|
|
waitingForPlayer = true
|
|
end
|
|
----
|
|
|
|
[#discsearchblanking]
|
|
==== discSearchBlanking
|
|
|
|
[source,text]
|
|
----
|
|
discSearchBlanking(...)
|
|
----
|
|
|
|
Tut nichts. Auf Laserdisc-Hardware wählte das, ob der Bildschirm dunkel wurde, während der Player einen Frame suchte; der Software-Player von Singe springt, ohne dunkel zu werden, deshalb nimmt der Aufruf beliebige Argumente an, schreibt eine Trace-Zeile und kehrt zurück.
|
|
|
|
*Hinweise:*
|
|
|
|
WARNING: *Nicht implementiert.* Nur aus Gründen der Abwärtskompatibilität erhalten. Es gibt keinen Ersatz.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<discsearch,discSearch>>, <<discskipblanking,discSkipBlanking>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Skripte von vor 2.00 setzten die Austastmodi oft einmal beim Start. Harmlos.
|
|
discSearchBlanking(false)
|
|
discSkipBlanking(false)
|
|
discSearch(TITLE_FRAME)
|
|
----
|
|
|
|
[#discsetaudiotrack]
|
|
==== discSetAudioTrack
|
|
|
|
[source,text]
|
|
----
|
|
discSetAudioTrack(track)
|
|
----
|
|
|
|
Schaltet die Disc auf eine andere Tonspur um. Bereits eingereihtes Audio der alten Spur wird verworfen, und die Wiedergabe richtet sich neu aus, die Änderung ist also fast sofort zu hören. Mit Disc beendet eine Spur außerhalb von `0` bis `discGetAudioTracks() - 1` das Skript; ohne Disc tut der Aufruf nichts. Verwende ihn, um ein Sprachmenü umzusetzen oder eine zwischen Sitzungen gespeicherte Vorliebe zu beachten.
|
|
|
|
*Parameter:*
|
|
|
|
* `track` -- nullbasierter Index der Tonspur.
|
|
|
|
*Seit:* 2.10
|
|
*Siehe auch:* <<discgetaudiotrack,discGetAudioTrack>>, <<discgetaudiotracks,discGetAudioTracks>>, <<discgetlanguage,discGetLanguage>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Sprache anwenden, die der Spieler im Optionsbildschirm gewählt hat.
|
|
function applyLanguage(choice)
|
|
if choice >= 0 and choice < discGetAudioTracks() then
|
|
discSetAudioTrack(choice)
|
|
settings.language = choice
|
|
saveSettings()
|
|
end
|
|
end
|
|
----
|
|
|
|
[#discsetfps]
|
|
==== discSetFPS
|
|
|
|
[source,text]
|
|
----
|
|
discSetFPS(...)
|
|
----
|
|
|
|
Tut nichts. Die Bildrate stammt aus der Videodatei, deshalb nimmt der Aufruf beliebige Argumente an, schreibt eine Trace-Zeile und kehrt zurück. Nichts im mitgelieferten `Framework.singe` ruft ihn auf; das tun nur Skripte von vor 2.00.
|
|
|
|
*Hinweise:*
|
|
|
|
WARNING: *Nicht implementiert.* Nur aus Gründen der Abwärtskompatibilität erhalten. Die Bildrate wird aus der Videodatei gelesen und kann nicht aus einem Skript gesetzt werden.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<discgetframe,discGetFrame>>, <<discskiptoframe,discSkipToFrame>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein portiertes 1.x-Skript. Der Aufruf wird ignoriert; Bilder pro Sekunde sind eine Eigenschaft der Datei.
|
|
discSetFPS(29.97)
|
|
discSearch(TITLE_FRAME)
|
|
discPlay()
|
|
----
|
|
|
|
[#discskipbackward]
|
|
==== discSkipBackward
|
|
|
|
[source,text]
|
|
----
|
|
discSkipBackward(frames)
|
|
----
|
|
|
|
Springt um `frames` vom aktuellen Frame zurück, ohne den Wiedergabe- oder Pausenzustand zu ändern: eine laufende Disc spielt von der neuen Position aus weiter, eine pausierte bleibt dort pausiert. Der Aufruf wird ignoriert, solange die Disc gestoppt ist, und ohne Disc. Die Differenz wird nicht geprüft; ein negativer Wert springt vorwärts, und eine Differenz, die größer als der aktuelle Frame ist, wird bei Frame `0` begrenzt. Mit einer Frame-Datei gilt die Differenz für die Frame-Nummer, die `discGetFrame` meldet, ein Sprung kann also in ein früheres Segment hinüberreichen.
|
|
|
|
*Parameter:*
|
|
|
|
* `frames` -- Anzahl der Frames, die vom aktuellen Frame abgezogen werden.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<discskipforward,discSkipForward>>, <<discstepbackward,discStepBackward>>, <<discgetframe,discGetFrame>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die letzte Sekunde (30 Frames) wiederholen, wenn der Spieler eine Aufforderung verpasst.
|
|
function missedPrompt()
|
|
lives = lives - 1
|
|
discSkipBackward(30)
|
|
promptShown = false
|
|
end
|
|
----
|
|
|
|
[#discskipblanking]
|
|
==== discSkipBlanking
|
|
|
|
[source,text]
|
|
----
|
|
discSkipBlanking(...)
|
|
----
|
|
|
|
Tut nichts. Früher wählte es auf Laserdisc-Hardware, ob der Bildschirm bei einem Sprung dunkel wurde; der Software-Player von Singe wird nie dunkel, deshalb nimmt der Aufruf beliebige Argumente an, schreibt eine Trace-Zeile und kehrt zurück.
|
|
|
|
*Hinweise:*
|
|
|
|
WARNING: *Nicht implementiert.* Nur aus Gründen der Abwärtskompatibilität erhalten. Es gibt keinen Ersatz.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<discsearchblanking,discSearchBlanking>>, <<discskiptoframe,discSkipToFrame>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Skripte von vor 2.00 setzten die Austastmodi oft einmal beim Start. Harmlos.
|
|
discSearchBlanking(false)
|
|
discSkipBlanking(false)
|
|
discSearch(TITLE_FRAME)
|
|
----
|
|
|
|
[#discskipforward]
|
|
==== discSkipForward
|
|
|
|
[source,text]
|
|
----
|
|
discSkipForward(frames)
|
|
----
|
|
|
|
Springt um `frames` vom aktuellen Frame vorwärts, ohne den Wiedergabe- oder Pausenzustand zu ändern. Das Spiegelbild von `discSkipBackward`: ignoriert, solange gestoppt, und ohne Disc, die Differenz wird nicht geprüft, und ein Ziel hinter dem letzten Frame wird beim letzten Frame begrenzt. Mit einer Frame-Datei gilt die Differenz für die Frame-Nummer, die `discGetFrame` meldet, ein Sprung kann also in ein späteres Segment hinüberreichen.
|
|
|
|
*Parameter:*
|
|
|
|
* `frames` -- Anzahl der Frames, die zum aktuellen Frame addiert werden.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<discskipbackward,discSkipBackward>>, <<discstepforward,discStepForward>>, <<discgetframe,discGetFrame>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Einen wiederkehrenden Spieler das zweisekündige Intro der Szene überspringen lassen.
|
|
function onInputPressed(what)
|
|
if what == SWITCH_BUTTON1 and inSceneIntro then
|
|
discSkipForward(60)
|
|
inSceneIntro = false
|
|
end
|
|
end
|
|
----
|
|
|
|
[#discskiptoframe]
|
|
==== discSkipToFrame
|
|
|
|
[source,text]
|
|
----
|
|
discSkipToFrame(frame)
|
|
----
|
|
|
|
Springt zu `frame` und spielt von dort ab, egal in welchem Zustand die Disc ist; wenn nötig, wird der Stoppzustand aufgehoben. Im Gegensatz dazu springt `discSearch` und pausiert. Ein Frame vor dem Anfang oder hinter dem Ende wird auf den ersten oder letzten Frame begrenzt; mit einer Frame-Datei wird das Segment gewählt, das den Frame enthält, und die Position innerhalb davon begrenzt. Tut ohne Disc nichts, außer das Stopp-Flag zu löschen.
|
|
|
|
*Parameter:*
|
|
|
|
* `frame` -- Ziel-Frame-Nummer, nullbasiert.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<discsearch,discSearch>>, <<discplay,discPlay>>, <<discgetframe,discGetFrame>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Beim erneuten Spielen das Intro überspringen.
|
|
function startGame()
|
|
if hasPlayedBefore then
|
|
discSkipToFrame(GAMEPLAY_FIRST_FRAME)
|
|
else
|
|
discSkipToFrame(INTRO_FIRST_FRAME)
|
|
end
|
|
end
|
|
----
|
|
|
|
[#discstepbackward]
|
|
==== discStepBackward
|
|
|
|
[source,text]
|
|
----
|
|
discStepBackward()
|
|
----
|
|
|
|
Bewegt die Disc genau einen Frame zurück und pausiert darauf, egal in welchem Wiedergabezustand sie vorher war. Ein Schritt bei Frame `0` bleibt auf Frame `0`. Wird wie `discSkipBackward` ignoriert, solange die Disc gestoppt ist. Gedacht für framegenaues Debugging und Service-Bildschirme. Tut ohne Disc nichts.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<discstepforward,discStepForward>>, <<discskipbackward,discSkipBackward>>, <<discgetframe,discGetFrame>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Service-Bildschirm: links und rechts schieben die Disc jeweils um einen Frame.
|
|
function onInputPressed(what)
|
|
if what == SWITCH_LEFT then
|
|
discStepBackward()
|
|
elseif what == SWITCH_RIGHT then
|
|
discStepForward()
|
|
end
|
|
end
|
|
----
|
|
|
|
[#discstepforward]
|
|
==== discStepForward
|
|
|
|
[source,text]
|
|
----
|
|
discStepForward()
|
|
----
|
|
|
|
Bewegt die Disc genau einen Frame vorwärts und pausiert darauf, egal in welchem Wiedergabezustand sie vorher war. Ein Schritt auf dem letzten Frame bleibt auf dem letzten Frame. Wie `discStepBackward` wird der Aufruf ignoriert, solange die Disc gestoppt ist. Tut ohne Disc nichts.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<discstepbackward,discStepBackward>>, <<discskipforward,discSkipForward>>, <<discgetframe,discGetFrame>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Frame-Finder: Schritt für Schritt durch eine Szene gehen und die Frame-Nummer ausgeben.
|
|
function onOverlayUpdate()
|
|
overlayClear()
|
|
overlayPrint(8, 8, "Frame " .. discGetFrame())
|
|
if stepping then
|
|
discStepForward()
|
|
end
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#discstop]
|
|
==== discStop
|
|
|
|
[source,text]
|
|
----
|
|
discStop()
|
|
----
|
|
|
|
Stoppt die Disc. Die Wiedergabe hält an, das Video wird durch den klassischen blauen Bildschirm ersetzt, `discGetState` meldet `DISC_STOPPED`, und `discGetFrame` gibt `0` zurück, bis die Disc wieder gestartet wird. Während sie gestoppt ist, werden `discPause`, `discSkipForward`, `discSkipBackward`, `discStepForward` und `discStepBackward` ignoriert. Jeder der Aufrufe `discPlay`, `discSearch` oder `discSkipToFrame` hebt den Stoppzustand auf. Ein zweites `discStop` wird ignoriert.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<discplay,discPlay>>, <<discpause,discPause>>, <<discgetstate,discGetState>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Game Over: auf den blauen Bildschirm hinter der Punktetabelle zurückfallen.
|
|
function gameOver()
|
|
discStop()
|
|
soundPlay(SOUND_GAME_OVER)
|
|
showScores = true
|
|
end
|
|
----
|
|
|
|
[#emitter]
|
|
=== Emitter
|
|
|
|
Emitter erzeugen, bewegen und zeichnen Partikel; siehe <<particles,Partikel>>. Handles
|
|
sind Ganzzahlen. Ein 2D-Emitter lebt in Overlay-Koordinaten, Y nach unten, und zeichnet,
|
|
wenn `emitterDraw` es aus `onOverlayUpdate` verlangt; ein 3D-Emitter sitzt an einem Knoten,
|
|
lebt in Welteinheiten und zeichnet sich selbst in der Szene. Die Rezeptaufrufe lassen sich
|
|
in beliebiger Reihenfolge machen, vor oder nach dem Start des Emitters, und wirken auf die
|
|
nächsten geborenen Partikel (einige wenige, unten vermerkt, auch auf die lebenden).
|
|
Alles, was als Minimum und Maximum angegeben wird, wird für jedes Partikel zufällig
|
|
dazwischen gezogen. Partikel laufen mit der Uhr weiter, höchstens eine Zehntelsekunde
|
|
pro Frame, und halten an, solange das Spiel pausiert ist.
|
|
|
|
[#emitternew]
|
|
==== emitterNew
|
|
|
|
[source,text]
|
|
----
|
|
emitter = emitterNew()
|
|
emitter = emitterNew(node)
|
|
----
|
|
|
|
Erzeugt einen Emitter. Ohne Argument ist es ein 2D-Emitter an der Overlay-Position `(0, 0)`, der mit `emitterSetPosition` bewegt wird; mit einem Knoten ist es ein 3D-Emitter, dessen Partikel an der Weltposition dieses Knotens geboren werden, und er braucht die 3D-Szene, sodass der Aufruf auf einem Rechner ohne sie das Skript beendet (2D-Emitter funktionieren überall). Ein neuer Emitter strömt noch nicht; ruf `emitterStart` oder `emitterBurst` auf. Wird der Knoten gelöscht, werden seine Emitter mitgelöscht. Standardwerte: 50 Partikel pro Sekunde, die ein bis zwei Sekunden leben, 30 Grad Streuung, Weiß, das zu transparent verblasst, normales Blending, keine Schwerkraft, keine Dämpfung, keine Drehung, kein Radius, keine Textur, kein Schweif und keine Kollision, ein Pool von 1000; ein 2D-Emitter schickt sie mit 50 bis 100 Overlay-Einheiten pro Sekunde den Bildschirm hinauf und lässt sie von 16 auf 4 schrumpfen, ein 3D-Emitter mit 1 bis 2 Welteinheiten pro Sekunde die Y-Achse hinauf, schrumpfend von 0,2 auf 0,05.
|
|
|
|
*Rückgabe:* Das Handle des Emitters.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emitterdelete,emitterDelete>>, <<emitterstart,emitterStart>>, <<emitterburst,emitterBurst>>, <<emitterdraw,emitterDraw>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Abgas vom Triebwerksknoten des Schiffs; das Rezept wird gesetzt, bevor der Strom beginnt.
|
|
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)
|
|
----
|
|
|
|
Entfernt den Emitter, seine lebenden Partikel und seine Kopie der Textur sofort; nichts blendet aus. Verwende stattdessen `emitterStop`, um die letzten Partikel zu Ende leben zu lassen. Ein 3D-Emitter wird auch von selbst entfernt, wenn sein Knoten gelöscht wird. Ein Handle, das kein lebender Emitter ist, beendet das Skript, leere die Variable also danach.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emitternew,emitterNew>>, <<emitterstop,emitterStop>>, <<emitterisactive,emitterIsActive>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Explosion ist vorbei: ihren Emitter freigeben, sobald der letzte Funke erloschen ist.
|
|
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)
|
|
----
|
|
|
|
Das Bild, das jedes Partikel trägt: ein geladenes Sprite oder keines (ein Argument oder `nil`) für die eingebaute weiche Scheibe. Der Emitter behält seine eigene Kopie der Pixel des Sprites, jeden Frame eines animierten Sprites eingeschlossen, das Sprite kann danach also entladen werden. Das Setzen der Textur setzt den Frame-Bereich auf Frame `0` zurück und setzt jedes lebende Partikel auf Frame `0`, ruf `emitterSetFrames` also danach auf, nicht davor. Blending und Tönung wirken auf das Bild wie auf die Scheibe.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emittersetframes,emitterSetFrames>>, <<spriteload,spriteLoad>>, <<emittersetblend,emitterSetBlend>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Schnee: ein Spritesheet mit sechs Flockenformen, eine pro Partikel zufällig gewählt.
|
|
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)
|
|
----
|
|
|
|
Mit einem animierten Sprite als Textur nimmt jedes neue Partikel einen zufälligen Frame von `first` bis `last` und behält ihn sein Leben lang; Frames zählen ab `0`. Die Werte werden auf die Frames begrenzt, die die Textur hat, und `last` wird auf `first` angehoben, wenn es kleiner ist. Standard: nur Frame `0`, auf den auch `emitterSetTexture` zurücksetzt. Mit der eingebauten Scheibe oder einem Sprite aus einem einzigen Frame hat der Bereich keine Wirkung.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emittersettexture,emitterSetTexture>>, <<spriteload,spriteLoad>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Wölkchen-Sheet hat vier weiche Wölkchen, dann vier härtere Trümmerbrocken.
|
|
emitterSetTexture(smoke, puffSheet)
|
|
emitterSetFrames(smoke, 0, 3)
|
|
emitterSetTexture(debris, puffSheet)
|
|
emitterSetFrames(debris, 4, 7)
|
|
----
|
|
|
|
[#emittersetblend]
|
|
==== emitterSetBlend
|
|
|
|
[source,text]
|
|
----
|
|
emitterSetBlend(emitter, mode)
|
|
----
|
|
|
|
Wie sich Partikel mit dem verbinden, was hinter ihnen liegt. `PARTICLE_ALPHA` (Standard) blendet normal, für Rauch, Staub, Regen und Trümmer; `PARTICLE_ADD` addiert Licht, für Feuer, Funken, Leuchten und Magie, und dunkelt nie ab. Wirkt sofort auf die lebenden Partikel. Alles andere als diese beiden Konstanten beendet das Skript.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emittersetcolor,emitterSetColor>>, <<emittersettexture,emitterSetTexture>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Funken leuchten; der Rauch über ihnen nicht.
|
|
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)
|
|
----
|
|
|
|
Nur 2D: wohin `emitterDraw` die Partikel setzt. `PARTICLE_OVER` (Standard) zeichnet über allem auf dem Overlay; `PARTICLE_UNDER` zeichnet unter dem Overlay und über dem Video und der 3D-Szene, für Rauch hinter dem Punktestand. Bei einem 3D-Emitter wird die Einstellung angenommen und ignoriert. Alles andere als diese beiden Konstanten beendet das Skript.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emitterdraw,emitterDraw>>, <<emittersetposition,emitterSetPosition>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Regen fällt hinter dem HUD, die Trefferfunken davor.
|
|
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)
|
|
----
|
|
|
|
Wie viele Partikel pro Sekunde ein gestarteter Emitter erzeugt; Standard `50`, negative Werte werden zu `0`. Bruchteile summieren sich, sodass `0.5` alle zwei Sekunden eines erzeugt. Ändere den Wert während des Stroms, um eine Flamme oder eine Schubdüse zu drosseln. Ein voller Pool (`emitterSetMax`) erzeugt keine weiteren, bis einige sterben, egal bei welcher Rate.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emitterstart,emitterStart>>, <<emitterburst,emitterBurst>>, <<emittersetmax,emitterSetMax>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Der Abgasstrahl der Schubdüse folgt dem Gashebel.
|
|
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)
|
|
----
|
|
|
|
Wie lange jedes neue Partikel lebt, zufällig zwischen den beiden gezogen; Standard `1` bis `2`. Seine Größe und Farbe laufen über diese Zeit von ihrem Anfang zu ihrem Ende, die Lebensdauer bestimmt also das Tempo jedes Ausblendens. Das Minimum wird bei `0.001` oder darüber gehalten, und das Maximum wird auf das Minimum angehoben, wenn es kleiner ist. Lebende Partikel behalten die Lebensdauer, mit der sie geboren wurden.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emittersetsize,emitterSetSize>>, <<emittersetcolor,emitterSetColor>>, <<emittersetrate,emitterSetRate>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Mündungsfeuer: ein Stoß, der in einer Zehntelsekunde vorbei ist.
|
|
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)
|
|
----
|
|
|
|
Nur 3D: blendet ein Partikel über `distance` Welteinheiten aus, wo es auf Geometrie dahinter trifft, sodass ein Rauchwölkchen keine harte Linie zeichnet, wo es eine Wand kreuzt, und ein Springbrunnen in sein Becken eintaucht, statt es zu zerschneiden. `0` (der Standard) schaltet es aus; negative Werte werden zu `0`. Solange irgendein Emitter es verwendet, wird die Tiefe der Szene pro Frame einmal mehr gerendert, ein Raspberry Pi sollte es also sparsam einsetzen. Ein 2D-Emitter speichert den Wert und ignoriert ihn.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emittersetlit,emitterSetLit>>, <<emittersetblend,emitterSetBlend>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Rauch aus der Feuerschale wird weich am Pfosten, an dem er vorbeisteigt.
|
|
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)
|
|
----
|
|
|
|
Anfangsgeschwindigkeit jedes neuen Partikels, zufällig zwischen den beiden gezogen, in Overlay-Einheiten pro Sekunde (2D) oder Welteinheiten pro Sekunde (3D); Standard `50` bis `100` in 2D und `1` bis `2` in 3D. Das Maximum wird auf das Minimum angehoben, wenn es kleiner ist. Die Geschwindigkeit bestimmt, wie weit ein Partikel kommt, bevor Dämpfung, Schwerkraft und seine Lebensdauer übernehmen.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emittersetdirection,emitterSetDirection>>, <<emittersetdrag,emitterSetDrag>>, <<emittersetgravity,emitterSetGravity>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Springbrunnen wirft Tropfen kräftig und gerade; Staub bewegt sich kaum.
|
|
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)
|
|
----
|
|
|
|
Die mittlere Richtung, in die neue Partikel aufbrechen; nur die Richtung zählt, nicht die Länge. 2D-Standard `(0, -1)`, den Bildschirm hinauf, da Y im Overlay nach unten wächst; 3D-Standard `(0, 1, 0)`, die Y-Achse der Welt hinauf. Ein 2D-Emitter ignoriert `z`; ein 3D-Emitter, der zwei Werte bekommt, erhält `z = 0`. Ein Nullvektor wird als `(0, -1, 0)` behandelt. Bei einem 3D-Emitter ist die Richtung in Weltachsen angegeben, nicht in denen des Knotens, dreh sie also selbst, wenn sich der Knoten dreht.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emittersetspread,emitterSetSpread>>, <<emittersetspeed,emitterSetSpeed>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Abgas strömt hinter dem Schiff hinaus, egal wohin es zeigt.
|
|
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)
|
|
----
|
|
|
|
Der halbe Winkel des Kegels (3D) oder Fächers (2D) um die Richtung, in den neue Partikel geworfen werden, `0` bis `180`; `0` ist eine gerade Linie, `180` jede Richtung. Standard `30`; Werte außerhalb des Bereichs werden begrenzt. In 3D werden die Richtungen gleichmäßig über die Kappe des Kegels verteilt, sodass sich ein weiter Kegel nicht zu seiner Achse hin ballt.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emittersetdirection,emitterSetDirection>>, <<emittersetradius,emitterSetRadius>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Feuerwerk fliegt überallhin; die Fackelflamme bleibt eine schmale Säule.
|
|
emitterSetSpread(fireworks, 180)
|
|
emitterSetSpread(flame, 15)
|
|
----
|
|
|
|
[#emittersetgravity]
|
|
==== emitterSetGravity
|
|
|
|
[source,text]
|
|
----
|
|
emitterSetGravity(emitter, x, y)
|
|
emitterSetGravity(emitter, x, y, z)
|
|
----
|
|
|
|
Eine konstante Beschleunigung auf jedes Partikel, in Overlay-Einheiten (2D) oder Welteinheiten (3D) pro Sekunde zum Quadrat: `(0, 500)` zieht 2D-Funken den Bildschirm hinunter, `(0, -20)` lässt 2D-Rauch aufsteigen, `(0, -9.8, 0)` ist die Erde in der Szene. Standard keine. Sie wirkt ab dem nächsten Frame auf die lebenden Partikel. Partikel sind keine Physikkörper; gib ihnen `emitterSetCollide`, damit sie von einem Boden oder der Szene abprallen.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emittersetdrag,emitterSetDrag>>, <<emittersetcollide,emitterSetCollide>>, <<emittersetspeed,emitterSetSpeed>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Funken fallen, Rauch steigt, beides vom selben Schleifer.
|
|
emitterSetGravity(sparks, 0, 500)
|
|
emitterSetGravity(smoke, 0, -20)
|
|
|
|
-- Unter Wasser streben die Blasen zur Oberfläche.
|
|
emitterSetGravity(bubbles, 0, 3, 0)
|
|
----
|
|
|
|
[#emittersetdrag]
|
|
==== emitterSetDrag
|
|
|
|
[source,text]
|
|
----
|
|
emitterSetDrag(emitter, perSecond)
|
|
----
|
|
|
|
Wie schnell Partikel ihre Geschwindigkeit verlieren: in jedem Frame wird die Geschwindigkeit mit `1 - perSecond * dt` multipliziert, nie unter `0`, sodass `1` den größten Teil der Geschwindigkeit innerhalb einer Sekunde nimmt und `0` (der Standard) sie für immer behält. Negative Werte werden zu `0`. Rauch, Glut und Feuerwerk werden langsamer; Funken und Regen nicht. Wirkt ab dem nächsten Frame auf die lebenden Partikel.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emittersetgravity,emitterSetGravity>>, <<emittersetspeed,emitterSetSpeed>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Feuerwerksbombe: schnell hinaus, dann schweben und fallen.
|
|
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)
|
|
----
|
|
|
|
Größe bei der Geburt und beim Tod, in Overlay-Einheiten (2D) oder Welteinheiten (3D), gleichmäßig über die Lebensdauer des Partikels wechselnd; Standard `16` bis `4` in 2D und `0.2` bis `0.05` in 3D. `variation` (`0` bis `1`, Standard `0`) skaliert beide Größen jedes neuen Partikels mit einem einzigen Zufallsfaktor zwischen `1 - variation` und `1 + variation`, sodass ein Wölkchen sein ganzes Leben lang etwas größer oder kleiner ist. Negative Größen werden zu `0`, und `variation` wird begrenzt. Ein texturiertes Partikel wird als Quadrat dieser Größe gezeichnet; ein breiteres Bild wird gestaucht.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emittersetlife,emitterSetLife>>, <<emittersetcolor,emitterSetColor>>, <<emittersetspin,emitterSetSpin>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Rauch wächst, während er dünner wird; Funken schrumpfen zu nichts.
|
|
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)
|
|
----
|
|
|
|
Wovon Partikel abprallen. `COLLIDE_NONE` (Standard) ist nichts; `COLLIDE_FLOOR` ist eine waagerechte Ebene in der Höhe `floor`, unterhalb derer (größeres Y in 2D, kleineres Y in 3D) ein Partikel auf die Ebene zurückgesetzt und reflektiert wird; `COLLIDE_SCENE` ist die Physikwelt, nur für 3D-Emitter, mit einem Raycast pro Partikel entlang seiner Bewegung in jedem Frame, begrenzt auf `200` Raycasts pro Emitter und Frame und dort fortgesetzt, wo sie aufgehört haben, sodass ein großer Emitter die Kosten über ein paar Frames verteilt. Bei einem 2D-Emitter wird `COLLIDE_SCENE` angenommen und tut nichts. Ein anderer Modus als diese drei beendet das Skript.
|
|
|
|
*Parameter:*
|
|
|
|
* `mode` -- `COLLIDE_NONE`, `COLLIDE_FLOOR` oder `COLLIDE_SCENE`.
|
|
* `bounce` -- wie viel Geschwindigkeit einen Aufprall übersteht, `0` bis `1`, Standard `0.5`; begrenzt.
|
|
* `friction` -- wie viel Gleitgeschwindigkeit pro Aufprall verloren geht, `0` bis `1`, Standard `0.2`; begrenzt.
|
|
* `floor` -- Höhe der Bodenebene, in Welteinheiten bei einem 3D-Emitter oder in Overlay-Einheiten bei einem 2D-Emitter, Standard `0`. Wird nur von `COLLIDE_FLOOR` verwendet.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emittersetgravity,emitterSetGravity>>, <<emittersettrail,emitterSetTrail>>, <<emittersetsoftness,emitterSetSoftness>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Funken prallen auf dem Werkstattboden ab (Overlay-Zeile 440) und hinterlassen Streifen.
|
|
sparks = emitterNew()
|
|
emitterSetBlend(sparks, PARTICLE_ADD)
|
|
emitterSetGravity(sparks, 0, 500)
|
|
emitterSetCollide(sparks, COLLIDE_FLOOR, 0.4, 0.2, 440)
|
|
emitterSetTrail(sparks, 6, 3)
|
|
|
|
-- Glut in der Szene prallt von allem ab, was die Physikwelt enthält.
|
|
emitterSetCollide(embers, COLLIDE_SCENE, 0.3, 0.5)
|
|
----
|
|
|
|
[#emittersetcolor]
|
|
==== emitterSetColor
|
|
|
|
[source,text]
|
|
----
|
|
emitterSetColor(emitter, r, g, b, a, r2, g2, b2, a2)
|
|
----
|
|
|
|
Farbe und Deckkraft bei der Geburt (`r, g, b, a`) und beim Tod (`r2, g2, b2, a2`), jeweils `0` bis `255`, gleichmäßig über die Lebensdauer des Partikels wechselnd; Werte außerhalb des Bereichs werden begrenzt. Standard: von Weiß und deckend zu Weiß und transparent. Die Tönung multipliziert die Textur oder die eingebaute Scheibe, ein weißes Bild nimmt die Farbe also genau an. Ein Ein- und Ausblenden braucht zwei Emitter oder eine Textur, in der die Form steckt; ein Rezept fährt eine einzige gerade Rampe.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emittersetblend,emitterSetBlend>>, <<emittersetlife,emitterSetLife>>, <<emittersetsize,emitterSetSize>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Flamme: hellgelb am Fuß, verglimmt zu mattem Rot und ist weg.
|
|
emitterSetColor(flame, 255, 200, 90, 255, 255, 40, 0, 0)
|
|
-- Staubkörnchen blenden aus dem Nichts ein und schweben; ihre Lebensdauer beendet sie.
|
|
emitterSetColor(dust, 255, 240, 200, 0, 255, 240, 200, 160)
|
|
----
|
|
|
|
[#emittersetspin]
|
|
==== emitterSetSpin
|
|
|
|
[source,text]
|
|
----
|
|
emitterSetSpin(emitter, min, max)
|
|
----
|
|
|
|
Drehgeschwindigkeit jedes neuen Partikels in Grad pro Sekunde, zufällig zwischen den beiden gezogen; negative Werte drehen andersherum, und ein Bereich, der `0` überspannt, ergibt beides. Jedes Partikel beginnt mit einem zufälligen Winkel, ob es sich dreht oder nicht. Standard keine. Das Maximum wird auf das Minimum angehoben, wenn es kleiner ist. Die Drehung sieht man an einem texturierten Partikel oder einem Schweif; die eingebaute Scheibe ist rund und sieht in jedem Winkel gleich aus.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emittersettexture,emitterSetTexture>>, <<emittersetsize,emitterSetSize>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Blätter taumeln; Glut dreht sich kaum.
|
|
emitterSetTexture(leaves, leafSprite)
|
|
emitterSetSpin(leaves, -180, 180)
|
|
emitterSetSpin(embers, -20, 20)
|
|
----
|
|
|
|
[#emittersetradius]
|
|
==== emitterSetRadius
|
|
|
|
[source,text]
|
|
----
|
|
emitterSetRadius(emitter, radius)
|
|
----
|
|
|
|
Neue Partikel werden irgendwo innerhalb dieses Abstands vom Ursprung des Emitters geboren, gleichmäßig über eine Scheibe (2D) oder eine Kugel (3D), in Overlay- oder Welteinheiten. Standard `0`, ein Punkt; negative Werte werden zu `0`. Ein Radius füllt ein Volumen, einen Fackelkopf oder ein Stück Staub in der Luft, wo ein Punkt eine Nadel ergäbe.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emittersetspread,emitterSetSpread>>, <<emittersetposition,emitterSetPosition>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Staub in einer raumgroßen Luftkugel, das meiste schon da, wenn das Level beginnt.
|
|
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)
|
|
----
|
|
|
|
Nur 3D: schattiert jedes Partikel nach den Lichtern und dem Himmel der Szene, als wäre es eine kleine Kugel, die zur Kamera zeigt, sodass Rauch die Farbe einer Feuerschale annimmt und Staub in der Sonne glitzert. Standardmäßig aus; Schatten werden nicht berücksichtigt. Mit `PARTICLE_ADD` wird die Beleuchtung trotzdem auf die Tönung angewendet, bevor sie addiert wird. Ein 2D-Emitter speichert den Wert und ignoriert ihn.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emittersetsoftness,emitterSetSoftness>>, <<emittersetblend,emitterSetBlend>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Der Rauch über jeder Feuerschale nimmt ihren Feuerschein an.
|
|
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)
|
|
----
|
|
|
|
Ob lebende Partikel dem Emitter folgen. `false` (Standard) lässt jedes dort, wo es geboren wurde, sodass eine durch einen Raum getragene Fackel Rauch hinter sich lässt; `true` hält die ganze Wolke relativ zum Ursprung des Emitters, sodass sich der Abgasstrahl einer Schubdüse mit dem Schiff bewegt. Das Umschalten überführt die lebenden Partikel ohne Sprung von einem Bezugssystem ins andere. Bei einem 3D-Emitter ist der Ursprung die Weltposition des Knotens; bei einem 2D-Emitter ist es der Punkt von `emitterSetPosition`, eine lokale 2D-Wolke gleitet also ebenfalls mit. Schweife folgen derselben Regel.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emittersetposition,emitterSetPosition>>, <<emittersetradius,emitterSetRadius>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine magische Aura, die die Zauberin beim Gehen umhüllt.
|
|
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)
|
|
----
|
|
|
|
Der Pool: wie viele Partikel gleichzeitig leben dürfen, Standard `1000`. Ein voller Pool erzeugt keine weiteren, weder aus dem Strom noch aus einem Stoß, bis einige sterben. Eine Änderung der Anzahl baut den Pool neu und tötet jedes lebende Partikel, setz sie also vor dem Start. Bemiss den Pool als Rate mal längste Lebensdauer plus den größten Stoß. Eine Anzahl unter `1` beendet das Skript.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emittersetrate,emitterSetRate>>, <<emitterburst,emitterBurst>>, <<emittergetcount,emitterGetCount>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- 300 Tropfen pro Sekunde mit bis zu 1,6 Sekunden Leben brauchen etwa 480; lass Luft.
|
|
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)
|
|
----
|
|
|
|
Nur 2D: wo neue Partikel geboren werden, in Overlay-Koordinaten; Standard `(0, 0)`. Setze die Position in jedem Frame, um einem Sprite oder einem 2D-Physikkörper zu folgen. Lebende Partikel bleiben, wo sie sind, außer `emitterSetLocal` ist eingeschaltet. Ein 3D-Emitter folgt seinem Knoten und beendet hier das Skript; bewege stattdessen den Knoten.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emitterdraw,emitterDraw>>, <<emittersetlocal,emitterSetLocal>>, <<emittersetradius,emitterSetRadius>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Funken fliegen dort, wo das Schleifer-Sprite in diesem Frame ist.
|
|
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)
|
|
----
|
|
|
|
Ein Band hinter jedem Partikel durch seine letzten `length` Positionen, eine pro Frame aufgezeichnet (`2` bis `16`; `0` oder `1` für keines, der Standard), `width` breit in Welt- oder Overlay-Einheiten, in 2D wie in 3D. Das Band trägt die aktuelle Tönung des Partikels, verblasst zu seinem Ende hin zu transparent und wird mit der mittleren Spalte des Partikelbilds texturiert, sodass der weiche Rand der Scheibe zum Rand des Bands wird. Die Werte werden auf den Bereich begrenzt. Eine Änderung der Länge baut den Speicher der Schweife neu, lebende Partikel beginnen ihre Bänder also von vorn. Funken, Glut, Leuchtspurgeschosse, Kometen.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emittersetcollide,emitterSetCollide>>, <<emittersetsize,emitterSetSize>>, <<emittersetlocal,emitterSetLocal>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Leuchtspurgeschosse: winzig, schnell und ganz Streifen.
|
|
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)
|
|
----
|
|
|
|
Beginnt den stetigen Strom mit der eingestellten Rate; von da an erzeugt der Emitter in jedem Frame Partikel bis `emitterStop`. Einen Emitter zu starten, der bereits strömt, ändert nichts. Der Strom braucht keinen Zeichenaufruf, um zu laufen; ein 2D-Emitter, der strömt, aber nicht gezeichnet wird, verbraucht trotzdem seinen Pool.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emitterstop,emitterStop>>, <<emittersetrate,emitterSetRate>>, <<emitterburst,emitterBurst>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Triebwerk zündet, wenn der Spieler Gas gibt, und erlischt, wenn er es wegnimmt.
|
|
function onThrottle(open)
|
|
if open then
|
|
emitterStart(exhaust)
|
|
else
|
|
emitterStop(exhaust)
|
|
end
|
|
end
|
|
----
|
|
|
|
[#emitterstop]
|
|
==== emitterStop
|
|
|
|
[source,text]
|
|
----
|
|
emitterStop(emitter)
|
|
----
|
|
|
|
Beendet den Strom; die lebenden Partikel leben ihr Leben zu Ende, und jeder noch geschuldete Bruchteil eines Partikels wird verworfen. `emitterBurst` funktioniert auch bei einem gestoppten Emitter, und `emitterIsActive` bleibt `true`, bis das letzte Partikel stirbt. Verwende `emitterClear`, um die Partikel außerdem sofort zu entfernen.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emitterclear,emitterClear>>, <<emitterstart,emitterStart>>, <<emitterisactive,emitterIsActive>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Fackel erlischt: die Flamme stoppen, den letzten Rauch davontreiben lassen.
|
|
function onTorchDoused()
|
|
emitterStop(flame)
|
|
emitterStop(smoke)
|
|
soundPlay(hiss)
|
|
end
|
|
----
|
|
|
|
[#emitterburst]
|
|
==== emitterBurst
|
|
|
|
[source,text]
|
|
----
|
|
emitterBurst(emitter, count)
|
|
----
|
|
|
|
Erzeugt `count` Partikel auf einmal nach dem aktuellen Rezept, mit oder ohne Strom: eine Explosion, ein Spritzer, ein Wölkchen. Es werden nur so viele erzeugt, wie im Pool Platz haben; `emitterSetMax` legt den Platz fest. Ein 2D-Emitter stößt sie an seinem Punkt von `emitterSetPosition` aus, ein 3D-Emitter an seinem Knoten, verteilt über `emitterSetRadius`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emitterstart,emitterStart>>, <<emittersetmax,emitterSetMax>>, <<emittersetradius,emitterSetRadius>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Feuerwerk: ungefähr jede Sekunde platzt irgendwo über der Disc eine Bombe.
|
|
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)
|
|
----
|
|
|
|
Tötet jetzt jedes lebende Partikel und vergisst jeden noch geschuldeten Bruchteil eines Partikels. Der Strom wird nicht gestoppt: ein gestarteter Emitter erzeugt im nächsten Frame weiter neue Partikel, ruf also auch `emitterStop` auf, um ihn endgültig zu leeren. Verwende es, wenn eine Szene wechselt und die alten Funken nicht nachglimmen dürfen.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emitterstop,emitterStop>>, <<emitterdelete,emitterDelete>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Level neu starten: die Emitter bleiben, die alten Partikel gehen.
|
|
function resetLevel()
|
|
for _, emitter in ipairs(levelEmitters) do
|
|
emitterStop(emitter)
|
|
emitterClear(emitter)
|
|
end
|
|
placePlayer()
|
|
end
|
|
----
|
|
|
|
[#emitterdraw]
|
|
==== emitterDraw
|
|
|
|
[source,text]
|
|
----
|
|
emitterDraw(emitter)
|
|
----
|
|
|
|
Nur 2D, aus `onOverlayUpdate`: stellt die Partikel des Emitters in die Warteschlange, damit sie in diesem Frame gezeichnet werden, über oder unter dem Overlay je nach `emitterSetLayer`, in Overlay-Koordinaten, die auf das Fenster abgebildet werden. Emitter auf derselben Ebene zeichnen in der Reihenfolge, in der sie eingereiht wurden. Die Warteschlange leert sich in jedem Frame, der Aufruf gehört also in jeden Frame, in dem die Partikel zu sehen sein sollen; einen Emitter nicht zu zeichnen, hält ihn nicht an. Bis zu `64` Emitter pro Ebene werden in einem Frame gezeichnet. Ein 3D-Emitter zeichnet sich selbst in der Szene und beendet hier das Skript.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emittersetlayer,emitterSetLayer>>, <<emittersetposition,emitterSetPosition>>, <<onoverlayupdate,onOverlayUpdate>>
|
|
|
|
.Beispiel
|
|
[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)
|
|
----
|
|
|
|
Wie viele Partikel des Emitters in diesem Frame leben, `0` bis zur Größe des Pools. Nützlich, um einen Pool abzustimmen, für ein Budget auf dem Bildschirm oder um zu wissen, wann ein Stoß verklungen ist.
|
|
|
|
*Rückgabe:* Die Anzahl der lebenden Partikel, eine Ganzzahl.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emittersetmax,emitterSetMax>>, <<emitterisactive,emitterIsActive>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Entwicklungs-Overlay: wie viel von jedem Pool in Gebrauch ist.
|
|
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)
|
|
----
|
|
|
|
Ob der Emitter strömt oder noch lebende Partikel hat: `true` ab `emitterStart` oder `emitterBurst`, bis der Strom gestoppt und das letzte Partikel gestorben ist, `false`, sobald die Explosion vorbei ist. Ein strömender Emitter, dessen Pool gerade leer ist, ist trotzdem aktiv.
|
|
|
|
*Rückgabe:* Boolean.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<emitterstop,emitterStop>>, <<emittergetcount,emitterGetCount>>, <<emitterdelete,emitterDelete>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Einmalige Explosionen leben in einer Liste und werden freigegeben, wenn sie ausgebrannt sind.
|
|
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]
|
|
=== Schriftart
|
|
|
|
Singe hat zwei Text-Renderer. Die eingebaute Konsolenschrift wird von <<overlayprint,overlayPrint>> gezeichnet und braucht keine Assets; die Familie `font*` lädt TrueType-Dateien in einer festen Punktgröße und rendert sie entweder mit `fontPrint` direkt auf das Overlay oder mit `fontToSprite` in ein wiederverwendbares Sprite. Schriftart-Handles sind Ganzzahlen aus `fontLoad`, und die Engine hält eine global ausgewählte Schriftart, die `fontLoad` auf die gerade geladene Schriftart setzt und `fontSelect` ändert. Text wird in der aktuellen Vordergrundfarbe in der von `fontQuality` gewählten Qualität gerendert, und Textpositionen sind Overlay-Koordinaten. Gib Schriftarten mit `fontUnload` frei; `Singe/FreeSansBold.ttf` wird mit der Engine mitgeliefert und ist immer verfügbar.
|
|
|
|
[#fontload]
|
|
==== fontLoad
|
|
|
|
[source,text]
|
|
----
|
|
result = fontLoad(filename, pointSize)
|
|
----
|
|
|
|
Öffnet eine TrueType-Schriftart in einer Punktgröße und macht sie zur ausgewählten Schriftart. Die Größe ist für die Lebensdauer des Handles fest, lade dieselbe Datei also einmal pro benötigter Größe. Der Name geht wie jedes andere Asset durch das virtuelle Dateisystem; stelle `DIR` voran für Dateien, die mit deinem Spiel mitgeliefert werden, oder verwende `Singe/FreeSansBold.ttf` für den mitgelieferten Schriftschnitt. Eine Datei, die sich nicht öffnen lässt, beendet das Skript mit `Unable to open` und dem Namen.
|
|
|
|
*Parameter:*
|
|
|
|
* `filename` -- Pfad zu einer `.ttf`-Datei.
|
|
* `pointSize` -- ganzzahlige Punktgröße.
|
|
|
|
*Rückgabe:* ganzzahliges Schriftart-Handle.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<fontselect,fontSelect>>, <<fontunload,fontUnload>>, <<fontquality,fontQuality>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Zwei Größen des mitgelieferten Schriftschnitts: groß für den Titel, klein für das 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)
|
|
----
|
|
|
|
Rendert einen String mit der ausgewählten Schriftart, der aktuellen Qualität und der aktuellen Vordergrundfarbe (bei `FONT_QUALITY_SHADED` zusätzlich der Hintergrundfarbe) und blittet ihn dann mit seiner oberen linken Ecke bei `(x, y)` auf das Overlay. Ruf es aus `onOverlayUpdate` auf, wenn nötig in jedem Frame: die Engine behält jeden String, den sie rendert, mit Schriftart, Qualität, Farben und Text als Schlüssel, und ein späterer Aufruf, der denselben verlangt, blittet ihn nur noch. Strings, die etwa eine Sekunde lang nicht ausgegeben wurden, werden vergessen, und die Strings einer Schriftart verschwinden, wenn sie entladen wird, sodass Text, der sich ändert, nicht mehr kostet als schon immer, und Text, der sich nicht ändert, fast nichts. Eine ganze Seite Highscores in jedem Frame neu auszugeben ist in Ordnung. Ohne ausgewählte Schriftart endet das Skript mit einem Fehler.
|
|
|
|
*Parameter:*
|
|
|
|
* `x`, `y` -- obere linke Ecke in Overlay-Koordinaten; Nachkommastellen werden abgeschnitten.
|
|
* `text` -- der zu rendernde String.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<fonttosprite,fontToSprite>>, <<fontselect,fontSelect>>, <<colorforeground,colorForeground>>, <<overlayprint,overlayPrint>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Live-Anzeige der Audiokalibrierung; der Text ändert sich in jedem Frame, also direkt rendern.
|
|
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)
|
|
----
|
|
|
|
Wählt, wie `fontPrint` und `fontToSprite` Text von nun an rastern. Die Einstellung ist global, nicht pro Schriftart, und beginnt bei `FONT_QUALITY_SOLID`. Sprites, die `fontToSprite` schon erzeugt hat, behalten die Qualität, mit der sie gerendert wurden. Ein Wert außerhalb der drei Konstanten beendet das Skript mit einem Fehler.
|
|
|
|
*Parameter:*
|
|
|
|
* `mode` -- `FONT_QUALITY_SOLID` (`1`): am schnellsten, Ein-Bit-Kanten, keine Kantenglättung. `FONT_QUALITY_SHADED` (`2`): kantengeglättete Glyphen auf einem gefüllten Rechteck in der Hintergrundfarbe. `FONT_QUALITY_BLENDED` (`3`): kantengeglättete Glyphen mit Alpha pro Pixel, die sich über alles blenden; die übliche Wahl.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<fontprint,fontPrint>>, <<fonttosprite,fontToSprite>>, <<colorbackground,colorBackground>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Text in BLENDED über dem Video, aber ein Beschriftungsfeld in SHADED für die Debug-Anzeige.
|
|
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)
|
|
----
|
|
|
|
Macht eine geladene Schriftart zu der, die `fontPrint` und `fontToSprite` verwenden. Die Auswahl bleibt bis zum nächsten `fontSelect` oder `fontLoad` bestehen, da auch das Laden auswählt. Sie kann so oft wie nötig geändert werden, auch mehrmals innerhalb eines `onOverlayUpdate`. Ein unbekanntes oder entladenes Handle (oder gar keine Zahl) lässt die aktuelle Schriftart ausgewählt und gibt eine Warnung aus, so wie Hypseus Singe es durchgehen lässt.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<fontload,fontLoad>>, <<fontprint,fontPrint>>, <<fonttosprite,fontToSprite>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Titel im großen Schriftschnitt, Punktestand im kleinen, in jedem Frame.
|
|
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)
|
|
----
|
|
|
|
Rendert einen String mit der ausgewählten Schriftart, Qualität und den Farben in ein neues Sprite und gibt dessen Handle zurück. Das Sprite ist von da an unabhängig von der Schriftart, übersteht also `fontUnload`, und es funktioniert mit jedem `sprite*`-Aufruf: zeichne es, miss es, skaliere oder drehe es und gib es mit `spriteUnload` frei. Für einen String, der sich ändert, rendere ein neues Sprite und entlade das alte. Ohne ausgewählte Schriftart endet das Skript mit einem Fehler.
|
|
|
|
NOTE: Text aus Leistungsgründen in ein Sprite zu rendern ist veraltet. `fontPrint` behält jetzt, was es rendert, sodass es etwa so viel kostet, denselben String in jedem Frame auszugeben, wie ein daraus gemachtes Sprite zu zeichnen, ohne dass ein Sprite zu verwalten ist. Verwende `fontToSprite`, wenn der Text ein Sprite sein muss: um ihn zu skalieren, zu drehen, zu spiegeln oder mit `spriteGetWidth` und `spriteGetHeight` zu messen.
|
|
|
|
*Rückgabe:* ganzzahliges Sprite-Handle.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<fontprint,fontPrint>>, <<spritedraw,spriteDraw>>, <<spritegetwidth,spriteGetWidth>>, <<spriteunload,spriteUnload>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Titel, der sich drehend hereinwächst: Text, der ein Sprite sein muss, um gedreht und skaliert zu werden.
|
|
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)
|
|
----
|
|
|
|
Schließt eine Schriftart und macht ihr Handle ungültig. Sprites, die mit `fontToSprite` daraus gemacht wurden, sind nicht betroffen. War die entladene Schriftart die ausgewählte, ist danach keine Schriftart ausgewählt, und das nächste `fontPrint` oder `fontToSprite` beendet das Skript mit einem Fehler, bis `fontSelect` oder `fontLoad` eine andere wählt.
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<fontload,fontLoad>>, <<fontselect,fontSelect>>, <<onshutdown,onShutdown>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Schriftarten und die Text-Sprites, die sie erzeugt haben, freigeben.
|
|
function onShutdown()
|
|
spriteUnload(titleSprite)
|
|
for _, line in ipairs(descriptionLines) do
|
|
spriteUnload(line)
|
|
end
|
|
fontUnload(titleFont)
|
|
fontUnload(hudFont)
|
|
end
|
|
----
|
|
|
|
[#guiref]
|
|
=== Gui
|
|
|
|
Eine GUI ist ein RmlUi-Dokumentbaum fester Größe, der in jedem Frame in seine eigene Textur gerendert wird: `guiNew` erzeugt eine und gibt ein ganzzahliges Handle zurück, `guiLoad` legt ein RML-Dokument hinein und gibt ein Dokument-Handle zurück, `guiDraw` legt sie über das Overlay, und `materialSetGui` zeigt sie auf einem Material in der Szene. Dokumente sind HTML-artiges RML, gestaltet mit CSS-artigem RCSS, und werden wie Sprites über das virtuelle Dateisystem geladen; `Singe/gui.rcss` ist das mitgelieferte Theme und `Singe/FreeSansBold.ttf` der Ersatzschriftschnitt. Die Helfer nach id, `guiGetValue`, `guiSetValue` und `guiSetHandler`, erreichen Elemente ohne das Objektmodell; alles andere ist die Lua-API von RmlUi auf dem globalen `rmlui`, wo eine GUI der Kontext namens `gui` gefolgt von ihrem Handle ist. Bis zu sechzehn GUIs gibt es gleichzeitig, jede mit zweiunddreißig Dokumenten. Eine GUI braucht das GPU-Gerät, deshalb beendet `guiNew` das Skript auf einem Rechner ohne eines; ein falsches GUI-Handle löst `No GUI N` aus, ein falsches Dokument-Handle `No document N in GUI G`. Siehe <<gui,GUI>> unter Spieleentwicklung.
|
|
|
|
[#guiclose]
|
|
==== guiClose
|
|
|
|
[source,text]
|
|
----
|
|
guiClose(gui, document)
|
|
----
|
|
|
|
Entlädt das Dokument aus der GUI und verwirft seine Elemente, seine Handler und seine Skripte. Das Dokument-Handle ist danach ungültig. Ein Dokument, das sein eigenes Skript über die API von RmlUi schließt (`document:Close()`), wird auf dieselbe Weise entladen. Löst bei einem ungültigen Handle `No document N in GUI G` aus.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<guiload,guiLoad>>, <<guihide,guiHide>>, <<guidelete,guiDelete>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Seite für den Highscore-Eintrag wird nur einmal gebraucht; nach der Eingabe des Namens weg damit.
|
|
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)
|
|
----
|
|
|
|
Gibt die GUI, jedes Dokument darin und ihre Textur frei und beendet das Aktualisieren und Rendern in jedem Frame, das sie kostete. Materialien, die sie über `materialSetGui` zeigen, kehren zu ihrer eigenen Textur zurück. Das Handle ist danach ungültig und beendet das Skript, wenn es noch einmal verwendet wird; da eine GUI weiter rendert, ob sie gezeichnet wird oder nicht, lösche die, mit denen ein Spiel fertig ist, statt sie zu verstecken.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<guinew,guiNew>>, <<guiclose,guiClose>>, <<materialsetgui,materialSetGui>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Optionsseite lebt nur, solange der Spieler darin ist.
|
|
function closeOptions()
|
|
guiDelete(optionsGui)
|
|
optionsGui = nil
|
|
discPlay()
|
|
end
|
|
----
|
|
|
|
[#guidraw]
|
|
==== guiDraw
|
|
|
|
[source,text]
|
|
----
|
|
guiDraw(gui)
|
|
guiDraw(gui, x, y)
|
|
guiDraw(gui, x, y, width, height)
|
|
----
|
|
|
|
Legt die Textur der GUI für diesen Frame über das Overlay, in Overlay-Koordinaten, sodass sie wie alles andere auf dem Overlay dem Sinden-Rahmen und der Overscan-Skalierung folgt: mit einem Argument über das ganze Overlay, in ihrer eigenen Größe mit einer Ecke bei `x, y` oder skaliert in das Rechteck `width` mal `height`. Die Stelle des Aufrufs im Zeichnen des Skripts entscheidet, was unter der GUI liegt und was darüber: alles, was vorher auf das Overlay gezeichnet wurde, bleibt darunter, und alles, was danach gezeichnet wird -- bis zum nächsten `guiDraw` oder bis das Skript das nächste Mal an der Reihe ist -- kommt auf eine eigene Ebene, die über diese GUI gelegt wird. Ein Zeiger, der nach dem HUD gezeichnet wird, liegt über dem HUD; ein danach gezeichnetes zweites Dokument liegt über dem Zeiger, mit seiner eigenen Ebene darüber. Bis zu sechzehn solcher Ebenen pro Frame; ein Aufruf darüber hinaus zeichnet seine GUI ohne etwas darüber. `overlayClear` leert die Ebene, auf die gerade gezeichnet wird, und `overlaySetMonochrome` macht nur das Basis-Overlay grau. Der Aufruf gilt einen Frame; mach ihn aus `onOverlayUpdate` (oder `singeMain`) in jedem Frame, in dem die GUI sichtbar sein soll. GUIs zeichnen über dem Overlay in der Reihenfolge der Aufrufe (die letzte oben), unter Partikeln in `PARTICLE_OVER` und der Pausenanzeige, und die GUI, die zuletzt unter dem Zeiger gezeichnet wurde, ist die, die die Maus bekommt. Bis zu sechzehn Aufrufe pro Frame werden beachtet; weitere werden ignoriert. Jede andere Anzahl von Argumenten löst `Expected 1, 3 or 5 arguments` aus.
|
|
|
|
*Parameter:*
|
|
|
|
* `x`, `y` -- die obere linke Ecke, in Overlay-Koordinaten.
|
|
* `width`, `height` -- die Größe, in der gezeichnet wird, in Overlay-Pixeln; ohne Angabe die eigene Größe der GUI.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<guinew,guiNew>>, <<materialsetgui,materialSetGui>>, <<overlaysetresolution,overlaySetResolution>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das HUD sitzt in der unteren rechten Ecke mit einem Drittel der Breite des Overlays, und das Visier
|
|
-- der Lightgun wird danach gezeichnet, sodass das Panel das Visier nie verdeckt.
|
|
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)
|
|
----
|
|
|
|
Dasselbe wie <<guidraw,`guiDraw`>>, nur dass das Rechteck über das ganze Fenster gemessen wird statt über das Bild. Die Koordinaten sind weiterhin Overlay-Koordinaten -- `0, 0` ist oben links im Fenster und `overlayGetWidth(), overlayGetHeight()` unten rechts -- sodass eine GUI auf der Bezel-Grafik neben dem Bild, darüber oder darunter sitzen kann. Ohne Bezel und ohne `--scalefactor`, `--shiftx` oder `--shifty` ist das Bild das ganze Fenster, und dies ist genau `guiDraw`.
|
|
|
|
GUIs im Bildschirmraum zeichnen nach allem anderen, die Bezel-Grafik eingeschlossen, und teilen sich die sechzehn Zeichenaufrufe pro Frame mit `guiDraw`. Ein Klick findet sie auf dieselbe Weise: der Zeiger wird in den Bildschirmraum übertragen, bevor er geprüft wird, sodass eine GUI im Bildschirmraum, die Eingaben annimmt, die Maus bekommt, wo auch immer sie gezeichnet wurde.
|
|
|
|
*Parameter:*
|
|
|
|
* `x`, `y` -- die obere linke Ecke, in Overlay-Koordinaten, über das Fenster gemessen.
|
|
* `width`, `height` -- die Größe, in der gezeichnet wird; ohne Angabe die eigene Größe der GUI.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<guidraw,guiDraw>>, xref:Manual.adoc#bezels[Bezels], <<scorebezelenable,scoreBezelEnable>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Karte auf der linken Säule der Automatengrafik, außerhalb des Bilds.
|
|
function onOverlayUpdate()
|
|
overlayClear()
|
|
guiDrawScreen(mapGui, 8, 44, 38, 114)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#guigetheight]
|
|
==== guiGetHeight
|
|
|
|
[source,text]
|
|
----
|
|
height = guiGetHeight(gui)
|
|
----
|
|
|
|
Die Höhe der GUI in ihren eigenen Pixeln, wie an `guiNew` übergeben. Beendet das Skript bei einem ungültigen Handle.
|
|
|
|
*Rückgabe:* Eine Ganzzahl.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<guigetwidth,guiGetWidth>>, <<guinew,guiNew>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Einen kleinen Dialog auf dem Overlay zentrieren.
|
|
local x = (overlayGetWidth() - guiGetWidth(dialog)) / 2
|
|
local y = (overlayGetHeight() - guiGetHeight(dialog)) / 2
|
|
guiDraw(dialog, x, y)
|
|
----
|
|
|
|
[#guigetstats]
|
|
==== guiGetStats
|
|
|
|
[source,text]
|
|
----
|
|
stats = guiGetStats()
|
|
----
|
|
|
|
Was das Rendern aller GUIs im letzten abgeschlossenen Frame gekostet hat, zum Abstimmen von Dokumenten, die Filter, Masken und Schatten verwenden (die alle über eine Ebene in der Größe der GUI zeichnen). Die Zähler gelten für alle GUIs zusammen. In einem Frame, in dem sich das Bild keiner GUI geändert hat, sind sie alle null, da nichts neu gezeichnet wurde (siehe <<guirefresh,guiRefresh>>).
|
|
|
|
*Rückgabe:* Eine Tabelle mit den Feldern `drawCalls` (indizierte Zeichenaufrufe, einschließlich Maskenschreibvorgängen, Kompositionen und Filterdurchgängen), `layerPushes` (betretene Ebenen für Filter, Maskenbilder und Box-Schatten), `filterPasses` (Durchgänge in voller Größe und Blits innerhalb von Filterketten; eine Unschärfe kostet mehrere), `maskWrites` (Zeichenaufrufe in die Stencil-Clipmaske), `layerBytes` (Bytes der derzeit belegten Ebenentexturen; jede hat die Größe ihrer GUI bei vier Bytes pro Pixel) und `passes` (begonnene Render-Durchgänge; jeder Ebenenwechsel beginnt einen).
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<guinew,guiNew>>, <<gui,GUI>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Beobachten, was ein Bildschirm voller Effekte kostet.
|
|
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)
|
|
----
|
|
|
|
Der Wert des Elements mit dieser `id` im Dokument, als String: der Wert eines Formularelements (der Text eines Textfelds, der `value` einer Option eines `select`, ein Schieberegler in der eigenen Formatierung von RmlUi wie `"85.000000"`, das Attribut `value` eines Kontrollkästchens oder Optionsfelds, `"on"`, wenn es keines hat, ob es angekreuzt ist oder nicht; sein Zustand ist sein Attribut `checked`, das über die API von RmlUi gelesen wird) oder bei jedem anderen Element sein inneres RML. Gibt `nil` zurück, wenn kein Element diese id hat, sodass eine Seite abgefragt werden kann, ohne das Skript zu beenden; ein falsches GUI- oder Dokument-Handle beendet es. Werte, die länger als 4095 Zeichen sind, werden abgeschnitten.
|
|
|
|
*Rückgabe:* Ein String oder `nil`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<guisetvalue,guiSetValue>>, <<guisethandler,guiSetHandler>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das ganze Formular lesen, wenn Start gedrückt wird.
|
|
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)
|
|
----
|
|
|
|
Die Breite der GUI in ihren eigenen Pixeln, wie an `guiNew` übergeben. Beendet das Skript bei einem ungültigen Handle.
|
|
|
|
*Rückgabe:* Eine Ganzzahl.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<guigetheight,guiGetHeight>>, <<guinew,guiNew>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Liegt der abgefragte Punkt in der rechten Hälfte der Seite des Monitors?
|
|
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)
|
|
----
|
|
|
|
Versteckt das Dokument: es wird nicht gezeichnet und nimmt keine Eingaben an, behält aber seine Elemente und Werte, und `guiShow` bringt es so zurück, wie es war. Löst bei einem ungültigen Handle `No document N in GUI G` aus.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<guishow,guiShow>>, <<guiclose,guiClose>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Zwei Seiten in einer GUI: die Optionsseite verschwindet, während die Steuerungsseite erscheint.
|
|
guiSetHandler(gui, options, "controls", "click", function()
|
|
guiHide(gui, options)
|
|
guiShow(gui, controls)
|
|
end)
|
|
----
|
|
|
|
[#guiload]
|
|
==== guiLoad
|
|
|
|
[source,text]
|
|
----
|
|
document = guiLoad(gui, filename)
|
|
----
|
|
|
|
Lädt ein RML-Dokument in die GUI, zeigt es an und gibt ein ganzzahliges Dokument-Handle für die anderen Aufrufe zurück. Der Name geht wie der eines Sprites durch das virtuelle Dateisystem und wird relativ zu dem Ordner aufgelöst, in dem Singe gestartet wurde, stelle also `DIR` voran für Dateien, die mit deinem Spiel mitgeliefert werden; Stylesheets, Bilder und Schriftarten, die im Dokument genannt werden, werden relativ zu der Datei aufgelöst, die sie nennt, und dann über dieselbe Suche, sodass ein gepacktes Spiel sie findet. Die `<script>`-Blöcke und `on*`-Attribute des Dokuments laufen im Lua-Zustand des Spiels, und es ist als `rmlui.contexts["guiN"].documents["<body id>"]` erreichbar. Ein Dokument, das sich nicht laden lässt, beendet das Skript mit dem Namen und dem Grund von RmlUi (eine fehlende Datei, ein Parse-Fehler); die eigenen Warnungen von RmlUi zu Markup und Stil gehen an die Konsole. Jede GUI fasst bis zu zweiunddreißig Dokumente.
|
|
|
|
*Rückgabe:* Das Dokument-Handle, eine Ganzzahl.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<guinew,guiNew>>, <<guishow,guiShow>>, <<guihide,guiHide>>, <<guiclose,guiClose>>, <<guiloadfont,guiLoadFont>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Titelseite und das HUD teilen sich eine GUI in der Größe des Overlays.
|
|
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)
|
|
----
|
|
|
|
Lädt einen TrueType- oder OpenType-Schriftschnitt für jede GUI, sodass Dokumente ihn in `font-family` über den Familiennamen in der Datei nennen können (wobei `font-weight` und `font-style` unter den Schnitten einer Familie wählen). Mit `fallback` `true` bedient der Schnitt auch jede Glyphe, die kein anderer Schnitt hat, was bis dahin der engine-eigene `Singe/FreeSansBold.ttf` tut (Familie `FreeSans`, Gewicht fett, der Schnitt, den das mitgelieferte Theme verwendet). Lade Schriftarten vor den Dokumenten, die sie verwenden. Jeder Schnitt kostet einen Glyphenatlas pro Größe, in der er verwendet wird. Eine Datei, die sich nicht öffnen lässt, beendet das Skript mit `Unable to load` und dem Namen.
|
|
|
|
*Parameter:*
|
|
|
|
* `filename` -- die Schriftdatei, über das virtuelle Dateisystem wie bei `fontLoad`.
|
|
* `fallback` -- `true`, um dies zum Ersatzschnitt zu machen; `false` (der Standard) für einen gewöhnlichen Schnitt.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<guiload,guiLoad>>, <<fontload,fontLoad>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Der Anzeigeschnitt des Spiels für Überschriften; das mitgelieferte FreeSans bleibt für den Fließtext.
|
|
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)
|
|
----
|
|
|
|
Erzeugt eine GUI von `width` mal `height` Pixeln, die in jedem Frame in ihre eigene Textur gerendert wird, ob sie gezeichnet wird oder nicht, und gibt ihr Handle zurück. Die Größe ist der Koordinatenraum des Dokuments: `100%` des Body ist die ganze Textur, und ein `dp` ist eines ihrer Pixel. Eine GUI in der Größe des Overlays passt zu einer Seite im Vollbild; ein HUD-Element oder ein Monitor in der Szene will eine kleinere, und ein Raspberry Pi will alles bescheiden. Eine neue GUI zeigt nur an, bis `guiSetInput` Eingaben einschaltet. Löst bei einer Größe von null oder kleiner `The size must be positive` aus und `No GUI available`, wenn schon sechzehn in Gebrauch sind oder der Rechner kein GPU-Gerät hat (eine GUI braucht dasselbe Gerät wie die 3D-Szene).
|
|
|
|
*Parameter:*
|
|
|
|
* `width` -- die Breite der Textur in Pixeln.
|
|
* `height` -- die Höhe der Textur in Pixeln.
|
|
|
|
*Rückgabe:* Das GUI-Handle, eine Ganzzahl.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<guiload,guiLoad>>, <<guidraw,guiDraw>>, <<guidelete,guiDelete>>, <<materialsetgui,materialSetGui>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Seite in der Größe des Overlays, über die Disc gezeichnet.
|
|
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)
|
|
----
|
|
|
|
Zeichnet die GUI im nächsten Frame erneut, auch wenn sich scheinbar nichts an ihr geändert hat. Eine GUI wird nur neu gezeichnet, wenn sich ihr Bild ändert: wenn das Dokument eines Frames genau das zeichnet, was der letzte Frame gezeichnet hat, und dazwischen nichts Neues erzeugt wurde, enthält die Textur dieses Bild schon und wird behalten, was der GPU den größten Teil der Kosten einer GUI in den Frames erspart, in denen ein HUD oder ein Menü stillsteht. Änderungen über RML, RCSS, die Lua-Schnittstelle von RmlUi oder die `gui*`-Aufrufe werden alle bemerkt. Dieser Aufruf ist der Ausweg, falls doch einmal eine übersehen wird -- eine GUI, die aufhört, sich zu aktualisieren, wenn sie es sollte -- und ein Spiel sollte ihn nicht brauchen; wenn doch, ist das eine Meldung wert. Eine GUI, die es nicht gibt, beendet das Skript.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<guidraw,guiDraw>>, <<guigetstats,guiGetStats>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Notbehelf, solange eine übersehene Änderung gemeldet ist: das HUD in jedem Frame neu zeichnen.
|
|
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)
|
|
----
|
|
|
|
Ruft `function(gui, document, id, event, value)` auf, wann immer das Element mit dieser `id` das genannte Ereignis auslöst: `"click"` für Schaltflächen und Zeilen, `"change"` für Formularelemente, `"submit"` für ein Formular oder jeden anderen Ereignisnamen von RmlUi (`"focus"`, `"mouseover"`, `"keydown"`, ...). `value` ist der Wert des Elements in diesem Moment als String, so wie `guiGetValue` ihn zurückgeben würde (ein Schieberegler kommt so, wie RmlUi ihn formatiert, `"85.000000"`, gib ihn also durch `tonumber`). Pro Element und Ereignis wird eine Funktion behalten; eine andere zu setzen ersetzt sie, und `nil` entfernt sie. Der Handler gehört zur `id`, nicht zu einem Element: setze ihn nach dem Neuaufbau erneut, wenn das Element ersetzt wird -- eine über `inner_rml` neu aufgebaute Liste erzeugt neue Elemente -- und er hängt sich an das Element, das die id jetzt trägt. Handler laufen, während die Ereignisse geschehen, zwischen den eigenen Callbacks des Spiels, und ein Fehler in einem beendet das Spiel wie ein Fehler in jedem Callback. Ids sind höchstens 63 Zeichen lang. Löst `No element "id" in document N of GUI G` aus, wenn das Element nicht existiert, und `Argument 5 must be a function or nil` bei allem anderen an dieser Stelle.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- das Attribut `id` des Elements.
|
|
* `event` -- der Ereignisname von RmlUi.
|
|
* `function` -- die aufzurufende Lua-Funktion oder `nil`, um keine mehr aufzurufen.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<guigetvalue,guiGetValue>>, <<guisetvalue,guiSetValue>>, <<guisetinput,guiSetInput>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Lautstärkeregler wirkt, während er bewegt wird; Start liest den Rest des Formulars.
|
|
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)
|
|
----
|
|
|
|
Ob Maus und Lightgun, Tasten, getippter Text und die Schalter des Frameworks von einem Pad oder einer Maus die GUI erreichen. Eine neue GUI zeigt nur an, was zu einem HUD oder einem Schild passt; schalte Eingaben für ein Menü oder ein Formular ein. Solange sie eingeschaltet sind, erreicht der Zeiger die GUI, wo auch immer sie flach gezeichnet (die zuletzt gezeichnete oben) oder auf einer Fläche in der Szene gezeigt wird, jede Taste geht an sie außer `Tab`, das die GUI nur bekommt, während in ein Textfeld getippt wird (RmlUi verwendet es, um den Fokus zu bewegen, und gibt es nie zurück, und ein Spiel, dessen Tasten `Tab` einschließen, würde es sonst verlieren), ein Textfeld mit Fokus schaltet die Texteingabe von SDL ein, und die Richtungen des Pads kommen als Pfeiltasten an, mit `ACTION_1` als Eingabetaste und `ACTION_2` als Esc. Was ein Element verwendet, erreicht nie `onInputPressed`, `onKeyPressed` oder die Maus-Callbacks; was nichts verwendet hat, fällt wie üblich zu ihnen durch. Beendet das Skript bei einem ungültigen Handle.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<guisethandler,guiSetHandler>>, <<guidraw,guiDraw>>, <<sceneprobegui,sceneProbeGui>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das HUD nimmt nie Eingaben an; die Pausenseite schon, solange sie angezeigt wird.
|
|
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)
|
|
----
|
|
|
|
Setzt den Wert des Elements mit dieser `id`: den Wert eines Formularelements (den Text eines Textfelds, die Zahl eines Schiebereglers, den `value` einer Option eines `select`; der Zustand `checked` eines Kontrollkästchens ist ein Attribut und wird über die API von RmlUi gesetzt) oder bei jedem anderen Element sein inneres RML, sodass ein Absatz oder ein `div` mit schlichtem Text oder mit Markup neu geschrieben werden kann. Das Dokument berechnet sein Layout im nächsten Frame neu. Löst `No element "id" in document N of GUI G` aus, wenn das Element nicht existiert.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- das Attribut `id` des Elements.
|
|
* `value` -- der neue Wert, ein String; Zahlen werden umgewandelt.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<guigetvalue,guiGetValue>>, <<guisethandler,guiSetHandler>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Punktezeile und die Lebenszeile werden in jedem Frame neu geschrieben; die Lebenszeile ist Markup.
|
|
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)
|
|
----
|
|
|
|
Zeigt ein von `guiHide` verstecktes Dokument an (ein Dokument wird angezeigt, wenn `guiLoad` zurückkehrt, der Aufruf ist dann also nicht nötig). Ein angezeigtes Dokument wird überall gezeichnet, wo seine GUI gezeichnet wird, und nimmt Eingaben an, wenn seine GUI es tut. Löst bei einem ungültigen Handle `No document N in GUI G` aus.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<guihide,guiHide>>, <<guiload,guiLoad>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Game-Over-Seite erscheint, wenn das letzte Leben verloren geht.
|
|
function onLastLifeLost()
|
|
guiSetValue(gui, gameOver, "final", string.format("%08d", score))
|
|
guiShow(gui, gameOver)
|
|
end
|
|
----
|
|
|
|
[#joint]
|
|
=== Gelenk
|
|
|
|
Gelenke verbinden zwei Körper, oder einen Körper mit der Welt, wenn der zweite Knoten `-1` ist, und geben ein ganzzahliges Gelenk-Handle zurück, das die anderen Aufrufe entgegennehmen. Anker und Achsen werden in Weltkoordinaten zu dem Zeitpunkt angegeben, an dem das Gelenk entsteht, mit den Körpern dort, wo sie sich dann befinden; platziere die Knoten also und gib ihnen Körper, bevor du sie durch ein Gelenk verbindest. Abstände sind in Welteinheiten, Scharnierwinkel in Grad. Wird einer der beiden Körper oder sein Knoten gelöscht, wird das Gelenk mitgelöscht; ein Handle, das gelöscht wurde, beendet das Skript, wenn es wieder übergeben wird. Siehe <<physics,Physik>> unter Spieleentwicklung, Gelenke.
|
|
|
|
[#jointhinge]
|
|
==== jointHinge
|
|
|
|
[source,text]
|
|
----
|
|
joint = jointHinge(nodeA, nodeB, ax, ay, az, dx, dy, dz)
|
|
----
|
|
|
|
Erzeugt ein Scharnier zwischen den beiden Körpern durch den Weltpunkt `(ax, ay, az)` entlang der Achse `(dx, dy, dz)`: eine Tür, ein Rad, ein Hebel, eine Wippe. Die Körper teilen sich diesen Punkt und diese Achse und können sich nur um sie drehen. Die Achse wird normalisiert, ihre Länge spielt also keine Rolle; eine Nullachse wird zu `(0, 1, 0)`. Beide Knoten müssen einen Körper tragen, oder `nodeB` darf `-1` sein, um `nodeA` per Scharnier an der Welt zu befestigen. Beendet das Skript, wenn ein Knoten keinen Körper hat oder das Gelenk nicht erzeugt werden kann.
|
|
|
|
*Parameter:*
|
|
|
|
* `nodeA` -- der Körper, dessen Drehung gemessen wird; ein positiver `jointSetLimits`-Winkel ist seine Drehung um die Achse nach der Rechte-Hand-Regel.
|
|
* `nodeB` -- der andere Körper oder `-1` für die Welt.
|
|
* `ax`, `ay`, `az` -- der Anker, ein Weltpunkt, durch den die Scharnierachse geht.
|
|
* `dx`, `dy`, `dz` -- die Scharnierachse in Weltkoordinaten.
|
|
|
|
*Rückgabe:* Das Gelenk-Handle.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<jointsetlimits,jointSetLimits>>, <<jointdelete,jointDelete>>, <<jointball,jointBall>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Tür, entlang ihrer linken Kante an der Welt angeschlagen, die sich 100 Grad in eine Richtung öffnet.
|
|
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)
|
|
-- Später schwingt sie durch einen Stoß nahe der Klinke auf.
|
|
bodyApplyImpulse(door, 0, 0, -60, 0.5, -0.62, 1.45)
|
|
----
|
|
|
|
[#jointball]
|
|
==== jointBall
|
|
|
|
[source,text]
|
|
----
|
|
joint = jointBall(nodeA, nodeB, ax, ay, az)
|
|
----
|
|
|
|
Erzeugt ein Kugelgelenk am Weltpunkt `(ax, ay, az)`: Die Körper können sich beliebig darum drehen, sich aber nicht voneinander lösen. Ein Pendel, ein Kettenglied, ein hängendes Schild, eine Ragdoll-Schulter. Beide Knoten müssen einen Körper tragen, oder `nodeB` darf `-1` sein, um `nodeA` an einem festen Punkt in der Welt aufzuhängen. Ein Kugelgelenk hat keine Grenzen, deshalb lehnt `jointSetLimits` es ab. Beendet das Skript, wenn ein Knoten keinen Körper hat oder das Gelenk nicht erzeugt werden kann.
|
|
|
|
*Parameter:*
|
|
|
|
* `nodeA` -- der erste Körper.
|
|
* `nodeB` -- der andere Körper oder `-1` für die Welt.
|
|
* `ax`, `ay`, `az` -- der Anker, der Weltpunkt, um den die Körper schwenken.
|
|
|
|
*Rückgabe:* Das Gelenk-Handle.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<jointhinge,jointHinge>>, <<jointdelete,jointDelete>>, <<bodyapplyimpulse,bodyApplyImpulse>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Kette aus vier Gliedern an einem Haken, jedes per Kugelgelenk mit dem darüber verbunden.
|
|
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)
|
|
----
|
|
|
|
Lässt die Körper relativ zueinander nur entlang der Achse `(dx, dy, dz)` durch den Anker `(ax, ay, az)` gleiten, wobei ihre Ausrichtung erhalten bleibt: eine Schublade, ein Kolben, ein Aufzug, eine Schiebetür. Die Achse wird normalisiert, ihre Länge spielt also keine Rolle; eine Nullachse wird zu `(0, 1, 0)`. Beide Knoten müssen einen Körper tragen, oder `nodeB` darf `-1` sein, damit `nodeA` auf einer fest in der Welt verankerten Schiene läuft. Beendet das Skript, wenn ein Knoten keinen Körper hat oder das Gelenk nicht erzeugt werden kann.
|
|
|
|
*Parameter:*
|
|
|
|
* `nodeA` -- der Körper, dessen Weg gemessen wird; ein positiver `jointSetLimits`-Abstand liegt in Achsrichtung.
|
|
* `nodeB` -- der andere Körper oder `-1` für die Welt.
|
|
* `ax`, `ay`, `az` -- ein Weltpunkt auf der Schiene.
|
|
* `dx`, `dy`, `dz` -- die Bewegungsrichtung in Weltkoordinaten.
|
|
|
|
*Rückgabe:* Das Gelenk-Handle.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<jointsetlimits,jointSetLimits>>, <<jointhinge,jointHinge>>, <<bodysetvelocity,bodySetVelocity>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Kiste auf einer Schiene entlang X, zu Beginn seitwärts angestoßen und 2.4 Einheiten weiter gestoppt.
|
|
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)
|
|
----
|
|
|
|
Begrenzt den Winkel eines Scharniers in Grad oder den Weg eines Schiebegelenks in Welteinheiten, nach beiden Seiten von der Stellung aus, in der das Gelenk begann. Ein positiver Scharnierwinkel ist eine Drehung von `nodeA` um die Achse nach der Rechte-Hand-Regel; ein positiver Schiebeweg liegt in Achsrichtung. `low` wird auf höchstens `0` begrenzt und `high` auf mindestens `0`, sodass die Ausgangsstellung immer im Bereich liegt. Ruf es erneut auf, um den Bereich jederzeit zu ändern. Beendet das Skript bei einem Kugelgelenk oder einem Handle, das nicht mehr existiert.
|
|
|
|
*Parameter:*
|
|
|
|
* `joint` -- das Handle aus `jointHinge` oder `jointSlider`.
|
|
* `low` -- der äußerste negative Winkel oder Abstand, `0` oder darunter.
|
|
* `high` -- der äußerste positive Winkel oder Abstand, `0` oder darüber.
|
|
|
|
*Hinweise:* Die beiden Körper tauschen die Rollen, wenn sie in umgekehrter Reihenfolge angegeben werden, was das Vorzeichen des Bereichs spiegelt.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<jointhinge,jointHinge>>, <<jointslider,jointSlider>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Hebel, der 30 Grad in jede Richtung kippt und einrastet, sobald das Rätsel gelöst ist.
|
|
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)
|
|
----
|
|
|
|
Entfernt das Gelenk; die Körper sind wieder frei voneinander und behalten die Geschwindigkeit, die sie gerade hatten. Gelenke verschwinden auch mit einem ihrer Körper, deshalb wird dies nur gebraucht, um zwei Körper voneinander zu lösen, die beide bleiben. Beendet das Skript bei einem Handle, das nicht existiert oder bereits gelöscht wurde.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<jointhinge,jointHinge>>, <<jointball,jointBall>>, <<bodydelete,bodyDelete>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Seil reißt, wenn die daran hängende Kiste hart genug getroffen wird.
|
|
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
|
|
|
|
Jeder Controller, den Singe öffnet, ist ein Gamepad und wird über die `controller*`-Aufrufe
|
|
gelesen; ein Gerät, das SDL nicht erkennt, erreicht sie über eine Belegung, die Singe dafür
|
|
schreibt, es gibt also nichts anderes aufzurufen. Siehe xref:Manual.adoc#devices[Jedes Gerät ist
|
|
ein Gamepad]. Die beiden Aufrufe hier sorgen dafür, dass ein Stick eine Maus vertreten kann.
|
|
|
|
[#joymouseenable]
|
|
==== joyMouseEnable
|
|
|
|
[source,text]
|
|
----
|
|
joyMouseEnable(enabled)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Schaltet Joystick-als-Maus ein oder aus, während das Spiel läuft. Ist es eingeschaltet, steuert der linke Stick des Gamepad-Slots `0` den Zeiger, den das Spiel sieht, über dasselbe `onMouseMoved`, das auch eine echte Maus erreicht, und bleibt dabei innerhalb des Bildes; `--js_range` ist die Geschwindigkeit, und die Achse folgt ihrer eigenen Totzone. Das macht ein Lightgun-Spiel an einem Automaten mit Stick und ohne Maus spielbar.
|
|
|
|
Es startet mit `--joymouse`, das standardmäßig aus ist, und ein `singeReload` setzt es dorthin zurück. In diesem Standard weicht Singe von Hypseus ab: Die mitgelieferte `controls.cfg` legt diesen Stick bereits auf die vier Richtungsschalter, und wenn er ungefragt auch noch den Cursor steuerte, würde das jedes vorhandene Spiel überraschen.
|
|
|
|
*Parameter:*
|
|
|
|
* `enabled` -- Boolean.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00 (Hypseus)
|
|
*Siehe auch:* <<joymouseisenabled,joyMouseIsEnabled>>, <<onmousemoved,onMouseMoved>>, <<mousegetposition,mouseGetPosition>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Schießspiel, das den Spieler mit dem Stick zielen lässt, wenn keine Maus angeschlossen ist.
|
|
if mouseHowMany() == 0 then
|
|
joyMouseEnable(true)
|
|
end
|
|
----
|
|
|
|
[#joymouseisenabled]
|
|
==== joyMouseIsEnabled
|
|
|
|
[source,text]
|
|
----
|
|
enabled = joyMouseIsEnabled()
|
|
----
|
|
|
|
Ob der Stick gerade den Mauscursor steuert, gleich ob das durch `--joymouse` oder durch `joyMouseEnable` entschieden wurde.
|
|
|
|
*Rückgabe:* Boolean.
|
|
|
|
*Seit:* 3.00
|
|
*Siehe auch:* <<joymouseenable,joyMouseEnable>>
|
|
|
|
[#keyboard]
|
|
=== Tastatur
|
|
|
|
Singe liest die Tastatur in einem von zwei Modi, die mit `keyboardSetMode` gewählt werden. Im `MODE_NORMAL` (der Standard) werden Tasten über die Belegungen in `controls.cfg` in die `SWITCH_*`-Werte übersetzt, die `onInputPressed` und `onInputReleased` erreichen, und nicht belegte Tasten werden ignoriert; im `MODE_FULL` erreicht jede Taste das Skript roh, über `onKeyPressed` und `onKeyReleased` sowie über `onInputPressed` und `onInputReleased`. Unabhängig vom Modus fragen `keyboardIsDown`, `keyboardGetLastDown` und `keyboardGetLastUp` den aktuellen Tastaturzustand ab, und genau das will das Thread-Modell (siehe <<threaded,Thread-Modell>>). Ein Scancode bezeichnet eine physische Tastenposition (die Taste `A` ist auf jedem Layout Scancode `4`), ein Keysym dagegen das Zeichen, das die Taste erzeugt; die Tabelle `SCANCODE` in `Framework.singe` benennt jeden Scancode, `keyboardIsDown` nimmt Scancodes entgegen, und `onKeyPressed` liefert beides. Die Callbacks selbst sind unter <<enginecallbacks,Engine-Callbacks>> beschrieben.
|
|
|
|
[#keyboardcatchquit]
|
|
==== keyboardCatchQuit
|
|
|
|
[source,text]
|
|
----
|
|
keyboardCatchQuit(catch)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Übergibt den Beenden-Schalter an das Skript, genau wie `singeSetQuitKeyEnabled(not catch)`: `keyboardCatchQuit(true)` verhindert, dass die Engine bei `INPUT_QUIT` beendet wird, und `false` stellt das wieder her. Es ist eine in `Framework.singe` definierte Funktion, die erhalten bleibt, damit für Hypseus Singe geschriebene Spiele unverändert laufen.
|
|
|
|
*Hinweise:* Veralteter Alias.
|
|
|
|
WARNING: Neuer Code sollte <<singesetquitkeyenabled,singeSetQuitKeyEnabled>> aufrufen.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<singesetquitkeyenabled,singeSetQuitKeyEnabled>>, <<singequit,singeQuit>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Hypseus-Spielcode; die moderne Schreibweise ist 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()
|
|
----
|
|
|
|
Gibt den Scancode der Taste zurück, die im aktuellen Frame zuletzt gedrückt wurde, oder `0`, wenn seit dem letzten Frame nichts gedrückt wurde. Der Wert wird nach jedem Aufruf von `onOverlayUpdate` gelöscht, was ihn zu einer einmaligen Abfrage „wurde gerade eine Taste gedrückt“ für das Thread-Modell macht. Controller- und Maustasten werden hier ebenfalls festgehalten, als ihre Codes `GAMEPAD_N` und `MOUSE_N`, da die Engine sie über denselben Weg leitet wie Tasten. Der Wert wird unabhängig vom Tastaturmodus und von den Belegungen in `controls.cfg` festgehalten.
|
|
|
|
*Rückgabe:* ganzzahliger Scancode oder `0`.
|
|
|
|
*Seit:* 2.10
|
|
*Siehe auch:* <<keyboardgetlastup,keyboardGetLastUp>>, <<keyboardisdown,keyboardIsDown>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Thread-Modell: auf einen Tastendruck pro Frame reagieren.
|
|
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()
|
|
----
|
|
|
|
Gibt den Scancode der Taste zurück, die im aktuellen Frame zuletzt losgelassen wurde, oder `0`, wenn seit dem letzten Frame nichts losgelassen wurde. Er wird wie bei `keyboardGetLastDown` nach jedem Aufruf von `onOverlayUpdate` gelöscht und hält Controller- und Maustastencodes auf dieselbe Weise fest.
|
|
|
|
*Rückgabe:* ganzzahliger Scancode oder `0`.
|
|
|
|
*Seit:* 2.10
|
|
*Siehe auch:* <<keyboardgetlastdown,keyboardGetLastDown>>, <<keyboardisdown,keyboardIsDown>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein aufgeladener Schuss feuert, wenn die Feuertaste losgelassen wird.
|
|
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()
|
|
----
|
|
|
|
Gibt den geltenden Tastaturmodus zurück: `MODE_NORMAL` (`0`) oder `MODE_FULL` (`1`). Jedes Skript beginnt im `MODE_NORMAL`, auch nach einem Neuladen.
|
|
|
|
*Rückgabe:* ganzzahliger Modus.
|
|
|
|
*Seit:* 1.x (RDG)
|
|
*Siehe auch:* <<keyboardsetmode,keyboardSetMode>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Gemeinsame Hilfsfunktion für Texteingabe, die den Modus wiederherstellt, den der Aufrufer gerade hatte.
|
|
function promptForName()
|
|
local previous = keyboardGetMode()
|
|
keyboardSetMode(MODE_FULL)
|
|
entering = true
|
|
restoreMode = previous
|
|
end
|
|
----
|
|
|
|
[#keyboardgetmodifiers]
|
|
==== keyboardGetModifiers
|
|
|
|
[source,text]
|
|
----
|
|
modifiers = keyboardGetModifiers()
|
|
----
|
|
|
|
Gibt die gerade gehaltenen Modifikatortasten als Bitmaske aus SDL-`KMOD_*`-Bits zurück. Prüfe sie mit einem bitweisen Und gegen die Werte in der Tabelle `MODIFIER`: die Einträge für einzelne Tasten wie `MODIFIER.LSHIFT` und die zusammengefassten `MODIFIER.SHIFT`, `MODIFIER.CTRL`, `MODIFIER.ALT` und `MODIFIER.GUI`, die beide Seiten abdecken. Es liest den aktuellen Zustand, funktioniert also in jedem Tastaturmodus und aus jedem Callback.
|
|
|
|
*Rückgabe:* ganzzahlige Bitmaske.
|
|
|
|
*Seit:* 2.10
|
|
*Siehe auch:* <<keyboardgetlastdown,keyboardGetLastDown>>, <<onkeypressedonkeyreleased,onKeyPressed>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Umschalt+Tab blendet das Statistik-Overlay ein und aus.
|
|
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)
|
|
----
|
|
|
|
Prüft, ob eine Taste gerade physisch gehalten wird, anhand des Zustands, den die Engine für jedes Tastenereignis führt, das sie sieht. Es hängt weder vom Tastaturmodus noch von `controls.cfg` ab und funktioniert aus jedem Callback, eignet sich also für Bewegung, die weitergehen soll, solange eine Taste gehalten wird. Scancodes außerhalb des Bereichs, den SDL definiert, ergeben `false`, statt abzubrechen.
|
|
|
|
*Parameter:*
|
|
|
|
* `scancode` -- ein Wert aus der Tabelle `SCANCODE`, etwa `SCANCODE.LEFT.value`.
|
|
|
|
*Rückgabe:* Boolean.
|
|
|
|
*Seit:* 2.10
|
|
*Siehe auch:* <<keyboardgetlastdown,keyboardGetLastDown>>, <<controllergetbutton,controllerGetButton>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Bewegung in acht Richtungen aus gehaltenen Tasten.
|
|
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)
|
|
----
|
|
|
|
Wechselt zwischen den beiden Tastaturmodellen. Im `MODE_NORMAL` erreichen nur in `controls.cfg` belegte Tasten das Skript, als `SWITCH_*`-Werte, die Tastenwiederholung wird verworfen, sodass jeder Druck einmal ankommt, und die Engine reagiert auf ihre eigenen Schalter (Pause, Beenden, Screenshot, Maus einfangen). Im `MODE_FULL` erreicht jede Taste `onInputPressed` und `onInputReleased` als ihr Keysym und `onKeyPressed` und `onKeyReleased` als Keysym und Scancode, der Text, den ein Tastendruck erzeugt, erreicht `onTextInput`, die Tastenwiederholung wird für die Texteingabe geliefert, und Tastaturbelegungen der engineeigenen Schalter werden ignoriert, damit das Spiel jede Taste behält; Belegungen von Controller- und Maustasten wirken weiterhin, und diese Tasten erreichen das Skript weiterhin als ihre `SWITCH_*`-Werte, da sie nicht getippt werden können. Die meisten Spiele setzen den Modus einmal beim Start; ein Spiel mit Texteingabe wechselt für deren Dauer zu `MODE_FULL` und wieder zurück. Jeder andere Wert bricht das Skript ab. Im `MODE_FULL` sind die Keysyms von vier Tasten zugleich Schalternummern -- Rücktaste ist `8` (`SWITCH_BUTTON3`), Tab `9` (`SWITCH_COIN1`), Eingabetaste `13` und Esc `27` -- und ein Skript, das `onInputPressed` für einen Pad- oder Mausschalter liest und zugleich diese Tasten erwartet, sollte deshalb mit `keyboardIsDown` nach der Taste sehen, bevor es den Wert als den Schalter behandelt.
|
|
|
|
*Parameter:*
|
|
|
|
* `mode` -- `MODE_NORMAL` (`0`) oder `MODE_FULL` (`1`).
|
|
|
|
*Seit:* 1.x (RDG)
|
|
*Siehe auch:* <<keyboardgetmode,keyboardGetMode>>, <<oninputpressedoninputreleased,onInputPressed>>, <<onkeypressedonkeyreleased,onKeyPressed>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Highscore-Eingabe nimmt rohe Tasten, danach kehrt der Spielmodus zurück.
|
|
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]
|
|
=== Licht
|
|
|
|
Lichter sind Knoten, die ein Licht tragen, von `lightNew` erzeugt und mit den Knotenaufrufen bewegt, verknüpft und animiert. Bis zu acht sichtbare Lichter leuchten in einem Frame, in Knotenreihenfolge; `sceneSetAmbient` und ein Himmel aus `sceneSetSky` fügen Licht aus allen Richtungen hinzu. Richtungs- und Spotlichter leuchten entlang der -Z-Achse des Knotens, richte sie also mit `nodeLookAt` oder `nodeSetRotation` aus; Punktlichter brauchen nur eine Position. Farben sind Ganzzahlen von `0` bis `255`, Intensitäten sind Faktoren im linearen Licht der Szene, Winkel sind in Grad und Abstände in Welteinheiten. Jeder `lightSet*`-Aufruf beendet das Skript bei einem Knoten, der kein Licht trägt. Siehe <<scenes3d,3D-Szenen>> zu Beleuchtung, Belichtung und Schatten.
|
|
|
|
[#lightnew]
|
|
==== lightNew
|
|
|
|
[source,text]
|
|
----
|
|
node = lightNew(type)
|
|
node = lightNew(type, parent)
|
|
----
|
|
|
|
Erzeugt einen neuen Knoten, der ein Licht trägt, und gibt das Knoten-Handle zurück. Das Licht beginnt weiß mit Intensität `1`, ohne Reichweitenbegrenzung und, bei einem Spot, mit einem Kegel von `20` bis `30` Grad. Platziere und richte es mit den Knotenaufrufen aus: `nodeSetPosition` für ein Punkt- oder Spotlicht, `nodeLookAt` für ein Richtungs- oder Spotlicht. Ein Licht, das einem sich bewegenden Knoten untergeordnet ist, bewegt sich mit ihm, sodass ein Scheinwerfer an einem Auto oder eine Fackel in einer Hand nur ein Aufruf ist. Beendet das Skript bei einem unbekannten Typ oder wenn kein Knoten erzeugt werden kann.
|
|
|
|
*Parameter:*
|
|
|
|
* `type` -- `LIGHT_DIRECTIONAL` (eine Sonne; nur ihre Richtung zählt), `LIGHT_POINT` (eine Glühbirne, die mit der Entfernung in alle Richtungen schwächer wird) oder `LIGHT_SPOT` (ein Kegel entlang der -Z-Achse des Knotens).
|
|
* `parent` -- der Elternknoten; die Wurzel, wenn er weggelassen wird.
|
|
|
|
*Rückgabe:* Das Knoten-Handle des Lichts, eine Ganzzahl.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<lightsetcolor,lightSetColor>>, <<lightsetintensity,lightSetIntensity>>, <<lightsetrange,lightSetRange>>, <<lightsetcone,lightSetCone>>, <<scenesetambient,sceneSetAmbient>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Warmes Führungslicht von links oben, ein kühles Aufhelllicht von rechts
|
|
-- und ein Scheinwerfer, der mit dem Auto mitfährt.
|
|
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)
|
|
----
|
|
|
|
Die Farbe des Lichts, `0` bis `255` pro Kanal (Werte außerhalb des Bereichs werden begrenzt), als sRGB behandelt; standardmäßig weiß. Die Helligkeit gehört in `lightSetIntensity`, nutze die Farbe also für den Farbton (eine warme Glühbirne `255, 230, 190`, ein kühler Mond `150, 190, 255`) und lass mindestens einen Kanal nahe `255`. Beendet das Skript, wenn der Knoten kein Licht trägt.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<lightsetintensity,lightSetIntensity>>, <<lightnew,lightNew>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Leuchtreklame wechselt durch ihre Farben.
|
|
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)
|
|
----
|
|
|
|
Die Form des Kegels eines Spotlichts, beide Winkel von der Achse aus gemessen (der -Z-Achse des Knotens): volle Helligkeit innerhalb von `innerDegrees`, die bis `outerDegrees` sanft auf nichts abfällt. Die Standardwerte sind `20` und `30`. Ein schmaler Abstand zwischen beiden ergibt einen hart umrandeten Bühnenspot, ein breiter einen weichen Lichtfleck. Die Werte werden für jedes Licht gespeichert, aber nur ein `LIGHT_SPOT` verwendet sie. Beendet das Skript, wenn der Knoten kein Licht trägt.
|
|
|
|
*Parameter:*
|
|
|
|
* `node` -- der Knoten des Lichts.
|
|
* `innerDegrees` -- halber Winkel der vollen Helligkeit, in Grad von der Achse.
|
|
* `outerDegrees` -- halber Winkel, bei dem das Licht nichts mehr erreicht, in Grad von der Achse; wird auf `innerDegrees` angehoben, wenn er kleiner ist.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<lightnew,lightNew>>, <<lightsetrange,lightSetRange>>, <<nodelookat,nodeLookAt>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein hart umrandeter Bühnenspot aus dem Lichtrigg auf den Sänger.
|
|
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)
|
|
----
|
|
|
|
Der Helligkeitsfaktor des Lichts im linearen Licht der Szene, standardmäßig `1`. Ein Richtungslicht der Intensität `1` beleuchtet eine ihm zugewandte weiße Fläche bis zu Weiß. Ein Punkt- oder Spotlicht fällt mit der Entfernung als `1 / (1 + d^2)` ab, seine Intensität ist also die Helligkeit am Licht selbst, die Hälfte davon eine Einheit entfernt und ein Hundertstel zehn Einheiten entfernt; Glühbirnen brauchen meist Werte deutlich über `1`, `10` bis `50` für einen Raum, und `sceneSetExposure` holt die ganze Szene wieder in den Bereich. Beendet das Skript, wenn der Knoten kein Licht trägt.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<lightsetrange,lightSetRange>>, <<scenesetexposure,sceneSetExposure>>, <<lightsetcolor,lightSetColor>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Laterne, die im Wind flackert.
|
|
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)
|
|
----
|
|
|
|
Wie weit ein Punkt- oder Spotlicht reicht, in Welteinheiten; `0` (der Standard) bedeutet keine Begrenzung. Das Licht fällt an der Reichweite zusätzlich zu seinem Abfall mit der Entfernung auf genau nichts ab, sodass Lichter nicht aufploppen, wenn Objekte sie überqueren. Eine Reichweite begrenzt auch, wie weit die Shadow Map eines schattenwerfenden Lichts reichen muss, was seine Schatten scharf hält. Richtungslichter ignorieren sie. Beendet das Skript, wenn der Knoten kein Licht trägt.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<lightsetintensity,lightSetIntensity>>, <<lightsetshadow,lightSetShadow>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Straßenlaternen die Straße entlang, jede beleuchtet ihr eigenes Stück.
|
|
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)
|
|
----
|
|
|
|
Lässt das Licht Schatten von allem Opaken in der Szene werfen oder beendet das; standardmäßig aus. Jedes der acht Lichter darf Schatten werfen, in beliebiger Kombination. Ein Spotlicht erhält eine Shadow Map; ein Richtungslicht erhält eine Map pro Kaskade (`sceneSetShadowCascades`); ein Punktlicht erhält eine Cube Map aus sechs Ansichten, die jede Richtung abdeckt (eine Glühbirne in einem Raum), zum sechsfachen Preis. `nodeSetShadow` befreit ein Mesh vom Schattenwerfen. Beendet das Skript, wenn der Knoten kein Licht trägt.
|
|
|
|
*Hinweise:* Jede Shadow Map kostet pro Frame einen zusätzlichen Durchgang über die Szene, eine Cube Map sechs und eine kaskadierte Sonne so viele, wie sie Kaskaden hat; die Größe der Maps wird mit `sceneSetShadowSize` gesetzt. Eine moderne integrierte GPU schafft acht Schattenwerfer bequem; ein Raspberry Pi will einen oder zwei bei `512`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<scenesetshadowsize,sceneSetShadowSize>>, <<scenesetshadowcascades,sceneSetShadowCascades>>, <<nodesetshadow,nodeSetShadow>>, <<lightnew,lightNew>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Sonne wirft Schatten; die Lampen leuchten nur. Das Glas der Glühbirne
|
|
-- darf seine eigene Lampe nicht verschatten.
|
|
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
|
|
|
|
Ein Material bestimmt, wie ein Mesh aussieht: eine Grundfarbe mit Alpha, eine optionale Grundtextur aus einem Sprite, einer KTX2-Datei, einem Video oder einer gerenderten Ansicht, und die Metallic-Roughness-Parameter von glTF mit vier optionalen Maps. `materialNew` gibt ein ganzzahliges Handle zurück; übergib es an `nodeSetMesh` oder `nodeSetMaterial` und an jeden `materialSet*`-Aufruf. Materialien werden gemeinsam genutzt, eine Änderung an einem ändert also jedes Mesh, das damit gezeichnet wird, und ein Knoten ohne Material wird weiß und halb rau gezeichnet. Farben sind Ganzzahlen von `0` bis `255` pro Kanal und werden als sRGB behandelt; Faktoren wie die Rauheit reichen von `0` bis `1`. Ein ungültiges Handle beendet das Skript. Siehe <<scenes3d,3D-Szenen>> dazu, wie Materialien beleuchtet werden.
|
|
|
|
[#materialnew]
|
|
==== materialNew
|
|
|
|
[source,text]
|
|
----
|
|
material = materialNew()
|
|
----
|
|
|
|
Legt ein neues Material an und gibt sein Handle zurück. Es beginnt weiß und deckend (Farbe `255, 255, 255, 255`), mit Rauheit `0.5`, Metallic `0`, ohne Emissionsfarbe, mit Kachelung `1, 1`, linearer Filterung, einseitig, beleuchtet und ohne Textur oder Maps. Setz, was davon abweicht, und übergib das Handle an `nodeSetMesh`. Geladene Modelle bringen ihre eigenen Materialien mit, dies ist also für Meshes, die im Skript gebaut werden. Beendet das Skript auf einem Rechner ohne nutzbare GPU.
|
|
|
|
*Rückgabe:* Das Material-Handle, eine Ganzzahl.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<materialsetcolor,materialSetColor>>, <<materialsettexture,materialSetTexture>>, <<materialdelete,materialDelete>>, <<nodesetmesh,nodeSetMesh>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Gebürstetes Gold für einen Pokal, durchscheinendes blaues Glas für seine Vitrine.
|
|
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)
|
|
----
|
|
|
|
Gibt das Material und jede Textur frei, die es hochgeladen hat. Jeder Knoten, der damit gezeichnet wurde, fällt auf das Standardaussehen zurück (weiß, halb rau); weise diesen Knoten also zuerst mit `nodeSetMaterial` ein anderes zu, wenn sie ein Aussehen behalten sollen. Das Handle ist danach ungültig und beendet das Skript, wenn es erneut verwendet wird. Das private Material, das ein Knoten unter `nodeSetSprite` oder `nodeSetText` für sich selbst anlegt, gehört dem Knoten und kann nicht gelöscht werden; der Aufruf beendet das Skript, und es wird freigegeben, wenn das Sprite oder der Text entfernt wird. Materialien sind billig; sie zu löschen ist vor allem bei solchen wichtig, die große Texturen trugen.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<materialnew,materialNew>>, <<nodesetmaterial,nodeSetMaterial>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Aussehen des Bodens im Level austauschen, wenn die Jahreszeit wechselt.
|
|
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)
|
|
----
|
|
|
|
Bei `true` wird das Material durchscheinend gezeichnet, gemischt nach dem Alpha der Grundfarbe multipliziert mit dem Alpha der Textur, nach jedem deckenden Mesh und vom entferntesten an sortiert. Gemischte Meshes schreiben keine Tiefe, lass es also für feste Oberflächen auf `false` (der Standard): Bei einem deckenden Mesh mit eingeschaltetem Mischen können Dinge dahinter durchscheinen. Verwende es mit einem Alpha unter `255` aus `materialSetColor` oder einer Textur mit Transparenz.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<materialsetcolor,materialSetColor>>, <<materialsettexture,materialSetTexture>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Kraftfeld, das sichtbar wird, wenn es getroffen wird.
|
|
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)
|
|
----
|
|
|
|
Setzt die Grundfarbe, `0` bis `255` pro Kanal (Werte außerhalb des Bereichs werden begrenzt), wobei `a` standardmäßig `255` ist. Die Farbe wird als sRGB behandelt und in linearem Licht beleuchtet. Hat das Material eine Textur, ein Video oder eine Ansicht, wird diese mit der Farbe multipliziert, Weiß lässt das Bild also, wie es ist, und eine dunklere Farbe tönt es. Das Alpha wirkt nur mit `materialSetBlend`.
|
|
|
|
*Parameter:*
|
|
|
|
* `material` -- das Material-Handle.
|
|
* `r, g, b` -- Rot, Grün und Blau, `0` bis `255`.
|
|
* `a` -- Deckkraft, `0` bis `255`; `255`, wenn weggelassen.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<materialsettexture,materialSetTexture>>, <<materialsetblend,materialSetBlend>>, <<materialsetemissive,materialSetEmissive>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Teamfarben auf dem einen Auto-Mesh.
|
|
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)
|
|
----
|
|
|
|
Zeichnet beide Seiten jedes Dreiecks, statt die Rückseite zu verwerfen. Nötig für offene Formen, die von hinten gesehen werden, etwa eine einzelne `meshPlane`, die von unten betrachtet wird, eine Flagge oder eine Blattkarte. Geschlossene Formen sehen so oder so gleich aus, lass es bei ihnen also auf `false` (der Standard): Ein doppelseitiges Material kostet Füllrate, und Schatten werden ebenfalls von beiden Seiten geworfen.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<meshplane,meshPlane>>, <<materialsetblend,materialSetBlend>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Banner, das über der Straße hängt, von beiden Seiten lesbar.
|
|
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)
|
|
----
|
|
|
|
Das Licht, das die Oberfläche von sich aus abgibt, `0` bis `255` pro Kanal (Werte außerhalb des Bereichs werden begrenzt), zusätzlich zur Beleuchtung: Bildschirme, Glühbirnen, Neon, Instrumententafeln. Der Standard ist Schwarz, kein Leuchten. Es beleuchtet nichts anderes und wirft kein Licht; setz dafür einen Lichtknoten an dieselbe Stelle. Mit `materialSetEmissiveMap` gibt die Map an, wo auf der Oberfläche diese Farbe gilt. Emittierende Oberflächen, die hell genug sind, um die Schwelle von `sceneSetBloom` zu überschreiten, leuchten.
|
|
|
|
*Hinweise:* Ein unbeleuchtetes Material (`materialSetUnlit`) zeigt nur seine Grundfarbe; die Emissionsfarbe wird darauf ignoriert.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<materialsetemissivemap,materialSetEmissiveMap>>, <<materialsetunlit,materialSetUnlit>>, <<scenesetbloom,sceneSetBloom>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Warnlampe, die rot pulsiert, wenn der Reaktor überhitzt.
|
|
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)
|
|
----
|
|
|
|
Eine Textur, die angibt, welche Teile der Oberfläche leuchten. Ihre Farbe wird mit der Farbe aus `materialSetEmissive` multipliziert, setz diese also ebenfalls: Weiß, um die eigenen Farben der Map zu verwenden, oder eine Tönung. Das Bild stammt aus einem geladenen Sprite (eine Kopie wird hochgeladen, das Sprite darf danach also entladen werden) oder aus einer `.ktx2`-Datei nach Namen, komprimiert auf der GPU gehalten. Mit `nil` oder ohne zweites Argument wird die Map entfernt. Beendet das Skript, wenn die Textur nicht hochgeladen werden kann oder die Datei keine KTX2-Textur ist.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<materialsetemissive,materialSetEmissive>>, <<materialsettexture,materialSetTexture>>, <<materialsettiling,materialSetTiling>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Bedienfeld, dessen Knöpfe und Anzeigen aufleuchten.
|
|
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)
|
|
----
|
|
|
|
Wie die Texturen des Materials abgetastet werden. `FILTER_LINEAR` (der Standard) mischt zwischen Texeln und zwischen Mipmap-Stufen, mit anisotroper Filterung auf Oberflächen, die unter flachem Winkel gesehen werden. `FILTER_NEAREST` nimmt das nächstgelegene Texel und die nächstgelegene Mipmap-Stufe, sodass Pixel-Art auf einem Quad scharf bleibt. Jede Textur trägt eine vollständige Mipmap-Kette, eine klein gesehene Textur flimmert also in keinem Fall. Jeder andere Wert beendet das Skript.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<materialsettexture,materialSetTexture>>, <<materialsetunlit,materialSetUnlit>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Kachel aus 16 mal 16 Pixeln, ohne Verschmieren auf einen Block gezeichnet.
|
|
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)
|
|
----
|
|
|
|
Zeigt eine GUI aus `guiNew` als Grundfarbtextur des Materials, in jedem Frame aktualisiert: ein Monitor, ein Cockpit-Bedienfeld, ein Schild, mit demselben Dokument, das ein `guiDraw` flach zeigen würde. Meist kombiniert mit `materialSetUnlit`, damit die Seite nicht von den Lichtern schattiert wird, und wie jede Textur von der Grundfarbe getönt. Mit `nil` oder ohne zweites Argument kehrt das Material zu seiner eigenen Textur zurück, die darunter erhalten bleibt; das Löschen der GUI bewirkt dasselbe. Das Setzen einer GUI verwirft jedes Video und jede Ansicht, die das Material zeigte. Solange die GUI Eingaben annimmt, erreichen die Maus oder eine Lightgun, die auf eine mit dem Material gezeichnete Oberfläche zeigt, sie über dieselbe Abtastung wie `sceneProbeGui`. Beendet das Skript bei einem ungültigen GUI-Handle.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<guinew,guiNew>>, <<guidraw,guiDraw>>, <<sceneprobegui,sceneProbeGui>>, <<materialsetview,materialSetView>>, <<materialsetvideo,materialSetVideo>>, <<materialsetunlit,materialSetUnlit>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Optionsseite auf dem Bildschirm des modellierten Automaten.
|
|
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)
|
|
----
|
|
|
|
Wie metallisch die Oberfläche ist, `0` für Nichtmetalle (Lack, Kunststoff, Holz, Haut) bis `1` für Metalle, die ihre Glanzlichter und Spiegelungen mit der Grundfarbe tönen. Der Standard ist `0`. Werte dazwischen sind für Mischungen und abgenutzte Oberflächen; die meisten Materialien wollen das eine oder das andere Ende. Mit `materialSetMetallicRoughnessMap` multipliziert der Blaukanal der Map diesen Wert. Ein Metall hat ohne einen Himmel aus `sceneSetSky` wenig zu spiegeln, kombiniere also beides.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<materialsetroughness,materialSetRoughness>>, <<materialsetmetallicroughnessmap,materialSetMetallicRoughnessMap>>, <<scenesetsky,sceneSetSky>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Verchromte Stoßstange, lackierte Karosserie.
|
|
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)
|
|
----
|
|
|
|
Eine Textur, die die Oberflächenbeschaffenheit über die Fläche variiert: Ihr Grünkanal multipliziert die Rauheit aus `materialSetRoughness` und ihr Blaukanal den Metallic-Wert aus `materialSetMetallic`, so wie glTF sie packt, eine Map aus einem glTF-Werkzeug funktioniert also unverändert. Setz beide Faktoren auf `1`, um allein die Werte der Map zu verwenden. Das Bild stammt aus einem Sprite oder einer `.ktx2`-Datei nach Namen; `nil` oder kein zweites Argument entfernt die Map. Beendet das Skript, wenn die Textur nicht hochgeladen werden kann.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<materialsetmetallic,materialSetMetallic>>, <<materialsetroughness,materialSetRoughness>>, <<materialsettexture,materialSetTexture>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Zerkratzte Metallbeplankung: glänzend, wo der Lack abgeschabt ist.
|
|
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)
|
|
----
|
|
|
|
Eine Normal Map im Tangentenraum (die übliche bläuliche Art, flach bei `128, 128, 255`), die die Beleuchtung pro Texel neigt, sodass Ziegel, Rillen und Nieten ohne zusätzliche Geometrie das Licht einfangen. Jedes Mesh hat Tangenten dafür: die eigenen eines Modells, wenn die Datei sie enthält, sonst aus den UVs berechnet. Das Bild stammt aus einem Sprite oder einer `.ktx2`-Datei nach Namen; `nil` oder kein zweites Argument entfernt die Map. Beendet das Skript, wenn die Textur nicht hochgeladen werden kann.
|
|
|
|
*Parameter:*
|
|
|
|
* `material` -- das Material-Handle.
|
|
* `sprite` oder `"file.ktx2"` -- die Map, aus einem geladenen Sprite oder einer KTX2-Datei; `nil` oder weggelassen entfernt sie.
|
|
* `strength` -- wie stark die Unebenheiten die Beleuchtung neigen, `1` (der Standard) wie erstellt, `0` flach, über `1` übertrieben.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<materialsettexture,materialSetTexture>>, <<materialsetocclusionmap,materialSetOcclusionMap>>, <<materialsettiling,materialSetTiling>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Ziegelmauer aus einer flachen Ebene, deren Mörtelfugen die tiefe Sonne einfangen.
|
|
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)
|
|
----
|
|
|
|
Lässt das Material Tiefe und keine Farbe schreiben. Was das Mesh verdeckt, ist
|
|
verborgen, als wäre das Mesh massiv, aber das Mesh selbst ist nicht zu sehen: Alles dahinter
|
|
Gezeichnete -- ein gemalter Hintergrund, die Disc -- scheint durch. Es ist
|
|
der Stellvertreter für etwas, das im Bild, aber nicht in der Szene ist: eine Säule in
|
|
einem gemalten Raum, hinter der eine 3D-Figur hergehen muss, ein Türrahmen, ein Tisch.
|
|
Ein Occluder wirft keinen Schatten, da das Gemälde schon einen hat, und nimmt
|
|
kein Licht an. `false` (der Standard) macht das Material wieder gewöhnlich.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<materialsetblend,materialSetBlend>>, <<materialsetunlit,materialSetUnlit>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Säule, die im Gemälde ist: eine unsichtbare Box, wo sie steht, damit
|
|
-- der Held dahinter verschwindet und auf der anderen Seite wieder auftaucht.
|
|
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)
|
|
----
|
|
|
|
Eine Ambient-Occlusion-Map, die Verdeckung im Rotkanal (Weiß für offen, dunkel für Spalten), die das Umgebungs- und Himmelslicht in Ecken und Falten abdunkelt. Direktes Licht von Lichtknoten ist davon nicht betroffen, wie in glTF. Das Bild stammt aus einem Sprite oder einer `.ktx2`-Datei nach Namen; `nil` oder kein zweites Argument entfernt die Map. Beendet das Skript, wenn die Textur nicht hochgeladen werden kann.
|
|
|
|
*Parameter:*
|
|
|
|
* `material` -- das Material-Handle.
|
|
* `sprite` oder `"file.ktx2"` -- die Map, aus einem geladenen Sprite oder einer KTX2-Datei; `nil` oder weggelassen entfernt sie.
|
|
* `strength` -- wie viel von der Map angewendet wird, von `0`, gar nicht, bis `1` (der Standard), voll.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<materialsetnormalmap,materialSetNormalMap>>, <<scenesetambient,sceneSetAmbient>>, <<scenesetenvironment,sceneSetEnvironment>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Vorberechnete Schattierung für die Höhlenwände, halbe Stärke, damit die Lampen die Ecken noch erreichen.
|
|
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)
|
|
----
|
|
|
|
Wie rau die Oberfläche ist, `0` für ein spiegelscharfes Glanzlicht bis `1` für matt. Der Standard ist `0.5`. Glanzlichter folgen dem GGX-Modell, niedrige Werte ergeben also einen kleinen hellen Fleck und scharfe Himmelsspiegelungen, hohe Werte einen breiten weichen Schimmer und verschwommene Spiegelungen. Mit `materialSetMetallicRoughnessMap` multipliziert der Grünkanal der Map diesen Wert.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<materialsetmetallic,materialSetMetallic>>, <<materialsetmetallicroughnessmap,materialSetMetallicRoughnessMap>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Straße wird nass, wenn es regnet: derselbe Asphalt, schärfere Glanzlichter.
|
|
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)
|
|
----
|
|
|
|
Verwendet das Bild eines Sprites als Grundfarbtextur (eine Kopie wird hochgeladen, das Sprite darf danach also entladen werden) oder eine KTX2-Datei nach Namen, die auf der GPU im Blockformat komprimiert gehalten wird, das der Rechner hat (siehe Komprimierte Texturen unter <<scenes3d,3D-Szenen>>). Mit `nil` oder ohne zweites Argument wird die Textur entfernt, und das Material zeigt nur seine Grundfarbe. Das Setzen einer Textur verwirft außerdem jedes Video und jede Ansicht, die das Material zeigte. Texturkoordinaten stammen aus dem Mesh; die Grundformen legen ein Bild einmal über jede Fläche, und `materialSetTiling` wiederholt es. Ein Name, der keine KTX2-Datei ist, beendet das Skript (lade andere Bilder mit `spriteLoad`), ebenso ein fehlgeschlagenes Hochladen.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<spriteload,spriteLoad>>, <<materialsettiling,materialSetTiling>>, <<materialsetvideo,materialSetVideo>>, <<materialsetview,materialSetView>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Kiste, texturiert mit einem Sprite, das das Spiel ohnehin für das Menü lädt,
|
|
-- und ein Boden aus einer komprimierten Textur, die mit dem Spiel ausgeliefert wird.
|
|
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)
|
|
----
|
|
|
|
Wie oft sich die Texturen des Materials (Grundtextur und Maps gleichermaßen) über den UV-Bereich `0` bis `1` einer Oberfläche wiederholen, standardmäßig `1` und `1`. `24, 24` auf einer 120 Einheiten breiten Heightmap kachelt eine Textur alle fünf Einheiten. Texturen wiederholen sich über ihre Ränder hinaus, das Bild muss also nahtlos kacheln. Werte unter `1` zeigen einen Teil des Bildes; negative Werte spiegeln es.
|
|
|
|
*Parameter:*
|
|
|
|
* `material` -- das Material-Handle.
|
|
* `u` -- Wiederholungen in U-Richtung der Oberfläche.
|
|
* `v` -- Wiederholungen in V-Richtung der Oberfläche.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<materialsettexture,materialSetTexture>>, <<meshheightmap,meshHeightmap>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Gras alle fünf Einheiten über die Hügel.
|
|
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)
|
|
----
|
|
|
|
Zeigt die Grundfarbe und die Textur genau so, wie sie sind, und ignoriert jedes Licht, das Umgebungslicht und den Himmel: für Videobildschirme, Monitore, die eine Ansicht zeigen, Skyboxen und flach schattierte Looks. Nebel wirkt weiterhin. Die Emissionsfarbe und die Maps werden bei einem unbeleuchteten Material nicht verwendet. Der Standard ist `false`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<materialsetemissive,materialSetEmissive>>, <<materialsetvideo,materialSetVideo>>, <<materialsetview,materialSetView>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Marquee des Arcade-Automaten: ein Bild, das von innen beleuchtet ist,
|
|
-- die Lichter des Raums sollen es also nicht schattieren.
|
|
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)
|
|
----
|
|
|
|
Zeigt eine gerenderte Ansicht aus `viewNew` als Grundfarbtextur des Materials, in jedem Frame aktualisiert: ein Überwachungsmonitor, ein Rückspiegel, ein Portal. Meist kombiniert mit `materialSetUnlit`, damit das Bild nicht von den Lichtern schattiert wird, und wie jede Textur von der Grundfarbe getönt. Mit `nil` oder ohne zweites Argument kehrt das Material zu seiner eigenen Textur zurück, die darunter erhalten bleibt. Das Setzen einer Ansicht verwirft jedes Video, das das Material zeigte. Beendet das Skript bei einem ungültigen Ansichts-Handle.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<viewnew,viewNew>>, <<viewsetcamera,viewSetCamera>>, <<materialsetvideo,materialSetVideo>>, <<materialsetunlit,materialSetUnlit>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Rückspiegel am Auto des Spielers.
|
|
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)
|
|
----
|
|
|
|
Verwendet die Laserdisc (ohne `video`) oder ein mit `videoLoad` geladenes Video als Grundfarbtextur, in jedem Frame aktualisiert, solange es läuft. Meist kombiniert mit `materialSetUnlit`, damit das Bild nicht von den Lichtern getönt wird. Das Video läuft weiter, ob es zusätzlich auf dem Overlay gezeichnet wird oder nicht, und mehrere Materialien dürfen dasselbe zeigen. Das Setzen eines Videos verwirft jede Textur und jede Ansicht, die das Material hatte; `materialSetTexture` entfernt es wieder.
|
|
|
|
*Hinweise:* Die Disc-Form beendet das Skript in einem Spiel ohne Disc, ebenso ein ungültiges Video-Handle.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<videoload,videoLoad>>, <<materialsetunlit,materialSetUnlit>>, <<materialsettexture,materialSetTexture>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein modellierter Automat, dessen Bildschirm die Laserdisc zeigt, und eine Wand aus
|
|
-- Monitoren, die einen Attract-Clip in Schleife abspielen.
|
|
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]
|
|
=== Mesh
|
|
|
|
Ein Mesh ist Geometrie auf der GPU: eine Grundform, die die Engine baut, ein
|
|
Gelände aus einem Bild oder Vertexdaten aus deinem Skript. Handles sind
|
|
Ganzzahlen, die von den `mesh*`-Konstruktoren zurückgegeben werden; beliebig
|
|
viele Knoten können sich ein Mesh teilen, und Knoten, die dasselbe Mesh mit
|
|
demselben Material zeichnen, werden als ein instanzierter Aufruf gezeichnet,
|
|
erzeuge also ein Mesh und viele Knoten. Größen sind in Szeneneinheiten, und
|
|
jede Grundform ist auf den Ursprung ihres Knotens zentriert, sofern nicht
|
|
anders angegeben, sodass die Transformation des Knotens sie platziert, dreht
|
|
und skaliert. Texturkoordinaten laufen einmal über jede Fläche oder
|
|
Oberfläche von `0` bis `1`. Jeder Konstruktor braucht den unter
|
|
<<scenes3d,3D-Szenen>> beschriebenen 3D-Renderer und bricht das Skript ab,
|
|
wenn dieser nicht verfügbar ist.
|
|
|
|
[#meshbox]
|
|
==== meshBox
|
|
|
|
[source,text]
|
|
----
|
|
mesh = meshBox(width, height, depth)
|
|
----
|
|
|
|
Ein Quader, `width` entlang X, `height` entlang Y und `depth` entlang Z,
|
|
auf den Ursprung zentriert. Jede der sechs Flächen hat ihre eigenen vier
|
|
Vertices mit einer flachen Normale, sodass Kanten scharf schattiert werden,
|
|
und die Textur wird einmal über jede Fläche gelegt. Kisten, Wände, Böden und
|
|
Autokarosserien sind allesamt Quader.
|
|
|
|
*Rückgabe:* Das Mesh-Handle.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<nodesetmesh,nodeSetMesh>>, <<meshplane,meshPlane>>, <<bodynew,bodyNew>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine steinerne Bodenplatte und ein Stapel Kisten, die sich ein Mesh teilen.
|
|
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)
|
|
----
|
|
|
|
Ein auf Y stehender Kegel mit geschlossener Grundfläche: der Grundkreis mit
|
|
`radius` bei `-height / 2` und die Spitze bei `+height / 2`. `segments`
|
|
(Standard `24`, mindestens `3`) gibt an, wie viele Facetten ihn umlaufen; die
|
|
Seiten werden über sie hinweg weich schattiert. Die Textur legt sich einmal
|
|
um die Seiten und einmal über die Grundfläche.
|
|
|
|
*Rückgabe:* Das Mesh-Handle.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<meshcylinder,meshCylinder>>, <<nodesetmesh,nodeSetMesh>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Lampenschirm über der Glühbirne und eine Reihe Kiefern aus je einem Kegel.
|
|
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)
|
|
----
|
|
|
|
Ein Zylinder entlang Y, an beiden Enden geschlossen, von `-height / 2` bis
|
|
`+height / 2`. `segments` (Standard `24`, mindestens `3`) gibt an, wie viele
|
|
Facetten ihn umlaufen, weich schattiert. Für ein Rad oder eine Münze dreh den
|
|
Knoten um `90` Grad um X oder Z, sodass die Achse dort liegt, wo du sie
|
|
brauchst.
|
|
|
|
*Rückgabe:* Das Mesh-Handle.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<meshcone,meshCone>>, <<meshtorus,meshTorus>>, <<nodesetrotation,nodeSetRotation>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Laternenpfahl und ein auf die Seite gelegter Reifen als Rad.
|
|
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)
|
|
----
|
|
|
|
Gibt die GPU-Puffer des Meshs und seine CPU-Kopie frei. Knoten, die es
|
|
verwendet haben, bleiben ohne Mesh (und ohne Morph-Gewichte) zurück und
|
|
zeichnen nichts, bis sie ein anderes erhalten; das Handle wird ungültig und
|
|
kann von einem späteren Konstruktor wiederverwendet werden. Meshes werden
|
|
nicht freigegeben, wenn ein Knoten gelöscht wird, lösch ein Mesh also selbst,
|
|
wenn nichts es mehr verwenden wird.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<meshnew,meshNew>>, <<nodesetmesh,nodeSetMesh>>, <<nodedelete,nodeDelete>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Strecke zwischen den Runden aus einer neuen Heightmap neu bauen.
|
|
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)
|
|
----
|
|
|
|
Ein Geländemesh aus einem Graustufenbild, `2` bis `1025` Pixel in jede
|
|
Richtung: ein Vertex pro Pixel über `sizeX` mal `sizeZ` Einheiten, auf den
|
|
Knoten zentriert, jeder angehoben um den Rotkanal des Pixels (`0` Schwarz bis
|
|
`255` Weiß) mal `sizeY`, wobei die oberste Zeile des Bildes am fernen
|
|
(`-Z`-)Rand liegt. Normalen werden aus den Steigungen berechnet, und die
|
|
Texturkoordinaten laufen über das Ganze von `0` bis `1`, sodass
|
|
`materialSetTiling` die Wiederholung der Textur festlegt. Die Höhenwerte
|
|
werden behalten: `terrainGetHeight` liest sie, und ein `SHAPE_MESH`-Körper auf
|
|
dem Knoten wird zu einem Höhenfeld. Eine Datei, die sich nicht öffnen oder
|
|
dekodieren lässt, die falsche Größe oder kein 3D-Renderer bricht das Skript
|
|
ab.
|
|
|
|
*Parameter:*
|
|
|
|
* `image` -- der spielrelative Name des Bildes, in jedem Format, das `spriteLoad` liest.
|
|
* `sizeX` -- die Breite des Geländes entlang X in Welteinheiten.
|
|
* `sizeY` -- die Höhe eines weißen Pixels über einem schwarzen.
|
|
* `sizeZ` -- die Tiefe des Geländes entlang Z in Welteinheiten.
|
|
|
|
*Rückgabe:* Das Mesh-Handle.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<terraingetheight,terrainGetHeight>>, <<materialsettiling,materialSetTiling>>, <<bodynew,bodyNew>>
|
|
|
|
.Beispiel
|
|
[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)
|
|
-- Die Füße des Drachen auf dem Hang, wo immer der Boden dort ist.
|
|
nodeSetPosition(dragon, -8, terrainGetHeight(hills, -8, 3) or 0, 3)
|
|
----
|
|
|
|
[#meshnew]
|
|
==== meshNew
|
|
|
|
[source,text]
|
|
----
|
|
mesh = meshNew(positions, normals, uvs, indices)
|
|
----
|
|
|
|
Baut Geometrie aus Zahlentabellen. Tangenten für Normal Mapping werden für
|
|
dich berechnet, und ebenso weiche Normalen, wenn `normals` `nil` ist, indem
|
|
die Dreiecke gemittelt werden, die sich jeden Vertex teilen. Dreiecke zeigen
|
|
nach vorn, wenn ihre Vertices von außen gesehen gegen den Uhrzeigersinn
|
|
laufen; gib dem Material `materialSetDoubleSided`, wenn die Rückseite zu sehen
|
|
sein muss. Eine Tabelle der falschen Form oder ein Index außerhalb der
|
|
Vertexliste bricht das Skript mit einer Meldung ab, die sagt, was davon.
|
|
|
|
*Parameter:*
|
|
|
|
* `positions` -- `x, y, z` für jeden Vertex, einer nach dem anderen; mindestens ein Vertex.
|
|
* `normals` -- `x, y, z` pro Vertex, gleich viele wie in `positions`, oder `nil`, um sie berechnen zu lassen.
|
|
* `uvs` -- `u, v` pro Vertex (`0` bis `1`, `v` im Bild nach unten) oder `nil` für keine (jeder Vertex bei `0, 0`).
|
|
* `indices` -- drei Vertexnummern pro Dreieck, ab `1` gezählt in der Reihenfolge, in der die Vertices in `positions` stehen; mindestens ein Dreieck.
|
|
|
|
*Rückgabe:* Das Mesh-Handle.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<nodesetmesh,nodeSetMesh>>, <<materialsetdoublesided,materialSetDoubleSided>>, <<meshdelete,meshDelete>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein flacher Pfeil entlang +X, von oben gesehen (doppelseitig, damit er auch von unten funktioniert).
|
|
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)
|
|
----
|
|
|
|
Ein flaches Rechteck in der XZ-Ebene, nach +Y gewandt, `width` entlang X und
|
|
`depth` entlang Z, auf den Ursprung zentriert, mit der Textur einmal darüber
|
|
gelegt. Zwei Argumente ergeben ein einzelnes Quad. `columns` (und `rows`, das
|
|
standardmäßig `columns` ist) unterteilen es in so viele Quads in jede
|
|
Richtung, was ein Soft Body (`softNew`) braucht, um sich wie Stoff zu biegen;
|
|
beide müssen mindestens `1` sein. Dreh den Knoten um `90` Grad um X, um es
|
|
nach +Z gewandt aufzurichten. Es ist einseitig, sofern sein Material nicht
|
|
doppelseitig ist.
|
|
|
|
*Rückgabe:* Das Mesh-Handle.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<materialsetdoublesided,materialSetDoubleSided>>, <<materialsetvideo,materialSetVideo>>, <<softnew,softNew>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Der Boden, ein Bildschirm, der die Disc zeigt, und eine Flagge, die zu Stoff werden soll.
|
|
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)
|
|
----
|
|
|
|
Eine Kugel mit `radius`, auf den Ursprung zentriert, aus Breiten- und
|
|
Längenbändern gebaut: `segments` (Standard `32`, mindestens `3`) um den
|
|
Äquator und halb so viele von Pol zu Pol. Die Textur legt sich einmal um sie
|
|
herum, `v` vom oberen Pol zum unteren. Weniger Segmente ergeben ein leichteres
|
|
Mesh für Bälle, die aus der Ferne gesehen werden; mehr ergeben aus der Nähe
|
|
eine glattere Silhouette.
|
|
|
|
*Rückgabe:* Das Mesh-Handle.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<meshtorus,meshTorus>>, <<nodesetmesh,nodeSetMesh>>, <<bodynew,bodyNew>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Glaskugel auf dem Boden und eine kleine Markierungskugel, die ein Aussehen teilt.
|
|
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)
|
|
----
|
|
|
|
Ein Ring, der in der XZ-Ebene um Y liegt, auf den Ursprung zentriert:
|
|
`radius` vom Mittelpunkt bis zur Mitte des Rohrs und `tubeRadius` der eigene
|
|
Radius des Rohrs. `segments` (Standard `32`, mindestens `3`) laufen um den
|
|
Ring und halb so viele (mindestens `3`) um das Rohr. Die Textur legt sich in
|
|
`u` einmal um den Ring und in `v` einmal um das Rohr.
|
|
|
|
*Rückgabe:* Das Mesh-Handle.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<meshsphere,meshSphere>>, <<meshcylinder,meshCylinder>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein goldener Ring, auf die Kante gestellt, damit der Ball hindurchrollen kann.
|
|
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]
|
|
=== Mikrofon
|
|
|
|
Ein Mikrofon, für ein Gesangsspiel oder alles, was zuhört. Es ist immer nur
|
|
ein Aufnahmegerät geöffnet, gehört als ein Kanal mit 48000 Samples pro
|
|
Sekunde, gleich was die Hardware liefert; SDL wandelt um. Singe behält die
|
|
neuesten acht Sekunden dessen, was es hört, und jeder Aufruf unten antwortet
|
|
daraus, das Fragen kostet also nichts, was das Spiel nicht ohnehin schon
|
|
bezahlt hat.
|
|
|
|
Was ein Spiel meist will, ist <<micpitch,`micPitch`>> -- der gesungene Ton --
|
|
und <<miclevel,`micLevel`>>. Ein Spiel, das den Klang selbst analysiert oder
|
|
ihn irgendwohin schickt, liest die Samples mit <<micread,`micRead`>>; die
|
|
<<voice,Stimme>>-Aufrufe kodieren sie für ein Netzwerk.
|
|
|
|
Das Mikrofon wird geschlossen, wenn das Spiel endet. Nichts wird geöffnet, bevor ein Skript danach fragt.
|
|
|
|
[NOTE]
|
|
====
|
|
Ein gesungener Ton erreicht das Spiel ein wenig, nachdem er gesungen wurde:
|
|
Die Soundkarte, der Treiber und die Puffer halten ihn jeweils einen Moment
|
|
fest. Ein Gesangsspiel, das das Timing bewertet, zieht das ab.
|
|
<<miccalibrate,`micCalibrate`>> misst es, indem es Klicks über die
|
|
Lautsprecher abspielt und misst, wann sie zurückkommen, und das Ergebnis wird
|
|
für den Rechner behalten, wie die <<singesetaudiocalibration,Audiokalibrierung>>.
|
|
Mithören und Kalibrierung wurden bisher nur ohne echte Hardware getestet;
|
|
Tonhöhe, Pegel, Lesen und Aufnehmen wurden mit einem bekannten Signal getestet.
|
|
====
|
|
|
|
[#miccalibrate]
|
|
==== micCalibrate
|
|
|
|
[source,text]
|
|
----
|
|
started = micCalibrate()
|
|
----
|
|
|
|
Misst die Latenz des Mikrofons: Spielt fünf Klicks über die Lautsprecher ab,
|
|
im Abstand von Sekundenbruchteilen, und misst für jeden, wann er über das
|
|
Mikrofon zurückkommt. Das dauert ein paar Sekunden; <<miccalibration,`micCalibration`>>
|
|
sagt, wann es fertig ist. Das Ergebnis ist das des mittleren der gehörten
|
|
Klicks und wird für diesen Rechner behalten. Mach es in einem ruhigen Raum,
|
|
mit Lautsprechern, die laut genug sind, dass das Mikrofon sie hört.
|
|
|
|
*Rückgabe:* `true`, wenn die Messung gestartet wurde; `false`, wenn kein Mikrofon
|
|
geöffnet ist oder kein Lautsprecher die Klicks abspielen kann.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<miccalibration,micCalibration>>, <<miclatency,micLatency>>
|
|
|
|
[#miccalibration]
|
|
==== micCalibration
|
|
|
|
[source,text]
|
|
----
|
|
state, latency = micCalibration()
|
|
----
|
|
|
|
Wie eine Kalibrierung vorankommt.
|
|
|
|
*Rückgabe:* `state`, eines von `"idle"`, `"running"`, `"done"` oder `"failed"`
|
|
(weniger als drei der Klicks wurden zurück gehört), und die Latenz in
|
|
Millisekunden, sobald eine bekannt ist, sonst `nil`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<miccalibrate,micCalibrate>>
|
|
|
|
.Beispiel
|
|
[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()
|
|
----
|
|
|
|
Hört auf zuzuhören und beendet damit auch jedes Aufnehmen und Mithören. Das
|
|
Mikrofon wird auch geschlossen, wenn das Spiel endet.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#micisopen]
|
|
==== micIsOpen
|
|
|
|
[source,text]
|
|
----
|
|
open = micIsOpen()
|
|
----
|
|
|
|
*Rückgabe:* `true`, solange ein Mikrofon geöffnet ist.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#miclatency]
|
|
==== micLatency
|
|
|
|
[source,text]
|
|
----
|
|
milliseconds = micLatency()
|
|
----
|
|
|
|
*Rückgabe:* die Zeit zwischen dem Entstehen eines Klangs und dem Moment, in dem
|
|
das Spiel ihn hört, wie auf diesem Rechner von <<miccalibrate,`micCalibrate`>>
|
|
gemessen (zu einem beliebigen Zeitpunkt, in einem beliebigen Spiel) oder von
|
|
<<micsetlatency,`micSetLatency`>> gesetzt; bis dahin `nil`.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#miclevel]
|
|
==== micLevel
|
|
|
|
[source,text]
|
|
----
|
|
rms, peak = micLevel()
|
|
----
|
|
|
|
Wie laut der zuletzt gehörte Moment war.
|
|
|
|
*Rückgabe:* der RMS- und der Spitzenwert des letzten Sample-Blocks, jeweils
|
|
`0` bis `1`. Ein gleichmäßiger Ton bei Vollaussteuerung ergibt etwa `0.71` RMS.
|
|
Beide sind `0`, wenn kein Mikrofon geöffnet ist.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#miclist]
|
|
==== micList
|
|
|
|
[source,text]
|
|
----
|
|
names = micList()
|
|
----
|
|
|
|
*Rückgabe:* eine Liste der Namen der Mikrofone, die dieser Rechner hat, für
|
|
<<micopen,`micOpen`>>. Sie wird jedes Mal neu erstellt, sodass ein seit dem
|
|
letzten Aufruf angeschlossenes Mikrofon erscheint.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#micmonitor]
|
|
==== micMonitor
|
|
|
|
[source,text]
|
|
----
|
|
ok = micMonitor(on [, volume])
|
|
----
|
|
|
|
Spielt das, was das Mikrofon hört, über die Lautsprecher zurück, sodass sich
|
|
ein Sänger über der Musik selbst hört. Aus, bis es verlangt wird: Auf den
|
|
meisten Geräten kommt die Stimme so spät zurück, dass es irritiert, und bei
|
|
einem Laptop speisen die Lautsprecher das Mikrofon. Alles, was länger als eine
|
|
Zehntelsekunde auf die Wiedergabe wartet, wird verworfen, statt verspätet
|
|
gehört zu werden.
|
|
|
|
*Parameter:*
|
|
|
|
* `on` -- Boolean.
|
|
* `volume` -- Zahl, `0` bis `1`; `1`, sofern nicht angegeben.
|
|
|
|
*Rückgabe:* `true` oder `false`, wenn kein Mikrofon geöffnet ist oder kein Lautsprecher zum Abspielen da ist.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#micopen]
|
|
==== micOpen
|
|
|
|
[source,text]
|
|
----
|
|
opened = micOpen([name])
|
|
----
|
|
|
|
Beginnt zuzuhören, und zwar dem genannten Mikrofon (aus <<miclist,`micList`>>)
|
|
oder dem Standardmikrofon des Rechners. Ein bereits geöffnetes Mikrofon wird
|
|
zuerst geschlossen.
|
|
|
|
*Rückgabe:* `true`, wenn es geöffnet wurde.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
if not micOpen() then
|
|
message = "No microphone. This game needs one."
|
|
end
|
|
----
|
|
|
|
[#micpitch]
|
|
==== micPitch
|
|
|
|
[source,text]
|
|
----
|
|
hz, clarity = micPitch()
|
|
----
|
|
|
|
Der gesungene Ton, ermittelt aus den neuesten 43 Millisekunden, die gehört
|
|
wurden. Es findet einen Grundton zwischen 50 und 2000 Hz, was jede Singstimme
|
|
abdeckt, und trifft bei einem sauberen Ton auf ein, zwei Hertz genau.
|
|
|
|
*Rückgabe:* der Grundton in Hz und wie eindeutig der Klang ein einzelner Ton
|
|
ist, `0` bis `1`. Bei nichts Stimmhaftem -- Stille, einem Atemzug, einem
|
|
Husten, einer Menschenmenge -- ist `hz` gleich `0`. Wandle Hz mit
|
|
`69 + 12 * math.log(hz / 440, 2)` in eine Notennummer um: 69 ist das A über dem
|
|
mittleren C, und jede 1 ist ein Halbton.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
.Beispiel
|
|
[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 -- Halbtöne zu hoch (positiv) oder zu tief
|
|
if math.abs(off) < 0.5 then
|
|
score = score + 1
|
|
end
|
|
end
|
|
----
|
|
|
|
[#micread]
|
|
==== micRead
|
|
|
|
[source,text]
|
|
----
|
|
samples = micRead([max])
|
|
----
|
|
|
|
Was seit dem letzten Aufruf gehört wurde, das Älteste zuerst.
|
|
|
|
*Parameter:*
|
|
|
|
* `max` -- Zahl, die größte Anzahl zurückzugebender Samples; alles Wartende, sofern nicht angegeben.
|
|
|
|
*Rückgabe:* ein String aus 16-Bit-Samples, je zwei Bytes, Little Endian, ein
|
|
Kanal mit 48000 pro Sekunde: `#samples / 2` davon. Lies eines mit
|
|
`string.unpack("<i2", samples, n * 2 - 1)`. Ein Spiel, das länger als acht
|
|
Sekunden nicht liest, verliert die ältesten.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#micrecordstart]
|
|
==== micRecordStart
|
|
|
|
[source,text]
|
|
----
|
|
started = micRecordStart(name)
|
|
----
|
|
|
|
Schreibt alles, was von jetzt an gehört wird, in eine WAV-Datei (ein Kanal,
|
|
16 Bit, 48000 pro Sekunde) bis <<micrecordstop,`micRecordStop`>>. Der Name ist
|
|
eine beliebige Datei, die das Spiel schreiben darf, und landet dort, wo die
|
|
anderen geschriebenen Dateien des Spiels landen.
|
|
|
|
*Rückgabe:* `true`, wenn die Datei geöffnet wurde.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#micrecordstop]
|
|
==== micRecordStop
|
|
|
|
[source,text]
|
|
----
|
|
micRecordStop()
|
|
----
|
|
|
|
Beendet die Aufnahme.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#micsetlatency]
|
|
==== micSetLatency
|
|
|
|
[source,text]
|
|
----
|
|
micSetLatency(milliseconds | nil)
|
|
----
|
|
|
|
Setzt die Mikrofonlatenz dieses Rechners, wie es ein Service-Menü anhand einer
|
|
auf anderem Weg gemachten Messung tut; `nil` vergisst sie. Sie wird mit den
|
|
anderen Einstellungen des Rechners gespeichert.
|
|
|
|
*Parameter:*
|
|
|
|
* `milliseconds` -- Zahl, `0` bis `1000`, oder `nil`.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#midi]
|
|
=== MIDI
|
|
|
|
Zwei Dinge, die nichts miteinander zu tun haben, teilen sich diesen Namen, und
|
|
es lohnt sich, sie auseinanderzuhalten.
|
|
|
|
Eine **MIDI-Datei** ist Musik und wird wie jede andere Musik abgespielt: Gib
|
|
`.mid` an <<musicload,`musicLoad`>>. Singe synthetisiert sie aus einem
|
|
SoundFont, denn eine MIDI-Datei enthält Noten statt Klang. Singe liefert
|
|
keinen SoundFont mit -- ein guter ist Dutzende Megabyte groß, für ein Format,
|
|
das fast kein Spiel nutzt -- deshalb nenne einen mit `--soundfont`, leg einen
|
|
unter `Singe/soundfont.sf2` ab, damit ein gepacktes Spiel seinen eigenen
|
|
mitbringt, oder lass Singe den finden, den deine Distribution installiert hat.
|
|
Die Zeile `SoundFont:` im Kopf des Trace sagt, welcher verwendet wurde. Gibt es
|
|
nirgends einen SoundFont, schlägt das Laden einer `.mid` fehl und sagt das.
|
|
|
|
**MIDI-Ports** sind die Aufrufe unten. Sie senden und empfangen Live-Nachrichten
|
|
auf einem echten Port: ein Automat, der ein externes Klangmodul ansteuert, oder
|
|
ein Keyboard, eine Faderbox oder eine Bedienoberfläche, die als weiteres
|
|
Eingabegerät gelesen werden. Nichts hier synthetisiert etwas, und nichts hier
|
|
rührt eine MIDI-Datei an.
|
|
|
|
Ports werden ab `0` nummeriert. Nichts wird geöffnet, bevor ein Skript zum
|
|
ersten Mal fragt -- ein MIDI-Port kostet einen Dateideskriptor und eine Runde
|
|
Treiberkonfiguration, und ein Spiel, das MIDI nie erwähnt, bezahlt nie dafür --
|
|
und so nimmt der erste dieser Aufrufe den Bestand des Rechners auf, und
|
|
<<midirescan,`midiRescan`>> sucht erneut nach einem seither angeschlossenen
|
|
Gerät. Es dürfen gleichzeitig ein Eingabe-
|
|
und ein Ausgabeport geöffnet sein. Kanäle sind `1` bis `16`, so wie ein Mensch
|
|
sie zählt, nicht `0` bis `15` wie auf der Leitung, und jeder andere Wert ist
|
|
`0` bis `127`. Auf einem Rechner ganz ohne MIDI -- ohne Ports oder ohne
|
|
installierte MIDI-Bibliothek -- sind die Anzahlen `0`, die Öffnen-Aufrufe
|
|
antworten `false`, und die Sende-Aufrufe antworten `false`; nichts schlägt
|
|
fehl.
|
|
|
|
Linux erreicht jeden Port über den ALSA-Sequencer, der sowohl die
|
|
Hardware-Schnittstellen umfasst als auch jeden Synthesizer oder jedes andere
|
|
Programm, das einen anmeldet. macOS verwendet CoreMIDI und Windows die
|
|
Multimedia-MIDI-Aufrufe, wobei Ausgabegerät `0` meist der Synthesizer ist, den
|
|
das System selbst mitbringt.
|
|
|
|
[#midicloseinput]
|
|
==== midiCloseInput
|
|
|
|
[source,text]
|
|
----
|
|
midiCloseInput()
|
|
----
|
|
|
|
Hört auf, dem Eingabeport zuzuhören. <<onmidimessage,`onMidiMessage`>> wird nicht mehr ausgelöst. Einen Port zu schließen, der nicht geöffnet ist, bewirkt nichts.
|
|
|
|
*Parameter:*
|
|
|
|
* Keine.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<midiopeninput,midiOpenInput>>
|
|
|
|
[#midicloseoutput]
|
|
==== midiCloseOutput
|
|
|
|
[source,text]
|
|
----
|
|
midiCloseOutput()
|
|
----
|
|
|
|
Gibt den Ausgabeport frei. Die Sende-Aufrufe antworten von nun an `false`. Einen Port zu schließen, der nicht geöffnet ist, bewirkt nichts.
|
|
|
|
*Parameter:*
|
|
|
|
* Keine.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<midiopenoutput,midiOpenOutput>>
|
|
|
|
[#midicontrolchange]
|
|
==== midiControlChange
|
|
|
|
[source,text]
|
|
----
|
|
sent = midiControlChange(channel, control, value)
|
|
----
|
|
|
|
Bewegt einen Controller auf dem geöffneten Ausgabeport: Lautstärke ist Controller `7`, Panorama `10`, das Haltepedal `64`.
|
|
|
|
*Parameter:*
|
|
|
|
* `channel` -- Zahl; `1` bis `16`.
|
|
* `control` -- Zahl; `0` bis `127`.
|
|
* `value` -- Zahl; `0` bis `127`.
|
|
|
|
*Rückgabe:* `true`, wenn die Nachricht hinausging.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<midisend,midiSend>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Lautstärke des Moduls absenken, während eine Figur spricht.
|
|
midiControlChange(1, 7, 40)
|
|
----
|
|
|
|
[#midiinputcount]
|
|
==== midiInputCount
|
|
|
|
[source,text]
|
|
----
|
|
count = midiInputCount()
|
|
----
|
|
|
|
Wie viele MIDI-Eingabeports dieser Rechner hat. `0`, wenn es keine gibt oder die Plattform gar kein MIDI hat.
|
|
|
|
*Parameter:*
|
|
|
|
* Keine.
|
|
|
|
*Rückgabe:* eine Zahl.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<midiinputname,midiInputName>>
|
|
|
|
[#midiinputname]
|
|
==== midiInputName
|
|
|
|
[source,text]
|
|
----
|
|
name = midiInputName(index)
|
|
----
|
|
|
|
Der Name eines Eingabeports, für einen Einstellungsbildschirm, auf dem ein Spieler einen auswählen kann.
|
|
|
|
*Parameter:*
|
|
|
|
* `index` -- Zahl; `0` bis `midiInputCount() - 1`.
|
|
|
|
*Rückgabe:* ein String oder `nil`, wenn der Index keinen Port bezeichnet.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<midiinputcount,midiInputCount>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Einstellungsbildschirm, der die Eingangsports auflistet, einen pro Zeile.
|
|
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()
|
|
----
|
|
|
|
Ob ein Eingabeport geöffnet ist.
|
|
|
|
*Parameter:*
|
|
|
|
* Keine.
|
|
|
|
*Rückgabe:* `true` oder `false`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<midiopeninput,midiOpenInput>>
|
|
|
|
[#midiisoutputopen]
|
|
==== midiIsOutputOpen
|
|
|
|
[source,text]
|
|
----
|
|
open = midiIsOutputOpen()
|
|
----
|
|
|
|
Ob ein Ausgabeport geöffnet ist. Es lohnt sich, vor einer Reihe von Nachrichten zu fragen, da sonst jede einzeln `false` antworten würde.
|
|
|
|
*Parameter:*
|
|
|
|
* Keine.
|
|
|
|
*Rückgabe:* `true` oder `false`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<midiopenoutput,midiOpenOutput>>
|
|
|
|
[#midinoteoff]
|
|
==== midiNoteOff
|
|
|
|
[source,text]
|
|
----
|
|
sent = midiNoteOff(channel, key)
|
|
----
|
|
|
|
Lässt einen Ton auf dem geöffneten Ausgabeport los. Ein nicht beendeter Ton klingt, bis etwas ihn stoppt, deshalb braucht jedes `midiNoteOn` einen dieser Aufrufe.
|
|
|
|
*Parameter:*
|
|
|
|
* `channel` -- Zahl; `1` bis `16`.
|
|
* `key` -- Zahl; `0` bis `127`, wobei `60` das mittlere C ist.
|
|
|
|
*Rückgabe:* `true`, wenn die Nachricht hinausging.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<midinoteon,midiNoteOn>>
|
|
|
|
[#midinoteon]
|
|
==== midiNoteOn
|
|
|
|
[source,text]
|
|
----
|
|
sent = midiNoteOn(channel, key, velocity)
|
|
----
|
|
|
|
Lässt einen Ton auf dem geöffneten Ausgabeport erklingen.
|
|
|
|
*Parameter:*
|
|
|
|
* `channel` -- Zahl; `1` bis `16`. Kanal `10` ist auf einem General-MIDI-Gerät Schlagzeug.
|
|
* `key` -- Zahl; `0` bis `127`, wobei `60` das mittlere C ist.
|
|
* `velocity` -- Zahl; `0` bis `127`, wie hart die Taste angeschlagen wurde.
|
|
|
|
*Rückgabe:* `true`, wenn die Nachricht hinausging.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<midinoteoff,midiNoteOff>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Stinger auf einem externen Modul, wenn der Spieler getroffen wird.
|
|
midiNoteOn(10, 49, 110)
|
|
midiNoteOff(10, 49)
|
|
----
|
|
|
|
[#midiopeninput]
|
|
==== midiOpenInput
|
|
|
|
[source,text]
|
|
----
|
|
opened = midiOpenInput(index)
|
|
----
|
|
|
|
Hört einem Eingabeport zu. <<onmidimessage,`onMidiMessage`>> wird dann für jede ankommende Nachricht ausgelöst. Welcher Eingabeport auch immer geöffnet war, wird zuerst geschlossen.
|
|
|
|
*Parameter:*
|
|
|
|
* `index` -- Zahl; `0` bis `midiInputCount() - 1`.
|
|
|
|
*Rückgabe:* `true`, wenn der Port geöffnet wurde.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<midicloseinput,midiCloseInput>>, <<onmidimessage,onMidiMessage>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
if midiInputCount() > 0 then
|
|
midiOpenInput(0)
|
|
end
|
|
----
|
|
|
|
[#midiopenoutput]
|
|
==== midiOpenOutput
|
|
|
|
[source,text]
|
|
----
|
|
opened = midiOpenOutput(index)
|
|
----
|
|
|
|
Nimmt einen Ausgabeport für die Sende-Aufrufe. Welcher Ausgabeport auch immer geöffnet war, wird zuerst geschlossen.
|
|
|
|
*Parameter:*
|
|
|
|
* `index` -- Zahl; `0` bis `midiOutputCount() - 1`.
|
|
|
|
*Rückgabe:* `true`, wenn der Port geöffnet wurde.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<midicloseoutput,midiCloseOutput>>
|
|
|
|
[#midioutputcount]
|
|
==== midiOutputCount
|
|
|
|
[source,text]
|
|
----
|
|
count = midiOutputCount()
|
|
----
|
|
|
|
Wie viele MIDI-Ausgabeports dieser Rechner hat.
|
|
|
|
*Parameter:*
|
|
|
|
* Keine.
|
|
|
|
*Rückgabe:* eine Zahl.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<midioutputname,midiOutputName>>
|
|
|
|
[#midioutputname]
|
|
==== midiOutputName
|
|
|
|
[source,text]
|
|
----
|
|
name = midiOutputName(index)
|
|
----
|
|
|
|
Der Name eines Ausgabeports.
|
|
|
|
*Parameter:*
|
|
|
|
* `index` -- Zahl; `0` bis `midiOutputCount() - 1`.
|
|
|
|
*Rückgabe:* ein String oder `nil`, wenn der Index keinen Port bezeichnet.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<midioutputcount,midiOutputCount>>
|
|
|
|
[#midipitchbend]
|
|
==== midiPitchBend
|
|
|
|
[source,text]
|
|
----
|
|
sent = midiPitchBend(channel, value)
|
|
----
|
|
|
|
Verbiegt die Tonhöhe eines Kanals. `8192` ist die Mitte und bedeutet keine Verbiegung; wie weit `0` und `16383` reichen, ist eine Einstellung des empfangenden Geräts, meist zwei Halbtöne.
|
|
|
|
*Parameter:*
|
|
|
|
* `channel` -- Zahl; `1` bis `16`.
|
|
* `value` -- Zahl; `0` bis `16383`.
|
|
|
|
*Rückgabe:* `true`, wenn die Nachricht hinausging.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#midiprogramchange]
|
|
==== midiProgramChange
|
|
|
|
[source,text]
|
|
----
|
|
sent = midiProgramChange(channel, program)
|
|
----
|
|
|
|
Wählt den Klang, den ein Kanal spielt. Die Nummern sind die des empfangenden Geräts; auf einem General-MIDI-Gerät ist `0` ein Konzertflügel.
|
|
|
|
*Parameter:*
|
|
|
|
* `channel` -- Zahl; `1` bis `16`.
|
|
* `program` -- Zahl; `0` bis `127`.
|
|
|
|
*Rückgabe:* `true`, wenn die Nachricht hinausging.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#midirescan]
|
|
==== midiRescan
|
|
|
|
[source,text]
|
|
----
|
|
midiRescan()
|
|
----
|
|
|
|
Sucht erneut nach Ports, für ein Gerät, das angeschlossen wurde, während das Spiel läuft. Ein geöffneter Port bleibt geöffnet. Indizes sind nur so stabil wie die Liste, lies die Namen danach also neu, statt anzunehmen, dass Port `2` immer noch derselbe ist.
|
|
|
|
*Parameter:*
|
|
|
|
* Keine.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<midiinputcount,midiInputCount>>, <<midioutputcount,midiOutputCount>>
|
|
|
|
[#midisend]
|
|
==== midiSend
|
|
|
|
[source,text]
|
|
----
|
|
sent = midiSend(byte [, byte [, byte]])
|
|
sent = midiSend(string)
|
|
----
|
|
|
|
Jede beliebige Nachricht, für die, die die benannten Aufrufe nicht abdecken. Mit Zahlen sendet es diese so, wie sie sind, also ein Statusbyte und bis zu zwei Datenbytes -- und beachte, dass das Statusbyte den Kanal in seinen unteren vier Bits trägt, ab `0` gezählt, anders als jeder andere Aufruf hier. Mit einem String sendet es den ganzen String; so geht eine System-Exclusive-Nachricht hinaus.
|
|
|
|
*Parameter:*
|
|
|
|
* `byte` -- Zahl; `0` bis `255`, eine bis drei davon.
|
|
* `string` -- ein String aus Bytes, unverändert gesendet.
|
|
|
|
*Rückgabe:* `true`, wenn die Nachricht hinausging.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<midinoteon,midiNoteOn>>, <<midicontrolchange,midiControlChange>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Alle Noten aus auf Kanal 1, wofür es keinen benannten Aufruf gibt.
|
|
midiSend(0xB0, 123, 0)
|
|
|
|
-- Eine System-Exclusive-Nachricht, mit allen Bytes.
|
|
midiSend(string.char(0xF0, 0x7E, 0x7F, 0x09, 0x01, 0xF7))
|
|
----
|
|
|
|
[#model]
|
|
=== Modell
|
|
|
|
Modelle sind binäre glTF-2.0-Dateien (`.glb`), die einmal geladen und beliebig
|
|
oft platziert werden. Ein Modell muss in sich geschlossen sein (Puffer und
|
|
Bilder in der Datei); eines, das auf eine Datei daneben verweist, wird
|
|
abgelehnt. Das Laden liefert ein ganzzahliges Handle; `modelInstance` baut die
|
|
Knoten des Modells in die Szene ein und gibt ihre Wurzel zurück, die dann jeder
|
|
`node*`- und `animation*`-Aufruf entgegennimmt. Positionen und Größen innerhalb
|
|
eines Modells sind in Welteinheiten, so wie die Datei sie speichert; der
|
|
Wurzelknoten skaliert die ganze Instanz. Siehe <<scenes3d,3D-Szenen>>.
|
|
|
|
[#modelload]
|
|
==== modelLoad
|
|
|
|
[source,text]
|
|
----
|
|
model = modelLoad(name)
|
|
----
|
|
|
|
Liest eine `.glb` über dieselbe Suche wie jedes andere Asset (Spieleverzeichnis, Datenordner, gepackte Datenbank), lädt ihre Meshes und Materialien einmal auf die GPU hoch und behält ihren Knotenbaum, ihre Skins und Animationen zum Instanzieren. Ruf es einmal beim Start auf und instanziere das Handle so oft wie nötig; dieselbe Datei zweimal zu laden erzeugt zwei Kopien auf der GPU. Es braucht die 3D-Szene und schlägt deshalb auf einem Rechner ohne geeignete GPU fehl wie jeder andere 3D-Aufruf. Eine fehlerhafte Datei beendet das Skript mit einer Meldung, die das Problem nennt: Die Datei konnte nicht gelesen werden, ist kein glTF, verweist auf einen externen Puffer oder ein externes Bild, besteht die Validierung nicht, enthält eine Grundform, die nicht hochgeladen werden konnte, hat einen Skin mit mehr als `128` Gelenken oder hat ein geskinntes Mesh, das ein Gelenk mit der Nummer `128` oder höher nennt. Ein geskinntes Mesh, das ein Gelenk jenseits der Skins nennt, die es verwenden, wird geladen, ebenso eine Animation, deren Keyframes nicht gelesen werden können (dieser Sampler wird übersprungen); beides wird im Programm-Trace vermerkt. Eine Textur, die nicht dekodiert werden kann, oder ein fehlendes KTX2-Bild lässt das Laden nicht fehlschlagen; das Material kommt ohne diese Map aus, und der Programm-Trace (`--program`) sagt, welche es ist.
|
|
|
|
*Rückgabe:* Das Modell-Handle, eine Ganzzahl.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<modelinstance,modelInstance>>, <<modelgetanimations,modelGetAnimations>>, <<modeldelete,modelDelete>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Einmal laden, dreimal platzieren.
|
|
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)
|
|
----
|
|
|
|
Gibt die Meshes, Materialien und eingelesenen Daten des Modells frei. Jede daraus erzeugte Instanz wird zugleich vergessen: Ihre Knoten bleiben in der Szene (Position, Elternknoten und Kinder unverändert), zeichnen aber nichts, ihre Animationen bleiben stehen, wo sie sind, und ein späterer `animation*`-Aufruf auf ihrer Wurzel beendet das Skript, weil der Knoten keine Modellinstanz mehr ist. Lösch die Wurzeln der Instanzen mit `nodeDelete`, wenn die Knoten auch nicht mehr gebraucht werden. Ein Handle, das kein geladenes Modell ist, beendet das Skript.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<modelload,modelLoad>>, <<modelinstance,modelInstance>>, <<nodedelete,nodeDelete>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Hangar-Level verlassen: seine Requisiten und das Modell dahinter verwerfen.
|
|
function unloadHangar()
|
|
for _, crate in ipairs(hangarCrates) do
|
|
nodeDelete(crate)
|
|
end
|
|
hangarCrates = {}
|
|
modelDelete(crateModel)
|
|
crateModel = nil
|
|
end
|
|
----
|
|
|
|
[#modelgetanimations]
|
|
==== modelGetAnimations
|
|
|
|
[source,text]
|
|
----
|
|
names = modelGetAnimations(model)
|
|
----
|
|
|
|
Listet die Animationsclips des Modells in Dateireihenfolge als Tabelle von Strings auf, sodass `names[3]` der Clip ist, den `animationPlay(node, 3)` abspielt. Ein Clip, den der Exporter unbenannt gelassen hat, ergibt einen leeren String; er kann trotzdem über seine Nummer abgespielt werden. Ein Modell ohne Animationen ergibt eine leere Tabelle. Ein Handle, das kein geladenes Modell ist, beendet das Skript.
|
|
|
|
*Rückgabe:* Eine Tabelle von Strings, ab `1` indiziert.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<animationplay,animationPlay>>, <<modelload,modelLoad>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Clips einmal nach Namen suchen, damit der Rest des Spiels Nummern verwendet.
|
|
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)
|
|
----
|
|
|
|
Baut den Knotenbaum des Modells unter einem neuen Wurzelknoten auf, der selbst unter `parent` oder der Wurzel der Szene hängt. Die Wurzel ist ein gewöhnlicher Knoten: Positioniere, drehe, skaliere oder verknüpfe sie wie jeden anderen und lösch sie mit `nodeDelete`, um die ganze Instanz zu entfernen. Die eigenen Knoten des Modells übernehmen ihre glTF-Namen (oder den Namen ihres Meshs, wenn der Knoten unbenannt ist), sodass `nodeFind(name, node)` sie erreicht; ein Mesh mit mehreren Materialien wird zu einem Kindknoten pro Teil; Lichter in der Datei werden zu Szenenlichtern mit der Farbe, Intensität, Reichweite und dem Kegel aus der Datei; Skins werden angehängt und Morph-Gewichte gesetzt. Die Knoten stehen in der Pose der Datei, bis eine Animation läuft. Ein Handle, das kein geladenes Modell ist, oder ein `parent`, das kein Knoten ist, beendet das Skript, ebenso, wenn die Knoten ausgehen.
|
|
|
|
*Rückgabe:* Das Handle des Wurzelknotens der Instanz.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<modelload,modelLoad>>, <<nodefind,nodeFind>>, <<animationplay,animationPlay>>, <<nodedelete,nodeDelete>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Geschützturm auf der Wanne des Panzers, der sich also mit der Wanne dreht und für sich schwenkt.
|
|
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]
|
|
=== Maus
|
|
|
|
Singe liest bis zu vier Mäuse und behandelt Lightguns als Mäuse. Im standardmäßigen Einzelmausmodus steuert jedes Zeigegerät einen Cursor, der als Maus `0` gemeldet wird; `mouseSetMode(MOUSE_MANY)` unterscheidet die Geräte, was ein Lightgun-Automat für zwei Spieler braucht. Positionen kommen in Overlay-Koordinaten an, nicht in Bildschirmpixeln, über `onMouseMoved` (siehe <<enginecallbacks,Engine-Callbacks>>) und auf Abruf von `mouseGetPosition`. Maustasten und das Mausrad sind Schalter wie Tasten: Sie erreichen `onInputPressed` und `onInputReleased`, wenn `controls.cfg` einen Code aus den Tabellen `MOUSE_0` bis `MOUSE_3` belegt, und die mitgelieferten Standardwerte belegen die linke Taste mit `SWITCH_BUTTON3`, die rechte mit `SWITCH_BUTTON1` und die mittlere mit `SWITCH_BUTTON2`. Die Engine startet mit eingefangener Maus, die eingeschaltet ist, sofern nicht `--nomouse` angegeben oder keine Maus gefunden wurde.
|
|
|
|
[#mousedisable]
|
|
==== mouseDisable
|
|
|
|
[source,text]
|
|
----
|
|
mouseDisable()
|
|
----
|
|
|
|
Schaltet die Mauseingabe aus, genau wie `mouseSetEnabled(false)`. Es ist eine in `Framework.singe` definierte Funktion, die erhalten bleibt, damit ältere Spiele unverändert laufen.
|
|
|
|
*Hinweise:* Veralteter Alias.
|
|
|
|
WARNING: Neuer Code sollte <<mousesetenabled,mouseSetEnabled>> mit `false` aufrufen.
|
|
|
|
*Seit:* 1.18 (RDG)
|
|
*Siehe auch:* <<mousesetenabled,mouseSetEnabled>>, <<mouseenable,mouseEnable>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Älterer Spielcode; die moderne Schreibweise ist mouseSetEnabled(false).
|
|
function cutsceneBegin()
|
|
mouseDisable()
|
|
discSearch(CUTSCENE_START)
|
|
end
|
|
----
|
|
|
|
[#mouseenable]
|
|
==== mouseEnable
|
|
|
|
[source,text]
|
|
----
|
|
mouseEnable()
|
|
----
|
|
|
|
Schaltet die Mauseingabe ein, genau wie `mouseSetEnabled(true)`, und bleibt wie dieses aus, wenn Singe mit `--nomouse` gestartet wurde. Es ist eine in `Framework.singe` definierte Funktion, die erhalten bleibt, damit ältere Spiele unverändert laufen.
|
|
|
|
*Hinweise:* Veralteter Alias.
|
|
|
|
WARNING: Neuer Code sollte <<mousesetenabled,mouseSetEnabled>> mit `true` aufrufen.
|
|
|
|
*Seit:* 1.18 (RDG)
|
|
*Siehe auch:* <<mousesetenabled,mouseSetEnabled>>, <<mousedisable,mouseDisable>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Älterer Spielcode; die moderne Schreibweise ist mouseSetEnabled(true).
|
|
function cutsceneEnd()
|
|
mouseEnable()
|
|
discSearch(LEVEL_START)
|
|
end
|
|
----
|
|
|
|
[#mousegetname]
|
|
==== mouseGetName
|
|
|
|
[source,text]
|
|
----
|
|
name = mouseGetName(mouse)
|
|
----
|
|
|
|
Der Name, den der Maustreiber einem Gerät gibt, oder `nil`, wenn es kein Gerät mit diesem Index gibt. Damit lassen sich zwei Pistolen an einem Automaten auf einem Service-Bildschirm unterscheiden, was sonst Raterei ist, wenn sich beide als Maus `0` und `1` melden. Ein Index außerhalb von `0` bis `3` bricht das Skript ab.
|
|
|
|
*Parameter:*
|
|
|
|
* `mouse` -- Geräteindex, `0` bis `3`.
|
|
|
|
*Rückgabe:* String oder `nil`.
|
|
|
|
*Seit:* 3.00
|
|
*Siehe auch:* <<mousehowmany,mouseHowMany>>, <<mousesetmode,mouseSetMode>>, <<controllergetname,controllerGetName>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Pistolen auf dem Kalibrierungsbildschirm benennen.
|
|
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()
|
|
----
|
|
|
|
Hypseus-Erweiterung, immer `0`, aus dem Grund, den <<controllergetpadding,`controllerGetPadding`>> nennt. Hypseus addiert diesen Offset zu den Eingabecodes, die seine Mäuse im relativen Modus melden; die Codes von Singe tragen keinen solchen Offset, es gibt also nichts abzuziehen.
|
|
|
|
*Rückgabe:* eine Ganzzahl, immer `0`.
|
|
|
|
*Seit:* 3.00
|
|
*Siehe auch:* <<controllergetpadding,controllerGetPadding>>
|
|
|
|
[#mousegetposition]
|
|
==== mouseGetPosition
|
|
|
|
[source,text]
|
|
----
|
|
x, y = mouseGetPosition(index)
|
|
----
|
|
|
|
Gibt die zuletzt gemeldete Position einer Maus in Overlay-Koordinaten zurück, dieselben Werte, die das jüngste `onMouseMoved` für dieses Gerät trug. Im Einzelmausmodus bewegt sich nur Maus `0`; im Mehrmausmodus hat jedes Gerät seine eigene Position, die in der Mitte des Videos beginnt. Die Position wird nicht mehr aktualisiert, solange die Mauseingabe ausgeschaltet ist. Frag sie in `onOverlayUpdate` oder `singeMain` ab, wenn das einfacher ist, als den Callback zu verfolgen. Ein Index außerhalb von `0` bis `3` bricht das Skript ab.
|
|
|
|
*Parameter:*
|
|
|
|
* `index` -- Mausindex, `0` bis `3`.
|
|
|
|
*Rückgabe:* zwei Ganzzahlen, `x` und `y`, in Overlay-Koordinaten.
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<mousehowmany,mouseHowMany>>, <<onmousemoved,onMouseMoved>>, <<singewantscrosshairs,singeWantsCrosshairs>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Fadenkreuz dort zeichnen, wohin die Pistole zeigt.
|
|
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()
|
|
----
|
|
|
|
Gibt die Anzahl der Mäuse zurück, die die Engine beim Start gefunden hat, `0` bis `4`. Später angeschlossene Geräte werden nicht gezählt. Wurde keine gefunden, bleibt die Maus ausgeschaltet. Entscheide damit, ob du einen zweiten Lightgun-Spieler anbietest.
|
|
|
|
*Rückgabe:* Ganzzahl, `0` bis `4`.
|
|
|
|
*Seit:* 1.18 (RDG)
|
|
*Siehe auch:* <<mousesetmode,mouseSetMode>>, <<mousegetposition,mouseGetPosition>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Zwei Pistolen vorhanden: jedem Spieler einen Cursor geben.
|
|
if mouseHowMany() >= 2 then
|
|
mouseSetMode(MOUSE_MANY)
|
|
players = 2
|
|
else
|
|
players = 1
|
|
end
|
|
----
|
|
|
|
[#mousesetcaptured]
|
|
==== mouseSetCaptured
|
|
|
|
[source,text]
|
|
----
|
|
mouseSetCaptured(captured)
|
|
----
|
|
|
|
Fängt die Maus ein, beschränkt sie auf das Singe-Fenster und versteckt den Cursor, oder gibt sie frei und zeigt den Cursor wieder. Die Engine fängt die Maus beim Start ein, die meisten Spiele rufen dies also nie auf; ein Level-Editor im Fenster oder ein Spiel, das den Desktop-Cursor sichtbar haben will, gibt sie frei. Der Schalter, der in `controls.cfg` mit `INPUT_GRAB` belegt ist, schaltet denselben Zustand über die Tastatur um.
|
|
|
|
*Parameter:*
|
|
|
|
* `captured` -- Boolean; `true` fängt ein und versteckt, `false` gibt frei und zeigt.
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<mousesetenabled,mouseSetEnabled>>, <<mousegetposition,mouseGetPosition>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Der Level-Editor will den Desktop-Cursor; das Spiel versteckt ihn wieder.
|
|
function editorToggle()
|
|
editing = not editing
|
|
mouseSetCaptured(not editing)
|
|
end
|
|
----
|
|
|
|
[#mousesetenabled]
|
|
==== mouseSetEnabled
|
|
|
|
[source,text]
|
|
----
|
|
mouseSetEnabled(enabled)
|
|
----
|
|
|
|
Schaltet die Mauseingabe ein oder aus. Solange sie aus ist, werden Bewegungs-, Tasten- und Radereignisse verworfen, bevor sie das Skript erreichen: `onMouseMoved` wird nicht mehr ausgelöst, Maustasten erzeugen keine Schalter mehr, und `mouseGetPosition` gibt weiter die letzte Position zurück, die es hatte. Der Cursor selbst bewegt sich weiterhin. Das Einschalten beachtet `--nomouse`; wurde Singe mit dieser Option gestartet, bleibt die Maus aus. Ein Argument, das kein Boolean ist, bricht das Skript ab.
|
|
|
|
*Parameter:*
|
|
|
|
* `enabled` -- Boolean.
|
|
|
|
*Seit:* 3.00
|
|
*Siehe auch:* <<mousesetcaptured,mouseSetCaptured>>, <<mousesetmode,mouseSetMode>>, <<mouseenable,mouseEnable>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Pistole ignorieren, solange die Attract-Schleife läuft.
|
|
function attractBegin()
|
|
mouseSetEnabled(false)
|
|
discSearch(ATTRACT_START)
|
|
end
|
|
|
|
function attractEnd()
|
|
mouseSetEnabled(true)
|
|
end
|
|
----
|
|
|
|
[#mousesetmode]
|
|
==== mouseSetMode
|
|
|
|
[source,text]
|
|
----
|
|
mouseSetMode(mode)
|
|
----
|
|
|
|
Wählt, wie Mäuse gelesen werden. Im `MOUSE_SINGLE` (der Standard) steuern die Zeigerereignisse des Fensters einen Cursor, der als Gerät `0` gemeldet wird, wobei absolute Positionen und relative Bewegung beide in Overlay-Koordinaten umgerechnet werden. Im `MOUSE_MANY` wird jedes physische Gerät getrennt gelesen: Relative Bewegung wird zu einer Position pro Gerät aufsummiert, die auf den Videobereich begrenzt ist, absolute Geräte wie Grafiktabletts und manche Lightguns werden auf das Video abgebildet, und `onMouseMoved` meldet den Geräteindex. Beide Modi liefern dieselben fünf Tasten und das senkrechte Mausrad als Schalter. `SINGLE_MOUSE` und `MANY_MOUSE` werden als ältere Schreibweisen akzeptiert; jeder andere Wert bricht das Skript ab. `--manymouse` ist die eigene Entscheidung des Benutzers und hat Vorrang vor diesem Aufruf: Ist es angegeben, ist der Modus von Anfang an `MOUSE_MANY`, und `mouseSetMode(MOUSE_SINGLE)` schaltet ihn nicht ab, sodass ein Automat mit zwei Pistolen von außen auch mit einem Spiel funktioniert, das nie danach fragt.
|
|
|
|
*Parameter:*
|
|
|
|
* `mode` -- `MOUSE_SINGLE` (`100`) oder `MOUSE_MANY` (`200`).
|
|
|
|
*Seit:* 1.18 (RDG)
|
|
*Siehe auch:* <<mousehowmany,mouseHowMany>>, <<onmousemoved,onMouseMoved>>, <<mousegetposition,mouseGetPosition>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Kooperatives Lightgun-Spiel: Jede Pistole steuert ihren eigenen 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]
|
|
=== Musik
|
|
|
|
Hintergrundmusik, getrennt von den Soundeffekten geladen und abgespielt und mit eigener Lautstärke. Hypseus lässt Musik genau aus diesem Grund auf einem zweiten Mixer laufen, und Singe gibt ihr stattdessen eine eigene Verstärkung: <<musicsetvolume,`musicSetVolume`>> verändert nie einen Soundeffekt, und <<soundsetvolume,`soundSetVolume`>> verändert nie die Musik.
|
|
|
|
Ein Musik-Handle ist eine eigene Art von Handle, weder ein Sound-Handle noch ein Kanal: Ein Handle ist ein Stück, und es spielt, pausiert, setzt fort und stoppt für sich allein. Anders als bei den sechzehn Soundeffekt-Kanälen gibt es keine Kanalbegrenzung.
|
|
|
|
Die Lautstärkeskala ist die von Hypseus, `0` bis `128`, fast doppelt so groß wie die `0` bis `63` der `sound`-Familie, sodass ein für Hypseus geschriebenes Spiel seine eigenen Zahlen behält. Musik beginnt so laut wie die Soundeffekte, mit dem, was `--volume_nonvldp` gesetzt hat; Hypseus startet seinen Musik-Mixer dagegen bei einem Drittel, was jedes Hypseus-Spiel sofort mit `musicSetVolume(soundGetVolume() * 2)` überschreibt.
|
|
|
|
Jedes Format, das die Sound-Familie liest, wird auch hier gelesen: WAV, Ogg Vorbis, Opus, FLAC, MP3, WavPack und der Rest. Der Kopf des Trace, den `--program` und `--trace` schreiben, listet in seiner Zeile `Audio:` genau auf, welche Decoder der Build enthält.
|
|
|
|
Das Pausieren der Engine (die Pausentaste oder `singeSetPauseFlag`) pausiert die Musik mit, und das Fortsetzen bringt sie zurück; ein Neuladen stoppt jedes Stück und gibt es frei.
|
|
|
|
[#musicgetposition]
|
|
==== musicGetPosition
|
|
|
|
[source,text]
|
|
----
|
|
seconds = musicGetPosition(id)
|
|
----
|
|
|
|
Wo sich der Zuhörer in einem laufenden Stück befindet, in Sekunden: die eigene Position des Mixers abzüglich dessen, was noch auf dem Weg zu den Lautsprechern ist -- die Gerätewarteschlange, die Audiokalibrierung des Rechners und die Audioverzögerung des Spiels -- und so kommt ein darauf abgestimmtes Bild zusammen mit dem Klang an. Es ist die Uhr, nach der animiert wird, wenn die Musik führt, wie in einer Demo oder einem Rhythmusspiel, und sie bleibt beim Lied, wie lange es auch läuft, während das Zählen von Frames oder Ticks abdriftet. Der Mixer bewegt seine Position jeweils um einen Puffer, zwischen den Sprüngen läuft die Antwort also mit der Uhr weiter, nie mehr als 50 ms, und sie bewegt sich in jedem Frame gleichmäßig. Ein pausiertes Stück steht still. Ein Stück, das nicht läuft, antwortet `-1`.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Zahl; ein Musik-Handle.
|
|
|
|
*Rückgabe:* Zahl; Sekunden seit Beginn des Stücks oder `-1`.
|
|
|
|
*Seit:* 3.00
|
|
*Siehe auch:* <<musicplay,musicPlay>>, <<singegetaudiolatency,singeGetAudioLatency>>, <<singegetaudiocalibration,singeGetAudioCalibration>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Bei jedem Schlag eines Lieds mit 120 Schlägen pro Minute aufblitzen, genau auf dem Schlag, wie er zu hören ist.
|
|
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])
|
|
----
|
|
|
|
Hypseus-Erweiterung. Ob dieses Stück läuft oder -- ohne Handle -- ob irgendeine Musik läuft. Ein pausiertes Stück läuft nicht.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- optionale Zahl; ein Musik-Handle.
|
|
|
|
*Rückgabe:* Boolean.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<musicplay,musicPlay>>, <<musicpause,musicPause>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
if not musicIsPlaying(theme) then
|
|
musicPlay(theme, -1)
|
|
end
|
|
----
|
|
|
|
[#musicload]
|
|
==== musicLoad
|
|
|
|
[source,text]
|
|
----
|
|
id = musicLoad(filename)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Lädt ein Musikstück über das virtuelle Dateisystem und gibt sein Handle zurück. Die ganze Datei wird vorab dekodiert, wie bei einem Sound, ein langes Stück kostet also Speicher; das Handle gilt bis <<musicunload,`musicUnload`>> oder zu einem Neuladen.
|
|
|
|
Eine Datei, die fehlt oder die kein einkompilierter Decoder lesen kann, beendet das Skript, wie bei <<soundload,`soundLoad`>>.
|
|
|
|
*Parameter:*
|
|
|
|
* `filename` -- String; ein Pfad, den das virtuelle Dateisystem erreichen kann.
|
|
|
|
*Rückgabe:* Zahl, das Musik-Handle.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<musicplay,musicPlay>>, <<musicunload,musicUnload>>, <<soundload,soundLoad>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
local theme = musicLoad("music/attract.mp3")
|
|
musicSetVolume(96)
|
|
musicPlay(theme, -1)
|
|
----
|
|
|
|
[#musicpause]
|
|
==== musicPause
|
|
|
|
[source,text]
|
|
----
|
|
musicPause([id])
|
|
----
|
|
|
|
Hypseus-Erweiterung. Pausiert dieses Stück an seiner Stelle oder jede Musik, wenn kein Handle angegeben ist. <<musicresume,`musicResume`>> setzt es wieder fort.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- optionale Zahl; ein Musik-Handle.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<musicresume,musicResume>>, <<musicstop,musicStop>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Der Spieler hat die Karte geöffnet; die Musik anhalten statt neu starten.
|
|
musicPause()
|
|
----
|
|
|
|
[#musicplay]
|
|
==== musicPlay
|
|
|
|
[source,text]
|
|
----
|
|
id = musicPlay(id [, loops])
|
|
----
|
|
|
|
Hypseus-Erweiterung. Spielt ein Stück von seinem Anfang an und antwortet mit seinem Handle, so wie die Spiele von Hypseus es aufbewahren (`sndHandle = musicPlay(music)`), um das Stück später zu stoppen. `loops` bedeutet dasselbe wie bei <<soundplay,`soundPlay`>>: `0`, der Standard, spielt einmal, `-1` spielt endlos, und `N` wiederholt `N`-mal zusätzlich. Die Anzahl wird auf `-1` bis `64` begrenzt, so wie Hypseus sie begrenzt, damit ein Spiel, das mehr verlangt, bekommt, was es dort bekommen hätte.
|
|
|
|
Ein Stück abzuspielen, das bereits läuft, startet es erneut von vorn.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Zahl; ein Musik-Handle.
|
|
* `loops` -- optionale Zahl; `-1` bis `64`. Standard `0`.
|
|
|
|
*Rückgabe:* Zahl; das Handle, das es bekommen hat.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<musicstop,musicStop>>, <<musicsetvolume,musicSetVolume>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
musicPlay(theme, -1) -- Immer im Kreis, bis etwas es stoppt.
|
|
----
|
|
|
|
[#musicresume]
|
|
==== musicResume
|
|
|
|
[source,text]
|
|
----
|
|
musicResume([id])
|
|
----
|
|
|
|
Hypseus-Erweiterung. Setzt ein pausiertes Stück fort oder jede pausierte Musik, wenn kein Handle angegeben ist.
|
|
|
|
Die eigene Form von Hypseus ohne Argument tut gar nichts; die von Singe setzt alles fort, und das ist das Gegenstück zu <<musicpause,`musicPause`>>.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- optionale Zahl; ein Musik-Handle.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<musicpause,musicPause>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
musicResume()
|
|
----
|
|
|
|
[#musicsetvolume]
|
|
==== musicSetVolume
|
|
|
|
[source,text]
|
|
----
|
|
volume = musicSetVolume(volume)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Setzt die Verstärkung für alle Musik auf einmal, auf der Skala `0` bis `128` von Hypseus, und antwortet mit dem übernommenen Pegel. Es verändert nichts außer Musik: Die Soundeffekte behalten, was <<soundsetvolume,`soundSetVolume`>> ihnen gegeben hat, und `soundSetVolume` lässt die Musik in Ruhe.
|
|
|
|
Ein Pegel außerhalb von `0` bis `128` beendet das Skript. Der Pegel gilt für bereits laufende Stücke und für alle danach geladenen und kehrt bei einem Neuladen zum Anfangspegel zurück.
|
|
|
|
*Parameter:*
|
|
|
|
* `volume` -- Zahl; `0` bis `128`.
|
|
|
|
*Rückgabe:* Zahl, der gesetzte Pegel.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<soundsetvolume,soundSetVolume>>, <<musicplay,musicPlay>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Was ein Hypseus-Spiel schreibt: der Sound-Pegel, verdoppelt auf die Musikskala.
|
|
musicSetVolume(soundGetVolume() * 2)
|
|
----
|
|
|
|
[#musicstop]
|
|
==== musicStop
|
|
|
|
[source,text]
|
|
----
|
|
musicStop([id [, fadeMs]])
|
|
----
|
|
|
|
Hypseus-Erweiterung. Stoppt dieses Stück oder jede Musik, wenn kein Handle angegeben ist (ein `nil`-Handle zählt als keines). `fadeMs` blendet es über so viele Millisekunden aus, statt es abzuschneiden; `0`, der Standard, stoppt sofort. Eine negative Ausblendzeit beendet das Skript.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- optionale Zahl; ein Musik-Handle.
|
|
* `fadeMs` -- optionale Zahl; Millisekunden zum Ausblenden. Standard `0`.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<musicplay,musicPlay>>, <<musicpause,musicPause>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
musicStop(theme, 1500) -- Über anderthalb Sekunden ausgeblendet.
|
|
----
|
|
|
|
[#musicunload]
|
|
==== musicUnload
|
|
|
|
[source,text]
|
|
----
|
|
musicUnload(id)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Stoppt das Stück und gibt es frei. Das Handle ist danach ungültig, und es zu verwenden beendet das Skript.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Zahl; ein Musik-Handle.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<musicload,musicLoad>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
musicUnload(theme)
|
|
----
|
|
|
|
[#nav]
|
|
=== Navigation
|
|
|
|
Navigationsmeshes, aus Szenengeometrie gebacken, und die Crowd-Agenten, die sie ablaufen (siehe <<scenes3d,3D-Szenen>>, Navigation). `navNew` und `navLoad` geben ein Mesh-Handle zurück und `navAgentNew` ein Agenten-Handle; beide sind Ganzzahlen, werden bei jedem Aufruf geprüft, und ein ungültiges beendet das Skript. Positionen sind in Welteinheiten, Geschwindigkeiten in Einheiten pro Sekunde und Steigungen in Grad. Bis zu acht Meshes gleichzeitig, 128 Agenten auf jedem und 256 insgesamt. Die Abfragen und die Agenten brauchen ein Mesh, das `navBuild` gebacken oder `navLoad` gelesen hat; vorher geben die Abfragen `nil` zurück, `navDraw` zeichnet nichts, und `navAgentNew` beendet das Skript.
|
|
|
|
[#navaddnode]
|
|
==== navAddNode
|
|
|
|
[source,text]
|
|
----
|
|
navAddNode(nav, node)
|
|
----
|
|
|
|
Fügt das Mesh des Knotens und das Mesh jedes Knotens darunter der Geometrie hinzu, aus der das Navigationsmesh gebacken wird, so wie sie gerade in der Welt stehen; platziere sie also zuerst. Der Wurzelknoten einer Modellinstanz fügt das ganze Modell hinzu, und ein aus Grundformen gebautes Level lässt sich Knoten für Knoten hinzufügen. Nur Mesh-Geometrie zählt: Körper, Sprites und Text fügen nichts hinzu. Beendet das Skript, sobald das Mesh gebaut ist.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<navnew,navNew>>, <<navbuild,navBuild>>, <<modelinstance,modelInstance>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Boden und Wände sind begehbare Geometrie; die Kisten nicht, sie bewegen sich.
|
|
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)
|
|
----
|
|
|
|
Nimmt den Agenten vom Mesh und gibt sein Handle frei; der Knoten bleibt, wo er ist, und wird nicht mehr bewegt. Das Löschen des Meshs mit `navDelete` tut das für jeden Agenten darauf. Ein ungültiges Handle beendet das Skript.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<navagentnew,navAgentNew>>, <<navdelete,navDelete>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Wächter, der stirbt, hört auf zu navigieren; ab hier übernimmt die Ragdoll das Modell.
|
|
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)
|
|
----
|
|
|
|
Wie die Crowd den Agenten in diesem Frame bewegt, in Welteinheiten pro Sekunde entlang jeder Achse; die Länge reicht bis zur Geschwindigkeit des Agenten und fällt auf null, wenn er anhält. Damit wählst du zwischen einer Steh-, einer Geh- und einer Laufanimation oder passt den Gehzyklus an die zurückgelegte Strecke an.
|
|
|
|
*Rückgabe:* Drei Zahlen.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<navagentmoveto,navAgentMoveTo>>, <<animationplay,animationPlay>>
|
|
|
|
.Beispiel
|
|
[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)
|
|
----
|
|
|
|
Ob der Agent das letzte Ziel erreicht hat, das `navAgentMoveTo` ihm gegeben hat: höchstens anderthalb seiner Radien vom Ziel entfernt und fast zum Stehen gekommen. Es wird `true` in dem Moment, in dem `onNavArrived` ausgelöst wird, und bleibt `true` bis zum nächsten `navAgentMoveTo`; für einen Agenten, der nie irgendwohin geschickt oder unterwegs angehalten wurde, ist es `false`.
|
|
|
|
*Rückgabe:* Wahr oder falsch.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<onnavarrived,onNavArrived>>, <<navagentmoveto,navAgentMoveTo>>, <<navagentstop,navAgentStop>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Patrouille wartet an jedem Posten eine Weile, bevor sie weitergeht.
|
|
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)
|
|
----
|
|
|
|
Schickt den Agenten zu Fuß zu dem Punkt des Meshs, der `x, y, z` am nächsten liegt, und sucht diesen bis zu 2 Einheiten seitlich und 4 nach oben oder unten. Die Crowd plant die Route über die folgenden Frames und steuert den Agenten unterwegs um die anderen Agenten herum; `onNavArrived` wird ausgelöst, sobald er dort ankommt. Gibt `false` zurück und lässt den Agenten, wie er war, wenn kein Punkt des Meshs nah genug ist oder die Crowd die Anfrage ablehnt. Ein neues Ziel ersetzt das alte jederzeit.
|
|
|
|
*Parameter:*
|
|
|
|
* `agent` -- das Handle von `navAgentNew`.
|
|
* `x`, `y`, `z` -- das Ziel in Welteinheiten; es wird auf das Mesh eingerastet.
|
|
|
|
*Rückgabe:* Wahr, wenn der Agent losgegangen ist.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<navagentstop,navAgentStop>>, <<navnearest,navNearest>>, <<navpath,navPath>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Der Begleiter folgt dem Helden, zweimal pro Sekunde neu ausgerichtet.
|
|
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) -- Der Held ist nicht auf dem Mesh: er springt, schwimmt
|
|
end
|
|
end
|
|
return OVERLAY_NOT_UPDATED
|
|
end
|
|
----
|
|
|
|
[#navagentnew]
|
|
==== navAgentNew
|
|
|
|
[source,text]
|
|
----
|
|
agent = navAgentNew(nav, node, radius, height, speed)
|
|
----
|
|
|
|
Setzt einen Agenten dieser Größe auf das Mesh, an den Punkt, der der Weltposition des Knotens am nächsten liegt (die unveränderte Position, wenn innerhalb von 2 Einheiten seitlich und 4 nach oben oder unten nichts liegt), mit einer Gehgeschwindigkeit von `speed` Einheiten pro Sekunde und einer Beschleunigung von 8 Einheiten pro Sekunde zum Quadrat. Radius und Höhe werden auf mindestens `0.01` gehalten, so wie `navNew` seine eigenen hält. Von da an bewegt die Engine den Knoten in jedem Frame entlang des Meshs, im Koordinatensystem seines Elternknotens, und dreht ihn um Y in seine Laufrichtung, wann immer er sich schneller als `0.05` Einheiten pro Sekunde bewegt; Knoten blicken entlang -Z, wie Modelle. Die Crowd steuert Agenten umeinander herum, nimmt Kurven vorweg und hält sie auseinander. Um eine Spielfigur auf dem Knoten zu steuern, statt den Knoten zu platzieren, siehe `navAgentSetPlayer`. Beendet das Skript, wenn das Mesh nicht gebaut ist oder schon 128 Agenten darauf sind.
|
|
|
|
*Parameter:*
|
|
|
|
* `nav` -- ein gebautes Mesh von `navNew` oder `navLoad`.
|
|
* `node` -- der zu bewegende Knoten; meist die Wurzel einer Modellinstanz.
|
|
* `radius` -- der Radius des Agenten, mit dem er Abstand zu anderen hält, in Welteinheiten; höchstens der eigene Radius des Meshs.
|
|
* `height` -- die Höhe des Agenten, in Welteinheiten.
|
|
* `speed` -- die höchste Gehgeschwindigkeit, in Welteinheiten pro Sekunde.
|
|
|
|
*Rückgabe:* Das Handle des Agenten, eine Ganzzahl.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<navagentmoveto,navAgentMoveTo>>, <<navagentsetplayer,navAgentSetPlayer>>, <<navagentdelete,navAgentDelete>>, <<navrandompoint,navRandomPoint>>
|
|
|
|
.Beispiel
|
|
[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)
|
|
----
|
|
|
|
Mit `true` hört der Agent auf, seinen Knoten zu platzieren, und gibt stattdessen in jedem Frame seine Geschwindigkeit als `playerMove` (nur waagerecht) an die Spielfigur auf dem Knoten weiter, sodass Treppen, Steigungen, Schieben und Kollisionen weiter funktionieren; vor jeder Aktualisierung der Crowd wird die Weltposition des Controllers als die eigene des Agenten zurückgelesen, sodass ein Abdrängen vom Kurs in der Planung berücksichtigt wird. Der Knoten dreht sich weiterhin in seine Laufrichtung. Der Knoten braucht einen Controller von `playerNew`, sonst bewegt sich nichts. Mit `false` platziert der Agent den Knoten wieder selbst.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<playernew,playerNew>>, <<playermove,playerMove>>, <<navagentnew,navAgentNew>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Begleiter, der dem Helden durch das Level folgt und herumgestoßen werden kann.
|
|
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)
|
|
----
|
|
|
|
Vergisst das Ziel des Agenten: Die Crowd steuert ihn auf nichts mehr zu, er bremst an Ort und Stelle bis zum Stillstand ab, und `onNavArrived` wird nicht ausgelöst. Andere Agenten gehen weiterhin um ihn herum. Schick ihn mit `navAgentMoveTo` irgendwohin, damit er wieder losgeht.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<navagentmoveto,navAgentMoveTo>>, <<navagentisarrived,navAgentIsArrived>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Wächter erstarren, wenn der Alarm ertönt, und drehen sich zum Geräusch.
|
|
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)
|
|
----
|
|
|
|
Backt das begehbare Mesh aus allem, was `navAddNode` hinzugefügt hat: Böden und Steigungen bis zum Winkel des Meshs, Stufen bis zu seiner Höhe, wobei um jede Wand und jede Abbruchkante der Radius des Agenten abgeschnitten und alles weggelassen wird, über dem weniger als die Höhe des Agenten frei ist. Das Backen arbeitet mit Zellen, die ein Drittel des Agentenradius breit sind, und umfasst höchstens 4096 mal 4096 davon, ein kleiner Radius über einem großen Level ist also langsam und kann fehlschlagen; eine Zehntelsekunde für Sponza auf einem Desktop, ein paar Sekunden auf einem Raspberry Pi. Die hinzugefügte Geometrie wird danach freigegeben, und es kann nichts mehr hinzugefügt werden; back stattdessen ein weiteres Mesh. `navSave` bewahrt das Ergebnis auf. Beendet das Skript, wenn nichts hinzugefügt wurde, das Mesh schon gebaut ist oder das Backen fehlschlägt.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<navnew,navNew>>, <<navaddnode,navAddNode>>, <<navsave,navSave>>, <<navdraw,navDraw>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Gebacken wird einmal, während der Titelbildschirm zu sehen ist, und fürs nächste Mal aufbewahrt.
|
|
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) -- Ein Frame, um das gebackene Mesh kurz anzusehen
|
|
end
|
|
----
|
|
|
|
[#navdelete]
|
|
==== navDelete
|
|
|
|
[source,text]
|
|
----
|
|
navDelete(nav)
|
|
----
|
|
|
|
Gibt das Mesh und jeden Agenten darauf frei; ihre Knoten bleiben, wo sie sind. Das Mesh-Handle und die Agenten-Handles sind danach ungültig und beenden das Skript, wenn sie verwendet werden. Der Platz ist frei für ein neues `navNew` oder `navLoad`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<navnew,navNew>>, <<navagentdelete,navAgentDelete>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
function loadLevel(index)
|
|
if levelNav then
|
|
navDelete(levelNav) -- Nimmt die Agenten des alten Levels mit
|
|
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)
|
|
----
|
|
|
|
Zeichnet die Polygone des gebackenen Meshs für diesen Frame als Linien über die Szene, cyan, sofern keine Farbe angegeben ist, jedes Polygon als Fächer aus Dreiecken, sodass ein Polygon mit mehr als drei Seiten seine Diagonalen zeigt. Ruf es in jedem Frame auf, solange du hinsiehst; es kostet drei Linien pro Dreieck, und ein noch nicht gebautes Mesh zeichnet nichts. Damit prüfst du, neben `physicsSetDebug`, ob ein gebackenes Mesh den Boden abdeckt und die Möbel auslässt.
|
|
|
|
*Parameter:*
|
|
|
|
* `nav` -- das Mesh.
|
|
* `r`, `g`, `b` -- die Linienfarbe, je `0` bis `255`; Standard `0, 255, 255`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<linedraw,lineDraw>>, <<navbuild,navBuild>>, <<physicssetdebug,physicsSetDebug>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
function onOverlayUpdate()
|
|
if showNavMesh then
|
|
navDraw(nav) -- Cyan über dem Boden
|
|
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)
|
|
----
|
|
|
|
Lädt ein Mesh, das `navSave` geschrieben hat, und sucht es zuerst unter den Dateien des Spiels (so kann ein gebackenes Mesh mit dem Spiel ausgeliefert werden, gepackt oder nicht) und dann im Datenordner des Spiels. Das Mesh kommt gebaut zurück, bereit für Agenten und Abfragen, und nimmt kein `navAddNode` mehr an. Steigung und Stufe, mit denen es gebacken wurde, stehen in der Datei; Radius und Höhe der Agenten sind das, womit die Crowd steuert, gib also die Werte an, für die es gebacken wurde. Beendet das Skript, wenn die Datei nicht gelesen werden kann, kein Navigationsmesh ist, kürzer ist als das Mesh, das ihr Kopf beschreibt (abgeschnitten oder von Hand bearbeitet), oder kein Mesh-Platz frei ist.
|
|
|
|
*Parameter:*
|
|
|
|
* `name` -- der Name der Datei, relativ zum Spieleverzeichnis oder zum Datenordner.
|
|
* `agentRadius` -- der Agentenradius, für den das Mesh gebacken wurde, in Welteinheiten.
|
|
* `agentHeight` -- die Agentenhöhe, für die es gebacken wurde, in Welteinheiten.
|
|
|
|
*Rückgabe:* Das Mesh-Handle, eine Ganzzahl.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<navsave,navSave>>, <<navnew,navNew>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das mitgelieferte gebackene Mesh für das Museum; Agenten 0,8 breit und 1,7 hoch.
|
|
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)
|
|
----
|
|
|
|
Der Punkt des Meshs, der einem Punkt am nächsten liegt, gesucht bis zu 2 Einheiten seitlich und 4 nach oben oder unten, oder `nil`, wenn nichts so nah ist. Damit rastest du einen Spawnpunkt, einen Abwurfpunkt oder einen Klick auf dem Boden ein, bevor du ihn einem Agenten übergibst, und findest die Höhe des Bodens unter einem Punkt.
|
|
|
|
*Parameter:*
|
|
|
|
* `nav` -- das Mesh.
|
|
* `x`, `y`, `z` -- der einzurastende Punkt, in Welteinheiten.
|
|
|
|
*Rückgabe:* Drei Zahlen oder `nil`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<navpath,navPath>>, <<navagentmoveto,navAgentMoveTo>>, <<sceneunproject,sceneUnproject>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Den Gegner auf dem Boden erscheinen lassen, der dem Spawner am nächsten ist, oder gar nicht.
|
|
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)
|
|
----
|
|
|
|
Beginnt ein Navigationsmesh für Agenten mit diesem Radius und dieser Höhe, die Stufen bis `maxStep` Einheiten und Steigungen bis `maxSlope` Grad erklimmen können. Radius und Höhe werden auf mindestens `0.01` gehalten, die Steigung zwischen `0` und `89` und die Stufe auf mindestens `0`. Der Radius legt auch die Auflösung des Backens fest, da eine Zelle ein Drittel davon ist; er entscheidet also, wie lange `navBuild` braucht und wie groß ein Level sein kann, das es abdeckt. Füge Geometrie mit `navAddNode` hinzu, dann `navBuild`. Beendet das Skript, wenn schon acht Meshes existieren.
|
|
|
|
*Parameter:*
|
|
|
|
* `agentRadius` -- der Radius der Agenten, in Welteinheiten; Wände werden um so viel aufgepolstert.
|
|
* `agentHeight` -- wie hoch sie sind; unter niedrigeren Decken ist nichts begehbar.
|
|
* `maxSlope` -- die steilste begehbare Steigung, in Grad.
|
|
* `maxStep` -- die höchste Stufe, die sie erklimmen, in Welteinheiten.
|
|
|
|
*Rückgabe:* Das Mesh-Handle, eine Ganzzahl.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<navaddnode,navAddNode>>, <<navbuild,navBuild>>, <<navload,navLoad>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Sponza für einen Agenten, 0,8 breit und 1,6 hoch, der 0,3 hoch steigt und Steigungen bis 45 Grad geht.
|
|
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)
|
|
----
|
|
|
|
Die Eckpunkte des kürzesten Wegs von einem Punkt zu einem anderen, beide zuerst auf das Mesh eingerastet, als Tabelle von `{ x, y, z }`-Tabellen, die erste am Start und die letzte am Ende; bis zu 256 Eckpunkte. Kann das Ende nicht erreicht werden, endet der Weg am nächsten erreichbaren Punkt unterwegs, sodass der letzte Eckpunkt immer dort liegt, wo ein Agent stehen kann. `nil`, wenn eines der Enden nicht in der Nähe des Meshs liegt oder kein Weg gefunden wird. Damit zeichnest du eine Route, misst, wie weit etwas zu Fuß entfernt ist, oder bewegst selbst etwas daran entlang; Agenten tun das von sich aus.
|
|
|
|
*Parameter:*
|
|
|
|
* `nav` -- das Mesh.
|
|
* `x0`, `y0`, `z0` -- wo der Weg beginnt.
|
|
* `x1`, `y1`, `z1` -- wo er endet.
|
|
|
|
*Rückgabe:* Eine Tabelle von `{ x, y, z }`-Tabellen oder `nil`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<navagentmoveto,navAgentMoveTo>>, <<navraycast,navRaycast>>, <<navnearest,navNearest>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Wie weit der Ausgang zu Fuß ist, für das Entfernungs-Anzeigefeld der Karte.
|
|
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)
|
|
----
|
|
|
|
Ein zufälliger Punkt auf dem begehbaren Mesh, irgendwo innerhalb seiner Reichweite, zum Umherwandern, zum Verstreuen von Gegenständen zum Aufsammeln oder zum Platzieren eines Spawnpunkts. `nil` nur, wenn das Mesh nicht gebaut ist.
|
|
|
|
*Rückgabe:* Drei Zahlen oder `nil`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<navagentmoveto,navAgentMoveTo>>, <<navnearest,navNearest>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Zwanzig Münzen über den Boden streuen, jede ein wenig darüber.
|
|
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)
|
|
----
|
|
|
|
Ob ein gerader Weg von einem Punkt zu einem anderen das Mesh verlässt, an einer Wand oder einer Abbruchkante, und wo er es täte. Der Start wird auf das Mesh eingerastet; `false` allein heißt, dass der Weg frei ist oder dass der Start nicht in der Nähe des Meshs liegt. Ist er blockiert, ist der Punkt dort, wo die Linie den Rand des Meshs kreuzt, mit einer zwischen den beiden Enden interpolierten Höhe. Eine billige Prüfung, ob eine Abkürzung existiert, bevor du `navPath` fragst.
|
|
|
|
*Parameter:*
|
|
|
|
* `nav` -- das Mesh.
|
|
* `x0`, `y0`, `z0` -- wo der Weg beginnt.
|
|
* `x1`, `y1`, `z1` -- wo er enden würde.
|
|
|
|
*Rückgabe:* `false` oder `true` und der Punkt, an dem der Weg das Mesh verlässt.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<navpath,navPath>>, <<physicsraycast,physicsRaycast>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine geworfene Granate rollt nur so weit, wie der Boden es zulässt.
|
|
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)
|
|
----
|
|
|
|
Schreibt das gebackene Mesh unter `name` in den Datenordner des Spiels, für `navLoad` beim nächsten Mal oder zum Kopieren in den Spielordner, um es auszuliefern. Das Mesh muss gebaut sein. Beendet das Skript, wenn es das nicht ist oder die Datei nicht geschrieben werden kann.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<navload,navLoad>>, <<navbuild,navBuild>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Einmal auf diesem Rechner gebacken, in späteren Läufen dann aus dem Datenordner geladen.
|
|
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]
|
|
=== Netzwerk
|
|
|
|
Was die Engine unter `Singe/Net.singe` hinzufügt, dem HTTP-Client des Menüs.
|
|
Die meisten Spiele berühren es nie: Einen Punktestand senden oder einen Katalog
|
|
abrufen läuft über `Net.singe`, das dies für dich aufruft.
|
|
|
|
[#nettrustsystem]
|
|
==== netTrustSystem
|
|
|
|
[source,text]
|
|
----
|
|
lists, report = netTrustSystem(context)
|
|
----
|
|
|
|
Fügt die eigenen vertrauenswürdigen Stammzertifikate des Rechners zu einem
|
|
LuaSec-Client-Kontext hinzu, zusätzlich zu dem, was der Kontext schon enthält
|
|
(normalerweise das mitgelieferte `Singe/cacerts.pem`). Eine mit dem Kontext
|
|
aufgebaute Verbindung wird dann bestätigt, wenn eine der beiden Listen die
|
|
Wurzel des Servers kennt. Die Liste des Rechners ist, der Reihe nach: die Datei
|
|
oder das Verzeichnis, das `SSL_CERT_FILE` oder `SSL_CERT_DIR` nennt, wenn
|
|
gesetzt; sonst unter Linux das Bündel der Distribution (der erste der üblichen
|
|
sechs Orte, der existiert), unter Windows der Stammzertifikatspeicher des
|
|
Systems oder unter macOS die Vertrauensanker des Systems. `cacerts.local.pem`
|
|
im Datenordner wird ebenfalls hinzugefügt, wenn es existiert; die Engine
|
|
schreibt es nie.
|
|
|
|
`lists` ist die Anzahl der Listen, die etwas hinzugefügt haben. `report` nennt,
|
|
was befragt wurde, ein `what path` pro Zeile (`what` ist `system` oder
|
|
`local`), für eine Fehlermeldung, mit der ein Mensch etwas anfangen kann.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<utilsha256,utilSha256>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Kontext für jede Anfrage: das mitgelieferte Bündel, darüber die Wurzeln dieses Rechners.
|
|
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]
|
|
=== Knoten
|
|
|
|
Jedes Objekt in einer Szene ist ein Knoten in einem einzigen Baum: Knoten `0`
|
|
ist die Wurzel, und jeder andere Knoten hängt unter einem Elternknoten. Ein
|
|
Knoten trägt eine Position, eine Rotation und eine Skalierung relativ zu diesem
|
|
Elternknoten und kann ein Mesh mit einem Material zeichnen, ein Sprite oder
|
|
eine Textzeile zeigen oder als Licht leuchten; Kinder erben die Transformation
|
|
von allem über ihnen. Handles sind Ganzzahlen, die `nodeNew`, `modelInstance`
|
|
und `lightNew` zurückgeben; ein Handle, dessen Knoten gelöscht wurde, bricht
|
|
das Skript ab, wenn es wieder übergeben wird, und ein freigegebenes Handle kann
|
|
von einem späteren `nodeNew` wiederverwendet werden. Positionen und Größen sind
|
|
in Szeneneinheiten und Rotationen in Grad, mit +Y nach oben und -Z nach vorn,
|
|
wie unter <<scenes3d,3D-Szenen>> beschrieben.
|
|
|
|
[#nodenew]
|
|
==== nodeNew
|
|
|
|
[source,text]
|
|
----
|
|
node = nodeNew()
|
|
node = nodeNew(parent)
|
|
----
|
|
|
|
Erzeugt einen leeren Knoten im Ursprung seines Elternknotens, ungedreht, mit
|
|
Skalierung `1`, sichtbar, Schatten werfend, ohne Mesh oder Material. Ohne
|
|
`parent` kommt der Knoten unter die Wurzel; mit einem wird er das letzte Kind
|
|
dieses Knotens und bewegt sich von da an mit ihm. Leere Knoten dienen als
|
|
Drehpunkte und Gruppen: Platziere einen, häng dann Meshes, Lichter oder eine
|
|
Kamera darunter und beweg die Gruppe als Ganzes.
|
|
|
|
*Rückgabe:* Das Knoten-Handle.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<nodesetmesh,nodeSetMesh>>, <<nodesetparent,nodeSetParent>>, <<nodedelete,nodeDelete>>, <<lightnew,lightNew>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Geschützturm, dessen Rohr sich am Turm dreht, der sich wiederum auf dem Sockel dreht.
|
|
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)
|
|
----
|
|
|
|
Entfernt den Knoten und alles darunter und gibt jedes Handle in diesem
|
|
Teilbaum frei; ein Sprite oder Text auf einem davon wird ebenfalls freigegeben.
|
|
Meshes und Materialien, die die Knoten gezeichnet haben, bleiben unberührt, da
|
|
andere Knoten sie teilen können. War der gelöschte Knoten die Kamera, fällt die
|
|
Szene auf die Standardansicht zurück. Der Wurzelknoten kann nicht gelöscht
|
|
werden; der Versuch bricht das Skript ab.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<nodenew,nodeNew>>, <<nodesetvisible,nodeSetVisible>>, <<meshdelete,meshDelete>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Schüsse leben zwei Sekunden, dann verschwinden ihre Knoten.
|
|
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)
|
|
----
|
|
|
|
Sucht per Tiefensuche nach dem ersten Knoten namens `name`, beginnend bei
|
|
`root` (der Wurzel der Szene, wenn weggelassen), und prüft `root` selbst vor
|
|
seinen Kindern, den Teilbaum jedes Kindes der Reihe nach. Gibt `nil` zurück,
|
|
wenn nichts passt. Modellknoten tragen die Namen aus ihrer glTF-Datei, sodass
|
|
ein Knochen, ein Mesh oder ein in Blender als Markierung platziertes Empty unter
|
|
der Wurzel der Instanz über diesen Namen gefunden werden kann. Mit
|
|
`nodeSetName` gesetzte Namen werden genauso gefunden.
|
|
|
|
*Rückgabe:* Ein Knoten-Handle oder `nil`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<nodesetname,nodeSetName>>, <<nodegetname,nodeGetName>>, <<modelinstance,modelInstance>>, <<nodegetchildren,nodeGetChildren>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Fackel an die rechte Hand des Helden hängen, gefunden über den Namen ihres Knochens.
|
|
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)
|
|
----
|
|
|
|
Die direkten Kinder des Knotens als Tabelle von Handles, in der Reihenfolge,
|
|
in der sie angehängt wurden; ein Knoten, der einen neuen Elternknoten bekommt,
|
|
kommt ans Ende von dessen Liste. Enkel sind nicht enthalten; geh rekursiv vor,
|
|
um einen ganzen Teilbaum abzulaufen. Eine leere Tabelle heißt, dass der Knoten
|
|
keine Kinder hat.
|
|
|
|
*Rückgabe:* Eine Tabelle von Knoten-Handles.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<nodegetparent,nodeGetParent>>, <<nodefind,nodeFind>>, <<nodesetparent,nodeSetParent>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Jedem Teil einer geladenen Requisite dasselbe abgenutzte Material geben.
|
|
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)
|
|
----
|
|
|
|
Das aktuelle Gewicht eines Morph-Ziels des Meshs des Knotens, angegeben über
|
|
den Namen oder über die Nummer ab `1` in der Reihenfolge, in der
|
|
`nodeGetMorphs` sie auflistet. Der Knoten ist der, der das Mesh trägt, nicht
|
|
die Wurzel einer Modellinstanz. Ein Name oder eine Nummer, die das Mesh nicht
|
|
hat, bricht das Skript ab.
|
|
|
|
*Rückgabe:* Eine Zahl, `0`, bis etwas das Ziel setzt.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<nodesetmorph,nodeSetMorph>>, <<nodegetmorphs,nodeGetMorphs>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Lächeln über mehrere Frames einblenden, statt es schlagartig einzuschalten.
|
|
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)
|
|
----
|
|
|
|
Die Morph-Ziele des Meshs des Knotens als Tabelle von Namen, in der
|
|
Reihenfolge, in der die Datei sie speichert; diese Reihenfolge ist die
|
|
Nummerierung, die `nodeSetMorph` und `nodeGetMorph` annehmen. Exporter, die
|
|
keine Namen schreiben, ergeben leere Strings, und diese Ziele lassen sich
|
|
trotzdem über die Nummer setzen. Ein Knoten ohne Mesh oder ein Mesh ohne Ziele
|
|
ergibt eine leere Tabelle.
|
|
|
|
*Rückgabe:* Eine Tabelle von Strings.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<nodesetmorph,nodeSetMorph>>, <<nodegetmorph,nodeGetMorph>>, <<modelload,modelLoad>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Zielnamen des Gesichts einmal Nummern zuordnen, damit die Lippensynchronisation sie indizieren kann.
|
|
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)
|
|
----
|
|
|
|
Der Name des Knotens: was auch immer `nodeSetName` ihm gegeben hat, bei einem
|
|
Modellknoten sein glTF-Name, ein leerer String, wenn er keines von beiden hat.
|
|
Der Wurzelknoten heißt `root`.
|
|
|
|
*Rückgabe:* Ein String.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<nodesetname,nodeSetName>>, <<nodefind,nodeFind>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Den Knotenbaum eines Modells auf der Konsole ausgeben, während man die Knochennamen herausfindet.
|
|
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)
|
|
----
|
|
|
|
Das Handle des Elternknotens oder `nil` für die Wurzel. Jeder andere Knoten
|
|
hat einen Elternknoten, und sei es nur die Wurzel.
|
|
|
|
*Rückgabe:* Ein Knoten-Handle oder `nil`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<nodesetparent,nodeSetParent>>, <<nodegetchildren,nodeGetChildren>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Von einem angeklickten Mesh-Knoten bis zur Spitze seiner Modellinstanz hinaufsteigen.
|
|
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)
|
|
----
|
|
|
|
Die Position des Knotens relativ zu seinem Elternknoten, wie zuletzt von
|
|
`nodeSetPosition`, `nodeMove` oder der Engine gesetzt (ein Körper, ein Fahrzeug
|
|
oder eine Animation, die ihn bewegt). Wo der Knoten in der Welt ist, sagt
|
|
`nodeGetWorldPosition`.
|
|
|
|
*Rückgabe:* Drei Zahlen.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<nodesetposition,nodeSetPosition>>, <<nodegetworldposition,nodeGetWorldPosition>>, <<nodemove,nodeMove>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Kamera folgt dem Auto, drei Einheiten darüber, drei rechts daneben und neun dahinter.
|
|
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)
|
|
----
|
|
|
|
Die Rotation des Knotens relativ zu seinem Elternknoten als
|
|
Einheitsquaternion, die Form, in der Modelle und Animationen sie speichern.
|
|
Damit kopierst du eine Rotation zwischen Knoten oder bewahrst eine Pose ohne
|
|
die Mehrdeutigkeit von Euler-Winkeln auf.
|
|
|
|
*Rückgabe:* Vier Zahlen.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<nodesetquaternion,nodeSetQuaternion>>, <<nodegetrotation,nodeGetRotation>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Pose der Tür vor dem Ragdoll-Test merken und sie danach zurücksetzen.
|
|
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)
|
|
----
|
|
|
|
Die Rotation des Knotens relativ zu seinem Elternknoten als Euler-Winkel in
|
|
Grad, auf dieselbe Weise zerlegt, wie `nodeSetRotation` sie zusammensetzt: um
|
|
Y gedreht, dann um X geneigt, dann um Z gerollt. Die Winkel beschreiben
|
|
dieselbe Rotation, die gesetzt wurde, müssen aber nicht dieselben Zahlen sein:
|
|
`X` kommt zwischen `-90` und `90` zurück, und wenn es `90` oder `-90`
|
|
erreicht, wird die Verdrehung `Y` zugeschlagen und `Z` als `0` gemeldet.
|
|
|
|
*Rückgabe:* Drei Zahlen.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<nodesetrotation,nodeSetRotation>>, <<nodegetquaternion,nodeGetQuaternion>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Kompass im Overlay nach dem Kurs des Schiffs.
|
|
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)
|
|
----
|
|
|
|
Die eigene Skalierung des Knotens pro Achse, wie von `nodeSetScale` gesetzt
|
|
(`1, 1, 1` bei einem neuen Knoten). Die Skalierungen der Elternknoten sind
|
|
nicht eingerechnet.
|
|
|
|
*Rückgabe:* Drei Zahlen.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<nodesetscale,nodeSetScale>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Power-up in jedem Frame ein wenig zur vollen Größe wachsen lassen.
|
|
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)
|
|
----
|
|
|
|
Wo der Ursprung des Knotens in Weltkoordinaten liegt, mit der Transformation
|
|
jedes Elternknotens angewandt, Stand des zuletzt gerenderten Frames. In diesem
|
|
Frame gemachte Änderungen (deine oder die eines Körpers) zeigen sich nach dem
|
|
nächsten Rendern, und ein in diesem Frame erzeugter Knoten meldet bis dahin
|
|
`0, 0, 0`. Das ist die Position, die du `sceneProject` oder einer
|
|
Abstandsprüfung übergibst.
|
|
|
|
*Rückgabe:* Drei Zahlen.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<sceneproject,sceneProject>>, <<nodelookat,nodeLookAt>>, <<nodegetposition,nodeGetPosition>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Namensschild im Overlay über der Ente, wohin die Physik sie auch gebracht hat.
|
|
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)
|
|
----
|
|
|
|
Dreht den Knoten so, dass sein eigenes -Z auf eine Position in der Welt zeigt,
|
|
und hält sein +Y so nah an der Aufwärtsrichtung der Welt, wie die Richtung es
|
|
zulässt. Das wollen Kameras und Lichter, und es dient ebenso einem
|
|
Geschützturm oder einem Kopf, der ein Ziel verfolgt. Nur die Rotation des
|
|
Knotens ändert sich; seine Position bleibt. Fällt das Ziel mit der Position
|
|
des Knotens zusammen, ändert sich nichts.
|
|
|
|
*Hinweise:* Der Weltpunkt wird mit der Transformation des Elternknotens aus dem
|
|
zuletzt gerenderten Frame in die Koordinaten des Elternknotens umgerechnet; ein
|
|
Knoten, dessen Elternknoten sich in eben diesem Frame bewegt hat, zielt von
|
|
dort, wo der Elternknoten war.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<cameraset,cameraSet>>, <<lightnew,lightNew>>, <<nodesetrotation,nodeSetRotation>>
|
|
|
|
.Beispiel
|
|
[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)
|
|
----
|
|
|
|
Bewegt den Knoten entlang seiner eigenen Achsen: Der Versatz wird mit der
|
|
aktuellen Rotation des Knotens gedreht und zu seiner Position addiert, sodass
|
|
`nodeMove(ship, 0, 0, -0.1)` das Schiff vorwärts fährt, wohin es auch zeigt,
|
|
und `nodeMove(ship, 0.1, 0,
|
|
0)` es seitlich nach rechts versetzt. Für einen absoluten Ort in den
|
|
Koordinaten des Elternknotens nimm `nodeSetPosition`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<nodesetposition,nodeSetPosition>>, <<noderotate,nodeRotate>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Panzersteuerung: mit links und rechts drehen, mit hoch und runter fahren.
|
|
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)
|
|
----
|
|
|
|
Dreht den Knoten um die angegebenen Grad um seine eigenen Achsen, zusätzlich
|
|
zu seiner aktuellen Rotation: `nodeRotate(wheel, 5, 0, 0)` in jedem Frame
|
|
lässt das Rad um sein eigenes X kreisen, wie auch immer die Achse geneigt ist.
|
|
Die drei Winkel werden so kombiniert, wie `nodeSetRotation` sie kombiniert (Y,
|
|
dann X, dann Z), bevor sie angewandt werden. Positive Winkel drehen gegen den
|
|
Uhrzeigersinn, wenn man entlang der Achse zum Ursprung blickt.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<nodesetrotation,nodeSetRotation>>, <<nodemove,nodeMove>>, <<nodesetquaternion,nodeSetQuaternion>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Logo dreht sich pro Frame um ein halbes Grad um seine eigene Hochachse.
|
|
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)
|
|
----
|
|
|
|
Dreht den Knoten in jedem Frame zur Kamera hin. `BILLBOARD_ALL` richtet ihn
|
|
frontal zu ihr aus und übernimmt alle drei Achsen von der Kamera;
|
|
`BILLBOARD_Y` dreht nur um die Hochachse der Welt, sodass der Knoten aufrecht
|
|
bleibt (Bäume, Lebensbalken); `BILLBOARD_NONE`, der Standard, dreht gar nicht.
|
|
Weltposition und Skalierung des Knotens bleiben erhalten, und seine eigene
|
|
Rotation wird ignoriert, solange ein Modus gesetzt ist. Jeder andere Wert
|
|
bricht das Skript ab. Es funktioniert bei Meshes ebenso wie bei Sprites und
|
|
Text und dreht sich zu der Kamera, die gerade zeichnet: Auch eine Ansicht
|
|
sieht das Billboard von vorn, während sein Schatten der Kamera des Fensters
|
|
folgt.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<nodesetsprite,nodeSetSprite>>, <<nodesettext,nodeSetText>>, <<cameraset,cameraSet>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Name über dem Fuchs, frontal zur Kamera, und ein Kisten-Symbol, das
|
|
-- sich nur um seine eigene Hochachse dreht.
|
|
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)
|
|
----
|
|
|
|
Wählt, wo im Fenster der Knoten gezeichnet wird. `LAYER_SCENE`, der Standard,
|
|
zeichnet ihn in der Szene unter dem Overlay; `LAYER_FRONT` zeichnet ihn nach
|
|
dem Overlay, mit derselben Kamera und denselben Lichtern, in eine eigene
|
|
transparente Ebene. Ein Knoten in der vorderen Ebene nimmt seine Kinder mit,
|
|
gleich wie sie selbst eingestellt sind. Jeder andere Wert bricht das Skript
|
|
ab. Siehe <<scenefront,Vor dem Overlay>>.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<sceneenable,sceneEnable>>, <<nodesetvisible,nodeSetVisible>>, <<emittersetlayer,emitterSetLayer>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Schiff des Spielers steht auf dem 2D-Kartenbildschirm, solange er offen ist, und kehrt
|
|
-- in die Welt zurück, wenn er sich schließt.
|
|
function showMap(open)
|
|
nodeSetLayer(ship, open and LAYER_FRONT or LAYER_SCENE)
|
|
end
|
|
----
|
|
|
|
[#nodesetmaterial]
|
|
==== nodeSetMaterial
|
|
|
|
[source,text]
|
|
----
|
|
nodeSetMaterial(node, material)
|
|
nodeSetMaterial(node, nil)
|
|
----
|
|
|
|
Legt ein Material auf das Mesh, das der Knoten hat, und behält das Mesh. `nil`
|
|
entfernt es und lässt das Standardaussehen: weiß, halb rau, ohne Textur. Damit
|
|
gestaltest du ein Teil eines Modells um, wechselst ein Hervorhebungsmaterial
|
|
ein und aus oder kleidest ein Mesh ein, das die Engine für dich gemacht hat,
|
|
etwa die Röhre eines Seils aus `softNew`. Ein Material-Handle, das nicht
|
|
existiert, bricht das Skript ab.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<nodesetmesh,nodeSetMesh>>, <<materialnew,materialNew>>, <<softnew,softNew>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Seil hängt am Kran; die Engine macht seine Röhre, wir kleiden sie ein.
|
|
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)
|
|
----
|
|
|
|
Gibt dem Knoten ein Mesh zum Zeichnen, mit einem Material oder, wenn
|
|
weggelassen, dem Standardaussehen (weiß, halb rau). Beide Argumente müssen
|
|
gültige Handles sein; ein fehlendes Mesh oder Material bricht das Skript ab.
|
|
Jeder Knoten kann zeichnen, auch einer innerhalb einer Modellinstanz, und
|
|
beliebig viele Knoten können sich ein Mesh teilen. Die Morph-Gewichte des
|
|
Knotens werden auf die Ziele des neuen Meshs bemessen und beginnen bei
|
|
`0`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<meshbox,meshBox>>, <<materialnew,materialNew>>, <<nodesetmaterial,nodeSetMaterial>>, <<nodesetvisible,nodeSetVisible>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Autokarosserie mit vier Rädern, jedes ein Zylinder, so gedreht, dass sein X die Achse ist.
|
|
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)
|
|
----
|
|
|
|
Legt fest, wie viel eines Morph-Ziels (Blend Shape) das Mesh des Knotens
|
|
zeigt, über den Namen oder über die Nummer ab `1`; Gewichte liegen meist
|
|
zwischen `0` und `1`, und mehrere Ziele addieren sich. Der Knoten ist der, der
|
|
das Mesh trägt, bei einem Modell also ein Kind der Instanzwurzel (`nodeFind`
|
|
mit dem Namen des Meshs oder `nodeGetChildren`). Ein Ziel, das das Mesh nicht
|
|
hat, bricht das Skript ab. Eine Animation mit einem Gewichtskanal schreibt die
|
|
Gewichte in jedem Frame, solange sie läuft, und überschreibt, was du gesetzt
|
|
hast.
|
|
|
|
*Hinweise:* Bis zu acht Ziele mit einem Gewicht ungleich null wirken pro Mesh
|
|
in einem Frame; bei mehr werden die acht mit dem größten Betrag des Gewichts
|
|
verwendet.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<nodegetmorph,nodeGetMorph>>, <<nodegetmorphs,nodeGetMorphs>>, <<animationplay,animationPlay>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Lippensynchronisation aus einer Tabelle von Mundöffnungen pro Disc-Frame.
|
|
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)
|
|
----
|
|
|
|
Benennt den Knoten, damit `nodeFind` ihn später finden kann, und ersetzt
|
|
jeden Namen, den er hatte (den glTF-Namen eines Modellknotens eingeschlossen).
|
|
Namen müssen nicht eindeutig sein; `nodeFind` gibt den ersten Treffer in der
|
|
Reihenfolge der Tiefensuche zurück.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<nodefind,nodeFind>>, <<nodegetname,nodeGetName>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Erzeugte Kisten bekommen Namen, damit das Level-Skript sie über ihren Platz findet.
|
|
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)
|
|
----
|
|
|
|
Verschiebt den Knoten mit seinem ganzen Teilbaum unter einen anderen
|
|
Elternknoten, als dessen letztes Kind. Lokale Position, Rotation und
|
|
Skalierung des Knotens bleiben, wie sie sind, er sitzt nun also mit diesem
|
|
Versatz zum neuen Elternknoten und bewegt sich mit ihm; übergib `0`, um ihn
|
|
zur Wurzel zurückzubringen. Die Wurzel kann nicht verschoben werden, und ein
|
|
Knoten kann nicht unter sich selbst oder einen seiner eigenen Nachkommen;
|
|
beides bricht das Skript ab.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<nodegetparent,nodeGetParent>>, <<nodenew,nodeNew>>, <<nodesetposition,nodeSetPosition>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Kiste aufheben: Sie fährt von nun an in der Hand mit und wird später abgesetzt.
|
|
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)
|
|
----
|
|
|
|
Setzt den Knoten auf `x, y, z` in den Koordinaten seines Elternknotens; für
|
|
einen Knoten unter der Wurzel sind das Weltkoordinaten. Kinder kommen mit. Ein
|
|
Knoten mit einem dynamischen Körper wird danach von der Physik bewegt; setz
|
|
seine Position vor `bodyNew` oder teleportiere ihn über die Körper-Aufrufe.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<nodemove,nodeMove>>, <<nodegetposition,nodeGetPosition>>, <<nodesetrotation,nodeSetRotation>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Kisten, entlang eines Grats fallen gelassen, jede sechs Einheiten über dem Boden an ihrer Stelle.
|
|
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)
|
|
----
|
|
|
|
Setzt die Rotation des Knotens relativ zu seinem Elternknoten als Quaternion,
|
|
für Werte, die aus `nodeGetQuaternion`, einem Modell oder deiner eigenen
|
|
Rechnung stammen. Die Quaternion wird beim Übernehmen normalisiert, muss also
|
|
nicht die Länge eins haben.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<nodesetrotation,nodeSetRotation>>, <<nodegetquaternion,nodeGetQuaternion>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Der Geist kopiert die Pose des Helden genau, einen Takt später.
|
|
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)
|
|
----
|
|
|
|
Setzt die Rotation des Knotens relativ zu seinem Elternknoten als Euler-Winkel
|
|
in Grad und ersetzt, was er hatte: Der Knoten wird zuerst um sein Y gedreht
|
|
(Gieren), dann um sein X geneigt (Nicken), dann um sein Z gerollt. Positive
|
|
Winkel drehen gegen den Uhrzeigersinn, wenn man entlang der Achse zum Ursprung
|
|
blickt, sodass `nodeSetRotation(plane, 90, 0, 0)` eine Bodenebene aufrichtet,
|
|
bis sie nach +Z zeigt.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<noderotate,nodeRotate>>, <<nodesetquaternion,nodeSetQuaternion>>, <<nodelookat,nodeLookAt>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Monitorbildschirm auf einem Ständer, der zur Kamera bei +Z zeigt.
|
|
local monitor = nodeNew()
|
|
nodeSetMesh(monitor, meshPlane(2.0, 1.5), screen)
|
|
nodeSetPosition(monitor, -3, 2.1, 1.06)
|
|
nodeSetRotation(monitor, 90, 0, 0)
|
|
-- Eine Rampe, um 25 Grad um Z geneigt.
|
|
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)
|
|
----
|
|
|
|
Skaliert den Knoten und alles darunter, gleichmäßig mit einem Faktor oder pro
|
|
Achse mit dreien. Geladene Modelle sind oft in Zentimetern oder Metern;
|
|
skaliere die Instanzwurzel einmal passend zu deiner Szene und platziere Dinge
|
|
darunter in den eigenen Einheiten des Modells. Sprites und Text auf dem Knoten
|
|
werden ebenfalls skaliert, zusätzlich zu ihrer `height`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<nodegetscale,nodeGetScale>>, <<modelinstance,modelInstance>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Der Fuchs ist in Zentimetern modelliert: ein Hundertstel bringt ihn auf Meter.
|
|
local fox = modelInstance(foxModel)
|
|
nodeSetParent(fox, player)
|
|
nodeSetScale(fox, 0.01)
|
|
nodeSetRotation(fox, 0, 90, 0)
|
|
-- Eine abgeflachte Kugel als Pfütze.
|
|
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)
|
|
----
|
|
|
|
Ob das Mesh des Knotens in Shadow Maps gezeichnet wird; `true` bei jedem neuen
|
|
Knoten. Schalte es für das Mesh um ein Licht herum aus (eine Glühbirne, eine
|
|
Flamme), damit es sein eigenes Licht nicht abschattet, und für alles, was
|
|
leuchtet. Der Knoten zeichnet weiterhin und empfängt weiterhin Schatten von
|
|
anderen. Es gilt nur für den Knoten, nicht für seine Kinder.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<lightsetshadow,lightSetShadow>>, <<nodesetvisible,nodeSetVisible>>, <<materialsetunlit,materialSetUnlit>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Glühbirne in ihrem Schirm: Das Glas wirft keinen Schatten, der Schirm schon.
|
|
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)
|
|
----
|
|
|
|
Zeigt ein geladenes Sprite auf dem Knoten: ein Quad, `height` Welteinheiten
|
|
hoch (Standard `1`) und so breit, wie das Seitenverhältnis des Bilds es
|
|
ergibt, nach dem Alpha des Bilds geblendet, von beiden Seiten gezeichnet und
|
|
von der Szene beleuchtet, sofern `lit` nicht `false` ist. Jeder Frame eines
|
|
animierten Sprites wird hochgeladen, und der erste wird gezeigt;
|
|
`nodeSetSpriteFrame` wählt einen anderen. Das Quad wird zum Mesh des Knotens
|
|
mit einem eigenen Material und ersetzt jedes Mesh, Sprite oder jeden Text, den
|
|
er hatte; `nil` oder kein zweites Argument nimmt das Bild ab und lässt den
|
|
Knoten leer. Es bricht das Skript ab, wenn der 3D-Renderer nicht verfügbar
|
|
ist.
|
|
|
|
*Parameter:*
|
|
|
|
* `sprite` -- ein Handle von `spriteLoad` oder `nil` zum Entfernen.
|
|
* `height` -- die Höhe des Quads in Welteinheiten, Standard `1`; die Skalierung des Knotens multipliziert sie.
|
|
* `lit` -- `true` (Standard) schattiert das Bild mit den Lichtern und Schatten der Szene; `false` zeigt es, wie es ist.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<nodesetspriteframe,nodeSetSpriteFrame>>, <<nodesetbillboard,nodeSetBillboard>>, <<spriteload,spriteLoad>>, <<nodesettext,nodeSetText>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Kisten-Symbol, das über dem Fuchs schwebt und sich zum Spieler dreht.
|
|
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)
|
|
-- Ein unbeleuchtetes Leuchten, das im Schatten genauso wirkt.
|
|
local flare = nodeNew(torchTip)
|
|
nodeSetSprite(flare, spriteLoad("flare.png"), 0.3, false)
|
|
nodeSetBillboard(flare, BILLBOARD_ALL)
|
|
----
|
|
|
|
[#nodesetspriteframe]
|
|
==== nodeSetSpriteFrame
|
|
|
|
[source,text]
|
|
----
|
|
nodeSetSpriteFrame(node, frame)
|
|
----
|
|
|
|
Welcher Frame des animierten Sprites des Knotens gezeigt wird, gezählt ab
|
|
`0`. Das Sprite des Knotens läuft nicht von selbst weiter; treib es aus
|
|
`onOverlayUpdate` an. Ein Knoten ohne Sprite oder ein Frame, den es nicht hat,
|
|
bricht das Skript ab; Text und ein unbewegtes Sprite haben nur Frame `0`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<nodesetsprite,nodeSetSprite>>, <<spriteload,spriteLoad>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Lagerfeuer-Sprite, das seine Frames mit zehn pro Sekunde durchläuft.
|
|
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)
|
|
----
|
|
|
|
Zeigt eine Textzeile auf dem Knoten, gerendert in der gewählten Schriftart
|
|
(`fontSelect`) in der aktuellen Qualität (`fontQuality`) in der
|
|
Vordergrundfarbe (`colorForeground`), auf einem Quad, `height` Welteinheiten
|
|
hoch (Standard `1`) und so breit, wie der gerenderte Text es ergibt. Er ist
|
|
unbeleuchtet, geblendet und von beiden Seiten gezeichnet: Namensschilder,
|
|
Schilder, ein Punktestand über einem Tisch. Wie ein Sprite wird er zum Mesh
|
|
des Knotens und ersetzt jedes Mesh, Sprite oder jeden Text dort; ruf es erneut
|
|
auf, um den Text zu ändern, und mit `nil` oder ohne zweites Argument, um ihn
|
|
abzunehmen. Ohne gewählte Schriftart oder ohne den 3D-Renderer bricht es das
|
|
Skript ab.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<nodesetsprite,nodeSetSprite>>, <<nodesetbillboard,nodeSetBillboard>>, <<fontselect,fontSelect>>, <<colorforeground,colorForeground>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein festes Schild an der Lampe und ein Punktestand, der sich aktualisiert, wenn er sich ändert.
|
|
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)
|
|
----
|
|
|
|
Versteckt oder zeigt den Knoten und alles darunter; das eigene Flag eines
|
|
Kindes gilt weiterhin, wenn sein Elternknoten wieder gezeigt wird. Versteckte
|
|
Meshes werden nicht gezeichnet, und versteckte Lichter hören auf zu leuchten.
|
|
Damit hältst du eine Markierung oder eine Ersatz-Requisite bereit, ohne sie zu
|
|
löschen und neu zu bauen.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<nodesetmesh,nodeSetMesh>>, <<nodedelete,nodeDelete>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Markierung, die dort erscheint, wo der Ball zuletzt den Schläger getroffen hat.
|
|
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
|
|
|
|
Das Overlay ist eine 32-Bit-RGBA-Fläche, die in jedem Frame über das Video gelegt wird; alles 2D-Zeichnen landet darauf. Seine Koordinaten sind die mit `overlaySetResolution` gesetzte Overlay-Auflösung, standardmäßig die halbe Breite und Höhe des Videos, und werden bei der Anzeige auf das Fenster skaliert. Zeichne aus `onOverlayUpdate` (oder im <<threaded,Thread-Modell>> aus `singeMain`) und gib `OVERLAY_UPDATED` zurück, wenn sich etwas geändert hat. Die `overlay*`-Grundformen zeichnen ein Pixel breite Umrisse in der aktuellen Vordergrundfarbe und schneiden stillschweigend ab, was außerhalb der Fläche liegt. `overlayPrint` ist die Ausnahme: Es nimmt Koordinaten in Zeichenzellen und kopiert die eingebaute Konsolenschrift unverändert, ohne die Farbeinstellungen zu beachten.
|
|
|
|
[#overlaybanner]
|
|
==== overlayBanner
|
|
|
|
[source,text]
|
|
----
|
|
overlayBanner(text [, height])
|
|
----
|
|
|
|
Hypseus-Erweiterung. Zeigt eine kurze Meldung über dem Bild für etwa dreißig gezeichnete Frames und nimmt sie dann wieder weg. `height` ist, wo sie sitzt, in Prozent von oben über das Bild, von 1 bis 95; der Standard ist 47, der von Hypseus selbst, und eine Zahl außerhalb dieses Bereichs wird durch den Standard ersetzt statt abgelehnt.
|
|
|
|
Eine Meldung mit mehr als 60 Zeichen wird ignoriert, genau wie Hypseus sie ignoriert. Das Banner wird von demselben mitgelieferten Dokument gezeichnet, das die Untertitel verwenden (`Singe/subtitle.rml`, gestaltet durch `Singe/subtitle.rcss`), es sieht also aus wie ein Untertitel und wird mit ihnen gestaltet; solange es zu sehen ist, nimmt es den Platz jedes Untertitels ein, und der Untertitel kehrt zurück, wenn seine Zeit abgelaufen ist. Der Text wird als Text gezeichnet, sodass `<`, `>` und `&` als sie selbst auf dem Bildschirm ankommen.
|
|
|
|
*Parameter:*
|
|
|
|
* `text` -- String; bis zu 60 Zeichen.
|
|
* `height` -- optionale Zahl; 1 bis 95, ein Prozentwert von oben über das Bild. Standard 47.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<srtload,srtLoad>>, <<subtitles,Untertitel>>, <<overlayprint,overlayPrint>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Dem Spieler sagen, was gerade passiert ist, ohne dafür ein HUD zu bauen.
|
|
overlayBanner("Extra life!", 20)
|
|
----
|
|
|
|
[#overlaybox]
|
|
==== overlayBox
|
|
|
|
[source,text]
|
|
----
|
|
overlayBox(x1, y1, x2, y2)
|
|
----
|
|
|
|
Zeichnet den Umriss des achsenparallelen Rechtecks mit den gegenüberliegenden Ecken `(x1, y1)` und `(x2, y2)` als vier ein Pixel breite Linien in der Vordergrundfarbe. Beide Ecken gehören dazu und können in beliebiger Reihenfolge angegeben werden. Eine gefüllte Variante gibt es nicht; füll das ganze Overlay mit `overlayClear` auf einer Hintergrundfarbe oder eine Fläche mit einem gestreckten Sprite.
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<overlayline,overlayLine>>, <<colorforeground,colorForeground>>, <<overlayclear,overlayClear>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Overlay rot umrahmen und jeden Plattformblock umreißen.
|
|
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)
|
|
----
|
|
|
|
Zeichnet einen ein Pixel breiten Kreisumriss in der Vordergrundfarbe, zentriert auf `(x, y)`, mit dem Mittelpunktalgorithmus. Ein Radius von `0` oder weniger zeichnet nichts. Koordinaten und Radius sind Ganzzahlen in Overlay-Einheiten; Nachkommastellen werden abgeschnitten.
|
|
|
|
*Parameter:*
|
|
|
|
* `x`, `y` -- Mittelpunkt in Overlay-Koordinaten.
|
|
* `radius` -- Radius in Overlay-Einheiten.
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<overlayellipse,overlayEllipse>>, <<overlayplot,overlayPlot>>, <<colorforeground,colorForeground>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Fadenkreuz, das rot wird, solange der Spieler getroffen ist.
|
|
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()
|
|
----
|
|
|
|
Füllt das ganze Overlay mit der aktuellen Hintergrundfarbe. Ist die Farbe dieselbe wie beim letzten Löschen, wird nur das neu gefüllt, was seitdem gezeichnet wurde, sodass das Löschen eines Overlays mit ein paar Textzeilen fast nichts kostet, wie groß es auch ist. Der Hintergrund beginnt als transparentes Schwarz, ohne einen Aufruf von `colorBackground` löscht dies das Overlay also zurück zu unsichtbar, und das Video scheint durch. Ein teilweise deckender Hintergrund dämpft das Video; ein vollständig deckender verdeckt es. Die meisten Spiele rufen es als Erstes in `onOverlayUpdate` auf. Argumente werden ignoriert.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<colorbackground,colorBackground>>, <<onoverlayupdate,onOverlayUpdate>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das pausierte Video hinter dem Service-Menü auf halbe Helligkeit dämpfen.
|
|
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)
|
|
----
|
|
|
|
Zeichnet einen ein Pixel breiten Ellipsenumriss in der Vordergrundfarbe, einbeschrieben in das achsenparallele Rechteck mit den gegenüberliegenden Ecken `(x1, y1)` und `(x2, y2)`. Die Ecken können in beliebiger Reihenfolge angegeben werden. Ein Rechteck mit gleicher Breite und Höhe ergibt einen Kreis, das ist also auch der Weg, einen Kreis aus seinem umschließenden Rechteck statt aus seinem Mittelpunkt zu zeichnen.
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<overlaycircle,overlayCircle>>, <<overlaybox,overlayBox>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Den gewählten Automaten mit einem Oval hervorheben, das zu seinem Feld passt.
|
|
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()
|
|
----
|
|
|
|
Gibt die Höhe einer Zeichenzelle der eingebauten Konsolenschrift in Pixeln zurück, die Einheit, in der `overlayPrint` zählt. Die Konsolenschrift ist ein einziges eingebettetes Bild mit 256 Glyphen, das einmal geladen wird, bevor irgendein Skript läuft, der Wert ändert sich also nie, solange das Programm läuft. Argumente werden ignoriert.
|
|
|
|
*Rückgabe:* ganzzahlige Höhe einer Zelle in Pixeln.
|
|
|
|
*Seit:* 3.00
|
|
*Siehe auch:* <<overlaygetfontwidth,overlayGetFontWidth>>, <<overlayprint,overlayPrint>>, <<overlaygetheight,overlayGetHeight>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Wie viele Zeilen Konsolentext passen, wie groß das Overlay auch ist.
|
|
ROWS = overlayGetHeight() // overlayGetFontHeight()
|
|
overlayPrint(0, ROWS - 1, "along the bottom")
|
|
----
|
|
|
|
[#overlaygetfontwidth]
|
|
==== overlayGetFontWidth
|
|
|
|
[source,text]
|
|
----
|
|
result = overlayGetFontWidth()
|
|
----
|
|
|
|
Gibt die Breite einer Zeichenzelle der eingebauten Konsolenschrift in Pixeln zurück, die Einheit, in der `overlayPrint` zählt. Nimm sie mit `overlayGetWidth`, um auszurechnen, wie viele Spalten passen, oder um Konsolentext an etwas auszurichten, das in Pixeln gezeichnet ist. Teile mit `//` statt `/`: Die gewöhnliche Division von Lua ergibt eine Zahl mit Nachkommastellen, und eine Zellenposition muss ganzzahlig sein. Argumente werden ignoriert.
|
|
|
|
*Rückgabe:* ganzzahlige Breite einer Zelle in Pixeln.
|
|
|
|
*Seit:* 3.00
|
|
*Siehe auch:* <<overlaygetfontheight,overlayGetFontHeight>>, <<overlayprint,overlayPrint>>, <<overlaygetwidth,overlayGetWidth>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Zeile Konsolentext zentrieren, ohne die Spaltenzahl fest einzutragen.
|
|
local columns = overlayGetWidth() // overlayGetFontWidth()
|
|
local text = "GAME OVER"
|
|
|
|
overlayPrint((columns - #text) // 2, 10, text)
|
|
----
|
|
|
|
[#overlaygetheight]
|
|
==== overlayGetHeight
|
|
|
|
[source,text]
|
|
----
|
|
result = overlayGetHeight()
|
|
----
|
|
|
|
Gibt die Höhe der Overlay-Fläche in Overlay-Einheiten zurück: den zuletzt an `overlaySetResolution` übergebenen Wert oder, vor jedem Aufruf, die halbe Videohöhe. Lies sie beim Start, nach dem Setzen der Auflösung, um Positionen nach der tatsächlichen Zeichenfläche anzuordnen. Argumente werden ignoriert.
|
|
|
|
*Rückgabe:* ganzzahlige Höhe.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<overlaygetwidth,overlayGetWidth>>, <<overlaysetresolution,overlaySetResolution>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Textfeld am unteren Rand verankern, wie groß das Overlay auch ist.
|
|
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()
|
|
----
|
|
|
|
Gibt die Breite der Overlay-Fläche in Overlay-Einheiten zurück: den zuletzt an `overlaySetResolution` übergebenen Wert oder, vor jedem Aufruf, die halbe Videobreite. Nimm sie, um Elemente rechtsbündig auszurichten oder zu zentrieren, statt eine Breite fest einzutragen. Argumente werden ignoriert.
|
|
|
|
*Rückgabe:* ganzzahlige Breite.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<overlaygetheight,overlayGetHeight>>, <<overlaysetresolution,overlaySetResolution>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Marquee rechtsbündig ausrichten und den Titel zentrieren, unabhängig von der Auflösung.
|
|
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)
|
|
----
|
|
|
|
Zeichnet eine ein Pixel breite Bresenham-Linie in der Vordergrundfarbe von `(x1, y1)` nach `(x2, y2)`, beide Endpunkte eingeschlossen. Pixel außerhalb des Overlays werden verworfen, Endpunkte dürfen also außerhalb der Fläche liegen. Koordinaten werden auf Ganzzahlen abgeschnitten.
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<overlayplot,overlayPlot>>, <<overlaybox,overlayBox>>, <<colorforeground,colorForeground>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Den Laser von der Waffe zum Zielpunkt zeichnen und ein einfaches Zielkreuz.
|
|
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)
|
|
----
|
|
|
|
Setzt ein Overlay-Pixel auf die Vordergrundfarbe. Ein Punkt außerhalb der Fläche wird ignoriert. Jeder Aufruf sperrt und entsperrt das Overlay, Tausende Punkte pro Frame aus Lua zu setzen ist also langsam; nimm ein Sprite für alles mit Struktur und heb dies für Sterne, Funken und Debugging-Markierungen auf.
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<overlayline,overlayLine>>, <<colorforeground,colorForeground>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein langsam funkelndes Sternenfeld hinter dem Titel.
|
|
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)
|
|
----
|
|
|
|
Gibt einen String in der eingebauten Konsolenschrift fester Breite der Engine aus. Anders als bei jedem anderen Zeichenaufruf ist die Position eine Zeichenzelle: Die Engine multipliziert `column` und `row` mit Breite und Höhe der Glyphen, sodass `(0, 0)` die Zelle oben links ist und `(1, 0)` eine Glyphe weiter rechts. Glyphen werden unverändert aus dem Schriftbild kopiert; die aktuellen Vorder- und Hintergrundfarben werden nicht angewandt, und nur der eigene Hintergrund der Schrift ist transparent. Jedes Byte des Strings wählt eine der 256 Glyphen der Schrift, der Text wird also als Bytes behandelt, nicht als UTF-8. Zeichen, die über den rechten Rand des Overlays hinausliefen, werden verworfen, und Text, der jenseits des Rands beginnt, zeichnet nichts.
|
|
|
|
*Parameter:*
|
|
|
|
* `column`, `row` -- Zeichenzelle, `0` oben links.
|
|
* `text` -- der auszugebende String.
|
|
|
|
*Hinweise:* Die Konsolenschrift ist für Diagnose- und Service-Bildschirme gedacht. Nimm `fontPrint` oder `fontToSprite` für farbigen oder proportionalen Text. Die Größe einer Zelle in Pixeln ist `overlayGetFontWidth` mal `overlayGetFontHeight`, also ist `overlayGetWidth() // overlayGetFontWidth()` die Anzahl der Spalten, die passen.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<fontprint,fontPrint>>, <<overlayclear,overlayClear>>, <<colorbackground,colorBackground>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Diagnose in der Ecke, während ein Download fertig wird.
|
|
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)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Zeigt das Overlay in Grau: Vor jedem Hochladen wandelt die Engine eine Kopie davon mit den Gewichten nach BT.601 in Luma um und lässt den Alphakanal, wie er ist (Hypseus wendet darauf einen Schwellenwert an). Die eigenen Pixel des Overlays bleiben farbig, nichts, was das Skript zurückliest, ändert sich also, und das Zeichnen geht wie gewohnt weiter. Kombiniere es mit `vldpSetMonochrome` für einen Schwarzweißmodus. Ein Argument, das kein Boolean ist, bricht das Skript ab.
|
|
|
|
*Parameter:*
|
|
|
|
* `enabled` -- Boolean.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<vldpsetmonochrome,vldpSetMonochrome>>, <<overlayclear,overlayClear>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Einstellung steuert sowohl die Disc als auch das Overlay.
|
|
function applyChroma(color)
|
|
vldpSetMonochrome(not color)
|
|
overlaySetMonochrome(not color)
|
|
end
|
|
----
|
|
|
|
[#overlaysetresolution]
|
|
==== overlaySetResolution
|
|
|
|
[source,text]
|
|
----
|
|
overlaySetResolution(width, height)
|
|
----
|
|
|
|
Ersetzt das Overlay durch eine neue Fläche der angegebenen Größe und verwirft alles bisher Gezeichnete. Von da an ist jede Overlay-Koordinate, jede Sprite-Position und jedes Ergebnis von `overlayGetWidth` oder `overlayGetHeight` im neuen Koordinatenraum, den die Engine bei der Anzeige auf das Fenster skaliert. Ruf es einmal beim Start auf, vor dem Zeichnen; der Standard ist in jeder Dimension die halbe Videoauflösung. Eine Breite oder Höhe von `0` oder weniger beendet das Skript mit einem Fehler. Höhere Auflösungen bringen feinere Platzierung und schärferen Text für mehr Kompositionsarbeit pro Frame; die native Größe der Disc aus `discGetWidth` und `discGetHeight` ist die übliche Wahl.
|
|
|
|
*Parameter:*
|
|
|
|
* `width` -- neue Overlay-Breite in Pixeln, größer als `0`.
|
|
* `height` -- neue Overlay-Höhe in Pixeln, größer als `0`.
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<overlaygetwidth,overlayGetWidth>>, <<overlaygetheight,overlayGetHeight>>, <<discgetwidth,discGetWidth>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- An die Disc anpassen und das Menü dann in diesem Koordinatenraum anordnen.
|
|
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()
|
|
----
|
|
|
|
Hypseus-Erweiterung, angenommen und ignoriert. Hypseus blittet sein Overlay mit einem Schwellenwert von `128` auf dem Alphakanal, sodass ein halb transparentes Pixel entweder voll deckend oder verschwunden ist, und dies bittet es, die Fläche stattdessen so hochzuladen, wie sie ist, und richtig zu blenden. Das Overlay von Singe war schon immer volles RGBA mit echtem Alpha pro Pixel, was dies verlangt, gilt also bereits.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00
|
|
|
|
[#setoverlaylinearscale]
|
|
==== setOverlayLinearScale
|
|
|
|
[source,text]
|
|
----
|
|
setOverlayLinearScale(enabled)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Wählt, wie das Overlay auf dem Weg ins Fenster gefiltert wird. Mit `true`, dem Standard und dem, was ein Neuladen wiederherstellt, wird es linear abgetastet, sodass ein Overlay mit niedriger Auflösung weich wird statt klotzig; mit `false` wird das nächste Pixel genommen, was Pixel-Art, ein Pixel breite Umrisse und kleinen Text scharf hält. Linear ist der Standard, weil jede Version von Singe das Overlay so gezeichnet hat; Hypseus nimmt standardmäßig stattdessen das nächste Pixel, ein von Hypseus übernommenes Spiel, das dies nie aufruft, sieht also etwas weicher aus, bis es das tut. Am Zeichnen ändert sich nichts, nur die abschließende Skalierung von der Overlay-Auflösung auf das Fenster, sodass alles auf dem Overlay -- die Grundformen, Schriften, Sprites und die 3D-Ebene -- gemeinsam durch den einen Filter geht und das Disc-Bild dahinter unberührt bleibt. Die Einstellung übersteht `overlaySetResolution`, das das Overlay ersetzt. Ein Argument, das kein Boolean ist, bricht das Skript ab.
|
|
|
|
*Parameter:*
|
|
|
|
* `enabled` -- Boolean.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<overlaysetresolution,overlaySetResolution>>, <<spritequality,spriteQuality>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein 320x240-Overlay auf einem 4K-Bildschirm; der gemalte Titel weich, die Pixelgrafik des Spiels scharf.
|
|
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)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Wählt, ob das Overlay und die über das Bild gezeichneten GUIs über dem Bezel-Artwork liegen oder darunter. Nur Artwork, das vor dem Bild gezeichnet wird (`--bezelflip`), liegt überhaupt je über ihnen, bei Artwork dahinter -- dem Standard -- ändert dies also nichts. `false` ist die Einstellung, mit der ein Spiel beginnt, und der eigene Standard von Hypseus. Sie bleibt gespeichert, bis das Skript neu lädt.
|
|
|
|
Die Einstellung wird nur festgehalten, solange Artwork geladen ist; ohne tut der Aufruf nichts und antwortet `false` und `"0"`, genau wie Hypseus. Ein Argument, das kein Boolean ist, bricht das Skript ab.
|
|
|
|
*Parameter:*
|
|
|
|
* `enabled` -- Boolean; `true` legt das Overlay und die GUIs über das Artwork.
|
|
|
|
*Rückgabe:* Boolean, ob Artwork geladen ist, und seine Kennung als String (`"0"`, wenn keines geladen ist).
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<mainbezelloaded,mainBezelLoaded>>, <<guidrawscreen,guiDrawScreen>>, xref:Manual.adoc#bezels[Bezels]
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Der Rahmen des Automaten überlappt das Bild, aber die Punktezeile muss lesbar bleiben.
|
|
if mainBezelLoaded() then
|
|
setOverlayOnTop(true)
|
|
end
|
|
----
|
|
|
|
[#setoverlayopacity]
|
|
==== setOverlayOpacity
|
|
|
|
[source,text]
|
|
----
|
|
setOverlayOpacity(alpha)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Wie deckend das ganze Overlay ist, von `0` (unsichtbar) bis `255` (so, wie das Spiel es gezeichnet hat). Es multipliziert das Alpha, das die eigenen Pixel des Overlays schon tragen, sodass ein halb transparentes Sprite halb so deckend bleibt wie der Rest; es ändert den Inhalt des Overlays nicht, und nichts muss neu gezeichnet werden.
|
|
|
|
Der Wert wird auf `0` bis `255` begrenzt statt abgelehnt, und ein Skript beginnt bei `255`. Er bleibt gespeichert, bis das Skript neu lädt, und übersteht eine Änderung der Overlay-Auflösung.
|
|
|
|
Die GUIs, die 3D-Szene und das Bezel-Artwork werden getrennt gezeichnet und sind nicht betroffen.
|
|
|
|
*Parameter:*
|
|
|
|
* `alpha` -- Zahl; `0` bis `255`.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<setoverlaylinearscale,setOverlayLinearScale>>, <<overlaysetresolution,overlaySetResolution>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das ganze Head-up-Display über eine halbe Sekunde ausblenden.
|
|
local fade = 255
|
|
function onOverlayUpdate()
|
|
fade = math.max(0, fade - 8)
|
|
setOverlayOpacity(fade)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#physicsref]
|
|
=== Physik
|
|
|
|
Aufrufe auf Ebene der Welt: die Strahlabfrage, die Schwerkraft, der 2D-Modus, das Pausieren und das Debug-Zeichnen. Die Physik wird mit der Engine initialisiert, vor dem ersten `bodyNew` gibt es also nichts einzuschalten; die Welt rechnet sechzig Schritte pro Sekunde nach der Uhr, nach den Animationen und bevor die Szene rendert. Positionen und Abstände sind in Welteinheiten und die Schwerkraft in Einheiten pro Sekunde zum Quadrat. Wie Körper, Knoten und der Frame zusammenpassen, steht unter <<physics,Physik>> in Spieleentwicklung.
|
|
|
|
[#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)
|
|
----
|
|
|
|
Findet den ersten Körper entlang eines Strahls von `(x, y, z)` in Richtung `(dx, dy, dz)`, innerhalb von `maxDistance` Welteinheiten. Die Richtung wird normalisiert, jede Länge funktioniert also, aber eine Nullrichtung trifft nichts. Trigger und Spielfiguren werden wie jeder andere Körper getroffen; eine Spielfigur wird über ihren Knoten gemeldet. Liefert `sceneUnproject` zwei Punkte entlang der Maus, ist das die Objektauswahl per Maus; senkrecht nach unten von einem Punkt aus ist es eine Höhenmessung.
|
|
|
|
*Parameter:*
|
|
|
|
* `x`, `y`, `z` -- der Start des Strahls in Weltkoordinaten.
|
|
* `dx`, `dy`, `dz` -- seine Richtung; nur die Richtung zählt.
|
|
* `maxDistance` -- wie weit gesucht wird; Standard `1000`, und `0` oder weniger bedeutet den Standard.
|
|
|
|
*Rückgabe:* Der Knoten des Körpers, der Trefferpunkt und die Oberflächennormale dort oder ein einzelnes `nil`, wenn nichts getroffen wird.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<sceneunproject,sceneUnproject>>, <<bodyapplyimpulse,bodyApplyImpulse>>, <<linedraw,lineDraw>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Markierung auf den ebenen Boden setzen, der gerade unter dem Cursor liegt.
|
|
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)
|
|
----
|
|
|
|
Mit `true` bewegen sich von nun an erzeugte Körper nur in X und Y und drehen sich nur um Z: die Welt eines 2D-Spiels, meist in Overlay-Koordinaten mit einer Schwerkraft in Richtung +Y, sodass Dinge den Bildschirm hinunterfallen. Bereits erzeugte Körper behalten ihre Freiheit, ruf es also vor jedem `bodyNew` auf; `false` kehrt für danach erzeugte Körper zu vollem 3D zurück. Spielfiguren sind anders: Solange der Modus an ist, läuft jede Spielfigur, wann immer sie erzeugt wurde, entlang X mit auf `0` gehaltenem Z, und `playerMove` nimmt einen einzigen Wert. Die 2D-Welt funktioniert ohne GPU, auch auf dem Raspberry Pi 3.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<physicssetgravity,physicsSetGravity>>, <<bodynew,bodyNew>>, <<playermove,playerMove>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Welt eines Plattformspiels in Overlay-Pixeln: Schwerkraft den Bildschirm hinunter, ein Boden und Wände.
|
|
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)
|
|
----
|
|
|
|
Zeichnet in jedem Frame, solange die Maske gesetzt ist, als Linien über die Szene, was die Simulation enthält: `DEBUG_SHAPES` die Form jedes bewegten Körpers als Drahtgitter (Soft Bodies eingeschlossen und eingeschaltete Spielfiguren in Gelb), `DEBUG_CONSTRAINTS` Gelenke, Ragdoll-Gelenke und Fahrzeug-Constraints mit ihren Grenzen, `DEBUG_CONTACTS` ein rotes Kreuz, wo sich Körper in diesem Schritt getroffen haben, und `DEBUG_VELOCITIES` eine Linie entlang der Geschwindigkeit jedes bewegten Körpers. Addiere die Flags; `DEBUG_ALL` sind alle vier. Die Formen statischer Körper bleiben aus, sofern nicht `DEBUG_STATIC` hinzukommt, weil ein Level-Mesh sehr viele Linien ergibt. `DEBUG_NONE` schaltet es ab, und solange es aus ist, wird nichts gezeichnet und nichts kostet etwas.
|
|
|
|
*Hinweise:* Ein Build von Singe ohne den Debug-Renderer von Jolt gibt eine Meldung auf der Konsole aus und zeichnet nichts.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<linedraw,lineDraw>>, <<navdraw,navDraw>>, <<physicssetenabled,physicsSetEnabled>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Physik-Einblendung mit einer Debug-Taste umschalten; auch die statischen Formen, da das Level klein ist.
|
|
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)
|
|
----
|
|
|
|
Pausiert die Simulation mit `false` und setzt sie mit `true` fort. Solange sie pausiert ist, läuft kein Schritt: Dynamische Körper halten still und behalten ihre Geschwindigkeiten, kinematische Körper folgen ihren Knoten nicht mehr, Spielfiguren bewegen sich nicht mehr, und kein `onCollision` oder `onTrigger` wird ausgelöst. Während der Pause sammelt sich keine Zeit an, das Fortsetzen holt die verpassten Schritte also nicht nach. Die eigene Pause des Spiels tut dasselbe ohne diesen Aufruf; nimm ihn für ein Menü, eine Zwischensequenz oder ein Standbild, bei dem der Rest des Spiels weiterläuft.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<physicssetgravity,physicsSetGravity>>, <<bodyisresting,bodyIsResting>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Welt einfrieren, solange das Inventar offen ist.
|
|
function openInventory()
|
|
inventoryOpen = true
|
|
physicsSetEnabled(false)
|
|
end
|
|
|
|
function closeInventory()
|
|
inventoryOpen = false
|
|
physicsSetEnabled(true)
|
|
end
|
|
----
|
|
|
|
[#physicssetgravity]
|
|
==== physicsSetGravity
|
|
|
|
[source,text]
|
|
----
|
|
physicsSetGravity(x, y, z)
|
|
----
|
|
|
|
Setzt die Beschleunigung, die jeder dynamische Körper und jede Spielfigur spürt, in Welteinheiten pro Sekunde zum Quadrat entlang jeder Weltachse. Der Standard ist `0, -9.81, 0`, die Erdschwerkraft in Metern; `0, 0, 0` ist der Weltraum, und ein 2D-Spiel in Overlay-Pixeln will etwa `0, 900, 0`, da Y im Overlay den Bildschirm hinunterläuft. Die der Schwerkraft entgegengesetzte Richtung ist für Spielfiguren „oben“: die Richtung, in der sie stehen, springen und Stufen erklimmen.
|
|
|
|
Eine Änderung der Schwerkraft weckt jeden Körper, Soft Bodies eingeschlossen, sodass eine zur Ruhe gekommene Kiste fällt, wenn sich die Schwerkraft zur Seite dreht, und Stoff weht, wenn sich ein aus Schwerkraft gemachter Wind ändert. Dieselbe Schwerkraft erneut zu setzen weckt nichts.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<bodynew,bodyNew>>, <<physicsset2d,physicsSet2D>>, <<playersetgravityscale,playerSetGravityScale>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Mond-Level: ein Sechstel der Erdanziehung und ein passend längerer Sprung.
|
|
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]
|
|
=== Spielfigur
|
|
|
|
Eine Spielfigur ist ein Character-Controller auf einem Knoten: Während ein Körper dorthin geht, wohin Kräfte ihn schicken, geht eine Spielfigur dorthin, wohin das Skript sagt, läuft Böden entlang, steigt Stufen hinauf, gleitet an Wänden entlang, fährt auf Plattformen mit und schiebt leichte Körper beiseite. Spielfiguren werden über ihren Knoten angesprochen, und ein Knoten trägt einen Körper oder eine Spielfigur, nicht beides. Größen und Geschwindigkeiten sind in Welteinheiten (Meter in einer 3D-Szene, Overlay-Pixel in einer 2D-Welt), und die Toleranzen des Controllers skalieren mit der Form, sodass sich eine 30 Pixel große Kapsel wie eine von 0,3 Metern verhält. Die Spielfigur treibt bei jedem Schritt die Position ihres Knotens und überlässt die Rotation dem Skript. Jeder Aufruf hier beendet das Skript, wenn der Knoten keine Spielfigur hat. Siehe <<physics,Physik>> in Spieleentwicklung, Spielfiguren.
|
|
|
|
[#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)
|
|
----
|
|
|
|
Setzt eine Spielfigur auf den Knoten, dort, wo der Knoten gerade in der Welt ist: eine Kapsel mit diesem Radius und dieser Gesamthöhe oder jede konvexe `bodyNew`-Form mit ihren Größen, skaliert mit der Weltskalierung des Knotens. Die Form steht auf dem Ursprung des Knotens, der Knoten sind also die Füße. Sie beginnt mit einer Masse von `70` Kilogramm, einer Schubkraft von `300` Newton, einer Hanggrenze von `45` Grad, einer Stufenhöhe von `0.3`, skaliert mit der Form, dem Schwerkraftfaktor `1` und den Standardwerten fürs Schwimmen. Ersetzt jeden Körper und jede Spielfigur, die der Knoten hatte. Beendet das Skript bei `SHAPE_MESH` (eine Spielfigur braucht eine konvexe Form; nimm `SHAPE_HULL`), wenn auf diesem Rechner keine Physik verfügbar ist oder wenn die Spielfigur nicht erzeugt werden kann (eine Hülle mit zu wenig Mesh oder schon sechzehn Spielfiguren).
|
|
|
|
*Parameter:*
|
|
|
|
* `radius`, `height` -- eine Kapsel, Radius und Gesamthöhe entlang der Aufwärtsrichtung, Kappen eingeschlossen.
|
|
* `SHAPE_*` -- eine Formkonstante, gefolgt von ihren `bodyNew`-Größen; `SHAPE_HULL` nimmt keine und umhüllt das Mesh des Knotens und alles darunter.
|
|
|
|
*Hinweise:* Ein Aufruf mit genau drei Argumenten ist immer eine Kapsel, eine Kugel braucht also nach ihrem Radius ein viertes Argument (eine beliebige Zahl), um als `SHAPE_SPHERE` gelesen zu werden.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<playermove,playerMove>>, <<playerdelete,playerDelete>>, <<bodynew,bodyNew>>, <<nodesetparent,nodeSetParent>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Der Held: eine Kapsel, am Knoten hängt das Fox-Modell, die Füße auf dem Boden.
|
|
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)
|
|
-- Ein Roboter ist ein Quader; er biegt rechtwinklig ab und kann nicht rollen.
|
|
robot = nodeNew()
|
|
playerNew(robot, SHAPE_BOX, 0.8, 1.6, 0.6)
|
|
----
|
|
|
|
[#playerdelete]
|
|
==== playerDelete
|
|
|
|
[source,text]
|
|
----
|
|
playerDelete(node)
|
|
----
|
|
|
|
Entfernt die Spielfigur; der Knoten und was darunter hängt bleiben, wo sie sind. Das Löschen des Knotens entfernt auch seine Spielfigur, ebenso, wenn der Knoten mit `bodyNew` einen Körper bekommt. Beendet das Skript, wenn der Knoten keine Spielfigur hat.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<playernew,playerNew>>, <<playersetenabled,playerSetEnabled>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Der Held steigt ins Auto: Der Controller verschwindet, das Modell fährt mit.
|
|
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)
|
|
----
|
|
|
|
Teilt der Spielfigur die Geschwindigkeit mit, die sie in diesem Frame will, in Welteinheiten pro Sekunde. Am Boden oder in der Luft werden die beiden waagerechten Werte so verwendet, wie sie angegeben sind, zusätzlich zu der Plattform, auf der sie steht; der senkrechte Anteil kommt von der Schwerkraft, dem Boden und `playerJump`, und `vy` wird ignoriert. Schwimmt die Spielfigur, steuern sie alle drei Werte, und das Wasser führt ihre Geschwindigkeit mit dem Widerstand aus `playerSetSwim` sanft an sie heran, wobei sie mit der Sinkgeschwindigkeit sinkt, wenn `vy` gleich `0` ist. In einer 2D-Welt läuft ein einzelnes `vx` entlang X, und jedes `vz` wird ignoriert. Ruf es in jedem Frame aus `onOverlayUpdate` auf: Die Anforderung wird von den Schritten dieses Frames verbraucht, und in einem Frame ohne Anforderung steht sie still.
|
|
|
|
*Parameter:*
|
|
|
|
* `vx`, `vz` -- die gewünschte waagerechte Geschwindigkeit, Welteinheiten pro Sekunde (2D: Overlay-Pixel entlang X).
|
|
* `vy` -- die gewünschte senkrechte Geschwindigkeit beim Schwimmen; positiv ist nach oben.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<playerjump,playerJump>>, <<playersetvelocity,playerSetVelocity>>, <<playerisswimming,playerIsSwimming>>, <<physicsset2d,physicsSet2D>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Mit dem linken Stick gehen, aufwärts schwimmen, solange der Knopf gehalten wird, in Laufrichtung schauen.
|
|
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)
|
|
----
|
|
|
|
Ob die Spielfigur beim letzten Schritt im Wasser war: die Mitte ihrer Form unter der Oberfläche eines `bodySetWater`-Volumens. Beim Schwimmen steuert `playerMove` in drei Achsen, die Schwerkraft weicht der Sinkgeschwindigkeit, und die Strömung des Wassers trägt die Spielfigur mit.
|
|
|
|
*Rückgabe:* Boolean.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<playersetswim,playerSetSwim>>, <<bodysetwater,bodySetWater>>, <<playermove,playerMove>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Animation wechseln, wenn der Held ins Wasser geht.
|
|
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)
|
|
----
|
|
|
|
Bittet die Spielfigur, beim nächsten Schritt mit dieser Aufwärtsgeschwindigkeit, in Welteinheiten pro Sekunde, vom Boden abzuheben. Das wird nur gewährt, wenn gerade begehbarer Boden unter den Füßen ist, sodass ein gehaltener Knopf die Spielfigur nicht hüpfen lässt und ein Sprung in der Luft nichts tut. Eine negative Geschwindigkeit wird als `0` behandelt. Die Spielfigur behält ihre waagerechte Geschwindigkeit während des Sprungs, und `playerMove` steuert sie auch in der Luft.
|
|
|
|
*Rückgabe:* `true`, wenn der Sprung gewährt wurde.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<playerisonground,playerIsOnGround>>, <<playermove,playerMove>>, <<playersetgravityscale,playerSetGravityScale>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Auf den Aktionsknopf springen, mit einem Geräusch nur, wenn es wirklich geschehen ist.
|
|
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)
|
|
----
|
|
|
|
Legt fest, wie sich die Spielfigur im Wasser verhält: wie schnell sie sinkt, in Welteinheiten pro Sekunde, wenn `playerMove` keine senkrechte Bewegung verlangt (Standard `0.3`), und wie schnell das Wasser ihre Geschwindigkeit zu dem hinzieht, was `playerMove` verlangt, als Rate pro Sekunde (Standard `2`, sodass der größte Teil des Unterschieds innerhalb einer Sekunde verschwunden ist; `0` ändert die Geschwindigkeit nie). Beide werden auf mindestens `0` begrenzt. Ein schwerer Taucher sinkt schnell, mit hohem Widerstand; ein Korken sinkt gar nicht.
|
|
|
|
*Parameter:*
|
|
|
|
* `sinkSpeed` -- das Absinken ohne senkrechte Eingabe, Einheiten pro Sekunde.
|
|
* `drag` -- die Rate, mit der das Wasser die Geschwindigkeit zur Anforderung hinzieht, pro Sekunde.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<playerisswimming,playerIsSwimming>>, <<playermove,playerMove>>, <<bodysetwater,bodySetWater>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Rüstung zieht den Helden hinunter; leg sie ab, um zu treiben.
|
|
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)
|
|
----
|
|
|
|
Setzt die ganze Geschwindigkeit der Spielfigur, in Welteinheiten pro Sekunde entlang jeder Weltachse, und verwirft jede in diesem Frame gemachte `playerMove`-Anforderung. Der senkrechte Anteil ist das, was bleibt: Eine Aufwärtsgeschwindigkeit hebt die Spielfigur vom Boden, und sie fällt unter der Schwerkraft zurück, das ist ein Sprungfeld oder ein Abprallen. Der waagerechte Anteil hält nur bis zum nächsten Schritt, wenn er durch das ersetzt wird, was `playerMove` verlangt (nichts, wenn es nicht aufgerufen wird), weil eine Spielfigur keinen eigenen waagerechten Schwung hat. Ein Rückstoß oder ein Dash ist hier also ein Stoß nach oben plus eine `playerMove`-Geschwindigkeit, die über die folgenden Frames eingespeist wird.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<playergetvelocity,playerGetVelocity>>, <<playermove,playerMove>>, <<playerjump,playerJump>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Treffer des Ogers wirft den Helden hoch und über die nächsten Frames von ihm weg.
|
|
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)
|
|
----
|
|
|
|
Die Geschwindigkeit der Spielfigur nach dem letzten Schritt, in Welteinheiten pro Sekunde entlang jeder Weltachse, einschließlich dessen, was der Boden, auf dem sie mitfährt, ihr gegeben hat. Lies den senkrechten Anteil, um Steigen von Fallen zu unterscheiden, oder das Ganze für ein dumpfes Landegeräusch.
|
|
|
|
*Rückgabe:* Drei Zahlen.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<playersetvelocity,playerSetVelocity>>, <<playerisonground,playerIsOnGround>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Fallschaden aus der Geschwindigkeit, mit der der Held auf dem Boden aufgeschlagen ist.
|
|
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)
|
|
----
|
|
|
|
Ob die Spielfigur beim letzten Schritt auf begehbarem Boden steht: einer Fläche, die nicht steiler als `playerSetSlope` ist. In der Luft oder auf einem zu steilen Hang ist es `false`. Das ist dieselbe Prüfung, die `playerJump` macht.
|
|
|
|
*Rückgabe:* Boolean.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<playergetground,playerGetGround>>, <<playerjump,playerJump>>, <<playersetslope,playerSetSlope>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Coyote Time: Ein Sprung ist noch ein paar Frames lang erlaubt, nachdem man über eine Kante gelaufen ist.
|
|
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)
|
|
----
|
|
|
|
Worauf die Spielfigur steht, und die Oberflächennormale des Bodens dort. Der Knoten ist der Körper unter den Füßen, ob begehbar oder nicht; in der Luft ist er `nil`, und die Normale ist dann `0, 1, 0`. Unterscheide über den Knoten eine bewegliche Plattform, Eis oder eine Druckplatte vom Boden und neige über die Normale ein Modell in einen Hang.
|
|
|
|
*Rückgabe:* Der Knoten, auf dem sie steht, oder `nil`, dann die Normale.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<playerisonground,playerIsOnGround>>, <<bodysetfriction,bodySetFriction>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Schritte klingen auf der Holzbrücke anders, und die Platte öffnet das Tor.
|
|
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)
|
|
----
|
|
|
|
Die höchste Stufe, die die Spielfigur ohne Sprung erklimmt, in Welteinheiten; alles Höhere ist eine Wand, an der sie entlanggleitet. Der Standard ist `0.3` bei einer Form, deren kleinste halbe Ausdehnung `0.3` ist (eine Kapsel mit Radius `0.3`), und skaliert mit der Form, sodass eine 30 Pixel große Spielfigur in einer 2D-Welt mit 30 Pixeln beginnt. Dieselbe Strecke ist, wie weit die Spielfigur über die Kante einer Stufe hinweg nach unten am Boden haftet, eine größere Stufe hält sie also auch auf Gefällen am Boden. Negative Werte werden auf `0` begrenzt.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<playersetslope,playerSetSlope>>, <<playernew,playerNew>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Treppen in diesem Level steigen 0,35 pro Stufe; sicherstellen, dass der Held sie schafft.
|
|
playerNew(hero, 0.3, 1.0)
|
|
playerSetStep(hero, 0.4)
|
|
-- Die Absätze des 2D-Plattformspiels sind 26 Pixel hoch.
|
|
playerNew(runner, 16, 48)
|
|
playerSetStep(runner, 26)
|
|
----
|
|
|
|
[#playersetslope]
|
|
==== playerSetSlope
|
|
|
|
[source,text]
|
|
----
|
|
playerSetSlope(node, degrees)
|
|
----
|
|
|
|
Der steilste Hang, den die Spielfigur hinaufgeht, in Grad gegen die Waagerechte; steilerer Boden gilt nicht als Stehen, die Spielfigur rutscht ihn also hinunter und kann nicht von ihm abspringen. Der Standard ist `45`, und der Wert wird zwischen `0` und `89` begrenzt. Ein niedriger Wert macht Hügel zu Hindernissen, ein hoher lässt die Spielfigur fast alles erklimmen, was keine Wand ist.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<playersetstep,playerSetStep>>, <<playerisonground,playerIsOnGround>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Bergziege klettert, wo der Held rutscht.
|
|
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)
|
|
----
|
|
|
|
Die Masse der Spielfigur, verwendet, wenn sie dynamische Körper schiebt, und als ihr Gewicht auf jedem, auf dem sie steht. Der Standard ist `70` Kilogramm; Werte unter `0.001` werden darauf angehoben. Die Masse wirkt sich nicht darauf aus, wie die Spielfigur fällt oder geht, nur auf das Schieben, kombiniere sie also mit `playerSetPush`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<playersetpush,playerSetPush>>, <<bodysetmass,bodySetMass>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Der Troll walzt durch, was der Held umgehen muss.
|
|
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)
|
|
----
|
|
|
|
Wie stark die Spielfigur dynamische Körper schiebt, in die sie hineinläuft, in Newton. Der Standard `300` bewegt Kisten und Bälle, aber keine Automaten; `0` geht um alles herum, als wäre es statisch. Negative Werte werden auf `0` begrenzt. Das eigene `bodySetMass` eines Körpers entscheidet, was eine gegebene Stärke bewegen kann.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<playersetmass,playerSetMass>>, <<bodysetmass,bodySetMass>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Der Stärketrank lässt den Helden zehn Sekunden lang den Felsbrocken bewegen.
|
|
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)
|
|
----
|
|
|
|
Multipliziert die Schwerkraft der Welt für diese Spielfigur allein: `1` (der Standard) fällt wie alles andere, `0` schwebt, wo sie ist (eine Leiter, ein Raum ohne Schwerkraft, ein fliegendes Reittier), mehr fällt schneller und fühlt sich schwer an, und weniger ergibt einen schwebenden Sprung. Es ändert nicht, was `playerJump` beim Absprung tut, nur wie schnell die Spielfigur wieder herunterkommt. Beim Schwimmen tritt die Sinkgeschwindigkeit des Wassers an die Stelle der Schwerkraft.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<physicssetgravity,physicsSetGravity>>, <<playerjump,playerJump>>, <<playersetswim,playerSetSwim>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Auf der Leiter klettert der Held mit dem Stick, statt zu fallen.
|
|
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)
|
|
----
|
|
|
|
Teleportiert die Spielfigur an eine Position in der Welt, ohne unterwegs etwas zu berühren, und hält sie schlagartig an. Der Knoten bewegt sich mit und behält seine Rotation. Nimm es zum Erscheinen, Wiedererscheinen und für Levelwechsel; den Knoten stattdessen mit `nodeSetPosition` zu bewegen lässt die Spielfigur, wo sie war, und der Knoten wird beim nächsten Schritt zurückgezogen.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<playersetvelocity,playerSetVelocity>>, <<nodesetposition,nodeSetPosition>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Nach einem Sturz in die Grube zurück zum letzten Checkpoint.
|
|
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)
|
|
----
|
|
|
|
Mit `false` verlässt die Spielfigur die Simulation: Ihr Körper wird aus der Welt entfernt, sie bewegt sich also nicht, fällt nicht, treibt ihren Knoten nicht und blockiert nichts, `onTrigger` meldet, dass sie jeden Trigger verlässt, in dem sie stand, und beim Debug-Zeichnen wird sie ausgelassen. `true` setzt den Körper zurück und nimmt ihn dort wieder auf, wo der Knoten gerade ist. Schalte eine Spielfigur aus, solange eine Ragdoll, eine Zwischensequenz oder ein Fahrzeug über die Figur verfügt.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<playerdelete,playerDelete>>, <<ragdollnew,ragdollNew>>, <<bodysetenabled,bodySetEnabled>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein harter Treffer übergibt das Skelett des Helden an die 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
|
|
|
|
Das Skelett eines geskinnten Modells, an die Physik übergeben (siehe <<physics,Physik>>, Ragdolls). Eine Ragdoll wird über den Knoten angesprochen, der `ragdollNew` übergeben wurde, meist die Wurzel einer Modellinstanz, und ein Knoten ohne Ragdoll beendet in jedem Aufruf nach `ragdollNew` das Skript. Knochen werden über ihre Gelenkknoten benannt, die Namen, die `nodeFind` sieht, ohne Rücksicht auf Groß- und Kleinschreibung verglichen; Abstände sind in Welteinheiten, Winkel in Grad, Impulse in Newtonsekunden und die Motorstärke in Newtonmetern. Bis zu acht Ragdolls gleichzeitig mit je bis zu 48 Knochen.
|
|
|
|
[#ragdollnew]
|
|
==== ragdollNew
|
|
|
|
[source,text]
|
|
----
|
|
ragdollNew(node)
|
|
----
|
|
|
|
Sucht am Knoten und darunter nach dem ersten geskinnten Mesh und baut aus seinem Skelett ein Ragdoll-Rezept: Jedes Gelenk, das ein Kindgelenk hat, wird zu einem Knochen, einer Kapsel, die vom Gelenk zu seinen Kindern verläuft und mit dem nächsten Knochen darüber durch ein Swing-Twist-Gelenk verbunden ist, begrenzt auf 45 Grad Schwenk und 30 Grad Verdrehung. Gelenke ohne Kindgelenk, etwa Fingerspitzen und die Spitze eines Schwanzes, bewegen sich mit ihrem Elternknochen mit und können in den anderen Aufrufen nicht genannt werden. Noch wird nichts simuliert: Das Modell animiert weiter bis `ragdollActivate`. Ein erneuter Aufruf auf demselben Knoten verwirft zuerst die alte Ragdoll. Beendet das Skript, wenn keine Physik verfügbar ist, wenn unter dem Knoten kein geskinntes Mesh mit Knochen liegt oder wenn schon acht Ragdolls existieren.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<ragdollactivate,ragdollActivate>>, <<ragdollsetjoint,ragdollSetJoint>>, <<modelinstance,modelInstance>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Wächter, der auf den Spieler zuläuft; die Ragdoll ist vor dem Zusammenprall bereit.
|
|
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) -- Nur das Rezept; läuft weiter
|
|
----
|
|
|
|
[#ragdolldelete]
|
|
==== ragdollDelete
|
|
|
|
[source,text]
|
|
----
|
|
ragdollDelete(node)
|
|
----
|
|
|
|
Entfernt die Ragdoll: ihre Körper und Gelenke, wenn sie aktiv ist, dann das Rezept. Die Gelenkknoten bleiben, wo die Physik sie gelassen hat, das Modell liegt also, wie es gefallen ist, bis eine Animation es bewegt, und der Knoten ist frei für ein neues `ragdollNew`. Ein Knoten ohne Ragdoll beendet das Skript.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<ragdollnew,ragdollNew>>, <<ragdolldeactivate,ragdollDeactivate>>, <<nodedelete,nodeDelete>>
|
|
|
|
.Beispiel
|
|
[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) -- Fünf Sekunden reglos: aufräumen
|
|
nodeDelete(body.node)
|
|
table.remove(fallen, i)
|
|
end
|
|
end
|
|
return OVERLAY_NOT_UPDATED
|
|
end
|
|
----
|
|
|
|
[#ragdollsetjoint]
|
|
==== ragdollSetJoint
|
|
|
|
[source,text]
|
|
----
|
|
ragdollSetJoint(node, jointName, radius, swingDegrees, twistDegrees)
|
|
----
|
|
|
|
Stimmt das Rezept eines Knochens ab. Die Werte werden gelesen, wenn die Körper erzeugt werden, ruf es also vor `ragdollActivate` auf; eine bereits aktive Ragdoll behält ihre Körper, bis sie deaktiviert und erneut aktiviert wird. Der Radius `0` verlangt die automatische Kapsel, 0,22 der Knochenlänge und nie dünner als `0.02` Einheiten. Der Schwenk ist der halbe Öffnungswinkel des Kegels, durch den sich der Knochen relativ zu seinem Elternknochen bewegen darf, die Verdrehung, wie weit er sich um seine eigene Achse drehen darf; beide werden auf `0` bis `179` begrenzt. Nur Knochen können genannt werden, nicht die Endgelenke; der Name wird ohne Rücksicht auf Groß- und Kleinschreibung verglichen, und ein Name ohne Knochen beendet das Skript.
|
|
|
|
*Parameter:*
|
|
|
|
* `node` -- der Knoten der Ragdoll.
|
|
* `jointName` -- der Name des Gelenkknotens, wie im Modell.
|
|
* `radius` -- der Radius der Kapsel in Welteinheiten oder `0` für automatisch.
|
|
* `swingDegrees` -- wie weit der Knochen von seinem Elternknochen wegschwenken darf; Standard `45`.
|
|
* `twistDegrees` -- wie weit er sich um seine eigene Achse verdrehen darf; Standard `30`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<ragdollnew,ragdollNew>>, <<ragdollactivate,ragdollActivate>>, <<nodefind,nodeFind>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
ragdollNew(fox)
|
|
ragdollSetJoint(fox, "b_Neck_04", 0, 30, 20) -- Ein steiferer Hals
|
|
ragdollSetJoint(fox, "b_Tail01_012", 0.03, 80, 60) -- Ein peitschender Schwanz
|
|
for _, leg in ipairs({ "b_LeftLeg01_015", "b_RightLeg01_019" }) do
|
|
ragdollSetJoint(fox, leg, 0, 60, 15) -- Hüften schwenken weit, verdrehen kaum
|
|
end
|
|
ragdollActivate(fox)
|
|
----
|
|
|
|
[#ragdollactivate]
|
|
==== ragdollActivate
|
|
|
|
[source,text]
|
|
----
|
|
ragdollActivate(node)
|
|
----
|
|
|
|
Übergibt das Skelett aus seiner aktuellen Pose an die Physik: Jede Animationsebene auf dem Knoten stoppt, für jeden Knochen wird dort, wo sein Gelenk gerade steht, eine dynamische Kapsel erzeugt, zwischen ihnen werden die Swing-Twist-Gelenke erzeugt, und von da an treibt jeder Physikschritt die Gelenkknoten aus den Körpern, sodass das geskinnte Mesh folgt. Die Knochen kollidieren mit der Welt und miteinander, außer jeder Knochen mit seinem Elternknochen. Die Pose in diesem Moment ist das, wohin `ragdollSetStrength` zieht. Eine bereits aktive Ragdoll bleibt, wie sie ist. Beendet das Skript, wenn der Körper eines Knochens nicht erzeugt werden kann.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<ragdolldeactivate,ragdollDeactivate>>, <<ragdollapplyimpulse,ragdollApplyImpulse>>, <<ragdollsetstrength,ragdollSetStrength>>
|
|
|
|
.Beispiel
|
|
[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) -- Der Controller lässt los
|
|
ragdollActivate(guardFox) -- Ab hier übernimmt die Physik das Skelett
|
|
ragdollApplyImpulse(guardFox, "b_Spine02_03", 40, 30, 0)
|
|
end
|
|
end
|
|
----
|
|
|
|
[#ragdolldeactivate]
|
|
==== ragdollDeactivate
|
|
|
|
[source,text]
|
|
----
|
|
ragdollDeactivate(node)
|
|
----
|
|
|
|
Entfernt die Körper und Gelenke und gibt das Skelett zurück. Die Gelenkknoten behalten die Pose, in der die Ragdoll sie gelassen hat, und nichts bewegt sie, bis eine Animation läuft; ein `animationPlay` mit Überblendung blendet aus dieser Pose heraus, sodass eine am Boden liegende Figur aufstehen kann. Das Rezept bleibt, sodass `ragdollActivate` später wieder übernehmen kann, wo auch immer die Animation das Skelett hinbewegt hat. Tut nichts bei einer Ragdoll, die nicht aktiv ist.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<ragdollactivate,ragdollActivate>>, <<animationplay,animationPlay>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Sobald der Körper zur Ruhe gekommen ist, steht der Held über eine halbe Sekunde auf.
|
|
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)
|
|
----
|
|
|
|
Treibt jedes Gelenk mit einem Motor an, der zu der Pose zieht, in der die Ragdoll aktiviert wurde, begrenzt auf so viele Newtonmeter; `0` (Standard) schaltet die Motoren aus, und der Körper wird schlaff. Die Motoren sind Federn mit 4 Hz, kritisch gedämpft. Es kann vor dem Aktivieren oder während sich die Ragdoll bewegt gesetzt werden und wirkt ab dem nächsten Schritt. Einen Körper stellt es nicht auf, da sich nichts vom Boden abstößt: Ein paar Dutzend Newtonmeter verwandeln einen Sturz in ein Taumeln und lassen eine am Boden liegende Figur ihre Gliedmaßen einziehen.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<ragdollactivate,ragdollActivate>>, <<ragdollisresting,ragdollIsResting>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Schlaff zu Boden geschlagen; einen Moment später spannt sich der Kämpfer an und rollt sich zusammen.
|
|
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)
|
|
----
|
|
|
|
Gibt dem Körper des Knochens, der am genannten Gelenk beginnt, einen Impuls in Newtonsekunden entlang der Weltachsen: der Schlag, der Schuss, die Explosion. Die Ragdoll muss aktiv sein, ruf also zuerst `ragdollActivate` auf und stoß dann. Gibt `false` zurück und tut nichts, wenn die Ragdoll nicht aktiv ist oder kein Knochen diesen Namen hat (Endgelenke sind keine Knochen); der Name wird ohne Rücksicht auf Groß- und Kleinschreibung verglichen.
|
|
|
|
*Parameter:*
|
|
|
|
* `node` -- der Knoten der Ragdoll.
|
|
* `jointName` -- der Name des Gelenkknotens, wie im Modell.
|
|
* `x`, `y`, `z` -- der Impuls in Newtonsekunden, entlang der Weltachsen.
|
|
|
|
*Rückgabe:* Wahr, wenn der Knochen gefunden wurde und die Ragdoll aktiv war.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<ragdollactivate,ragdollActivate>>, <<bodyapplyimpulse,bodyApplyImpulse>>, <<nodefind,nodeFind>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Treffer von der Position des Schützen aus: Der Impuls zeigt von der Waffe weg.
|
|
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)
|
|
----
|
|
|
|
Ob die Physik das Skelett gerade treibt: `true` von `ragdollActivate` bis `ragdollDeactivate` oder `ragdollDelete`. Ein Knoten ohne Ragdoll beendet das Skript.
|
|
|
|
*Rückgabe:* Wahr oder falsch.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<ragdollactivate,ragdollActivate>>, <<ragdollisresting,ragdollIsResting>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Der Lebensbalken hängt nur über dem Modell, solange es auf den Beinen ist.
|
|
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)
|
|
----
|
|
|
|
Ob der Körper jedes Knochens eingeschlafen ist, was die Physik tut, sobald ein Körper einen Moment lang still war. `false`, solange die Ragdoll nicht aktiv ist, es kann also nicht dazu dienen, ein untätiges Modell von einem zur Ruhe gekommenen zu unterscheiden; prüf auch `ragdollIsActive`.
|
|
|
|
*Rückgabe:* Wahr oder falsch.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<ragdollisactive,ragdollIsActive>>, <<ragdolldeactivate,ragdollDeactivate>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Runde endet, sobald sich der Verlierer nicht mehr bewegt.
|
|
function onOverlayUpdate()
|
|
if roundOver and ragdollIsResting(loser) then
|
|
restingFrames = restingFrames + 1
|
|
if restingFrames == 90 then
|
|
showResults()
|
|
end
|
|
end
|
|
return OVERLAY_NOT_UPDATED
|
|
end
|
|
----
|
|
|
|
[#save]
|
|
=== Spielstand
|
|
|
|
Alles, was das Spiel beim nächsten Start noch wissen soll: Highscores, die
|
|
Optionen des Spielers, wie weit er gekommen ist. Eine Tabelle pro Spiel, als
|
|
JSON im eigenen Datenordner des Spiels abgelegt (siehe
|
|
<<singegetdatapath,`singeGetDataPath`>>), sodass ein gepacktes `.game`
|
|
schreibgeschützt bleibt und zwei Spiele sich nie in die Quere kommen.
|
|
|
|
Die Schlüssel bestimmst du. Werte dürfen Zahlen, Strings, Booleans oder
|
|
Tabellen daraus sein, beliebig tief verschachtelt -- eine ganze
|
|
Highscore-Tabelle geht als ein einziger Wert hinein. Eine Funktion oder ein
|
|
Handle auf etwas, das der Engine gehört, nicht: Beides bedeutet beim nächsten
|
|
Lauf nichts, und so etwas zu speichern ist ein Fehler statt einer späteren
|
|
Überraschung.
|
|
|
|
**Was du setzt, wird gespeichert, und was du zurückbekommst, gehört dir.**
|
|
Tabellen werden beim Hineingeben und beim Herausholen kopiert; eine Tabelle zu
|
|
ändern, die du zurückgelesen hast, ändert also nichts, bis du sie wieder
|
|
speicherst, und eine Tabelle zu ändern, die du gespeichert hast, greift danach
|
|
nicht in den Spielstand hinein. Für eine Vorschau, die der Spieler dann
|
|
verwirft, genügt es deshalb, sie nicht zu speichern.
|
|
|
|
Geschrieben wird einmal am Ende jedes Frames, der etwas geändert hat, egal wie
|
|
viele Schlüssel du darin setzt, und noch einmal, wenn das Spiel beendet wird --
|
|
einschließlich allem, was in <<onshutdown,`onShutdown`>> gesetzt wird. Für
|
|
einen Automaten, der jederzeit den Strom verlieren kann, schreibt
|
|
<<saveflush,`saveFlush`>> sofort. Die Datei wird neben die alte geschrieben und
|
|
über sie umbenannt, sodass ein Stromausfall während des Schreibens den vorigen
|
|
Spielstand hinterlässt statt der Hälfte eines neuen.
|
|
|
|
Ganzzahlen kommen als Ganzzahlen zurück. JSON hat nur eine Art von Zahl und Lua
|
|
zwei, sodass eine als `100` gespeicherte Punktzahl sonst als `100.0`
|
|
zurückkäme und mit Dezimalpunkt ausgegeben würde.
|
|
|
|
[#saveclear]
|
|
==== saveClear
|
|
|
|
[source,text]
|
|
----
|
|
saveClear()
|
|
----
|
|
|
|
Vergisst alles und schreibt den leeren Spielstand hinaus. Für das „Highscores zurücksetzen“ eines Service-Menüs.
|
|
|
|
*Parameter:*
|
|
|
|
* Keine.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#savedelete]
|
|
==== saveDelete
|
|
|
|
[source,text]
|
|
----
|
|
saveDelete(key)
|
|
----
|
|
|
|
Entfernt einen Schlüssel. Einen Schlüssel zu löschen, den es nie gab, ist kein Fehler.
|
|
|
|
*Parameter:*
|
|
|
|
* `key` -- der zu entfernende Schlüssel.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#saveflush]
|
|
==== saveFlush
|
|
|
|
[source,text]
|
|
----
|
|
saveFlush()
|
|
----
|
|
|
|
Schreibt den Spielstand jetzt statt am Ende des Frames. Nutze es nach etwas, das du auf keinen Fall verlieren willst -- einem neuen Highscore auf einem Automaten, der an der Steckdose ausgeschaltet wird.
|
|
|
|
*Parameter:*
|
|
|
|
* Keine.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<saveset,saveSet>>
|
|
|
|
[#saveget]
|
|
==== saveGet
|
|
|
|
[source,text]
|
|
----
|
|
value = saveGet(key [, default])
|
|
----
|
|
|
|
Was unter diesem Schlüssel gespeichert wurde. Wurde nichts gespeichert, kommt der Standardwert zurück, oder `nil`, wenn es keinen gibt -- so unterscheidet man den ersten Lauf von einem späteren.
|
|
|
|
*Parameter:*
|
|
|
|
* `key` -- der zu lesende Schlüssel.
|
|
* `default` -- optional; was geantwortet wird, wenn der Schlüssel nicht da ist.
|
|
|
|
*Rückgabe:* der gespeicherte Wert, der Standardwert oder `nil`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<saveset,saveSet>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Läufe zählen und die zehn besten Punktzahlen merken.
|
|
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()
|
|
----
|
|
|
|
Eine Kopie des ganzen Spielstands, zum Lesen: um ihn beim Debuggen auszugeben oder um Schlüssel durchzugehen, deren Namen das Spiel nicht im Voraus kennt. Zu ändern, was zurückkommt, ändert nichts -- eine ganze Tabelle schreibt <<savesetall,`saveSetAll`>>.
|
|
|
|
*Parameter:*
|
|
|
|
* Keine.
|
|
|
|
*Rückgabe:* eine Tabelle; leer, wenn nichts gespeichert wurde.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<savesetall,saveSetAll>>, <<saveget,saveGet>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Was ist da drin? inspect wird genau dafür mitgeliefert.
|
|
debugPrint(require("inspect")(saveGetAll()))
|
|
----
|
|
|
|
[#saveset]
|
|
==== saveSet
|
|
|
|
[source,text]
|
|
----
|
|
saveSet(key, value)
|
|
----
|
|
|
|
Bewahrt einen Wert auf, bis das Spiel deinstalliert wird. Geschrieben wird am Ende des Frames, sodass zwanzig Schlüssel, in einer Schleife gesetzt, eine einzige Datei schreiben.
|
|
|
|
*Parameter:*
|
|
|
|
* `key` -- der zu schreibende Schlüssel.
|
|
* `value` -- eine Zahl, ein String, ein Boolean oder eine Tabelle daraus.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<saveget,saveGet>>, <<saveflush,saveFlush>>
|
|
|
|
[#savesetall]
|
|
==== saveSetAll
|
|
|
|
[source,text]
|
|
----
|
|
saveSetAll(everything)
|
|
----
|
|
|
|
Ersetzt den ganzen Spielstand in einem Schritt durch diese Tabelle. Schlüssel, die da waren und in der übergebenen Tabelle fehlen, sind weg.
|
|
|
|
Dieser Aufruf existiert, um einen alten Spielstand auf einen neuen Aufbau umzustellen. Mit <<saveclear,`saveClear`>> und einer Schleife von <<saveset,`saveSet`>> gibt es einen Moment, in dem der Spielstand leer ist, und ein Rechner, der in diesem Moment ausgeschaltet wird, verliert alles; dieser Aufruf hat keinen solchen Moment, denn die Datei wird einmal am Ende des Frames aus der fertigen Tabelle geschrieben.
|
|
|
|
*Parameter:*
|
|
|
|
* `everything` -- Tabelle; was der Spielstand nun enthalten soll. Dieselben Werte, die `saveSet` nimmt.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<savegetall,saveGetAll>>, <<saveclear,saveClear>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Version 1 hielt eine Punktzahl in zwei Schlüsseln; Version 2 hält eine Tabelle davon.
|
|
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]
|
|
=== Szene
|
|
|
|
Die Szene ist die 3D-Ebene, die zwischen dem Video der Disc und dem Overlay gezeichnet wird; Knoten, die mit `nodeSetLayer` in die vordere Ebene gelegt werden, werden stattdessen nach dem Overlay gezeichnet. Sie ist aus, bis `sceneEnable(true)` aufgerufen wird, hat die Größe des Overlays und wird jeden Frame mit der Farbe von `sceneSetBackground` gelöscht. Die `sceneSet*`-Aufrufe bestimmen das Aussehen der ganzen Ebene: Umgebungslicht, Himmel, Nebel, Belichtung und Tonkurve, Bloom, Kantenglättung und Schattenqualität; Farben sind Ganzzahlen von `0` bis `255`, und Entfernungen sind in Welteinheiten. `sceneProject` und `sceneUnproject` verbinden Weltkoordinaten und Overlay-Koordinaten, und `sceneGetStats` meldet die Arbeit des letzten Frames. Siehe <<scenes3d,3D-Szenen>>.
|
|
|
|
[#sceneenable]
|
|
==== sceneEnable
|
|
|
|
[source,text]
|
|
----
|
|
sceneEnable(enabled)
|
|
----
|
|
|
|
Schaltet die 3D-Ebene ein oder aus. Knoten, Meshes und Materialien können gebaut werden, bevor sie an ist, und überstehen ihr Ausschalten; nur das Zeichnen hört auf. Beendet das Skript, wenn sie ohne nutzbare GPU eingeschaltet wird (siehe <<scenes3d,3D-Szenen>>); ein Spiel mit einem 2D-Ersatz fragt deshalb zuerst `singeCapabilities().SCENE` ab. Die Anzeige wird in jedem Fall im nächsten Frame aufgefrischt.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<scenesetbackground,sceneSetBackground>>, <<scenesetantialias,sceneSetAntialias>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Auf das einfache Overlay zurückfallen, wenn 3D nicht verfügbar ist.
|
|
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()
|
|
----
|
|
|
|
Die Größe der Ebene, also die Auflösung des Overlays: der Raum, in dem `sceneProject` und `sceneUnproject` arbeiten, und die Größe, in der die Szene gerendert wird. Ändern lässt sie sich mit `overlaySetResolution`. Bevor die Ebene eine Größe hat, sind beide Werte `0`.
|
|
|
|
*Rückgabe:* Zwei Ganzzahlen, die Breite und die Höhe in Overlay-Koordinaten.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<overlaysetresolution,overlaySetResolution>>, <<sceneunproject,sceneUnproject>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Fadenkreuz in der Mitte der Ansicht und das Ding darunter.
|
|
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()
|
|
----
|
|
|
|
Die Zahlen des letzten Frames von der Kamera des Fensters: die zum Zeichnen gesammelten Meshes, wie viele davon im Sichtfeld der Kamera lagen, die Zeichenaufrufe, die nach dem Instanzieren daraus wurden, und der Texturspeicher, den die Szene belegt, in Kilobyte. Zum Abstimmen: Eine Szene mit Hunderten `drawn`, aber einer Handvoll `calls` teilt ihre Meshes gut, und ein `textureKB`, das nicht in den Speicher eines Raspberry Pi passt, verlangt nach KTX2-Texturen. Gerenderte Ansichten zählen nicht.
|
|
|
|
*Rückgabe:* Vier Ganzzahlen.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<sceneenable,sceneEnable>>, <<modelinstance,modelInstance>>, <<materialsettexture,materialSetTexture>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Debug-Anzeigefeld in der Ecke, während das Level abgestimmt wird.
|
|
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)
|
|
----
|
|
|
|
Misst die Zeit der 3D-Szene. `sceneMeasure(true)` startet: Jeder Frame danach
|
|
wartet, bis die GPU die Szene fertig hat, und zählt diese Zeit zusammen, sodass
|
|
die Arbeit gemessen wird statt des Takts der Anzeige. `sceneMeasure(false)`
|
|
hält an und gibt die Millisekunden zurück, die ein Frame im Schnitt brauchte,
|
|
oder `nil`, wenn kein Frame gezeichnet wurde. Das Warten kostet Bildrate, also
|
|
miss ein paar Sekunden, nicht ein ganzes Spiel.
|
|
|
|
*Rückgabe:* nichts beim Starten; eine Zahl oder `nil` beim Anhalten.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<singesavetier,singeSaveTier>>, <<scenegetstats,sceneGetStats>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Zwei Sekunden Titelbildschirm sagen, ob die vollen Effekte passen.
|
|
sceneMeasure(true)
|
|
-- ... zwei Sekunden später:
|
|
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)
|
|
----
|
|
|
|
Welche GUI auf der Oberfläche unter einem Overlay-Punkt liegt und wo darauf: ein Strahl von der Kamera durch den Punkt, mit der Kamera des zuletzt gerenderten Frames, gegen die Dreiecke jedes sichtbaren Knotens, dessen Material eine GUI zeigt (`materialSetGui`), wobei die Texturkoordinaten des nächsten Treffers die Stelle auf der GUI angeben. Gibt das GUI-Handle und das Pixel in den eigenen Koordinaten dieser GUI zurück (`0, 0` oben links, `guiGetWidth`, `guiGetHeight` unten rechts), oder nur `nil`, wenn der Punkt auf keiner solchen Oberfläche liegt, die Szene aus ist oder noch nichts gerendert wurde. Die Engine stellt dieselbe Frage, wenn die Maus oder eine Lightgun auf die Szene zeigt, während eine GUI, die Eingaben annimmt, auf einer Oberfläche liegt und nichts flach Gezeichnetes unter dem Zeiger ist, sodass Klicks auf einen Monitor in der Szene kein Skript brauchen; der Aufruf ist für ein Spiel, das die Antwort selbst haben will, für ein Laservisier, einen Hover-Effekt oder einen Test.
|
|
|
|
*Parameter:*
|
|
|
|
* `sx, sy` -- der Punkt, in Overlay-Koordinaten (eine Maus- oder Lightgun-Position).
|
|
|
|
*Rückgabe:* Eine Ganzzahl und zwei Ganzzahlen, oder `nil`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<materialsetgui,materialSetGui>>, <<guisetinput,guiSetInput>>, <<sceneproject,sceneProject>>, <<sceneunproject,sceneUnproject>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Der Punkt einer Lightgun leuchtet dort, wo er auf dem Bildschirm des Cockpits landet.
|
|
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)
|
|
----
|
|
|
|
Wo ein Weltpunkt in Overlay-Koordinaten landet, mit der Kamera des zuletzt gerenderten Frames. `depth` ist `0` an der nahen Clipping-Ebene und `1` an der fernen, zum Sortieren von Beschriftungen oder um sie mit der Entfernung auszublenden. `inFront` ist `false`, wenn der Punkt hinter der Kamera liegt, und `x` und `y` sind dann bedeutungslos; vor dem ersten gerenderten Frame gibt es nichts, wogegen projiziert werden kann, und jeder Wert kommt als `0` zurück, mit `inFront` `false`. Punkte vor der Kamera, aber außerhalb der Ansicht kommen mit Koordinaten jenseits der Ränder des Overlays zurück; prüf sie also vor dem Zeichnen gegen `sceneGetSize`. Der übliche Einsatz ist ein Namensschild oder Lebensbalken, mit `fontPrint` oder `spriteDraw` über einem 3D-Objekt gezeichnet.
|
|
|
|
*Parameter:*
|
|
|
|
* `wx, wy, wz` -- der Weltpunkt, in Welteinheiten.
|
|
|
|
*Rückgabe:* Zwei Zahlen, eine Zahl und ein Boolean.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<sceneunproject,sceneUnproject>>, <<nodegetworldposition,nodeGetWorldPosition>>, <<scenegetsize,sceneGetSize>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Name und ein Lebensbalken, die über jeder Figur schweben.
|
|
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)
|
|
----
|
|
|
|
Der Weltpunkt `distance` Einheiten entlang des Strahls durch ein Overlay-Pixel, mit der Kamera des zuletzt gerenderten Frames. Zwei Entfernungen ergeben einen Strahl zum Auswählen mit `physicsRaycast`; eine setzt ein Objekt dorthin, „wo der Spieler geklickt hat“. Bevor der erste Frame gezeichnet wurde, gibt es den Ursprung zurück.
|
|
|
|
*Parameter:*
|
|
|
|
* `sx, sy` -- das Overlay-Pixel, in Overlay-Koordinaten.
|
|
* `distance` -- wie weit entlang des Strahls, in Welteinheiten; `0` ist die nahe Clipping-Ebene.
|
|
|
|
*Rückgabe:* Drei Zahlen, der Weltpunkt.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<sceneproject,sceneProject>>, <<mousegetposition,mouseGetPosition>>, <<physicsraycast,physicsRaycast>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Markierung auf das setzen, was der Spieler angeklickt hat, oder 8 Einheiten entfernt, wenn nichts.
|
|
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)
|
|
----
|
|
|
|
Licht von überall, `0` bis `255` pro Kanal, als sRGB behandelt; der Standard ist ein mattes Grau von `26, 26, 26`. Es hellt die Seiten auf, die kein Licht erreicht, damit sie nicht schwarz sind, und `materialSetOcclusionMap` dunkelt es in Ritzen ab. Ist ein Himmel gesetzt und `sceneSetEnvironment` an, ersetzt das Licht des Himmels diesen Wert.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<lightnew,lightNew>>, <<scenesetenvironment,sceneSetEnvironment>>, <<scenesetsky,sceneSetSky>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein mondbeschienener Hof: überall ein schwaches Blau, ein kalter Mond als Richtungslicht.
|
|
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)
|
|
----
|
|
|
|
Schaltet 4x-Multisampling ein oder aus; es ist standardmäßig an, wo immer die GPU es für die Formate der Ebene anbietet, und fehlt stillschweigend, wo nicht. Es auszuschalten bringt auf einem Raspberry Pi Geschwindigkeit. Die Renderziele werden sofort neu gebaut, ruf es also bei der Einrichtung auf statt in jedem Frame.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<sceneenable,sceneEnable>>, <<scenesetshadowsize,sceneSetShadowSize>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Qualitätseinstellung aus dem Optionsbildschirm des Spiels.
|
|
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)
|
|
----
|
|
|
|
Die Farbe, mit der die Ebene jeden Frame gelöscht wird, `0` bis `255` pro Kanal. `a` ist `255`, wenn es weggelassen wird, ein deckender Hintergrund; unter `255` scheint das Video durch, und `0` (die übliche Wahl für ein Spiel mit Disc) macht den Hintergrund völlig transparent. Bis zum ersten Aufruf wird die Ebene schwarz und transparent gelöscht. Ein Himmel aus `sceneSetSky` wird über den Hintergrund gezeichnet, sodass das Video nicht mehr durchscheint, solange einer gesetzt ist.
|
|
|
|
*Parameter:*
|
|
|
|
* `r, g, b` -- die Löschfarbe.
|
|
* `a` -- das Lösch-Alpha; `255`, wenn weggelassen.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<sceneenable,sceneEnable>>, <<scenesetsky,sceneSetSky>>, <<scenesetfog,sceneSetFog>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Disc ist hinter dem 3D-Cockpit zu sehen; ein deckender Nachthimmel für das 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)
|
|
----
|
|
|
|
Ein Leuchten um alles, was heller als `threshold` ist, genommen aus dem linearen Licht der Szene vor Belichtung und Tonkurve (wo `1` Weiß ist) und mit `strength` wieder hinzugefügt. Die Standards sind ein Schwellwert von `1` und eine Stärke von `0`, also kein Bloom; `0.2` bis `0.4` ist dezent, `1` ist ein Dunst. Das Leuchten wird über eine Kette von Durchgängen in halber Größe weichgezeichnet, kostet also wenig, und nur die Kamera des Fensters bekommt es: Gerenderte Ansichten lassen es aus. Negative Werte werden auf `0` begrenzt.
|
|
|
|
*Parameter:*
|
|
|
|
* `threshold` -- die lineare Helligkeit, über der ein Pixel leuchtet; `1` ist Weiß vor der Tonkurve.
|
|
* `strength` -- wie viel des Leuchtens wieder hinzugefügt wird; `0` für nichts.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<scenesetexposure,sceneSetExposure>>, <<materialsetemissive,materialSetEmissive>>, <<scenesettonemap,sceneSetTonemap>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Neon und Scheinwerfer leuchten; der Asphalt nicht.
|
|
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)
|
|
----
|
|
|
|
Ob der mit `sceneSetSky` gesetzte Himmel die Szene beleuchtet: sein diffuses Licht aus allen Richtungen anstelle von `sceneSetAmbient` und seine Spiegelungen auf Metallen und glänzenden Oberflächen, scharf auf einem Spiegel und umso unschärfer, je höher die Rauheit. Standardmäßig an, sodass ein Himmel die Szene beleuchtet, sobald er gesetzt ist; `false` behält den Himmel nur als Hintergrund, und das flache Umgebungslicht kehrt zurück. Ohne Himmel hat es keine Wirkung.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<scenesetsky,sceneSetSky>>, <<scenesetambient,sceneSetAmbient>>, <<scenesetskyintensity,sceneSetSkyIntensity>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein gemalter Hintergrund, der das Chrom nicht tönen darf.
|
|
sceneSetSky(DIR .. "Skies/matteBackdrop.png")
|
|
sceneSetEnvironment(false)
|
|
sceneSetAmbient(40, 40, 45)
|
|
----
|
|
|
|
[#scenesetexposure]
|
|
==== sceneSetExposure
|
|
|
|
[source,text]
|
|
----
|
|
sceneSetExposure(stops)
|
|
----
|
|
|
|
Hellt die ganze Szene in fotografischen Blendenstufen auf oder dunkelt sie ab, `-10` bis `10` (Werte außerhalb werden begrenzt): `+1` verdoppelt das Licht, `-1` halbiert es. Der Standard ist `0`. Es wird nach dem Bloom und vor der Tonkurve angewandt, sodass eine helle Szene bei `+2` zu Weiß hin gestaucht statt abgeschnitten wird und eine Höhle und ein Strand dieselben Lichter teilen können. Animiere es für eine langsame Anpassung beim Gang von drinnen ins Tageslicht.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<scenesettonemap,sceneSetTonemap>>, <<lightsetintensity,lightSetIntensity>>, <<scenesetskyintensity,sceneSetSkyIntensity>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Augen gewöhnen sich: die Belichtung jeden Frame sanft zum Zielwert des Bereichs führen.
|
|
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()
|
|
----
|
|
|
|
Entfernungsnebel: klar bis `near` Einheiten von der Kamera, ganz in der Nebelfarbe ab `far`, auf Meshes (auch unbeleuchteten) und Partikeln gleichermaßen; additive Partikel blenden aus, statt grau zu werden. Ohne Argumente oder mit einem `far`, das nicht größer als `near` ist, gibt es keinen Nebel. Der Himmel wird nicht vernebelt, wähl also eine Farbe nahe seinem Horizont oder, wenn es keinen Himmel gibt, nahe dem Hintergrund. Jede andere Anzahl von Argumenten beendet das Skript.
|
|
|
|
*Parameter:*
|
|
|
|
* `r, g, b` -- die Nebelfarbe, `0` bis `255`.
|
|
* `near` -- die Entfernung, bei der der Nebel beginnt, in Welteinheiten von der Kamera.
|
|
* `far` -- die Entfernung, ab der nur noch Nebel bleibt.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<scenesetsky,sceneSetSky>>, <<scenesetbackground,sceneSetBackground>>, <<camerasetperspective,cameraSetPerspective>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Morgendunst über den Hügeln, der sich im Laufe des Levels auflöst.
|
|
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)
|
|
----
|
|
|
|
In wie viele Maps der Schatten eines Richtungslichts entlang der Blickrichtung aufgeteilt wird, `1` bis `4` (Werte außerhalb werden begrenzt; der Standard ist `3`). Jede Kaskade deckt einen Ausschnitt des Blickfelds der Kamera bis `sceneSetShadowDistance` ab, der nächste Ausschnitt der kleinste, sodass Schatten nahe der Kamera scharf sind, egal wie groß das Level ist. `1` passt eine Map an die ganze Szene an, was für kleine Szenen richtig ist, und wird bei einer orthografischen Kamera immer verwendet. Jede Kaskade ist ein weiterer Schattendurchgang für jedes schattenwerfende Richtungslicht; `2` ist eine vernünftige Wahl auf einem Raspberry Pi.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<scenesetshadowdistance,sceneSetShadowDistance>>, <<scenesetshadowsize,sceneSetShadowSize>>, <<lightsetshadow,lightSetShadow>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein großes Außenlevel: scharfe Schatten nahe dem Spieler, die weit reichen.
|
|
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)
|
|
----
|
|
|
|
Wie weit kaskadierte Schatten von der Kamera aus reichen, in Welteinheiten; der Standard ist `60`, und Werte unter `1` werden auf `1` begrenzt. Dahinter wirft ein Richtungslicht keinen Schatten. Kürzere Entfernungen machen jede Kaskade schärfer, da dieselben Maps weniger Fläche abdecken; setz sie also etwa so weit, wie der Spieler einen Schatten erkennen kann. Es zählt nur bei mehr als einer Kaskade unter einer perspektivischen Kamera.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<scenesetshadowcascades,sceneSetShadowCascades>>, <<scenesetshadowsize,sceneSetShadowSize>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Schatten müssen nur bis zur hinteren Wand der Arena reichen.
|
|
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)
|
|
----
|
|
|
|
Texel pro Seite jeder Shadow Map, jeder Kaskade und jeder Würfelfläche, `256` bis `4096` (Werte außerhalb werden begrenzt; der Standard ist `1024`). Größer ist schärfer und langsamer, im Speicher ebenso wie in der Zeit, denn jedes schattenwerfende Licht hat seine eigenen Maps in dieser Größe. `2048` passt zu einem Desktop, `512` zu einem Raspberry Pi. Die Maps werden im nächsten Frame neu gebaut.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<lightsetshadow,lightSetShadow>>, <<scenesetshadowcascades,sceneSetShadowCascades>>, <<scenesetantialias,sceneSetAntialias>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Shadow Maps an den Rechner anpassen.
|
|
if singeGetPlatform() == "raspberrypi" then
|
|
sceneSetShadowSize(512)
|
|
else
|
|
sceneSetShadowSize(2048)
|
|
end
|
|
----
|
|
|
|
[#scenesetsky]
|
|
==== sceneSetSky
|
|
|
|
[source,text]
|
|
----
|
|
sceneSetSky(file)
|
|
sceneSetSky(nil)
|
|
sceneSetSky()
|
|
----
|
|
|
|
Hüllt die Szene in ein äquirektanguläres Panorama: ein Radiance-`.hdr` (wie HDRI-Seiten sie ausliefern, mit vollem Umfang) oder ein PNG, JPEG oder anderes Bild, das SDL_image liest, aus sRGB decodiert. Der Name ist spielrelativ wie bei `spriteLoad`. Der Himmel wird hinter allem anstelle der Hintergrundfarbe gezeichnet (sodass das Video nicht mehr durchscheint) und beleuchtet die Szene, sofern nicht `sceneSetEnvironment(false)` gilt. Seine Mitte zeigt nach -Z und seine Oberseite nach oben. Er wird in einen Würfel mit bis zu 1024 Texeln pro Fläche umgerechnet, sodass ein Bild von 2048 mal 1024 reichlich ist und größere auf einem Raspberry Pi einen Moment zur Vorbereitung brauchen. Ohne Argument oder mit `nil` wird der Himmel entfernt. Beendet das Skript, wenn die Datei nicht gelesen werden kann oder 3D nicht verfügbar ist.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<scenesetskyintensity,sceneSetSkyIntensity>>, <<scenesetenvironment,sceneSetEnvironment>>, <<scenesetfog,sceneSetFog>>, <<scenesetbackground,sceneSetBackground>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein bedeckter Himmel für das Hafenlevel, mit einem auf seinen Horizont abgestimmten Nebel.
|
|
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)
|
|
----
|
|
|
|
Die Helligkeit des Himmels und des Lichts, das er wirft, `1` wie geladen; negative Werte werden auf `0` begrenzt. Nützlich, um einen zu hellen PNG-Himmel abzudunkeln, einen `.hdr`-Himmel zum Hauptlicht der Szene zu machen oder den Himmel ein- und auszublenden. Es multipliziert den Himmel selbst, anders als `sceneSetExposure`, das alles skaliert.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<scenesetsky,sceneSetSky>>, <<scenesetexposure,sceneSetExposure>>, <<scenesetenvironment,sceneSetEnvironment>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Abenddämmerung: Der Himmel und sein Licht werden über zehn Sekunden dunkler.
|
|
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)
|
|
----
|
|
|
|
Die Kurve, die das lineare Licht der Szene auf den Bildschirm abbildet, angewandt nach der Belichtung. `TONEMAP_NEUTRAL` (der Standard) hält die Farben getreu und staucht nur das hellste Licht; `TONEMAP_ACES` ist die filmische Kurve mit mehr Kontrast und einem wärmeren Auslauf; `TONEMAP_NONE` schneidet bei Weiß ab. Videomaterialien, unbeleuchtete Oberflächen und der Himmel durchlaufen dieselbe Kurve, sodass ein Spiel, dessen Disc genau wie gefilmt aussehen muss, `TONEMAP_NONE` mit einer Belichtung von `0` will. Jeder andere Wert beendet das Skript.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<scenesetexposure,sceneSetExposure>>, <<scenesetbloom,sceneSetBloom>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein filmischer Look für die Zwischensequenz, getreue Farben fürs Spielen.
|
|
function onCutsceneStart()
|
|
sceneSetTonemap(TONEMAP_ACES)
|
|
sceneSetExposure(0.5)
|
|
end
|
|
|
|
function onCutsceneEnd()
|
|
sceneSetTonemap(TONEMAP_NEUTRAL)
|
|
sceneSetExposure(0)
|
|
end
|
|
----
|
|
|
|
[#script]
|
|
=== Skript
|
|
|
|
WARNING: Mit diesen Funktionen übergibt ein Singe-Skript die Kontrolle an ein anderes Singe-Skript: der Mechanismus hinter dem eingebauten Launcher `Menu.singe`. Spieleentwickler sollten nicht zu ihnen greifen; bau dein Spiel als ein einziges Skript und überlass die Verkettung dem Menüsystem. Sie sind hier der Vollständigkeit halber dokumentiert und für alle, die das Menü selbst pflegen.
|
|
|
|
Beide Aufrufe nehmen eine Tabelle in der Form eines `GAMES[]`-Eintrags in `games.dat` und stellen sie für die Skriptausführung der Engine in die Warteschlange. Die neue Konfiguration geht von der laufenden aus, sodass Befehlszeilenoptionen wie der Datenordner und der Trace übernommen werden, die Disc, das Video und der Container aber nie: Jeder Eintrag bringt seine eigenen mit. Das aktuelle Skript endet am Ende des Frames, `onShutdown` wird ausgelöst, und das wartende Skript startet mit einem frischen Lua-Zustand (siehe <<enginecallbacks,Engine-Callbacks>>). Der Datenordner des neuen Skripts folgt derselben Regel wie ein Start über die Befehlszeile (siehe `singeGetDataPath`); kann er nicht angelegt werden, wird das aufrufende Skript abgebrochen.
|
|
|
|
[#scriptexecute]
|
|
==== scriptExecute
|
|
|
|
[source,text]
|
|
----
|
|
scriptExecute(config)
|
|
----
|
|
|
|
Ersetzt das laufende Skript durch das, welches die Tabelle beschreibt. Das aktuelle Skript hält am Ende dieses Frames an, und `onShutdown` läuft; die Kontrolle kehrt nie zu ihm zurück, und wenn das neue Skript endet, geht die Engine zu dem über, was sonst noch in der Warteschlange steht, oder beendet sich. Die String-Schlüssel, die die Engine kennt, sind `SCRIPT`, `CONTAINER`, `VIDEO`, `STRETCH`, `NO_MOUSE`, `RESOLUTION_X`, `RESOLUTION_Y`, `SINDEN_GUN`, `AUDIO_TRACK`, `AUDIO_DELAY`, `LEGACY_SPRITE_ARGS`, `CANVAS_X` und `CANVAS_Y`; andere Schlüssel werden ignoriert, und Schlüssel, die keine Strings sind, werden übersprungen. Ein `SCRIPT`, `CONTAINER` oder `VIDEO`, das kein String ist, bricht das Skript ab, und ebenso ein Argument, das keine Tabelle ist.
|
|
|
|
*Parameter:*
|
|
|
|
* `config` -- eine Tabelle mit `games.dat`-Feldern, mindestens `SCRIPT` oder `CONTAINER`.
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<scriptpush,scriptPush>>, <<singequit,singeQuit>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Launcher, der nie zu sich selbst zurückkehrt.
|
|
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)
|
|
----
|
|
|
|
Startet das Skript, das die Tabelle beschreibt, und stellt die aktuelle Konfiguration dahinter in die Warteschlange, sodass das aktuelle Skript, wenn das neue endet, wieder von vorn läuft. Nichts bleibt erhalten: `onShutdown` des aktuellen Skripts läuft jetzt, sein Lua-Zustand wird verworfen, und der spätere Lauf ist ein Neustart, weshalb `Menu.singe` seine Auswahl vor dem Push in eine Datei speichert. Die Tabelle nimmt dieselben Schlüssel wie `scriptExecute`.
|
|
|
|
*Parameter:*
|
|
|
|
* `config` -- eine Tabelle mit `games.dat`-Feldern, wie bei `scriptExecute`.
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<scriptexecute,scriptExecute>>, <<singequit,singeQuit>>, <<onshutdown,onShutdown>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Menu.singe: das gewählte Spiel starten und danach zurückkommen.
|
|
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
|
|
|
|
Die Funktionen im Namensraum `singe*` steuern die Engine selbst: das Fenster, Screenshots, das Pausensystem, Beenden und Neuladen, die Audioverzögerung für Lippensynchronität und ein paar Pfade und Flags, die von der Befehlszeile und aus `games.dat` kommen. Nichts hier nimmt ein Handle. Das Pausenverhalten ist unter <<pausing,Pausieren>> beschrieben, die Werte der Audioverzögerung unter <<audiosync,Audio-Synchronisation>>.
|
|
|
|
[#allowsocketcall]
|
|
==== allowSocketCall
|
|
|
|
[source,text]
|
|
----
|
|
allowed = allowSocketCall()
|
|
----
|
|
|
|
Hypseus-Erweiterung, in Singe immer `false`. Hypseus antwortet damit, ob der Benutzer ihm erlaubt hat, Daten an einen Statistikserver zu senden, was dort `-noserversend` abschaltet; das Senden selbst ist dort auskommentiert. Singe sendet nirgendwohin etwas, also ist nichts erlaubt, und die Antwort ändert sich nie. Die Funktion gibt es, damit ein Spiel, das fragt, nicht an einer fehlenden Funktion stehen bleibt.
|
|
|
|
*Rückgabe:* ein Boolean, immer `false`.
|
|
|
|
*Seit:* 3.00
|
|
|
|
[#getuserstring]
|
|
==== getUserString
|
|
|
|
[source,text]
|
|
----
|
|
id = getUserString()
|
|
----
|
|
|
|
Hypseus-Erweiterung, immer ein leerer String. Trotz des Namens ist es kein String, den der Benutzer gesetzt hat: Hypseus antwortet mit der Installations-ID des Rechners, die es mit Netzwerkstatistiken sendet, genommen aus der `MachineGuid` von Windows oder der Machine-ID von Linux. Singe sendet nirgendwohin etwas und gibt einem Spiel keinen Fingerabdruck des Rechners, auf dem es läuft.
|
|
|
|
*Rückgabe:* ein String, immer leer.
|
|
|
|
*Seit:* 3.00
|
|
*Siehe auch:* <<allowsocketcall,allowSocketCall>>
|
|
|
|
[#singedisablepausekey]
|
|
==== singeDisablePauseKey
|
|
|
|
[source,text]
|
|
----
|
|
singeDisablePauseKey()
|
|
----
|
|
|
|
Übergibt die Pausentaste an das Skript, genau wie `singeSetPauseKeyEnabled(false)`. Es ist eine in `Framework.singe` definierte Funktion, die erhalten bleibt, damit ältere Spiele unverändert laufen.
|
|
|
|
*Hinweise:* Veralteter Alias.
|
|
|
|
WARNING: Neuer Code sollte <<singesetpausekeyenabled,singeSetPauseKeyEnabled>> mit `false` aufrufen.
|
|
|
|
*Seit:* 1.18 (RDG)
|
|
*Siehe auch:* <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>, <<singeenablepausekey,singeEnablePauseKey>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Älterer Spielcode; die moderne Schreibweise ist singeSetPauseKeyEnabled(false).
|
|
singeDisablePauseKey()
|
|
|
|
function onInputPressed(what)
|
|
if what == SWITCH_PAUSE then
|
|
togglePauseMenu()
|
|
end
|
|
end
|
|
----
|
|
|
|
[#singeenablepausekey]
|
|
==== singeEnablePauseKey
|
|
|
|
[source,text]
|
|
----
|
|
singeEnablePauseKey()
|
|
----
|
|
|
|
Gibt die Pausentaste an die Engine zurück, genau wie `singeSetPauseKeyEnabled(true)`. Es ist eine in `Framework.singe` definierte Funktion, die erhalten bleibt, damit ältere Spiele unverändert laufen.
|
|
|
|
*Hinweise:* Veralteter Alias.
|
|
|
|
WARNING: Neuer Code sollte <<singesetpausekeyenabled,singeSetPauseKeyEnabled>> mit `true` aufrufen.
|
|
|
|
*Seit:* 1.18 (RDG)
|
|
*Siehe auch:* <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>, <<singedisablepausekey,singeDisablePauseKey>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Älterer Spielcode; die moderne Schreibweise ist singeSetPauseKeyEnabled(true).
|
|
function pauseMenuClose()
|
|
singeSetPauseFlag(false)
|
|
singeEnablePauseKey()
|
|
end
|
|
----
|
|
|
|
[#utilsha256]
|
|
==== utilSha256
|
|
|
|
[source,text]
|
|
----
|
|
digest = utilSha256(data)
|
|
----
|
|
|
|
SHA-256 eines Strings, als 64 Hexadezimalzeichen in Kleinbuchstaben. Binärsicher, funktioniert also mit den
|
|
Bytes einer Datei ebenso gut wie mit Text.
|
|
|
|
Um zu entscheiden, ob die Bytes, die du hast, die Bytes sind, die dir versprochen wurden: ein
|
|
heruntergeladenes Spiel gegen den Hashwert, den sein Katalogeintrag trägt, oder der öffentliche
|
|
Schlüssel eines Servers gegen einen Pin. Das mitgelieferte Modul `md5` gibt es weiterhin für eine
|
|
Prüfsumme, aber ein Hashwert, mit dem entschieden wird, ob man etwas *vertraut*, muss einer sein,
|
|
dem man noch vertrauen kann.
|
|
|
|
*Parameter:*
|
|
|
|
* `data` -- ein String aus Bytes. Ein leerer bricht das Skript ab.
|
|
|
|
*Rückgabe:* String, 64 Hexadezimalzeichen in Kleinbuchstaben.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Einen Download ablehnen, der nicht unversehrt angekommen ist.
|
|
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()
|
|
----
|
|
|
|
Was dieser Rechner ausführen kann. Die Tabelle hat einen Eintrag pro
|
|
Fähigkeitsname, den eine `REQUIRES`-Liste in `games.dat` verwenden kann
|
|
(`SCENE`, `GUI`, `SHADOWS`, `POST`, `SKIN`, `MORPH`), jeder `true` oder
|
|
`false`, sowie `tier`: die gemessene Leistungsklasse des Rechners von 0 (kein
|
|
3D) bis 3 (eine Desktop-GPU), oder `nil`, bevor das Menü sie gemessen hat. Ein
|
|
Spiel fragt, damit es auf etwas verzichten kann, statt deswegen abgewiesen zu
|
|
werden.
|
|
|
|
*Rückgabe:* eine Tabelle.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<singehasgpu,singeHasGpu>>, <<singesavetier,singeSaveTier>>, <<scenemeasure,sceneMeasure>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Schatten, wo der Rechner sie hat, eine einfache Sonne, wo nicht.
|
|
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()
|
|
----
|
|
|
|
Gibt die geltende Audioverzögerung des Rechners in Millisekunden zurück: den Wert, den der Kalibrierbildschirm des Menüs in `machine.cfg` im obersten Datenordner gespeichert hat, oder einen seitdem mit `singeSetAudioCalibration` gesetzten. Null, wenn der Rechner nie kalibriert wurde. Sie gilt für jedes Spiel auf dem Rechner, zusätzlich zur Verzögerung pro Spiel aus `singeGetAudioDelay`.
|
|
|
|
*Rückgabe:* ganzzahlige Millisekunden.
|
|
|
|
*Seit:* 3.00
|
|
*Siehe auch:* <<singesetaudiocalibration,singeSetAudioCalibration>>, <<singegetaudiodelay,singeGetAudioDelay>>, <<singegetaudiolatency,singeGetAudioLatency>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Menu.singe: den Kalibrierbildschirm mit dem gespeicherten Wert beginnen.
|
|
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()
|
|
----
|
|
|
|
Gibt die geltende Audioverzögerung des Spiels in Millisekunden zurück, ob sie aus `--audiodelay`, aus dem Schlüssel `AUDIO_DELAY` im `games.dat`-Eintrag des Spiels oder von `singeSetAudioDelay` kommt. Null, wenn keine gesetzt wurde. Sie wird zur Kalibrierung des Rechners und zur gemessenen Gerätelatenz addiert, wenn die Engine entscheidet, wann jeder Videoframe gezeigt wird.
|
|
|
|
*Rückgabe:* ganzzahlige Millisekunden.
|
|
|
|
*Seit:* 3.00
|
|
*Siehe auch:* <<singesetaudiodelay,singeSetAudioDelay>>, <<singegetaudiocalibration,singeGetAudioCalibration>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Service-Menü: den aktuellen Sync-Versatz neben seinem Schieberegler zeigen.
|
|
function drawSyncSetting(y)
|
|
local delay = singeGetAudioDelay()
|
|
fontPrint(40, y, string.format("A/V offset: %+d ms", delay))
|
|
end
|
|
----
|
|
|
|
[#singegetaudiolatency]
|
|
==== singeGetAudioLatency
|
|
|
|
[source,text]
|
|
----
|
|
milliseconds = singeGetAudioLatency()
|
|
----
|
|
|
|
Gibt die Warteschlange des Audiogeräts zurück, die die Engine beim Öffnen des Mixers gemessen hat, in Millisekunden: die Zeit zwischen der Übergabe von Samples an den Mixer und ihrem Eintreffen am Gerät, vor jeder Kalibrierung oder Verzögerung pro Spiel. Die Engine gleicht sie bereits aus; ein Kalibrierbildschirm braucht sie, um sein Klicken und seinen Blitz zu planen, und in einer Diagnoseansicht lohnt es sich, sie auszugeben.
|
|
|
|
*Rückgabe:* ganzzahlige Millisekunden.
|
|
|
|
*Seit:* 3.00
|
|
*Siehe auch:* <<singegetaudiocalibration,singeGetAudioCalibration>>, <<singegetaudiodelay,singeGetAudioDelay>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Menu.singe: Der Blitz folgt dem Klicken um die Gerätewarteschlange plus den Kandidaten.
|
|
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()
|
|
----
|
|
|
|
Gibt den Ordner zurück, in den Singe für dieses Spiel schreibt, mit einem abschließenden Pfadtrenner, sodass ein Dateiname direkt angehängt werden kann. Es ist ein nach dem Spiel benannter Unterordner der Basis von `--datadir` (bei einem gepackten Spiel der Name der Datenbank und dann der Ordner des Skripts darin); ohne `--datadir` ist die Basis `data` im Spieleverzeichnis. Screenshots, Videoindizes und `trace.txt` landen dort, und dorthin gehören auch Spielstände, Highscores und Einstellungen; Singe 2.x schrieb sie neben das Spiel, das schreibgeschützt sein kann.
|
|
|
|
*Rückgabe:* String-Pfad, der auf einen Trenner endet.
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<singegetscriptpath,singeGetScriptPath>>, <<singescreenshot,singeScreenshot>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Menu.singe behält seine letzte Auswahl zwischen den Läufen.
|
|
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()
|
|
----
|
|
|
|
Die `GAME_ID` aus dem `games.dat`-Eintrag dieses Spiels, oder `nil`, wenn er keine hat. Es ist die
|
|
ID, unter der der Online-Dienst das Spiel kennt, und der Schlüssel, unter dem eine Bestenliste
|
|
geführt wird.
|
|
|
|
**Das Format ist eine UUID**, in der kanonischen Form mit 36 Zeichen -- acht Hexziffern, drei
|
|
Gruppen zu vier, dann zwölf, durch Bindestriche getrennt:
|
|
|
|
----
|
|
GAME_ID = "6f1e7b62-0a4e-4d9c-9b2f-1c7a5e3d8a10"
|
|
----
|
|
|
|
Nichts anderes wird angenommen. Die ID ist ein Pfadsegment in den URLs des Dienstes, sodass eine mit
|
|
einem Schrägstrich oder Fragezeichen veröffentlicht würde und dann nie erreichbar wäre, und zwei
|
|
IDs, die sich nur in der Groß- und Kleinschreibung unterscheiden, wären für die Datenbank zwei
|
|
Spiele und für jeden, der sie liest, eines. Verglichen wird ohne Rücksicht auf Groß- und
|
|
Kleinschreibung, gespeichert in Kleinbuchstaben, sodass eine in Großbuchstaben getippte ID dasselbe
|
|
Spiel ist.
|
|
|
|
Jede UUID tut es -- erzeug eine, wie du willst, oder lass `publish.lua` eine ausgeben. Ein Spiel
|
|
ohne ID kann gespielt, aber nicht gewertet werden: Es gäbe nichts Beständiges, unter dem eine
|
|
Bestenliste geführt werden könnte, und eine zur Laufzeit erfundene ID ergäbe jedes Mal eine andere
|
|
Bestenliste, wenn die Dateien verschoben werden.
|
|
|
|
**Ein `.game`, eine ID.** Ein `.game` mit mehreren Einträgen in seiner `games.dat` -- eine
|
|
Sammlung oder ein Spiel in zwei Sprachen -- ist ein einziger Download, und jeder Eintrag darin
|
|
trägt dieselbe `GAME_ID`. Der Online-Dienst führt jeden Eintrag als eigenes Spiel und nimmt kein
|
|
`.game` an, dessen Einträge sich über ihre ID uneinig sind oder bei dem sie in einem Eintrag fehlt.
|
|
Ihre Punkte bleiben trotzdem getrennt: siehe <<onlinescores,Online-Highscores>>.
|
|
|
|
*Rückgabe:* String oder `nil`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<onlinescores,Online-Highscores>>, <<gamesdat,games.dat>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Bestenliste nur anbieten, wenn dieses Spiel eine Identität hat, unter der es gewertet wird.
|
|
if singeGetGameId() then
|
|
showLeaderboardButton()
|
|
end
|
|
----
|
|
|
|
[#singegetheight]
|
|
==== singeGetHeight
|
|
|
|
[source,text]
|
|
----
|
|
height = singeGetHeight()
|
|
----
|
|
|
|
Gibt die Höhe des Singe-Fensters in Bildschirmpixeln zurück, nicht in Overlay-Einheiten, so wie das Fenster im Moment des Aufrufs ist. Zum Zeichnen nimm `overlayGetHeight`; dies ist für einen Einstellungsbildschirm, der die Anzeige meldet, oder um zu entscheiden, wie viel Detail sich eine Szene leisten kann.
|
|
|
|
*Rückgabe:* ganzzahlige Pixel.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<singegetwidth,singeGetWidth>>, <<overlaygetheight,overlayGetHeight>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Anzeige im Service-Menü melden.
|
|
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()
|
|
----
|
|
|
|
Gibt das Pausenflag der Engine zurück: `true`, solange das Spiel durch die Pausentaste oder durch `singeSetPauseFlag` pausiert ist. Das ist unabhängig davon, ob die Disc auf einem angehaltenen Frame steht (`discGetState`); lies es, um zu wissen, ob eine Pause absichtlich angefordert wurde. Solange die Pausentaste das Spiel eingefroren hält, läuft kein Callback, sodass das Flag aus einem Callback heraus nur nach dem eigenen `singeSetPauseFlag(true)` des Skripts als `true` zu sehen ist.
|
|
|
|
*Rückgabe:* Boolean.
|
|
|
|
*Seit:* 1.x (RDG)
|
|
*Siehe auch:* <<singesetpauseflag,singeSetPauseFlag>>, <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Timer stehen still, solange die eigene Pause des Spiels aktiv ist.
|
|
function onOverlayUpdate()
|
|
if not singeGetPauseFlag() then
|
|
elapsed = elapsed + 1
|
|
updateEnemies()
|
|
end
|
|
drawFrame()
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#singegetscriptpath]
|
|
==== singeGetScriptPath
|
|
|
|
[source,text]
|
|
----
|
|
path = singeGetScriptPath()
|
|
----
|
|
|
|
Gibt den Pfad der laufenden Skriptdatei zurück, so wie die Engine ihn aufgelöst hat. `Framework.singe` leitet aus diesem Wert die globale Variable `DIR` ab, den Ordner des Skripts mit abschließendem Trenner, und mit `DIR` bauen die meisten Spiele die Pfade zu ihren Assets.
|
|
|
|
*Rückgabe:* String-Pfad.
|
|
|
|
*Seit:* 1.15 (RDG)
|
|
*Siehe auch:* <<singegetdatapath,singeGetDataPath>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- So leitet Framework.singe DIR ab; Spiele laden dann relativ dazu.
|
|
DIR = singeGetScriptPath():match("(.*[/\\])") or "./"
|
|
titleSprite = spriteLoad(DIR .. "images/title.png")
|
|
----
|
|
|
|
[#singegetsysteminfo]
|
|
==== singeGetSystemInfo
|
|
|
|
[source,text]
|
|
----
|
|
info = singeGetSystemInfo()
|
|
----
|
|
|
|
Alles, was der Kopf des Trace über diesen Rechner und diesen Lauf weiß, in einer
|
|
Tabelle. Sie ist für einen Service-Bildschirm und für Fehlerberichte gedacht: die
|
|
Antwort auf „worauf lief es“, ohne den Spieler zu bitten, ein Log zu suchen.
|
|
|
|
*Rückgabe:* eine Tabelle mit diesen Feldern, alle Strings außer den vier Größen:
|
|
|
|
* `version` -- die Version der Engine, wie `SINGE_VERSION_STRING`.
|
|
* `os` -- Name und Release des Betriebssystems sowie die Architektur.
|
|
* `cpu` -- der Prozessor, seine Kernzahl und der Speicher in Megabyte.
|
|
* `renderer` -- der Name des verwendeten 2D-Renderers von SDL.
|
|
* `gpu` -- der SDL_GPU-Treiber hinter der 3D-Szene oder ein Hinweis, dass 3D
|
|
nicht verfügbar ist.
|
|
* `decoder` -- welche Hardware-Videodecoder der Build versuchen wird, in Worten.
|
|
* `audio` -- die einkompilierten Audioformate, durch Kommas getrennt.
|
|
* `soundFont` -- der SoundFont, den die MIDI-Wiedergabe verwendet, oder warum es keinen gibt.
|
|
* `midi` -- die offenen MIDI-Ports oder ein Hinweis, dass keine angefordert wurden.
|
|
* `dataPath` -- der Datenordner dieses Spiels.
|
|
* `dataRoot` -- der Datenordner, der der Engine übergeben wurde, direkt über `dataPath`,
|
|
unter dem jedes Spiel auf dem Rechner schreibt.
|
|
* `platform` -- der Plattformteil des Release-Namens, etwa `Linux-x86_64`:
|
|
welches Release diese ausführbare Datei ist und nach welchem man den Dienst fragt.
|
|
* `executable` -- der absolute Pfad der laufenden ausführbaren Datei.
|
|
* `windowWidth`, `windowHeight` -- Ganzzahlen, das Fenster in Pixeln.
|
|
* `canvasWidth`, `canvasHeight` -- Ganzzahlen, die Zeichenfläche in Pixeln.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<singegetdatapath,singeGetDataPath>>, xref:Manual.adoc#servicetools[Die Service-Tools]
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Diagnosebildschirm auf eine Taste.
|
|
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()
|
|
----
|
|
|
|
Gibt die Zeit der Uhr in Millisekunden seit dem Start der Engine zurück. Es ist die Uhr für Timer, Entprellung und Animation, und sie läuft weiter, während das Spiel durch die Pausentaste eingefroren ist; ein Timer, der pausierte Zeit nicht mitzählen darf, sollte also `singeGetPauseFlag` lesen oder ab dem Moment des Auftauens messen. Luas `os.clock()` misst normalerweise Prozessorzeit, die stillsteht, während die Engine untätig ist; Singe ersetzt sie durch echte Sekunden seit dem Start der Engine, sodass bestehende Spiele, die sie als Uhr benutzt haben, weiter funktionieren.
|
|
|
|
*Rückgabe:* ganzzahlige Millisekunden.
|
|
|
|
*Seit:* 3.00
|
|
*Siehe auch:* <<singegetpauseflag,singeGetPauseFlag>>, <<debugprint,debugPrint>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Aufforderung „insert coin“ zweimal pro Sekunde blinken lassen.
|
|
function drawAttractPrompt()
|
|
local phase = (singeGetTicks() // 250) % 2
|
|
if phase == 0 then
|
|
fontPrint(120, 200, "INSERT COIN")
|
|
end
|
|
end
|
|
----
|
|
|
|
[#singegetwidth]
|
|
==== singeGetWidth
|
|
|
|
[source,text]
|
|
----
|
|
width = singeGetWidth()
|
|
----
|
|
|
|
Gibt die Breite des Singe-Fensters in Bildschirmpixeln zurück, das Gegenstück zu `singeGetHeight`. Zum Zeichnen auf das Overlay dient stattdessen `overlayGetWidth`.
|
|
|
|
*Rückgabe:* ganzzahlige Pixel.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<singegetheight,singeGetHeight>>, <<overlaygetwidth,overlayGetWidth>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Texturbudget nach der Fenstergröße wählen.
|
|
local pixels = singeGetWidth() * singeGetHeight()
|
|
if pixels > 1920 * 1080 then
|
|
textureSet = "high"
|
|
else
|
|
textureSet = "low"
|
|
end
|
|
----
|
|
|
|
[#singehasgpu]
|
|
==== singeHasGpu
|
|
|
|
[source,text]
|
|
----
|
|
present = singeHasGpu()
|
|
----
|
|
|
|
Ob das GPU-Gerät gestartet ist. Alles, was darüber gezeichnet wird, ist ohne
|
|
eines nicht verfügbar: die 3D-Szene, 3D-Partikel und jedes GUI-Dokument.
|
|
2D-Partikel werden mit dem Overlay gezeichnet und brauchen kein Gerät. Frag dies
|
|
ab, bevor du etwas davon anforderst, denn <<guinew,guiNew>> und die
|
|
Szenenaufrufe beenden das Skript, statt `false` zu antworten -- es gibt keine
|
|
Möglichkeit, es zu versuchen und sich davon zu erholen.
|
|
|
|
Einem Rechner kann eines fehlen, weil er gar keinen Vulkan-, Direct3D-12- oder
|
|
Metal-Treiber hat oder weil `--nogpu` angegeben wurde. Das mitgelieferte Menü
|
|
ruft dies beim Start auf und wählt anhand der Antwort einen Renderer (siehe xref:Manual.adoc#menurenderers[Die zwei Renderer]).
|
|
|
|
*Rückgabe:* Boolean; wahr, wenn ein GPU-Gerät vorhanden ist.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<guinew,guiNew>>, <<singegetsysteminfo,singeGetSystemInfo>>, <<sceneperformance,Leistung und Anforderungen>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Spiel mit einem 3D-Titelbildschirm und einem flachen als Rückfall.
|
|
if singeHasGpu() then
|
|
buildSceneTitle()
|
|
else
|
|
buildSpriteTitle()
|
|
end
|
|
----
|
|
|
|
[#singeisrecording]
|
|
==== singeIsRecording
|
|
|
|
[source,text]
|
|
----
|
|
recording = singeIsRecording()
|
|
----
|
|
|
|
Ob eine Aufnahme läuft oder angefordert wurde und mit dem nächsten gezeichneten Frame beginnt. Siehe <<singerecordstart,singeRecordStart>>.
|
|
|
|
*Rückgabe:* Boolean.
|
|
|
|
*Seit:* 3.00
|
|
*Siehe auch:* <<singerecordstart,singeRecordStart>>, <<singerecordstop,singeRecordStop>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein roter Punkt in der Ecke des eigenen Pausenbildschirms des Spiels, solange es aufgenommen wird.
|
|
if singeIsRecording() then
|
|
overlayCircle(overlayGetWidth() - 12, 12, 5)
|
|
end
|
|
----
|
|
|
|
[#singepack]
|
|
==== singePack
|
|
|
|
[source,text]
|
|
----
|
|
result = singePack(directory, database)
|
|
----
|
|
|
|
Packt das Spiel in `directory` in die Einzeldatei-Datenbank `database`, genau wie `singe --pack` es von der Befehlszeile tut, und gibt `true` zurück, wenn es gelungen ist. Der Ordner muss der eigene Ordner eines Spiels sein: einer, der keinen Support-Ordner `Singe/`, keinen Launcher und nichts anderes enthält, was der Packer ablehnt, und die Gründe für eine Ablehnung werden wie bei der Befehlszeile auf der Konsole ausgegeben. So kann ein Autorenwerkzeug ein Spiel veröffentlichen, ohne die Engine zu verlassen.
|
|
|
|
*Parameter:*
|
|
|
|
* `directory` -- der Ordner des Spiels, so wie der Packer ihn nimmt.
|
|
* `database` -- die zu schreibende `.game`-Datei; sie wird ersetzt, falls sie existiert.
|
|
|
|
*Rückgabe:* `true` bei Erfolg, sonst `false`.
|
|
|
|
*Seit:* 3.00
|
|
*Siehe auch:* <<singlefile,Spiele in einer Datei>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Spiel neben seinem Ordner veröffentlichen und Bescheid geben.
|
|
if singePack(singeGetDataPath() .. "CoinRun", singeGetDataPath() .. "CoinRun.game") then
|
|
debugPrint("packed")
|
|
end
|
|
----
|
|
|
|
[#singequit]
|
|
==== singeQuit
|
|
|
|
[source,text]
|
|
----
|
|
singeQuit()
|
|
----
|
|
|
|
Beendet das laufende Skript am Ende dieses Frames: Die Spielschleife hält an, `onShutdown` läuft, und die Engine geht zum nächsten wartenden Skript über oder beendet sich, wenn es keines gibt. Ein aus dem Menü mit `scriptPush` gestartetes Spiel kehrt deshalb zum Menü zurück, das von vorn beginnt. Im Thread-Modell erledigt das die Rückkehr aus `singeMain` für dich. Der Schalter, der in `controls.cfg` auf `INPUT_QUIT` gelegt ist, und das Schließen des Fensters beenden das Skript auf dieselbe Weise. Ruf es nicht aus `onShutdown` auf, das schon der Weg nach draußen ist.
|
|
|
|
*Seit:* 1.x (RDG)
|
|
*Siehe auch:* <<onshutdown,onShutdown>>, <<singereload,singeReload>>, <<scriptpush,scriptPush>>
|
|
|
|
.Beispiel
|
|
[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)
|
|
----
|
|
|
|
Beendet die Engine wie `singeQuit` und startet, sobald alles abgebaut ist, die ausführbare Datei unter `path` an ihrer Stelle mit denselben Argumenten. So bringt das Tool Engine Update des Menüs einen Automaten auf ein neueres Singe, das es neben das laufende gelegt hat: Die neue Datei wird ausführbar gemacht und gestartet, und unter Windows, das ein laufendes Image nicht ersetzt, wird sie neben diesem unter ihrem eigenen Namen gestartet. Die Datei muss bereits existieren; andernfalls antwortet der Aufruf `false`, ohne etwas zu tun. Hier wird nichts entpackt oder kopiert: Das neue Singe entpackt seine eigenen Support-Dateien beim ersten Lauf.
|
|
|
|
*Parameter:*
|
|
|
|
* `path` -- die zu startende ausführbare Datei.
|
|
|
|
*Rückgabe:* `true`, wenn der Neustart eingerichtet ist, `false`, wenn die Datei nicht da ist.
|
|
|
|
*Seit:* 3.00
|
|
*Siehe auch:* <<singequit,singeQuit>>, <<singegetsysteminfo,singeGetSystemInfo>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein neueres Singe wurde heruntergeladen und geprüft; an es übergeben.
|
|
local folder = singeGetSystemInfo().executable:match("^(.*[/\\])")
|
|
singeRelaunch(folder .. "Singe-v3.01-Linux-x86_64")
|
|
----
|
|
|
|
[#singerecordstart]
|
|
==== singeRecordStart
|
|
|
|
[source,text]
|
|
----
|
|
singeRecordStart([name])
|
|
----
|
|
|
|
Beginnt ab dem nächsten gezeichneten Frame, das Spiel aufzunehmen, Bild und Ton zusammen, als MP4 im Datenordner des Spiels (siehe `singeGetDataPath`). Ohne Namen heißen die Dateien `singe000.mp4` aufwärts, wie bei Screenshots; mit einem Namen geht die Aufnahme in diese Datei, wobei `.mp4` angehängt wird, wenn der Name keine Endung hat, und eine Datei dieses Namens ersetzt wird, und ein Name, der einen Ordner enthält, bricht das Skript ab. Ein Aufruf während einer laufenden Aufnahme wird ignoriert. Der Schalter, der in `controls.cfg` auf `INPUT_RECORD` gelegt ist, standardmäßig `F10`, startet und stoppt dieselben Aufnahmen.
|
|
|
|
Das Bild ist das ganze Fenster, wie es gezeigt wird, Letterbox eingeschlossen, in Fenstergröße, genau das, was ein Screenshot erfasst; der Ton ist die fertige Mischung, Musik, Effekte und Video-Tonspuren zusammen. Beides wird in einem eigenen Thread codiert, sodass das Spiel für das Zurückkopieren jedes Frames von der GPU bezahlt und für wenig sonst. Das Bild ist H.264, wo der Rechner einen Hardware-Encoder hat, den Singe benutzen kann -- VA-API unter Linux, VideoToolbox unter macOS, Media Foundation unter Windows, V4L2 auf den ARM-Boards, die einen haben -- und überall sonst MPEG-4 aus dem eigenen Encoder von FFmpeg; der Ton ist AAC. Der Trace nennt den Encoder, wenn die Aufnahme beginnt, und zählt beim Stoppen alle verworfenen Frames.
|
|
|
|
Im Spiel läuft die Aufnahme mit 60 Frames pro Sekunde nach der echten Uhr. Ein Frame, der kommt, während der Encoder noch beschäftigt ist, wird aus der Aufnahme verworfen, statt das Spiel warten zu lassen, und der Ton wird um die Warteschlange des Audiogeräts nach hinten verschoben, sodass beides so zusammenpasst wie auf dem Bildschirm. Unter `--deterministic` läuft die Aufnahme stattdessen mit einem Frame pro Taktschritt, und die Engine wartet auf den Encoder, statt etwas zu verwerfen; mit `--record` gestartet, mischt ein deterministischer Lauf seinen Ton außerdem Schritt für Schritt in den Speicher, sodass die Aufnahme Frame für Frame und Sample für Sample exakt ist.
|
|
|
|
*Parameter:*
|
|
|
|
* `name` -- optionaler String; ein Dateiname, kein Pfad.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00
|
|
*Siehe auch:* <<singerecordstop,singeRecordStop>>, <<singeisrecording,singeIsRecording>>, <<singescreenshot,singeScreenshot>>, <<deterministic,Deterministischer Testmodus>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Der Attract-Modus nimmt seinen eigenen Trailer auf: ein Durchlauf, dann Schluss.
|
|
function attractBegin()
|
|
singeRecordStart("trailer")
|
|
end
|
|
|
|
function attractEnd()
|
|
singeRecordStop()
|
|
end
|
|
----
|
|
|
|
[#singerecordstop]
|
|
==== singeRecordStop
|
|
|
|
[source,text]
|
|
----
|
|
singeRecordStop()
|
|
----
|
|
|
|
Schließt die Aufnahme beim nächsten gezeichneten Frame ab: Was noch wartet, wird codiert und die Datei geschlossen, sodass sie von Anfang bis Ende abspielbar ist. Wird ignoriert, wenn nichts aufgenommen wird. Eine Aufnahme, die beim Ende des Spiels noch läuft, wird auf dieselbe Weise abgeschlossen.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00
|
|
*Siehe auch:* <<singerecordstart,singeRecordStart>>, <<singeisrecording,singeIsRecording>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Runde des Spiels aufnehmen: von der ersten Münze bis Game Over.
|
|
function onGameOver()
|
|
singeRecordStop()
|
|
end
|
|
----
|
|
|
|
[#singereload]
|
|
==== singeReload
|
|
|
|
[source,text]
|
|
----
|
|
singeReload()
|
|
----
|
|
|
|
Führt das Spiel am Ende dieses Frames erneut von seinem Skript aus, wie es `F5` und eine geänderte Datei unter `--reload` tun; dieser Aufruf funktioniert auch ohne die Option. Jeder Sound hört auf, der Lua-Zustand wird verworfen, ohne `onShutdown` aufzurufen, alles, was das Skript geladen hat, wird freigegeben, Szene, Physik (einschließlich ihrer Debug-Zeichnung), Partikel und Navigation werden zurückgesetzt, das Overlay kehrt zu seiner Standardauflösung zurück, und Lautstärke der Soundeffekte, Mausmodus, Mausfang, Tastaturmodus, Pausentaste, Zuhörer und Pausenflag kehren zu ihren Standards zurück. Tasten und Knöpfe, die über das Neuladen hinweg gehalten werden, werden bis zum Loslassen ignoriert, wie bei einem frischen Start, und die Disc wird angehalten auf ihrem ersten Frame geparkt, mit der Standard-Tonspur ausgewählt, wie beim Start. Die Engine selbst (das Fenster, die Controller, die Mäuse) bleibt, wie sie ist. Es passt zum Neustart-Eintrag eines Debug-Menüs oder zu einem Level-Editor, der neu lädt, was er gerade gespeichert hat.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<singequit,singeQuit>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Editor: das Level speichern, dann das Spiel damit neu starten.
|
|
function onKeyPressed(keysym, scancode)
|
|
if scancode == SCANCODE.F9.value then
|
|
saveLevel(singeGetDataPath() .. "level.lua")
|
|
singeReload()
|
|
end
|
|
end
|
|
----
|
|
|
|
[#singesavegeometry]
|
|
==== singeSaveGeometry
|
|
|
|
[source,text]
|
|
----
|
|
singeSaveGeometry()
|
|
----
|
|
|
|
Bewahrt die aktuelle Skalierung, Verschiebung und Drehung des Bildes für diesen
|
|
Rechner auf. Sie kommen in `machine.cfg` im obersten Datenordner, neben die
|
|
Audioverzögerung, und die Engine wendet sie auf jedes Spiel an, das sie danach
|
|
startet, aus dem Menü oder von der Befehlszeile, und über `singeReload` hinweg.
|
|
Eine tatsächlich auf der Befehlszeile eingetippte Option gewinnt:
|
|
`--scalefactor` übersteuert die gespeicherte Skalierung, `--shiftx` oder
|
|
`--shifty` die gespeicherte Verschiebung, `--rotate` die gespeicherte Drehung.
|
|
Die Datei wird ganz geschrieben, sodass die Audioverzögerung erhalten bleibt,
|
|
wie sie ist.
|
|
|
|
Das ist eine Einstellung des Automaten, keine des Spiels: So wird ein Monitor
|
|
einmal ausgerichtet statt einmal pro Spiel. Das Tool Display im mitgelieferten
|
|
Menü ist genau dieser Aufruf hinter Knopf 1.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<vldpsetscale,vldpSetScale>>, <<vldpsetshift,vldpSetShift>>, <<vldpsetrotate,vldpSetRotate>>, <<singesetaudiocalibration,singeSetAudioCalibration>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Der Betreiber ist mit dem Bild zufrieden: für den Automaten behalten.
|
|
function onInputPressed(input)
|
|
if input == SWITCH_BUTTON1 then
|
|
singeSaveGeometry()
|
|
end
|
|
end
|
|
----
|
|
|
|
|
|
[#singesavetier]
|
|
==== singeSaveTier
|
|
|
|
[source,text]
|
|
----
|
|
singeSaveTier(tier)
|
|
----
|
|
|
|
Bewahrt die Leistungsklasse dieses Rechners, 0 bis 3, in `machine.cfg` neben
|
|
der Audioverzögerung und der Geometrie auf, wo
|
|
<<singecapabilities,singeCapabilities>> sie meldet und das Menü sie mit dem
|
|
`TIER` jedes Spiels vergleicht. Das mitgelieferte Menü misst die
|
|
Leistungsklasse selbst, daran, wie lange sein 3D-Hintergrund zum Zeichnen
|
|
braucht (<<scenemeasure,sceneMeasure>>), und speichert sie nur, wenn sie sich
|
|
ändert; niemand fragt den Spieler.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<singecapabilities,singeCapabilities>>, <<singesavegeometry,singeSaveGeometry>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Rechner ganz ohne 3D hat Leistungsklasse 0.
|
|
if not singeHasGpu() then
|
|
singeSaveTier(0)
|
|
end
|
|
----
|
|
|
|
[#singescreenshot]
|
|
==== singeScreenshot
|
|
|
|
[source,text]
|
|
----
|
|
singeScreenshot()
|
|
singeScreenshot(name)
|
|
----
|
|
|
|
Fordert einen Screenshot des ganzen Fensters an, Letterbox eingeschlossen, aufgenommen nach dem Zeichnen des nächsten Frames und als PNG im Datenordner des Spiels gespeichert (siehe `singeGetDataPath`). Ohne Namen heißen die Dateien `singe000.png` aufwärts; jeder Skriptlauf sucht ab null nach dem ersten freien Namen, und spätere Aufnahmen machen nach der zuletzt gespeicherten weiter. Mit einem Namen geht die Aufnahme in diese Datei, wobei `.png` angehängt wird, wenn der Name keine Endung hat, und eine Datei dieses Namens ersetzt wird; ein Name, der einen Ordner enthält, bricht das Skript ab. Den Screenshot zu benennen ist für Skripte gedacht, die absichtlich Bilder erzeugen, etwa die, die die Anleitungen eines Buches bebildern, damit ein erneuter Lauf das Bild ersetzt, statt eines hinzuzufügen. Der Aufruf erzwingt ein Neuzeichnen, sodass die Aufnahme auch gemacht wird, während die Anzeige ruht. Der Schalter, der in `controls.cfg` auf `INPUT_SCREENSHOT` gelegt ist, tut dasselbe.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<singegetdatapath,singeGetDataPath>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Testskript hält ein paar Frames fest, während es durch die Szene fliegt.
|
|
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)
|
|
----
|
|
|
|
Setzt die Audioverzögerung des Rechners, wendet sie sofort an und schreibt sie in `machine.cfg` im obersten Datenordner, sodass jedes Spiel auf dem Rechner sie übernimmt. Der Kalibrierbildschirm des Menüs ruft es auf; ein Spiel mit eigenem Service-Menü darf das auch. Der Wert ist positiv, wenn der Ton später zu hören ist, als das Gerät meldet; die Engine verzögert die Videoanzeige um so viel. Werte außerhalb von `-1000` bis `1000` brechen das Skript ab. Kann `machine.cfg` nicht geschrieben werden, gilt der Wert für diesen Lauf trotzdem, und eine Meldung geht an die Konsole.
|
|
|
|
*Parameter:*
|
|
|
|
* `milliseconds` -- Ganzzahl, `-1000` bis `1000`, positiv, wenn der Ton zu spät kommt.
|
|
|
|
*Seit:* 3.00
|
|
*Siehe auch:* <<singegetaudiocalibration,singeGetAudioCalibration>>, <<singesetaudiodelay,singeSetAudioDelay>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Menu.singe: die vom Spieler eingestellte Kalibrierung behalten oder verwerfen.
|
|
function calibrationEnd(save)
|
|
if save then
|
|
singeSetAudioCalibration(CAL_VALUE)
|
|
end
|
|
CALIBRATING = false
|
|
end
|
|
----
|
|
|
|
[#singesetaudiodelay]
|
|
==== singeSetAudioDelay
|
|
|
|
[source,text]
|
|
----
|
|
singeSetAudioDelay(milliseconds)
|
|
----
|
|
|
|
Setzt die Audioverzögerung des Spiels für diesen Lauf: wie viel später (positiv) oder früher (negativ) der Ton zu hören ist, als die Engine messen kann, sodass sich die Videoanzeige passend verschiebt. Sie gilt für die Disc und für jedes Video, das das Skript abspielt, zusätzlich zur Kalibrierung des Rechners. Nichts wird gespeichert; ein Spiel, das die Einstellung in einem Service-Menü anbietet, speichert den Wert mit seinen anderen Einstellungen und ruft dies beim Start erneut auf. Werte außerhalb von `-1000` bis `1000` brechen das Skript ab.
|
|
|
|
*Parameter:*
|
|
|
|
* `milliseconds` -- Ganzzahl, `-1000` bis `1000`, positiv, wenn der Ton zu spät kommt.
|
|
|
|
*Seit:* 3.00
|
|
*Siehe auch:* <<singegetaudiodelay,singeGetAudioDelay>>, <<singesetaudiocalibration,singeSetAudioCalibration>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Service-Menü: den Versatz ein wenig verschieben und merken.
|
|
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)
|
|
----
|
|
|
|
Setzt den Titel des Singe-Fensters. Ruf es einmal beim Start mit dem Anzeigenamen des Spiels auf; nichts anderes verwendet den Wert. Das Argument muss ein String oder eine Zahl sein.
|
|
|
|
*Parameter:*
|
|
|
|
* `title` -- der Fenstertitel.
|
|
|
|
*Seit:* 1.15 (RDG)
|
|
*Siehe auch:* <<singeversion,singeVersion>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
singeSetGameName("Asteroid Patrol")
|
|
keyboardSetMode(MODE_NORMAL)
|
|
hudFont = fontLoad(DIR .. "fonts/FreeSansBold.ttf", 18)
|
|
----
|
|
|
|
[#singesetpauseflag]
|
|
==== singeSetPauseFlag
|
|
|
|
[source,text]
|
|
----
|
|
singeSetPauseFlag(paused)
|
|
----
|
|
|
|
Setzt das Pausenflag der Engine aus dem Skript. `true` hält die Disc an, falls sie läuft, sowie jedes geladene Video und jeden Soundeffekt-Kanal und merkt sich, welche liefen; `false` setzt genau diese fort. Anders als die Pausentaste friert dies das Skript nicht ein: Callbacks werden weiter ausgelöst, sodass das Skript sein Pausenmenü zeichnen und entscheiden kann, wann es das Flag löscht. Spiele, die ihre eigene Pause verwalten, sollten außerdem mit `singeSetPauseKeyEnabled(false)` die Pausentaste übernehmen, denn die Taste der Engine schaltet dasselbe Flag um und würde sonst gegen das Skript arbeiten. Ein Argument, das kein Boolean ist, bricht das Skript ab.
|
|
|
|
*Parameter:*
|
|
|
|
* `paused` -- Boolean.
|
|
|
|
*Seit:* 1.x (RDG)
|
|
*Siehe auch:* <<singegetpauseflag,singeGetPauseFlag>>, <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>
|
|
|
|
.Beispiel
|
|
[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)
|
|
----
|
|
|
|
Legt fest, wem die Pausentaste gehört, die in `controls.cfg` auf `INPUT_PAUSE` gelegt ist. Solange sie aktiviert ist (der Standard, bei jedem Neuladen wiederhergestellt), gehört sie der Engine: Ein Druck auf die Taste friert das Spiel vollständig ein (siehe <<pausing,Pausieren>>), und in `MODE_NORMAL` erhält das Skript nie `SWITCH_PAUSE`. Solange sie deaktiviert ist, ignoriert die Engine die Taste, und `SWITCH_PAUSE` erreicht `onInputPressed` und `onInputReleased` wie jeder andere Schalter, sodass das Spiel mit `singeSetPauseFlag` seine eigene Pause betreiben kann. Ein Argument, das kein Boolean ist, bricht das Skript ab.
|
|
|
|
*Parameter:*
|
|
|
|
* `enabled` -- Boolean.
|
|
|
|
*Seit:* 3.00
|
|
*Siehe auch:* <<singesetpauseflag,singeSetPauseFlag>>, <<singegetpauseflag,singeGetPauseFlag>>, <<singedisablepausekey,singeDisablePauseKey>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Engine darf während des Spiels pausieren, aber nicht, während der Spieler seine Initialen eingibt.
|
|
function beginInitials()
|
|
singeSetPauseKeyEnabled(false)
|
|
keyboardSetMode(MODE_FULL)
|
|
end
|
|
|
|
function endInitials()
|
|
keyboardSetMode(MODE_NORMAL)
|
|
singeSetPauseKeyEnabled(true)
|
|
end
|
|
----
|
|
|
|
[#singesetquitkeyenabled]
|
|
==== singeSetQuitKeyEnabled
|
|
|
|
[source,text]
|
|
----
|
|
singeSetQuitKeyEnabled(enabled)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Legt fest, ob sich die Engine beim Schalter beendet, der in `controls.cfg` auf `INPUT_QUIT` gelegt ist, ob von der Tastatur oder einem Controller. Ist es aktiviert (der Standard, bei jedem Neuladen wiederhergestellt), beendet ein Druck darauf das Spiel. Ist es deaktiviert, ignoriert die Engine den Schalter, sodass ein Spiel erst nachfragen oder speichern kann; das Skript erhält `SWITCH_QUIT` in jedem Fall über `onInputPressed` und `onInputReleased`, und `singeQuit` beendet das Spiel weiterhin. Der Schließen-Knopf des Fensters beendet immer. `Framework.singe` definiert `keyboardCatchQuit(catch)`, den Namen von Hypseus Singe für dieselbe Einstellung mit umgekehrtem Sinn. Ein Argument, das kein Boolean ist, bricht das Skript ab.
|
|
|
|
*Parameter:*
|
|
|
|
* `enabled` -- Boolean.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<keyboardcatchquit,keyboardCatchQuit>>, <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>, <<singequit,singeQuit>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Nachfragen, bevor ein laufendes Spiel verlassen wird.
|
|
singeSetQuitKeyEnabled(false)
|
|
|
|
function onInputPressed(what)
|
|
if what == SWITCH_QUIT then
|
|
if confirmQuit() then
|
|
singeQuit()
|
|
end
|
|
end
|
|
end
|
|
----
|
|
|
|
[#singeversion]
|
|
==== singeVersion
|
|
|
|
[source,text]
|
|
----
|
|
version = singeVersion()
|
|
----
|
|
|
|
Gibt die Version der Engine als Zahl zurück, Hauptversion plus Nebenversion als Bruchteil, sodass `3.00` als `3` zurückkommt. Vergleich sie mit `<` und `>=`, um eine zu alte Engine abzulehnen oder um Funktionen herum zu verzweigen; `SINGE_VERSION_MAJOR`, `SINGE_VERSION_MINOR` und `SINGE_VERSION_STRING` werden außerdem als globale Variablen gesetzt, für Skripte, die die Teile oder den Text wollen.
|
|
|
|
*Rückgabe:* Zahl.
|
|
|
|
*Seit:* 1.x (RDG)
|
|
*Siehe auch:* <<singesetgamename,singeSetGameName>>
|
|
|
|
.Beispiel
|
|
[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()
|
|
----
|
|
|
|
Gibt `true` zurück, sofern Singe nicht mit `--nocrosshair` gestartet wurde. Zeichne das Software-Fadenkreuz nur, wenn es `true` zurückgibt; Spieler mit Lightguns, die ihr eigenes Fadenkreuz auf den Bildschirm bringen, schalten das der Software mit dieser Option ab. Der Wert ändert sich während eines Laufs nicht, lies ihn also einmal.
|
|
|
|
*Rückgabe:* Boolean.
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<mousegetposition,mouseGetPosition>>, <<onmousemoved,onMouseMoved>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
drawCrosshair = singeWantsCrosshairs()
|
|
|
|
function onOverlayUpdate()
|
|
overlayClear()
|
|
drawTargets()
|
|
if drawCrosshair then
|
|
spriteDraw(reticle, cursorX, cursorY, true)
|
|
end
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#soft]
|
|
=== Soft Bodies
|
|
|
|
Stoff, Druckkörper und Seile: Meshes, deren Vertices als Partikel simuliert werden (siehe <<physics,Physik>>, Soft Bodies). Ein Soft Body wird über seinen Knoten angesprochen, und ein Knoten ohne einen beendet in jedem Aufruf nach `softNew` das Skript. Positionen sind in Welteinheiten, Masse in Kilogramm, und die Steifigkeitswerte reichen von `0` bis `1`. Die Engine schreibt das Mesh des Knotens nach jedem Schritt aus den Partikeln neu, und die Simulation endet von selbst, wenn der Knoten gelöscht wird oder ein anderes Mesh bekommt. Bis zu 16 Soft Bodies gleichzeitig, mit je bis zu 32 Pins.
|
|
|
|
[#softnew]
|
|
==== softNew
|
|
|
|
[source,text]
|
|
----
|
|
softNew(node, SOFT_CLOTH)
|
|
softNew(node, SOFT_BODY)
|
|
softNew(node, SOFT_ROPE, x, y, z, segments, radius)
|
|
----
|
|
|
|
Bei `SOFT_CLOTH` und `SOFT_BODY` wird das Mesh des Knotens, mit drei oder mehr Vertices, zu den Partikeln: Vertices an derselben Position (innerhalb von `0.0001` Einheiten) werden zu einem verschweißt, dort platziert, wo die Transformation des Knotens sie in der Welt hinsetzt, und die Dreiecke ergeben die Dehnungs-, Scher- und Biege-Constraints. Stoff kollidiert auf beiden Seiten und ist das, was ein offenes Tuch braucht; ein Körper ist ein geschlossenes Mesh, das `softSetPressure` aufblasen kann. Bei `SOFT_ROPE` braucht der Knoten kein Mesh: `segments + 1` Partikel laufen in einer Linie von der Weltposition des Knotens bis zum Weltpunkt, der Knoten bekommt ein neues sechsseitiges Röhren-Mesh mit diesem Radius um sie herum (und behält das Material, das er hatte), und die Partikel kollidieren als Kugeln mit diesem Radius, während Partikel von Stoff und Körpern Kugeln mit Radius `0.03` sind. Jede Art beginnt mit Dehnung `0.9`, Biegung `0.2`, Masse `1` Kilogramm, Dämpfung `0.1` und Druck `0`. Ein Soft Body, der schon auf dem Knoten ist, wird entfernt, bevor der neue erzeugt wird, sodass ein fehlschlagender Aufruf den Knoten ohne einen zurücklässt; das Röhren-Mesh eines Seils geht mit, weshalb der Knoten eines Seils nicht zu Stoff oder einem Körper werden kann (der Aufruf scheitert mangels Mesh). Beendet das Skript, wenn Physik nicht verfügbar ist, die Art unbekannt ist, die Zahl der Argumente nicht zur Art passt, das Mesh fehlt, ein Seil keine Segmente oder keinen Radius hat oder schon 16 Soft Bodies existieren.
|
|
|
|
*Parameter:*
|
|
|
|
* `node` -- der Knoten, der das Mesh trägt (Stoff, Körper), oder der Anfangspunkt des Seils.
|
|
* `x`, `y`, `z` -- das ferne Ende des Seils, in Welteinheiten.
|
|
* `segments` -- wie viele Glieder das Seil hat, `1` oder mehr; ein Dutzend ist reichlich.
|
|
* `radius` -- der Radius des Seils, in Welteinheiten; auch die Größe, mit der seine Partikel kollidieren.
|
|
|
|
*Hinweise:* Ein Mesh, das sich mehrere Knoten teilen, wird für alle neu geschrieben, also gib einem Soft Body ein eigenes Mesh. Geskinnte Meshes können keine Soft Bodies sein.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<softpin,softPin>>, <<softsetstiffness,softSetStiffness>>, <<softsetpressure,softSetPressure>>, <<meshplane,meshPlane>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Flagge an der Stange: ein aufrechtes Gitter, entlang seiner linken Kante angeheftet.
|
|
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) -- Entlang der Stange
|
|
end
|
|
----
|
|
|
|
[#softdelete]
|
|
==== softDelete
|
|
|
|
[source,text]
|
|
----
|
|
softDelete(node)
|
|
----
|
|
|
|
Beendet die Simulation und gibt ihren Körper frei. Der Knoten behält sein Mesh in der Form, in der es zuletzt geschrieben wurde, sodass ein fallengelassenes Tuch über der Kiste drapiert bleibt und ein Seil seine Röhre behält; die ursprüngliche Form des Meshs wird nicht wiederhergestellt. Ein Knoten ohne Soft Body beendet das Skript.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<softnew,softNew>>, <<nodedelete,nodeDelete>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Sobald sich das Tuch über die Statue gelegt hat, es einfrieren und nicht mehr dafür bezahlen.
|
|
function onOverlayUpdate()
|
|
if covering then
|
|
settleFrames = settleFrames + 1
|
|
if settleFrames > 240 then
|
|
softDelete(sheet) -- Der Faltenwurf bleibt, wie er ist
|
|
covering = false
|
|
end
|
|
end
|
|
return OVERLAY_NOT_UPDATED
|
|
end
|
|
----
|
|
|
|
[#softpin]
|
|
==== softPin
|
|
|
|
[source,text]
|
|
----
|
|
softPin(node, x, y, z)
|
|
softPin(node, x, y, z, otherNode)
|
|
----
|
|
|
|
Sucht das Partikel, das dem Weltpunkt am nächsten liegt, wo immer die Partikel gerade sind, und heftet es fest. Allein wird das Partikel festgehalten, wo es ist; mit `otherNode` wird es vor jedem Schritt an die Weltposition dieses Knotens versetzt, sodass ein Seil an einem schwingenden Kran hängt und ein Umhang den Schultern folgt. Der Punkt muss nicht auf dem Mesh liegen, da das nächste Partikel gewinnt, und dasselbe Partikel erneut anzuheften ändert nur, wem es folgt. Nach 32 Pins tut der Aufruf nichts.
|
|
|
|
*Parameter:*
|
|
|
|
* `node` -- der Knoten des Soft Body.
|
|
* `x`, `y`, `z` -- ein Weltpunkt; das ihm nächste Partikel wird angeheftet.
|
|
* `otherNode` -- ein Knoten, dem das Partikel von nun an folgen soll.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<softunpin,softUnpin>>, <<softnew,softNew>>, <<nodegetworldposition,nodeGetWorldPosition>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Seil vom Haken des Krans hinaus und hinunter, das mit dem Kran schwingt.
|
|
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) -- Das obere Ende fährt am Haken mit
|
|
----
|
|
|
|
[#softunpin]
|
|
==== softUnpin
|
|
|
|
[source,text]
|
|
----
|
|
softUnpin(node, x, y, z)
|
|
----
|
|
|
|
Löst das angeheftete Partikel, das dem Weltpunkt am nächsten liegt. Ist dieses Partikel nicht angeheftet, passiert nichts. Der Körper wird um die aktuelle Form herum neu gebaut, was auch jedes Partikel schlagartig anhält, sodass eine losgelassene Flagge aus der Ruhe zu fallen beginnt.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<softpin,softPin>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Ecke des Banners reißt los, wenn die Windböe trifft.
|
|
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)
|
|
----
|
|
|
|
Wie stark sich das Material dagegen wehrt, gedehnt (und, bei Stoff, geschert) und gefaltet zu werden, `0` bis `1`; Werte außerhalb werden begrenzt. `1` ist starr, `0` das Lockerste, was der Solver zulässt. Standards `0.9` und `0.2`. Der Körper wird um die aktuelle Form herum neu gebaut: Die Ruhelängen werden zu den Längen dieses Moments, und jedes Partikel hält an; richte das Material also ein, bevor sich der Stoff zu bewegen beginnt. Ein Seil hat nur Dehnung.
|
|
|
|
*Parameter:*
|
|
|
|
* `node` -- der Knoten des Soft Body.
|
|
* `stretch` -- Widerstand gegen Dehnung, `0` bis `1`; Standard `0.9`.
|
|
* `bend` -- Widerstand gegen Falten, `0` bis `1`; Standard `0.2`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<softsetmass,softSetMass>>, <<softsetdamping,softSetDamping>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Seide, Segeltuch und eine Gummiplane aus demselben Gitter.
|
|
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)
|
|
----
|
|
|
|
Bläst einen `SOFT_BODY` auf: `0` (Standard) ist ein schlaffer Beutel und ein paar Einheiten ein praller Ballon; ein negativer Wert wird zu `0`. Es ändert den laufenden Körper, ohne ihn neu zu bauen, und weckt ihn, sodass ein Ballon aufgepumpt oder abgelassen werden kann, während er hüpft. Stoff und Seil haben kein Volumen zum Aufblasen.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<softnew,softNew>>, <<softsetstiffness,softSetStiffness>>, <<meshsphere,meshSphere>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
softNew(balloon, SOFT_BODY) -- Ein meshSphere von 0.5
|
|
softSetMass(balloon, 0.6)
|
|
softSetPressure(balloon, 8) -- Ein praller Ballon
|
|
|
|
function onOverlayUpdate()
|
|
if punctured then
|
|
pressure = math.max(pressure - 0.1, 0)
|
|
softSetPressure(balloon, pressure) -- Zischt über ein paar Sekunden hinweg leer
|
|
end
|
|
return OVERLAY_NOT_UPDATED
|
|
end
|
|
----
|
|
|
|
[#softsetmass]
|
|
==== softSetMass
|
|
|
|
[source,text]
|
|
----
|
|
softSetMass(node, kilograms)
|
|
----
|
|
|
|
Die Masse des ganzen Körpers, gleichmäßig auf seine Partikel verteilt; Standard `1`, mindestens `0.001`. Angeheftete Partikel wiegen trotzdem nichts. Der Körper wird um die aktuelle Form herum neu gebaut, und jedes Partikel hält an, also setz sie vorab.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<softsetstiffness,softSetStiffness>>, <<softsetdamping,softSetDamping>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein an vier Ecken angeheftetes Tuch fängt eine darauf fallende Kiste auf.
|
|
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) -- Schwer genug, um nicht zu flattern
|
|
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)
|
|
----
|
|
|
|
Wie schnell die Bewegung der Partikel abklingt, `0` aufwärts; Standard `0.1`, und ein negativer Wert wird zu `0`. Höhere Werte beruhigen eine Flagge, die nicht aufhört, Wellen zu schlagen, oder ein Seil, das ewig schwingt. Der Körper wird um die aktuelle Form herum neu gebaut, und jedes Partikel hält an.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<softsetmass,softSetMass>>, <<softsetstiffness,softSetStiffness>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Vorhang in stiller Luft kommt schnell zur Ruhe; derselbe Vorhang am offenen Fenster bewegt sich weiter.
|
|
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]
|
|
=== Sound
|
|
|
|
Soundeffekte sind Audioclips (WAV ist die sichere Wahl), die in den Speicher geladen und über SDL_mixer gemischt werden, getrennt vom eigenen Ton der Disc und vom Ton der `video*`-Assets. `soundLoad` gibt ein Sound-Handle zurück, das die Daten des Clips bezeichnet; `soundPlay` startet diesen Clip auf einem von sechzehn Mixerkanälen und gibt die Kanalnummer zurück, `0` bis `15`, die `soundPause`, `soundResume`, `soundStop`, `soundIsPlaying` und jeder `soundSet*`-Aufruf nehmen. Ein Clip kann auf mehreren Kanälen gleichzeitig spielen, und eine Kanalnummer wird wiederverwendet, sobald der Kanal frei ist; halte die beiden Arten von Nummern also auseinander. Eine Gesamtlautstärke der Effekte (`soundSetVolume`, `0` bis `63`) skaliert jeden Kanal. Ein spielender Kanal kann auch in der 3D-Szene sitzen, einem Knoten folgend oder fest an einem Punkt in Welteinheiten, wo er nach seiner Entfernung vom Zuhörer im Stereobild verteilt und abgeschwächt wird (siehe <<scenes3d,3D-Szenen>>); für 2D-Spiele verteilt `soundSetPan` einen Kanal ohne Szene im Stereobild.
|
|
|
|
[#soundfullstop]
|
|
==== soundFullStop
|
|
|
|
[source,text]
|
|
----
|
|
soundFullStop()
|
|
----
|
|
|
|
Stoppt jeden Effektkanal auf einmal, pausierte eingeschlossen, und gibt sie zur Wiederverwendung frei. Kein Clip wird entladen; die Handles aus `soundLoad` bleiben gültig. Jeder Kanal, der spielte oder pausiert war, meldet sich über `onSoundCompleted`, genau wie nach `soundStop`. Ruf es bei einem Szenenwechsel oder vor einer Zwischensequenz auf, statt jeden gestarteten Kanal zu verfolgen.
|
|
|
|
*Seit:* 1.16
|
|
*Siehe auch:* <<soundstop,soundStop>>, <<onsoundcompleted,onSoundCompleted>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
function enterAttractMode()
|
|
soundFullStop()
|
|
ambientChannel = -1
|
|
engineChannel = -1
|
|
discPlay()
|
|
end
|
|
----
|
|
|
|
[#soundgetposition]
|
|
==== soundGetPosition
|
|
|
|
[source,text]
|
|
----
|
|
x, y, z, gain = soundGetPosition(channel)
|
|
----
|
|
|
|
Meldet, wo ein platzierter Kanal relativ zum Zuhörer sitzt, als Richtung der Länge eins im Bezugssystem des Zuhörers (`x` positiv nach rechts, `y` positiv nach oben, `z` positiv hinter den Zuhörer), und den Lautstärkefaktor, den ihm seine Entfernung gerade gibt, `0` bis `1`. Die Werte werden einmal pro Frame aus dem Zuhörer und dem Knoten oder Punkt des Kanals aufgefrischt. Ein Kanal, der seit seinem Start nicht in der Szene platziert wurde, gibt `0, 0, 0, 1` zurück. Gedacht für Anzeigen im Radarstil und zum Prüfen einer Mischung aus einem Testskript; ein Kanal außerhalb von `0` bis `15` bricht das Skript ab.
|
|
|
|
*Rückgabe:* drei Zahlen für die Richtung und eine für die Abschwächung durch die Entfernung.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<soundsetnode,soundSetNode>>, <<soundsetposition,soundSetPosition>>, <<soundsetrange,soundSetRange>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Einen Pfeil im HUD auf den nächsten hörbaren Gegner richten.
|
|
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()
|
|
----
|
|
|
|
Gibt die Gesamtlautstärke der Effekte zurück, `0` bis `63`. Beim Start spiegelt sie die Effektlautstärke von der Befehlszeile wider; ein Spiel, das eine Lautstärkeeinstellung zeigt, sollte sie also lesen, statt das Maximum anzunehmen.
|
|
|
|
*Rückgabe:* Ganzzahl, `0` bis `63`.
|
|
|
|
*Seit:* 1.16
|
|
*Siehe auch:* <<soundsetvolume,soundSetVolume>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Effekte absenken, während der Erzähler spricht, und danach zurückholen.
|
|
function narratorBegin()
|
|
savedVolume = soundGetVolume()
|
|
soundSetVolume(math.floor(savedVolume / 3))
|
|
narratorChannel = soundPlay(narratorClip)
|
|
end
|
|
----
|
|
|
|
[#soundisplaying]
|
|
==== soundIsPlaying
|
|
|
|
[source,text]
|
|
----
|
|
playing = soundIsPlaying(channel)
|
|
----
|
|
|
|
Prüft, ob ein Kanal gerade spielt. Ein pausierter Kanal meldet `false`, ebenso ein Kanal, dessen Clip zu Ende ist. Ein Kanal außerhalb von `0` bis `15` bricht das Skript ab; ein Kanal, der bloß verstummt ist, nicht.
|
|
|
|
*Rückgabe:* Boolean.
|
|
|
|
*Seit:* 1.16 (RDG)
|
|
*Siehe auch:* <<soundplay,soundPlay>>, <<soundpause,soundPause>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Den Alarm nicht neu starten, solange er noch tönt.
|
|
function raiseAlarm()
|
|
if alarmChannel < 0 or not soundIsPlaying(alarmChannel) then
|
|
alarmChannel = soundPlay(alarmClip)
|
|
end
|
|
end
|
|
----
|
|
|
|
[#soundload]
|
|
==== soundLoad
|
|
|
|
[source,text]
|
|
----
|
|
id = soundLoad(filename)
|
|
----
|
|
|
|
Lädt einen Audioclip in den Speicher und gibt sein Handle zurück. Der Pfad geht durch das virtuelle Dateisystem des Spiels, er funktioniert also aus einem losen Ordner genauso wie aus einem gepackten Spiel. Lade Clips einmal beim Start, nicht in einem Callback, der jeden Frame ausgelöst wird. Eine Datei, die sich nicht öffnen oder decodieren lässt, bricht das Skript ab.
|
|
|
|
*Rückgabe:* ganzzahliges Sound-Handle. Das ist ein Daten-Handle, keine Kanalnummer.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<soundplay,soundPlay>>, <<soundunload,soundUnload>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Clip-Handles, einmal geladen und in onShutdown entladen.
|
|
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)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Baut einen Sound aus Bytes in einem Lua-String statt aus einer Datei. Jedes Format, das <<soundload,`soundLoad`>> liest, wird hier gelesen, und das zurückgegebene Handle wird mit dem Rest der `sound`-Familie genau gleich verwendet.
|
|
|
|
Der String darf Nullbytes enthalten; seine Länge, nicht seine erste Null, entscheidet, wie viel gelesen wird. Die Bytes werden während des Aufrufs decodiert, und der String wird nicht behalten, er darf danach also verworfen werden. Bytes, die kein Audio sind, oder ein leerer String beenden das Skript, wie es eine unlesbare Datei bei `soundLoad` tut.
|
|
|
|
Nutze es für Audio, das ein Spiel selbst erzeugt, entpackt oder entschlüsselt. Audio, das das virtuelle Dateisystem über seinen Namen erreicht, lädt man besser mit `soundLoad`, das nicht die ganze Datei in einem Lua-String hält -- das gilt auch für Audio in einem gepackten `.game`.
|
|
|
|
*Parameter:*
|
|
|
|
* `data` -- String aus Bytes.
|
|
|
|
*Rückgabe:* Zahl, das Sound-Handle.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<soundload,soundLoad>>, <<spriteloaddata,spriteLoadData>>, <<soundplay,soundPlay>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Soundeffekt, den das Spiel aus seinem eigenen Archiv entpackt.
|
|
local bytes = myArchive:extract("beep.wav")
|
|
local beep = soundLoadData(bytes)
|
|
soundPlay(beep)
|
|
----
|
|
|
|
[#soundpause]
|
|
==== soundPause
|
|
|
|
[source,text]
|
|
----
|
|
wasPlaying = soundPause(channel)
|
|
----
|
|
|
|
Pausiert einen Kanal an Ort und Stelle; `soundResume` setzt ihn an derselben Stelle fort. Ein pausierter Kanal gilt weiter als belegt, `soundPlay` gibt ihn also nicht an einen anderen Clip aus, und `soundIsPlaying` meldet für ihn `false`. Einen Kanal zu pausieren, der nicht spielt, bewirkt nichts. Ein Kanal außerhalb von `0` bis `15` bricht das Skript ab.
|
|
|
|
*Rückgabe:* Boolean, `true`, wenn der Kanal im Moment des Aufrufs spielte.
|
|
|
|
*Seit:* 1.16 (RDG)
|
|
*Siehe auch:* <<soundresume,soundResume>>, <<soundisplaying,soundIsPlaying>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Motorgeräusch anhalten, während der Funkspruch läuft.
|
|
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)
|
|
----
|
|
|
|
Startet einen geladenen Clip auf dem ersten freien Kanal und gibt die Nummer dieses Kanals zurück. Ein Kanal gilt als frei, wenn er weder spielt noch pausiert ist. Der neue Kanal beginnt mit der Gesamtlautstärke (oder mit `volume` darunter), mittig, ohne Position in der Szene und mit dem Standardbereich, egal was die vorige Nutzung des Kanals gesetzt hat. Sind alle sechzehn Kanäle belegt, spielt nichts, und `-1` (`SOUND_ERROR_INVALID`) kommt zurück; ein Spiel, das viele sich überlagernde Sounds abfeuert, sollte darauf prüfen. Ein Handle, das nicht geladen ist, bricht das Skript ab.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Sound-Handle aus `soundLoad`.
|
|
* `loops` -- `0` (der Standard) spielt den Clip einmal, eine positive Zahl wiederholt ihn so oft zusätzlich, `-1` wiederholt ihn, bis er gestoppt wird. Alles unter `-1` bricht das Skript ab.
|
|
* `volume` -- die eigene Lautstärke des Kanals, `0` bis `63`, unter der Gesamtlautstärke; der Standard ist `63`, der Clip so laut, wie die Gesamtlautstärke erlaubt. Ein Wert außerhalb dieses Bereichs bricht das Skript ab. `soundSetChannelVolume` ändert sie, während der Clip spielt.
|
|
|
|
*Rückgabe:* ganzzahlige Kanalnummer, `0` bis `15`, oder `-1`, wenn kein Kanal frei ist.
|
|
|
|
*Seit:* 1.x; `volume` seit 3.00.
|
|
*Siehe auch:* <<soundload,soundLoad>>, <<soundstop,soundStop>>, <<soundsetchannelvolume,soundSetChannelVolume>>, <<soundsetnode,soundSetNode>>
|
|
|
|
.Beispiel
|
|
[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)
|
|
----
|
|
|
|
Setzt einen Kanal, den `soundPause` angehalten hat, dort fort, wo er aufgehört hat. Einen Kanal fortzusetzen, der nicht pausiert ist, bewirkt nichts. Ein Kanal außerhalb von `0` bis `15` bricht das Skript ab.
|
|
|
|
*Rückgabe:* Boolean, `true`, wenn der Kanal im Moment des Aufrufs pausiert war.
|
|
|
|
*Seit:* 1.16 (RDG)
|
|
*Siehe auch:* <<soundpause,soundPause>>
|
|
|
|
.Beispiel
|
|
[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)
|
|
----
|
|
|
|
Setzt die eigene Lautstärke eines Kanals, zusätzlich zur Gesamtlautstärke aus `soundSetVolume`: Der Kanal spielt mit der Gesamtlautstärke mal diesem Wert, mal seiner Abschwächung durch die Entfernung, wenn er in der Szene ist. Jeder Kanal beginnt mit voller Lautstärke, wenn `soundPlay` ihn nimmt, sofern `soundPlay` keine Lautstärke bekommen hat, sodass ein leiser Schritt und ein lauter Schuss sich eine Gesamtlautstärke teilen können. Werte außerhalb von `0` bis `63` brechen das Skript ab, ebenso ein Kanal außerhalb von `0` bis `15`.
|
|
|
|
*Parameter:*
|
|
|
|
* `channel` -- Kanalnummer aus `soundPlay`.
|
|
* `volume` -- Ganzzahl, `0` (stumm) bis `63` (so laut wie die Gesamtlautstärke).
|
|
|
|
*Seit:* 3.00
|
|
*Siehe auch:* <<soundplay,soundPlay>>, <<soundsetvolume,soundSetVolume>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Motor, der mit dem Gas lauter wird: ein wiederholter Clip, dessen Kanallautstärke
|
|
-- jeden Frame dem Pedal folgt.
|
|
engine = soundPlay(engineClip, -1, 20)
|
|
|
|
function onOverlayUpdate()
|
|
soundSetChannelVolume(engine, math.floor(20 + throttle * 43))
|
|
end
|
|
----
|
|
|
|
[#soundsetlistener]
|
|
==== soundSetListener
|
|
|
|
[source,text]
|
|
----
|
|
soundSetListener(node)
|
|
soundSetListener()
|
|
----
|
|
|
|
Wählt den Knoten, von dem aus platzierte Kanäle gehört werden, in die Richtung blickend, in die er blickt. Ohne Argument oder mit `nil` ist der Zuhörer die Kamera der Szene, was für ein Spiel in der Ich-Perspektive oder mit fester Kamera richtig ist; ein Third-Person-Spiel nimmt meist lieber den Knoten der Spielfigur, damit Sounds um die Figur herum wandern statt um die Kamera. Wird der Knoten später gelöscht, fällt der Zuhörer auf die Kamera zurück. Ein Knoten-Handle, das nicht existiert, bricht das Skript ab.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<soundsetnode,soundSetNode>>, <<soundsetposition,soundSetPosition>>, <<cameraset,cameraSet>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Verfolgerkamera, aber die Welt vom Auto aus hören.
|
|
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)
|
|
----
|
|
|
|
Hängt einen spielenden Kanal an einen Knoten der Szene, sodass er jeden Frame von dort zu hören ist, wo der Knoten gerade ist: Der Kanal wird im Stereobild in die Richtung des Knotens vom Zuhörer aus verteilt und gemäß `soundSetRange` nach seiner Entfernung abgeschwächt. Ein Aufruf ohne Knoten oder mit `nil` nimmt den Kanal mit voller Gesamtlautstärke wieder aus der Szene. Ein Kanal, dessen Knoten gelöscht wird, fällt von selbst auf einfache Wiedergabe zurück. Die Verbindung hält, bis der Kanal gestoppt oder von `soundPlay` wiederverwendet wird. Ein Kanal außerhalb von `0` bis `15` oder ein Knoten, der nicht existiert, bricht das Skript ab.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<soundsetposition,soundSetPosition>>, <<soundsetrange,soundSetRange>>, <<soundsetlistener,soundSetListener>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Jede Feuerschale knistert von dort, wo sie steht.
|
|
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)
|
|
----
|
|
|
|
Verteilt einen Kanal für Spiele ohne Szene im Stereobild: `-1` ist ganz links, `1` ganz rechts, `0` mittig, und Werte außerhalb dieses Bereichs werden begrenzt. Weg von der Mitte wird nur der gegenüberliegende Lautsprecher leiser; bei `pan = 0.5` spielt der linke Lautsprecher mit halber Verstärkung und der rechte mit voller. Der Aufruf nimmt den Kanal aus der Szene, falls `soundSetNode` oder `soundSetPosition` ihn dort platziert hatte. Ein Kanal außerhalb von `0` bis `15` bricht das Skript ab.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<soundplay,soundPlay>>, <<soundsetnode,soundSetNode>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein vorbeifahrendes Auto danach im Stereobild verteilen, wo es auf dem Overlay ist.
|
|
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)
|
|
----
|
|
|
|
Setzt einen spielenden Kanal an einen festen Punkt in der Szene, in Welteinheiten, und ersetzt jeden Knoten, dem er gefolgt ist. Er wird dann wie ein an einen Knoten gehängter Kanal vom Zuhörer aus im Stereobild verteilt und abgeschwächt. Passt zu einem Sound, der an seinem Platz bleibt, etwa einem Wasserfall oder einem klingelnden Telefon; eine bewegte Quelle geht leichter mit `soundSetNode`. Ein Kanal außerhalb von `0` bis `15` bricht das Skript ab.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<soundsetnode,soundSetNode>>, <<soundsetrange,soundSetRange>>, <<soundgetposition,soundGetPosition>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Der Wasserfall bewegt sich nie, also wird kein Knoten gebraucht.
|
|
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)
|
|
----
|
|
|
|
Legt fest, wie ein platzierter Kanal mit der Entfernung vom Zuhörer leiser wird. Innerhalb von `near` spielt er mit voller Lautstärke; darüber hinaus ist seine Verstärkung `near` geteilt durch die Entfernung; über das letzte Fünftel von `far` blendet er außerdem linear aus und erreicht bei `far` Stille. Die Standards sind `1` und `30` Welteinheiten und werden wiederhergestellt, wann immer `soundPlay` den Kanal wiederverwendet. Ein Flüstern will ein kleines `far`, ein Wasserfall ein großes.
|
|
|
|
*Parameter:*
|
|
|
|
* `channel` -- Kanalnummer aus `soundPlay`.
|
|
* `near` -- Entfernung, innerhalb derer der Kanal volle Lautstärke hat, größer als `0`.
|
|
* `far` -- Entfernung, bei der er stumm ist, größer als `near`. Jedes andere Paar bricht das Skript ab.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<soundsetnode,soundSetNode>>, <<soundsetposition,soundSetPosition>>, <<soundgetposition,soundGetPosition>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Schritte einer Wache sind nur aus der Nähe hörbar; die Sirene trägt weit.
|
|
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)
|
|
----
|
|
|
|
Setzt die Gesamtlautstärke der Effekte und wendet sie sofort auf jeden Kanal an, ob spielend oder pausiert. Die eigene Lautstärke jedes Kanals aus `soundSetChannelVolume` und, bei platzierten Kanälen, die Abschwächung durch die Entfernung wirken zusätzlich. Werte außerhalb von `0` bis `63` brechen das Skript ab.
|
|
|
|
*Parameter:*
|
|
|
|
* `volume` -- Ganzzahl, `0` (stumm) bis `63` (am lautesten).
|
|
|
|
*Seit:* 1.16
|
|
*Siehe auch:* <<soundgetvolume,soundGetVolume>>, <<soundsetchannelvolume,soundSetChannelVolume>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Lautstärkeregler im Service-Menü, gesteuert über die Schalter links und rechts.
|
|
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)
|
|
----
|
|
|
|
Hält einen Kanal sofort an, ob spielend oder pausiert, und gibt ihn zur Wiederverwendung frei. Der Kanal meldet sich über `onSoundCompleted`, genau wie wenn der Clip von selbst endet; ein Skript, das in diesem Callback Sounds aneinanderreiht, sollte also prüfen, ob es den Kanal selbst gestoppt hat. Das nächste `soundPlay` kann dieselbe Nummer wieder ausgeben, verwirf die Nummer also auf der Lua-Seite. Ein Kanal außerhalb von `0` bis `15` bricht das Skript ab.
|
|
|
|
*Rückgabe:* Boolean, `true`, wenn der Kanal im Moment des Aufrufs spielte. Ein pausierter Kanal gibt `false` zurück, wird aber trotzdem gestoppt.
|
|
|
|
*Seit:* 1.x (RDG)
|
|
*Siehe auch:* <<soundfullstop,soundFullStop>>, <<onsoundcompleted,onSoundCompleted>>
|
|
|
|
.Beispiel
|
|
[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)
|
|
----
|
|
|
|
Gibt einen geladenen Clip frei und macht sein Handle ungültig. Der Mixer hält die Daten des Clips am Leben, bis jeder Kanal, der ihn noch spielt, gestoppt hat, sodass das Entladen keinen spielenden Kanal abschneidet. Jede spätere Verwendung des Handles, auch ein weiteres `soundUnload`, bricht das Skript ab. Der übliche Ort dafür ist `onShutdown`.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Sound-Handle aus `soundLoad`, keine Kanalnummer.
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<soundload,soundLoad>>, <<onshutdown,onShutdown>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
function onShutdown()
|
|
soundUnload(shotClip)
|
|
soundUnload(missClip)
|
|
soundUnload(coinClip)
|
|
soundUnload(ambientClip)
|
|
fontUnload(hudFont)
|
|
end
|
|
----
|
|
|
|
[#sprite]
|
|
=== Sprite
|
|
|
|
Sprites sind 2D-Bitmaps, die mit `spriteLoad` von der Festplatte geladen oder mit `fontToSprite` aus Text gerendert und in Overlay-Koordinaten auf das Overlay gezeichnet werden. Jeder `sprite*`-Aufruf nimmt das ganzzahlige Handle als erstes Argument (Singe 2.10 stellte es ans Ende; siehe <<migrating,Umstieg von Singe 2.10>>), und ein Handle, das entladen wurde, beendet das Skript mit einem Fehler. Jedes Format, das SDL_image liest, wird angenommen; ein animiertes GIF oder WEBP mit zwei oder mehr Frames lädt als Animation, die auf Frame `0` steht, angehalten und ohne Wiederholung, bis `spritePlay` und `spriteLoop` etwas anderes sagen. Farbe `0` wird transparent gezeichnet, wie bei Hypseus: Palettenindex `0` oder reines Schwarz, und das schließt deckendes Schwarz in einem Bild mit Alphakanal ein, dessen Alpha ebenfalls beachtet wird. Um das deckende Schwarz eines Bildes zu behalten, lade es nach <<drawtransparent,`drawTransparent(true)`>>. Gib jedes Sprite mit `spriteUnload` frei, meist aus <<onshutdown,onShutdown>>.
|
|
|
|
*Sprite-Sheets.* Hypseus Singe erweitert die Familie um Sprite-Sheets: `spriteLoadFrames` zerlegt einen einzeiligen Streifen gleich großer Frames in eine Animation, die nicht läuft, und `spriteDrawFrame` zeichnet einen gewählten Frame (ab 1 gezählt), ohne die Wiedergabe anzutasten. Ansonsten ist ein Sheet eine gewöhnliche Animation, also nehmen `spriteSetFrame` (ab 0 gezählt), `spritePlay`, `spriteDraw`, `nodeSetSprite` und `emitterSetTexture` es alle an, und `spriteFrameWidth` und `spriteFrameHeight` melden die unskalierte Größe eines Frames. Die mit „Hypseus-Erweiterung“ markierten Einträge gibt es, damit für Hypseus Singe geschriebene Spiele unverändert laufen.
|
|
|
|
[#drawtransparent]
|
|
==== drawTransparent
|
|
|
|
[source,text]
|
|
----
|
|
drawTransparent(boolean)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Entscheidet, ob Farbe 0 auf den danach geladenen Sprites und dem danach gerenderten Text transparent gezeichnet wird. `false`, der Standard, stanzt Farbe 0 aus, wie Hypseus es tut: Palettenindex 0 oder reines Schwarz, und das schließt deckendes Schwarz in einem Bild mit Alphakanal ein, sodass schwarze Schrift oder ein schwarzer Umriss verschwindet. `true` zeichnet sie allein nach ihrem Alphakanal, sodass deckendes Schwarz schwarz bleibt. Bereits geladene Sprites behalten die Einstellung, mit der sie geladen wurden, und jedes Skript beginnt bei `false`.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Schrift des Logos ist deckendes Schwarz: lade es allein nach seinem Alpha, dann zurück
|
|
-- zum üblichen Farbschlüssel für die eigenen Grafiken des Spiels.
|
|
drawTransparent(true)
|
|
logo = spriteLoad("art/logo.png")
|
|
drawTransparent(false)
|
|
----
|
|
|
|
[#spriteblend]
|
|
==== spriteBlend
|
|
|
|
[source,text]
|
|
----
|
|
spriteBlend(boolean, sprite)
|
|
----
|
|
|
|
Hypseus-Erweiterung, angenommen und ignoriert. Sie ist ein Notbehelf für jene Engine: Hypseus lädt Sprites mit ausgeschaltetem Blend-Modus von SDL, und dies schaltet ihn für ein Sprite wieder ein. Singe zeichnet jedes Sprite ohnehin mit seinem Alphakanal, das Blending ist also von vornherein nie aus. Das Sprite-Handle wird trotzdem geprüft, ein falsches ist also weiterhin ein Fehler. Um stattdessen das Licht eines Sprites zu addieren oder mit ihm zu multiplizieren, siehe <<spritesetblend,`spriteSetBlend`>>.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 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)
|
|
----
|
|
|
|
Blittet das Sprite so auf das Overlay, wie es gerade gedreht und skaliert ist. Ruf es aus `onOverlayUpdate` auf. Die ersten beiden Formen zeichnen in der Größe, die `spriteGetWidth` und `spriteGetHeight` melden, mit `(x, y)` als oberer linker Ecke; die letzten beiden strecken dieses Bild in das Rechteck von `(x, y)` bis einschließlich `(x2, y2)`, immer mit Nächster-Nachbar-Abtastung, was auch immer `spriteQuality` sagt. Ist das Sprite eine laufende Animation, rückt das Zeichnen sie zuerst um die echte Zeit weiter, die seit dem vorigen Zeichnen (oder seit `spritePlay`) vergangen ist, durchläuft so viele Frames, wie diese Zeit abdeckt, und zeigt jeden Frame mindestens 10 ms lang, auch wenn die Datei weniger verlangt. Ist der letzte Frame vorüber, springt die Wiedergabe auf Frame `0` zurück, falls `spriteLoop` gesetzt ist; ansonsten bleibt sie auf dem letzten Frame stehen und hält an. Teile außerhalb des Overlays werden abgeschnitten.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Sprite-Handle aus `spriteLoad` oder `fontToSprite`.
|
|
* `x`, `y` -- Ankerpunkt in Overlay-Koordinaten; Nachkommastellen werden abgeschnitten.
|
|
* `x2`, `y2` -- gegenüberliegende Ecke des Zielrechtecks, einschließlich, für die gestreckten Formen.
|
|
* `centered` -- Boolean. Bei `true` wird das Bild (oder das gestreckte Rechteck) so verschoben, dass `(x, y)` sein Mittelpunkt ist statt seiner oberen linken Ecke.
|
|
|
|
*Hinweise:* Eine laufende Animation, die eine Weile nicht gezeichnet wird, holt beim nächsten Zeichnen auf; ruf `spritePause` auf, wenn sie stattdessen ihren Frame halten soll. Verwende für gedrehte Sprites die zentrierte Form, da die Drehung den Begrenzungsrahmen verändert und ein Anker oben links zu wandern scheint.
|
|
|
|
*Seit:* 1.x. Zentrierte und gestreckt-zentrierte Formen in 2.10 hinzugefügt.
|
|
*Siehe auch:* <<spriteload,spriteLoad>>, <<spriterotate,spriteRotate>>, <<spritegetwidth,spriteGetWidth>>, <<videodraw,videoDraw>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Automaten-Artwork in natürlicher Größe, das Attract-Standbild in seinen Kasten gequetscht
|
|
-- und ein Cursor, der an seinem Mittelpunkt verankert ist.
|
|
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)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Blittet einen Frame eines Sprite-Sheets (oder einer beliebigen Animation) mit `(x, y)` als oberer linker Ecke auf das Overlay, ohne zu ändern, auf welchem Frame das Sprite steht oder ob es läuft. Frames werden ab `1` gezählt, wie Hypseus sie zählt; ein Frame außerhalb von `1` bis zur Frame-Anzahl zeichnet Frame `1`. Ein Standbild zeichnet sich selbst und ignoriert `frame`. Eine mit `spriteFlip` gesetzte Spiegelung gilt. Die eigene Drehung und Skalierung des Sprites aus `spriteRotate` und `spriteScale` werden ignoriert: Die erste Form zeichnet den Frame in seiner natürlichen Größe, die anderen multiplizieren die Breite des Frames mit `scaleX` (oder `scale`) und seine Höhe mit `scaleY` (oder `scale`), mit Nächster-Nachbar-Abtastung. Ruf es aus `onOverlayUpdate` auf.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Sprite-Handle aus `spriteLoadFrames` oder `spriteLoad`.
|
|
* `x`, `y` -- obere linke Ecke in Overlay-Koordinaten; Nachkommastellen werden abgeschnitten.
|
|
* `frame` -- Frame-Nummer, `1` bis zur Frame-Anzahl; Nachkommastellen werden abgeschnitten.
|
|
* `scale`, `scaleX`, `scaleY` -- Größenfaktoren, `1` für natürliche Größe.
|
|
|
|
*Hinweise:* `spriteSetFrame` zählt Frames ab `0`; `spriteDrawFrame` zählt ab `1`.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<spriteloadframes,spriteLoadFrames>>, <<spritedraw,spriteDraw>>, <<spritesetframe,spriteSetFrame>>, <<spriteframewidth,spriteFrameWidth>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Ziffernstreifen mit zehn Frames zeichnet den Punktestand, eine Ziffer pro Frame.
|
|
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)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Zeichnet ein Rechteck aus einem Sprite mit seiner oberen linken Ecke bei `x`, `y`: in seiner eigenen Größe oder, wenn `x2` und `y2` angegeben sind, so gestreckt, dass es den Kasten bis zu dieser Ecke füllt, so wie <<spritedraw,`spriteDraw`>> ein ganzes Sprite streckt. Es ist der einzige Aufruf, der ein Quellrechteck nimmt: `spriteDraw` nimmt das ganze Sprite und <<spritedrawframe,`spriteDrawFrame`>> einen ganzen Frame eines Sheets, während dieser ein beliebiges Rechteck nimmt. So schneidet ein Spiel eine Kachel, eine Ziffer oder einen Buchstaben aus einem Bild aus, ohne es viele Male zu laden.
|
|
|
|
Die gestreckte Form ist Singes eigene, und auf ihr baut ein Straßenrennspiel auf. Jede Bildschirmzeile unterhalb des Horizonts ist ein Stück Straße in einer gewissen Entfernung, also schneidet das Spiel eine Zeile aus einem Bild des Straßenquerschnitts aus und streckt sie auf die Breite der Straße in dieser Entfernung, zentriert dort, wohin die Kurven sie getragen haben. Gut hundert Aufrufe pro Frame zeichnen die ganze Straße samt Kurven und Hügeln, und nichts wird neu aufgebaut. Dieselbe Form zeichnet ein Sprite, das teilweise hinter einer Hügelkuppe verborgen ist: Schneide seinen oberen Teil aus und strecke ihn auf das, was noch zu sehen ist. <<spritescale,`spriteScale`>> kann keins von beidem, da es jedes Mal, wenn sich die Größe ändert, das ganze Bild neu aufbaut.
|
|
|
|
Das Rechteck ist in den eigenen Pixeln des Sprites angegeben und muss darin liegen; ein Rechteck, das das nicht tut, oder eines ohne Breite oder Höhe beendet das Skript und nennt die Größe des Sprites, und ebenso ein `x2` ohne `y2`. Eine Streckung, deren Kasten keine Breite oder Höhe hat, weil `x2` links von `x` oder `y2` über `y` liegt, zeichnet nichts. Die Streckung nimmt den nächstgelegenen Pixel, statt zu mischen, und hält so die Farben einer ein Pixel hohen Zeile genau so, wie sie gezeichnet wurden. Das Sprite, aus dem gezeichnet wird, ist das Sprite, wie es gerade gezeichnet wird, also ändert ein <<spriterotate,`spriteRotate`>> oder ein `spriteScale`, was die Koordinaten bedeuten.
|
|
|
|
Unter `LEGACY_SPRITE_ARGS` steht das Handle an dritter Stelle, `spriteDrawGrid(x, y, spr, sourceX, sourceY, width, height)`, wo Hypseus es hinsetzt, und `x2`, `y2` folgen am Ende wie hier.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Zahl; ein Sprite-Handle.
|
|
* `x`, `y` -- Zahlen; wo die obere linke Ecke des Rechtecks landet, in Overlay-Koordinaten.
|
|
* `sourceX`, `sourceY` -- Zahlen; die obere linke Ecke des Rechtecks im Sprite.
|
|
* `width`, `height` -- Zahlen; seine Größe, mindestens 1.
|
|
* `x2`, `y2` -- Zahlen, optional und nur zusammen; die untere rechte Ecke des Kastens, in den es gestreckt wird, einschließlich.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe; die gestreckte Form ist neu in 3.00 und Singes eigene.
|
|
*Siehe auch:* <<spritedraw,spriteDraw>>, <<spritedrawframe,spriteDrawFrame>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Zahl, gezeichnet aus einem Streifen von zehn Ziffern, jede 16 mal 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
|
|
----
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine gerade Straße bis zum Horizont, eine Bildschirmzeile nach der anderen. road.png ist 256 Pixel breit und zwei
|
|
-- Zeilen hoch: ein heller Querschnitt der Straße und ein dunkler, die sich die Straße hinunter abwechseln, damit man
|
|
-- ihre Bewegung sieht. Zeile y des Bildschirms ist der Boden in der Entfernung 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)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Zeichnet den Frame, den <<spriterotateframe,`spriteRotateFrame`>> zuletzt gedreht hat, *zentriert* auf `x`, `y`. Dies ist das Gegenstück zu jenem Aufruf und der Grund, weshalb es beide gibt: <<spritedrawframe,`spriteDrawFrame`>> ignoriert absichtlich die Drehung und Skalierung eines Sprites, also verwendet ein Spiel, das einen Frame eines Sheets gedreht haben will, stattdessen diese beiden.
|
|
|
|
Mit einem Maßstab wird der gedrehte Frame beim Zeichnen um ihn gestreckt -- zusätzlich zu dem Maßstab, mit dem er gedreht wurde -- und das Ergebnis ist weiterhin auf `x`, `y` zentriert. `scaleX` allein skaliert beide Achsen.
|
|
|
|
Zeichnen vor jedem `spriteRotateFrame` beendet das Skript, da es keinen gedrehten Frame zum Zeichnen gibt.
|
|
|
|
Unter `LEGACY_SPRITE_ARGS` steht das Handle am Ende, wie Hypseus es schreibt.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Zahl; ein Sprite-Handle.
|
|
* `x`, `y` -- Zahlen; der *Mittelpunkt* des gezeichneten Frames, in Overlay-Koordinaten.
|
|
* `scale` oder `scaleX`, `scaleY` -- optionale Zahlen; ein weiterer Maßstab beim Zeichnen.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<spriterotateframe,spriteRotateFrame>>, <<spritedrawframe,spriteDrawFrame>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Kompassnadel: ein Frame des Sheets, der dorthin zeigt, wohin die Spielfigur geht.
|
|
spriteRotateFrame(needle, heading, 1)
|
|
spriteDrawRotatedFrame(needle, 320, 240)
|
|
----
|
|
|
|
[#spriteflip]
|
|
==== spriteFlip
|
|
|
|
[source,text]
|
|
----
|
|
spriteFlip(id, horizontal, vertical)
|
|
----
|
|
|
|
Spiegelt das Sprite von links nach rechts, von oben nach unten oder beides, bis es wieder mit `false, false` aufgerufen wird. Die Spiegelung gilt für jedes Zeichnen des Sprites: `spriteDraw`, jeden Frame aus `spriteDrawFrame` und den gedrehten Frame aus `spriteDrawRotatedFrame`. Sie wird nach der Drehung und Skalierung aus `spriteRotate` und `spriteScale` angewendet, ein gedrehtes Sprite wird also so gespiegelt, wie es auf dem Bildschirm erscheint, und ein gedrehter Frame wird einfach in die andere Richtung gespiegelt, statt erneut gedreht zu werden. Das gespiegelte Bild wird nur neu aufgebaut, wenn sich die Flags ändern; ein von `spriteDrawFrame` gezeichneter Frame wird einmal gespiegelt und behalten, bis ein anderer Frame verlangt wird. `spriteDrawGrid` ignoriert die Spiegelung, da sein Rechteck aus dem ungespiegelten Bild geschnitten wird.
|
|
|
|
Eine Figur, die in beide Richtungen läuft, braucht nur Grafiken, die in eine Richtung schauen: Spiegle das Sprite, wenn sie sich umdreht.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Sprite-Handle.
|
|
* `horizontal` -- `true` spiegelt von links nach rechts.
|
|
* `vertical` -- `true` spiegelt von oben nach unten.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00
|
|
*Siehe auch:* <<spriterotate,spriteRotate>>, <<spritedrawframe,spriteDrawFrame>>, <<spritedrawrotatedframe,spriteDrawRotatedFrame>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Sheet des Helden schaut nach rechts; nach links schauen dieselben Frames, gespiegelt.
|
|
function onOverlayUpdate()
|
|
overlayClear()
|
|
spriteFlip(hero, heroVx < 0, false)
|
|
spriteDrawFrame(hero, heroX, heroY, walkFrame)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#spriteframeheight]
|
|
==== spriteFrameHeight
|
|
|
|
[source,text]
|
|
----
|
|
height = spriteFrameHeight(id)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Gibt die Höhe eines Frames des Sprites vor jeder Skalierung oder Drehung zurück: die Höhe des Streifens bei einem Sheet aus `spriteLoadFrames`, die Höhe eines Frames bei einer Animation und die Höhe des Bildes bei einem Standbild (Hypseus meldet hier die gezeichnete Fläche). `spriteGetHeight` meldet stattdessen die Größe, wie sie gezeichnet wird.
|
|
|
|
*Rückgabe:* Ganzzahl.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<spriteframewidth,spriteFrameWidth>>, <<spritegetheight,spriteGetHeight>>, <<spriteloadframes,spriteLoadFrames>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Einen Frame auf einem Punkt zentrieren.
|
|
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)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Gibt die Breite eines Frames des Sprites vor jeder Skalierung oder Drehung zurück: die Breite des Streifens geteilt durch die Frame-Anzahl bei einem Sheet aus `spriteLoadFrames`, die Breite eines Frames bei einer Animation und die Breite des Bildes bei einem Standbild (Hypseus meldet dort `0`). `spriteGetWidth` meldet stattdessen die Größe, wie sie gezeichnet wird.
|
|
|
|
*Rückgabe:* Ganzzahl.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<spriteframeheight,spriteFrameHeight>>, <<spritegetwidth,spriteGetWidth>>, <<spriteloadframes,spriteLoadFrames>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Frames eines Streifens nebeneinander auslegen.
|
|
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)
|
|
----
|
|
|
|
Gibt den nullbasierten Index des Frames zurück, den das Sprite gerade zeigt. Standbilder und Animationen, die nicht weitergerückt sind, melden `0`. Frames rücken nur innerhalb von `spriteDraw` weiter, der Wert ändert sich also zwischen Zeichenaufrufen, nicht zwischen beliebigen Aufrufen. Verwende es, um Sound oder Spiellogik auf einen bestimmten Frame einer Animation abzustimmen.
|
|
|
|
*Rückgabe:* ganzzahliger Frame-Index, `0` bei einem Standbild.
|
|
|
|
*Seit:* 2.10
|
|
*Siehe auch:* <<spritesetframe,spriteSetFrame>>, <<spriteisplaying,spriteIsPlaying>>, <<spritedraw,spriteDraw>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Den Schuss auf dem Frame auslösen, auf dem das Mündungsfeuer erscheint.
|
|
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)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Wie viele Frames ein Sprite hat: die Frame-Anzahl eines von <<spriteloadframes,`spriteLoadFrames`>> geladenen Sheets oder eines animierten GIF oder WEBP und `0` bei einem Standbild. Es ist die Anzahl, bis zu der die nullbasierte Frame-Nummer von <<spritesetframe,`spriteSetFrame`>> läuft, und eins mehr als die größte, die es annimmt.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Zahl; ein Sprite-Handle.
|
|
|
|
*Rückgabe:* Zahl; `0` bei einem Standbild.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<spriteloadframes,spriteLoadFrames>>, <<spritegetframe,spriteGetFrame>>, <<spritesetframe,spriteSetFrame>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Zum nächsten Frame weiterschalten, wie auch immer das Sheet ausgefallen ist.
|
|
local frames = spriteGetFrames(walk)
|
|
if frames > 0 then
|
|
spriteSetFrame(walk, (spriteGetFrame(walk) + 1) % frames)
|
|
end
|
|
----
|
|
|
|
[#spritegetheight]
|
|
==== spriteGetHeight
|
|
|
|
[source,text]
|
|
----
|
|
result = spriteGetHeight(id)
|
|
----
|
|
|
|
Gibt die Höhe des Sprites zurück, wie es jetzt gezeichnet würde, in Overlay-Koordinaten. Skalierung und Drehung, die mit `spriteScale`, `spriteRotate` oder `spriteRotateAndScale` angewendet wurden, sind eingeschlossen, ein gedrehtes Sprite meldet also die Höhe seines gedrehten Begrenzungsrahmens, nicht die des Quellbildes. Bei einer Animation wird der aktuelle Frame gemessen. Die gestreckten Formen von `spriteDraw` beeinflussen den Wert nicht.
|
|
|
|
*Rückgabe:* ganzzahlige Höhe.
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<spritegetwidth,spriteGetWidth>>, <<spritescale,spriteScale>>, <<spriterotate,spriteRotate>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Vorgerenderte Beschreibungszeilen stapeln, im Abstand der Höhe der Text-Sprites.
|
|
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)
|
|
----
|
|
|
|
Gibt die Breite des Sprites zurück, wie es jetzt gezeichnet würde, in Overlay-Koordinaten, einschließlich jeder gerade angewendeten Skalierung und Drehung. Bei einer Animation wird der aktuelle Frame gemessen. Zusammen mit `spriteGetHeight` ist das der übliche Weg, ein Sprite in einem Kasten zu zentrieren oder mit `fontToSprite` gerenderten Text zu messen.
|
|
|
|
*Rückgabe:* ganzzahlige Breite.
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<spritegetheight,spriteGetHeight>>, <<spritescale,spriteScale>>, <<fonttosprite,fontToSprite>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Automatenbild in seinem Feld zentrieren und den Titel über die Breite des Overlays.
|
|
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)
|
|
----
|
|
|
|
Meldet, ob die Animation des Sprites gerade weiterrückt. Ein frisch geladenes Sprite meldet `false`; `spritePlay` schaltet sie ein, und `spritePause` oder das Ende einer Animation ohne Wiederholung schaltet sie aus. Frag es regelmäßig ab, um herauszufinden, wann eine einmalige Animation fertig ist.
|
|
|
|
*Rückgabe:* Boolean, `true`, solange die Animation läuft.
|
|
|
|
*Hinweise:* `spritePlay` und `spritePause` prüfen nicht auf Animationsdaten, ein Standbild meldet also, was diese Aufrufe zuletzt gesetzt haben.
|
|
|
|
*Seit:* 2.10
|
|
*Siehe auch:* <<spriteplay,spritePlay>>, <<spritepause,spritePause>>, <<spriteloop,spriteLoop>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Menü zurückhalten, bis die einmalige Logo-Animation fertig ist.
|
|
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)
|
|
----
|
|
|
|
Lädt ein Bild und gibt ein ganzzahliges Handle für die anderen `sprite*`-Aufrufe zurück. Der Name geht durch das virtuelle Dateisystem der Engine, die Datei kann also ungepackt auf der Festplatte oder in einem gepackten Spiel liegen; er wird relativ zu dem Ordner aufgelöst, in dem Singe gestartet wurde, stelle also für Dateien, die mit deinem Spiel ausgeliefert werden, `DIR` voran. Jedes Format in <<formats,Video-, Audio- und Containerformate>> wird gelesen. Ein animiertes GIF oder WEBP mit zwei oder mehr Frames lädt als Animation, auf Frame `0` abgestellt, nicht laufend und ohne Wiederholung; jede andere Datei, auch ein GIF mit nur einem Frame, lädt als Standbild. Ein Bild mit mehr als 8 Bit pro Kanal oder mit Gleitkommapixeln wird beim Laden in 8-Bit-RGBA umgewandelt und von da an als solches behandelt. Pixel mit dem Rohwert `0` im eigenen Pixelformat der Datei werden transparent. Das Laden ist synchron, und eine Datei, die sich nicht öffnen oder dekodieren lässt, beendet das Skript mit der Fehlermeldung des Laders, statt `nil` zurückzugeben.
|
|
|
|
Die optionale Größe ist für ein Vektorbild -- ein SVG -- gedacht, das keine eigene Größe hat, die etwas taugt: Das Bild wird so gerastert, dass es unter Wahrung seiner eigenen Proportionen in diesen Kasten passt, statt in der Größe der Datei gerastert und hochskaliert zu werden. Frag <<spritegetwidth,`spriteGetWidth`>> und <<spritegetheight,`spriteGetHeight`>>, was dabei herausgekommen ist, denn wenn eine quadratische Zeichnung in einen breiten Kasten eingepasst wird, bleibt Breite ungenutzt. Jedes andere Format ignoriert die Größe.
|
|
|
|
*Parameter:*
|
|
|
|
* `filename` -- String; das zu ladende Bild.
|
|
* `width` -- optionale Zahl; die Breite, in die ein SVG eingepasst wird, `1` oder mehr.
|
|
* `height` -- optionale Zahl; die Höhe, in die ein SVG eingepasst wird, `1` oder mehr. Beide müssen zusammen angegeben werden.
|
|
|
|
*Rückgabe:* ganzzahliges Sprite-Handle.
|
|
|
|
*Hinweise:* Animationen starten nicht von selbst. Ruf nach dem Laden `spriteLoop(id, true)` und `spritePlay(id)` auf, wenn die Animation laufen und sich wiederholen soll.
|
|
|
|
*Seit:* 1.x. Unterstützung für animierte GIF/WEBP in 2.10 hinzugefügt; die Vektorgröße in 3.00.
|
|
*Siehe auch:* <<spriteunload,spriteUnload>>, <<spriteplay,spritePlay>>, <<spriteloop,spriteLoop>>, <<fonttosprite,fontToSprite>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Menügrafiken einmal beim Start laden; die Attract-Schleife ist ein animiertes WEBP.
|
|
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)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Baut ein Sprite aus einem Bild, das in einem Lua-String steht, statt aus einer Datei. Jedes Format, das <<spriteload,`spriteLoad`>> liest, wird hier gelesen, animierte GIF und WEBP eingeschlossen, und eine so geladene Animation verhält sich genau wie eine aus einer Datei geladene.
|
|
|
|
Der String darf Nullbytes enthalten; seine Länge, nicht seine erste Null, entscheidet, wie viel gelesen wird. Das Bild wird während des Aufrufs dekodiert, und der String wird nicht behalten. Bytes, die kein Bild sind, oder ein leerer String beenden das Skript, so wie eine unlesbare Datei bei `spriteLoad`.
|
|
|
|
Verwende es für Grafiken, die ein Spiel selbst erzeugt, entpackt oder entschlüsselt. Grafiken, die das virtuelle Dateisystem über ihren Namen erreicht -- auch Grafiken in einer gepackten `.game` -- lädt man besser mit `spriteLoad`, das nicht die ganze Datei in einem Lua-String hält.
|
|
|
|
*Parameter:*
|
|
|
|
* `data` -- String aus Bytes.
|
|
|
|
*Rückgabe:* Zahl, das Sprite-Handle.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<spriteload,spriteLoad>>, <<soundloaddata,soundLoadData>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Bild, das das Spiel in seiner eigenen Datendatei aufbewahrt.
|
|
local bytes = myArchive:extract("logo.png")
|
|
local logo = spriteLoadData(bytes)
|
|
spriteDraw(logo, 10, 10)
|
|
----
|
|
|
|
[#spriteloadframes]
|
|
==== spriteLoadFrames
|
|
|
|
[source,text]
|
|
----
|
|
result = spriteLoadFrames(count, filename)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Lädt ein Bild, das `count` gleich große Frames in einer einzigen Zeile enthält, und gibt ein Sprite-Handle zurück. Die Frame-Breite ist die Bildbreite geteilt durch `count` (ganzzahlige Division; übrige Spalten rechts fallen weg), und die Frame-Höhe ist die Bildhöhe. Das Sheet wird zu einer gewöhnlichen Animation, auf Frame `0` abgestellt, nicht laufend und ohne Wiederholung, sodass `spriteDraw` den aktuellen Frame zeigt statt des ganzen Streifens (wie Hypseus ihn zeichnet), und `spriteDrawFrame` wählt jeden Frame direkt. Der Name geht wie bei `spriteLoad` durch das virtuelle Dateisystem, und Pixel mit dem Rohwert `0` werden in jedem Frame transparent. Eine Anzahl außerhalb von `2` bis `512`, ein Bild, das schmaler als `count` Pixel ist, oder eine Datei, die sich nicht lesen lässt, beendet das Skript mit einem Fehler.
|
|
|
|
*Parameter:*
|
|
|
|
* `count` -- Anzahl der Frames über die Breite des Bildes, `2` bis `512`.
|
|
* `filename` -- das Streifenbild.
|
|
|
|
*Rückgabe:* ganzzahliges Sprite-Handle.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<spritedrawframe,spriteDrawFrame>>, <<spriteframewidth,spriteFrameWidth>>, <<spriteload,spriteLoad>>, <<spriteunload,spriteUnload>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Autostreifen mit fünf Frames; Frame 3 ist das Auto, das geradeaus zeigt.
|
|
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)
|
|
----
|
|
|
|
Legt fest, ob die Animation nach ihrem letzten Frame wieder bei Frame `0` beginnt oder dort anhält. Neue Sprites wiederholen nicht. Die Einstellung wird jedes Mal abgefragt, wenn `spriteDraw` über den letzten Frame hinausgeht, sie kann also geändert werden, während die Animation läuft. Sie an einem Sprite zu setzen, das schon angehalten hat, startet die Wiedergabe nicht neu; dafür ruf `spritePlay` auf. Das Argument muss ein Lua-Boolean sein.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Sprite-Handle.
|
|
* `loop` -- Boolean. `true` springt zurück auf Frame `0`; `false` (der Standard) hält auf dem letzten Frame an, und `spriteIsPlaying` gibt von da an `false` zurück.
|
|
|
|
*Seit:* 2.10
|
|
*Siehe auch:* <<spriteplay,spritePlay>>, <<spriteisplaying,spriteIsPlaying>>, <<spritesetframe,spriteSetFrame>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Fackel flackert endlos; die Explosion spielt einmal pro Treffer.
|
|
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)
|
|
----
|
|
|
|
Erzeugt ein leeres Sprite, jedes Pixel transparent, das <<spritesetpixels,`spriteSetPixels`>> füllen soll: ein Bild, das das Skript selbst macht, statt es zu laden. Jede Seite ist 1 bis 4096 Pixel lang; alles andere beendet das Skript. Das Sprite ist ein Standbild und kann alles, was jedes andere Standbild kann: Es lässt sich zeichnen, drehen, skalieren, strecken und einfärben, und es kann die Textur eines Materials sein.
|
|
|
|
*Parameter:*
|
|
|
|
* `width`, `height` -- Zahlen; seine Größe in Pixeln.
|
|
|
|
*Rückgabe:* Zahl, das Sprite-Handle.
|
|
|
|
*Seit:* 3.00
|
|
*Siehe auch:* <<spritesetpixels,spriteSetPixels>>, <<spriteloaddata,spriteLoadData>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
local minimap = spriteNew(64, 64)
|
|
----
|
|
|
|
[#spritepause]
|
|
==== spritePause
|
|
|
|
[source,text]
|
|
----
|
|
spritePause(id)
|
|
----
|
|
|
|
Hält die Animation auf ihrem aktuellen Frame an. Der Frame wird von `spriteDraw` weiter gezeichnet, und die pausierte Zeit wird nicht mitgezählt, wenn `spritePlay` fortsetzt, der Frame springt also nicht vor. Ein Sprite zu pausieren, das nicht läuft, ändert nichts.
|
|
|
|
*Seit:* 2.10
|
|
*Siehe auch:* <<spriteplay,spritePlay>>, <<spriteisplaying,spriteIsPlaying>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Den Gegner mitten im Schritt einfrieren, während das Treffer-Aufblitzen läuft.
|
|
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)
|
|
----
|
|
|
|
Startet die Animation oder setzt sie von ihrem aktuellen Frame aus fort, wobei der erste Schritt ab dem Moment des Aufrufs gemessen wird. Ein Sprite, das bereits läuft, bleibt unberührt, der Frame-Timer wird also nicht zurückgesetzt. Ein neues Sprite muss mit diesem Aufruf gestartet werden; Laden allein animiert es nicht.
|
|
|
|
*Hinweise:* Eine Animation ohne Wiederholung, die ihren letzten Frame erreicht hat, bleibt auf diesem Frame, wenn sie erneut abgespielt wird. Um sie noch einmal abzuspielen, ruf zuerst `spriteSetFrame(id, 0)` auf.
|
|
|
|
*Seit:* 2.10
|
|
*Siehe auch:* <<spritepause,spritePause>>, <<spritesetframe,spriteSetFrame>>, <<spriteloop,spriteLoop>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die einmalige Explosion am Aufprallpunkt neu starten.
|
|
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)
|
|
----
|
|
|
|
Wählt die Abtastung, die verwendet wird, wenn das Sprite für Drehung oder Skalierung neu aufgebaut wird. `RENDER_PIXELATED` behält harte Pixelkanten, `RENDER_SMOOTH` interpoliert. Neue Sprites sind pixelig. Die Einstellung zählt nur, solange eine Drehung oder ein Maßstab ungleich `1.0` wirkt; ein untransformiertes Sprite wird direkt aus seinem Quellbild gezeichnet, und die gestreckten Formen von `spriteDraw` verwenden immer Nächster-Nachbar-Abtastung. Den Modus zu ändern baut das transformierte Bild sofort neu auf, setze ihn also einmal nach dem Laden statt in jedem Frame.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Sprite-Handle.
|
|
* `mode` -- `RENDER_PIXELATED` (`0`) oder `RENDER_SMOOTH` (`1`). Jede Zahl ungleich null wählt glatt.
|
|
|
|
*Seit:* 2.10
|
|
*Siehe auch:* <<spritescale,spriteScale>>, <<spriterotate,spriteRotate>>, <<spriterotateandscale,spriteRotateAndScale>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Fotografische Marquee-Grafik soll interpolieren; Pixelgrafik muss scharf bleiben.
|
|
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)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Wird aus Kompatibilitätsgründen angenommen und tut nichts. In Hypseus wendet das Flag die transparente Farbe erneut auf die Kopien an, die `spriteRotate` und `spriteScale` erzeugen und die ihren Hintergrund sonst deckend zeichnen würden; die transformierten Kopien von Singe tragen die Transparenz in ihrem Alphakanal, es gibt also nichts neu auszustanzen. Ein falsches Handle oder ein Argument, das kein Boolean ist, bricht das Skript ab, wie bei jedem anderen Sprite-Aufruf.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Sprite-Handle.
|
|
* `enabled` -- Boolean, ignoriert.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<spriterotate,spriteRotate>>, <<spritescale,spriteScale>>, <<spriteload,spriteLoad>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Hypseus-Spielcode; in Singe harmlos.
|
|
badge = spriteLoad(DIR .. "badge.png")
|
|
spriteResetColorKey(badge, true)
|
|
----
|
|
|
|
[#spriterotate]
|
|
==== spriteRotate
|
|
|
|
[source,text]
|
|
----
|
|
spriteRotate(id, degrees)
|
|
----
|
|
|
|
Dreht das Sprite im Uhrzeigersinn um seinen Mittelpunkt. Der Winkel wird modulo 360 reduziert und darf gebrochen oder negativ sein. Der aktuelle Maßstab bleibt erhalten. Das gedrehte Bild wird nur neu aufgebaut, wenn sich der Winkel tatsächlich ändert; denselben Winkel in jedem Frame zu wiederholen kostet also nichts, während ein neuer Winkel in jedem Frame auch in jedem Frame neu aufbaut. Die Drehung vergrößert den Begrenzungsrahmen, also ändern sich `spriteGetWidth` und `spriteGetHeight`, und die zentrierte Form von `spriteDraw` ist die richtige.
|
|
|
|
*Seit:* 2.10
|
|
*Siehe auch:* <<spriterotateandscale,spriteRotateAndScale>>, <<spritedraw,spriteDraw>>, <<spritequality,spriteQuality>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Kisten aus einer 2D-Physikszene, im Winkel ihrer Körper gezeichnet.
|
|
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)
|
|
----
|
|
|
|
Setzt Drehung und Maßstab zusammen. Das Ergebnis ist dasselbe wie `spriteRotate` gefolgt von `spriteScale`, aber das transformierte Bild wird einmal statt zweimal neu aufgebaut, und gar nicht, wenn sich nichts geändert hat. Skalierungsfaktoren sollten größer als `0` sein; ein negativer wird ohne sein Vorzeichen genommen, und einer, der keine Pixel übrig ließe, null eingeschlossen, lässt stattdessen eines übrig.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Sprite-Handle.
|
|
* `degrees` -- Drehung im Uhrzeigersinn, modulo 360 reduziert.
|
|
* `scale` -- einheitlicher Faktor, `1.0` für natürliche Größe.
|
|
* `scaleX`, `scaleY` -- getrennte waagerechte und senkrechte Faktoren.
|
|
|
|
*Seit:* 2.10
|
|
*Siehe auch:* <<spriterotate,spriteRotate>>, <<spritescale,spriteScale>>, <<spritequality,spriteQuality>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Münze, die sich dreht und schrumpft, während sie zum Punktezähler fliegt.
|
|
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)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Nimmt einen Frame aus einem Sprite-Sheet, dreht ihn um `degrees` im Uhrzeigersinn, skaliert ihn mit dem eigenen <<spritescale,`spriteScale`>> des Sprites und behält das Ergebnis für <<spritedrawrotatedframe,`spriteDrawRotatedFrame`>>. Die eigene gezeichnete Fläche des Sprites bleibt unberührt, also zeigen <<spritedraw,`spriteDraw`>> und `spriteDrawFrame` weiter, was sie gezeigt haben.
|
|
|
|
Frames werden ab 1 gezählt, wie `spriteDrawFrame` sie zählt (und anders als `spriteGetFrame` und `spriteSetFrame`, die ab 0 zählen); ein Frame außerhalb des Sheets ist Frame 1 statt eines Fehlers. Ein Standbild dreht sein ganzes Bild.
|
|
|
|
Drehen ist nicht umsonst: Der Frame wird jedes Mal Pixel für Pixel neu gezeichnet, dreh ihn also, wenn sich der Winkel ändert, statt in jedem Frame. Der Winkel wird modulo 360 genommen, und <<spritequality,`spriteQuality`>> entscheidet, ob die gedrehten Kanten geglättet werden.
|
|
|
|
Unter `LEGACY_SPRITE_ARGS` steht das Handle am Ende, `spriteRotateFrame(degrees, frame, spr)`, wie Hypseus es schreibt.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Zahl; ein Sprite-Handle.
|
|
* `degrees` -- Zahl; im Uhrzeigersinn.
|
|
* `frame` -- Zahl; 1 bis zur Frame-Anzahl.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<spritedrawrotatedframe,spriteDrawRotatedFrame>>, <<spriterotate,spriteRotate>>, <<spritescale,spriteScale>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Schiffs-Sprite, dessen Rumpf Frame 1 und dessen Schub Frame 2 ist.
|
|
spriteRotateFrame(ship, angle, thrusting and 2 or 1)
|
|
spriteDrawRotatedFrame(ship, shipX, shipY)
|
|
----
|
|
|
|
[#spritescale]
|
|
==== spriteScale
|
|
|
|
[source,text]
|
|
----
|
|
spriteScale(id, scale)
|
|
spriteScale(id, scaleX, scaleY)
|
|
----
|
|
|
|
Skaliert das Sprite um seinen Mittelpunkt, einheitlich oder pro Achse, und behält die aktuelle Drehung. Faktoren müssen größer als `0` sein; `1.0` ist natürliche Größe, und ein Faktor von genau `1.0` auf beiden Achsen ohne Drehung zeichnet das Quellbild direkt. Das skalierte Bild wird nur neu aufgebaut, wenn sich ein Faktor ändert, mit der von `spriteQuality` gewählten Abtastung. Nimm lieber die gestreckte Form von `spriteDraw`, wenn sich die Zielgröße in jedem Frame ändert, da sie nichts neu aufbaut.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Sprite-Handle.
|
|
* `scale` -- einheitlicher Faktor.
|
|
* `scaleX`, `scaleY` -- getrennte waagerechte und senkrechte Faktoren.
|
|
|
|
*Seit:* 2.10
|
|
*Siehe auch:* <<spriterotateandscale,spriteRotateAndScale>>, <<spritequality,spriteQuality>>, <<spritedraw,spriteDraw>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Den hervorgehobenen Menüeintrag vergrößern; neu aufgebaut wird nur, wenn sich die Auswahl bewegt.
|
|
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)
|
|
----
|
|
|
|
Multipliziert bei jedem Zeichnen des Sprites, in jeder Form, das Alpha jedes Pixels mit `alpha / 255`: `spriteDraw`, `spriteDrawFrame`, `spriteDrawGrid` und `spriteDrawRotatedFrame`, gedreht oder nicht. `255`, der Standard, zeichnet es, wie es ist, und `0` gar nicht, ein Logo blendet also ohne zweites Bild ein und aus. Werte außerhalb des Bereichs werden begrenzt.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Zahl; ein Sprite-Handle.
|
|
* `alpha` -- Zahl; `0` bis `255`.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00
|
|
*Siehe auch:* <<spritesetblend,spriteSetBlend>>, <<spritesettint,spriteSetTint>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Der Titel blendet über zwei Sekunden ein.
|
|
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)
|
|
----
|
|
|
|
Wie jedes Zeichnen des Sprites es über das legt, was schon auf dem Overlay ist. `SPRITE_ALPHA`, der Standard, zeichnet es nach seinem Alpha darüber. `SPRITE_ADD` addiert seine Farbe, skaliert mit seinem Alpha, zu dem, was da ist, und kann also nur aufhellen: Leuchten, Licht, Funken und die Shadebobs der alten Demos, bei denen ein Ball, der immer wieder über sich selbst gezeichnet wird, zu Weiß ausbrennt. `SPRITE_MULTIPLY` dunkelt das, was da ist, mit seiner Farbe ab, für Schatten und getöntes Glas.
|
|
|
|
Addierte und multiplizierte Pixel behalten das eigene Alpha des Overlays, sie zeigen sich also nur dort, wo schon etwas auf dem Overlay gezeichnet ist: Über einem transparenten Overlay, über einem Video oder der 3D-Szene, ändern sie nichts. Zeichne zuerst etwas Deckendes, oder verwende `emitterSetBlend` mit `PARTICLE_ADD`, das über alles addiert. Alles außer den drei Konstanten beendet das Skript.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Zahl; ein Sprite-Handle.
|
|
* `blend` -- `SPRITE_ALPHA`, `SPRITE_ADD` oder `SPRITE_MULTIPLY`.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00
|
|
*Siehe auch:* <<spritesetalpha,spriteSetAlpha>>, <<spritesettint,spriteSetTint>>, <<emittersetblend,emitterSetBlend>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Shadebobs: ein weicher weißer Ball, entlang einer Bahn addiert und nie gelöscht, sodass die Spur zu Weiß aufläuft.
|
|
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)
|
|
----
|
|
|
|
Springt mit einer Animation auf den angegebenen Frame und startet den Verzögerungstimer dieses Frames neu, sodass der Frame seine volle Dauer gezeigt wird, bevor der nächste kommt. Der Wiedergabezustand ändert sich nicht: Ein pausiertes Sprite bleibt auf dem neuen Frame pausiert, und ein laufendes macht von dort aus weiter. Der Aufruf wird ignoriert bei Standbildern, bei Frames außerhalb von `0` bis zum letzten Frame und wenn das Sprite diesen Frame schon zeigt.
|
|
|
|
*Hinweise:* Frames werden hier ab `0` gezählt; `spriteDrawFrame`, die Hypseus-Erweiterung, zählt sie ab `1`.
|
|
|
|
*Seit:* 2.10
|
|
*Siehe auch:* <<spritegetframe,spriteGetFrame>>, <<spriteplay,spritePlay>>, <<spritedrawframe,spriteDrawFrame>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Tür in dem Zustand zeigen, in dem sie laut Spiellogik ist.
|
|
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)
|
|
----
|
|
|
|
Ersetzt jedes Pixel eines Standbild-Sprites aus einem String, Zeile für Zeile von oben. Ohne Palette hat der String vier Bytes pro Pixel: Rot, Grün, Blau und Alpha. Mit einer ist es ein Byte pro Pixel, jeweils die Nummer einer Farbe in der Palette, die aus 256 Farben zu je drei Bytes (Rot, Grün und Blau, alle deckend) oder vier (mit Alpha) besteht. Ein String der falschen Länge, eine Palette der falschen Länge oder ein animiertes Sprite beendet das Skript.
|
|
|
|
So zeichnet ein Skript ein Bild Pixel für Pixel, schnell genug für jeden Frame: die Plasmas, Tunnel, das Feuer und das Wasser der alten Demos, eine Minikarte oder eine Höhenkarte, die gerade bearbeitet wird. Den String zu bauen ist Arbeit des Skripts, halte das Bild also klein und strecke es beim Zeichnen: Eine Lua-Schleife füllt ein Bild von 320 mal 240 auf einem Desktop-Rechner in wenigen Millisekunden. Der Palettenmodus ist die alte Art, billig zu animieren: Setze dieselben Pixel noch einmal mit der um eine Farbe weitergedrehten Palette, und alles wechselt die Farbe, ohne dass etwas neu berechnet wird. Ein Sprite, das gedreht oder skaliert ist, wird mit seinen neuen Pixeln erneut gedreht und skaliert; ein Material, das das Sprite zeigt, sieht die Änderung erst, wenn <<materialsettexture,`materialSetTexture`>> wieder mit dem Sprite aufgerufen wird, was es neu hochlädt.
|
|
|
|
Auch ein geladenes Bild kann neue Pixel bekommen; es hat die Größe, mit der es geladen wurde.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Zahl; ein Standbild-Sprite.
|
|
* `pixels` -- String; `width * height * 4` Bytes oder `width * height` mit einer Palette.
|
|
* `palette` -- optionaler String; 768 oder 1024 Bytes.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00
|
|
*Siehe auch:* <<spritenew,spriteNew>>, <<materialsettexture,materialSetTexture>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Feuer: jedes Pixel der Durchschnitt der Pixel darunter, beim Aufsteigen abkühlend, durch eine Palette von
|
|
-- Schwarz über Rot und Gelb bis Weiß. In jedem Frame über den unteren Teil des Bildschirms gestreckt.
|
|
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)
|
|
----
|
|
|
|
Multipliziert bei jedem Zeichnen des Sprites, in jeder Form, die Farbe jedes Pixels mit `r`, `g`, `b` / 255. Ein weißes Sprite zeichnet in jeder Farbe, und jedes Sprite lässt sich ohne zweites Bild abdunkeln oder zu einer Farbe hin verschieben: Aus einem weißen Ball wird ein ganzer Regenbogen davon, und ein weißes Sprite von einem Pixel, mit `spriteDraw` gestreckt, wird zu einem gefüllten Rechteck in jeder Farbe. `255, 255, 255`, der Standard, zeichnet es, wie es ist. Werte außerhalb des Bereichs werden begrenzt.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Zahl; ein Sprite-Handle.
|
|
* `r`, `g`, `b` -- Zahlen; `0` bis `255`.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00
|
|
*Siehe auch:* <<spritesetalpha,spriteSetAlpha>>, <<spritesetblend,spriteSetBlend>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Ring aus Bällen, jeder in seiner eigenen Farbe, aus einem weißen Bild.
|
|
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)
|
|
----
|
|
|
|
Gibt das Sprite frei: das Quellbild, die transformierte Kopie, falls es eine gibt, und alle Animations-Frames. Das Handle ist danach ungültig, und jede weitere Verwendung beendet das Skript mit einem Fehler. Von `fontToSprite` erzeugte Sprites werden genauso freigegeben. Sprites, die beim Ende des Spiels noch geladen sind, gibt die Engine frei, aber Spiele, die während des Spielens Sprites erzeugen (zum Beispiel Text, der neu gerendert wird, wenn sich der Punktestand ändert), müssen die alten entladen, sonst wächst der Speicher ohne Grenze.
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<spriteload,spriteLoad>>, <<fonttosprite,fontToSprite>>, <<onshutdown,onShutdown>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Den Punktestand nur neu rendern, wenn er sich ändert, und das vorige Sprite freigeben.
|
|
function updateScoreSprite()
|
|
if scoreSprite ~= nil then
|
|
spriteUnload(scoreSprite)
|
|
end
|
|
scoreSprite = fontToSprite("SCORE " .. score)
|
|
end
|
|
|
|
function onShutdown()
|
|
spriteUnload(scoreSprite)
|
|
spriteUnload(cabinetSprite)
|
|
end
|
|
----
|
|
|
|
[#subtitleref]
|
|
=== Untertitel
|
|
|
|
SubRip-Untertitel über dem Disc-Bild. Ein Spiel lädt eine `.srt` mit <<srtload,`srtLoad`>>, schaltet sie mit <<srtenable,`srtEnable`>> ein, und die Engine blendet jeden Cue ein und wieder aus, sobald die Disc ihn erreicht. Siehe <<subtitles,Untertitel>> für das Dateiformat, die Zeitsteuerung und wie man sie umgestaltet.
|
|
|
|
Die Untertitel und <<overlaybanner,`overlayBanner`>> teilen sich ein mitgeliefertes Dokument, `Singe/subtitle.rml`, gestaltet von `Singe/subtitle.rcss`; ein Spiel, das ein anderes Aussehen will, liefert seine eigene `subtitle.rml` neben seinem Skript aus, und Singe verwendet dann diese (siehe <<subtitles,Untertitel>>). Beide werden über das Bild gezeichnet, über dem Overlay und unter den GUIs im Bildschirmraum, und sie nehmen keine Mauseingaben an.
|
|
|
|
[#srtclear]
|
|
==== srtClear
|
|
|
|
[source,text]
|
|
----
|
|
srtClear()
|
|
----
|
|
|
|
Hypseus-Erweiterung. Nimmt sofort vom Bildschirm, was dort steht -- einen Cue oder ein Banner -- und lässt die geladenen Cues und den Eingeschaltet-Zustand in Ruhe. Der Cue, der zu sehen war, bleibt weg, bis die Disc den nächsten erreicht; nichts muss neu geladen und nichts wieder eingeschaltet werden.
|
|
|
|
*Parameter:* keine.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<srtenable,srtEnable>>, <<overlaybanner,overlayBanner>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Zwischensequenz endet früh; die Zeile entfernen, die noch zu sehen war.
|
|
srtClear()
|
|
----
|
|
|
|
[#srtenable]
|
|
==== srtEnable
|
|
|
|
[source,text]
|
|
----
|
|
srtEnable(enabled)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Ob die geladenen Cues angezeigt werden, während die Disc läuft. Ausschalten nimmt einen gerade sichtbaren Cue mit weg; erneutes Einschalten zeigt den Cue an, der den Frame abdeckt, auf dem die Disc steht. Ein Skript beginnt mit ausgeschalteten Untertiteln, ein Spiel, das welche lädt, muss sie also einschalten.
|
|
|
|
Ein Argument, das kein Boolean ist, beendet das Skript.
|
|
|
|
*Parameter:*
|
|
|
|
* `enabled` -- Boolean.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<srtload,srtLoad>>, <<srtclear,srtClear>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
srtEnable(SETTINGS.subtitles)
|
|
----
|
|
|
|
[#srtload]
|
|
==== srtLoad
|
|
|
|
[source,text]
|
|
----
|
|
loaded = srtLoad(filename)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Liest eine SubRip-Datei über das virtuelle Dateisystem und wandelt ihre Zeitstempel in Disc-Frames bei der eigenen Bildrate der Disc um. Laden ersetzt, was vorher geladen war; <<srtenable,`srtEnable`>> zeigt es dann an.
|
|
|
|
Der Aufruf antwortet mit `false` und hat nichts geladen, wenn sich die Datei nicht lesen lässt, wenn sie keinen Cue enthält, der sich parsen lässt, oder wenn es keine Disc gibt, an der sie sich ausrichten ließe -- eine fehlerhafte Datei ist also ein `false`, kein Absturz. In jedem dieser Fälle wird eine Warnung mit dem Namen der Datei ausgegeben. Cues, die sich in einer sonst guten Datei nicht parsen lassen, werden übersprungen, wie Hypseus sie überspringt.
|
|
|
|
Da die Zeitstempel beim Laden der Datei zu Frame-Nummern werden und die Engine die Frame-Nummer abgleicht, wird ein Cue wiedergefunden, wohin die Disc auch geschickt wird: Eine Suche, ein Skip oder ein Einzelschritt landet sofort auf der richtigen Zeile.
|
|
|
|
*Parameter:*
|
|
|
|
* `filename` -- String; ein Pfad, den das virtuelle Dateisystem erreichen kann.
|
|
|
|
*Rückgabe:* Boolean, ob ein Cue geladen wurde.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<srtenable,srtEnable>>, <<srtposition,srtPosition>>, <<subtitles,Untertitel>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
if srtLoad("subs/" .. language .. ".srt") then
|
|
srtEnable(true)
|
|
end
|
|
----
|
|
|
|
[#srtloadtrack]
|
|
==== srtLoadTrack
|
|
|
|
[source,text]
|
|
----
|
|
loaded = srtLoadTrack(track)
|
|
----
|
|
|
|
Dasselbe wie <<srtload,`srtLoad`>>, nur dass die Untertitel aus dem eigenen Container der Disc kommen statt aus einer `.srt`-Datei daneben. Alles nach dem Laden ist gleich: Die Cues werden zu Disc-Frame-Nummern bei der Bildrate der Disc, sie werden also nach jedem Sprung wiedergefunden, und <<srtenable,`srtEnable`>>, <<srtposition,`srtPosition`>> und <<srtclear,`srtClear`>> funktionieren genauso.
|
|
|
|
Die ganze Datei wird einmal durchlaufen, um die Spur einzusammeln, ohne Bilder zu dekodieren, bei einer langen Disc dauert das also einen Moment -- tu es, während etwas anderes auf dem Bildschirm ist, statt zwischen zwei Frames des Spielgeschehens.
|
|
|
|
Eine Spur, die Bilder statt Wörter enthält (VobSub oder PGS), lässt sich nicht lesen: Der Aufruf antwortet mit `false`, wie bei einer unlesbaren `.srt`, und lässt die Untertitel gelöscht zurück. Ebenso bei einer Spurnummer, die nichts benennt, und ebenso bei einem Spiel ohne Disc.
|
|
|
|
*Parameter:*
|
|
|
|
* `track` -- Zahl; `0` bis `discGetSubtitleTracks() - 1`.
|
|
|
|
*Rückgabe:* `true`, wenn mindestens ein Cue gelesen wurde.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<srtload,srtLoad>>, <<discgetsubtitletracks,discGetSubtitleTracks>>, <<srtenable,srtEnable>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Untertitel, die der Rip mitgebracht hat, mit einer .srt neben dem Spiel als Rückfallebene.
|
|
if discGetSubtitleTracks() > 0 then
|
|
srtLoadTrack(0)
|
|
else
|
|
srtLoad(DIR .. "subtitles.srt")
|
|
end
|
|
srtEnable(true)
|
|
----
|
|
|
|
[#srtposition]
|
|
==== srtPosition
|
|
|
|
[source,text]
|
|
----
|
|
srtPosition(percent)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Wo ein Untertitel sitzt, in Prozent der Bildhöhe von oben, von 1 bis 95. Der Standard ist 80. Eine Zahl außerhalb dieses Bereichs wird ignoriert, wie Hypseus sie ignoriert, statt begrenzt oder abgelehnt zu werden.
|
|
|
|
Die Höhe gilt sofort für den Cue auf dem Bildschirm und für jeden Cue danach. <<overlaybanner,`overlayBanner`>> trägt seine eigene Höhe und ist nicht betroffen.
|
|
|
|
*Parameter:*
|
|
|
|
* `percent` -- Zahl; 1 bis 95.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<srtload,srtLoad>>, <<subtitles,Untertitel>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Zeilen von der Punktetafel am unteren Rand fernhalten.
|
|
srtPosition(70)
|
|
----
|
|
|
|
[#onlinescores]
|
|
=== Online-Highscores
|
|
|
|
Ein Spiel meldet eine Punktzahl mit einer Zeile:
|
|
|
|
[source,lua]
|
|
----
|
|
scoreSubmit(playerScore)
|
|
----
|
|
|
|
Alles andere wurde einmal im Menü eingerichtet: welcher Dienst, welches Konto und
|
|
unter welchem Namen der Spieler erscheint (siehe
|
|
xref:Manual.adoc#onlineservice[Online: Konten, Spiele und Punkte]). Ein Spiel braucht nur eine
|
|
`GAME_ID` in seinem Eintrag in `games.dat` -- eine UUID, unter der die Bestenliste geführt wird. Siehe
|
|
<<singegetgameid,singeGetGameId>> für das Format.
|
|
|
|
|===
|
|
| Aufruf | Was er tut
|
|
|
|
| `scoreBegin()`
|
|
| Meldet, dass ein Durchgang beginnt. Optional, aber den Aufruf wert: siehe unten.
|
|
|
|
| `scoreSubmit(value [, board [, meta]])`
|
|
| Reiht eine Punktzahl in die Warteschlange ein. Gibt `false` zurück, wenn der Automat nicht angemeldet ist oder das Spiel keine `GAME_ID` hat.
|
|
|
|
| `scoreLowestWins([board])`
|
|
| Legt fest, dass eine Bestenliste die niedrigste Punktzahl zuerst einordnet. Ruf es vor der ersten Punktzahl der Bestenliste auf.
|
|
|
|
| `scoreBoard(onDone [, board])`
|
|
| Holt die Bestenliste. `onDone(ok, result)` mit `result.top` (eine Liste von `{name, value}`),
|
|
`result.standing` (`value` und `rank` dieses Spielers und die Anzahl der `players`) und
|
|
`result.order`, `"highest"` oder `"lowest"`, je nachdem, wie die Bestenliste ordnet. Ein Automat, der nicht
|
|
angemeldet ist, kann sie auch holen: siehe unten.
|
|
|
|
| `scorePlayerName()`
|
|
| Der Name, unter dem dieser Automat meldet, oder `nil`, wenn noch keiner gewählt wurde.
|
|
|
|
| `scoreSignedIn()`
|
|
| Ob dieser Automat bei einem Online-Konto angemeldet ist.
|
|
|
|
| `scoreWaiting()`
|
|
| Wie viele Punktzahlen in der Warteschlange stehen und noch nicht gesendet sind.
|
|
|
|
| `scoreUpdate()`
|
|
| Einmal pro Frame, aus `onOverlayUpdate`. Ohne es geht nichts voran.
|
|
|===
|
|
|
|
**Eine Punktzahl wird eingereiht, nicht gesendet.** Sie wird auf die Festplatte geschrieben und dem Dienst
|
|
übergeben, wenn eine Verbindung besteht, denn ein Automat ist oft offline, und ein Spiel darf
|
|
nie an der Leitung hängen bleiben, um seine eigene Bestenliste zu zeigen. Eine eingereihte Punktzahl übersteht es,
|
|
wenn der Rechner ausgeschaltet wird.
|
|
|
|
**Jeder kann eine Bestenliste lesen.** Ein Automat, der nicht angemeldet ist, holt trotzdem die
|
|
Bestenliste eines Spiels, das der Dienst allen anbietet, ohne `standing`, weil es keinen
|
|
Spieler gibt, der irgendwo platziert sein könnte; so kann ein Spiel zeigen, wohin es melden würde,
|
|
und den Spieler bitten, sich anzumelden. `scoreSignedIn()` unterscheidet die beiden Fälle: Ein Automat,
|
|
der angemeldet ist, aber noch keinen Namen gewählt hat, hat ebenfalls keinen `scorePlayerName()`, und
|
|
ihn zum Anmelden aufzufordern wäre falsch. Ein Spiel, das der Dienst bestimmten
|
|
Konten vorbehält, hat keine Bestenliste für einen Automaten, der bei keinem davon angemeldet ist; das
|
|
Holen schlägt fehl, wie es bei einem Spiel fehlschlüge, das der Dienst nicht kennt. Melden braucht
|
|
immer ein Konto.
|
|
|
|
**Ein Spiel kann mehr als eine Bestenliste haben.** Gib einen Namen für die Bestenliste an, um jeden Spielmodus
|
|
getrennt zu halten -- ein Zeitrennen getrennt von einer Punktesumme, leicht getrennt von
|
|
schwer -- und lies sie mit demselben Namen zurück. Bestenlisten werden auch nach dem Skript getrennt,
|
|
das gerade läuft: Ohne Namen kommt eine Punktzahl auf die Bestenliste, die nach dem
|
|
Skript benannt ist, seinem Dateinamen ohne Endung, also meldet `Collection/Raid.singe`
|
|
an `raid`; mit einem Namen kommt der Name des Skripts zuerst, also meldet
|
|
`scoreSubmit(points, "hard")` aus diesem Skript an `raid.hard`.
|
|
Jeder Titel in einer `.game` trägt dieselbe `GAME_ID`, und dies ist es, was die
|
|
Spiele einer Sammlung auf eigenen Bestenlisten hält, selbst wenn zwei von ihnen ihre
|
|
Modi gleich benennen, während ein Spiel, das zweimal aufgeführt ist (etwa in zwei Sprachen: zwei Einträge
|
|
in `games.dat`, ein Skript), einen einzigen Satz behält. Namen von Bestenlisten sind kleingeschrieben und enthalten
|
|
Buchstaben, Ziffern, `-`, `_` und `.`; alles andere wird zu `-`. Der ganze Name hat
|
|
höchstens 32 Zeichen, und der Teil des Skripts wird zuerst gekürzt, damit der Name des Modus
|
|
erhalten bleibt.
|
|
|
|
**Eine Bestenliste kann die niedrigste Punktzahl zuerst einordnen.** Schläge, Tode, Sekunden: Manche
|
|
Spiele zählen etwas, von dem ein Spieler weniger haben will. Ruf `scoreLowestWins` mit dem
|
|
Namen der Bestenliste auf, so wie `scoreSubmit` ihn nimmt, und zwar vor ihrer ersten Punktzahl; dann laufen jede
|
|
an diese Bestenliste gesendete Punktzahl und jedes Holen von ihr in diese Richtung. Die erste Punktzahl einer Bestenliste
|
|
legt ihre Richtung für immer fest, sodass keine Bestenliste je in beide Richtungen geordnet wird: Eine Punktzahl, die
|
|
danach in die andere Richtung gesendet wird, wird abgelehnt, und die Warteschlange verwirft sie, statt es
|
|
erneut zu versuchen. Auf einer solchen Bestenliste ist null eine Punktzahl, und ebenso eine Zahl unter null, eine
|
|
Runde unter Par.
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Golfspiel: Die wenigsten Schläge gewinnen, und Par ist eine eigene Bestenliste.
|
|
scoreLowestWins("strokes")
|
|
scoreLowestWins("par")
|
|
|
|
function roundOver(strokes, coursePar)
|
|
scoreSubmit(strokes, "strokes")
|
|
scoreSubmit(strokes - coursePar, "par") -- -2 ist zwei unter Par
|
|
end
|
|
----
|
|
|
|
**Der Name auf einer Bestenliste ist ein Handle, nie eine Adresse.** Er wird im
|
|
Tool Online Account gewählt, getrennt vom Konto geführt und ist das Einzige an einem
|
|
Spieler, das andere Spieler sehen. Drei bis zwanzig Zeichen aus Buchstaben, Ziffern,
|
|
Bindestrichen und Unterstrichen, beginnend und endend mit einem Buchstaben oder einer Ziffer -- keine
|
|
Leerzeichen, keine Punkte und kein `@`, sodass ein Handle nicht einmal wie eine E-Mail-Adresse aussehen kann.
|
|
Groß- und Kleinschreibung zählt für die Eindeutigkeit nicht, also sind `Ann` und `ANN` ein Name.
|
|
|
|
**Ein Handle wird einmal gewählt.** Freies Umbenennen hieße, dass die Geschichte einer Bestenliste
|
|
eigentlich niemandem gehört, also kann ein Spieler sein Handle danach nicht ändern, und eine E-Mail-Adresse
|
|
lässt sich überhaupt nicht ändern.
|
|
|
|
Ein Handle, das niemand beanspruchen sollte (`admin`, `staff`), oder eines mit Wörtern, die auf
|
|
einem Bildschirm in einem öffentlichen Raum nichts zu suchen haben, wird abgelehnt, wenn es gewählt wird. Eine Punktzahl wird
|
|
abgelehnt, bis ein Name gewählt wurde, denn eine Punktzahl, auf der niemandes Name steht,
|
|
steht in keinem brauchbaren Sinn auf einer Bestenliste.
|
|
|
|
**Was eine Punktzahl bedeutet.** Der Automat ist der Rechner des Spielers, eine gemeldete
|
|
Punktzahl ist also eine *Behauptung* -- nichts, was der Client tun kann, macht mehr daraus, und
|
|
sie mit einem im Client mitgelieferten Geheimnis zu signieren, sähe nur nach Sicherheit aus.
|
|
Deshalb gibt hier nichts vor, eine zu prüfen, und ein Spiel sollte nicht so geschrieben werden,
|
|
als täte etwas anderes es.
|
|
|
|
**`scoreBegin()` aufzurufen lohnt sich** für das Eine, das keine Behauptung ist. Ruf es
|
|
auf, wenn ein Durchgang beginnt; die Punktzahl, die folgt, trägt mit, wie lange der Durchgang gedauert hat,
|
|
gemessen am anderen Ende statt auf diesem Rechner. Diese Zahl ist nicht
|
|
durch den Rechner des Spielers gegangen, und genau das macht sie wertvoll.
|
|
|
|
Nichts wird deswegen abgelehnt, also kostet der Aufruf ein Spiel eine Zeile und kann nie
|
|
eine legitime Punktzahl abweisen. Ein Automat, der offline ist, wenn ein Durchgang beginnt,
|
|
zeichnet einfach keine Zeit auf, was ehrlich ist und nicht kaputt.
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
function startLevel()
|
|
scoreBegin() -- die Zeitmessung beginnt jetzt, am anderen Ende
|
|
...
|
|
end
|
|
----
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Am Ende eines Spiels die Punktzahl melden und dann zeigen, wo sie gelandet ist.
|
|
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]
|
|
=== Statistik
|
|
|
|
Das Overlay für Entwickler, in der oberen linken Ecke: wie lange der Frame gedauert hat und
|
|
sein schlechtester Wert der letzten zwei Sekunden, die Bildrate, der Speicher von Lua, wie viele Sprites,
|
|
Sounds und Timer am Leben sind, wie viele Knoten der Szene gezeichnet wurden und in wie
|
|
vielen Batches, wie viel Texturspeicher sie belegt und was die Disc gerade tut.
|
|
|
|
Es wird mit der eigenen Debug-Schrift von SDL gezeichnet, braucht also keine Schriftdatei, kein Asset und kein
|
|
GUI-Dokument, und es erscheint sogar auf einem Rechner, auf dem das 3D-Gerät nie
|
|
hochgekommen ist. Es wird gezeichnet, nachdem <<singescreenshot,`singeScreenshot`>> sein Bild aufgenommen hat,
|
|
landet also nie in einem Screenshot.
|
|
|
|
Lass es in einem ausgelieferten Spiel aus. Nichts hindert dich daran, es in deinem
|
|
eigenen Service-Menü hinter eine Taste zu legen.
|
|
|
|
[#statsenable]
|
|
==== statsEnable
|
|
|
|
[source,text]
|
|
----
|
|
statsEnable(enabled)
|
|
----
|
|
|
|
Zeigt das Overlay an oder blendet es aus. Solange es angezeigt wird, wird das Bild in jedem Frame neu gezeichnet, die Zahlen bewegen sich also weiter, auch wenn sich sonst nichts auf dem Bildschirm bewegt.
|
|
|
|
*Parameter:*
|
|
|
|
* `enabled` -- Boolean.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<statsisenabled,statsIsEnabled>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Entwicklertaste hinter einem Flag, damit das ausgelieferte Spiel keine solche Taste hat.
|
|
function onInputPressed(key)
|
|
if DEVELOPER and key == SWITCH_SERVICE then
|
|
statsEnable(not statsIsEnabled())
|
|
end
|
|
end
|
|
----
|
|
|
|
[#statsisenabled]
|
|
==== statsIsEnabled
|
|
|
|
[source,text]
|
|
----
|
|
shown = statsIsEnabled()
|
|
----
|
|
|
|
Ob das Overlay eingeschaltet ist.
|
|
|
|
*Parameter:*
|
|
|
|
* Keine.
|
|
|
|
*Rückgabe:* `true` oder `false`.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#terrain]
|
|
=== Gelände
|
|
|
|
Höhenkarten-Boden, gemacht mit `meshHeightmap`, das ein Graustufenbild in ein Mesh mit einem Vertex pro Pixel verwandelt; `terrainGetHeight` liest dieses Mesh über den Knoten, der es trägt, wieder aus. Positionen und Höhen sind in Welteinheiten, und Position und Maßstab des Knotens werden berücksichtigt. Siehe Gelände in <<scenes3d,3D-Szenen>>.
|
|
|
|
[#terraingetheight]
|
|
==== terrainGetHeight
|
|
|
|
[source,text]
|
|
----
|
|
height = terrainGetHeight(node, x, z)
|
|
----
|
|
|
|
Die Welthöhe des Höhenkarten-Meshes auf `node` an der Weltposition `x, z`, interpoliert zwischen den nächsten vier Abtastwerten, oder `nil` jenseits seiner Ränder oder wenn das Mesh des Knotens keine Höhenkarte ist. Weltposition und Maßstab des Knotens zählen; seine Drehung wird ignoriert, da Gelände flach liegt. Verwende es, um Bäume zu platzieren, Dinge auf dem Boden erscheinen zu lassen oder für eine Kamera, die der Oberfläche folgt, ohne die Kosten eines Raycasts. Beendet das Skript bei einem Knoten-Handle, das nicht gültig ist.
|
|
|
|
*Parameter:*
|
|
|
|
* `node` -- der Knoten, dessen Mesh aus `meshHeightmap` kam.
|
|
* `x, z` -- die abzutastende Weltposition.
|
|
|
|
*Rückgabe:* Eine Zahl oder `nil` außerhalb des Geländes.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<meshheightmap,meshHeightmap>>, <<physicsraycast,physicsRaycast>>, <<nodesetposition,nodeSetPosition>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Bäume auf den Hügeln und eine Kamera, die nie unter den Boden taucht.
|
|
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]
|
|
=== Timer
|
|
|
|
Etwas, das später geschehen soll, ohne dass du selbst Frames zählst. Timer laufen auf der
|
|
Uhr der Engine, die unter `--deterministic` die virtuelle ist, sodass sich ein zeitgesteuertes
|
|
Spiel zwischen zwei Läufen genau wiederholt. Sie feuern mit den übrigen Callbacks des Frames,
|
|
auf demselben Thread wie alles andere -- es gibt nichts zu sperren und
|
|
nichts, das feuern kann, während dein Skript mitten in etwas steckt.
|
|
|
|
Ein Timer, der in Rückstand geraten ist -- ein langes Laden, ein Haltepunkt -- feuert einmal, wenn
|
|
das Spiel aufholt, statt die zwanzig Male zu feuern, die er verpasst hat.
|
|
|
|
Alles wird vergessen, wenn das Skript neu lädt, ein Timer aus dem letzten Lauf
|
|
feuert also nie in den nächsten hinein.
|
|
|
|
[#timerafter]
|
|
==== timerAfter
|
|
|
|
[source,text]
|
|
----
|
|
id = timerAfter(milliseconds, function)
|
|
----
|
|
|
|
Ruft die Funktion einmal auf, später. Die Funktion bekommt die eigene id des Timers, sodass eine Funktion mehreren Timern dienen kann.
|
|
|
|
*Parameter:*
|
|
|
|
* `milliseconds` -- Zahl; null oder weniger feuert im nächsten Frame.
|
|
* `function` -- aufgerufen als `function(id)`.
|
|
|
|
*Rückgabe:* eine Zahl; die id des Timers.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<timercancel,timerCancel>>, <<timerevery,timerEvery>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Explosion ausklingen lassen, bevor der Game-Over-Bildschirm kommt.
|
|
soundPlay(bang)
|
|
timerAfter(1500, function()
|
|
showGameOver()
|
|
end)
|
|
----
|
|
|
|
[#timercancel]
|
|
==== timerCancel
|
|
|
|
[source,text]
|
|
----
|
|
timerCancel(id)
|
|
----
|
|
|
|
Stoppt einen Timer. Einen abzubrechen, der schon gefeuert hat, ist kein Fehler, also muss nichts vorher prüfen.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Zahl; aus `timerAfter` oder `timerEvery`.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#timerevery]
|
|
==== timerEvery
|
|
|
|
[source,text]
|
|
----
|
|
id = timerEvery(milliseconds, function)
|
|
----
|
|
|
|
Ruft die Funktion immer wieder auf, bis der Timer abgebrochen wird.
|
|
|
|
*Parameter:*
|
|
|
|
* `milliseconds` -- Zahl; wie lange zwischen den Aufrufen.
|
|
* `function` -- aufgerufen als `function(id)`.
|
|
|
|
*Rückgabe:* eine Zahl; die id des Timers.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<timercancel,timerCancel>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Den Countdown einmal pro Sekunde weiterzählen und stoppen, wenn er abgelaufen ist.
|
|
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)
|
|
----
|
|
|
|
Ob der Timer noch einmal feuern wird: `true` für einen wiederholenden, bis er abgebrochen wird, `false` für einen einmaligen, der schon ausgelöst hat.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Zahl.
|
|
|
|
*Rückgabe:* `true` oder `false`.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#tween]
|
|
=== Tween
|
|
|
|
Eine Zahl über die Zeit von einem Wert zu einem anderen bewegen, entlang einer Kurve. Ein- und Ausblendungen, Gleitbewegungen,
|
|
Kamerafahrten, ein Lebensbalken, der sich leert, ein Menü, das hereingleitet -- alles dasselbe.
|
|
|
|
`tweenValue` gibt dir die Zahl und lässt dich entscheiden, was sie bedeutet. Das ist
|
|
Absicht: Die Engine hat Knoten, Sprites, GUI-Elemente, Materialien und Lichter,
|
|
und ein Tween, der jedes davon kennte, brächte fünf Sätze von Aufrufen mit, die im Gleichschritt
|
|
zu halten wären. Einer, der eine Zahl übergibt, bringt keinen mit, und er funktioniert mit Dingen, von denen die Engine
|
|
nie gehört hat.
|
|
|
|
Die Easing-Kurven sind die üblichen. `EASE_LINEAR` ist gar keine Kurve;
|
|
`EASE_QUAD_*`, `EASE_CUBIC_*`, `EASE_SINE_*` und `EASE_EXPO_*` reichen von sanft
|
|
bis scharf; `EASE_BACK_*` schießt über das Ziel hinaus und kommt zurück; `EASE_ELASTIC_*` federt;
|
|
`EASE_BOUNCE_*` fällt und springt auf. Jede gibt es als `_IN` (langsam am Anfang),
|
|
`_OUT` (langsam am Ende) und `_IN_OUT` (beides).
|
|
|
|
[#tweencancel]
|
|
==== tweenCancel
|
|
|
|
[source,text]
|
|
----
|
|
tweenCancel(id)
|
|
----
|
|
|
|
Stoppt einen Tween, wo er gerade ist. Keine der beiden Funktionen wird wieder aufgerufen, und der Wert bleibt, wo immer er angekommen war.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Zahl; aus `tweenValue`.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#tweenisactive]
|
|
==== tweenIsActive
|
|
|
|
[source,text]
|
|
----
|
|
running = tweenIsActive(id)
|
|
----
|
|
|
|
Ob der Tween noch läuft.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Zahl.
|
|
|
|
*Rückgabe:* `true` oder `false`.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#tweenvalue]
|
|
==== tweenValue
|
|
|
|
[source,text]
|
|
----
|
|
id = tweenValue(from, to, milliseconds, easing, onUpdate [, onDone])
|
|
----
|
|
|
|
Bewegt eine Zahl über `milliseconds` von `from` nach `to` und ruft dabei in jedem Frame `onUpdate(value, progress)` auf -- `progress` läuft von 0 bis 1 und ignoriert die Kurve -- und am Ende einmal `onDone()`. Das letzte `onUpdate` landet immer genau auf `to`, sodass nichts ein Pixel zu kurz endet.
|
|
|
|
*Parameter:*
|
|
|
|
* `from`, `to` -- Zahlen.
|
|
* `milliseconds` -- Zahl; null oder weniger kommt im nächsten Frame an.
|
|
* `easing` -- einer der `EASE_`-Werte.
|
|
* `onUpdate` -- aufgerufen als `function(value, progress)`.
|
|
* `onDone` -- optional; ohne Argumente aufgerufen, wenn er fertig ist.
|
|
|
|
*Rückgabe:* eine Zahl; die id des Tweens.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<tweencancel,tweenCancel>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Den Titel hereingleiten lassen und gleichzeitig das Overlay einblenden.
|
|
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]
|
|
=== Fahrzeug
|
|
|
|
Autos, Motorräder, Panzer und Boote, gebaut auf einem Chassis-Körper (siehe <<physics,Physik>>, Fahrzeuge und Wasser). Ein Fahrzeug hat kein eigenes Handle: Es wird über seinen Chassis-Knoten angesprochen, der bereits einen `BODY_DYNAMIC`-Körper aus `bodyNew` tragen muss, und ein Knoten ohne Fahrzeug beendet in jedem Aufruf nach `vehicleNew` das Skript. Das Chassis blickt nach -Z mit Y oben; Entfernungen sind Welteinheiten, Kräfte Newton, Drehmomente Newtonmeter und Motordrehzahlen Umdrehungen pro Minute. Jeder Abstimmungsaufruf markiert das Fahrzeug für einen Neuaufbau beim nächsten Physikschritt, um die Radpositionen herum, die beim Hinzufügen der Räder festgehalten wurden. Bis zu 16 Fahrzeuge gleichzeitig, jedes mit bis zu 16 Rädern.
|
|
|
|
[#vehiclenew]
|
|
==== vehicleNew
|
|
|
|
[source,text]
|
|
----
|
|
vehicleNew(node, kind)
|
|
----
|
|
|
|
Macht aus dem dynamischen Körper auf `node` das Chassis eines Fahrzeugs. Ein Auto, Motorrad oder Panzer braucht mindestens zwei Räder aus `vehicleAddWheel`, bevor der nächste Physikschritt es baut; ein Boot hat keine Räder und wird von einem Propeller geschoben, der nur unter einer mit `bodySetWater` gemachten Oberfläche wirkt. Das neue Fahrzeug beginnt mit den Standardwerten: ein Motor mit 500 Nm, der bei 6000 U/min sein Maximum erreicht und im Leerlauf mit 1000 dreht, das Fünfgang-Automatikgetriebe von Jolt, 35 Grad Lenkeinschlag, Bremsen mit 1500 Nm und eine Handbremse mit 4000 Nm, eine Federung mit 1,5 Hz bei 0,5 Dämpfung und keine Stabilisatoren. Ein erneuter Aufruf auf demselben Knoten verwirft zuerst das alte Fahrzeug und seine Räder. Beendet das Skript bei einer unbekannten Art, einem Knoten, dessen Körper nicht dynamisch ist, einer Art mit Rädern in einer 2D-Welt oder wenn schon 16 Fahrzeuge existieren.
|
|
|
|
*Parameter:*
|
|
|
|
* `node` -- der Chassis-Knoten, der einen `BODY_DYNAMIC`-Körper trägt.
|
|
* `kind` -- `VEHICLE_CAR` (beliebig viele Achsen), `VEHICLE_MOTORCYCLE` (zwei Räder hintereinander, die sich in Kurven legen), `VEHICLE_TANK` (Räder auf zwei Ketten, wendet auf der Stelle) oder `VEHICLE_BOAT` (ein schwimmender Rumpf mit Propeller und Ruder).
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<vehicleaddwheel,vehicleAddWheel>>, <<vehicledrive,vehicleDrive>>, <<vehiclesetthrust,vehicleSetThrust>>, <<bodynew,bodyNew>>
|
|
|
|
.Beispiel
|
|
[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)
|
|
----
|
|
|
|
Entfernt das Fahrzeug von seinem Chassis: den Rad-Constraint, die Fahrereingabe und die ganze Abstimmung. Der Körper bleibt ein gewöhnlicher dynamischer Körper, ein Wrack rutscht und überschlägt sich also weiter, und die Radknoten bleiben, wo der letzte Schritt sie hingestellt hat; nichts bewegt sie wieder, außer das Skript tut es. Ein Knoten ohne Fahrzeug beendet das Skript.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<vehiclenew,vehicleNew>>, <<bodydelete,bodyDelete>>
|
|
|
|
.Beispiel
|
|
[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) -- Das Chassis überschlägt sich als Körper weiter
|
|
soundPlay(crashSound)
|
|
emitterStart(smoke)
|
|
end
|
|
end
|
|
----
|
|
|
|
[#vehicleaddwheel]
|
|
==== vehicleAddWheel
|
|
|
|
[source,text]
|
|
----
|
|
index = vehicleAddWheel(node, wheelNode, radius, width, suspension)
|
|
----
|
|
|
|
Fügt dem Fahrzeug ein Rad hinzu, dort, wo `wheelNode` relativ zum Chassis steht, wenn das Fahrzeug das nächste Mal gebaut wird. Die Federung hängt das Rad unbelastet `suspension` Einheiten unter diesen Punkt und federt bis auf die Hälfte davon ein. Von da an schreibt die Engine nach jedem Schritt die Welttransformation des Radknotens, mit X als Achse und Y oben, also gehört der sichtbare Reifen auf einen Kindknoten: ein um 90 Grad um Z gedrehter `meshCylinder`. Räder, die entlang der Front vor der durchschnittlichen Radposition liegen, zählen als Vorderräder: Sie lenken standardmäßig und haben keine Handbremse. Ein Panzer ordnet seine Räder der linken oder rechten Kette zu, je nachdem, auf welcher Seite des Chassis sie sitzen, und braucht mindestens eines auf jeder Seite. Werte unter `0.001` werden darauf angehoben. Beendet das Skript beim 17. Rad.
|
|
|
|
*Parameter:*
|
|
|
|
* `node` -- der Chassis-Knoten.
|
|
* `wheelNode` -- der Knoten, an dem das Rad hängt; seine Position relativ zum Chassis ist der Befestigungspunkt, und die Engine stellt ihn von nun an.
|
|
* `radius` -- der Radius des Rads, in Welteinheiten.
|
|
* `width` -- die Breite des Rads, in Welteinheiten.
|
|
* `suspension` -- die Länge der Federung bei voller Ausdehnung, in Welteinheiten; sie federt bis auf die Hälfte ein.
|
|
|
|
*Rückgabe:* Der Index des Rads, ab `0`, für `vehicleSetWheel`, `vehicleIsWheelOnGround` und `vehicleGetWheelSlip`.
|
|
|
|
*Hinweise:* Ein Fahrzeug wird erst gebaut, wenn es zwei Räder hat, und ein Auto oder Motorrad braucht außerdem ein angetriebenes Rad; bis dahin steht es als einfacher Körper da, und die Fahraufrufe tun nichts.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<vehiclesetwheel,vehicleSetWheel>>, <<vehiclesetsuspension,vehicleSetSuspension>>, <<nodefind,nodeFind>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Lkw-Modell trägt leere Knoten, die nach seinen Rädern benannt sind.
|
|
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)
|
|
----
|
|
|
|
Stellt den Propeller eines Boots ein: die Kraft, mit der er bei Vollgas schiebt, Standard `2000` Newton, und den Punkt im Bezugssystem des Rumpfs, an dem er schiebt, Standard `0, -0.2, 1`, knapp unter Deck am Heck. Der Schub wirkt entlang der Front des Rumpfs und nur, solange dieser Punkt unter einer Wasseroberfläche ist; derselbe Punkt entscheidet, ob Ruder und Bremse wirken. Eine negative Kraft wird zu `0`. Arten mit Rädern speichern die Werte und ignorieren sie.
|
|
|
|
*Parameter:*
|
|
|
|
* `node` -- der Knoten des Rumpfs.
|
|
* `maxForce` -- der Schub bei Vollgas, in Newton.
|
|
* `x`, `y`, `z` -- wo der Propeller sitzt, im Bezugssystem des Rumpfs; das Heck ist +Z.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<vehiclesetrudder,vehicleSetRudder>>, <<bodysetwater,bodySetWater>>, <<bodysetbuoyancy,bodySetBuoyancy>>
|
|
|
|
.Beispiel
|
|
[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) -- Liegt hoch im Becken
|
|
vehicleNew(raft, VEHICLE_BOAT)
|
|
vehicleSetThrust(raft, 600, 0, -0.1, 1.2) -- Ein Außenborder, ans Heck gehängt
|
|
vehicleSetRudder(raft, 300)
|
|
----
|
|
|
|
[#vehiclesetwheel]
|
|
==== vehicleSetWheel
|
|
|
|
[source,text]
|
|
----
|
|
vehicleSetWheel(node, index, steered, driven)
|
|
----
|
|
|
|
Legt fest, ob ein Rad mit der Lenkeingabe einschlägt und ob der Motor es antreibt, und ersetzt die Standardwerte für dieses Rad: Vorderräder lenken, und jedes Rad wird angetrieben außer dem vorderen eines Motorrads. Angetriebene Räder werden quer über das Chassis paarweise zu Differenzialen zusammengefasst, eines pro Achse, und das Drehmoment des Motors wird gleichmäßig auf die Achsen verteilt; ein angetriebenes Rad ohne Partner ist eine eigene Achse. Ein Panzer ignoriert beide Flags, da seine Ketten jedes Rad antreiben und den Rumpf drehen. Wirkt beim Neuaufbau im nächsten Schritt. Beendet das Skript bei einem Index, für den das Fahrzeug kein Rad hat.
|
|
|
|
*Parameter:*
|
|
|
|
* `node` -- der Chassis-Knoten.
|
|
* `index` -- der Index des Rads aus `vehicleAddWheel`.
|
|
* `steered` -- `true`, damit das Rad bei vollem Einschlag bis zu `vehicleSetSteering` einschlägt.
|
|
* `driven` -- `true`, damit der Motor es dreht.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<vehicleaddwheel,vehicleAddWheel>>, <<vehiclesetsteering,vehicleSetSteering>>, <<vehiclesetantiroll,vehicleSetAntiRoll>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Sportwagen mit Hinterradantrieb: Die Vorderräder lenken nur.
|
|
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)
|
|
----
|
|
|
|
Stellt den Motor ein: Spitzendrehmoment in Newtonmetern, die Drehzahlgrenze und die Leerlaufdrehzahl in Umdrehungen pro Minute. Die Standardwerte sind `500`, `6000` und `1000`, und die Leerlaufdrehzahl ist wieder `1000`, wenn sie weggelassen wird. Das Drehmoment wird auf `1` oder mehr gehalten, die Drehzahlgrenze auf `100` oder mehr und die Leerlaufdrehzahl zwischen `1` und der Drehzahlgrenze. Ein Boot hat keinen Motor und ignoriert den Aufruf. Wirkt beim Neuaufbau im nächsten Schritt.
|
|
|
|
*Parameter:*
|
|
|
|
* `node` -- der Chassis-Knoten.
|
|
* `maxTorque` -- Spitzendrehmoment, in Newtonmetern.
|
|
* `maxRpm` -- die Drehzahlgrenze, in Umdrehungen pro Minute.
|
|
* `minRpm` -- die Leerlaufdrehzahl, in Umdrehungen pro Minute; Standard `1000`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<vehiclesetgears,vehicleSetGears>>, <<vehiclegetrpm,vehicleGetRpm>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
vehicleSetEngine(tank, 1500, 4000, 800) -- Ein Diesel: Drehmoment untenherum
|
|
vehicleSetEngine(bike, 150, 9000, 1200) -- Ein Schreihals
|
|
|
|
function onOverlayUpdate()
|
|
local rpm = vehicleGetRpm(bike)
|
|
overlayClear()
|
|
fontPrint(20, 20, string.format("%5.0f rpm", rpm))
|
|
if rpm > 8500 then
|
|
spriteDraw(shiftLight, 200, 20) -- Zeit zum Hochschalten
|
|
end
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#vehiclesetgears]
|
|
==== vehicleSetGears
|
|
|
|
[source,text]
|
|
----
|
|
vehicleSetGears(node, { ratio, ... })
|
|
vehicleSetGears(node, { ratio, ... }, reverseRatio)
|
|
vehicleSetGears(node, { ratio, ... }, reverseRatio, automatic)
|
|
----
|
|
|
|
Stellt das Getriebe ein: ein bis acht Vorwärtsübersetzungen, den ersten Gang zuerst, die Übersetzung des Rückwärtsgangs (Standard `2.9`, als positive Zahl angegeben) und ob das Getriebe selbst schaltet (Standard `true`). Ein Fahrzeug, das diesen Aufruf nie bekommt, fährt mit dem Fünfganggetriebe von Jolt, `2.66`, `1.78`, `1.3`, `1.0` und `0.74`, mit demselben Rückwärtsgang. Ist `automatic` false, schaltet das Getriebe nicht mehr von selbst, und es gibt keinen Aufruf, um es aus dem Skript zu schalten. Ein Boot hat kein Getriebe und ignoriert den Aufruf. Wirkt beim Neuaufbau im nächsten Schritt. Beendet das Skript, wenn die Tabelle leer ist oder mehr als acht Übersetzungen enthält.
|
|
|
|
*Parameter:*
|
|
|
|
* `node` -- der Chassis-Knoten.
|
|
* `{ ratio, ... }` -- Übersetzungen der Vorwärtsgänge, Motorumdrehungen pro Getriebeumdrehung, der erste Gang zuerst; eine bis acht davon.
|
|
* `reverseRatio` -- die Übersetzung des Rückwärtsgangs; Standard `2.9`.
|
|
* `automatic` -- `true` (Standard), damit das Getriebe selbst schaltet.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<vehiclegetgear,vehicleGetGear>>, <<vehiclesetengine,vehicleSetEngine>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein eng gestuftes Sechsganggetriebe für den Rennwagen, ein lang übersetztes Dreiganggetriebe für den Bus.
|
|
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)
|
|
----
|
|
|
|
Stellt das Ruder eines Boots ein: das Drehmoment um die Hochachse des Rumpfs bei voller Lenkeingabe, Standard `800` Newtonmeter. Das Ruder greift mit der Geschwindigkeit, ein Fünftel des Drehmoments im Stand und das volle ab 3 Welteinheiten pro Sekunde, und es dreht andersherum, während das Boot rückwärts fährt. Es wirkt nur, solange der Propellerpunkt unter Wasser ist. Ein negatives Drehmoment wird zu `0`; Arten mit Rädern ignorieren den Aufruf.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<vehiclesetthrust,vehicleSetThrust>>, <<vehicledrive,vehicleDrive>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
vehicleNew(patrolBoat, VEHICLE_BOAT)
|
|
vehicleSetThrust(patrolBoat, 4000, 0, -0.4, 3.5)
|
|
vehicleSetRudder(patrolBoat, 2500) -- Schwerer Rumpf, großes Ruder
|
|
|
|
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)
|
|
----
|
|
|
|
Stellt die Feder jedes Rads ein: ihre Eigenfrequenz in Hertz (Standard `1.5`, auf `0.1` oder mehr gehalten; höher ist härter) und ihren Dämpfungsgrad (Standard `0.5`; `0` schwingt nach, `1` kommt ohne Überschwingen zur Ruhe). Wirkt beim Neuaufbau im nächsten Schritt, auf alle Räder gleichzeitig; eine Feder pro Rad gibt es nicht. Ein Boot hat keine Federung und ignoriert den Aufruf.
|
|
|
|
*Parameter:*
|
|
|
|
* `node` -- der Chassis-Knoten.
|
|
* `frequency` -- die Eigenfrequenz der Feder, in Hertz.
|
|
* `damping` -- der Dämpfungsgrad, ab `0` aufwärts.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<vehicleaddwheel,vehicleAddWheel>>, <<vehiclesetantiroll,vehicleSetAntiRoll>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Der Buggy schwebt über die Dünen; der Rennwagen bewegt sich kaum auf seinen Federn.
|
|
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)
|
|
----
|
|
|
|
Legt fest, wie weit die gelenkten Räder bei vollem Einschlag einschlagen, in Grad; Standard `35`, begrenzt auf `0` bis `89`. Eine Lenkeingabe von `-1` oder `1` aus `vehicleDrive` erreicht diesen Winkel, kleinere Eingaben einen entsprechenden Anteil davon. Wirkt beim Neuaufbau im nächsten Schritt. Ein Panzer lenkt mit seinen Ketten und ein Boot mit seinem Ruder, also ignorieren beide den Aufruf.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<vehiclesetwheel,vehicleSetWheel>>, <<vehicledrive,vehicleDrive>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Gokart wendet auf der Stelle; der Lkw braucht einen weiten Bogen.
|
|
vehicleSetSteering(kart, 45)
|
|
vehicleSetSteering(truck, 25)
|
|
vehicleSetSteering(bus, 20)
|
|
if difficulty == "arcade" then
|
|
vehicleSetSteering(truck, 35) -- Nachsichtiges Fahrverhalten
|
|
end
|
|
----
|
|
|
|
[#vehiclesetbrakes]
|
|
==== vehicleSetBrakes
|
|
|
|
[source,text]
|
|
----
|
|
vehicleSetBrakes(node, brakeTorque, handBrakeTorque)
|
|
----
|
|
|
|
Stellt das Bremsmoment ein, das jedes Rad bei voll getretenem Pedal aufbringen kann, Standard `1500` Newtonmeter, und das Moment der Handbremse an den Hinterrädern, Standard `4000`; die Vorderräder, also die vor der durchschnittlichen Radposition, haben keine Handbremse. Negative Werte werden zu `0`. Ein Panzer bremst seine Ketten mit den Kettenstandardwerten von Jolt und ignoriert den Aufruf, und ein Boot bremst durch einen Widerstand gegen seine eigene Geschwindigkeit, also ignoriert es den Aufruf ebenfalls. Wirkt beim Neuaufbau im nächsten Schritt.
|
|
|
|
*Parameter:*
|
|
|
|
* `node` -- der Chassis-Knoten.
|
|
* `brakeTorque` -- Bremsmoment der Fußbremse an jedem Rad, in Newtonmetern.
|
|
* `handBrakeTorque` -- Bremsmoment der Handbremse an den Hinterrädern, in Newtonmetern.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<vehicledrive,vehicleDrive>>, <<vehiclegetwheelslip,vehicleGetWheelSlip>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
vehicleSetBrakes(car, 2200, 6000) -- Rennbeläge und eine Handbremse, bereit zum Driften
|
|
|
|
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)
|
|
----
|
|
|
|
Fügt an jeder Achse einen Stabilisator hinzu, in Newton pro Meter; `0` (Standard) baut keinen ein, und ein negativer Wert wird zu `0`. Ein Stabilisator verbindet die beiden angetriebenen Räder einer Achse, eine Achse, deren Räder nicht angetrieben sind, bekommt also keinen, und ein Motorrad hat keinen. Ein paar Hundert halten ein Auto in Kurven flach; zu viel hebt die kurveninneren Räder an. Ein Panzer ignoriert den Aufruf. Wirkt beim Neuaufbau im nächsten Schritt.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<vehiclesetsuspension,vehicleSetSuspension>>, <<vehiclesetwheel,vehicleSetWheel>>
|
|
|
|
.Beispiel
|
|
[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) -- Bleibt flach durch die Schikane
|
|
----
|
|
|
|
[#vehicledrive]
|
|
==== vehicleDrive
|
|
|
|
[source,text]
|
|
----
|
|
vehicleDrive(node, forward, right)
|
|
vehicleDrive(node, forward, right, brake)
|
|
vehicleDrive(node, forward, right, brake, handBrake)
|
|
----
|
|
|
|
Setzt die Eingabe des Fahrers, die bis zum nächsten Aufruf gilt, ruf es also in jedem Frame aus `onOverlayUpdate` auf. Gas und Lenkung reichen von `-1` bis `1`, Bremse und Handbremse von `0` bis `1`; Werte außerhalb werden begrenzt, und weggelassene Eingaben sind `0`. Ein Auto oder Motorrad bekommt alle vier als Fahrereingabe von Jolt, wobei negatives Gas über den Rückwärtsgang zurücksetzt. Ein Panzer nimmt `forward` als sein Gas, und `right` bremst die innere Kette ab; ohne Gas laufen die Ketten gegeneinander, sodass sich der Rumpf auf der Stelle dreht, und die Handbremse wird ignoriert. Ein Boot nimmt `forward` als Gas des Propellers, `right` als Ruder und `brake` als Widerstand gegen die Geschwindigkeit des Rumpfs, ignoriert die Handbremse und tut gar nichts, solange der Propeller außerhalb des Wassers ist. Jede Eingabe ungleich null weckt ein Chassis, das eingeschlafen ist.
|
|
|
|
*Parameter:*
|
|
|
|
* `node` -- der Chassis-Knoten.
|
|
* `forward` -- Gas, `-1` bis `1`; negativ fährt rückwärts.
|
|
* `right` -- Lenkung, `-1` (links) bis `1` (rechts).
|
|
* `brake` -- Fußbremse, `0` bis `1`; Standard `0`.
|
|
* `handBrake` -- Handbremse, `0` bis `1`; Standard `0`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<vehiclegetspeed,vehicleGetSpeed>>, <<vehiclesetbrakes,vehicleSetBrakes>>, <<controllergetaxis,controllerGetAxis>>
|
|
|
|
.Beispiel
|
|
[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 -- Rückwärts, solange A gedrückt ist
|
|
end
|
|
vehicleDrive(car, gas, steer, brake, handBrake)
|
|
return OVERLAY_NOT_UPDATED
|
|
end
|
|
----
|
|
|
|
[#vehiclegetspeed]
|
|
==== vehicleGetSpeed
|
|
|
|
[source,text]
|
|
----
|
|
speed = vehicleGetSpeed(node)
|
|
----
|
|
|
|
Die Geschwindigkeit des Chassis-Körpers entlang seiner Front, in Welteinheiten pro Sekunde, negativ, während er rückwärts fährt; seitliche Bewegung in einem Drift zählt nicht. Funktioniert für jede Art, Boote eingeschlossen, ab dem Moment, in dem `vehicleNew` aufgerufen wird.
|
|
|
|
*Rückgabe:* Eine Zahl.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<vehiclegetrpm,vehicleGetRpm>>, <<vehiclegetgear,vehicleGetGear>>, <<bodygetvelocity,bodyGetVelocity>>
|
|
|
|
.Beispiel
|
|
[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)
|
|
----
|
|
|
|
Die Drehzahl des Motors in Umdrehungen pro Minute, zwischen der Leerlaufdrehzahl und der Drehzahlgrenze aus `vehicleSetEngine`, für einen Drehzahlmesser oder die Tonhöhe einer Motorgeräuschschleife. `0` für ein Boot, das keinen Motor hat, und für ein Fahrzeug, das noch nicht gebaut wurde, weil ihm Räder fehlen.
|
|
|
|
*Rückgabe:* Eine Zahl.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<vehiclesetengine,vehicleSetEngine>>, <<vehiclegetgear,vehicleGetGear>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Drehzahlbalken, der über 6000 rot wird.
|
|
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)
|
|
----
|
|
|
|
Der Gang, den das Getriebe eingelegt hat: `0` im Leerlauf, `-1` im Rückwärtsgang, ab `1` aufwärts für die Vorwärtsgänge in der Reihenfolge, in der `vehicleSetGears` sie angegeben hat. `0` für ein Boot und für ein Fahrzeug, das noch nicht gebaut wurde.
|
|
|
|
*Rückgabe:* Eine Ganzzahl.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<vehiclesetgears,vehicleSetGears>>, <<vehiclegetrpm,vehicleGetRpm>>
|
|
|
|
.Beispiel
|
|
[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)
|
|
----
|
|
|
|
Ob der Cast der Federung dieses Rads in diesem Schritt etwas gefunden hat, gegen das sie drücken kann. `false` für einen Index, für den das Fahrzeug kein Rad hat, und für ein Fahrzeug, das noch nicht gebaut wurde; in keinem der beiden Fälle wird ein Fehler ausgelöst.
|
|
|
|
*Rückgabe:* Wahr oder falsch.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<vehicleaddwheel,vehicleAddWheel>>, <<vehiclegetwheelslip,vehicleGetWheelSlip>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- In der Luft, wenn kein Rad den Boden berührt; eine Landung nach einem Sprung lässt die Kamera wackeln.
|
|
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)
|
|
----
|
|
|
|
Wie stark das Rad entlang seiner Rollrichtung durchdreht oder rutscht, von `0` haftend bis `1` durchdrehend oder blockiert, für Reifenquietschen, Rauch und Bremsspuren. Immer `0` für die Räder eines Panzers, für einen Index, für den das Fahrzeug kein Rad hat, und für ein Fahrzeug, das noch nicht gebaut wurde.
|
|
|
|
*Rückgabe:* Eine Zahl.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<vehicleiswheelonground,vehicleIsWheelOnGround>>, <<vehiclegetspeed,vehicleGetSpeed>>
|
|
|
|
.Beispiel
|
|
[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]
|
|
=== Ansicht
|
|
|
|
Eine Ansicht ist eine zweite Kamera, die in jedem Frame in eine Textur gerendert wird, für einen Monitor, einen Spiegel oder ein Portal in der Szene: `viewNew` erzeugt eine und gibt ein ganzzahliges Handle zurück, `viewSetCamera` hängt sie an einen Knoten, und `materialSetView` zeigt sie auf einem Material. Bis zu vier gibt es gleichzeitig; jede rendert die ganze Szene noch einmal in ihrer eigenen Größe, in Pixeln unabhängig vom Overlay, mit der Projektion der Hauptkamera, den Schatten des Frames (die Kaskaden eines Richtungslichts eingeschlossen: Jeder Punkt nimmt die feinste Kaskade, die ihn abdeckt) und ohne Bloom, setze also wenige und kleine ein. Jede Ansicht dreht Billboards (`nodeSetBillboard`) zu ihrer eigenen Kamera und sortiert Zeichenaufrufe mit Blending von ihr aus von hinten nach vorn, sodass eine Ansicht, deren Kamera von woanders schaut, Billboards trotzdem von vorn und transparente Objekte in der richtigen Reihenfolge sieht. Ein falsches Handle beendet das Skript. Siehe <<scenes3d,3D-Szenen>>.
|
|
|
|
[#viewdelete]
|
|
==== viewDelete
|
|
|
|
[source,text]
|
|
----
|
|
viewDelete(view)
|
|
----
|
|
|
|
Gibt die Ansicht und ihre Texturen frei und beendet den zusätzlichen Render-Durchgang, den sie gekostet hat. Materialien, die sie zeigen, kehren zu ihrer eigenen Textur zurück. Das Handle ist danach ungültig und beendet das Skript, wenn es wieder verwendet wird; da es nur vier Ansichten gleichzeitig gibt, lösche die, die ein Level nicht mehr braucht.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<viewnew,viewNew>>, <<materialsetview,materialSetView>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Monitore im Sicherheitsraum rendern nur, solange die Spielfigur darin ist.
|
|
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)
|
|
----
|
|
|
|
Erzeugt eine Kamera, die in jedem Frame in eine Textur von `width` mal `height` gerendert wird (jeweils auf `1` bis `4096` begrenzt), aus der Standardansicht, bis `viewSetCamera` einen Knoten nennt, und gibt das Ansicht-Handle für `materialSetView` zurück. Die Ansicht verwendet die Projektion aus `cameraSetPerspective` oder `cameraSetOrthographic` in ihrem eigenen Seitenverhältnis. Die Szene rendert pro Ansicht einmal mehr, also reichen `512` mal `384` für einen Monitor völlig, und ein Raspberry Pi will es kleiner. Beendet das Skript, wenn 3D nicht verfügbar ist oder schon vier Ansichten in Gebrauch sind.
|
|
|
|
*Parameter:*
|
|
|
|
* `width` -- die Breite der Textur in Pixeln.
|
|
* `height` -- die Höhe der Textur in Pixeln.
|
|
|
|
*Rückgabe:* Das Ansicht-Handle, eine Ganzzahl.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<viewsetcamera,viewSetCamera>>, <<materialsetview,materialSetView>>, <<viewdelete,viewDelete>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Überwachungsmonitor, der eine Kamera hinter der Spielfigur zeigt.
|
|
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)
|
|
----
|
|
|
|
Der Knoten, von dem aus die Ansicht schaut, entlang seiner eigenen -Z-Achse wie die Hauptkamera, also zählt die Bewegung seiner Elternknoten. Mit `nil` oder ohne zweites Argument kehrt die Ansicht zur Standardansicht von `(0, 0, 5)` mit Blick auf den Ursprung zurück. Hänge den Knoten für einen Rückspiegel an ein Auto, oder spiegle die Position der Hauptkamera an einer Wand für einen Spiegel darin. Beendet das Skript bei einem Knoten-Handle, das nicht gültig ist.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<viewnew,viewNew>>, <<cameraset,cameraSet>>, <<nodelookat,nodeLookAt>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Der Monitor in der Lobby wechselt zwischen drei Überwachungskameras.
|
|
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]
|
|
=== Video
|
|
|
|
Die Familie `video*` verwaltet zusätzliche Videoclips über der Disc: eine Figur, die in einer Ecke spricht, eine vorgerenderte Animation an einem Wendepunkt der Geschichte, mehrere gleichzeitige Einspieler. Mit `videoLoad` können beliebig viele Videos geladen werden, und jedes gibt ein opakes ganzzahliges Handle zurück, das jede andere `video*`-Funktion als erstes Argument nimmt; ein Handle, das entladen wurde, beendet das Skript. Videos werden mit `videoDraw` in Overlay-Koordinaten auf das Overlay gezeichnet und wie Sprites darübergelegt, und ein Video läuft nur weiter, während es gezeichnet wird (oder über <<materialsetvideo,materialSetVideo>> als Material dient). Jedes Video hat seine eigene Tonspurauswahl und Stereolautstärke, unabhängig von der Disc. Wie Sprites müssen Videos aus `onOverlayUpdate` gezeichnet und schließlich mit `videoUnload` freigegeben werden; die Zeichenregeln stehen unter <<overlay,Overlay>>.
|
|
|
|
[#videodraw]
|
|
==== videoDraw
|
|
|
|
[source,text]
|
|
----
|
|
videoDraw(id, x, y, x2, y2)
|
|
videoDraw(id, x, y, centered)
|
|
----
|
|
|
|
Bringt ein Video zu dem Frame, den seine Uhr verlangt, und zeichnet diesen Frame auf das Overlay. Die Uhr folgt dem Audio des Videos, wenn es welches hat, und sonst der realen Zeit, ein Aufruf kann also null, einen oder mehrere Frames weiterrücken; ein Video, das sein Ende erreicht, springt zu Frame `0` zurück. Die Form mit fünf Argumenten streckt den Frame in das Rechteck von `(x, y)` bis einschließlich `(x2, y2)`, immer mit Nearest-Neighbor-Abtastung und ohne Rücksicht auf `videoScale`, `videoRotate` und `videoQuality`. Die Form mit vier Argumenten zeichnet den Frame in seiner nativen Größe, transformiert durch `videoScale` und `videoRotate`, verankert an seiner linken oberen Ecke oder, wenn `centered` `true` ist, an seiner Mitte; der transformierte Frame wird nur neu erzeugt, wenn sich der Frame oder die Transformation ändert, und gedrehte Ecken sind transparent. Hat der Decoder noch keinen Frame erzeugt, wird nichts gezeichnet.
|
|
|
|
Ein Frame, den `videoScale` oder `videoRotate` weder skaliert noch dreht, wird stattdessen von der GPU unter dem Overlay gezeichnet und kostet so gut wie nichts, wenn er dort auf das Overlay kommt, wo seit einem `overlayClear` auf eine transparente Farbe nichts gezeichnet wurde -- etwa als Hintergrundvideo, das zuerst gezeichnet wird. Alles, was danach gezeichnet wird, liegt weiterhin darüber. Er wird dann so auf den Bildschirm skaliert wie das Overlay, statt zuerst per Nearest-Neighbor ins Overlay. Überall sonst kopiert die CPU den Frame ins Overlay, was auf einem kleinen Rechner mehrere Millisekunden pro Frame kostet; zeichne ein Video also vor allem, was darüber liegt, nicht danach.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Video-Handle aus `videoLoad`.
|
|
* `x`, `y` -- Ankerpunkt in Overlay-Koordinaten.
|
|
* `x2`, `y2` -- gegenüberliegende Ecke des Zielrechtecks, einschließlich.
|
|
* `centered` -- Boolean. `true` setzt die Mitte des gezeichneten Frames auf `(x, y)`, `false` seine linke obere Ecke. Verwende `true` für gedrehte Videos.
|
|
|
|
*Hinweise:* Ein Video, das nicht gezeichnet wird, läuft nicht weiter, ein verborgener Clip ist also von selbst pausiert. Zeichne nur aus `onOverlayUpdate`.
|
|
|
|
*Seit:* 2.00. Die skalierte/gedrehte Form mit dem Flag `centered` kam in 2.10 hinzu.
|
|
*Siehe auch:* <<videoload,videoLoad>>, <<videoscale,videoScale>>, <<videorotate,videoRotate>>, <<spritedraw,spriteDraw>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Attract-Clip in einem festen Kasten, als Schleife über seine eigenen Start- und End-Frames.
|
|
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)
|
|
----
|
|
|
|
Gibt den nullbasierten Index der Tonspur zurück, die ein geladenes Video abspielt. `videoLoad` wählt die durch `--audio` oder den Eintrag `AUDIO_TRACK` in `games.dat` genannte Spur (Standard `0`), sofern die Datei so viele Spuren hat; ein Video ganz ohne Audio meldet `-1`.
|
|
|
|
*Rückgabe:* ganzzahliger Spurindex.
|
|
|
|
*Seit:* 2.10
|
|
*Siehe auch:* <<videosetaudiotrack,videoSetAudioTrack>>, <<videogetaudiotracks,videoGetAudioTracks>>, <<videogetlanguage,videoGetLanguage>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Im Options-Overlay zeigen, welche Synchronfassung die Zwischensequenz verwendet.
|
|
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)
|
|
----
|
|
|
|
Gibt zurück, wie viele Audiostreams ein geladenes Video enthält. Ein stummer Clip meldet `0`; sichere Schleifen und Modulo-Rechnungen dagegen ab.
|
|
|
|
*Rückgabe:* ganzzahlige Anzahl, `0` oder mehr.
|
|
|
|
*Seit:* 2.10
|
|
*Siehe auch:* <<videogetlanguage,videoGetLanguage>>, <<videosetaudiotrack,videoSetAudioTrack>>, <<videogetaudiotrack,videoGetAudioTrack>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Sprachmenü nur anbieten, wenn die Zwischensequenz wirklich eine Auswahl hat.
|
|
cutscene = videoLoad("videos/briefing.mkv")
|
|
if videoGetAudioTracks(cutscene) > 1 then
|
|
buildLanguageMenu(cutscene)
|
|
else
|
|
languageMenuEnabled = false
|
|
end
|
|
----
|
|
|
|
[#videogetframe]
|
|
==== videoGetFrame
|
|
|
|
[source,text]
|
|
----
|
|
frame = videoGetFrame(id)
|
|
----
|
|
|
|
Gibt den Frame zurück, auf dem ein geladenes Video steht, nullbasiert. Die Position ändert sich nur, während das Video läuft und gezeichnet wird, und springt auf `0` zurück, wenn der Clip von vorn beginnt. Vergleiche sie mit deinen eigenen Start- und Endmarken, um einen Abschnitt als Schleife zu spielen, oder mit `videoGetFrameCount`, um das Ende eines einmal abgespielten Clips zu erkennen.
|
|
|
|
*Rückgabe:* ganzzahlige Frame-Nummer.
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<videoseek,videoSeek>>, <<videogetframecount,videoGetFrameCount>>, <<videodraw,videoDraw>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Steuerung an den Spieler zurückgeben, wenn das Briefing einmal durchgelaufen ist.
|
|
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)
|
|
----
|
|
|
|
Gibt die Gesamtzahl der Frames eines geladenen Videos zurück, gezählt aus der Frame-Tabelle der Datei, als sie indiziert wurde. Die Frames reichen von `0` bis `count - 1`. Verwende es für Fortschrittsbalken, um eine Schleife umlaufen zu lassen oder um zu erkennen, wann ein einmal abgespielter Clip fast fertig ist.
|
|
|
|
*Rückgabe:* ganzzahlige Anzahl der Frames.
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<videogetframe,videoGetFrame>>, <<videoseek,videoSeek>>, <<videoisplaying,videoIsPlaying>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Fortschrittslinie unter einem Ladeclip: ein Umriss und eine Füllung, die wächst.
|
|
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)
|
|
----
|
|
|
|
Gibt die Höhe der Frames eines geladenen Videos in Pixeln zurück, die native Größe der Datei und nicht die Größe, in der es gezeichnet wird. Kombiniere es mit `videoScale`, um einen Clip unter Beibehaltung seines Seitenverhältnisses in einen Kasten einzupassen.
|
|
|
|
*Rückgabe:* ganzzahlige Höhe in Pixeln.
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<videogetwidth,videoGetWidth>>, <<videoscale,videoScale>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Den Clip ohne Verzerrung in ein 320x240-Fenster einpassen.
|
|
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)
|
|
----
|
|
|
|
Gibt das in der Datei gespeicherte Sprach-Tag für eine Tonspur eines geladenen Videos zurück. Das Tag ist ein dreibuchstabiger ISO-639-Code wie `"eng"` oder `"jpn"`; eine Spur ohne dreibuchstabiges Tag ergibt `"unk"`. Eine Spur außerhalb von `0` bis `videoGetAudioTracks(id) - 1` beendet das Skript.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Video-Handle.
|
|
* `track` -- nullbasierter Index der Tonspur.
|
|
|
|
*Rückgabe:* String mit dem Sprachcode.
|
|
|
|
*Seit:* 2.10
|
|
*Siehe auch:* <<videogetlanguagedescription,videoGetLanguageDescription>>, <<videogetaudiotracks,videoGetAudioTracks>>, <<videosetaudiotrack,videoSetAudioTrack>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Synchronfassung der Zwischensequenz an die für die Disc gewählte Sprache anpassen.
|
|
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)
|
|
----
|
|
|
|
Schlägt den englischen Namen eines Sprachcodes nach. Anders als die übrigen `video*`-Funktionen nimmt sie kein Handle; sie ist ein reines Nachschlagen in einer Tabelle, das zweibuchstabige Codes nach ISO 639-1 und dreibuchstabige Codes nach ISO 639-2 (sowohl die T- als auch die B-Form) annimmt, verglichen ohne Rücksicht auf Groß- und Kleinschreibung. Unbekannte Codes, auch das `"unk"`, das für Spuren ohne Tag zurückkommt, ergeben `"Unknown"`. `Framework.singe` stellt sie zur Symmetrie mit der Disc-API auch als `discGetLanguageDescription` bereit.
|
|
|
|
*Parameter:*
|
|
|
|
* `code` -- Sprachcode wie `"en"`, `"eng"` oder `"jpn"`.
|
|
|
|
*Rückgabe:* String mit dem Namen der Sprache oder `"Unknown"`.
|
|
|
|
*Seit:* 2.10
|
|
*Siehe auch:* <<videogetlanguage,videoGetLanguage>>, <<discgetlanguage,discGetLanguage>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Aus den Spuren eines Videos ein Menü zur Sprachauswahl bauen.
|
|
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()
|
|
----
|
|
|
|
Gibt die Stereolautstärke eines geladenen Videos als zwei Prozentwerte zurück. Ein frisch geladenes Video steht auf beiden Kanälen auf dem Pegel von `--volume_nonvldp` (Standard `100`). Beachte, dass zwei Werte zurückkommen, der linke zuerst.
|
|
|
|
Ohne Argument aufgerufen ist es eine Hypseus-Erweiterung, die stattdessen die eigene Lautstärke der Laserdisc liefert, als einzelne Zahl auf der Skala `0` bis `64` von Hypseus statt als Prozentwert. Dessen Spiele verdoppeln sie für <<musicsetvolume,`musicSetVolume`>>, dessen Bereich `0` bis `128` ist. Der Pegel folgt `--volume_vldp`.
|
|
|
|
*Rückgabe:* zwei Ganzzahlen, links und rechts, jeweils `0` bis `100`; oder ohne Argument eine Ganzzahl, `0` bis `64`.
|
|
|
|
*Seit:* 2.00, die Disc-Form ohne Argument 3.00
|
|
*Siehe auch:* <<videosetvolume,videoSetVolume>>, <<videoload,videoLoad>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Zwischensequenz leiser stellen, während eine Sprachzeile läuft, und danach wiederherstellen.
|
|
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)
|
|
----
|
|
|
|
Gibt die Breite der Frames eines geladenen Videos in Pixeln zurück, die native Größe der Datei und nicht die Größe, in der es gezeichnet wird.
|
|
|
|
*Rückgabe:* ganzzahlige Breite in Pixeln.
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<videogetheight,videoGetHeight>>, <<videoscale,videoScale>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Einen in nativer Größe gezeichneten Clip oben im Overlay zentrieren.
|
|
local x = (overlayGetWidth() - videoGetWidth(clip)) / 2
|
|
function onOverlayUpdate()
|
|
videoDraw(clip, x, 16, false)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#videoisplaying]
|
|
==== videoIsPlaying
|
|
|
|
[source,text]
|
|
----
|
|
playing = videoIsPlaying(id)
|
|
----
|
|
|
|
Meldet, ob ein geladenes Video im Wiedergabezustand ist, also ob seit dem letzten `videoPause` `videoPlay` aufgerufen wurde. Auch ein laufendes Video rückt nur weiter, während es gezeichnet wird. Nach `videoLoad` beginnen Videos pausiert.
|
|
|
|
*Rückgabe:* Boolean.
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<videoplay,videoPlay>>, <<videopause,videoPause>>, <<videogetframe,videoGetFrame>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Taste schaltet den Bild-im-Bild-Clip um.
|
|
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)
|
|
----
|
|
|
|
Öffnet eine Videodatei und gibt ihr Handle zurück. Jeder Container und jeder Codec, den das mitgelieferte FFmpeg dekodieren kann, wird angenommen. Die Engine erzeugt oder liest einen Frame-Index in einem Datenordner, der nach dem Ort des Videos unter Singes oberstem Datenordner benannt ist, das erste Laden einer großen Datei kann also einen Moment dauern; lässt sich dieser Ordner nicht anlegen, wird das Skript beendet. Das Video beginnt pausiert auf Frame `0`, mit der Tonspur aus `--audio` oder dem Eintrag `AUDIO_TRACK` in `games.dat` (Standard `0`), sofern die Datei so viele Spuren hat, und mit beiden Kanälen auf dem Pegel von `--volume_nonvldp`. Die Frames werden nach RGB dekodiert, damit sie auf das Overlay gezeichnet werden können.
|
|
|
|
*Parameter:*
|
|
|
|
* `filename` -- Pfad zur Videodatei, relativ zum Spieleverzeichnis oder innerhalb eines gepackten Spiels.
|
|
|
|
*Rückgabe:* ganzzahliges Video-Handle.
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<videoplay,videoPlay>>, <<videodraw,videoDraw>>, <<videounload,videoUnload>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Den Attract-Clip des gewählten Spiels laden und auf den Anfang seiner Schleife setzen.
|
|
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)
|
|
----
|
|
|
|
Pausiert ein geladenes Video. Seine Uhr bleibt stehen, seine Tonspur pausiert, und `videoDraw` zeichnet weiter den Frame, auf dem es angehalten hat. Ein bereits pausiertes Video noch einmal zu pausieren schadet nicht.
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<videoplay,videoPlay>>, <<videoisplaying,videoIsPlaying>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Den Attract-Clip anhalten, solange der Kalibrierungsbildschirm offen ist.
|
|
function calibrationBegin()
|
|
calibrating = true
|
|
if attractVideo then
|
|
videoPause(attractVideo)
|
|
end
|
|
end
|
|
----
|
|
|
|
[#videoplay]
|
|
==== videoPlay
|
|
|
|
[source,text]
|
|
----
|
|
videoPlay(id)
|
|
----
|
|
|
|
Startet ein geladenes Video ab seinem aktuellen Frame oder setzt es dort fort. Die Uhr wird bei diesem Frame neu gestartet, ein nach langer Pause fortgesetztes Video macht also dort weiter, wo es angehalten hat, statt nach vorn zu springen, und die Tonspur läuft mit ihm weiter. Das Video rückt nur weiter, während es mit `videoDraw` gezeichnet oder als Material verwendet wird.
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<videopause,videoPause>>, <<videoseek,videoSeek>>, <<videodraw,videoDraw>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Briefing über der pausierten Disc starten.
|
|
function startBriefing()
|
|
discPause()
|
|
videoSeek(briefing, 0)
|
|
videoPlay(briefing)
|
|
briefingRunning = true
|
|
end
|
|
----
|
|
|
|
[#videoquality]
|
|
==== videoQuality
|
|
|
|
[source,text]
|
|
----
|
|
videoQuality(id, smooth)
|
|
----
|
|
|
|
Wählt die Filterung, die verwendet wird, wenn die Form von `videoDraw` mit vier Argumenten das Video skaliert oder dreht. Jeder Wert ungleich null wählt `RENDER_SMOOTH` (bilinear), null wählt `RENDER_PIXELATED` (Nearest-Neighbor), den Standard. Eine Änderung markiert den transformierten Frame für die Neuerzeugung beim nächsten Zeichnen. Sie wirkt weder auf die gestreckte Form mit fünf Argumenten, die immer Nearest-Neighbor verwendet, noch auf ein nicht transformiertes Video.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Video-Handle.
|
|
* `smooth` -- `RENDER_PIXELATED` (`0`) oder `RENDER_SMOOTH` (`1`).
|
|
|
|
*Seit:* 2.10
|
|
*Siehe auch:* <<videoscale,videoScale>>, <<videorotate,videoRotate>>, <<videodraw,videoDraw>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein fotografischer Clip, verkleinert in eine Ecke, sieht gefiltert besser aus.
|
|
pipVideo = videoLoad("videos/copilot.mkv")
|
|
videoScale(pipVideo, 0.4)
|
|
videoQuality(pipVideo, RENDER_SMOOTH)
|
|
videoPlay(pipVideo)
|
|
----
|
|
|
|
[#videorotate]
|
|
==== videoRotate
|
|
|
|
[source,text]
|
|
----
|
|
videoRotate(id, degrees)
|
|
----
|
|
|
|
Setzt die Drehung, die die Form von `videoDraw` mit vier Argumenten anwendet, in Grad im Uhrzeigersinn, nach derselben Konvention wie `spriteRotate`. Der Winkel wird modulo `360` reduziert und behält sein Vorzeichen, `450` und `90` sind also gleich, und `-90` dreht eine Vierteldrehung gegen den Uhrzeigersinn. Der mit `videoScale` gesetzte Maßstab bleibt erhalten. Gedreht wird um die Mitte des Frames, zeichne gedrehte Videos also mit `centered` auf `true`. Ein geänderter Winkel markiert den transformierten Frame für die Neuerzeugung beim nächsten Zeichnen.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Video-Handle.
|
|
* `degrees` -- Drehung in Grad, im Uhrzeigersinn.
|
|
|
|
*Seit:* 2.10
|
|
*Siehe auch:* <<videorotateandscale,videoRotateAndScale>>, <<videoscale,videoScale>>, <<videodraw,videoDraw>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Radarbild langsam drehen, während es läuft.
|
|
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)
|
|
----
|
|
|
|
Setzt Drehung und Maßstab in einem Aufruf, nach denselben Regeln wie `videoRotate` und `videoScale`: Grad im Uhrzeigersinn, modulo `360` reduziert, und Maßstabsfaktoren, die größer als `0` sein müssen. Verwende es, wenn sich beides in jedem Frame ändert, damit der transformierte Frame einmal statt zweimal neu erzeugt wird.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Video-Handle.
|
|
* `degrees` -- Drehung in Grad, im Uhrzeigersinn.
|
|
* `scale` -- einheitlicher Maßstabsfaktor, `1.0` für die native Größe.
|
|
* `scaleX`, `scaleY` -- getrennte waagerechte und senkrechte Faktoren.
|
|
|
|
*Seit:* 2.10
|
|
*Siehe auch:* <<videorotate,videoRotate>>, <<videoscale,videoScale>>, <<videodraw,videoDraw>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Clip taumelt und schrumpft davon, während er weggeschickt wird.
|
|
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)
|
|
----
|
|
|
|
Setzt den Maßstab, den die Form von `videoDraw` mit vier Argumenten anwendet, als Faktor der nativen Größe; `1.0` ist unskaliert. Die mit `videoRotate` gesetzte Drehung bleibt erhalten. Ein geänderter Maßstab markiert den transformierten Frame für die Neuerzeugung beim nächsten Zeichnen. Faktoren von `0` oder weniger lassen sich nicht rendern und lassen die Engine beim nächsten `videoDraw` abbrechen, halte animierte Maßstäbe also über null. Die gestreckte Form von `videoDraw` mit fünf Argumenten ignoriert diese Einstellung.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Video-Handle.
|
|
* `scale` -- einheitlicher Maßstabsfaktor, größer als `0`.
|
|
* `scaleX`, `scaleY` -- getrennte waagerechte und senkrechte Faktoren, jeder größer als `0`.
|
|
|
|
*Seit:* 2.10
|
|
*Siehe auch:* <<videorotateandscale,videoRotateAndScale>>, <<videoquality,videoQuality>>, <<videogetwidth,videoGetWidth>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Zwischensequenz in einem Viertel ihrer Größe in der Ecke zeigen, während das Spiel weiterläuft.
|
|
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)
|
|
----
|
|
|
|
Setzt ein geladenes Video auf `frame` und startet dort seine Uhr neu. Der Wiedergabe- oder Pausenzustand bleibt unverändert: ein laufendes Video macht beim nächsten Zeichnen ab dem neuen Frame weiter, ein pausiertes zeigt ihn. Die Frame-Nummer wird modulo der Anzahl der Frames umgebrochen, `-1` ist also der letzte Frame, und ein Wert hinter dem Ende landet wieder innerhalb des Clips.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Video-Handle.
|
|
* `frame` -- Ziel-Frame, nullbasiert.
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<videogetframe,videoGetFrame>>, <<videogetframecount,videoGetFrameCount>>, <<videoplay,videoPlay>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Den Attract-Clip zwischen den beiden Marken aus games.dat als Schleife spielen.
|
|
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)
|
|
----
|
|
|
|
Schaltet ein geladenes Video auf eine andere seiner Tonspuren um. Bereits eingereihtes Audio der alten Spur wird verworfen, und die Wiedergabe richtet sich neu aus, die Änderung ist also fast sofort zu hören. Die bereits verwendete Spur zu wählen tut nichts. Eine Spur außerhalb von `0` bis `videoGetAudioTracks(id) - 1` beendet das Skript, was bei einem stummen Clip jeden beliebigen Wert bedeutet.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Video-Handle.
|
|
* `track` -- nullbasierter Index der Tonspur.
|
|
|
|
*Seit:* 2.10
|
|
*Siehe auch:* <<videogetaudiotrack,videoGetAudioTrack>>, <<videogetaudiotracks,videoGetAudioTracks>>, <<videogetlanguage,videoGetLanguage>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Menü speichert für den Attract-Clip jedes Spiels eine eigene Tonspur.
|
|
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)
|
|
----
|
|
|
|
Setzt die Stereolautstärke eines geladenen Videos als Prozentwerte. Die Werte werden kommentarlos auf `0` bis `100` begrenzt. Die Einstellung wird auch bei einem stummen Clip gemerkt und von `videoGetVolume` zurückgemeldet.
|
|
|
|
Mit einem Argument aufgerufen ist es eine Hypseus-Erweiterung, die stattdessen die eigene Lautstärke der Laserdisc setzt, auf der Skala `0` bis `64` von Hypseus statt als Prozentwert: das Gegenstück zum bloßen <<videogetvolume,`videoGetVolume()`>>. Ein Pegel außerhalb von `0` bis `64` wird ignoriert, statt fatal zu sein, und genau das macht Hypseus mit einem solchen.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Video-Handle.
|
|
* `left`, `right` -- Kanallautstärken, `0` (stumm) bis `100` (voll).
|
|
* `volume` -- die Lautstärke der Laserdisc, `0` (stumm) bis `64` (voll).
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<videogetvolume,videoGetVolume>>, <<discaudio,discAudio>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Den Ton der Zwischensequenz in weniger als einer halben Sekunde ausblenden, während die Steuerung zurückkehrt.
|
|
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)
|
|
----
|
|
|
|
Hält den Decoder an und gibt alles frei, was ein geladenes Video belegt: Decoder, Tonspur, zwischengespeicherte Frames und die transformierte Fläche. Das Handle ist danach ungültig, und es an irgendeine `video*`-Funktion zu übergeben beendet das Skript, leere also die Variable, die es enthielt. Videos, die beim Ende des Skripts noch geladen sind, gibt die Engine frei.
|
|
|
|
*Parameter:*
|
|
|
|
* `id` -- Video-Handle.
|
|
|
|
*Seit:* 2.00
|
|
*Siehe auch:* <<videoload,videoLoad>>, <<videopause,videoPause>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Assets des gewählten Spiels verwerfen, bevor das nächste geladen wird.
|
|
function unloadGameAssets()
|
|
if attractVideo then
|
|
videoUnload(attractVideo)
|
|
attractVideo = nil
|
|
end
|
|
end
|
|
----
|
|
|
|
[#vldp]
|
|
=== VLDP
|
|
|
|
Der Namensraum `vldp*` ist älter als Singe 2.00 und existiert, damit ältere Skripte weiterlaufen. `vldpGetHeight` und `vldpGetWidth` sind dieselben Bindungen wie `discGetHeight` und `discGetWidth`, `vldpSetVerbose` tut nichts, und `vldpGetPixel` ist das einzige Mitglied ohne modernes Gegenstück: Es liest ein Pixel des aktuellen Disc-Frames an Overlay-Koordinaten und ist immer noch der übliche Weg zur Treffererkennung auf vorab aufgenommenem Video. Ohne Disc gibt es Schwarz zurück (siehe <<withoutadisc,Spiele ohne Disc>>).
|
|
|
|
WARNING: *Veraltet.* Bevorzuge in neuem Code <<discgetheight,discGetHeight>> und <<discgetwidth,discGetWidth>>. `vldpGetPixel` bleibt aktuell.
|
|
|
|
Hypseus Singe hat elf weitere Mitglieder hinzugefügt, die unter diesem Präfix bleiben, damit seine Spiele unverändert laufen: `vldpFocusArea` und `vldpResetFocus` beschneiden das Disc-Bild, `vldpGetScale` und `vldpSetScale` melden und ändern den Skalierungsfaktor der Anzeige, `vldpGetRotate` und `vldpSetRotate` tun dasselbe für die Drehung der Darstellung, `vldpGetYUVPixel` liest rohe Abtastwerte, und `vldpFlash`, `vldpSetBlend`, `vldpSetLuma` und `vldpSetMonochrome` ändern, wie das Bild selbst aussieht. Jedes ist unten mit „Hypseus-Erweiterung“ gekennzeichnet. Maßstab und Drehung sind zwei der Eingaben für xref:Manual.adoc#videorect[Das Videorechteck].
|
|
|
|
Die vier Bildregler arbeiten auf dem dekodierten Frame, auf seinen Luma- und Chroma-Ebenen, bevor er den Bildschirm erreicht: `vldpSetBlend` und `vldpSetLuma` werden nacheinander angewendet, während jeder Frame hochgeladen wird, `vldpSetMonochrome` ersetzt das Chroma, und `vldpFlash` ersetzt das Bild für einen Frame vollständig. Sie kosten pro Frame einen Durchgang über die Luma-Ebene, solange sie eingeschaltet sind, und gar nichts, solange sie aus sind, sie lassen `vldpGetPixel` und `vldpGetYUVPixel` das Bild so lesen, wie es dekodiert wurde, und mit einer Frame-Datei folgen sie der Wiedergabe von Segment zu Segment. Alle vier gehen wieder aus, wenn das Skript neu geladen wird (außer Monochrom, das zu dem zurückkehrt, was `--monochrome` verlangt hat), und keiner von ihnen berührt ein mit `videoDraw` gezeichnetes Video.
|
|
|
|
[#vldpflash]
|
|
==== vldpFlash
|
|
|
|
[source,text]
|
|
----
|
|
vldpFlash()
|
|
----
|
|
|
|
Hypseus-Erweiterung. Legt ein weißes Bild auf die Disc, so wie ein echter Laserdisc-Player aufblitzt, wenn er sucht. Es erscheint sofort, und die nächste Aktualisierung der Disc nimmt es wieder weg, es hält also einen gezeichneten Frame lang, ob die Disc läuft, pausiert oder gestoppt ist.
|
|
|
|
Das Weiß ist der Wert, mit dem Hypseus blitzt, 90 Prozent Luma mit neutralem Chroma, nicht reines Weiß, und genau das ist das Weiß eines Videosignals tatsächlich. Overlay, GUIs und das Bezel-Artwork werden wie üblich darübergezeichnet, ein Heads-up-Display bleibt also während des Blitzes lesbar.
|
|
|
|
Ohne Disc oder bei einer Disc, deren erster Frame noch nicht dekodiert wurde, tut der Aufruf nichts.
|
|
|
|
*Parameter:* keine.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<discsearch,discSearch>>, <<vldpsetluma,vldpSetLuma>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Einen Treffer unterstreichen.
|
|
vldpFlash()
|
|
----
|
|
|
|
[#vldpfocusarea]
|
|
==== vldpFocusArea
|
|
|
|
[source,text]
|
|
----
|
|
vldpFocusArea(x, y, width, height)
|
|
vldpFocusArea()
|
|
----
|
|
|
|
Hypseus-Erweiterung. Zeigt nur ein Rechteck des Disc-Frames, angegeben in Disc-Pixeln, gestreckt auf den Raum, den sonst der ganze Frame füllt, mit der üblichen weichen Skalierung des Renderers; das Seitenverhältnis folgt dem, was `width` und `height` ergeben. Das Rechteck wird innerhalb des Frames gehalten, und eines, das ganz außerhalb liegt, bricht das Skript ab. Ohne Argumente wird wieder der ganze Frame gezeigt (Hypseus ignoriert diesen Aufruf und verwendet `vldpResetFocus`; Singe nimmt beides an). Der Fokus ist nur ein Anzeigeeffekt: `vldpGetPixel`, `vldpGetYUVPixel`, die Mauszuordnung und die Disc auf einem 3D-Material behalten alle die ungezoomten Koordinaten, Treffererkennung auf einem gezoomten Bild muss der Aufrufer also selbst skalieren. Ohne Disc tut der Aufruf nichts. Wird zurückgesetzt, wenn das Skript neu geladen wird.
|
|
|
|
*Parameter:*
|
|
|
|
* `x`, `y` -- linke obere Ecke in Disc-Pixeln.
|
|
* `width`, `height` -- Größe des Rechtecks in Disc-Pixeln.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<vldpresetfocus,vldpResetFocus>>, <<vldpgetwidth,vldpGetWidth>>, <<vldpgetpixel,vldpGetPixel>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Während der Verfolgungsjagd auf die mittleren 80 % des Bildes zoomen.
|
|
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()
|
|
----
|
|
|
|
Gibt die Höhe des Disc-Videos in Pixeln zurück oder ohne Disc die Höhe der Zeichenfläche. Das ist dieselbe Bindung wie `discGetHeight`, registriert unter ihrem Namen aus der Zeit vor 2.00.
|
|
|
|
*Rückgabe:* ganzzahlige Höhe in Pixeln.
|
|
|
|
*Hinweise:*
|
|
|
|
WARNING: *Veralteter Alias.* Verwende <<discgetheight,discGetHeight>> in neuem Code.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<discgetheight,discGetHeight>>, <<vldpgetwidth,vldpGetWidth>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein 1.x-Skript, das sein Overlay nach der Disc bemisst.
|
|
overlaySetResolution(vldpGetWidth(), vldpGetHeight())
|
|
centerX = vldpGetWidth() / 2
|
|
centerY = vldpGetHeight() / 2
|
|
----
|
|
|
|
[#vldpgetpixel]
|
|
==== vldpGetPixel
|
|
|
|
[source,text]
|
|
----
|
|
r, g, b = vldpGetPixel(x, y)
|
|
----
|
|
|
|
Liest die Farbe eines Pixels des Disc-Frames, der gerade auf dem Bildschirm ist. Die Koordinaten sind Overlay-Koordinaten: Die Engine teilt sie durch den Maßstab von Overlay zu Video und schneidet die Nachkommastellen ab, du übergibst also dieselben Zahlen, mit denen du zeichnest. Gibt `0, 0, 0` zurück, wenn es keine Disc gibt, wenn noch kein Frame dekodiert wurde oder wenn die skalierten Koordinaten außerhalb des Videos liegen. Spiele, die es verwenden, markieren Ziele im Quellmaterial typischerweise mit einer markanten Farbe und prüfen beim Betätigen des Abzugs das Pixel unter dem Fadenkreuz, statt für jeden Frame Trefferzonen anzulegen.
|
|
|
|
*Parameter:*
|
|
|
|
* `x`, `y` -- Overlay-Koordinaten; Bruchwerte werden angenommen.
|
|
|
|
*Rückgabe:* drei Ganzzahlen, Rot, Grün und Blau, jeweils `0` bis `255`.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<discgetframe,discGetFrame>>, <<mousegetposition,mouseGetPosition>>, <<overlaysetresolution,overlaySetResolution>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Treffertest für die Lightgun: Ziele sind auf Gegner-Frames in reinem Magenta gemalt.
|
|
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()
|
|
----
|
|
|
|
Hypseus-Erweiterung. Gibt die Drehung der Darstellung in Grad im Uhrzeigersinn zurück: die Option `--rotate` oder das, was `vldpSetRotate` zuletzt gesetzt hat, und `0`, wenn keins von beiden. Zurückgegeben werden immer nur `0`, `90`, `180` und `270`, denn das sind die einzigen Winkel, in die Singe dreht.
|
|
|
|
*Rückgabe:* Ganzzahl, `0`, `90`, `180` oder `270`.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<vldpsetrotate,vldpSetRotate>>, <<vldpgetscale,vldpGetScale>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Automat im Hochformat will seine Punkteanzeige an der langen Seite.
|
|
if vldpGetRotate() == 90 or vldpGetRotate() == 270 then
|
|
layoutPanel(PANEL_TALL)
|
|
else
|
|
layoutPanel(PANEL_WIDE)
|
|
end
|
|
----
|
|
|
|
[#vldpgetscale]
|
|
==== vldpGetScale
|
|
|
|
[source,text]
|
|
----
|
|
scale = vldpGetScale()
|
|
----
|
|
|
|
Hypseus-Erweiterung. Gibt den Skalierungsfaktor der Anzeige als Prozentwert zurück: die Option `--scalefactor` oder das, was `vldpSetScale` zuletzt angewendet hat, und `100`, wenn keins von beiden angegeben wurde. Hypseus-Spiele teilen `100` durch ihn, um die Geometrie ihres Overlays an das verkleinerte Bild anzupassen. Er hat nichts mit `vldpFocusArea` oder der Overlay-Auflösung zu tun.
|
|
|
|
*Rückgabe:* Ganzzahl, `25` bis `100`.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<vldpsetscale,vldpSetScale>>, <<vldpfocusarea,vldpFocusArea>>, <<overlaygetwidth,overlayGetWidth>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Hypseus-Spielcode: die Visiere skalieren mit dem Bild.
|
|
if vldpGetScale ~= nil then
|
|
gunScale = 100 / vldpGetScale()
|
|
else
|
|
gunScale = 1
|
|
end
|
|
----
|
|
|
|
[#vldpgetshift]
|
|
==== vldpGetShift
|
|
|
|
[source,text]
|
|
----
|
|
x, y = vldpGetShift()
|
|
----
|
|
|
|
Der Versatz des Bildes innerhalb des Fensters als Prozentwerte, die Optionen
|
|
`--shiftx` und `--shifty` oder das, was <<vldpsetshift,vldpSetShift>> zuletzt
|
|
angewendet hat, und `0, 0`, wenn keins von beiden angegeben wurde. Im Extremfall
|
|
füllt eine Verschiebung genau den Platz, den `--scalefactor` übrig gelassen hat,
|
|
`100` bei einem Maßstab von `100` bewegt also nichts.
|
|
|
|
*Rückgabe:* zwei Ganzzahlen, jeweils `-100` bis `100`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<vldpsetshift,vldpSetShift>>, <<vldpgetscale,vldpGetScale>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Melden, wohin der Betreiber das Bild gesetzt hat.
|
|
local x, y = vldpGetShift()
|
|
fontPrint(10, 10, string.format("shift %d, %d", x, y))
|
|
----
|
|
|
|
[#vldpgetwidth]
|
|
==== vldpGetWidth
|
|
|
|
[source,text]
|
|
----
|
|
width = vldpGetWidth()
|
|
----
|
|
|
|
Gibt die Breite des Disc-Videos in Pixeln zurück oder ohne Disc die Breite der Zeichenfläche. Das ist dieselbe Bindung wie `discGetWidth`, registriert unter ihrem Namen aus der Zeit vor 2.00.
|
|
|
|
*Rückgabe:* ganzzahlige Breite in Pixeln.
|
|
|
|
*Hinweise:*
|
|
|
|
WARNING: *Veralteter Alias.* Verwende <<discgetwidth,discGetWidth>> in neuem Code.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<discgetwidth,discGetWidth>>, <<vldpgetheight,vldpGetHeight>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein 1.x-Skript, das sein Overlay nach der Disc bemisst.
|
|
overlaySetResolution(vldpGetWidth(), vldpGetHeight())
|
|
centerX = vldpGetWidth() / 2
|
|
centerY = vldpGetHeight() / 2
|
|
----
|
|
|
|
[#vldpgetyuvpixel]
|
|
==== vldpGetYUVPixel
|
|
|
|
[source,text]
|
|
----
|
|
y, u, v = vldpGetYUVPixel(x, y)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Liest die rohen Abtastwerte eines Pixels des gerade gezeigten Disc-Frames, vor jeder Farbumrechnung: den Luma-Wert und die zwei Chroma-Werte, jeweils `0` bis `255`, mit Chroma in der 4:2:0-Auflösung des dekodierten Bildes (neutral ist `128`). Die Koordinaten sind Overlay-Koordinaten, skaliert und abgeschnitten genau wie bei `vldpGetPixel`. Gibt `-1, -1, -1` zurück, wenn es keine Disc gibt, wenn noch kein Frame dekodiert wurde oder wenn die skalierten Koordinaten außerhalb des Videos liegen. Unbeeinflusst von `vldpFocusArea` und `vldpSetMonochrome`. Günstiger als `vldpGetPixel`, wenn nur die Helligkeit zählt.
|
|
|
|
*Parameter:*
|
|
|
|
* `x`, `y` -- Overlay-Koordinaten; Bruchwerte werden angenommen.
|
|
|
|
*Rückgabe:* drei Ganzzahlen, `Y`, `U` und `V`, jeweils `0` bis `255`, oder `-1, -1, -1`.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<vldpgetpixel,vldpGetPixel>>, <<discgetframe,discGetFrame>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Straße ist dunkel; alles Helle unter dem Auto ist ein Hindernis.
|
|
function carHitsSomething()
|
|
local y = vldpGetYUVPixel(carX + carWidth / 2, carY + carHeight)
|
|
return y >= 120
|
|
end
|
|
----
|
|
|
|
[#vldpresetfocus]
|
|
==== vldpResetFocus
|
|
|
|
[source,text]
|
|
----
|
|
vldpResetFocus()
|
|
----
|
|
|
|
Hypseus-Erweiterung. Zeigt nach `vldpFocusArea` wieder den ganzen Disc-Frame, genau wie `vldpFocusArea()` ohne Argumente. Der eigene Name von Hypseus für das Zurücksetzen, erhalten, damit seine Spiele unverändert laufen.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<vldpfocusarea,vldpFocusArea>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
function onChaseEnded()
|
|
vldpResetFocus()
|
|
end
|
|
----
|
|
|
|
[#vldpsetblend]
|
|
==== vldpSetBlend
|
|
|
|
[source,text]
|
|
----
|
|
vldpSetBlend(enabled)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Glättet das Disc-Bild von Zeile zu Zeile: Jeder Luma-Wert wird zum Mittelwert aus sich selbst und den Werten direkt darüber und darunter. Das ist der eigene Drei-Tap-Blendfilter von Hypseus, und es gibt ihn, um einer Überspielung im Zeilensprungverfahren die harten Kanten zu nehmen, ohne das Bild seitlich weicher zu machen.
|
|
|
|
Die Chroma-Ebenen bleiben unberührt, die Glättung ändert also das Helligkeitsdetail und nicht die Farbe. Ausschalten bringt das Bild genau so zurück, wie es dekodiert wurde.
|
|
|
|
Ohne Disc tut der Aufruf nichts, wird aber trotzdem gemerkt, sodass er wirkt, wenn später eine Disc geladen wird. Ein Argument, das kein Boolean ist, beendet das Skript.
|
|
|
|
*Parameter:*
|
|
|
|
* `enabled` -- Boolean.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<vldpsetluma,vldpSetLuma>>, <<vldpsetmonochrome,vldpSetMonochrome>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein Schalter im Service-Bildschirm für eine verrauschte Überspielung.
|
|
vldpSetBlend(SETTINGS.softenVideo)
|
|
----
|
|
|
|
[#vldpsetluma]
|
|
==== vldpSetLuma
|
|
|
|
[source,text]
|
|
----
|
|
vldpSetLuma(enabled [, level])
|
|
----
|
|
|
|
Hypseus-Erweiterung. Macht das Disc-Bild heller oder dunkler. `level` reicht von 0 bis 8, und 4, der Standard, ändert nichts: Der Abstand zu 4 wird in Achteln zu jedem Luma-Wert addiert, 0 halbiert also die Helligkeit, 8 hebt sie um die Hälfte an, und die Stufen dazwischen sind gleichmäßig. Eine Stufe außerhalb von 0 bis 8 beendet das Skript.
|
|
|
|
Da nur das Luma geändert wird, bleibt die Farbe, wo sie ist: Eine höhere Stufe hellt das Bild auf, ohne die Farbe auszuwaschen, was der Bildregler eines echten Players tut und was ein Farbmultiplikator nicht täte. Ausschalten oder Stufe 4 bringt das Bild genau so zurück, wie es dekodiert wurde.
|
|
|
|
`vldpSetLuma(true)` ohne Stufe tut deshalb sichtbar nichts, und so verhält sich auch Hypseus.
|
|
|
|
Ohne Disc tut der Aufruf nichts, wird aber trotzdem gemerkt, sodass er wirkt, wenn später eine Disc geladen wird.
|
|
|
|
*Parameter:*
|
|
|
|
* `enabled` -- Boolean.
|
|
* `level` -- optionale Zahl; 0 bis 8, wobei 4 neutral ist. Standard 4.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<vldpsetblend,vldpSetBlend>>, <<vldpsetmonochrome,vldpSetMonochrome>>, <<vldpflash,vldpFlash>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Eine Nachtszene, die der Spieler nicht erkennen kann; lass ihn sie aufhellen.
|
|
vldpSetLuma(true, 6)
|
|
----
|
|
|
|
[#vldpsetmonochrome]
|
|
==== vldpSetMonochrome
|
|
|
|
[source,text]
|
|
----
|
|
vldpSetMonochrome(enabled)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Zeigt die Disc nur in Luma: Die Chroma-Ebenen werden beim Hochladen jedes Frames durch neutrale Werte ersetzt, eine pausierte Disc ändert sich also sofort. Mit einer Frame-Datei folgt die Einstellung der Wiedergabe von Segment zu Segment. `vldpGetPixel` und `vldpGetYUVPixel` geben weiterhin die Farbwerte zurück, und zusätzliche, mit `videoDraw` gezeichnete Videos sind nicht betroffen. Kombiniere es mit `overlaySetMonochrome` für einen Schwarzweißmodus. Ohne Disc tut der Aufruf nichts; wird zurückgesetzt, wenn das Skript neu geladen wird. Ein Argument, das kein Boolean ist, bricht das Skript ab.
|
|
|
|
*Parameter:*
|
|
|
|
* `enabled` -- Boolean.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<overlaysetmonochrome,overlaySetMonochrome>>, <<vldpgetpixel,vldpGetPixel>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein DIP-Schalter wählt Farbe oder Schwarzweiß.
|
|
vldpSetMonochrome(not dipChroma)
|
|
overlaySetMonochrome(not dipChroma)
|
|
----
|
|
|
|
[#vldpsetrotate]
|
|
==== vldpSetRotate
|
|
|
|
[source,text]
|
|
----
|
|
applied = vldpSetRotate(degrees)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Dreht die ganze Darstellung im Uhrzeigersinn: Disc, Overlay, GUIs, 3D-Szene und Partikel drehen sich alle zusammen, und Maus- und Lightgun-Positionen werden zurückgedreht, sodass ein Klick weiterhin dort landet, wo gezeichnet wird. Eine Vierteldrehung vertauscht die Form der dargestellten Fläche, sodass ein Hochformatfenster ausgefüllt wird, statt das Bild mit Letterbox in einen Streifen zu setzen.
|
|
|
|
Hypseus nimmt jeden Winkel von `0` bis `359` an; Singe nimmt nur `0`, `90`, `180` und `270` und antwortet auf alles andere mit `false`, statt das Skript zu beenden. Ein beliebiger Winkel ließe der Mauszuordnung und dem Sinden-Rahmen keine sinnvolle Bedeutung, und kein Spiel in der Hypseus-Bibliothek verlangt einen.
|
|
|
|
Die Drehung kostet ein bildschirmgroßes Renderziel und einen zusätzlichen Blit pro Frame, solange sie nicht null ist, und gar nichts, solange sie es ist. Wird beim Neuladen des Skripts auf den Wert von `--rotate` zurückgesetzt.
|
|
|
|
*Parameter:*
|
|
|
|
* `degrees` -- Ganzzahl, `0`, `90`, `180` oder `270`.
|
|
|
|
*Rückgabe:* Boolean, true, wenn die Drehung angewendet wurde.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<vldpgetrotate,vldpGetRotate>>, <<vldpsetscale,vldpSetScale>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein senkrechter Shooter-Abschnitt auf einem drehbaren Monitor.
|
|
if not vldpSetRotate(90) then
|
|
debugPrint("This build will not turn the screen.")
|
|
end
|
|
----
|
|
|
|
[#vldpsetscale]
|
|
==== vldpSetScale
|
|
|
|
[source,text]
|
|
----
|
|
applied = vldpSetScale(percent)
|
|
----
|
|
|
|
Hypseus-Erweiterung. Verkleinert das Bild um seine Mitte auf `percent` des Fensters, das Gegenstück zu `--scalefactor` zur Laufzeit, und baut das Videorechteck neu auf, sodass Disc, Overlay, GUIs, 3D-Szene, Partikel, ein etwaiger Sinden-Rahmen und die Mauszuordnung ihm alle folgen. Werte außerhalb von `25` bis `100` ergeben `false` und ändern nichts, statt das Skript zu beenden.
|
|
|
|
Änderungen sind auf eine alle 15 Millisekunden begrenzt, so wie Hypseus seinen eigenen Zoom bei gehaltener Taste begrenzt; ein Aufruf innerhalb dieses Zeitfensters ergibt `false`, ein Skript, das den Maßstab über eine gehaltene Taste steuert, sollte also weiter aufrufen, bis es greift. Wird beim Neuladen des Skripts auf den Wert von `--scalefactor` zurückgesetzt.
|
|
|
|
*Parameter:*
|
|
|
|
* `percent` -- Ganzzahl, `25` bis `100`.
|
|
|
|
*Rückgabe:* Boolean, true, wenn der Maßstab angewendet wurde.
|
|
|
|
*Seit:* 3.00, als Erweiterung von Hypseus Singe.
|
|
*Siehe auch:* <<vldpgetscale,vldpGetScale>>, <<vldpsetrotate,vldpSetRotate>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Bei jedem Druck des Spielers auf die Einstelltaste einen Schritt herauszoomen.
|
|
function onInputPressed(input)
|
|
if input == SWITCH_BUTTON3 then
|
|
vldpSetScale(vldpGetScale() - 5)
|
|
end
|
|
end
|
|
----
|
|
|
|
[#vldpsetshift]
|
|
==== vldpSetShift
|
|
|
|
[source,text]
|
|
----
|
|
vldpSetShift(x, y)
|
|
----
|
|
|
|
Verschiebt das Bild innerhalb des Fensters, das Gegenstück zu `--shiftx` und
|
|
`--shifty` zur Laufzeit, und baut das Videorechteck neu auf, sodass Disc,
|
|
Overlay, GUIs, 3D-Szene, Partikel, ein etwaiger Sinden-Rahmen und die
|
|
Mauszuordnung ihm alle folgen. Die Werte sind Prozentwerte des Platzes, den
|
|
`--scalefactor` übrig gelassen hat, bei einem Maßstab von `100` gibt es also
|
|
nichts, wohin es sich bewegen könnte. Ein Wert außerhalb von `-100` bis `100`
|
|
beendet das Skript, anders als bei <<vldpsetscale,vldpSetScale>>, das `false`
|
|
ergibt.
|
|
|
|
*Parameter:*
|
|
|
|
* `x` -- Ganzzahl, `-100` bis `100`.
|
|
* `y` -- Ganzzahl, `-100` bis `100`.
|
|
|
|
*Rückgabe:* nichts.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<vldpgetshift,vldpGetShift>>, <<vldpsetscale,vldpSetScale>>, <<singesavegeometry,singeSaveGeometry>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Das Bild bei jedem Druck des Betreibers auf die Taste ein Stück nach links schieben.
|
|
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(...)
|
|
----
|
|
|
|
Tut nichts. Früher schaltete es die Protokollierung im ursprünglichen Videoplayer um; der Aufruf nimmt beliebige Argumente an, schreibt eine Trace-Zeile und kehrt zurück. Das Tracing von Engine und Skript läuft jetzt über Befehlszeilenoptionen (`--program` und `--trace`).
|
|
|
|
*Hinweise:*
|
|
|
|
WARNING: *Nicht implementiert.* Nur aus Gründen der Abwärtskompatibilität erhalten. Verwende `--trace` auf der Befehlszeile, um API-Aufrufe zu protokollieren.
|
|
|
|
*Seit:* 1.x
|
|
*Siehe auch:* <<debugprint,debugPrint>>, <<discgetstate,discGetState>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Portierter 1.x-Startcode. Der Aufruf wird ignoriert; starte Singe stattdessen mit --trace.
|
|
vldpSetVerbose(true)
|
|
overlaySetResolution(discGetWidth(), discGetHeight())
|
|
discSearch(TITLE_FRAME)
|
|
----
|
|
|
|
[#voice]
|
|
=== Stimme
|
|
|
|
Die Bausteine, aus denen ein Sprachchat gebaut wird, ohne das Netzwerk: Opus,
|
|
um eine Stimme klein genug zum Senden zu machen und wieder zurück, ein
|
|
Prozessor, der ein Mikrofon säubert, bevor gesendet wird, und ein Jitter-Buffer,
|
|
der das, was spät und durcheinander ankommt, in der richtigen Reihenfolge
|
|
abspielt. Die Pakete zu senden und die anderen Spieler zu finden ist Sache des
|
|
Spiels selbst oder einer späteren Version von Singe.
|
|
|
|
Samples sind Strings aus 16-Bit-Werten, je zwei Bytes, Little Endian, so wie
|
|
<<micread,`micRead`>> sie liefert; bei zwei Kanälen wechseln sich links und
|
|
rechts ab. Ein *Frame* ist ein festes Stück Zeit: 20 Millisekunden, 960 Samples
|
|
bei 48000, ist das übliche für eine Stimme.
|
|
|
|
Jedes Objekt hier ist ein Handle. <<voicefree,`voiceFree`>> gibt eines frei;
|
|
alle werden freigegeben, wenn das Spiel endet.
|
|
|
|
.Ein Mikrofon, gereinigt, kodiert und wieder dekodiert
|
|
[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) -- das ist es, was du sendest
|
|
local heard = opusDecode(decoder, packet) -- und das spielt die andere Seite ab
|
|
end
|
|
end
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#jitterget]
|
|
==== jitterGet
|
|
|
|
[source,text]
|
|
----
|
|
packet, status = jitterGet(jitter [, span])
|
|
----
|
|
|
|
Das Paket, das jetzt abgespielt werden soll, und der Buffer rückt um einen
|
|
Schritt weiter. Ruf es einmal je Frame-Dauer auf, egal, was angekommen ist.
|
|
|
|
*Parameter:*
|
|
|
|
* `span` -- Zahl, die zu nehmenden Samples; ohne Angabe der Schritt.
|
|
|
|
*Rückgabe:* das Paket oder `nil` und ein Status: `"ok"`, `"missing"` (nichts ist
|
|
fällig: gib `nil` an <<opusdecode,`opusDecode`>>, um die Lücke zu füllen) oder
|
|
`"insertion"` (der Buffer wächst, um verspätete Pakete aufzufangen: spiel ein
|
|
wenig Stille ab).
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#jitternew]
|
|
==== jitterNew
|
|
|
|
[source,text]
|
|
----
|
|
jitter = jitterNew([step])
|
|
----
|
|
|
|
Ein Jitter-Buffer: Pakete gehen nach ihrem Zeitstempel hinein, wenn sie
|
|
ankommen, und kommen der Reihe nach heraus, einen Schritt nach dem anderen, ein
|
|
wenig verzögert, damit ein verspätetes es noch schafft.
|
|
|
|
*Parameter:*
|
|
|
|
* `step` -- Zahl, die Samples, um die jedes <<jitterget,`jitterGet`>> weiterrückt;
|
|
`960`, sofern nicht angegeben.
|
|
|
|
*Rückgabe:* das Handle des Jitter-Buffers.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#jitterput]
|
|
==== jitterPut
|
|
|
|
[source,text]
|
|
----
|
|
jitterPut(jitter, packet, timestamp [, span])
|
|
----
|
|
|
|
Ein Paket, das angekommen ist. Der Buffer behält seine eigene Kopie.
|
|
|
|
*Parameter:*
|
|
|
|
* `packet` -- String, 1 bis 4000 Bytes.
|
|
* `timestamp` -- Zahl, wo das Paket beginnt, in Samples: Der Sender zählt sie,
|
|
`0`, `960`, `1920` und so weiter.
|
|
* `span` -- Zahl, die Samples, die das Paket abdeckt; der Schritt, sofern nicht angegeben.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#opusdecode]
|
|
==== opusDecode
|
|
|
|
[source,text]
|
|
----
|
|
samples = opusDecode(decoder, packet | nil)
|
|
----
|
|
|
|
Ein Paket zurück zu Samples. `nil` für ein Paket, das nie angekommen ist, bittet
|
|
Opus, die Lücke mit etwas Plausiblem zu füllen, so lang wie das letzte Paket.
|
|
|
|
*Rückgabe:* die Samples.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#opusdecodernew]
|
|
==== opusDecoderNew
|
|
|
|
[source,text]
|
|
----
|
|
decoder = opusDecoderNew([rate [, channels]])
|
|
----
|
|
|
|
*Parameter:*
|
|
|
|
* `rate` -- Zahl: 8000, 12000, 16000, 24000 oder 48000; `48000`, sofern nicht angegeben.
|
|
* `channels` -- Zahl, `1` oder `2`; `1`, sofern nicht angegeben.
|
|
|
|
*Rückgabe:* das Handle des Decoders.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#opusencode]
|
|
==== opusEncode
|
|
|
|
[source,text]
|
|
----
|
|
packet = opusEncode(encoder, samples)
|
|
----
|
|
|
|
Ein Frame aus Samples als ein Paket. Ein Frame umfasst 2,5, 5, 10, 20, 40 oder
|
|
60 Millisekunden an Samples; alles andere wird abgelehnt.
|
|
|
|
*Rückgabe:* das Paket, ein String.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#opusencodernew]
|
|
==== opusEncoderNew
|
|
|
|
[source,text]
|
|
----
|
|
encoder = opusEncoderNew([rate [, channels [, application]]])
|
|
----
|
|
|
|
*Parameter:*
|
|
|
|
* `rate` -- Zahl: 8000, 12000, 16000, 24000 oder 48000; `48000`, sofern nicht angegeben.
|
|
* `channels` -- Zahl, `1` oder `2`; `1`, sofern nicht angegeben.
|
|
* `application` -- String: `"voip"` für Sprache, darauf abgestimmt, verstanden zu
|
|
werden; `"audio"` für Musik; `"lowdelay"` für die geringste Verzögerung auf
|
|
Kosten von etwas Qualität. `"voip"`, sofern nicht angegeben.
|
|
|
|
*Rückgabe:* das Handle des Encoders.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#opusencoderset]
|
|
==== opusEncoderSet
|
|
|
|
[source,text]
|
|
----
|
|
opusEncoderSet(encoder, bitrate [, complexity])
|
|
----
|
|
|
|
*Parameter:*
|
|
|
|
* `bitrate` -- Zahl, Bits pro Sekunde; `0` lässt sie, wie sie ist. Eine Stimme ist
|
|
bei 16000 bis 24000 klar.
|
|
* `complexity` -- Zahl, `0` bis `10`: wie viel Arbeit für Qualität aufgewendet wird.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#voicefree]
|
|
==== voiceFree
|
|
|
|
[source,text]
|
|
----
|
|
voiceFree(handle)
|
|
----
|
|
|
|
Gibt einen Opus-Encoder oder -Decoder, einen Sprachprozessor oder einen Jitter-Buffer frei.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#voiceprocess]
|
|
==== voiceProcess
|
|
|
|
[source,text]
|
|
----
|
|
cleaned, voiced = voiceProcess(processor, mic [, speaker])
|
|
----
|
|
|
|
Ein Frame vom Mikrofon, gereinigt: das Echo dessen, was die Lautsprecher
|
|
spielten, herausgenommen, Hintergrundgeräusche unterdrückt und der Pegel
|
|
ausgeglichen.
|
|
|
|
*Parameter:*
|
|
|
|
* `mic` -- String, ein Frame aus Samples vom Mikrofon.
|
|
* `speaker` -- String, derselbe Frame dessen, was die Lautsprecher gespielt haben,
|
|
falls etwas: etwa die Stimmen der anderen Spieler. Dessen Echo wird herausgenommen.
|
|
|
|
*Rückgabe:* der gereinigte Frame und `true`, wenn er wie eine Stimme klingt -- so
|
|
sendet man nichts, solange niemand spricht.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#voiceprocessornew]
|
|
==== voiceProcessorNew
|
|
|
|
[source,text]
|
|
----
|
|
processor = voiceProcessorNew([frame [, rate [, tailMs]]])
|
|
----
|
|
|
|
*Parameter:*
|
|
|
|
* `frame` -- Zahl, Samples pro Aufruf; `960`, sofern nicht angegeben.
|
|
* `rate` -- Zahl; `48000`, sofern nicht angegeben.
|
|
* `tailMs` -- Zahl, das längste Echo, das ausgelöscht wird, in Millisekunden: wie
|
|
lange ein Klang im Raum nachhallt. `200`, sofern nicht angegeben.
|
|
|
|
*Rückgabe:* das Handle des Prozessors.
|
|
|
|
*Seit:* 3.00.
|
|
|
|
[#enginecallbacks]
|
|
=== Engine-Callbacks
|
|
|
|
Das sind Funktionen, die das Skript definiert und die Singe aufruft, wenn das passende Ereignis eintritt; alle sind optional, und eine nicht definierte wird übersprungen. Eingabe-Callbacks werden ausgelöst, während Ereignisse gelesen werden, `onOverlayUpdate` läuft höchstens einmal alle 15 Millisekunden, und keiner von ihnen läuft, solange die Pausentaste das Spiel eingefroren hält (siehe <<pausing,Pausieren>>). Ein Fehler in einem Callback beendet das Spiel mit einem Traceback. Im Thread-Modell übernimmt `singeMain` den Großteil der Logik pro Frame (siehe <<threaded,Thread-Modell>>): Die anderen Callbacks werden weiterhin ausgelöst, aber `Framework.singe` installiert sein eigenes `onOverlayUpdate`, um die Koroutine `singeMain` anzutreiben, ein Spiel im Thread-Modell definiert also `singeMain`, `onShutdown` und die Eingabe-Callbacks, die es haben will.
|
|
|
|
[#oncontrollermoved]
|
|
==== onControllerMoved
|
|
|
|
[source,text]
|
|
----
|
|
function onControllerMoved(axis, value, which)
|
|
end
|
|
----
|
|
|
|
Wird für jedes Achsenbewegungsereignis eines verbundenen Controllers aufgerufen, auch für Bewegung innerhalb der Totzone, ein Stick unter einem Daumen löst ihn also viele Male pro Sekunde aus. Richtungen jenseits von `SINGE_DEAD_ZONE` wirken außerdem als Schalter, wenn `controls.cfg` sie belegt, ein Spiel, das nur digitale Lenkung braucht, kann also `GAMEPAD_0.AXIS_LEFT_X_L` und `GAMEPAD_0.AXIS_LEFT_X_R` belegen und diesen Callback ignorieren. Der Wert wird für `controllerGetAxis` gespeichert, bevor der Callback läuft.
|
|
|
|
*Parameter:*
|
|
|
|
* `axis` -- `0` bis `5`, entsprechend den Konstanten `GAMEPAD_AXIS_*`.
|
|
* `value` -- rohe Stellung, `-32768` bis `32767`; Trigger ruhen bei `0`.
|
|
* `which` -- Controller-Index, `0` bis `3`, der Index, den `controllerGetAxis` und die Tabellen `GAMEPAD_N` verwenden.
|
|
|
|
*Siehe auch:* <<controllergetaxis,controllerGetAxis>>, <<controllergetbutton,controllerGetButton>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Analoge Lenkung nur über den linken Stick des ersten Controllers.
|
|
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
|
|
----
|
|
|
|
Wird aufgerufen, wenn eine logische Eingabe gedrückt oder losgelassen wird. Was ankommt, hängt vom Tastaturmodus ab. In `MODE_NORMAL` ist `what` ein `SWITCH_*`-Wert: Tasten, Controller-Knöpfe, Achsenrichtungen jenseits der Totzone, Mausknöpfe und das Mausrad kommen alle auf diesem Weg an, übersetzt durch `controls.cfg`, jeder Druck einmal und ohne Tastenwiederholung, und Eingaben, die keinem Schalter zugeordnet sind, werden ignoriert. `SWITCH_PAUSE` kommt nur nach `singeSetPauseKeyEnabled(false)` an; sonst friert die Engine das Spiel bei dieser Taste ein, und das Skript sieht sie nie. In `MODE_FULL` ist `what` das Keysym der Taste (der Zeichenwert, nicht der Scancode), Wiederholungen werden geliefert, und Controller- und Mausknöpfe übergeben `0`; jedes Ereignis erreicht außerdem `onKeyPressed` und `onKeyReleased`, die den Scancode oder Knopfcode tragen. Wenn die Pausentaste das Spiel einfriert, wird zuerst jeder Schalter, den das Skript gedrückt hält, über `onInputReleased` losgelassen, und beim Auftauen wird alles, was noch tatsächlich gedrückt ist, erneut gedrückt, sodass ein Spiel nie einen veralteten Knopf sieht. Was in der ersten Sekunde nach dem Start des Skripts oder nachdem das Fenster den Fokus zurückerhalten hat, gedrückt wird, gilt als von vorher gehalten und wird samt seinem Loslassen verschluckt. Ein Schalter kommt von gar keinem Bedienelement: `SWITCH_MOUSE_DISCONNECT` wird von der Engine ausgelöst, wenn eine Maus oder Lightgun abgezogen wird, einmal als Druck und sofort danach einmal als Loslassen, sodass ein Lightgun-Spiel merkt, dass seine Lightgun weg ist (siehe xref:Manual.adoc#mousedevices[Mäuse, Lightguns und wer entscheidet]).
|
|
|
|
*Siehe auch:* <<keyboardsetmode,keyboardSetMode>>, <<onkeypressedonkeyreleased,onKeyPressed>>, <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>, <<controllergetbutton,controllerGetButton>>
|
|
|
|
.Beispiel
|
|
[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
|
|
----
|
|
|
|
Wird nur in `MODE_FULL` aufgerufen, für jede Taste, die gedrückt oder losgelassen wird, mit dem logischen Keysym (dem Zeichen, das die Taste im aktuellen Layout erzeugt) und dem physischen Scancode (der Position der Taste, verglichen mit der Tabelle `SCANCODE`). Gehaltene Tasten wiederholen sich in diesem Modus, und genau das will eine Texteingabe. Controller- und Mausknöpfe kommen ebenfalls durch, mit `keysym` `0` und ihrem `GAMEPAD_N`- oder `MOUSE_N`-Code als `scancode`. Für Text verwende `keysym`; für Steuerungen, bei denen Tasten als Knöpfe dienen, verwende `scancode`, damit die Belegung auf jedem Layout gleich ist. Tastaturbelegungen der eigenen Schalter der Engine wirken in diesem Modus nicht, ein Spiel in `MODE_FULL` muss also seinen eigenen Ausweg bieten.
|
|
|
|
*Siehe auch:* <<keyboardsetmode,keyboardSetMode>>, <<keyboardgetmodifiers,keyboardGetModifiers>>, <<oninputpressedoninputreleased,onInputPressed>>
|
|
|
|
[#ontextinput]
|
|
==== onTextInput
|
|
|
|
[source,text]
|
|
----
|
|
function onTextInput(text)
|
|
end
|
|
----
|
|
|
|
Wird nur in `MODE_FULL` aufgerufen, nachdem Umschaltzustand und Layout der Tastatur angewendet wurden, mit dem Text, den ein Tastendruck erzeugt hat -- ein Zeichen oder ein paar, in UTF-8. So kommt ein Großbuchstabe, ein Anführungszeichen oder ein Buchstabe aus einem nicht englischen Layout so an, wie die Person ihn getippt hat. `onKeyPressed` liefert denselben Druck als sein Keysym ohne Umschaltung und seinen Scancode und ist der richtige für Tasten, die Befehle sind; dieser hier ist der richtige für Tasten, die Text sind. Eine GUI mit einem fokussierten Textfeld bekommt den Text zuerst, und das Skript hört nichts davon.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<onkeypressedonkeyreleased,onKeyPressed>>, <<keyboardsetmode,keyboardSetMode>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Highscore-Initialen: Buchstaben als Text; Eingabetaste und Rücktaste nach Position.
|
|
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
|
|
----
|
|
|
|
Wird für jede Nachricht aufgerufen, die auf dem MIDI-Eingangsport ankommt, den
|
|
<<midiopeninput,`midiOpenInput`>> geöffnet hat, einmal pro Nachricht, zusammen
|
|
mit den übrigen Eingaben des Frames. Die drei Zahlen sind die Nachricht, wie ein
|
|
Gerät sie sendet: `status` trägt die Art in seinen oberen vier Bits und den
|
|
Kanal in seinen unteren vier, gezählt ab `0`, eine Note auf dem ersten Kanal kommt also
|
|
als `0x90` an. `bytes` ist die ganze Nachricht als String, was eine
|
|
System-Exclusive-Nachricht braucht und woraus eine Nachricht, die länger als drei
|
|
Bytes ist, gelesen werden muss.
|
|
|
|
Nachrichten, die ankommen, während kein Port offen ist, werden nicht eingereiht;
|
|
nichts von vor dem Öffnen des Ports wird geliefert.
|
|
|
|
*Parameter:*
|
|
|
|
* `status` -- Zahl; das Statusbyte, `0x80` bis `0xFF`.
|
|
* `data1` -- Zahl; das erste Datenbyte oder `0`, wenn die Nachricht keins hat.
|
|
* `data2` -- Zahl; das zweite Datenbyte oder `0`, wenn die Nachricht keins hat.
|
|
* `bytes` -- String; die ganze Nachricht, wie lang sie auch ist.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<midiopeninput,midiOpenInput>>, <<midisend,midiSend>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Ein MIDI-Keyboard spielt das Spiel: das mittlere C und darüber sind die vier Feuerknöpfe.
|
|
local MIDI_NOTE_ON = 0x90
|
|
|
|
function onMidiMessage(status, data1, data2, bytes)
|
|
-- Die Art steht in den oberen vier Bits; der Kanal in den unteren vier wird hier ignoriert.
|
|
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
|
|
----
|
|
|
|
Wird für jedes Mausbewegungsereignis aufgerufen, mit der Position und der Bewegung seit dem vorigen Ereignis in Overlay-Koordinaten. Im Modus `MOUSE_SINGLE` wird die Zeigerposition des Fensters in Overlay-Koordinaten umgerechnet, und `which` ist immer `0`. Im Modus `MOUSE_MANY` wird die relative Bewegung jedes Geräts zu seiner eigenen Position aufsummiert, begrenzt auf den Videobereich, und `which` ist der Geräteindex; ein absolutes Gerät wie ein Grafiktablett meldet seine abgebildete Position mit `xRelative` und `yRelative` gleich `0`. Die Position wird für `mouseGetPosition` gespeichert, bevor der Callback läuft, ein Spiel kann diesen Callback also ignorieren und stattdessen abfragen. Gezeichnet werden muss trotzdem erst in `onOverlayUpdate`: Behalte hier die letzte Position in globalen Variablen und zeichne den Cursor dort.
|
|
|
|
*Parameter:*
|
|
|
|
* `x`, `y` -- Position in Overlay-Koordinaten.
|
|
* `xRelative`, `yRelative` -- Bewegung seit dem vorigen Ereignis, in Overlay-Koordinaten.
|
|
* `which` -- Mausindex, `0` bis `3`.
|
|
|
|
*Siehe auch:* <<mousegetposition,mouseGetPosition>>, <<mousesetmode,mouseSetMode>>, <<mousesetenabled,mouseSetEnabled>>
|
|
|
|
.Beispiel
|
|
[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
|
|
----
|
|
|
|
Wird nach einem Physikschritt einmal für jeden neuen Kontakt zwischen zwei Körpern aufgerufen, nie wieder für ein Paar, das in Berührung bleibt. Eine Kiste, die landet, ein Ball, der den Automaten trifft, das Auto des Spielers, das eine Wand streift. Kontakte mit Triggern melden sich stattdessen über `onTrigger`. Die Ereignisse eines Schritts werden danach gemeinsam geliefert, bevor der Frame gezeichnet wird.
|
|
|
|
*Parameter:*
|
|
|
|
* `nodeA`, `nodeB` -- die Knoten der beiden Körper.
|
|
* `x`, `y`, `z` -- wo sie sich berührt haben, in Welteinheiten.
|
|
* `speed` -- wie schnell sie aufeinandertrafen, in Welteinheiten pro Sekunde.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<ontrigger,onTrigger>>, <<bodynew,bodyNew>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Harte Landungen machen dort Lärm, wo sie passieren.
|
|
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
|
|
----
|
|
|
|
Wird aufgerufen, wenn ein Körper einen mit `bodySetTrigger` gemachten Trigger betritt oder verlässt, einmal auf dem Weg hinein und einmal auf dem Weg hinaus, unabhängig von den beteiligten Formen (ein Mesh oder eine zusammengesetzte Form, die an mehreren Stellen berührt, zählt trotzdem als ein Körper). Trigger schieben nie etwas, das hier ist also der Weg, zu bemerken, dass ein Körper eine Tür durchquert, einen Checkpoint erreicht oder in eine Todeszone fällt.
|
|
|
|
*Parameter:*
|
|
|
|
* `trigger` -- der Knoten des Trigger-Körpers.
|
|
* `other` -- der Knoten, der ihn betreten oder verlassen hat.
|
|
* `entered` -- `true` auf dem Weg hinein, `false` auf dem Weg hinaus.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<bodysettrigger,bodySetTrigger>>, <<oncollision,onCollision>>
|
|
|
|
.Beispiel
|
|
[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
|
|
----
|
|
|
|
Wird aufgerufen, wenn ein Navigationsagent das Ziel erreicht, das `navAgentMoveTo` ihm gegeben hat, einmal pro Ankunft, nach der Navigationsaktualisierung des Frames. Schick den Agenten woandershin oder spiel seine Leerlaufanimation ab. Ein Skript, das lieber abfragt, kann stattdessen `navAgentIsArrived` aufrufen.
|
|
|
|
*Parameter:*
|
|
|
|
* `agent` -- das Handle aus `navAgentNew`.
|
|
|
|
*Seit:* 3.00.
|
|
*Siehe auch:* <<navagentmoveto,navAgentMoveTo>>, <<navagentisarrived,navAgentIsArrived>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Endlos zwischen Wegpunkten patrouillieren.
|
|
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
|
|
----
|
|
|
|
Der eine Callback, in dem Zeichnen erlaubt ist. Singe ruft ihn höchstens einmal alle 15 Millisekunden auf, solange das Spiel nicht eingefroren ist, gezählt ab dem Beginn des vorigen Aufrufs, die Zeit, die das Skript selbst braucht, fällt also in diese 15 Millisekunden, statt zu ihnen hinzuzukommen; jeder anderswo gemachte Aufruf von `overlay*`, `spriteDraw`, `videoDraw`, `fontPrint` oder Ähnlichem erscheint nicht auf dem Bildschirm und kann das Overlay beschädigen. Gib `OVERLAY_UPDATED` (`1`) zurück, wenn sich das Overlay geändert hat, damit Singe es erneut über das Video legt, oder `OVERLAY_NOT_UPDATED` (`0`), wenn sich nichts geändert hat, damit das vorige Gesamtbild wiederverwendet wird; gar nichts zurückzugeben zählt als `OVERLAY_NOT_UPDATED`. Die Anzeige wird ohnehin neu gezeichnet, sobald ein neuer Video-Frame ankommt oder eine 3D-Szene eingeschaltet ist, der Rückgabewert ist also eine Optimierung für statische Bildschirme. Nachdem er zurückgekehrt ist, löscht die Engine die Werte hinter `keyboardGetLastDown` und `keyboardGetLastUp`. Im Thread-Modell definiert `Framework.singe` diese Funktion selbst, um `singeMain` fortzusetzen, und gibt immer `OVERLAY_UPDATED` zurück; definiere sie nicht zusätzlich.
|
|
|
|
*Rückgabe:* `OVERLAY_UPDATED` oder `OVERLAY_NOT_UPDATED`.
|
|
|
|
*Siehe auch:* <<singemain,singeMain>>, <<keyboardgetlastdown,keyboardGetLastDown>>, <<onshutdown,onShutdown>>
|
|
|
|
.Beispiel
|
|
[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
|
|
----
|
|
|
|
Wird einmal aufgerufen, wenn das Skript endet, ob durch `singeQuit`, den Beenden-Schalter, das Schließen des Fensters, `scriptExecute` oder `scriptPush`, nach dem letzten Frame und bevor der Lua-Zustand verworfen wird. `singeReload` ruft ihn nicht auf. Speichere hier Einstellungen und gib die Sprites, Sounds, Schriftarten und Videos frei, die das Skript geladen hat; die Engine stoppt danach jeden Sound und gibt frei, was übrig ist, ein vergessenes Handle ist also kein Leck, aber ausdrückliches Entladen behält die Reihenfolge beim Herunterfahren unter der Kontrolle des Skripts. Ruf von hier aus nicht `singeQuit` auf.
|
|
|
|
*Siehe auch:* <<singequit,singeQuit>>, <<soundunload,soundUnload>>, <<scriptpush,scriptPush>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
function onShutdown()
|
|
saveHighScores()
|
|
spriteUnload(reticle)
|
|
fontUnload(hudFont)
|
|
soundUnload(shotClip)
|
|
soundUnload(coinClip)
|
|
end
|
|
----
|
|
|
|
[#onsoundcompleted]
|
|
==== onSoundCompleted
|
|
|
|
[source,text]
|
|
----
|
|
function onSoundCompleted(channel)
|
|
end
|
|
----
|
|
|
|
Wird jedes Mal mit der Kanalnummer aufgerufen, wenn ein Kanal stoppt: wenn sein Clip endet, wenn die letzte Schleife fertig ist und wenn `soundStop` oder `soundFullStop` ihn anhält, ein Skript, das von hier aus Sounds verkettet, sollte also wissen, welche Kanäle es selbst gestoppt hat. Der Mixer bemerkt den Stopp auf seinem eigenen Thread, und die Engine liefert den Callback zwischen den Frames auf dem Spiel-Thread; solange die Pausentaste das Spiel eingefroren hält, warten die Meldungen, und sie werden verworfen, wenn ein Skript endet oder neu geladen wird. Bis zu 64 Abschlüsse werden zwischen den Frames eingereiht; was in einem Frame darüber hinausgeht, geht verloren.
|
|
|
|
*Parameter:*
|
|
|
|
* `channel` -- die Kanalnummer, die der Aufruf `soundPlay` zurückgegeben hat.
|
|
|
|
*Siehe auch:* <<soundplay,soundPlay>>, <<soundstop,soundStop>>, <<soundisplaying,soundIsPlaying>>
|
|
|
|
.Beispiel
|
|
[source,lua]
|
|
----
|
|
-- Die Musik-Playlist der Reihe nach abspielen, für immer.
|
|
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
|
|
----
|
|
|
|
Der Einstiegspunkt des Thread-Modells. Definiert ein Skript sie, verpackt `Framework.singe` sie in eine Koroutine und installiert ein `onOverlayUpdate`, das die Koroutine einmal pro Frame fortsetzt, sodass die Funktion als gerade Linie aus Spiellogik läuft, die `singeYield()` (ein Alias von `coroutine.yield`) aufruft, wo immer sie einen Frame vergehen lassen will. Alles, was vor einem Yield gezeichnet wird, landet im Overlay dieses Frames. Die Rückkehr aus `singeMain` ruft `singeQuit` auf; ein Fehler darin beendet das Spiel mit einem Traceback. Die Callbacks für Eingabe, Sound, Physik und Navigation werden zwischen den Fortsetzungen weiterhin ausgelöst, ein Spiel im Thread-Modell kann sie also mit Abfragen mischen.
|
|
|
|
*Siehe auch:* <<onoverlayupdate,onOverlayUpdate>>, <<keyboardgetlastdown,keyboardGetLastDown>>, <<singequit,singeQuit>>
|
|
|
|
.Beispiel
|
|
[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]
|
|
=== Hypseus-Kompatibilität
|
|
|
|
IMPORTANT: Alles in diesem Abschnitt existiert, damit ein für Hypseus Singe
|
|
geschriebenes Spiel unverändert läuft. *Nichts davon wird für ein neues Spiel
|
|
empfohlen.* Bevorzuge den Singe-Aufruf, den jeder Eintrag nennt: Er ist der, der
|
|
weiter Funktionen dazugewinnt, und er schleppt nicht die Maßstäbe,
|
|
Argumentreihenfolgen oder festen Antworten einer anderen Engine mit.
|
|
|
|
Hypseus Singe registriert in seiner Engine einige Namen, die Singe stattdessen aus
|
|
`Singe/Framework.singe` beantwortet, damit ein für Hypseus geschriebenes Spiel
|
|
unverändert läuft. Jeder wird nur definiert, wenn die Engine ihn nicht schon
|
|
bereitstellt, eine künftige Umsetzung in der Engine gewinnt also. `keyboardCatchQuit`
|
|
ist unter <<keyboard,Tastatur>> dokumentiert; `setOverlayLinearScale` und
|
|
`setOverlayOnTop` sind echte Engine-Aufrufe unter <<overlay,Overlay>>;
|
|
`mainBezelLoaded` und die sieben `scoreBezel`-Aufrufe sind echte Engine-Aufrufe
|
|
unter <<bezelref,Bezel>>; `getFValue`, `ratioGetX` und `ratioGetY` sind echte
|
|
Engine-Aufrufe unter <<commandlineref,Befehlszeile>>; `joyMouseEnable` ist ein
|
|
echter Engine-Aufruf unter <<joystick,Joystick>>; `allowSocketCall` und
|
|
`getUserString` sind echte Engine-Aufrufe unter <<singe,Singe>>, und
|
|
`drawTransparent` unter <<sprite,Sprite>>; und die ganze Familie `music` ist jetzt
|
|
ein Satz echter Engine-Aufrufe unter <<musicref,Musik>>. Die sechs
|
|
`spriteAnim`-Namen unten sind einfache Aliase, gesetzt nach dem Umhüllen der
|
|
veralteten Sprite-Aufrufe, sie nehmen also die Argumentreihenfolge, die auch der
|
|
Rest der Sprite-Familie nimmt: die von Hypseus (Handle zuletzt) unter
|
|
`LEGACY_SPRITE_ARGS` und die von Singe 3.00 (Handle zuerst) ohne es.
|
|
|
|
[cols="1,1",options="header"]
|
|
|===
|
|
| Funktion | Von Framework.singe bereitgestellt als
|
|
| `setOverlaySize(preset [, width, height])` | `overlaySetResolution` mit einer gewählten Größe: `1` ist die Videogröße der Disc, `3` ist `360x240`, `4` ist die angegebene Größe (ignoriert, wenn sie fehlt, wie in Hypseus), und alles andere, `2` eingeschlossen, ist die halbe Disc auf beiden Achsen. Ohne Disc melden `discGetWidth` und `discGetHeight` die Zeichenfläche, und in diese zeichnet ein Spiel ohne Disc.
|
|
| `setOverlayResolution(width, height)` | Ein Alias von <<overlaysetresolution,`overlaySetResolution`>>. Hypseus registriert eine Funktion unter beiden Namen.
|
|
| `rewriteStatus()` | Stub, der `false` zurückgibt. In Hypseus meldet er, ob Asset-Pfade für eine Installation mit `-espath` umgeschrieben werden; das virtuelle Dateisystem von Singe findet die Dateien eines Spiels dort, wo sie installiert wurden, es wird also nichts umgeschrieben, und das Spiel behält seinen eigenen Asset-Ordner.
|
|
| `hypseusGetHeight()` | Ein Alias von <<singegetheight,`singeGetHeight`>>.
|
|
| `hypseusGetWidth()` | Ein Alias von <<singegetwidth,`singeGetWidth`>>.
|
|
| `takeScreenshot()` | Ein Alias von <<singescreenshot,`singeScreenshot`>>. Hypseus verweigert einen zweiten Screenshot innerhalb von fünf Sekunden; Singe hat keine solche Drosselung.
|
|
| `mouseHowManyReal()` | Ein Alias von <<mousehowmany,`mouseHowMany`>>. Hypseus zählt echte Mäuse getrennt von denen, die es aus einem Gamepad ableitet; Singe hat keine abgeleiteten Mäuse, seine Zahl ist also schon die echte.
|
|
| `spriteAnimGetFrame(spr)` | Ein Alias von <<spritegetframe,`spriteGetFrame`>>.
|
|
| `spriteAnimIsPlaying(spr)` | Ein Alias von <<spriteisplaying,`spriteIsPlaying`>>.
|
|
| `spriteAnimLoop(loop, spr)` | Ein Alias von <<spriteloop,`spriteLoop`>>.
|
|
| `spriteAnimPause(spr)` | Ein Alias von <<spritepause,`spritePause`>>.
|
|
| `spriteAnimPlay(spr)` | Ein Alias von <<spriteplay,`spritePlay`>>.
|
|
| `spriteAnimSetFrame(frame, spr)` | Ein Alias von <<spritesetframe,`spriteSetFrame`>>.
|
|
|===
|
|
|
|
[#unimplementedandlegacy]
|
|
=== Nicht implementiert und veraltet
|
|
|
|
Eine Handvoll API-Funktionen wird zur Kompatibilität auf Skriptebene beibehalten, und die mit „Tut nichts“ markierten tun nichts. Sie lassen sich gefahrlos aufrufen; sie schreiben einfach eine Trace-Meldung und kehren zurück. Bevorzuge für neuen Code die aufgeführten Ersatzfunktionen.
|
|
|
|
[cols="1,1,1",options="header"]
|
|
|===
|
|
| Funktion | Status | Ersatz
|
|
| `discChangeSpeed` | Tut nichts | -- (war für Wiedergabe mit variabler Geschwindigkeit auf echter LD-Hardware)
|
|
| `discSearchBlanking` | Tut nichts | -- (war das Austasten des Bildschirms während eines Sprungs auf echter LD-Hardware)
|
|
| `discSkipBlanking` | Tut nichts | -- (derselbe Grund)
|
|
| `discSetFPS` | Tut nichts | Die Bildrate wird aus der Videodatei gelesen.
|
|
| `vldpGetHeight` | Funktioniert | <<discgetheight,`discGetHeight`>>
|
|
| `vldpGetWidth` | Funktioniert | <<discgetwidth,`discGetWidth`>>
|
|
| `vldpSetVerbose` | Tut nichts | --
|
|
| `colorBackground(r,g,b)` | Funktioniert | Bevorzuge die Form mit 4 Argumenten, `colorBackground(r, g, b, a)`.
|
|
| `colorForeground(r,g,b)` | Funktioniert | Bevorzuge die Form mit 4 Argumenten, `colorForeground(r, g, b, a)`.
|
|
| `daphneGetWidth` / `daphneGetHeight` / `daphneScreenshot` | Aliase in `Framework.singe` | Bevorzuge die `singe*`-Entsprechungen.
|
|
| `discPauseAtFrame` | Funktioniert (Alias) | <<discsearch,`discSearch`>>.
|
|
|===
|
|
|
|
Außerdem bindet `Framework.singe` `random = { new = math.random }` neu, damit Skripte aus der Zeit vor 2.00, die `random.new()` verwenden, weiterhin funktionieren. Neuer Code sollte direkt `math.random` verwenden.
|